Current section

Files

Jump to
forge lib forge server.ex
Raw

lib/forge/server.ex

defmodule Forge.Server do
@moduledoc """
Server to handle abci messages
"""
use GenServer
require Logger
alias Forge.{
Account,
AccountState,
Context,
Crypto,
Mpt,
TransactionMessage,
TransferMessage,
Tx
}
defmodule State do
@moduledoc false
alias Forge.{Account, Context}
@typedoc """
The state of the server. It contains keys as folows:
* `:trie`: the MPT reference.
* `:app_info`: application name and version tuple.
* `:handler`: then module to handle `Forge.Handler` behavior.
* `:context`: see `Forge.Context`.
"""
@type t :: %__MODULE__{
trie: Account.t(),
app_info: tuple(),
handler: module(),
context: Context.t()
}
defstruct [:trie, :app_info, :handler, :context]
end
@msg_handler_prefix "handle_"
@account_handler_prefix "account_"
@chain_handler_prefix "chain_"
def start_link(handler) do
GenServer.start_link(__MODULE__, handler, name: __MODULE__)
end
def get_app_state do
GenServer.call(__MODULE__, :get_app_state)
end
def unquote(:"$handle_undefined_function")(func, args) do
func_name = Atom.to_string(func)
cond do
String.starts_with?(func_name, @msg_handler_prefix) ->
[request] = args
GenServer.call(__MODULE__, {func, request})
String.starts_with?(func_name, @account_handler_prefix) ->
[addr] = args
GenServer.call(__MODULE__, {func, addr})
String.starts_with?(func_name, @chain_handler_prefix) ->
GenServer.call(__MODULE__, {func, args})
true ->
nil
end
end
# Callbacks
def init(handler) do
dbpath = Application.get_env(:forge, :db, "./states.db")
app_info = Application.get_env(:forge, :app_info, {"Forge", "0.0.1"})
{:ok, %State{trie: Mpt.open(dbpath), app_info: app_info, handler: handler}}
end
def handle_call(:get_app_state, _from, %{trie: trie} = state) do
{:reply, trie, state}
end
def handle_call({:account_balance, address}, _from, %{trie: trie} = state) do
ret =
case Account.get(trie, address) do
nil -> 0
%{balance: balance} -> balance
end
{:reply, ret, state}
end
def handle_call({:account_nonce, address}, _from, %{trie: trie} = state) do
ret =
case Account.get(trie, address) do
nil -> 0
%{nonce: nonce} -> nonce
end
{:reply, ret, state}
end
def handle_call({:account_info, address}, _from, %{trie: trie} = state) do
{:reply, Account.get(trie, address), state}
end
def handle_call({:chain_info, []}, _from, %{trie: trie} = state) do
{:reply, Mpt.get_info(trie), state}
end
def handle_call({:handle_info, request}, _from, %{trie: trie, app_info: app_info} = state) do
{app_name, app_version} = app_info
last_block = Mpt.get_last_block(trie)
app_hash = Mpt.get_app_hash(trie)
Logger.info(
"Tendermint version: #{request.version}, last_block: #{last_block}, app_hash: #{
Base.encode16(app_hash)
}"
)
response = %Abci.ResponseInfo{
data: app_name,
version: app_version,
last_block_height: last_block,
last_block_app_hash: app_hash
}
{:reply, response, state}
end
def handle_call({:handle_begin_block, request}, _from, state) do
%Abci.RequestBeginBlock{
hash: _hash,
header: %Abci.Header{
app_hash: app_hash,
consensus_hash: consensus_hash,
data_hash: data_hash,
evidence_hash: _evidence_hash,
height: height,
last_block_id: _last_block,
last_commit_hash: _last_commit_hash,
last_results_hash: _last_result_hash,
next_validators_hash: _next_validators_hash,
num_txs: num_txs,
proposer_address: proposer_address,
time: timestamp,
total_txs: total_txs,
validators_hash: _validators_hash
},
last_commit_info: _last_commit_info
} = request
Logger.debug(fn ->
"Begin block: hash #{Base.encode16(app_hash)}, consensus_hash: #{
Base.encode16(consensus_hash)
}, data_hash: #{Base.encode16(data_hash)}, height: #{height}, num_txs: #{num_txs}, total_txs: #{
total_txs
}, proposer: #{Base.encode64(proposer_address)}, time: #{
DateTime.from_unix!(timestamp.seconds)
}"
end)
response = %Abci.ResponseBeginBlock{tags: []}
{:reply, response, %{state | context: %Context{block_time: timestamp, block_height: height}}}
end
def handle_call({:handle_check_tx, request}, _from, %{trie: trie, handler: handler} = state) do
%Abci.RequestCheckTx{tx: data} = request
data = decode_tx_data(data)
tx = TransactionMessage.decode(data)
Logger.debug(fn -> "Check tx: #{inspect(tx)}" end)
response = struct(Abci.ResponseCheckTx, verify(tx, trie, handler))
{:reply, response, state}
end
def handle_call(
{:handle_deliver_tx, request},
_from,
%{trie: trie, handler: handler, context: context} = state
) do
%Abci.RequestDeliverTx{tx: data} = request
hash = Crypto.get_tx_hash(data)
data = decode_tx_data(data)
tx = TransactionMessage.decode(data)
Logger.debug(fn -> "Deliver tx: #{inspect(tx)}, hash #{hash}" end)
response = struct(Abci.ResponseDeliverTx, verify(tx, trie, handler))
context = struct(context, tx_hash: hash, address: tx.from)
trie =
case response.code == 0 do
true -> update_state(tx, trie, context, handler)
_ -> trie
end
{:reply, response, %{state | trie: trie}}
end
def handle_call({:handle_end_block, request}, _from, state) do
height = request.height
Logger.debug(fn -> "End block: #{height}" end)
response = %Abci.ResponseEndBlock{
validator_updates: [],
tags: []
}
{:reply, response, state}
end
def handle_call({:handle_commit, request}, _from, %{trie: trie, context: context} = state) do
Logger.debug(fn -> "Commit block #{context.block_height}: #{inspect(request)}" end)
Mpt.update_block(trie, context.block_height)
response = %Abci.ResponseCommit{
data: Mpt.get_app_hash(trie)
}
{:reply, response, state}
end
def handle_call({type, request}, _from, state) do
# forward to default message handler
response = apply(ExAbci.Server, type, [request])
{:reply, response, state}
end
# private functions
defp verify(tx, trie, handler) do
result =
case Tx.verify(tx) do
:ok -> verify_trie(tx, trie, handler)
_ -> false
end
case result do
false ->
%{
code: 500,
data: <<>>,
info: "tx not signed correctly or not match with current state",
log: "failed to verify tx",
gas_wanted: 0,
gas_used: 0,
tags: []
}
_ ->
%{
code: 0,
data: <<>>,
info: <<>>,
log: <<>>,
gas_wanted: 0,
gas_used: 0,
tags: []
}
end
end
defp verify_trie(%TransactionMessage{value: {:transfer, transfer}} = tx, trie, _handler) do
case Account.get(trie, tx.from) do
%AccountState{nonce: nonce, balance: balance} ->
# we would only allow transfer only if target user exists.
# Application need to consider a proper way to make that happen.
verify_nonce(tx, nonce) and tx.from !== transfer.to and balance >= transfer.total and
Account.get(trie, transfer.to) != nil
_ ->
false
end
end
defp verify_trie(%TransactionMessage{value: {:any, msg}} = tx, trie, handler) do
case Account.get(trie, tx.from) do
%AccountState{nonce: nonce} = sender_state ->
verify_nonce(tx, nonce) and handler.verify(msg, trie, sender_state)
_ ->
handler.verify(msg, trie, nil)
end
end
defp verify_nonce(tx, nonce), do: tx.nonce === nonce
defp update_state(%TransactionMessage{value: {:transfer, transfer}} = tx, trie, context, _) do
%TransferMessage{to: to, total: total} = transfer
# since the tx is verified at this moment acc1 must exist
acc1 = Account.get(trie, tx.from)
acc1_rebirth = Account.renaissance(acc1, acc1.balance - total, nil)
trie = Account.put(trie, tx.from, acc1_rebirth, context)
acc2 = Account.get(trie, to)
acc2_rebirth = Account.renaissance(acc2, acc2.balance + total, nil, false)
Account.put(trie, to, acc2_rebirth, context)
end
defp update_state(%TransactionMessage{value: {:any, any}} = tx, trie, context, handler) do
# for unknown transaction, we leave application to handle it - application shall use Account.genesis / Account.renaissance / Account.put to update the state of related account
handler.update_state(tx.from, any, trie, context)
end
defp decode_tx_data(data) do
Base.decode64!(data)
rescue
_ -> data
end
end