コンテンツにスキップ

マルチシグアカウントを設定する⚓︎

上級

マルチシグアカウントマルチシグ とも呼ばれ、そのアカウントだけではトランザクションを開始することはできません。 代わりに、連署人 アカウントがマルチシグアカウントに代わってトランザクションを作成し、署名します。

このチュートリアルでは、通常のアカウントを 2 人の連署人のうち、どちらか一方の承認を必要とするマルチシグアカウントに変換する方法を説明します。 アカウントがすでにマルチシグの場合は、連署人を削除して通常のアカウントに戻す方法を説明します。

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

Multisignature TreeMultisignature AccountマルチシグアカウントCosignatory 0連署人 0Cosignatory 0->Multisignature AccountCosignatory 1連署人 1Cosignatory 1->Multisignature Account

前提条件⚓︎

始める前に、次の準備をしてください。

さらに、トランザクションのアナウンスと承認の方法を理解するため、XEM を送信する チュートリアルを確認してください。

完全なコード⚓︎

import json
import os
import time
import urllib.request

from symbolchain.CryptoTypes import PrivateKey
from symbolchain.facade.NemFacade import NemFacade
from symbolchain.nc import Amount
from symbolchain.nem.FeeCalculator import calculate_transaction_fee
from symbolchain.nem.Network import NetworkTimestamp

NODE_URL = os.getenv('NODE_URL', 'http://libertalia.nemtest.net:7890')
print(f'Using node {NODE_URL}')

facade = NemFacade('testnet')

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

# Set up 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 = NemFacade.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 = []
for i in range(2):
    COSIGNATORY_PRIVATE_KEY = os.getenv(
        f'COSIGNATORY{i}_PRIVATE_KEY', KEY_TEMPLATE.format(i + 2))
    key_pair = NemFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY))
    cosignatory_key_pairs.append(key_pair)
    addr = facade.network.public_key_to_address(key_pair.public_key)
    print(f'Cosignatory {i} address: '
        f'{addr} (public key {key_pair.public_key})')


# Helper function to announce a transaction
def announce_transaction(payload, label):
    announce_path = '/transaction/announce'
    print(f'Announcing {label} to {announce_path}')
    request = urllib.request.Request(
        f'{NODE_URL}{announce_path}',
        data=payload.encode(),
        headers={'Content-Type': 'application/json'},
        method='POST'
    )
    with urllib.request.urlopen(request) as announce_response:
        result = json.loads(announce_response.read().decode())
    print(f'  Result: {result["message"]}')
    return result['message']


# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    status_path = f'/transaction/get?hash={tx_hash}'
    print(f'Waiting for {label} confirmation from {status_path}')
    is_confirmed = False
    for _ in range(120):
        try:
            with urllib.request.urlopen(
                f'{NODE_URL}{status_path}'
            ) as status_response:
                confirmed = json.loads(status_response.read().decode())
                height = confirmed['meta']['height']
                print(f'{label} confirmed in block {height}')
                is_confirmed = True
                break
        except urllib.error.HTTPError:
            print('  Transaction status: pending')
        time.sleep(1)
    if not is_confirmed:
        print(f'{label} confirmation took too long.')


# Returns the cosignatory addresses of the provided multisig
# account, or an empty list if the account is not multisig
def get_multisig_cosignatories(address):
    account_path = f'/account/get?address={address}'
    print(f'Getting cosignatories from {account_path}')
    url = f'{NODE_URL}{account_path}'
    with urllib.request.urlopen(url) as account_response:
        account_info = json.loads(account_response.read().decode())
        found_cosignatories = [
            cosignatory['address']
            for cosignatory in account_info['meta']['cosignatories']
        ]
        if not found_cosignatories:
            print('  Response: No cosignatories')
            return []
        print(f'  Response: {found_cosignatories}')
        return found_cosignatories



# Returns a transaction that turns a regular account into a multisig
def multisig_enable_transaction(tx_timestamp, tx_deadline,
        approval_delta):
    # Create a multisig account modification transaction
    # that adds the cosignatories
    modifications = [
        {'modification': {
            'modification_type': 'add_cosignatory',
            'cosignatory_public_key': key_pair.public_key
        }}
        for key_pair in cosignatory_key_pairs
    ]
    transaction = facade.transaction_factory.create({
        'type': 'multisig_account_modification_transaction_v2',
        # This is the account that will be turned into a multisig
        'signer_public_key': multisig_key_pair.public_key,
        'timestamp': tx_timestamp.timestamp,
        'deadline': tx_deadline.timestamp,
        # Change of the number of cosignatures
        # required to approve transactions
        'min_approval_delta': approval_delta,
        'modifications': modifications
    })

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {fee / 1_000_000} XEM')
    print('Enabling the multisig with the modification transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

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



# Returns a transaction that removes one cosignatory from the multisig
def multisig_removal_transaction(tx_timestamp, tx_deadline,
        removed_key_pair, approval_delta):
    # Create a multisig account modification transaction
    # that removes a single cosignatory
    inner_transaction = facade.transaction_factory.create({
        'type': 'multisig_account_modification_transaction_v2',
        # This is the multisig account that will be modified
        'signer_public_key': multisig_key_pair.public_key,
        'timestamp': tx_timestamp.timestamp,
        'deadline': tx_deadline.timestamp,
        # Change of the number of cosignatures
        # required to approve transactions
        'min_approval_delta': approval_delta,
        'modifications': [
            {'modification': {
                'modification_type': 'delete_cosignatory',
                'cosignatory_public_key': removed_key_pair.public_key
            }}
        ]
    })

    # Wrap the modification in a multisig transaction
    inner_fee = calculate_transaction_fee(inner_transaction)
    inner_transaction.fee = Amount(inner_fee)
    transaction = facade.transaction_factory.create({
        'type': 'multisig_transaction_v1',
        # This is the cosignatory that initiates the removal
        'signer_public_key': cosignatory_key_pairs[0].public_key,
        'timestamp': tx_timestamp.timestamp,
        'deadline': tx_deadline.timestamp,
        'inner_transaction':
            facade.transaction_factory.to_non_verifiable_transaction(
                inner_transaction)
    })

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {(inner_fee + fee) / 1_000_000} XEM')
    print('Disabling the multisig with the multisig transaction:')
    print(json.dumps(transaction.to_json(), indent=2))

    # Sign the transaction with the cosignatory's key
    signature = facade.sign_transaction(
        cosignatory_key_pairs[0], transaction)
    facade.transaction_factory.attach_signature(transaction, signature)
    return transaction


try:
    # Fetch current network time
    time_path = '/time-sync/network-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())
        network_time = response_json['receiveTimeStamp'] // 1000
        print(f'  Network time: {network_time} s since the nemesis block')

    # Derived fields from network time
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)

    # 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
        transactions = [multisig_enable_transaction(
            timestamp, deadline, 1)]
    else:
        # Disable the multisig
        transactions = [
            multisig_removal_transaction(
                timestamp, deadline, cosignatory_key_pairs[1], 0),
            multisig_removal_transaction(
                timestamp, deadline, cosignatory_key_pairs[0], -1)
        ]

    # Announce each transaction and wait for confirmation
    for signed_transaction in transactions:
        transaction_hash = facade.hash_transaction(signed_transaction)
        print(f'Built transaction with hash: {transaction_hash}')
        json_payload = facade.transaction_factory.to_json(
            signed_transaction)
        announce_result = announce_transaction(
            json_payload, 'transaction')
        if 'SUCCESS' != announce_result:
            print('Transaction rejected')
            break
        wait_for_confirmation(transaction_hash, 'transaction')

except Exception as e:
    print(e)

Download source

import { PrivateKey } from 'symbol-sdk';
import {
    NemFacade,
    NetworkTimestamp,
    calculateTransactionFee,
    models
} from 'symbol-sdk/nem';

const NODE_URL = process.env.NODE_URL ||
    'http://libertalia.nemtest.net:7890';
console.log('Using node', NODE_URL);

const facade = new NemFacade('testnet');

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

// Set up 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 NemFacade.KeyPair(
    new PrivateKey(MULTISIG_PRIVATE_KEY));
const multisigAddress = facade.network.publicKeyToAddress(
    multisigKeyPair.publicKey);
console.log(`Multisig address: ${multisigAddress}`,
    `(public key ${multisigKeyPair.publicKey})`);

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

// Helper function to announce a transaction
async function announceTransaction(payload, label) {
    const announcePath = '/transaction/announce';
    console.log(`Announcing ${label} to ${announcePath}`);
    const announceResponse = await fetch(`${NODE_URL}${announcePath}`, {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: payload
    });
    const result = await announceResponse.json();
    console.log('  Result:', result.message);
    return result.message;
}

// Helper function to wait for transaction confirmation
async function waitForConfirmation(transactionHash, label) {
    const statusPath = `/transaction/get?hash=${transactionHash}`;
    console.log(`Waiting for ${label} confirmation from`, statusPath);
    let isConfirmed = false;
    for (let attempt = 1; 120 >= attempt; ++attempt) {
        const response = await fetch(`${NODE_URL}${statusPath}`);
        if (response.ok) {
            const confirmed = await response.json();
            console.log(`${label} confirmed in block`,
                confirmed.meta.height);
            isConfirmed = true;
            break;
        }
        console.log('  Transaction status: pending');
        await new Promise(resolve => { setTimeout(resolve, 1000); });
    }
    if (!isConfirmed)
        console.warn(`${label} confirmation took too long.`);
}

// Returns the cosignatory addresses of the provided multisig
// account, or an empty list if the account is not multisig
async function getMultisigCosignatories(address) {
    const accountPath = `/account/get?address=${address}`;
    console.log(`Getting cosignatories from ${accountPath}`);
    const response = await fetch(`${NODE_URL}${accountPath}`);
    const accountInfo = await response.json();
    const foundCosignatories = accountInfo.meta.cosignatories
        .map(cosignatory => cosignatory.address);
    if (0 === foundCosignatories.length) {
        console.log('  Response: No cosignatories');
        return [];
    }
    console.log('  Response:', JSON.stringify(foundCosignatories));
    return foundCosignatories;
}


// Returns a transaction that turns a regular account into a multisig
function multisigEnableTransaction(timestamp, deadline, approvalDelta) {
    // Create a multisig account modification transaction
    // that adds the cosignatories
    const modifications = cosignatoryKeyPairs.map(keyPair => ({
        modification: {
            modificationType: 'add_cosignatory',
            cosignatoryPublicKey: keyPair.publicKey.toString()
        }
    }));
    const transaction = facade.transactionFactory.create({
        type: 'multisig_account_modification_transaction_v2',
        // This is the account that will be turned into a multisig
        signerPublicKey: multisigKeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        // Change of the number of cosignatures
        // required to approve transactions
        minApprovalDelta: approvalDelta,
        modifications
    });

    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log(`  Transaction fee: ${Number(fee) / 1_000_000} XEM`);
    console.log(
        'Enabling the multisig with the modification transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

    // Sign the transaction with the multisig's key
    const signature = facade.signTransaction(
        multisigKeyPair, transaction);
    facade.transactionFactory.static.attachSignature(
        transaction, signature);
    return transaction;
}


// Returns a transaction that removes one cosignatory from the multisig
function multisigRemovalTransaction(timestamp, deadline,
    removedKeyPair, approvalDelta) {
    // Create a multisig account modification transaction
    // that removes a single cosignatory
    const innerTransaction = facade.transactionFactory.create({
        type: 'multisig_account_modification_transaction_v2',
        // This is the multisig account that will be modified
        signerPublicKey: multisigKeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        // Change of the number of cosignatures
        // required to approve transactions
        minApprovalDelta: approvalDelta,
        modifications: [
            {
                modification: {
                    modificationType: 'delete_cosignatory',
                    cosignatoryPublicKey:
                        removedKeyPair.publicKey.toString()
                }
            }
        ]
    });

    // Wrap the modification in a multisig transaction
    const innerFee = calculateTransactionFee(innerTransaction);
    innerTransaction.fee = new models.Amount(innerFee);
    const transaction = facade.transactionFactory.create({
        type: 'multisig_transaction_v1',
        // This is the cosignatory that initiates the removal
        signerPublicKey: cosignatoryKeyPairs[0].publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        innerTransaction: facade.transactionFactory.static
            .toNonVerifiableTransaction(innerTransaction)
    });

    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log('  Transaction fee:',
        `${Number(innerFee + fee) / 1_000_000} XEM`);
    console.log(
        'Disabling the multisig with the multisig transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

    // Sign the transaction with the cosignatory's key
    const signature = facade.signTransaction(
        cosignatoryKeyPairs[0], transaction);
    facade.transactionFactory.static
        .attachSignature(transaction, signature);
    return transaction;
}

try {
    // Fetch current network time
    const timePath = '/time-sync/network-time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000);
    console.log('  Network time:', networkTime,
        's since the nemesis block');

    // Derived fields from network time
    const timestamp = new NetworkTimestamp(networkTime);
    const deadline = timestamp.addHours(2);

    // Get current state of the multisig account and decide
    // which operation to perform
    const cosignatories = await getMultisigCosignatories(multisigAddress);
    let transactions;
    if (0 === cosignatories.length) {
        // Enable the multisig
        transactions = [multisigEnableTransaction(
            timestamp, deadline, 1)];
    } else {
        // Disable the multisig
        transactions = [
            multisigRemovalTransaction(
                timestamp, deadline, cosignatoryKeyPairs[1], 0),
            multisigRemovalTransaction(
                timestamp, deadline, cosignatoryKeyPairs[0], -1)
        ];
    }

    // Announce each transaction and wait for confirmation
    for (const signedTransaction of transactions) {
        const transactionHash = facade.hashTransaction(signedTransaction)
            .toString();
        console.log('Built transaction with hash:', transactionHash);
        const jsonPayload = facade.transactionFactory.static
            .toJson(signedTransaction);
        const result = await announceTransaction(
            jsonPayload, 'transaction');
        if ('SUCCESS' !== result) {
            console.log('Transaction rejected');
            break;
        }
        await waitForConfirmation(transactionHash, 'transaction');
    }

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

Download source

コードの説明⚓︎

コードでは、トランザクションをアナウンスし、その承認を待つ 2 つのヘルパー関数を定義します。 これらの動作の詳細については、XEM を送信する チュートリアルを参照してください。 残りのヘルパー関数については、以下のセクションで説明します。

次に、マルチシグアカウントに必要な キーをセットアップ し、 現在のネットワーク時刻を取得 して、 マルチシグアカウントの 現在の設定を判定 します。

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

アカウントをセットアップする⚓︎

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

# Set up 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 = NemFacade.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 = []
for i in range(2):
    COSIGNATORY_PRIVATE_KEY = os.getenv(
        f'COSIGNATORY{i}_PRIVATE_KEY', KEY_TEMPLATE.format(i + 2))
    key_pair = NemFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY))
    cosignatory_key_pairs.append(key_pair)
    addr = facade.network.public_key_to_address(key_pair.public_key)
    print(f'Cosignatory {i} address: '
        f'{addr} (public key {key_pair.public_key})')
const KEY_PREFIX = '0'.repeat(63);

// Set up 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 NemFacade.KeyPair(
    new PrivateKey(MULTISIG_PRIVATE_KEY));
const multisigAddress = facade.network.publicKeyToAddress(
    multisigKeyPair.publicKey);
console.log(`Multisig address: ${multisigAddress}`,
    `(public key ${multisigKeyPair.publicKey})`);

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

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

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

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

マルチシグアカウントには、トランザクション手数料を支払うために十分な資金が必要です。 デフォルト値を使用する場合、このアカウントにはすでに資金がある可能性があります。

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

ネットワーク時刻を取得する⚓︎

    # Fetch current network time
    time_path = '/time-sync/network-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())
        network_time = response_json['receiveTimeStamp'] // 1000
        print(f'  Network time: {network_time} s since the nemesis block')

    # Derived fields from network time
    timestamp = NetworkTimestamp(network_time)
    deadline = timestamp.add_hours(2)
    // Fetch current network time
    const timePath = '/time-sync/network-time';
    console.log('Fetching current network time from', timePath);
    const timeResponse = await fetch(`${NODE_URL}${timePath}`);
    const timeJSON = await timeResponse.json();
    const networkTime = Math.floor(timeJSON.receiveTimeStamp / 1000);
    console.log('  Network time:', networkTime,
        's since the nemesis block');

    // Derived fields from network time
    const timestamp = new NetworkTimestamp(networkTime);
    const deadline = timestamp.addHours(2);

ネットワーク時刻は /time-sync/network-time GET から取得し、XEM を送信する チュートリアルで説明されている手順に従って、トランザクションの timestampdeadline フィールドを導出します。

マルチシグ操作を判定する⚓︎

# Returns the cosignatory addresses of the provided multisig
# account, or an empty list if the account is not multisig
def get_multisig_cosignatories(address):
    account_path = f'/account/get?address={address}'
    print(f'Getting cosignatories from {account_path}')
    url = f'{NODE_URL}{account_path}'
    with urllib.request.urlopen(url) as account_response:
        account_info = json.loads(account_response.read().decode())
        found_cosignatories = [
            cosignatory['address']
            for cosignatory in account_info['meta']['cosignatories']
        ]
        if not found_cosignatories:
            print('  Response: No cosignatories')
            return []
        print(f'  Response: {found_cosignatories}')
        return found_cosignatories
// Returns the cosignatory addresses of the provided multisig
// account, or an empty list if the account is not multisig
async function getMultisigCosignatories(address) {
    const accountPath = `/account/get?address=${address}`;
    console.log(`Getting cosignatories from ${accountPath}`);
    const response = await fetch(`${NODE_URL}${accountPath}`);
    const accountInfo = await response.json();
    const foundCosignatories = accountInfo.meta.cosignatories
        .map(cosignatory => cosignatory.address);
    if (0 === foundCosignatories.length) {
        console.log('  Response: No cosignatories');
        return [];
    }
    console.log('  Response:', JSON.stringify(foundCosignatories));
    return foundCosignatories;
}

このヘルパーは /account/get GET エンドポイントを使い、指定したアドレスの現在の連署人一覧を取得します。 空のリストが返された場合、そのアカウントはマルチシグアカウントとして設定されていません。

既存のマルチシグ設定を確認してください

分かりやすくするため、このチュートリアルでは、連署人のリストが 空でない 場合、そのアカウントはこのチュートリアルで設定されたマルチシグであると仮定します。

連署人が異なる場合など、設定が想定したものではない場合、削除トランザクションは拒否されます。

アプリケーションでは、変更を試みる前に、連署人の完全なリストと必要な署名の最小数を含む現在の設定を必ず確認してください。

    # 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
        transactions = [multisig_enable_transaction(
            timestamp, deadline, 1)]
    else:
        # Disable the multisig
        transactions = [
            multisig_removal_transaction(
                timestamp, deadline, cosignatory_key_pairs[1], 0),
            multisig_removal_transaction(
                timestamp, deadline, cosignatory_key_pairs[0], -1)
        ]
    // Get current state of the multisig account and decide
    // which operation to perform
    const cosignatories = await getMultisigCosignatories(multisigAddress);
    let transactions;
    if (0 === cosignatories.length) {
        // Enable the multisig
        transactions = [multisigEnableTransaction(
            timestamp, deadline, 1)];
    } else {
        // Disable the multisig
        transactions = [
            multisigRemovalTransaction(
                timestamp, deadline, cosignatoryKeyPairs[1], 0),
            multisigRemovalTransaction(
                timestamp, deadline, cosignatoryKeyPairs[0], -1)
        ];
    }

返された連署人によって、アカウントがマルチシグとして設定されているかどうかが決まり、マルチシグを有効化または無効化するトランザクションを作成するかどうかも決まります。

トランザクションを構築する関数と、それらが使用する差分値については、次の 2 つのセクションで説明します。

マルチシグを有効化する⚓︎

# Returns a transaction that turns a regular account into a multisig
def multisig_enable_transaction(tx_timestamp, tx_deadline,
        approval_delta):
    # Create a multisig account modification transaction
    # that adds the cosignatories
    modifications = [
        {'modification': {
            'modification_type': 'add_cosignatory',
            'cosignatory_public_key': key_pair.public_key
        }}
        for key_pair in cosignatory_key_pairs
    ]
    transaction = facade.transaction_factory.create({
        'type': 'multisig_account_modification_transaction_v2',
        # This is the account that will be turned into a multisig
        'signer_public_key': multisig_key_pair.public_key,
        'timestamp': tx_timestamp.timestamp,
        'deadline': tx_deadline.timestamp,
        # Change of the number of cosignatures
        # required to approve transactions
        'min_approval_delta': approval_delta,
        'modifications': modifications
    })
// Returns a transaction that turns a regular account into a multisig
function multisigEnableTransaction(timestamp, deadline, approvalDelta) {
    // Create a multisig account modification transaction
    // that adds the cosignatories
    const modifications = cosignatoryKeyPairs.map(keyPair => ({
        modification: {
            modificationType: 'add_cosignatory',
            cosignatoryPublicKey: keyPair.publicKey.toString()
        }
    }));
    const transaction = facade.transactionFactory.create({
        type: 'multisig_account_modification_transaction_v2',
        // This is the account that will be turned into a multisig
        signerPublicKey: multisigKeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        // Change of the number of cosignatures
        // required to approve transactions
        minApprovalDelta: approvalDelta,
        modifications
    });

連署人の追加や削除を含むアカウントのマルチシグ設定変更は、すべて MultisigAccountModificationTransactionV2 を使って行います。

トランザクションでは、次の項目を指定します。

  • : マルチシグ設定の変更では、タイプ MultisigAccountModificationTransactionV2 を使用します。

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

  • : ネットワーク時刻の手順で計算した値。

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

    この場合、アカウントは最初は通常のアカウントなので、必要な連署数の現在値は 0 です。 連署人の 1 人からの署名を必要とするマルチシグアカウントに変換するため、差分を 1 に設定します。

    次のセクションで示すように、現在値を 減らす 場合は差分を負の値にします。

  • : アカウントの連署人に対する変更のリスト。 各変更では、公開鍵 で識別される 1 人の連署人を追加または削除します。

    この場合、add_cosignatory 変更を 2 つ使って、セットアップ段階 で準備した連署人を追加します。

安全対策

プロトコルには、アカウントが無効な状態に固定されることを防ぐ安全機構が含まれています。 無効なマルチシグ設定になるトランザクションはエラーで拒否されます。 例えば、次の場合です。

  • 登録済みの連署人の数が、承認に必要な連署数に達していない
  • すでに連署人であるアカウントを追加する
  • 連署人ではないアカウントを削除する
  • 1 つのトランザクションで複数の連署人を削除する
  • マルチシグアカウントを連署人として追加する
    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {fee / 1_000_000} XEM')
    print('Enabling the multisig with the modification transaction:')
    print(json.dumps(transaction.to_json(), indent=2))
    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log(`  Transaction fee: ${Number(fee) / 1_000_000} XEM`);
    console.log(
        'Enabling the multisig with the modification transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

トランザクション手数料は で計算し、トランザクションに付加します。 マルチシグアカウント変更トランザクションの固定手数料は 0.5 XEM で、手数料表 に示されています。

    # Sign the transaction with the multisig's key
    signature = facade.sign_transaction(multisig_key_pair, transaction)
    facade.transaction_factory.attach_signature(transaction, signature)
    return transaction
    // Sign the transaction with the multisig's key
    const signature = facade.signTransaction(
        multisigKeyPair, transaction);
    facade.transactionFactory.static.attachSignature(
        transaction, signature);
    return transaction;

最後に、トランザクションに署名します。 この場合、マルチシグに変換するアカウントの署名だけが必要です。 連署人は変換トランザクションに署名しません。

以降は連署人がトランザクションを開始します

アカウントでマルチシグを有効にすると、そのアカウント自身の署名は受け付けられなくなります。 そのアカウントから送信するトランザクション(送金や、さらにマルチシグを変更するトランザクションなど)は、次のセクションに示すように、連署人が開始して署名する必要があります。

マルチシグを無効化する⚓︎

マルチシグ設定を無効にするには、すべての連署人を削除する必要があります。 手順は有効化の場合と似ていますが、2 つの重要な違いがあります。 連署人は 1 人ずつ削除する必要があり、マルチシグアカウント自身はトランザクションに署名できません。

# Returns a transaction that removes one cosignatory from the multisig
def multisig_removal_transaction(tx_timestamp, tx_deadline,
        removed_key_pair, approval_delta):
    # Create a multisig account modification transaction
    # that removes a single cosignatory
    inner_transaction = facade.transaction_factory.create({
        'type': 'multisig_account_modification_transaction_v2',
        # This is the multisig account that will be modified
        'signer_public_key': multisig_key_pair.public_key,
        'timestamp': tx_timestamp.timestamp,
        'deadline': tx_deadline.timestamp,
        # Change of the number of cosignatures
        # required to approve transactions
        'min_approval_delta': approval_delta,
        'modifications': [
            {'modification': {
                'modification_type': 'delete_cosignatory',
                'cosignatory_public_key': removed_key_pair.public_key
            }}
        ]
    })
// Returns a transaction that removes one cosignatory from the multisig
function multisigRemovalTransaction(timestamp, deadline,
    removedKeyPair, approvalDelta) {
    // Create a multisig account modification transaction
    // that removes a single cosignatory
    const innerTransaction = facade.transactionFactory.create({
        type: 'multisig_account_modification_transaction_v2',
        // This is the multisig account that will be modified
        signerPublicKey: multisigKeyPair.publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        // Change of the number of cosignatures
        // required to approve transactions
        minApprovalDelta: approvalDelta,
        modifications: [
            {
                modification: {
                    modificationType: 'delete_cosignatory',
                    cosignatoryPublicKey:
                        removedKeyPair.publicKey.toString()
                }
            }
        ]
    });

このヘルパーは、連署人を削除する MultisigAccountModificationTransactionV2 を構築します。 削除する連署人と適用する承認差分をパラメーターとして受け取ります。 設定を変更する対象がマルチシグアカウントであるため、 にはマルチシグアカウントの公開鍵を設定します。

マルチシグ操作を判定する で示したように、このヘルパーは 2 回呼び出されます。

1 回目の呼び出しでは、 を承認差分 0 で削除します。これは、連署人が 1 人残るためです。

2 回目の呼び出しでは、残った連署人を承認差分 -1 で削除し、必要な連署数を 1 から 0 に減らします。

    # Wrap the modification in a multisig transaction
    inner_fee = calculate_transaction_fee(inner_transaction)
    inner_transaction.fee = Amount(inner_fee)
    transaction = facade.transaction_factory.create({
        'type': 'multisig_transaction_v1',
        # This is the cosignatory that initiates the removal
        'signer_public_key': cosignatory_key_pairs[0].public_key,
        'timestamp': tx_timestamp.timestamp,
        'deadline': tx_deadline.timestamp,
        'inner_transaction':
            facade.transaction_factory.to_non_verifiable_transaction(
                inner_transaction)
    })
    // Wrap the modification in a multisig transaction
    const innerFee = calculateTransactionFee(innerTransaction);
    innerTransaction.fee = new models.Amount(innerFee);
    const transaction = facade.transactionFactory.create({
        type: 'multisig_transaction_v1',
        // This is the cosignatory that initiates the removal
        signerPublicKey: cosignatoryKeyPairs[0].publicKey.toString(),
        timestamp: timestamp.timestamp,
        deadline: deadline.timestamp,
        innerTransaction: facade.transactionFactory.static
            .toNonVerifiableTransaction(innerTransaction)
    });

マルチシグアカウントは自分でトランザクションに署名できないため、それぞれの変更を MultisigTransactionV1 でラップします。

内部の変更トランザクションは で変換し、ラップ用のマルチシグトランザクションに埋め込めるようにします。

    # Calculate and attach the transaction fee
    fee = calculate_transaction_fee(transaction)
    transaction.fee = Amount(fee)
    print(f'  Transaction fee: {(inner_fee + fee) / 1_000_000} XEM')
    print('Disabling the multisig with the multisig transaction:')
    print(json.dumps(transaction.to_json(), indent=2))
    // Calculate and attach the transaction fee
    const fee = calculateTransactionFee(transaction);
    transaction.fee = new models.Amount(fee);
    console.log('  Transaction fee:',
        `${Number(innerFee + fee) / 1_000_000} XEM`);
    console.log(
        'Disabling the multisig with the multisig transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

内部トランザクションとラッパーの両方に手数料がかかります。変更には 0.5 XEM、マルチシグラッパーには 0.15 XEM で、手数料表 に示されています。 どちらの手数料もマルチシグアカウントから差し引かれます。 マルチシグに代わってトランザクションを開始する連署人が手数料を支払うことはありません。

    # Sign the transaction with the cosignatory's key
    signature = facade.sign_transaction(
        cosignatory_key_pairs[0], transaction)
    facade.transaction_factory.attach_signature(transaction, signature)
    return transaction
    // Sign the transaction with the cosignatory's key
    const signature = facade.signTransaction(
        cosignatoryKeyPairs[0], transaction);
    facade.transactionFactory.static
        .attachSignature(transaction, signature);
    return transaction;

最後に、それぞれのマルチシグトランザクションに、それを開始する連署人、つまりラッパーの に設定された連署人が署名します。 ここでは、2 つの削除を が開始して署名します。

このマルチシグでは必要な署名が 1 つだけなので、署名は 1 つで十分です。 より厳しい設定では、他のトランザクションと同様に、削除に追加の連署人の承認が必要になります。ただし、削除対象の連署人自身の署名は要件に数えられません。

最後に残った連署人の削除は特殊なケースです。 マルチシグアカウントの代わりに署名できるのは連署人だけなので、最後の連署人が 2 つ目のトランザクションで示すように、自身の削除に署名します。

連署人を逆の順序で削除することもできます。 違いは、各トランザクションを開始して署名する連署人だけです。

その他の設定を無効化する

削除トランザクションが拒否された場合、このチュートリアルのデフォルトとは異なる設定、例えば 2-of-2 マルチシグになっている可能性があります。

/account/get GET が返す minCosignatories フィールドで必要な連署数を確認し、必要に応じて削除トランザクションを調整してください。

例えば、2-of-2 マルチシグを無効にするには、次のようにします。

  1. Cosignatory 1 を min_approval_delta-1 に設定して削除します。連署人が 1 人残った状態で、アカウントが 2 つの署名を必要とし続けることはできないためです。
  2. 承認されたら、min_approval_delta-1 に設定して Cosignatory 0 を削除します。

この場合、削除対象の連署人自身の署名は必要な承認に数えられないため、Cosignatory 0 は両方の削除に署名できます。

トランザクションを送信する⚓︎

    # Announce each transaction and wait for confirmation
    for signed_transaction in transactions:
        transaction_hash = facade.hash_transaction(signed_transaction)
        print(f'Built transaction with hash: {transaction_hash}')
        json_payload = facade.transaction_factory.to_json(
            signed_transaction)
        announce_result = announce_transaction(
            json_payload, 'transaction')
        if 'SUCCESS' != announce_result:
            print('Transaction rejected')
            break
        wait_for_confirmation(transaction_hash, 'transaction')
    // Announce each transaction and wait for confirmation
    for (const signedTransaction of transactions) {
        const transactionHash = facade.hashTransaction(signedTransaction)
            .toString();
        console.log('Built transaction with hash:', transactionHash);
        const jsonPayload = facade.transactionFactory.static
            .toJson(signedTransaction);
        const result = await announceTransaction(
            jsonPayload, 'transaction');
        if ('SUCCESS' !== result) {
            console.log('Transaction rejected');
            break;
        }
        await waitForConfirmation(transactionHash, 'transaction');
    }

最後の手順では、XEM を送信する チュートリアルで説明したように、トランザクションをアナウンスして承認を待ちます。

マルチシグを無効にする場合、2 つのマルチシグトランザクションを順番にアナウンスします。 2 回目の削除は 1 回目の削除が処理された後でのみ有効になるため、コードは 1 回目のトランザクションが承認されてから 2 回目をアナウンスします。

出力⚓︎

以下は、プログラムの実行時の出力例です。

Using node http://libertalia.nemtest.net:7890
Multisig address: TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6 (public key D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2)
Cosignatory 0 address: TAWOQNIMCCFO6MT7JLLFER746HKBBUVU7KQUSDJX (public key AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3)
Cosignatory 1 address: TC7BQFXISQEOPN2PCPPPOM3V4R3XDPNVEHEQLID4 (public key 26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23)
Fetching current network time from /time-sync/network-time
  Network time: 357324103 s since the nemesis block
Getting cosignatories from /account/get?address=TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6
  Response: No cosignatories
  Transaction fee: 0.5 XEM
Enabling the multisig with the modification transaction:
{
  "type": 4097,
  "version": 2,
  "network": 152,
  "timestamp": 357324103,
  "signer_public_key": "D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2",
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "fee": "500000",
  "deadline": 357331303,
  "modifications": [
    {
      "modification": {
        "modification_type": 1,
        "cosignatory_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3"
      }
    },
    {
      "modification": {
        "modification_type": 1,
        "cosignatory_public_key": "26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23"
      }
    }
  ],
  "min_approval_delta": 1
}
Built transaction with hash: 4F50710F7AB5C92ED1913D0EB4EE2E29D2AA6CC9C5C6509FB1C43BFDAF124C8B
Announcing transaction to /transaction/announce
  Result: SUCCESS
Waiting for transaction confirmation from /transaction/get?hash=4F50710F7AB5C92ED1913D0EB4EE2E29D2AA6CC9C5C6509FB1C43BFDAF124C8B
  Transaction status: pending
  Transaction status: pending
  ...
transaction confirmed in block 715434

出力の要点は次のとおりです。

  • 2~4 行目: 関係するすべてのアカウントのアドレスと公開鍵。
  • 8 行目Response: No cosignatories): 現在、連署人が設定されていない。
  • 24 行目と 30 行目cosignatory_public_key): 追加される連署人の公開鍵。
  • 34 行目"min_approval_delta": 1): 必要な署名数が 1 つ増える。
Using node http://libertalia.nemtest.net:7890
Multisig address: TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6 (public key D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2)
Cosignatory 0 address: TAWOQNIMCCFO6MT7JLLFER746HKBBUVU7KQUSDJX (public key AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3)
Cosignatory 1 address: TC7BQFXISQEOPN2PCPPPOM3V4R3XDPNVEHEQLID4 (public key 26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23)
Fetching current network time from /time-sync/network-time
  Network time: 357324204 s since the nemesis block
Getting cosignatories from /account/get?address=TBLXIOUO4EP5YR74HYXS3BFBGONZBUHP3NIS2HJ6
  Response: ['TC7BQFXISQEOPN2PCPPPOM3V4R3XDPNVEHEQLID4', 'TAWOQNIMCCFO6MT7JLLFER746HKBBUVU7KQUSDJX']
  Transaction fee: 0.65 XEM
Disabling the multisig with the multisig transaction:
{
  "type": 4100,
  "version": 1,
  "network": 152,
  "timestamp": 357324204,
  "signer_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3",
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "fee": "150000",
  "deadline": 357331404,
  "inner_transaction": {
    "type": 4097,
    "version": 2,
    "network": 152,
    "timestamp": 357324204,
    "signer_public_key": "D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2",
    "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
    "fee": "500000",
    "deadline": 357331404,
    "modifications": [
      {
        "modification": {
          "modification_type": 2,
          "cosignatory_public_key": "26D999AD34795F20D33886047A8CB7DE1ED0042AB7ED1017C602222C8B2A4C23"
        }
      }
    ],
    "min_approval_delta": 0
  },
  "cosignatures": []
}
  Transaction fee: 0.65 XEM
Disabling the multisig with the multisig transaction:
{
  "type": 4100,
  "version": 1,
  "network": 152,
  "timestamp": 357324204,
  "signer_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3",
  "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
  "fee": "150000",
  "deadline": 357331404,
  "inner_transaction": {
    "type": 4097,
    "version": 2,
    "network": 152,
    "timestamp": 357324204,
    "signer_public_key": "D656155B48D4E71E4C59EC6FAEB5EB4F214DE8BC3C65D5BF6A3D9931B4E5ACF2",
    "signature": "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000",
    "fee": "500000",
    "deadline": 357331404,
    "modifications": [
      {
        "modification": {
          "modification_type": 2,
          "cosignatory_public_key": "AC1FC0D95CA3255D20C57C179EE6E694A47A725C48DB362CC4978D7745C6A5C3"
        }
      }
    ],
    "min_approval_delta": -1
  },
  "cosignatures": []
}
Built transaction with hash: E66D3B5D36D7711C5A3C0D99C502258345E086241DF7CCD810DFB4A0ED8CC90D
Announcing transaction to /transaction/announce
  Result: SUCCESS
Waiting for transaction confirmation from /transaction/get?hash=E66D3B5D36D7711C5A3C0D99C502258345E086241DF7CCD810DFB4A0ED8CC90D
  Transaction status: pending
  Transaction status: pending
  ...
transaction confirmed in block 715438
Built transaction with hash: 70CAE1EEF8432A834C0E4EBE3A1EB6A1774F4AE42939E05AE9F120A9AF456051
Announcing transaction to /transaction/announce
  Result: SUCCESS
Waiting for transaction confirmation from /transaction/get?hash=70CAE1EEF8432A834C0E4EBE3A1EB6A1774F4AE42939E05AE9F120A9AF456051
  Transaction status: pending
  Transaction status: pending
  ...
transaction confirmed in block 715439

出力の要点は次のとおりです。

  • 2~4 行目: 関係するすべてのアカウントのアドレスと公開鍵。
  • 8 行目Response: [ ... ]): 既存の連署人が検出された。
  • 29~37 行目(1 つ目のマルチシグトランザクション): 必要な署名数は変わらず、既存の連署人が 1 人削除される。
  • 61~69 行目(2 つ目のマルチシグトランザクション): 必要な署名数が 1 つ減り、最後に残った連署人が削除される。

出力に表示されたトランザクションハッシュを使って、NEM テストネットエクスプローラー でトランザクションを検索できます。

まとめ⚓︎

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

手順 関連ドキュメント
現在のマルチシグ設定を取得する /account/get GET
マルチシグアカウントを有効化する MultisigAccountModificationTransactionV2
マルチシグアカウントを無効化する MultisigAccountModificationTransactionV2
変更をマルチシグトランザクションでラップする MultisigTransactionV1