Hyperlane Setup
This guide walks you through configuring the Hyperlane interoperability bridge on the crescendo-1 testnet. Hyperlane enables cross-chain messaging between BitSong and EVM chains (such as Base Sepolia) through a system of on-chain contracts and modules.
The setup consists of 7 transactions that create the full Hyperlane messaging stack on the BitSong side.
Overview
The Hyperlane bridge requires several components working together to secure and route messages:
MultisigISM
RoutingISM
Mailbox
MerkleTreeHook
IGP
Prerequisites
Before starting, ensure you have the following ready:
- A fully synced BitSong node on the
crescendo-1testnet - The
bitsongdbinary installed and available in your PATH jqinstalled (sudo apt install jq)- Agent keys generated (you need the validator EVM address for Step 1)
- A funded account with testnet TBTSG tokens
Verify your node is running and synced:
bitsongd status --node tcp://localhost:26657 | jq '.sync_info.latest_block_height'
Configuration Reference
These are the parameters used throughout the setup. Replace values as needed for your environment.
crescendo-1.7171.84532.utbtsg.Set these as environment variables in your terminal for convenience:
export CHAIN_ID="crescendo-1"
export DOMAIN_ID="7171"
export REMOTE_DOMAIN="84532"
export DENOM="utbtsg"
export KEY_NAME="<your-key-name>"
export NODE="tcp://localhost:26657"
# From the agent keys step
export VALIDATOR_ADDR="<your-validator-evm-address>"
Installation Steps
Each transaction returns a txhash. The commands below capture it and extract the resource ID into a shell variable, so subsequent steps can reference it directly.
bitsongd query tx $TX_HASH --output json | jq '.events[] | select(.type | test("hyperlane|warp"))'
_id (e.g. ism_id, mailbox_id, hook_id, token_id).Create the MultisigISM
The Message ID Multisig ISM (Interchain Security Module) verifies incoming cross-chain messages by checking that a threshold of validators have signed the message.
For a single-validator testnet setup, use your own address with a threshold of 1:
TX_HASH=$(bitsongd tx hyperlane ism create-message-id-multisig \
$VALIDATOR_ADDR 1 \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y | jq -r '.txhash')
echo "TX Hash: $TX_HASH"
$VALIDATOR_ADDR). Do not use a placeholder address. The validator agent must sign with the same key that is registered in the MultisigISM.For a multi-validator setup, replace the command above with a comma-separated list of all validator EVM addresses and the required signature threshold:
TX_HASH=$(bitsongd tx hyperlane ism create-message-id-multisig \
"$VALIDATOR_ADDR_1,$VALIDATOR_ADDR_2,$VALIDATOR_ADDR_3" 2 \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y | jq -r '.txhash')
echo "TX Hash: $TX_HASH"
2 out of 3 validators provides fault tolerance — the bridge remains operational even if one validator goes offline.Once the transaction is included in a block, extract the ISM ID:
MULTISIG_ISM_ID=$(bitsongd query tx $TX_HASH --output json | \
jq -r 'first(.events[] | select(.type | test("MultisigIsm")) | .attributes[] | select(.key == "ism_id") | .value | fromjson)')
echo "MultisigISM ID: $MULTISIG_ISM_ID"
Create the RoutingISM
The RoutingISM directs message verification to the correct ISM based on the origin domain. This allows different security configurations for messages arriving from different chains.
TX_HASH=$(bitsongd tx hyperlane ism create-routing \
--routes="[{\"domain\":$REMOTE_DOMAIN,\"ism\":\"$MULTISIG_ISM_ID\"}]" \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y | jq -r '.txhash')
echo "TX Hash: $TX_HASH"
Extract the RoutingISM ID:
ROUTING_ISM_ID=$(bitsongd query tx $TX_HASH --output json | \
jq -r 'first(.events[] | select(.type | test("RoutingIsm")) | .attributes[] | select(.key == "ism_id") | .value | fromjson)')
echo "RoutingISM ID: $ROUTING_ISM_ID"
Create the Mailbox
The Mailbox is the central contract for Hyperlane messaging. It dispatches outgoing messages and processes incoming messages, using the RoutingISM for verification.
TX_HASH=$(bitsongd tx hyperlane mailbox create \
$ROUTING_ISM_ID $DOMAIN_ID \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y | jq -r '.txhash')
echo "TX Hash: $TX_HASH"
Extract the Mailbox ID:
MAILBOX_ID=$(bitsongd query tx $TX_HASH --output json | \
jq -r 'first(.events[] | select(.type | test("Mailbox")) | .attributes[] | select(.key == "mailbox_id") | .value | fromjson)')
echo "Mailbox ID: $MAILBOX_ID"
Create the MerkleTreeHook
The MerkleTreeHook records dispatched message hashes into a Merkle tree. Hyperlane validators use this tree to generate proofs that messages were actually sent.
TX_HASH=$(bitsongd tx hyperlane hooks merkle create \
$MAILBOX_ID \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y | jq -r '.txhash')
echo "TX Hash: $TX_HASH"
Extract the MerkleTreeHook ID:
MERKLE_HOOK_ID=$(bitsongd query tx $TX_HASH --output json | \
jq -r 'first(.events[] | select(.type | test("MerkleTreeHook")) | .attributes[] | select(.key == "merkle_tree_hook_id") | .value | fromjson)')
echo "MerkleTreeHook ID: $MERKLE_HOOK_ID"
Create the IGP
The Interchain Gas Paymaster (IGP) allows message senders to pay for gas on the destination chain. This ensures that relayers are compensated for delivering messages.
TX_HASH=$(bitsongd tx hyperlane hooks igp create \
$DENOM \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y | jq -r '.txhash')
echo "TX Hash: $TX_HASH"
Extract the IGP ID:
IGP_ID=$(bitsongd query tx $TX_HASH --output json | \
jq -r 'first(.events[] | select(.type | test("Igp")) | .attributes[] | select(.key == "igp_id") | .value | fromjson)')
echo "IGP ID: $IGP_ID"
Configure IGP Gas Oracle
Set the destination gas configuration for the remote chain. This tells the IGP how to calculate gas costs for messages going to Base Sepolia.
bitsongd tx hyperlane hooks igp set-destination-gas-config \
$IGP_ID $REMOTE_DOMAIN \
<token-exchange-rate> <gas-price> <gas-overhead> \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y
The gas parameters control pricing:
1000000000.75000 is standard for Hyperlane message delivery.tokenExchangeRate = (ETH_price / BTSG_price) × (10^6 / 10^18) × 10^10
tokenExchangeRate = (1851.30 / 0.000825) × (10^6 / 10^18) × 10^10
= 2,244,000 × 10^-12 × 10^10
= 2,244,000 × 10^-2
= 22440
# Example with the values above — adjust to current prices
bitsongd tx hyperlane hooks igp set-destination-gas-config \
$IGP_ID $REMOTE_DOMAIN \
22440 1000000000 75000 \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y
--gaspaymentenforcement '[{"type": "none"}]', messages are delivered regardless of whether gas was paid. The exchange rate primarily affects how much TBTSG is quoted to users for gas prepayment. For production, keep this value updated with real market prices.Set Mailbox Hooks
Link the IGP and MerkleTreeHook to the Mailbox. The default hook (IGP) runs on every dispatched message unless overridden, and the required hook (MerkleTree) always runs.
bitsongd tx hyperlane mailbox set \
$MAILBOX_ID \
--default-hook=$IGP_ID \
--required-hook=$MERKLE_HOOK_ID \
--from $KEY_NAME \
--keyring-backend test \
--chain-id $CHAIN_ID \
--node $NODE \
--gas auto --gas-adjustment 1.5 \
--fees 10000${DENOM} \
--output json -y
Verify the Setup
After completing all steps, verify that every component was created correctly using the query commands below.
bitsongd query hyperlane mailboxes --output json | jq '.'
bitsongd query hyperlane ism isms --output json | jq '.'
bitsongd query hyperlane hooks igps --output json | jq '.'
bitsongd query hyperlane hooks merkle-tree-hooks --output json | jq '.'
Troubleshooting
The ID is returned in the transaction events, not in msg_responses. If $TX_HASH is set, filter for Hyperlane events:
bitsongd query tx $TX_HASH --output json | jq '.events[] | select(.type | test("hyperlane|warp"))'
This shows all Hyperlane-related events. Look for the attribute ending in _id (e.g. ism_id, mailbox_id, hook_id, token_id). The value is JSON-encoded, so use fromjson to unwrap it:
# Example: extract ism_id from a MultisigISM creation transaction
bitsongd query tx $TX_HASH --output json | \
jq -r '.events[] | select(.type | test("MultisigIsm")) | .attributes[] | select(.key == "ism_id") | .value | fromjson'
The --routes flag expects a JSON array. When using variables with double quotes, escape the inner quotes:
--routes="[{\"domain\":$REMOTE_DOMAIN,\"ism\":\"$MULTISIG_ISM_ID\"}]"
Alternatively, use single quotes with quote-breaking for variable substitution:
--routes='[{"domain":'"$REMOTE_DOMAIN"',"ism":"'"$MULTISIG_ISM_ID"'"}]'
Verify the following common issues:
- Token ID check: Ensure the token exists via
bitsongd query warp tokens --output json | jq '.' - Address format: The EVM address must be correctly converted to bytes32 format (64 hex characters after
0x) - Funds: Your account must have sufficient funds for gas and fees
Next Steps
The Hyperlane messaging stack is deployed. Next, start the validator and relayer agents, then deploy the warp route for cross-chain token transfers.