scripts/bw-compatibility-test: add backwards compat test

In this commit, a new backwards compatibility test is added. See the
added README.md file in this commit for all the info.
This commit is contained in:
Elle Mouton 2025-02-20 20:36:47 -03:00
parent e3d9fcb5ac
commit f0d4ea10a2
No known key found for this signature in database
GPG key ID: D7D916376026F177
8 changed files with 835 additions and 0 deletions

View file

@ -0,0 +1,3 @@
BITCOIND_VERSION=26
LND_LATEST_VERSION=v0.18.5-beta
TIMEOUT=15

View file

@ -0,0 +1,125 @@
# Basic Backwards Compatibility Test
This directory houses some docker compose files and various bash helpers all
used by the main `test.sh` file which runs the test. The idea is to be able to
test that a node can be upgraded from a stable release to a checked out branch
of LND and still function as expected with nodes running the stable release.
## Test Flow
The test sets up the following network:
```
Alice <---> Bob <---> Charlie <---> Dave
```
Initially, all the nodes are running a tagged LND release. This is all
configured via the main `docker-compose.yaml` file.
1. The Bob node is the node we will focus on. While Bob is still on a stable
version of LND, we ensure that he can: send and receive multi-hop payments as
well as route payments.
2. Bob is then shutdown.
3. The `docker-compose.override.yaml` file is then loaded and used to spin up
Bob again but this time using the checked out branch of LND. This is done by
using the `dev.Dockerfile` in the LND repo.
4. The test now waits for this new version of Bob (which uses the same data
directory as the previous version) to sync up with the network, reactivate
its channels.
5. Finally, basic send, receive and routing tests are run to ensure that Bob
is still functional after the upgrade.
## How to use this directory
1. If you would just like to run the full test from start to finish, then all
you need to do is run:
```bash
./test.sh
```
2. If you would like to run the test in parts, then you can use the `execute.sh`
script to call the various functions in the directory. Here is an example:
```bash
# Spin up the docker containers.
./execute.sh compose_up
# Wait for the nodes to start.
./execute.sh wait_for_nodes alice bob charlie dave
# Query various nodes.
./execute.sh alice getinfo
# Set-up a basic channel network.
./execute.sh setup-network
# Wait for bob to see all the channels in the network.
./execute.sh wait_graph_sync bob 3
# Open a channel between Bob and Charlie.
./execute.sh open_channel bob charlie
# Send a payment from Alice to Dave.
./execute.sh send_payment alice dave
# Take down a single node.
./execute.sh compose_stop dave
# Start a single node.
./execute.sh compose_start dave
```
## File Descriptions:
##### `test.sh`
This script runs the full backwards compatibility test.
```bash
./test.sh
```
##### `execute.sh`
A helper script that allows you to call the various functions in the directory.
This is useful if you are debugging something and want to step through the test
steps manually while keeping the main network running.
For example:
```bash
# Spin up the docker containers.
./execute.sh compose_up
# Query various nodes.
./execute.sh alice getinfo
```
#### `compose.sh`
This script contains all the docker-compose variables and helper functions that
are used in the test.
#### `network.sh`
This script contains all the various helper functions that can be used to
interact with the Lightning nodes in the network.
### `vars.sh`
Any global variables used across the other scripts are defined here.
#### `docker-compose.yaml`
This docker compose file contains the network configuration for all the nodes
running a stable LND tag.
#### `docker-compose.override.yaml`
This compose file adds defines the `bob-pr` service which will pull and build
the LND branch that is currently checked out.

View file

@ -0,0 +1,61 @@
#!/bin/bash
# DIR is set to the directory of this script.
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
# The way we call docker-compose depends on the installation.
if which docker-compose > /dev/null; then
COMPOSE_CMD="docker-compose"
else
COMPOSE_CMD="docker compose"
fi
# Common arguments that we want to pass to docker-compose.
# By default, this only includes the main docker-compose file
# and not the override file. Use the `compose_upgrade` method
# to load both docker compose files.
COMPOSE_ARGS="-f $DIR/docker-compose.yaml -p regtest"
COMPOSE="$COMPOSE_CMD $COMPOSE_ARGS"
# compose_upgrade sets COMPOSE_ARGS and COMPOSE such that
# both the main docker-compose file and the override file
# are loaded.
function compose_upgrade() {
export COMPOSE_ARGS="-p regtest"
export COMPOSE="$COMPOSE_CMD $COMPOSE_ARGS"
}
# compose_up starts the docker-compose cluster.
function compose_up() {
echo "🐳 Starting the cluster"
$COMPOSE up -d --quiet-pull
}
# compose_down tears down the docker-compose cluster
# and removes all volumes and orphans.
function compose_down() {
echo "🐳 Tearing down the cluster"
$COMPOSE down --volumes --remove-orphans
}
# compose_stop stops a specific service in the cluster.
function compose_stop() {
local service="$1"
echo "🐳 Stopping $service"
$COMPOSE stop "$service"
}
# compose_start starts a specific service in the cluster.
function compose_start() {
local service="$1"
echo "🐳 Starting $service"
$COMPOSE up -d $service
}
# compose_rebuild forces the rebuild of the image for a
# specific service in the cluster.
function compose_rebuild() {
local service="$1"
echo "🐳 Rebuilding $service"
$COMPOSE build --no-cache $service
}

View file

@ -0,0 +1,43 @@
services:
bob-pr:
build:
context: ../../
dockerfile: dev.Dockerfile
container_name: bob-pr
restart: unless-stopped
ports:
- 10012:10009
- 9742:9735
- 8092:8080
networks:
regtest:
aliases:
- bob
volumes:
- "bob:/root/.lnd"
depends_on:
- bitcoind
command: >
lnd
--logdir=/root/.lnd
--alias=bob
--rpclisten=0.0.0.0:10009
--restlisten=0.0.0.0:8080
--color=#cccccc
--noseedbackup
--bitcoin.active
--bitcoin.regtest
--bitcoin.node=bitcoind
--bitcoind.rpchost=bitcoind
--bitcoind.rpcuser=lightning
--bitcoind.rpcpass=lightning
--bitcoind.zmqpubrawblock=tcp://bitcoind:28332
--bitcoind.zmqpubrawtx=tcp://bitcoind:28333
--debuglevel=debug
--externalip=bob
--tlsextradomain=bob
--accept-keysend
--protocol.option-scid-alias
--protocol.zero-conf
--protocol.simple-taproot-chans
--trickledelay=50

View file

@ -0,0 +1,197 @@
services:
bitcoind:
image: lightninglabs/bitcoin-core:${BITCOIND_VERSION}
container_name: bitcoind
restart: unless-stopped
ports:
- 18443:18443
- 18444:18444
- 28332:28332
- 28333:28333
networks:
regtest:
aliases:
- bitcoind
command:
- "-txindex"
- "-regtest"
- "-rest"
- "-printtoconsole"
- "-zmqpubrawblock=tcp://0.0.0.0:28332"
- "-zmqpubrawtx=tcp://0.0.0.0:28333"
- "-rpcport=18443"
- "-rpcbind=0.0.0.0"
- "-rpcauth=lightning:8492220e715bbfdf5f165102bfd7ed4$$88090545821ed5e9db614588c0afbad575ccc14681fb77f3cae6899bc419af67"
- "-rpcallowip=0.0.0.0/0"
- "-fallbackfee=0.0002"
- "-peerblockfilters=1"
- "-blockfilterindex=1"
- "-wallet=/home/bitcoin/.bitcoin/regtest/wallets/miner"
environment:
- HOME=/home/bitcoin
volumes:
- bitcoind:/home/bitcoin/.bitcoin
alice:
image: lightninglabs/lnd:${LND_LATEST_VERSION}
container_name: alice
restart: unless-stopped
ports:
- 10011:10009
- 9741:9735
- 8091:8080
networks:
regtest:
aliases:
- alice
volumes:
- "alice:/root/.lnd"
depends_on:
- bitcoind
command:
- "--logdir=/root/.lnd"
- "--alias=alice"
- "--rpclisten=0.0.0.0:10009"
- "--restlisten=0.0.0.0:8080"
- "--color=#cccccc"
- "--noseedbackup"
- "--bitcoin.active"
- "--bitcoin.regtest"
- "--bitcoin.node=bitcoind"
- "--bitcoind.rpchost=bitcoind"
- "--bitcoind.rpcuser=lightning"
- "--bitcoind.rpcpass=lightning"
- "--bitcoind.zmqpubrawblock=tcp://bitcoind:28332"
- "--bitcoind.zmqpubrawtx=tcp://bitcoind:28333"
- "--debuglevel=debug"
- "--externalip=alice"
- "--tlsextradomain=alice"
- "--accept-keysend"
- "--protocol.option-scid-alias"
- "--protocol.zero-conf"
- "--protocol.simple-taproot-chans"
- "--trickledelay=50"
bob:
image: lightninglabs/lnd:${LND_LATEST_VERSION}
container_name: bob
restart: unless-stopped
ports:
- 10012:10009
- 9742:9735
- 8092:8080
networks:
regtest:
aliases:
- bob
volumes:
- "bob:/root/.lnd"
depends_on:
- bitcoind
command:
- "--logdir=/root/.lnd"
- "--alias=bob"
- "--rpclisten=0.0.0.0:10009"
- "--restlisten=0.0.0.0:8080"
- "--color=#cccccc"
- "--noseedbackup"
- "--bitcoin.active"
- "--bitcoin.regtest"
- "--bitcoin.node=bitcoind"
- "--bitcoind.rpchost=bitcoind"
- "--bitcoind.rpcuser=lightning"
- "--bitcoind.rpcpass=lightning"
- "--bitcoind.zmqpubrawblock=tcp://bitcoind:28332"
- "--bitcoind.zmqpubrawtx=tcp://bitcoind:28333"
- "--debuglevel=debug"
- "--externalip=bob"
- "--tlsextradomain=bob"
- "--accept-keysend"
- "--protocol.option-scid-alias"
- "--protocol.zero-conf"
- "--protocol.simple-taproot-chans"
- "--trickledelay=50"
charlie:
image: lightninglabs/lnd:${LND_LATEST_VERSION}
container_name: charlie
restart: unless-stopped
ports:
- 10013:10009
- 9743:9735
- 8093:8080
networks:
regtest:
aliases:
- charlie
volumes:
- "charlie:/root/.lnd"
depends_on:
- bitcoind
command:
- "--logdir=/root/.lnd"
- "--alias=charlie"
- "--rpclisten=0.0.0.0:10009"
- "--restlisten=0.0.0.0:8080"
- "--color=#cccccc"
- "--noseedbackup"
- "--bitcoin.active"
- "--bitcoin.regtest"
- "--bitcoin.node=bitcoind"
- "--bitcoind.rpchost=bitcoind"
- "--bitcoind.rpcuser=lightning"
- "--bitcoind.rpcpass=lightning"
- "--bitcoind.zmqpubrawblock=tcp://bitcoind:28332"
- "--bitcoind.zmqpubrawtx=tcp://bitcoind:28333"
- "--debuglevel=debug"
- "--externalip=charlie"
- "--tlsextradomain=charlie"
- "--accept-keysend"
- "--trickledelay=50"
dave:
image: lightninglabs/lnd:${LND_LATEST_VERSION}
container_name: dave
restart: unless-stopped
ports:
- 10014:10009
- 9744:9735
- 8094:8080
networks:
regtest:
aliases:
- dave
volumes:
- "dave:/root/.lnd"
depends_on:
- bitcoind
command:
- "--logdir=/root/.lnd"
- "--alias=dave"
- "--rpclisten=0.0.0.0:10009"
- "--restlisten=0.0.0.0:8080"
- "--color=#cccccc"
- "--noseedbackup"
- "--bitcoin.active"
- "--bitcoin.regtest"
- "--bitcoin.node=bitcoind"
- "--bitcoind.rpchost=bitcoind"
- "--bitcoind.rpcuser=lightning"
- "--bitcoind.rpcpass=lightning"
- "--bitcoind.zmqpubrawblock=tcp://bitcoind:28332"
- "--bitcoind.zmqpubrawtx=tcp://bitcoind:28333"
- "--debuglevel=debug"
- "--externalip=dave"
- "--tlsextradomain=dave"
- "--accept-keysend"
- "--trickledelay=50"
networks:
regtest:
volumes:
bitcoind:
alice:
bob:
charlie:
dave:

View file

@ -0,0 +1,16 @@
#!/bin/bash
# The execute.sh file can be used to call any helper functions directly
# from the command line. For example:
# $ ./execute.sh compose-up
# DIR is set to the directory of this script.
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$DIR/.env"
source "$DIR/compose.sh"
source "$DIR/network.sh"
CMD=$1
shift
$CMD "$@"

View file

@ -0,0 +1,337 @@
#!/bin/bash
# DIR is set to the directory of this script.
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$DIR/.env"
# Global variable to keep track of which Bob container to use.
# Once Bob is upgraded to the PR version, this variable must
# be updated to 'bob-pr'.
BOB=bob
# upgrade_bob shuts down the stable Bob container, upgrades the
# compose variables to use the PR version of Bob, rebuilds the
# Bob container, and starts the PR version of Bob.
function upgrade_bob() {
# Shutdown Bob.
compose_stop bob
# Upgrade the compose variables so that the Bob configuration
# is swapped out for the PR version.
compose_upgrade
export BOB=bob-pr
# Force the rebuild of the Bob container.
compose_rebuild bob-pr
# This should now start with the new version of Bob.
compose_start bob-pr
}
# wait_for_nodes waits for all the nodes in the argument list to
# start.
function wait_for_nodes() {
local nodes=("$@")
for node in "${nodes[@]}"; do
wait_for_node $node
done
echo "🏎️ All nodes have started!"
}
# wait_for_node waits for the given node in the cluster to start, with a timeout.
wait_for_node() {
if [[ $# -ne 1 ]]; then
echo "❌ Error: wait_for_node requires exactly 1 argument (node)"
echo "Usage: wait_for_node <node>"
return 1
fi
local node="$1"
local start_time=$(date +%s)
echo -n "⌛ Waiting for $node to start (timeout: ${TIMEOUT}s)"
while ! $node getinfo 2>/dev/null | grep -q identity_pubkey; do
echo -n "."
sleep 0.5
# Check if timeout has been reached
local elapsed_time=$(( $(date +%s) - start_time ))
if [[ $elapsed_time -ge $TIMEOUT ]]; then
echo
echo "❌ Error: Timeout after $TIMEOUT seconds waiting for $node to start"
return 1
fi
done
echo
echo "$node has started"
}
# do_for is a generic function to execute a command for a set of nodes.
do_for() {
if [[ $# -lt 2 ]]; then
echo "❌ Error: do_for requires at least 2 arguments (function and nodes)"
echo "Usage: do_for <function> [node1] [node2] [node3]..."
return 1
fi
local func="$1"
shift
local nodes=("$@")
for node in "${nodes[@]}"; do
"$func" "$node"
done
}
# setup_network sets up the basic A <> B <> C <> D network.
function setup_network() {
wait_for_nodes alice bob charlie dave
setup_bitcoin
do_for fund_node alice bob charlie dave
mine 6
connect_nodes alice bob
connect_nodes bob charlie
connect_nodes charlie dave
open_channel alice bob
open_channel bob charlie
open_channel charlie dave
echo "Set up network: Alice <-> Bob <-> Charlie <-> Dave"
mine 7
wait_graph_sync alice 3
wait_graph_sync bob 3
wait_graph_sync charlie 3
wait_graph_sync dave 3
}
# fund_node funds the specified node with 5 BTC.
function fund_node() {
local node="$1"
ADDR=$( $node newaddress p2wkh | jq .address -r)
bitcoin sendtoaddress "$ADDR" 5 > /dev/null
echo "💰 Funded $node with 5 BTC"
}
# connect_nodes connects two specified nodes.
function connect_nodes() {
if [[ $# -ne 2 ]]; then
echo "❌ Error: connect_nodes requires exactly 2 arguments (node1 and node2)"
echo "Usage: connect_nodes <node1> <node2>"
return 1
fi
local node1="$1"
local node2="$2"
echo -ne "📞 Connecting $node1 to $node2...\r"
KEY_2=$( $node2 getinfo | jq .identity_pubkey -r)
$node1 connect "$KEY_2"@$node2:9735 > /dev/null
echo -ne " \r"
echo "📞 Connected $node1 to $node2"
}
# open_channel opens a channel between two specified nodes.
function open_channel() {
if [[ $# -ne 2 ]]; then
echo "❌ Error: open_channel requires exactly 2 arguments (node1 and node2)"
echo "Usage: open_channel <node1> <node2>"
return 1
fi
local node1="$1"
local node2="$2"
KEY_2=$( $node2 getinfo | jq .identity_pubkey -r)
$node1 openchannel --node_key "$KEY_2" --local_amt 15000000 --push_amt 7000000 > /dev/null
echo "🔗 Opened channel between $node1 and $node2"
}
# Function to check if a node's graph has the expected number of channels
wait_graph_sync() {
if [[ $# -ne 2 ]]; then
echo "❌ Error: graph_synced requires exactly 2 arguments (node and num_chans)"
echo "Usage: graph_synced <node> <num_chans>"
return 1
fi
local node="$1"
local num_chans="$2"
while :; do
num_channels=$($node getnetworkinfo | jq -r '.num_channels')
# Ensure num_channels is a valid number before proceeding
if [[ "$num_channels" =~ ^[0-9]+$ ]]; then
echo -ne "$node sees $num_channels channels...\r"
if [[ "$num_channels" -eq num_chans ]]; then
echo "👀 $node sees all the channels!"
break # Exit loop when num_channels reaches num_chans
fi
fi
sleep 1
done
}
# send_payment attempts to send a payment between two specified nodes.
send_payment() {
if [[ $# -ne 2 ]]; then
echo "❌ Error: send_payment requires exactly 2 arguments (from_node and to_node)"
echo "Usage: send_payment <from_node> <to_node>"
return 1
fi
local from_node="$1"
local to_node="$2"
# Generate invoice and capture error output
local invoice_output
if ! invoice_output=$($to_node addinvoice 10000 2>&1); then
echo "❌ Error: Failed to generate invoice from $to_node"
echo "📜 Details: $invoice_output"
return 1
fi
# Extract payment request
local PAY_REQ
PAY_REQ=$(echo "$invoice_output" | jq -r '.payment_request')
# Ensure invoice creation was successful
if [[ -z "$PAY_REQ" || "$PAY_REQ" == "null" ]]; then
echo "❌ Error: Invoice response did not contain a valid payment request."
echo "📜 Raw Response: $invoice_output"
return 1
fi
# Send payment and capture error output
local payment_output
if ! payment_output=$($from_node payinvoice --force "$PAY_REQ" 2>&1); then
echo "❌ Error: Payment failed from $from_node to $to_node"
echo "📜 Details: $payment_output"
return 1
fi
echo "💸 Payment sent from $from_node to $to_node"
}
# print_version prints the commit hash that the given node is running.
print_version() {
if [[ $# -ne 1 ]]; then
echo "❌ Error: print_version requires exactly 1 argument (node)"
echo "Usage: print_version <node>"
return 1
fi
local node="$1"
# Get the commit hash
local commit_hash
commit_hash=$($node version 2>/dev/null | jq -r '.lnd.commit_hash' | sed 's/^[ \t]*//')
# Ensure commit hash is retrieved
if [[ -z "$commit_hash" || "$commit_hash" == "null" ]]; then
echo "❌ Error: Could not retrieve commit hash for $node"
return 1
fi
echo " $node is running on commit $commit_hash"
}
# wait_for_active_chans waits for a node to have the expected number of active channels.
wait_for_active_chans() {
if [[ $# -ne 2 ]]; then
echo "❌ Error: wait_for_active_chans requires exactly 2 arguments (node and expected_active_channels)"
echo "Usage: wait_for_active_chans <node> <num_channels>"
return 1
fi
local node="$1"
local expected_channels="$2"
echo "🟠 Waiting for $node to have exactly $expected_channels active channels..."
while :; do
# Get the active channel count
local active_count
active_count=$($node --network=regtest listchannels 2>/dev/null | jq '[.channels[] | select(.active == true)] | length')
# Ensure active_count is a valid number
if [[ "$active_count" =~ ^[0-9]+$ ]]; then
echo -ne "$node sees $active_count active channels...\r"
# Exit loop only if the expected number of channels is active
if [[ "$active_count" -eq "$expected_channels" ]]; then
break
fi
fi
sleep 1
done
echo
echo "🟢 $node now has exactly $expected_channels active channels!"
}
# mine mines a number of blocks on the regtest network. If no
# argument is provided, it defaults to 6 blocks.
function mine() {
NUMBLOCKS="${1-6}"
bitcoin generatetoaddress "$NUMBLOCKS" "$(bitcoin getnewaddress "" legacy)" > /dev/null
}
# setup_bitcoin performs various operations on the regtest bitcoind node
# so that it is ready to be used by the Lightning nodes and so that it can
# be used to fund the nodes.
function setup_bitcoin() {
echo "🔗 Setting up Bitcoin node"
bitcoin createwallet miner > /dev/null
ADDR_BTC=$(bitcoin getnewaddress "" legacy)
bitcoin generatetoaddress 106 "$ADDR_BTC" > /dev/null
bitcoin getbalance > /dev/null
echo "🔗 Bitcoin node is set up"
}
function bitcoin() {
docker exec -i -u bitcoin bitcoind bitcoin-cli -regtest -rpcuser=lightning -rpcpassword=lightning "$@"
}
function alice() {
docker exec -i alice lncli --network regtest "$@"
}
function bob() {
docker exec -i "$BOB" lncli --network regtest "$@"
}
function bob-pr() {
docker exec -i bob-pr lncli --network regtest "$@"
}
function charlie() {
docker exec -i charlie lncli --network regtest "$@"
}
function dave() {
docker exec -i dave lncli --network regtest "$@"
}

View file

@ -0,0 +1,53 @@
#!/bin/bash
# Stop the script if an error is returned by any step.
set -e
# DIR is set to the directory of this script.
DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
source "$DIR/compose.sh"
source "$DIR/network.sh"
source "$DIR/.env"
cd $DIR
# Spin up the network in detached mode.
compose_up
# Ensure that the cluster is shut down when the script exits
# regardless of success
trap compose_down EXIT
# Set up the network.
setup_network
# Print the initial version of each node.
do_for print_version alice bob charlie dave
# Test that Bob can send a multi-hop payment.
send_payment bob dave
# Test that Bob can receive a multi-hop payment.
send_payment dave bob
# Test that Bob can route a payment.
send_payment alice dave
# Upgrade the compose variables so that the Bob configuration
# is swapped out for the PR version.
upgrade_bob
# Wait for Bob to start.
wait_for_node bob
wait_for_active_chans bob 2
# Show that Bob is now running the current branch.
do_for print_version bob
# Repeat the basic tests.
send_payment bob dave
send_payment dave bob
send_payment alice dave
echo "🛡️⚔️🫡 Backwards compatibility test passed! 🫡⚔️🛡️"