LoginSignup
18
4

More than 3 years have passed since last update.

NEM2のSDKを作っててハマったこといろいろ

Last updated at Posted at 2019-12-21

この記事は nemアドベントカレンダーの22日目の記事です。

どうも、りゅーた です。フリーのエンジニアやってます。

NEM2(Catapult) の Swift版 SDK の開発をしてます。
Advent Calendar までには Fushicho3 対応してリリースできると思ってたんですが、間に合いませんでした。すみません。毎度バージョンアップによるAPIとかバイナリフォーマットの差分が激しくて・・・(完全なる言い訳)

SDK開発してると、基本すべての API に一度は触れることになりますし、テストコード書く時に全種のトランザクションを一度は作成することになるので、あまり気にしてなかった API の細かい動作など気づくことがあります。

今回はそんな感じで NEM2 の SDK 開発中に私がハマった事象・トラブルシューティングをいくつか寄せ集めました。
ニッチな内容が多分に含まれてるかと思いますが、同様の問題踏んだ際にお役に立てれば。

以下、サンプルコードは TypeScript で、nem2-sdk-typescript 0.16.0 を使っています。

MosaicAddressRestrictionTransaction 設定つもりなのにモザイク送信できない -> 送信側の設定も必要だった。

NEM2 の新機能に、モザイク制限 の機能があります。
モザイク制限機能についてはすでに mincoshi さんが Advent Calendar 内でも記事を書いてくれているのでそちらもご参照ください。
CatapultのMosaicRestriction(モザイク制限)機能

特定のモザイクの転送を指定したアドレスにのみ制限するといった機能で、手順も Dev Center に記載 のある通りで、ざっくりいうと

  1. restrictable プロパティを true にしてモザイクを作成する
  2. モザイクに対してグローバルな設定を定義
  3. アドレス固有の設定を定義

という手順で行われ、2. のグローバルな設定は MosaicGlobalRestrictionTransaction、3. のアドレス固有 MosaicAddressRestrictionTransaction というトランザクションが該当します。

このモザイク制限、モザイクの発行者にも適用されるようで、3. のアドレス固有の設定をモザイク発行者の方に行っていなくてエラーになるということがありました。

例として、MosaicGlobalRestrictionTransaction でグローバル設定を 10 以上に

    const mosaicGlobalRestrictionTransaction = MosaicGlobalRestrictionTransaction.create(
        Deadline.create(),
        mosaicId,
        restrictionKey,
        UInt64.fromUint(0),
        MosaicRestrictionType.NONE,
        UInt64.fromUint(10),
        MosaicRestrictionType.GE,
        networkType,
    );

MosaicAddressRestrictionTransactiontargetAccount のアカウントに 20 を設定

    const mosaicAddressRestrictionTransaction = MosaicAddressRestrictionTransaction.create(
        Deadline.create(),
        mosaicId,
        restrictionKey,
        targetAccount.address,
        UInt64.fromUint(20),
        networkType,
    );

当然 20 > 10 なので、targetAccount にモザイクを送りつけることができるだろうとモザイク発行者から targetAccount への送信トランザクションを作ってみると、
"Failure_RestrictionMosaic_Account_Unauthorized" エラーで失敗します。

発行者のアカウントの方にも MosaicAddressRestrictionTransaction で設定する必要があります。

    const mosaicAddressRestrictionTransaction = MosaicAddressRestrictionTransaction.create(
        Deadline.create(),
        mosaicId,
        restrictionKey,
        issuerAccount.address,
        UInt64.fromUint(20),
        networkType,
    );

アカウント制限を使おうとしてエラー -> 仕様上使えない組合せの設定値を使おうとしていた。

制限系のものでもう一つ。
特定のアカウントアドレスや特定のモザイクの送受信を制限する機能として、アカウント制限 の機能があります。

早い話、上記 dev center のリンクの最初にあるこの表

制限 受信トランザクション 送信トランザクション
AccountAddressRestriction ✔️ ✔️
AccountMosaicRestriction ✔️
AccountOperationRestriction ✔️

のとおりで、組み合わせによっては使えない制限があります。

内部的な話を少しすると、このアカウント制限の設定値は

  • 種類
    アドレス(0x0001)、モザイク(0x0002)、トランザクション種別(0x0004)
  • 受信か送信か
    受信(0x0000), 送信(0x4000)
  • 許容か拒否か
    許容(0x0000), 拒否(0x8000)

の三種類の値の論理和で計算される様になっていて、
例えば特定のアドレスからの受信を拒否する場合、

アドレス(0x0001) | 受信(0x0000) | 拒否(0x8000) = 0x8001

が設定値として使われる感じです。
このあたりはアプリの方で計算する必要はなくて、SDKの方で使える値が定義されてるのであまり気にする必要はありませんが、上記のとおりすべての組合せが有効なわけではないので、直に REST を叩きに行きたい猛者の方は少し注意が必要です。

メタデータの更新ができない -> メタデータの更新は現在値との XOR で行う必要があった

NEM2 の新機能の一つ、メタデータ について。

アカウント、モザイク、ネームスペースのそれぞれに任意の 1024 バイトまでの文字列を付与することができるって感じの機能です。

私がメタデータいじってた時には気づかなかったのですが、この節に書いてる内容の大部分すでに dev center でドキュメント化されてました。まずはそちらを参照したほうが良さそうです。

該当のドキュメントはこちら
メタデータエントリの更新

メタデータは非常に汎用性がありそうな機能ですが、このメタデータの値を 更新 がうまくできずハマりました。

例を書いていきます。

まずは新規で AccountMetadataTransactionを発行してアカウントにメタデータを付与してみます。

    const firstMetadataValue = 'first metadata';
    const metadataKey = UInt64.fromUint(5);
    const accountMetadataTransaction = AccountMetadataTransaction.create(
        Deadline.create(),
        metadataAccount.publicKey,
        metadataKey,
        firstMetadataValue.length,
        firstMetadataValue,
        networkType,
    );

    const aggregateTransaction = AggregateTransaction.createComplete(Deadline.create(),
        [accountMetadataTransaction.toAggregate(metadataAccount.publicAccount)],
        networkType,
        [],
    );

トランザクションを送信、承認された後にアカウントのメタデータを確認します。

    const firstMetaData = await metadataHttp.getAccountMetadataByKeyAndSender(metadataAccount.address, metadataKey.toHex(), metadataAccount.publicKey).toPromise();

    console.log(firstMetaData.metadataEntry.value);

結果

first metadata

OK ですね。

ではこのメタデータを上書き更新したくなった場合。
試しに上記と同じ方法で更新してみます。

    const secondMetadataValue = 'second metadata';
    const accountMetadataTransactionUpdate = AccountMetadataTransaction.create(
        Deadline.create(),
        metadataAccount.publicKey,
        metadataKey,
        secondMetadataValue.length,
        secondMetadataValue,
        networkType,
    );

"Failure_Metadata_Value_Size_Delta_Mismatch" というエラーがでて失敗しました。

上記で secondMetadataValue.length でメタデータの長さを与えている箇所、実は現在の長さとの差分を与えるパラメータです。というわけで修正して

    const secondMetadataValue = 'second metadata';
    const accountMetadataTransactionUpdate = AccountMetadataTransaction.create(
        Deadline.create(),
        metadataAccount.publicKey,
        metadataKey,
        secondMetadataValue.length - firstMetadataValue.length, // 修正した
        secondMetadataValue,
        networkType,
    );

これでトランザクション通りました。さきほどと同じようにメタデータを取得してみます。

    const secondMetaData = await metadataHttp.getAccountMetadataByKeyAndSender(metadataAccount.address, metadataKey.toHex(), metadataAccount.publicKey).toPromise();
    console.log(secondMetaData.metadataEntry.value);

結果

Da

何コレ?な感じになりました。

で、見出しの通りですが、上記で secondMetadataValue を与えているところ、更新後の値そのものではなくて 現在の値とのXOR(排他論理和) を与える必要があります。

つまり、メタデータを更新する際は

  1. 現在のメタデータを取得
  2. 現在のメタデータと新しいメタデータの文字列を UTF-8 としてバイト列に変換
  3. 2つのバイト列の XOR を取る
  4. 長さの差分と、3. で計算したバイト列を UTF-8 として文字列に変換したものをトランザクションの引数として渡す

という手順を踏む必要があります。

ややこしいですね?
でも大丈夫です、TypeScript の SDK には上記処理ラップした MetadataTransactionService というものがあります。メタデータを更新する際はこれを利用するのが手っ取り早そうです。

    const accountMetadataTransactionUpdate = await metadataTransactionService.createMetadataTransaction(
        Deadline.create(),
        networkType,
        MetadataType.Account,
        metadataAccount.publicAccount,
        metadataKey,
        secondMetadataValue,
        metadataAccount.publicAccount
    ).toPromise();

内部で現在のメタデータの値を取得する処理(APIリクエスト)が走るので、createMetadataTransaction の戻り値は Observable になります。
このトランザクションを送信して先ほどと同じようにメタデータの値を取り直すと、

結果

second metadata

更新できました。
MetadataTransactionService も内部的には上記の 1-4 の処理が走ってることになりますので、知っておくとメタデータ関連の動作で期待した結果が得られなかった時に役に立つやもしれません。

メタデータトランザクションを単体で使おうとしてエラー -> アグリゲートトランザクションでラップしないとダメだった

上記の例でさらっと AccountMetadataTransactionAggregateTransaction でラップして送信していますが、メタデータ関連のトランザクションはこんな漢字でアグリゲートトランザクションでラップして送信しないと弾かれます。

サーバ側のログで関連しそうなものとして、

rejected transaction with type: Account_Metadata (top level not supported)

みたいなものも出力されています。
サンプルコードもアグリゲートトランザクション使う形になってますので、それに従う形にしましょう。

トランザクションを大量に発行したつもりなのに数個しか成功していない -> 同じパラメータのトランザクションだったので、バイナリレベルで同じトランザクションになっていてハッシュ値も同じになってた

アカウントのトランザクション取得、ページング用の引数 QueryParams があるのですが、このパラメータが正しく動くことを確認するために大量のトランザクションを同時に発行しようとしたときのこと。

こんなコードを書いてしまいました。

    let transactions: Transaction[] = []
    for (let i = 0; i < 10; i++) {
        const transaction = TransferTransaction.create(
            Deadline.create(),
            receiver.address,
            [NetworkCurrencyMosaic.createRelative(0)],
            PlainMessage.create(''),
            networkType
        );
        transactions.push(transaction)
    }

    for (let transaction of transactions) {
        const signedTransaction = account.sign(transaction, generationHash);
        const result = await transactionHttp.announce(signedTransaction).toPromise();
    }

実際にはもう少し複雑だったんですが簡単に書くと上記のような感じで、
トランザクションを最初にたくさん作り、それを後に一つずつ署名してアナウンスする、という流れでした。

これで発行したトランザクションが承認されるのを待ってトランザクションを取得したところ、10個トランザクションを作成したはずが 3つしかトランザクションが取得できませんでした。

これ、どうやら近い時間にトランザクションを作成しすぎたせいで、Deadline.create() で作成したデッドラインが同じ値になり、結果的にバイナリレベルで同じトランザクションを複数回発行してしまったためにどこかで弾かれてしまったようです。

現状、トランザクションのバイナリで時間に関係する要素はデッドラインしか無いので、デッドラインが同じ、他のパラメータ、署名者も同じになるとバイナリレベルで同じトランザクションになってしまいます。

さらに、トランザクションのハッシュも

  • トランザクションのバイナリ
  • 署名
  • ジェネレーションハッシュ
  • 署名者の公開鍵

を使って計算しているので、これらが一致するとトランザクションのハッシュも同じになります。そのため、何かエラーが発生したのか確認したくても TransactionHttp#getTransactionStatus でトランザクションのステータスを調べることもできません。(ハッシュ値が一致する成功したトランザクションのステータスが取れてしまう)

そうそうこんなコードを書くこと無いとは思いますが、getTransactionStatus で成功が返ってきているように見えたので、何が起きたのか把握するのに大分時間がかかりました・・・

その他 (小ネタ)

  • Failure** 系のエラーが出力されて何だコレってなった時は dev center の下記ページ見ましょう。
    https://nemtech.github.io/ja/api.html#status-errors
    エラーの説明が書いてあるので何を間違えたのか調べる足がかりになります。
    最近のバージョンであまり見なくなったような気がしますが、エラーコードとして大きな負の値が出てきた時も 16進数変換してこのページを見れば何のエラーかわかります。

  • いろいろ無料なネットワークの作り方
    ネームスペースやらモザイクやらを作っていろいろ遊んだり、テストしたりするのに使える各種手数料無料のネットワークの作り方を書こうとおもっていたのですが、既に taberu-salad さんが記事あげてくれていました。
    プライベートチェーン用にカスタマイズしたfushicho-2を動かす
    私もほぼほぼ同じ設定で fushicho-3 で動かしているので、参考になるかと思います。

  • モザイクIDとネームスペースIDの見分け方
    モザイクIDとネームスペースID はどちらも 64bit の整数ですが、ネームスペースIDの最上位ビットは 1、モザイクIDの最上位ビットは 0 となって値が被らないようになっています。
    RESTを直接叩いたときなど、IDを2進数か16進数で表示したらどちらのIDか判別できます。

さいごに

NEM1の時も SDK 作ってたんですが、それと比べると NEM2 の SDK は規模が大きくなったなぁと感じます。
それだけ使えるトランザクションの種類や API が増えたということなのでしょう。
いろいろ遊べるパーツが増えたので一度触ってみてくだされ。

18
4
0

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
18
4