コンテンツにスキップ

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

上級

マルチシグアカウント(「マルチシグ」とも呼ばれます)は、それ単体ではトランザクションを開始できません。代わりに、「連署者(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.FeeCalculator import calculate_transaction_fee
from symbolchain.symbol.Network import NetworkTimestamp

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

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))
    key_pair = SymbolFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY))
    cosignatory_key_pairs.append(key_pair)
    addr = facade.network.public_key_to_address(key_pair.public_key)
    cosignatory_addresses.append(addr)
    print(f'Cosignatory {i} address: '
        f'{addr} (public key {key_pair.public_key})')


# 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 announce_response:
        print(f'  Response: {announce_response.read().decode()}')


# Helper function to wait for transaction confirmation
def wait_for_confirmation(tx_hash, label):
    print(f'Waiting for {label} confirmation...')
    for attempt in range(60):
        time.sleep(1)
        try:
            url = f'{NODE_URL}/transactionStatus/{tx_hash}'
            with urllib.request.urlopen(url) as confirm_response:
                status = json.loads(confirm_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 RuntimeError(
                        f'{label} failed: {status["code"]}')
        except urllib.error.HTTPError:
            print('  Transaction status: unknown')
    raise TimeoutError(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 multisig_response:
            status = json.loads(multisig_response.read().decode())
            found_cosignatories = (
                status['multisig']['cosignatoryAddresses'])
            print(f'  Response: {found_cosignatories}')
            return found_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
    # and calculate fee for the final transaction size
    transaction.fee = Amount(calculate_transaction_fee(
        transaction, fee_multiplier, 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(
        calculate_transaction_fee(transaction, fee_multiplier))
    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


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_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')

    # 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
        agg_transaction = multisig_enable_transaction()
    else:
        # Disable the multisig
        agg_transaction = multisig_disable_transaction()
    json_payload = facade.transaction_factory.to_json(agg_transaction)

    # Announce and wait for confirmation
    agg_transaction_hash = facade.hash_transaction(agg_transaction)
    print(
        f'Built aggregate transaction with hash: {agg_transaction_hash}')
    announce_transaction(json_payload, 'aggregate transaction')
    wait_for_confirmation(agg_transaction_hash, 'aggregate transaction')

except Exception as e:
    print(e)

Download source

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

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

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; 2 > i; 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})`);
}

// 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; 60 > attempt; 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 ('confirmed' === status.group) {
                console.log(`${label} confirmed in`, attempt, 'seconds');
                return;
            }
            if ('failed' === status.group)
                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}`);
    const response = await fetch(`${NODE_URL}${multisigPath}`);
    if (!response.ok) {
        console.log('  Response: No cosignatories');
        return [];
    }
    const json = await response.json();
    const cosignatories = json.multisig.cosignatoryAddresses;
    console.log('  Response:', JSON.stringify(cosignatories));
    return cosignatories;
}

// Returns a transaction that turns a regular account into a multisig
function multisigEnableTransaction(timestamp, feeMultiplier) {
    // 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
    // and calculate fee for the final transaction size
    transaction.fee = new models.Amount(calculateTransactionFee(
        transaction, feeMultiplier, 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, feeMultiplier) {
    // 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(
        calculateTransactionFee(transaction, feeMultiplier));
    console.log(
        'Disabling the multisig with the aggregate transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

    // Sign the aggregate with the first cosigner's signature
    SymbolTransactionFactory.attachSignature(transaction,
        facade.signTransaction(cosignatoryKeyPairs[0], transaction));

    return transaction;
}

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 medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);

    // Get current state of the multisig account and decide which
    // operation to perform
    const cosignatories =
        await getMultisigCosignatories(multisigAddress);
    let transaction;
    if (0 === cosignatories.length) {
        // Enable the multisig
        transaction =
            multisigEnableTransaction(timestamp, feeMultiplier);
    } else {
        // Disable the multisig
        transaction =
            multisigDisableTransaction(timestamp, feeMultiplier);
    }
    const payload = SymbolTransactionFactory.toJson(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

//JAVA 21+
//DEPS org.symbol:symbol-sdk:3.3.1

import java.io.IOException;
import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import org.symbol.sdk.CryptoTypes;
import org.symbol.sdk.facade.SymbolFacade;
import org.symbol.sdk.symbol.Address;
import org.symbol.sdk.symbol.FeeCalculator;
import org.symbol.sdk.symbol.KeyPair;
import org.symbol.sdk.symbol.NetworkTimestamp;
import org.symbol.sdk.symbol.SymbolTransactionFactory;
import org.symbol.sdk.symbol.models.*;

final class ConfigureMultisig {
    private static final ObjectMapper JSON_MAPPER = new ObjectMapper();

    private static final HttpClient HTTP_CLIENT =
        HttpClient.newHttpClient();

    private static final String KEY_PREFIX = "0".repeat(63);

    private final String nodeUrl = System.getenv().getOrDefault(
        "NODE_URL", "https://reference.symboltest.net:3001");

    private final SymbolFacade facade = new SymbolFacade("testnet");

    private KeyPair multisigKeyPair;

    private Address multisigAddress;

    private final List<KeyPair> cosignatoryKeyPairs =
        new ArrayList<>();

    private final List<Address> cosignatoryAddresses =
        new ArrayList<>();

    private void announceTransaction(
        final String payload,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Announcing %s to /transactions%n", label);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + "/transactions"))
            .header("Content-Type", "application/json")
            .PUT(HttpRequest.BodyPublishers.ofString(payload))
            .build();
        final HttpResponse<String> response = HTTP_CLIENT.send(
            request, BodyHandlers.ofString());
        System.out.printf("  Response: %s%n", response.body());
    }

    private void waitForConfirmation(
        final String transactionHash,
        final String label
    ) throws IOException, InterruptedException {
        System.out.printf("Waiting for %s confirmation...%n", label);
        for (int attempt = 0; 60 > attempt; ++attempt) {
            Thread.sleep(1000);
            try {
                final String statusPath =
                    "/transactionStatus/" + transactionHash;
                final HttpRequest statusRequest = HttpRequest.newBuilder(
                    URI.create(nodeUrl + statusPath)).GET().build();
                final HttpResponse<String> statusResponse = HTTP_CLIENT
                    .send(statusRequest, BodyHandlers.ofString());
                final JsonNode status =
                    JSON_MAPPER.readTree(statusResponse.body());
                final String group = status.get("group").asText();
                System.out.printf("  Transaction status: %s%n", group);
                if ("confirmed".equals(group)) {
                    System.out.printf("%s confirmed in %d seconds%n",
                        label, attempt);
                    return;
                }
                if ("failed".equals(group))
                    throw new IOException(String.format("%s failed: %s",
                        label, status.get("code").asText()));
            } catch (final IOException ex) {
                if (ex.getMessage().contains("failed"))
                    throw ex;

                System.out.println("  Transaction status: unknown");
            }
        }
        throw new IOException(String.format(
            "%s not confirmed after 60 seconds", label));
    }


    // Returns the cosignatory addresses of the provided multisig account,
    // or an empty array if the account is not multisig or has
    // never been used
    private JsonNode getMultisigCosignatories(
        final Address address
    ) throws IOException, InterruptedException {
        final String multisigPath = String.format(
            "/account/%s/multisig", address);
        System.out.printf("Getting cosignatories from %s%n",
            multisigPath);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + multisigPath)).GET().build();
        final HttpResponse<String> response = HTTP_CLIENT
            .send(request, BodyHandlers.ofString());
        if (2 != response.statusCode() / 100) {
            System.out.println("  Response: No cosignatories");
            return JSON_MAPPER.createArrayNode();
        }

        final JsonNode cosignatories = JSON_MAPPER
            .readTree(response.body()).get("multisig")
            .get("cosignatoryAddresses");
        System.out.printf("  Response: %s%n", cosignatories);
        return cosignatories;
    }

    // Returns a transaction that turns a regular account into a multisig
    private Transaction multisigEnableTransaction(
        final NetworkTimestamp timestamp,
        final long feeMultiplier
    ) throws IOException {

        // Create an embedded multisig account modification transaction
        // that adds two cosignatories
        final EmbeddedTransaction embeddedTransaction =
            facade.transactionFactory.createEmbedded(Map.of(
                "type", "multisig_account_modification_transaction_v1",
                "signerPublicKey", multisigKeyPair.getPublicKey(),
                "minApprovalDelta", 1,
                "minRemovalDelta", 1,
                // Cast one value to infer Map<String, Object>,
                // as expected by the SDK.
                "addressAdditions", (Object) cosignatoryAddresses));

        // Build the aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions = List.of(
            embeddedTransaction);
        final var transaction = (AggregateCompleteTransactionV3)
            facade.transactionFactory.create(Map.of(
                "type", "aggregate_complete_transaction_v3",
                "signerPublicKey", multisigKeyPair.getPublicKey(),
                "deadline", timestamp.addHours(2).timestamp,
                "transactionsHash", SymbolFacade.hashEmbeddedTransactions(
                    embeddedTransactions),
                // Cast one value to infer Map<String, Object>,
                // as expected by the SDK.
                "transactions", (Object) embeddedTransactions));
        final int cosignatureCount = cosignatoryKeyPairs.size();
        transaction.setFee(new Amount(
            FeeCalculator.calculateTransactionFee(
                transaction, feeMultiplier, cosignatureCount)));
        System.out.println(
            "Enabling the multisig with the aggregate transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));


        // Sign the aggregate transaction with the multisig's signature
        SymbolTransactionFactory.attachSignature(
            transaction,
            facade.signTransaction(multisigKeyPair, transaction));

        // Append signatures from all cosignatories
        final List<Cosignature> cosignatures = new ArrayList<>();
        for (final KeyPair cosignatoryKeyPair : cosignatoryKeyPairs)
            cosignatures.add(facade.cosignTransaction(
                cosignatoryKeyPair, transaction));
        transaction.setCosignatures(cosignatures);

        return transaction;
    }

    // Returns a transaction that turns a multisig into a regular account
    private Transaction multisigDisableTransaction(
        final NetworkTimestamp timestamp,
        final long feeMultiplier
    ) throws IOException {

        // Create two embedded multisig account modification transactions
        // because cosignatories must be removed one by one
        final EmbeddedTransaction embeddedTransaction1 =
            facade.transactionFactory.createEmbedded(Map.of(
                "type", "multisig_account_modification_transaction_v1",
                "signerPublicKey", multisigKeyPair.getPublicKey(),
                "minApprovalDelta", 0,
                "minRemovalDelta", 0,
                // Cast one value to infer Map<String, Object>,
                // as expected by the SDK.
                "addressDeletions",
                    (Object) List.of(cosignatoryAddresses.get(1))));
        final EmbeddedTransaction embeddedTransaction2 =
            facade.transactionFactory.createEmbedded(Map.of(
                "type", "multisig_account_modification_transaction_v1",
                "signerPublicKey", multisigKeyPair.getPublicKey(),
                "minApprovalDelta", -1,
                "minRemovalDelta", -1,
                // Cast one value to infer Map<String, Object>,
                // as expected by the SDK.
                "addressDeletions",
                    (Object) List.of(cosignatoryAddresses.get(0))));

        // Build the aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions = List.of(
            embeddedTransaction1, embeddedTransaction2);
        final var transaction = (AggregateCompleteTransactionV3)
            facade.transactionFactory.create(Map.of(
                "type", "aggregate_complete_transaction_v3",
                "signerPublicKey", cosignatoryKeyPairs.get(0)
                    .getPublicKey(),
                "deadline", timestamp.addHours(2).timestamp,
                "transactionsHash", SymbolFacade.hashEmbeddedTransactions(
                    embeddedTransactions),
                // Cast one value to infer Map<String, Object>,
                // as expected by the SDK.
                "transactions", (Object) embeddedTransactions));
        transaction.setFee(new Amount(
            FeeCalculator.calculateTransactionFee(
                transaction, feeMultiplier)));
        System.out.println(
            "Disabling the multisig with the aggregate transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        SymbolTransactionFactory.attachSignature(
            transaction,
            facade.signTransaction(
                cosignatoryKeyPairs.get(0), transaction));

        return transaction;
    }

    public static void main(final String[] args) {
        new ConfigureMultisig().run();
    }

    private void run() {
        System.out.printf("Using node %s%n", nodeUrl);


        final String multisigPrivateKey = System.getenv().getOrDefault(
            "MULTISIG_PRIVATE_KEY", KEY_PREFIX + "1");
        multisigKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(multisigPrivateKey));
        multisigAddress = facade.network.publicKeyToAddress(
            multisigKeyPair.getPublicKey());
        System.out.printf("Multisig address: %s (public key %s)%n",
            multisigAddress, multisigKeyPair.getPublicKey());

        for (int i = 0; 2 > i; ++i) {
            final String cosignatoryPrivateKey =
                System.getenv().getOrDefault(
                    String.format("COSIGNATORY%d_PRIVATE_KEY", i),
                    KEY_PREFIX + (i + 2));
            final KeyPair keyPair = new KeyPair(
                new CryptoTypes.PrivateKey(cosignatoryPrivateKey));
            cosignatoryKeyPairs.add(keyPair);
            final Address address = facade.network.publicKeyToAddress(
                keyPair.getPublicKey());
            cosignatoryAddresses.add(address);
            System.out.printf(
                "Cosignatory %d address: %s (public key %s)%n",
                i, address, keyPair.getPublicKey());
        }


        try {
            // Fetch current network time
            final String timePath = "/node/time";
            System.out.printf(
                "Fetching current network time from %s%n", timePath);
            final HttpRequest timeRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + timePath)).GET().build();
            final HttpResponse<String> timeResponse = HTTP_CLIENT.send(
                timeRequest, BodyHandlers.ofString());
            final JsonNode timeJson = JSON_MAPPER.readTree(
                timeResponse.body());
            final NetworkTimestamp timestamp = new NetworkTimestamp(
                timeJson.get("communicationTimestamps")
                    .get("receiveTimestamp").asLong());
            System.out.printf("  Network time: %dms since nemesis%n",
                timestamp.timestamp);

            // Fetch recommended fees
            final String feePath = "/network/fees/transaction";
            System.out.printf(
                "Fetching recommended fees from %s%n", feePath);
            final HttpRequest feeRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + feePath)).GET().build();
            final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
                feeRequest, BodyHandlers.ofString());
            final JsonNode feeJson = JSON_MAPPER.readTree(
                feeResponse.body());
            final long feeMultiplier = Math.max(
                feeJson.get("medianFeeMultiplier").asLong(),
                feeJson.get("minFeeMultiplier").asLong());
            System.out.printf("  Fee multiplier: %d%n", feeMultiplier);


            // Get current state of the multisig account and decide which
            // operation to perform
            final JsonNode cosignatories =
                getMultisigCosignatories(multisigAddress);
            final Transaction transaction;
            if (cosignatories.isEmpty())
                transaction = multisigEnableTransaction(
                    timestamp, feeMultiplier);
            else
                transaction = multisigDisableTransaction(
                    timestamp, feeMultiplier);

            final String payload =
                SymbolTransactionFactory.toJson(transaction);

            // Announce and wait for confirmation
            final String transactionHash =
                facade.hashTransaction(transaction).toString();
            System.out.printf(
                "Built aggregate transaction with hash: %s%n",
                transactionHash);
            announceTransaction(payload, "aggregate transaction");
            waitForConfirmation(transactionHash, "aggregate transaction");

        } catch (final Exception ex) {
            System.out.printf("%s%n", ex.getMessage());
        }
    }
}

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))
    key_pair = SymbolFacade.KeyPair(PrivateKey(COSIGNATORY_PRIVATE_KEY))
    cosignatory_key_pairs.append(key_pair)
    addr = facade.network.public_key_to_address(key_pair.public_key)
    cosignatory_addresses.append(addr)
    print(f'Cosignatory {i} address: '
        f'{addr} (public key {key_pair.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; 2 > i; 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})`);
}
        final String multisigPrivateKey = System.getenv().getOrDefault(
            "MULTISIG_PRIVATE_KEY", KEY_PREFIX + "1");
        multisigKeyPair = new KeyPair(
            new CryptoTypes.PrivateKey(multisigPrivateKey));
        multisigAddress = facade.network.publicKeyToAddress(
            multisigKeyPair.getPublicKey());
        System.out.printf("Multisig address: %s (public key %s)%n",
            multisigAddress, multisigKeyPair.getPublicKey());

        for (int i = 0; 2 > i; ++i) {
            final String cosignatoryPrivateKey =
                System.getenv().getOrDefault(
                    String.format("COSIGNATORY%d_PRIVATE_KEY", i),
                    KEY_PREFIX + (i + 2));
            final KeyPair keyPair = new KeyPair(
                new CryptoTypes.PrivateKey(cosignatoryPrivateKey));
            cosignatoryKeyPairs.add(keyPair);
            final Address address = facade.network.publicKeyToAddress(
                keyPair.getPublicKey());
            cosignatoryAddresses.add(address);
            System.out.printf(
                "Cosignatory %d address: %s (public key %s)%n",
                i, address, keyPair.getPublicKey());
        }

このチュートリアルでは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_multiplier = response_json['medianFeeMultiplier']
        minimum_multiplier = response_json['minFeeMultiplier']
        fee_multiplier = max(median_multiplier, minimum_multiplier)
        print(f'  Fee multiplier: {fee_multiplier}')
    // 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 medianMultiplier = feeJSON.medianFeeMultiplier;
    const minimumMultiplier = feeJSON.minFeeMultiplier;
    const feeMultiplier = Math.max(medianMultiplier, minimumMultiplier);
    console.log('  Fee multiplier:', feeMultiplier);
            // Fetch current network time
            final String timePath = "/node/time";
            System.out.printf(
                "Fetching current network time from %s%n", timePath);
            final HttpRequest timeRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + timePath)).GET().build();
            final HttpResponse<String> timeResponse = HTTP_CLIENT.send(
                timeRequest, BodyHandlers.ofString());
            final JsonNode timeJson = JSON_MAPPER.readTree(
                timeResponse.body());
            final NetworkTimestamp timestamp = new NetworkTimestamp(
                timeJson.get("communicationTimestamps")
                    .get("receiveTimestamp").asLong());
            System.out.printf("  Network time: %dms since nemesis%n",
                timestamp.timestamp);

            // Fetch recommended fees
            final String feePath = "/network/fees/transaction";
            System.out.printf(
                "Fetching recommended fees from %s%n", feePath);
            final HttpRequest feeRequest = HttpRequest.newBuilder(
                URI.create(nodeUrl + feePath)).GET().build();
            final HttpResponse<String> feeResponse = HTTP_CLIENT.send(
                feeRequest, BodyHandlers.ofString());
            final JsonNode feeJson = JSON_MAPPER.readTree(
                feeResponse.body());
            final long feeMultiplier = Math.max(
                feeJson.get("medianFeeMultiplier").asLong(),
                feeJson.get("minFeeMultiplier").asLong());
            System.out.printf("  Fee multiplier: %d%n", feeMultiplier);

転送トランザクション チュートリアルで説明されているプロセスに従い、ネットワーク時間と推奨手数料をそれぞれ /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 multisig_response:
            status = json.loads(multisig_response.read().decode())
            found_cosignatories = (
                status['multisig']['cosignatoryAddresses'])
            print(f'  Response: {found_cosignatories}')
            return found_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}`);
    const response = await fetch(`${NODE_URL}${multisigPath}`);
    if (!response.ok) {
        console.log('  Response: No cosignatories');
        return [];
    }
    const json = await response.json();
    const cosignatories = json.multisig.cosignatoryAddresses;
    console.log('  Response:', JSON.stringify(cosignatories));
    return cosignatories;
}
    // Returns the cosignatory addresses of the provided multisig account,
    // or an empty array if the account is not multisig or has
    // never been used
    private JsonNode getMultisigCosignatories(
        final Address address
    ) throws IOException, InterruptedException {
        final String multisigPath = String.format(
            "/account/%s/multisig", address);
        System.out.printf("Getting cosignatories from %s%n",
            multisigPath);
        final HttpRequest request = HttpRequest.newBuilder(
            URI.create(nodeUrl + multisigPath)).GET().build();
        final HttpResponse<String> response = HTTP_CLIENT
            .send(request, BodyHandlers.ofString());
        if (2 != response.statusCode() / 100) {
            System.out.println("  Response: No cosignatories");
            return JSON_MAPPER.createArrayNode();
        }

        final JsonNode cosignatories = JSON_MAPPER
            .readTree(response.body()).get("multisig")
            .get("cosignatoryAddresses");
        System.out.printf("  Response: %s%n", cosignatories);
        return cosignatories;
    }

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

    # 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
        agg_transaction = multisig_enable_transaction()
    else:
        # Disable the multisig
        agg_transaction = multisig_disable_transaction()
    json_payload = facade.transaction_factory.to_json(agg_transaction)
    // Get current state of the multisig account and decide which
    // operation to perform
    const cosignatories =
        await getMultisigCosignatories(multisigAddress);
    let transaction;
    if (0 === cosignatories.length) {
        // Enable the multisig
        transaction =
            multisigEnableTransaction(timestamp, feeMultiplier);
    } else {
        // Disable the multisig
        transaction =
            multisigDisableTransaction(timestamp, feeMultiplier);
    }
    const payload = SymbolTransactionFactory.toJson(transaction);
            // Get current state of the multisig account and decide which
            // operation to perform
            final JsonNode cosignatories =
                getMultisigCosignatories(multisigAddress);
            final Transaction transaction;
            if (cosignatories.isEmpty())
                transaction = multisigEnableTransaction(
                    timestamp, feeMultiplier);
            else
                transaction = multisigDisableTransaction(
                    timestamp, feeMultiplier);

            final String payload =
                SymbolTransactionFactory.toJson(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
        });
        // Create an embedded multisig account modification transaction
        // that adds two cosignatories
        final EmbeddedTransaction embeddedTransaction =
            facade.transactionFactory.createEmbedded(Map.of(
                "type", "multisig_account_modification_transaction_v1",
                "signerPublicKey", multisigKeyPair.getPublicKey(),
                "minApprovalDelta", 1,
                "minRemovalDelta", 1,
                // Cast one value to infer Map<String, Object>,
                // as expected by the SDK.
                "addressAdditions", (Object) 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
    # and calculate fee for the final transaction size
    transaction.fee = Amount(calculate_transaction_fee(
        transaction, fee_multiplier, 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
    // and calculate fee for the final transaction size
    transaction.fee = new models.Amount(calculateTransactionFee(
        transaction, feeMultiplier, cosignatoryKeyPairs.length));
    console.log('Enabling the multisig with the aggregate transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));
        // Build the aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions = List.of(
            embeddedTransaction);
        final var transaction = (AggregateCompleteTransactionV3)
            facade.transactionFactory.create(Map.of(
                "type", "aggregate_complete_transaction_v3",
                "signerPublicKey", multisigKeyPair.getPublicKey(),
                "deadline", timestamp.addHours(2).timestamp,
                "transactionsHash", SymbolFacade.hashEmbeddedTransactions(
                    embeddedTransactions),
                // Cast one value to infer Map<String, Object>,
                // as expected by the SDK.
                "transactions", (Object) embeddedTransactions));
        final int cosignatureCount = cosignatoryKeyPairs.size();
        transaction.setFee(new Amount(
            FeeCalculator.calculateTransactionFee(
                transaction, feeMultiplier, cosignatureCount)));
        System.out.println(
            "Enabling the multisig with the aggregate transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

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

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

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

    # 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));
    }
        // Sign the aggregate transaction with the multisig's signature
        SymbolTransactionFactory.attachSignature(
            transaction,
            facade.signTransaction(multisigKeyPair, transaction));

        // Append signatures from all cosignatories
        final List<Cosignature> cosignatures = new ArrayList<>();
        for (final KeyPair cosignatoryKeyPair : cosignatoryKeyPairs)
            cosignatures.add(facade.cosignTransaction(
                cosignatoryKeyPair, transaction));
        transaction.setCosignatures(cosignatures);

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

署名の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]]
        });
        // Create two embedded multisig account modification transactions
        // because cosignatories must be removed one by one
        final EmbeddedTransaction embeddedTransaction1 =
            facade.transactionFactory.createEmbedded(Map.of(
                "type", "multisig_account_modification_transaction_v1",
                "signerPublicKey", multisigKeyPair.getPublicKey(),
                "minApprovalDelta", 0,
                "minRemovalDelta", 0,
                // Cast one value to infer Map<String, Object>,
                // as expected by the SDK.
                "addressDeletions",
                    (Object) List.of(cosignatoryAddresses.get(1))));
        final EmbeddedTransaction embeddedTransaction2 =
            facade.transactionFactory.createEmbedded(Map.of(
                "type", "multisig_account_modification_transaction_v1",
                "signerPublicKey", multisigKeyPair.getPublicKey(),
                "minApprovalDelta", -1,
                "minRemovalDelta", -1,
                // Cast one value to infer Map<String, Object>,
                // as expected by the SDK.
                "addressDeletions",
                    (Object) List.of(cosignatoryAddresses.get(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(
        calculate_transaction_fee(transaction, fee_multiplier))
    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(
        calculateTransactionFee(transaction, feeMultiplier));
    console.log(
        'Disabling the multisig with the aggregate transaction:');
    console.log(JSON.stringify(transaction.toJson(), null, 2));

    // Sign the aggregate with the first cosigner's signature
    SymbolTransactionFactory.attachSignature(transaction,
        facade.signTransaction(cosignatoryKeyPairs[0], transaction));
        // Build the aggregate transaction
        final List<EmbeddedTransaction> embeddedTransactions = List.of(
            embeddedTransaction1, embeddedTransaction2);
        final var transaction = (AggregateCompleteTransactionV3)
            facade.transactionFactory.create(Map.of(
                "type", "aggregate_complete_transaction_v3",
                "signerPublicKey", cosignatoryKeyPairs.get(0)
                    .getPublicKey(),
                "deadline", timestamp.addHours(2).timestamp,
                "transactionsHash", SymbolFacade.hashEmbeddedTransactions(
                    embeddedTransactions),
                // Cast one value to infer Map<String, Object>,
                // as expected by the SDK.
                "transactions", (Object) embeddedTransactions));
        transaction.setFee(new Amount(
            FeeCalculator.calculateTransactionFee(
                transaction, feeMultiplier)));
        System.out.println(
            "Disabling the multisig with the aggregate transaction:");
        System.out.println(JSON_MAPPER.writerWithDefaultPrettyPrinter()
            .writeValueAsString(transaction.toJson()));

        SymbolTransactionFactory.attachSignature(
            transaction,
            facade.signTransaction(
                cosignatoryKeyPairs.get(0), transaction));

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

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

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

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

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

    # Announce and wait for confirmation
    agg_transaction_hash = facade.hash_transaction(agg_transaction)
    print(
        f'Built aggregate transaction with hash: {agg_transaction_hash}')
    announce_transaction(json_payload, 'aggregate transaction')
    wait_for_confirmation(agg_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');
            // Announce and wait for confirmation
            final String transactionHash =
                facade.hashTransaction(transaction).toString();
            System.out.printf(
                "Built aggregate transaction with hash: %s%n",
                transactionHash);
            announceTransaction(payload, "aggregate transaction");
            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
設定を埋め込みトランザクションにラップする , MultisigAccountModificationTransactionV1