Forge In Action

著者: 孙博山(ArcBlock ソフトウェアエンジニア)
校正: 傅禹翰(ArcBlock インターン)
Forge とは?
Ruby on Rails が Web アプリケーションを構築するためのフレームワークであるのと同様に、Forge はブロックチェーン dApps を構築するためのフレームワークです。ブロックチェーンは、公開検証が可能な分散型データベースと簡単に理解できます。
従来のアプリケーションはデータをデータベースに保存しますが、分散型アプリケーションである dApp はデータをブロックチェーンに保存します。
dApp の構築は従来のアプリケーションよりもはるかに複雑です。P2P、コンセンサスアルゴリズム、ネットワークプロトコルなど、一連の基盤アーキテクチャを先に整えてから、ビジネス要件を実現するユーザーロジックを記述する必要があります。ブロックチェーンベースの dApp を構築するためのフレームワークである Forge は、この作業の大部分をすでに行い、アプリケーションが呼び出すための一連のインターフェースを提供しています。そのため、アプリケーション開発者は自身のビジネスロジックだけに集中すればよく、Forge がデータをブロックチェーンに保存してアプリケーションから利用できるようにします。
ブロックチェーンとは?
Forge にはブロックチェーンに由来する概念がいくつかありますが、多くの開発者はブロックチェーンにあまり詳しくありません。ここでは、これからの開発を理解する助けとなるよう、最も基本的な概念を簡単に紹介します。
ブロックチェーンは、ブロックで構成されたチェーンであり、実際には一種のデータ構造です。その形は Linked List(連結リスト)に少し似ています。連結リストには 1、2、3 のような単純なデータを保存できますが、ブロックチェーンに保存されるデータは何でしょうか?答えは Transaction です。
Transaction とは?
transaction(トランザクション、略称 tx)は、各ブロックに保存されるデータです。
ブロックはブロックヘッダーと内容から構成されます。ヘッダーにはブロック高や直前のブロックのハッシュなどの情報が保存され、内容には一つひとつの tx が格納されます。なぜブロック内のデータを transaction(取引)と呼ぶのでしょうか?世界初のブロックチェーンプロジェクトであるビットコインでは、各ブロックに一件ずつのビットコイン取引記録が保存されていたため、その後のさまざまなブロックチェーンプロジェクトも、ブロックチェーン内のデータを取引、すなわち transaction と呼ぶようになりました。
Forge の概念
有用なアプリケーションを作る際には、通常ユーザーが関わり、ユーザーは何らかのアセットを作成し、それらを取引するなどの操作を行います。Forge はこれらの操作を二つの基本概念に抽象化しています。
- account アカウント
- asset アセット
Account
Account は従来のアプリケーションにおけるアカウントの概念です。ただし、従来のアプリケーションではユーザーアカウントをユーザー名とパスワードで作成するのに対し、ブロックチェーンの世界ではチェーン上のアドレスと秘密鍵によって作成します。
なぜユーザー名とパスワードでユーザーアカウントを作らないのでしょうか?ブロックチェーンの世界には、実際にはユーザーログインという概念がないからです。従来のアプリケーションでは、ユーザーはログインに成功すると送金や Weibo への投稿などの操作を行えます。ではビットコインでは、ユーザー同士がアカウントにログインせず、どのように送金するのでしょうか?答えはデジタル署名です。送金トランザクションをビットコインウォレットの秘密鍵で署名してブロックチェーンに送信し、その後、他者がその署名済みトランザクションを検証すれば有効となり、一件の送金トランザクションが完了します。したがって、ウォレットという概念もビットコインによって導入されました。
Asset
Asset(アセット)はあらゆるものを表せます。記事、画像、地図、証明書などです。アセットは特定のユーザーまたはアプリケーションによって作成でき、一度作成されると、取引や利用などの操作に使えます。具体的に何を行うかはアプリケーション次第です。
Forge の Transaction
前述のとおり、ビットコインに存在する唯一の Transaction は送金です。一方、フル機能のフレームワークである Forge は、アカウント作成、アセット作成、送金、交換など十数種類の Transaction を標準でサポートしています。各イベントの発生は、一つひとつの Transaction としてチェーン上に公開されます。
つまり、開発者がブロックチェーン上で開発を行うということは、突き詰めれば Forge を通じて一つひとつの Transaction をブロックチェーンに公開することです。
Forge を起動すると、独立したオペレーティングシステムのプロセスになります。では、開発者が作成したアプリケーションは、どの Transaction を発行すべきかを Forge にどう伝えるのでしょうか?Forge は GraphQL と gRPC の二つの方法を提供しています。
Forge とやり取りするには?
Forge 自体は、二つのやり取りの方法を提供しています。
- GraphQL
- gRPC
これは、普段サーバーが提供する API を呼び出す方法とは少し異なるかもしれません。私たちが日常的に利用する API 呼び出しの多くは、JSON を使って HTTP リクエストを API に送り、リソースを取得します。なぜ Forge は JSON API を使わないのでしょうか?
理由は単純で、効率です。GraphQL と gRPC の利点についてここでは詳しく説明しませんが、この二つの技術を簡単に紹介します。
GraphQL の使い方
GraphQL は Facebook がオープンソース化した技術で、ユーザーがサーバーからより効率的かつ迅速にリソースを取得できるようにすることを目的としています。
GraphQL はネットワークのアプリケーション層で HTTP/1.1 または HTTP/2 プロトコルの POST リクエストを使用します。サーバーはクライアントから送られた Query リクエストを受信し、処理後に JSON の結果を返します。
クライアントが送信できるリクエストは三種類に分かれます。
- Query:リソースの読み取りに使用
- Mutation:リソースの作成や変更に使用
- Subscription:イベントの購読に使用
Forge では、Query は通常チェーン上のデータの照会、Mutation は通常チェーンへの Transaction の送信、Subscription はチェーン上で発生するイベントの購読に使います。
gRPC の使い方
gRPC は Google が開発した RPC フレームワークです。簡単に言えば、次のとおりです。
gRPC = protobuf + HTTP/2Protocol Buffer(略称 Protobuf)も、Google が開発したシリアライズ/デシリアライズ規格です。XML や JSON より効率的なシリアライズ方式です。あらかじめ .proto ファイルを定義し、転送する情報にどのフィールドがあり、それぞれが何番であるかを記録します。その後、シリアライズ時にはフィールドの値だけをエンコードすることで、容量を節約します。使い方は次のとおりです。
- ユーザーは転送する情報のフィールドを定義して
.protoファイルに記述し、公式またはコミュニティが提供する使用言語向けのプラグインで、それを.cpp、.ex、.pyなどのファイルにコンパイルします。 - プログラム内で、生成されたモジュールが提供するシリアライズ関数を使い、データオブジェクトをネットワーク転送用のバイナリに変換します。受信側はデシリアライズ関数を使い、受信したバイナリをデータオブジェクトに戻します。
Protobuf によるデータのシリアライズは容量を大幅に節約できます。ネットワーク上で転送するデータが減るため、リクエストはより効率的になります。ただし、その代わりに次のものが必要です。
- まず、サーバー側が定義した
.protoファイル - 使用する言語に対応する
protoc(公式提供の protobuf コンパイラー)のプラグイン
Forge が使用するすべての proto ファイルは ArcBlock/forge-abi リポジトリにあります。Google は C++、C#、Go、Python のプラグインを公式にサポートしており、それ以外の言語についてはコミュニティから探す必要があります。
では、gRPC とは何でしょうか?図を見ながら説明します。
- まずサーバー側が一連のリクエスト/レスポンス用
.protoファイルを定義します - クライアントは送信するリクエストを protobuf でバイナリにシリアライズし、HTTP/2 プロトコルを通じてサーバーへ送信します
- サーバーはリクエストを受信して処理し、protobuf でシリアライズしたバイナリのレスポンスを返します。クライアントはレスポンスを受信後、デシリアライズして結果を取得します
HTTP/1.1 ではなく HTTP/2 プロトコルを使用するのは、データをより効率的に転送するためです。また、gRPC を利用するには公式またはコミュニティが提供する gRPC ライブラリが必要です。
GraphQL と gRPC、どちらを使うべきか?
Forge は GraphQL と gRPC という二つのやり取りの方法を提供しています。では、どちらを使うべきでしょうか?
GraphQL は導入が簡単で、HTTP クライアントと JSON ソースだけでデータを送受信できます。一方、gRPC は導入が複雑で、protobuf を理解し、gRPC ライブラリを使ってデータを送受信する必要があります。
私たちは gRPC を推奨します。導入は少し難しく見えますが、より柔軟に使用できるからです。一方、GraphQL は導入が簡単で、単純な照会により適しています。
Forge で transaction を送信するには?
前述のとおり、開発者がブロックチェーン上で開発を行うということは、突き詰めれば Forge を通じて一つひとつの transaction をブロックチェーンに公開することです。また、Forge は GraphQL と gRPC によるやり取りを提供しています。次に、Forge で gRPC を使って transaction を送信する方法を説明します。
送信の流れは簡単でしょう!Forge で定義された transaction を gRPC で Forge に送るだけで、その後 Forge が結果としてハッシュを返します。
それでは、Forge で定義されている transaction がどのようなものか見てみましょう。
Forge における transaction の定義は、arcblock/forge-abi/lib/protobuf/type.proto にあります。
message Transaction {
string from = 1; # 这个tx是谁发的,即钱包地址
uint64 nonce = 2; # nonce 用来防止重敌攻击,每次需要递增发送
string chain_id = 3; # tx发送至的链的id
bytes pk = 4; # 发tx的钱包的公钥
bytes signature = 13; # 发tx的钱包的签名
repeated multisig signatures = 14; # 多方签名
google.protobuf.Any itx = 15; # inner transaction ,这个tx具体是干啥的
}私たちが行うべきことは、この transaction を構築して Forge に送信することです。次に、チェーン上にウォレットアカウントを作成する具体例を紹介します。
Forge のウォレット
ウォレットの作成は 2 段階です。
- ローカルでウォレットを作成する
- このウォレットをチェーン上で宣言(declare)し、ユーザーアカウントの作成を完了する
ここまで説明してきましたが、そもそもウォレットとは何でしょうか?
ウォレットは実際には、公開鍵、秘密鍵、アドレスを保存するデータ構造であり、protobuf で定義されています。
message WalletInfo {
bytes sk = 2; # 私钥
bytes pk = 3; # 公钥
string address = 4; # DID地址
}私たちのウォレットは DID 仕様をサポートしており、3 つの選択項目があります。
- role type:ロール
- key type:秘密鍵アルゴリズム
- hash type:ハッシュアルゴリズム
message WalletType{
KeyType key = 1;
HashType hash = 2;
EncodingType address = 3;
RoleType role = 4;
}詳細については、arcblock/abt-did-spec にある DID 作成に関するドキュメントを参照してください。
以下のサンプルコードは Elixir で記述されており、オープンソース化済みの Forge-elixir-sdk ライブラリを使用しています。
wallet_type = ForgeAbi.WalletType.new(role: :role_account, key: :ed25519, hash: :sha3)
wallet = ForgeSdk.Wallet.util.create(wallet_type)
%ForgeABi.WalletInfo{
address: "z1mwolwq...." # DID地址,里面包含了私钥类型,哈希算法及角色
pk: <<85,199, ...>> # 公钥,32字节
sk: <<19,21,248,...>> # 私钥,我们用的ed25519,私钥地址包括了公钥,共64字节。
}これで、ウォレットをローカルに作成できました。さらにチェーン上で宣言する必要があります。
先ほど述べたように、チェーン上で何かを行うには transaction を送信する必要があります。
message Transaction{
string from = 1;
uint64 nonce = 2;
string chain_id = 3;
bytes pk = 4 ;
bytes signature = 13;
repeated Mulitisig signatures = 14;
google.protobuf.Any itx = 15
}まだ signature、signatures、itx が未設定です。signatures はマルチシグであり、この段階では使わないため、気にする必要はありません。署名を見る前に、まず itx を見てみましょう。
Forge の itx とは?
itx は inner transaction の略称です。すでに tx があるのに、なぜ itx も必要なのでしょうか?
例えるなら、手紙を書くようなものです。どの手紙にも題名、宛名、本文、日付、署名などがありますが、手紙ごとに本文の内容は異なります。
tx は差出人、題名、署名などを含む手紙のテンプレートであり、itx は具体的な内容を表す手紙の本文です。Forge は十数種類の tx をサポートしており、つまり十数種類の itx があります。
作成したばかりのウォレットをチェーン上で宣言するための itx は declare と呼ばれます。
message DeclareTx{
string moniker = 1 ; #表示这个钱包账户的别名
....
}ここでは、使用しない他のフィールドを省略しています。では、この declare tx をどのように itx として作成するのでしょうか?transaction で定義されている itx の型をもう一度見てみましょう。
google.protobuf.Any itx = 15;
型は google.protobuf.Any です。これは Google が提供する型で、名前のとおり任意の型を扱うための汎用型です。定義は次のとおりです。
message Any{
string type_url = 1;
bytes value = 2;
}任意の型なら value だけで表せばよいのではないでしょうか?type_url は何のためにあるのでしょうか?これはアプリケーションに対し、この任意型が実際に何の型なのかを伝えるものです。Google は本来、この type_url を URL として設計しましたが、ここでは URL である必要はなく、任意の文字列にできます。
Forge で定義される type_url は次のような形式です。
fg:t:declare # forge缩写:type:itx类型declare = ForgeAbi.DeclareTx.new(moniker: "jonsnow")
value = ForgeAbi.DeclareTx.encede(declare)
itx = Google.Proto.Any.new(type_url: "fg:t:declare", value: value)
%Google.Proto.Any{type_url: "fg:t:declare", value: "\n\ajonsnow"} # 这个就是用 protobuf 编码的 declare itxでは、もう一度 tx を見てみましょう。
message Transaction {
string from = 1; # wallet.address
uint64 nonce = 2; # 1
string chain_id =3; # forge
bytes pk = 4; # wallet.pk
bytes signature = 13;
repeated Multisig signatures = 14;
google.protobuf.Any itx = 15;
}残る最後のステップは署名です。
Forge で tx に署名するには?
Forge のウォレットは、ed25519 と secp256k1 という二つの楕円曲線デジタル署名アルゴリズムをサポートしています。デジタル署名とは、ウォレットの秘密鍵で tx のハッシュに署名し、その後、他者が公開鍵を使って検証できるようにするものです。
signature = sign(data, sk)
# data 为 tx 序列化后的二进制哈希
# sk 这里是钱包的私钥hash = mcrypto.hash(%Sha3{}, ForgeAbi.Transaction.encode(tx))
sig = Mcrypto.sign!(%Ed25519{}, hash, wallet.sk)
tx = %{tx | signature: sig}これで、ついに tx の構築と署名が完了しました!
あとは、この tx を Forge に送信するだけです!
Forge に tx を送信するには?
gRPC を使って Forge とやり取りするため、gRPC が提供する tx 送信用サービスを使うだけです。このサービスは Forge では send_tx と呼ばれ、arcblock/forge-abi/lib/protobuf/service.proto で定義されています。
この操作を行うには、使用する言語の gRPC ライブラリのドキュメントを参照する必要があります。Elixir では次のようにします。
Forgesdk.send_tx(tx: tx)
"48c265bb...."返されたハッシュが、この tx のチェーン上のハッシュです。このハッシュを使って、チェーン上で状態を照会できます。tx を Forge に送信すると、Forge は tx を送信したウォレットアドレスが有効か、署名が有効かなど、一連のチェックを行います。その後、Forge はこの tx を下位のコンセンサスエンジンに送り、P2P ネットワークへブロードキャストします。最終的に新しいブロックにパッケージされ、送信した tx がチェーン上に記録されます。もちろん、チェーンに記録されたからといって tx が成功したとは限りません。tx の状態も確認する必要があります。
Forge でよく使われる tx
ここまで、declare tx を構築して署名し、Forge へ正常に送信する方法を学びました。これにより、Forge 上にウォレットアカウントを作成できました。次に、Forge でよく使われる tx を見てみましょう。
次のようなシナリオを想定します。
ユーザー A がアカウントを作成し、チェックインして token を獲得した後、アセット(ゲームマップ)を作成します。 そして、このアセットを別のユーザー B に無償で譲渡します。その後、ユーザー A がいくらかの token を使ってユーザー B からこのアセットを購入し、一回の交換を完了します。
declare はすでに見たので、次に poke を見てみましょう。
poke tx
poke は「つつく」という意味で、チェックインして 25 token を受け取るために使います。受け取れるのは一日一回だけです。
tx を送信するとき、tx の構造は常に同じで、異なるのは itx の内容と署名だけです。tx の構造をもう一度見てみましょう。
message Transaction{
string from = 1; # wallet.address
uint64 nonce = 2; # 0 <- 注意对于poke来说nonce要用0
string chain_id = 3; # Forge
bytes pk = 4; # wallet.pk
bytes signature = 13;
repeated Multisig signatures = 14;
google.protobuf.Any itx = 15; # itx <- 改用poke tx
}poke tx の定義は次のとおりです。
message PokeTx {
string data = 1; # 签到的日期,用当天
string address = 2; # 向哪个钱包地址签到,这个是固定的地址,“zzzzz..”(36 个 z)
}poke = ForgeAbi.PokeTx.new(data:"2019-05-28", address:"zzzzzzz...")
value = ForgeAbi.PokeTx.encode(poke)
itx = Google.proto.Any.new(type_url: "fg:t:poke", value: value)
%Google.Proto.Any{type_url: "fg:t:poke", value: <<10,10,50,...>>}次に、この itx を上の tx に入れて署名し、チェーンへ送信しましょう!
ForgeSdk.send_tx(tx: tx)
"66313AFB...."成功後にチェーン上で確認すると、jonsnow アカウントに 25 token が追加されています!
これでウォレットが作成され、25 token を保有することになりました。次にアセットを作成する方法を見てみましょう。
create_asset tx
asset はアセットを表し、取引可能なあらゆるものを表現できます。ここではゲームマップを例にします。まず create_asset の定義を見てみましょう。
message CreateAssetTx{
string moniker = 1; # 这个资产的别名
google.protobuf.Any data= 2;
bool readonly = 3;
bool transferable = 4; # 是否可转让
uint32 ttl = 5;
string parent = 6;
string address = 7; # 资产地址
}ここでは 7 つのフィールドが定義されていますが、そのうち 4 つだけに注目し、残りは気にしなくてかまいません。
map = %Google.Protobuf.Any{value: "this is my map"}
asset = ForgeAbi.CreateAssetTx.new(transferable: true, moniker: "map1", data: map)次に、asset のアドレスがまだ空なので、自分で計算する必要があります。
Forge におけるすべての ID は DID 標準をサポートしており、asset のアドレスも DID です。では、asset のアドレスはどのように計算するのでしょうか?
hash = Mcrypto.hash(%SHA3{}, ForgeAbi.createAssetTx.encode(itx)) # 之后的步骤请参考abt-did-spec文档中的步骤,这里算出的哈希作为第5步的输入。并且在选role-type时要选asset。アドレスを計算したら、上の asset に設定します。
value = ForgeAbi.CreateAssetTx.encode(asset)
itx = Google.Proto.Any.new(type_url: "fg:t:create-asset", value: value)
%Google.Proto.Any{type_url: "fg:t:create_asset", value:<<10.4.109....>>}以降は流れ作業です。itx を tx に入れ、署名し、送信が成功すれば asset の作成は完了です!その中には "this is my map" という内容が保存されています。
では次に、このアセットを別のアカウントへ移転します。ここでは transfer tx を使います。
transfer tx
transfer(譲渡)はユーザーが一方的に行う操作です。ユーザーはユーザー B にお金やアセットを送れます。そのため、まず二つ目のウォレットを作成する必要があります。
wallet_type = ForgeAbi.WalletType.new(role: :role_account, key: :ed25519, hash: :sha3)
wallet2 = ForgeSdk.Wallet.Util.create(wallet_type)その後、declare tx を使ってチェーン上で宣言します。ここでは詳しい手順を繰り返しません。
次に transfer tx の定義を見てみましょう。
message TransferTx {
string to = 1; # 目标钱包地址
BigUint value = 2; # 给多少钱
repeated string assets = 3; # 有哪些资产
}ここでは、先ほど作成したマップアセットを一つだけ譲渡するため、asset のアドレスだけが必要です。
map1 = "ejdqnc..."
transfer = ForgeAbi.TransferTx.new(to: wallet2.address, assets: [map1])
value = ForgeAbi.TransferTx.encode(transfer)
itx = Google.Proto.Any.new(type_url: "fg:t:transfer", value: value)
%Googel.Proto.Any{type_url: "fg:t:transfer", value:<<10,35,122,...>>}その後はいつもの手順で、itx を tx に入れ、署名してチェーンへ送信します。成功すると、もともとユーザー A が所有していたアセットはユーザー B のものになります!
最後に exchange tx を見てみましょう。
exchange tx
これまで説明したすべての tx は署名が一つだけ必要でしたが、exchange tx には二つの署名が必要です。アセットを交換するには、交換する双方の同意が必要だからです。
exchange tx の定義を見てみましょう。
message Exchange {
string to = 1; # 与哪个地址交换
ExchangeInfo sender = 2; # 发送人信息
Exchangeinfo receiver = 3; # 接受人信息
}
message Exchangeinfo {
BigUint value = 1; # 交换的金额
repeated string asets = 2; # 交换的资产
}
message BigUint{
bytes value = 1; # 因为金额是大整数,所以我们用bytes来表示
}itx を構築します。
exchange = ForgeAbi.ExchangeTx.new(
to: wallet2.address,
sender: ForgeAbi.Exchangeinfo.new(value: ForgeAbi.token.to.uint(2)),
receiver: ForgeAbi.ExchangeInfo.new(assets: [map1]))
value = ForgeAbi.ExchangeTx.encode(exchange)
itx = Google.Proto.Any.new(type_url: "fg:t:exchange", value: value)次はいつもの手順で、itx を tx に入れて署名します。ここまでで tx に残っている最後のステップは、これまで使っていなかった Multisig(マルチシグ)です。
message Transaction{
string from = 1; # walle.address
uint64 nonce = 2; # 1
string chain_id = 3; # Forge
bytes pk = 4; # wallet.pk
bytes signature = 13; # signature
repeated Multisig signatures = 14;
google.protobuf.Any itx = 15; # itx
}multisig の定義を見てみましょう。
message Multisig{
string signer = 1; # 用户B的地址
bytes pk = 2; # 用户B的公钥
bytes signature = 3; # 用户B的签名
}この multisig はどのように構築するのでしょうか?とても簡単です。ユーザー B のアドレスと公開鍵を設定して tx に入れ、その後ユーザー B が署名するだけです!
mulitisig = ForgeAbi.Multisig.new(signer: wallet2.address, pk: wallet2.pk) # 创建一个mulitisig的map
tx = %{tx | signstures: [multisig]} # 将其放入tx的signatures字段中,注意现在这个mulitisig的签名还是空哦
signature = Forgesdk.Wallet.Util.sign!(wallet2, ForgeAbi.Transaction.encode(tx)) # 将这个tx让用户B签名
multisig = %{multisig | signature: signature} # 签好之后把签名设入multisig的map中
tx = %{tx | signatures: [multisig]} # 最后将签名的multisig放入tx中これで、tx はユーザー A とユーザー B の両方によって署名され、チェーンへ送信できるようになりました!
成功すると、アセットは A の名義に移り、A は B に 2 token を支払い、交換が完了します!
全体の流れを図に示します。









