The commands are all executed by the fendermint
binary, which is produced from the fendermint_app
crate,
so we have many ways to run the program:
fendermint <args>
, after runningcargo install --path fendermint/app
./target/release/fendermint <args>
, after runningcargo build --release
cargo run -p fendermint_app --release -- <args>
The same is also available for step-by-step execution in the Milestone-1 demo.
TIP: If something goes wrong with the RPC commands, try to run them with
fendermint --log-level debug rpc ...
to see the JSON-RPC requests and responses.
The first step we need to do is create a Genesis file which we'll pass to Tendermint, which will pass it to Fendermint through ABCI. This Genesis file will be in JSON format, as that is the convention with Tendermint, but we could also encode it in CBOR if we wanted.
Ostensibly the Genesis content could be coming from the parent chain itself, so the child subnet participants don't have to go through the manual steps below, but we still have these options to start a standalone network.
In the following sections we will create a Genesis file for a network named test
. Start by creating a directory to hold the files:
mkdir test-network
First, create a new genesis.json
file devoid of accounts and validators. The --base-fee
here is completely arbitrary.
cargo run -p fendermint_app -- genesis --genesis-file test-network/genesis.json new --chain-name test --base-fee 1000 --timestamp 1680101412
We can check what the contents look like:
$ cat test-network/genesis.json
{
"timestamp": 1680101412,
"chain_name": "test",
"network_version": 18,
"base_fee": "1000",
"validators": [],
"accounts": []
}
Next, let's create some cryptographic key pairs we want want to use either for accounts or validators at Genesis.
mkdir test-network/keys
for NAME in alice bob charlie dave; do
cargo run -p fendermint_app -- key gen --out-dir test-network/keys --name $NAME;
done
Check that the keys have been created:
$ ls test-network/keys
alice.pk alice.sk bob.pk bob.sk charlie.pk charlie.sk dave.pk dave.sk
$ cat test-network/keys/alice.pk
Ak5Juk793ZAg/7Ojj4bzOmIFGpwLhET1vg2ROihUJFkq
Add one of the keys we created to the Genesis file as a stand-alone account:
cargo run -p fendermint_app -- \
genesis --genesis-file test-network/genesis.json \
add-account --public-key test-network/keys/alice.pk --balance 10
Check that the balance is correct:
$ cat test-network/genesis.json | jq .accounts
[
{
"meta": {
"Account": {
"owner": "f1jqqlnr5b56rnmc34ywp7p7i2lg37ty23s2bmg4y"
}
},
"balance": "10000000000000000000"
}
]
The owner
we see is an f1
type address with the hash of the public key. Technically it's an Address
type,
but it has to be one based on a public key, otherwise we would not be able to validate signatures later.
Let's add an example of the other possible account type, a multi-sig account:
cargo run -p fendermint_app -- \
genesis --genesis-file test-network/genesis.json \
add-multisig --public-key test-network/keys/bob.pk --public-key test-network/keys/charlie.pk --public-key test-network/keys/dave.pk \
--threshold 2 --vesting-start 0 --vesting-duration 1000000 --balance 30
Check that all three of the signatories have been added:
$ cat test-network/genesis.json | jq .accounts[1]
{
"meta": {
"Multisig": {
"signers": [
"f1kgtzp5nuob3gdccagivcgns7e25be2c2rqozilq",
"f1bvdmcbct6vwoh3rvkhoth2fe66p6prpbsziqbfi",
"f1hgeqjtadqmyabmy2kij2smn5jiiud75kva6bzny"
],
"threshold": 2,
"vesting_duration": 1000000,
"vesting_start": 0
}
},
"balance": "30000000000000000000"
}
Finally, let's add one validator to the Genesis, with a monopoly on voting power, so we can run a standalone node:
cargo run -p fendermint_app -- \
genesis --genesis-file test-network/genesis.json \
add-validator --public-key test-network/keys/bob.pk --power 1;
The value of power doens't matter in this case, as bob
is our only validator. Check the result:
$ cat test-network/genesis.json | jq .validators
[
{
"public_key": "BCImfwVC/LeFJN9bB612aCtjbCYWuilf2SorSUXez/QEy8cVKNuvTU/EOTibo3hIyOQslvSouzIpR24h1kkqCSI=",
"power": 1
},
]
The public key was spliced in as it was, in base64 format, which is how it would appear in Tendermint's
own genesis file format. Note that here we don't have the option to use Address
, because we have to return
these as actual PublicKey
types to Tendermint through ABCI, not as a hash of a key.
First, follow the instructions in getting started with CometBFT to install the binary,
then initialize a genesis file for CometBFT at ~/.cometbft
.
rm -rf ~/.cometbft
cometbft init
The logs show that it created keys and a genesis file:
I[2023-03-29|09:58:06.324] Found private validator module=main keyFile=/home/aakoshh/.cometbft/config/priv_validator_key.json stateFile=/home/aakoshh/.cometbft/data/priv_validator_state.json
I[2023-03-29|09:58:06.324] Found node key module=main path=/home/aakoshh/.cometbft/config/node_key.json
I[2023-03-29|09:58:06.324] Found genesis file module=main path=/home/aakoshh/.cometbft/config/genesis.json
We don't want to use the random values created by CometBFT; instead we need to use some CLI commands to convert the artifacts file we created earlier to the format CometBFT accepts. Start with the genesis file:
mv ~/.cometbft/config/genesis.json ~/.cometbft/config/genesis.json.orig
cargo run -p fendermint_app -- \
genesis --genesis-file test-network/genesis.json \
into-tendermint --out ~/.cometbft/config/genesis.json
Check the contents of the created Comet BFT Genesis file:
~/.cometbft/config/genesis.json
$ cat ~/.cometbft/config/genesis.json
{
"genesis_time": "2023-03-29T14:50:12Z",
"chain_id": "test",
"initial_height": "0",
"consensus_params": {
"block": {
"max_bytes": "22020096",
"max_gas": "-1",
"time_iota_ms": "1000"
},
"evidence": {
"max_age_num_blocks": "100000",
"max_age_duration": "172800000000000",
"max_bytes": "1048576"
},
"validator": {
"pub_key_types": [
"secp256k1"
]
}
},
"validators": [],
"app_hash": "",
"app_state": {
"accounts": [
{
"balance": "10000000000000000000",
"meta": {
"Account": {
"owner": "f1jqqlnr5b56rnmc34ywp7p7i2lg37ty23s2bmg4y"
}
}
},
{
"balance": "30000000000000000000",
"meta": {
"Multisig": {
"signers": [
"f1kgtzp5nuob3gdccagivcgns7e25be2c2rqozilq",
"f1bvdmcbct6vwoh3rvkhoth2fe66p6prpbsziqbfi",
"f1hgeqjtadqmyabmy2kij2smn5jiiud75kva6bzny"
],
"threshold": 2,
"vesting_duration": 1000000,
"vesting_start": 0
}
}
}
],
"base_fee": "1000",
"chain_name": "test",
"network_version": 18,
"timestamp": 1680101412,
"validators": [
{
"power": 1,
"public_key": "BCImfwVC/LeFJN9bB612aCtjbCYWuilf2SorSUXez/QEy8cVKNuvTU/EOTibo3hIyOQslvSouzIpR24h1kkqCSI="
}
]
}
}
We can see that our original genesis.json
has been made part of CometBFT's version under app_state
,
and that the top level validators
are empty, to be filled out by the application during the init_chain
ABCI call.
By default CometBFT uses Ed25519 validator keys, but in theory it can use anything that looks like a key.
We can run the following command to replace the default priv_validator_key.json
file with one based on
one of the validators we created.
mv ~/.cometbft/config/priv_validator_key.json ~/.cometbft/config/priv_validator_key.json.orig
cargo run -p fendermint_app -- \
key into-tendermint --secret-key test-network/keys/bob.sk --out ~/.cometbft/config/priv_validator_key.json
See if it looks reasonable:
~/.cometbft/config/priv_validator_key.json
$ cat ~/.cometbft/config/priv_validator_key.json
{
"address": "66FA0CFB373BD737DBFC7CE70BEF994DD42A3812",
"priv_key": {
"type": "tendermint/PrivKeySecp256k1",
"value": "04Gsfaw4RHZ5hTbXO/3hz2N567tz5E1yxChM1ZrEi1E="
},
"pub_key": {
"type": "tendermint/PubKeySecp256k1",
"value": "AiImfwVC/LeFJN9bB612aCtjbCYWuilf2SorSUXez/QE"
}
}
$ cat test-network/keys/bob.pk
AiImfwVC/LeFJN9bB612aCtjbCYWuilf2SorSUXez/QE
The Fendermint Application and CometBFT will run as separate processes.
Now we are ready to start our Fendermint Application, which will listen to requests coming from Tendermint through the ABCI interface.
First, let's make sure we have put all the necessary files in an easy to remember place under ~/.fendermint
.
mkdir -p ~/.fendermint/data
cp -r ./fendermint/app/config ~/.fendermint/config
We will need the actor bundle to load. We can configure its location via environment variables, but the default
configuration will look for it at ~/.fendermint/bundle.car
, so we might as well put it there.
make actor-bundle
cp ../builtin-actors/output/bundle.car ~/.fendermint/bundle.car
Now, start the application.
cargo run -p fendermint_app --release -- run
It is important to use the --release
option, otherwise it will take too long to load the Wasm actor modules and
CometBFT will issue a timeout (by default we have 3 seconds to execute requests).
With the default --log-level info
we can see that the application is listening:
2023-03-29T09:17:28.548878Z INFO fendermint::cmd: reading configuration path="/home/aakoshh/.fendermint/config"
2023-03-29T09:17:28.549700Z INFO fendermint::cmd::run: opening database path="/home/aakoshh/.fendermint/data/rocksdb"
2023-03-29T09:17:28.879916Z INFO tower_abci::server: starting ABCI server addr="127.0.0.1:26658"
2023-03-29T09:17:28.880023Z INFO tower_abci::server: bound tcp listener local_addr=127.0.0.1:26658
If we need to restart the application from scratch, we can do so by erasing all RocksDB state:
rm -rf ~/.fendermint/data/rocksdb
CometBFT can be configured via ~/.cometbft/config/config.toml
; see the default settings here.
Now we are ready to start CometBFT and let it connect to the Fendermint Application.
cometbft start
If we need to restart the application from scratch, we can erase all CometBFT state like so:
cometbft unsafe-reset-all
If all goes well, we will see block created in the Fendermint Application log as well the CometBFT log:
Application log
$ rm -rf ~/.fendermint/data/rocksdb && cargo run -p fendermint_app --release -- --log-level debug run
...
2023-05-19T09:13:45.400896Z DEBUG tower_abci::v037::server: new request request=Info(Info { version: "0.37.1", block_version: 11, p2p_version: 8, abci_version: "1.0.0" })
...
2023-05-19T09:13:45.401018Z DEBUG tower_abci::v037::server: flushing response response=Ok(Info(Info { data: "fendermint", version: "0.1.0", app_version: 0, last_block_height: block::Height(0), last_block_app_hash: AppHash(0171A0E402203AAAC8F10B0E837FDF2546C98BF164972B07B49196E25322711E3C4807CF8AD8) }))
2023-05-19T09:13:45.401262Z DEBUG tower_abci::v037::server: new request request=InitChain(...)
...
2023-05-19T09:13:54.062109Z DEBUG tower_abci::v037::server: new request request=PrepareProposal(...)
...
2023-05-19T09:13:54.083246Z DEBUG tower_abci::v037::server: new request request=ProcessProposal(ProcessProposal { ..., height: block::Height(3), ... })
...
2023-05-19T09:13:54.105797Z DEBUG fendermint_app::app: begin block height=3
2023-05-19T09:13:54.105922Z DEBUG fendermint_app::app: initialized exec state
...
2023-05-19T09:13:54.110007Z DEBUG fendermint_app::app: commit state state_root="bafy2bzacebh4fbl6rv7tlxxf2zsxqifjr424tkykwmgffqaho6mvr6hy7dq42" timestamp=1684487633
CometBFT log
$ cometbft unsafe-reset-all && cometbft start
...
I[2023-05-19|10:13:45.449] Completed ABCI Handshake - CometBFT and App are synced module=consensus appHeight=0 appHash=0171A0E402203AAAC8F10B0E837FDF2546C98BF164972B07B49196E25322711E3C4807CF8AD8
I[2023-05-19|10:13:45.449] Version info module=main tendermint_version=0.37.1 abci=1.0.0 block=11 p2p=8 commit_hash=2af25aea6
I[2023-05-19|10:13:45.449] This node is a validator module=consensus addr=1202F4D1C5ACCC8219E2973394CBD06FD1F33B5A pubKey=PubKeySecp256k1{02DBBA09ABF7888AA63D75534A8A0CD79209B0E549DFB3FDE015FC61069D1C7232}
...
I[2023-05-19|10:13:54.061] Timed out module=consensus dur=984.901925ms height=3 round=0 step=RoundStepNewHeight
I[2023-05-19|10:13:54.079] received proposal module=consensus proposal="Proposal{3/0 (08CCBA6EDC7B6E77022D98A1BA528F34D2BDFFB94FE02DD36A3ECB873C321E07:1:ADBA4ABBE9A6, -1) 28842808EA1D @ 2023-05-19T09:13:54.072518233Z}"
I[2023-05-19|10:13:54.082] received complete proposal block module=consensus height=3 hash=08CCBA6EDC7B6E77022D98A1BA528F34D2BDFFB94FE02DD36A3ECB873C321E07
I[2023-05-19|10:13:54.098] finalizing commit of block module=consensus height=3 hash=08CCBA6EDC7B6E77022D98A1BA528F34D2BDFFB94FE02DD36A3ECB873C321E07 root=0171A0E402204FC2857E8D7F35DEE5D6657820A98F35C9AB0AB30C52C007779958F8F8F8E1CD num_txs=0
I[2023-05-19|10:13:54.106] executed block module=state height=3 num_valid_txs=0 num_invalid_txs=0
I[2023-05-19|10:13:54.110] committed state module=state height=3 num_txs=0 app_hash=0171A0E402204FC2857E8D7F35DEE5D6657820A98F35C9AB0AB30C52C007779958F8F8F8E1CD
I[2023-05-19|10:13:54.116] indexed block exents module=txindex height=3
...
Note that the first block execution is very slow because we have to load the Wasm engine, as indicated by the first proposal having a timeout, but after that the blocks come in fast, one per second.
If we want to use evm
related API, such as running fendermint/eth/api/examples/ethers.rs
, we need to start ETH API process.
The ETH RPC api runs on top of cometbft. Make sure you have cometbft running properly. The architecture is as follows:
+---------------------------+
| Node |
| |
| ------------------ |
| | fendermint run | |
| | | |
| | :26658 | |
| ------------------ |
| ^ |
| | |
| ------------------ |
| | cometbft | |
| | | |
| | :26657 | |
| ------------------ |
| | |
| :26657 |
+---------------------------+
^
|
-----------------------------
| Ethereum API |
| |
| +-------------------+ |
| | fendermint eth run| |
| | | |
| | :8545 | |
| +-------------------+ |
| | |
| :8545 |
-----------------------------
To start the ethereum RPC api with:
cargo run -p fendermint_app --release -- eth run
We will see:
ETH API log
2023-07-20T12:30:48.385026Z INFO fendermint::cmd: reading configuration path="/home/admin/.fendermint/config"
2023-07-20T12:30:48.435387Z INFO fendermint_eth_api: bound Ethereum API listen_addr=127.0.0.1:8545
We can try query the chain id by:
curl -X POST -i -H 'Content-Type: application/json' -d '{"jsonrpc":"2.0","id":0,"method":"eth_chainId","params":[]}' http://localhost:8545
The Fendermint binary has some commands to support querying state. Behind the scenes it uses the tendermint_rpc
crate to talk
to the CometBFT JSON-RPC endpoints, which forward the requests to the Application through ABCI.
Assuming both processes have been started, see if we can query the state of one of our actors. For this we need the actor address,
which we saw in the genesis.json
file earlier.
To make it easier to reuse these addresses, let's store them in variables:
ALICE_ADDR=$(cargo run -p fendermint_app --release -- key address --public-key test-network/keys/alice.pk)
BOB_ADDR=$(cargo run -p fendermint_app --release -- key address --public-key test-network/keys/bob.pk)
cargo run -p fendermint_app --release -- \
rpc query actor-state --address $ALICE_ADDR
The state is printed to STDOUT as JSON:
$ echo $ALICE_ADDR
f1i2izmkzef5q6udtdooeujzfsuieybxzl2yer5ey
$ cargo run -p fendermint_app --release -- rpc query actor-state --address $ALICE_ADDR
{
"id": 100,
"state": {
"balance": "10000000000000000000",
"code": "bafk2bzacealdyp7dmpc6eir65qhuh2hgv7onmv53rzzyp5futafmjjlxrt6fg",
"delegated_address": null,
"sequence": 0,
"state": "bafy2bzaceas2zajrutdp7ugb6w2lpmow3z3klr3gzqimxtuz22tkkqallfch4"
}
}
What we see here is the general ActorState which contains the balance, the nonce, the Wasm code CID, and the state root hash of the
actual actor implementation, which in this case is an Account
actor.
We can retrieve the raw state of the account with the ipld
command by using the state.state
from above as the --cid
argument:
cargo run -p fendermint_app --release -- \
rpc query ipld --cid bafy2bzaceas2zajrutdp7ugb6w2lpmow3z3klr3gzqimxtuz22tkkqallfch4
The binary contents are printed with Base64 encoding, which we could pipe to a file. It would be more useful to run this query programmatically and parse it to the appropriate data structure from builtin-actors.
gVUBRpGWKyQvYeoOY3OJROSyogmA3ys=
The simplest transaction we can do is to transfer tokens from one account to another.
For example we can send 0.1 tokens from Alice to Bob:
cargo run -p fendermint_app --release -- \
rpc transfer --secret-key test-network/keys/alice.sk --to $BOB_ADDR --sequence 0 --value 0.1 --chain-name test
Note that we are using --sequence 0
because this is the first transaction we make using Alice's key.
The transfer
command waits for the commit results of the transaction:
$ cargo run -p fendermint_app --release -- rpc transfer --secret-key test-network/keys/alice.sk --to $BOB_ADDR --sequence 0 --value 0.1 --chain-name test
Finished dev [unoptimized + debuginfo] target(s) in 0.40s
Running `target/debug/fendermint rpc transfer --secret-key test-network/keys/alice.sk --to f1kgtzp5nuob3gdccagivcgns7e25be2c2rqozilq --sequence 0 --value 0.1`
{
"response": {
"check_tx": {
"code": 0,
"codespace": "",
"data": null,
"events": [],
"gas_used": "0",
"gas_wanted": "10000000000",
"info": "",
"log": "",
"mempool_error": "",
"priority": "0",
"sender": "f1jqqlnr5b56rnmc34ywp7p7i2lg37ty23s2bmg4y"
},
"deliver_tx": {
"code": 0,
"codespace": "",
"data": null,
"events": [],
"gas_used": "1124863",
"gas_wanted": "10000000000",
"info": "",
"log": ""
},
"hash": "01828E0A350445ED3E8028D045EE99B5547B6834DB7296B799B95707EB546EC2",
"height": "46"
},
"return_data": null
}
The code: 0
parts indicate that both check and delivery were successful. Let's check the resulting states:
$ cargo run -p fendermint_app --release -- rpc query actor-state --address $BOB_ADDR | jq .state.balance
"1000"
$ cargo run -p fendermint_app --release -- rpc query actor-state --address $ALICE_ADDR | jq "{balance: .state.balance, sequence: .state.sequence}"
{
"balance": "999999999999999000",
"sequence": 1
}
Great, Alice's nonce was correctly increased as well.
When we want to deploy a smart contract to the FVM, the currently supported way is by deploying EVM contracts to FEVM.
First, we need the solc
compiler to produce the binaries we want deployed; take a look at the test contracts in the builtin-actors
repo for example.
Say we want to deploy the SimpleCoin
contract from that directory.
CONTRACT=../builtin-actors/actors/evm/tests/contracts/SimpleCoin.bin
cargo run -p fendermint_app --release -- \
rpc fevm --secret-key test-network/keys/alice.sk --sequence 1 --chain-name test \
create --contract $CONTRACT
Note that now we are using --sequence 1
because this is the second transaction sent by Alice.
The output shows what addresses have been assigned to the created contract,
which we can use to call the contract, namely by copying the delegated_address
.
$ CREATE=$(cargo run -p fendermint_app --release -- \
rpc fevm --secret-key test-network/keys/alice.sk --sequence 1 --chain-name test \
create --contract $CONTRACT)
$ echo $CREATE | jq .return_data
{
"actor_address": "f0105",
"actor_id": 105,
"actor_id_as_eth_address": "ff00000000000000000000000000000000000069",
"delegated_address": "f410f7do6trb2wkh6vwj6wpg5oxpgbipmj6btcc3kipq",
"eth_address": "f8dde9c43ab28fead93eb3cdd75de60a1ec4f833",
"robust_address": "f2yapgav7dqzifnry3ccqfg3xdzek73zd7rjvwsci"
}
$ DELEGATED_ADDR=$(echo $CREATE | jq -r .return_data.delegated_address)
Now that we have a contract deployed, we can call it. The arguments in the followign example are taken from fvm-bench. We need to increment the --sequence
again.
$ cargo run -p fendermint_app --release -- \
rpc fevm --secret-key test-network/keys/alice.sk --sequence 2 \
invoke --contract $DELEGATED_ADDR \
--method f8b2cb4f --method-args 000000000000000000000000ff00000000000000000000000000000000000064 \
| jq .return_data
"0000000000000000000000000000000000000000000000000000000000002710"
If we look at the method signatures we can see that this is calling getBalance, and indeed if we decode 2710
from hexadecimal to decimal, we get the 10000
balance the owner should have.
To avoid having to come up with ABI encoded arguments in hexadecimal format, we can use the RPC client in combination with ethers excellent abigen
functionality.
Here's an example of doing that with the SimpleCoin contract.
$ cargo run -p fendermint_rpc --release --example simplecoin -- --secret-key test-network/keys/alice.sk --verbose
2023-05-19T10:18:47.234878Z DEBUG fendermint_rpc::client: Using HTTP client to submit request to: http://127.0.0.1:26657/
...
2023-05-19T10:18:47.727563Z INFO simplecoin: contract deployed contract_address="f410fvbmxiqdn6svyo5oubfbzxsorkvydcb5ecmlbwma" actor_id=107
...
2023-05-19T10:18:48.805085Z INFO simplecoin: owner balance balance="10000" owner_eth_addr="ff00000000000000000000000000000000000064"
Note that the script figures out the Alice's nonce on its own, so we don't have to pass it in. It also has an example of running an EVM view method (which is read-only) either as as a distributed read-transaction (which is included on the chain and costs gas) or a query anwered by our node without involving the blockchain. Both have their uses, depending on our level of trust.