Help us understand the problem. What is going on with this article?

protobufによるフレキシブルなコントラクトデータの管理

More than 1 year has passed since last update.

protobufによるフレキシブルなコントラクトデータの管理

by umegaya
1 / 29

今日はdappの運用にprotobufを使うこと、のお話なんですがその前に


動機:ゲーム向けのサーバーフレームワークとしてのEthereum


前提

  • コントラクトのコードはimmutable

前提

  • コントラクトのコードはimmutable
  • コントラクトのデータ構造はコードでしか定義できない

前提

  • コントラクトのコードはimmutable
  • コントラクトのデータ構造はコードでしか定義できない

=> コントラクトのデータ構造はimmutable


懸念:最初のコントラクトに無いデータが必要になったらどうすんの?


データのマイグレーション


メインネット上のゲームのコードを読んでみた


丸コピー

  • ロジックとデータを同じコントラクトに置くと否応無しにこうなる
  • データを新しいコントラクトにコピーするタイミングでカラムを変更する
    • FooServiceに対して新しいデータ構造を持つFooServiceV2を作る
    • FooServiceからFooServiceV2にgas limitの範囲内で少しずつデータをコピーするFooServiceMigraterを作成し実行する

ストレージコントラクトとその拡張

  • ロジックとデータを扱うコントラクトを分離する
    • データだけを保持するFooServiceStorageをつくりFooServiceからそれを使う
  • 追加データはFooServiceStorageExtraを作りそこにおく
    • FooServiceV2はFooServiceStorageとFooServiceStorageExtra両方を見るようにする
  • Extra2, Extra3, ... (ToT)

データのマイグレーション:そんなものはなかった


protobufならまともなマイグレーションができるのでは?


protobuf

  • googleが開発したIDL
    • 複数のアプリで共有するデータ構造をmessageと呼び、以下のような感じで定義する
    • =の後の番号が重要
message Foo {
  uint64 id = 1;
  Nested nested = 2;

  message Nested {
    string text = 1;
    bytes data = 2;
  }
}

protobuf

  • 定義されたmessageをコンパイルして各種プログラム言語向けのコードができる
  • バイナリにシリアライズすることで効率的にデータの移動が行える
    • このフォーマットがナイス
p := pb.Foo{
  Id: 1234,
  Nested: *pb.Foo_Nested{
    Text: "hoge",
    Data: []byte{2, 3, 5},
  },
}
out, err := proto.Marshal(p) //out is []byte
p := &pb.Foo{}
if err := proto.Unmarshal(in, p); err != nil { //in is []byte
        log.Fatalln("Failed to parse Foo:", err)
}

protobuf(重要)

  • データに割り当てた番号を再利用しない
    • 例えばuint64 id = 1;の1は、他のデータの番号としては二度と割当てないようにする
  • その条件下で一旦シリアライズされたデータはどのバージョンのコードとも読み込みの互換性があることが保証される
message Foo {
  uint64 id = 1; //他のデータで1を使わない
  Nested nested = 2;

  message Nested {
    string text = 1;
    bytes data = 2;
  }
}

protobuf

  • 例) message Foofloat f = 3;を追加した場合
  • 古いコードで新しいデータを読む
    • fに設定されたデータは単に無視される
  • 新しいコードで古いデータを読む
    • fは自明なデフォルト値(0.0f)で初期化されている
message Foo {
  uint64 id = 1; 
  Nested nested = 2;
  float f = 3;

  message Nested {
    string text = 1;
    bytes data = 2;
  }
}

まともなマイグレーション

  • データスキーマをprotobufで定義する。contractにはbytesとして保存
    • versionというカラムを各スキーマに持たせておくのがミソ
message Record {
  uint32 version = 1;
  uint32 id = 2;
  //実際のデータカラムたちが続く
}

まともなマイグレーション

  • FooServiceは各データスキーマの現バージョンを持っておく
    • スキーマのバージョンを上げる時にはFooServiceの新しいバージョンをデプロイ
  • public mapping(uint => bytes) entriesとかを持つFooServiceStorageをつくりFooServiceからそれを使うようにしておく
contract FooService {
  using pb_Record for pb_Record.Data;
  uint const CURRENT_RECORD_VERSION = 1;
  FooServiceStorage storage;
  ...
}

まともなマイグレーション

  • データを新しく作るとき、versionカラムには今のバージョンをセットしてFooServiceStorageに保存する
function newRecord() public returns (uint) {
  pb_Record.Data memory data;
  r.id = newId();
  r.version = CURRENT_RECORD_VERSION;
  storage.entries[r.id] = r.encode();
}

まともなマイグレーション

  • データを読み出すとき、versionカラムと現在のFooServiceが持っているバージョンが一致していなければ、その差分について必要な初期化を行う
  • まともにマイグレーションできそうな感じがしてくる
function getRecord(uint id) internal returns (pb_Record.Data r) {
  bytes bs = storage.entries[id];
  r.decode(bs); //バージョンがどう変わっていても問題が生じない
  if (r.version != CURRENT_RECORD_VERSION) {
    //ここで新しいレコードに値をセットしたりする
    r.version = CURRENT_RECORD_VERSION; //最新版になった
    storage.entries[r.id] = r.encode(); //結果を再度保存
  }
}

さらなるメリット

  • コントラクトからstructを返すのと同等なことがポータブルに実現できる
    • protobufでserializeされたbytesを返す関数を作り、それを受け側でdeserializeする
    • ABIEncoderV2不要&すでに他の実用的な大部分のプログラム言語で可能
    • ABIEncoderV2がprotobufレベルでサポートされるのは時間がかかるだろう(現状はethers.jsぐらい?)
  • ABIEncoderV2に対してかなりの間優位性を保てるのでは

g


問題は


solidityでprotobufって使えるの?

g


solidityでprotobufって使えるの?

  • protobuf verion 2用のコンパイラがあった
    • しかしprotobufの現バージョンは3
    • メンテナンスされてない上にhaskellで書かれている
  • 2を使う場合、他の部分で苦労が予想される
    • 3年半ほど新しいリリースはされてない
    • protobuf3推奨のものも多い(grpcなど)

作りました


pb3sol

  • https://github.com/umegaya/pb3sol
    • pythonでのprotobuf3コンパイラ実装 +
    • addressなど、solidityのnative typeを利用可
    • jsで上記のnative typeを使いやすくするnpmを用意(soltype-pb)
  • よろしければ試してフィードバックください

Thank you!!

umegaya
programmer who wants to rest in peace
https://github.com/umegaya
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした