Multi-hop Transfers
Guide to sending multi-hop payments through the Fiber Network
TL;DR
Connect your local node to Fiber's public relay nodes, open a channel, and send multi-hop payments through the Testnet. Fiber supports two routing modes: gossip routing (source routing, the sender builds the full path) and trampoline routing (the sender delegates path-finding to a relay node). Your local node does not need a public IP.
┌───────┐ ┌───────┐ ┌───────┐
│ nodeA │ ─────▶ │ node1 │ ─────▶ │ node2 │
│ local │ │public │ │public │
└───────┘ └───────┘ └───────┘
sender relay receiverPublic Node Addresses
Mainnet
| Node | Pubkey |
|---|---|
| node1 | 03a8d7da8d0934363dbc17f52c872e8d833016415266eabb3527439c5dd17adc6b |
| node2 | 033a69e5be369dab43aefa96fa729d83c571ccb066f312136c6ab2d354fcc028f9 |
Testnet
| Node | Pubkey |
|---|---|
| node1 | 02b6d4e3ab86a2ca2fad6fae0ecb2e1e559e0b911939872a90abdda6d20302be71 |
| node2 | 0291a6576bd5a94bd74b27080a48340875338fff9f6d6361fe6b8db8d0d1912fcc |
Local Node Setup
-
Download fnn from the releases page:
mkdir tmp && cd tmp tar xzvf fnn-latest.tar.gzmacOS Security
xattr -d com.apple.quarantine fnn fnn-cli -
Create account and export private key:
mkdir -p testnet-fnn/nodeA/ckb ./ckb-cli account new # save the lock_arg ./ckb-cli account export --lock-arg <YOUR_LOCK_ARG> --extended-privkey-path ./exported-key head -n 1 ./exported-key > testnet-fnn/nodeA/ckb/key chmod 600 testnet-fnn/nodeA/ckb/key -
Copy config and tools:
cp config/testnet/config.yml testnet-fnn/nodeA cp fnn-cli testnet-fnn/nodeAHTTP Proxy Issues
export NO_PROXY=127.0.0.1,localhost -
Fund nodeA's address with 10,000 CKB:
-
Start nodeA:
FIBER_SECRET_KEY_PASSWORD='123' RUST_LOG=info ./fnn -c testnet-fnn/nodeA/config.yml -d testnet-fnn/nodeA > testnet-fnn/nodeA/a.log 2>&1 &
CKB Multi-Hop Payment (nodeA → node1 → node2)
CLI amounts are in shannons (1 CKB = 100,000,000). RPC amounts are hex strings (e.g. "0x5f5e100" = 100,000,000).
1. Connect to node1
First get node1's pubkey:
curl -s 'http://18.162.235.225:8227' \
-H 'Content-Type: application/json' \
-d '{"id":1,"jsonrpc":"2.0","method":"node_info"}' | grep -o '"pubkey":"[^"]*"'Then connect:
cd testnet-fnn/nodeA && ./fnn-cli peer connect_peer \
--pubkey 02b6d4e3ab86a2ca2fad6fae0ecb2e1e559e0b911939872a90abdda6d20302be71curl -s 'http://127.0.0.1:8227' \
-H 'Content-Type: application/json' \
-d '{
"id": 1, "jsonrpc": "2.0", "method": "connect_peer",
"params": [{"pubkey": "<node1_pubkey>"}]
}'await sdk.connectPeer({
address: "/ip4/18.162.235.225/tcp/8119/p2p/QmXen3eUHhywmutEzydCsW4hXBoeVmdET2FJvMX69XJ1Eo",
});2. Open a CKB Channel
node1's auto_accept_min_ckb_funding_amount is set to 400 CKB. Here we fund 500 CKB.
cd testnet-fnn/nodeA && ./fnn-cli channel open_channel \
--pubkey <node1_pubkey> \
--funding-amount 50000000000 \
--public truecurl -s 'http://127.0.0.1:8227' \
-H 'Content-Type: application/json' \
-d '{
"id": 2, "jsonrpc": "2.0", "method": "open_channel",
"params": [{"pubkey": "<node1_pubkey>", "funding_amount": "0xba43b7400", "public": true}]
}'// funding_amount: 0xba43b7400 = 500 CKB in shannons
const tempChannelId = await sdk.openChannel({
pubkey: "<node1_pubkey>",
fundingAmount: "0xba43b7400",
public: true,
});
console.log("Temporary channel ID:", tempChannelId);3. Wait for ChannelReady
./fnn-cli channel list_channelscurl -s 'http://127.0.0.1:8227' \
-H 'Content-Type: application/json' \
-d '{"id": 3, "jsonrpc": "2.0", "method": "list_channels", "params": [{"pubkey": "<node1_pubkey>"}]}'const channels = await sdk.listChannels({ pubkey: "<node1_pubkey>" });
for (const ch of channels) {
console.log(`${ch.channelId} — ${ch.state.stateName}`);
}After ChannelReady, wait a few minutes before sending payments — the gossip protocol needs time to sync channel announcements and routing info across the network.
4. Create an Invoice on node2
Generate a preimage: payment_preimage="0x$(openssl rand -hex 32)"
./fnn-cli --url http://18.163.221.211:8227 invoice new_invoice \
--amount 100000000 \
--currency Fibt \
--description "test invoice" \
--expiry 3600 \
--payment-preimage $payment_preimagecurl -s 'http://18.163.221.211:8227' \
-H 'Content-Type: application/json' \
-d "{
\"id\": 4, \"jsonrpc\": \"2.0\", \"method\": \"new_invoice\",
\"params\": [{
\"amount\": \"0x5f5e100\",
\"currency\": \"Fibt\",
\"description\": \"test invoice\",
\"expiry\": \"0xe10\",
\"payment_preimage\": \"$payment_preimage\",
\"hash_algorithm\": \"sha256\"
}]
}"// Create invoice on node2 (endpoint: http://18.163.221.211:8227)
const node2 = new FiberSDK({ endpoint: "http://18.163.221.211:8227" });
const paymentPreimage = "0x" + "01".repeat(32);
const { invoiceAddress } = await node2.newInvoice({
amount: "0x5f5e100",
currency: "Fibt",
description: "test invoice",
expiry: "0xe10",
paymentPreimage,
hashAlgorithm: "sha256",
});
console.log("Invoice:", invoiceAddress);5. Send Payment (nodeA → node1 → node2)
Fiber supports two routing modes for multi-hop payments:
Option A: Gossip Routing (Source Routing)
The sender discovers routes via the gossip protocol and constructs the full payment path locally. No additional parameters are needed — the node automatically finds the best route to the destination.
After ChannelReady, wait a few minutes for gossip to propagate channel announcements across the network before attempting gossip-routed payments.
./fnn-cli payment send_payment --invoice "<invoice_address>"curl -s 'http://127.0.0.1:8227' \
-H 'Content-Type: application/json' \
-d '{"id": 6, "jsonrpc": "2.0", "method": "send_payment", "params": [{"invoice": "<invoice_address>"}]}'const result = await sdk.sendPayment({ invoice: "<invoice_address>" });
console.log("Payment hash:", result.paymentHash);
console.log("Status:", result.status);Option B: Trampoline Routing
The sender delegates route-finding to a relay node. This is useful when the sender doesn't have full network topology (e.g. a mobile wallet). Specify trampoline_hops with the relay node's pubkey.
./fnn-cli payment send_payment --invoice "<invoice_address>" \
--trampoline-hops <node1_pubkey>curl -s 'http://127.0.0.1:8227' \
-H 'Content-Type: application/json' \
-d '{"id": 6, "jsonrpc": "2.0", "method": "send_payment", "params": [{"invoice": "<invoice_address>", "trampoline_hops": ["<node1_pubkey>"]}]}'const result = await sdk.sendPayment({
invoice: "<invoice_address>",
trampolineHops: ["<node1_pubkey>"],
});
console.log("Payment hash:", result.paymentHash);
console.log("Status:", result.status);6. Close the Channel
./fnn-cli channel shutdown_channel \
--channel-id <channel_id> \
--close-script '{"code_hash":"0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8","hash_type":"type","args":"<your_lock_arg>"}'curl -s 'http://127.0.0.1:8227' \
-H 'Content-Type: application/json' \
-d '{
"id": 9, "jsonrpc": "2.0", "method": "shutdown_channel",
"params": [{
"channel_id": "<channel_id>",
"close_script": {
"code_hash": "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
"hash_type": "type",
"args": "<your_lock_arg>"
},
"fee_rate": "0x3FC"
}]
}'await sdk.shutdownChannel({
channelId: "<channel_id>",
closeScript: {
codeHash: "0x9bd7e06f3ecf4be0f2fcd2188b23f1b9fcc88e5d4b65a8637b17723bbda3cce8",
hashType: "type",
args: "<your_lock_arg>",
},
feeRate: "0x3FC",
});
console.log("Channel closed.");