跳到主要內容

Forge In Action

孙博山(ArcBlock 软件工程师)
ArcBlockBlockchainForgeForge Framework

作者: 孫博山(ArcBlock 軟體工程師)

校對: 傅禹翰(ArcBlock 實習生)

什麼是 Forge?

猶如 Ruby on Rails 是一套建構 Web 應用程式的框架,Forge 是一套建構區塊鏈 dApps 的框架。區塊鏈可以簡單地理解成資料庫,公開可驗證的去中心化資料庫。

一個傳統的應用把資料儲存在資料庫裡,一個 dApp 去中心化應用把資料放在區塊鏈之中。

建構一個 dApp 比建構傳統應用程式複雜許多,必須先搭好 P2P、共識演算法、網路協議等一系列底層架構,然後才能撰寫使用者邏輯來實現業務需求。Forge 是一套建構區塊鏈 dApp 的框架,已經完成大量工作,並提供一套介面供應用程式呼叫。因此,應用程式開發者只需關心自己的業務邏輯,Forge 會將資料儲存在區塊鏈中,供應用程式使用。

區塊鏈是什麼?

Forge 中有一些概念是源於區塊鏈的,而很多開發者對於區塊鏈並不是很熟悉,這裡簡單介紹一下一些最基本的概念,以助於之後開發的理解,

區塊鏈就是一條由區塊組成的鏈,它其實是一種資料結構,長得有點像 Linked List 連結串列。用連結串列可以儲存 1、2、3 等簡單資料,那區塊鏈中儲存的是什麼呢?答案是 Transaction。

Transaction 是什麼?

transaction 交易,簡稱為 tx,是儲存在每個區塊中的資料。

一個區塊由區塊頭和內容組成,頭中儲存了塊高、上個區塊的雜湊等資訊,而內容則是一個個的 tx。為什麼區塊中的資料叫做 transaction 交易呢?因為世界上第一個區塊鏈專案比特幣中,每一區塊中存的就是一筆筆比特幣交易記錄,所以後續的各種區塊鏈專案都用交易即 transaction 來作為區塊鏈中的資料。

Forge 中的概念

當我們要做一個有用的應用程式時,通常會涉及到使用者,使用者會建立一些資產,並且將這些資產進行交易等行為。Forge 將這些行為抽象為兩個基本的概念:

  • account 帳號
  • asset 資產

Account

Account 就是傳統應用中帳戶的概念,只不過在傳統應用中,一個使用者帳號是用使用者名稱和密碼來建立的;而在區塊鏈的世界中,使用者帳號是由鏈包地址和私鑰來建立的。

為什麼不用使用者名稱/密碼來建立使用者帳戶呢? 因為在區塊鏈的世界中,其實是沒有一個使用者登入的概念的。我們知道,在傳統的應用中,使用者登入成功後可以進行一些操作。比如說轉帳、發微博等。在比特幣中,使用者之間是如何在不登入帳戶的情況下進行轉帳的呢?答案是透過數位簽章,即將轉帳交易用位元錢包的私鑰進行簽名後發到區塊鏈上;之後這個簽名的交易經由別人驗證後就算是有效的了,這樣一筆轉帳的交易就算是完成了。所以錢包的概念也是比特幣引進的。

Asset

Asset 資產則用來表示任何東西,可以是一篇文章、一張圖片、一張地圖或是一個證書。資產可以由某個使用者建立,或者應用程式來建立,一旦建立後,可以用來進行交易、使用等行為。具體是做什麼取決於應用程式。

Forge 中的 Transaction

前面說到,比特幣中有且僅有的一種 Transaction 就是轉帳,Forge 作為一個全功能框架,原生支援十幾種 Transaction,包括建立帳號、建立資產、轉帳、交換等。每一次事件的發生,都等價成一個個的 Transaction 發布到鏈上。

所以說若開發者想在區塊鏈上做開發,歸根結底就是透過 Forge 在區塊鏈上發布一個個 Transaction。

我們知道,當 Forge 啟動之後,便是一個單獨的作業系統程式,開發者開發的應用程式如何與 Forge 互動來告知其應當發什麼 Transaction 呢? 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/2

Protocol Buffer 簡稱 Protobuf,也是 Google 自家出的一種序列化/反序列化標準。是比 XML、JSON 更加高效的序列化方式。它是透過預先定義好一個 .proto 檔案,記錄了要傳輸的資訊都有哪些欄位以及它們的編號,之後序列化的時候只對欄位的值進行編碼,以達到節省空間的目的,使用方法如下:

  1. 使用者定義要傳輸的資訊有哪些欄位,寫到一個 .proto 檔案中,然後用官方或社群提供的你要用的語言的外掛將其編譯成 .cpp 或 .ex 或 .py 檔案中。
  2. 在你的程式中,用剛才生成出來的模組提供的序列化函式,將一個資料物件轉化成二進位制以便在網路中進行傳輸,接受方用反序列化函式得到的二進位制轉化回資料物件。

用 Protobuf 進行的對資料的序列化能很大程度上節省空間,這樣傳輸在網路上的資料變少了,請求就更高效了。但是需要付出的代價就是:

  1. 首先要有服務端定義的 .proto 檔案
  2. 你要用的語言要有 protoc(官方提供的 protobuf 的編譯器)的外掛。

Forge 所有用到的 proto 檔案都在 ArcBlock/forge-abi 倉庫中;Google 官方支援 C++、C#、Go、Python 的外掛,其他的語言要到社群中去找了。

那麼,gRPC 是啥呢?看圖說話:

  • 首先伺服器端定義好一套請求/響應的.proto 檔案
  • 客戶端把要發的請求透過 protobuf 序列化成二進位制後,透過 HTTP/2 協議發給伺服器
  • 伺服器收到請求,處理之,然後再以 protobuf 序列化的二進位制發回響應——客戶端收到響應後,反序列化拿到結果

之所以用 HTTP/2 協議而不再用 HTTP/1.1 是為了能夠更高效地傳輸資料。同時,需要用一個官方提供的或是社群提供的 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 步,

  1. 在本地建立一個錢包
  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 未填, signaures 是多方簽名,我們這一步還用不到,不用管它,在看簽名之前我們先來看一下 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....>>}

接下來的步驟就是流水線作業,將:tx 塞入 tx 中,簽名,傳送成功後,一個 asset 就建立好了!裡面的內容放的就是"this is my map"。

OK, 接下來我們要把該資產轉移給另一個帳戶,這會用到 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 兩個 token,交換成功!

整個流程的圖示:

本頁涉及

產品

  • Forge sunset

    一套用來建造鏈上應用的 SDK 和工具鏈,不必分別拼裝節點、資料層和客戶端。它後來成了 ArcBlock Chain。

術語

  • 區塊鏈

    透過密碼學連接記錄,並按共同的驗證與共識規則確定交易歷史的分散式帳本。