Forge
Forge is a simple elixir blockchain framework to build new chains easily. Built on top of tendermint and ex_abci, Forge implemented the core part of the state db and exposes interfaces to maintain the states so that developers could build their own blockchain app easily.
In a very high level, a typical blockchain app consists of:
- networking: for secure p2p connectivity and data replication. Handled by Tendermint.
- mempool: for broadcasting tx. Handled by Tendermint.
- consensus: for agreeing on most recent block. Handled by Tendermint.
- storage: account states. Handled by Forge.
- VM: for executing turning-complete contracts. TBD.
- app logic: e.g. permissions. Should be handled by application.
- RPC: for providing functionalities for user to interact with the chain. Partly handled by Forge, the rest shall be handled by application.
Forge provides these functionalities:
- An ethereum compatible wallet system. Meaning your existing account, your private key could be used in Forge-enabled application.
- An ethereum-like state db. Forge stores your application state into leveldb (rocksdb in future). All your application states are maintained in a merkle patricia tree(MPT). Every transaction will lead to the change of MPT, which will generate a new root_hash, and every time a block is committed, the current root_hash will be delivered to tendermint as the app_hash for that block.
- Transaction / Account state definition. The transaction and the account state are defined with protobuf, you can find the definition in lib/protos/forge.proto and rebuild it with
make rebuild-proto
. These data structures are designed to be extensible so you can leverage and extend them in your application. Tendermint messaage handling. All the following messages are handled:
info
: you can define your application name and version in config. Otherwise Forge will return a default one. See: config/config.exs.begin_block
: block context are stored in the gen server state.check_tx
: after verified transaction signature, application provided callback functionverify
will be called to make sure the tx is valid.deliver_tx
: besides the function ofcheck_tx
, the application provided callback functionupdate_state
will be called to persist the update to the states db after this transaction.end_block
: do nothing.commit_block
: update the current block height to the states db. Return the current root_hash to tendermint as app_hash.
- Transaction sign and verification. The cryptography part of the transaction sign / verification are handled by Forge. Developer just need to verify whether the tx is valid against the application state.
Installation
The package can be installed by adding :forge to your list of dependencies in mix.exs:
def deps do
[
{:forge, "~> 0.4.0"}
]
end
the docs can be found at https://hexdocs.pm/forge.
Usage
Config
After adding Forge into your dependency, you can put these config to define the db and app info:
config :forge, db: "./states.db"
config :forge, :app_info, {"Final Chapter", "0.1.0"}
Supervision Tree
You shall add ExAbci.Listener
and Forge.Server
into your application supervision tree.
Forge.Server
needs a module which implements Forge.Handler
callback:
def start(_type, _args) do
children = [
ExAbci.Listener.child_spec(Forge.Server),
{Forge.Server, YourAwesomeChain.Handler}
]
opts = [strategy: :one_for_one, name: Forge.Supervisor]
Supervisor.start_link(children, opts)
end
Forge.Handler callback
You need to implement two callback functions provided by Forge.Handler
:
@callback verify(TransactionMessage.t(), Account.t(), AccountState.t()) :: true | false
@callback update_state(String.t(), Any.t(), Account.t(), map()) :: Account.t()
Forge will call your verify
callback upon check_tx
/ deliver_tx
with the transaction, the MPT, and the sender’s account state. Once the transaction satisfied the state, you shall return true
to Forge.
When a tx is being calculated on deliver_tx
, Forge will call your update_state
callback with the transaction, the sender address, custom data inside the transaction, MPT and the current context (e.g. block height, tx hash). Once the transaction is processed, the update MPT reference shall be returned.
Extending the transaction
The transaction is defined in Forge, like this:
message TransactionMessage {
bytes from = 1;
uint64 nonce = 2;
bytes public_key = 3;
bytes signature = 4;
oneof value {
TransferMessage transfer = 10;
google.protobuf.Any any = 50;
}
}
message TransferMessage {
bytes to = 1;
uint64 total = 2;
}
Forge can process the most basic transfer transactions without application handle anything. But if application want a much wider behavior in the chain, e.g. account holder can publish a post into the chain, then you should define your own transaction message inside the Forge transaction. For example, you can define a CreatePostMessage like this:
syntax = "proto3";
package Example;
message CreatePostMessage {
string title = 1;
string content = 2;
}
after building this with grpc-elixir, you can encode and sign your transaction with Forge.CustomTransaction.create
:
type_url = "example.com/CreatePostMessage";
type_mod = Example.CreatePostMessage; # this module is generated by elixir grpc tool.
attrs = %{title: "hello", content: "world"}
CustomTransaction.create(sender_address, nonce, type_url, type_mod, attrs, public_key, private_key)
Extending the account state
The basic account state is defined in Forge, like this:
message AccountState {
uint64 balance = 1;
uint64 nonce = 2;
uint64 num_txs = 3;
bytes address = 4;
bytes owner = 5;
// 5-9 reserve for future
bytes genesis_tx = 10;
bytes renaissance_tx = 11;
google.protobuf.Timestamp genesis_time = 12;
google.protobuf.Timestamp renaissance_time = 13;
// 14-49 reserve for future
google.protobuf.Any data = 50;
}
nonce
, num_txs
, address
, genesis_tx
, renaissance_tx
, tenesis_time
and renaissance_time
will be updated by Forge, you don’t need to worried about them. balance
will be maintained by the handling of TransferMessage
, but your custom transaction message can manipulate that as well. You could extend data
in your way, for example, you want to keep a list of posts for the user, and each post has its own account:
message UserAccountState {
string nickname = 1;
repeated string posts = 2;
}
message PostAccountState {
bytes title = 1;
bytes content = 2;
}
After compiling the proto definition, you can encode them and use Account.renaissance
to update it.