SIP-1000準拠のデコーダーを作る

# はじめに

### SIPについて
コミュニティのみなさんがまとめていただいてます。

https://symbol-community.com/ja/community/304

https://drive.google.com/file/d/122hovzaodAAie1dueN1LEuTag9gpj3rW/view

> SIP は "Symbol Improvement Proposal" の頭文字を取り略称表記したもので、Symbolの仕様や機能を改善するための提案、その申請の形式で、Bitcoinでは BIP 、Ethereumでは EIP という形式があります。
現在Symbolの仕様や機能の改善等についての提案は、Discordなどでコアチームに対して都度任意の形式で行われており、 BIP や EIP のようなフォーマルな形式は存在していません。
そのため今回の SIP-1000 もフォーマルな形式に則ったものではありませんが、 BIP や EIP に倣い SIP-1000 として提案し、意見を募集するものです。同時に、その提案フローを書くためのガイドラインを SIP として提案し、その形式についての意見も求めています。

### SIP-1000提案

:::note warn
SIP-1000提案のステータスは現在[DRAFT]、アイデアの共有と初期フィードバックの受付中です。みなさまからの意見を幅広く募集しています。
:::


https://github.com/kicnft/SIPs/blob/main/SIPS/sip-1000.md

### 概要
> フルオンチェーンNFTの作成時など、1つのトランザクションでは格納できないサイズのデータを記録する方法として、データを分割してアグリゲートトランザクションなどを使用して記録するのが一般的ですが、その分割形式やデータの性質に関する情報の規格を提案します。

### 本提案の趣旨
> Symbolにはすでに様々なフルオンチェーンのデータエンコード・デコード手法が存在しています。 それぞれに特徴やメリットが存在して分散性が生かされた状態ではありますが、このままでは将来的に混乱する可能性も無視できません。 そのため、アクセスしようとしているフルオンチェーンがどのような形式で保存されているのかを的確に知ることのできるメタデータを提案します。


# サンプルデコーダー
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3633677/c6b56630-64d9-c2fa-2310-db6ae57714ca.png)
作りました。今回はこのサンプルデコーダーについてのお話です。

### Git Pages デモページ
https://kicnft.github.io/SIPs/SIPS/sip1000decoder.html


テキストエリアに以下で紹介するJSONを貼りつけて出力ボタンをクリックすると、画像が表示されます。
接続するノードによっては少し時間がかかるので、ボタン押した後はしばらくお待ちください。
F12で開発者コンソールのネットワークを開いておけば進捗過程が見えて面白いかもしれません。


# 基本形

```js

//SIP-1000準拠のJSON
const json = `{
    "id":"5F9048314851E3A7",
    "type":"nftDrive",
    "encodeType":"base64",
    ...
    "structure":{
    }
}`;

//読み込み
const sip1000 = JSON.parse(json);
//ノードよりデータリストを取得
const dataList = await getDataList(sip1000);
//デコード
const body = decodeBody(dataList,sip1000);
//画面出力
appendImg(body,sip1000);
```

SIP1000準拠のJSONファイルを読み込みノードからデータリストを取得します。
JSONからデータリスト内のヘッダ情報を解析してデータ本体部分を抽出することで画面表示が可能になります。

```js
node = window.origin;
bundle = await import("https://www.unpkg.com/symbol-sdk@3.2.1/dist/bundle.web.js");
core = bundle.core;
sym  = bundle.symbol;
chain = new sym.SymbolFacade("mainnet");

function appendImg(src){
  (tag= document.createElement('img')).src = src;
  document.getElementsByTagName('body')[0].appendChild(tag);
}

async function getDataList(sip1000){

    let dataList;
    switch (sip1000.type) {

        case "nftDrive":
            dataList = await getDataListByMosaicId(sip1000);
        break;
    }
    return dataList;
}

function decodeBody(dataList,sip1000){
    let body;
    switch (sip1000.encodeType) {
        case "base64":

        break;
    }
    return body;
}
```

getDataListでは sip1000のtype値を読み取り、適切なgetDataListByXXXXXへ渡されます。
decodeBodyでは sip1000のencodeTypeを読み取り、適切なデコード方法が採用されます。
この2方法については後ほど説明します。


# NFT-DriveデータのJSON定義方法

NFT-Driveの特徴は
- listType: array
- encodeType: base64

です。

#### idにモザイクIDを指定
```json
{
    "@context": "https://kicnft.github.io/SIPs/SIPS/sip-1000/context.jsonld",
    "id":"5F9048314851E3A7",
    "type":"nftDrive",
    "listType":"array",
    "fromHeight":3019112,
    "fileName":"sample.jpeg",
    "mimeType":"image/jpeg",
    "encodeType":"base64",
    "bodySize":3948619,
    "listSize":3914,
    "structure":{
        "transactionType":16724,
        "messageType":0,
        "baseHeader":1,
        "initHeader":15
    }
}
```

typeに`nftDrive`を指定するとidをモザイクIDとしてとらえ、そのownerAddressに指定されたアカウントがfromHeight以降に記録したトランザクションを対象としてデータを取得します。


#### idにアドレスを指定
```json
{
    "@context": "https://kicnft.github.io/SIPs/SIPS/sip-1000/context.jsonld",
    "id":"ND6CPMMMMRFFEHCKUK2VPSI7VYMORCV54FXWTHA",
    "type":"sourceAddress",
    "listType":"array",
    "fromHeight":3019112,
    "fileName":"sample.jpeg",
    "mimeType":"image/jpeg",
    "encodeType":"base64",
    "bodySize":3948619,
    "listSize":3914,
    "structure":{
        "transactionType":16724,
        "messageType":0,
        "baseHeader":1,
        "initHeader":15
    }
}
```
structureはものすごく簡潔に記述することができます。
なお、SIP-1000準拠のデータはデータの開始位置となるトランザクションハッシュ値をidとして、以下のように書くことも可能です。

#### idにトランザクションハッシュ値を指定
```json
{
    "@context": "https://kicnft.github.io/SIPs/SIPS/sip-1000/context.jsonld",
    "id":"573B729E99B2C0E4D5CAEA152F31BBF6E50D61A188E4CBC0FBD8212C5B80C955",
    "type":"transactionHash",
    "listType":"array",
    "fromHeight":3019112,
    "fileName":"sample.jpeg",
    "mimeType":"image/jpeg",
    "encodeType":"base64",
    "bodySize":3948619,
    "listSize":3914,
    "structure":{
        "transactionType":16724,
        "messageType":0,
        "baseHeader":1,
        "initHeader":15
    }
}
```

データ開始位置へのアクセス方法が異なるだけで、すべて同じ画像を生成することができます。


# Metal On SymbolデータのJSON定義方法

### idにMetal IDを指定
```json
{
    "@context": "https://kicnft.github.io/SIPs/SIPS/sip-1000/context.jsonld",
    "type":"metalOnSymbol",
    "id":"FeEk8qNW7vM4ihn5Fke815MvY81Wiv9C1L9FWbSJjm91oA",
    "listType":"linked",
    "mimeType":"image/png",
    "encodeType":"blob",
    "fileName":"test.png",
    "bodySize":348404,
    "listSize":345,
    "structure":{
        "type":"byteIndex",
        "byteIndex":[
            {
                "offset":0,"type":"bitIndex",
                "bitIndex":[
                    {"offset":0,"type":"key","key":"magic","length":1},
                    {"offset":1,"type":"key","key":"textFlag","length":1}
                ]
            },
            {"offset":1,"type":"key","key":"version"},
            {"offset":2,"type":"key","key":"Additive"},
            {"offset":4,"type":"key","key":"nextKey"},
            {"offset":12,"type":"key","key":"chunk"}
        ]
    }
}
```

メタデータ内に次のデータ取得位置の情報などが含まれているため、structureの記述がかなり複雑になります。サンプルデコーダーではすべての値を参照しているわけではありません。

またメタデータを一意に特定できるハッシュ値、コンポジットハッシュ値を指定して画像を取得することも可能です。

### idにコンポジットハッシュ値を指定
```json
{
    "@context": "https://kicnft.github.io/SIPs/SIPS/sip-1000/context.jsonld",
    "type":"compositeHash",
    "id":"c95463e31409819a65eebea8951bdbf7f9bf06cd9acd7c5212e73519b87af375",
    "listType":"linked",
    "mimeType":"image/png",
    "encodeType":"blob",
    "fileName":"test.png",
    "bodySize":348404,
    "listSize":345,
    "structure":{
        "type":"byteIndex",
        "byteIndex":[
            {
                "offset":0,"type":"bitIndex",
                "bitIndex":[
                    {"offset":0,"type":"key","key":"magic","length":1},
                    {"offset":1,"type":"key","key":"textFlag","length":1}
                ]
            },
            {"offset":1,"type":"key","key":"version"},
            {"offset":2,"type":"key","key":"Additive"},
            {"offset":4,"type":"key","key":"nextKey"},
            {"offset":12,"type":"key","key":"chunk"}
        ]
    }
}
```
# COMSAデータのJSON定義方法

### idにモザイクIDを指定
```json
{
    "@context": "https://kicnft.github.io/SIPs/SIPS/sip-1000/context.jsonld",
    "type":"comsa",
    "id":"2871C49D4253246B",
    "listType":"static",
    "mimeType":"image/png",
    "encodeType":"blob",
    "fileName":"test.png",
    "bodySize":386515,
    "listSize":378,
    "structure":{
        "staticList":
        [
            "D77BFE313AF3EF1F",
            "AACFBE3CC93EABF3",
            "A0B069B710B3754C"
        ],
        "requestType":"scopedMetadataKey",
        "responseListType":"transactionHash",
        "type":"transactions",
        "transactions":{
            "transactionType":16724,
            "map":"message",
            "baseHeader":1,
            "messageType":-1
        }
    }
}
```
COMSAはstructureにscopedMetadataKeyのstaticListを保持します。このKeyで取得できるメタデータのValue値にtransactionHash値のリストが格納されているので、そこから集約されたトランザクションのデータを取得していく流れになります。

COMSAもコンポジットハッシュ値を用いてデータの開始位置を指定することが出来ます。

### idにコンポジットハッシュ値を指定
```json
{
    "@context": "https://kicnft.github.io/SIPs/SIPS/sip-1000/context.jsonld",
    "type":"compositeHash",
    "id":"C6B9CB3FBD8B7455E2CDB6977F2AA69B3592568375FA8E03A9EBEF352B27947C",
    "listType":"static",
    "mimeType":"image/png",
    "encodeType":"blob",
    "fileName":"test.png",
    "bodySize":386515,
    "listSize":378,
    "structure":{
        "staticList":
        [
            "D77BFE313AF3EF1F",
            "AACFBE3CC93EABF3",
            "A0B069B710B3754C"
        ],
        "requestType":"scopedMetadataKey",
        "responseListType":"transactionHash",
        "type":"transactions",
        "transactions":{
            "transactionType":16724,
            "map":"message",
            "baseHeader":1,
            "messageType":-1
        }
    }
}
```

# NFT-Driveデコーダーの実装

```js
async function getDataListByNftDrive(height,signerPublicKey,sip1000){

    let listSize = 0;
    let pageIndex = 1;
    let hashList = [];
    let json;
    const txList = [];
    do {
        const res = await fetch(
            `${node}/transactions/confirmed?type=16961&type=16705&pageSize=100&pageNumber=${pageIndex}`
            + `&fromHeight=${height}&signerPublicKey=${signerPublicKey}`
        );
        json = await res.json();
        hashList = json.data.map(x => x.meta.hash);

        for(let hash of hashList){
            
            const res = await fetch(node + `/transactions/confirmed/${hash}`);
            const txJson = await res.json();
            const chunk = txJson.transaction.transactions.map(x=>x.transaction.message);
            listSize += txJson.transaction.transactions.length;
            txList.push(chunk);
        }
        pageIndex++;
    
    } while(
        json.data.length == json.pagination.pageSize 
        && listSize < sip1000.listSize
    );

    const paddingSize = core.utils.uint8ToHex([sip1000.messageType]).length;
    const textDecoder = new TextDecoder();
    txList.sort(function(a, b) {

        const a2 = Number(textDecoder.decode(core.utils.hexToUint8(a[0].slice(paddingSize))));
        const b2 = Number(textDecoder.decode(core.utils.hexToUint8(b[0].slice(paddingSize))));
        return (a2 > b2) ? 1 : -1;
    })

    let isInit = true;
    let dataList = [];
    for(let item of txList){

        let body;
        if(isInit){
            isInit = false;
            body = item.slice(sip1000.structure.initHeader);
        }else{
            body = item.slice(sip1000.structure.baseHeader);
        } 
        body = body.map(x => x.slice(paddingSize));
        dataList = dataList.concat(body);
    }
    return dataList;  
}

```

- 公開鍵とfromHeightで承認済みのアグリゲートトランザクションを検索してハッシュ値を抽出
- ハッシュ値からアグリゲートトランザクションの中身を検索してメッセージ部分を抽出
- アグリゲートトランザクション単位で最初の1件目のトランザクションの数値でソート
    - これは承認が遅れたトランザクションの順序を補正するため
- ヘッダー部分を除去して結合

# Metal on Symbolデコーダーの実装

```js
async function getDataListByMetal(compositeHash,sip1000){

    let magicOffset;
    let textFlagOffset;
    let chunkOffset;

    for(item of sip1000.structure.byteIndex){
        if(item.type === "key"){
            if(item.key==="chunk"){
                chunkOffset = item.offset;
            }
        }else if(item.type === "bitIndex"){
            for(bitItem of item.bitIndex){
                if(bitItem.key === "magic"){
                    magicOffset = bitItem.offset;
                }else if(bitItem.key === "textFlag"){
                    textFlagOffset = bitItem.offset;
                }
            }
        }
    }
    
    const metaRes = await fetch(`${node}/metadata/${compositeHash}`);
    const metaJson = await metaRes.json();
    let queryString = getMetalQuery(metaJson.metadataEntry);

    const dataList = [];
    let metalRes = getMetalData(metaJson.metadataEntry.value,magicOffset,textFlagOffset,chunkOffset,sip1000);
    dataList.push(metalRes.data);

    if(metalRes.magicBit == 0){
        do {

            const res = await fetch(`${node}/metadata?${queryString}`);
            const json = await res.json();
            queryString = getMetalQuery(json.data[0].metadataEntry);
            metalRes = getMetalData(json.data[0].metadataEntry.value,magicOffset,textFlagOffset,chunkOffset,sip1000);
            dataList.push(metalRes.data);
        } while(
            metalRes.magicBit == 0
            && dataList.length < sip1000.listSize
        );
    }
    console.log(dataList.length);
    return dataList;
}
```

- sip1000からmagic、textFlag、chunk値のオフセット値を抽出
- コンポジットハッシュ値からメタデータを取得、magic値が1になるまでchunk値を取得してリストに格納

# COMSA デコーダーの実装
```js
async function getDataListByComsa(metaList,sip1000){

    const textDecoder = new TextDecoder();
    let dataList = [];
    for(let key of sip1000.structure.staticList){
        if(dataList.length >= sip1000.listSize){break;}

        const metadata = metaList.filter(x=>x.metadataEntry.scopedMetadataKey === key);
        if(metadata.length > 0){
            const hashList = JSON.parse(
              textDecoder.decode(core.utils.hexToUint8(metadata[0].metadataEntry.value))
            );
            for(hash of hashList){
              const resTx = await fetch(node + `/transactions/confirmed/${hash}`);
              const jsonTx = await resTx.json();
              const chunk = jsonTx.transaction.transactions.map(x=>x.transaction.message);
              dataList = dataList.concat(chunk.slice(1));
            }
        }
    }
    console.log(dataList.length);
    return dataList;
}
```
- staticListから1件ずつscopedMetadataKeyを取り出し、トランザクションハッシュリストを取得
- トランザクションハッシュ値からアグリゲートトランザクションを検索し、messageに埋め込まれたデータをリストに格納していく。


# encodeType別デコード方法

encodeTypeには base64とblobがあります。
表示方法が異なるので、適宜出力方法を切り替える必要があります。

```js
function decodeBody(dataList,sip1000){
    let body;
    switch (sip1000.encodeType) {
        case "base64":
            const textDecoder = new TextDecoder();
            let contentText = "";
            let bodySize = 0;
            for(item of dataList){
              uint8Item = core.utils.hexToUint8(item);
              bodySize += uint8Item.length;
              contentText +=  textDecoder.decode(uint8Item);
              if(bodySize >= sip1000.bodySize){
                break;
              }
            }
            body = contentText;
        break;
        case "blob":

            let contentArray = new Uint8Array();
            for(item of dataList){
                contentArray = new Uint8Array([...contentArray,...core.utils.hexToUint8(item)])
                if(contentArray.length >= sip1000.bodySize){
                    break;
                }
            }
            imgblob = new Blob([contentArray],{type:sip1000.mimeType});
            fileUrl = URL.createObjectURL(imgblob);
            body = fileUrl;
        break;
    }
    return body;
}
```

以上です。
ソースコードは公開しておりますので、ご興味がありましたら覗いてみてください。

https://github.com/kicnft/SIPs/blob/main/SIPS/sip1000decoder.html