LoginSignup
9

More than 1 year has passed since last update.

COMSAのNFT情報-復元方法

Posted at

目的

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

オールオンチェーンとは?

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

どんなデータがある?

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

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

NFT(Mosaic)

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

項目 説明
Mosaic ID 200998081E2F8EDD NFT自体の識別ID
Alias N/A ネームスペース。紐付いてないので名前解決はできない
Divisibility 0 可分性。発行数1なので0
Address NBWKWT2HMH7ADCZO2GDZAXEXXDISOKQOFSD3YWA Mosaicの初期発行者。テックビューロが発行(Mosaicレンタル手数料を肩代わり)している
Value 1 発行数 NFTとして1つのみが発行されている
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
AACFBE3CC93EABF3
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
テックビューロ連署アカウント
マルチシグ連署者
NACULBLI52TY44ULYDH2SDQ5R4EJUQVQBRT7XBA 12DF16F6DC102B9BB17C450E56315E63EEEDCC3E7E6692B37C9A0B1C22D93DCD

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)

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

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を検索する(これは数が増えれば検索時間がかかるので課題になる)
  2. その中の一番目のインナートランザクションメッセージを解析しhashが同じ値で比較し、2番目以降のインナートランザクションメッセージを抽出し、番号順に並べ替える
  3. データを結合し復元する

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

ちょっと、高負荷なのでrestの制限に引っかかるけど、取得のサンプルコード(クリックで展開します)
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)
  }

})()

実行結果は以下となる。

$ 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のファイルハッシュを確認する。
同じファイルハッシュなので、復元できていることが分かる。

$ sha256sum image.png
3c0d667bcffd98744e28b3253be904834cef9cb486509442d2c94e74db81fb12  image.png

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

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
9