2026-06-11 00:24:54 -04:00
|
|
|
# Bitcoin RPC Relay for External Wallets
|
|
|
|
|
|
|
|
|
|
This note captures the pattern used to let an external wallet, such as Wasabi,
|
|
|
|
|
use an Archipelago Bitcoin node for transaction relay without exposing the
|
|
|
|
|
node's admin RPC credentials.
|
|
|
|
|
|
|
|
|
|
## Goal
|
|
|
|
|
|
|
|
|
|
Expose a public HTTPS JSON-RPC endpoint that can broadcast transactions and read
|
|
|
|
|
basic chain/mempool state, while preventing wallet and admin RPC access.
|
|
|
|
|
|
|
|
|
|
The endpoint should be fronted by nginx or another TLS reverse proxy:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
wallet client -> https://<subdomain>/ -> reverse proxy -> Archipelago node nginx -> bitcoind RPC
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Do not expose Bitcoin RPC credentials with wallet/admin access to external
|
|
|
|
|
users.
|
|
|
|
|
|
|
|
|
|
## Restricted RPC User
|
|
|
|
|
|
|
|
|
|
Create a separate RPC user, currently named `txrelay`, with an `rpcauth` secret
|
|
|
|
|
and a Bitcoin RPC whitelist.
|
|
|
|
|
|
|
|
|
|
Allowed RPC methods:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
sendrawtransaction
|
2026-06-12 03:00:15 -04:00
|
|
|
submitpackage
|
2026-06-11 00:24:54 -04:00
|
|
|
testmempoolaccept
|
|
|
|
|
getmempoolinfo
|
|
|
|
|
getrawmempool
|
|
|
|
|
getmempoolentry
|
|
|
|
|
getnetworkinfo
|
|
|
|
|
getblockchaininfo
|
|
|
|
|
getblockcount
|
|
|
|
|
getblockhash
|
|
|
|
|
getblockheader
|
|
|
|
|
getrawtransaction
|
2026-06-12 03:00:15 -04:00
|
|
|
gettxout
|
2026-06-11 00:24:54 -04:00
|
|
|
decoderawtransaction
|
|
|
|
|
decodescript
|
|
|
|
|
estimatesmartfee
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Wallet/admin access is denied by setting `-rpcwhitelistdefault=0` and giving the
|
|
|
|
|
`txrelay` user only the method whitelist above.
|
|
|
|
|
|
|
|
|
|
Secrets live under:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
/var/lib/archipelago/secrets/bitcoin-rpc-txrelay-password
|
|
|
|
|
/var/lib/archipelago/secrets/bitcoin-rpc-txrelay-rpcauth
|
|
|
|
|
/var/lib/archipelago/secrets/bitcoin-rpc-txrelay-client.env
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Do not commit these files or paste them into docs.
|
|
|
|
|
|
|
|
|
|
## Archipelago UI/API Flow
|
|
|
|
|
|
|
|
|
|
The productized flow is managed from the Bitcoin Core/Knots custom UI in the
|
|
|
|
|
`Transaction Relay Sharing` panel.
|
|
|
|
|
|
|
|
|
|
Implemented RPC methods:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
bitcoin.relay-status
|
|
|
|
|
bitcoin.relay-update-settings
|
|
|
|
|
bitcoin.relay-request-peer
|
|
|
|
|
bitcoin.relay-approve-request
|
|
|
|
|
bitcoin.relay-reject-request
|
|
|
|
|
bitcoin.relay-create-tor-service
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
When peer sharing is enabled, `bitcoin.relay-update-settings` automatically
|
|
|
|
|
provisions the restricted `txrelay` password, `rpcauth`, and client env file if
|
|
|
|
|
they do not already exist. If those files were just generated, restart Bitcoin
|
|
|
|
|
Core/Knots so `bitcoind` reloads the `txrelay` `rpcauth` and whitelist flags.
|
|
|
|
|
|
|
|
|
|
The UI shows:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
HTTP / HTTPS / Tor relay endpoint settings
|
|
|
|
|
local sync status
|
|
|
|
|
restricted credential readiness, without printing the password
|
|
|
|
|
trusted peer dropdown, disabled until the local node is synchronized
|
|
|
|
|
incoming relay requests with approve/reject actions
|
|
|
|
|
outbound relay requests and approval status
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Approving an incoming peer request sends the selected endpoint plus restricted
|
|
|
|
|
`txrelay` credentials through the existing encrypted peer-message path. On the
|
|
|
|
|
requesting node, approved peer credentials are stored in a per-peer secret env
|
|
|
|
|
file:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
/var/lib/archipelago/secrets/bitcoin-relay-peer-<peer-pubkey-prefix>.env
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The UI returns the credential secret path and approved endpoint metadata, but it
|
|
|
|
|
does not display the raw password.
|
|
|
|
|
|
|
|
|
|
For dev review, the mock server exposes the Bitcoin UI at:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
http://localhost:8102/app/bitcoin-ui/
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Bitcoin Startup Flags
|
|
|
|
|
|
|
|
|
|
The Bitcoin Knots app should add the restricted user only when the secret exists:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
RPC_TXRELAY_AUTH="$(printenv BITCOIN_RPC_TXRELAY_RPCAUTH || true)"
|
|
|
|
|
RPC_TXRELAY_FLAGS="-rpcwhitelistdefault=0"
|
|
|
|
|
if [ -n "$RPC_TXRELAY_AUTH" ]; then
|
2026-06-12 03:00:15 -04:00
|
|
|
RPC_TXRELAY_FLAGS="$RPC_TXRELAY_FLAGS -rpcauth=$RPC_TXRELAY_AUTH -rpcwhitelist=txrelay:sendrawtransaction,submitpackage,testmempoolaccept,getmempoolinfo,getrawmempool,getmempoolentry,getnetworkinfo,getblockchaininfo,getblockcount,getblockhash,getblock,getblockheader,getrawtransaction,gettxout,gettxspendingprevout,decoderawtransaction,decodescript,estimatesmartfee,uptime,ping,getconnectioncount,getpeerinfo,getindexinfo,getdeploymentinfo,getchaintips"
|
2026-06-11 00:24:54 -04:00
|
|
|
fi
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Then include `$RPC_TXRELAY_FLAGS` in the `bitcoind` command. Keep the local
|
|
|
|
|
`archipelago` RPC user unrestricted for internal services by using
|
|
|
|
|
`-rpcwhitelistdefault=0` and only setting a whitelist for `txrelay`.
|
|
|
|
|
|
|
|
|
|
The current implementation touches:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
apps/bitcoin-knots/manifest.yml
|
|
|
|
|
scripts/container-specs.sh
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Node nginx
|
|
|
|
|
|
|
|
|
|
The Archipelago node can expose a host-based nginx vhost that proxies to local
|
|
|
|
|
Bitcoin RPC:
|
|
|
|
|
|
|
|
|
|
```nginx
|
|
|
|
|
limit_req_zone $binary_remote_addr zone=bitcoin_rpc_ext:10m rate=5r/s;
|
|
|
|
|
|
|
|
|
|
server {
|
|
|
|
|
listen 80;
|
|
|
|
|
server_name rpc.example.com;
|
|
|
|
|
|
|
|
|
|
client_max_body_size 2m;
|
|
|
|
|
|
|
|
|
|
location / {
|
|
|
|
|
limit_req zone=bitcoin_rpc_ext burst=20 nodelay;
|
|
|
|
|
limit_req_status 429;
|
|
|
|
|
|
|
|
|
|
proxy_pass http://127.0.0.1:8332;
|
|
|
|
|
proxy_http_version 1.1;
|
|
|
|
|
proxy_set_header Host $host;
|
|
|
|
|
proxy_set_header X-Real-IP $remote_addr;
|
|
|
|
|
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
|
|
|
|
proxy_set_header X-Forwarded-Proto $scheme;
|
|
|
|
|
proxy_connect_timeout 5s;
|
|
|
|
|
proxy_send_timeout 120s;
|
|
|
|
|
proxy_read_timeout 120s;
|
|
|
|
|
proxy_buffering off;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If another public reverse proxy terminates TLS, point it at:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
http://<archipelago-lan-ip>:80
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For the tested node the LAN upstream was:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
http://192.168.1.116:80
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The public proxy should serve a valid TLS certificate for the chosen subdomain.
|
|
|
|
|
|
|
|
|
|
## DNS and Routing
|
|
|
|
|
|
|
|
|
|
Use a subdomain that resolves to the public reverse proxy:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
Type: A
|
|
|
|
|
Host/Name: <subdomain-only>
|
|
|
|
|
Value: <public-ip>
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
For example, if the desired hostname is `rpc.example.com`, the DNS host/name
|
|
|
|
|
field is usually only `rpc`, not the full `rpc.example.com`. Entering the full
|
|
|
|
|
hostname in some DNS panels can accidentally create:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
rpc.example.com.example.com
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
The public proxy should forward:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
TCP 443 -> TLS reverse proxy for the subdomain
|
|
|
|
|
TCP 80 -> optional, needed for HTTP-01 certificate issuance or redirects
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
If the public proxy is separate from the Archipelago node, configure it with:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
server_name: <subdomain>
|
|
|
|
|
scheme: http
|
|
|
|
|
upstream host: <archipelago-lan-ip>
|
|
|
|
|
upstream port: 80
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Verification
|
|
|
|
|
|
|
|
|
|
Check authoritative DNS:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
dig @<authoritative-dns-ip> <subdomain> A +noall +answer +authority
|
|
|
|
|
dig @1.1.1.1 +short <subdomain> A
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check TLS:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
openssl s_client -connect <subdomain>:443 -servername <subdomain> </dev/null
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check the public RPC path:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
. /var/lib/archipelago/secrets/bitcoin-rpc-txrelay-client.env
|
|
|
|
|
|
|
|
|
|
curl -sS --user "$BITCOIN_RPC_TXRELAY_USER:$BITCOIN_RPC_TXRELAY_PASSWORD" \
|
|
|
|
|
--data-binary '{"jsonrpc":"1.0","id":"check","method":"getblockchaininfo","params":[]}' \
|
|
|
|
|
"<relay-endpoint-url>"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Check that transaction broadcast reaches Bitcoin RPC, without needing a real
|
|
|
|
|
transaction:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
curl -sS --user "$BITCOIN_RPC_TXRELAY_USER:$BITCOIN_RPC_TXRELAY_PASSWORD" \
|
|
|
|
|
--data-binary '{"jsonrpc":"1.0","id":"badtx","method":"sendrawtransaction","params":["00"]}' \
|
|
|
|
|
"<relay-endpoint-url>"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Expected result is a Bitcoin RPC validation error such as `TX decode failed`,
|
|
|
|
|
which confirms the request reached `sendrawtransaction`.
|
|
|
|
|
|
2026-06-12 03:00:15 -04:00
|
|
|
If a wallet verifies the connection but reports `RPC Forbidden` during
|
|
|
|
|
broadcast, the credentials authenticated but the broadcast method was outside
|
|
|
|
|
the loaded `txrelay` whitelist. Restart the active Bitcoin backend after
|
|
|
|
|
updating the whitelist, then test both `sendrawtransaction` and, for newer
|
|
|
|
|
package-relay clients, `submitpackage`. Also confirm the public reverse proxy
|
|
|
|
|
passes the wallet's `Authorization` header through to `127.0.0.1:8332`; do not
|
|
|
|
|
point public wallet traffic at the Bitcoin UI `/bitcoin-rpc/` helper, because
|
|
|
|
|
that helper injects the local dashboard credential.
|
|
|
|
|
|
2026-06-11 00:24:54 -04:00
|
|
|
Check that wallet/admin RPC is blocked:
|
|
|
|
|
|
|
|
|
|
```sh
|
|
|
|
|
curl -sS -o /tmp/txrelay-deny.json -w '%{http_code}\n' \
|
|
|
|
|
--user "$BITCOIN_RPC_TXRELAY_USER:$BITCOIN_RPC_TXRELAY_PASSWORD" \
|
|
|
|
|
--data-binary '{"jsonrpc":"1.0","id":"deny","method":"listwallets","params":[]}' \
|
|
|
|
|
"<relay-endpoint-url>"
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
Expected result:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
403
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
## Tested Outcome
|
|
|
|
|
|
|
|
|
|
The working endpoint used in this setup was:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
https://shard.tx1138.com/
|
|
|
|
|
```
|
|
|
|
|
|
|
|
|
|
It was verified with:
|
|
|
|
|
|
|
|
|
|
```text
|
|
|
|
|
DNS resolves
|
|
|
|
|
TLS certificate is valid
|
|
|
|
|
txrelay credentials authenticate
|
|
|
|
|
getblockchaininfo returns chain=main
|
|
|
|
|
sendrawtransaction reaches Bitcoin RPC
|
|
|
|
|
listwallets is blocked for txrelay
|
|
|
|
|
```
|