COMSAのNFT情報-復元方法

## 目的

COMSAのNFTがどんなNFTなのかを徹底的に説明してみる

## オールオンチェーンとは？

全てのデータをブロックチェーン上に乗せていることをオールオンチェーンという

## どんなデータがある？

大きく３つに分かれているので、この３つのトランザクションで作られたものをオールオンチェーンNFTと呼んでいる

|種類|トランザクションタイプ|説明|
|---|---|---|
|NFT | Mosaicトランザクション | 無期限の発行数1のトークン |
|画像ファイル|アグリゲートトランザクション| アグリゲートトランザクションを複数使用し、内部にあるインナートランザクションのメッセージに格納 | 
|サムネイルファイル|アグリゲートトランザクション| １つのアグリゲートトランザクションを使用し、内部にあるインナートランザクションのメッセージに格納 | 

## NFT(Mosaic)

ここからは実際にあるNFTを見る(テックビューロが発行した一番最初の忍者NFTをサンプルにする)

https://symbol.fyi/mosaics/200998081E2F8EDD

|項目|値 |説明 |
|---|---|---|
| Mosaic ID	| 200998081E2F8EDD |NFT自体の識別ID|
| Alias	| N/A | ネームスペース。紐付いてないので名前解決はできない|
| Divisibility | 0 |可分性。発行数1なので0|
| Address | NBWKWT2HMH7ADCZO2GDZAXEXXDISOKQOFSD3YWA | Mosaicの初期発行者。テックビューロが発行(Mosaicレンタル手数料を肩代わり)している|
| Value	| 1 | 発行数 NFTとして１つのみが発行されている|
| Revision	| 1 | これはSymbol側のバージョン|
| Height | 916695 | NFT(Mosaic)が発行されたブロック高 |
|Block | Finalized |ブロックがファイナライズされた。つまりロールバックで消えることはない|
|Expired In Block |INFINITY | NFT(Mosaic)が無期限で発行|
|Supply Mutable	| false | 発行数をあとから変更できるか。できない|
| Transferable |true | NFTを転送できるかどうか。できる|
| Restrictable |true | NFTの転送先などを制限できるかどうか。できる|
| Revokable	| false | NFTを発行者が取り返すことができるかどうか。できない。|

メタデータがいくつかあり、Mosaicに関連した情報のKeyが分散されている

|Key|値|説明|
| --- | --- | --- |
|DA030AA7795EBE75 |{"version":"comsa-nft-1.0","name":"54656368427572656175436F7270","title":"E78DBCE8BF85E58F82E4B88A","hash":"3c0d667bcffd98744e28b3253be904834cef9cb486509442d2c94e74db81fb12","type":"NFT","mime_type":"image/png","media":"image","address":"NA7QPJZT7XGRVZSEGH4ONKWKFN5QAP72KCGU3FQ","mosaic":"200998081E2F8EDD","endorser":"N/A"}|NFTに関する情報が記載されている。一部HEX化されている。|
|A0B069B710B3754C<br>AACFBE3CC93EABF3<br>D77BFE313AF3EF1F|[A3446C73BB090E031A105F4123026EDFCD14B48DC8F8BAB3E323800AE80868DA,トランザクションハッシュ...]|画像ファイル実体のアグリゲートトランザクションが複数ある|
|B4F07181247C4201|1|- (今のところ、1しかない)|
|AD05EB49A5415213|E585A8E381A6E381AEE4BABAE381ABE38396E383ADE38383E382AFE38381E382A7E383BCE383B3E381AEE58A9BE38292E4BC9DE38188E3828BE3819FE38281E381ABE78FBEE3828CE3819F6D696A696EE382B5E382A4E38390E383BCE5BF8DE88085E380820AE382A4E383A9E382B9E38388E383ACE383BCE382BFE383BC20E69689E897A4E5B9B8E5BBB6E6B08FE381A8E69BB8E5AEB620E88487E794B0E9BE8DE5B3AFE6B08FE381ABE38288E3828BE4BD9CE59381E38082|HEX化されたNFTの説明|
|FE58A23DBB642C67|39|画像ファイルの総アグリゲートトランザクション数|
|8D9A3BDD21391AA2|comsa-nft-1.0|comsa NFTバージョン管理データ|
|89BCD45087AF19BF|N/A|エンドーサー名|
|C66A4EBE09577AF6|["EBBB8BFCEF99D1AAD771156E8582AE2D2681D9A3EE380E0888A1A23C7DB9CE72"]|サムネイルのアグリゲートトランザクション|

## 画像ファイル(Aggregate Complate Transaction)

大量のアグリゲートトランザクションがあるので、ここでは一つだけを抽出して解析する。
アグリゲートトランザクションとは、複数のトランザクションを一つに纏めて送信できる。
テックビューロが使用しているアドレスはマルチシグ化(2 of 6)されており、本来アグリゲートボンデッドトランザクションによる連署署名をする必要があるが、Aggregate Completeを使用しているため、オフライン署名をしている。(アグリゲートの説明は、今回の説明では省く)
そのため、署名者が若干複雑になっており、Aggregate Completeの署名者はテックビューロ連署アカウントで、インナートランザクションはテックビューロアカウントとなっている。

|種類|アドレス|公開鍵|
|---|---|---|
|テックビューロアカウント|NBWKWT2HMH7ADCZO2GDZAXEXXDISOKQOFSD3YWA|179DE5E32109AE013AF001934D6522DAE459A23C67981973E61CD5D1685D007D|
|テックビューロ連署アカウント<br>マルチシグ連署者|NACULBLI52TY44ULYDH2SDQ5R4EJUQVQBRT7XBA|12DF16F6DC102B9BB17C450E56315E63EEEDCC3E7E6692B37C9A0B1C22D93DCD|

https://symbol.fyi/transactions/A3446C73BB090E031A105F4123026EDFCD14B48DC8F8BAB3E323800AE80868DA

|No|インナートランザクションタイプ|宛先|値|説明|
|---|---|---|---|---|
|1|転送トランザクション|{"version":"comsa-nft-1.0","name":"TechBureauCorp","title":"獼迅参上","hash":"3c0d667bcffd98744e28b3253be904834cef9cb486509442d2c94e74db81fb12","type":"NFT","mime_type":"image/png","media":"image","address":"NA7QPJZT7XGRVZSEGH4ONKWKFN5QAP72KCGU3FQ","mosaic":"200998081E2F8EDD","endorser":"N/A"}|NA7QPJZT7XGRVZSEGH4ONKWKFN5QAP72KCGU3FQ|MosaicのメタデータにあるDA030AA7795EBE75と全く同じ情報でNFT情報がある。宛先はクリエイターのアドレスに対して送信されている|
|2-99|転送トランザクション|01470#T2hS7pSTQnS1XCbIJfJ0VkAuky2v75zeulxHKjrXqYCZiEgAMMaAcafkKxdnBwd0Wq8pEMQJ0nxv98aprpICgUQmCmhJTAxDYX0mr...|NA7QPJZT7XGRVZSEGH4ONKWKFN5QAP72KCGU3FQ|ヘッダに番号があり、#以降はBase64の文字列。この番号順で#以降の文字列を結合すれば、画像ファイルができる。宛先はクリエイターのアドレスに対して送信されている。|

## サムネイルファイル(Aggregate Complate Transaction)

画像ファイルとちがい、１つのアグリゲートトランザクション内に収まっている。
ただ、しくみは画像ファイルと一緒。

https://symbol.fyi/transactions/EBBB8BFCEF99D1AAD771156E8582AE2D2681D9A3EE380E0888A1A23C7DB9CE72

|No|インナートランザクションタイプ|宛先|値|説明|
|---|---|---|---|---|
|1|転送トランザクション|{"version":"comsa-nft-1.0","name":"TechBureauCorp","title":"獼迅参上","hash":"3c0d667bcffd98744e28b3253be904834cef9cb486509442d2c94e74db81fb12","type":"NFT","mime_type":"image/png","media":"image","address":"NA7QPJZT7XGRVZSEGH4ONKWKFN5QAP72KCGU3FQ","mosaic":"200998081E2F8EDD","endorser":"N/A"}|NA7QPJZT7XGRVZSEGH4ONKWKFN5QAP72KCGU3FQ|MosaicのメタデータにあるDA030AA7795EBE75と全く同じ情報でNFT情報がある。宛先はクリエイターのアドレスに対して送信されている|
|2-16|転送トランザクション|00000#/9j/4AAQSkZJRgABAQAAAQABAAD/2wBDABALDA4MChAODQ4SERATGCgaGBYWGDEjJR0oOjM9PDkzODdASFxOQERXRTc4UG1RV19iZ2hnPk1xeXBkeFxlZ2P/2wBDARESEhgVGC8aGi9jQjhCY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2NjY2P/wAARCADIASw...|NA7QPJZT7XGRVZSEGH4ONKWKFN5QAP72KCGU3FQ|ヘッダに番号があり、#以降はBase64の文字列。この番号順で#以降の文字列を結合すれば、画像ファイルができる。宛先はクリエイターのアドレスに対して送信されている。|


##　NFTに関する情報

この値は、Mosaicのメタデータ及び、画像ファイル・サムネイルファイルのインナートランザクションメッセージ、全てに入っている。

```
{
   version: "comsa-nft-1.0", // comsaのバージョン
   name: "TechBureauCorp", // 作成者名
   title: "獼迅参上", // NFTのタイトル
   hash: "3c0d667bcffd98744e28b3253be904834cef9cb486509442d2c94e74db81fb12", //ファイルのSHA256のチェックサム
   type: "NFT", // 発行種別
   mime_type: "image/png", // NFTのMIME
   media: "image", // NFTのMIME
   address: "NA7QPJZT7XGRVZSEGH4ONKWKFN5QAP72KCGU3FQ", // クリエイターのアドレス
   mosaic: "200998081E2F8EDD", // 紐付いたMosaic Id
   endorser: "N/A" // エンドーサー名
}
```

## COMSAを介さずにSymbolだけで画像ファイルを復元する

Mosaicのメタデータに記載されているトランザクションのハッシュを見てやれば簡単に復元できるが、メタデータは後から変更できるから、テックビューロがやろうと思えば違う画像に差し替えできるよというような話があったので、あえてMosaicのメタデータを使わないで画像ファイルを復元できることを説明する。

* 元の画像のSHA256のチェックサム値を取得(とりあえずここでは割愛して、3c0d667bcffd98744e28b3253be904834cef9cb486509442d2c94e74db81fb12で。正直NFT情報にあるものでユニークなものならなんでもいい)
* テックビューロアカウントからクリエイターアドレス宛に送っている
    * ただしアグリゲートコンプリートトランザクションから探すときは、連署者であるテックビューロ連署者アカウントから検索する
* インナートランザクションの一番目にNFTの情報がある
* インナートランザクションのメッセージの最初には番号がある

上記情報からメタデータを使わずトランザクションメッセージのみで復元するには？

1. テックビューロ連署者アカウントからクリエイターアドレス宛のAggregate Complate Transactionを検索する(これは数が増えれば検索時間がかかるので課題になる)
1. その中の一番目のインナートランザクションメッセージを解析しhashが同じ値で比較し、2番目以降のインナートランザクションメッセージを抽出し、番号順に並べ替える
1. データを結合し復元する

これでメタデータを使わずに復元できる。

<details><summary>ちょっと、高負荷なのでrestの制限に引っかかるけど、取得のサンプルコード(クリックで展開します)</summary><div>

```javascript
const {
  RepositoryFactoryHttp,
  Order,
  TransactionGroup,
  Address,
  TransactionType,
  TransferTransaction
} = require('symbol-sdk');
const {
  filter
} = require('rxjs/operators');
const config = require('config');
const fileType = require('file-type');
const fs = require('fs').promises;

// Symbolノード　負荷に耐えてそうなノードで・・・
const url = 'https://0-0-0-3.high-performance.symbol-node.jp:3001';

const fileWritePath = './';

// クリエイターのアドレス
const rawCreatorAddress = 'NA7QPJZT7XGRVZSEGH4ONKWKFN5QAP72KCGU3FQ';
// クリエイターの作成したファイルハッシュ
const creatorFileHash = '3c0d667bcffd98744e28b3253be904834cef9cb486509442d2c94e74db81fb12';

// テックビューロの連署発行公開鍵
const rawTechbureauPublicKey = '12DF16F6DC102B9BB17C450E56315E63EEEDCC3E7E6692B37C9A0B1C22D93DCD';


// 番号をsort順にするため
const numCompare = (a, b) => {
  const numA = Number(a.split('#', 2)[0]);
  const numB = Number(b.split('#', 2)[0]);

  let comparison = 0;
  if (numA > numB) {
    comparison = 1;
  } else if (numA < numB) {
    comparison = -1;
  }
  return comparison;
}


(async () => {
  try {
  const repo = new RepositoryFactoryHttp(url);

  const transactionRepository = repo.createTransactionRepository();

  const creatorAddress = Address.createFromRawAddress(rawCreatorAddress);

  //Aggregate Complateトランザクションを検索
  const searchTransaction = async (pageNumber, signer) => {
    return await transactionRepository.search({
      signerPublicKey: signer,
      group: TransactionGroup.Confirmed,
      type: [TransactionType.AGGREGATE_COMPLETE],
      pageSize: 50,
      pageNumber: pageNumber,
      order: Order.Asc
    }).toPromise()
  }

  // トランザクションハッシュを検索
  const getTransaction = async (hash) => {
    return await transactionRepository.getTransaction(hash, TransactionGroup.Confirmed).toPromise();
  }

  const aggregateHash = [];

  let check = true;
  let count = 1;
  let before_hash_count = 0;
  let count_limit = 1;

  while (check) {
    const searchtx = await searchTransaction(count, rawTechbureauPublicKey);
    searchtx.data.map(async(transaction) => {
      const getTxResult = await getTransaction(transaction.transactionInfo.hash);
      if (getTxResult.innerTransactions[0] instanceof TransferTransaction && getTxResult.innerTransactions[0].recipientAddress.plain() == creatorAddress.plain()) {
        try {
          const messageJson = JSON.parse(getTxResult.innerTransactions[0].message.payload);

          if (messageJson.version.match(/comsa/) && messageJson.hash === creatorFileHash) {
            // 雑だけど、サムネイルとどうやって判別しようてきなので、99未満はちがう(100kb以下のサイズは考えてない)
            if (getTxResult.innerTransactions[1].message.payload.match(/^00000#/) != null && getTxResult.innerTransactions.length < 99) return
            aggregateHash.push(getTxResult.transactionInfo.hash)
          }
        } catch {
          return
        }
      }
    });
    if (before_hash_count > 0 && before_hash_count == aggregateHash.length) count_limit++;
    before_hash_count = aggregateHash.length;
    // 5回同じなら終わる
    if (count_limit > 5) break;
    searchtx.isLastPage ? check = false : count++
  }

  console.log('AggregateHash Count: ', aggregateHash.length);
  console.log(aggregateHash);

  const rawMessages = await Promise.all(aggregateHash.map(async (hash) => {
    const tx = await getTransaction(hash);
    return tx.innerTransactions.map((r, i) => {
      // 0のメッセージはヘッダとしているので無視
      if (i > 0) return r.message.payload
    });
  }))

  // 配列をフラットにし、番号順にソートし、base64のデータだけにする
  const flattenMessage = [].concat(...rawMessages).filter(v => v).sort(numCompare).map((r) => r.split('#', 2)[1]);

  // decode
  const decode = Buffer.from(flattenMessage.join(), 'base64');

  // file Type取得
  const fileExtension = (await fileType.fromBuffer(decode)).ext

  // ファイル名
  const fileName = 'image.' + fileExtension;

  // ファイル作成
  const res = await fs.writeFile(fileWritePath + '/' + fileName, decode);

  console.log("### Save " + fileWritePath + '/' + fileName + " ### ")

  } catch(e) {
    console.error(e)
  }

})()

```

</div></details>


実行結果は以下となる。

```
$ node verify_file_check_test.js 
AggregateHash Count:  39
[
  '6FDC73EBFFABCD9A93F8C6E2FC76172927D9EB1DFE17B0E86528A9FA851FB5F7',
  'E36A207550C444E46DF2944980FDEAF87206858B158EBC6940AF9BD39B52C891',
  '19A17D5F2A08AC564592E0BD78BC3FE2EE063AC54CF1C313CCD1558B321E3B59',
  '026752EBC91523CECC20238900DF3D0204E10C20CA9B3DF534F3E73A55B68022',
  '63D4151F4B5CDB8579AC32B4CC73CEB52B2237C05733054642278E95326D5441',
  'BDF7232E94920899E6934985A73E5C9667D1E9B3998BF38DB5FB0C8D0868D78C',
  '6F9DDAFE9318553A181650194D51E4FDCE499C6055A39D601675A3606D34F776',
  '4BED9219CA05FF48566B3E72706F02EC7FA0F4669359D5E6FE8BC71089776F94',
  'AEE053362CB4FD70F0342870C29F63927529C364D7DC38F14592FC4432D8ED6D',
  '234579C6D3F3D0107D89122978D8F8AB630BFEDDE8B524219D6A9CBA12D0AD93',
  '3A0D8A9DBF237DA7D917FD12179A055C869D0279F9930B8083A2729D033EE5B7',
  'F52F8C1E3FEFB4D5AFDBCA267B6BD90D322A35DF50CE5A7625D3CADC83E143F8',
  'A3446C73BB090E031A105F4123026EDFCD14B48DC8F8BAB3E323800AE80868DA',
  '3EF16B0F23E041AEF18BE4E96DA0EDDCC6DCC72A01DEAD814701CA201CFCA13D',
  '8F77CEB745182AB8475303DCC943BC5D271564161E4AE95BF1EBB61EF4DC2BB3',
  '880906CB45A019E5076502F26BB61E9DA1C9C25641644AE84FA4A6C44AF19D07',
  '01A8023640DD7DE2051ECA4F39A6FCBFEDA02858821A96DE98C65E232C38E6EE',
  'ECB2F2CE16CBACBD82E550A950093CC3AFB85F0EC68277E43E5E8635AB787D37',
  '4B7D5F38AA6AC45F24EB6A666B88186A05D8E25DA6A079A2B4D151DEF4B509E8',
  '3B1E2DE5733937E8FD5C05EE66FF1CE90948048594799FDD87CFA0B95428F7AF',
  'CE9EB5A21FD2041D6746493BCA4992711D8230A73331F84E16A349292DFAE68A',
  '0CC9EC559A71B72B1BD21C4EEEA3CF33A27C24E12000135DDB68A05C5038489F',
  'C440C772215F05F8DEFC0679CE7F06FDACAEF139EDC69A5DF654A67A8DE85DF7',
  'A09A1EF46862CDC3D67077C1124595E711C38258C78CBBAC503F2ED30BE5E3B1',
  '5BA29E01DC5C02D56BDAB6CD9DF706224CDAE024FF72FCAAB2983F35817DB95D',
  '988D535A6AFCEBE8E2DCDE34B7906E5197E9DDC17B905266D7C855E0EE218C59',
  '51904FE180501860C17FF6C77263741480F593758619257411505E7DA993551D',
  '680606756B21393427D8419D8461C01AB5007CB0FF82005211D0A08408ED77A6',
  'AD90613B03BEDF312BFE8839B1C7AC55539CC2EA44FD6CF497D553B43EEBF25D',
  '73B77CB4A5C071A6859ED70F06479E904DAE18F139D070932C3F47BB1D3F5E53',
  'AB5CFA9FBFAC98A9C7810D036984C601BC94C7A0D86407FF15EC14B905C11A5F',
  '4CEFB8739363BB45136BE15817B16A448C88B11FB2F84A060D5C7540EEA733D1',
  'B167D92EE0A47C495D8616E4B2849966AD07107358EAC77CE461A5FDED3A6197',
  '6104E3E3E247C112A3476DAB7C9B79E9D425E22F05AB4DAF4805350315D34291',
  '2A20F7E2F709AABE13A3D2590A335BD0FCF228F1BFC30992301B0BD2CF781590',
  '5B166956CA072AFEE8115178EED9DE05EE0D409B6CB781DB9263C04F28774CE4',
  '6883BDB3D3C12CF65BE08F83829322B38774A505A432B98206BD7434A8010B97',
  'D179E1C9E4D96E97D678B9FA1EE6F4F1083345AB108DD4C5128151FA6FC93907',
  'DAA7708A0C1F793E52928C6B5B76E9012813E0D6BB1E884E9B09814B62906764'
]

### Save .//image.png ### 
```

作成したイメージのsha256のファイルハッシュを確認する。
同じファイルハッシュなので、復元できていることが分かる。

```bash
$ sha256sum image.png
3c0d667bcffd98744e28b3253be904834cef9cb486509442d2c94e74db81fb12  image.png
```

以上、メタデータを使わずともトランザクションメッセージのみでCOMSA-NFT画像復元する方法を記載した。