Skip to main content
This tutorial deploys IBC on two local Besu chains and transfers a token between them. It uses an attestation light client in one direction and a Besu consensus light client for the other. By the end, you’ll have the following:
  • IBC CLI installed and configured
  • IBC contracts deployed on two local Besu chains
  • IBC connections established between the chains
  • A relayer running
  • An attestor running
  • An Interchain Fungible Token that can be transferred between the chains

Prerequisites

  • Docker installed and running
  • Go v1.26.10 or later
  • jq installed
  • Git installed

1. Set up your environment

Start by installing IBC CLI and running two local Besu chains.
  1. Clone the IBC repository and build the binary:
The binary is located at bin/ibc.
  1. This tutorial uses two local Besu chains, which are EVM-based. Start both chains:
The script initializes two single-validator Besu chains, starts them, and waits until both answer RPC calls. It finishes by printing something similar to the following:
Now there are two live chains, each producing blocks.
  1. You can use the following command to look at the funded accounts included in the setup script:
Five accounts are derived from one mnemonic and funded on both chains, so the same key works on either side. The next step uses these.

2. Create a config and keys

IBC CLI uses one config file for all its operations.
  1. Start by creating the config file:
The config is stored in ~/.ibc/ibc.yml by default. This is the home directory for everything that follows: the config, the keystore, the deployment records, and the relayer’s database.
  1. Import a deployer key from the accounts created during the chain setup. This key becomes the access manager’s admin, which governs the IBC router, multicall, and the GMP app. It is also the default owner of any token you deploy.
Passing --populate-config adds each key to the config as a named signer. Later commands then name a key by alias instead of by path.
  1. Import a relayer key, which signs packet delivery and pays for the relayer’s gas:
  1. Generate an attestor key for chain 41002. It signs attestations and needs no funding. Never reuse an attestor key across clients: see Attestors.
  1. Next, register the first chain’s details in the config. This passes the chain ID, RPC and websocket endpoints, and deployer key.
  1. Register the second chain’s details:
The --deployer flag names the key that signs deployments on that chain, so you won’t need to pass it again.

3. Deploy IBC on both chains

With both chains registered, deploy the IBC contracts on each.
  1. Deploy the IBC core on each chain. This deploys the ICS26 router, multicall, and the access manager:
Each fresh deployment sends seven transactions: an access manager, implementations and proxies for the router and multicall, and two calls that open their relay entrypoints to any caller. Your deployer key is the access manager’s admin. The --populate-config flag writes each chain’s router and multicall addresses to ~/.ibc/ibc.yml.
Rerunning skips contracts already recorded and deployed, and resumes any unfinished deployment steps.
These commands enable public relaying and make the deployer the access manager’s admin. For production deployments, restrict relaying to authorized keys. Transfer admin authority to an appropriate account. Follow Grant, change, and revoke a role for both procedures.
  1. Deploy an attestation light client on 41001 to verify chain 41002:
The --attestors flag resolves the key alias to its address. That address becomes the client’s attestation set on chain. The --threshold 1 flag requires one signature for each proof. The set and threshold are fixed when the client is deployed.
  1. Deploy a Besu consensus light client on 41002 to verify chain 41001:
This client verifies Besu validator signatures and storage proofs. It needs no attestor for 41001. The tutorial uses a 14-day trusting period: a trusted state older than that cannot authorize an update. See Besu light client for the trust assumptions. Each client verifies the opposite chain: Both sides share one client identifier, derived from the two chain IDs in sorted order, so it reads the same on either chain. Here it is cli-41001-41002, which will be used later to identify the connection.

4. Deploy the application

The next steps deploy the General Message Passing (GMP) app and the Interchain Fungible Token (IFT) contracts on each chain. IFT uses GMP to send and receive messages between the chains and facilitate token transfers.
  1. Deploy the GMP app on each chain. This allows cross-chain contract calls between the chains:
  1. Deploy an IFT contract on each chain. This is the token that will be transferred between the chains:
An IFT token rejects every transfer until it has a rate limit, so deploy ift requires one: each direction lets up to --rate-limit-capacity base units through, refilling over --rate-limit-window (24 hours by default). Here that is 1000 tokens at 18 decimals. Rerunning with a different limit updates it.
  1. Use the following commands to read both token addresses out of the deployment records. These will be used in later steps:
If you need to view these addresses later, the deploy show command prints a chain’s deployment record as JSON.
  1. The next step is to link the two token contracts together. Registering a bridge tells each token to accept mints from its counterpart on the other chain. A transfer then burns tokens on the source chain and mints them on the destination.
This command registers both sides. It ties each token to the client pointing at the other chain, which is how a transfer knows where to land.

5. Configure and start the relayer

Next, you’ll need to configure the relayer to start sending packets between the two chains.
  1. Generate and save the relayer’s configuration:
This merges the deployed router and multicall addresses, connection, and local attestor into your existing config. It preserves settings such as server, db, and signers. The --yes flag skips confirmation before writing ~/.ibc/ibc.yml. The two signer flags select the key that submits relay transactions on each chain. Set both for this new connection; each is checked against your configured signers. You imported relayer in step 3 of section 2. The connection uses type: attestation on 41001 and type: besu on 41002. The attestors section declares one local attestor for 41002, using signer attestor-41002. The relayer runs that attestor in the same process.
  1. Use the validate command to check the result against both chains before starting anything:
  1. Open a new terminal in the cli directory to start the relayer and attestor. Leave your first terminal open for the transfer commands.
The readiness line confirms that startup completed. For ongoing API readiness, use the gRPC health check, which also checks database reachability. “Dual mode” means the local attestor runs inside the relayer’s process. Leave it running, and go back to your first terminal for the rest of the tutorial.

6. Transfer a token

The next steps will mint the token and send it to the other chain.
  1. Mint an initial supply of the token into the deployer account.
The --from key must be the token’s owner, which is the deployer. Amounts are in base units, so this is 100 tokens at 18 decimals.
  1. Send 10 tokens from one chain to the other, keeping the transaction hash. $IFT_A is the token address you stored earlier.
Note: The --timeout flag sets how long the packet has to be delivered, counted from when you send. It defaults to 15 minutes and cannot exceed one day. A packet that misses that window can only be timed out and refunded, and the deadline cannot be changed after the send.
The send prints {"txHash": "0x..."}. That hash is stored in $TX, which the next command uses:
Once this transaction is mined, the tokens are burned on the source chain. They are minted on the destination only when the packet is delivered. If delivery fails, or the packet times out, the tokens are minted back to the sender once a relayer carries that outcome home.
  1. The relayer will automatically detect that the IBC packet was created and relay it.
  2. Now you can check the status of the transfer:
It will read PACKET_STATE_PENDING for up to a minute, then it should read PACKET_STATE_SUCCEEDED:
This shows three transactions across two chains: the send you made, the receive on 41002, and the acknowledgement carried back to 41001. The receive uses a Besu proof of the send on 41001. The relayer proves the recorded acknowledgement on 41002 with an attestation. This transfer uses both client types.
  1. Confirm the tokens arrived on the destination chain:
Then check that the source chain was debited:

What you built

Two local Besu chains now have IBC contracts and connected token contracts. One relayer hosts an attestor for 41002 and relays using both client types. Your first transfer moved 10 tokens from 41001 to 41002.

Next steps

  • Overview explains what the three parts are made of and how they fit together.
  • Run a standalone attestor moves an attestor out of the relayer’s process and into its own.
  • Run a standalone relayer brings up a relayer against a deployment, including one that replaces a relayer that stopped.