9.マルチシグ化
アカウントのマルチシグ化について説明します。
注意事項
一つのマルチシグアカウントに登録できる連署者の数は25個です。 一つのアカウントは最大25個のマルチシグの連署者になれます。 マルチシグは最大3階層まで構成できます。 本書では1階層のマルチシグのみ解説します。
9.0 アカウントの準備
この章のサンプルソースコードで使用するアカウントを作成し、それぞれの秘密鍵を出力しておきます。 本章でマルチシグ化したアカウントBobは、Carolの秘密鍵を紛失すると使えなくなってしまうのでご注意ください。
bobKey = new symbolSdk.symbol.KeyPair(symbolSdk.PrivateKey.random());
bobAddress = facade.network.publicKeyToAddress(bobKey.publicKey);
carol1Key = new symbolSdk.symbol.KeyPair(symbolSdk.PrivateKey.random());
carol1Address = facade.network.publicKeyToAddress(carol1Key.publicKey);
carol2Key = new symbolSdk.symbol.KeyPair(symbolSdk.PrivateKey.random());
carol2Address = facade.network.publicKeyToAddress(carol2Key.publicKey);
carol3Key = new symbolSdk.symbol.KeyPair(symbolSdk.PrivateKey.random());
carol3Address = facade.network.publicKeyToAddress(carol3Key.publicKey);
carol4Key = new symbolSdk.symbol.KeyPair(symbolSdk.PrivateKey.random());
carol4Address = facade.network.publicKeyToAddress(carol4Key.publicKey);
carol5Key = new symbolSdk.symbol.KeyPair(symbolSdk.PrivateKey.random());
carol5Address = facade.network.publicKeyToAddress(carol5Key.publicKey);
console.log(bobKey.privateKey.toString());
console.log(carol1Key.privateKey.toString());
console.log(carol2Key.privateKey.toString());
console.log(carol3Key.privateKey.toString());
console.log(carol4Key.privateKey.toString());
console.log(carol5Key.privateKey.toString());
テストネットの場合はFAUCETでネットワーク手数料分をbobとcarol1に補給しておきます。
URL出力
console.log(
  "https://testnet.symbol.tools/?recipient=" +
    bobAddress.toString() +
    "&amount=20",
);
console.log(
  "https://testnet.symbol.tools/?recipient=" +
    carol1Address.toString() +
    "&amount=20",
);
9.1 マルチシグの登録
Symbolではマルチシグアカウントを新規に作成するのではなく、既存アカウントについて連署者を指定してマルチシグ化します。 マルチシグ化には連署者に指定されたアカウントの承諾署名(オプトイン)が必要なため、アグリゲートトランザクションを使用します。
// マルチシグ設定Tx作成
multisigTx = facade.transactionFactory.createEmbedded({
  type: "multisig_account_modification_transaction_v1", // Txタイプ:マルチシグ設定Tx
  signerPublicKey: bobKey.publicKey, // マルチシグ化したいアカウントの公開鍵を指定
  minApprovalDelta: 3, // minApproval:承認のために必要な最小署名者数増分
  minRemovalDelta: 3, // minRemoval:除名のために必要な最小署名者数増分
  addressAdditions: [
    // 追加対象アドレスリスト
    carol1Address,
    carol2Address,
    carol3Address,
    carol4Address,
  ],
  addressDeletions: [], // 除名対象アドレスリスト
});
// マークルハッシュの算出
embeddedTransactions = [multisigTx];
merkleHash = facade.constructor.hashEmbeddedTransactions(embeddedTransactions);
// アグリゲートTx作成
aggregateTx = facade.transactionFactory.create({
  type: "aggregate_complete_transaction_v2",
  signerPublicKey: bobKey.publicKey, // マルチシグ化したいアカウントの公開鍵を指定
  deadline: facade.network.fromDatetime(Date.now()).addHours(2).timestamp, //Deadline:有効期限
  transactionsHash: merkleHash,
  transactions: embeddedTransactions,
});
// 連署により追加される連署情報のサイズを追加して最終的なTxサイズを算出する
requiredCosignatures = 4; // 連署者の数:4
calculatedCosignatures =
  requiredCosignatures > aggregateTx.cosignatures.length
    ? requiredCosignatures
    : aggregateTx.cosignatures.length;
sizePerCosignature = 8 + 32 + 64;
calculatedSize =
  aggregateTx.size -
  aggregateTx.cosignatures.length * sizePerCosignature +
  calculatedCosignatures * sizePerCosignature;
aggregateTx.fee = new symbolSdk.symbol.Amount(BigInt(calculatedSize * 100)); //手数料
// マルチシグ化し たいアカウントによる署名
sig = facade.signTransaction(bobKey, aggregateTx);
jsonPayload = facade.transactionFactory.constructor.attachSignature(
  aggregateTx,
  sig,
);
// 追加・除外対象として指定したアカウントによる連署
coSig1 = facade.cosignTransaction(carol1Key, aggregateTx, false);
aggregateTx.cosignatures.push(coSig1);
coSig2 = facade.cosignTransaction(carol2Key, aggregateTx, false);
aggregateTx.cosignatures.push(coSig2);
coSig3 = facade.cosignTransaction(carol3Key, aggregateTx, false);
aggregateTx.cosignatures.push(coSig3);
coSig4 = facade.cosignTransaction(carol4Key, aggregateTx, false);
aggregateTx.cosignatures.push(coSig4);
// アナウンス
await fetch(new URL("/transactions", NODE), {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    payload: symbolSdk.utils.uint8ToHex(aggregateTx.serialize()),
  }),
})
  .then((res) => res.json())
  .then((json) => {
    return json;
  });
9.2 確認
マルチシグ化したアカウントの確認
multisigInfo = await fetch(
  new URL("/account/" + bobAddress.toString() + "/multisig", NODE),
  {
    method: "GET",
    headers: { "Content-Type": "application/json" },
  },
)
  .then((res) => res.json())
  .then((json) => {
    return json.account;
  });
console.log(multisigInfo);
出力例
> {version: 1, accountAddress: '9828118AF434E834DD45D3E449C3DA49804964416E254B7F', minApproval: 3, minRemoval: 3, cosignatoryAddresses: Array(4), …}
    accountAddress: "9828118AF434E834DD45D3E449C3DA49804964416E254B7F"
  > cosignatoryAddresses: Array(4)
      0: "9874440B7EF2C407445B05A0D8F3253D82F0D39FAA9CF1F6"
      1: "9877E194AC74C47FFDFEF6A5035CA7806F6EB5258377B6F4"
      2: "9898C50E14AF3AB8869A3A72D5C67BD31BF32EFFF3D6CA2E"
      3: "98E76E4AD1623DC4B3F357A29C9496EDD48E2830ABE0CFC3"
      length: 4
    minApproval: 3
    minRemoval: 3
    multisigAddresses: []
    version: 1
cosignatoryAddressesが連署者として登録されていることがわかります。 また、minApproval:3 によりトランザクションが成立するために必要な署名数3 minRemoval: 3により連署者を取り外すために必要な署名者数は3であることがわかります。
連署者アカウントの確認
multisigInfo = await fetch(
  new URL("/account/" + carol1Address.toString() + "/multisig", NODE),
  {
    method: "GET",
    headers: { "Content-Type": "application/json" },
  },
)
  .then((res) => res.json())
  .then((json) => {
    return json.account;
  });
console.log(multisigInfo);
出力例
> {version: 1, accountAddress: '9898C50E14AF3AB8869A3A72D5C67BD31BF32EFFF3D6CA2E', minApproval: 0, minRemoval: 0, cosignatoryAddresses: Array(0), …}
    accountAddress: "9898C50E14AF3AB8869A3A72D5C67BD31BF32EFFF3D6CA2E"
    cosignatoryAddresses: []
    minApproval: 0
    minRemoval: 0
  > multisigAddresses: Array(1)
      0: "9828118AF434E834DD45D3E449C3DA49804964416E254B7F"
      length: 1
    version: 1
multisigAddresses に対して連署する権利を持っていることが分かります。
9.3 マルチシグ署名
マルチシグ化したアカウントからモザイクを送信します。
アグリゲートコンプリートトランザクションで送信
アグリゲートコンプリートトランザクションの場合、ノードにアナウンスする前に連署者の署名を全て集めてからトランザクションを作成します。
namespaceIds = symbolSdk.symbol.generateNamespacePath("symbol.xym");
namespaceId = namespaceIds[namespaceIds.length - 1];
// アグリゲートTxに含めるTxを作成
tx = facade.transactionFactory.createEmbedded({
  type: "transfer_transaction_v1", // Txタイプ:転送Tx
  signerPublicKey: bobKey.publicKey, // マルチシグ化したアカウントの公開鍵
  recipientAddress: aliceAddress.toString(),
  mosaics: [
    { mosaicId: namespaceId, amount: 1000000n }, // 1XYM送金
  ],
  message: new Uint8Array([0x00, ...new TextEncoder("utf-8").encode("test")]), // 平文メッセージ
});
// マークルハッシュの算出
embeddedTransactions = [tx];
merkleHash = facade.constructor.hashEmbeddedTransactions(embeddedTransactions);
// アグリゲートTx作成
aggregateTx = facade.transactionFactory.create({
  type: "aggregate_complete_transaction_v2",
  signerPublicKey: carol1Key.publicKey, // 起案者アカウントの公開鍵を指定
  deadline: facade.network.fromDatetime(Date.now()).addHours(2).timestamp, //Deadline:有効期限
  transactionsHash: merkleHash,
  transactions: embeddedTransactions,
});
// 連署により追加される連署情報のサイズを追加して最終的なTxサイズを算出する
requiredCosignatures = 2; // 連署者の数:2
calculatedCosignatures =
  requiredCosignatures > aggregateTx.cosignatures.length
    ? requiredCosignatures
    : aggregateTx.cosignatures.length;
sizePerCosignature = 8 + 32 + 64;
calculatedSize =
  aggregateTx.size -
  aggregateTx.cosignatures.length * sizePerCosignature +
  calculatedCosignatures * sizePerCosignature;
aggregateTx.fee = new symbolSdk.symbol.Amount(BigInt(calculatedSize * 100)); //手数料
// 起案者アカウントによる署名
sig = facade.signTransaction(carol1Key, aggregateTx);
jsonPayload = facade.transactionFactory.constructor.attachSignature(
  aggregateTx,
  sig,
);
// 連署者アカウントによる連署
coSig2 = facade.cosignTransaction(carol2Key, aggregateTx, false);
aggregateTx.cosignatures.push(coSig2);
coSig3 = facade.cosignTransaction(carol3Key, aggregateTx, false);
aggregateTx.cosignatures.push(coSig3);
// アナウンス
await fetch(new URL("/transactions", NODE), {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    payload: symbolSdk.utils.uint8ToHex(aggregateTx.serialize()),
  }),
})
  .then((res) => res.json())
  .then((json) => {
    return json;
  });