LoginSignup
23
2

More than 1 year has passed since last update.

Symbolアドレスの謎を紐解く with catbuffer

Last updated at Posted at 2021-12-01

突然ですが、SymbolのSDKやREST APIを使っていて

「なんでアカウントアドレスは16進数の形やNから始まるplainアドレスがあるんだろう…ややこしくて使いにくいな…」

と思ったことはありませんか?

もし答えがYesならあなたはこの記事を読むに相応しいです、読み終えた頃には気分がスッキリしてSymbolLifeがより楽しくなるでしょう。Noなら…必要はないかもしれませんが、読んでいただけたら喜びます。

筆者は2021/12/1現在、C#のSymbolCatbufferをplanetさんと一緒に開発しています。そこで得た知見を元に本記事を作成していきます。ちなみに当初、筆者は冒頭の質問に対して同じ疑問を持っていました。

catbufferって一体なんだ?

まず、catbufferとはなんぞや?という点を説明させてください。おそらく名前はcatapult(カタパルト)のcatを受け継ぎそこにBufferを足したものだと思われます。

現在、C++,Javascript(Typescript),Python,JAVA用のgeneratorとschemaが存在します。

generatorにschemaを流し込むとその言語のcatbufferが生成されるイメージです。が、この変は意味不明な場合は気にしなくていいです。

例えばSDKを使っていたらトランザクションを作成して、それをネットワークにアナウンスしますよね?
一番良く使われる(だろう)例としてトランスファートランザクションの例をあげると、この時何をアナウンスするかというと署名されたトランザクションです。

const tx = symbol.TransferTransaction.create(
  symbol.Deadline.create(EPOCH_ADJUSTMENT),
  recieveAccount.address, 
  [new symbol.Mosaic (new symbol.MosaicId('3A8416DB2D53B6C8'),symbol.UInt64.fromUint(1000000))],
  symbol.PlainMessage.create('Hello Symbol from NEM!'),
  symbol.NetworkType.TEST_NET,
  symbol.UInt64.fromUint(100000)
);
const signedTx = signAccount.sign(tx, GENERATION_HASH);

new symbol.TransactionHttp(NODE)
    .announce(signedTx)
    .subscribe((x) => console.log(x), (err) => console.error(err));

こんな感じかと。

で、それをサーバーが受け取ってゴニョゴニョするわけですが僕はcatapult-restやcatapult-clientについては知見がないのでそこはスルーします。もしご興味があればオープンソースなので頑張れば分かるはずですのでどなたかぜひ記事にしていただければ読み漁ります。

ちなみに上記トランザクションの詳細はこちらです。

で、ここでのsignedTxにはpayloadという変数があります。

C700000000000000A33DB9F460BB992A3C928ECBCE17BC71485B8C7F5E683C053E153493E930DA3CA6808ABA0A17090AA81A077787FF95A10700F6B8BC06689D74B7DF39C880190FB055C6F655CD3101A04567F9499F24BE7AB970C879887BD3C6644AB7CAA22D220000000001985441A086010000000000F38C98320000000098DF73288794C96F89A388150A171E68D0503F84F0CBAF4E1700010000000000C8B6532DDB16843A40420F00000000000048656C6C6F2053796D626F6C2066726F6D204E454D21

こんなやつ。

この16進数の羅列にすべてが詰まっていると言っても過言ではありません。おそらくサーバー側はこれらの羅列を受け取りごにょごにょしてブロックチェーンに刻んでいるのだと思われます。つまりアナウンスを行うアプリケーション側ではこれら羅列を作成する必要があり、当然REST APIから受け取ったこれらをアプリケーション側で使うにはクラス等に戻す必要があるわけです。

実はSDKにはcatbufferがimportされているためその箇所はcatbufferにまかせています。つまり各言語でSymbolを扱うにはSDKに内包されているcatbufferが必要不可欠なんです。

もしお時間あればSDKを辿っていけば最終的にはだいたいcatbufferにたどり着きます。

さて、ここまででcatbufferの存在をなんとなくでも理解いただけたら幸いです。

順番に紐解いていきましょう

ここからは上記の16進数の羅列を紐解いていきましょう。とは言え全部を紐解くと、めちゃくちゃ大変なので冒頭の話にあったアドレスに特化して進めていきます。(そうするために無理やり冒頭の質問を置いた感もある)

※なお、実際はhexをbyte配列に変えてさらにstreamに…みたいなことが行われていますがそのあたり端折ります。

まず、トランスファートランザクションのpayloadを作成するにはcatbufferのTransferTransactionBuilderを利用します。(ここから突然C#に変わります。詳しくは説明しませんがなんとなく理解いただけるかと…

public class TransferTransactionBuilder: TransactionBuilder {

  /* Transfer transaction body. */
  public TransferTransactionBodyBuilder transferTransactionBody;
  /*
  * Constructor - Creates an object from stream.
  *
  * @param stream Byte stream to use to serialize the object.
  */
  internal TransferTransactionBuilder(BinaryReader stream)
      : base(stream)
  {
      try {
          transferTransactionBody = TransferTransactionBodyBuilder.LoadFromBinary(stream);
      } catch (Exception e) {
          throw new Exception(e.ToString());
      }
  }

TransferTransactionBuilderはTransactionBuilderを継承したものですが親クラスはアドレスに関係ないのでスルーします。子クラスではtransferTransactionBodyという変数を持ちコンストラクタではTransferTransactionBodyBuilder.LoadFromBinary()を用いてインスタンスを作成します。

public class TransferTransactionBodyBuilder: ISerializer {

  /* Recipient address. */
  public UnresolvedAddressDto recipientAddress;
  /* Reserved padding to align mosaics on 8-byte boundary. */
  public int transferTransactionBody_Reserved1;
  /* Reserved padding to align mosaics on 8-byte boundary. */
  public byte transferTransactionBody_Reserved2;
  /* Attached mosaics. */
  public List<UnresolvedMosaicBuilder> mosaics;
  /* Attached message. */
  public byte[] message;
  /*
  * Constructor - Creates an object from stream.
  *
  * @param stream Byte stream to use to serialize the object.
  */
  internal TransferTransactionBodyBuilder(BinaryReader stream)
  {
      try {
          recipientAddress = UnresolvedAddressDto.LoadFromBinary(stream);

続いてTransferTransactionBodyBuilderです。ここで出ました。recipientAddressという変数を見ていただけるかと思います。そしてその変数の構造体はUnresolvedAddressDtoでコンストラクタではUnresolvedAddressDto.LoadFromBinary()でインスタンスを作成しています。

引き続きUnresolvedAddressDtoを見てみましょう

public struct UnresolvedAddressDto
    {
        /* Unresolved address. */
        private readonly byte[] unresolvedAddress;

ついに来ましたbyte配列になっていますね。最終的にはブロックチェーンに関する全てはbyte配列やint等16進数で表せるものになります。これ以上深くは潜れません。

さて、addressのことがなんとなく分かったと思うので、最後にpayloadを元にインスタンスを作成し、出来上がったTransferTransactionBuilderからaddress箇所を抜き出してみましょう。

const string hex = "C700000000000000A33DB9F460BB992A3C928ECBCE17BC71485B8C7F5E683C053E153493E930DA3CA6808ABA0A17090AA81A077787FF95A10700F6B8BC06689D74B7DF39C880190FB055C6F655CD3101A04567F9499F24BE7AB970C879887BD3C6644AB7CAA22D220000000001985441A086010000000000F38C98320000000098DF73288794C96F89A388150A171E68D0503F84F0CBAF4E1700010000000000C8B6532DDB16843A40420F00000000000048656C6C6F2053796D626F6C2066726F6D204E454D21";
var barr = GeneratorUtils.HexToBytes(hex); // 16進数をbyte配列に
var ms = new MemoryStream(barr);
var br = new BinaryReader(ms);
// インスタンス作成
var transferTransactionBuilder = TransferTransactionBuilder.LoadFromBinary(br);
// ここでbyte配列で取り出す
var recipientAddress = transferTransactionBuilder.transferTransactionBody.recipientAddress.GetUnresolvedAddress();
// byte配列を16進数の形に変換
Console.WriteLine(GeneratorUtils.ToHex(recipientAddress));

さて、それではコンソールに表示されるのは…

98DF73288794C96F89A388150A171E68D0503F84F0CBAF4E

じゃん!
これが先ほどsignedTx.payloadに含まれている受取人アドレスの箇所です。なんとなーく見覚えありませんか?そうです。

APIで受け取れるrecipientAddressの値です。

ここまで読んでいただいた方なら理解いただけると思いますが他にもsignedTx.payloadにはモザイクの種類や量、signerの情報などこのトランザクションの全てが詰まっています。

そして、僕たちがよく見ているNから始まるsymbolアドレスというのは、この16進数を元に生成された分かりやすいアドレスの形であって、こんぴゅーた的には16進数のほうを使用しているわけですね!
※これは逆。plainなアドレスが先。そらそう。プレーンだもん。詳細は少し下で

しかし、ここで疑問があります。なんでアドレスには16進数だけを使わずにN(orT)から始まるplainアドレスがあるんでしょう。結局、そこが分からないので冒頭の疑問は完全に解決していないような…誰か知ってる方いたら教えて下さい。

※追記
目指せ北海道さんのSymbol解体新書P40によると

  1. 公開鍵を 256bit の SHA3 にて変換する
  2. さらに、160-bit RIPEMED_160 方式によってハッシュ化する(2 重ハッシュ)
  3. ネットワークバージョン(1 バイト)を RIPMED_160 ハッシュの前に付け加える
  4. 結果を 256-bit SHA3 によってハッシュ化し、最初の 3 バイトをチェックサムとして取り出 す
  5. ステップ 3 とステップ 4 によって得られたチェックサムを結合する
  6. 得られた結果を Base32 によってエンコードする

つまり、公開鍵からplainなアドレスを生成しそれを16進数にしているわけですね。しかしこの解体新書。かなり骨太なのでいつか理解できるよう少しずつ読んでいきます。

最後に

つい数ヶ月前まで僕はHTMLとCSSとJavascript(といってもレガシーなのがちょっとだけ)が分かる程度でした。が、Symbolを色々触るようになり、自分自身レベルアップを実感しています。沢山の記事を書き残していただいた先人の方々。twitterやその他サービス等で困った時に助けてくれる方々、ちょっとしたことでもいいねしてくれたりリツイートして応援してくれる方々。何より発端である声掛けをしていただき、困ったときに助けていただけるplanetさんには本当に感謝しています。

catbufferもまだ完成していませんし、この後は Symbol for Unity を作って個人ゲーム開発者さんのもっと身近にSymbolが存在する状況を作りたいと思っています(実現するかは未定

本記事を読んで頂きありがとうございました。

23
2
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
2