Skip to Content
Partner IntegrationBSA Integration Guide

BSA Integration Guide

Bitcoin Smart Accounts (BSA) lock BTC into a Taproot vault for use in DeFi, with a depositor-controlled unilateral exit.

You construct, sign, and broadcast the Bitcoin transactions yourself — lock construction and verification use the bsa-core Golang library, and sign using your own key infrastructure.

Lombard Security Consortium is the token operator of the BSA protocol and provides a web API to get protocol parameters, submit deposit information (comprised of multiple UTXOs/tranches) and view protocol status for each tranche. Full runnable reference: bsa-cli (scripts/lock.sh, scripts/unilateral-unlock.sh, internal/lock).

Technical Infrastructure Requirements

The integrator infrastructure must support the following capabilities:

  • R1 — Make HTTPS calls to the Lombard API
  • R2 — Sign Taproot (BIP-341) Bitcoin transactions in form of PSBTs according to BIP-174
  • R3 — Broadcast Bitcoin transactions
  • R4 — Use Child Pays For Parent (CPFP) mechanic to bump transaction fees, when needed
  • R5 — Execute transactions on the Ethereum blockchain to call methods on a given smart contract
  • R6 — Run an Arbitration Oracle. More details in the dedicated integration guidelines.

Protocol flows

The following steps provide a description on how to generate and verify PSBTs needed at each step of the protocol. Infrastructure requirements are reported in parenthesis.

Lock flow

1. Generate — derive the vault address and build the unsigned PSBTs (lock transaction + unlock, rebalance, and cooperative-unlock challenges).

import ( "encoding/json" "net/http" bsa "github.com/lombard-finance/bsa-core" bsalock "github.com/lombard-finance/bsa-core/lock" ) // 1) Fetch protocol params (per-chain arbiters + SAR, token operator, HRP, timelocks). req, _ := http.NewRequest(http.MethodGet, "https://api.devnet-bft.lombard-fi.com/v2/bsa/protocol/params", nil) req.Header.Set("Authorization", "Bearer "+jwt) resp, _ := http.DefaultClient.Do(req) var pp struct { Params struct { Bech32HRPSegwit string `json:"bech32_hrp_segwit"` MinerFeeSat string `json:"miner_fee_sat"` UnlockingTimeBlocks uint32 `json:"unlocking_time_blocks"` UnlockingChallengeTimeBlocks uint32 `json:"unlocking_challenge_time_blocks"` RebalanceChallengeTimeBlocks uint32 `json:"rebalance_challenge_time_blocks"` Chains map[string]struct { Arbiters []struct{ Pubkey string `json:"pubkey"` } `json:"arbiters"` SARAddress string `json:"sar_address"` TokenAddress string `json:"token_address"` } `json:"chains"` TokenOperators []struct{ Pubkey string `json:"pubkey"` } `json:"token_operators"` } `json:"params"` } json.NewDecoder(resp.Body).Decode(&pp) // 2) Map params -> bsa.Conf. Params are keyed by chain ID under `chains`; // pick the entry for your DeFi chain. hexToBytes/atoi are your helpers // (the params encode bytes as 0x-hex). chainID := defiChainID // 0x-hex chain-ID key, e.g. "0x...aa36a7" chain := pp.Params.Chains[chainID] conf := &bsa.Conf{ Bech32HRPSegwit: pp.Params.Bech32HRPSegwit, DefiChainID: hexToBytes(chainID), OperatorPK: hexToBytes(pp.Params.TokenOperators[0].Pubkey), TokenAddress: hexToBytes(chain.TokenAddress), UnlockingTimeBlocks: pp.Params.UnlockingTimeBlocks, UnlockingChallengeTimeBlocks: pp.Params.UnlockingChallengeTimeBlocks, RebalanceChallengeTimeBlocks: pp.Params.RebalanceChallengeTimeBlocks, MinerFeeSats: atoi(pp.Params.MinerFeeSat), } arbitrationPKs := pp.Params.Chains[chainID].Arbiters // any non-empty subset of arbiters can be chosen according to depositor specification. // 3) Build the vault address + unsigned PSBTs. depositorPK, fundingUTXOs, // tranches, defiAddress, nonce, and unlockScript are your inputs. vault, lockTx, psbtSets, err := bsalock.GenerateLock( conf, depositorPK, unlockScript, arbitrationPKs, defiAddress, conf.DefiChainID, nonce, []int64{100000, 200000}, fundingUTXOs, nil)

2. Sign — sign each PSBT in psbtSets from step 1 with the depositor key and extract the signatures.

Sign each PSBT with your depositor’s key using your own infrastructure (HSM, MPC, or wallet). These Schnorr signatures become the per-tranche unlock_challenge_sig, rebalance_challenge_sig, and cooperative_unlock_sig in the submit body. See bsa-cli internal/psbt and ExtractSignatureFromPSBT for a reference implementation of the PSBT signing/extraction mechanics.

3. Submit — register the vault, tranches, and signatures, then poll until the lock is accepted.

import ( "bytes" "encoding/json" "net/http" ) // body: JSON built from steps 1-2 (vault address, tranches + signatures) and the // protocol params. Full request/response schema: POST /v2/bsa/tranches (below). req, _ := http.NewRequest(http.MethodPost, "https://api.devnet-bft.lombard-fi.com/v2/bsa/tranches", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+jwt) req.Header.Set("Content-Type", "application/json") resp, err := http.DefaultClient.Do(req)

4. Verify — read the lock back from the SAR contract and verify it cryptographically.

import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" bsalock "github.com/lombard-finance/bsa-core/lock" // sar: your SAR contract binding, abigen'd from the SAR ABI ) // sarAddr = sars[].address from /v2/bsa/protocol/params (for your DeFi chain). // Read the on-chain tweak + operator signatures for the locked UTXO. utxo, rawTweak, unlockScript, err := sarContract.GetUTXODetailsWithTweak( &bind.CallOpts{}, txID32, vout) // utxo.Status == 1 once confirmed (else retry) sigs := [3][]byte{ utxo.Utxo.OperatorUnlockSig, utxo.Utxo.UnlockingChallengeResolutionSig, utxo.Utxo.RebalanceChallengeResolutionSig, } // Cryptographically verify the lock locally, using conf from step 1. err = bsalock.VerifyLock(conf, txID, vout, utxo.Utxo.Value.Int64(), int64(utxo.Fee), unlockScript, rawTweak, utxo.Utxo.DstAddress.Bytes(), sigs)

5. Broadcast — broadcast the signed lock transaction to Bitcoin.

import ( "encoding/json" "fmt" "github.com/btcsuite/btcd/rpcclient" ) client, err := rpcclient.New(&rpcclient.ConnConfig{ Host: rpcHost, User: rpcUser, Pass: rpcPass, HTTPPostMode: true, DisableTLS: true, }, nil) defer client.Shutdown() // signedLockTxHex: the fully-signed lock transaction from step 2. txid, err := client.RawRequest("sendrawtransaction", []json.RawMessage{json.RawMessage(fmt.Sprintf("%q", signedLockTxHex))})

Cooperative unlock flow

Reclaim the BTC skipping timelock waits — entirely client-side and on-chain, with no calls to Lombard API. Subject to an on-chain fee and reliant on Lombard Security Consortium collaboration.

1. Burn BTC.b — burn BTC.b on the SAR by specifying which UTXO to unlock with any of the unlock methods. Subject to a network fee in ETH that can be fetched via SAR method getCooperativeUnlockFee(uint256 utxosCount_)

2. Wait Funds — Lombard monitors the SAR and triggers a direct unlock from the vault to the depositor BTC address.

3. [OPTIONAL] Trigger unilateral unlock — If Lombard does not deliver funds, depositor can still proceed with the unilateral unlock flow.

Unilateral unlock flow

Reclaim the BTC once the timelock allows — entirely client-side and on-chain, with no Lombard involvement (e.g., calls to API below).

1. Burn BTC.b — burn BTC.b on the SAR by specifying which UTXO to unlock with any of the unilateralUnlock methods.

Warning Failure to set a UTXOs status to Unlocked, by burning the corresponding amount of BTC.b on the SAR before initiating an unlock on the Bitcoin network will result collateral rebalance. In this scenario, funds are not lost, but can only be redeemed from Lombard’s general reserves.

2. Generate — read the operator’s unlock signature + tweak from the SAR contract, then build the unlock PSBT.

import ( "github.com/ethereum/go-ethereum/accounts/abi/bind" // sar: your SAR contract binding (abigen) ) // sarAddr = sars[].address from the protocol params you fetched at lock time // (no API call here). The UTXO must be unlockable on-chain. utxo, rawTweak, unlockScript, err := sarContract.GetUTXODetailsWithTweak( &bind.CallOpts{}, txID32, vout) operatorUnlockSig := utxo.Utxo.OperatorUnlockSig // Build the unlock PSBT spending the locked UTXO to your unlock address, using // the tweak + operator signature. See bsa-cli internal/lock/unlock.go for the // construction (built on bsa-core tx and account helpers).

3. Sign — sign the unlock PSBT with the depositor key in your infrastructure, as in the lock flow’s Sign step.

4. Broadcast — broadcast the unlock transaction to Bitcoin.

// Obtain the finalized transaction via bsa-core method tx.ExtractUnlockTx() after the PSBT include both depositor signature (from your signing infra) and token operator signature (fetched from SAR) // Same as the lock flow's Broadcast step (rpcclient + sendrawtransaction), // with the signed unlock transaction hex. client.RawRequest("sendrawtransaction", []json.RawMessage{json.RawMessage(fmt.Sprintf("%q", signedUnlockTxHex))})

5. [OPTIONAL] Fee bumping — The unlock transaction includes a 330 sats output to the depositor unlock scriptPubKey. Such output can be used to bump the unilateral unlock transaction fees via CPFP mechanism in case network conditions prevent the transaction inclusion in a block because of low miner fees.

6. Wait timelock — Wait the timelock expiration after transaction is included in a block.

7. Complete unlock — Pull funds from the timelock address. bsa-core utility function tx.CreateAndSignSpendTimelockTx() can be used as reference to generate such transaction.

Arbitration Oracle resolution flows

In case the Arbitration Oracle is involved any challenge resolution because of an illegitimate operation by Lombard Security Consortium, a transaction is submitted to the Bitcoin blockchain to redirect funds to the depositor. Such a transaction, in periods of network congestion, may need a fee bump.

  1. Monitor Bitcoin blockchain — Challenge addresses to monitor, per each BSA instance, can be computed starting from SAR information

    1. Get BSA instance information from SAR via getUTXODetailsWithTweak()
    2. Parse the BSA instance information with bsa-core NewFromContractData()
    3. Derive Rebalance and Unlock challenge addresses to monitor

    Note This is the main task of the controller component in bsa-arbitration-controller, thus fulfilled by running an AO.

  2. [OPTIONAL] Bump fees with CPFP — A fee bump is necessary if ALL the following condition hold:

    1. there is a UTXO locked in any of the monitored challenge addresses
    2. a UTXO locked in a challenge address is inputed in a transaction to the depositor address
    3. the transaction spending the locked UTXO is not chosen from mempool for block inclusion because of low miner fees for a number of consecutive blocks
      1. this can be timestamped against the UCA/RCA relative timelock from when a. occured.

← Back to BSA Integration Overview

Last updated on