Conversation
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 .
Codecov Report✅ All modified and coverable lines are covered by tests. 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. 🚀 New features to boost your workflow:
|
Greptile SummaryThis PR integrates BTCD — a Go-based Bitcoin full-node implementation — into Polar as a new bitcoin backend option, alongside the existing Key changes:
Issues identified:
Confidence Score: 3/5
Sequence DiagramsequenceDiagram
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)
Last reviewed commit: 3d2bc61 |
| const { | ||
| method, | ||
| headers, | ||
| body, | ||
| rejectUnauthorized = true, | ||
| } = Object.assign(defaultOptions, options); |
There was a problem hiding this comment.
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:
| const { | |
| method, | |
| headers, | |
| body, | |
| rejectUnauthorized = true, | |
| } = Object.assign(defaultOptions, options); | |
| const { | |
| method, | |
| headers, | |
| body, | |
| rejectUnauthorized = true, | |
| } = { ...defaultOptions, ...options }; |
| const compatibleBackends = filteredBackends.filter( | ||
| n => isVersionCompatible(n.version, requiredVersion) || n.implementation === 'btcd', | ||
| ); |
There was a problem hiding this comment.
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.
| 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); | |
| }); |
| // if (lndNodes > 0) { | ||
| // const compat = repoState.images.LND.compatibility as Record<string, string>; | ||
| // version = compat[repoState.images.LND.latest]; | ||
| // } |
There was a problem hiding this comment.
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:
| // 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); |
| const variables = { | ||
| rpcUser: bitcoinCredentials.user, | ||
| rpcPass: bitcoinCredentials.pass, | ||
| nodeName: backend.name, |
There was a problem hiding this comment.
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.
| 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`; |
There was a problem hiding this comment.
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.
| # 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" |
There was a problem hiding this comment.
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:
| # 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"] |
There was a problem hiding this comment.
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.
| VOLUME ["/root/.btcwallet"] | |
| VOLUME ["/home/btcwallet/.btcwallet"] |
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
bitcoindandbtcdnodes.NOTE: Because the
btcdimage is not yet available on Docker Hub, you'll need to build it locally using theDockerfilein this PR.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:

Network started
