23
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 1 year has passed since last update.

nemAdvent Calendar 2021

Day 19

Symbolのメタデータを割当・更新(ついでに解剖)する

Last updated at Posted at 2021-12-18

はじめに

本記事では3つの構成に分けてSymbolのメタデータについてお話したいと思います。

実践に使いたいだけで、他に興味がない方はぜひソースコードだけご利用ください。

Symbolのメタデータについて簡単に

ブロックチェーンSymbolでは

・アカウント
・ネームスペース
・モザイク

の3つにメタデータをkey => valueの形で割当・更新することが可能です。

公式記事はこちら

メタデータを割当・更新するには"誰か"の署名が必要です。もし、その誰か?が例えば権威のある人や、何かを承認する立場にある人が使えばこのメタデータは証明書としても活用できます。

最近、自分はブロックチェーンゲームのことを考えることが多いのですが、例えばゲームアカウントが署名したメタデータならそのゲーム内でのアイテムパラメータとしても活用できます。また、そのメタデータを「そのゲームが署名しているなら信用できる」といった形で他のゲームでも使うことができるかもしれません。

色々考えれば卒業証明やトレーサビリティなどいくらでも活用の範囲はあると思います。しかし、数点気になることがあります。

それは

1.どうやって証明するの?
2.ブロックチェーンって変更できないのに更新って??

どうやって証明するの?

Symbolでは1000を超えるノードがもつAPIでデータを取得することが可能です。しかし、証明されたい人が嘘のノード情報を持っていたとして、それを提示することで嘘の証明になるのではないか?という疑問が生まれます。(ぶっちゃけ自分もこの疑問は考えてもなかったんですが最近理解してきました)

それら検証に関してはこちらの記事を見ていただければちょっと難しいですが雰囲気わかると思います。まぁようするに(詳しく説明できない…)「検証は可能」ということです。

ブロックチェーンって変更できないのに更新って??

これに関しては銀行通帳をイメージすると分かりやすいのではないでしょうか。例えば、本日時点で残高がゼロでも昨日時点で100万円あればその履歴は残っていますよね。ただし、トランザクション上で記帳されているのは現在残高ではなく前回との更新値、上記の例で言えば -100万円 です。これも通帳と同じような箇所がありますね。ですが、APIを使ってメタデータで検索すると現在の値、つまり100万円になっていますがこいつはAPIの仕様といった感じでしょうか。

つまりデータ自体は表面上変わっているように見えますが、その履歴はブロックチェーン上に残っているので、更新した所で全部丸見えです。今回の記事を書くにあたりこのあたりを調査してきましたのでご興味のある方は更新時の仕組みの解剖も御覧いただければ喜びます。

そうでもない方はソースコードだけご利用ください。

メタデータ割当・更新のソースコード

とは言っても公式で公開されているので特段新しいものはありません。ぜひこちらを参考にしていただければいいのですが同じものだと意味もないので少し変えていきます。

公式ではアグリゲートボンデッドを使用しています。

ボブが(学校の先生かな?)アリスへの割当で(こっちが多分卒業生的なほう)メタデータトランザクションを作成し、ボブの署名でネットワークへアナウンスする。ボブは10XYMを一旦ロックされ、もしアリスが署名しなければ10XYMは返ってきません。(なのでボンデッドはほぼ確実に署名されるケースで使いましょう)

その後、アリスが署名して完了。

※こちらの公式にはネームスペースやモザイクにメタデータの割当をする方法も書かれていますので、お時間あるときにぜひゆっくりご覧ください。ついでに言うとボンデッドとかコンプリートとかもあるしなんでもあるのでぜひに。

そうではなく、本記事ではアグリゲートコンプリート、両者の秘密鍵がわかっている場合にまとめて署名して終わらせてしまう方法で進めたいと思います。これは作成したいアプリケーションなどによって変わってくるので、よく考えてご利用ください。

メタデータ割当

import * as symbol from "symbol-sdk"

const networkType = symbol.NetworkType.TEST_NET;
const nodeUrl = "https://test.hideyoshi-node.net:3001/";
const epochAdjustment = 1637848847;
const networkGenerationHash = "7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836";

const alicePrivatekey = "*******";
const aliceAccount =
    symbol.Account.createFromPrivateKey(alicePrivatekey, networkType);
const bobPrivatekey ="*******";
const bobAccount =
    symbol.Account.createFromPrivateKey(bobPrivatekey, networkType);

// 以下 key value です。書き換えてください。
const key = KeyGenerator.generateUInt64Key('CERT');
const value = 'before';

const metadataTransaction =
    symbol.AccountMetadataTransaction.create(
        symbol.Deadline.create(epochAdjustment),
        aliceAccount.address,
        key,
        value.length,
        value,
        networkType,
    );

const aggregateTransaction = symbol.AggregateTransaction.createComplete(
    symbol.Deadline.create(epochAdjustment),
    [
        metadataTransaction.toAggregate(bobAccount.publicAccount),
    ],
    networkType,
    [],
    symbol.UInt64.fromUint(2000000),
);

const signedTransaction = bobAccount.signTransactionWithCosignatories(
    aggregateTransaction,
    [aliceAccount],
    networkGenerationHash,
);

const repositoryFactory = new symbol.RepositoryFactoryHttp(nodeUrl);
const transactionHttp = repositoryFactory.createTransactionRepository();
transactionHttp.announce(signedTransaction).subscribe(
  (x) => console.log(x),
  (er) => console.error(er),
);

両者の秘密鍵を知ってるケースってあんまりないと思うんですがサービス運営者がアカウントを発行してメタデータを割り当てるケースなどは考えられるかと。そのアカウントの秘密鍵知ってちゃまずいやん!ってつっこみがあるとすれば、そのアカウントをマルチシグで譲渡してしまえば秘密鍵知っててもサワレナイ。のでありえないこともないかと思います。

メタデータの更新

どちらかと言えばこちらが本題の更新です。

例のごとくこちらが公式です。

変更点は先ほどと同じくボンデッド→コンプリート。

そしてもう一点。既存のメタデータを取得する方法として公式では更新の際には、メタデータを条件で検索しネットワークから取得してそれを更新という流れです。それが普通だしなんら問題はないんですがcompositeHashというハッシュで一発でメタデータを取得する方法もあるのでその方法で進めます。

ただし、その方法はnpm install js-sha3 でパッケージをインストールする必要があるので1つ手間が増えちゃいます。

import * as symbol from "symbol-sdk"
import { sha3_256 } from "js-sha3";

const networkType = symbol.NetworkType.TEST_NET;
const nodeUrl = "https://test.hideyoshi-node.net:3001/";
const epochAdjustment = 1637848847;
const networkGenerationHash = "7FCCD304802016BEBBCD342A332F91FF1F3BB5E902988B352697BE245F48E836";

const alicePrivatekey = "*******";
const aliceAccount =
    symbol.Account.createFromPrivateKey(alicePrivatekey, networkType);
const bobPrivatekey ="*******";
const bobAccount =
    symbol.Account.createFromPrivateKey(bobPrivatekey, networkType);

function getCompositeHash(
    sourceAddress,
    targetAddress,
    scopeKey,
    targetId,
    type) {
    const hasher = sha3_256.create();
    hasher.update(symbol.Convert.hexToUint8(sourceAddress));
    hasher.update(symbol.Convert.hexToUint8(targetAddress));
    hasher.update(symbol.Convert.hexToUint8Reverse(scopeKey));
    hasher.update(symbol.Convert.hexToUint8Reverse(targetId));
    hasher.update(Uint8Array.from([type]));
    return hasher.hex().toUpperCase();
}

async function updateMetadata() {
    const key = symbol.KeyGenerator.generateUInt64Key("CERT");

    const metaRepo = new symbol.MetadataHttp(nodeUrl);
    const compositeHash =
        getCompositeHash(
            bobAccount.address.encoded(),
            aliceAccount.address.encoded(), 
            key.toHex(),
            "0000000000000000",
            0
        );
    const currentMetaData =
        await metaRepo.getMetadata(compositeHash).toPromise();
    const currentMetaValue = currentMetaData.metadataEntry.value;
    const currentMetaValueBytes =
        symbol.Convert.utf8ToUint8(currentMetaValue);
    const newMetaValue = "after"
    const newMetaDataValueBytes = symbol.Convert.utf8ToUint8(newMetaValue);

    const epochAdjustment = await repositoryFactory
        .getEpochAdjustment()
        .toPromise();

    const updateuMetadataTransaction =
        symbol.AccountMetadataTransaction.create(
            symbol.Deadline.create(epochAdjustment),
            aliceAccount.address,
            key,
            newMetaDataValueBytes.length - currentMetaValueBytes.length,
            symbol.Convert.decodeHex(symbol.Convert.xor(currentMetaValueBytes, newMetaDataValueBytes)),
            networkType,
        );

    const aggregateTransaction = symbol.AggregateTransaction.createComplete(
        symbol.Deadline.create(epochAdjustment),
        [
            updateuMetadataTransaction
                .toAggregate(bobAccount.publicAccount),
        ],
        networkType,
        [],
        symbol.UInt64.fromUint(2000000),
    );

    const signedTransaction = bobAccount.signTransactionWithCosignatories(
        aggregateTransaction,
        [aliceAccount],
        networkGenerationHash,
    );

    const transactionHttp = repositoryFactory.createTransactionRepository();
    const result = await transactionHttp.announce(signedTransaction).toPromise();
    console.log(result.message);
}

以上です。とにかく気をつけないといけないのは"差分"です。AccountMetadataTransaction.createでデータの長さものvalueも両方差分で作成する必要があります。そのため更新前のメタデータの情報が必要になります。

本来ならここで終えてもいいんですが、この記事を作成するために用意したメタデータの更新時トランザクションのvalueを見てしまい、これが何者か気になったのでこのあと探ります。

更新時の仕組みの解剖

以下がテストネットですがアカウントへのメタデータを割当→更新した際のトランザクションです。

割当時

ブロック高: 27492
value: 6265666F7265
※このvalueを16進数から文字列にするとbeforeになります。

更新時

ブロック高: 27511
value: 0303120A0065

さて、こいつが先ほど言った前回との差分トランザクションです。が、このvalueを先ほどと同じようにデコードしたところで文字化けみたいなものしか出てきません。(今回は更新後のvalueをafterにしましたが差分をデコードすると e になりました。このeに意味はありません、無理やり差分をデコードしただけ)

さて、更新時のデータの差分はソースコード上では

symbol.Convert.decodeHex(
    symbol.Convert.xor(currentMetaValueBytes, newMetaDataValueBytes)
)

となっていました。
んでこのxorメソッドの中身は

xor(value1, value2) {
    const buffer1 = Buffer.from(value1.buffer);
    const buffer2 = Buffer.from(value2.buffer);
    const length = Math.max(buffer1.length, buffer2.length);
    const delta = [];
    for (let i = 0; i < length; ++i) {
        const xorBuffer = buffer1[i] ^ buffer2[i];
        delta.push(xorBuffer);
    }
    return Convert.uint8ToHex(Uint8Array.from(delta));
}

まず、それぞれのvalueをbufferにしてますね。んで、そのbufferの長さが長い方の長さを取る。ちなみにこの引数のvalueは事前にここでUint8Arrayになってます。

const currentMetaValueBytes = symbol.Convert.utf8ToUint8(currentMetaValue);

そして、その長さ分だけ各bufferの値に対してのビット排他的論理和 (XOR) でそれを新しい配列に格納している。ちなみにビット排他的論理和 (XOR) なんて言葉は最近まで知りませんでした。

2進数にしてどちらか一方が 1 なら 1 を返します。両方0や両方1は0を返す。そして最後にその2進数を10進数にする。

そしてこのxorメソッド内ではこの配列をUint8Arrayにしてからさらに16進数文字列にしています。

やってみる

さて、せっかくなので今回のケースで解剖していきます。

まずxorメソッドにわたす引数は

const currentValue = "before";
const newValue = "after";
const currentValueBytes = Convert.utf8ToUint8(currentValue);
const newValueBytes = Convert.utf8ToUint8(newValue);

currentValueBytesnewValueBytes です。beforeとafterをUint8Arrayに変えたものですね。

んで、これら配列の中身は

buffer1: 98,101,102,111,114,101
buffer2: 97,102,116,101,114

こんな感じです。次に一文字ずつビット排他的論理和(言いたいだけ)で返します。まず1文字目の98の2進数は 1100010 で97は 1100001 です。

1100010
1100001
-------
0000011

0000011 なので10進数だと3ですね。全部ここでやる意味もないので料理番組風に全部の結果だけ、ジャン!

3,3,18,10,0,101

はい、最後にこの配列を16進数に変換してくっつけると…
(それがここ-> Convert.decodeHex()

0303120A0065

はい、きた。
更新時トランザクションのvalueと同じになりました。

で?って感じもありますが、Symbolのメタデータは更新したとしてもトランザクションに全部のってるのでデータを変更しているわけではないということですね。metadataのAPI側でそれらをいい感じにごにょごにょして表示しているんだと思います。

さいごに

さて、これで本記事は終わりです。主に活用するのはソースコードの箇所である[メタデータ割当・更新のソースコード]かと思いますが、そもそも公式でだいたい書いてあることなので他の箇所が無ければ意味もない記事だなーとも思う。(他に価値があるかは…)

一点注意点としては同じことをデスクトップウォレットでもできるんですが、なんか16進数化のところが二重になって再現できません。多分これはウォレット側の仕様だと思われ、そこまで調べる気にはなりませんでした。

最近はTypescriptやjavaのSDKを眺めながら同じことをC#で再現するため、こんな感じで解剖をずっとやってます。どの言語の知識もほとんどないので全部ググりながら一個ずつ検証して正しければ採用!みたいなw

ではでは、読みにくい記事を最後まで読んでいただきありがとうございました。

23
8
1

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
  3. You can use dark theme
What you can do with signing up
23
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?