代理アカウントを立ててアカウントを守る - Symbol Blockchain


## 環境

- symbol-sdk: 3.2.2

## 登場人物

- Alice …Carol のファン
- Bob …Carol のマネージャ
- Carol …アイドル

Carol のファンである Alice は、Carol にプレゼントを贈りたい。
マネージャの Bob はプレゼントに偽装した危険物を Carol が受け取ってしまうとのを防ぎたい。
ファンへの返事は Carol 本人からして欲しい。

```mermaid
sequenceDiagram
    participant Alice
    participant Bob
    participant Carol
    Alice->>Bob: Carolへのプレゼント132XYM
    Bob->>Carol: チェックしたAliceからのプレゼント132XYM
    Carol->>Alice: プレゼントの返事
```

## アカウント

### Alice

- 一般的な普通のアカウント

https://testnet.symbol.fyi/accounts/TAMPVDQ3M2ZMTVARAATWM4QWSSNAM6FPUDLOIYI

### Bob

- Carol アカウントの親シグ

https://testnet.symbol.fyi/accounts/TAKTEDIIWBWKC5CBD4ERPZFTTO2EA67X3JL6M4I

### Carol

- Bob アカウントの子シグ
- Bob 以外から受信できない
- 送信は誰にでもできる

https://testnet.symbol.fyi/accounts/TBQ7WKPDJPKTMQPFSV636DA5JBLOIOTH42255OA

#### Carol への制限追加

以下をアグリゲートでまとめて送信します。

- Bob 以外からの受信不可
- マルチシグ化(連署者は Bob)

<details><summary>ソース</summary>

```typescript
import {
  descriptors,
  KeyPair,
  models,
  Network,
  SymbolAccount,
  SymbolFacade,
  SymbolTransactionFactory,
} from "symbol-sdk/symbol";
import { Env } from "./Env.js";
import { PrivateKey, utils } from "symbol-sdk";

const facade = new SymbolFacade(Network.TESTNET);
const bobPrikey = new PrivateKey(Env.BOB_PRIVATE_KEY);
const bob = new SymbolAccount(facade, new KeyPair(bobPrikey));
const carolPrikey = new PrivateKey(Env.CAROL_PRIVATE_KEY);
const carol = new SymbolAccount(facade, new KeyPair(carolPrikey));

// ボブ以外から受信不可
const f = models.AccountRestrictionFlags.ADDRESS.value; // アドレス制限
const flags = new models.AccountRestrictionFlags(f); // 指定アドレスからのみ受信許可
const accountRestrictionDescriptor =
  new descriptors.AccountAddressRestrictionTransactionV1Descriptor(
    flags,
    [bob.address],
    []
  );
const accountRestrictionEmbeddedTx =
  facade.createEmbeddedTransactionFromTypedDescriptor(
    accountRestrictionDescriptor,
    carol.publicKey
  );

// マルチシグ
const multisigDescriptor =
  new descriptors.MultisigAccountModificationTransactionV1Descriptor(
    1,
    1,
    [bob.address],
    []
  );
const multisigEmbeddedTx = facade.createEmbeddedTransactionFromTypedDescriptor(
  multisigDescriptor,
  carol.publicKey
);

// アグリゲートTx作成
const embeddedTransactions = [accountRestrictionEmbeddedTx, multisigEmbeddedTx];
const aggregateDescriptor =
  new descriptors.AggregateCompleteTransactionV2Descriptor(
    facade.static.hashEmbeddedTransactions(embeddedTransactions),
    embeddedTransactions
  );
const aggregateTx = facade.createTransactionFromTypedDescriptor(
  aggregateDescriptor,
  bob.publicKey,
  100,
  60 * 60 * 2,
  1
) as models.AggregateCompleteTransactionV2;

// 署名
const payloadJson = SymbolTransactionFactory.attachSignature(
  aggregateTx,
  bob.signTransaction(aggregateTx)
);

// 連署
aggregateTx.cosignatures.push(carol.cosignTransaction(aggregateTx, false));

// アナウンス
const response = await fetch(new URL("/transactions", Env.REST_GATEWAY_URL), {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({
    payload: utils.uint8ToHex(aggregateTx.serialize()),
  }),
});
const responseJson = await response.json();
console.log(responseJson.message);
```

</details>

## Alice から Carol へ転送

Carol は Bob からのみ受信できるので、失敗します。

<details><summary>ソース</summary>

```typescript
import {
  KeyPair,
  Network,
  SymbolAccount,
  SymbolFacade,
  SymbolTransactionFactory,
  descriptors,
  models,
} from "symbol-sdk/symbol";
import { PrivateKey } from "symbol-sdk";
import { Env } from "./Env.js";

const facade = new SymbolFacade(Network.TESTNET);
const alicePrikey = new PrivateKey(Env.ALICE_PRIVATE_KEY);
const alice = new SymbolAccount(facade, new KeyPair(alicePrikey));
const carolPrikey = new PrivateKey(Env.CAROL_PRIVATE_KEY);
const carol = new SymbolAccount(facade, new KeyPair(carolPrikey));

// 転送モザイク設定
const sendMosaics = [
  new descriptors.UnresolvedMosaicDescriptor(
    new models.UnresolvedMosaicId(Env.CURRENCY_MOSAIC_ID),
    new models.Amount(100_000000n)
  ),
];

// 平文メッセージ
const messageData = "\0Hello, Carol!!";

// 転送トランザクション生成
const transferDescriptor = new descriptors.TransferTransactionV1Descriptor(
  carol.address,
  sendMosaics,
  messageData
);
const transferTx = facade.createTransactionFromTypedDescriptor(
  transferDescriptor,
  alice.publicKey,
  100,
  60 * 60 * 2
);

// 署名
const payloadJson = SymbolTransactionFactory.attachSignature(
  transferTx,
  alice.signTransaction(transferTx)
);

// アナウンス
const transactionsResponse = await fetch(
  new URL("/transactions", Env.REST_GATEWAY_URL),
  {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: payloadJson,
  }
);
const responseJson = await transactionsResponse.json();
console.log(responseJson.message);
```

</details>

## Alice から Carol へアグリゲート転送

Bob への転送に紛れて**アグリゲート**で Carol に転送しようとしてみても、失敗します。

<details><summary>ソース</summary>

```typescript
import { PrivateKey } from "symbol-sdk";
import {
  SymbolFacade,
  Network,
  SymbolAccount,
  KeyPair,
  descriptors,
  models,
  SymbolTransactionFactory,
} from "symbol-sdk/symbol";
import { Env } from "./Env.js";

const facade = new SymbolFacade(Network.TESTNET);
const alicePrikey = new PrivateKey(Env.ALICE_PRIVATE_KEY);
const alice = new SymbolAccount(facade, new KeyPair(alicePrikey));
const bobPrikey = new PrivateKey(Env.BOB_PRIVATE_KEY);
const bob = new SymbolAccount(facade, new KeyPair(bobPrikey));
const carolPrikey = new PrivateKey(Env.CAROL_PRIVATE_KEY);
const carol = new SymbolAccount(facade, new KeyPair(carolPrikey));

// Bobへの転送
const transferDescriptor1 = new descriptors.TransferTransactionV1Descriptor(
  bob.address,
  [
    new descriptors.UnresolvedMosaicDescriptor(
      new models.UnresolvedMosaicId(Env.CURRENCY_MOSAIC_ID),
      new models.Amount(100_000000n)
    ),
  ],
  "\0Hello, Bob!!"
);
const transferTx1 = facade.createEmbeddedTransactionFromTypedDescriptor(
  transferDescriptor1,
  alice.publicKey
);

// Carolへの転送
const transferDescriptor2 = new descriptors.TransferTransactionV1Descriptor(
  carol.address,
  [
    new descriptors.UnresolvedMosaicDescriptor(
      new models.UnresolvedMosaicId(Env.CURRENCY_MOSAIC_ID),
      new models.Amount(100_000000n)
    ),
  ],
  "\0Hello, Carol!!"
);
const transferTx2 = facade.createEmbeddedTransactionFromTypedDescriptor(
  transferDescriptor2,
  alice.publicKey
);

// アグリゲートTx作成
const embeddedTransactions = [transferTx1, transferTx2];
const aggregateDescriptor =
  new descriptors.AggregateCompleteTransactionV2Descriptor(
    facade.static.hashEmbeddedTransactions(embeddedTransactions),
    embeddedTransactions
  );
const aggregateTx = facade.createTransactionFromTypedDescriptor(
  aggregateDescriptor,
  alice.publicKey,
  100,
  60 * 60 * 2
) as models.AggregateCompleteTransactionV2;

// 署名
const payloadJson = SymbolTransactionFactory.attachSignature(
  aggregateTx,
  alice.signTransaction(aggregateTx)
);

// アナウンス
const response = await fetch(new URL("/transactions", Env.REST_GATEWAY_URL), {
  method: "PUT",
  headers: { "Content-Type": "application/json" },
  body: payloadJson,
});
const responseJson = await response.json();
console.log(responseJson.message);
```

</details>

## Alice から Bob へ、Bob から Carol へ、そして Carol から Alice へ

Alice から Bob への転送は許可されているので、Bob が受け取り Carol へ渡します。受け取るかチェックを行いたいので、Alice からはシークレットロックで送信してもらいます。
シークレットロックで送ってもらうことで、Bob が受け取りを拒否した場合、期限が切れるとともに自動的に Alice に返還されます。

1. Alice から Bob へシークレットロックを送信
1. Bob はシークレットロックを確認
1. シークレットロックの内容に問題がない場合プルーフを送信
1. Bob は Carol にシークレットロック分のモザイクを転送
1. Bob は Carol に Alice への返事を依頼

3~5 をアグリゲート処理します。
Bob はシークレットのモザイクをそのまま Carol へ渡し、こう返事を出せと命令します。

もはや、Carol は Bob の言いなり…

シークレットロック使用済みや期限切れとかのチェックは省略。

<details><summary>ソース</summary>

```typescript
import { sha3_256 } from "@noble/hashes/sha3";
import { getRandomValues } from "crypto";
import { Hash256, PrivateKey, utils } from "symbol-sdk";
import {
  descriptors,
  KeyPair,
  models,
  Network,
  SymbolAccount,
  SymbolFacade,
  SymbolTransactionFactory,
} from "symbol-sdk/symbol";
import { Env } from "./Env.js";

const facade = new SymbolFacade(Network.TESTNET);
const alicePrikey = new PrivateKey(Env.ALICE_PRIVATE_KEY);
const alice = new SymbolAccount(facade, new KeyPair(alicePrikey));
const bobPrikey = new PrivateKey(Env.BOB_PRIVATE_KEY);
const bob = new SymbolAccount(facade, new KeyPair(bobPrikey));
const carolPrikey = new PrivateKey(Env.CAROL_PRIVATE_KEY);
const carol = new SymbolAccount(facade, new KeyPair(carolPrikey));

/** Alice->BobシークレットTx */
// シークレットとプルーフ作成
const proof = getRandomValues(new Uint8Array(20));
const secretDigest = sha3_256.create().update(proof).digest();
const secret = new Hash256(secretDigest);
console.log(`secret: ${utils.uint8ToHex(secretDigest)}`);
console.log(`proof : ${utils.uint8ToHex(proof)}`);
// シークレットロックトランザクション作成
const secretLockDescriptor = new descriptors.SecretLockTransactionV1Descriptor(
  bob.address,
  secret,
  new descriptors.UnresolvedMosaicDescriptor(
    new models.UnresolvedMosaicId(Env.CURRENCY_MOSAIC_ID),
    new models.Amount(132_000000n)
  ),
  new models.BlockDuration(5n),
  models.LockHashAlgorithm.SHA3_256
);
const secretLockTx = facade.createTransactionFromTypedDescriptor(
  secretLockDescriptor,
  alice.publicKey,
  100,
  60 * 60 * 2
);
// Alice署名
const payloadJson = SymbolTransactionFactory.attachSignature(
  secretLockTx,
  alice.signTransaction(secretLockTx)
);
const secretLockTxHash = facade.hashTransaction(secretLockTx);
// アナウンス
const secretLockResponse = await fetch(
  new URL("/transactions", Env.REST_GATEWAY_URL),
  {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: payloadJson,
  }
);
console.log((await secretLockResponse.json()).message);

/** 承認待機 */
// シークレットTx承認待ち
for (let i = 0; i < 100; i++) {
  await new Promise((resolve) => setTimeout(resolve, 1000));
  const secretTxStatus = await fetch(
    new URL(`/transactionStatus/${secretLockTxHash}`, Env.REST_GATEWAY_URL),
    {
      method: "GET",
      headers: { "Content-Type": "application/json" },
    }
  );
  if ((await secretTxStatus.json()).group === "confirmed") {
    break;
  }
}

/** シークレットロック確認 */
// シークレットロック読み
const secretInfoJson = await fetch(
  new URL(
    `/lock/secret?secret=${utils.uint8ToHex(secretDigest)}`,
    Env.REST_GATEWAY_URL
  ),
  {
    method: "GET",
    headers: { "Content-Type": "application/json" },
  }
);
const secretInfo = await secretInfoJson.json();
const secretMosaicId = new models.UnresolvedMosaicId(
  BigInt("0x" + secretInfo.data[0].lock.mosaicId)
);
const secretAmount = new models.Amount(BigInt(secretInfo.data[0].lock.amount));

/** プルーフと転送Tx送信 */
// シークレットプルーフTx
const embeddedSecretProofDescriptor =
  new descriptors.SecretProofTransactionV1Descriptor(
    bob.address,
    secret,
    models.LockHashAlgorithm.SHA3_256,
    proof
  );
const embeddedSecretProofTx =
  facade.createEmbeddedTransactionFromTypedDescriptor(
    embeddedSecretProofDescriptor,
    bob.publicKey
  );
// Bob->Carol転送Tx
const embeddedTransferDescriptor1 =
  new descriptors.TransferTransactionV1Descriptor(
    carol.address,
    [new descriptors.UnresolvedMosaicDescriptor(secretMosaicId, secretAmount)],
    ""
  );
const embeddedTransferDescriptorTx1 =
  facade.createEmbeddedTransactionFromTypedDescriptor(
    embeddedTransferDescriptor1,
    bob.publicKey
  );
// Carol->Alice転送Tx
const embeddedTransferDescriptor2 =
  new descriptors.TransferTransactionV1Descriptor(
    alice.address,
    [],
    `\0Thank You!! (${secretAmount.value})`
  );
const embeddedTransferDescriptorTx2 =
  facade.createEmbeddedTransactionFromTypedDescriptor(
    embeddedTransferDescriptor2,
    carol.publicKey
  );
// アグリゲートTx作成
const embeddedTransactions = [
  embeddedSecretProofTx,
  embeddedTransferDescriptorTx1,
  embeddedTransferDescriptorTx2,
];
const aggregateDescriptor =
  new descriptors.AggregateCompleteTransactionV2Descriptor(
    facade.static.hashEmbeddedTransactions(embeddedTransactions),
    embeddedTransactions
  );
const aggregateTx = facade.createTransactionFromTypedDescriptor(
  aggregateDescriptor,
  bob.publicKey,
  100,
  60 * 60 * 2,
  1
) as models.AggregateCompleteTransactionV2;
// 署名
const aggregateTxSignature = facade.signTransaction(bob.keyPair, aggregateTx);
const aggregateTxPayloadJson = SymbolTransactionFactory.attachSignature(
  aggregateTx,
  aggregateTxSignature
);
// アグリゲートTxアナウンス
const aggregateResponse = await fetch(
  new URL("/transactions", Env.REST_GATEWAY_URL),
  {
    method: "PUT",
    headers: { "Content-Type": "application/json" },
    body: aggregateTxPayloadJson,
  }
);
console.log((await aggregateResponse.json()).message);
```

</details>

トランザクション

https://testnet.symbol.fyi/transactions/09F2A15B80C6B96FC2DB04B4BC40701F2A4C5CF27B83D6B387580FE4F9F274A5