mirror of
https://github.com/aljazceru/lspd.git
synced 2025-12-20 15:24:23 +01:00
automation
This commit is contained in:
23
.github/actions/build-lspd/action.yaml
vendored
Normal file
23
.github/actions/build-lspd/action.yaml
vendored
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
name: 'Build LSPD'
|
||||||
|
description: 'Build LSPD and upload the build artifacts.'
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Build LSPD
|
||||||
|
run: |
|
||||||
|
go get github.com/breez/lspd
|
||||||
|
go get github.com/breez/lspd/cln_plugin
|
||||||
|
go build .
|
||||||
|
go build -o lspd_plugin ./cln_plugin/cmd
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: build-artifacts
|
||||||
|
path: |
|
||||||
|
./lspd
|
||||||
|
./lspd_plugin
|
||||||
36
.github/actions/process-test-state/action.yaml
vendored
Normal file
36
.github/actions/process-test-state/action.yaml
vendored
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
name: "Process Test State"
|
||||||
|
description: "Check, tar and upload test state"
|
||||||
|
inputs:
|
||||||
|
artifact-name:
|
||||||
|
description: "Name of the artifact"
|
||||||
|
required: true
|
||||||
|
default: "test_state_artifact"
|
||||||
|
test-state-path:
|
||||||
|
description: "Path of the test state directory"
|
||||||
|
required: true
|
||||||
|
default: "/home/runner/test_state"
|
||||||
|
runs:
|
||||||
|
using: "composite"
|
||||||
|
steps:
|
||||||
|
- name: Check if test_state directory exists
|
||||||
|
id: check-test-state
|
||||||
|
run: |
|
||||||
|
if [ -d "${{ inputs.test-state-path }}" ]; then
|
||||||
|
echo "exists=true" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "exists=false" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Tar state
|
||||||
|
run: |
|
||||||
|
find ${{ inputs.test-state-path }} -type f -o -type d | tar -czf ${{ inputs.test-state-path }}.tar.gz -T -
|
||||||
|
shell: bash
|
||||||
|
if: env.exists == 'true'
|
||||||
|
|
||||||
|
- name: Upload test_state as artifact
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: ${{ inputs.artifact-name }}
|
||||||
|
path: ${{ inputs.test-state-path }}.tar.gz
|
||||||
|
if: env.exists == 'true'
|
||||||
42
.github/actions/setup-bitcoin/action.yaml
vendored
Normal file
42
.github/actions/setup-bitcoin/action.yaml
vendored
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
name: 'Setup Bitcoin Core'
|
||||||
|
description: 'Download and install Bitcoin Core'
|
||||||
|
inputs:
|
||||||
|
bitcoin-version:
|
||||||
|
description: 'Version of Bitcoin Core'
|
||||||
|
required: true
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Cache Bitcoin Core
|
||||||
|
id: cache-bitcoin
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}/bin/bitcoind
|
||||||
|
~/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}/bin/bitcoin-cli
|
||||||
|
key: bitcoin-core-${{ inputs.bitcoin-version }}
|
||||||
|
|
||||||
|
- name: Setup dependencies
|
||||||
|
if: steps.cache-bitcoin.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
sudo apt-get update
|
||||||
|
sudo apt-get install -y axel
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Download and install Bitcoin Core
|
||||||
|
if: steps.cache-bitcoin.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
mkdir -p ~/bitcoin-core-${{ inputs.bitcoin-version }}
|
||||||
|
cd ~/bitcoin-core-${{ inputs.bitcoin-version }}
|
||||||
|
axel https://bitcoincore.org/bin/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}-x86_64-linux-gnu.tar.gz
|
||||||
|
tar -xzf bitcoin-${{ inputs.bitcoin-version }}-x86_64-linux-gnu.tar.gz
|
||||||
|
rm bitcoin-${{ inputs.bitcoin-version }}-x86_64-linux-gnu.tar.gz
|
||||||
|
sudo cp ~/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}/bin/bitcoind /usr/bin/
|
||||||
|
sudo cp ~/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}/bin/bitcoin-cli /usr/bin/
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Copy Binaries
|
||||||
|
run: |
|
||||||
|
sudo cp ~/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}/bin/bitcoind /usr/bin/
|
||||||
|
sudo cp ~/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}/bin/bitcoin-cli /usr/bin/
|
||||||
|
shell: bash
|
||||||
109
.github/actions/setup-clightning/action.yaml
vendored
Normal file
109
.github/actions/setup-clightning/action.yaml
vendored
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
name: 'Setup Core Lightning'
|
||||||
|
description: 'Set up Core Lightning on the runner'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
checkout-version:
|
||||||
|
description: 'v23.05.1'
|
||||||
|
required: true
|
||||||
|
default: 'v23.05.1'
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Cache Core Lightning
|
||||||
|
id: cache-core-lightning
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
lightning_git/lightningd/lightning_hsmd
|
||||||
|
lightning_git/lightningd/lightning_gossipd
|
||||||
|
lightning_git/lightningd/lightning_openingd
|
||||||
|
lightning_git/lightningd/lightning_dualopend
|
||||||
|
lightning_git/lightningd/lightning_channeld
|
||||||
|
lightning_git/lightningd/lightning_closingd
|
||||||
|
lightning_git/lightningd/lightning_onchaind
|
||||||
|
lightning_git/lightningd/lightning_connectd
|
||||||
|
lightning_git/lightningd/lightning_websocketd
|
||||||
|
lightning_git/lightningd/lightningd
|
||||||
|
lightning_git/plugins/offers
|
||||||
|
lightning_git/plugins/topology
|
||||||
|
lightning_git/plugins/spenderp
|
||||||
|
lightning_git/plugins/test/run-route-overlong
|
||||||
|
lightning_git/plugins/test/run-funder_policy
|
||||||
|
lightning_git/plugins/pay
|
||||||
|
lightning_git/plugins/bkpr/test/run-bkpr_db
|
||||||
|
lightning_git/plugins/bkpr/test/run-recorder
|
||||||
|
lightning_git/plugins/funder
|
||||||
|
lightning_git/plugins/bookkeeper
|
||||||
|
lightning_git/plugins/txprepare
|
||||||
|
lightning_git/plugins/keysend
|
||||||
|
lightning_git/plugins/fetchinvoice
|
||||||
|
lightning_git/plugins/bcli
|
||||||
|
lightning_git/plugins/cln-grpc
|
||||||
|
lightning_git/plugins/commando
|
||||||
|
lightning_git/plugins/autoclean
|
||||||
|
lightning_git/plugins/chanbackup
|
||||||
|
lightning_git/plugins/sql
|
||||||
|
lightning_git/cli/lightning-cli
|
||||||
|
lightning_git/devtools/bolt11-cli
|
||||||
|
lightning_git/devtools/decodemsg
|
||||||
|
lightning_git/devtools/onion
|
||||||
|
lightning_git/devtools/dump-gossipstore
|
||||||
|
lightning_git/devtools/gossipwith
|
||||||
|
lightning_git/devtools/create-gossipstore
|
||||||
|
lightning_git/devtools/mkcommit
|
||||||
|
lightning_git/devtools/mkfunding
|
||||||
|
lightning_git/devtools/mkclose
|
||||||
|
lightning_git/devtools/mkgossip
|
||||||
|
key: core-lightning-${{ inputs.checkout-version }}
|
||||||
|
|
||||||
|
- name: Setup Python 3.8
|
||||||
|
if: steps.cache-core-lightning.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/setup-python@v4
|
||||||
|
with:
|
||||||
|
python-version: 3.8
|
||||||
|
|
||||||
|
- name: Setup Rust
|
||||||
|
if: steps.cache-core-lightning.outputs.cache-hit != 'true'
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Install dependencies
|
||||||
|
if: steps.cache-core-lightning.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
sudo apt-get install -y autoconf automake build-essential git libtool libgmp-dev libsqlite3-dev python3 python3-pip net-tools zlib1g-dev libsodium-dev gettext valgrind libpq-dev shellcheck cppcheck libsecp256k1-dev jq
|
||||||
|
sudo apt-get remove -y protobuf-compiler
|
||||||
|
curl -OL https://github.com/protocolbuffers/protobuf/releases/download/v3.12.0/protoc-3.12.0-linux-x86_64.zip
|
||||||
|
sudo unzip -o protoc-3.12.0-linux-x86_64.zip -d /usr/local bin/protoc
|
||||||
|
sudo unzip -o protoc-3.12.0-linux-x86_64.zip -d /usr/local 'include/*'
|
||||||
|
rm -f protoc-3.12.0-linux-x86_64.zip
|
||||||
|
sudo chmod 755 /usr/local/bin/protoc
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Install Python dependencies
|
||||||
|
if: steps.cache-core-lightning.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
pip3 install --upgrade pip
|
||||||
|
pip3 install poetry mako
|
||||||
|
pip install grpcio-tools
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Checkout and build lightning
|
||||||
|
if: steps.cache-core-lightning.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: ElementsProject/lightning
|
||||||
|
ref: ${{ inputs.checkout-version }}
|
||||||
|
path: lightning_git
|
||||||
|
|
||||||
|
- name: Build Lightning
|
||||||
|
if: steps.cache-core-lightning.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
cd lightning_git
|
||||||
|
./configure --enable-developer --enable-rust
|
||||||
|
poetry install
|
||||||
|
poetry run make -j `nproc`
|
||||||
|
shell: bash
|
||||||
16
.github/actions/setup-itest/action.yaml
vendored
Normal file
16
.github/actions/setup-itest/action.yaml
vendored
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
name: 'Cache itest'
|
||||||
|
description: 'Fetch LSPD Integration Test and cache the go directory'
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Cache itest
|
||||||
|
id: cache-itest
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go
|
||||||
|
key: itest
|
||||||
|
- name: Get LSPD Integration Test
|
||||||
|
if: steps.cache-itest.outputs.cache-hit != 'true'
|
||||||
|
run: go get github.com/breez/lspd/itest
|
||||||
|
shell: bash
|
||||||
45
.github/actions/setup-lnd-client/action.yaml
vendored
Normal file
45
.github/actions/setup-lnd-client/action.yaml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: 'Setup LND Client'
|
||||||
|
description: 'Set up LND for the Client on the runner'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
client-ref:
|
||||||
|
description: 'The Git reference for the Client version of LND'
|
||||||
|
required: true
|
||||||
|
default: 'v0.16.2-breez'
|
||||||
|
|
||||||
|
go-version:
|
||||||
|
description: 'The Go version for building LND'
|
||||||
|
required: true
|
||||||
|
default: ^1.19
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Cache LND client
|
||||||
|
id: cache-lnd-client
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go_lnd_client/bin/lnd
|
||||||
|
key: go_lnd_client-${{ inputs.client-ref }}-${{ inputs.go-version }}
|
||||||
|
|
||||||
|
- name: Set up Go 1.x
|
||||||
|
if: steps.cache-lnd-client.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ inputs.go-version }}
|
||||||
|
|
||||||
|
- name: Checkout LND for Client
|
||||||
|
if: steps.cache-lnd-client.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: breez/lnd
|
||||||
|
ref: ${{ inputs.client-ref }}
|
||||||
|
path: lnd_client
|
||||||
|
|
||||||
|
- name: Build LND for Client
|
||||||
|
if: steps.cache-lnd-client.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
cd lnd_client
|
||||||
|
env GOPATH=~/go_lnd_client make install
|
||||||
|
shell: bash
|
||||||
45
.github/actions/setup-lnd-lsp/action.yaml
vendored
Normal file
45
.github/actions/setup-lnd-lsp/action.yaml
vendored
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
name: 'Setup LND LSP'
|
||||||
|
description: 'Set up LND for LSP on the runner'
|
||||||
|
|
||||||
|
inputs:
|
||||||
|
lsp-ref:
|
||||||
|
description: 'The Git reference for the LSP version of LND'
|
||||||
|
required: true
|
||||||
|
default: 'breez-node-v0.16.2-beta'
|
||||||
|
|
||||||
|
go-version:
|
||||||
|
description: 'The Go version for building LND'
|
||||||
|
required: true
|
||||||
|
default: ^1.19
|
||||||
|
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Cache LND LSP
|
||||||
|
id: cache-lnd-lsp
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go_lnd_lsp/bin/lnd
|
||||||
|
key: go_lnd_lsp-${{ inputs.lsp-ref }}-${{ inputs.go-version }}
|
||||||
|
|
||||||
|
- name: Set up Go 1.x
|
||||||
|
if: steps.cache-lnd-lsp.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/setup-go@v4
|
||||||
|
with:
|
||||||
|
go-version: ${{ inputs.go-version }}
|
||||||
|
|
||||||
|
- name: Checkout LND for LSP
|
||||||
|
if: steps.cache-lnd-lsp.outputs.cache-hit != 'true'
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
with:
|
||||||
|
repository: breez/lnd
|
||||||
|
ref: ${{ inputs.lsp-ref }}
|
||||||
|
path: lnd_lsp
|
||||||
|
|
||||||
|
- name: Build LND for LSP
|
||||||
|
if: steps.cache-lnd-lsp.outputs.cache-hit != 'true'
|
||||||
|
run: |
|
||||||
|
cd lnd_lsp
|
||||||
|
env GOPATH=~/go_lnd_lsp make install tags='submarineswaprpc chanreservedynamic routerrpc walletrpc chainrpc signrpc invoicesrpc'
|
||||||
|
shell: bash
|
||||||
144
.github/actions/test-lspd/action.yaml
vendored
Normal file
144
.github/actions/test-lspd/action.yaml
vendored
Normal file
@@ -0,0 +1,144 @@
|
|||||||
|
name: 'Test LSPD'
|
||||||
|
description: 'Downloads artifacts, sets permissions, caches and runs a specified test and processes the result as an artifact.'
|
||||||
|
inputs:
|
||||||
|
TESTRE:
|
||||||
|
description: 'Test regular expression to run.'
|
||||||
|
required: true
|
||||||
|
artifact-name:
|
||||||
|
description: 'Artifact name for the test state.'
|
||||||
|
required: true
|
||||||
|
bitcoin-version:
|
||||||
|
description: 'Bitcoin version.'
|
||||||
|
required: true
|
||||||
|
LSP_REF:
|
||||||
|
description: 'LSP reference.'
|
||||||
|
required: true
|
||||||
|
CLIENT_REF:
|
||||||
|
description: 'Client reference.'
|
||||||
|
required: true
|
||||||
|
GO_VERSION:
|
||||||
|
description: 'Go version.'
|
||||||
|
required: true
|
||||||
|
CLN_VERSION:
|
||||||
|
description: 'Core Lightning version.'
|
||||||
|
required: true
|
||||||
|
runs:
|
||||||
|
using: 'composite'
|
||||||
|
steps:
|
||||||
|
- name: Download build artifacts
|
||||||
|
uses: actions/download-artifact@v3
|
||||||
|
with:
|
||||||
|
name: build-artifacts
|
||||||
|
|
||||||
|
- name: Set permissions
|
||||||
|
run: |
|
||||||
|
chmod 755 lspd
|
||||||
|
chmod 755 lspd_plugin
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Cache LND client
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/go_lnd_client/bin/lnd
|
||||||
|
key: go_lnd_client-${{ inputs.CLIENT_REF }}-${{ inputs.GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Cache LND LSP
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: ~/go_lnd_lsp/bin/lnd
|
||||||
|
key: go_lnd_lsp-${{ inputs.LSP_REF }}-${{ inputs.GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Cache Core Lightning
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
lightning_git/lightningd/lightning_hsmd
|
||||||
|
lightning_git/lightningd/lightning_gossipd
|
||||||
|
lightning_git/lightningd/lightning_openingd
|
||||||
|
lightning_git/lightningd/lightning_dualopend
|
||||||
|
lightning_git/lightningd/lightning_channeld
|
||||||
|
lightning_git/lightningd/lightning_closingd
|
||||||
|
lightning_git/lightningd/lightning_onchaind
|
||||||
|
lightning_git/lightningd/lightning_connectd
|
||||||
|
lightning_git/lightningd/lightning_websocketd
|
||||||
|
lightning_git/lightningd/lightningd
|
||||||
|
lightning_git/plugins/offers
|
||||||
|
lightning_git/plugins/topology
|
||||||
|
lightning_git/plugins/spenderp
|
||||||
|
lightning_git/plugins/test/run-route-overlong
|
||||||
|
lightning_git/plugins/test/run-funder_policy
|
||||||
|
lightning_git/plugins/pay
|
||||||
|
lightning_git/plugins/bkpr/test/run-bkpr_db
|
||||||
|
lightning_git/plugins/bkpr/test/run-recorder
|
||||||
|
lightning_git/plugins/funder
|
||||||
|
lightning_git/plugins/bookkeeper
|
||||||
|
lightning_git/plugins/txprepare
|
||||||
|
lightning_git/plugins/keysend
|
||||||
|
lightning_git/plugins/fetchinvoice
|
||||||
|
lightning_git/plugins/bcli
|
||||||
|
lightning_git/plugins/cln-grpc
|
||||||
|
lightning_git/plugins/commando
|
||||||
|
lightning_git/plugins/autoclean
|
||||||
|
lightning_git/plugins/chanbackup
|
||||||
|
lightning_git/plugins/sql
|
||||||
|
lightning_git/cli/lightning-cli
|
||||||
|
lightning_git/devtools/bolt11-cli
|
||||||
|
lightning_git/devtools/decodemsg
|
||||||
|
lightning_git/devtools/onion
|
||||||
|
lightning_git/devtools/dump-gossipstore
|
||||||
|
lightning_git/devtools/gossipwith
|
||||||
|
lightning_git/devtools/create-gossipstore
|
||||||
|
lightning_git/devtools/mkcommit
|
||||||
|
lightning_git/devtools/mkfunding
|
||||||
|
lightning_git/devtools/mkclose
|
||||||
|
lightning_git/devtools/mkgossip
|
||||||
|
key: core-lightning-${{ inputs.CLN_VERSION }}
|
||||||
|
|
||||||
|
- name: Cache Bitcoin Core
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}/bin/bitcoind
|
||||||
|
~/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}/bin/bitcoin-cli
|
||||||
|
key: bitcoin-core-${{ inputs.bitcoin-version }}
|
||||||
|
|
||||||
|
- name: Cache itest
|
||||||
|
id: cache-itest
|
||||||
|
uses: actions/cache@v3
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/go
|
||||||
|
key: itest
|
||||||
|
|
||||||
|
- name: Test LSPD
|
||||||
|
run: |
|
||||||
|
go get github.com/breez/lspd/itest
|
||||||
|
go test -timeout 45m -v \
|
||||||
|
./itest \
|
||||||
|
-test.run \
|
||||||
|
${{ inputs.TESTRE }} \
|
||||||
|
--bitcoindexec ~/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}/bin/bitcoind \
|
||||||
|
--bitcoincliexec ~/bitcoin-core-${{ inputs.bitcoin-version }}/bitcoin-${{ inputs.bitcoin-version }}/bin/bitcoin-cli \
|
||||||
|
--lightningdexec ${{ github.workspace }}/lightning_git/lightningd/lightningd \
|
||||||
|
--lndexec ~/go_lnd_lsp/bin/lnd \
|
||||||
|
--lndmobileexec ~/go_lnd_client/bin/lnd \
|
||||||
|
--clnpluginexec ${{ github.workspace }}/lspd_plugin \
|
||||||
|
--lspdexec ${{ github.workspace }}/lspd \
|
||||||
|
--lspdmigrationsdir ${{ github.workspace }}/postgresql/migrations \
|
||||||
|
--preservelogs \
|
||||||
|
--testdir /home/runner/test_state || echo "step_failed=true" >> $GITHUB_ENV
|
||||||
|
shell: bash
|
||||||
|
|
||||||
|
- name: Process test state
|
||||||
|
uses: ./.github/actions/process-test-state
|
||||||
|
with:
|
||||||
|
artifact-name: ${{ inputs.artifact-name }}
|
||||||
|
test-state-path: /home/runner/test_state
|
||||||
|
|
||||||
|
- name: Fail the workflow if the tests failed
|
||||||
|
run: |
|
||||||
|
if [[ "${{ env.step_failed }}" == "true" ]]
|
||||||
|
then
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
shell: bash
|
||||||
126
.github/workflows/integration_tests.yaml
vendored
Normal file
126
.github/workflows/integration_tests.yaml
vendored
Normal file
@@ -0,0 +1,126 @@
|
|||||||
|
name: integration tests
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ master ]
|
||||||
|
pull_request:
|
||||||
|
env:
|
||||||
|
BITCOIN_VERSION: '25.0'
|
||||||
|
LSP_REF: 'breez-node-v0.16.4-beta'
|
||||||
|
CLIENT_REF: 'v0.16.4-breez-2'
|
||||||
|
GO_VERSION: '^1.19'
|
||||||
|
CLN_VERSION: 'v23.05.1'
|
||||||
|
jobs:
|
||||||
|
|
||||||
|
setup-bitcoin-core:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Bitcoin Core
|
||||||
|
if: steps.cache-bitcoin.outputs.cache-hit != 'true'
|
||||||
|
uses: ./.github/actions/setup-bitcoin
|
||||||
|
with:
|
||||||
|
bitcoin-version: ${{ env.BITCOIN_VERSION }}
|
||||||
|
|
||||||
|
setup-lnd-lsp:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up LND LSP
|
||||||
|
if: steps.cache-lnd-lsp.outputs.cache-hit != 'true'
|
||||||
|
uses: ./.github/actions/setup-lnd-lsp
|
||||||
|
with:
|
||||||
|
lsp-ref: ${{ env.LSP_REF }}
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
setup-lnd-client:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up LND client
|
||||||
|
if: steps.cache-lnd-client.outputs.cache-hit != 'true'
|
||||||
|
uses: ./.github/actions/setup-lnd-client
|
||||||
|
with:
|
||||||
|
client-ref: ${{ env.CLIENT_REF }}
|
||||||
|
go-version: ${{ env.GO_VERSION }}
|
||||||
|
|
||||||
|
setup-cln:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Set up Core Lightning
|
||||||
|
uses: ./.github/actions/setup-clightning
|
||||||
|
with:
|
||||||
|
checkout-version: ${{ env.CLN_VERSION }}
|
||||||
|
|
||||||
|
build-lspd:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Build LSPD and Upload Artifacts
|
||||||
|
uses: ./.github/actions/build-lspd
|
||||||
|
|
||||||
|
setup-itest:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
- name: Setup itest
|
||||||
|
uses: ./.github/actions/setup-itest
|
||||||
|
|
||||||
|
|
||||||
|
run-test:
|
||||||
|
runs-on: ubuntu-22.04
|
||||||
|
needs:
|
||||||
|
- setup-itest
|
||||||
|
- setup-bitcoin-core
|
||||||
|
- setup-lnd-client
|
||||||
|
- setup-lnd-lsp
|
||||||
|
- setup-cln
|
||||||
|
- build-lspd
|
||||||
|
name: test ${{ matrix.implementation }} ${{ matrix.test }}
|
||||||
|
strategy:
|
||||||
|
max-parallel: 6
|
||||||
|
matrix:
|
||||||
|
test: [
|
||||||
|
testOpenZeroConfChannelOnReceive,
|
||||||
|
testOpenZeroConfSingleHtlc,
|
||||||
|
testZeroReserve,
|
||||||
|
testFailureBobOffline,
|
||||||
|
testNoBalance,
|
||||||
|
testRegularForward,
|
||||||
|
testProbing,
|
||||||
|
testInvalidCltv,
|
||||||
|
registerPaymentWithTag,
|
||||||
|
testOpenZeroConfUtxo,
|
||||||
|
testDynamicFeeFlow,
|
||||||
|
testOfflineNotificationPaymentRegistered,
|
||||||
|
testOfflineNotificationRegularForward,
|
||||||
|
testOfflineNotificationZeroConfChannel,
|
||||||
|
]
|
||||||
|
implementation: [
|
||||||
|
LND,
|
||||||
|
CLN
|
||||||
|
]
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v3
|
||||||
|
|
||||||
|
- name: Run and Process Test State
|
||||||
|
uses: ./.github/actions/test-lspd
|
||||||
|
with:
|
||||||
|
TESTRE: "TestLspd/${{ matrix.implementation }}-lspd:_${{ matrix.test }}"
|
||||||
|
artifact-name: TestLspd-${{ matrix.implementation }}-lspd_${{ matrix.test }}
|
||||||
|
bitcoin-version: ${{ env.BITCOIN_VERSION }}
|
||||||
|
LSP_REF: ${{ env.LSP_REF }}
|
||||||
|
CLIENT_REF: ${{ env.CLIENT_REF }}
|
||||||
|
GO_VERSION: ${{ env.GO_VERSION }}
|
||||||
|
CLN_VERSION: ${{ env.CLN_VERSION }}
|
||||||
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
.vscode
|
||||||
|
go.sum
|
||||||
|
lspd
|
||||||
|
lspd_plugin
|
||||||
20
LICENSE
Normal file
20
LICENSE
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
Copyright (c) 2022 Breez
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining
|
||||||
|
a copy of this software and associated documentation files (the
|
||||||
|
"Software"), to deal in the Software without restriction, including
|
||||||
|
without limitation the rights to use, copy, modify, merge, publish,
|
||||||
|
distribute, sublicense, and/or sell copies of the Software, and to
|
||||||
|
permit persons to whom the Software is furnished to do so, subject to
|
||||||
|
the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be
|
||||||
|
included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||||
|
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
||||||
|
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
||||||
|
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
||||||
|
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
||||||
|
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
||||||
|
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
72
README.md
Normal file
72
README.md
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
# lspd simple server
|
||||||
|
lspd is a simple deamon that provides [LSP](https://medium.com/breez-technology/introducing-lightning-service-providers-fe9fb1665d5f) services to [Breez clients](https://github.com/breez/breezmobile).
|
||||||
|
|
||||||
|
This is a simple example of an lspd that works with an [lnd](https://github.com/lightningnetwork/lnd) node or a [cln](https://github.com/ElementsProject/lightning) node.
|
||||||
|
|
||||||
|
## Deployment
|
||||||
|
Installation and configuration instructions for both implementations can be found here:
|
||||||
|
### Manual install
|
||||||
|
- [CLN](./docs/CLN.md) - step by step installation instructions for CLN
|
||||||
|
- [LND](./docs/LND.md) - step by step installation instructions for LND
|
||||||
|
### Automated deployment
|
||||||
|
- [AWS](./docs/aws.md) - automated deployment of bitcoind, CLN and lspd to AWS, together with
|
||||||
|
- [Bash](./docs/bash.md) - install everything on any debian/ubuntu server
|
||||||
|
|
||||||
|
## Implement your own lspd
|
||||||
|
You can create your own lsdp by implementing the grpc methods described [here](https://github.com/breez/lspd/blob/master/rpc/lspd.md).
|
||||||
|
|
||||||
|
## Flow for creating channels
|
||||||
|
When Alice wants Bob to pay her an amount and Alice doesn't have a channel with sufficient capacity, she calls the lspd function RegisterPayment() and sending the paymentHash, paymentSecret (for mpp payments), destination (Alice pubkey), and two amounts.
|
||||||
|
The first amount (incoming from the lsp point of view) is the amount BOB will pay. The second amount (outgoing from the lsp point of view) is the amount Alice will receive. The difference between these two amounts is the fees for the lsp.
|
||||||
|
In order to open the channel on the fly, the lsp is connecting to lnd using the interceptor api.
|
||||||
|
|
||||||
|
## Probing support
|
||||||
|
The lsp supports probing non-mpp payments if the payment hash for probing is sha256('probing-01:' || payment_hash) when payment_hash is the hash of the real payment.
|
||||||
|
|
||||||
|
## Integration tests
|
||||||
|
In order to run the integration tests, you need:
|
||||||
|
- Docker running
|
||||||
|
- python3 installed
|
||||||
|
- A development build of lightningd v23.05.1
|
||||||
|
- lnd v0.16.4 lsp version https://github.com/breez/lnd/commit/cebcdf1b17fdedf7d69207d98c31cf8c3b257531
|
||||||
|
- lnd v0.16.4 breez client version https://github.com/breez/lnd/commit/3c0854adcfc924a6d759a6ee4640c41266b9f8b4
|
||||||
|
- bitcoind (tested with v23.0)
|
||||||
|
- bitcoin-cli (tested with v23.0)
|
||||||
|
- build of lspd (go build .)
|
||||||
|
- build of lspd cln plugin (go build -o lspd_plugin cln_plugin/cmd)
|
||||||
|
|
||||||
|
To run the integration tests, run the following command from the lspd root directory (replacing the appropriate paths).
|
||||||
|
|
||||||
|
```
|
||||||
|
go test -timeout 20m -v ./itest \
|
||||||
|
--lightningdexec /full/path/to/lightningd \
|
||||||
|
--lndexec /full/path/to/lnd \
|
||||||
|
--lndmobileexec /full/path/to/lnd \
|
||||||
|
--clnpluginexec /full/path/to/lspd_plugin \
|
||||||
|
--lspdexec /full/path/to/lspd \
|
||||||
|
--lspdmigrationsdir /full/path/to/lspd/postgresql/migrations
|
||||||
|
```
|
||||||
|
|
||||||
|
- Required: `--lightningdexec` Full path to lightningd development build executable. Defaults to `lightningd` in `$PATH`.
|
||||||
|
- Required: `--lndexec` Full path to LSP LND executable. Defaults to `lnd` in `$PATH`.
|
||||||
|
- Required: `--lndmobileexec` Full path to Breez mobile client LND executable. No default.
|
||||||
|
- Required: `--lspdexec` Full path to `lspd` executable to test. Defaults to `lspd` in `$PATH`.
|
||||||
|
- Required: `--clnpluginexec` Full path to the lspd cln plugin executable. No default.
|
||||||
|
- Required: `--lspdmigrationsdir` Path to directory containing postgres migrations for lspd. (Should be `./postgresql/migrations`)
|
||||||
|
- Recommended: `--bitcoindexec` Full path to `bitcoind`. Defaults to `bitcoind` in `$PATH`.
|
||||||
|
- Recommended: `--bitcoincliexec` Full path to `bitcoin-cli`. Defaults to `bitcoin-cli` in `$PATH`.
|
||||||
|
- Recommended: `--testdir` uses the testdir as root directory for test files. Recommended because the CLN `lightning-rpc` socket max path length is 104-108 characters. Defaults to a temp directory (which has a long path length usually).
|
||||||
|
- Optional: `--preservelogs` persists only the logs in the testing directory.
|
||||||
|
- Optional: `--preservestate` preserves all artifacts from the lightning nodes, miners, postgres container and startup scripts.
|
||||||
|
- Optional: `--dumplogs` dumps all logs to the console after a test is complete.
|
||||||
|
|
||||||
|
Unfortunately the tests cannot be cancelled with CTRL+C without having to clean
|
||||||
|
up some artefacts. Here's where to look:
|
||||||
|
- lspd process
|
||||||
|
- lightningd process
|
||||||
|
- lnd process
|
||||||
|
- bitcoind process
|
||||||
|
- docker container for postgres with default name
|
||||||
|
|
||||||
|
It may be a good idea to clean your testdir every once in a while if you're
|
||||||
|
using the `preservelogs` or `preservestate` flags.
|
||||||
19
basetypes/outpoint.go
Normal file
19
basetypes/outpoint.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package basetypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewOutPoint(fundingTxID []byte, index uint32) (*wire.OutPoint, error) {
|
||||||
|
var h chainhash.Hash
|
||||||
|
err := h.SetBytes(fundingTxID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("h.SetBytes(%x) error: %v", fundingTxID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return wire.NewOutPoint(&h, index), nil
|
||||||
|
}
|
||||||
51
basetypes/short_channel_id.go
Normal file
51
basetypes/short_channel_id.go
Normal file
@@ -0,0 +1,51 @@
|
|||||||
|
package basetypes
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ShortChannelID uint64
|
||||||
|
|
||||||
|
func NewShortChannelIDFromString(channelID string) (*ShortChannelID, error) {
|
||||||
|
if channelID == "" {
|
||||||
|
c := ShortChannelID(0)
|
||||||
|
return &c, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
fields := strings.Split(channelID, "x")
|
||||||
|
if len(fields) != 3 {
|
||||||
|
return nil, fmt.Errorf("invalid short channel id %v", channelID)
|
||||||
|
}
|
||||||
|
var blockHeight, txIndex, txPos int64
|
||||||
|
var err error
|
||||||
|
if blockHeight, err = strconv.ParseInt(fields[0], 10, 64); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse block height %v", fields[0])
|
||||||
|
}
|
||||||
|
if txIndex, err = strconv.ParseInt(fields[1], 10, 64); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse block height %v", fields[1])
|
||||||
|
}
|
||||||
|
if txPos, err = strconv.ParseInt(fields[2], 10, 64); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to parse block height %v", fields[2])
|
||||||
|
}
|
||||||
|
|
||||||
|
result := ShortChannelID(
|
||||||
|
lnwire.ShortChannelID{
|
||||||
|
BlockHeight: uint32(blockHeight),
|
||||||
|
TxIndex: uint32(txIndex),
|
||||||
|
TxPosition: uint16(txPos),
|
||||||
|
}.ToUint64(),
|
||||||
|
)
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ShortChannelID) ToString() string {
|
||||||
|
u := uint64(*c)
|
||||||
|
blockHeight := (u >> 40) & 0xFFFFFF
|
||||||
|
txIndex := (u >> 16) & 0xFFFFFF
|
||||||
|
outputIndex := u & 0xFFFF
|
||||||
|
return fmt.Sprintf("%dx%dx%d", blockHeight, txIndex, outputIndex)
|
||||||
|
}
|
||||||
3
basetypes/time.go
Normal file
3
basetypes/time.go
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
package basetypes
|
||||||
|
|
||||||
|
var TIME_FORMAT string = "2006-01-02T15:04:05.999Z"
|
||||||
211
btceclegacy/ciphering.go
Normal file
211
btceclegacy/ciphering.go
Normal file
@@ -0,0 +1,211 @@
|
|||||||
|
// Copyright (c) 2015-2016 The btcsuite developers
|
||||||
|
// Use of this source code is governed by an ISC
|
||||||
|
// license that can be found in the LICENSE file.
|
||||||
|
|
||||||
|
package btceclegacy
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"crypto/aes"
|
||||||
|
"crypto/cipher"
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/rand"
|
||||||
|
"crypto/sha256"
|
||||||
|
"crypto/sha512"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
// ErrInvalidMAC occurs when Message Authentication Check (MAC) fails
|
||||||
|
// during decryption. This happens because of either invalid private key or
|
||||||
|
// corrupt ciphertext.
|
||||||
|
ErrInvalidMAC = errors.New("invalid mac hash")
|
||||||
|
|
||||||
|
// errInputTooShort occurs when the input ciphertext to the Decrypt
|
||||||
|
// function is less than 134 bytes long.
|
||||||
|
errInputTooShort = errors.New("ciphertext too short")
|
||||||
|
|
||||||
|
// errUnsupportedCurve occurs when the first two bytes of the encrypted
|
||||||
|
// text aren't 0x02CA (= 712 = secp256k1, from OpenSSL).
|
||||||
|
errUnsupportedCurve = errors.New("unsupported curve")
|
||||||
|
|
||||||
|
errInvalidXLength = errors.New("invalid X length, must be 32")
|
||||||
|
errInvalidYLength = errors.New("invalid Y length, must be 32")
|
||||||
|
errInvalidPadding = errors.New("invalid PKCS#7 padding")
|
||||||
|
|
||||||
|
// 0x02CA = 714
|
||||||
|
ciphCurveBytes = [2]byte{0x02, 0xCA}
|
||||||
|
// 0x20 = 32
|
||||||
|
ciphCoordLength = [2]byte{0x00, 0x20}
|
||||||
|
)
|
||||||
|
|
||||||
|
// Encrypt encrypts data for the target public key using AES-256-CBC. It also
|
||||||
|
// generates a private key (the pubkey of which is also in the output). The only
|
||||||
|
// supported curve is secp256k1. The `structure' that it encodes everything into
|
||||||
|
// is:
|
||||||
|
//
|
||||||
|
// struct {
|
||||||
|
// // Initialization Vector used for AES-256-CBC
|
||||||
|
// IV [16]byte
|
||||||
|
// // Public Key: curve(2) + len_of_pubkeyX(2) + pubkeyX +
|
||||||
|
// // len_of_pubkeyY(2) + pubkeyY (curve = 714)
|
||||||
|
// PublicKey [70]byte
|
||||||
|
// // Cipher text
|
||||||
|
// Data []byte
|
||||||
|
// // HMAC-SHA-256 Message Authentication Code
|
||||||
|
// HMAC [32]byte
|
||||||
|
// }
|
||||||
|
//
|
||||||
|
// The primary aim is to ensure byte compatibility with Pyelliptic. Also, refer
|
||||||
|
// to section 5.8.1 of ANSI X9.63 for rationale on this format.
|
||||||
|
func Encrypt(pubkey *btcec.PublicKey, in []byte) ([]byte, error) {
|
||||||
|
ephemeral, err := btcec.NewPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
ecdhKey := secp256k1.GenerateSharedSecret(ephemeral, pubkey)
|
||||||
|
derivedKey := sha512.Sum512(ecdhKey)
|
||||||
|
keyE := derivedKey[:32]
|
||||||
|
keyM := derivedKey[32:]
|
||||||
|
|
||||||
|
paddedIn := addPKCSPadding(in)
|
||||||
|
// IV + Curve params/X/Y + padded plaintext/ciphertext + HMAC-256
|
||||||
|
out := make([]byte, aes.BlockSize+70+len(paddedIn)+sha256.Size)
|
||||||
|
iv := out[:aes.BlockSize]
|
||||||
|
if _, err = io.ReadFull(rand.Reader, iv); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// start writing public key
|
||||||
|
pb := ephemeral.PubKey().SerializeUncompressed()
|
||||||
|
offset := aes.BlockSize
|
||||||
|
|
||||||
|
// curve and X length
|
||||||
|
copy(out[offset:offset+4], append(ciphCurveBytes[:], ciphCoordLength[:]...))
|
||||||
|
offset += 4
|
||||||
|
// X
|
||||||
|
copy(out[offset:offset+32], pb[1:33])
|
||||||
|
offset += 32
|
||||||
|
// Y length
|
||||||
|
copy(out[offset:offset+2], ciphCoordLength[:])
|
||||||
|
offset += 2
|
||||||
|
// Y
|
||||||
|
copy(out[offset:offset+32], pb[33:])
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
// start encryption
|
||||||
|
block, err := aes.NewCipher(keyE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mode := cipher.NewCBCEncrypter(block, iv)
|
||||||
|
mode.CryptBlocks(out[offset:len(out)-sha256.Size], paddedIn)
|
||||||
|
|
||||||
|
// start HMAC-SHA-256
|
||||||
|
hm := hmac.New(sha256.New, keyM)
|
||||||
|
hm.Write(out[:len(out)-sha256.Size]) // everything is hashed
|
||||||
|
copy(out[len(out)-sha256.Size:], hm.Sum(nil)) // write checksum
|
||||||
|
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decrypt decrypts data that was encrypted using the Encrypt function.
|
||||||
|
func Decrypt(priv *btcec.PrivateKey, in []byte) ([]byte, error) {
|
||||||
|
// IV + Curve params/X/Y + 1 block + HMAC-256
|
||||||
|
if len(in) < aes.BlockSize+70+aes.BlockSize+sha256.Size {
|
||||||
|
return nil, errInputTooShort
|
||||||
|
}
|
||||||
|
|
||||||
|
// read iv
|
||||||
|
iv := in[:aes.BlockSize]
|
||||||
|
offset := aes.BlockSize
|
||||||
|
|
||||||
|
// start reading pubkey
|
||||||
|
if !bytes.Equal(in[offset:offset+2], ciphCurveBytes[:]) {
|
||||||
|
return nil, errUnsupportedCurve
|
||||||
|
}
|
||||||
|
offset += 2
|
||||||
|
|
||||||
|
if !bytes.Equal(in[offset:offset+2], ciphCoordLength[:]) {
|
||||||
|
return nil, errInvalidXLength
|
||||||
|
}
|
||||||
|
offset += 2
|
||||||
|
|
||||||
|
xBytes := in[offset : offset+32]
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
if !bytes.Equal(in[offset:offset+2], ciphCoordLength[:]) {
|
||||||
|
return nil, errInvalidYLength
|
||||||
|
}
|
||||||
|
offset += 2
|
||||||
|
|
||||||
|
yBytes := in[offset : offset+32]
|
||||||
|
offset += 32
|
||||||
|
|
||||||
|
pb := make([]byte, 65)
|
||||||
|
pb[0] = byte(0x04) // uncompressed
|
||||||
|
copy(pb[1:33], xBytes)
|
||||||
|
copy(pb[33:], yBytes)
|
||||||
|
// check if (X, Y) lies on the curve and create a Pubkey if it does
|
||||||
|
pubkey, err := btcec.ParsePubKey(pb)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for cipher text length
|
||||||
|
if (len(in)-aes.BlockSize-offset-sha256.Size)%aes.BlockSize != 0 {
|
||||||
|
return nil, errInvalidPadding // not padded to 16 bytes
|
||||||
|
}
|
||||||
|
|
||||||
|
// read hmac
|
||||||
|
messageMAC := in[len(in)-sha256.Size:]
|
||||||
|
|
||||||
|
// generate shared secret
|
||||||
|
ecdhKey := secp256k1.GenerateSharedSecret(priv, pubkey)
|
||||||
|
derivedKey := sha512.Sum512(ecdhKey)
|
||||||
|
keyE := derivedKey[:32]
|
||||||
|
keyM := derivedKey[32:]
|
||||||
|
|
||||||
|
// verify mac
|
||||||
|
hm := hmac.New(sha256.New, keyM)
|
||||||
|
hm.Write(in[:len(in)-sha256.Size]) // everything is hashed
|
||||||
|
expectedMAC := hm.Sum(nil)
|
||||||
|
if !hmac.Equal(messageMAC, expectedMAC) {
|
||||||
|
return nil, ErrInvalidMAC
|
||||||
|
}
|
||||||
|
|
||||||
|
// start decryption
|
||||||
|
block, err := aes.NewCipher(keyE)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
mode := cipher.NewCBCDecrypter(block, iv)
|
||||||
|
// same length as ciphertext
|
||||||
|
plaintext := make([]byte, len(in)-offset-sha256.Size)
|
||||||
|
mode.CryptBlocks(plaintext, in[offset:len(in)-sha256.Size])
|
||||||
|
|
||||||
|
return removePKCSPadding(plaintext)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Implement PKCS#7 padding with block size of 16 (AES block size).
|
||||||
|
|
||||||
|
// addPKCSPadding adds padding to a block of data
|
||||||
|
func addPKCSPadding(src []byte) []byte {
|
||||||
|
padding := aes.BlockSize - len(src)%aes.BlockSize
|
||||||
|
padtext := bytes.Repeat([]byte{byte(padding)}, padding)
|
||||||
|
return append(src, padtext...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// removePKCSPadding removes padding from data that was added with addPKCSPadding
|
||||||
|
func removePKCSPadding(src []byte) ([]byte, error) {
|
||||||
|
length := len(src)
|
||||||
|
padLength := int(src[length-1])
|
||||||
|
if padLength > aes.BlockSize || length < aes.BlockSize {
|
||||||
|
return nil, errInvalidPadding
|
||||||
|
}
|
||||||
|
|
||||||
|
return src[:length-padLength], nil
|
||||||
|
}
|
||||||
21
chain/fee_estimator.go
Normal file
21
chain/fee_estimator.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package chain
|
||||||
|
|
||||||
|
import "context"
|
||||||
|
|
||||||
|
type FeeStrategy int
|
||||||
|
|
||||||
|
const (
|
||||||
|
FeeStrategyFastest FeeStrategy = 0
|
||||||
|
FeeStrategyHalfHour FeeStrategy = 1
|
||||||
|
FeeStrategyHour FeeStrategy = 2
|
||||||
|
FeeStrategyEconomy FeeStrategy = 3
|
||||||
|
FeeStrategyMinimum FeeStrategy = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
type FeeEstimation struct {
|
||||||
|
SatPerVByte float64
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeeEstimator interface {
|
||||||
|
EstimateFeeRate(context.Context, FeeStrategy) (*FeeEstimation, error)
|
||||||
|
}
|
||||||
437
channel_opener_server.go
Normal file
437
channel_opener_server.go
Normal file
@@ -0,0 +1,437 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/basetypes"
|
||||||
|
"github.com/breez/lspd/btceclegacy"
|
||||||
|
"github.com/breez/lspd/interceptor"
|
||||||
|
"github.com/breez/lspd/lightning"
|
||||||
|
lspdrpc "github.com/breez/lspd/rpc"
|
||||||
|
ecies "github.com/ecies/go/v2"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
type channelOpenerServer struct {
|
||||||
|
lspdrpc.ChannelOpenerServer
|
||||||
|
store interceptor.InterceptStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewChannelOpenerServer(
|
||||||
|
store interceptor.InterceptStore,
|
||||||
|
) *channelOpenerServer {
|
||||||
|
return &channelOpenerServer{
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type contextKey string
|
||||||
|
|
||||||
|
func (s *channelOpenerServer) ChannelInformation(ctx context.Context, in *lspdrpc.ChannelInformationRequest) (*lspdrpc.ChannelInformationReply, error) {
|
||||||
|
node, token, err := s.getNode(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
params, err := s.createOpeningParamsMenu(ctx, node, token)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &lspdrpc.ChannelInformationReply{
|
||||||
|
Name: node.nodeConfig.Name,
|
||||||
|
Pubkey: node.nodeConfig.NodePubkey,
|
||||||
|
Host: node.nodeConfig.Host,
|
||||||
|
ChannelCapacity: int64(node.nodeConfig.PublicChannelAmount),
|
||||||
|
TargetConf: int32(node.nodeConfig.TargetConf),
|
||||||
|
MinHtlcMsat: int64(node.nodeConfig.MinHtlcMsat),
|
||||||
|
BaseFeeMsat: int64(node.nodeConfig.BaseFeeMsat),
|
||||||
|
FeeRate: node.nodeConfig.FeeRate,
|
||||||
|
TimeLockDelta: node.nodeConfig.TimeLockDelta,
|
||||||
|
ChannelFeePermyriad: int64(node.nodeConfig.ChannelFeePermyriad),
|
||||||
|
ChannelMinimumFeeMsat: int64(node.nodeConfig.ChannelMinimumFeeMsat),
|
||||||
|
LspPubkey: node.publicKey.SerializeCompressed(), // TODO: Is the publicKey different from the ecies public key?
|
||||||
|
MaxInactiveDuration: int64(node.nodeConfig.MaxInactiveDuration),
|
||||||
|
OpeningFeeParamsMenu: params,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *channelOpenerServer) createOpeningParamsMenu(
|
||||||
|
ctx context.Context,
|
||||||
|
node *node,
|
||||||
|
token string,
|
||||||
|
) ([]*lspdrpc.OpeningFeeParams, error) {
|
||||||
|
var menu []*lspdrpc.OpeningFeeParams
|
||||||
|
|
||||||
|
settings, err := s.store.GetFeeParamsSettings(token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to fetch fee params settings: %v", err)
|
||||||
|
return nil, fmt.Errorf("failed to get opening_fee_params")
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, setting := range settings {
|
||||||
|
validUntil := time.Now().UTC().Add(setting.Validity)
|
||||||
|
params := &lspdrpc.OpeningFeeParams{
|
||||||
|
MinMsat: setting.Params.MinMsat,
|
||||||
|
Proportional: setting.Params.Proportional,
|
||||||
|
ValidUntil: validUntil.Format(basetypes.TIME_FORMAT),
|
||||||
|
MaxIdleTime: setting.Params.MaxIdleTime,
|
||||||
|
MaxClientToSelfDelay: setting.Params.MaxClientToSelfDelay,
|
||||||
|
}
|
||||||
|
|
||||||
|
promise, err := createPromise(node, params)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to create promise: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
params.Promise = *promise
|
||||||
|
menu = append(menu, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Slice(menu, func(i, j int) bool {
|
||||||
|
if menu[i].MinMsat == menu[j].MinMsat {
|
||||||
|
return menu[i].Proportional < menu[j].Proportional
|
||||||
|
}
|
||||||
|
|
||||||
|
return menu[i].MinMsat < menu[j].MinMsat
|
||||||
|
})
|
||||||
|
return menu, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func paramsHash(params *lspdrpc.OpeningFeeParams) ([]byte, error) {
|
||||||
|
// First hash all the values in the params in a fixed order.
|
||||||
|
items := []interface{}{
|
||||||
|
params.MinMsat,
|
||||||
|
params.Proportional,
|
||||||
|
params.ValidUntil,
|
||||||
|
params.MaxIdleTime,
|
||||||
|
params.MaxClientToSelfDelay,
|
||||||
|
}
|
||||||
|
blob, err := json.Marshal(items)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("paramsHash error: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
hash := sha256.Sum256(blob)
|
||||||
|
return hash[:], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func createPromise(node *node, params *lspdrpc.OpeningFeeParams) (*string, error) {
|
||||||
|
hash, err := paramsHash(params)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// Sign the hash with the private key of the LSP id.
|
||||||
|
sig, err := ecdsa.SignCompact(node.privateKey, hash[:], true)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("createPromise: SignCompact error: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
promise := hex.EncodeToString(sig)
|
||||||
|
return &promise, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func verifyPromise(node *node, params *lspdrpc.OpeningFeeParams) error {
|
||||||
|
hash, err := paramsHash(params)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sig, err := hex.DecodeString(params.Promise)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("verifyPromise: hex.DecodeString error: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
pub, _, err := ecdsa.RecoverCompact(sig, hash)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("verifyPromise: RecoverCompact(%x) error: %v", sig, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !node.publicKey.IsEqual(pub) {
|
||||||
|
log.Print("verifyPromise: not signed by us", err)
|
||||||
|
return fmt.Errorf("invalid promise")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func validateOpeningFeeParams(node *node, params *lspdrpc.OpeningFeeParams) bool {
|
||||||
|
if params == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
err := verifyPromise(node, params)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
t, err := time.Parse(basetypes.TIME_FORMAT, params.ValidUntil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("validateOpeningFeeParams: time.Parse(%v, %v) error: %v", basetypes.TIME_FORMAT, params.ValidUntil, err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if time.Now().UTC().After(t) {
|
||||||
|
log.Printf("validateOpeningFeeParams: promise not valid anymore: %v", t)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *channelOpenerServer) RegisterPayment(
|
||||||
|
ctx context.Context,
|
||||||
|
in *lspdrpc.RegisterPaymentRequest,
|
||||||
|
) (*lspdrpc.RegisterPaymentReply, error) {
|
||||||
|
node, token, err := s.getNode(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := ecies.Decrypt(node.eciesPrivateKey, in.Blob)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ecies.Decrypt(%x) error: %v", in.Blob, err)
|
||||||
|
data, err = btceclegacy.Decrypt(node.privateKey, in.Blob)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("btcec.Decrypt(%x) error: %v", in.Blob, err)
|
||||||
|
return nil, fmt.Errorf("btcec.Decrypt(%x) error: %w", in.Blob, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var pi lspdrpc.PaymentInformation
|
||||||
|
err = proto.Unmarshal(data, &pi)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("proto.Unmarshal(%x) error: %v", data, err)
|
||||||
|
return nil, fmt.Errorf("proto.Unmarshal(%x) error: %w", data, err)
|
||||||
|
}
|
||||||
|
log.Printf("RegisterPayment - Destination: %x, pi.PaymentHash: %x, pi.PaymentSecret: %x, pi.IncomingAmountMsat: %v, pi.OutgoingAmountMsat: %v, pi.Tag: %v",
|
||||||
|
pi.Destination, pi.PaymentHash, pi.PaymentSecret, pi.IncomingAmountMsat, pi.OutgoingAmountMsat, pi.Tag)
|
||||||
|
|
||||||
|
if len(pi.Tag) > 1000 {
|
||||||
|
return nil, fmt.Errorf("tag too long")
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(pi.Tag) != 0 {
|
||||||
|
var tag json.RawMessage
|
||||||
|
err = json.Unmarshal([]byte(pi.Tag), &tag)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("tag is not a valid json object")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO: Remove this nil check and the else cluase when we enforce all
|
||||||
|
// clients to use opening_fee_params.
|
||||||
|
if pi.OpeningFeeParams != nil {
|
||||||
|
valid := validateOpeningFeeParams(node, pi.OpeningFeeParams)
|
||||||
|
if !valid {
|
||||||
|
return nil, fmt.Errorf("invalid opening_fee_params")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
log.Printf("DEPRECATED: RegisterPayment with deprecated fee mechanism.")
|
||||||
|
pi.OpeningFeeParams = &lspdrpc.OpeningFeeParams{
|
||||||
|
MinMsat: uint64(node.nodeConfig.ChannelMinimumFeeMsat),
|
||||||
|
Proportional: uint32(node.nodeConfig.ChannelFeePermyriad * 100),
|
||||||
|
ValidUntil: time.Now().UTC().Add(time.Duration(time.Hour * 24)).Format(basetypes.TIME_FORMAT),
|
||||||
|
MaxIdleTime: uint32(node.nodeConfig.MaxInactiveDuration / 600),
|
||||||
|
MaxClientToSelfDelay: uint32(10000),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = checkPayment(pi.OpeningFeeParams, pi.IncomingAmountMsat, pi.OutgoingAmountMsat)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("checkPayment(%v, %v) error: %v", pi.IncomingAmountMsat, pi.OutgoingAmountMsat, err)
|
||||||
|
return nil, fmt.Errorf("checkPayment(%v, %v) error: %v", pi.IncomingAmountMsat, pi.OutgoingAmountMsat, err)
|
||||||
|
}
|
||||||
|
params := &interceptor.OpeningFeeParams{
|
||||||
|
MinMsat: pi.OpeningFeeParams.MinMsat,
|
||||||
|
Proportional: pi.OpeningFeeParams.Proportional,
|
||||||
|
ValidUntil: pi.OpeningFeeParams.ValidUntil,
|
||||||
|
MaxIdleTime: pi.OpeningFeeParams.MaxIdleTime,
|
||||||
|
MaxClientToSelfDelay: pi.OpeningFeeParams.MaxClientToSelfDelay,
|
||||||
|
Promise: pi.OpeningFeeParams.Promise,
|
||||||
|
}
|
||||||
|
err = s.store.RegisterPayment(token, params, pi.Destination, pi.PaymentHash, pi.PaymentSecret, pi.IncomingAmountMsat, pi.OutgoingAmountMsat, pi.Tag)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("RegisterPayment() error: %v", err)
|
||||||
|
return nil, fmt.Errorf("RegisterPayment() error: %w", err)
|
||||||
|
}
|
||||||
|
return &lspdrpc.RegisterPaymentReply{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *channelOpenerServer) OpenChannel(ctx context.Context, in *lspdrpc.OpenChannelRequest) (*lspdrpc.OpenChannelReply, error) {
|
||||||
|
node, _, err := s.getNode(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
r, err, _ := node.openChannelReqGroup.Do(in.Pubkey, func() (interface{}, error) {
|
||||||
|
pubkey, err := hex.DecodeString(in.Pubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
channelCount, err := node.client.GetNodeChannelCount(pubkey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var outPoint *wire.OutPoint
|
||||||
|
if channelCount == 0 {
|
||||||
|
outPoint, err = node.client.OpenChannel(&lightning.OpenChannelRequest{
|
||||||
|
CapacitySat: node.nodeConfig.ChannelAmount,
|
||||||
|
Destination: pubkey,
|
||||||
|
TargetConf: &node.nodeConfig.TargetConf,
|
||||||
|
MinHtlcMsat: node.nodeConfig.MinHtlcMsat,
|
||||||
|
IsPrivate: node.nodeConfig.ChannelPrivate,
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error in OpenChannel: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Response from OpenChannel: (TX: %v)", outPoint.String())
|
||||||
|
}
|
||||||
|
|
||||||
|
return &lspdrpc.OpenChannelReply{TxHash: outPoint.Hash.String(), OutputIndex: outPoint.Index}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return r.(*lspdrpc.OpenChannelReply), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (n *node) getSignedEncryptedData(in *lspdrpc.Encrypted) (string, []byte, bool, error) {
|
||||||
|
usedEcies := true
|
||||||
|
signedBlob, err := ecies.Decrypt(n.eciesPrivateKey, in.Data)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ecies.Decrypt(%x) error: %v", in.Data, err)
|
||||||
|
usedEcies = false
|
||||||
|
signedBlob, err = btceclegacy.Decrypt(n.privateKey, in.Data)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("btcec.Decrypt(%x) error: %v", in.Data, err)
|
||||||
|
return "", nil, usedEcies, fmt.Errorf("btcec.Decrypt(%x) error: %w", in.Data, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var signed lspdrpc.Signed
|
||||||
|
err = proto.Unmarshal(signedBlob, &signed)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("proto.Unmarshal(%x) error: %v", signedBlob, err)
|
||||||
|
return "", nil, usedEcies, fmt.Errorf("proto.Unmarshal(%x) error: %w", signedBlob, err)
|
||||||
|
}
|
||||||
|
pubkey, err := btcec.ParsePubKey(signed.Pubkey)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("unable to parse pubkey: %v", err)
|
||||||
|
return "", nil, usedEcies, fmt.Errorf("unable to parse pubkey: %w", err)
|
||||||
|
}
|
||||||
|
wireSig, err := lnwire.NewSigFromRawSignature(signed.Signature)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, usedEcies, fmt.Errorf("failed to decode signature: %v", err)
|
||||||
|
}
|
||||||
|
sig, err := wireSig.ToSignature()
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, usedEcies, fmt.Errorf("failed to convert from wire format: %v",
|
||||||
|
err)
|
||||||
|
}
|
||||||
|
// The signature is over the sha256 hash of the message.
|
||||||
|
digest := chainhash.HashB(signed.Data)
|
||||||
|
if !sig.Verify(digest, pubkey) {
|
||||||
|
return "", nil, usedEcies, fmt.Errorf("invalid signature")
|
||||||
|
}
|
||||||
|
return hex.EncodeToString(signed.Pubkey), signed.Data, usedEcies, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *channelOpenerServer) CheckChannels(ctx context.Context, in *lspdrpc.Encrypted) (*lspdrpc.Encrypted, error) {
|
||||||
|
node, _, err := s.getNode(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeID, data, usedEcies, err := node.getSignedEncryptedData(in)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("getSignedEncryptedData error: %v", err)
|
||||||
|
return nil, fmt.Errorf("getSignedEncryptedData error: %v", err)
|
||||||
|
}
|
||||||
|
var checkChannelsRequest lspdrpc.CheckChannelsRequest
|
||||||
|
err = proto.Unmarshal(data, &checkChannelsRequest)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("proto.Unmarshal(%x) error: %v", data, err)
|
||||||
|
return nil, fmt.Errorf("proto.Unmarshal(%x) error: %w", data, err)
|
||||||
|
}
|
||||||
|
closedChannels, err := node.client.GetClosedChannels(nodeID, checkChannelsRequest.WaitingCloseChannels)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("GetClosedChannels(%v) error: %v", checkChannelsRequest.FakeChannels, err)
|
||||||
|
return nil, fmt.Errorf("GetClosedChannels(%v) error: %w", checkChannelsRequest.FakeChannels, err)
|
||||||
|
}
|
||||||
|
checkChannelsReply := lspdrpc.CheckChannelsReply{
|
||||||
|
NotFakeChannels: make(map[string]uint64),
|
||||||
|
ClosedChannels: closedChannels,
|
||||||
|
}
|
||||||
|
dataReply, err := proto.Marshal(&checkChannelsReply)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("proto.Marshall() error: %v", err)
|
||||||
|
return nil, fmt.Errorf("proto.Marshal() error: %w", err)
|
||||||
|
}
|
||||||
|
pubkey, err := btcec.ParsePubKey(checkChannelsRequest.EncryptPubkey)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("unable to parse pubkey: %v", err)
|
||||||
|
return nil, fmt.Errorf("unable to parse pubkey: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var encrypted []byte
|
||||||
|
if usedEcies {
|
||||||
|
encrypted, err = ecies.Encrypt(node.eciesPublicKey, dataReply)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ecies.Encrypt() error: %v", err)
|
||||||
|
return nil, fmt.Errorf("ecies.Encrypt() error: %w", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
encrypted, err = btceclegacy.Encrypt(pubkey, dataReply)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("btcec.Encrypt() error: %v", err)
|
||||||
|
return nil, fmt.Errorf("btcec.Encrypt() error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &lspdrpc.Encrypted{Data: encrypted}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *channelOpenerServer) getNode(ctx context.Context) (*node, string, error) {
|
||||||
|
nd := ctx.Value(contextKey("node"))
|
||||||
|
if nd == nil {
|
||||||
|
return nil, "", status.Errorf(codes.PermissionDenied, "Not authorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
nodeContext, ok := nd.(*nodeContext)
|
||||||
|
if !ok {
|
||||||
|
return nil, "", status.Errorf(codes.PermissionDenied, "Not authorized")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nodeContext.node, nodeContext.token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkPayment(params *lspdrpc.OpeningFeeParams, incomingAmountMsat, outgoingAmountMsat int64) error {
|
||||||
|
fees := incomingAmountMsat * int64(params.Proportional) / 1_000_000 / 1_000 * 1_000
|
||||||
|
if fees < int64(params.MinMsat) {
|
||||||
|
fees = int64(params.MinMsat)
|
||||||
|
}
|
||||||
|
if incomingAmountMsat-outgoingAmountMsat < fees {
|
||||||
|
return fmt.Errorf("not enough fees")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
282
cln/cln_client.go
Normal file
282
cln/cln_client.go
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
package cln
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/basetypes"
|
||||||
|
"github.com/breez/lspd/lightning"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/elementsproject/glightning/glightning"
|
||||||
|
"golang.org/x/exp/slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClnClient struct {
|
||||||
|
client *glightning.Lightning
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
OPEN_STATUSES = []string{"CHANNELD_NORMAL"}
|
||||||
|
PENDING_STATUSES = []string{"OPENINGD", "CHANNELD_AWAITING_LOCKIN"}
|
||||||
|
CLOSING_STATUSES = []string{"CHANNELD_SHUTTING_DOWN", "CLOSINGD_SIGEXCHANGE", "CLOSINGD_COMPLETE", "AWAITING_UNILATERAL", "FUNDING_SPEND_SEEN", "ONCHAIN"}
|
||||||
|
CLOSED_STATUSES = []string{"CLOSED"}
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewClnClient(socketPath string) (*ClnClient, error) {
|
||||||
|
rpcFile := filepath.Base(socketPath)
|
||||||
|
if rpcFile == "" || rpcFile == "." {
|
||||||
|
return nil, fmt.Errorf("invalid socketPath '%s'", socketPath)
|
||||||
|
}
|
||||||
|
lightningDir := filepath.Dir(socketPath)
|
||||||
|
if lightningDir == "" || lightningDir == "." {
|
||||||
|
return nil, fmt.Errorf("invalid socketPath '%s'", socketPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := glightning.NewLightning()
|
||||||
|
client.SetTimeout(60)
|
||||||
|
client.StartUp(rpcFile, lightningDir)
|
||||||
|
return &ClnClient{
|
||||||
|
client: client,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnClient) GetInfo() (*lightning.GetInfoResult, error) {
|
||||||
|
info, err := c.client.GetInfo()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("CLN: client.GetInfo() error: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &lightning.GetInfoResult{
|
||||||
|
Alias: info.Alias,
|
||||||
|
Pubkey: info.Id,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnClient) IsConnected(destination []byte) (bool, error) {
|
||||||
|
pubKey := hex.EncodeToString(destination)
|
||||||
|
peer, err := c.client.GetPeer(pubKey)
|
||||||
|
if err != nil {
|
||||||
|
if strings.Contains(err.Error(), "not found") {
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("CLN: client.GetPeer(%v) error: %v", pubKey, err)
|
||||||
|
return false, fmt.Errorf("CLN: client.GetPeer(%v) error: %w", pubKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.Connected {
|
||||||
|
log.Printf("CLN: destination online: %x", destination)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("CLN: destination offline: %x", destination)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnClient) OpenChannel(req *lightning.OpenChannelRequest) (*wire.OutPoint, error) {
|
||||||
|
pubkey := hex.EncodeToString(req.Destination)
|
||||||
|
var minConfs *uint16
|
||||||
|
if req.MinConfs != nil {
|
||||||
|
m := uint16(*req.MinConfs)
|
||||||
|
minConfs = &m
|
||||||
|
}
|
||||||
|
var minDepth *uint16
|
||||||
|
if req.IsZeroConf {
|
||||||
|
var d uint16 = 0
|
||||||
|
minDepth = &d
|
||||||
|
}
|
||||||
|
|
||||||
|
var rate *glightning.FeeRate
|
||||||
|
if req.FeeSatPerVByte != nil {
|
||||||
|
rate = &glightning.FeeRate{
|
||||||
|
Rate: uint(*req.FeeSatPerVByte * 1000),
|
||||||
|
Style: glightning.PerKb,
|
||||||
|
}
|
||||||
|
} else if req.TargetConf != nil {
|
||||||
|
if *req.TargetConf < 3 {
|
||||||
|
rate = &glightning.FeeRate{
|
||||||
|
Directive: glightning.Urgent,
|
||||||
|
}
|
||||||
|
} else if *req.TargetConf < 30 {
|
||||||
|
rate = &glightning.FeeRate{
|
||||||
|
Directive: glightning.Normal,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rate = &glightning.FeeRate{
|
||||||
|
Directive: glightning.Slow,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fundResult, err := c.client.FundChannelExt(
|
||||||
|
pubkey,
|
||||||
|
glightning.NewSat(int(req.CapacitySat)),
|
||||||
|
rate,
|
||||||
|
!req.IsPrivate,
|
||||||
|
minConfs,
|
||||||
|
glightning.NewMsat(0),
|
||||||
|
minDepth,
|
||||||
|
glightning.NewMsat(0),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("CLN: client.FundChannelExt(%v, %v) error: %v", pubkey, req.CapacitySat, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fundingTxId, err := chainhash.NewHashFromStr(fundResult.FundingTxId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("CLN: chainhash.NewHashFromStr(%s) error: %v", fundResult.FundingTxId, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
channelPoint, err := basetypes.NewOutPoint(fundingTxId[:], uint32(fundResult.FundingTxOutputNum))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("CLN: NewOutPoint(%s, %d) error: %v", fundingTxId.String(), fundResult.FundingTxOutputNum, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return channelPoint, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnClient) GetChannel(peerID []byte, channelPoint wire.OutPoint) (*lightning.GetChannelResult, error) {
|
||||||
|
pubkey := hex.EncodeToString(peerID)
|
||||||
|
peer, err := c.client.GetPeer(pubkey)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("CLN: client.GetPeer(%s) error: %v", pubkey, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
fundingTxID := channelPoint.Hash.String()
|
||||||
|
for _, c := range peer.Channels {
|
||||||
|
log.Printf("getChannel destination: %s, Short channel id: %v, local alias: %v , FundingTxID:%v, State:%v ", pubkey, c.ShortChannelId, c.Alias.Local, c.FundingTxId, c.State)
|
||||||
|
if slices.Contains(OPEN_STATUSES, c.State) && c.FundingTxId == fundingTxID {
|
||||||
|
confirmedChanID, err := basetypes.NewShortChannelIDFromString(c.ShortChannelId)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("NewShortChannelIDFromString %v error: %v", c.ShortChannelId, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
initialChanID, err := basetypes.NewShortChannelIDFromString(c.Alias.Local)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Printf("NewShortChannelIDFromString %v error: %v", c.Alias.Local, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &lightning.GetChannelResult{
|
||||||
|
InitialChannelID: *initialChanID,
|
||||||
|
ConfirmedChannelID: *confirmedChanID,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("No channel found: getChannel(%v, %v)", pubkey, fundingTxID)
|
||||||
|
return nil, fmt.Errorf("no channel found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnClient) GetNodeChannelCount(nodeID []byte) (int, error) {
|
||||||
|
pubkey := hex.EncodeToString(nodeID)
|
||||||
|
peer, err := c.client.GetPeer(pubkey)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("CLN: client.GetPeer(%s) error: %v", pubkey, err)
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
openPendingStatuses := append(OPEN_STATUSES, PENDING_STATUSES...)
|
||||||
|
for _, c := range peer.Channels {
|
||||||
|
if slices.Contains(openPendingStatuses, c.State) {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnClient) GetClosedChannels(nodeID string, channelPoints map[string]uint64) (map[string]uint64, error) {
|
||||||
|
r := make(map[string]uint64)
|
||||||
|
if len(channelPoints) == 0 {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
peer, err := c.client.GetPeer(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("CLN: client.GetPeer(%s) error: %v", nodeID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lookup := make(map[string]uint64)
|
||||||
|
for _, c := range peer.Channels {
|
||||||
|
if slices.Contains(CLOSING_STATUSES, c.State) {
|
||||||
|
cid, err := basetypes.NewShortChannelIDFromString(c.ShortChannelId)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("CLN: GetClosedChannels NewShortChannelIDFromString(%v) error: %v", c.ShortChannelId, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
outnum := uint64(*cid) & 0xFFFFFF
|
||||||
|
cp := fmt.Sprintf("%s:%d", c.FundingTxId, outnum)
|
||||||
|
lookup[cp] = uint64(*cid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for c, h := range channelPoints {
|
||||||
|
if _, ok := lookup[c]; !ok {
|
||||||
|
r[c] = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnClient) GetPeerId(scid *basetypes.ShortChannelID) ([]byte, error) {
|
||||||
|
scidStr := scid.ToString()
|
||||||
|
peers, err := c.client.ListPeers()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var dest *string
|
||||||
|
for _, p := range peers {
|
||||||
|
for _, ch := range p.Channels {
|
||||||
|
if ch.Alias.Local == scidStr ||
|
||||||
|
ch.Alias.Remote == scidStr ||
|
||||||
|
ch.ShortChannelId == scidStr {
|
||||||
|
dest = &p.Id
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dest == nil {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return hex.DecodeString(*dest)
|
||||||
|
}
|
||||||
|
|
||||||
|
var pollingInterval = 400 * time.Millisecond
|
||||||
|
|
||||||
|
func (c *ClnClient) WaitOnline(peerID []byte, deadline time.Time) error {
|
||||||
|
peerIDStr := hex.EncodeToString(peerID)
|
||||||
|
for {
|
||||||
|
peer, err := c.client.GetPeer(peerIDStr)
|
||||||
|
if err == nil && peer.Connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-time.After(time.Until(deadline)):
|
||||||
|
return fmt.Errorf("timeout")
|
||||||
|
case <-time.After(pollingInterval):
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnClient) WaitChannelActive(peerID []byte, deadline time.Time) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
309
cln/cln_interceptor.go
Normal file
309
cln/cln_interceptor.go
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
package cln
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/basetypes"
|
||||||
|
"github.com/breez/lspd/cln_plugin/proto"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
"github.com/breez/lspd/interceptor"
|
||||||
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/record"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/keepalive"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClnHtlcInterceptor struct {
|
||||||
|
interceptor *interceptor.Interceptor
|
||||||
|
config *config.NodeConfig
|
||||||
|
pluginAddress string
|
||||||
|
client *ClnClient
|
||||||
|
pluginClient proto.ClnPluginClient
|
||||||
|
initWg sync.WaitGroup
|
||||||
|
doneWg sync.WaitGroup
|
||||||
|
stopRequested bool
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClnHtlcInterceptor(conf *config.NodeConfig, client *ClnClient, interceptor *interceptor.Interceptor) (*ClnHtlcInterceptor, error) {
|
||||||
|
i := &ClnHtlcInterceptor{
|
||||||
|
config: conf,
|
||||||
|
pluginAddress: conf.Cln.PluginAddress,
|
||||||
|
client: client,
|
||||||
|
interceptor: interceptor,
|
||||||
|
}
|
||||||
|
|
||||||
|
i.initWg.Add(1)
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ClnHtlcInterceptor) Start() error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
log.Printf("Dialing cln plugin on '%s'", i.pluginAddress)
|
||||||
|
conn, err := grpc.DialContext(
|
||||||
|
ctx,
|
||||||
|
i.pluginAddress,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||||
|
Time: time.Duration(10) * time.Second,
|
||||||
|
Timeout: time.Duration(10) * time.Second,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("grpc.Dial error: %v", err)
|
||||||
|
cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
i.pluginClient = proto.NewClnPluginClient(conn)
|
||||||
|
i.ctx = ctx
|
||||||
|
i.cancel = cancel
|
||||||
|
i.stopRequested = false
|
||||||
|
return i.intercept()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ClnHtlcInterceptor) intercept() error {
|
||||||
|
inited := false
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
if !inited {
|
||||||
|
i.initWg.Done()
|
||||||
|
}
|
||||||
|
log.Printf("CLN intercept(): stopping. Waiting for in-progress interceptions to complete.")
|
||||||
|
i.doneWg.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if i.ctx.Err() != nil {
|
||||||
|
return i.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Connecting CLN HTLC interceptor.")
|
||||||
|
interceptorClient, err := i.pluginClient.HtlcStream(i.ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("pluginClient.HtlcStream(): %v", err)
|
||||||
|
<-time.After(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if i.ctx.Err() != nil {
|
||||||
|
return i.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inited {
|
||||||
|
inited = true
|
||||||
|
i.initWg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop receiving if stop if requested. The defer func on top of this
|
||||||
|
// function will assure all htlcs that are currently being processed
|
||||||
|
// will complete.
|
||||||
|
if i.stopRequested {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := interceptorClient.Recv()
|
||||||
|
if err != nil {
|
||||||
|
// If it is just the error result of the context cancellation
|
||||||
|
// the we exit silently.
|
||||||
|
status, ok := status.FromError(err)
|
||||||
|
if ok && status.Code() == codes.Canceled {
|
||||||
|
log.Printf("Got code canceled. Break.")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise it an unexpected error, we fail the test.
|
||||||
|
log.Printf("unexpected error in interceptor.Recv() %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
i.doneWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
paymentHash, err := hex.DecodeString(request.Htlc.PaymentHash)
|
||||||
|
if err != nil {
|
||||||
|
interceptorClient.Send(i.defaultResolution(request))
|
||||||
|
i.doneWg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
scid, err := basetypes.NewShortChannelIDFromString(request.Onion.ShortChannelId)
|
||||||
|
if err != nil {
|
||||||
|
interceptorClient.Send(i.defaultResolution(request))
|
||||||
|
i.doneWg.Done()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
interceptResult := i.interceptor.Intercept(scid, paymentHash, request.Onion.ForwardMsat, request.Onion.OutgoingCltvValue, request.Htlc.CltvExpiry)
|
||||||
|
switch interceptResult.Action {
|
||||||
|
case interceptor.INTERCEPT_RESUME_WITH_ONION:
|
||||||
|
interceptorClient.Send(i.resumeWithOnion(request, interceptResult))
|
||||||
|
case interceptor.INTERCEPT_FAIL_HTLC_WITH_CODE:
|
||||||
|
interceptorClient.Send(
|
||||||
|
i.failWithCode(request, interceptResult.FailureCode),
|
||||||
|
)
|
||||||
|
case interceptor.INTERCEPT_RESUME:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
interceptorClient.Send(
|
||||||
|
i.defaultResolution(request),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
i.doneWg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
<-time.After(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ClnHtlcInterceptor) Stop() error {
|
||||||
|
// Setting stopRequested to true will make the interceptor stop receiving.
|
||||||
|
i.stopRequested = true
|
||||||
|
|
||||||
|
// Wait until all already received htlcs are handled, responses sent back.
|
||||||
|
i.doneWg.Wait()
|
||||||
|
|
||||||
|
// Close the grpc connection.
|
||||||
|
i.cancel()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ClnHtlcInterceptor) WaitStarted() {
|
||||||
|
i.initWg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ClnHtlcInterceptor) resumeWithOnion(request *proto.HtlcAccepted, interceptResult interceptor.InterceptResult) *proto.HtlcResolution {
|
||||||
|
//decoding and encoding onion with alias in type 6 record.
|
||||||
|
payload, err := hex.DecodeString(request.Onion.Payload)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("resumeWithOnion: hex.DecodeString(%v) error: %v", request.Onion.Payload, err)
|
||||||
|
return i.failWithCode(request, interceptor.FAILURE_TEMPORARY_CHANNEL_FAILURE)
|
||||||
|
}
|
||||||
|
newPayload, err := encodePayloadWithNextHop(payload, interceptResult.ChannelId, interceptResult.AmountMsat)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("encodePayloadWithNextHop error: %v", err)
|
||||||
|
return i.failWithCode(request, interceptor.FAILURE_TEMPORARY_CHANNEL_FAILURE)
|
||||||
|
}
|
||||||
|
|
||||||
|
newPayloadStr := hex.EncodeToString(newPayload)
|
||||||
|
|
||||||
|
chanId := lnwire.NewChanIDFromOutPoint(interceptResult.ChannelPoint).String()
|
||||||
|
log.Printf("forwarding htlc to the destination node and a new private channel was opened")
|
||||||
|
return &proto.HtlcResolution{
|
||||||
|
Correlationid: request.Correlationid,
|
||||||
|
Outcome: &proto.HtlcResolution_Continue{
|
||||||
|
Continue: &proto.HtlcContinue{
|
||||||
|
ForwardTo: &chanId,
|
||||||
|
Payload: &newPayloadStr,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ClnHtlcInterceptor) defaultResolution(request *proto.HtlcAccepted) *proto.HtlcResolution {
|
||||||
|
return &proto.HtlcResolution{
|
||||||
|
Correlationid: request.Correlationid,
|
||||||
|
Outcome: &proto.HtlcResolution_Continue{
|
||||||
|
Continue: &proto.HtlcContinue{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ClnHtlcInterceptor) failWithCode(request *proto.HtlcAccepted, code interceptor.InterceptFailureCode) *proto.HtlcResolution {
|
||||||
|
return &proto.HtlcResolution{
|
||||||
|
Correlationid: request.Correlationid,
|
||||||
|
Outcome: &proto.HtlcResolution_Fail{
|
||||||
|
Fail: &proto.HtlcFail{
|
||||||
|
Failure: &proto.HtlcFail_FailureMessage{
|
||||||
|
FailureMessage: i.mapFailureCode(code),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func encodePayloadWithNextHop(payload []byte, channelId uint64, amountToForward uint64) ([]byte, error) {
|
||||||
|
bufReader := bytes.NewBuffer(payload)
|
||||||
|
var b [8]byte
|
||||||
|
varInt, err := sphinx.ReadVarInt(bufReader, &b)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to read payload length %x: %v", payload, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
innerPayload := make([]byte, varInt)
|
||||||
|
if _, err := io.ReadFull(bufReader, innerPayload[:]); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode payload %x: %v", innerPayload[:], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, _ := tlv.NewStream()
|
||||||
|
tlvMap, err := s.DecodeWithParsedTypes(bytes.NewReader(innerPayload))
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("DecodeWithParsedTypes failed for %x: %v", innerPayload[:], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
tt := record.NewNextHopIDRecord(&channelId)
|
||||||
|
ttbuf := bytes.NewBuffer([]byte{})
|
||||||
|
if err := tt.Encode(ttbuf); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode nexthop %x: %v", innerPayload[:], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
amt := record.NewAmtToFwdRecord(&amountToForward)
|
||||||
|
amtbuf := bytes.NewBuffer([]byte{})
|
||||||
|
if err := amt.Encode(amtbuf); err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to encode AmtToFwd %x: %v", innerPayload[:], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uTlvMap := make(map[uint64][]byte)
|
||||||
|
for t, b := range tlvMap {
|
||||||
|
if t == record.NextHopOnionType {
|
||||||
|
uTlvMap[uint64(t)] = ttbuf.Bytes()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if t == record.AmtOnionType {
|
||||||
|
uTlvMap[uint64(t)] = amtbuf.Bytes()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
uTlvMap[uint64(t)] = b
|
||||||
|
}
|
||||||
|
tlvRecords := tlv.MapToRecords(uTlvMap)
|
||||||
|
s, err = tlv.NewStream(tlvRecords...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("tlv.NewStream(%x) error: %v", tlvRecords, err)
|
||||||
|
}
|
||||||
|
var newPayloadBuf bytes.Buffer
|
||||||
|
err = s.Encode(&newPayloadBuf)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("encode error: %v", err)
|
||||||
|
}
|
||||||
|
return newPayloadBuf.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *ClnHtlcInterceptor) mapFailureCode(original interceptor.InterceptFailureCode) string {
|
||||||
|
switch original {
|
||||||
|
case interceptor.FAILURE_TEMPORARY_CHANNEL_FAILURE:
|
||||||
|
return "1007"
|
||||||
|
case interceptor.FAILURE_TEMPORARY_NODE_FAILURE:
|
||||||
|
return "2002"
|
||||||
|
case interceptor.FAILURE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS:
|
||||||
|
return "400F"
|
||||||
|
default:
|
||||||
|
log.Printf("Unknown failure code %v, default to temporary channel failure.", original)
|
||||||
|
return "1007" // temporary channel failure
|
||||||
|
}
|
||||||
|
}
|
||||||
44
cln_plugin/channel_acceptor.go
Normal file
44
cln_plugin/channel_acceptor.go
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
package cln_plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
sj "go.starlark.net/lib/json"
|
||||||
|
"go.starlark.net/starlark"
|
||||||
|
)
|
||||||
|
|
||||||
|
func channelAcceptor(acceptScript string, method string, openChannel json.RawMessage) (json.RawMessage, error) {
|
||||||
|
reject, _ := json.Marshal(struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
}{Result: "reject"})
|
||||||
|
accept, _ := json.Marshal(struct {
|
||||||
|
Result string `json:"result"`
|
||||||
|
}{Result: "continue"})
|
||||||
|
|
||||||
|
if acceptScript == "" {
|
||||||
|
return accept, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
sd := starlark.StringDict{
|
||||||
|
"method": starlark.String(method),
|
||||||
|
"openchannel": starlark.String(openChannel),
|
||||||
|
}
|
||||||
|
for _, k := range sj.Module.Members.Keys() {
|
||||||
|
sd[k] = sj.Module.Members[k]
|
||||||
|
}
|
||||||
|
value, err := starlark.Eval(
|
||||||
|
&starlark.Thread{},
|
||||||
|
"",
|
||||||
|
acceptScript,
|
||||||
|
sd,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return reject, err
|
||||||
|
}
|
||||||
|
s, ok := value.(starlark.String)
|
||||||
|
if !ok {
|
||||||
|
return reject, fmt.Errorf("not a string")
|
||||||
|
}
|
||||||
|
return json.RawMessage(s.GoString()), nil
|
||||||
|
}
|
||||||
117
cln_plugin/cln_messages.go
Normal file
117
cln_plugin/cln_messages.go
Normal file
@@ -0,0 +1,117 @@
|
|||||||
|
package cln_plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Request struct {
|
||||||
|
Id json.RawMessage `json:"id,omitempty"`
|
||||||
|
Method string `json:"method"`
|
||||||
|
JsonRpc string `json:"jsonrpc"`
|
||||||
|
Params json.RawMessage `json:"params,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Id json.RawMessage `json:"id"`
|
||||||
|
JsonRpc string `json:"jsonrpc"`
|
||||||
|
Result Result `json:"result,omitempty"`
|
||||||
|
Error *RpcError `json:"error,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Result interface{}
|
||||||
|
|
||||||
|
type RpcError struct {
|
||||||
|
Code int `json:"code"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
Data json.RawMessage `json:"data,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Manifest struct {
|
||||||
|
Options []Option `json:"options"`
|
||||||
|
RpcMethods []*RpcMethod `json:"rpcmethods"`
|
||||||
|
Dynamic bool `json:"dynamic"`
|
||||||
|
Subscriptions []string `json:"subscriptions,omitempty"`
|
||||||
|
Hooks []Hook `json:"hooks,omitempty"`
|
||||||
|
FeatureBits *FeatureBits `json:"featurebits,omitempty"`
|
||||||
|
NonNumericIds bool `json:"nonnumericids"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Option struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
Default *string `json:"default,omitempty"`
|
||||||
|
Multi *bool `json:"multi,omitempty"`
|
||||||
|
Deprecated *bool `json:"deprecated,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type RpcMethod struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Usage string `json:"usage"`
|
||||||
|
Description string `json:"description"`
|
||||||
|
LongDescription *string `json:"long_description,omitempty"`
|
||||||
|
Deprecated *bool `json:"deprecated,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Hook struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Before []string `json:"before,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeatureBits struct {
|
||||||
|
Node *string `json:"node,omitempty"`
|
||||||
|
Init *string `json:"init,omitempty"`
|
||||||
|
Channel *string `json:"channel,omitempty"`
|
||||||
|
Invoice *string `json:"invoice,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitMessage struct {
|
||||||
|
Options map[string]interface{} `json:"options,omitempty"`
|
||||||
|
Configuration *InitConfiguration `json:"configuration,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InitConfiguration struct {
|
||||||
|
LightningDir string `json:"lightning-dir"`
|
||||||
|
RpcFile string `json:"rpc-file"`
|
||||||
|
Startup bool `json:"startup"`
|
||||||
|
Network string `json:"network"`
|
||||||
|
FeatureSet *FeatureBits `json:"feature_set"`
|
||||||
|
Proxy *Proxy `json:"proxy"`
|
||||||
|
TorV3Enabled bool `json:"torv3-enabled"`
|
||||||
|
AlwaysUseProxy bool `json:"always_use_proxy"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Proxy struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Address string `json:"address"`
|
||||||
|
Port int `json:"port"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtlcAccepted struct {
|
||||||
|
Onion *Onion `json:"onion"`
|
||||||
|
Htlc *Htlc `json:"htlc"`
|
||||||
|
ForwardTo string `json:"forward_to"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Onion struct {
|
||||||
|
Payload string `json:"payload"`
|
||||||
|
ShortChannelId string `json:"short_channel_id"`
|
||||||
|
ForwardMsat uint64 `json:"forward_msat"`
|
||||||
|
OutgoingCltvValue uint32 `json:"outgoing_cltv_value"`
|
||||||
|
SharedSecret string `json:"shared_secret"`
|
||||||
|
NextOnion string `json:"next_onion"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Htlc struct {
|
||||||
|
ShortChannelId string `json:"short_channel_id"`
|
||||||
|
Id uint64 `json:"id"`
|
||||||
|
AmountMsat uint64 `json:"amount_msat"`
|
||||||
|
CltvExpiry uint32 `json:"cltv_expiry"`
|
||||||
|
CltvExpiryRelative uint32 `json:"cltv_expiry_relative"`
|
||||||
|
PaymentHash string `json:"payment_hash"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LogNotification struct {
|
||||||
|
Level string `json:"level"`
|
||||||
|
Message string `json:"message"`
|
||||||
|
}
|
||||||
622
cln_plugin/cln_plugin.go
Normal file
622
cln_plugin/cln_plugin.go
Normal file
@@ -0,0 +1,622 @@
|
|||||||
|
// The code in this plugin is highly inspired by and sometimes copied from
|
||||||
|
// github.com/niftynei/glightning. Therefore pieces of this code are subject
|
||||||
|
// to Copyright Lisa Neigut (Blockstream) 2019.
|
||||||
|
|
||||||
|
package cln_plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SubscriberTimeoutOption = "lsp-subscribertimeout"
|
||||||
|
ListenAddressOption = "lsp-listen"
|
||||||
|
channelAcceptScript = "lsp-channel-accept-script"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultSubscriberTimeout = "1m"
|
||||||
|
DefaultChannelAcceptorScript = ""
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
MaxIntakeBuffer = 500 * 1024 * 1023
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
SpecVersion = "2.0"
|
||||||
|
ParseError = -32700
|
||||||
|
InvalidRequest = -32600
|
||||||
|
MethodNotFound = -32601
|
||||||
|
InvalidParams = -32603
|
||||||
|
InternalErr = -32603
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
TwoNewLines = []byte("\n\n")
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClnPlugin struct {
|
||||||
|
done chan struct{}
|
||||||
|
server *server
|
||||||
|
in *os.File
|
||||||
|
out *bufio.Writer
|
||||||
|
writeMtx sync.Mutex
|
||||||
|
channelAcceptScript string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClnPlugin(in, out *os.File) *ClnPlugin {
|
||||||
|
c := &ClnPlugin{
|
||||||
|
done: make(chan struct{}),
|
||||||
|
in: in,
|
||||||
|
out: bufio.NewWriter(out),
|
||||||
|
}
|
||||||
|
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts the cln plugin.
|
||||||
|
// NOTE: The grpc server is started in the handleInit function.
|
||||||
|
func (c *ClnPlugin) Start() {
|
||||||
|
c.setupLogging()
|
||||||
|
go c.listenRequests()
|
||||||
|
<-c.done
|
||||||
|
s := c.server
|
||||||
|
if s != nil {
|
||||||
|
<-s.completed
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stops the cln plugin. Drops any remaining work immediately.
|
||||||
|
// Pending htlcs will be replayed when cln starts again.
|
||||||
|
func (c *ClnPlugin) Stop() {
|
||||||
|
log.Printf("Stop called. Stopping plugin.")
|
||||||
|
close(c.done)
|
||||||
|
|
||||||
|
s := c.server
|
||||||
|
if s != nil {
|
||||||
|
s.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// listens stdout for requests from cln and sends the requests to the
|
||||||
|
// appropriate handler in fifo order.
|
||||||
|
func (c *ClnPlugin) listenRequests() error {
|
||||||
|
scanner := bufio.NewScanner(c.in)
|
||||||
|
buf := make([]byte, 1024)
|
||||||
|
scanner.Buffer(buf, MaxIntakeBuffer)
|
||||||
|
|
||||||
|
// cln messages are split by a double newline.
|
||||||
|
scanner.Split(scanDoubleNewline)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.done:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
if !scanner.Scan() {
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
msg := scanner.Bytes()
|
||||||
|
|
||||||
|
// Always log the message json
|
||||||
|
log.Println(string(msg))
|
||||||
|
|
||||||
|
// pass down a copy so things stay sane
|
||||||
|
msg_buf := make([]byte, len(msg))
|
||||||
|
copy(msg_buf, msg)
|
||||||
|
|
||||||
|
// NOTE: processMsg is synchronous, so it should only do quick work.
|
||||||
|
c.processMsg(msg_buf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listens to responses to htlc_accepted requests from the grpc server.
|
||||||
|
func (c *ClnPlugin) listenServer() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
id, result := c.server.Receive()
|
||||||
|
|
||||||
|
// The server may return nil if it is stopped.
|
||||||
|
if result == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
serid, _ := json.Marshal(&id)
|
||||||
|
c.sendToCln(&Response{
|
||||||
|
Id: serid,
|
||||||
|
JsonRpc: SpecVersion,
|
||||||
|
Result: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// processes a single message from cln. Sends the message to the appropriate
|
||||||
|
// handler.
|
||||||
|
func (c *ClnPlugin) processMsg(msg []byte) {
|
||||||
|
if len(msg) == 0 {
|
||||||
|
c.sendError(nil, InvalidRequest, "Got an invalid zero length request")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handle request batches.
|
||||||
|
if msg[0] == '[' {
|
||||||
|
var requests []*Request
|
||||||
|
err := json.Unmarshal(msg, &requests)
|
||||||
|
if err != nil {
|
||||||
|
c.sendError(
|
||||||
|
nil,
|
||||||
|
ParseError,
|
||||||
|
fmt.Sprintf("Failed to unmarshal request batch: %v", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, request := range requests {
|
||||||
|
c.processRequest(request)
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Parse the received buffer into a request object.
|
||||||
|
var request Request
|
||||||
|
err := json.Unmarshal(msg, &request)
|
||||||
|
if err != nil {
|
||||||
|
c.sendError(
|
||||||
|
nil,
|
||||||
|
ParseError,
|
||||||
|
fmt.Sprintf("failed to unmarshal request: %v", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.processRequest(&request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnPlugin) processRequest(request *Request) {
|
||||||
|
// Make sure the spec version is expected.
|
||||||
|
if request.JsonRpc != SpecVersion {
|
||||||
|
c.sendError(request.Id, InvalidRequest, fmt.Sprintf(
|
||||||
|
`Invalid jsonrpc, expected '%s' got '%s'`,
|
||||||
|
SpecVersion,
|
||||||
|
request.JsonRpc,
|
||||||
|
))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send the message to the appropriate handler.
|
||||||
|
switch request.Method {
|
||||||
|
case "getmanifest":
|
||||||
|
c.handleGetManifest(request)
|
||||||
|
case "init":
|
||||||
|
c.handleInit(request)
|
||||||
|
case "shutdown":
|
||||||
|
c.handleShutdown(request)
|
||||||
|
case "htlc_accepted":
|
||||||
|
c.handleHtlcAccepted(request)
|
||||||
|
case "openchannel":
|
||||||
|
// handle open channel in a goroutine, because order doesn't matter.
|
||||||
|
go c.handleOpenChannel(request)
|
||||||
|
case "openchannel2":
|
||||||
|
// handle open channel in a goroutine, because order doesn't matter.
|
||||||
|
go c.handleOpenChannel(request)
|
||||||
|
case "getchannelacceptscript":
|
||||||
|
c.sendToCln(&Response{
|
||||||
|
JsonRpc: SpecVersion,
|
||||||
|
Id: request.Id,
|
||||||
|
Result: c.channelAcceptScript,
|
||||||
|
})
|
||||||
|
case "setchannelacceptscript":
|
||||||
|
c.handleSetChannelAcceptScript(request)
|
||||||
|
default:
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
MethodNotFound,
|
||||||
|
fmt.Sprintf("Method '%s' not found", request.Method),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns this plugin's manifest to cln.
|
||||||
|
func (c *ClnPlugin) handleGetManifest(request *Request) {
|
||||||
|
c.sendToCln(&Response{
|
||||||
|
Id: request.Id,
|
||||||
|
JsonRpc: SpecVersion,
|
||||||
|
Result: &Manifest{
|
||||||
|
Options: []Option{
|
||||||
|
{
|
||||||
|
Name: ListenAddressOption,
|
||||||
|
Type: "string",
|
||||||
|
Description: "listen address for the htlc_accepted lsp " +
|
||||||
|
"grpc server",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: SubscriberTimeoutOption,
|
||||||
|
Type: "string",
|
||||||
|
Description: "the maximum duration we will hold a htlc " +
|
||||||
|
"if no subscriber is active. golang duration string.",
|
||||||
|
Default: &DefaultSubscriberTimeout,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: channelAcceptScript,
|
||||||
|
Type: "string",
|
||||||
|
Description: "starlark script for channel acceptor.",
|
||||||
|
Default: &DefaultChannelAcceptorScript,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
RpcMethods: []*RpcMethod{
|
||||||
|
{
|
||||||
|
Name: "getchannelacceptscript",
|
||||||
|
Description: "Get the startlark channel acceptor script",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Name: "setchannelacceptscript",
|
||||||
|
Description: "Set the startlark channel acceptor script",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Dynamic: true,
|
||||||
|
Hooks: []Hook{
|
||||||
|
{Name: "htlc_accepted"},
|
||||||
|
{Name: "openchannel"},
|
||||||
|
{Name: "openchannel2"},
|
||||||
|
},
|
||||||
|
NonNumericIds: true,
|
||||||
|
Subscriptions: []string{
|
||||||
|
"shutdown",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles plugin initialization. Parses startup options and starts the grpc
|
||||||
|
// server.
|
||||||
|
func (c *ClnPlugin) handleInit(request *Request) {
|
||||||
|
// Deserialize the init message.
|
||||||
|
var initMsg InitMessage
|
||||||
|
err := json.Unmarshal(request.Params, &initMsg)
|
||||||
|
if err != nil {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
ParseError,
|
||||||
|
fmt.Sprintf("Failed to unmarshal init params: %v", err),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the channel acceptor script option.
|
||||||
|
sc, ok := initMsg.Options[channelAcceptScript]
|
||||||
|
if !ok {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
InvalidParams,
|
||||||
|
fmt.Sprintf("Missing option '%s'", channelAcceptScript),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.channelAcceptScript, ok = sc.(string)
|
||||||
|
if !ok {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
InvalidParams,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Invalid value '%v' for option '%s'",
|
||||||
|
sc,
|
||||||
|
channelAcceptScript,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the listen address option.
|
||||||
|
l, ok := initMsg.Options[ListenAddressOption]
|
||||||
|
if !ok {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
InvalidParams,
|
||||||
|
fmt.Sprintf("Missing option '%s'", ListenAddressOption),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
addr, ok := l.(string)
|
||||||
|
if !ok || addr == "" {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
InvalidParams,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Invalid value '%v' for option '%s'",
|
||||||
|
l,
|
||||||
|
ListenAddressOption,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get the subscriber timeout option.
|
||||||
|
t, ok := initMsg.Options[SubscriberTimeoutOption]
|
||||||
|
if !ok {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
InvalidParams,
|
||||||
|
fmt.Sprintf("Missing option '%s'", SubscriberTimeoutOption),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s, ok := t.(string)
|
||||||
|
if !ok || s == "" {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
InvalidParams,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Invalid value '%v' for option '%s'",
|
||||||
|
t,
|
||||||
|
SubscriberTimeoutOption,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
subscriberTimeout, err := time.ParseDuration(s)
|
||||||
|
if err != nil {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
InvalidParams,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Invalid value '%v' for option '%s'",
|
||||||
|
s,
|
||||||
|
SubscriberTimeoutOption,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Start the grpc server.
|
||||||
|
c.server = NewServer(addr, subscriberTimeout)
|
||||||
|
go c.server.Start()
|
||||||
|
err = c.server.WaitStarted()
|
||||||
|
if err != nil {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
InternalErr,
|
||||||
|
fmt.Sprintf("Failed to start server: %s", err.Error()),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listen for responses from the grpc server.
|
||||||
|
go c.listenServer()
|
||||||
|
|
||||||
|
// Let cln know the plugin is initialized.
|
||||||
|
c.sendToCln(&Response{
|
||||||
|
Id: request.Id,
|
||||||
|
JsonRpc: SpecVersion,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Handles the shutdown message. Stops any work immediately.
|
||||||
|
func (c *ClnPlugin) handleShutdown(request *Request) {
|
||||||
|
c.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends a htlc_accepted message to the grpc server.
|
||||||
|
func (c *ClnPlugin) handleHtlcAccepted(request *Request) {
|
||||||
|
var htlc HtlcAccepted
|
||||||
|
err := json.Unmarshal(request.Params, &htlc)
|
||||||
|
if err != nil {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
ParseError,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Failed to unmarshal htlc_accepted params:%s [%s]",
|
||||||
|
err.Error(),
|
||||||
|
request.Params,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.server.Send(idToString(request.Id), &htlc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnPlugin) handleSetChannelAcceptScript(request *Request) {
|
||||||
|
var params []string
|
||||||
|
err := json.Unmarshal(request.Params, ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
ParseError,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Failed to unmarshal setchannelacceptscript params:%s [%s]",
|
||||||
|
err.Error(),
|
||||||
|
request.Params,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(params) >= 1 {
|
||||||
|
c.channelAcceptScript = params[0]
|
||||||
|
}
|
||||||
|
c.sendToCln(&Response{
|
||||||
|
JsonRpc: SpecVersion,
|
||||||
|
Id: request.Id,
|
||||||
|
Result: c.channelAcceptScript,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func unmarshalOpenChannel(request *Request) (r json.RawMessage, err error) {
|
||||||
|
switch request.Method {
|
||||||
|
case "openchannel":
|
||||||
|
var openChannel struct {
|
||||||
|
OpenChannel json.RawMessage `json:"openchannel"`
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(request.Params, &openChannel)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r = openChannel.OpenChannel
|
||||||
|
case "openchannel2":
|
||||||
|
var openChannel struct {
|
||||||
|
OpenChannel json.RawMessage `json:"openchannel2"`
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(request.Params, &openChannel)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
r = openChannel.OpenChannel
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
func (c *ClnPlugin) handleOpenChannel(request *Request) {
|
||||||
|
p, err := unmarshalOpenChannel(request)
|
||||||
|
if err != nil {
|
||||||
|
c.sendError(
|
||||||
|
request.Id,
|
||||||
|
ParseError,
|
||||||
|
fmt.Sprintf(
|
||||||
|
"Failed to unmarshal openchannel params:%s [%s]",
|
||||||
|
err.Error(),
|
||||||
|
request.Params,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
result, err := channelAcceptor(c.channelAcceptScript, request.Method, p)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("channelAcceptor error - request: %s error: %v", request, err)
|
||||||
|
}
|
||||||
|
c.sendToCln(&Response{
|
||||||
|
JsonRpc: SpecVersion,
|
||||||
|
Id: request.Id,
|
||||||
|
Result: result,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends an error to cln.
|
||||||
|
func (c *ClnPlugin) sendError(id json.RawMessage, code int, message string) {
|
||||||
|
// Log the error to cln first.
|
||||||
|
c.log("error", message)
|
||||||
|
|
||||||
|
// Then create an error message.
|
||||||
|
resp := &Response{
|
||||||
|
JsonRpc: SpecVersion,
|
||||||
|
Error: &RpcError{
|
||||||
|
Code: code,
|
||||||
|
Message: message,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(id) > 0 {
|
||||||
|
resp.Id = id
|
||||||
|
}
|
||||||
|
|
||||||
|
c.sendToCln(resp)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sends a message to cln.
|
||||||
|
func (c *ClnPlugin) sendToCln(msg interface{}) {
|
||||||
|
c.writeMtx.Lock()
|
||||||
|
defer c.writeMtx.Unlock()
|
||||||
|
|
||||||
|
data, err := json.Marshal(msg)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to marshal message for cln, ignoring message: %+v", msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, TwoNewLines...)
|
||||||
|
c.out.Write(data)
|
||||||
|
c.out.Flush()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnPlugin) setupLogging() {
|
||||||
|
in, out := io.Pipe()
|
||||||
|
log.SetFlags(log.Ltime | log.Lshortfile)
|
||||||
|
log.SetOutput(out)
|
||||||
|
go func(in io.Reader) {
|
||||||
|
// everytime we get a new message, log it thru c-lightning
|
||||||
|
scanner := bufio.NewScanner(in)
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-c.done:
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
if !scanner.Scan() {
|
||||||
|
if err := scanner.Err(); err != nil {
|
||||||
|
log.Fatalf(
|
||||||
|
"can't print out to std err, killing: %v",
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, line := range strings.Split(scanner.Text(), "\n") {
|
||||||
|
c.log("info", line)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}(in)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnPlugin) log(level string, message string) {
|
||||||
|
params, _ := json.Marshal(&LogNotification{
|
||||||
|
Level: level,
|
||||||
|
Message: message,
|
||||||
|
})
|
||||||
|
|
||||||
|
c.sendToCln(&Request{
|
||||||
|
Method: "log",
|
||||||
|
JsonRpc: SpecVersion,
|
||||||
|
Params: params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper method for the bufio scanner to split messages on double newlines.
|
||||||
|
func scanDoubleNewline(
|
||||||
|
data []byte,
|
||||||
|
atEOF bool,
|
||||||
|
) (advance int, token []byte, err error) {
|
||||||
|
for i := 0; i < len(data); i++ {
|
||||||
|
if data[i] == '\n' && (i+1) < len(data) && data[i+1] == '\n' {
|
||||||
|
return i + 2, data[:i], nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// this trashes anything left over in
|
||||||
|
// the buffer if we're at EOF, with no /n/n present
|
||||||
|
return 0, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// converts a raw cln id to string. The CLN id can either be an integer or a
|
||||||
|
// string. if it's a string, the quotes are removed.
|
||||||
|
func idToString(id json.RawMessage) string {
|
||||||
|
if len(id) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
str := string(id)
|
||||||
|
str = strings.TrimSpace(str)
|
||||||
|
str = strings.Trim(str, "\"")
|
||||||
|
str = strings.Trim(str, "'")
|
||||||
|
return str
|
||||||
|
}
|
||||||
21
cln_plugin/cmd/main.go
Normal file
21
cln_plugin/cmd/main.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/cln_plugin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
plugin := cln_plugin.NewClnPlugin(os.Stdin, os.Stdout)
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
<-c
|
||||||
|
// Stop everything gracefully on stop signal
|
||||||
|
plugin.Stop()
|
||||||
|
}()
|
||||||
|
plugin.Start()
|
||||||
|
}
|
||||||
5
cln_plugin/genproto.sh
Executable file
5
cln_plugin/genproto.sh
Executable file
@@ -0,0 +1,5 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
SCRIPTDIR=$(dirname $0)
|
||||||
|
PROTO_ROOT=$SCRIPTDIR/proto
|
||||||
|
|
||||||
|
protoc --go_out=$PROTO_ROOT --go_opt=paths=source_relative --go-grpc_out=$PROTO_ROOT --go-grpc_opt=paths=source_relative -I=$PROTO_ROOT $PROTO_ROOT/*.proto
|
||||||
787
cln_plugin/proto/cln_plugin.pb.go
Normal file
787
cln_plugin/proto/cln_plugin.pb.go
Normal file
@@ -0,0 +1,787 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.28.1
|
||||||
|
// protoc v3.21.12
|
||||||
|
// source: cln_plugin.proto
|
||||||
|
|
||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type HtlcAccepted struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Correlationid string `protobuf:"bytes,1,opt,name=correlationid,proto3" json:"correlationid,omitempty"`
|
||||||
|
Onion *Onion `protobuf:"bytes,2,opt,name=onion,proto3" json:"onion,omitempty"`
|
||||||
|
Htlc *Htlc `protobuf:"bytes,3,opt,name=htlc,proto3" json:"htlc,omitempty"`
|
||||||
|
ForwardTo string `protobuf:"bytes,4,opt,name=forward_to,json=forwardTo,proto3" json:"forward_to,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcAccepted) Reset() {
|
||||||
|
*x = HtlcAccepted{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcAccepted) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*HtlcAccepted) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *HtlcAccepted) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use HtlcAccepted.ProtoReflect.Descriptor instead.
|
||||||
|
func (*HtlcAccepted) Descriptor() ([]byte, []int) {
|
||||||
|
return file_cln_plugin_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcAccepted) GetCorrelationid() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Correlationid
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcAccepted) GetOnion() *Onion {
|
||||||
|
if x != nil {
|
||||||
|
return x.Onion
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcAccepted) GetHtlc() *Htlc {
|
||||||
|
if x != nil {
|
||||||
|
return x.Htlc
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcAccepted) GetForwardTo() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ForwardTo
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type Onion struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Payload string `protobuf:"bytes,1,opt,name=payload,proto3" json:"payload,omitempty"`
|
||||||
|
ShortChannelId string `protobuf:"bytes,2,opt,name=short_channel_id,json=shortChannelId,proto3" json:"short_channel_id,omitempty"`
|
||||||
|
ForwardMsat uint64 `protobuf:"varint,3,opt,name=forward_msat,json=forwardMsat,proto3" json:"forward_msat,omitempty"`
|
||||||
|
OutgoingCltvValue uint32 `protobuf:"varint,4,opt,name=outgoing_cltv_value,json=outgoingCltvValue,proto3" json:"outgoing_cltv_value,omitempty"`
|
||||||
|
SharedSecret string `protobuf:"bytes,5,opt,name=shared_secret,json=sharedSecret,proto3" json:"shared_secret,omitempty"`
|
||||||
|
NextOnion string `protobuf:"bytes,6,opt,name=next_onion,json=nextOnion,proto3" json:"next_onion,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Onion) Reset() {
|
||||||
|
*x = Onion{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Onion) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Onion) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Onion) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[1]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Onion.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Onion) Descriptor() ([]byte, []int) {
|
||||||
|
return file_cln_plugin_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Onion) GetPayload() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Payload
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Onion) GetShortChannelId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ShortChannelId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Onion) GetForwardMsat() uint64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.ForwardMsat
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Onion) GetOutgoingCltvValue() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.OutgoingCltvValue
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Onion) GetSharedSecret() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.SharedSecret
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Onion) GetNextOnion() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.NextOnion
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type Htlc struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
ShortChannelId string `protobuf:"bytes,1,opt,name=short_channel_id,json=shortChannelId,proto3" json:"short_channel_id,omitempty"`
|
||||||
|
Id uint64 `protobuf:"varint,2,opt,name=id,proto3" json:"id,omitempty"`
|
||||||
|
AmountMsat uint64 `protobuf:"varint,3,opt,name=amount_msat,json=amountMsat,proto3" json:"amount_msat,omitempty"`
|
||||||
|
CltvExpiry uint32 `protobuf:"varint,4,opt,name=cltv_expiry,json=cltvExpiry,proto3" json:"cltv_expiry,omitempty"`
|
||||||
|
CltvExpiryRelative uint32 `protobuf:"varint,5,opt,name=cltv_expiry_relative,json=cltvExpiryRelative,proto3" json:"cltv_expiry_relative,omitempty"`
|
||||||
|
PaymentHash string `protobuf:"bytes,6,opt,name=payment_hash,json=paymentHash,proto3" json:"payment_hash,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Htlc) Reset() {
|
||||||
|
*x = Htlc{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[2]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Htlc) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*Htlc) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *Htlc) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[2]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use Htlc.ProtoReflect.Descriptor instead.
|
||||||
|
func (*Htlc) Descriptor() ([]byte, []int) {
|
||||||
|
return file_cln_plugin_proto_rawDescGZIP(), []int{2}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Htlc) GetShortChannelId() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.ShortChannelId
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Htlc) GetId() uint64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.Id
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Htlc) GetAmountMsat() uint64 {
|
||||||
|
if x != nil {
|
||||||
|
return x.AmountMsat
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Htlc) GetCltvExpiry() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.CltvExpiry
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Htlc) GetCltvExpiryRelative() uint32 {
|
||||||
|
if x != nil {
|
||||||
|
return x.CltvExpiryRelative
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *Htlc) GetPaymentHash() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PaymentHash
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtlcResolution struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Correlationid string `protobuf:"bytes,1,opt,name=correlationid,proto3" json:"correlationid,omitempty"`
|
||||||
|
// Types that are assignable to Outcome:
|
||||||
|
// *HtlcResolution_Fail
|
||||||
|
// *HtlcResolution_Continue
|
||||||
|
// *HtlcResolution_Resolve
|
||||||
|
Outcome isHtlcResolution_Outcome `protobuf_oneof:"outcome"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcResolution) Reset() {
|
||||||
|
*x = HtlcResolution{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[3]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcResolution) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*HtlcResolution) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *HtlcResolution) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[3]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use HtlcResolution.ProtoReflect.Descriptor instead.
|
||||||
|
func (*HtlcResolution) Descriptor() ([]byte, []int) {
|
||||||
|
return file_cln_plugin_proto_rawDescGZIP(), []int{3}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcResolution) GetCorrelationid() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Correlationid
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *HtlcResolution) GetOutcome() isHtlcResolution_Outcome {
|
||||||
|
if m != nil {
|
||||||
|
return m.Outcome
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcResolution) GetFail() *HtlcFail {
|
||||||
|
if x, ok := x.GetOutcome().(*HtlcResolution_Fail); ok {
|
||||||
|
return x.Fail
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcResolution) GetContinue() *HtlcContinue {
|
||||||
|
if x, ok := x.GetOutcome().(*HtlcResolution_Continue); ok {
|
||||||
|
return x.Continue
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcResolution) GetResolve() *HtlcResolve {
|
||||||
|
if x, ok := x.GetOutcome().(*HtlcResolution_Resolve); ok {
|
||||||
|
return x.Resolve
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type isHtlcResolution_Outcome interface {
|
||||||
|
isHtlcResolution_Outcome()
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtlcResolution_Fail struct {
|
||||||
|
Fail *HtlcFail `protobuf:"bytes,2,opt,name=fail,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtlcResolution_Continue struct {
|
||||||
|
Continue *HtlcContinue `protobuf:"bytes,3,opt,name=continue,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtlcResolution_Resolve struct {
|
||||||
|
Resolve *HtlcResolve `protobuf:"bytes,4,opt,name=resolve,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*HtlcResolution_Fail) isHtlcResolution_Outcome() {}
|
||||||
|
|
||||||
|
func (*HtlcResolution_Continue) isHtlcResolution_Outcome() {}
|
||||||
|
|
||||||
|
func (*HtlcResolution_Resolve) isHtlcResolution_Outcome() {}
|
||||||
|
|
||||||
|
type HtlcContinue struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Payload *string `protobuf:"bytes,1,opt,name=payload,proto3,oneof" json:"payload,omitempty"`
|
||||||
|
ForwardTo *string `protobuf:"bytes,2,opt,name=forward_to,json=forwardTo,proto3,oneof" json:"forward_to,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcContinue) Reset() {
|
||||||
|
*x = HtlcContinue{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[4]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcContinue) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*HtlcContinue) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *HtlcContinue) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[4]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use HtlcContinue.ProtoReflect.Descriptor instead.
|
||||||
|
func (*HtlcContinue) Descriptor() ([]byte, []int) {
|
||||||
|
return file_cln_plugin_proto_rawDescGZIP(), []int{4}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcContinue) GetPayload() string {
|
||||||
|
if x != nil && x.Payload != nil {
|
||||||
|
return *x.Payload
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcContinue) GetForwardTo() string {
|
||||||
|
if x != nil && x.ForwardTo != nil {
|
||||||
|
return *x.ForwardTo
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtlcFail struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
// Types that are assignable to Failure:
|
||||||
|
// *HtlcFail_FailureMessage
|
||||||
|
// *HtlcFail_FailureOnion
|
||||||
|
Failure isHtlcFail_Failure `protobuf_oneof:"failure"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcFail) Reset() {
|
||||||
|
*x = HtlcFail{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[5]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcFail) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*HtlcFail) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *HtlcFail) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[5]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use HtlcFail.ProtoReflect.Descriptor instead.
|
||||||
|
func (*HtlcFail) Descriptor() ([]byte, []int) {
|
||||||
|
return file_cln_plugin_proto_rawDescGZIP(), []int{5}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *HtlcFail) GetFailure() isHtlcFail_Failure {
|
||||||
|
if m != nil {
|
||||||
|
return m.Failure
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcFail) GetFailureMessage() string {
|
||||||
|
if x, ok := x.GetFailure().(*HtlcFail_FailureMessage); ok {
|
||||||
|
return x.FailureMessage
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcFail) GetFailureOnion() string {
|
||||||
|
if x, ok := x.GetFailure().(*HtlcFail_FailureOnion); ok {
|
||||||
|
return x.FailureOnion
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
type isHtlcFail_Failure interface {
|
||||||
|
isHtlcFail_Failure()
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtlcFail_FailureMessage struct {
|
||||||
|
FailureMessage string `protobuf:"bytes,1,opt,name=failure_message,json=failureMessage,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HtlcFail_FailureOnion struct {
|
||||||
|
FailureOnion string `protobuf:"bytes,2,opt,name=failure_onion,json=failureOnion,proto3,oneof"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*HtlcFail_FailureMessage) isHtlcFail_Failure() {}
|
||||||
|
|
||||||
|
func (*HtlcFail_FailureOnion) isHtlcFail_Failure() {}
|
||||||
|
|
||||||
|
type HtlcResolve struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
PaymentKey string `protobuf:"bytes,1,opt,name=payment_key,json=paymentKey,proto3" json:"payment_key,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcResolve) Reset() {
|
||||||
|
*x = HtlcResolve{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[6]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcResolve) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*HtlcResolve) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *HtlcResolve) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_cln_plugin_proto_msgTypes[6]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use HtlcResolve.ProtoReflect.Descriptor instead.
|
||||||
|
func (*HtlcResolve) Descriptor() ([]byte, []int) {
|
||||||
|
return file_cln_plugin_proto_rawDescGZIP(), []int{6}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *HtlcResolve) GetPaymentKey() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.PaymentKey
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_cln_plugin_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_cln_plugin_proto_rawDesc = []byte{
|
||||||
|
0x0a, 0x10, 0x63, 0x6c, 0x6e, 0x5f, 0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2e, 0x70, 0x72, 0x6f,
|
||||||
|
0x74, 0x6f, 0x22, 0x8c, 0x01, 0x0a, 0x0c, 0x48, 0x74, 0x6c, 0x63, 0x41, 0x63, 0x63, 0x65, 0x70,
|
||||||
|
0x74, 0x65, 0x64, 0x12, 0x24, 0x0a, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69,
|
||||||
|
0x6f, 0x6e, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72,
|
||||||
|
0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x69, 0x64, 0x12, 0x1c, 0x0a, 0x05, 0x6f, 0x6e, 0x69,
|
||||||
|
0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x06, 0x2e, 0x4f, 0x6e, 0x69, 0x6f, 0x6e,
|
||||||
|
0x52, 0x05, 0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x0a, 0x04, 0x68, 0x74, 0x6c, 0x63, 0x18,
|
||||||
|
0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x05, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x52, 0x04, 0x68, 0x74,
|
||||||
|
0x6c, 0x63, 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x74, 0x6f,
|
||||||
|
0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x54,
|
||||||
|
0x6f, 0x22, 0xe2, 0x01, 0x0a, 0x05, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x70,
|
||||||
|
0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x70, 0x61,
|
||||||
|
0x79, 0x6c, 0x6f, 0x61, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x63,
|
||||||
|
0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52,
|
||||||
|
0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12,
|
||||||
|
0x21, 0x0a, 0x0c, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18,
|
||||||
|
0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0b, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x4d, 0x73,
|
||||||
|
0x61, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x5f, 0x63,
|
||||||
|
0x6c, 0x74, 0x76, 0x5f, 0x76, 0x61, 0x6c, 0x75, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52,
|
||||||
|
0x11, 0x6f, 0x75, 0x74, 0x67, 0x6f, 0x69, 0x6e, 0x67, 0x43, 0x6c, 0x74, 0x76, 0x56, 0x61, 0x6c,
|
||||||
|
0x75, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x73, 0x68, 0x61, 0x72, 0x65, 0x64, 0x5f, 0x73, 0x65, 0x63,
|
||||||
|
0x72, 0x65, 0x74, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x73, 0x68, 0x61, 0x72, 0x65,
|
||||||
|
0x64, 0x53, 0x65, 0x63, 0x72, 0x65, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x6e, 0x65, 0x78, 0x74, 0x5f,
|
||||||
|
0x6f, 0x6e, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x6e, 0x65, 0x78,
|
||||||
|
0x74, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x22, 0xd7, 0x01, 0x0a, 0x04, 0x48, 0x74, 0x6c, 0x63, 0x12,
|
||||||
|
0x28, 0x0a, 0x10, 0x73, 0x68, 0x6f, 0x72, 0x74, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c,
|
||||||
|
0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x73, 0x68, 0x6f, 0x72, 0x74,
|
||||||
|
0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x49, 0x64, 0x12, 0x0e, 0x0a, 0x02, 0x69, 0x64, 0x18,
|
||||||
|
0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x02, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x6d, 0x6f,
|
||||||
|
0x75, 0x6e, 0x74, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, 0x04, 0x52, 0x0a,
|
||||||
|
0x61, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x63, 0x6c,
|
||||||
|
0x74, 0x76, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0d, 0x52,
|
||||||
|
0x0a, 0x63, 0x6c, 0x74, 0x76, 0x45, 0x78, 0x70, 0x69, 0x72, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x63,
|
||||||
|
0x6c, 0x74, 0x76, 0x5f, 0x65, 0x78, 0x70, 0x69, 0x72, 0x79, 0x5f, 0x72, 0x65, 0x6c, 0x61, 0x74,
|
||||||
|
0x69, 0x76, 0x65, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x12, 0x63, 0x6c, 0x74, 0x76, 0x45,
|
||||||
|
0x78, 0x70, 0x69, 0x72, 0x79, 0x52, 0x65, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x12, 0x21, 0x0a,
|
||||||
|
0x0c, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x68, 0x18, 0x06, 0x20,
|
||||||
|
0x01, 0x28, 0x09, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x48, 0x61, 0x73, 0x68,
|
||||||
|
0x22, 0xb9, 0x01, 0x0a, 0x0e, 0x48, 0x74, 0x6c, 0x63, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74,
|
||||||
|
0x69, 0x6f, 0x6e, 0x12, 0x24, 0x0a, 0x0d, 0x63, 0x6f, 0x72, 0x72, 0x65, 0x6c, 0x61, 0x74, 0x69,
|
||||||
|
0x6f, 0x6e, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x63, 0x6f, 0x72, 0x72,
|
||||||
|
0x65, 0x6c, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x69, 0x64, 0x12, 0x1f, 0x0a, 0x04, 0x66, 0x61, 0x69,
|
||||||
|
0x6c, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x09, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x46, 0x61,
|
||||||
|
0x69, 0x6c, 0x48, 0x00, 0x52, 0x04, 0x66, 0x61, 0x69, 0x6c, 0x12, 0x2b, 0x0a, 0x08, 0x63, 0x6f,
|
||||||
|
0x6e, 0x74, 0x69, 0x6e, 0x75, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e, 0x48,
|
||||||
|
0x74, 0x6c, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x65, 0x48, 0x00, 0x52, 0x08, 0x63,
|
||||||
|
0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x65, 0x12, 0x28, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x6f, 0x6c,
|
||||||
|
0x76, 0x65, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0c, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x52,
|
||||||
|
0x65, 0x73, 0x6f, 0x6c, 0x76, 0x65, 0x48, 0x00, 0x52, 0x07, 0x72, 0x65, 0x73, 0x6f, 0x6c, 0x76,
|
||||||
|
0x65, 0x42, 0x09, 0x0a, 0x07, 0x6f, 0x75, 0x74, 0x63, 0x6f, 0x6d, 0x65, 0x22, 0x6c, 0x0a, 0x0c,
|
||||||
|
0x48, 0x74, 0x6c, 0x63, 0x43, 0x6f, 0x6e, 0x74, 0x69, 0x6e, 0x75, 0x65, 0x12, 0x1d, 0x0a, 0x07,
|
||||||
|
0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52,
|
||||||
|
0x07, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x88, 0x01, 0x01, 0x12, 0x22, 0x0a, 0x0a, 0x66,
|
||||||
|
0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x74, 0x6f, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48,
|
||||||
|
0x01, 0x52, 0x09, 0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x54, 0x6f, 0x88, 0x01, 0x01, 0x42,
|
||||||
|
0x0a, 0x0a, 0x08, 0x5f, 0x70, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x42, 0x0d, 0x0a, 0x0b, 0x5f,
|
||||||
|
0x66, 0x6f, 0x72, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x74, 0x6f, 0x22, 0x67, 0x0a, 0x08, 0x48, 0x74,
|
||||||
|
0x6c, 0x63, 0x46, 0x61, 0x69, 0x6c, 0x12, 0x29, 0x0a, 0x0f, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72,
|
||||||
|
0x65, 0x5f, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x48,
|
||||||
|
0x00, 0x52, 0x0e, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67,
|
||||||
|
0x65, 0x12, 0x25, 0x0a, 0x0d, 0x66, 0x61, 0x69, 0x6c, 0x75, 0x72, 0x65, 0x5f, 0x6f, 0x6e, 0x69,
|
||||||
|
0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0c, 0x66, 0x61, 0x69, 0x6c,
|
||||||
|
0x75, 0x72, 0x65, 0x4f, 0x6e, 0x69, 0x6f, 0x6e, 0x42, 0x09, 0x0a, 0x07, 0x66, 0x61, 0x69, 0x6c,
|
||||||
|
0x75, 0x72, 0x65, 0x22, 0x2e, 0x0a, 0x0b, 0x48, 0x74, 0x6c, 0x63, 0x52, 0x65, 0x73, 0x6f, 0x6c,
|
||||||
|
0x76, 0x65, 0x12, 0x1f, 0x0a, 0x0b, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65,
|
||||||
|
0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x70, 0x61, 0x79, 0x6d, 0x65, 0x6e, 0x74,
|
||||||
|
0x4b, 0x65, 0x79, 0x32, 0x3d, 0x0a, 0x09, 0x43, 0x6c, 0x6e, 0x50, 0x6c, 0x75, 0x67, 0x69, 0x6e,
|
||||||
|
0x12, 0x30, 0x0a, 0x0a, 0x48, 0x74, 0x6c, 0x63, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x0f,
|
||||||
|
0x2e, 0x48, 0x74, 0x6c, 0x63, 0x52, 0x65, 0x73, 0x6f, 0x6c, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x1a,
|
||||||
|
0x0d, 0x2e, 0x48, 0x74, 0x6c, 0x63, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x28, 0x01,
|
||||||
|
0x30, 0x01, 0x42, 0x28, 0x5a, 0x26, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d,
|
||||||
|
0x2f, 0x62, 0x72, 0x65, 0x65, 0x7a, 0x2f, 0x6c, 0x73, 0x70, 0x64, 0x2f, 0x63, 0x6c, 0x6e, 0x5f,
|
||||||
|
0x70, 0x6c, 0x75, 0x67, 0x69, 0x6e, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x06, 0x70, 0x72,
|
||||||
|
0x6f, 0x74, 0x6f, 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_cln_plugin_proto_rawDescOnce sync.Once
|
||||||
|
file_cln_plugin_proto_rawDescData = file_cln_plugin_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_cln_plugin_proto_rawDescGZIP() []byte {
|
||||||
|
file_cln_plugin_proto_rawDescOnce.Do(func() {
|
||||||
|
file_cln_plugin_proto_rawDescData = protoimpl.X.CompressGZIP(file_cln_plugin_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_cln_plugin_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_cln_plugin_proto_msgTypes = make([]protoimpl.MessageInfo, 7)
|
||||||
|
var file_cln_plugin_proto_goTypes = []interface{}{
|
||||||
|
(*HtlcAccepted)(nil), // 0: HtlcAccepted
|
||||||
|
(*Onion)(nil), // 1: Onion
|
||||||
|
(*Htlc)(nil), // 2: Htlc
|
||||||
|
(*HtlcResolution)(nil), // 3: HtlcResolution
|
||||||
|
(*HtlcContinue)(nil), // 4: HtlcContinue
|
||||||
|
(*HtlcFail)(nil), // 5: HtlcFail
|
||||||
|
(*HtlcResolve)(nil), // 6: HtlcResolve
|
||||||
|
}
|
||||||
|
var file_cln_plugin_proto_depIdxs = []int32{
|
||||||
|
1, // 0: HtlcAccepted.onion:type_name -> Onion
|
||||||
|
2, // 1: HtlcAccepted.htlc:type_name -> Htlc
|
||||||
|
5, // 2: HtlcResolution.fail:type_name -> HtlcFail
|
||||||
|
4, // 3: HtlcResolution.continue:type_name -> HtlcContinue
|
||||||
|
6, // 4: HtlcResolution.resolve:type_name -> HtlcResolve
|
||||||
|
3, // 5: ClnPlugin.HtlcStream:input_type -> HtlcResolution
|
||||||
|
0, // 6: ClnPlugin.HtlcStream:output_type -> HtlcAccepted
|
||||||
|
6, // [6:7] is the sub-list for method output_type
|
||||||
|
5, // [5:6] is the sub-list for method input_type
|
||||||
|
5, // [5:5] is the sub-list for extension type_name
|
||||||
|
5, // [5:5] is the sub-list for extension extendee
|
||||||
|
0, // [0:5] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_cln_plugin_proto_init() }
|
||||||
|
func file_cln_plugin_proto_init() {
|
||||||
|
if File_cln_plugin_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_cln_plugin_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*HtlcAccepted); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_cln_plugin_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*Onion); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_cln_plugin_proto_msgTypes[2].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*Htlc); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_cln_plugin_proto_msgTypes[3].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*HtlcResolution); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_cln_plugin_proto_msgTypes[4].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*HtlcContinue); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_cln_plugin_proto_msgTypes[5].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*HtlcFail); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_cln_plugin_proto_msgTypes[6].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*HtlcResolve); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_cln_plugin_proto_msgTypes[3].OneofWrappers = []interface{}{
|
||||||
|
(*HtlcResolution_Fail)(nil),
|
||||||
|
(*HtlcResolution_Continue)(nil),
|
||||||
|
(*HtlcResolution_Resolve)(nil),
|
||||||
|
}
|
||||||
|
file_cln_plugin_proto_msgTypes[4].OneofWrappers = []interface{}{}
|
||||||
|
file_cln_plugin_proto_msgTypes[5].OneofWrappers = []interface{}{
|
||||||
|
(*HtlcFail_FailureMessage)(nil),
|
||||||
|
(*HtlcFail_FailureOnion)(nil),
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: file_cln_plugin_proto_rawDesc,
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 7,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_cln_plugin_proto_goTypes,
|
||||||
|
DependencyIndexes: file_cln_plugin_proto_depIdxs,
|
||||||
|
MessageInfos: file_cln_plugin_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_cln_plugin_proto = out.File
|
||||||
|
file_cln_plugin_proto_rawDesc = nil
|
||||||
|
file_cln_plugin_proto_goTypes = nil
|
||||||
|
file_cln_plugin_proto_depIdxs = nil
|
||||||
|
}
|
||||||
56
cln_plugin/proto/cln_plugin.proto
Normal file
56
cln_plugin/proto/cln_plugin.proto
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
option go_package="github.com/breez/lspd/cln_plugin/proto";
|
||||||
|
|
||||||
|
service ClnPlugin {
|
||||||
|
rpc HtlcStream(stream HtlcResolution) returns (stream HtlcAccepted);
|
||||||
|
}
|
||||||
|
|
||||||
|
message HtlcAccepted {
|
||||||
|
string correlationid = 1;
|
||||||
|
Onion onion = 2;
|
||||||
|
Htlc htlc = 3;
|
||||||
|
string forward_to = 4;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Onion {
|
||||||
|
string payload = 1;
|
||||||
|
string short_channel_id = 2;
|
||||||
|
uint64 forward_msat = 3;
|
||||||
|
uint32 outgoing_cltv_value = 4;
|
||||||
|
string shared_secret = 5;
|
||||||
|
string next_onion = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message Htlc {
|
||||||
|
string short_channel_id = 1;
|
||||||
|
uint64 id = 2;
|
||||||
|
uint64 amount_msat = 3;
|
||||||
|
uint32 cltv_expiry = 4;
|
||||||
|
uint32 cltv_expiry_relative = 5;
|
||||||
|
string payment_hash = 6;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HtlcResolution {
|
||||||
|
string correlationid = 1;
|
||||||
|
oneof outcome {
|
||||||
|
HtlcFail fail = 2;
|
||||||
|
HtlcContinue continue = 3;
|
||||||
|
HtlcResolve resolve = 4;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message HtlcContinue {
|
||||||
|
optional string payload = 1;
|
||||||
|
optional string forward_to = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message HtlcFail {
|
||||||
|
oneof failure {
|
||||||
|
string failure_message = 1;
|
||||||
|
string failure_onion = 2;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
message HtlcResolve {
|
||||||
|
string payment_key = 1;
|
||||||
|
}
|
||||||
137
cln_plugin/proto/cln_plugin_grpc.pb.go
Normal file
137
cln_plugin/proto/cln_plugin_grpc.pb.go
Normal file
@@ -0,0 +1,137 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.2.0
|
||||||
|
// - protoc v3.21.12
|
||||||
|
// source: cln_plugin.proto
|
||||||
|
|
||||||
|
package proto
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.32.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion7
|
||||||
|
|
||||||
|
// ClnPluginClient is the client API for ClnPlugin service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type ClnPluginClient interface {
|
||||||
|
HtlcStream(ctx context.Context, opts ...grpc.CallOption) (ClnPlugin_HtlcStreamClient, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type clnPluginClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClnPluginClient(cc grpc.ClientConnInterface) ClnPluginClient {
|
||||||
|
return &clnPluginClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clnPluginClient) HtlcStream(ctx context.Context, opts ...grpc.CallOption) (ClnPlugin_HtlcStreamClient, error) {
|
||||||
|
stream, err := c.cc.NewStream(ctx, &ClnPlugin_ServiceDesc.Streams[0], "/ClnPlugin/HtlcStream", opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
x := &clnPluginHtlcStreamClient{stream}
|
||||||
|
return x, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClnPlugin_HtlcStreamClient interface {
|
||||||
|
Send(*HtlcResolution) error
|
||||||
|
Recv() (*HtlcAccepted, error)
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type clnPluginHtlcStreamClient struct {
|
||||||
|
grpc.ClientStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *clnPluginHtlcStreamClient) Send(m *HtlcResolution) error {
|
||||||
|
return x.ClientStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *clnPluginHtlcStreamClient) Recv() (*HtlcAccepted, error) {
|
||||||
|
m := new(HtlcAccepted)
|
||||||
|
if err := x.ClientStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClnPluginServer is the server API for ClnPlugin service.
|
||||||
|
// All implementations must embed UnimplementedClnPluginServer
|
||||||
|
// for forward compatibility
|
||||||
|
type ClnPluginServer interface {
|
||||||
|
HtlcStream(ClnPlugin_HtlcStreamServer) error
|
||||||
|
mustEmbedUnimplementedClnPluginServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedClnPluginServer must be embedded to have forward compatible implementations.
|
||||||
|
type UnimplementedClnPluginServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedClnPluginServer) HtlcStream(ClnPlugin_HtlcStreamServer) error {
|
||||||
|
return status.Errorf(codes.Unimplemented, "method HtlcStream not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedClnPluginServer) mustEmbedUnimplementedClnPluginServer() {}
|
||||||
|
|
||||||
|
// UnsafeClnPluginServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to ClnPluginServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeClnPluginServer interface {
|
||||||
|
mustEmbedUnimplementedClnPluginServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterClnPluginServer(s grpc.ServiceRegistrar, srv ClnPluginServer) {
|
||||||
|
s.RegisterService(&ClnPlugin_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _ClnPlugin_HtlcStream_Handler(srv interface{}, stream grpc.ServerStream) error {
|
||||||
|
return srv.(ClnPluginServer).HtlcStream(&clnPluginHtlcStreamServer{stream})
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClnPlugin_HtlcStreamServer interface {
|
||||||
|
Send(*HtlcAccepted) error
|
||||||
|
Recv() (*HtlcResolution, error)
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
type clnPluginHtlcStreamServer struct {
|
||||||
|
grpc.ServerStream
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *clnPluginHtlcStreamServer) Send(m *HtlcAccepted) error {
|
||||||
|
return x.ServerStream.SendMsg(m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *clnPluginHtlcStreamServer) Recv() (*HtlcResolution, error) {
|
||||||
|
m := new(HtlcResolution)
|
||||||
|
if err := x.ServerStream.RecvMsg(m); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ClnPlugin_ServiceDesc is the grpc.ServiceDesc for ClnPlugin service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var ClnPlugin_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "ClnPlugin",
|
||||||
|
HandlerType: (*ClnPluginServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{},
|
||||||
|
Streams: []grpc.StreamDesc{
|
||||||
|
{
|
||||||
|
StreamName: "HtlcStream",
|
||||||
|
Handler: _ClnPlugin_HtlcStream_Handler,
|
||||||
|
ServerStreams: true,
|
||||||
|
ClientStreams: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Metadata: "cln_plugin.proto",
|
||||||
|
}
|
||||||
401
cln_plugin/server.go
Normal file
401
cln_plugin/server.go
Normal file
@@ -0,0 +1,401 @@
|
|||||||
|
package cln_plugin
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/cln_plugin/proto"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/keepalive"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Internal htlc_accepted message meant for the sendQueue.
|
||||||
|
type htlcAcceptedMsg struct {
|
||||||
|
id string
|
||||||
|
htlc *HtlcAccepted
|
||||||
|
timeout time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Internal htlc result message meant for the recvQueue.
|
||||||
|
type htlcResultMsg struct {
|
||||||
|
id string
|
||||||
|
result interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
proto.ClnPluginServer
|
||||||
|
listenAddress string
|
||||||
|
subscriberTimeout time.Duration
|
||||||
|
grpcServer *grpc.Server
|
||||||
|
mtx sync.Mutex
|
||||||
|
stream proto.ClnPlugin_HtlcStreamServer
|
||||||
|
newSubscriber chan struct{}
|
||||||
|
started chan struct{}
|
||||||
|
done chan struct{}
|
||||||
|
completed chan struct{}
|
||||||
|
startError chan error
|
||||||
|
sendQueue chan *htlcAcceptedMsg
|
||||||
|
recvQueue chan *htlcResultMsg
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creates a new grpc server
|
||||||
|
func NewServer(listenAddress string, subscriberTimeout time.Duration) *server {
|
||||||
|
// TODO: Set a sane max queue size
|
||||||
|
return &server{
|
||||||
|
listenAddress: listenAddress,
|
||||||
|
subscriberTimeout: subscriberTimeout,
|
||||||
|
// The send queue exists to buffer messages until a subscriber is active.
|
||||||
|
sendQueue: make(chan *htlcAcceptedMsg, 10000),
|
||||||
|
// The receive queue exists mainly to allow returning timeouts to the
|
||||||
|
// cln plugin. If there is no subscriber active within the subscriber
|
||||||
|
// timeout period these results can be put directly on the receive queue.
|
||||||
|
recvQueue: make(chan *htlcResultMsg, 10000),
|
||||||
|
started: make(chan struct{}),
|
||||||
|
startError: make(chan error, 1),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Starts the grpc server. Blocks until the servver is stopped. WaitStarted can
|
||||||
|
// be called to ensure the server is started without errors if this function
|
||||||
|
// is run as a goroutine.
|
||||||
|
func (s *server) Start() error {
|
||||||
|
s.mtx.Lock()
|
||||||
|
if s.grpcServer != nil {
|
||||||
|
s.mtx.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lis, err := net.Listen("tcp", s.listenAddress)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ERROR Server failed to listen: %v", err)
|
||||||
|
s.startError <- err
|
||||||
|
s.mtx.Unlock()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.done = make(chan struct{})
|
||||||
|
s.completed = make(chan struct{})
|
||||||
|
s.newSubscriber = make(chan struct{})
|
||||||
|
s.grpcServer = grpc.NewServer(
|
||||||
|
grpc.KeepaliveParams(keepalive.ServerParameters{
|
||||||
|
Time: time.Duration(1) * time.Second,
|
||||||
|
Timeout: time.Duration(10) * time.Second,
|
||||||
|
}),
|
||||||
|
grpc.KeepaliveEnforcementPolicy(keepalive.EnforcementPolicy{
|
||||||
|
MinTime: time.Duration(1) * time.Second,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
s.mtx.Unlock()
|
||||||
|
proto.RegisterClnPluginServer(s.grpcServer, s)
|
||||||
|
|
||||||
|
log.Printf("Server starting to listen on %s.", s.listenAddress)
|
||||||
|
go s.listenHtlcRequests()
|
||||||
|
go s.listenHtlcResponses()
|
||||||
|
close(s.started)
|
||||||
|
err = s.grpcServer.Serve(lis)
|
||||||
|
close(s.completed)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Waits until the server has started, or errored during startup.
|
||||||
|
func (s *server) WaitStarted() error {
|
||||||
|
select {
|
||||||
|
case <-s.started:
|
||||||
|
return nil
|
||||||
|
case err := <-s.startError:
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stops all work from the grpc server immediately.
|
||||||
|
func (s *server) Stop() {
|
||||||
|
s.mtx.Lock()
|
||||||
|
defer s.mtx.Unlock()
|
||||||
|
log.Printf("Server Stop() called.")
|
||||||
|
if s.grpcServer == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
s.grpcServer.Stop()
|
||||||
|
s.grpcServer = nil
|
||||||
|
|
||||||
|
close(s.done)
|
||||||
|
<-s.completed
|
||||||
|
log.Printf("Server stopped.")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Grpc method that is called when a new client subscribes. There can only be
|
||||||
|
// one subscriber active at a time. If there is an error receiving or sending
|
||||||
|
// from or to the subscriber, the subscription is closed.
|
||||||
|
func (s *server) HtlcStream(stream proto.ClnPlugin_HtlcStreamServer) error {
|
||||||
|
s.mtx.Lock()
|
||||||
|
if s.stream == nil {
|
||||||
|
log.Printf("Got a new HTLC stream subscription request.")
|
||||||
|
} else {
|
||||||
|
s.mtx.Unlock()
|
||||||
|
log.Printf("Got a HTLC stream subscription request, but subscription " +
|
||||||
|
"was already active.")
|
||||||
|
return fmt.Errorf("already subscribed")
|
||||||
|
}
|
||||||
|
|
||||||
|
s.stream = stream
|
||||||
|
|
||||||
|
// Notify listeners that a new subscriber is active. Replace the chan with
|
||||||
|
// a new one immediately in case this subscriber is dropped later.
|
||||||
|
close(s.newSubscriber)
|
||||||
|
s.newSubscriber = make(chan struct{})
|
||||||
|
s.mtx.Unlock()
|
||||||
|
|
||||||
|
<-stream.Context().Done()
|
||||||
|
log.Printf("HtlcStream context is done. Return: %v", stream.Context().Err())
|
||||||
|
|
||||||
|
// Remove the subscriber.
|
||||||
|
s.mtx.Lock()
|
||||||
|
s.stream = nil
|
||||||
|
s.mtx.Unlock()
|
||||||
|
|
||||||
|
return stream.Context().Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enqueues a htlc_accepted message for send to the grpc client.
|
||||||
|
func (s *server) Send(id string, h *HtlcAccepted) {
|
||||||
|
s.sendQueue <- &htlcAcceptedMsg{
|
||||||
|
id: id,
|
||||||
|
htlc: h,
|
||||||
|
timeout: time.Now().Add(s.subscriberTimeout),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receives the next htlc resolution message from the grpc client. Returns id
|
||||||
|
// and message. Blocks until a message is available. Returns a nil message if
|
||||||
|
// the server is done. This function effectively waits until a subscriber is
|
||||||
|
// active and has sent a message.
|
||||||
|
func (s *server) Receive() (string, interface{}) {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return "", nil
|
||||||
|
case msg := <-s.recvQueue:
|
||||||
|
return msg.id, msg.result
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listens to sendQueue for htlc_accepted requests from cln. The message will be
|
||||||
|
// held until a subscriber is active, or the subscriber timeout expires. The
|
||||||
|
// messages are sent to the grpc client in fifo order.
|
||||||
|
func (s *server) listenHtlcRequests() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
log.Printf("listenHtlcRequests received done. Stop listening.")
|
||||||
|
return
|
||||||
|
case msg := <-s.sendQueue:
|
||||||
|
s.handleHtlcAccepted(msg)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Attempts to send a htlc_accepted message to the grpc client. The message will
|
||||||
|
// be held until a subscriber is active, or the subscriber timeout expires.
|
||||||
|
func (s *server) handleHtlcAccepted(msg *htlcAcceptedMsg) {
|
||||||
|
for {
|
||||||
|
s.mtx.Lock()
|
||||||
|
stream := s.stream
|
||||||
|
ns := s.newSubscriber
|
||||||
|
s.mtx.Unlock()
|
||||||
|
|
||||||
|
// If there is no active subscription, wait until there is a new
|
||||||
|
// subscriber, or the message times out.
|
||||||
|
if stream == nil {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
log.Printf("handleHtlcAccepted received server done. Stop processing.")
|
||||||
|
return
|
||||||
|
case <-ns:
|
||||||
|
log.Printf("got a new subscriber. continue handleHtlcAccepted.")
|
||||||
|
continue
|
||||||
|
case <-time.After(time.Until(msg.timeout)):
|
||||||
|
log.Printf(
|
||||||
|
"WARNING: htlc with id '%s' timed out after '%v' waiting "+
|
||||||
|
"for grpc subscriber: %+v",
|
||||||
|
msg.id,
|
||||||
|
s.subscriberTimeout,
|
||||||
|
msg.htlc,
|
||||||
|
)
|
||||||
|
|
||||||
|
// If the subscriber timeout expires while holding the htlc
|
||||||
|
// we short circuit the htlc by sending the default result
|
||||||
|
// (continue) to cln.
|
||||||
|
s.recvQueue <- &htlcResultMsg{
|
||||||
|
id: msg.id,
|
||||||
|
result: s.defaultResult(),
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a subscriber. Attempt to send the htlc_accepted message.
|
||||||
|
err := stream.Send(&proto.HtlcAccepted{
|
||||||
|
Correlationid: msg.id,
|
||||||
|
Onion: &proto.Onion{
|
||||||
|
Payload: msg.htlc.Onion.Payload,
|
||||||
|
ShortChannelId: msg.htlc.Onion.ShortChannelId,
|
||||||
|
ForwardMsat: msg.htlc.Onion.ForwardMsat,
|
||||||
|
OutgoingCltvValue: msg.htlc.Onion.OutgoingCltvValue,
|
||||||
|
SharedSecret: msg.htlc.Onion.SharedSecret,
|
||||||
|
NextOnion: msg.htlc.Onion.NextOnion,
|
||||||
|
},
|
||||||
|
Htlc: &proto.Htlc{
|
||||||
|
ShortChannelId: msg.htlc.Htlc.ShortChannelId,
|
||||||
|
Id: msg.htlc.Htlc.Id,
|
||||||
|
AmountMsat: msg.htlc.Htlc.AmountMsat,
|
||||||
|
CltvExpiry: msg.htlc.Htlc.CltvExpiry,
|
||||||
|
CltvExpiryRelative: msg.htlc.Htlc.CltvExpiryRelative,
|
||||||
|
PaymentHash: msg.htlc.Htlc.PaymentHash,
|
||||||
|
},
|
||||||
|
ForwardTo: msg.htlc.ForwardTo,
|
||||||
|
})
|
||||||
|
|
||||||
|
// If there is no error, we're done.
|
||||||
|
if err == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we end up here, there was an error sending the message to the
|
||||||
|
// grpc client.
|
||||||
|
// TODO: If the Send errors, but the context is not done, this will
|
||||||
|
// currently retry immediately. Check whether the context is really
|
||||||
|
// done on an error!
|
||||||
|
log.Printf("Error sending htlc_accepted message to subscriber. Retrying: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Listens to htlc responses from the grpc client and appends them to the
|
||||||
|
// receive queue. The messages from the receive queue are read in the Receive
|
||||||
|
// function.
|
||||||
|
func (s *server) listenHtlcResponses() {
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
log.Printf("listenHtlcResponses received done. Stopping listening.")
|
||||||
|
return
|
||||||
|
default:
|
||||||
|
resp := s.recv()
|
||||||
|
s.recvQueue <- &htlcResultMsg{
|
||||||
|
id: resp.Correlationid,
|
||||||
|
result: s.mapResult(resp.Outcome),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Helper function that blocks until a message from a grpc client is received
|
||||||
|
// or the server stops. Either returns a received message, or nil if the server
|
||||||
|
// has stopped.
|
||||||
|
func (s *server) recv() *proto.HtlcResolution {
|
||||||
|
for {
|
||||||
|
// make a copy of the used fields, to make sure state updates don't
|
||||||
|
// surprise us. The newSubscriber chan is swapped whenever a new
|
||||||
|
// subscriber arrives.
|
||||||
|
s.mtx.Lock()
|
||||||
|
stream := s.stream
|
||||||
|
ns := s.newSubscriber
|
||||||
|
s.mtx.Unlock()
|
||||||
|
|
||||||
|
if stream == nil {
|
||||||
|
log.Printf("Got no subscribers for receive. Waiting for subscriber.")
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
log.Printf("Done signalled, stopping receive.")
|
||||||
|
return nil
|
||||||
|
case <-ns:
|
||||||
|
log.Printf("New subscription available for receive, continue receive.")
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// There is a subscription active. Attempt to receive a message.
|
||||||
|
r, err := stream.Recv()
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("Received HtlcResolution %+v", r)
|
||||||
|
return r
|
||||||
|
}
|
||||||
|
|
||||||
|
// Receiving the message failed, so the subscription is broken. Remove
|
||||||
|
// it if it hasn't been updated already. We'll try receiving again in
|
||||||
|
// the next iteration of the for loop.
|
||||||
|
// TODO: If the Recv errors, but the context is not done, this will
|
||||||
|
// currently retry immediately. Check whether the context is really
|
||||||
|
// done on an error!
|
||||||
|
log.Printf("Recv() errored, Retrying: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Maps a grpc result to the corresponding result for cln. The cln message
|
||||||
|
// is a raw json message, so it's easiest to use a map directly.
|
||||||
|
func (s *server) mapResult(outcome interface{}) interface{} {
|
||||||
|
// result: continue
|
||||||
|
cont, ok := outcome.(*proto.HtlcResolution_Continue)
|
||||||
|
if ok {
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"result": "continue",
|
||||||
|
}
|
||||||
|
|
||||||
|
if cont.Continue.ForwardTo != nil {
|
||||||
|
result["forward_to"] = *cont.Continue.ForwardTo
|
||||||
|
}
|
||||||
|
|
||||||
|
if cont.Continue.Payload != nil {
|
||||||
|
result["payload"] = *cont.Continue.Payload
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// result: fail
|
||||||
|
fail, ok := outcome.(*proto.HtlcResolution_Fail)
|
||||||
|
if ok {
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"result": "fail",
|
||||||
|
}
|
||||||
|
|
||||||
|
fm, ok := fail.Fail.Failure.(*proto.HtlcFail_FailureMessage)
|
||||||
|
if ok {
|
||||||
|
result["failure_message"] = fm.FailureMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
fo, ok := fail.Fail.Failure.(*proto.HtlcFail_FailureOnion)
|
||||||
|
if ok {
|
||||||
|
result["failure_onion"] = fo.FailureOnion
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// result: resolve
|
||||||
|
resolve, ok := outcome.(*proto.HtlcResolution_Resolve)
|
||||||
|
if ok {
|
||||||
|
result := map[string]interface{}{
|
||||||
|
"result": "resolve",
|
||||||
|
"payment_key": resolve.Resolve.PaymentKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// On an unknown result we haven't implemented all possible cases from the
|
||||||
|
// grpc message. We don't understand what's going on, so we'll return
|
||||||
|
// result: continue.
|
||||||
|
log.Printf("Unexpected htlc resolution type %T: %+v", outcome, outcome)
|
||||||
|
return s.defaultResult()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Returns a result: continue message.
|
||||||
|
func (s *server) defaultResult() interface{} {
|
||||||
|
return map[string]interface{}{
|
||||||
|
"result": "continue",
|
||||||
|
}
|
||||||
|
}
|
||||||
98
config/config.go
Normal file
98
config/config.go
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
type NodeConfig struct {
|
||||||
|
// Name of the LSP. If empty, the node's alias will be taken instead.
|
||||||
|
Name string `json:name,omitempty`
|
||||||
|
|
||||||
|
// The public key of the lightning node.
|
||||||
|
NodePubkey string `json:nodePubkey,omitempty`
|
||||||
|
|
||||||
|
// Hex encoded private key of the LSP. This is used to decrypt traffic from
|
||||||
|
// clients.
|
||||||
|
LspdPrivateKey string `json:"lspdPrivateKey"`
|
||||||
|
|
||||||
|
// Tokens used to authenticate to lspd. These tokens must be unique for each
|
||||||
|
// configured node, so it's obvious which node an rpc call is meant for.
|
||||||
|
Tokens []string `json:"tokens"`
|
||||||
|
|
||||||
|
// The network location of the lightning node, e.g. `12.34.56.78:9012` or
|
||||||
|
// `localhost:10011`
|
||||||
|
Host string `json:"host"`
|
||||||
|
|
||||||
|
// Public channel amount is a reserved amount for public channels. If a
|
||||||
|
// zero conf channel is opened, it will never have this exact amount.
|
||||||
|
PublicChannelAmount int64 `json:"publicChannelAmount,string"`
|
||||||
|
|
||||||
|
// The capacity of opened channels through the OpenChannel rpc.
|
||||||
|
ChannelAmount uint64 `json:"channelAmount,string"`
|
||||||
|
|
||||||
|
// Value indicating whether channels opened through the OpenChannel rpc
|
||||||
|
// should be private.
|
||||||
|
ChannelPrivate bool `json:"channelPrivate"`
|
||||||
|
|
||||||
|
// Number of blocks after which an opened channel is considered confirmed.
|
||||||
|
TargetConf uint32 `json:"targetConf,string"`
|
||||||
|
|
||||||
|
// Minimum number of confirmations inputs for zero conf channel opens should
|
||||||
|
// have.
|
||||||
|
MinConfs *uint32 `json:"minConfs,string"`
|
||||||
|
|
||||||
|
// Smallest htlc amount routed over channels opened with the OpenChannel
|
||||||
|
// rpc call.
|
||||||
|
MinHtlcMsat uint64 `json:"minHtlcMsat,string"`
|
||||||
|
|
||||||
|
// The base fee for routing payments over the channel. It is configured on
|
||||||
|
// the node itself, but this value is returned in the ChannelInformation rpc.
|
||||||
|
BaseFeeMsat uint64 `json:"baseFeeMsat,string"`
|
||||||
|
|
||||||
|
// The fee rate for routing payments over the channel. It is configured on
|
||||||
|
// the node itself, but this value is returned in the ChannelInformation rpc.
|
||||||
|
FeeRate float64 `json:"feeRate,string"`
|
||||||
|
|
||||||
|
// Minimum timelock delta required for opening a zero conf channel.
|
||||||
|
TimeLockDelta uint32 `json:"timeLockDelta,string"`
|
||||||
|
|
||||||
|
// Fee for opening a zero conf channel in satoshi per 10000 satoshi based
|
||||||
|
// on the incoming payment amount.
|
||||||
|
ChannelFeePermyriad int64 `json:"channelFeePermyriad,string"`
|
||||||
|
|
||||||
|
// Minimum fee for opening a zero conf channel in millisatoshi.
|
||||||
|
ChannelMinimumFeeMsat int64 `json:"channelMinimumFeeMsat,string"`
|
||||||
|
|
||||||
|
// Channel capacity that is added on top of the incoming payment amount
|
||||||
|
// when a new zero conf channel is opened. In satoshi.
|
||||||
|
AdditionalChannelCapacity int64 `json:"additionalChannelCapacity,string"`
|
||||||
|
|
||||||
|
// The channel can be closed if not used this duration in seconds.
|
||||||
|
MaxInactiveDuration uint64 `json:"maxInactiveDuration,string"`
|
||||||
|
|
||||||
|
// The maximum time to hold a htlc after sending a notification when the
|
||||||
|
// peer is offline.
|
||||||
|
NotificationTimeout string `json:"notificationTimeout,string"`
|
||||||
|
|
||||||
|
// Set this field to connect to an LND node.
|
||||||
|
Lnd *LndConfig `json:"lnd,omitempty"`
|
||||||
|
|
||||||
|
// Set this field to connect to a CLN node.
|
||||||
|
Cln *ClnConfig `json:"cln,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LndConfig struct {
|
||||||
|
// Address to the grpc api.
|
||||||
|
Address string `json:"address"`
|
||||||
|
|
||||||
|
// tls cert for the grpc api.
|
||||||
|
Cert string `json:"cert"`
|
||||||
|
|
||||||
|
// macaroon to use.
|
||||||
|
Macaroon string `json:"macaroon"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ClnConfig struct {
|
||||||
|
// The address to the cln htlc acceptor grpc api shipped with lspd.
|
||||||
|
PluginAddress string `json:"pluginAddress"`
|
||||||
|
|
||||||
|
// File path to the cln lightning-roc socket file. Find the path in
|
||||||
|
// cln-dir/mainnet/lightning-rpc
|
||||||
|
SocketPath string `json:"socketPath"`
|
||||||
|
}
|
||||||
333
deploy/deploy.yml
Normal file
333
deploy/deploy.yml
Normal file
@@ -0,0 +1,333 @@
|
|||||||
|
AWSTemplateFormatVersion: '2010-09-09'
|
||||||
|
|
||||||
|
Parameters:
|
||||||
|
KeyName:
|
||||||
|
Description: Name of an existing EC2 KeyPair to enable SSH access
|
||||||
|
Type: 'AWS::EC2::KeyPair::KeyName'
|
||||||
|
|
||||||
|
LSPName:
|
||||||
|
Description: LSP Name
|
||||||
|
Type: String
|
||||||
|
|
||||||
|
VPCID:
|
||||||
|
Description: The ID of the VPC in which to create the resources
|
||||||
|
Type: 'AWS::EC2::VPC::Id'
|
||||||
|
|
||||||
|
LatestAmiId:
|
||||||
|
Type: 'AWS::SSM::Parameter::Value<AWS::EC2::Image::Id>'
|
||||||
|
Default: '/aws/service/canonical/ubuntu/server/jammy/stable/current/amd64/hvm/ebs-gp2/ami-id'
|
||||||
|
|
||||||
|
Resources:
|
||||||
|
# EC2 Instance
|
||||||
|
EC2Instance:
|
||||||
|
Type: 'AWS::EC2::Instance'
|
||||||
|
Properties:
|
||||||
|
InstanceType: m6a.xlarge
|
||||||
|
ImageId: !Ref LatestAmiId
|
||||||
|
KeyName: !Ref KeyName
|
||||||
|
BlockDeviceMappings:
|
||||||
|
- DeviceName: "/dev/sda1"
|
||||||
|
Ebs:
|
||||||
|
VolumeSize: 1024
|
||||||
|
VolumeType: gp2
|
||||||
|
DeleteOnTermination: true
|
||||||
|
UserData:
|
||||||
|
Fn::Base64:
|
||||||
|
!Sub |
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Elevate privileges
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
sudo bash "$0" "$@"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
# Redirect all outputs to a log file
|
||||||
|
exec > >(tee -a "/tmp/deployment.log") 2>&1
|
||||||
|
# fix locale if on debian
|
||||||
|
if grep -q "Debian" /etc/os-release; then
|
||||||
|
sed -i '/^# en_US.UTF-8 UTF-8/s/^# //' /etc/locale.gen
|
||||||
|
locale-gen
|
||||||
|
echo "export LC_ALL=en_US.UTF-8" >> /etc/bash.bashrc
|
||||||
|
echo "export LANG=en_US.UTF-8" >> /etc/bash.bashrc
|
||||||
|
|
||||||
|
fi
|
||||||
|
source /etc/bash.bashrc
|
||||||
|
# create users
|
||||||
|
sudo adduser --disabled-password --gecos "" lightning
|
||||||
|
sudo adduser --disabled-password --gecos "" bitcoin
|
||||||
|
sudo adduser --disabled-password --gecos "" lspd
|
||||||
|
|
||||||
|
# Create a file to store the credentials
|
||||||
|
CREDENTIALS="/home/lspd/credentials.txt"
|
||||||
|
touch "$CREDENTIALS"
|
||||||
|
# Generate a random password for PostgreSQL users
|
||||||
|
LSPD_DB_PASSWORD=$(</dev/urandom tr -dc 'A-Za-z0-9' | head -c 20)
|
||||||
|
LIGHTNING_DB_PASSWORD=$(</dev/urandom tr -dc 'A-Za-z0-9' | head -c 20)
|
||||||
|
# Output the password to a file
|
||||||
|
echo "### PostgreSQL Credentials ###" >> "$CREDENTIALS"
|
||||||
|
echo "postgres lspd:" >> "$CREDENTIALS"
|
||||||
|
echo "username: lspd " >> "$CREDENTIALS"
|
||||||
|
echo "password: $LSPD_DB_PASSWORD" >> "$CREDENTIALS"
|
||||||
|
echo "postgres lightning:" >> "$CREDENTIALS"
|
||||||
|
echo "username: lightning" >> "$CREDENTIALS"
|
||||||
|
echo "password: $LIGHTNING_DB_PASSWORD" >> "$CREDENTIALS"
|
||||||
|
|
||||||
|
# Generic name if no name is provided (running locally)
|
||||||
|
if [ -z "$LSPName" ]; then
|
||||||
|
LSPName="lsp-$(</dev/urandom tr -dc 'A-Za-z0-9' | head -c 5)"
|
||||||
|
fi
|
||||||
|
# Install dependencies and required packages
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update
|
||||||
|
apt-get upgrade -y
|
||||||
|
sudo apt-get install -y git autoconf automake build-essential libtool libgmp-dev libsqlite3-dev python3 python3-pip net-tools zlib1g-dev postgresql postgresql-client-common postgresql-client postgresql postgresql-contrib libpq5 libsodium-dev gettext cargo protobuf-compiler libgmp3-dev python-is-python3 libpq-dev jq
|
||||||
|
|
||||||
|
sudo pip3 install mako grpcio grpcio-tools
|
||||||
|
|
||||||
|
# Modify the pg_hba.conf file to set md5 password authentication for local connections
|
||||||
|
PG_VERSION=$(psql -V | awk '{print $3}' | awk -F"." '{print $1}')
|
||||||
|
sed -i 's/local all all peer/local all all md5/g' /etc/postgresql/$PG_VERSION/main/pg_hba.conf
|
||||||
|
|
||||||
|
# Create PostgreSQL users and databases
|
||||||
|
sudo -i -u postgres psql -c "CREATE ROLE lightning;"
|
||||||
|
sudo -i -u postgres psql -c "CREATE DATABASE lightning;"
|
||||||
|
sudo -i -u postgres psql -c "ALTER ROLE lightning WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD '$LIGHTNING_DB_PASSWORD';"
|
||||||
|
sudo -i -u postgres psql -c "ALTER DATABASE lightning OWNER TO lightning;"
|
||||||
|
|
||||||
|
sudo -i -u postgres psql -c "CREATE ROLE lspd;"
|
||||||
|
sudo -i -u postgres psql -c "ALTER ROLE lspd WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD '$LSPD_DB_PASSWORD';"
|
||||||
|
sudo -i -u postgres psql -c "CREATE DATABASE lspd WITH TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8';"
|
||||||
|
sudo -i -u postgres psql -c "ALTER DATABASE lspd OWNER TO lspd;"
|
||||||
|
|
||||||
|
# Restart PostgreSQL to apply changes
|
||||||
|
service postgresql restart
|
||||||
|
|
||||||
|
|
||||||
|
# Create directories under /opt
|
||||||
|
sudo mkdir -p /opt/lightning /opt/lspd
|
||||||
|
|
||||||
|
# Install go
|
||||||
|
wget https://go.dev/dl/go1.20.6.linux-amd64.tar.gz
|
||||||
|
sudo tar -C /usr/local -xzf go1.20.6.linux-amd64.tar.gz
|
||||||
|
echo "export PATH=$PATH:/usr/local/go/bin" | sudo tee -a /etc/bash.bashrc
|
||||||
|
source /etc/bash.bashrc
|
||||||
|
|
||||||
|
|
||||||
|
# Install rust
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
|
||||||
|
# Install bitcoin
|
||||||
|
wget https://bitcoincore.org/bin/bitcoin-core-25.0/bitcoin-25.0-x86_64-linux-gnu.tar.gz -O /opt/bitcoin.tar.gz
|
||||||
|
tar -xzf /opt/bitcoin.tar.gz -C /opt/
|
||||||
|
cd /opt/bitcoin-*/bin
|
||||||
|
chmod 710 /etc/bitcoin
|
||||||
|
sudo install -m 0755 -t /usr/local/bin *
|
||||||
|
|
||||||
|
|
||||||
|
cat <<EOL | sudo tee /etc/systemd/system/bitcoind.service
|
||||||
|
[Unit]
|
||||||
|
Description=Bitcoin daemon
|
||||||
|
After=network.target
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/var/lib/bitcoind
|
||||||
|
ExecStart=bitcoind -daemon -pid=/run/bitcoind/bitcoind.pid -conf=/etc/bitcoin/bitcoin.conf -datadir=/var/lib/bitcoind -startupnotify='systemd-notify --ready' -shutdownnotify='systemd-notify --stopping'
|
||||||
|
PermissionsStartOnly=true
|
||||||
|
ExecStartPre=/bin/chgrp bitcoin /var/lib/bitcoind
|
||||||
|
Type=notify
|
||||||
|
NotifyAccess=all
|
||||||
|
PIDFile=/run/bitcoind/bitcoind.pid
|
||||||
|
Restart=on-failure
|
||||||
|
TimeoutStartSec=infinity
|
||||||
|
TimeoutStopSec=600
|
||||||
|
User=bitcoin
|
||||||
|
Group=bitcoin
|
||||||
|
RuntimeDirectory=bitcoind
|
||||||
|
RuntimeDirectoryMode=0710
|
||||||
|
ConfigurationDirectory=bitcoin
|
||||||
|
StateDirectory=bitcoind
|
||||||
|
StateDirectoryMode=0710
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# cat to a bitcoin.conf file
|
||||||
|
RPCPASSWORD=$(</dev/urandom tr -dc 'A-Za-z0-9' | head -c 20)
|
||||||
|
echo "### Bitcoin Configuration ###" >> "$CREDENTIALS"
|
||||||
|
echo "rpcuser: lnd" >> "$CREDENTIALS"
|
||||||
|
echo "rpcpassword: $RPCPASSWORD" >> "$CREDENTIALS"
|
||||||
|
sudo mkdir /etc/bitcoin/
|
||||||
|
sudo touch /etc/bitcoin/bitcoin.conf
|
||||||
|
cat <<EOL | sudo tee /etc/bitcoin/bitcoin.conf
|
||||||
|
txindex=1
|
||||||
|
daemon=1
|
||||||
|
rpcuser=lnd
|
||||||
|
rpcpassword=$RPCPASSWORD
|
||||||
|
minrelaytxfee=0.00000000
|
||||||
|
incrementalrelayfee=0.00000010
|
||||||
|
zmqpubrawblock=tcp://127.0.0.1:28332
|
||||||
|
zmqpubrawtx=tcp://127.0.0.1:28333
|
||||||
|
EOL
|
||||||
|
chmod 710 /etc/bitcoin
|
||||||
|
sudo mkdir /home/lightning/.bitcoin/
|
||||||
|
sudo mkdir /root/.bitcoin/
|
||||||
|
sudo ln -s /etc/bitcoin/bitcoin.conf /home/lightning/.bitcoin/bitcoin.conf
|
||||||
|
sudo ln -s /etc/bitcoin/bitcoin.conf /root/.bitcoin/bitcoin.conf
|
||||||
|
###################################
|
||||||
|
######## Install lightning ########
|
||||||
|
###################################
|
||||||
|
sudo mkdir /home/lightning/.lightning/
|
||||||
|
cat <<EOL | sudo tee /home/lightning/.lightning/config
|
||||||
|
bitcoin-rpcuser=lnd
|
||||||
|
bitcoin-rpcpassword=$RPCPASSWORD
|
||||||
|
bitcoin-rpcconnect=127.0.0.1
|
||||||
|
bitcoin-rpcport=8332
|
||||||
|
addr=:9735
|
||||||
|
bitcoin-retry-timeout=3600
|
||||||
|
alias="${LSPName}"
|
||||||
|
wallet=postgres://lightning:$LIGHTNING_DB_PASSWORD@localhost:5432/lightning
|
||||||
|
|
||||||
|
EOL
|
||||||
|
git clone https://github.com/ElementsProject/lightning.git /opt/lightning
|
||||||
|
cd /opt/lightning
|
||||||
|
git checkout v23.05
|
||||||
|
./configure --enable-developer
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
cat <<EOL | sudo tee /etc/systemd/system/lightningd.service
|
||||||
|
[Unit]
|
||||||
|
Description=Lightning Network Daemon (lightningd)
|
||||||
|
After=network.target
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/lightningd --plugin=/home/lightning/.lightning/plugins/lspd_plugin --lsp-listen=127.0.0.1:12312 --max-concurrent-htlcs=30
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=full
|
||||||
|
Restart=on-failure
|
||||||
|
User=lightning
|
||||||
|
Group=lightning
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Install lspd
|
||||||
|
git clone https://github.com/breez/lspd.git /opt/lspd
|
||||||
|
cd /opt/lspd
|
||||||
|
source /etc/bash.bashrc
|
||||||
|
export PATH=$PATH:/usr/local/go/bin
|
||||||
|
sudo env "PATH=$PATH" go get
|
||||||
|
sudo env "PATH=$PATH" go get github.com/breez/lspd/cln_plugin
|
||||||
|
sudo env "PATH=$PATH" go build .
|
||||||
|
sudo env "PATH=$PATH" go build -o lspd_plugin ./cln_plugin/cmd
|
||||||
|
sudo cp lspd /usr/local/bin/
|
||||||
|
sudo mkdir /home/lightning/.lightning/plugins
|
||||||
|
sudo cp lspd_plugin /home/lightning/.lightning/plugins/
|
||||||
|
|
||||||
|
cat <<EOL | sudo tee /etc/systemd/system/lspd.service
|
||||||
|
[Unit]
|
||||||
|
Description=Lightning Service Daemon (lspd)
|
||||||
|
After=network.target
|
||||||
|
[Service]
|
||||||
|
User=lspd
|
||||||
|
EnvironmentFile=/home/lspd/.env
|
||||||
|
WorkingDirectory=/opt/lspd
|
||||||
|
ExecStart=/usr/local/bin/lspd
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
EOL
|
||||||
|
|
||||||
|
|
||||||
|
sudo chown -R lightning:lightning /home/lightning/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable bitcoind.service
|
||||||
|
sudo systemctl enable lspd.service
|
||||||
|
sudo systemctl enable lightningd.service
|
||||||
|
sudo systemctl start bitcoind.service
|
||||||
|
sudo systemctl start lightningd.service
|
||||||
|
|
||||||
|
sleep 60
|
||||||
|
echo "### Lightning Credentials ###" >> "$CREDENTIALS"
|
||||||
|
sudo echo "cln hsm_secret backup:" >> "$CREDENTIALS"
|
||||||
|
sudo xxd /home/lightning/.lightning/bitcoin/hsm_secret >> "$CREDENTIALS"
|
||||||
|
|
||||||
|
# Post install
|
||||||
|
PUBKEY=$(sudo -u lightning lightning-cli getinfo | jq .id | cut -d "\"" -f 2)
|
||||||
|
|
||||||
|
LSPD_PRIVATE_KEY=$(lspd genkey | awk -F= '{print $2}' | cut -d "\"" -f 2)
|
||||||
|
TOKEN=$(lspd genkey | awk -F= '{print $2}' | cut -d "\"" -f 2)
|
||||||
|
EXTERNAL_IP=$(curl -s http://whatismyip.akamai.com/)
|
||||||
|
echo "### LSPD Credentials ###" >> "$CREDENTIALS"
|
||||||
|
echo "token: $TOKEN" >> "$CREDENTIALS"
|
||||||
|
echo "lspd_private_key: $LSPD_PRIVATE_KEY" >> "$CREDENTIALS"
|
||||||
|
|
||||||
|
cat <<EOL | sudo tee /home/lspd/.env
|
||||||
|
|
||||||
|
LISTEN_ADDRESS=0.0.0.0:8888
|
||||||
|
LSPD_PRIVATE_KEY="$LSPD_PRIVATE_KEY"
|
||||||
|
AWS_REGION="<REPLACE ME>"
|
||||||
|
AWS_ACCESS_KEY_ID="<REPLACE ME>"
|
||||||
|
AWS_SECRET_ACCESS_KEY="<REPLACE ME>"
|
||||||
|
DATABASE_URL="postgres://lspd:$LSPD_DB_PASSWORD@localhost/lspd"
|
||||||
|
|
||||||
|
OPENCHANNEL_NOTIFICATION_TO='["REPLACE ME <email@example.com>"]'
|
||||||
|
OPENCHANNEL_NOTIFICATION_CC='["REPLACE ME <test@example.com>"]'
|
||||||
|
OPENCHANNEL_NOTIFICATION_FROM="test@example.com"
|
||||||
|
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_TO='["REPLACE ME <email@example.com>"]'
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_CC='["REPLACE ME <email@example.com>"]'
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_FROM="replaceme@example.com"
|
||||||
|
|
||||||
|
MEMPOOL_API_BASE_URL=https://mempool.space/api/v1/
|
||||||
|
MEMPOOL_PRIORITY=economy
|
||||||
|
NODES='[ { "name": "${LSPName}", "nodePubkey": "$PUBKEY", "lspdPrivateKey": "$LSPD_PRIVATE_KEY", "token": "$TOKEN", "host": "$EXTERNAL_IP:8888", "publicChannelAmount": "1000183", "channelAmount": "100000", "channelPrivate": false, "targetConf": "6", "minConfs": "6", "minHtlcMsat": "600", "baseFeeMsat": "1000", "feeRate": "0.000001", "timeLockDelta": "144", "channelFeePermyriad": "40", "channelMinimumFeeMsat": "2000000", "additionalChannelCapacity": "100000", "maxInactiveDuration": "3888000", "cln": { "pluginAddress": "127.0.0.1:12312", "socketPath": "/home/lightning/.lightning/bitcoin/lightning-rpc" } } ]'
|
||||||
|
|
||||||
|
EOL
|
||||||
|
sudo systemctl start lspd.service
|
||||||
|
echo "Installation complete"
|
||||||
|
sudo chmod 400 /home/lspd/credentials.txt
|
||||||
|
echo "Make sure to backup the credentials.txt file that can be found at /home/lspd/credentials.txt"
|
||||||
|
SecurityGroupIds:
|
||||||
|
- !GetAtt EC2SecurityGroup.GroupId
|
||||||
|
Tags:
|
||||||
|
- Key: Name
|
||||||
|
Value: lspd
|
||||||
|
|
||||||
|
# EC2 Elastic IP
|
||||||
|
EIP:
|
||||||
|
Type: 'AWS::EC2::EIP'
|
||||||
|
Properties:
|
||||||
|
Tags:
|
||||||
|
- Key: Name
|
||||||
|
Value: lspd
|
||||||
|
|
||||||
|
# EC2 Elastic IP Association
|
||||||
|
EIPAssociation:
|
||||||
|
Type: 'AWS::EC2::EIPAssociation'
|
||||||
|
Properties:
|
||||||
|
InstanceId: !Ref EC2Instance
|
||||||
|
EIP: !Ref EIP
|
||||||
|
|
||||||
|
# EC2 Security Group
|
||||||
|
EC2SecurityGroup:
|
||||||
|
Type: 'AWS::EC2::SecurityGroup'
|
||||||
|
Properties:
|
||||||
|
VpcId: !Ref VPCID
|
||||||
|
GroupDescription: Security Group for EC2 instance
|
||||||
|
SecurityGroupIngress:
|
||||||
|
- IpProtocol: tcp
|
||||||
|
FromPort: 22
|
||||||
|
ToPort: 22
|
||||||
|
CidrIp: 84.255.203.183/32
|
||||||
|
- IpProtocol: tcp
|
||||||
|
FromPort: 9735
|
||||||
|
ToPort: 9735
|
||||||
|
CidrIp: 0.0.0.0/0
|
||||||
|
- IpProtocol: tcp
|
||||||
|
FromPort: 8888
|
||||||
|
ToPort: 8888
|
||||||
|
CidrIp: 0.0.0.0/0
|
||||||
257
deploy/lspd-install.sh
Normal file
257
deploy/lspd-install.sh
Normal file
@@ -0,0 +1,257 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Elevate privileges
|
||||||
|
if [ "$EUID" -ne 0 ]; then
|
||||||
|
sudo bash "$0" "$@"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
# Redirect all outputs to a log file
|
||||||
|
exec > >(tee -a "/tmp/deployment.log") 2>&1
|
||||||
|
# fix locale if on debian
|
||||||
|
if grep -q "Debian" /etc/os-release; then
|
||||||
|
sed -i '/^# en_US.UTF-8 UTF-8/s/^# //' /etc/locale.gen
|
||||||
|
locale-gen
|
||||||
|
echo "export LC_ALL=en_US.UTF-8" >> /etc/bash.bashrc
|
||||||
|
echo "export LANG=en_US.UTF-8" >> /etc/bash.bashrc
|
||||||
|
|
||||||
|
fi
|
||||||
|
source /etc/bash.bashrc
|
||||||
|
# create users
|
||||||
|
sudo adduser --disabled-password --gecos "" lightning
|
||||||
|
sudo adduser --disabled-password --gecos "" bitcoin
|
||||||
|
sudo adduser --disabled-password --gecos "" lspd
|
||||||
|
|
||||||
|
# Create a file to store the credentials
|
||||||
|
CREDENTIALS="/home/lspd/credentials.txt"
|
||||||
|
touch "$CREDENTIALS"
|
||||||
|
# Generate a random password for PostgreSQL users
|
||||||
|
LSPD_DB_PASSWORD=$(</dev/urandom tr -dc 'A-Za-z0-9' | head -c 20)
|
||||||
|
LIGHTNING_DB_PASSWORD=$(</dev/urandom tr -dc 'A-Za-z0-9' | head -c 20)
|
||||||
|
# Output the password to a file
|
||||||
|
echo "### PostgreSQL Credentials ###" >> "$CREDENTIALS"
|
||||||
|
echo "postgres lspd:" >> "$CREDENTIALS"
|
||||||
|
echo "username: lspd " >> "$CREDENTIALS"
|
||||||
|
echo "password: $LSPD_DB_PASSWORD" >> "$CREDENTIALS"
|
||||||
|
echo "postgres lightning:" >> "$CREDENTIALS"
|
||||||
|
echo "username: lightning" >> "$CREDENTIALS"
|
||||||
|
echo "password: $LIGHTNING_DB_PASSWORD" >> "$CREDENTIALS"
|
||||||
|
|
||||||
|
# Generic name if no name is provided (running locally)
|
||||||
|
if [ -z "$LSPName" ]; then
|
||||||
|
LSPName="lsp-$(</dev/urandom tr -dc 'A-Za-z0-9' | head -c 5)"
|
||||||
|
fi
|
||||||
|
# Install dependencies and required packages
|
||||||
|
export DEBIAN_FRONTEND=noninteractive
|
||||||
|
apt-get update
|
||||||
|
apt-get upgrade -y
|
||||||
|
sudo apt-get install -y git autoconf automake build-essential libtool libgmp-dev libsqlite3-dev python3 python3-pip net-tools zlib1g-dev postgresql postgresql-client-common postgresql-client postgresql postgresql-contrib libpq5 libsodium-dev gettext cargo protobuf-compiler libgmp3-dev python-is-python3 libpq-dev jq
|
||||||
|
|
||||||
|
sudo pip3 install mako grpcio grpcio-tools
|
||||||
|
|
||||||
|
# Modify the pg_hba.conf file to set md5 password authentication for local connections
|
||||||
|
PG_VERSION=$(psql -V | awk '{print $3}' | awk -F"." '{print $1}')
|
||||||
|
sed -i 's/local all all peer/local all all md5/g' /etc/postgresql/$PG_VERSION/main/pg_hba.conf
|
||||||
|
|
||||||
|
# Create PostgreSQL users and databases
|
||||||
|
sudo -i -u postgres psql -c "CREATE ROLE lightning;"
|
||||||
|
sudo -i -u postgres psql -c "CREATE DATABASE lightning;"
|
||||||
|
sudo -i -u postgres psql -c "ALTER ROLE lightning WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD '$LIGHTNING_DB_PASSWORD';"
|
||||||
|
sudo -i -u postgres psql -c "ALTER DATABASE lightning OWNER TO lightning;"
|
||||||
|
|
||||||
|
sudo -i -u postgres psql -c "CREATE ROLE lspd;"
|
||||||
|
sudo -i -u postgres psql -c "ALTER ROLE lspd WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD '$LSPD_DB_PASSWORD';"
|
||||||
|
sudo -i -u postgres psql -c "CREATE DATABASE lspd WITH TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8';"
|
||||||
|
sudo -i -u postgres psql -c "ALTER DATABASE lspd OWNER TO lspd;"
|
||||||
|
|
||||||
|
# Restart PostgreSQL to apply changes
|
||||||
|
service postgresql restart
|
||||||
|
|
||||||
|
|
||||||
|
# Create directories under /opt
|
||||||
|
sudo mkdir -p /opt/lightning /opt/lspd
|
||||||
|
|
||||||
|
# Install go
|
||||||
|
wget https://go.dev/dl/go1.20.6.linux-amd64.tar.gz
|
||||||
|
sudo tar -C /usr/local -xzf go1.20.6.linux-amd64.tar.gz
|
||||||
|
echo "export PATH=$PATH:/usr/local/go/bin" | sudo tee -a /etc/bash.bashrc
|
||||||
|
source /etc/bash.bashrc
|
||||||
|
|
||||||
|
|
||||||
|
# Install rust
|
||||||
|
curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||||
|
|
||||||
|
# Install bitcoin
|
||||||
|
wget https://bitcoincore.org/bin/bitcoin-core-25.0/bitcoin-25.0-x86_64-linux-gnu.tar.gz -O /opt/bitcoin.tar.gz
|
||||||
|
tar -xzf /opt/bitcoin.tar.gz -C /opt/
|
||||||
|
cd /opt/bitcoin-*/bin
|
||||||
|
chmod 710 /etc/bitcoin
|
||||||
|
sudo install -m 0755 -t /usr/local/bin *
|
||||||
|
|
||||||
|
|
||||||
|
cat <<EOL | sudo tee /etc/systemd/system/bitcoind.service
|
||||||
|
[Unit]
|
||||||
|
Description=Bitcoin daemon
|
||||||
|
After=network.target
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=/var/lib/bitcoind
|
||||||
|
ExecStart=bitcoind -daemon -pid=/run/bitcoind/bitcoind.pid -conf=/etc/bitcoin/bitcoin.conf -datadir=/var/lib/bitcoind -startupnotify='systemd-notify --ready' -shutdownnotify='systemd-notify --stopping'
|
||||||
|
PermissionsStartOnly=true
|
||||||
|
ExecStartPre=/bin/chgrp bitcoin /var/lib/bitcoind
|
||||||
|
Type=notify
|
||||||
|
NotifyAccess=all
|
||||||
|
PIDFile=/run/bitcoind/bitcoind.pid
|
||||||
|
Restart=on-failure
|
||||||
|
TimeoutStartSec=infinity
|
||||||
|
TimeoutStopSec=600
|
||||||
|
User=bitcoin
|
||||||
|
Group=bitcoin
|
||||||
|
RuntimeDirectory=bitcoind
|
||||||
|
RuntimeDirectoryMode=0710
|
||||||
|
ConfigurationDirectory=bitcoin
|
||||||
|
StateDirectory=bitcoind
|
||||||
|
StateDirectoryMode=0710
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=full
|
||||||
|
ProtectHome=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# cat to a bitcoin.conf file
|
||||||
|
RPCPASSWORD=$(</dev/urandom tr -dc 'A-Za-z0-9' | head -c 20)
|
||||||
|
echo "### Bitcoin Configuration ###" >> "$CREDENTIALS"
|
||||||
|
echo "rpcuser: lnd" >> "$CREDENTIALS"
|
||||||
|
echo "rpcpassword: $RPCPASSWORD" >> "$CREDENTIALS"
|
||||||
|
sudo mkdir /etc/bitcoin/
|
||||||
|
sudo touch /etc/bitcoin/bitcoin.conf
|
||||||
|
cat <<EOL | sudo tee /etc/bitcoin/bitcoin.conf
|
||||||
|
txindex=1
|
||||||
|
daemon=1
|
||||||
|
rpcuser=lnd
|
||||||
|
rpcpassword=$RPCPASSWORD
|
||||||
|
minrelaytxfee=0.00000000
|
||||||
|
incrementalrelayfee=0.00000010
|
||||||
|
zmqpubrawblock=tcp://127.0.0.1:28332
|
||||||
|
zmqpubrawtx=tcp://127.0.0.1:28333
|
||||||
|
EOL
|
||||||
|
chmod 710 /etc/bitcoin
|
||||||
|
sudo mkdir /home/lightning/.bitcoin/
|
||||||
|
sudo mkdir /root/.bitcoin/
|
||||||
|
sudo ln -s /etc/bitcoin/bitcoin.conf /home/lightning/.bitcoin/bitcoin.conf
|
||||||
|
sudo ln -s /etc/bitcoin/bitcoin.conf /root/.bitcoin/bitcoin.conf
|
||||||
|
###################################
|
||||||
|
######## Install lightning ########
|
||||||
|
###################################
|
||||||
|
sudo mkdir /home/lightning/.lightning/
|
||||||
|
cat <<EOL | sudo tee /home/lightning/.lightning/config
|
||||||
|
bitcoin-rpcuser=lnd
|
||||||
|
bitcoin-rpcpassword=$RPCPASSWORD
|
||||||
|
bitcoin-rpcconnect=127.0.0.1
|
||||||
|
bitcoin-rpcport=8332
|
||||||
|
addr=:9735
|
||||||
|
bitcoin-retry-timeout=3600
|
||||||
|
alias="${LSPName}"
|
||||||
|
wallet=postgres://lightning:$LIGHTNING_DB_PASSWORD@localhost:5432/lightning
|
||||||
|
|
||||||
|
EOL
|
||||||
|
git clone https://github.com/ElementsProject/lightning.git /opt/lightning
|
||||||
|
cd /opt/lightning
|
||||||
|
git checkout v23.05
|
||||||
|
./configure --enable-developer
|
||||||
|
make
|
||||||
|
make install
|
||||||
|
cat <<EOL | sudo tee /etc/systemd/system/lightningd.service
|
||||||
|
[Unit]
|
||||||
|
Description=Lightning Network Daemon (lightningd)
|
||||||
|
After=network.target
|
||||||
|
[Service]
|
||||||
|
ExecStart=/usr/local/bin/lightningd --plugin=/home/lightning/.lightning/plugins/lspd_plugin --lsp-listen=127.0.0.1:12312 --max-concurrent-htlcs=30
|
||||||
|
MemoryDenyWriteExecute=true
|
||||||
|
NoNewPrivileges=true
|
||||||
|
PrivateDevices=true
|
||||||
|
PrivateTmp=true
|
||||||
|
ProtectSystem=full
|
||||||
|
Restart=on-failure
|
||||||
|
User=lightning
|
||||||
|
Group=lightning
|
||||||
|
EOL
|
||||||
|
|
||||||
|
# Install lspd
|
||||||
|
git clone https://github.com/breez/lspd.git /opt/lspd
|
||||||
|
cd /opt/lspd
|
||||||
|
source /etc/bash.bashrc
|
||||||
|
export PATH=$PATH:/usr/local/go/bin
|
||||||
|
sudo env "PATH=$PATH" go get
|
||||||
|
sudo env "PATH=$PATH" go get github.com/breez/lspd/cln_plugin
|
||||||
|
sudo env "PATH=$PATH" go build .
|
||||||
|
sudo env "PATH=$PATH" go build -o lspd_plugin ./cln_plugin/cmd
|
||||||
|
sudo cp lspd /usr/local/bin/
|
||||||
|
sudo mkdir /home/lightning/.lightning/plugins
|
||||||
|
sudo cp lspd_plugin /home/lightning/.lightning/plugins/
|
||||||
|
|
||||||
|
cat <<EOL | sudo tee /etc/systemd/system/lspd.service
|
||||||
|
[Unit]
|
||||||
|
Description=Lightning Service Daemon (lspd)
|
||||||
|
After=network.target
|
||||||
|
[Service]
|
||||||
|
User=lspd
|
||||||
|
EnvironmentFile=/home/lspd/.env
|
||||||
|
WorkingDirectory=/opt/lspd
|
||||||
|
ExecStart=/usr/local/bin/lspd
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=5
|
||||||
|
EOL
|
||||||
|
|
||||||
|
|
||||||
|
sudo chown -R lightning:lightning /home/lightning/
|
||||||
|
sudo systemctl daemon-reload
|
||||||
|
sudo systemctl enable bitcoind.service
|
||||||
|
sudo systemctl enable lspd.service
|
||||||
|
sudo systemctl enable lightningd.service
|
||||||
|
sudo systemctl start bitcoind.service
|
||||||
|
sudo systemctl start lightningd.service
|
||||||
|
|
||||||
|
sleep 60
|
||||||
|
echo "### Lightning Credentials ###" >> "$CREDENTIALS"
|
||||||
|
sudo echo "cln hsm_secret backup:" >> "$CREDENTIALS"
|
||||||
|
sudo xxd /home/lightning/.lightning/bitcoin/hsm_secret >> "$CREDENTIALS"
|
||||||
|
|
||||||
|
# Post install
|
||||||
|
PUBKEY=$(sudo -u lightning lightning-cli getinfo | jq .id | cut -d "\"" -f 2)
|
||||||
|
|
||||||
|
LSPD_PRIVATE_KEY=$(lspd genkey | awk -F= '{print $2}' | cut -d "\"" -f 2)
|
||||||
|
TOKEN=$(lspd genkey | awk -F= '{print $2}' | cut -d "\"" -f 2)
|
||||||
|
EXTERNAL_IP=$(curl -s http://whatismyip.akamai.com/)
|
||||||
|
echo "### LSPD Credentials ###" >> "$CREDENTIALS"
|
||||||
|
echo "token: $TOKEN" >> "$CREDENTIALS"
|
||||||
|
echo "lspd_private_key: $LSPD_PRIVATE_KEY" >> "$CREDENTIALS"
|
||||||
|
|
||||||
|
cat <<EOL | sudo tee /home/lspd/.env
|
||||||
|
|
||||||
|
LISTEN_ADDRESS=0.0.0.0:8888
|
||||||
|
LSPD_PRIVATE_KEY="$LSPD_PRIVATE_KEY"
|
||||||
|
AWS_REGION="<REPLACE ME>"
|
||||||
|
AWS_ACCESS_KEY_ID="<REPLACE ME>"
|
||||||
|
AWS_SECRET_ACCESS_KEY="<REPLACE ME>"
|
||||||
|
DATABASE_URL="postgres://lspd:$LSPD_DB_PASSWORD@localhost/lspd"
|
||||||
|
|
||||||
|
OPENCHANNEL_NOTIFICATION_TO='["REPLACE ME <email@example.com>"]'
|
||||||
|
OPENCHANNEL_NOTIFICATION_CC='["REPLACE ME <test@example.com>"]'
|
||||||
|
OPENCHANNEL_NOTIFICATION_FROM="test@example.com"
|
||||||
|
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_TO='["REPLACE ME <email@example.com>"]'
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_CC='["REPLACE ME <email@example.com>"]'
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_FROM="replaceme@example.com"
|
||||||
|
|
||||||
|
MEMPOOL_API_BASE_URL=https://mempool.space/api/v1/
|
||||||
|
MEMPOOL_PRIORITY=economy
|
||||||
|
NODES='[ { "name": "${LSPName}", "nodePubkey": "$PUBKEY", "lspdPrivateKey": "$LSPD_PRIVATE_KEY", "token": "$TOKEN", "host": "$EXTERNAL_IP:8888", "publicChannelAmount": "1000183", "channelAmount": "100000", "channelPrivate": false, "targetConf": "6", "minConfs": "6", "minHtlcMsat": "600", "baseFeeMsat": "1000", "feeRate": "0.000001", "timeLockDelta": "144", "channelFeePermyriad": "40", "channelMinimumFeeMsat": "2000000", "additionalChannelCapacity": "100000", "maxInactiveDuration": "3888000", "cln": { "pluginAddress": "127.0.0.1:12312", "socketPath": "/home/lightning/.lightning/bitcoin/lightning-rpc" } } ]'
|
||||||
|
|
||||||
|
EOL
|
||||||
|
sudo systemctl start lspd.service
|
||||||
|
echo "Installation complete"
|
||||||
|
sudo chmod 400 /home/lspd/credentials.txt
|
||||||
|
echo "Make sure to backup the credentials.txt file that can be found at /home/lspd/credentials.txt"
|
||||||
74
docs/CLN.md
Normal file
74
docs/CLN.md
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
|
||||||
|
## Installation instructions for core lightning and lspd
|
||||||
|
### Requirements
|
||||||
|
- CLN (complied with developer mode on) or LND
|
||||||
|
- lspd
|
||||||
|
- lspd plugin for cln
|
||||||
|
- postgresql
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
#### CLN
|
||||||
|
Follow compilation steps for CLN [here](https://github.com/ElementsProject/lightning/blob/master/doc/getting-started/getting-started/installation.md) to enable developer mode.
|
||||||
|
|
||||||
|
#### lspd
|
||||||
|
Needs to be build from source:
|
||||||
|
```
|
||||||
|
git clone https://github.com/breez/lspd
|
||||||
|
cd lspd
|
||||||
|
go build . # compile lspd
|
||||||
|
go build -o lspd_plugin ./cln_plugin/cmd # compile lspd cln plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
#### Postgresql
|
||||||
|
Lspd supports postgresql backend. To create database and new role to access it on your postgres server use:
|
||||||
|
##### Postgresql server
|
||||||
|
```
|
||||||
|
CREATE ROLE <username>;
|
||||||
|
ALTER ROLE <username> WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD '<password>';
|
||||||
|
CREATE DATABASE <dbname> WITH TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8';
|
||||||
|
ALTER DATABASE <dbname> OWNER TO <username>;
|
||||||
|
```
|
||||||
|
##### RDS on AWS
|
||||||
|
```
|
||||||
|
CREATE ROLE <username>;
|
||||||
|
ALTER ROLE <username> WITH INHERIT NOCREATEROLE NOCREATEDB LOGIN NOBYPASSRLS PASSWORD '<password>';
|
||||||
|
CREATE DATABASE <dbname> WITH TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8';
|
||||||
|
ALTER DATABASE <dbname> OWNER TO <username>;
|
||||||
|
```
|
||||||
|
|
||||||
|
### Configuration
|
||||||
|
1. Create a random token (for instance using the command `openssl rand -base64 48`, or `./lspd genkey`)
|
||||||
|
1. Define the environment variables as described in [sample.env](./sample.env). If `CERTMAGIC_DOMAIN` is defined, certificate for this domain is automatically obtained and renewed from Let's Encrypt. In this case, the port needs to be 443. If `CERTMAGIC_DOMAIN` is not defined, lspd needs to run behind a reverse proxy like treafik or nginx.
|
||||||
|
|
||||||
|
ENV variables:
|
||||||
|
- `LISTEN_ADDRESS` defines the host:port for the lspd grpc server
|
||||||
|
- `CERTMAGIC_DOMAIN` domain on which lspd will be accessible
|
||||||
|
- `DATABASE_URL` postgresql db url
|
||||||
|
- `AWS_REGION` AWS region for SES emailing
|
||||||
|
- `AWS_ACCESS_KEY_ID` API key for SES emailing
|
||||||
|
- `AWS_SECRET_ACCESS_KEY`API secret for SES emailing
|
||||||
|
- `MEMPOOL_API_BASE_URL` uses fee estimation for opening new channels (default: https://mempool.space)
|
||||||
|
- `MEMPOOL_PRIORITY` priority with which open new channels using mempool api (default: economy)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
### Running lspd on CLN
|
||||||
|
In order to run lspd on top of CLN, you need to run the lspd process and run cln with the provided cln plugin. You also need lightningd compiled with developer mode on (`./configure --enable-developer`)
|
||||||
|
|
||||||
|
The cln plugin (go build -o lspd_plugin cln_plugin/cmd) is best started with a bash script to pass environment variables (note this LISTEN_ADDRESS is the listen address for communication between lspd and the plugin, this is not the listen address mentioned in the 'final step')
|
||||||
|
|
||||||
|
```bash
|
||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
export LISTEN_ADDRESS=<listen address>
|
||||||
|
/path/to/lspd_plugin
|
||||||
|
```
|
||||||
|
|
||||||
|
1. Run cln with the following options set:
|
||||||
|
- `--plugin=/path/to/shell/script.sh`: to use lspd as plugin
|
||||||
|
- `--max-concurrent-htlcs=30`: In order to use zero reserve channels on the client side, (local max_accepted_htlcs + remote max_accepted_htlcs + 2) * dust limit must be lower than the channel capacity. Reduce max-concurrent-htlcs or increase channel capacity accordingly.
|
||||||
|
- `--dev-allowdustreserve=true`: In order to allow zero reserve on the client side (requires developer mode turned on)
|
||||||
|
1. Run lspd
|
||||||
|
|
||||||
|
### Final step
|
||||||
|
1. Share with Breez the TOKEN and the LISTEN_ADDRESS you've defined (send to contact@breez.technology)
|
||||||
53
docs/LND.md
Normal file
53
docs/LND.md
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
|
||||||
|
## Installation instructions for lnd and lspd
|
||||||
|
### Requirements
|
||||||
|
- lnd
|
||||||
|
- lspd
|
||||||
|
- postgresql
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
#### LND
|
||||||
|
Follow LND installation instructions [here](https://github.com/lightningnetwork/lnd/blob/master/docs/INSTALL.md).
|
||||||
|
|
||||||
|
#### lspd
|
||||||
|
Needs to be build from source:
|
||||||
|
```
|
||||||
|
git clone https://github.com/breez/lspd
|
||||||
|
cd lspd
|
||||||
|
go build . # compile lspd
|
||||||
|
go build -o lspd_plugin # compile lspd cln plugin
|
||||||
|
```
|
||||||
|
### Postgresql
|
||||||
|
Lspd supports postgresql backend. Create new database and user for lspd:
|
||||||
|
```
|
||||||
|
CREATE ROLE <username>;
|
||||||
|
ALTER ROLE <username> WITH NOSUPERUSER INHERIT NOCREATEROLE NOCREATEDB LOGIN NOREPLICATION NOBYPASSRLS PASSWORD '<password>';
|
||||||
|
CREATE DATABASE <dbname> WITH TEMPLATE = template0 ENCODING = 'UTF8' LC_COLLATE = 'en_US.UTF-8' LC_CTYPE = 'en_US.UTF-8';
|
||||||
|
ALTER DATABASE <dbname> OWNER TO <username>;
|
||||||
|
``````
|
||||||
|
|
||||||
|
|
||||||
|
### Configure
|
||||||
|
1. Create a random token (for instance using the command `openssl rand -base64 48`, or `./lspd genkey`)
|
||||||
|
1. Define the environment variables as described in [sample.env](./sample.env). If `CERTMAGIC_DOMAIN` is defined, certificate for this domain is automatically obtained and renewed from Let's Encrypt. In this case, the port needs to be 443. If `CERTMAGIC_DOMAIN` is not defined, lspd needs to run behind a reverse proxy like treafik or nginx.
|
||||||
|
|
||||||
|
ENV variables:
|
||||||
|
- `LISTEN_ADDRESS` defines the host:port for the lspd grpc server
|
||||||
|
- `CERTMAGIC_DOMAIN` domain on which lspd will be accessible
|
||||||
|
- `DATABASE_URL` postgresql db url
|
||||||
|
- `AWS_REGION`
|
||||||
|
- `AWS_ACCESS_KEY_ID`
|
||||||
|
- `AWS_SECRET_ACCESS_KEY`
|
||||||
|
- `MEMPOOL_API_BASE_URL` uses fee estimation for opening new channels (default: https://mempool.space)
|
||||||
|
- `MEMPOOL_PRIORITY` priority with which open new channels using mempool api (default: economy)
|
||||||
|
|
||||||
|
### Running lspd on LND
|
||||||
|
1. Run LND with the following options set:
|
||||||
|
- `--protocol.zero-conf`: for being able to open zero conf channels
|
||||||
|
- `--protocol.option-scid-alias`: required for zero conf channels
|
||||||
|
- `--requireinterceptor`: to make sure all htlcs are intercepted by lspd
|
||||||
|
- `--bitcoin.chanreservescript="0"` to allow the client to have zero reserve on their side
|
||||||
|
1. Run lspd
|
||||||
|
|
||||||
|
### Final step
|
||||||
|
1. Share with Breez the TOKEN and the LISTEN_ADDRESS you've defined (send to contact@breez.technology)
|
||||||
42
docs/aws.md
Normal file
42
docs/aws.md
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
## Automated deployment of LSPD stack to AWS
|
||||||
|
Cloudformation template for automated deployment of lspd, bitcoind and cln with postgresql backend.
|
||||||
|
### Requirements
|
||||||
|
- AWS account
|
||||||
|
- AWS SES configured
|
||||||
|
|
||||||
|
### Deployment
|
||||||
|
[Cloudformation template](../deploy/deploy.yml) will automatically deploy several things:
|
||||||
|
- new ec2 instance (m6a.xlarge) to your selected VPC
|
||||||
|
- bitcoind
|
||||||
|
- clnd (with postgresql as backend)
|
||||||
|
- lspd
|
||||||
|
|
||||||
|
### After deployment steps
|
||||||
|
#### Configure email notifications
|
||||||
|
|
||||||
|
Edit file ```/home/lspd/.env```.
|
||||||
|
|
||||||
|
1) set your SES credentials:
|
||||||
|
```
|
||||||
|
AWS_REGION="<REPLACE ME>"
|
||||||
|
AWS_ACCESS_KEY_ID="<REPLACE ME>"
|
||||||
|
AWS_SECRET_ACCESS_KEY="<REPLACE ME>"
|
||||||
|
```
|
||||||
|
|
||||||
|
2) configure email
|
||||||
|
```
|
||||||
|
OPENCHANNEL_NOTIFICATION_TO='["REPLACE ME <email@example.com>"]'
|
||||||
|
OPENCHANNEL_NOTIFICATION_CC='["REPLACE ME <test@example.com>"]'
|
||||||
|
OPENCHANNEL_NOTIFICATION_FROM="test@example.com"
|
||||||
|
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_TO='["REPLACE ME <email@example.com>"]'
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_CC='["REPLACE ME <email@example.com>"]'
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_FROM="replaceme@example.com"
|
||||||
|
```
|
||||||
|
#### Backup credentials
|
||||||
|
|
||||||
|
All credentials are generated automatically and are written down in ```/home/lspd/credentials.txt```
|
||||||
|
|
||||||
|
**Store them securely and delete the file.**
|
||||||
|
### Debugging
|
||||||
|
Log file of deployment is written to ```/tmp/deployment.log``` where you can see the entire output of what happend during deployment.
|
||||||
57
docs/bash.md
Normal file
57
docs/bash.md
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
## Automated install of LSPD stack for linux
|
||||||
|
### Requirements
|
||||||
|
- ubuntu or debian based distribution
|
||||||
|
- AWS SES credentials
|
||||||
|
- root / user without sudo password
|
||||||
|
|
||||||
|
### Installation
|
||||||
|
To install bitcoind,cln and lspd to your system simply run:
|
||||||
|
|
||||||
|
```curl -sL https://raw.githubusercontent.com/breez/lspd/master/deploy/lspd-install.sh | sudo bash -```
|
||||||
|
|
||||||
|
It will automatically configure your server and install all needed dependencies for running LSPD stack. You will have to manually change the name of your LSPD and your cln alias.
|
||||||
|
|
||||||
|
LSPD:
|
||||||
|
|
||||||
|
```
|
||||||
|
vim /home/lspd/.env
|
||||||
|
# change the name variable in the last line, it will have randomly generated name like "lsp-53v4"
|
||||||
|
NODES='[ { "name": "${LSPName}"
|
||||||
|
```
|
||||||
|
|
||||||
|
CLN:
|
||||||
|
```
|
||||||
|
vim /home/lightning/.lightning/config
|
||||||
|
# change alias row, it will have randomly generated name like "lsp-53v4"
|
||||||
|
alias="${LSPName}"
|
||||||
|
```
|
||||||
|
|
||||||
|
### After deployment steps
|
||||||
|
#### Configure email notifications
|
||||||
|
|
||||||
|
Edit file ```/home/lspd/.env```.
|
||||||
|
|
||||||
|
1) set your SES credentials:
|
||||||
|
```
|
||||||
|
AWS_REGION="<REPLACE ME>"
|
||||||
|
AWS_ACCESS_KEY_ID="<REPLACE ME>"
|
||||||
|
AWS_SECRET_ACCESS_KEY="<REPLACE ME>"
|
||||||
|
```
|
||||||
|
|
||||||
|
2) configure email
|
||||||
|
```
|
||||||
|
OPENCHANNEL_NOTIFICATION_TO='["REPLACE ME <email@example.com>"]'
|
||||||
|
OPENCHANNEL_NOTIFICATION_CC='["REPLACE ME <test@example.com>"]'
|
||||||
|
OPENCHANNEL_NOTIFICATION_FROM="test@example.com"
|
||||||
|
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_TO='["REPLACE ME <email@example.com>"]'
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_CC='["REPLACE ME <email@example.com>"]'
|
||||||
|
CHANNELMISMATCH_NOTIFICATION_FROM="replaceme@example.com"
|
||||||
|
```
|
||||||
|
#### Backup credentials
|
||||||
|
|
||||||
|
All credentials are generated automatically and are written down in ```/home/lspd/credentials.txt```
|
||||||
|
|
||||||
|
**Store them securely and delete the file.**
|
||||||
|
### Debugging
|
||||||
|
Log file of deployment is written to ```/tmp/deployment.log``` where you can see the entire output of what happend during deployment.
|
||||||
194
go.mod
Normal file
194
go.mod
Normal file
@@ -0,0 +1,194 @@
|
|||||||
|
module github.com/breez/lspd
|
||||||
|
|
||||||
|
go 1.19
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aws/aws-sdk-go v1.34.0
|
||||||
|
github.com/breez/lntest v0.0.26
|
||||||
|
github.com/btcsuite/btcd v0.23.5-0.20230228185050-38331963bddd
|
||||||
|
github.com/btcsuite/btcd/btcec/v2 v2.3.2
|
||||||
|
github.com/btcsuite/btcd/chaincfg/chainhash v1.0.2
|
||||||
|
github.com/caddyserver/certmagic v0.11.2
|
||||||
|
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1
|
||||||
|
github.com/docker/docker v20.10.24+incompatible
|
||||||
|
github.com/docker/go-connections v0.4.0
|
||||||
|
github.com/elementsproject/glightning v0.0.0-20230525134205-ef34d849f564
|
||||||
|
github.com/golang/protobuf v1.5.2
|
||||||
|
github.com/grpc-ecosystem/go-grpc-middleware v1.3.0
|
||||||
|
github.com/jackc/pgtype v1.8.1
|
||||||
|
github.com/jackc/pgx/v4 v4.13.0
|
||||||
|
github.com/lightningnetwork/lightning-onion v1.2.1-0.20221202012345-ca23184850a1
|
||||||
|
github.com/lightningnetwork/lnd v0.16.2-beta
|
||||||
|
github.com/lightningnetwork/lnd/tlv v1.1.0
|
||||||
|
github.com/stretchr/testify v1.8.1
|
||||||
|
go.starlark.net v0.0.0-20230612165344-9532f5667272
|
||||||
|
golang.org/x/exp v0.0.0-20230321023759-10a507213a29
|
||||||
|
golang.org/x/sync v0.1.0
|
||||||
|
google.golang.org/grpc v1.50.1
|
||||||
|
google.golang.org/protobuf v1.27.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Microsoft/go-winio v0.5.2 // indirect
|
||||||
|
github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect
|
||||||
|
github.com/docker/distribution v2.8.2+incompatible // indirect
|
||||||
|
github.com/docker/go-units v0.5.0 // indirect
|
||||||
|
github.com/ethereum/go-ethereum v1.10.17 // indirect
|
||||||
|
github.com/golang-jwt/jwt/v4 v4.4.2 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.2 // indirect
|
||||||
|
github.com/google/uuid v1.3.0 // indirect
|
||||||
|
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 // indirect
|
||||||
|
github.com/lightninglabs/neutrino/cache v1.1.1 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.16 // indirect
|
||||||
|
github.com/moby/term v0.0.0-20221120202655-abb19827d345 // indirect
|
||||||
|
github.com/morikuni/aec v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/go-digest v1.0.0 // indirect
|
||||||
|
github.com/opencontainers/image-spec v1.0.2 // indirect
|
||||||
|
github.com/pkg/errors v0.9.1 // indirect
|
||||||
|
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0 // indirect
|
||||||
|
gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.0.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracegrpc v1.0.1 // indirect
|
||||||
|
golang.org/x/mod v0.8.0 // indirect
|
||||||
|
golang.org/x/net v0.8.0 // indirect
|
||||||
|
golang.org/x/tools v0.6.0 // indirect
|
||||||
|
gotest.tools/v3 v3.4.0 // indirect
|
||||||
|
lukechampine.com/uint128 v1.2.0 // indirect
|
||||||
|
modernc.org/cc/v3 v3.40.0 // indirect
|
||||||
|
modernc.org/ccgo/v3 v3.16.13 // indirect
|
||||||
|
modernc.org/libc v1.22.2 // indirect
|
||||||
|
modernc.org/mathutil v1.5.0 // indirect
|
||||||
|
modernc.org/memory v1.4.0 // indirect
|
||||||
|
modernc.org/opt v0.1.3 // indirect
|
||||||
|
modernc.org/sqlite v1.20.3 // indirect
|
||||||
|
modernc.org/strutil v1.1.3 // indirect
|
||||||
|
modernc.org/token v1.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||||
|
github.com/aead/siphash v1.0.1 // indirect
|
||||||
|
github.com/andybalholm/brotli v1.0.3 // indirect
|
||||||
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
|
github.com/btcsuite/btcd/btcutil v1.1.3 // indirect
|
||||||
|
github.com/btcsuite/btcd/btcutil/psbt v1.1.8 // indirect
|
||||||
|
github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f // indirect
|
||||||
|
github.com/btcsuite/btcwallet v0.16.9 // indirect
|
||||||
|
github.com/btcsuite/btcwallet/wallet/txauthor v1.3.2 // indirect
|
||||||
|
github.com/btcsuite/btcwallet/wallet/txrules v1.2.0 // indirect
|
||||||
|
github.com/btcsuite/btcwallet/wallet/txsizes v1.2.3 // indirect
|
||||||
|
github.com/btcsuite/btcwallet/walletdb v1.4.0 // indirect
|
||||||
|
github.com/btcsuite/btcwallet/wtxmgr v1.5.0 // indirect
|
||||||
|
github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
|
||||||
|
github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
|
||||||
|
github.com/btcsuite/winsvc v1.0.0 // indirect
|
||||||
|
github.com/cenkalti/backoff/v4 v4.1.1 // indirect
|
||||||
|
github.com/cespare/xxhash/v2 v2.1.1 // indirect
|
||||||
|
github.com/coreos/go-semver v0.3.0 // indirect
|
||||||
|
github.com/coreos/go-systemd/v22 v22.3.2 // indirect
|
||||||
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
|
github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
|
||||||
|
github.com/decred/dcrd/lru v1.0.0 // indirect
|
||||||
|
github.com/dsnet/compress v0.0.1 // indirect
|
||||||
|
github.com/dustin/go-humanize v1.0.0 // indirect
|
||||||
|
github.com/ecies/go/v2 v2.0.4
|
||||||
|
github.com/fergusstrange/embedded-postgres v1.10.0 // indirect
|
||||||
|
github.com/go-acme/lego/v3 v3.7.0 // indirect
|
||||||
|
github.com/go-errors/errors v1.0.1 // indirect
|
||||||
|
github.com/gofrs/uuid v4.2.0+incompatible // indirect
|
||||||
|
github.com/gogo/protobuf v1.3.2 // indirect
|
||||||
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
|
github.com/google/btree v1.0.1 // indirect
|
||||||
|
github.com/gorilla/websocket v1.4.2 // indirect
|
||||||
|
github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
|
||||||
|
github.com/grpc-ecosystem/grpc-gateway/v2 v2.5.0 // indirect
|
||||||
|
github.com/jackc/chunkreader/v2 v2.0.1 // indirect
|
||||||
|
github.com/jackc/pgconn v1.10.0 // indirect
|
||||||
|
github.com/jackc/pgio v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgpassfile v1.0.0 // indirect
|
||||||
|
github.com/jackc/pgproto3/v2 v2.1.1 // indirect
|
||||||
|
github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
|
||||||
|
github.com/jackc/puddle v1.1.3 // indirect
|
||||||
|
github.com/jessevdk/go-flags v1.4.0 // indirect
|
||||||
|
github.com/jmespath/go-jmespath v0.4.0 // indirect
|
||||||
|
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||||
|
github.com/jrick/logrotate v1.0.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.11 // indirect
|
||||||
|
github.com/juju/clock v1.0.2 // indirect
|
||||||
|
github.com/juju/errors v1.0.0 // indirect
|
||||||
|
github.com/juju/loggo v1.0.0 // indirect
|
||||||
|
github.com/juju/testing v1.0.1 // indirect
|
||||||
|
github.com/kkdai/bstream v1.0.0 // indirect
|
||||||
|
github.com/klauspost/compress v1.13.6 // indirect
|
||||||
|
github.com/klauspost/cpuid v1.2.3 // indirect
|
||||||
|
github.com/klauspost/pgzip v1.2.5 // indirect
|
||||||
|
github.com/kr/pretty v0.3.0 // indirect
|
||||||
|
github.com/lib/pq v1.10.3 // indirect
|
||||||
|
github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
|
||||||
|
github.com/lightninglabs/neutrino v0.15.0 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/clock v1.1.0 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/healthcheck v1.2.2 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/kvdb v1.4.1 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/queue v1.1.0 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/ticker v1.1.0 // indirect
|
||||||
|
github.com/lightningnetwork/lnd/tor v1.1.0 // indirect
|
||||||
|
github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect
|
||||||
|
github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
|
||||||
|
github.com/mholt/archiver/v3 v3.5.0 // indirect
|
||||||
|
github.com/miekg/dns v1.1.43 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.1 // indirect
|
||||||
|
github.com/nwaples/rardecode v1.1.2 // indirect
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.8 // indirect
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 // indirect
|
||||||
|
github.com/prometheus/client_golang v1.11.1 // indirect
|
||||||
|
github.com/prometheus/client_model v0.2.0 // indirect
|
||||||
|
github.com/prometheus/common v0.26.0 // indirect
|
||||||
|
github.com/prometheus/procfs v0.6.0 // indirect
|
||||||
|
github.com/rogpeppe/fastuuid v1.2.0 // indirect
|
||||||
|
github.com/rogpeppe/go-internal v1.8.1 // indirect
|
||||||
|
github.com/sirupsen/logrus v1.7.0 // indirect
|
||||||
|
github.com/soheilhy/cmux v0.1.5 // indirect
|
||||||
|
github.com/spf13/pflag v1.0.5 // indirect
|
||||||
|
github.com/stretchr/objx v0.5.0 // indirect
|
||||||
|
github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
|
||||||
|
github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
|
||||||
|
github.com/ulikunitz/xz v0.5.10 // indirect
|
||||||
|
github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
|
||||||
|
github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
|
||||||
|
go.etcd.io/bbolt v1.3.6 // indirect
|
||||||
|
go.etcd.io/etcd/api/v3 v3.5.7 // indirect
|
||||||
|
go.etcd.io/etcd/client/pkg/v3 v3.5.7 // indirect
|
||||||
|
go.etcd.io/etcd/client/v2 v2.305.7 // indirect
|
||||||
|
go.etcd.io/etcd/client/v3 v3.5.7 // indirect
|
||||||
|
go.etcd.io/etcd/pkg/v3 v3.5.7 // indirect
|
||||||
|
go.etcd.io/etcd/raft/v3 v3.5.7 // indirect
|
||||||
|
go.etcd.io/etcd/server/v3 v3.5.7 // indirect
|
||||||
|
go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.25.0 // indirect
|
||||||
|
go.opentelemetry.io/otel v1.0.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/sdk v1.0.1 // indirect
|
||||||
|
go.opentelemetry.io/otel/trace v1.0.1 // indirect
|
||||||
|
go.opentelemetry.io/proto/otlp v0.9.0 // indirect
|
||||||
|
go.uber.org/atomic v1.7.0 // indirect
|
||||||
|
go.uber.org/multierr v1.8.0 // indirect
|
||||||
|
go.uber.org/zap v1.17.0 // indirect
|
||||||
|
golang.org/x/crypto v0.7.0 // indirect
|
||||||
|
golang.org/x/sys v0.6.0 // indirect
|
||||||
|
golang.org/x/term v0.6.0 // indirect
|
||||||
|
golang.org/x/text v0.8.0 // indirect
|
||||||
|
golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba // indirect
|
||||||
|
google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect
|
||||||
|
gopkg.in/errgo.v1 v1.0.1 // indirect
|
||||||
|
gopkg.in/macaroon-bakery.v2 v2.0.1 // indirect
|
||||||
|
gopkg.in/macaroon.v2 v2.0.0 // indirect
|
||||||
|
gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
|
||||||
|
gopkg.in/square/go-jose.v2 v2.3.1 // indirect
|
||||||
|
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
sigs.k8s.io/yaml v1.2.0 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/lightningnetwork/lnd v0.16.2-beta => github.com/breez/lnd v0.15.0-beta.rc6.0.20230501134702-cebcdf1b17fd
|
||||||
|
|
||||||
|
replace github.com/elementsproject/glightning => github.com/breez/glightning v0.0.1-breez
|
||||||
201
grpc_server.go
Normal file
201
grpc_server.go
Normal file
@@ -0,0 +1,201 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/cln"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
"github.com/breez/lspd/lightning"
|
||||||
|
"github.com/breez/lspd/lnd"
|
||||||
|
"github.com/breez/lspd/notifications"
|
||||||
|
lspdrpc "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/caddyserver/certmagic"
|
||||||
|
ecies "github.com/ecies/go/v2"
|
||||||
|
grpc_middleware "github.com/grpc-ecosystem/go-grpc-middleware"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type grpcServer struct {
|
||||||
|
address string
|
||||||
|
certmagicDomain string
|
||||||
|
lis net.Listener
|
||||||
|
s *grpc.Server
|
||||||
|
nodes map[string]*node
|
||||||
|
c lspdrpc.ChannelOpenerServer
|
||||||
|
n notifications.NotificationsServer
|
||||||
|
}
|
||||||
|
|
||||||
|
type nodeContext struct {
|
||||||
|
token string
|
||||||
|
node *node
|
||||||
|
}
|
||||||
|
|
||||||
|
type node struct {
|
||||||
|
client lightning.Client
|
||||||
|
nodeConfig *config.NodeConfig
|
||||||
|
privateKey *btcec.PrivateKey
|
||||||
|
publicKey *btcec.PublicKey
|
||||||
|
eciesPrivateKey *ecies.PrivateKey
|
||||||
|
eciesPublicKey *ecies.PublicKey
|
||||||
|
openChannelReqGroup singleflight.Group
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewGrpcServer(
|
||||||
|
configs []*config.NodeConfig,
|
||||||
|
address string,
|
||||||
|
certmagicDomain string,
|
||||||
|
c lspdrpc.ChannelOpenerServer,
|
||||||
|
n notifications.NotificationsServer,
|
||||||
|
) (*grpcServer, error) {
|
||||||
|
if len(configs) == 0 {
|
||||||
|
return nil, fmt.Errorf("no nodes supplied")
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes := make(map[string]*node)
|
||||||
|
for _, config := range configs {
|
||||||
|
pk, err := hex.DecodeString(config.LspdPrivateKey)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("hex.DecodeString(config.lspdPrivateKey=%v) error: %v", config.LspdPrivateKey, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
eciesPrivateKey := ecies.NewPrivateKeyFromBytes(pk)
|
||||||
|
eciesPublicKey := eciesPrivateKey.PublicKey
|
||||||
|
privateKey, publicKey := btcec.PrivKeyFromBytes(pk)
|
||||||
|
|
||||||
|
node := &node{
|
||||||
|
nodeConfig: config,
|
||||||
|
privateKey: privateKey,
|
||||||
|
publicKey: publicKey,
|
||||||
|
eciesPrivateKey: eciesPrivateKey,
|
||||||
|
eciesPublicKey: eciesPublicKey,
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Lnd == nil && config.Cln == nil {
|
||||||
|
return nil, fmt.Errorf("node has to be either cln or lnd")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Lnd != nil && config.Cln != nil {
|
||||||
|
return nil, fmt.Errorf("node cannot be both cln and lnd")
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Lnd != nil {
|
||||||
|
node.client, err = lnd.NewLndClient(config.Lnd)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if config.Cln != nil {
|
||||||
|
node.client, err = cln.NewClnClient(config.Cln.SocketPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, token := range config.Tokens {
|
||||||
|
_, exists := nodes[token]
|
||||||
|
if exists {
|
||||||
|
return nil, fmt.Errorf("cannot have multiple nodes with the same token")
|
||||||
|
}
|
||||||
|
|
||||||
|
nodes[token] = node
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return &grpcServer{
|
||||||
|
address: address,
|
||||||
|
certmagicDomain: certmagicDomain,
|
||||||
|
nodes: nodes,
|
||||||
|
c: c,
|
||||||
|
n: n,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *grpcServer) Start() error {
|
||||||
|
// Make sure all nodes are available and set name and pubkey if not set
|
||||||
|
// in config.
|
||||||
|
for _, n := range s.nodes {
|
||||||
|
info, err := n.client.GetInfo()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to get info from host %s", n.nodeConfig.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.nodeConfig.Name == "" {
|
||||||
|
n.nodeConfig.Name = info.Alias
|
||||||
|
}
|
||||||
|
|
||||||
|
if n.nodeConfig.NodePubkey == "" {
|
||||||
|
n.nodeConfig.NodePubkey = info.Pubkey
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var lis net.Listener
|
||||||
|
if s.certmagicDomain == "" {
|
||||||
|
var err error
|
||||||
|
lis, err = net.Listen("tcp", s.address)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tlsConfig, err := certmagic.TLS([]string{s.certmagicDomain})
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to run certmagic: %v", err)
|
||||||
|
}
|
||||||
|
lis, err = tls.Listen("tcp", s.address, tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to listen: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
srv := grpc.NewServer(
|
||||||
|
grpc_middleware.WithUnaryServerChain(func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) {
|
||||||
|
if md, ok := metadata.FromIncomingContext(ctx); ok {
|
||||||
|
for _, auth := range md.Get("authorization") {
|
||||||
|
if !strings.HasPrefix(auth, "Bearer ") {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
token := strings.Replace(auth, "Bearer ", "", 1)
|
||||||
|
node, ok := s.nodes[token]
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
return handler(context.WithValue(ctx, contextKey("node"), &nodeContext{
|
||||||
|
token: token,
|
||||||
|
node: node,
|
||||||
|
}), req)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, status.Errorf(codes.PermissionDenied, "Not authorized")
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
lspdrpc.RegisterChannelOpenerServer(srv, s.c)
|
||||||
|
notifications.RegisterNotificationsServer(srv, s.n)
|
||||||
|
|
||||||
|
s.s = srv
|
||||||
|
s.lis = lis
|
||||||
|
if err := srv.Serve(lis); err != nil {
|
||||||
|
return fmt.Errorf("failed to serve: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *grpcServer) Stop() {
|
||||||
|
srv := s.s
|
||||||
|
if srv != nil {
|
||||||
|
srv.GracefulStop()
|
||||||
|
}
|
||||||
|
}
|
||||||
175
interceptor/email.go
Normal file
175
interceptor/email.go
Normal file
@@ -0,0 +1,175 @@
|
|||||||
|
package interceptor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"html/template"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
|
||||||
|
"github.com/aws/aws-sdk-go/aws"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/awserr"
|
||||||
|
"github.com/aws/aws-sdk-go/aws/session"
|
||||||
|
"github.com/aws/aws-sdk-go/service/ses"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
charset = "UTF-8"
|
||||||
|
)
|
||||||
|
|
||||||
|
func addresses(a string) (addr []*string) {
|
||||||
|
json.Unmarshal([]byte(a), &addr)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendEmail(to, cc, from, content, subject string) error {
|
||||||
|
|
||||||
|
sess, err := session.NewSession(&aws.Config{})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error in session.NewSession: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
svc := ses.New(sess)
|
||||||
|
|
||||||
|
input := &ses.SendEmailInput{
|
||||||
|
Destination: &ses.Destination{
|
||||||
|
CcAddresses: addresses(cc),
|
||||||
|
ToAddresses: addresses(to),
|
||||||
|
},
|
||||||
|
Message: &ses.Message{
|
||||||
|
Body: &ses.Body{
|
||||||
|
Html: &ses.Content{
|
||||||
|
Charset: aws.String(charset),
|
||||||
|
Data: aws.String(content),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Subject: &ses.Content{
|
||||||
|
Charset: aws.String(charset),
|
||||||
|
Data: aws.String(subject),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Source: aws.String(from),
|
||||||
|
}
|
||||||
|
// Attempt to send the email.
|
||||||
|
result, err := svc.SendEmail(input)
|
||||||
|
if err != nil {
|
||||||
|
if aerr, ok := err.(awserr.Error); ok {
|
||||||
|
switch aerr.Code() {
|
||||||
|
case ses.ErrCodeMessageRejected:
|
||||||
|
log.Println(ses.ErrCodeMessageRejected, aerr.Error())
|
||||||
|
case ses.ErrCodeMailFromDomainNotVerifiedException:
|
||||||
|
log.Println(ses.ErrCodeMailFromDomainNotVerifiedException, aerr.Error())
|
||||||
|
case ses.ErrCodeConfigurationSetDoesNotExistException:
|
||||||
|
log.Println(ses.ErrCodeConfigurationSetDoesNotExistException, aerr.Error())
|
||||||
|
default:
|
||||||
|
log.Println(aerr.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Print the error, cast err to awserr.Error to get the Code and
|
||||||
|
// Message from an error.
|
||||||
|
log.Println(err.Error())
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Email sent with result:\n%v", result)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendChannelMismatchNotification(nodeID string, notFakeChannels, closedChannels map[string]uint64) error {
|
||||||
|
var html bytes.Buffer
|
||||||
|
|
||||||
|
tpl := `
|
||||||
|
<h2>NodeID: {{ .NodeID }}</h2>
|
||||||
|
{{ if .NotFakeChannels }}<h3>Channels not fake anynmore</h3><table>
|
||||||
|
<tr><th>Channel Point</th><th>Height Hint</th></tr>
|
||||||
|
{{ range $key, $value := .NotFakeChannels }}<tr><td>{{ $key }}</td><td>{{ $value }}</td></tr>{{ end }}
|
||||||
|
</table>{{ end }}
|
||||||
|
{{ if .ClosedChannels }}<h3>Closed Channels</h3><table>
|
||||||
|
<tr><th>Channel Point</th><th>Height Hint</th></tr>
|
||||||
|
{{ range $key, $value := .ClosedChannels }}<tr><td>{{ $key }}</td><td>{{ $value }}</td></tr>{{ end }}
|
||||||
|
</table>{{ end }}
|
||||||
|
`
|
||||||
|
t, err := template.New("ChannelMismatchEmail").Parse(tpl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := t.Execute(&html, struct {
|
||||||
|
NodeID string
|
||||||
|
NotFakeChannels map[string]uint64
|
||||||
|
ClosedChannels map[string]uint64
|
||||||
|
}{nodeID, notFakeChannels, closedChannels}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sendEmail(
|
||||||
|
os.Getenv("CHANNELMISMATCH_NOTIFICATION_TO"),
|
||||||
|
os.Getenv("CHANNELMISMATCH_NOTIFICATION_CC"),
|
||||||
|
os.Getenv("CHANNELMISMATCH_NOTIFICATION_FROM"),
|
||||||
|
html.String(),
|
||||||
|
"Channel(s) Mismatch",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error sending open channel email: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func sendOpenChannelEmailNotification(
|
||||||
|
paymentHash []byte, incomingAmountMsat int64,
|
||||||
|
destination []byte, capacity int64,
|
||||||
|
channelPoint string,
|
||||||
|
tag *string,
|
||||||
|
) error {
|
||||||
|
var html bytes.Buffer
|
||||||
|
|
||||||
|
tpl := `
|
||||||
|
<table>
|
||||||
|
<tr><td>Payment Hash:</td><td>{{ .PaymentHash }}</td></tr>
|
||||||
|
<tr><td>Incoming Amount (msat):</td><td>{{ .IncomingAmountMsat }}</td></tr>
|
||||||
|
<tr><td>Destination Node:</td><td>{{ .Destination }}</td></tr>
|
||||||
|
<tr><td>Channel capacity (sat):</td><td>{{ .Capacity }}</td></tr>
|
||||||
|
<tr><td>Channel point:</td><td>{{ .ChannelPoint }}</td></tr>
|
||||||
|
<tr><td>Tag:</td><td>{{ .Tag }}</td></tr>
|
||||||
|
</table>
|
||||||
|
`
|
||||||
|
t, err := template.New("OpenChannelEmail").Parse(tpl)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tagStr := ""
|
||||||
|
if tag != nil {
|
||||||
|
tagStr = *tag
|
||||||
|
}
|
||||||
|
if err := t.Execute(&html, map[string]string{
|
||||||
|
"PaymentHash": hex.EncodeToString(paymentHash),
|
||||||
|
"IncomingAmountMsat": strconv.FormatUint(uint64(incomingAmountMsat), 10),
|
||||||
|
"Destination": hex.EncodeToString(destination),
|
||||||
|
"Capacity": strconv.FormatUint(uint64(capacity), 10),
|
||||||
|
"ChannelPoint": channelPoint,
|
||||||
|
"Tag": tagStr,
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = sendEmail(
|
||||||
|
os.Getenv("OPENCHANNEL_NOTIFICATION_TO"),
|
||||||
|
os.Getenv("OPENCHANNEL_NOTIFICATION_CC"),
|
||||||
|
os.Getenv("OPENCHANNEL_NOTIFICATION_FROM"),
|
||||||
|
html.String(),
|
||||||
|
"Open Channel - Interceptor",
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Error sending open channel email: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
7
interceptor/htlc_interceptor.go
Normal file
7
interceptor/htlc_interceptor.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package interceptor
|
||||||
|
|
||||||
|
type HtlcInterceptor interface {
|
||||||
|
Start() error
|
||||||
|
Stop() error
|
||||||
|
WaitStarted()
|
||||||
|
}
|
||||||
427
interceptor/intercept.go
Normal file
427
interceptor/intercept.go
Normal file
@@ -0,0 +1,427 @@
|
|||||||
|
package interceptor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"math/big"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/basetypes"
|
||||||
|
"github.com/breez/lspd/chain"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
"github.com/breez/lspd/lightning"
|
||||||
|
"github.com/breez/lspd/notifications"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"golang.org/x/sync/singleflight"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InterceptAction int
|
||||||
|
|
||||||
|
const (
|
||||||
|
INTERCEPT_RESUME InterceptAction = 0
|
||||||
|
INTERCEPT_RESUME_WITH_ONION InterceptAction = 1
|
||||||
|
INTERCEPT_FAIL_HTLC_WITH_CODE InterceptAction = 2
|
||||||
|
)
|
||||||
|
|
||||||
|
type InterceptFailureCode uint16
|
||||||
|
|
||||||
|
var (
|
||||||
|
FAILURE_TEMPORARY_CHANNEL_FAILURE InterceptFailureCode = 0x1007
|
||||||
|
FAILURE_TEMPORARY_NODE_FAILURE InterceptFailureCode = 0x2002
|
||||||
|
FAILURE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS InterceptFailureCode = 0x400F
|
||||||
|
)
|
||||||
|
|
||||||
|
type InterceptResult struct {
|
||||||
|
Action InterceptAction
|
||||||
|
FailureCode InterceptFailureCode
|
||||||
|
Destination []byte
|
||||||
|
AmountMsat uint64
|
||||||
|
TotalAmountMsat uint64
|
||||||
|
ChannelPoint *wire.OutPoint
|
||||||
|
ChannelId uint64
|
||||||
|
PaymentSecret []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type Interceptor struct {
|
||||||
|
client lightning.Client
|
||||||
|
config *config.NodeConfig
|
||||||
|
store InterceptStore
|
||||||
|
feeEstimator chain.FeeEstimator
|
||||||
|
feeStrategy chain.FeeStrategy
|
||||||
|
payHashGroup singleflight.Group
|
||||||
|
notificationService *notifications.NotificationService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewInterceptor(
|
||||||
|
client lightning.Client,
|
||||||
|
config *config.NodeConfig,
|
||||||
|
store InterceptStore,
|
||||||
|
feeEstimator chain.FeeEstimator,
|
||||||
|
feeStrategy chain.FeeStrategy,
|
||||||
|
notificationService *notifications.NotificationService,
|
||||||
|
) *Interceptor {
|
||||||
|
return &Interceptor{
|
||||||
|
client: client,
|
||||||
|
config: config,
|
||||||
|
store: store,
|
||||||
|
feeEstimator: feeEstimator,
|
||||||
|
feeStrategy: feeStrategy,
|
||||||
|
notificationService: notificationService,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Interceptor) Intercept(scid *basetypes.ShortChannelID, reqPaymentHash []byte, reqOutgoingAmountMsat uint64, reqOutgoingExpiry uint32, reqIncomingExpiry uint32) InterceptResult {
|
||||||
|
reqPaymentHashStr := hex.EncodeToString(reqPaymentHash)
|
||||||
|
resp, _, _ := i.payHashGroup.Do(reqPaymentHashStr, func() (interface{}, error) {
|
||||||
|
token, params, paymentHash, paymentSecret, destination, incomingAmountMsat, outgoingAmountMsat, channelPoint, tag, err := i.store.PaymentInfo(reqPaymentHash)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("paymentInfo(%x) error: %v", reqPaymentHash, err)
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
|
||||||
|
FailureCode: FAILURE_TEMPORARY_NODE_FAILURE,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
isRegistered := paymentSecret != nil
|
||||||
|
// Sanity check. If the payment is registered, the destination is always set.
|
||||||
|
if isRegistered && (destination == nil || len(destination) != 33) {
|
||||||
|
log.Printf("ERROR: Payment was registered without destination. paymentHash: %s", reqPaymentHashStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
isProbe := isRegistered && !bytes.Equal(paymentHash, reqPaymentHash)
|
||||||
|
nextHop, _ := i.client.GetPeerId(scid)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("GetPeerId(%s) error: %v", scid.ToString(), err)
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_RESUME,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the payment was registered, but the next hop is not the destination
|
||||||
|
// that means we are not the last hop of the payment, so we'll just forward.
|
||||||
|
if isRegistered && nextHop != nil && !bytes.Equal(nextHop, destination) {
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_RESUME,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// nextHop is set if the sender's scid corresponds to a known channel.
|
||||||
|
// destination is set if the payment was registered for a channel open.
|
||||||
|
// The 'actual' next hop will be either of those.
|
||||||
|
if nextHop == nil {
|
||||||
|
|
||||||
|
// If the next hop cannot be deduced from the scid, and the payment
|
||||||
|
// is not registered, there's nothing left to be done. Just continue.
|
||||||
|
if !isRegistered {
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_RESUME,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The payment was registered, so the next hop is the registered
|
||||||
|
// destination
|
||||||
|
nextHop = destination
|
||||||
|
}
|
||||||
|
|
||||||
|
isConnected, err := i.client.IsConnected(nextHop)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("IsConnected(%x) error: %v", nextHop, err)
|
||||||
|
return &InterceptResult{
|
||||||
|
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
|
||||||
|
FailureCode: FAILURE_TEMPORARY_CHANNEL_FAILURE,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if isProbe {
|
||||||
|
// If this is a known probe, we'll quit early for non-connected clients.
|
||||||
|
if !isConnected {
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_RESUME,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// If it's a probe, they are connected, but need a channel, we'll return
|
||||||
|
// INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS. This is an out-of-spec
|
||||||
|
// error code, because it shouldn't be returned by an intermediate
|
||||||
|
// node. But senders implementnig the probing-01: prefix should
|
||||||
|
// know that the actual payment would probably succeed.
|
||||||
|
if channelPoint == nil {
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
|
||||||
|
FailureCode: FAILURE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !isConnected {
|
||||||
|
// Make sure the client is connected by potentially notifying them to come online.
|
||||||
|
notifyResult := i.notify(reqPaymentHashStr, nextHop, isRegistered)
|
||||||
|
if notifyResult != nil {
|
||||||
|
return *notifyResult, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The peer is online, we can resume the htlc if it's not a channel open.
|
||||||
|
if !isRegistered {
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_RESUME,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// The first htlc of a MPP will open the channel.
|
||||||
|
if channelPoint == nil {
|
||||||
|
// TODO: When opening_fee_params is enforced, turn this check in a temporary channel failure.
|
||||||
|
if params == nil {
|
||||||
|
log.Printf("DEPRECATED: Intercepted htlc with deprecated fee mechanism. Using default fees. payment hash: %s", reqPaymentHashStr)
|
||||||
|
params = &OpeningFeeParams{
|
||||||
|
MinMsat: uint64(i.config.ChannelMinimumFeeMsat),
|
||||||
|
Proportional: uint32(i.config.ChannelFeePermyriad * 100),
|
||||||
|
ValidUntil: time.Now().UTC().Add(time.Duration(time.Hour * 24)).Format(basetypes.TIME_FORMAT),
|
||||||
|
MaxIdleTime: uint32(i.config.MaxInactiveDuration / 600),
|
||||||
|
MaxClientToSelfDelay: uint32(10000),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the cltv delta is enough.
|
||||||
|
if int64(reqIncomingExpiry)-int64(reqOutgoingExpiry) < int64(i.config.TimeLockDelta) {
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
|
||||||
|
FailureCode: FAILURE_TEMPORARY_CHANNEL_FAILURE,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
validUntil, err := time.Parse(basetypes.TIME_FORMAT, params.ValidUntil)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("time.Parse(%s, %s) failed. Failing channel open: %v", basetypes.TIME_FORMAT, params.ValidUntil, err)
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
|
||||||
|
FailureCode: FAILURE_TEMPORARY_CHANNEL_FAILURE,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Make sure the opening_fee_params are not expired.
|
||||||
|
// If they are expired, but the current chain fee is fine, open channel anyway.
|
||||||
|
if time.Now().UTC().After(validUntil) {
|
||||||
|
if !i.isCurrentChainFeeCheaper(token, params) {
|
||||||
|
log.Printf("Intercepted expired payment registration. Failing payment. payment hash: %x, valid until: %s", paymentHash, params.ValidUntil)
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
|
||||||
|
FailureCode: FAILURE_TEMPORARY_CHANNEL_FAILURE,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Intercepted expired payment registration. Opening channel anyway, because it's cheaper at the current rate. paymenthash: %s, params: %+v", reqPaymentHashStr, params)
|
||||||
|
}
|
||||||
|
|
||||||
|
channelPoint, err = i.openChannel(reqPaymentHash, destination, incomingAmountMsat, tag)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("openChannel(%x, %v) err: %v", destination, incomingAmountMsat, err)
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
|
||||||
|
FailureCode: FAILURE_TEMPORARY_CHANNEL_FAILURE,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var bigProd, bigAmt big.Int
|
||||||
|
amt := (bigAmt.Div(bigProd.Mul(big.NewInt(outgoingAmountMsat), big.NewInt(int64(reqOutgoingAmountMsat))), big.NewInt(incomingAmountMsat))).Int64()
|
||||||
|
|
||||||
|
deadline := time.Now().Add(60 * time.Second)
|
||||||
|
|
||||||
|
for {
|
||||||
|
chanResult, _ := i.client.GetChannel(destination, *channelPoint)
|
||||||
|
if chanResult != nil {
|
||||||
|
log.Printf("channel opened successfully alias: %v, confirmed: %v", chanResult.InitialChannelID.ToString(), chanResult.ConfirmedChannelID.ToString())
|
||||||
|
|
||||||
|
err := i.store.InsertChannel(
|
||||||
|
uint64(chanResult.InitialChannelID),
|
||||||
|
uint64(chanResult.ConfirmedChannelID),
|
||||||
|
channelPoint.String(),
|
||||||
|
destination,
|
||||||
|
time.Now(),
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("insertChannel error: %v", err)
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
|
||||||
|
FailureCode: FAILURE_TEMPORARY_CHANNEL_FAILURE,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
channelID := uint64(chanResult.ConfirmedChannelID)
|
||||||
|
if channelID == 0 {
|
||||||
|
channelID = uint64(chanResult.InitialChannelID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_RESUME_WITH_ONION,
|
||||||
|
Destination: destination,
|
||||||
|
ChannelPoint: channelPoint,
|
||||||
|
ChannelId: channelID,
|
||||||
|
PaymentSecret: paymentSecret,
|
||||||
|
AmountMsat: uint64(amt),
|
||||||
|
TotalAmountMsat: uint64(outgoingAmountMsat),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("waiting for channel to get opened.... %v\n", destination)
|
||||||
|
if time.Now().After(deadline) {
|
||||||
|
log.Printf("Stop retrying getChannel(%v, %v)", destination, channelPoint.String())
|
||||||
|
break
|
||||||
|
}
|
||||||
|
<-time.After(1 * time.Second)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Error: Channel failed to open... timed out. ")
|
||||||
|
return InterceptResult{
|
||||||
|
Action: INTERCEPT_FAIL_HTLC_WITH_CODE,
|
||||||
|
FailureCode: FAILURE_TEMPORARY_CHANNEL_FAILURE,
|
||||||
|
}, nil
|
||||||
|
})
|
||||||
|
|
||||||
|
return resp.(InterceptResult)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Interceptor) notify(reqPaymentHashStr string, nextHop []byte, isRegistered bool) *InterceptResult {
|
||||||
|
// If not connected, send a notification to the registered
|
||||||
|
// notification service for this client if available.
|
||||||
|
notified, err := i.notificationService.Notify(
|
||||||
|
hex.EncodeToString(nextHop),
|
||||||
|
reqPaymentHashStr,
|
||||||
|
)
|
||||||
|
|
||||||
|
// If this errors or the client is not notified, the client
|
||||||
|
// is offline or unknown. We'll resume the HTLC (which will
|
||||||
|
// result in UNKOWN_NEXT_PEER)
|
||||||
|
if err != nil || !notified {
|
||||||
|
return &InterceptResult{
|
||||||
|
Action: INTERCEPT_RESUME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Notified %x of pending htlc", nextHop)
|
||||||
|
d, err := time.ParseDuration(i.config.NotificationTimeout)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("WARN: No NotificationTimeout set. Using default 1m")
|
||||||
|
d = time.Minute
|
||||||
|
}
|
||||||
|
timeout := time.Now().Add(d)
|
||||||
|
|
||||||
|
// Wait for a while to allow the client to come online.
|
||||||
|
err = i.client.WaitOnline(nextHop, timeout)
|
||||||
|
|
||||||
|
// If there's an error waiting, resume the htlc. It will
|
||||||
|
// probably fail with UNKNOWN_NEXT_PEER.
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"waiting for peer %x to come online failed with %v",
|
||||||
|
nextHop,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
return &InterceptResult{
|
||||||
|
Action: INTERCEPT_RESUME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Peer %x is back online. Continue htlc.", nextHop)
|
||||||
|
// At this point we know a few things.
|
||||||
|
// - This is either a channel partner or a registered payment
|
||||||
|
// - they were offline
|
||||||
|
// - They got notified about the htlc
|
||||||
|
// - They came back online
|
||||||
|
// So if this payment was not registered, this is a channel
|
||||||
|
// partner and we have to wait for the channel to become active
|
||||||
|
// before we can forward.
|
||||||
|
if !isRegistered {
|
||||||
|
err = i.client.WaitChannelActive(nextHop, timeout)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"waiting for channnel with %x to become active failed with %v",
|
||||||
|
nextHop,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
return &InterceptResult{
|
||||||
|
Action: INTERCEPT_RESUME,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Interceptor) isCurrentChainFeeCheaper(token string, params *OpeningFeeParams) bool {
|
||||||
|
settings, err := i.store.GetFeeParamsSettings(token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to get fee params settings: %v", err)
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, setting := range settings {
|
||||||
|
if setting.Params.MinMsat <= params.MinMsat {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Interceptor) openChannel(paymentHash, destination []byte, incomingAmountMsat int64, tag *string) (*wire.OutPoint, error) {
|
||||||
|
capacity := incomingAmountMsat/1000 + i.config.AdditionalChannelCapacity
|
||||||
|
if capacity == i.config.PublicChannelAmount {
|
||||||
|
capacity++
|
||||||
|
}
|
||||||
|
|
||||||
|
var targetConf *uint32
|
||||||
|
confStr := "<nil>"
|
||||||
|
var feeEstimation *float64
|
||||||
|
feeStr := "<nil>"
|
||||||
|
if i.feeEstimator != nil {
|
||||||
|
fee, err := i.feeEstimator.EstimateFeeRate(
|
||||||
|
context.Background(),
|
||||||
|
i.feeStrategy,
|
||||||
|
)
|
||||||
|
if err == nil {
|
||||||
|
feeEstimation = &fee.SatPerVByte
|
||||||
|
feeStr = fmt.Sprintf("%.5f", *feeEstimation)
|
||||||
|
} else {
|
||||||
|
log.Printf("Error estimating chain fee, fallback to target conf: %v", err)
|
||||||
|
targetConf = &i.config.TargetConf
|
||||||
|
confStr = fmt.Sprintf("%v", *targetConf)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"Opening zero conf channel. Destination: %x, capacity: %v, fee: %s, targetConf: %s",
|
||||||
|
destination,
|
||||||
|
capacity,
|
||||||
|
feeStr,
|
||||||
|
confStr,
|
||||||
|
)
|
||||||
|
channelPoint, err := i.client.OpenChannel(&lightning.OpenChannelRequest{
|
||||||
|
Destination: destination,
|
||||||
|
CapacitySat: uint64(capacity),
|
||||||
|
MinConfs: i.config.MinConfs,
|
||||||
|
IsPrivate: true,
|
||||||
|
IsZeroConf: true,
|
||||||
|
FeeSatPerVByte: feeEstimation,
|
||||||
|
TargetConf: targetConf,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("client.OpenChannelSync(%x, %v) error: %v", destination, capacity, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
sendOpenChannelEmailNotification(
|
||||||
|
paymentHash,
|
||||||
|
incomingAmountMsat,
|
||||||
|
destination,
|
||||||
|
capacity,
|
||||||
|
channelPoint.String(),
|
||||||
|
tag,
|
||||||
|
)
|
||||||
|
err = i.store.SetFundingTx(paymentHash, channelPoint)
|
||||||
|
return channelPoint, err
|
||||||
|
}
|
||||||
28
interceptor/store.go
Normal file
28
interceptor/store.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package interceptor
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpeningFeeParamsSetting struct {
|
||||||
|
Validity time.Duration
|
||||||
|
Params *OpeningFeeParams
|
||||||
|
}
|
||||||
|
type OpeningFeeParams struct {
|
||||||
|
MinMsat uint64 `json:"min_msat,string"`
|
||||||
|
Proportional uint32 `json:"proportional"`
|
||||||
|
ValidUntil string `json:"valid_until"`
|
||||||
|
MaxIdleTime uint32 `json:"max_idle_time"`
|
||||||
|
MaxClientToSelfDelay uint32 `json:"max_client_to_self_delay"`
|
||||||
|
Promise string `json:"promise"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type InterceptStore interface {
|
||||||
|
PaymentInfo(htlcPaymentHash []byte) (string, *OpeningFeeParams, []byte, []byte, []byte, int64, int64, *wire.OutPoint, *string, error)
|
||||||
|
SetFundingTx(paymentHash []byte, channelPoint *wire.OutPoint) error
|
||||||
|
RegisterPayment(token string, params *OpeningFeeParams, destination, paymentHash, paymentSecret []byte, incomingAmountMsat, outgoingAmountMsat int64, tag string) error
|
||||||
|
InsertChannel(initialChanID, confirmedChanId uint64, channelPoint string, nodeID []byte, lastUpdate time.Time) error
|
||||||
|
GetFeeParamsSettings(token string) ([]*OpeningFeeParamsSetting, error)
|
||||||
|
}
|
||||||
69
itest/bob_offline_test.go
Normal file
69
itest/bob_offline_test.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testFailureBobOffline(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
})
|
||||||
|
channelId := alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(2100000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: p.lsp,
|
||||||
|
})
|
||||||
|
|
||||||
|
p.BreezClient().SetHtlcAcceptor(innerAmountMsat)
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// Kill the mobile client
|
||||||
|
log.Printf("Stopping breez client")
|
||||||
|
p.BreezClient().Stop()
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat)
|
||||||
|
_, err := alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
assert.Contains(p.t, err.Error(), "WIRE_UNKNOWN_NEXT_PEER")
|
||||||
|
|
||||||
|
log.Printf("Starting breez client again")
|
||||||
|
p.BreezClient().Start()
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Alice paying again")
|
||||||
|
_, err = alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
assert.Nil(p.t, err)
|
||||||
|
}
|
||||||
122
itest/breez_client.go
Normal file
122
itest/breez_client.go
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/zpay32"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BreezClient interface {
|
||||||
|
Name() string
|
||||||
|
Harness() *lntest.TestHarness
|
||||||
|
Node() lntest.LightningNode
|
||||||
|
Start()
|
||||||
|
Stop() error
|
||||||
|
SetHtlcAcceptor(totalMsat uint64)
|
||||||
|
ResetHtlcAcceptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
type generateInvoicesRequest struct {
|
||||||
|
innerAmountMsat uint64
|
||||||
|
outerAmountMsat uint64
|
||||||
|
description string
|
||||||
|
lsp LspNode
|
||||||
|
}
|
||||||
|
|
||||||
|
type invoice struct {
|
||||||
|
bolt11 string
|
||||||
|
paymentHash []byte
|
||||||
|
paymentSecret []byte
|
||||||
|
paymentPreimage []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func GenerateInvoices(n BreezClient, req generateInvoicesRequest) (invoice, invoice) {
|
||||||
|
preimage, err := GenerateRandomBytes(32)
|
||||||
|
lntest.CheckError(n.Harness().T, err)
|
||||||
|
|
||||||
|
innerInvoice := n.Node().CreateBolt11Invoice(&lntest.CreateInvoiceOptions{
|
||||||
|
AmountMsat: req.innerAmountMsat,
|
||||||
|
Description: &req.description,
|
||||||
|
Preimage: &preimage,
|
||||||
|
})
|
||||||
|
outerInvoice := AddHopHint(n, innerInvoice.Bolt11, req.lsp, lntest.ShortChannelID{
|
||||||
|
BlockHeight: 1,
|
||||||
|
TxIndex: 0,
|
||||||
|
OutputIndex: 0,
|
||||||
|
}, &req.outerAmountMsat)
|
||||||
|
|
||||||
|
inner := invoice{
|
||||||
|
bolt11: innerInvoice.Bolt11,
|
||||||
|
paymentHash: innerInvoice.PaymentHash,
|
||||||
|
paymentSecret: innerInvoice.PaymentSecret,
|
||||||
|
paymentPreimage: preimage,
|
||||||
|
}
|
||||||
|
outer := invoice{
|
||||||
|
bolt11: outerInvoice,
|
||||||
|
paymentHash: innerInvoice.PaymentHash[:],
|
||||||
|
paymentSecret: innerInvoice.PaymentSecret,
|
||||||
|
paymentPreimage: preimage,
|
||||||
|
}
|
||||||
|
|
||||||
|
return inner, outer
|
||||||
|
}
|
||||||
|
|
||||||
|
func ContainsHopHint(t *testing.T, invoice string) bool {
|
||||||
|
rawInvoice, err := zpay32.Decode(invoice, &chaincfg.RegressionNetParams)
|
||||||
|
lntest.CheckError(t, err)
|
||||||
|
|
||||||
|
return len(rawInvoice.RouteHints) > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func AddHopHint(n BreezClient, invoice string, lsp LspNode, chanid lntest.ShortChannelID, amountMsat *uint64) string {
|
||||||
|
rawInvoice, err := zpay32.Decode(invoice, &chaincfg.RegressionNetParams)
|
||||||
|
lntest.CheckError(n.Harness().T, err)
|
||||||
|
|
||||||
|
if amountMsat != nil {
|
||||||
|
milliSat := lnwire.MilliSatoshi(*amountMsat)
|
||||||
|
rawInvoice.MilliSat = &milliSat
|
||||||
|
}
|
||||||
|
|
||||||
|
lspNodeId, err := btcec.ParsePubKey(lsp.NodeId())
|
||||||
|
lntest.CheckError(n.Harness().T, err)
|
||||||
|
rawInvoice.RouteHints = append(rawInvoice.RouteHints, []zpay32.HopHint{
|
||||||
|
{
|
||||||
|
NodeID: lspNodeId,
|
||||||
|
ChannelID: chanid.ToUint64(),
|
||||||
|
FeeBaseMSat: lspBaseFeeMsat,
|
||||||
|
FeeProportionalMillionths: lspFeeRatePpm,
|
||||||
|
CLTVExpiryDelta: lspCltvDelta,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf(
|
||||||
|
"Encoding invoice. privkey: '%x', invoice: '%+v', original bolt11: '%s'",
|
||||||
|
n.Node().PrivateKey().Serialize(),
|
||||||
|
rawInvoice,
|
||||||
|
invoice,
|
||||||
|
)
|
||||||
|
newInvoice, err := rawInvoice.Encode(zpay32.MessageSigner{
|
||||||
|
SignCompact: func(msg []byte) ([]byte, error) {
|
||||||
|
hash := sha256.Sum256(msg)
|
||||||
|
sig, err := ecdsa.SignCompact(n.Node().PrivateKey(), hash[:], true)
|
||||||
|
log.Printf(
|
||||||
|
"sign outer invoice. msg: '%x', hash: '%x', sig: '%x', err: %v",
|
||||||
|
msg,
|
||||||
|
hash,
|
||||||
|
sig,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
return sig, err
|
||||||
|
},
|
||||||
|
})
|
||||||
|
lntest.CheckError(n.Harness().T, err)
|
||||||
|
|
||||||
|
return newInvoice
|
||||||
|
}
|
||||||
335
itest/cln_breez_client.go
Normal file
335
itest/cln_breez_client.go
Normal file
@@ -0,0 +1,335 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/breez/lspd/cln_plugin/proto"
|
||||||
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/record"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
"google.golang.org/grpc/keepalive"
|
||||||
|
)
|
||||||
|
|
||||||
|
var pluginContent string = `#!/usr/bin/env python3
|
||||||
|
"""Use the openchannel hook to selectively opt-into zeroconf
|
||||||
|
"""
|
||||||
|
|
||||||
|
from pyln.client import Plugin
|
||||||
|
|
||||||
|
plugin = Plugin()
|
||||||
|
|
||||||
|
|
||||||
|
@plugin.hook('openchannel')
|
||||||
|
def on_openchannel(openchannel, plugin, **kwargs):
|
||||||
|
plugin.log(repr(openchannel))
|
||||||
|
mindepth = int(0)
|
||||||
|
|
||||||
|
plugin.log(f"This peer is in the zeroconf allowlist, setting mindepth={mindepth}")
|
||||||
|
return {'result': 'continue', 'mindepth': mindepth}
|
||||||
|
|
||||||
|
plugin.run()
|
||||||
|
`
|
||||||
|
|
||||||
|
var pluginStartupContent string = `python3 -m venv %s > /dev/null 2>&1
|
||||||
|
source %s > /dev/null 2>&1
|
||||||
|
pip install pyln-client > /dev/null 2>&1
|
||||||
|
python %s
|
||||||
|
`
|
||||||
|
|
||||||
|
type clnBreezClient struct {
|
||||||
|
name string
|
||||||
|
scriptDir string
|
||||||
|
pluginFilePath string
|
||||||
|
htlcAcceptorAddress string
|
||||||
|
htlcAcceptor func(*proto.HtlcAccepted) *proto.HtlcResolution
|
||||||
|
htlcAcceptorCancel context.CancelFunc
|
||||||
|
harness *lntest.TestHarness
|
||||||
|
isInitialized bool
|
||||||
|
node *lntest.ClnNode
|
||||||
|
mtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newClnBreezClient(h *lntest.TestHarness, m *lntest.Miner, name string) BreezClient {
|
||||||
|
scriptDir := h.GetDirectory(name)
|
||||||
|
pluginFilePath := filepath.Join(scriptDir, "start_zero_conf_plugin.sh")
|
||||||
|
htlcAcceptorPort, err := lntest.GetPort()
|
||||||
|
if err != nil {
|
||||||
|
h.T.Fatalf("Could not get port for htlc acceptor plugin: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
htlcAcceptorAddress := fmt.Sprintf("127.0.0.1:%v", htlcAcceptorPort)
|
||||||
|
node := lntest.NewClnNode(
|
||||||
|
h,
|
||||||
|
m,
|
||||||
|
name,
|
||||||
|
fmt.Sprintf("--plugin=%s", pluginFilePath),
|
||||||
|
fmt.Sprintf("--plugin=%s", *clnPluginExec),
|
||||||
|
fmt.Sprintf("--lsp-listen=%s", htlcAcceptorAddress),
|
||||||
|
// NOTE: max-concurrent-htlcs is 30 on mainnet by default. In cln V22.11
|
||||||
|
// there is a check for 'all dust' commitment transactions. The max
|
||||||
|
// concurrent HTLCs of both sides of the channel * dust limit must be
|
||||||
|
// lower than the channel capacity in order to open a zero conf zero
|
||||||
|
// reserve channel. Relevant code:
|
||||||
|
// https://github.com/ElementsProject/lightning/blob/774d16a72e125e4ae4e312b9e3307261983bec0e/openingd/openingd.c#L481-L520
|
||||||
|
"--max-concurrent-htlcs=30",
|
||||||
|
)
|
||||||
|
|
||||||
|
return &clnBreezClient{
|
||||||
|
name: name,
|
||||||
|
harness: h,
|
||||||
|
node: node,
|
||||||
|
scriptDir: scriptDir,
|
||||||
|
pluginFilePath: pluginFilePath,
|
||||||
|
htlcAcceptorAddress: htlcAcceptorAddress,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clnBreezClient) Name() string {
|
||||||
|
return c.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clnBreezClient) Harness() *lntest.TestHarness {
|
||||||
|
return c.harness
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clnBreezClient) Node() lntest.LightningNode {
|
||||||
|
return c.node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clnBreezClient) Start() {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
if !c.isInitialized {
|
||||||
|
c.initialize()
|
||||||
|
c.isInitialized = true
|
||||||
|
}
|
||||||
|
|
||||||
|
c.node.Start()
|
||||||
|
c.startHtlcAcceptor()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clnBreezClient) ResetHtlcAcceptor() {
|
||||||
|
c.htlcAcceptor = nil
|
||||||
|
}
|
||||||
|
func (c *clnBreezClient) SetHtlcAcceptor(totalMsat uint64) {
|
||||||
|
c.htlcAcceptor = func(htlc *proto.HtlcAccepted) *proto.HtlcResolution {
|
||||||
|
origPayload, err := hex.DecodeString(htlc.Onion.Payload)
|
||||||
|
if err != nil {
|
||||||
|
c.harness.T.Fatalf("failed to hex decode onion payload %s: %v", htlc.Onion.Payload, err)
|
||||||
|
}
|
||||||
|
bufReader := bytes.NewBuffer(origPayload)
|
||||||
|
var b [8]byte
|
||||||
|
varInt, err := sphinx.ReadVarInt(bufReader, &b)
|
||||||
|
if err != nil {
|
||||||
|
c.harness.T.Fatalf("Failed to read payload: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
innerPayload := make([]byte, varInt)
|
||||||
|
if _, err := io.ReadFull(bufReader, innerPayload[:]); err != nil {
|
||||||
|
c.harness.T.Fatalf("failed to decode payload %x: %v", innerPayload[:], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
s, _ := tlv.NewStream()
|
||||||
|
tlvMap, err := s.DecodeWithParsedTypes(bytes.NewReader(innerPayload))
|
||||||
|
if err != nil {
|
||||||
|
c.harness.T.Fatalf("DecodeWithParsedTypes failed for %x: %v", innerPayload[:], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
amt := record.NewAmtToFwdRecord(&htlc.Htlc.AmountMsat)
|
||||||
|
amtbuf := bytes.NewBuffer([]byte{})
|
||||||
|
if err := amt.Encode(amtbuf); err != nil {
|
||||||
|
c.harness.T.Fatalf("failed to encode AmtToFwd %x: %v", innerPayload[:], err)
|
||||||
|
}
|
||||||
|
|
||||||
|
uTlvMap := make(map[uint64][]byte)
|
||||||
|
for t, b := range tlvMap {
|
||||||
|
if t == record.AmtOnionType {
|
||||||
|
uTlvMap[uint64(t)] = amtbuf.Bytes()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if t == record.MPPOnionType {
|
||||||
|
addr := [32]byte{}
|
||||||
|
copy(addr[:], b[:32])
|
||||||
|
mppbuf := bytes.NewBuffer([]byte{})
|
||||||
|
mpp := record.NewMPP(
|
||||||
|
lnwire.MilliSatoshi(totalMsat),
|
||||||
|
addr,
|
||||||
|
)
|
||||||
|
record := mpp.Record()
|
||||||
|
record.Encode(mppbuf)
|
||||||
|
uTlvMap[uint64(t)] = mppbuf.Bytes()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
uTlvMap[uint64(t)] = b
|
||||||
|
}
|
||||||
|
tlvRecords := tlv.MapToRecords(uTlvMap)
|
||||||
|
s, err = tlv.NewStream(tlvRecords...)
|
||||||
|
if err != nil {
|
||||||
|
c.harness.T.Fatalf("tlv.NewStream(%+v) error: %v", tlvRecords, err)
|
||||||
|
}
|
||||||
|
var newPayloadBuf bytes.Buffer
|
||||||
|
err = s.Encode(&newPayloadBuf)
|
||||||
|
if err != nil {
|
||||||
|
c.harness.T.Fatalf("encode error: %v", err)
|
||||||
|
}
|
||||||
|
payload := hex.EncodeToString(newPayloadBuf.Bytes())
|
||||||
|
|
||||||
|
return &proto.HtlcResolution{
|
||||||
|
Correlationid: htlc.Correlationid,
|
||||||
|
Outcome: &proto.HtlcResolution_Continue{
|
||||||
|
Continue: &proto.HtlcContinue{
|
||||||
|
Payload: &payload,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clnBreezClient) startHtlcAcceptor() {
|
||||||
|
ctx, cancel := context.WithCancel(c.harness.Ctx)
|
||||||
|
c.htlcAcceptorCancel = cancel
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(time.Second):
|
||||||
|
}
|
||||||
|
|
||||||
|
conn, err := grpc.DialContext(
|
||||||
|
ctx,
|
||||||
|
c.htlcAcceptorAddress,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithKeepaliveParams(keepalive.ClientParameters{
|
||||||
|
Time: time.Duration(10) * time.Second,
|
||||||
|
Timeout: time.Duration(10) * time.Second,
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%s: Dial htlc acceptor error: %v", c.name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
client := proto.NewClnPluginClient(conn)
|
||||||
|
acceptor, err := client.HtlcStream(ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%s: client.HtlcStream() error: %v", c.name, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
htlc, err := acceptor.Recv()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%s: acceptor.Recv() error: %v", c.name, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
f := c.htlcAcceptor
|
||||||
|
var resp *proto.HtlcResolution
|
||||||
|
if f != nil {
|
||||||
|
resp = f(htlc)
|
||||||
|
}
|
||||||
|
if resp == nil {
|
||||||
|
resp = &proto.HtlcResolution{
|
||||||
|
Correlationid: htlc.Correlationid,
|
||||||
|
Outcome: &proto.HtlcResolution_Continue{
|
||||||
|
Continue: &proto.HtlcContinue{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = acceptor.Send(resp)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("%s: acceptor.Send() error: %v", c.name, err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clnBreezClient) initialize() error {
|
||||||
|
var cleanups []*lntest.Cleanup
|
||||||
|
|
||||||
|
pythonFilePath := filepath.Join(c.scriptDir, "zero_conf_plugin.py")
|
||||||
|
pythonFile, err := os.OpenFile(pythonFilePath, os.O_CREATE|os.O_WRONLY, 0755)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create python file '%s': %v", pythonFilePath, err)
|
||||||
|
}
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: python file", c.name),
|
||||||
|
Fn: pythonFile.Close,
|
||||||
|
})
|
||||||
|
|
||||||
|
pythonWriter := bufio.NewWriter(pythonFile)
|
||||||
|
_, err = pythonWriter.WriteString(pluginContent)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return fmt.Errorf("failed to write content to python file '%s': %v", pythonFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pythonWriter.Flush()
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return fmt.Errorf("failed to flush python file '%s': %v", pythonFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pluginFile, err := os.OpenFile(c.pluginFilePath, os.O_CREATE|os.O_WRONLY, 0755)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return fmt.Errorf("failed to create plugin file '%s': %v", c.pluginFilePath, err)
|
||||||
|
}
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: python file", c.name),
|
||||||
|
Fn: pluginFile.Close,
|
||||||
|
})
|
||||||
|
|
||||||
|
pluginWriter := bufio.NewWriter(pluginFile)
|
||||||
|
venvDir := filepath.Join(c.scriptDir, "venv")
|
||||||
|
activatePath := filepath.Join(venvDir, "bin", "activate")
|
||||||
|
_, err = pluginWriter.WriteString(fmt.Sprintf(pluginStartupContent, venvDir, activatePath, pythonFilePath))
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return fmt.Errorf("failed to write content to plugin file '%s': %v", c.pluginFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = pluginWriter.Flush()
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return fmt.Errorf("failed to flush plugin file '%s': %v", c.pluginFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *clnBreezClient) Stop() error {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
if c.htlcAcceptorCancel != nil {
|
||||||
|
c.htlcAcceptorCancel()
|
||||||
|
c.htlcAcceptorCancel = nil
|
||||||
|
}
|
||||||
|
return c.node.Stop()
|
||||||
|
}
|
||||||
228
itest/cln_lspd_node.go
Normal file
228
itest/cln_lspd_node.go
Normal file
@@ -0,0 +1,228 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
"github.com/breez/lspd/notifications"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
ecies "github.com/ecies/go/v2"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
var clnPluginExec = flag.String(
|
||||||
|
"clnpluginexec", "", "full path to cln plugin wrapper binary",
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClnLspNode struct {
|
||||||
|
harness *lntest.TestHarness
|
||||||
|
lightningNode *lntest.ClnNode
|
||||||
|
lspBase *lspBase
|
||||||
|
logFilePath string
|
||||||
|
runtime *clnLspNodeRuntime
|
||||||
|
isInitialized bool
|
||||||
|
mtx sync.Mutex
|
||||||
|
pluginBinary string
|
||||||
|
pluginAddress string
|
||||||
|
}
|
||||||
|
|
||||||
|
type clnLspNodeRuntime struct {
|
||||||
|
logFile *os.File
|
||||||
|
cmd *exec.Cmd
|
||||||
|
rpc lspd.ChannelOpenerClient
|
||||||
|
notificationRpc notifications.NotificationsClient
|
||||||
|
cleanups []*lntest.Cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewClnLspdNode(h *lntest.TestHarness, m *lntest.Miner, mem *mempoolApi, name string, nodeConfig *config.NodeConfig) LspNode {
|
||||||
|
scriptDir := h.GetDirectory("lspd")
|
||||||
|
pluginBinary := *clnPluginExec
|
||||||
|
pluginPort, err := lntest.GetPort()
|
||||||
|
if err != nil {
|
||||||
|
h.T.Fatalf("failed to get port for the htlc interceptor plugin.")
|
||||||
|
}
|
||||||
|
pluginAddress := fmt.Sprintf("127.0.0.1:%d", pluginPort)
|
||||||
|
|
||||||
|
args := []string{
|
||||||
|
fmt.Sprintf("--plugin=%s", pluginBinary),
|
||||||
|
fmt.Sprintf("--lsp-listen=%s", pluginAddress),
|
||||||
|
fmt.Sprintf("--fee-base=%d", lspBaseFeeMsat),
|
||||||
|
fmt.Sprintf("--fee-per-satoshi=%d", lspFeeRatePpm),
|
||||||
|
fmt.Sprintf("--cltv-delta=%d", lspCltvDelta),
|
||||||
|
"--max-concurrent-htlcs=30",
|
||||||
|
"--dev-allowdustreserve=true",
|
||||||
|
"--allow-deprecated-apis=true",
|
||||||
|
}
|
||||||
|
lightningNode := lntest.NewClnNode(h, m, name, args...)
|
||||||
|
cln := &config.ClnConfig{
|
||||||
|
PluginAddress: pluginAddress,
|
||||||
|
SocketPath: filepath.Join(lightningNode.SocketDir(), lightningNode.SocketFile()),
|
||||||
|
}
|
||||||
|
lspbase, err := newLspd(h, mem, name, nodeConfig, nil, cln)
|
||||||
|
if err != nil {
|
||||||
|
h.T.Fatalf("failed to initialize lspd")
|
||||||
|
}
|
||||||
|
|
||||||
|
logFilePath := filepath.Join(scriptDir, "lspd.log")
|
||||||
|
h.RegisterLogfile(logFilePath, fmt.Sprintf("lspd-%s", name))
|
||||||
|
|
||||||
|
lspNode := &ClnLspNode{
|
||||||
|
harness: h,
|
||||||
|
lightningNode: lightningNode,
|
||||||
|
logFilePath: logFilePath,
|
||||||
|
lspBase: lspbase,
|
||||||
|
pluginBinary: pluginBinary,
|
||||||
|
pluginAddress: pluginAddress,
|
||||||
|
}
|
||||||
|
|
||||||
|
h.AddStoppable(lspNode)
|
||||||
|
return lspNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnLspNode) Start() {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
var cleanups []*lntest.Cleanup
|
||||||
|
if !c.isInitialized {
|
||||||
|
err := c.lspBase.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
c.harness.T.Fatalf("failed to initialize lsp: %v", err)
|
||||||
|
}
|
||||||
|
c.isInitialized = true
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: lsp base", c.lspBase.name),
|
||||||
|
Fn: c.lspBase.Stop,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lightningNode.Start()
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: lightning node", c.lspBase.name),
|
||||||
|
Fn: c.lightningNode.Stop,
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(c.harness.Ctx, c.lspBase.scriptFilePath)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
logFile, err := os.Create(c.logFilePath)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
c.harness.T.Fatalf("failed create lsp logfile: %v", err)
|
||||||
|
}
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: logfile", c.lspBase.name),
|
||||||
|
Fn: logFile.Close,
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd.Stdout = logFile
|
||||||
|
cmd.Stderr = logFile
|
||||||
|
|
||||||
|
log.Printf("%s: starting lspd %s", c.lspBase.name, c.lspBase.scriptFilePath)
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
c.harness.T.Fatalf("failed to start lspd: %v", err)
|
||||||
|
}
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: cmd", c.lspBase.name),
|
||||||
|
Fn: func() error {
|
||||||
|
proc := cmd.Process
|
||||||
|
if proc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
syscall.Kill(-proc.Pid, syscall.SIGINT)
|
||||||
|
|
||||||
|
log.Printf("About to wait for lspd to exit")
|
||||||
|
status, err := proc.Wait()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("waiting for lspd process error: %v, status: %v", err, status)
|
||||||
|
}
|
||||||
|
err = cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("waiting for lspd cmd error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
conn, err := grpc.Dial(
|
||||||
|
c.lspBase.grpcAddress,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithPerRPCCredentials(&token{token: "hello"}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
c.harness.T.Fatalf("failed to create grpc connection: %v", err)
|
||||||
|
}
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: grpc conn", c.lspBase.name),
|
||||||
|
Fn: conn.Close,
|
||||||
|
})
|
||||||
|
|
||||||
|
client := lspd.NewChannelOpenerClient(conn)
|
||||||
|
notif := notifications.NewNotificationsClient(conn)
|
||||||
|
c.runtime = &clnLspNodeRuntime{
|
||||||
|
logFile: logFile,
|
||||||
|
cmd: cmd,
|
||||||
|
rpc: client,
|
||||||
|
notificationRpc: notif,
|
||||||
|
cleanups: cleanups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnLspNode) Stop() error {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
if c.runtime == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lntest.PerformCleanup(c.runtime.cleanups)
|
||||||
|
c.runtime = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnLspNode) Harness() *lntest.TestHarness {
|
||||||
|
return c.harness
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnLspNode) PublicKey() *btcec.PublicKey {
|
||||||
|
return c.lspBase.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnLspNode) EciesPublicKey() *ecies.PublicKey {
|
||||||
|
return c.lspBase.eciesPubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnLspNode) Rpc() lspd.ChannelOpenerClient {
|
||||||
|
return c.runtime.rpc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ClnLspNode) NotificationsRpc() notifications.NotificationsClient {
|
||||||
|
return c.runtime.notificationRpc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ClnLspNode) NodeId() []byte {
|
||||||
|
return l.lightningNode.NodeId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ClnLspNode) LightningNode() lntest.LightningNode {
|
||||||
|
return l.lightningNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *ClnLspNode) PostgresBackend() *PostgresContainer {
|
||||||
|
return l.lspBase.postgresBackend
|
||||||
|
}
|
||||||
58
itest/cltv_test.go
Normal file
58
itest/cltv_test.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testInvalidCltv(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
})
|
||||||
|
channelId := alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(2100000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: p.lsp,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat)
|
||||||
|
|
||||||
|
// Decrement the delay in the first hop, so the cltv delta will become 143 (too little)
|
||||||
|
route.Hops[0].Delay--
|
||||||
|
_, err := alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
assert.Contains(p.t, err.Error(), "WIRE_TEMPORARY_CHANNEL_FAILURE")
|
||||||
|
}
|
||||||
42
itest/config_test.go
Normal file
42
itest/config_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestConfigParameters(t *testing.T) {
|
||||||
|
deadline, _ := t.Deadline()
|
||||||
|
h := lntest.NewTestHarness(t, deadline)
|
||||||
|
defer h.TearDown()
|
||||||
|
|
||||||
|
m := lntest.NewMiner(h)
|
||||||
|
m.Start()
|
||||||
|
|
||||||
|
mem := NewMempoolApi(h)
|
||||||
|
mem.Start()
|
||||||
|
|
||||||
|
lsp := NewClnLspdNode(h, m, mem, "lsp", nil)
|
||||||
|
lsp.Start()
|
||||||
|
|
||||||
|
log.Printf("Waiting %v to allow lsp server to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
|
||||||
|
info, err := lsp.Rpc().ChannelInformation(
|
||||||
|
h.Ctx,
|
||||||
|
&lspd.ChannelInformationRequest{},
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("failed to get channelinformation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(t, hex.EncodeToString(lsp.LightningNode().NodeId()), info.Pubkey)
|
||||||
|
assert.Equal(t, "lsp", info.Name)
|
||||||
|
}
|
||||||
141
itest/dynamic_fee_test.go
Normal file
141
itest/dynamic_fee_test.go
Normal file
@@ -0,0 +1,141 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testDynamicFeeFlow(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
})
|
||||||
|
channelId := alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
log.Printf("Getting channel information")
|
||||||
|
SetFeeParams(p.lsp, []*FeeParamSetting{
|
||||||
|
{
|
||||||
|
Validity: time.Second * 3600,
|
||||||
|
MinMsat: 3000000,
|
||||||
|
Proportional: 1000,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
||||||
|
info := ChannelInformation(p.lsp)
|
||||||
|
assert.Len(p.t, info.OpeningFeeParamsMenu, 1)
|
||||||
|
params := info.OpeningFeeParamsMenu[0]
|
||||||
|
assert.Equal(p.t, uint64(3000000), params.MinMsat)
|
||||||
|
|
||||||
|
log.Printf("opening_fee_params: %+v", params)
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(4200000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, params)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: p.lsp,
|
||||||
|
})
|
||||||
|
|
||||||
|
p.BreezClient().SetHtlcAcceptor(innerAmountMsat)
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Testing some bad registrations")
|
||||||
|
err := RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
OpeningFeeParams: &lspd.OpeningFeeParams{
|
||||||
|
// modify minmsat
|
||||||
|
MinMsat: params.MinMsat + 1,
|
||||||
|
Proportional: params.Proportional,
|
||||||
|
ValidUntil: params.ValidUntil,
|
||||||
|
MaxIdleTime: params.MaxIdleTime,
|
||||||
|
MaxClientToSelfDelay: params.MaxClientToSelfDelay,
|
||||||
|
Promise: params.Promise,
|
||||||
|
},
|
||||||
|
}, true)
|
||||||
|
assert.Contains(p.t, err.Error(), "invalid opening_fee_params")
|
||||||
|
|
||||||
|
err = RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
OpeningFeeParams: &lspd.OpeningFeeParams{
|
||||||
|
MinMsat: params.MinMsat,
|
||||||
|
Proportional: params.Proportional,
|
||||||
|
ValidUntil: params.ValidUntil,
|
||||||
|
MaxIdleTime: params.MaxIdleTime,
|
||||||
|
MaxClientToSelfDelay: params.MaxClientToSelfDelay,
|
||||||
|
// Modify promise
|
||||||
|
Promise: params.Promise + "aa",
|
||||||
|
},
|
||||||
|
}, true)
|
||||||
|
assert.Contains(p.t, err.Error(), "invalid opening_fee_params")
|
||||||
|
|
||||||
|
err = RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
// Fee too low
|
||||||
|
IncomingAmountMsat: int64(2999999),
|
||||||
|
OutgoingAmountMsat: int64(0),
|
||||||
|
OpeningFeeParams: params,
|
||||||
|
}, true)
|
||||||
|
assert.Contains(p.t, err.Error(), "not enough fees")
|
||||||
|
|
||||||
|
err = RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat + 1),
|
||||||
|
OpeningFeeParams: params,
|
||||||
|
}, true)
|
||||||
|
assert.Contains(p.t, err.Error(), "not enough fees")
|
||||||
|
|
||||||
|
// Now register the payment for real
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
OpeningFeeParams: info.OpeningFeeParamsMenu[0],
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat)
|
||||||
|
payResp, err := alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
lntest.CheckError(p.t, err)
|
||||||
|
bobInvoice := p.BreezClient().Node().GetInvoice(payResp.PaymentHash)
|
||||||
|
|
||||||
|
assert.Equal(p.t, payResp.PaymentPreimage, bobInvoice.PaymentPreimage)
|
||||||
|
assert.Equal(p.t, innerAmountMsat, bobInvoice.AmountReceivedMsat)
|
||||||
|
|
||||||
|
// Make sure capacity is correct
|
||||||
|
chans := p.BreezClient().Node().GetChannels()
|
||||||
|
assert.Equal(p.t, 1, len(chans))
|
||||||
|
c := chans[0]
|
||||||
|
AssertChannelCapacity(p.t, outerAmountMsat, c.CapacityMsat)
|
||||||
|
}
|
||||||
147
itest/intercept_zero_conf_test.go
Normal file
147
itest/intercept_zero_conf_test.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
var htlcInterceptorDelay = time.Second * 7
|
||||||
|
|
||||||
|
func testOpenZeroConfChannelOnReceive(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
})
|
||||||
|
alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(2100000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: p.lsp,
|
||||||
|
})
|
||||||
|
p.BreezClient().SetHtlcAcceptor(innerAmountMsat)
|
||||||
|
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
payResp := alice.Pay(outerInvoice.bolt11)
|
||||||
|
bobInvoice := p.BreezClient().Node().GetInvoice(payResp.PaymentHash)
|
||||||
|
|
||||||
|
assert.Equal(p.t, payResp.PaymentPreimage, bobInvoice.PaymentPreimage)
|
||||||
|
assert.Equal(p.t, innerAmountMsat, bobInvoice.AmountReceivedMsat)
|
||||||
|
|
||||||
|
// Make sure capacity is correct
|
||||||
|
chans := p.BreezClient().Node().GetChannels()
|
||||||
|
assert.Equal(p.t, 1, len(chans))
|
||||||
|
c := chans[0]
|
||||||
|
AssertChannelCapacity(p.t, outerAmountMsat, c.CapacityMsat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOpenZeroConfSingleHtlc(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
})
|
||||||
|
channelId := alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(2100000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: p.lsp,
|
||||||
|
})
|
||||||
|
|
||||||
|
p.BreezClient().SetHtlcAcceptor(innerAmountMsat)
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat)
|
||||||
|
payResp, err := alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
lntest.CheckError(p.t, err)
|
||||||
|
bobInvoice := p.BreezClient().Node().GetInvoice(payResp.PaymentHash)
|
||||||
|
|
||||||
|
assert.Equal(p.t, payResp.PaymentPreimage, bobInvoice.PaymentPreimage)
|
||||||
|
assert.Equal(p.t, innerAmountMsat, bobInvoice.AmountReceivedMsat)
|
||||||
|
|
||||||
|
// Make sure capacity is correct
|
||||||
|
chans := p.BreezClient().Node().GetChannels()
|
||||||
|
assert.Equal(p.t, 1, len(chans))
|
||||||
|
c := chans[0]
|
||||||
|
AssertChannelCapacity(p.t, outerAmountMsat, c.CapacityMsat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func constructRoute(
|
||||||
|
lsp lntest.LightningNode,
|
||||||
|
bob lntest.LightningNode,
|
||||||
|
aliceLspChannel lntest.ShortChannelID,
|
||||||
|
lspBobChannel lntest.ShortChannelID,
|
||||||
|
amountMsat uint64,
|
||||||
|
) *lntest.Route {
|
||||||
|
return &lntest.Route{
|
||||||
|
Hops: []*lntest.Hop{
|
||||||
|
{
|
||||||
|
Id: lsp.NodeId(),
|
||||||
|
Channel: aliceLspChannel,
|
||||||
|
AmountMsat: amountMsat + uint64(lspBaseFeeMsat) + (amountMsat * uint64(lspFeeRatePpm) / 1000000),
|
||||||
|
Delay: 144 + lspCltvDelta,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
Id: bob.NodeId(),
|
||||||
|
Channel: lspBobChannel,
|
||||||
|
AmountMsat: amountMsat,
|
||||||
|
Delay: 144,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
116
itest/lnd_breez_client.go
Normal file
116
itest/lnd_breez_client.go
Normal file
@@ -0,0 +1,116 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"flag"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/breez/lntest/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
var lndMobileExecutable = flag.String(
|
||||||
|
"lndmobileexec", "", "full path to lnd mobile binary",
|
||||||
|
)
|
||||||
|
|
||||||
|
type lndBreezClient struct {
|
||||||
|
name string
|
||||||
|
harness *lntest.TestHarness
|
||||||
|
node *lntest.LndNode
|
||||||
|
cancel context.CancelFunc
|
||||||
|
mtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLndBreezClient(h *lntest.TestHarness, m *lntest.Miner, name string) BreezClient {
|
||||||
|
lnd := lntest.NewLndNodeFromBinary(h, m, name, *lndMobileExecutable,
|
||||||
|
"--protocol.zero-conf",
|
||||||
|
"--protocol.option-scid-alias",
|
||||||
|
"--bitcoin.defaultchanconfs=0",
|
||||||
|
)
|
||||||
|
|
||||||
|
c := &lndBreezClient{
|
||||||
|
name: name,
|
||||||
|
harness: h,
|
||||||
|
node: lnd,
|
||||||
|
}
|
||||||
|
h.AddStoppable(c)
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lndBreezClient) Name() string {
|
||||||
|
return c.name
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lndBreezClient) Harness() *lntest.TestHarness {
|
||||||
|
return c.harness
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lndBreezClient) Node() lntest.LightningNode {
|
||||||
|
return c.node
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lndBreezClient) Start() {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
if c.node.IsStarted() {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.node.Start()
|
||||||
|
|
||||||
|
ctx, cancel := context.WithCancel(c.harness.Ctx)
|
||||||
|
c.cancel = cancel
|
||||||
|
go c.startChannelAcceptor(ctx)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lndBreezClient) Stop() error {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
// Stop the channel acceptor
|
||||||
|
if c.cancel != nil {
|
||||||
|
c.cancel()
|
||||||
|
c.cancel = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.node.Stop()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lndBreezClient) ResetHtlcAcceptor() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lndBreezClient) SetHtlcAcceptor(totalMsat uint64) {
|
||||||
|
// No need for a htlc acceptor in the LND breez client
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *lndBreezClient) startChannelAcceptor(ctx context.Context) error {
|
||||||
|
client, err := c.node.LightningClient().ChannelAcceptor(ctx)
|
||||||
|
if err != nil {
|
||||||
|
c.harness.T.Fatalf("%s: failed to create channel acceptor: %v", c.name, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
request, err := client.Recv()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
private := request.ChannelFlags&uint32(lnwire.FFAnnounceChannel) == 0
|
||||||
|
resp := &lnd.ChannelAcceptResponse{
|
||||||
|
PendingChanId: request.PendingChanId,
|
||||||
|
Accept: private,
|
||||||
|
}
|
||||||
|
if request.WantsZeroConf {
|
||||||
|
resp.MinAcceptDepth = 0
|
||||||
|
resp.ZeroConf = true
|
||||||
|
}
|
||||||
|
|
||||||
|
err = client.Send(resp)
|
||||||
|
if err != nil {
|
||||||
|
c.harness.T.Fatalf("%s: failed to send acceptor response: %v", c.name, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
251
itest/lnd_lspd_node.go
Normal file
251
itest/lnd_lspd_node.go
Normal file
@@ -0,0 +1,251 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
"github.com/breez/lspd/notifications"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
ecies "github.com/ecies/go/v2"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials/insecure"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LndLspNode struct {
|
||||||
|
harness *lntest.TestHarness
|
||||||
|
lightningNode *lntest.LndNode
|
||||||
|
logFilePath string
|
||||||
|
isInitialized bool
|
||||||
|
lspBase *lspBase
|
||||||
|
runtime *lndLspNodeRuntime
|
||||||
|
mtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
type lndLspNodeRuntime struct {
|
||||||
|
logFile *os.File
|
||||||
|
cmd *exec.Cmd
|
||||||
|
rpc lspd.ChannelOpenerClient
|
||||||
|
notificationRpc notifications.NotificationsClient
|
||||||
|
cleanups []*lntest.Cleanup
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLndLspdNode(h *lntest.TestHarness, m *lntest.Miner, mem *mempoolApi, name string, nodeConfig *config.NodeConfig) LspNode {
|
||||||
|
args := []string{
|
||||||
|
"--protocol.zero-conf",
|
||||||
|
"--protocol.option-scid-alias",
|
||||||
|
"--requireinterceptor",
|
||||||
|
"--bitcoin.defaultchanconfs=0",
|
||||||
|
fmt.Sprintf("--bitcoin.chanreservescript=\"0 if (chanAmt != %d) else chanAmt/100\"", publicChanAmount),
|
||||||
|
fmt.Sprintf("--bitcoin.basefee=%d", lspBaseFeeMsat),
|
||||||
|
fmt.Sprintf("--bitcoin.feerate=%d", lspFeeRatePpm),
|
||||||
|
fmt.Sprintf("--bitcoin.timelockdelta=%d", lspCltvDelta),
|
||||||
|
}
|
||||||
|
|
||||||
|
lightningNode := lntest.NewLndNode(h, m, name, args...)
|
||||||
|
lnd := &config.LndConfig{
|
||||||
|
Address: lightningNode.GrpcHost(),
|
||||||
|
Cert: string(lightningNode.TlsCert()),
|
||||||
|
Macaroon: hex.EncodeToString(lightningNode.Macaroon()),
|
||||||
|
}
|
||||||
|
lspBase, err := newLspd(h, mem, name, nodeConfig, lnd, nil)
|
||||||
|
if err != nil {
|
||||||
|
h.T.Fatalf("failed to initialize lspd")
|
||||||
|
}
|
||||||
|
scriptDir := filepath.Dir(lspBase.scriptFilePath)
|
||||||
|
logFilePath := filepath.Join(scriptDir, "lspd.log")
|
||||||
|
h.RegisterLogfile(logFilePath, fmt.Sprintf("lspd-%s", name))
|
||||||
|
|
||||||
|
lspNode := &LndLspNode{
|
||||||
|
harness: h,
|
||||||
|
lightningNode: lightningNode,
|
||||||
|
logFilePath: logFilePath,
|
||||||
|
lspBase: lspBase,
|
||||||
|
}
|
||||||
|
|
||||||
|
h.AddStoppable(lspNode)
|
||||||
|
return lspNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndLspNode) Start() {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
var cleanups []*lntest.Cleanup
|
||||||
|
wasInitialized := c.isInitialized
|
||||||
|
if !c.isInitialized {
|
||||||
|
err := c.lspBase.Initialize()
|
||||||
|
if err != nil {
|
||||||
|
c.harness.T.Fatalf("failed to initialize lsp: %v", err)
|
||||||
|
}
|
||||||
|
c.isInitialized = true
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: lsp base", c.lspBase.name),
|
||||||
|
Fn: c.lspBase.Stop,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
c.lightningNode.Start()
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: lsp lightning node", c.lspBase.name),
|
||||||
|
Fn: c.lightningNode.Stop,
|
||||||
|
})
|
||||||
|
|
||||||
|
if !wasInitialized {
|
||||||
|
scriptFile, err := os.ReadFile(c.lspBase.scriptFilePath)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
c.harness.T.Fatalf("failed to open scriptfile '%s': %v", c.lspBase.scriptFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.Remove(c.lspBase.scriptFilePath)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
c.harness.T.Fatalf("failed to remove scriptfile '%s': %v", c.lspBase.scriptFilePath, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
split := strings.Split(string(scriptFile), "\n")
|
||||||
|
for i, s := range split {
|
||||||
|
if strings.HasPrefix(s, "export NODES") {
|
||||||
|
j, _ := json.Marshal(map[string]string{
|
||||||
|
"address": c.lightningNode.GrpcHost(),
|
||||||
|
"cert": string(c.lightningNode.TlsCert()),
|
||||||
|
"macaroon": hex.EncodeToString(c.lightningNode.Macaroon())})
|
||||||
|
ext := fmt.Sprintf(`"lnd": %s}]'`, string(j))
|
||||||
|
start, _, _ := strings.Cut(s, `"lnd"`)
|
||||||
|
|
||||||
|
split[i] = start + ext
|
||||||
|
}
|
||||||
|
}
|
||||||
|
newContent := strings.Join(split, "\n")
|
||||||
|
err = os.WriteFile(c.lspBase.scriptFilePath, []byte(newContent), 0755)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
c.harness.T.Fatalf("failed to rewrite scriptfile '%s': %v", c.lspBase.scriptFilePath, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cmd := exec.CommandContext(c.harness.Ctx, c.lspBase.scriptFilePath)
|
||||||
|
cmd.SysProcAttr = &syscall.SysProcAttr{Setpgid: true}
|
||||||
|
logFile, err := os.Create(c.logFilePath)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
c.harness.T.Fatalf("failed create lsp logfile: %v", err)
|
||||||
|
}
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: logfile", c.lspBase.name),
|
||||||
|
Fn: logFile.Close,
|
||||||
|
})
|
||||||
|
|
||||||
|
cmd.Stdout = logFile
|
||||||
|
cmd.Stderr = logFile
|
||||||
|
|
||||||
|
log.Printf("%s: starting lspd %s", c.lspBase.name, c.lspBase.scriptFilePath)
|
||||||
|
err = cmd.Start()
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
c.harness.T.Fatalf("failed to start lspd: %v", err)
|
||||||
|
}
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: cmd", c.lspBase.name),
|
||||||
|
Fn: func() error {
|
||||||
|
proc := cmd.Process
|
||||||
|
if proc == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
syscall.Kill(-proc.Pid, syscall.SIGINT)
|
||||||
|
|
||||||
|
log.Printf("About to wait for lspd to exit")
|
||||||
|
status, err := proc.Wait()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("waiting for lspd process error: %v, status: %v", err, status)
|
||||||
|
}
|
||||||
|
err = cmd.Wait()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("waiting for lspd cmd error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
conn, err := grpc.Dial(
|
||||||
|
c.lspBase.grpcAddress,
|
||||||
|
grpc.WithTransportCredentials(insecure.NewCredentials()),
|
||||||
|
grpc.WithPerRPCCredentials(&token{token: "hello"}),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
c.harness.T.Fatalf("failed to create grpc connection: %v", err)
|
||||||
|
}
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: grpc conn", c.lspBase.name),
|
||||||
|
Fn: conn.Close,
|
||||||
|
})
|
||||||
|
|
||||||
|
client := lspd.NewChannelOpenerClient(conn)
|
||||||
|
notifyClient := notifications.NewNotificationsClient(conn)
|
||||||
|
c.runtime = &lndLspNodeRuntime{
|
||||||
|
logFile: logFile,
|
||||||
|
cmd: cmd,
|
||||||
|
rpc: client,
|
||||||
|
notificationRpc: notifyClient,
|
||||||
|
cleanups: cleanups,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndLspNode) Stop() error {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
if c.runtime == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lntest.PerformCleanup(c.runtime.cleanups)
|
||||||
|
c.runtime = nil
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndLspNode) Harness() *lntest.TestHarness {
|
||||||
|
return c.harness
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndLspNode) PublicKey() *btcec.PublicKey {
|
||||||
|
return c.lspBase.pubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndLspNode) EciesPublicKey() *ecies.PublicKey {
|
||||||
|
return c.lspBase.eciesPubkey
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndLspNode) Rpc() lspd.ChannelOpenerClient {
|
||||||
|
return c.runtime.rpc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndLspNode) NotificationsRpc() notifications.NotificationsClient {
|
||||||
|
return c.runtime.notificationRpc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LndLspNode) NodeId() []byte {
|
||||||
|
return l.lightningNode.NodeId()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LndLspNode) LightningNode() lntest.LightningNode {
|
||||||
|
return l.lightningNode
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *LndLspNode) PostgresBackend() *PostgresContainer {
|
||||||
|
return l.lspBase.postgresBackend
|
||||||
|
}
|
||||||
366
itest/lspd_node.go
Normal file
366
itest/lspd_node.go
Normal file
@@ -0,0 +1,366 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"path/filepath"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
"github.com/breez/lspd/notifications"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/decred/dcrd/dcrec/secp256k1/v4"
|
||||||
|
ecies "github.com/ecies/go/v2"
|
||||||
|
"google.golang.org/protobuf/proto"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lspdExecutable = flag.String(
|
||||||
|
"lspdexec", "", "full path to lpsd plugin binary",
|
||||||
|
)
|
||||||
|
lspdMigrationsDir = flag.String(
|
||||||
|
"lspdmigrationsdir", "", "full path to lspd sql migrations directory",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
lspBaseFeeMsat uint32 = 1000
|
||||||
|
lspFeeRatePpm uint32 = 1
|
||||||
|
lspCltvDelta uint16 = 144
|
||||||
|
)
|
||||||
|
|
||||||
|
type LspNode interface {
|
||||||
|
Start()
|
||||||
|
Stop() error
|
||||||
|
Harness() *lntest.TestHarness
|
||||||
|
PublicKey() *btcec.PublicKey
|
||||||
|
EciesPublicKey() *ecies.PublicKey
|
||||||
|
Rpc() lspd.ChannelOpenerClient
|
||||||
|
NotificationsRpc() notifications.NotificationsClient
|
||||||
|
NodeId() []byte
|
||||||
|
LightningNode() lntest.LightningNode
|
||||||
|
PostgresBackend() *PostgresContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
type lspBase struct {
|
||||||
|
harness *lntest.TestHarness
|
||||||
|
name string
|
||||||
|
binary string
|
||||||
|
env []string
|
||||||
|
scriptFilePath string
|
||||||
|
grpcAddress string
|
||||||
|
pubkey *secp256k1.PublicKey
|
||||||
|
eciesPubkey *ecies.PublicKey
|
||||||
|
postgresBackend *PostgresContainer
|
||||||
|
}
|
||||||
|
|
||||||
|
func newLspd(h *lntest.TestHarness, mem *mempoolApi, name string, nodeConfig *config.NodeConfig, lnd *config.LndConfig, cln *config.ClnConfig, envExt ...string) (*lspBase, error) {
|
||||||
|
scriptDir := h.GetDirectory(fmt.Sprintf("lspd-%s", name))
|
||||||
|
log.Printf("%s: Creating LSPD in dir %s", name, scriptDir)
|
||||||
|
|
||||||
|
pgLogfile := filepath.Join(scriptDir, "postgres.log")
|
||||||
|
h.RegisterLogfile(pgLogfile, fmt.Sprintf("%s-postgres", name))
|
||||||
|
postgresBackend, err := NewPostgresContainer(pgLogfile)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lspdBinary, err := getLspdBinary()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lspdPort, err := lntest.GetPort()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
lspdPrivateKeyBytes, err := GenerateRandomBytes(32)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, publ := btcec.PrivKeyFromBytes(lspdPrivateKeyBytes)
|
||||||
|
eciesPubl := ecies.NewPrivateKeyFromBytes(lspdPrivateKeyBytes).PublicKey
|
||||||
|
host := "localhost"
|
||||||
|
grpcAddress := fmt.Sprintf("%s:%d", host, lspdPort)
|
||||||
|
minConfs := uint32(1)
|
||||||
|
conf := &config.NodeConfig{
|
||||||
|
Name: name,
|
||||||
|
LspdPrivateKey: hex.EncodeToString(lspdPrivateKeyBytes),
|
||||||
|
Tokens: []string{"hello"},
|
||||||
|
Host: "host:port",
|
||||||
|
PublicChannelAmount: 1000183,
|
||||||
|
ChannelAmount: 100000,
|
||||||
|
ChannelPrivate: false,
|
||||||
|
TargetConf: 6,
|
||||||
|
MinConfs: &minConfs,
|
||||||
|
MinHtlcMsat: 600,
|
||||||
|
BaseFeeMsat: 1000,
|
||||||
|
FeeRate: 0.000001,
|
||||||
|
TimeLockDelta: 144,
|
||||||
|
ChannelFeePermyriad: 40,
|
||||||
|
ChannelMinimumFeeMsat: 2000000,
|
||||||
|
AdditionalChannelCapacity: 100000,
|
||||||
|
MaxInactiveDuration: 3888000,
|
||||||
|
Lnd: lnd,
|
||||||
|
Cln: cln,
|
||||||
|
}
|
||||||
|
|
||||||
|
if nodeConfig != nil {
|
||||||
|
if nodeConfig.MinConfs != nil {
|
||||||
|
conf.MinConfs = nodeConfig.MinConfs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%s: node config: %+v", name, conf)
|
||||||
|
confJson, _ := json.Marshal(conf)
|
||||||
|
nodes := fmt.Sprintf(`NODES='[%s]'`, string(confJson))
|
||||||
|
env := []string{
|
||||||
|
nodes,
|
||||||
|
fmt.Sprintf("DATABASE_URL=%s", postgresBackend.ConnectionString()),
|
||||||
|
fmt.Sprintf("LISTEN_ADDRESS=%s", grpcAddress),
|
||||||
|
fmt.Sprintf("MEMPOOL_API_BASE_URL=%s", mem.Address()),
|
||||||
|
"MEMPOOL_PRIORITY=economy",
|
||||||
|
}
|
||||||
|
|
||||||
|
env = append(env, envExt...)
|
||||||
|
|
||||||
|
scriptFilePath := filepath.Join(scriptDir, "start-lspd.sh")
|
||||||
|
|
||||||
|
l := &lspBase{
|
||||||
|
harness: h,
|
||||||
|
name: name,
|
||||||
|
env: env,
|
||||||
|
binary: lspdBinary,
|
||||||
|
scriptFilePath: scriptFilePath,
|
||||||
|
grpcAddress: grpcAddress,
|
||||||
|
pubkey: publ,
|
||||||
|
eciesPubkey: eciesPubl,
|
||||||
|
postgresBackend: postgresBackend,
|
||||||
|
}
|
||||||
|
h.AddStoppable(l)
|
||||||
|
h.AddCleanable(l)
|
||||||
|
return l, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lspBase) Stop() error {
|
||||||
|
return l.postgresBackend.Stop(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lspBase) Cleanup() error {
|
||||||
|
return l.postgresBackend.Cleanup(context.Background())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *lspBase) Initialize() error {
|
||||||
|
var cleanups []*lntest.Cleanup
|
||||||
|
migrationsDir, err := getMigrationsDir()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = l.postgresBackend.Start(l.harness.Ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
cleanups = append(cleanups, &lntest.Cleanup{
|
||||||
|
Name: fmt.Sprintf("%s: postgres container", l.name),
|
||||||
|
Fn: func() error {
|
||||||
|
return l.postgresBackend.Stop(context.Background())
|
||||||
|
},
|
||||||
|
})
|
||||||
|
err = l.postgresBackend.RunMigrations(l.harness.Ctx, migrationsDir)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
pgxPool, err := pgxpool.Connect(l.harness.Ctx, l.postgresBackend.ConnectionString())
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return fmt.Errorf("failed to connect to postgres: %w", err)
|
||||||
|
}
|
||||||
|
defer pgxPool.Close()
|
||||||
|
|
||||||
|
_, err = pgxPool.Exec(
|
||||||
|
l.harness.Ctx,
|
||||||
|
`DELETE FROM new_channel_params`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return fmt.Errorf("failed to delete new_channel_params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = pgxPool.Exec(
|
||||||
|
l.harness.Ctx,
|
||||||
|
`INSERT INTO new_channel_params (validity, params, token)
|
||||||
|
VALUES
|
||||||
|
(3600, '{"min_msat": "1000000", "proportional": 7500, "max_idle_time": 4320, "max_client_to_self_delay": 432}', 'hello'),
|
||||||
|
(259200, '{"min_msat": "1100000", "proportional": 7500, "max_idle_time": 4320, "max_client_to_self_delay": 432}', 'hello');`,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return fmt.Errorf("failed to insert new_channel_params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("%s: Creating lspd startup script at %s", l.name, l.scriptFilePath)
|
||||||
|
scriptFile, err := os.OpenFile(l.scriptFilePath, os.O_CREATE|os.O_WRONLY, 0755)
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
defer scriptFile.Close()
|
||||||
|
writer := bufio.NewWriter(scriptFile)
|
||||||
|
_, err = writer.WriteString("#!/bin/bash\n")
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, str := range l.env {
|
||||||
|
_, err = writer.WriteString("export " + str + "\n")
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = writer.WriteString(l.binary + "\n")
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = writer.Flush()
|
||||||
|
if err != nil {
|
||||||
|
lntest.PerformCleanup(cleanups)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ChannelInformation(l LspNode) *lspd.ChannelInformationReply {
|
||||||
|
info, err := l.Rpc().ChannelInformation(
|
||||||
|
l.Harness().Ctx,
|
||||||
|
&lspd.ChannelInformationRequest{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
l.Harness().T.Fatalf("Failed to get ChannelInformation: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterPayment(l LspNode, paymentInfo *lspd.PaymentInformation, continueOnError bool) error {
|
||||||
|
serialized, err := proto.Marshal(paymentInfo)
|
||||||
|
lntest.CheckError(l.Harness().T, err)
|
||||||
|
|
||||||
|
encrypted, err := ecies.Encrypt(l.EciesPublicKey(), serialized)
|
||||||
|
lntest.CheckError(l.Harness().T, err)
|
||||||
|
|
||||||
|
log.Printf("Registering payment")
|
||||||
|
_, err = l.Rpc().RegisterPayment(
|
||||||
|
l.Harness().Ctx,
|
||||||
|
&lspd.RegisterPaymentRequest{
|
||||||
|
Blob: encrypted,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
if !continueOnError {
|
||||||
|
lntest.CheckError(l.Harness().T, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
type FeeParamSetting struct {
|
||||||
|
Validity time.Duration
|
||||||
|
MinMsat uint64
|
||||||
|
Proportional uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetFeeParams(l LspNode, settings []*FeeParamSetting) error {
|
||||||
|
pgxPool, err := pgxpool.Connect(l.Harness().Ctx, l.PostgresBackend().ConnectionString())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to postgres: %w", err)
|
||||||
|
}
|
||||||
|
defer pgxPool.Close()
|
||||||
|
|
||||||
|
_, err = pgxPool.Exec(l.Harness().Ctx, "DELETE FROM new_channel_params")
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to delete new_channel_params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(settings) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
query := `INSERT INTO new_channel_params (validity, params, token) VALUES `
|
||||||
|
first := true
|
||||||
|
for _, setting := range settings {
|
||||||
|
if !first {
|
||||||
|
query += `,`
|
||||||
|
}
|
||||||
|
|
||||||
|
query += fmt.Sprintf(
|
||||||
|
`(%d, '{"min_msat": "%d", "proportional": %d, "max_idle_time": 4320, "max_client_to_self_delay": 432}', 'hello')`,
|
||||||
|
int64(setting.Validity.Seconds()),
|
||||||
|
setting.MinMsat,
|
||||||
|
setting.Proportional,
|
||||||
|
)
|
||||||
|
|
||||||
|
first = false
|
||||||
|
}
|
||||||
|
query += `;`
|
||||||
|
_, err = pgxPool.Exec(l.Harness().Ctx, query)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to insert new_channel_params: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getLspdBinary() (string, error) {
|
||||||
|
if lspdExecutable != nil {
|
||||||
|
return *lspdExecutable, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return exec.LookPath("lspd")
|
||||||
|
}
|
||||||
|
|
||||||
|
func getMigrationsDir() (string, error) {
|
||||||
|
if lspdMigrationsDir != nil {
|
||||||
|
return *lspdMigrationsDir, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return exec.LookPath("lspdmigrationsdir")
|
||||||
|
}
|
||||||
|
|
||||||
|
type token struct {
|
||||||
|
token string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (t *token) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||||
|
m := make(map[string]string)
|
||||||
|
m["authorization"] = "Bearer " + t.token
|
||||||
|
return m, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// RequireTransportSecurity indicates whether the credentials requires
|
||||||
|
// transport security.
|
||||||
|
func (t *token) RequireTransportSecurity() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
166
itest/lspd_test.go
Normal file
166
itest/lspd_test.go
Normal file
@@ -0,0 +1,166 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultTimeout time.Duration = time.Second * 120
|
||||||
|
|
||||||
|
func TestLspd(t *testing.T) {
|
||||||
|
testCases := allTestCases
|
||||||
|
runTests(t, testCases, "LND-lspd", lndLspFunc, lndClientFunc)
|
||||||
|
runTests(t, testCases, "CLN-lspd", clnLspFunc, clnClientFunc)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lndLspFunc(h *lntest.TestHarness, m *lntest.Miner, mem *mempoolApi, c *config.NodeConfig) LspNode {
|
||||||
|
return NewLndLspdNode(h, m, mem, "lsp", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func clnLspFunc(h *lntest.TestHarness, m *lntest.Miner, mem *mempoolApi, c *config.NodeConfig) LspNode {
|
||||||
|
return NewClnLspdNode(h, m, mem, "lsp", c)
|
||||||
|
}
|
||||||
|
|
||||||
|
func lndClientFunc(h *lntest.TestHarness, m *lntest.Miner) BreezClient {
|
||||||
|
return newLndBreezClient(h, m, "breez-client")
|
||||||
|
}
|
||||||
|
|
||||||
|
func clnClientFunc(h *lntest.TestHarness, m *lntest.Miner) BreezClient {
|
||||||
|
return newClnBreezClient(h, m, "breez-client")
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTests(
|
||||||
|
t *testing.T,
|
||||||
|
testCases []*testCase,
|
||||||
|
prefix string,
|
||||||
|
lspFunc LspFunc,
|
||||||
|
clientFunc ClientFunc,
|
||||||
|
) {
|
||||||
|
for _, testCase := range testCases {
|
||||||
|
testCase := testCase
|
||||||
|
t.Run(fmt.Sprintf("%s: %s", prefix, testCase.name), func(t *testing.T) {
|
||||||
|
runTest(t, testCase, prefix, lspFunc, clientFunc)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func runTest(
|
||||||
|
t *testing.T,
|
||||||
|
testCase *testCase,
|
||||||
|
prefix string,
|
||||||
|
lspFunc LspFunc,
|
||||||
|
clientFunc ClientFunc,
|
||||||
|
) {
|
||||||
|
log.Printf("%s: Running test case '%s'", prefix, testCase.name)
|
||||||
|
var dd time.Duration
|
||||||
|
to := testCase.timeout
|
||||||
|
if to == dd {
|
||||||
|
to = defaultTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
deadline := time.Now().Add(to)
|
||||||
|
log.Printf("Using deadline %v", deadline.String())
|
||||||
|
|
||||||
|
h := lntest.NewTestHarness(t, deadline)
|
||||||
|
defer h.TearDown()
|
||||||
|
|
||||||
|
log.Printf("Creating miner")
|
||||||
|
miner := lntest.NewMiner(h)
|
||||||
|
miner.Start()
|
||||||
|
|
||||||
|
log.Printf("Creating mempool api")
|
||||||
|
mem := NewMempoolApi(h)
|
||||||
|
mem.Start()
|
||||||
|
|
||||||
|
log.Printf("Creating lsp")
|
||||||
|
var lsp LspNode
|
||||||
|
if !testCase.skipCreateLsp {
|
||||||
|
lsp = lspFunc(h, miner, mem, nil)
|
||||||
|
lsp.Start()
|
||||||
|
}
|
||||||
|
c := clientFunc(h, miner)
|
||||||
|
c.Start()
|
||||||
|
log.Printf("Run testcase")
|
||||||
|
testCase.test(&testParams{
|
||||||
|
t: t,
|
||||||
|
h: h,
|
||||||
|
m: miner,
|
||||||
|
mem: mem,
|
||||||
|
c: c,
|
||||||
|
lsp: lsp,
|
||||||
|
lspFunc: lspFunc,
|
||||||
|
clientFunc: clientFunc,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type testCase struct {
|
||||||
|
name string
|
||||||
|
test func(t *testParams)
|
||||||
|
skipCreateLsp bool
|
||||||
|
timeout time.Duration
|
||||||
|
}
|
||||||
|
|
||||||
|
var allTestCases = []*testCase{
|
||||||
|
{
|
||||||
|
name: "testOpenZeroConfChannelOnReceive",
|
||||||
|
test: testOpenZeroConfChannelOnReceive,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testOpenZeroConfSingleHtlc",
|
||||||
|
test: testOpenZeroConfSingleHtlc,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testZeroReserve",
|
||||||
|
test: testZeroReserve,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testFailureBobOffline",
|
||||||
|
test: testFailureBobOffline,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testNoBalance",
|
||||||
|
test: testNoBalance,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testRegularForward",
|
||||||
|
test: testRegularForward,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testProbing",
|
||||||
|
test: testProbing,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testInvalidCltv",
|
||||||
|
test: testInvalidCltv,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "registerPaymentWithTag",
|
||||||
|
test: registerPaymentWithTag,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testOpenZeroConfUtxo",
|
||||||
|
test: testOpenZeroConfUtxo,
|
||||||
|
skipCreateLsp: true,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testDynamicFeeFlow",
|
||||||
|
test: testDynamicFeeFlow,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testOfflineNotificationPaymentRegistered",
|
||||||
|
test: testOfflineNotificationPaymentRegistered,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testOfflineNotificationRegularForward",
|
||||||
|
test: testOfflineNotificationRegularForward,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "testOfflineNotificationZeroConfChannel",
|
||||||
|
test: testOfflineNotificationZeroConfChannel,
|
||||||
|
},
|
||||||
|
}
|
||||||
85
itest/mempool_api.go
Normal file
85
itest/mempool_api.go
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
)
|
||||||
|
|
||||||
|
type RecommendedFeesResponse struct {
|
||||||
|
FastestFee float64 `json:"fastestFee"`
|
||||||
|
HalfHourFee float64 `json:"halfHourFee"`
|
||||||
|
HourFee float64 `json:"hourFee"`
|
||||||
|
EconomyFee float64 `json:"economyFee"`
|
||||||
|
MinimumFee float64 `json:"minimumFee"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type mempoolApi struct {
|
||||||
|
addr string
|
||||||
|
h *lntest.TestHarness
|
||||||
|
fees *RecommendedFeesResponse
|
||||||
|
lis net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMempoolApi(h *lntest.TestHarness) *mempoolApi {
|
||||||
|
port, err := lntest.GetPort()
|
||||||
|
if err != nil {
|
||||||
|
h.T.Fatalf("Failed to get port for mempool api: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &mempoolApi{
|
||||||
|
addr: fmt.Sprintf("127.0.0.1:%d", port),
|
||||||
|
h: h,
|
||||||
|
fees: &RecommendedFeesResponse{
|
||||||
|
MinimumFee: 1,
|
||||||
|
EconomyFee: 1,
|
||||||
|
HourFee: 1,
|
||||||
|
HalfHourFee: 1,
|
||||||
|
FastestFee: 1,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mempoolApi) Address() string {
|
||||||
|
return fmt.Sprintf("http://%s/api/v1/", m.addr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mempoolApi) SetFees(fees *RecommendedFeesResponse) {
|
||||||
|
m.fees = fees
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mempoolApi) Start() {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/api/v1/fees/recommended", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
j, err := json.Marshal(m.fees)
|
||||||
|
if err != nil {
|
||||||
|
m.h.T.Fatalf("Failed to marshal mempool fees: %v", err)
|
||||||
|
}
|
||||||
|
_, err = w.Write(j)
|
||||||
|
if err != nil {
|
||||||
|
m.h.T.Fatalf("Failed to write mempool response: %v", err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
lis, err := net.Listen("tcp", m.addr)
|
||||||
|
if err != nil {
|
||||||
|
m.h.T.Fatalf("failed to start mempool api: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
m.lis = lis
|
||||||
|
m.h.AddStoppable(m)
|
||||||
|
|
||||||
|
go http.Serve(lis, mux)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mempoolApi) Stop() error {
|
||||||
|
lis := m.lis
|
||||||
|
if lis == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
m.lis = nil
|
||||||
|
return lis.Close()
|
||||||
|
}
|
||||||
56
itest/no_balance_test.go
Normal file
56
itest/no_balance_test.go
Normal file
@@ -0,0 +1,56 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testNoBalance(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
})
|
||||||
|
channelId := alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(2100000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: p.lsp,
|
||||||
|
})
|
||||||
|
p.BreezClient().SetHtlcAcceptor(innerAmountMsat)
|
||||||
|
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat)
|
||||||
|
_, err := alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
assert.Contains(p.t, err.Error(), "WIRE_TEMPORARY_CHANNEL_FAILURE")
|
||||||
|
}
|
||||||
59
itest/notification_service.go
Normal file
59
itest/notification_service.go
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PaymentReceivedPayload struct {
|
||||||
|
Template string `json:"template" binding:"required,eq=payment_received"`
|
||||||
|
Data struct {
|
||||||
|
PaymentHash string `json:"payment_hash" binding:"required"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TxConfirmedPayload struct {
|
||||||
|
Template string `json:"template" binding:"required,eq=tx_confirmed"`
|
||||||
|
Data struct {
|
||||||
|
TxID string `json:"tx_id" binding:"required"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type AddressTxsChangedPayload struct {
|
||||||
|
Template string `json:"template" binding:"required,eq=address_txs_changed"`
|
||||||
|
Data struct {
|
||||||
|
Address string `json:"address" binding:"required"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type notificationDeliveryService struct {
|
||||||
|
addr string
|
||||||
|
handleFunc func(resp http.ResponseWriter, req *http.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func newNotificationDeliveryService(
|
||||||
|
addr string,
|
||||||
|
handleFunc func(resp http.ResponseWriter, req *http.Request),
|
||||||
|
) *notificationDeliveryService {
|
||||||
|
return ¬ificationDeliveryService{
|
||||||
|
addr: addr,
|
||||||
|
handleFunc: handleFunc,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *notificationDeliveryService) Start(ctx context.Context) error {
|
||||||
|
mux := http.NewServeMux()
|
||||||
|
mux.HandleFunc("/api/v1/notify", s.handleFunc)
|
||||||
|
lis, err := net.Listen("tcp", s.addr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
<-ctx.Done()
|
||||||
|
lis.Close()
|
||||||
|
}()
|
||||||
|
|
||||||
|
return http.Serve(lis, mux)
|
||||||
|
}
|
||||||
309
itest/notification_test.go
Normal file
309
itest/notification_test.go
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/breez/lspd/notifications"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testOfflineNotificationPaymentRegistered(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
})
|
||||||
|
channelId := alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(2100000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: p.lsp,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// Kill the mobile client
|
||||||
|
log.Printf("Stopping breez client")
|
||||||
|
p.BreezClient().Stop()
|
||||||
|
|
||||||
|
port, err := lntest.GetPort()
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(p.t, "failed to get port for deliveeery service")
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
|
delivered := make(chan struct{})
|
||||||
|
|
||||||
|
notify := newNotificationDeliveryService(addr, func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
var body PaymentReceivedPayload
|
||||||
|
err = json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
assert.NoError(p.t, err)
|
||||||
|
|
||||||
|
ph := hex.EncodeToString(innerInvoice.paymentHash)
|
||||||
|
assert.Equal(p.t, ph, body.Data.PaymentHash)
|
||||||
|
close(delivered)
|
||||||
|
})
|
||||||
|
go notify.Start(p.h.Ctx)
|
||||||
|
go func() {
|
||||||
|
<-delivered
|
||||||
|
log.Printf("Starting breez client again")
|
||||||
|
p.BreezClient().SetHtlcAcceptor(innerAmountMsat)
|
||||||
|
p.BreezClient().Start()
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
}()
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
|
||||||
|
url := "http://" + addr + "/api/v1/notify"
|
||||||
|
first := sha256.Sum256([]byte(url))
|
||||||
|
second := sha256.Sum256(first[:])
|
||||||
|
sig, err := ecdsa.SignCompact(p.BreezClient().Node().PrivateKey(), second[:], true)
|
||||||
|
assert.NoError(p.t, err)
|
||||||
|
|
||||||
|
p.lsp.NotificationsRpc().SubscribeNotifications(p.h.Ctx, ¬ifications.SubscribeNotificationsRequest{
|
||||||
|
Url: url,
|
||||||
|
Signature: sig,
|
||||||
|
})
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat)
|
||||||
|
_, err = alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
assert.Nil(p.t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOfflineNotificationRegularForward(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
p.BreezClient().Node().Fund(100000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channelAL := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
IsPublic: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Print("Opening channel between lsp and Breez client")
|
||||||
|
channelLB := p.lsp.LightningNode().OpenChannel(p.BreezClient().Node(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: 200000,
|
||||||
|
IsPublic: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Print("Waiting for channel between Alice and the lsp to be ready.")
|
||||||
|
alice.WaitForChannelReady(channelAL)
|
||||||
|
log.Print("Waiting for channel between LSP and Bob to be ready.")
|
||||||
|
p.lsp.LightningNode().WaitForChannelReady(channelLB)
|
||||||
|
p.BreezClient().Node().WaitForChannelReady(channelLB)
|
||||||
|
|
||||||
|
port, err := lntest.GetPort()
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(p.t, "failed to get port for deliveeery service")
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
|
delivered := make(chan struct{})
|
||||||
|
|
||||||
|
notify := newNotificationDeliveryService(addr, func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
var body PaymentReceivedPayload
|
||||||
|
err = json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
assert.NoError(p.t, err)
|
||||||
|
|
||||||
|
close(delivered)
|
||||||
|
})
|
||||||
|
go notify.Start(p.h.Ctx)
|
||||||
|
go func() {
|
||||||
|
<-delivered
|
||||||
|
log.Printf("Notification was delivered. Starting breez client again")
|
||||||
|
p.BreezClient().Start()
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
}()
|
||||||
|
|
||||||
|
url := "http://" + addr + "/api/v1/notify"
|
||||||
|
first := sha256.Sum256([]byte(url))
|
||||||
|
second := sha256.Sum256(first[:])
|
||||||
|
sig, err := ecdsa.SignCompact(p.BreezClient().Node().PrivateKey(), second[:], true)
|
||||||
|
assert.NoError(p.t, err)
|
||||||
|
|
||||||
|
p.lsp.NotificationsRpc().SubscribeNotifications(p.h.Ctx, ¬ifications.SubscribeNotificationsRequest{
|
||||||
|
Url: url,
|
||||||
|
Signature: sig,
|
||||||
|
})
|
||||||
|
|
||||||
|
<-time.After(time.Second * 2)
|
||||||
|
log.Printf("Adding bob's invoice")
|
||||||
|
amountMsat := uint64(2100000)
|
||||||
|
bobInvoice := p.BreezClient().Node().CreateBolt11Invoice(&lntest.CreateInvoiceOptions{
|
||||||
|
AmountMsat: amountMsat,
|
||||||
|
IncludeHopHints: true,
|
||||||
|
})
|
||||||
|
log.Printf(bobInvoice.Bolt11)
|
||||||
|
|
||||||
|
log.Printf("Bob going offline")
|
||||||
|
p.BreezClient().Stop()
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
payResp := alice.Pay(bobInvoice.Bolt11)
|
||||||
|
invoiceResult := p.BreezClient().Node().GetInvoice(bobInvoice.PaymentHash)
|
||||||
|
|
||||||
|
assert.Equal(p.t, payResp.PaymentPreimage, invoiceResult.PaymentPreimage)
|
||||||
|
assert.Equal(p.t, amountMsat, invoiceResult.AmountReceivedMsat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func testOfflineNotificationZeroConfChannel(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
IsPublic: true,
|
||||||
|
})
|
||||||
|
channelId := alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(2100000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: p.lsp,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
p.BreezClient().SetHtlcAcceptor(innerAmountMsat)
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
expectedheight := p.Miner().GetBlockHeight()
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat)
|
||||||
|
_, err := alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
assert.Nil(p.t, err)
|
||||||
|
|
||||||
|
<-time.After(time.Second * 2)
|
||||||
|
log.Printf("Adding bob's invoice for zero conf payment")
|
||||||
|
amountMsat := uint64(2100000)
|
||||||
|
|
||||||
|
bobInvoice := p.BreezClient().Node().CreateBolt11Invoice(&lntest.CreateInvoiceOptions{
|
||||||
|
AmountMsat: amountMsat,
|
||||||
|
IncludeHopHints: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
invoiceWithHint := bobInvoice.Bolt11
|
||||||
|
if !ContainsHopHint(p.t, bobInvoice.Bolt11) {
|
||||||
|
chans := p.BreezClient().Node().GetChannels()
|
||||||
|
assert.Len(p.t, chans, 1)
|
||||||
|
|
||||||
|
var id lntest.ShortChannelID
|
||||||
|
if chans[0].RemoteAlias != nil {
|
||||||
|
id = *chans[0].RemoteAlias
|
||||||
|
} else if chans[0].LocalAlias != nil {
|
||||||
|
id = *chans[0].LocalAlias
|
||||||
|
} else {
|
||||||
|
id = chans[0].ShortChannelID
|
||||||
|
}
|
||||||
|
invoiceWithHint = AddHopHint(p.BreezClient(), bobInvoice.Bolt11, p.Lsp(), id, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Invoice with hint: %s", invoiceWithHint)
|
||||||
|
|
||||||
|
// Kill the mobile client
|
||||||
|
log.Printf("Stopping breez client")
|
||||||
|
p.BreezClient().Stop()
|
||||||
|
p.BreezClient().ResetHtlcAcceptor()
|
||||||
|
|
||||||
|
port, err := lntest.GetPort()
|
||||||
|
if err != nil {
|
||||||
|
assert.FailNow(p.t, "failed to get port for delivery service")
|
||||||
|
}
|
||||||
|
addr := fmt.Sprintf("127.0.0.1:%d", port)
|
||||||
|
delivered := make(chan struct{})
|
||||||
|
|
||||||
|
notify := newNotificationDeliveryService(addr, func(resp http.ResponseWriter, req *http.Request) {
|
||||||
|
var body PaymentReceivedPayload
|
||||||
|
err = json.NewDecoder(req.Body).Decode(&body)
|
||||||
|
assert.NoError(p.t, err)
|
||||||
|
|
||||||
|
close(delivered)
|
||||||
|
})
|
||||||
|
go notify.Start(p.h.Ctx)
|
||||||
|
go func() {
|
||||||
|
<-delivered
|
||||||
|
log.Printf("Starting breez client again")
|
||||||
|
p.BreezClient().Start()
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
}()
|
||||||
|
|
||||||
|
url := "http://" + addr + "/api/v1/notify"
|
||||||
|
first := sha256.Sum256([]byte(url))
|
||||||
|
second := sha256.Sum256(first[:])
|
||||||
|
sig, err := ecdsa.SignCompact(p.BreezClient().Node().PrivateKey(), second[:], true)
|
||||||
|
assert.NoError(p.t, err)
|
||||||
|
|
||||||
|
p.lsp.NotificationsRpc().SubscribeNotifications(p.h.Ctx, ¬ifications.SubscribeNotificationsRequest{
|
||||||
|
Url: url,
|
||||||
|
Signature: sig,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("Alice paying zero conf invoice")
|
||||||
|
payResp := alice.Pay(invoiceWithHint)
|
||||||
|
invoiceResult := p.BreezClient().Node().GetInvoice(bobInvoice.PaymentHash)
|
||||||
|
|
||||||
|
assert.Equal(p.t, payResp.PaymentPreimage, invoiceResult.PaymentPreimage)
|
||||||
|
assert.Equal(p.t, amountMsat, invoiceResult.AmountReceivedMsat)
|
||||||
|
|
||||||
|
// Make sure we haven't accidentally mined blocks in between.
|
||||||
|
actualheight := p.Miner().GetBlockHeight()
|
||||||
|
assert.Equal(p.t, expectedheight, actualheight)
|
||||||
|
}
|
||||||
268
itest/postgres.go
Normal file
268
itest/postgres.go
Normal file
@@ -0,0 +1,268 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"sort"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/docker/docker/api/types"
|
||||||
|
"github.com/docker/docker/api/types/container"
|
||||||
|
"github.com/docker/docker/client"
|
||||||
|
"github.com/docker/go-connections/nat"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PostgresContainer struct {
|
||||||
|
id string
|
||||||
|
password string
|
||||||
|
port uint32
|
||||||
|
cli *client.Client
|
||||||
|
logfile string
|
||||||
|
isInitialized bool
|
||||||
|
isStarted bool
|
||||||
|
mtx sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostgresContainer(logfile string) (*PostgresContainer, error) {
|
||||||
|
port, err := lntest.GetPort()
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("could not get port: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &PostgresContainer{
|
||||||
|
password: "pgpassword",
|
||||||
|
port: port,
|
||||||
|
logfile: logfile,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PostgresContainer) Start(ctx context.Context) error {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if c.isStarted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
c.cli, err = client.NewClientWithOpts(client.FromEnv)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("could not create docker client: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if !c.isInitialized {
|
||||||
|
err := c.initialize(ctx)
|
||||||
|
if err != nil {
|
||||||
|
c.cli.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.cli.ContainerStart(ctx, c.id, types.ContainerStartOptions{})
|
||||||
|
if err != nil {
|
||||||
|
c.cli.Close()
|
||||||
|
return fmt.Errorf("failed to start docker container '%s': %w", c.id, err)
|
||||||
|
}
|
||||||
|
c.isStarted = true
|
||||||
|
|
||||||
|
HealthCheck:
|
||||||
|
for {
|
||||||
|
inspect, err := c.cli.ContainerInspect(ctx, c.id)
|
||||||
|
if err != nil {
|
||||||
|
c.cli.ContainerStop(ctx, c.id, nil)
|
||||||
|
c.cli.Close()
|
||||||
|
return fmt.Errorf("failed to inspect container '%s' during healthcheck: %w", c.id, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
status := inspect.State.Health.Status
|
||||||
|
switch status {
|
||||||
|
case "unhealthy":
|
||||||
|
c.cli.ContainerStop(ctx, c.id, nil)
|
||||||
|
c.cli.Close()
|
||||||
|
return fmt.Errorf("container '%s' unhealthy", c.id)
|
||||||
|
case "healthy":
|
||||||
|
for {
|
||||||
|
pgxPool, err := pgxpool.Connect(ctx, c.ConnectionString())
|
||||||
|
if err == nil {
|
||||||
|
pgxPool.Close()
|
||||||
|
break HealthCheck
|
||||||
|
}
|
||||||
|
|
||||||
|
<-time.After(50 * time.Millisecond)
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
<-time.After(200 * time.Millisecond)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
go c.monitorLogs(ctx)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PostgresContainer) initialize(ctx context.Context) error {
|
||||||
|
image := "postgres:15"
|
||||||
|
_, _, err := c.cli.ImageInspectWithRaw(ctx, image)
|
||||||
|
if err != nil {
|
||||||
|
if !client.IsErrNotFound(err) {
|
||||||
|
return fmt.Errorf("could not find docker image '%s': %w", image, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
pullReader, err := c.cli.ImagePull(ctx, image, types.ImagePullOptions{})
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to pull docker image '%s': %w", image, err)
|
||||||
|
}
|
||||||
|
defer pullReader.Close()
|
||||||
|
|
||||||
|
_, err = io.Copy(io.Discard, pullReader)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to download docker image '%s': %w", image, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
createResp, err := c.cli.ContainerCreate(ctx, &container.Config{
|
||||||
|
Image: image,
|
||||||
|
Cmd: []string{
|
||||||
|
"postgres",
|
||||||
|
"-c",
|
||||||
|
"log_statement=all",
|
||||||
|
},
|
||||||
|
Env: []string{
|
||||||
|
"POSTGRES_DB=postgres",
|
||||||
|
"POSTGRES_PASSWORD=pgpassword",
|
||||||
|
"POSTGRES_USER=postgres",
|
||||||
|
},
|
||||||
|
Healthcheck: &container.HealthConfig{
|
||||||
|
Test: []string{"CMD-SHELL", "pg_isready -U postgres"},
|
||||||
|
Interval: time.Second,
|
||||||
|
Timeout: time.Second,
|
||||||
|
Retries: 10,
|
||||||
|
},
|
||||||
|
}, &container.HostConfig{
|
||||||
|
PortBindings: nat.PortMap{
|
||||||
|
"5432/tcp": []nat.PortBinding{
|
||||||
|
{HostPort: strconv.FormatUint(uint64(c.port), 10)},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
nil,
|
||||||
|
nil,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to create docker container: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
c.id = createResp.ID
|
||||||
|
c.isInitialized = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PostgresContainer) Stop(ctx context.Context) error {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
|
||||||
|
if !c.isStarted {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
defer c.cli.Close()
|
||||||
|
err := c.cli.ContainerStop(ctx, c.id, nil)
|
||||||
|
c.isStarted = false
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PostgresContainer) Cleanup(ctx context.Context) error {
|
||||||
|
c.mtx.Lock()
|
||||||
|
defer c.mtx.Unlock()
|
||||||
|
cli, err := client.NewClientWithOpts(client.FromEnv)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer cli.Close()
|
||||||
|
return cli.ContainerRemove(ctx, c.id, types.ContainerRemoveOptions{
|
||||||
|
Force: true,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PostgresContainer) monitorLogs(ctx context.Context) {
|
||||||
|
i, err := c.cli.ContainerLogs(ctx, c.id, types.ContainerLogsOptions{
|
||||||
|
ShowStderr: true,
|
||||||
|
ShowStdout: true,
|
||||||
|
Timestamps: false,
|
||||||
|
Follow: true,
|
||||||
|
Tail: "40",
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not get container logs: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer i.Close()
|
||||||
|
|
||||||
|
file, err := os.Create(c.logfile)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Could not create container log file %v: %v", c.logfile, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
hdr := make([]byte, 8)
|
||||||
|
for {
|
||||||
|
_, err := i.Read(hdr)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
count := binary.BigEndian.Uint32(hdr[4:])
|
||||||
|
dat := make([]byte, count)
|
||||||
|
_, err = i.Read(dat)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = file.Write(dat)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PostgresContainer) ConnectionString() string {
|
||||||
|
return fmt.Sprintf("postgres://postgres:%s@127.0.0.1:%d/postgres", c.password, c.port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *PostgresContainer) RunMigrations(ctx context.Context, migrationDir string) error {
|
||||||
|
filenames, err := filepath.Glob(filepath.Join(migrationDir, "*.up.sql"))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to glob migration files: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sort.Strings(filenames)
|
||||||
|
|
||||||
|
pgxPool, err := pgxpool.Connect(ctx, c.ConnectionString())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to connect to postgres: %w", err)
|
||||||
|
}
|
||||||
|
defer pgxPool.Close()
|
||||||
|
|
||||||
|
for _, filename := range filenames {
|
||||||
|
data, err := os.ReadFile(filename)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to read migration file '%s': %w", filename, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = pgxPool.Exec(ctx, string(data))
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to execute migration file '%s': %w", filename, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
75
itest/probing_test.go
Normal file
75
itest/probing_test.go
Normal file
@@ -0,0 +1,75 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/sha256"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testProbing(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
})
|
||||||
|
channelId := alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(2100000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: p.lsp,
|
||||||
|
})
|
||||||
|
p.BreezClient().SetHtlcAcceptor(innerAmountMsat)
|
||||||
|
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
|
||||||
|
h := sha256.New()
|
||||||
|
_, _ = h.Write([]byte("probing-01:"))
|
||||||
|
_, _ = h.Write(outerInvoice.paymentHash)
|
||||||
|
fakePaymentHash := h.Sum(nil)
|
||||||
|
|
||||||
|
log.Printf("Alice paying with fake payment hash with Bob online %x", fakePaymentHash)
|
||||||
|
route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat)
|
||||||
|
_, err := alice.PayViaRoute(outerAmountMsat, fakePaymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
|
||||||
|
// Expect incorrect or unknown payment details if the peer is online
|
||||||
|
assert.Contains(p.t, err.Error(), "WIRE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS")
|
||||||
|
|
||||||
|
// Kill the mobile client
|
||||||
|
log.Printf("Stopping breez client")
|
||||||
|
p.BreezClient().Stop()
|
||||||
|
|
||||||
|
log.Printf("Alice paying with fake payment hash with Bob offline %x", fakePaymentHash)
|
||||||
|
_, err = alice.PayViaRoute(outerAmountMsat, fakePaymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
|
||||||
|
// Expect unknown next peer if the peer is offline
|
||||||
|
assert.Contains(p.t, err.Error(), "WIRE_UNKNOWN_NEXT_PEER")
|
||||||
|
}
|
||||||
54
itest/regular_forward_test.go
Normal file
54
itest/regular_forward_test.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testRegularForward(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
p.BreezClient().Node().Fund(100000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channelAL := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
IsPublic: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Print("Opening channel between lsp and Breez client")
|
||||||
|
channelLB := p.lsp.LightningNode().OpenChannel(p.BreezClient().Node(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: 200000,
|
||||||
|
IsPublic: false,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Print("Waiting for channel between Alice and the lsp to be ready.")
|
||||||
|
alice.WaitForChannelReady(channelAL)
|
||||||
|
log.Print("Waiting for channel between LSP and Bob to be ready.")
|
||||||
|
p.lsp.LightningNode().WaitForChannelReady(channelLB)
|
||||||
|
p.BreezClient().Node().WaitForChannelReady(channelLB)
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoice")
|
||||||
|
amountMsat := uint64(2100000)
|
||||||
|
bobInvoice := p.BreezClient().Node().CreateBolt11Invoice(&lntest.CreateInvoiceOptions{
|
||||||
|
AmountMsat: amountMsat,
|
||||||
|
IncludeHopHints: true,
|
||||||
|
})
|
||||||
|
log.Printf(bobInvoice.Bolt11)
|
||||||
|
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
payResp := alice.Pay(bobInvoice.Bolt11)
|
||||||
|
invoiceResult := p.BreezClient().Node().GetInvoice(bobInvoice.PaymentHash)
|
||||||
|
|
||||||
|
assert.Equal(p.t, payResp.PaymentPreimage, invoiceResult.PaymentPreimage)
|
||||||
|
assert.Equal(p.t, amountMsat, invoiceResult.AmountReceivedMsat)
|
||||||
|
}
|
||||||
55
itest/tag_test.go
Normal file
55
itest/tag_test.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerPaymentWithTag(p *testParams) {
|
||||||
|
expected := "{\"apiKey\": \"11111111111111\"}"
|
||||||
|
i := p.BreezClient().Node().CreateBolt11Invoice(&lntest.CreateInvoiceOptions{
|
||||||
|
AmountMsat: 25000000,
|
||||||
|
})
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: i.PaymentHash,
|
||||||
|
PaymentSecret: i.PaymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(25000000),
|
||||||
|
OutgoingAmountMsat: int64(21000000),
|
||||||
|
Tag: expected,
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
pgxPool, err := pgxpool.Connect(p.h.Ctx, p.lsp.PostgresBackend().ConnectionString())
|
||||||
|
if err != nil {
|
||||||
|
p.h.T.Fatalf("Failed to connect to postgres backend: %v", err)
|
||||||
|
}
|
||||||
|
defer pgxPool.Close()
|
||||||
|
|
||||||
|
rows, err := pgxPool.Query(
|
||||||
|
p.h.Ctx,
|
||||||
|
"SELECT tag FROM public.payments WHERE payment_hash=$1",
|
||||||
|
i.PaymentHash,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
p.h.T.Fatalf("Failed to query tag: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer rows.Close()
|
||||||
|
if !rows.Next() {
|
||||||
|
p.h.T.Fatal("No rows found")
|
||||||
|
}
|
||||||
|
|
||||||
|
var actual string
|
||||||
|
err = rows.Scan(&actual)
|
||||||
|
if err != nil {
|
||||||
|
p.h.T.Fatalf("Failed to get tag from row: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.Equal(p.h.T, expected, actual)
|
||||||
|
}
|
||||||
54
itest/test_common.go
Normal file
54
itest/test_common.go
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"log"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GenerateRandomBytes(n int) ([]byte, error) {
|
||||||
|
b := make([]byte, n)
|
||||||
|
_, err := rand.Read(b)
|
||||||
|
// Note that err == nil only if we read len(b) bytes.
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func AssertChannelCapacity(
|
||||||
|
t *testing.T,
|
||||||
|
outerAmountMsat uint64,
|
||||||
|
capacityMsat uint64,
|
||||||
|
) {
|
||||||
|
assert.Equal(t, ((outerAmountMsat/1000)+100000)*1000, capacityMsat)
|
||||||
|
}
|
||||||
|
|
||||||
|
func calculateInnerAmountMsat(lsp LspNode, outerAmountMsat uint64, params *lspd.OpeningFeeParams) uint64 {
|
||||||
|
var fee uint64
|
||||||
|
log.Printf("%+v", params)
|
||||||
|
if params == nil {
|
||||||
|
fee = outerAmountMsat * 40 / 10_000 / 1_000 * 1_000
|
||||||
|
if fee < 2000000 {
|
||||||
|
fee = 2000000
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
fee = outerAmountMsat * uint64(params.Proportional) / 1_000_000 / 1_000 * 1_000
|
||||||
|
if fee < params.MinMsat {
|
||||||
|
fee = params.MinMsat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if fee > outerAmountMsat {
|
||||||
|
lsp.Harness().Fatalf("Fee is higher than amount")
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("outer: %v, fee: %v", outerAmountMsat, fee)
|
||||||
|
return outerAmountMsat - fee
|
||||||
|
}
|
||||||
|
|
||||||
|
var publicChanAmount uint64 = 1000183
|
||||||
46
itest/test_params.go
Normal file
46
itest/test_params.go
Normal file
@@ -0,0 +1,46 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LspFunc func(h *lntest.TestHarness, m *lntest.Miner, mem *mempoolApi, c *config.NodeConfig) LspNode
|
||||||
|
type ClientFunc func(h *lntest.TestHarness, m *lntest.Miner) BreezClient
|
||||||
|
|
||||||
|
type testParams struct {
|
||||||
|
t *testing.T
|
||||||
|
h *lntest.TestHarness
|
||||||
|
m *lntest.Miner
|
||||||
|
mem *mempoolApi
|
||||||
|
c BreezClient
|
||||||
|
lsp LspNode
|
||||||
|
lspFunc LspFunc
|
||||||
|
clientFunc ClientFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *testParams) T() *testing.T {
|
||||||
|
return h.t
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *testParams) Miner() *lntest.Miner {
|
||||||
|
return h.m
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *testParams) Mempool() *mempoolApi {
|
||||||
|
return h.mem
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *testParams) Lsp() LspNode {
|
||||||
|
return h.lsp
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *testParams) Harness() *lntest.TestHarness {
|
||||||
|
return h.h
|
||||||
|
}
|
||||||
|
|
||||||
|
func (h *testParams) BreezClient() BreezClient {
|
||||||
|
return h.c
|
||||||
|
}
|
||||||
77
itest/zero_conf_utxo_test.go
Normal file
77
itest/zero_conf_utxo_test.go
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testOpenZeroConfUtxo(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
|
||||||
|
minConfs := uint32(0)
|
||||||
|
lsp := p.lspFunc(p.h, p.m, p.mem, &config.NodeConfig{MinConfs: &minConfs})
|
||||||
|
lsp.Start()
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
})
|
||||||
|
channelId := alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
tempaddr := lsp.LightningNode().GetNewAddress()
|
||||||
|
p.m.SendToAddress(tempaddr, 210000)
|
||||||
|
p.m.MineBlocks(6)
|
||||||
|
lsp.LightningNode().WaitForSync()
|
||||||
|
|
||||||
|
initialHeight := p.m.GetBlockHeight()
|
||||||
|
addr := lsp.LightningNode().GetNewAddress()
|
||||||
|
lsp.LightningNode().SendToAddress(addr, 200000)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(2100000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(lsp, outerAmountMsat, nil)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: lsp,
|
||||||
|
})
|
||||||
|
p.BreezClient().SetHtlcAcceptor(innerAmountMsat)
|
||||||
|
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
route := constructRoute(lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat)
|
||||||
|
payResp, err := alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
lntest.CheckError(p.t, err)
|
||||||
|
bobInvoice := p.BreezClient().Node().GetInvoice(payResp.PaymentHash)
|
||||||
|
|
||||||
|
assert.Equal(p.t, payResp.PaymentPreimage, bobInvoice.PaymentPreimage)
|
||||||
|
assert.Equal(p.t, innerAmountMsat, bobInvoice.AmountReceivedMsat)
|
||||||
|
|
||||||
|
// Make sure there's not accidently a block mined in between
|
||||||
|
finalHeight := p.m.GetBlockHeight()
|
||||||
|
assert.Equal(p.t, initialHeight, finalHeight)
|
||||||
|
}
|
||||||
64
itest/zero_reserve_test.go
Normal file
64
itest/zero_reserve_test.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package itest
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lntest"
|
||||||
|
lspd "github.com/breez/lspd/rpc"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
)
|
||||||
|
|
||||||
|
func testZeroReserve(p *testParams) {
|
||||||
|
alice := lntest.NewClnNode(p.h, p.m, "Alice")
|
||||||
|
alice.Start()
|
||||||
|
alice.Fund(10000000)
|
||||||
|
p.lsp.LightningNode().Fund(10000000)
|
||||||
|
|
||||||
|
log.Print("Opening channel between Alice and the lsp")
|
||||||
|
channel := alice.OpenChannel(p.lsp.LightningNode(), &lntest.OpenChannelOptions{
|
||||||
|
AmountSat: publicChanAmount,
|
||||||
|
})
|
||||||
|
channelId := alice.WaitForChannelReady(channel)
|
||||||
|
|
||||||
|
log.Printf("Adding bob's invoices")
|
||||||
|
outerAmountMsat := uint64(2100000)
|
||||||
|
innerAmountMsat := calculateInnerAmountMsat(p.lsp, outerAmountMsat, nil)
|
||||||
|
description := "Please pay me"
|
||||||
|
innerInvoice, outerInvoice := GenerateInvoices(p.BreezClient(),
|
||||||
|
generateInvoicesRequest{
|
||||||
|
innerAmountMsat: innerAmountMsat,
|
||||||
|
outerAmountMsat: outerAmountMsat,
|
||||||
|
description: description,
|
||||||
|
lsp: p.lsp,
|
||||||
|
})
|
||||||
|
p.BreezClient().SetHtlcAcceptor(innerAmountMsat)
|
||||||
|
|
||||||
|
log.Print("Connecting bob to lspd")
|
||||||
|
p.BreezClient().Node().ConnectPeer(p.lsp.LightningNode())
|
||||||
|
|
||||||
|
log.Printf("Registering payment with lsp")
|
||||||
|
RegisterPayment(p.lsp, &lspd.PaymentInformation{
|
||||||
|
PaymentHash: innerInvoice.paymentHash,
|
||||||
|
PaymentSecret: innerInvoice.paymentSecret,
|
||||||
|
Destination: p.BreezClient().Node().NodeId(),
|
||||||
|
IncomingAmountMsat: int64(outerAmountMsat),
|
||||||
|
OutgoingAmountMsat: int64(innerAmountMsat),
|
||||||
|
}, false)
|
||||||
|
|
||||||
|
// TODO: Fix race waiting for htlc interceptor.
|
||||||
|
log.Printf("Waiting %v to allow htlc interceptor to activate.", htlcInterceptorDelay)
|
||||||
|
<-time.After(htlcInterceptorDelay)
|
||||||
|
log.Printf("Alice paying")
|
||||||
|
route := constructRoute(p.lsp.LightningNode(), p.BreezClient().Node(), channelId, lntest.NewShortChanIDFromString("1x0x0"), outerAmountMsat)
|
||||||
|
alice.PayViaRoute(outerAmountMsat, outerInvoice.paymentHash, outerInvoice.paymentSecret, route)
|
||||||
|
|
||||||
|
// Make sure balance is correct
|
||||||
|
chans := p.BreezClient().Node().GetChannels()
|
||||||
|
assert.Equal(p.t, 1, len(chans))
|
||||||
|
|
||||||
|
c := chans[0]
|
||||||
|
assert.Equal(p.t, c.RemoteReserveMsat, c.CapacityMsat/100)
|
||||||
|
log.Printf("local reserve: %d, remote reserve: %d", c.LocalReserveMsat, c.RemoteReserveMsat)
|
||||||
|
assert.Zero(p.t, c.LocalReserveMsat)
|
||||||
|
}
|
||||||
41
lightning/client.go
Normal file
41
lightning/client.go
Normal file
@@ -0,0 +1,41 @@
|
|||||||
|
package lightning
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/basetypes"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GetInfoResult struct {
|
||||||
|
Alias string
|
||||||
|
Pubkey string
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetChannelResult struct {
|
||||||
|
InitialChannelID basetypes.ShortChannelID
|
||||||
|
ConfirmedChannelID basetypes.ShortChannelID
|
||||||
|
}
|
||||||
|
|
||||||
|
type OpenChannelRequest struct {
|
||||||
|
Destination []byte
|
||||||
|
CapacitySat uint64
|
||||||
|
MinHtlcMsat uint64
|
||||||
|
IsPrivate bool
|
||||||
|
IsZeroConf bool
|
||||||
|
MinConfs *uint32
|
||||||
|
FeeSatPerVByte *float64
|
||||||
|
TargetConf *uint32
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client interface {
|
||||||
|
GetInfo() (*GetInfoResult, error)
|
||||||
|
IsConnected(destination []byte) (bool, error)
|
||||||
|
OpenChannel(req *OpenChannelRequest) (*wire.OutPoint, error)
|
||||||
|
GetChannel(peerID []byte, channelPoint wire.OutPoint) (*GetChannelResult, error)
|
||||||
|
GetPeerId(scid *basetypes.ShortChannelID) ([]byte, error)
|
||||||
|
GetNodeChannelCount(nodeID []byte) (int, error)
|
||||||
|
GetClosedChannels(nodeID string, channelPoints map[string]uint64) (map[string]uint64, error)
|
||||||
|
WaitOnline(peerID []byte, deadline time.Time) error
|
||||||
|
WaitChannelActive(peerID []byte, deadline time.Time) error
|
||||||
|
}
|
||||||
528
lnd/client.go
Normal file
528
lnd/client.go
Normal file
@@ -0,0 +1,528 @@
|
|||||||
|
package lnd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/x509"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/basetypes"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
"github.com/breez/lspd/lightning"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LndClient struct {
|
||||||
|
client lnrpc.LightningClient
|
||||||
|
routerClient routerrpc.RouterClient
|
||||||
|
chainNotifierClient chainrpc.ChainNotifierClient
|
||||||
|
conn *grpc.ClientConn
|
||||||
|
listenerCtx context.Context
|
||||||
|
listenerCancel context.CancelFunc
|
||||||
|
peersubs map[string]map[uint64]chan struct{}
|
||||||
|
chansubs map[string]map[uint64]chan struct{}
|
||||||
|
submtx sync.RWMutex
|
||||||
|
index uint64
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLndClient(conf *config.LndConfig) (*LndClient, error) {
|
||||||
|
_, err := hex.DecodeString(conf.Macaroon)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to decode macaroon: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Creds file to connect to LND gRPC
|
||||||
|
cp := x509.NewCertPool()
|
||||||
|
if !cp.AppendCertsFromPEM([]byte(conf.Cert)) {
|
||||||
|
return nil, fmt.Errorf("credentials: failed to append certificates")
|
||||||
|
}
|
||||||
|
creds := credentials.NewClientTLSFromCert(cp, "")
|
||||||
|
macCred := NewMacaroonCredential(conf.Macaroon)
|
||||||
|
|
||||||
|
// Address of an LND instance
|
||||||
|
conn, err := grpc.Dial(
|
||||||
|
conf.Address,
|
||||||
|
grpc.WithTransportCredentials(creds),
|
||||||
|
grpc.WithPerRPCCredentials(macCred),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("Failed to connect to LND gRPC: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client := lnrpc.NewLightningClient(conn)
|
||||||
|
routerClient := routerrpc.NewRouterClient(conn)
|
||||||
|
chainNotifierClient := chainrpc.NewChainNotifierClient(conn)
|
||||||
|
return &LndClient{
|
||||||
|
client: client,
|
||||||
|
routerClient: routerClient,
|
||||||
|
chainNotifierClient: chainNotifierClient,
|
||||||
|
conn: conn,
|
||||||
|
peersubs: make(map[string]map[uint64]chan struct{}),
|
||||||
|
chansubs: make(map[string]map[uint64]chan struct{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) Close() {
|
||||||
|
cancel := c.listenerCancel
|
||||||
|
if cancel != nil {
|
||||||
|
cancel()
|
||||||
|
}
|
||||||
|
c.conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) StartListeners() {
|
||||||
|
c.listenerCtx, c.listenerCancel = context.WithCancel(context.Background())
|
||||||
|
go c.listenPeerEvents()
|
||||||
|
go c.listenChannelEvents()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) listenPeerEvents() {
|
||||||
|
ctx := c.listenerCtx
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, err := c.client.SubscribePeerEvents(
|
||||||
|
ctx,
|
||||||
|
&lnrpc.PeerEventSubscription{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("SubscribePeerEvents: %v", err)
|
||||||
|
<-time.After(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := sub.Recv()
|
||||||
|
if err != nil {
|
||||||
|
status, ok := status.FromError(err)
|
||||||
|
if ok && status.Code() == codes.Canceled {
|
||||||
|
log.Printf("listenPeerEvents: Got code canceled. Break.")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("unexpected error in listenPeerEvents: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Type != lnrpc.PeerEvent_PEER_ONLINE {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.submtx.RLock()
|
||||||
|
subs, ok := c.peersubs[msg.PubKey]
|
||||||
|
if ok {
|
||||||
|
for _, sub := range subs {
|
||||||
|
sub <- struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.submtx.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
<-time.After(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) listenChannelEvents() {
|
||||||
|
ctx := c.listenerCtx
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sub, err := c.client.SubscribeChannelEvents(
|
||||||
|
ctx,
|
||||||
|
&lnrpc.ChannelEventSubscription{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("listenChannelEvents: SubscribeChannelEvents: %v", err)
|
||||||
|
<-time.After(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
msg, err := sub.Recv()
|
||||||
|
if err != nil {
|
||||||
|
status, ok := status.FromError(err)
|
||||||
|
if ok && status.Code() == codes.Canceled {
|
||||||
|
log.Printf("listenChannelEvents: Got code canceled. Break.")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("unexpected error in listenChannelEvents: %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
if msg.Type != lnrpc.ChannelEventUpdate_ACTIVE_CHANNEL {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
ch := msg.GetActiveChannel()
|
||||||
|
point, err := extractChannelPoint(ch)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("listenChannelEvents: Failed to extract channel point %+v: %v", ch, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
c.submtx.RLock()
|
||||||
|
subs, ok := c.chansubs[point]
|
||||||
|
if ok {
|
||||||
|
for _, sub := range subs {
|
||||||
|
sub <- struct{}{}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.submtx.RUnlock()
|
||||||
|
}
|
||||||
|
|
||||||
|
<-time.After(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func extractChannelPoint(cp *lnrpc.ChannelPoint) (string, error) {
|
||||||
|
str := cp.GetFundingTxidStr()
|
||||||
|
if str == "" {
|
||||||
|
b := cp.GetFundingTxidBytes()
|
||||||
|
h, err := chainhash.NewHash(b)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
str = h.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Sprintf("%s:%d", str, cp.OutputIndex), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) GetInfo() (*lightning.GetInfoResult, error) {
|
||||||
|
info, err := c.client.GetInfo(context.Background(), &lnrpc.GetInfoRequest{})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("LND: client.GetInfo() error: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &lightning.GetInfoResult{
|
||||||
|
Alias: info.Alias,
|
||||||
|
Pubkey: info.IdentityPubkey,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) IsConnected(destination []byte) (bool, error) {
|
||||||
|
pubkey := hex.EncodeToString(destination)
|
||||||
|
|
||||||
|
r, err := c.client.GetPeerConnected(context.Background(), &lnrpc.GetPeerConnectedRequest{
|
||||||
|
Pubkey: pubkey,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("LND: client.GetPeerConnected() error: %v", err)
|
||||||
|
return false, fmt.Errorf("LND: client.GetPeerConnected() error: %w", err)
|
||||||
|
}
|
||||||
|
if r.Connected {
|
||||||
|
log.Printf("LND: destination online: %x", destination)
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("LND: destination offline: %x", destination)
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) OpenChannel(req *lightning.OpenChannelRequest) (*wire.OutPoint, error) {
|
||||||
|
lnReq := &lnrpc.OpenChannelRequest{
|
||||||
|
NodePubkey: req.Destination,
|
||||||
|
LocalFundingAmount: int64(req.CapacitySat),
|
||||||
|
PushSat: 0,
|
||||||
|
Private: req.IsPrivate,
|
||||||
|
CommitmentType: lnrpc.CommitmentType_ANCHORS,
|
||||||
|
ZeroConf: req.IsZeroConf,
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.MinConfs != nil {
|
||||||
|
minConfs := *req.MinConfs
|
||||||
|
lnReq.MinConfs = int32(minConfs)
|
||||||
|
if minConfs == 0 {
|
||||||
|
lnReq.SpendUnconfirmed = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if req.FeeSatPerVByte != nil {
|
||||||
|
lnReq.SatPerVbyte = uint64(*req.FeeSatPerVByte)
|
||||||
|
} else if req.TargetConf != nil {
|
||||||
|
lnReq.TargetConf = int32(*req.TargetConf)
|
||||||
|
}
|
||||||
|
|
||||||
|
channelPoint, err := c.client.OpenChannelSync(context.Background(), lnReq)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("LND: client.OpenChannelSync(%x, %v) error: %v", req.Destination, req.CapacitySat, err)
|
||||||
|
return nil, fmt.Errorf("LND: OpenChannel() error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := basetypes.NewOutPoint(channelPoint.GetFundingTxidBytes(), channelPoint.OutputIndex)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("LND: OpenChannel returned invalid outpoint. error: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) GetChannel(peerID []byte, channelPoint wire.OutPoint) (*lightning.GetChannelResult, error) {
|
||||||
|
r, err := c.client.ListChannels(context.Background(), &lnrpc.ListChannelsRequest{Peer: peerID})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("client.ListChannels(%x) error: %v", peerID, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
channelPointStr := channelPoint.String()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, c := range r.Channels {
|
||||||
|
log.Printf("getChannel(%x): %v", peerID, c.ChanId)
|
||||||
|
if c.ChannelPoint == channelPointStr && c.Active {
|
||||||
|
confirmedChanId := c.ChanId
|
||||||
|
if c.ZeroConf {
|
||||||
|
confirmedChanId = c.ZeroConfConfirmedScid
|
||||||
|
if confirmedChanId == hop.Source.ToUint64() {
|
||||||
|
confirmedChanId = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &lightning.GetChannelResult{
|
||||||
|
InitialChannelID: basetypes.ShortChannelID(c.ChanId),
|
||||||
|
ConfirmedChannelID: basetypes.ShortChannelID(confirmedChanId),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("No channel found: getChannel(%x)", peerID)
|
||||||
|
return nil, fmt.Errorf("no channel found")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) GetNodeChannelCount(nodeID []byte) (int, error) {
|
||||||
|
nodeIDStr := hex.EncodeToString(nodeID)
|
||||||
|
listResponse, err := c.client.ListChannels(context.Background(), &lnrpc.ListChannelsRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingResponse, err := c.client.PendingChannels(context.Background(), &lnrpc.PendingChannelsRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
count := 0
|
||||||
|
for _, channel := range listResponse.Channels {
|
||||||
|
if channel.RemotePubkey == nodeIDStr {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, p := range pendingResponse.PendingOpenChannels {
|
||||||
|
if p.Channel.RemoteNodePub == nodeIDStr {
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return count, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) GetClosedChannels(nodeID string, channelPoints map[string]uint64) (map[string]uint64, error) {
|
||||||
|
r := make(map[string]uint64)
|
||||||
|
if len(channelPoints) == 0 {
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
waitingCloseChannels, err := c.getWaitingCloseChannels(nodeID)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
wcc := make(map[string]struct{})
|
||||||
|
for _, c := range waitingCloseChannels {
|
||||||
|
wcc[c.Channel.ChannelPoint] = struct{}{}
|
||||||
|
}
|
||||||
|
for c, h := range channelPoints {
|
||||||
|
if _, ok := wcc[c]; !ok {
|
||||||
|
r[c] = h
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) getWaitingCloseChannels(nodeID string) ([]*lnrpc.PendingChannelsResponse_WaitingCloseChannel, error) {
|
||||||
|
pendingResponse, err := c.client.PendingChannels(context.Background(), &lnrpc.PendingChannelsRequest{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var waitingCloseChannels []*lnrpc.PendingChannelsResponse_WaitingCloseChannel
|
||||||
|
for _, p := range pendingResponse.WaitingCloseChannels {
|
||||||
|
if p.Channel.RemoteNodePub == nodeID {
|
||||||
|
waitingCloseChannels = append(waitingCloseChannels, p)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return waitingCloseChannels, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) GetPeerId(scid *basetypes.ShortChannelID) ([]byte, error) {
|
||||||
|
scidu64 := uint64(*scid)
|
||||||
|
peer, err := c.client.GetPeerIdByScid(context.Background(), &lnrpc.GetPeerIdByScidRequest{
|
||||||
|
Scid: scidu64,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if peer.PeerId == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
peerid, _ := hex.DecodeString(peer.PeerId)
|
||||||
|
return peerid, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) WaitOnline(peerID []byte, deadline time.Time) error {
|
||||||
|
pkStr := hex.EncodeToString(peerID)
|
||||||
|
signal := make(chan struct{}, 10)
|
||||||
|
defer close(signal)
|
||||||
|
|
||||||
|
c.submtx.Lock()
|
||||||
|
subid := c.index
|
||||||
|
c.index++
|
||||||
|
subs, ok := c.peersubs[pkStr]
|
||||||
|
if !ok {
|
||||||
|
subs = make(map[uint64]chan struct{})
|
||||||
|
c.peersubs[pkStr] = subs
|
||||||
|
}
|
||||||
|
|
||||||
|
subs[subid] = signal
|
||||||
|
c.submtx.Unlock()
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
c.submtx.Lock()
|
||||||
|
subs, ok := c.peersubs[pkStr]
|
||||||
|
if ok {
|
||||||
|
delete(subs, subid)
|
||||||
|
if len(subs) == 0 {
|
||||||
|
delete(c.peersubs, pkStr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.submtx.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
connected, err := c.IsConnected(peerID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if connected {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-signal:
|
||||||
|
return nil
|
||||||
|
case <-time.After(time.Until(deadline)):
|
||||||
|
return fmt.Errorf("deadline exceeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *LndClient) WaitChannelActive(peerID []byte, deadline time.Time) error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
// Fetch the channels for this peer
|
||||||
|
chans, err := c.client.ListChannels(ctx, &lnrpc.ListChannelsRequest{
|
||||||
|
Peer: peerID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(chans.Channels) == 0 {
|
||||||
|
return fmt.Errorf("no channels with peer")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit now if a channel is already active.
|
||||||
|
for _, ch := range chans.Channels {
|
||||||
|
if ch.Active {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
signal := make(chan struct{}, 10)
|
||||||
|
defer close(signal)
|
||||||
|
|
||||||
|
// Subscribe to channel active events from this channel
|
||||||
|
c.submtx.Lock()
|
||||||
|
for _, ch := range chans.Channels {
|
||||||
|
chansignal := make(chan struct{}, 10)
|
||||||
|
defer close(chansignal)
|
||||||
|
|
||||||
|
// forward signals from all channels to the signal aggregate
|
||||||
|
go func(c chan struct{}) {
|
||||||
|
for msg := range c {
|
||||||
|
signal <- msg
|
||||||
|
}
|
||||||
|
}(chansignal)
|
||||||
|
|
||||||
|
outpoint := ch.ChannelPoint
|
||||||
|
subid := c.index
|
||||||
|
c.index++
|
||||||
|
subs, ok := c.chansubs[outpoint]
|
||||||
|
if !ok {
|
||||||
|
subs = make(map[uint64]chan struct{})
|
||||||
|
c.chansubs[outpoint] = subs
|
||||||
|
}
|
||||||
|
|
||||||
|
subs[subid] = chansignal
|
||||||
|
defer func() {
|
||||||
|
c.submtx.Lock()
|
||||||
|
subs, ok := c.chansubs[outpoint]
|
||||||
|
if ok {
|
||||||
|
delete(subs, subid)
|
||||||
|
if len(subs) == 0 {
|
||||||
|
delete(c.chansubs, outpoint)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.submtx.Unlock()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
c.submtx.Unlock()
|
||||||
|
|
||||||
|
// Fetch the channels for this peer again, so there is no gap between the
|
||||||
|
// subscription and the call.
|
||||||
|
chans, err = c.client.ListChannels(ctx, &lnrpc.ListChannelsRequest{
|
||||||
|
Peer: peerID,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Exit now if a channel is already active.
|
||||||
|
for _, ch := range chans.Channels {
|
||||||
|
if ch.Active {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
select {
|
||||||
|
case <-signal:
|
||||||
|
return nil
|
||||||
|
case <-time.After(time.Until(deadline)):
|
||||||
|
return fmt.Errorf("deadline exceeded")
|
||||||
|
}
|
||||||
|
}
|
||||||
12
lnd/forwarding_event_store.go
Normal file
12
lnd/forwarding_event_store.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
package lnd
|
||||||
|
|
||||||
|
type CopyFromSource interface {
|
||||||
|
Next() bool
|
||||||
|
Values() ([]interface{}, error)
|
||||||
|
Err() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardingEventStore interface {
|
||||||
|
LastForwardingEvent() (int64, error)
|
||||||
|
InsertForwardingEvents(rowSrc CopyFromSource) error
|
||||||
|
}
|
||||||
183
lnd/forwarding_history.go
Normal file
183
lnd/forwarding_history.go
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
package lnd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/interceptor"
|
||||||
|
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/chainrpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type copyFromEvents struct {
|
||||||
|
events []*lnrpc.ForwardingEvent
|
||||||
|
idx int
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfe *copyFromEvents) Next() bool {
|
||||||
|
cfe.idx++
|
||||||
|
return cfe.idx < len(cfe.events)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfe *copyFromEvents) Values() ([]interface{}, error) {
|
||||||
|
event := cfe.events[cfe.idx]
|
||||||
|
values := []interface{}{
|
||||||
|
event.TimestampNs,
|
||||||
|
int64(event.ChanIdIn), int64(event.ChanIdOut),
|
||||||
|
event.AmtInMsat, event.AmtOutMsat}
|
||||||
|
return values, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (cfe *copyFromEvents) Err() error {
|
||||||
|
return cfe.err
|
||||||
|
}
|
||||||
|
|
||||||
|
type ForwardingHistorySync struct {
|
||||||
|
client *LndClient
|
||||||
|
interceptStore interceptor.InterceptStore
|
||||||
|
forwardingStore ForwardingEventStore
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewForwardingHistorySync(
|
||||||
|
client *LndClient,
|
||||||
|
interceptStore interceptor.InterceptStore,
|
||||||
|
forwardingStore ForwardingEventStore,
|
||||||
|
) *ForwardingHistorySync {
|
||||||
|
return &ForwardingHistorySync{
|
||||||
|
client: client,
|
||||||
|
interceptStore: interceptStore,
|
||||||
|
forwardingStore: forwardingStore,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingHistorySync) ChannelsSynchronize(ctx context.Context) {
|
||||||
|
lastSync := time.Now().Add(-6 * time.Minute)
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
stream, err := s.client.chainNotifierClient.RegisterBlockEpochNtfn(ctx, &chainrpc.BlockEpoch{})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("chainNotifierClient.RegisterBlockEpochNtfn(): %v", err)
|
||||||
|
<-time.After(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err := stream.Recv()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("stream.Recv: %v", err)
|
||||||
|
<-time.After(time.Second)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
if lastSync.Add(5 * time.Minute).Before(time.Now()) {
|
||||||
|
select {
|
||||||
|
case <-ctx.Done():
|
||||||
|
return
|
||||||
|
case <-time.After(1 * time.Minute):
|
||||||
|
}
|
||||||
|
err = s.ChannelsSynchronizeOnce()
|
||||||
|
lastSync = time.Now()
|
||||||
|
log.Printf("channelsSynchronizeOnce() err: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingHistorySync) ChannelsSynchronizeOnce() error {
|
||||||
|
log.Printf("channelsSynchronizeOnce - begin")
|
||||||
|
channels, err := s.client.client.ListChannels(context.Background(), &lnrpc.ListChannelsRequest{PrivateOnly: true})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ListChannels error: %v", err)
|
||||||
|
return fmt.Errorf("client.ListChannels() error: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("channelsSynchronizeOnce - received channels")
|
||||||
|
lastUpdate := time.Now()
|
||||||
|
for _, c := range channels.Channels {
|
||||||
|
nodeID, err := hex.DecodeString(c.RemotePubkey)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("hex.DecodeString in channelsSynchronizeOnce error: %v", err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
confirmedChanId := c.ChanId
|
||||||
|
if c.ZeroConf {
|
||||||
|
confirmedChanId = c.ZeroConfConfirmedScid
|
||||||
|
if confirmedChanId == hop.Source.ToUint64() {
|
||||||
|
confirmedChanId = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = s.interceptStore.InsertChannel(c.ChanId, confirmedChanId, c.ChannelPoint, nodeID, lastUpdate)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("insertChannel(%v, %v, %x) in channelsSynchronizeOnce error: %v", c.ChanId, c.ChannelPoint, nodeID, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
log.Printf("channelsSynchronizeOnce - done")
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingHistorySync) ForwardingHistorySynchronize(ctx context.Context) {
|
||||||
|
for {
|
||||||
|
if ctx.Err() != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
err := s.ForwardingHistorySynchronizeOnce()
|
||||||
|
log.Printf("forwardingHistorySynchronizeOnce() err: %v", err)
|
||||||
|
select {
|
||||||
|
case <-time.After(1 * time.Minute):
|
||||||
|
case <-ctx.Done():
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingHistorySync) ForwardingHistorySynchronizeOnce() error {
|
||||||
|
last, err := s.forwardingStore.LastForwardingEvent()
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("lastForwardingEvent() error: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("last1: %v", last)
|
||||||
|
last = last/1_000_000_000 - 1*3600
|
||||||
|
if last <= 0 {
|
||||||
|
last = 1
|
||||||
|
}
|
||||||
|
log.Printf("last2: %v", last)
|
||||||
|
now := time.Now()
|
||||||
|
endTime := uint64(now.Add(time.Hour * 24).Unix())
|
||||||
|
indexOffset := uint32(0)
|
||||||
|
for {
|
||||||
|
forwardHistory, err := s.client.client.ForwardingHistory(context.Background(), &lnrpc.ForwardingHistoryRequest{
|
||||||
|
StartTime: uint64(last),
|
||||||
|
EndTime: endTime,
|
||||||
|
NumMaxEvents: 10000,
|
||||||
|
IndexOffset: indexOffset,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("ForwardingHistory error: %v", err)
|
||||||
|
return fmt.Errorf("client.ForwardingHistory() error: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("Offset: %v, Events: %v", indexOffset, len(forwardHistory.ForwardingEvents))
|
||||||
|
if len(forwardHistory.ForwardingEvents) == 0 {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
indexOffset = forwardHistory.LastOffsetIndex
|
||||||
|
cfe := copyFromEvents{events: forwardHistory.ForwardingEvents, idx: -1}
|
||||||
|
err = s.forwardingStore.InsertForwardingEvents(&cfe)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("insertForwardingEvents() error: %v", err)
|
||||||
|
return fmt.Errorf("insertForwardingEvents() error: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
259
lnd/interceptor.go
Normal file
259
lnd/interceptor.go
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
package lnd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"log"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/basetypes"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
"github.com/breez/lspd/interceptor"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
sphinx "github.com/lightningnetwork/lightning-onion"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc/routerrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"github.com/lightningnetwork/lnd/record"
|
||||||
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"google.golang.org/grpc/codes"
|
||||||
|
"google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
type LndHtlcInterceptor struct {
|
||||||
|
fwsync *ForwardingHistorySync
|
||||||
|
interceptor *interceptor.Interceptor
|
||||||
|
config *config.NodeConfig
|
||||||
|
client *LndClient
|
||||||
|
stopRequested bool
|
||||||
|
initWg sync.WaitGroup
|
||||||
|
doneWg sync.WaitGroup
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewLndHtlcInterceptor(
|
||||||
|
conf *config.NodeConfig,
|
||||||
|
client *LndClient,
|
||||||
|
fwsync *ForwardingHistorySync,
|
||||||
|
interceptor *interceptor.Interceptor,
|
||||||
|
) (*LndHtlcInterceptor, error) {
|
||||||
|
i := &LndHtlcInterceptor{
|
||||||
|
config: conf,
|
||||||
|
client: client,
|
||||||
|
fwsync: fwsync,
|
||||||
|
interceptor: interceptor,
|
||||||
|
}
|
||||||
|
|
||||||
|
i.initWg.Add(1)
|
||||||
|
|
||||||
|
return i, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *LndHtlcInterceptor) Start() error {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
i.ctx = ctx
|
||||||
|
i.cancel = cancel
|
||||||
|
i.stopRequested = false
|
||||||
|
go i.fwsync.ForwardingHistorySynchronize(ctx)
|
||||||
|
go i.fwsync.ChannelsSynchronize(ctx)
|
||||||
|
|
||||||
|
return i.intercept()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *LndHtlcInterceptor) Stop() error {
|
||||||
|
// Setting stopRequested to true will make the interceptor stop receiving.
|
||||||
|
i.stopRequested = true
|
||||||
|
|
||||||
|
// Wait until all already received htlcs are handled, responses sent back.
|
||||||
|
i.doneWg.Wait()
|
||||||
|
|
||||||
|
// Close the grpc connection.
|
||||||
|
i.cancel()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *LndHtlcInterceptor) WaitStarted() {
|
||||||
|
i.initWg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *LndHtlcInterceptor) intercept() error {
|
||||||
|
inited := false
|
||||||
|
defer func() {
|
||||||
|
if !inited {
|
||||||
|
i.initWg.Done()
|
||||||
|
}
|
||||||
|
log.Printf("LND intercept(): stopping. Waiting for in-progress interceptions to complete.")
|
||||||
|
i.doneWg.Wait()
|
||||||
|
}()
|
||||||
|
|
||||||
|
for {
|
||||||
|
if i.ctx.Err() != nil {
|
||||||
|
return i.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
log.Printf("Connecting LND HTLC interceptor.")
|
||||||
|
interceptorClient, err := i.client.routerClient.HtlcInterceptor(i.ctx)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("routerClient.HtlcInterceptor(): %v", err)
|
||||||
|
<-time.After(time.Second)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
for {
|
||||||
|
if i.ctx.Err() != nil {
|
||||||
|
return i.ctx.Err()
|
||||||
|
}
|
||||||
|
|
||||||
|
if !inited {
|
||||||
|
inited = true
|
||||||
|
i.initWg.Done()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Stop receiving if stop if requested. The defer func on top of this
|
||||||
|
// function will assure all htlcs that are currently being processed
|
||||||
|
// will complete.
|
||||||
|
if i.stopRequested {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
request, err := interceptorClient.Recv()
|
||||||
|
if err != nil {
|
||||||
|
// If it is just the error result of the context cancellation
|
||||||
|
// the we exit silently.
|
||||||
|
status, ok := status.FromError(err)
|
||||||
|
if ok && status.Code() == codes.Canceled {
|
||||||
|
log.Printf("Got code canceled. Break.")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
// Otherwise it an unexpected error, we fail the test.
|
||||||
|
log.Printf("unexpected error in interceptor.Recv() %v", err)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
i.doneWg.Add(1)
|
||||||
|
go func() {
|
||||||
|
scid := basetypes.ShortChannelID(request.OutgoingRequestedChanId)
|
||||||
|
interceptResult := i.interceptor.Intercept(&scid, request.PaymentHash, request.OutgoingAmountMsat, request.OutgoingExpiry, request.IncomingExpiry)
|
||||||
|
switch interceptResult.Action {
|
||||||
|
case interceptor.INTERCEPT_RESUME_WITH_ONION:
|
||||||
|
onion, err := i.constructOnion(interceptResult, request.OutgoingExpiry, request.PaymentHash)
|
||||||
|
if err == nil {
|
||||||
|
interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||||
|
IncomingCircuitKey: request.IncomingCircuitKey,
|
||||||
|
Action: routerrpc.ResolveHoldForwardAction_RESUME,
|
||||||
|
OutgoingAmountMsat: interceptResult.AmountMsat,
|
||||||
|
OutgoingRequestedChanId: uint64(interceptResult.ChannelId),
|
||||||
|
OnionBlob: onion,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||||
|
IncomingCircuitKey: request.IncomingCircuitKey,
|
||||||
|
Action: routerrpc.ResolveHoldForwardAction_FAIL,
|
||||||
|
FailureCode: lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
case interceptor.INTERCEPT_FAIL_HTLC_WITH_CODE:
|
||||||
|
interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||||
|
IncomingCircuitKey: request.IncomingCircuitKey,
|
||||||
|
Action: routerrpc.ResolveHoldForwardAction_FAIL,
|
||||||
|
FailureCode: i.mapFailureCode(interceptResult.FailureCode),
|
||||||
|
})
|
||||||
|
case interceptor.INTERCEPT_RESUME:
|
||||||
|
fallthrough
|
||||||
|
default:
|
||||||
|
interceptorClient.Send(&routerrpc.ForwardHtlcInterceptResponse{
|
||||||
|
IncomingCircuitKey: request.IncomingCircuitKey,
|
||||||
|
Action: routerrpc.ResolveHoldForwardAction_RESUME,
|
||||||
|
OutgoingAmountMsat: request.OutgoingAmountMsat,
|
||||||
|
OutgoingRequestedChanId: request.OutgoingRequestedChanId,
|
||||||
|
OnionBlob: request.OnionBlob,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
i.doneWg.Done()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
<-time.After(time.Second)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *LndHtlcInterceptor) mapFailureCode(original interceptor.InterceptFailureCode) lnrpc.Failure_FailureCode {
|
||||||
|
switch original {
|
||||||
|
case interceptor.FAILURE_TEMPORARY_CHANNEL_FAILURE:
|
||||||
|
return lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE
|
||||||
|
case interceptor.FAILURE_TEMPORARY_NODE_FAILURE:
|
||||||
|
return lnrpc.Failure_TEMPORARY_NODE_FAILURE
|
||||||
|
case interceptor.FAILURE_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS:
|
||||||
|
return lnrpc.Failure_INCORRECT_OR_UNKNOWN_PAYMENT_DETAILS
|
||||||
|
default:
|
||||||
|
log.Printf("Unknown failure code %v, default to temporary channel failure.", original)
|
||||||
|
return lnrpc.Failure_TEMPORARY_CHANNEL_FAILURE
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *LndHtlcInterceptor) constructOnion(
|
||||||
|
interceptResult interceptor.InterceptResult,
|
||||||
|
reqOutgoingExpiry uint32,
|
||||||
|
reqPaymentHash []byte,
|
||||||
|
) ([]byte, error) {
|
||||||
|
pubKey, err := btcec.ParsePubKey(interceptResult.Destination)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("btcec.ParsePubKey(%x): %v", interceptResult.Destination, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionKey, err := btcec.NewPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("btcec.NewPrivateKey(): %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var addr [32]byte
|
||||||
|
copy(addr[:], interceptResult.PaymentSecret)
|
||||||
|
hop := route.Hop{
|
||||||
|
AmtToForward: lnwire.MilliSatoshi(interceptResult.AmountMsat),
|
||||||
|
OutgoingTimeLock: reqOutgoingExpiry,
|
||||||
|
MPP: record.NewMPP(lnwire.MilliSatoshi(interceptResult.TotalAmountMsat), addr),
|
||||||
|
CustomRecords: make(record.CustomSet),
|
||||||
|
}
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
err = hop.PackHopPayload(&b, uint64(0))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("hop.PackHopPayload(): %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
payload, err := sphinx.NewHopPayload(nil, b.Bytes())
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("sphinx.NewHopPayload(): %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var sphinxPath sphinx.PaymentPath
|
||||||
|
sphinxPath[0] = sphinx.OnionHop{
|
||||||
|
NodePub: *pubKey,
|
||||||
|
HopPayload: payload,
|
||||||
|
}
|
||||||
|
sphinxPacket, err := sphinx.NewOnionPacket(
|
||||||
|
&sphinxPath, sessionKey, reqPaymentHash,
|
||||||
|
sphinx.DeterministicPacketFiller,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("sphinx.NewOnionPacket(): %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var onionBlob bytes.Buffer
|
||||||
|
err = sphinxPacket.Encode(&onionBlob)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("sphinxPacket.Encode(): %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return onionBlob.Bytes(), nil
|
||||||
|
}
|
||||||
25
lnd/macaroon_credential.go
Normal file
25
lnd/macaroon_credential.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package lnd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MacaroonCredential struct {
|
||||||
|
MacaroonHex string
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMacaroonCredential(hex string) *MacaroonCredential {
|
||||||
|
return &MacaroonCredential{
|
||||||
|
MacaroonHex: hex,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MacaroonCredential) RequireTransportSecurity() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MacaroonCredential) GetRequestMetadata(ctx context.Context, uri ...string) (map[string]string, error) {
|
||||||
|
md := make(map[string]string)
|
||||||
|
md["macaroon"] = m.MacaroonHex
|
||||||
|
return md, nil
|
||||||
|
}
|
||||||
185
main.go
Normal file
185
main.go
Normal file
@@ -0,0 +1,185 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/chain"
|
||||||
|
"github.com/breez/lspd/cln"
|
||||||
|
"github.com/breez/lspd/config"
|
||||||
|
"github.com/breez/lspd/interceptor"
|
||||||
|
"github.com/breez/lspd/lnd"
|
||||||
|
"github.com/breez/lspd/mempool"
|
||||||
|
"github.com/breez/lspd/notifications"
|
||||||
|
"github.com/breez/lspd/postgresql"
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) > 1 && os.Args[1] == "genkey" {
|
||||||
|
p, err := btcec.NewPrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("btcec.NewPrivateKey() error: %v", err)
|
||||||
|
}
|
||||||
|
fmt.Printf("LSPD_PRIVATE_KEY=\"%x\"\n", p.Serialize())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
n := os.Getenv("NODES")
|
||||||
|
var nodes []*config.NodeConfig
|
||||||
|
err := json.Unmarshal([]byte(n), &nodes)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to unmarshal NODES env: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(nodes) == 0 {
|
||||||
|
log.Fatalf("need at least one node configured in NODES.")
|
||||||
|
}
|
||||||
|
|
||||||
|
mempoolUrl := os.Getenv("MEMPOOL_API_BASE_URL")
|
||||||
|
if mempoolUrl == "" {
|
||||||
|
log.Fatalf("No mempool url configured.")
|
||||||
|
}
|
||||||
|
|
||||||
|
feeEstimator, err := mempool.NewMempoolClient(mempoolUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to initialize mempool client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var feeStrategy chain.FeeStrategy
|
||||||
|
envFeeStrategy := os.Getenv("MEMPOOL_PRIORITY")
|
||||||
|
switch strings.ToLower(envFeeStrategy) {
|
||||||
|
case "minimum":
|
||||||
|
feeStrategy = chain.FeeStrategyMinimum
|
||||||
|
case "economy":
|
||||||
|
feeStrategy = chain.FeeStrategyEconomy
|
||||||
|
case "hour":
|
||||||
|
feeStrategy = chain.FeeStrategyHour
|
||||||
|
case "halfhour":
|
||||||
|
feeStrategy = chain.FeeStrategyHalfHour
|
||||||
|
case "fastest":
|
||||||
|
feeStrategy = chain.FeeStrategyFastest
|
||||||
|
default:
|
||||||
|
feeStrategy = chain.FeeStrategyEconomy
|
||||||
|
}
|
||||||
|
log.Printf("using mempool api for fee estimation: %v, fee strategy: %v:%v", mempoolUrl, envFeeStrategy, feeStrategy)
|
||||||
|
|
||||||
|
databaseUrl := os.Getenv("DATABASE_URL")
|
||||||
|
pool, err := postgresql.PgConnect(databaseUrl)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("pgConnect() error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
interceptStore := postgresql.NewPostgresInterceptStore(pool)
|
||||||
|
forwardingStore := postgresql.NewForwardingEventStore(pool)
|
||||||
|
notificationsStore := postgresql.NewNotificationsStore(pool)
|
||||||
|
notificationService := notifications.NewNotificationService(notificationsStore)
|
||||||
|
|
||||||
|
var interceptors []interceptor.HtlcInterceptor
|
||||||
|
for _, node := range nodes {
|
||||||
|
var htlcInterceptor interceptor.HtlcInterceptor
|
||||||
|
if node.Lnd != nil {
|
||||||
|
client, err := lnd.NewLndClient(node.Lnd)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to initialize LND client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
client.StartListeners()
|
||||||
|
fwsync := lnd.NewForwardingHistorySync(client, interceptStore, forwardingStore)
|
||||||
|
interceptor := interceptor.NewInterceptor(client, node, interceptStore, feeEstimator, feeStrategy, notificationService)
|
||||||
|
htlcInterceptor, err = lnd.NewLndHtlcInterceptor(node, client, fwsync, interceptor)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to initialize LND interceptor: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if node.Cln != nil {
|
||||||
|
client, err := cln.NewClnClient(node.Cln.SocketPath)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to initialize CLN client: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
interceptor := interceptor.NewInterceptor(client, node, interceptStore, feeEstimator, feeStrategy, notificationService)
|
||||||
|
htlcInterceptor, err = cln.NewClnHtlcInterceptor(node, client, interceptor)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to initialize CLN interceptor: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if htlcInterceptor == nil {
|
||||||
|
log.Fatalf("node has to be either cln or lnd")
|
||||||
|
}
|
||||||
|
|
||||||
|
interceptors = append(interceptors, htlcInterceptor)
|
||||||
|
}
|
||||||
|
|
||||||
|
address := os.Getenv("LISTEN_ADDRESS")
|
||||||
|
certMagicDomain := os.Getenv("CERTMAGIC_DOMAIN")
|
||||||
|
cs := NewChannelOpenerServer(interceptStore)
|
||||||
|
ns := notifications.NewNotificationsServer(notificationsStore)
|
||||||
|
s, err := NewGrpcServer(nodes, address, certMagicDomain, cs, ns)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatalf("failed to initialize grpc server: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
wg.Add(len(interceptors) + 1)
|
||||||
|
|
||||||
|
stopInterceptors := func() {
|
||||||
|
for _, interceptor := range interceptors {
|
||||||
|
interceptor.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, interceptor := range interceptors {
|
||||||
|
i := interceptor
|
||||||
|
go func() {
|
||||||
|
err := i.Start()
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("Interceptor stopped.")
|
||||||
|
} else {
|
||||||
|
log.Printf("FATAL. Interceptor stopped with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Done()
|
||||||
|
|
||||||
|
// If any interceptor stops, stop everything, so we're able to restart using systemd.
|
||||||
|
s.Stop()
|
||||||
|
stopInterceptors()
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
err := s.Start()
|
||||||
|
if err == nil {
|
||||||
|
log.Printf("GRPC server stopped.")
|
||||||
|
} else {
|
||||||
|
log.Printf("FATAL. GRPC server stopped with error: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Done()
|
||||||
|
|
||||||
|
// If the server stops, stop everything else, so we're able to restart using systemd.
|
||||||
|
stopInterceptors()
|
||||||
|
}()
|
||||||
|
|
||||||
|
c := make(chan os.Signal, 1)
|
||||||
|
signal.Notify(c, syscall.SIGINT, syscall.SIGTERM)
|
||||||
|
go func() {
|
||||||
|
sig := <-c
|
||||||
|
log.Printf("Received stop signal %v. Stopping.", sig)
|
||||||
|
|
||||||
|
// Stop everything gracefully on stop signal
|
||||||
|
s.Stop()
|
||||||
|
stopInterceptors()
|
||||||
|
}()
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
log.Printf("lspd exited")
|
||||||
|
}
|
||||||
90
mempool/mempool_client.go
Normal file
90
mempool/mempool_client.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package mempool
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/chain"
|
||||||
|
)
|
||||||
|
|
||||||
|
type MempoolClient struct {
|
||||||
|
apiBaseUrl string
|
||||||
|
httpClient *http.Client
|
||||||
|
}
|
||||||
|
|
||||||
|
type RecommendedFeesResponse struct {
|
||||||
|
FastestFee float64 `json:"fastestFee"`
|
||||||
|
HalfHourFee float64 `json:"halfHourFee"`
|
||||||
|
HourFee float64 `json:"hourFee"`
|
||||||
|
EconomyFee float64 `json:"economyFee"`
|
||||||
|
MinimumFee float64 `json:"minimumFee"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewMempoolClient(apiBaseUrl string) (*MempoolClient, error) {
|
||||||
|
if apiBaseUrl == "" {
|
||||||
|
return nil, fmt.Errorf("apiBaseUrl not set")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !strings.HasSuffix(apiBaseUrl, "/") {
|
||||||
|
apiBaseUrl = apiBaseUrl + "/"
|
||||||
|
}
|
||||||
|
|
||||||
|
return &MempoolClient{
|
||||||
|
apiBaseUrl: apiBaseUrl,
|
||||||
|
httpClient: http.DefaultClient,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *MempoolClient) EstimateFeeRate(
|
||||||
|
ctx context.Context,
|
||||||
|
strategy chain.FeeStrategy,
|
||||||
|
) (*chain.FeeEstimation, error) {
|
||||||
|
req, err := http.NewRequestWithContext(
|
||||||
|
ctx,
|
||||||
|
"GET",
|
||||||
|
m.apiBaseUrl+"fees/recommended",
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("http.NewRequestWithContext error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := m.httpClient.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("httpClient.Do error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
defer resp.Body.Close()
|
||||||
|
if !(resp.StatusCode >= 200 && resp.StatusCode < 300) {
|
||||||
|
return nil, fmt.Errorf("error statuscode %v: %w", resp.StatusCode, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body RecommendedFeesResponse
|
||||||
|
err = json.NewDecoder(resp.Body).Decode(&body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("failed to unmarshal response: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
var rate float64
|
||||||
|
switch strategy {
|
||||||
|
case chain.FeeStrategyFastest:
|
||||||
|
rate = body.FastestFee
|
||||||
|
case chain.FeeStrategyHalfHour:
|
||||||
|
rate = body.HalfHourFee
|
||||||
|
case chain.FeeStrategyHour:
|
||||||
|
rate = body.HourFee
|
||||||
|
case chain.FeeStrategyEconomy:
|
||||||
|
rate = body.EconomyFee
|
||||||
|
case chain.FeeStrategyMinimum:
|
||||||
|
rate = body.MinimumFee
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unsupported fee strategy: %v", strategy)
|
||||||
|
}
|
||||||
|
|
||||||
|
return &chain.FeeEstimation{
|
||||||
|
SatPerVByte: rate,
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
4
notifications/genproto.sh
Executable file
4
notifications/genproto.sh
Executable file
@@ -0,0 +1,4 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
SCRIPTDIR=$(dirname $0)
|
||||||
|
|
||||||
|
protoc --go_out=$SCRIPTDIR --go_opt=paths=source_relative --go-grpc_out=$SCRIPTDIR --go-grpc_opt=paths=source_relative -I=$SCRIPTDIR $SCRIPTDIR/*.proto
|
||||||
73
notifications/notification_service.go
Normal file
73
notifications/notification_service.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"log"
|
||||||
|
"net/http"
|
||||||
|
)
|
||||||
|
|
||||||
|
type NotificationService struct {
|
||||||
|
store Store
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationService(store Store) *NotificationService {
|
||||||
|
return &NotificationService{
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type PaymentReceivedPayload struct {
|
||||||
|
Template string `json:"template" binding:"required,eq=payment_received"`
|
||||||
|
Data struct {
|
||||||
|
PaymentHash string `json:"payment_hash" binding:"required"`
|
||||||
|
} `json:"data"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *NotificationService) Notify(
|
||||||
|
pubkey string,
|
||||||
|
paymenthash string,
|
||||||
|
) (bool, error) {
|
||||||
|
registrations, err := s.store.GetRegistrations(context.Background(), pubkey)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to get notification registrations for %s: %v", pubkey, err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &PaymentReceivedPayload{
|
||||||
|
Template: "payment_received",
|
||||||
|
Data: struct {
|
||||||
|
PaymentHash string "json:\"payment_hash\" binding:\"required\""
|
||||||
|
}{
|
||||||
|
PaymentHash: paymenthash,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err = json.NewEncoder(&buf).Encode(req)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to encode payment notification for %s: %v", pubkey, err)
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
notified := false
|
||||||
|
for _, r := range registrations {
|
||||||
|
resp, err := http.DefaultClient.Post(r, "application/json", &buf)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to send payment notification for %s to %s: %v", pubkey, r, err)
|
||||||
|
// TODO: Remove subscription?
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if resp.StatusCode != 200 {
|
||||||
|
log.Printf("Got non 200 status code (%s) for payment notification for %s to %s: %v", resp.Status, pubkey, r, err)
|
||||||
|
// TODO: Remove subscription?
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
notified = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return notified, nil
|
||||||
|
}
|
||||||
218
notifications/notifications.pb.go
Normal file
218
notifications/notifications.pb.go
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
// Code generated by protoc-gen-go. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// protoc-gen-go v1.28.1
|
||||||
|
// protoc v3.21.12
|
||||||
|
// source: notifications.proto
|
||||||
|
|
||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
|
||||||
|
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
|
||||||
|
reflect "reflect"
|
||||||
|
sync "sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// Verify that this generated code is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
|
||||||
|
// Verify that runtime/protoimpl is sufficiently up-to-date.
|
||||||
|
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubscribeNotificationsRequest struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
|
||||||
|
Url string `protobuf:"bytes,1,opt,name=url,proto3" json:"url,omitempty"`
|
||||||
|
Signature []byte `protobuf:"bytes,2,opt,name=signature,proto3" json:"signature,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SubscribeNotificationsRequest) Reset() {
|
||||||
|
*x = SubscribeNotificationsRequest{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_notifications_proto_msgTypes[0]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SubscribeNotificationsRequest) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SubscribeNotificationsRequest) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SubscribeNotificationsRequest) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_notifications_proto_msgTypes[0]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SubscribeNotificationsRequest.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SubscribeNotificationsRequest) Descriptor() ([]byte, []int) {
|
||||||
|
return file_notifications_proto_rawDescGZIP(), []int{0}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SubscribeNotificationsRequest) GetUrl() string {
|
||||||
|
if x != nil {
|
||||||
|
return x.Url
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SubscribeNotificationsRequest) GetSignature() []byte {
|
||||||
|
if x != nil {
|
||||||
|
return x.Signature
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type SubscribeNotificationsReply struct {
|
||||||
|
state protoimpl.MessageState
|
||||||
|
sizeCache protoimpl.SizeCache
|
||||||
|
unknownFields protoimpl.UnknownFields
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SubscribeNotificationsReply) Reset() {
|
||||||
|
*x = SubscribeNotificationsReply{}
|
||||||
|
if protoimpl.UnsafeEnabled {
|
||||||
|
mi := &file_notifications_proto_msgTypes[1]
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (x *SubscribeNotificationsReply) String() string {
|
||||||
|
return protoimpl.X.MessageStringOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (*SubscribeNotificationsReply) ProtoMessage() {}
|
||||||
|
|
||||||
|
func (x *SubscribeNotificationsReply) ProtoReflect() protoreflect.Message {
|
||||||
|
mi := &file_notifications_proto_msgTypes[1]
|
||||||
|
if protoimpl.UnsafeEnabled && x != nil {
|
||||||
|
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
|
||||||
|
if ms.LoadMessageInfo() == nil {
|
||||||
|
ms.StoreMessageInfo(mi)
|
||||||
|
}
|
||||||
|
return ms
|
||||||
|
}
|
||||||
|
return mi.MessageOf(x)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deprecated: Use SubscribeNotificationsReply.ProtoReflect.Descriptor instead.
|
||||||
|
func (*SubscribeNotificationsReply) Descriptor() ([]byte, []int) {
|
||||||
|
return file_notifications_proto_rawDescGZIP(), []int{1}
|
||||||
|
}
|
||||||
|
|
||||||
|
var File_notifications_proto protoreflect.FileDescriptor
|
||||||
|
|
||||||
|
var file_notifications_proto_rawDesc = []byte{
|
||||||
|
0x0a, 0x13, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e,
|
||||||
|
0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x0d, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74,
|
||||||
|
0x69, 0x6f, 0x6e, 0x73, 0x22, 0x4f, 0x0a, 0x1d, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62,
|
||||||
|
0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65,
|
||||||
|
0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x10, 0x0a, 0x03, 0x75, 0x72, 0x6c, 0x18, 0x01, 0x20, 0x01,
|
||||||
|
0x28, 0x09, 0x52, 0x03, 0x75, 0x72, 0x6c, 0x12, 0x1c, 0x0a, 0x09, 0x73, 0x69, 0x67, 0x6e, 0x61,
|
||||||
|
0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x69, 0x67, 0x6e,
|
||||||
|
0x61, 0x74, 0x75, 0x72, 0x65, 0x22, 0x1d, 0x0a, 0x1b, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69,
|
||||||
|
0x62, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52,
|
||||||
|
0x65, 0x70, 0x6c, 0x79, 0x32, 0x85, 0x01, 0x0a, 0x0d, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63,
|
||||||
|
0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x74, 0x0a, 0x16, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72,
|
||||||
|
0x69, 0x62, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73,
|
||||||
|
0x12, 0x2c, 0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73,
|
||||||
|
0x2e, 0x53, 0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69,
|
||||||
|
0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a,
|
||||||
|
0x2e, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2e, 0x53,
|
||||||
|
0x75, 0x62, 0x73, 0x63, 0x72, 0x69, 0x62, 0x65, 0x4e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61,
|
||||||
|
0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x70, 0x6c, 0x79, 0x22, 0x00, 0x42, 0x25, 0x5a, 0x23,
|
||||||
|
0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x62, 0x72, 0x65, 0x65, 0x7a,
|
||||||
|
0x2f, 0x6c, 0x73, 0x70, 0x64, 0x2f, 0x6e, 0x6f, 0x74, 0x69, 0x66, 0x69, 0x63, 0x61, 0x74, 0x69,
|
||||||
|
0x6f, 0x6e, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
file_notifications_proto_rawDescOnce sync.Once
|
||||||
|
file_notifications_proto_rawDescData = file_notifications_proto_rawDesc
|
||||||
|
)
|
||||||
|
|
||||||
|
func file_notifications_proto_rawDescGZIP() []byte {
|
||||||
|
file_notifications_proto_rawDescOnce.Do(func() {
|
||||||
|
file_notifications_proto_rawDescData = protoimpl.X.CompressGZIP(file_notifications_proto_rawDescData)
|
||||||
|
})
|
||||||
|
return file_notifications_proto_rawDescData
|
||||||
|
}
|
||||||
|
|
||||||
|
var file_notifications_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
|
||||||
|
var file_notifications_proto_goTypes = []interface{}{
|
||||||
|
(*SubscribeNotificationsRequest)(nil), // 0: notifications.SubscribeNotificationsRequest
|
||||||
|
(*SubscribeNotificationsReply)(nil), // 1: notifications.SubscribeNotificationsReply
|
||||||
|
}
|
||||||
|
var file_notifications_proto_depIdxs = []int32{
|
||||||
|
0, // 0: notifications.Notifications.SubscribeNotifications:input_type -> notifications.SubscribeNotificationsRequest
|
||||||
|
1, // 1: notifications.Notifications.SubscribeNotifications:output_type -> notifications.SubscribeNotificationsReply
|
||||||
|
1, // [1:2] is the sub-list for method output_type
|
||||||
|
0, // [0:1] is the sub-list for method input_type
|
||||||
|
0, // [0:0] is the sub-list for extension type_name
|
||||||
|
0, // [0:0] is the sub-list for extension extendee
|
||||||
|
0, // [0:0] is the sub-list for field type_name
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() { file_notifications_proto_init() }
|
||||||
|
func file_notifications_proto_init() {
|
||||||
|
if File_notifications_proto != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !protoimpl.UnsafeEnabled {
|
||||||
|
file_notifications_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*SubscribeNotificationsRequest); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
file_notifications_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} {
|
||||||
|
switch v := v.(*SubscribeNotificationsReply); i {
|
||||||
|
case 0:
|
||||||
|
return &v.state
|
||||||
|
case 1:
|
||||||
|
return &v.sizeCache
|
||||||
|
case 2:
|
||||||
|
return &v.unknownFields
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
type x struct{}
|
||||||
|
out := protoimpl.TypeBuilder{
|
||||||
|
File: protoimpl.DescBuilder{
|
||||||
|
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
|
||||||
|
RawDescriptor: file_notifications_proto_rawDesc,
|
||||||
|
NumEnums: 0,
|
||||||
|
NumMessages: 2,
|
||||||
|
NumExtensions: 0,
|
||||||
|
NumServices: 1,
|
||||||
|
},
|
||||||
|
GoTypes: file_notifications_proto_goTypes,
|
||||||
|
DependencyIndexes: file_notifications_proto_depIdxs,
|
||||||
|
MessageInfos: file_notifications_proto_msgTypes,
|
||||||
|
}.Build()
|
||||||
|
File_notifications_proto = out.File
|
||||||
|
file_notifications_proto_rawDesc = nil
|
||||||
|
file_notifications_proto_goTypes = nil
|
||||||
|
file_notifications_proto_depIdxs = nil
|
||||||
|
}
|
||||||
18
notifications/notifications.proto
Normal file
18
notifications/notifications.proto
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
syntax = "proto3";
|
||||||
|
|
||||||
|
option go_package = "github.com/breez/lspd/notifications";
|
||||||
|
|
||||||
|
package notifications;
|
||||||
|
|
||||||
|
service Notifications {
|
||||||
|
rpc SubscribeNotifications(SubscribeNotificationsRequest)
|
||||||
|
returns (SubscribeNotificationsReply) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubscribeNotificationsRequest {
|
||||||
|
string url = 1;
|
||||||
|
bytes signature = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message SubscribeNotificationsReply {
|
||||||
|
}
|
||||||
105
notifications/notifications_grpc.pb.go
Normal file
105
notifications/notifications_grpc.pb.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
|
||||||
|
// versions:
|
||||||
|
// - protoc-gen-go-grpc v1.2.0
|
||||||
|
// - protoc v3.21.12
|
||||||
|
// source: notifications.proto
|
||||||
|
|
||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
grpc "google.golang.org/grpc"
|
||||||
|
codes "google.golang.org/grpc/codes"
|
||||||
|
status "google.golang.org/grpc/status"
|
||||||
|
)
|
||||||
|
|
||||||
|
// This is a compile-time assertion to ensure that this generated file
|
||||||
|
// is compatible with the grpc package it is being compiled against.
|
||||||
|
// Requires gRPC-Go v1.32.0 or later.
|
||||||
|
const _ = grpc.SupportPackageIsVersion7
|
||||||
|
|
||||||
|
// NotificationsClient is the client API for Notifications service.
|
||||||
|
//
|
||||||
|
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
|
||||||
|
type NotificationsClient interface {
|
||||||
|
SubscribeNotifications(ctx context.Context, in *SubscribeNotificationsRequest, opts ...grpc.CallOption) (*SubscribeNotificationsReply, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type notificationsClient struct {
|
||||||
|
cc grpc.ClientConnInterface
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationsClient(cc grpc.ClientConnInterface) NotificationsClient {
|
||||||
|
return ¬ificationsClient{cc}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *notificationsClient) SubscribeNotifications(ctx context.Context, in *SubscribeNotificationsRequest, opts ...grpc.CallOption) (*SubscribeNotificationsReply, error) {
|
||||||
|
out := new(SubscribeNotificationsReply)
|
||||||
|
err := c.cc.Invoke(ctx, "/notifications.Notifications/SubscribeNotifications", in, out, opts...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// NotificationsServer is the server API for Notifications service.
|
||||||
|
// All implementations must embed UnimplementedNotificationsServer
|
||||||
|
// for forward compatibility
|
||||||
|
type NotificationsServer interface {
|
||||||
|
SubscribeNotifications(context.Context, *SubscribeNotificationsRequest) (*SubscribeNotificationsReply, error)
|
||||||
|
mustEmbedUnimplementedNotificationsServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
// UnimplementedNotificationsServer must be embedded to have forward compatible implementations.
|
||||||
|
type UnimplementedNotificationsServer struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (UnimplementedNotificationsServer) SubscribeNotifications(context.Context, *SubscribeNotificationsRequest) (*SubscribeNotificationsReply, error) {
|
||||||
|
return nil, status.Errorf(codes.Unimplemented, "method SubscribeNotifications not implemented")
|
||||||
|
}
|
||||||
|
func (UnimplementedNotificationsServer) mustEmbedUnimplementedNotificationsServer() {}
|
||||||
|
|
||||||
|
// UnsafeNotificationsServer may be embedded to opt out of forward compatibility for this service.
|
||||||
|
// Use of this interface is not recommended, as added methods to NotificationsServer will
|
||||||
|
// result in compilation errors.
|
||||||
|
type UnsafeNotificationsServer interface {
|
||||||
|
mustEmbedUnimplementedNotificationsServer()
|
||||||
|
}
|
||||||
|
|
||||||
|
func RegisterNotificationsServer(s grpc.ServiceRegistrar, srv NotificationsServer) {
|
||||||
|
s.RegisterService(&Notifications_ServiceDesc, srv)
|
||||||
|
}
|
||||||
|
|
||||||
|
func _Notifications_SubscribeNotifications_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
|
||||||
|
in := new(SubscribeNotificationsRequest)
|
||||||
|
if err := dec(in); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if interceptor == nil {
|
||||||
|
return srv.(NotificationsServer).SubscribeNotifications(ctx, in)
|
||||||
|
}
|
||||||
|
info := &grpc.UnaryServerInfo{
|
||||||
|
Server: srv,
|
||||||
|
FullMethod: "/notifications.Notifications/SubscribeNotifications",
|
||||||
|
}
|
||||||
|
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
|
||||||
|
return srv.(NotificationsServer).SubscribeNotifications(ctx, req.(*SubscribeNotificationsRequest))
|
||||||
|
}
|
||||||
|
return interceptor(ctx, in, info, handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Notifications_ServiceDesc is the grpc.ServiceDesc for Notifications service.
|
||||||
|
// It's only intended for direct use with grpc.RegisterService,
|
||||||
|
// and not to be introspected or modified (even as a copy)
|
||||||
|
var Notifications_ServiceDesc = grpc.ServiceDesc{
|
||||||
|
ServiceName: "notifications.Notifications",
|
||||||
|
HandlerType: (*NotificationsServer)(nil),
|
||||||
|
Methods: []grpc.MethodDesc{
|
||||||
|
{
|
||||||
|
MethodName: "SubscribeNotifications",
|
||||||
|
Handler: _Notifications_SubscribeNotifications_Handler,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
Streams: []grpc.StreamDesc{},
|
||||||
|
Metadata: "notifications.proto",
|
||||||
|
}
|
||||||
58
notifications/server.go
Normal file
58
notifications/server.go
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
context "context"
|
||||||
|
"crypto/sha256"
|
||||||
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrInvalidSignature = fmt.Errorf("invalid signature")
|
||||||
|
var ErrInternal = fmt.Errorf("internal error")
|
||||||
|
|
||||||
|
type server struct {
|
||||||
|
store Store
|
||||||
|
NotificationsServer
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewNotificationsServer(store Store) NotificationsServer {
|
||||||
|
return &server{
|
||||||
|
store: store,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *server) SubscribeNotifications(
|
||||||
|
ctx context.Context,
|
||||||
|
request *SubscribeNotificationsRequest,
|
||||||
|
) (*SubscribeNotificationsReply, error) {
|
||||||
|
first := sha256.Sum256([]byte(request.Url))
|
||||||
|
second := sha256.Sum256(first[:])
|
||||||
|
pubkey, wasCompressed, err := ecdsa.RecoverCompact(
|
||||||
|
request.Signature,
|
||||||
|
second[:],
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, ErrInvalidSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
if !wasCompressed {
|
||||||
|
return nil, ErrInvalidSignature
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.store.Register(ctx, hex.EncodeToString(pubkey.SerializeCompressed()), request.Url)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf(
|
||||||
|
"failed to register %x for notifications on url %s: %v",
|
||||||
|
pubkey.SerializeCompressed(),
|
||||||
|
request.Url,
|
||||||
|
err,
|
||||||
|
)
|
||||||
|
|
||||||
|
return nil, ErrInternal
|
||||||
|
}
|
||||||
|
|
||||||
|
return &SubscribeNotificationsReply{}, nil
|
||||||
|
}
|
||||||
10
notifications/store.go
Normal file
10
notifications/store.go
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
package notifications
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Store interface {
|
||||||
|
Register(ctx context.Context, pubkey string, url string) error
|
||||||
|
GetRegistrations(ctx context.Context, pubkey string) ([]string, error)
|
||||||
|
}
|
||||||
17
postgresql/connect.go
Normal file
17
postgresql/connect.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package postgresql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PgConnect(databaseUrl string) (*pgxpool.Pool, error) {
|
||||||
|
var err error
|
||||||
|
pgxPool, err := pgxpool.Connect(context.Background(), databaseUrl)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("pgxpool.Connect(%v): %w", databaseUrl, err)
|
||||||
|
}
|
||||||
|
return pgxPool, nil
|
||||||
|
}
|
||||||
68
postgresql/forwarding_event_store.go
Normal file
68
postgresql/forwarding_event_store.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package postgresql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/lnd"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ForwardingEventStore struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewForwardingEventStore(pool *pgxpool.Pool) *ForwardingEventStore {
|
||||||
|
return &ForwardingEventStore{pool: pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingEventStore) LastForwardingEvent() (int64, error) {
|
||||||
|
var last int64
|
||||||
|
err := s.pool.QueryRow(context.Background(),
|
||||||
|
`SELECT coalesce(MAX("timestamp"), 0) AS last FROM forwarding_history`).Scan(&last)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return last, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ForwardingEventStore) InsertForwardingEvents(rowSrc lnd.CopyFromSource) error {
|
||||||
|
|
||||||
|
tx, err := s.pool.Begin(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("pgxPool.Begin() error: %w", err)
|
||||||
|
}
|
||||||
|
defer tx.Rollback(context.Background())
|
||||||
|
|
||||||
|
_, err = tx.Exec(context.Background(), `
|
||||||
|
CREATE TEMP TABLE tmp_table ON COMMIT DROP AS
|
||||||
|
SELECT *
|
||||||
|
FROM forwarding_history
|
||||||
|
WITH NO DATA;
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CREATE TEMP TABLE error: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
count, err := tx.CopyFrom(context.Background(),
|
||||||
|
pgx.Identifier{"tmp_table"},
|
||||||
|
[]string{"timestamp", "chanid_in", "chanid_out", "amt_msat_in", "amt_msat_out"}, rowSrc)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("CopyFrom() error: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("count1: %v", count)
|
||||||
|
|
||||||
|
cmdTag, err := tx.Exec(context.Background(), `
|
||||||
|
INSERT INTO forwarding_history
|
||||||
|
SELECT *
|
||||||
|
FROM tmp_table
|
||||||
|
ON CONFLICT DO NOTHING
|
||||||
|
`)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("INSERT INTO forwarding_history error: %w", err)
|
||||||
|
}
|
||||||
|
log.Printf("count2: %v", cmdTag.RowsAffected())
|
||||||
|
return tx.Commit(context.Background())
|
||||||
|
}
|
||||||
162
postgresql/intercept_store.go
Normal file
162
postgresql/intercept_store.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package postgresql
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/breez/lspd/basetypes"
|
||||||
|
"github.com/breez/lspd/interceptor"
|
||||||
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/jackc/pgtype"
|
||||||
|
"github.com/jackc/pgx/v4"
|
||||||
|
"github.com/jackc/pgx/v4/pgxpool"
|
||||||
|
)
|
||||||
|
|
||||||
|
type extendedParams struct {
|
||||||
|
Token string `json:"token"`
|
||||||
|
Params interceptor.OpeningFeeParams `json:"fees_params"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type PostgresInterceptStore struct {
|
||||||
|
pool *pgxpool.Pool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewPostgresInterceptStore(pool *pgxpool.Pool) *PostgresInterceptStore {
|
||||||
|
return &PostgresInterceptStore{pool: pool}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresInterceptStore) PaymentInfo(htlcPaymentHash []byte) (string, *interceptor.OpeningFeeParams, []byte, []byte, []byte, int64, int64, *wire.OutPoint, *string, error) {
|
||||||
|
var (
|
||||||
|
p, tag *string
|
||||||
|
paymentHash, paymentSecret, destination []byte
|
||||||
|
incomingAmountMsat, outgoingAmountMsat int64
|
||||||
|
fundingTxID []byte
|
||||||
|
fundingTxOutnum pgtype.Int4
|
||||||
|
)
|
||||||
|
err := s.pool.QueryRow(context.Background(),
|
||||||
|
`SELECT payment_hash, payment_secret, destination, incoming_amount_msat, outgoing_amount_msat, funding_tx_id, funding_tx_outnum, opening_fee_params, tag
|
||||||
|
FROM payments
|
||||||
|
WHERE payment_hash=$1 OR sha256('probing-01:' || payment_hash)=$1`,
|
||||||
|
htlcPaymentHash).Scan(&paymentHash, &paymentSecret, &destination, &incomingAmountMsat, &outgoingAmountMsat, &fundingTxID, &fundingTxOutnum, &p, &tag)
|
||||||
|
if err != nil {
|
||||||
|
if err == pgx.ErrNoRows {
|
||||||
|
err = nil
|
||||||
|
}
|
||||||
|
return "", nil, nil, nil, nil, 0, 0, nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var cp *wire.OutPoint
|
||||||
|
if fundingTxID != nil {
|
||||||
|
cp, err = basetypes.NewOutPoint(fundingTxID, uint32(fundingTxOutnum.Int))
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("invalid funding txid in database %x", fundingTxID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var extParams *extendedParams
|
||||||
|
if p != nil {
|
||||||
|
err = json.Unmarshal([]byte(*p), &extParams)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to unmarshal OpeningFeeParams '%s': %v", *p, err)
|
||||||
|
return "", nil, nil, nil, nil, 0, 0, nil, nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return extParams.Token, &extParams.Params, paymentHash, paymentSecret, destination, incomingAmountMsat, outgoingAmountMsat, cp, tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresInterceptStore) SetFundingTx(paymentHash []byte, channelPoint *wire.OutPoint) error {
|
||||||
|
commandTag, err := s.pool.Exec(context.Background(),
|
||||||
|
`UPDATE payments
|
||||||
|
SET funding_tx_id = $2, funding_tx_outnum = $3
|
||||||
|
WHERE payment_hash=$1`,
|
||||||
|
paymentHash, channelPoint.Hash[:], channelPoint.Index)
|
||||||
|
log.Printf("setFundingTx(%x, %s, %d): %s err: %v", paymentHash, channelPoint.Hash.String(), channelPoint.Index, commandTag, err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresInterceptStore) RegisterPayment(token string, params *interceptor.OpeningFeeParams, destination, paymentHash, paymentSecret []byte, incomingAmountMsat, outgoingAmountMsat int64, tag string) error {
|
||||||
|
var t *string
|
||||||
|
if tag != "" {
|
||||||
|
t = &tag
|
||||||
|
}
|
||||||
|
|
||||||
|
p := []byte{}
|
||||||
|
if params != nil {
|
||||||
|
var err error
|
||||||
|
p, err = json.Marshal(extendedParams{Token: token, Params: *params})
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to marshal OpeningFeeParams: %v", err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
commandTag, err := s.pool.Exec(context.Background(),
|
||||||
|
`INSERT INTO
|
||||||
|
payments (destination, payment_hash, payment_secret, incoming_amount_msat, outgoing_amount_msat, tag, opening_fee_params)
|
||||||
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||||||
|
ON CONFLICT DO NOTHING`,
|
||||||
|
destination, paymentHash, paymentSecret, incomingAmountMsat, outgoingAmountMsat, t, p)
|
||||||
|
log.Printf("registerPayment(%x, %x, %x, %v, %v, %v, %s) rows: %v err: %v",
|
||||||
|
destination, paymentHash, paymentSecret, incomingAmountMsat, outgoingAmountMsat, tag, p, commandTag.RowsAffected(), err)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("registerPayment(%x, %x, %x, %v, %v, %v, %s) error: %w",
|
||||||
|
destination, paymentHash, paymentSecret, incomingAmountMsat, outgoingAmountMsat, tag, p, err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresInterceptStore) InsertChannel(initialChanID, confirmedChanId uint64, channelPoint string, nodeID []byte, lastUpdate time.Time) error {
|
||||||
|
|
||||||
|
query := `INSERT INTO
|
||||||
|
channels (initial_chanid, confirmed_chanid, channel_point, nodeid, last_update)
|
||||||
|
VALUES ($1, NULLIF($2, 0::int8), $3, $4, $5)
|
||||||
|
ON CONFLICT (channel_point) DO UPDATE SET confirmed_chanid=NULLIF($2, 0::int8), last_update=$5`
|
||||||
|
|
||||||
|
c, err := s.pool.Exec(context.Background(),
|
||||||
|
query, int64(initialChanID), int64(confirmedChanId), channelPoint, nodeID, lastUpdate)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("insertChannel(%v, %v, %s, %x) error: %v",
|
||||||
|
initialChanID, confirmedChanId, channelPoint, nodeID, err)
|
||||||
|
return fmt.Errorf("insertChannel(%v, %v, %s, %x) error: %w",
|
||||||
|
initialChanID, confirmedChanId, channelPoint, nodeID, err)
|
||||||
|
}
|
||||||
|
log.Printf("insertChannel(%v, %v, %x) result: %v",
|
||||||
|
initialChanID, confirmedChanId, nodeID, c.String())
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PostgresInterceptStore) GetFeeParamsSettings(token string) ([]*interceptor.OpeningFeeParamsSetting, error) {
|
||||||
|
rows, err := s.pool.Query(context.Background(), `SELECT validity, params FROM new_channel_params WHERE token=$1`, token)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("GetFeeParamsSettings(%v) error: %v", token, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings []*interceptor.OpeningFeeParamsSetting
|
||||||
|
for rows.Next() {
|
||||||
|
var validity int64
|
||||||
|
var param string
|
||||||
|
err = rows.Scan(&validity, ¶m)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var params *interceptor.OpeningFeeParams
|
||||||
|
err := json.Unmarshal([]byte(param), ¶ms)
|
||||||
|
if err != nil {
|
||||||
|
log.Printf("Failed to unmarshal fee param '%v': %v", param, err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
duration := time.Second * time.Duration(validity)
|
||||||
|
settings = append(settings, &interceptor.OpeningFeeParamsSetting{
|
||||||
|
Validity: duration,
|
||||||
|
Params: params,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return settings, nil
|
||||||
|
}
|
||||||
0
postgresql/migrations/000000_.down.sql
Normal file
0
postgresql/migrations/000000_.down.sql
Normal file
0
postgresql/migrations/000000_.up.sql
Normal file
0
postgresql/migrations/000000_.up.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE public.payments;
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
CREATE TABLE public.payments (
|
||||||
|
payment_hash bytea NOT NULL,
|
||||||
|
payment_request_out varchar NOT NULL,
|
||||||
|
CONSTRAINT payments_pkey PRIMARY KEY (payment_hash)
|
||||||
|
);
|
||||||
5
postgresql/migrations/000002_details.down.sql
Normal file
5
postgresql/migrations/000002_details.down.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE public.payments DROP COLUMN payment_secret;
|
||||||
|
ALTER TABLE public.payments DROP COLUMN destination;
|
||||||
|
ALTER TABLE public.payments DROP COLUMN incoming_amount_msat;
|
||||||
|
ALTER TABLE public.payments DROP COLUMN outgoing_amount_msat;
|
||||||
|
ALTER TABLE public.payments ADD payment_request_out varchar NOT NULL;
|
||||||
5
postgresql/migrations/000002_details.up.sql
Normal file
5
postgresql/migrations/000002_details.up.sql
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
ALTER TABLE public.payments DROP COLUMN payment_request_out;
|
||||||
|
ALTER TABLE public.payments ADD payment_secret bytea NOT NULL;
|
||||||
|
ALTER TABLE public.payments ADD destination bytea NOT NULL;
|
||||||
|
ALTER TABLE public.payments ADD incoming_amount_msat bigint NOT NULL;
|
||||||
|
ALTER TABLE public.payments ADD outgoing_amount_msat bigint NOT NULL;
|
||||||
2
postgresql/migrations/000003_funding_tx.down.sql
Normal file
2
postgresql/migrations/000003_funding_tx.down.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE public.payments DROP COLUMN funding_tx_id;
|
||||||
|
ALTER TABLE public.payments DROP COLUMN funding_tx_outnum;
|
||||||
2
postgresql/migrations/000003_funding_tx.up.sql
Normal file
2
postgresql/migrations/000003_funding_tx.up.sql
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
ALTER TABLE public.payments ADD funding_tx_id bytea NULL;
|
||||||
|
ALTER TABLE public.payments ADD funding_tx_outnum int NULL;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
DROP INDEX probe_payment_hash;
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
CREATE INDEX probe_payment_hash ON public.payments (sha256('probing-01:' || payment_hash));
|
||||||
1
postgresql/migrations/000005_forwarding_history.down.sql
Normal file
1
postgresql/migrations/000005_forwarding_history.down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE public.forwarding_history;
|
||||||
10
postgresql/migrations/000005_forwarding_history.up.sql
Normal file
10
postgresql/migrations/000005_forwarding_history.up.sql
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
CREATE TABLE public.forwarding_history (
|
||||||
|
"timestamp" bigint NOT NULL,
|
||||||
|
chanid_in bigint NOT NULL,
|
||||||
|
chanid_out bigint NOT NULL,
|
||||||
|
amt_msat_in bigint NOT NULL,
|
||||||
|
amt_msat_out bigint NOT NULL,
|
||||||
|
CONSTRAINT timestamp_pkey PRIMARY KEY ("timestamp")
|
||||||
|
);
|
||||||
|
CREATE INDEX forwarding_history_chanid_in_idx ON public.forwarding_history (chanid_in);
|
||||||
|
CREATE INDEX forwarding_history_chanid_out_idx ON public.forwarding_history (chanid_out);
|
||||||
1
postgresql/migrations/000006_channels.down.sql
Normal file
1
postgresql/migrations/000006_channels.down.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
DROP TABLE public.channels;
|
||||||
7
postgresql/migrations/000006_channels.up.sql
Normal file
7
postgresql/migrations/000006_channels.up.sql
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
CREATE TABLE public.channels (
|
||||||
|
chanid bigint NOT NULL,
|
||||||
|
channel_point varchar NULL,
|
||||||
|
nodeid bytea NULL,
|
||||||
|
CONSTRAINT chanid_pkey PRIMARY KEY (chanid)
|
||||||
|
);
|
||||||
|
CREATE INDEX channels_nodeid_idx ON public.channels (nodeid);
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE public.channels DROP COLUMN last_update;
|
||||||
1
postgresql/migrations/000007_channels_last_update.up.sql
Normal file
1
postgresql/migrations/000007_channels_last_update.up.sql
Normal file
@@ -0,0 +1 @@
|
|||||||
|
ALTER TABLE public.channels ADD COLUMN last_update TIMESTAMP;
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user