Lightning-to-Cardano bridge relay node. Combines an LDK Lightning node with a Cardano smart contract connector for cross-chain BTC/cBTC swaps.
Built on LDK and the Liquidity Manager Plutus V3 contract.
- Rust (edition 2024) — for native builds. Skip if you only run via Docker (see Docker).
- bitcoind (v25+) running in regtest or testnet mode — provided automatically by
docker-compose.signet.yml. - Cardano devnet (yaci-devkit) or Blockfrost API access for preprod/mainnet
- Only if you deploy a fresh Liquidity Manager yourself (see Deploying a fresh Liquidity Manager):
bitcoind -regtest -rpcuser=user -rpcpassword=pass -daemon
bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass \
createwallet testwallet
bitcoin-cli -regtest -rpcuser=user -rpcpassword=pass \
-rpcwallet=testwallet -generate 101cd lightning-liquidity-manager
bash scripts/test_local_devnet.sh --cleanThis starts yaci-devkit, generates operator credentials, mints test cBTC, and deploys the Liquidity Manager contract with 50M cBTC initial pool. See the lightning-liquidity-manager repo for details.
cargo build --release
export CARDANO_ENABLED=true
export CARDANO_SKEY_PATH=path/to/operator.sk
export CARDANO_SCRIPT_ADDRESS=addr_test1w...
export CARDANO_SCRIPT_CBOR_PATH=path/to/script_cbor.hex
export CARDANO_CBTC_POLICY_ID=<policy_hex>
export CARDANO_CBTC_ASSET_NAME=63425443
export CARDANO_OPERATOR_ADDRESS=addr_test1v...
export CARDANO_OPERATOR_PKH=<pkh_hex>
# For local devnet (defaults):
# CARDANO_BLOCKFROST_URL=http://localhost:8080/api/v1/
# CARDANO_BLOCKFROST_KEY=local
# CARDANO_API_PORT=3000
./target/release/cardano-lightning-relay \
user:pass@127.0.0.1:18443 ./ldk_data 9735 regtestThe recommended way to run on Preprod is via Docker Compose — see Option A below. It bundles Signet bitcoind alongside the relay and pre-wires the RPC endpoint.
Key Preprod-specific bits the Compose setup handles for you:
CARDANO_BLOCKFROST_URL=https://cardano-preprod.blockfrost.io/api/v0/- Uses built-in
Network::Preprodcost models (correct PlutusV3 canonical ordering) - Fetches protocol params from Blockfrost for correct fee calculation
Working branches: feat-cardano (this repo) and feat-deploy-preprod (the cardano-lightning-client crate).
The relay exposes a REST API on port 3000 (configurable via CARDANO_API_PORT).
| Method | Path | Description |
|---|---|---|
GET |
/pool/info |
Query pool state (total_liquidity, reserved, available) |
POST |
/pool/deposit |
Deposit cBTC into pool. Body: {"amount": <i64>} |
POST |
/pool/withdraw |
Withdraw cBTC from pool. Body: {"amount": <i64>} |
| Method | Path | Description |
|---|---|---|
POST |
/swap/request |
Request swap. Body: {"amount_cbtc": <i64>, "cardano_address": "<addr>"} |
GET |
/swap/status/{hash} |
Query swap status by payment hash |
| Method | Path | Description |
|---|---|---|
POST |
/offramp/request |
Request offramp. Body: {"bolt11": "<invoice>", "amount_cbtc": <i64>, "cardano_address": "<addr>"} |
POST |
/offramp/deposit |
Notify cBTC deposit. Body: {"offramp_id": <i64>, "cbtc_tx_hash": "<hash>"} |
GET |
/offramp/status/{id} |
Query offramp status by ID |
Two ways to run the relay in Docker.
docker-compose.signet.yml brings up bitcoind (Signet) and the relay in one go. The Bitcoin RPC endpoint is already wired (relay:relay@bitcoind:38332); you don't have to configure it yourself.
1. Clone both repos as siblings in the same parent directory. The build context is .. because the relay has a path-dependency on the local cardano-lightning-client crate. Both repos must be on their working feature branches — main does not contain the Cardano + Docker work yet:
mkdir cardano-lightning && cd cardano-lightning
git clone -b feat-deploy-preprod https://github.com/perun-network/cardano-lightning-client
git clone -b feat-cardano https://github.com/perun-network/cardano-lightning-relay
cd cardano-lightning-relay2. Create the secrets file. Compose loads secrets/preprod.env via env_file:, so it must exist or Compose refuses to start:
cp secrets/preprod.env.example secrets/preprod.envEdit secrets/preprod.env and fill in:
CARDANO_BLOCKFROST_KEY— Preprod project ID from blockfrost.io (free tier is fine)CARDANO_OPERATOR_ADDRESS(addr_test1v…),CARDANO_OPERATOR_PKH(hex),CARDANO_SCRIPT_ADDRESS(addr_test1w…),CARDANO_CBTC_POLICY_ID(hex) — all four come from the operator/team in step 3
3. Get the Cardano-side credentials from the operator/team. The Liquidity Manager validator is parameterized with the operator's PKH, so the script address + CBOR are bound to a single operator key. Every relay running against the shared pool also runs as that same operator. Coordinate with the operator to receive (over a private channel — Slack, encrypted mail, etc.):
operator.sk— Cardano signing key (sensitive)script_cbor.hex— parameterized validator CBOR (~10 kB hex)- Four hex values for
secrets/preprod.env:CARDANO_OPERATOR_ADDRESS,CARDANO_OPERATOR_PKHCARDANO_SCRIPT_ADDRESSCARDANO_CBTC_POLICY_ID(asset name is the literal63425443, already inpreprod.env.example)
Plus get your own free Blockfrost project ID for CARDANO_BLOCKFROST_KEY.
Important: Only one relay should be live against the same pool at a time — the script's state UTxO is single-spend, so concurrent transactions from two relays will fail. Coordinate handover with the operator.
(If instead you want to spin up a fresh, independent LM deployment for solo development, see Deploying a fresh Liquidity Manager below.)
4. Drop the artefacts into secrets/:
cp /path/to/operator.sk secrets/operator.sk
cp /path/to/script_cbor.hex secrets/script_cbor.hex
chmod 600 secrets/operator.skThen edit secrets/preprod.env and fill in the four hex values plus the Blockfrost key.
5. Build the image:
docker compose -f docker-compose.signet.yml build6. Start bitcoind first and wait for it to sync Signet (no pruning is configured; an initial sync typically takes one to several hours depending on the link):
docker compose -f docker-compose.signet.yml up -d bitcoind
# poll until "initialblockdownload": false:
docker exec bitcoind bitcoin-cli -signet \
-rpcuser=relay -rpcpassword=relay getblockchaininfo7. Start the relay:
docker compose -f docker-compose.signet.yml up -d relay
docker logs -f relayREST API on http://localhost:3002, Lightning P2P on 9735. Smoke check:
curl http://localhost:3002/pool/infoStop everything:
docker compose -f docker-compose.signet.yml downState persists in two named Docker volumes: bitcoin-data (chain state) and relay-data (LDK state + wallet seed at /data/wallet_seed.txt, auto-generated on first start if missing).
Use this only if you want a separate, independent LM deployment (e.g. for solo development) — it produces a new operator key, a new script address, and a new cBTC token, not linked to any existing pool. To run against an existing shared pool, use the credential handover described in step 3 above instead.
cd ..
git clone -b fix/audit-2026-04-10 https://github.com/perun-network/lightning-liquidity-manager
cd lightning-liquidity-manager
uv sync
export CARDANO_NETWORK=preprod
export CARDANO_BLOCKFROST_KEY=<your_blockfrost_preprod_project_id>
# Generate operator key + write credentials/deployment.json skeleton
uv run scripts/config.py
# → prints the operator address. Fund it with at least 1000 tADA from
# https://docs.cardano.org/cardano-testnets/tools/faucet, then continue:
aiken build
uv run scripts/init_contract.py 1000000 # mints + seeds pool with 1M cBTCAfter this, credentials/deployment.json and plutus-applied.json contain everything you need to fill secrets/preprod.env + secrets/script_cbor.hex in the relay repo. Mapping:
secrets/preprod.env variable |
source field in credentials/deployment.json |
|---|---|
CARDANO_OPERATOR_ADDRESS |
.operator.address |
CARDANO_OPERATOR_PKH |
.operator.pkh |
CARDANO_SCRIPT_ADDRESS |
.validator.parameterized_script_address |
CARDANO_CBTC_POLICY_ID |
.token.cbtc_policy |
CARDANO_CBTC_ASSET_NAME |
.token.cbtc_asset_name |
# Operator signing key
cp credentials/operator.sk ../cardano-lightning-relay/secrets/operator.sk
# Parameterized validator CBOR
jq -r '.validators[] | select(.title == "liquidity_manager.liquidity_manager.spend") | .compiledCode' \
plutus-applied.json \
> ../cardano-lightning-relay/secrets/script_cbor.hexBuild from the parent directory containing both cardano-lightning-relay/ and cardano-lightning-client/:
docker build -f cardano-lightning-relay/Dockerfile -t cardano-lightning-relay .
docker run --rm cardano-lightning-relay --helpRun against an external bitcoind. The entrypoint requires either positional arguments or BITCOIN_RPC_URL (full env-var list at the top of the Dockerfile):
docker run -d --name relay \
-p 9735:9735 -p 3002:3002 \
-v relay-data:/data \
-v "$(pwd)/secrets":/secrets:ro \
--env-file secrets/preprod.env \
-e BITCOIN_RPC_URL=user:pass@<bitcoind_host>:38332 \
-e BITCOIN_NETWORK=signet \
-e BITCOIN_ESPLORA_URL=https://mempool.space/signet/api \
-e BITCOIN_WALLET_SEED_PATH=/data/wallet_seed.txt \
-e LDK_MIN_CHANNEL_CONFIRMATIONS=1 \
-e CARDANO_ENABLED=1 \
-e CARDANO_API_PORT=3002 \
cardano-lightning-relaynix build # produces result/bin/cardano-lightning-relay
nix develop # dev shell with Rust 1.85, pkg-config, opensslRequires the cardano-lightning-client repo as a sibling directory.
Tests use Expect scripts that automate the full flow.
# Set up devnet (required before each test)
bash test_scripts/test_local_devnet.sh --clean
# Run the channel lifecycle test (from the relay repo root)
expect test_scripts/ms2_channel_lifecycle_test.exp # 5 channel lifecycles (30 assertions)Test evidence: evidence_ms2/
The relay has an interactive CLI. Type help for all commands. Key Cardano commands:
pool-info— Query pool statecardano-deposit <amount>— Deposit cBTCcardano-withdraw <amount>— Withdraw cBTCcancel-expired— Cancel expired invoices
Standard LDK commands (openchannel, closechannel, sendpayment, getinvoice, etc.) are also available.
Licensed under the Apache License, Version 2.0 (LICENSE or http://www.apache.org/licenses/LICENSE-2.0).