Skip to content

feat: add BTCD support in polar#1364

Open
Abdulkbk wants to merge 16 commits intojamaljsr:masterfrom
Abdulkbk:btcd-support-2
Open

feat: add BTCD support in polar#1364
Abdulkbk wants to merge 16 commits intojamaljsr:masterfrom
Abdulkbk:btcd-support-2

Conversation

@Abdulkbk
Copy link
Contributor

@Abdulkbk Abdulkbk commented Mar 3, 2026

Closes #302
Replaces #1357

Description

In this PR, we add BTCD, a Bitcoin full-node implementation written in Go, to Polar. Currently, only LND and litd support the btcd backend.

Steps to Test

  • Start a mix network with bitcoind and btcd nodes.

NOTE: Because the btcd image is not yet available on Docker Hub, you'll need to build it locally using the Dockerfile in this PR.

cd docker/btcd

docker build --build-arg checkout=allow-ip-regtest --build-arg git_url=https://github.com/Abdulkbk/btcd --tag=polarlightning/btcd:0.25.1 -f Dockerfile2 .

Notice that we're pointing to a btcd patch to build from. That's because the patch is needed before btcd can use a peer running in a separate Docker container to sync blocks. I have opened a PR addressing this.

Screenshots

Network creation:
image

Network started
image

We also add `create-wallet.sh` which helps us create the initial wallet setup.
Implement the btcd/btcwallet RPC client and service abstraction to
enable Polar to interact with btcd nodes the same way it does with
bitcoind.
This commit adds the methods for creating btcd and btcwallet
services in compose file.
In this commit, we ensure to check backend compatibility between
lightning nodes and bitcoin backend. Only Litd and LND supports
btcd backend.
In this committee we add Dockerfile2 which can be used to build a
BTCD image with the patch that let the nodes to sync in regtest.

docker build --build-arg checkout=allow-ip-regtest
--build-arg git_url=https://github.com/Abdulkbk/btcd
--tag=polarlightning/btcd:0.25.1 -f Dockerfile2 .
@Abdulkbk Abdulkbk changed the title Feat: Integrate BTCD in Polar feat: add BTCD support in polar Mar 3, 2026
@codecov
Copy link

codecov bot commented Mar 3, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 100.00%. Comparing base (711755f) to head (3d2bc61).

Additional details and impacted files
@@            Coverage Diff             @@
##            master     #1364    +/-   ##
==========================================
  Coverage   100.00%   100.00%            
==========================================
  Files          211       213     +2     
  Lines         7014      7274   +260     
  Branches      1398      1463    +65     
==========================================
+ Hits          7014      7274   +260     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@greptile-apps
Copy link

greptile-apps bot commented Mar 3, 2026

Greptile Summary

This PR integrates BTCD — a Go-based Bitcoin full-node implementation — into Polar as a new bitcoin backend option, alongside the existing bitcoind. Currently only LND and litd support btcd as a backend. The integration spans a full stack: Docker images (btcd + a companion btcwallet service), a new BtcdService/BtcdApi layer, updates to the compose file generator and node templates, port/constant definitions, a first-start bootstrap flow that acquires a mining address and restarts btcd, and UI changes to expose btcd node-count selection and connection details.

Key changes:

  • Adds BtcdNode type, btcdCredentials, and BasePorts.btcd constants; wires btcdService into bitcoinFactory.
  • Introduces a companion btcwallet container for each btcd node to provide wallet RPC; the btcdApi communicates with btcwallet over HTTPS (rejectUnauthorized: false).
  • Handles the btcd --miningaddr requirement at startup by deriving an address on first run, updating the compose file, and restarting the container.
  • Filters btcd from incompatible lightning backends (only LND/litd are supported); mounts btcd's TLS cert into LND/litd volumes.

Issues identified:

  • Security issue (src/shared/utils.ts): Object.assign(defaultOptions, options) mutates the shared module-level default. After the first btcd call with rejectUnauthorized: false, the mutated default persists and every subsequent httpRequest call that omits the flag will also skip TLS verification.
  • Design issue (src/utils/network.ts): The || n.implementation === 'btcd' guard in filterCompatibleBackends unconditionally bypasses version-compatibility checks for btcd, which could present incompatible btcd versions as valid backends for future LND/litd releases.
  • Code hygiene (src/utils/network.ts): Commented-out code left in createNetwork attempting to pin btcd version.
  • Credential mismatch (src/lib/docker/composeFile.ts): addBtcwallet uses bitcoinCredentials instead of btcdCredentials.
  • Configuration management (src/lib/docker/composeFile.ts): btcwallet image version is hardcoded instead of managed through defaultRepoState.
  • Certificate handling (docker/btcd/btcwallet/create-wallet.sh): TLS cert is regenerated on every container restart instead of being created once.
  • Documentation (docker/btcd/btcwallet/Dockerfile): VOLUME declaration points to /root/.btcwallet instead of the actual runtime path /home/btcwallet/.btcwallet.

Confidence Score: 3/5

  • PR is functionally complete for a first btcd integration but has a critical TLS validation bypass and unresolved design issues that should be addressed before merge.
  • The PR adds a well-architected BTCD backend integration with proper first-start mining-address bootstrap and peer connection handling. However, two critical issues reduce confidence: (1) Object.assign(defaultOptions, options) in src/shared/utils.ts mutates a shared object, causing rejectUnauthorized: false from btcd API calls to permanently disable TLS verification for all subsequent HTTPS calls in the entire application; (2) the btcd version-compatibility bypass in filterCompatibleBackends silently ignores version constraints and will not scale to future LND/litd releases that may require specific btcd versions. Additionally, the btcwallet image version is hardcoded, credentials use the wrong constant, TLS certificates are regenerated on every restart, and the Dockerfile has a misleading VOLUME declaration.
  • src/shared/utils.ts (critical: TLS mutation), src/utils/network.ts (design: version bypass + commented code), src/lib/docker/composeFile.ts (wrong credential constant, hardcoded image), docker/btcd/btcwallet/create-wallet.sh (cert handling), docker/btcd/btcwallet/Dockerfile (VOLUME path).

Sequence Diagram

sequenceDiagram
    participant Polar as Polar UI
    participant Network as NetworkModel
    participant Docker as DockerService
    participant BtcdSvc as BtcdService
    participant BtcdAPI as BtcdApi (HTTPS)
    participant BtcdC as btcd container
    participant WalletC as btcwallet container
    participant LND as LND/litd container

    Polar->>Network: startNetwork()
    Network->>Docker: saveComposeFile(network)
    Docker->>Docker: addBtcd(node) + addBtcwallet(node)
    Network->>Docker: startNode(btcdNode)
    Docker->>BtcdC: docker compose up btcd
    Docker->>WalletC: docker compose up btcwallet-backend1
    WalletC->>WalletC: create-wallet.sh (create wallet + TLS cert)

    Network->>BtcdSvc: waitUntilOnline(node)
    BtcdSvc->>BtcdAPI: getBlockchainInfo()
    BtcdAPI->>WalletC: POST https://127.0.0.1:{btcdWallet} (Basic auth)
    WalletC-->>BtcdAPI: response

    Network->>BtcdSvc: createDefaultWallet(node)
    BtcdSvc->>BtcdAPI: walletpassphrase (unlock)
    BtcdSvc->>BtcdAPI: getnewaddress → set miningAddr on node

    alt First start (no miningAddr previously)
        Network->>Docker: saveComposeFile (inject --miningaddr flag)
        Network->>Docker: startNode(btcdNode) restart with mining addr
        Network->>BtcdSvc: waitUntilOnline + connectPeers
    end

    Network->>BtcdSvc: connectPeers(node)
    BtcdSvc->>BtcdAPI: addnode(peer)

    Network->>Docker: startNode(lndNode)
    Docker->>LND: docker compose up lnd
    Note over LND,BtcdC: LND mounts btcd cert at ~/.btcd/rpc.cert<br/>connects via --btcd.rpchost / --btcd.dir
    LND->>BtcdC: JSON-RPC (TLS, btcdCredentials)
Loading

Last reviewed commit: 3d2bc61

Copy link

@greptile-apps greptile-apps bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

89 files reviewed, 7 comments

Edit Code Review Agent Settings | Greptile

Comment on lines +40 to +45
const {
method,
headers,
body,
rejectUnauthorized = true,
} = Object.assign(defaultOptions, options);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.assign(defaultOptions, options) mutates the module-level defaultOptions object in place. Because the btcd API always passes rejectUnauthorized: false, after the very first btcd call the global default permanently becomes { ..., rejectUnauthorized: false }. Any subsequent HTTPS call that doesn't explicitly set rejectUnauthorized: true will silently skip TLS verification.

The = true default in the destructure only fires when the property is undefined — once defaultOptions.rejectUnauthorized is set to false, it will never be undefined again, so the default is never used.

Fix by spreading into a fresh object instead of mutating:

Suggested change
const {
method,
headers,
body,
rejectUnauthorized = true,
} = Object.assign(defaultOptions, options);
const {
method,
headers,
body,
rejectUnauthorized = true,
} = { ...defaultOptions, ...options };

Comment on lines +187 to 189
const compatibleBackends = filteredBackends.filter(
n => isVersionCompatible(n.version, requiredVersion) || n.implementation === 'btcd',
);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The || n.implementation === 'btcd' short-circuit means a btcd node is always included in compatible backends regardless of version checks. If a future LND/litd version requires a minimum btcd version, this logic will present incompatible btcd nodes as valid choices.

If the intent is that btcd compatibility is not tracked yet, this should be documented. Consider either (a) removing the || n.implementation === 'btcd' until btcd version requirements are defined, or (b) adding a btcd entry to the compatibility table and removing the bypass. A clarifying comment explaining the intended behavior would also help future maintainers.

Suggested change
const compatibleBackends = filteredBackends.filter(
n => isVersionCompatible(n.version, requiredVersion) || n.implementation === 'btcd',
);
const compatibleBackends = filteredBackends.filter(n => {
if (n.implementation === 'btcd') return true; // btcd compatibility not yet defined
return isVersionCompatible(n.version, requiredVersion);
});

Comment on lines +606 to +609
// if (lndNodes > 0) {
// const compat = repoState.images.LND.compatibility as Record<string, string>;
// version = compat[repoState.images.LND.latest];
// }
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Commented-out code left in production. This appears to be an incomplete attempt to pin the btcd version to LND compatibility. Remove the commented block or replace with a clear // TODO: comment explaining the deferred work:

Suggested change
// if (lndNodes > 0) {
// const compat = repoState.images.LND.compatibility as Record<string, string>;
// version = compat[repoState.images.LND.latest];
// }
// add managed btcd nodes
range(btcdNodes).forEach(() => {
const version = repoState.images.btcd.latest;
// TODO: pin btcd version to LND compatibility once btcd compatibility table is defined
const cmd = getImageCommand(managedImages, 'btcd', version);

Comment on lines +301 to +304
const variables = {
rpcUser: bitcoinCredentials.user,
rpcPass: bitcoinCredentials.pass,
nodeName: backend.name,
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wrong credential constant used. bitcoinCredentials is used instead of btcdCredentials. Both currently resolve to the same values (polaruser/polarpass), so there is no runtime breakage today. However, using the wrong constant couples these two independent credential sets. Any future change to bitcoinCredentials would silently misconfigure the btcwallet RPC server and break btcd API authentication.

Suggested change
const variables = {
rpcUser: bitcoinCredentials.user,
rpcPass: bitcoinCredentials.pass,
nodeName: backend.name,
const variables = {
rpcUser: btcdCredentials.user,
rpcPass: btcdCredentials.pass,
nodeName: backend.name,
};

};

// use the node's image
const image = `polarlightning/btcwallet:0.16.13`;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The btcwallet image version is hardcoded as polarlightning/btcwallet:0.16.13 and cannot be configured through Polar's defaultRepoState / managed-image flow. Every other companion image (e.g., tapd, litd) is driven by dockerConfigs and repoState. Hardcoding the version makes future upgrades a manual code change rather than a configuration update, and it means the version isn't surfaced in the UI.

Consider adding a btcwallet entry to defaultRepoState.images and dockerConfigs so the version can be managed consistently alongside the rest of the node images.

Comment on lines +37 to +40
# Create self-signed certificate for TLS
echo "Creating self-signed certificate for TLS"
openssl ecparam -genkey -name prime256v1 -noout -out ${BTCWALLET_DIR}/rpc.key
openssl req -new -x509 -sha256 -days 365 -key ${BTCWALLET_DIR}/rpc.key -out ${BTCWALLET_DIR}/rpc.cert -subj "/CN=127.0.0.1"
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The TLS key/cert generation commands run unconditionally, outside the wallet-existence check. This means a new key-pair is generated each time btcwallet starts, even when the wallet already exists. While this doesn't break the API today (since rejectUnauthorized: false is used), constantly-rotating certificates is poor practice. Wrap the cert generation in a guard:

Suggested change
# Create self-signed certificate for TLS
echo "Creating self-signed certificate for TLS"
openssl ecparam -genkey -name prime256v1 -noout -out ${BTCWALLET_DIR}/rpc.key
openssl req -new -x509 -sha256 -days 365 -key ${BTCWALLET_DIR}/rpc.key -out ${BTCWALLET_DIR}/rpc.cert -subj "/CN=127.0.0.1"
if [ ! -f "${BTCWALLET_DIR}/rpc.cert" ]; then
echo "Creating self-signed certificate for TLS"
openssl ecparam -genkey -name prime256v1 -noout -out ${BTCWALLET_DIR}/rpc.key
openssl req -new -x509 -sha256 -days 365 -key ${BTCWALLET_DIR}/rpc.key \
-out ${BTCWALLET_DIR}/rpc.cert -subj "/CN=127.0.0.1"
fi

COPY create-wallet.sh /create-wallet.sh
RUN chmod a+x /entrypoint.sh /create-wallet.sh

VOLUME ["/root/.btcwallet"]
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The VOLUME declaration points to /root/.btcwallet instead of the actual runtime path. The entrypoint creates and runs as user btcwallet (home directory /home/btcwallet), so btcwallet's data is written to /home/btcwallet/.btcwallet. The VOLUME declaration is therefore dead code — the compose file correctly overrides it — but it is misleading for anyone reading the Dockerfile.

Suggested change
VOLUME ["/root/.btcwallet"]
VOLUME ["/home/btcwallet/.btcwallet"]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Feature Request: btcd node

1 participant