コンテンツにスキップ
上級

マルチシグアカウントの設定⚓︎

マルチシグアカウント(「マルチシグ」とも呼ばれます)は、それ単体ではトランザクションを開始できません。代わりに、「連署者(cosignatory)」アカウントがトランザクションを作成し、マルチシグアカウントに代わって署名を行います。

このチュートリアルでは、通常のアカウントを、2つの連署者のうち1つの承認を必要とするマルチシグアカウントに変換する方法を説明します。アカウントがすでにマルチシグである場合は、代わりに連署者を削除して通常のアカウントに戻す方法を実演します。

このチュートリアルで使用するマルチシグ構造は以下の通りです。

Multisignature Treeマルチシグアカウントマルチシグアカウント連署者 0連署者 0連署者 0->マルチシグアカウント連署者 1連署者 1連署者 1->マルチシグアカウント

マルチレベルマルチシグアカウント

連署者自体がマルチシグアカウントである、より複雑な構成も最大3階層までサポートされています。

マルチシグアカウントは任意の順序で設定できます。 ただし、一度アカウントがマルチシグに変換されると、そのアカウント自身でトランザクションに署名することはできなくなり、その時点で設定されている連署者に完全に従う必要があります。

前提条件⚓︎

開始する前に、以下を確認してください。

さらに、トランザクションがどのようにアナウンスされ承認されるかを理解するために 転送トランザクション のチュートリアルを、アグリゲートトランザクション の仕組みを理解するために アグリゲートコンプリートトランザクション のチュートリアルを復習してください。

完全なコード⚓︎

このチュートリアルの完全なコード一覧を以下に示します。 詳細な手順ごとの説明は次のセクションで行います。

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.SymbolFacade import SymbolFacade
from symbolchain.sc import Amount
from symbolchain.symbol.Network import NetworkTimestamp

NODE_URL = os.environ.get(
    'NODE_URL', 'https://reference.symboltest.net:3001')
print(f'Using node {NODE_URL}')

# Helper function to announce a transaction
def announce_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'
    )
    with urllib.request.urlopen(request) as response:
        print(f'  Response: {response.read().decode()}')

# Helper function to wait for transaction confirmation
def wait_for_confirmation(transaction_hash, label):
    print(f'Waiting for {label} confirmation...')
    for attempt in range(60):
        time.sleep(1)
        try:
            url = f'{NODE_URL}/transactionStatus/{transaction_hash}'
            with urllib.request.urlopen(url) as response:
                status = json.loads(response.read().decode())
                print(f'  Transaction status: {status["group"]}')
                if status['group'] == 'confirmed':
                    print(f'{label} confirmed in {attempt} seconds')
                    return
                if status['group'] == 'failed':
                    raise Exception(f'{label} failed: {status["code"]}')
        except urllib.error.HTTPError:
            print('  Transaction status: unknown')
    raise Exception(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 used
def get_multisig_cosignatories(address):
    multisig_path = f'/account/{address}/multisig'
    print(f'Getting cosignatories from {multisig_path}')
    try:
        url = f'{NODE_URL}{multisig_path}'
        with urllib.request.urlopen(url) as response:
            status = json.loads(response.read().decode())
            cosignatories = status['multisig']['cosignatoryAddresses']
            print(f'  Response: {cosignatories}')
            return cosignatories
    except urllib.error.HTTPError:
        # The address has never been used
        print('  Response: No cosignatories')
    return []

# Returns a transaction that turns a regular account into a multisig
def multisig_enable_transaction():
    # Create an embedded multisig account modification transaction
    # that adds two cosignatories
    embedded_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 transaction
    embedded_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 size
    transaction.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 signature
    facade.transaction_factory.attach_signature(transaction,
        facade.sign_transaction(multisig_key_pair, transaction))

    # Append signatures from all cosignatories
    for cosignatory_key_pair in cosignatory_key_pairs:
        transaction.cosignatures.append(
            facade.cosign_transaction(cosignatory_key_pair, transaction)
        )

    return transaction

# Returns a transaction that turns a multisig into a regular account
def multisig_disable_transaction():
    # Create two embedded multisig account modification transactions
    # because cosignatories must be removed one by one
    embedded_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 transaction
    embedded_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 signature
    facade.transaction_factory.attach_signature(transaction,
        facade.sign_transaction(cosignatory_key_pairs[0], transaction))

    return transaction

facade = SymbolFacade('testnet')

KEY_TEMPLATE = '0' * 63 + '{}'

# Setup the keys for the multisig account and its two cosignatories
MULTISIG_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 = []
for i in range(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 time
    time_path = '/node/time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        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 fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        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 perform
    cosignatories = get_multisig_cosignatories(multisig_address)
    if len(cosignatories) == 0:
        # Enable the multisig
        transaction = multisig_enable_transaction()
        # This operation must be signed by the multisig account
        signer_key_pair = multisig_key_pair
    else:
        # Disable the multisig
        transaction = multisig_disable_transaction()
        # This operation must be signed by one of the cosigners
        signer_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 confirmation
    transaction_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')

except Exception as e:
    print(e)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    KeyPair,
    SymbolTransactionFactory,
    models,
    NetworkTimestamp,
    SymbolFacade
} from 'symbol-sdk/symbol';

const NODE_URL = process.env.NODE_URL ||
    'https://reference.symboltest.net:3001';
console.log('Using node', NODE_URL);

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    console.log(`Announcing ${label} to /transactions`);
    const response = await fetch(`${NODE_URL}/transactions`, {
        method: 'PUT',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    console.log('  Response:', await response.text());
}

// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    console.log(`Waiting for ${label} confirmation...`);
    for (let attempt = 0; attempt < 60; attempt++) {
        await new Promise(resolve => setTimeout(resolve, 1000));
        try {
            const response = await fetch(
                `${NODE_URL}/transactionStatus/${transactionHash}`);
            const status = await response.json();
            console.log('  Transaction status:', status.group);
            if (status.group === 'confirmed') {
                console.log(`${label} confirmed in`, attempt, 'seconds');
                return;
            }
            if (status.group === 'failed') {
                throw new Error(`${label} failed: ${status.code}`);
            }
        } catch (e) {
            if (e.message.includes('failed'))
                throw e;
            console.log('  Transaction status: unknown');
        }
    }
    throw new Error(`${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 used
async function getMultisigCosignatories(address) {
    const multisigPath = `/account/${address}/multisig`;
    console.log(`Getting cosignatories from ${multisigPath}`);
    try {
        const response = await fetch(`${NODE_URL}${multisigPath}`);
        const json = await response.json();
        const cosignatories = json.multisig.cosignatoryAddresses;
        console.log('  Response:', JSON.stringify(cosignatories));
        return cosignatories;
    } catch {
        console.log('  Response: No cosignatories');
        return [];
    }
}

// Returns a transaction that turns a regular account into a multisig
function multisigEnableTransaction(timestamp, feeMult) {
    // Create an embedded multisig account modification transaction
    // that adds two cosignatories
    const embeddedTransaction = facade.transactionFactory
        .createEmbedded({
            type: 'multisig_account_modification_transaction_v1',
            // This is the account that will be turned into a multisig
            signerPublicKey: multisigKeyPair.publicKey,
            // Delta of the number of signatures required for approvals
            minApprovalDelta: 1,
            // Delta of the number of signatures required for removals
            minRemovalDelta: 1,
            addressAdditions: cosignatoryAddresses
        });

    // Build the aggregate transaction
    const embeddedTransactions = [embeddedTransaction];
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        // This is the account that will pay for this transaction
        signerPublicKey: 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 size
    transaction.fee = new models.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 signature
    SymbolTransactionFactory.attachSignature(transaction,
        facade.signTransaction(multisigKeyPair, transaction));

    // Append signatures from all cosignatories
    for (const cosignatoryKeyPair of cosignatoryKeyPairs) {
        transaction.cosignatures.push(
            facade.cosignTransaction(cosignatoryKeyPair, transaction));
    }

    return transaction;
}

// Returns a transaction that turns a multisig into a regular account
function multisigDisableTransaction(timestamp, feeMult) {
    // Create two embedded multisig account modification transactions
    // because cosignatories must be removed one by one
    const embeddedTransaction1 = facade.transactionFactory
        .createEmbedded({
            type: 'multisig_account_modification_transaction_v1',
            // This is the multisig account that will be modified
            signerPublicKey: multisigKeyPair.publicKey,
            // Keep required signatures unchanged for this step
            minApprovalDelta: 0,
            minRemovalDelta: 0,
            addressDeletions: [cosignatoryAddresses[1]]
        });
    const embeddedTransaction2 = facade.transactionFactory
        .createEmbedded({
            type: 'multisig_account_modification_transaction_v1',
            // This is the multisig account that will be modified
            signerPublicKey: multisigKeyPair.publicKey,
            // Decrease required signatures after final removal
            minApprovalDelta: -1,
            minRemovalDelta: -1,
            addressDeletions: [cosignatoryAddresses[0]]
        });

    // Build the aggregate transaction
    const embeddedTransactions = [embeddedTransaction1,
        embeddedTransaction2];
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        // This is the account that will pay for this transaction
        signerPublicKey: 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 = new models.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 signature
    SymbolTransactionFactory.attachSignature(transaction,
        facade.signTransaction(cosignatoryKeyPairs[0], transaction));

    return transaction;
}

const facade = new SymbolFacade('testnet');

const KEY_PREFIX = '0'.repeat(63);

// Setup the keys for the multisig account and its two cosignatories
const MULTISIG_PRIVATE_KEY = process.env.MULTISIG_PRIVATE_KEY || (
    KEY_PREFIX + '1');
const multisigKeyPair = new KeyPair(new PrivateKey(MULTISIG_PRIVATE_KEY));
const multisigAddress = facade.network.publicKeyToAddress(
    multisigKeyPair.publicKey);
console.log(`Multisig address: ${multisigAddress}`,
    `(public key ${multisigKeyPair.publicKey})`);

const cosignatoryKeyPairs = [];
const cosignatoryAddresses = [];
for (let i = 0; i < 2; i++) {
    const COSIGNATORY_PRIVATE_KEY =
        process.env[`COSIGNATORY${i}_PRIVATE_KEY`] || (
            KEY_PREFIX + String(i + 2));
    const kp = new KeyPair(new PrivateKey(COSIGNATORY_PRIVATE_KEY));
    cosignatoryKeyPairs.push(kp);
    const addr = facade.network.publicKeyToAddress(kp.publicKey);
    cosignatoryAddresses.push(addr);
    console.log(`Cosignatory ${i} address: ${addr}`,
        `(public key ${kp.publicKey})`);
}

try {
    // Fetch current network time
    const timePath = '/node/time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);
    console.log('  Network time:', timestamp.timestamp,
        'ms since nemesis');

    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMult = feeJSON.medianFeeMultiplier;
    const minimumMult = feeJSON.minFeeMultiplier;
    const feeMult = Math.max(medianMult, minimumMult);
    console.log('  Fee multiplier:', feeMult);

    // Get current state of the multisig account and decide which
    // operation to perform
    const cosignatories = await getMultisigCosignatories(multisigAddress);
    let transaction;
    let signerKeyPair;
    if (cosignatories.length === 0) {
        // Enable the multisig
        transaction = multisigEnableTransaction(timestamp, feeMult);
        // This operation must be signed by the multisig account
        signerKeyPair = multisigKeyPair;
    } else {
        // Disable the multisig
        transaction = multisigDisableTransaction(timestamp, feeMult);
        // This operation must be signed by one of the cosigners
        signerKeyPair = cosignatoryKeyPairs[0];
    }
    const payload = SymbolTransactionFactory.attachSignature(
        transaction,
        facade.signTransaction(signerKeyPair, transaction));

    // Announce and wait for confirmation
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log(
        'Built aggregate transaction with hash:', transactionHash);
    await announceTransaction(payload, 'aggregate transaction');
    await waitForConfirmation(transactionHash, 'aggregate transaction');

} catch (e) {
    console.error(e.message, '| Cause:', e.cause?.code ?? 'unknown');
}

Download source

コード解説⚓︎

コードは、2つのヘルパー関数の定義から始まります。 トランザクションのアナウンス方法や承認の追跡方法の詳細については、転送トランザクション のチュートリアルを参照してください。その他のヘルパー関数については、以下のセクションで説明します。

その後、チュートリアルは 必要な鍵の設定現在のネットワーク状態の取得、およびマルチシグアカウントの 現在の設定の検出 へと進みます。

アカウントがすでにマルチシグとして設定されているかどうかに応じて、状況に合わせて 有効化 または 無効化 するためのトランザクションが作成されます。 最後に、トランザクションが アナウンスおよび承認 されます。

アカウントの設定⚓︎

KEY_TEMPLATE = '0' * 63 + '{}'

# Setup the keys for the multisig account and its two cosignatories
MULTISIG_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 = []
for i in range(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})')
const KEY_PREFIX = '0'.repeat(63);

// Setup the keys for the multisig account and its two cosignatories
const MULTISIG_PRIVATE_KEY = process.env.MULTISIG_PRIVATE_KEY || (
    KEY_PREFIX + '1');
const multisigKeyPair = new KeyPair(new PrivateKey(MULTISIG_PRIVATE_KEY));
const multisigAddress = facade.network.publicKeyToAddress(
    multisigKeyPair.publicKey);
console.log(`Multisig address: ${multisigAddress}`,
    `(public key ${multisigKeyPair.publicKey})`);

const cosignatoryKeyPairs = [];
const cosignatoryAddresses = [];
for (let i = 0; i < 2; i++) {
    const COSIGNATORY_PRIVATE_KEY =
        process.env[`COSIGNATORY${i}_PRIVATE_KEY`] || (
            KEY_PREFIX + String(i + 2));
    const kp = new KeyPair(new PrivateKey(COSIGNATORY_PRIVATE_KEY));
    cosignatoryKeyPairs.push(kp);
    const addr = facade.network.publicKeyToAddress(kp.publicKey);
    cosignatoryAddresses.push(addr);
    console.log(`Cosignatory ${i} address: ${addr}`,
        `(public key ${kp.publicKey})`);
}

このチュートリアルでは3つの個別のアカウントが必要です。 それらの 秘密鍵 は環境変数を通じて提供できます。設定されていない場合は、デフォルト値が使用されます。

環境変数 デフォルト値 用途
MULTISIG_PRIVATE_KEY 0000..0001 マルチシグアカウント
COSIGNATORY0_PRIVATE_KEY 0000..0002 連署者アカウント1
COSIGNATORY1_PRIVATE_KEY 0000..0003 連署者アカウント2

各秘密鍵は64文字の16進数文字列です。

マルチシグアカウントとその連署者アカウント1は、トランザクションをアナウンスするのに十分な資金を保有している必要があります。デフォルト値を使用する場合、これらのアカウントにはすでに資金が供給されている可能性があります。

上記のスニペットは、後で使用するために各アカウントの キーペアアドレス を派生させて保存します。

ネットワーク時間と手数料の取得⚓︎

    # Fetch current network time
    time_path = '/node/time'
    print(f'Fetching current network time from {time_path}')
    with urllib.request.urlopen(f'{NODE_URL}{time_path}') as response:
        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 fees
    fee_path = '/network/fees/transaction'
    print(f'Fetching recommended fees from {fee_path}')
    with urllib.request.urlopen(f'{NODE_URL}{fee_path}') as response:
        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}')
    // Fetch current network time
    const timePath = '/node/time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const timestamp = new NetworkTimestamp(
        timeJSON.communicationTimestamps.receiveTimestamp);
    console.log('  Network time:', timestamp.timestamp,
        'ms since nemesis');

    // Fetch recommended fees
    const feePath = '/network/fees/transaction';
    console.log('Fetching recommended fees from', feePath);
    const feeResponse = await fetch(`${NODE_URL}${feePath}`);
    const feeJSON = await feeResponse.json();
    const medianMult = feeJSON.medianFeeMultiplier;
    const minimumMult = feeJSON.minFeeMultiplier;
    const feeMult = Math.max(medianMult, minimumMult);
    console.log('  Fee multiplier:', feeMult);

転送トランザクション チュートリアルで説明されているプロセスに従い、ネットワーク時間と推奨手数料をそれぞれ /node/time GET および /network/fees/transaction GET から取得します。

マルチシグの検出⚓︎

以下の関数は、/account/{address}/multisig GET エンドポイントを使用して、指定されたアドレスの現在の連署者リストを取得します。 アカウントがマルチシグとして設定されていない、または一度も使用されていない場合、関数は空のリストを返します。

# Returns the cosignatory addresses of the provided multisig account,
# or an empty list if the account is not multisig or has never been used
def get_multisig_cosignatories(address):
    multisig_path = f'/account/{address}/multisig'
    print(f'Getting cosignatories from {multisig_path}')
    try:
        url = f'{NODE_URL}{multisig_path}'
        with urllib.request.urlopen(url) as response:
            status = json.loads(response.read().decode())
            cosignatories = status['multisig']['cosignatoryAddresses']
            print(f'  Response: {cosignatories}')
            return cosignatories
    except urllib.error.HTTPError:
        # The address has never been used
        print('  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 used
async function getMultisigCosignatories(address) {
    const multisigPath = `/account/${address}/multisig`;
    console.log(`Getting cosignatories from ${multisigPath}`);
    try {
        const response = await fetch(`${NODE_URL}${multisigPath}`);
        const json = await response.json();
        const cosignatories = json.multisig.cosignatoryAddresses;
        console.log('  Response:', JSON.stringify(cosignatories));
        return cosignatories;
    } catch {
        console.log('  Response: No cosignatories');
        return [];
    }
}

このリストはチュートリアルの動作モードを決定し、適切な設定トランザクションを構築して署名するために使用されます。

    # Get current state of the multisig account and decide which
    # operation to perform
    cosignatories = get_multisig_cosignatories(multisig_address)
    if len(cosignatories) == 0:
        # Enable the multisig
        transaction = multisig_enable_transaction()
        # This operation must be signed by the multisig account
        signer_key_pair = multisig_key_pair
    else:
        # Disable the multisig
        transaction = multisig_disable_transaction()
        # This operation must be signed by one of the cosigners
        signer_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 perform
    const cosignatories = await getMultisigCosignatories(multisigAddress);
    let transaction;
    let signerKeyPair;
    if (cosignatories.length === 0) {
        // Enable the multisig
        transaction = multisigEnableTransaction(timestamp, feeMult);
        // This operation must be signed by the multisig account
        signerKeyPair = multisigKeyPair;
    } else {
        // Disable the multisig
        transaction = multisigDisableTransaction(timestamp, feeMult);
        // This operation must be signed by one of the cosigners
        signerKeyPair = cosignatoryKeyPairs[0];
    }
    const payload = SymbolTransactionFactory.attachSignature(
        transaction,
        facade.signTransaction(signerKeyPair, transaction));

マルチシグの有効化と無効化の唯一の違いは、作成されるトランザクションとそれに署名するアカウントです(次の2つのセクションで説明します)。

マルチシグの有効化⚓︎

連署者の追加や削除を含む、アカウントのマルチシグ設定へのすべての変更は、MultisigAccountModificationTransactionV1 を使用して行われます。これは必ず アグリゲートトランザクション 内に埋め込まれる必要があります。

    # Create an embedded multisig account modification transaction
    # that adds two cosignatories
    embedded_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 cosignatories
    const embeddedTransaction = facade.transactionFactory
        .createEmbedded({
            type: 'multisig_account_modification_transaction_v1',
            // This is the account that will be turned into a multisig
            signerPublicKey: multisigKeyPair.publicKey,
            // Delta of the number of signatures required for approvals
            minApprovalDelta: 1,
            // Delta of the number of signatures required for removals
            minRemovalDelta: 1,
            addressAdditions: cosignatoryAddresses
        });

埋め込まれた MultisigAccountModificationTransactionV1 には以下のフィールドが含まれます。

  • signer_public_key: マルチシグ設定を変更するアカウントの 公開鍵

  • min_approval_delta: マルチシグアカウントからのトランザクションを承認するために必要となる署名数の、「希望する値」と「現在の値」の差分。

    このケースでは、アカウントは最初は通常のアカウントであるため、現在の必要署名数は 0 です。 連署者のうち1人からの署名を必要とするマルチシグアカウントに変換するために、デルタは 1 に設定されます。

    デルタ値は、次のセクションで示すように、現在の値を減らすために負の値にすることもできます。

  • min_removal_delta: アカウント設定から連署者を削除するために必要な署名数の差分。 これにより、例えば通常のトランザクションの承認よりも、連署者の削除(より機微なガバナンス操作)に多くの署名を要求するといった設定が可能です。

  • address_additions: アカウントに追加される連署者のアドレスリスト。 cosignatory_addresses 変数は アカウントの設定 で準備したものです。

安全対策

プロトコルには、アカウントを無効な状態でロックされてしまうのを防ぐための安全メカニズムが含まれています。無効なマルチシグ構成となるトランザクションはエラーで拒否されます。例えば以下のような場合です。

  • 連署者の数が、最小必要署名数を下回る場合
  • 連署者ではないアドレスを削除しようとする場合
  • 必要な署名が不足している場合
  • 不要な署名が含まれている場合

埋め込みトランザクションは、それが唯一の内部トランザクションであっても、アグリゲートトランザクションにラップされます。

    # Build the aggregate transaction
    embedded_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 size
    transaction.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 transaction
    const embeddedTransactions = [embeddedTransaction];
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        // This is the account that will pay for this transaction
        signerPublicKey: 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 size
    transaction.fee = new models.Amount(
        feeMult * (transaction.size + 104 * cosignatoryKeyPairs.length));
    console.log('Enabling the multisig with the aggregate transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

簡単にするため、このチュートリアルでは [アグリゲートコンプリートトランザクション] (default: アグリゲートコンプリートトランザクション) を使用します。 詳細については、コンプリート および ボンデッド アグリゲートトランザクションのチュートリアルを参照してください。

すべての連署に必要な容量を考慮して、トランザクション手数料の計算には注意が払われています。

最後に、署名がアグリゲートトランザクションに付加されます。

    # Sign the aggregate transaction with the multisig's signature
    facade.transaction_factory.attach_signature(transaction,
        facade.sign_transaction(multisig_key_pair, transaction))

    # Append signatures from all cosignatories
    for cosignatory_key_pair in cosignatory_key_pairs:
        transaction.cosignatures.append(
            facade.cosign_transaction(cosignatory_key_pair, transaction)
        )
    // Sign the aggregate transaction with the multisig's signature
    SymbolTransactionFactory.attachSignature(transaction,
        facade.signTransaction(multisigKeyPair, transaction));

    // Append signatures from all cosignatories
    for (const cosignatoryKeyPair of cosignatoryKeyPairs) {
        transaction.cosignatures.push(
            facade.cosignTransaction(cosignatoryKeyPair, transaction));
    }

このケースでは、マルチシグに変換されるアカウントの署名と、新しい責任を追うことを明示的に承認するために連署者の署名が必要です。

署名の1つはトランザクションのメインの署名者であり、 を使用して追加されます。残りの署名は連署(cosignature)であり、 を使用して追加されます。メインの署名者の選択は、どのアカウントがトランザクション手数料を支払うかにのみ影響します。

一度アカウントのマルチシグが有効になると、そのアカウント自身の署名は不要になります。そのアカウントに関わるすべてのトランザクションには、代わりに設定された連署者の署名が必要となります。

マルチシグの無効化⚓︎

マルチシグ設定を無効にするには、すべての連署者を削除する必要があります。プロセスは有効化と似ていますが、2つの大きな違いがあります。連署者は1人ずつ削除しなければならないことと、マルチシグアカウント自体がトランザクションに署名できないことです。

このため、2つの MultisigAccountModificationTransactionV1 が作成されます。

    # Create two embedded multisig account modification transactions
    # because cosignatories must be removed one by one
    embedded_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 one
    const embeddedTransaction1 = facade.transactionFactory
        .createEmbedded({
            type: 'multisig_account_modification_transaction_v1',
            // This is the multisig account that will be modified
            signerPublicKey: multisigKeyPair.publicKey,
            // Keep required signatures unchanged for this step
            minApprovalDelta: 0,
            minRemovalDelta: 0,
            addressDeletions: [cosignatoryAddresses[1]]
        });
    const embeddedTransaction2 = facade.transactionFactory
        .createEmbedded({
            type: 'multisig_account_modification_transaction_v1',
            // This is the multisig account that will be modified
            signerPublicKey: multisigKeyPair.publicKey,
            // Decrease required signatures after final removal
            minApprovalDelta: -1,
            minRemovalDelta: -1,
            addressDeletions: [cosignatoryAddresses[0]]
        });

両方のトランザクションで、 signer_public_key はマルチシグアカウントの公開鍵に設定されます。

最初のトランザクションは、承認または削除のデルタを変更せずに cosignatory_addresses[1] を削除します。これは、まだ1人の連署者が残っており、署名が依然として必要だからです。

2番目のトランザクションは、最後に残った連署者を削除し、 min_approval_deltamin_removal_delta の両方を -1 に設定します。 この時点で、両方のフィールドの現在の値は 有効化 ステップで設定された 1 であり、希望する値は 0 であるため、デルタは -1 になります。

その後、両方の埋め込みトランザクションはアグリゲートトランザクションにラップされ、署名されます。

    # Build the aggregate transaction
    embedded_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 signature
    facade.transaction_factory.attach_signature(transaction,
        facade.sign_transaction(cosignatory_key_pairs[0], transaction))
    // Build the aggregate transaction
    const embeddedTransactions = [embeddedTransaction1,
        embeddedTransaction2];
    const transaction = facade.transactionFactory.create({
        type: 'aggregate_complete_transaction_v3',
        // This is the account that will pay for this transaction
        signerPublicKey: 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 = new models.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 signature
    SymbolTransactionFactory.attachSignature(transaction,
        facade.signTransaction(cosignatoryKeyPairs[0], transaction));

アグリゲートトランザクションは cosignatory_addresses[0] によって署名されます。 これが唯一の有効な選択肢です。一度アカウントに連署者が設定されると、そのアカウント自身でトランザクションに署名することはできなくなり、また cosignatory_addresses[1] は最初の埋め込みトランザクションが実行された後にマルチシグから削除されるためです。

結果として、連署(追加の署名)は不要です。メインの署名のみが必要です。 マルチシグは最小削除要件が1つの署名で設定されていたため、操作全体を単一の連署者によって開始し承認することができます。

連署者は、両方が同等の権限を持っているため、逆の順序で削除することもできました。唯一の違いは、どのアカウントがトランザクションに署名し、手数料を支払うかです。

アグリゲートトランザクションの送信⚓︎

最後のステップは、構築されたトランザクションをアナウンスし、転送トランザクション チュートリアルで説明されているように承認を待つことです。

    # Announce and wait for confirmation
    transaction_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 confirmation
    const transactionHash =
        facade.hashTransaction(transaction).toString();
    console.log(
        'Built aggregate transaction with hash:', transactionHash);
    await announceTransaction(payload, 'aggregate transaction');
    await waitForConfirmation(transactionHash, 'aggregate transaction');

出力⚓︎

以下に示す出力は、プログラムの典型的な2つの実行結果に対応しています。

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: 102518284736 ms since nemesis
Fetching recommended fees from /network/fees/transaction
  Fee multiplier: 100
Getting cosignatories from /account/TB6QOVCUOFRCF5QJSKPIQMLUVWGJS3KYFDETRPA/multisig
  Response: No cosignatories
Enabling the multisig with the aggregate transaction:
{
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "signer_public_key": "4CB5ABF6AD79FBF5ABBCCAFCC269D85CD2651ED4B885B5869F241AEDF0A5BA29",
  "version": 3,
  "network": 152,
  "type": 16705,
  "fee": "48000",
  "deadline": "102525484736",
  "transactions_hash": "08E301DCCE53F1A3317E72EDF2E318281B040434511547910204E40AA54FB701",
  "transactions": [
    {
      "signer_public_key": "4CB5ABF6AD79FBF5ABBCCAFCC269D85CD2651ED4B885B5869F241AEDF0A5BA29",
      "version": 1,
      "network": 152,
      "type": 16725,
      "min_removal_delta": 1,
      "min_approval_delta": 1,
      "address_additions": [
        "98427313FF41CEE4601CF077793A316C74BEB176EF30EFF7",
        "98436CEAFC25CD0F76EFA36C6926FC59EDD6D98F1B4D4247"
      ],
      "address_deletions": []
    }
  ],
  "cosignatures": []
}
Built aggregate transaction with hash: 9363F72333F34A0ACD649AECED9F916F664F16B341D308B06DB2CA0D976DCA03
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 34 seconds

出力の主なポイント:

  • 2-4行目: 関与するすべてのアカウントのアドレスと公開鍵。
  • 10行目 (Response: No cosignatories): 現在連署者は設定されていません。
  • 27行目 ("min_approval_delta": 1): トランザクション承認に必要な署名数が1つ増加します。
  • 28行目 ("min_removal_delta": 1): 連署者の削除に必要な署名数が1つ増加します。
  • 29行目 ("address_additions"): 連署者として追加されるアドレスのリスト。
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

出力の主なポイント:

  • 2-4行目: 関与するすべてのアカウントのアドレスと公開鍵。
  • 10行目 (Response: [ ... ]): 既存の連署者が検出されました。
  • 27-32行目 (最初の埋め込みトランザクション): 最小必要署名数は変更されず、新しい連署者は追加されず、既存の連署者が1人削除されます。
  • 39-44行目 (2番目の埋め込みトランザクション): 最小必要署名数が1つ減少し、新しい連署者は追加されず、最後に残った連署者が削除されます。

出力に示されているトランザクションハッシュを使用して、Symbol Testnet Explorer でトランザクションを検索できます。

結論⚓︎

このチュートリアルでは、以下の方法を説明しました。

ステップ 関連ドキュメント
現在のマルチシグ設定の取得 /account/{address}/multisig GET
マルチシグアカウントの有効化 MultisigAccountModificationTransactionV1
マルチシグアカウントの無効化 MultisigAccountModificationTransactionV1
設定を埋め込みトランザクションにラップする