ログイン中のQiita Team
ログイン中のチームがありません

Qiita Team にログイン
コミュニティ
OrganizationイベントアドベントカレンダーQiitadon (β)
サービス
Qiita JobsQiita ZineQiita Blog
JavaScript
Symbol
ブロックチェーン
Symbol-sdk
オンチェーンNFT
6
どのような問題がありますか?

投稿日

更新日

RawMessageを使ってファイルをブロックチェーンに書き込む

garushのNFT

先日,garushのオンチェーンNFTが話題になりました.公式でもオンチェーンNFTがサポートされるといった話であり,個人的にとても嬉しいです.
こちらをご覧ください

これはgarushのNFTのデータのトランザクションを示しています.そして,この情報こそがオンチェーンの情報です.データを見てみましょう

スクリーンショット 2021-12-10 17.30.48.png

Raw message....?????

私はRaw messageの存在を知りませんでした(encryptとplainだけだと思っていた)気になるので生のデータを見てみました.

スクリーンショット 2021-12-10 17.34.47.png

messageがRawなのがわかります.文字数をカウントすると2048,どうやらバイナリデータを1KBそのまま書き込んでいるようです.

RawMessageは何が嬉しいのか

ファイルのデータ(バイナリデータ)をPlainMessageで書き込む際はbase64 変換などで文字列化を行う必要があります.エンコードの性質上,base64の場合はファイルのサイズが137%に増加します.RawMessageで書き込めばノードへの負担を軽く,手数料も安くデータを書き込めます.

やってみる

エンコード

ファイルを読み込んでbufferを1KBごとにuint8配列で分割します.

path = 'tesnshi.jpeg'
var data = fs.readFileSync(path);
var uint8Arrays = [];
const messageSize = 1024;
for (var i = 0; i <= Math.floor(data.length/ messageSize); i++) {
    const arr = data.slice(i*messageSize,(i+1)*messageSize);
    uint8Arrays.push(toArrayBuffer(arr));
}
console.log(uint8Arrays);
console.log(uint8Arrays.length);

function toArrayBuffer(buffer) {
  var view = new Uint8Array(buffer.length);
  for (var i = 0; i < buffer.length; ++i) {
      console.log(buffer[i]);
    view[i] = buffer[i];
  }
  return view;
}

これを元にアグリゲートトランザクションを作成します.

var aggregateTXs = [];
var txs = []
var hashes = [];

for (var i = 0; i < uint8Arrays.length; i++) {
    const innerTransaction = xym.TransferTransaction.create(
      xym.Deadline.create(epochAdjustment),
      signer.address,
      [],
      xym.RawMessage.create(uint8Arrays[i]),
      networkType
    );

    txs.push(innerTransaction.toAggregate(signer.publicAccount));
    if (i % 100 == 99 || i == uint8Arrays.length-1 ) {
      const aggregateTransaction = xym.AggregateTransaction.createComplete(
        xym.Deadline.create(epochAdjustment),
        txs,
        networkType,
        [],
      ).setMaxFeeForAggregate(feemultiplier, 0);
      for(t of aggregateTransaction.innerTransactions)console.log(t.message.payload);
      const signedTransaction = signer.sign(
        aggregateTransaction,
        networkGenerationHash,
      );
      aggregateTXs.push(signedTransaction);
      hashes.push(signedTransaction.hash);
      txs = [];
    }
}

これでファイルのデータを含んだアグリゲートトランザクション群が完成しました.(データが溢れた場合は複数作成されます)
ではアナウンスしてみましょう!!

スクリーンショット 2021-12-10 18.00.35.png

どうして....

メッセージサイズを500にすると書き込めたのですが,Rawmessageの値が違っていました.公式は別の手段を使ってRawMessageを書き込んでいるようです.

本家を見に行ってみる

本家のGarushのpackage.jsonを見てみるとsymbol-sdk@1.0.3-message-improvement-202111021446が使用されていました.これを使えばいけそうです.今インストールしているsdkをアンインストールしてこちらを入れてみます.

成功しました.

デコードする

ファイルをチェーンに書き込めても復元できなければ無駄になってしまいます.復元してみましょう.

var file = '';
var data = [];
const hash = 'E518BEA1A8CFAE297F354E408E515D8F827F4C21A168371376755DDF4290AE16'
data.push(await transactionHttp.getTransaction(hash, xym.TransactionGroup.Confirmed).toPromise());
for (d of data) {
    const innerTxs = d.innerTransactions;
    for (tx of innerTxs) {
        file += tx.message.payload;
    }
}
console.log(file);
console.log(Buffer.from(file,'hex'));

fs.writeFile("test.jpeg", file, (err) => {
        if (err) throw err;
        console.log('正常に書き込みが完了しました');
 });

test.jpeg

勝ちました.

後はこのアグリゲートトランザクション群のハッシュを記録したトランザクションを生成し,データの内容を示すトランザクションをチェーンに書き込んであげればオンチェーン化完了です.

ファイルの情報を書き込む

今回はファイルの情報を次のようにしてみました

項目 内容
version 形式のバージョン
mime ファイルのマイムタイプ
ext ファイルの拡張子
name ファイル名
size サイズ
data アグリゲートのハッシュを,区切りで

これらの情報をJSON化し,次のようにしてみました.

{
"version":1,
"mime":"image/jpeg",
"ext":".jpeg",
"name":"tenshi.jpeg",
"size":39556,
"data":"53649368B53F3EDBD0B1FEBE7C7B083925A02DF7CD1E263D7FDCA2B87A0830C1,3EF259C24A339FF90EB19C65FD363CB2B3E14C4B2DE1B5DF2B014BE10D292F58"
}

これらの情報を含んだアグリゲートトランザクションを作成し,このアグリゲートトランザクションのハッシュをデータにアクセスするための情報トランザクションとして使用します.

書き込み

トランザクションの送信速度は_sleepの値を変更することで変えることができます.状況に応じて切り替えてください.

const xym = require('symbol-sdk');  
const fs = require('fs');
global.fetch = require('node-fetch');

const mime = require("mime");
const fPath = __dirname + '/';
const Path = require("path");
const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));


var networkGenerationHash ='';
var epochAdjustment = '';
var node = 'http://sym-test-01.opening-line.jp:3000';

var repo;
var transactionHttp;
var currencyMosaicId;
const networkType =  xym.NetworkType.TEST_NET;
var feemultiplier = 100;
const messageSize = 1024;

var transactionHash = "";

const privateKey = '****';
//送信元アカウント
const senderAccount = xym.Account.createFromPrivateKey(
  privateKey,
  networkType,
);

const filename = "iotw1952a.tif";//読み込むファイル名
const path = fPath + filename;
var fileData = fs.readFileSync(path);

(async()=>{
    repo = new xym.RepositoryFactoryHttp(node);
    transactionHttp = repo.createTransactionRepository();
    epochAdjustment = await repo.getEpochAdjustment().toPromise();
    networkGenerationHash = await repo.getGenerationHash().toPromise();
    currencyMosaicId = new xym.MosaicId((await repo.createNetworkRepository().getNetworkProperties().toPromise()).chain.currencyMosaicId.replace(/0x/, "").replace(/'/g, ""));
    const txs = makeData(fileData,senderAccount);
    await announceTx(txs);
})();


function toUint8Arryay(buffer) {
  var view = new Uint8Array(buffer.length);
  for (var i = 0; i < buffer.length; ++i) view[i] = buffer[i];
  return view;
}
function makeData(data,signer){

  console.log(data);
  console.log(data.length);
  var uint8Arrays = [];
  for (var i = 0; i <= Math.floor(data.length/ messageSize); i++) {
    const arr = data.slice(i*messageSize,(i+1)*messageSize);
    uint8Arrays.push(toUint8Arryay(arr));
  }

  var aggregateTXs = [];
  var txs = []
  var hashes = [];
  for (var i = 0; i < uint8Arrays.length; i++) {
    const innerTransaction = xym.TransferTransaction.create(
      xym.Deadline.create(epochAdjustment),
      signer.address,
      [],
      xym.RawMessage.create(uint8Arrays[i]),
      networkType
    );

    txs.push(innerTransaction.toAggregate(signer.publicAccount));
    if (i % 100 == 99 || i == uint8Arrays.length-1 ) {
      const aggregateTransaction = xym.AggregateTransaction.createComplete(
        xym.Deadline.create(epochAdjustment),
        txs,
        networkType,
        [],
      ).setMaxFeeForAggregate(feemultiplier, 0);
      const signedTransaction = signer.sign(
        aggregateTransaction,
        networkGenerationHash,
      );
      aggregateTXs.push(signedTransaction);
      hashes.push(signedTransaction.hash);
      txs = [];
    }
  }

  const dataInfoMsg = JSON.stringify({
    version:1,
    mime: mime.getType(path),
    ext: Path.extname(path),
    name: filename,
    size: data.length,
    data: hashes.join(',')
  });
  console.log(dataInfoMsg);

  var msgArr = [];
  const plainMsgSize = 1023;
  //msgのアグリゲート化
  for (var i = 0; i < Math.floor(dataInfoMsg.length / plainMsgSize) + 1; i++) msgArr.push(dataInfoMsg.substr(i * plainMsgSize, plainMsgSize));

  for (var i = 0; i < Math.floor(dataInfoMsg.length / plainMsgSize) + 1; i++) {
    const innerTransaction = xym.TransferTransaction.create(
      xym.Deadline.create(epochAdjustment),
      signer.address,
      [],
      xym.PlainMessage.create(msgArr[i]),
      networkType
    );

    txs.push(innerTransaction.toAggregate(signer.publicAccount));
    if (i % 100 == 99 || i == Math.floor(dataInfoMsg.length / plainMsgSize)) {
      const aggregateTransaction = xym.AggregateTransaction.createComplete(
        xym.Deadline.create(epochAdjustment),
        txs,
        networkType,
        [],
      ).setMaxFeeForAggregate(feemultiplier, 0);
      const signedTransaction = signer.sign(
        aggregateTransaction,
        networkGenerationHash,
      );
      aggregateTXs.push(signedTransaction);
      txs = [];
      console.log("informationTX:",signedTransaction.hash);
    }
  }

  return aggregateTXs;
}


async function announceTx(aggTxs,retry = 5){
  var failed = [];
  for(data of aggTxs){

    await _sleep(600);
    try{
        transactionHash = data.hash;
        console.log(node+'/transactionStatus/'+transactionHash);
        transactionHttp.announce(data).subscribe(
          (x) => console.log(x),
          (err) => console.error(err),
        );
    }catch{
        failed.push(data);
    }

  }
  if(retry==0)process.exit(1);
  if(failed.length!=0)announceTx(failed,retry-1);
}

読み込み

先ほどはSDKを使って読み込んでいたのですが,大きめのファイルをアップロードした際に読み込みに失敗しました.調べてみたところ,SDKにてRawMessageがPlainMessageとして判別される症状があり修正中のようです.そこで,直接restから読んでみました.

const xym = require('symbol-sdk');  
const fs = require('fs');
const fetch = require('node-fetch');
//速度の問題で接続ノードは日本がおすすめです
var node = 'http://sym-test-01.opening-line.jp:3000';

var repo;
var transactionHttp;

(async()=>{
  repo = new xym.RepositoryFactoryHttp(node);
  transactionHttp = repo.createTransactionRepository();
  const infoHash = '';//書き込んだファイルのデータハッシュ
  const dataInfo = JSON.parse(
    innerTxJoin(
      await transactionHttp.getTransaction(infoHash, xym.TransactionGroup.Confirmed).toPromise()
    )
  );
  console.log(dataInfo);
  const file = await makeFile(dataInfo.data);
  console.log(file.length);
  fs.writeFile(dataInfo.name, file, (err) => {
      if (err) throw err;
      console.log('書き込みが完了しました');
  });

})();


async function makeFile(transactionHashes){
  var file = '';
  var data = [];
  var bufs = [];
  const hashes = transactionHashes.split(',');

  console.log("===Status===");
  var i = 1;
  for (hash of hashes) {
    const innnerTx = (await fetchConfirmedTransaction(hash)
    ).transaction.transactions;
    data.push(innnerTx);
    console.log(i+"/"+hashes.length);
    i++;
  }

  for (txs of data){
    var i = 0;
    for(tx of txs){
      bufs.push(Buffer.from(tx.transaction.message,'hex'));
    }
  }
  return Buffer.concat(bufs);;
}

function innerTxJoin(aggTx){
  var data = '';
  const innerTxs = aggTx.innerTransactions;
  for (inTx of innerTxs)data += inTx.message.payload;
  return data;
}

async function fetchConfirmedTransaction(hash){  
  const data = await fetch(node+'/transactions/confirmed/'+hash);
  const json = JSON.parse(await data.text());
  return json;
}

重めのファイルをアップロードしてみる

重めのファイルをアップロードを試してみます.今回はnoirlabの画像を使ってみました.

57.8MBのサイズのものを使わせて頂きました.

アップロード費用:約6000XYM強(feeMultiPlierは100)
トランザクション数:約60000弱(内部トランザションを含む)

情報トランザクションのハッシュ:
3EF259C24A339FF90EB19C65FD36CCB2BFE14C4B2DE1B5DF2B014BE10D292F58

情報トランザクションを上の読み込みプログラムのハッシュの欄に入れるとDLができます.

更に大容量のファイルをアップロードしてみる

先ほどのファイルをzipファイルに4枚格納し,大容量にしてみました.

容量:229.5MB
トランザクション数:約226000
手数料:25000XYM(手数料率は100)

情報トランザクションのハッシュ:
A25E3B110F65C15E41FECD69DA78CECF81E637FFB4AA076BA9FAD8C3C6A72C92

ハッシュの数が増えすぎてアグリゲートトランザクションからはみ出してしまったため,別のアグリゲートにも格納するように改良しました.これでXYMがある限りはアップロードできると思います.

書き込み

const xym = require('symbol-sdk');  
const fs = require('fs');
global.fetch = require('node-fetch');

const mime = require("mime");
const fPath = __dirname + '/';
const Path = require("path");
const _sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));


var networkGenerationHash ='';
var epochAdjustment = '';
var node = 'http://sym-test-01.opening-line.jp:3000';

var repo;
var transactionHttp;
var currencyMosaicId;
const networkType =  xym.NetworkType.TEST_NET;
var feemultiplier = 100;
const messageSize = 1024;
const maxInnerTransacion = 100;

var transactionHash = '';

const privateKey = '***';
//送信元アカウント
const senderAccount = xym.Account.createFromPrivateKey(
  privateKey,
  networkType,
);

const filename = 'iotw4.zip';//読み込むファイル名
var fileData = fs.readFileSync(filename);

(async()=>{
    repo = new xym.RepositoryFactoryHttp(node);
    transactionHttp = repo.createTransactionRepository();
    epochAdjustment = await repo.getEpochAdjustment().toPromise();
    networkGenerationHash = await repo.getGenerationHash().toPromise();
    currencyMosaicId = new xym.MosaicId((await repo.createNetworkRepository().getNetworkProperties().toPromise()).chain.currencyMosaicId.replace(/0x/, "").replace(/'/g, ""));
    const txs = makeData(fileData,senderAccount);
    await announceTx(txs);
})();


function toUint8Arryay(buffer) {
  var view = new Uint8Array(buffer.length);
  for (var i = 0; i < buffer.length; ++i) view[i] = buffer[i];
  return view;
}
function makeData(data,signer){

  console.log(data);
  console.log(data.length);
  var uint8Arrays = [];
  for (var i = 0; i <= Math.floor(data.length/ messageSize); i++) {
    const arr = data.slice(i*messageSize,(i+1)*messageSize);
    uint8Arrays.push(toUint8Arryay(arr));
  }

  var aggregateTXs = [];
  var txs = []
  var hashes = [];
  for (var i = 0; i < uint8Arrays.length; i++) {
    const innerTransaction = xym.TransferTransaction.create(
      xym.Deadline.create(epochAdjustment),
      signer.address,
      [],
      xym.RawMessage.create(uint8Arrays[i]),
      networkType
    );

    txs.push(innerTransaction.toAggregate(signer.publicAccount));
    if (i % maxInnerTransacion == (maxInnerTransacion-1) || i == uint8Arrays.length-1 ) {
      const aggregateTransaction = xym.AggregateTransaction.createComplete(
        xym.Deadline.create(epochAdjustment),
        txs,
        networkType,
        [],
      ).setMaxFeeForAggregate(feemultiplier, 0);
      const signedTransaction = signer.sign(
        aggregateTransaction,
        networkGenerationHash,
      );
      aggregateTXs.push(signedTransaction);
      hashes.push(signedTransaction.hash);
      txs = [];
    }
  }

  const dataInfoMsg = JSON.stringify({
    version:1,
    mime: mime.getType(path),
    ext: Path.extname(path),
    name: filename,
    size: data.length,
    data: hashes.join(',')
  });
  console.log(dataInfoMsg);

  var msgArr = [];
  const plainMsgSize = 1023;

  var arr = [];
  //msgを99個ずつに分ける
  for (var i = 0; i <= Math.floor(dataInfoMsg.length / plainMsgSize) ; i++){
    arr.push(dataInfoMsg.substr(i * plainMsgSize, plainMsgSize));
    if(i%(maxInnerTransacion-1)==(maxInnerTransacion-2) ||
     i == Math.floor(dataInfoMsg.length / plainMsgSize)){
      msgArr.push(arr);
      arr = [];
    }
  }

  var nextHash = "";
  msgArr.reverse();
  console.log(msgArr);
  for(i=0;i<msgArr.length;i++){
    console.log("msg:",i);
    var txs = [];
    for(k=0;k<msgArr[i].length;k++){
      console.log("msg",msgArr[i][k]);
      const innerTransaction = xym.TransferTransaction.create(
        xym.Deadline.create(epochAdjustment),
        signer.address,
        [],
        xym.PlainMessage.create(msgArr[i][k]),
        networkType
      );
      txs.push(innerTransaction.toAggregate(signer.publicAccount));
    }
    if(i!=0){
      const innerTransaction = xym.TransferTransaction.create(
        xym.Deadline.create(epochAdjustment),
        signer.address,
        [],
        xym.PlainMessage.create("hash:"+nextHash),
        networkType
      );
      txs.push(innerTransaction.toAggregate(signer.publicAccount));
    }
    const aggregateTransaction = xym.AggregateTransaction.createComplete(
      xym.Deadline.create(epochAdjustment),
      txs,
      networkType,
      [],
    ).setMaxFeeForAggregate(feemultiplier, 0);
    const signedTx = signer.sign(
      aggregateTransaction,
      networkGenerationHash,
    );
    nextHash = signedTx.hash;
    if(i==0)console.log("informationHash: "+nextHash);
    aggregateTXs.push(signedTx);
  }

  return aggregateTXs;
}


async function announceTx(aggTxs,retry = 5){
  var failed = [];
  for(data of aggTxs){

    await _sleep(750);
    try{
        transactionHash = data.hash;
        console.log(node+'/transactionStatus/'+transactionHash);
        transactionHttp.announce(data).subscribe(
          (x) => console.log(x),
          (err) => console.error(err),
        );
    }catch{
        failed.push(data);
    }

  }
  if(retry==0)process.exit(1);
  if(failed.length!=0)announceTx(failed,retry-1);
}

読み込み

const xym = require('symbol-sdk');  
const fs = require('fs');
const fetch = require('node-fetch');

var node = 'http://sym-test-01.opening-line.jp:3000';

var repo;
var transactionHttp;

(async()=>{
  repo = new xym.RepositoryFactoryHttp(node);
  transactionHttp = repo.createTransactionRepository();
  infoHash = 'A25E3B110F65C15E41FECD69DA78CECF81E637FFB4AA076BA9FAD8C3C6A72C92';
  var data = '';
  while(true){
    const res = await getFileInfo(infoHash);
    data += res.data;
    if(res.hash!='')infoHash = res.hash;
    else break;
  }
  const dataInfo = JSON.parse(data);
  console.log(dataInfo);
  const file = await makeFile(dataInfo.data);
  console.log(file.length);
  fs.writeFile('t-'+dataInfo.name, file, (err) => {
      if (err) throw err;
      console.log('書き込みが完了しました');
  });

})();


async function makeFile(transactionHashes){
  var file = '';
  var data = [];
  var bufs = [];
  const hashes = transactionHashes.split(',');

  console.log("===Status===");
  var i = 1;
  for (hash of hashes) {
    const innnerTx = (await fetchConfirmedTransaction(hash)
    ).transaction.transactions;
    data.push(innnerTx);
    console.log(i+"/"+hashes.length);
    i++;
  }

  for (txs of data){
    for(tx of txs){
      bufs.push(Buffer.from(tx.transaction.message,'hex'));
    }
  }
  return Buffer.concat(bufs);
}

async function getFileInfo(hash){
  console.log(hash);
  const aggTx = await transactionHttp.getTransaction(
    hash,
    xym.TransactionGroup.Confirmed
    ).toPromise();
  var data = '';
  var nextHash= '';
  const innerTxs = aggTx.innerTransactions;
  console.log(innerTxs.length);
  for(i=0;i<innerTxs.length;i++){
    if(i!=innerTxs.length-1){
      data += innerTxs[i].message.payload;
    }else{
      console.log(i,innerTxs[i].message.payload.slice(0,5))
      if(innerTxs[i].message.payload.slice(0,5)=='hash:'){
        nextHash = innerTxs[i].message.payload.slice(5,);
      }else{
        data += innerTxs[i].message.payload;
      }
    }
  }
  return {"data":data,"hash":nextHash};
}

async function fetchConfirmedTransaction(hash,retry=5){ 
  if (retry == -1) {
    console.log("fetch error");
    process.exit(1);
  }
  try{
    const res = await fetch(node + '/transactions/confirmed/' + hash);
    if(res.status==200){
      const json = JSON.parse(await res.text());
      return json;
    }else{
      throw res.status;
    }

  }catch(e){
    console.log(e);
    console.log("retry",retry)
    return await fetchConfirmedTransaction(hash,retry-1);
  }

}

気づいたこと

・手数料率を100にしたため10ブロックで処理できましたが,実際には手数料を10にしたい方が多いと思うので実際には更にかかると思います.
・トランザクション数の増加に伴い,送信に失敗することがあるので,失敗したトランザクションを再度投げる仕組みが必要だと思います.
・メインネットで失敗すると取り返しがつかないのでテストネットでファイルをテストアップロードしてから実施するのが良いと思います
・ブロック生成が遅くなりました(私の環境では平均45秒)
・データの詰まったアグリゲートトランザクションは1ブロックあたり59個しか投げられないので,手数料率100の場合は0.8秒くらいの間隔で送信するとノードにかける負荷を抑えて送付できるかもしれません.

今後やってみたいこと

アップロードが正常に行われたか検証するプログラムや,安定して大量のTXを送付するプログラムを作成してみたいです.

その他

テスト中のあるアドレスのハーベスト量がエグいことになっていました.
スクリーンショット 2021-12-11 15.14.48.png

ユーザー登録して、Qiitaをもっと便利に使ってみませんか。
  1. あなたにマッチした記事をお届けします
    ユーザーやタグをフォローすることで、あなたが興味を持つ技術分野の情報をまとめてキャッチアップできます
  2. 便利な情報をあとで効率的に読み返せます
    気に入った記事を「ストック」することで、あとからすぐに検索できます
monakaJP
もなかが好物です。
この記事は以下の記事からリンクされています

コメント

この記事にコメントはありません。
あなたもコメントしてみませんか :)
ユーザー登録
すでにアカウントを持っている方はログイン
6
どのような問題がありますか?
ユーザー登録して、Qiitaをもっと便利に使ってみませんか

この機能を利用するにはログインする必要があります。ログインするとさらに下記の機能が使えます。

  1. ユーザーやタグのフォロー機能であなたにマッチした記事をお届け
  2. ストック機能で便利な情報を後から効率的に読み返せる
ユーザー登録ログイン
ストックするカテゴリー