Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
51 changes: 51 additions & 0 deletions .github/workflows/compat.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
name: Backward Compatibility Tests

on:
workflow_dispatch:
inputs:
version:
description: 'Historical version to test against (e.g. v0.8.0). Leave empty to test all configured versions.'
required: false
type: string
push:
branches:
- 'release/**'

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

defaults:
run:
shell: bash

env:
GOPATH: /home/runner/go

jobs:
compat-tests:
name: Compat Tests
runs-on: ubuntu-latest
timeout-minutes: 60
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # Full history needed for git worktree builds.

- name: Set up Go
uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: Cache compat binaries
uses: actions/cache@v4
with:
path: ~/.tapd-compat-bins
key: compat-bins-${{ runner.os }}-${{ runner.arch }}

- name: Build current integrated binary
run: make build-itest

- name: Run backward compatibility tests
run: make itest-cc-compat
14 changes: 14 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -273,6 +273,15 @@ itest-cc: build-itest clean-cc-itest-logs
date
$(GOTEST) ./itest/custom_channels -v -tags="$(ITEST_TAGS)" $(CC_TEST_FLAGS) -test.timeout=30m -logdir=regtest/.logs

itest-cc-compat: build-itest clean-cc-itest-logs
@$(call print, "Running backward compatibility integration tests.")
date
$(GOTEST) ./itest/custom_channels -v -tags="$(ITEST_TAGS)" -test.run=TestCustomChannelsCompat -test.timeout=60m -logdir=regtest/.logs

build-compat-binary:
@$(call print, "Building compat binary for $(version).")
@scripts/build-compat-binary.sh $(version)

itest-cc-parallel: build-itest build-itest-cc-binary clean-cc-itest-logs
@$(call print, "Running custom channel integration tests in parallel.")
date
Expand Down Expand Up @@ -445,6 +454,11 @@ gen-itest-test-vectors:

gen-test-vectors: gen-deterministic-test-vectors gen-itest-test-vectors

gen-compat-fixtures:
@$(call print, "Generating backward compatibility fixtures.")
make unit gen-test-vectors=true pkg=tapchannelmsg case=^TestGenerateCompatFixtures$
make unit gen-test-vectors=true pkg=rfqmsg case=^TestGenerateCompatFixtures$

test-vector-check: gen-deterministic-test-vectors
@$(call print, "Checking deterministic test vectors.")
if test -n "$$(git status | grep -e ".json")"; then echo "Test vectors not updated"; git status; git diff; exit 1; fi
Expand Down
184 changes: 184 additions & 0 deletions itest/custom_channels/compat_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
//go:build itest

package custom_channels

import (
"context"
"fmt"
"os"
"os/exec"
"path/filepath"
"testing"
"time"

"github.com/lightninglabs/taproot-assets/itest"
"github.com/lightningnetwork/lnd/lntest"
"github.com/lightningnetwork/lnd/lntest/miner"
"github.com/lightningnetwork/lnd/lntest/node"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/stretchr/testify/require"
)

// compatVersions lists the historical release versions to test backward
// compatibility against. Update this list before each release to include
// the latest 2 minor versions.
var compatVersions = []string{
// TODO: Uncomment once v0.8.0 is released and the integrated binary
// can be built from that tag.
// "v0.8.0",
}

// compatTestCases is the subset of test cases that exercise critical
// backward compatibility surfaces: channel open/close, routing, force
// close, and upgrade.
var compatTestCases = []*ccTestCase{
{
name: "core",
test: testCustomChannels,
},
{
name: "force close",
test: testCustomChannelsForceClose,
},
{
name: "v1 upgrade",
test: testCustomChannelsV1Upgrade,
},
}

// buildCompatBinary builds or retrieves a cached tapd-integrated binary for
// the given version tag. It returns the path to the binary.
func buildCompatBinary(t *testing.T, version string) string {
t.Helper()

// Locate the build script relative to the repo root.
repoRoot, err := exec.Command(
"git", "rev-parse", "--show-toplevel",
).Output()
require.NoError(t, err, "unable to find repo root")

script := filepath.Join(
string(repoRoot[:len(repoRoot)-1]),
"scripts", "build-compat-binary.sh",
)

// Run the build script. It prints the binary path on stdout.
//nolint:gosec
cmd := exec.Command(script, version)
cmd.Stderr = os.Stderr
out, err := cmd.Output()
require.NoError(t, err, "unable to build compat binary for %s",
version)

binaryPath := string(out[:len(out)-1]) // trim trailing newline
require.FileExists(t, binaryPath)

return binaryPath
}

// TestCustomChannelsCompat runs a subset of custom channel tests with one
// node running an older binary version for each historical version in
// compatVersions. This test is gated behind the `compat` build tag and
// intended to run only on release branches or via manual dispatch.
//
// The test creates a fresh network harness for each version, builds or
// retrieves the old binary, and runs each compat test case. In each test,
// one node (typically the "old" peer) is started with the historical binary
// via NewNodeWithBinary, while the other nodes use the current build.
func TestCustomChannelsCompat(t *testing.T) {
if len(compatVersions) == 0 {
t.Skip("no compat versions configured")
}

for _, version := range compatVersions {
version := version
t.Run(version, func(t *testing.T) {
runCompatSuite(t, version)
})
}
}

// runCompatSuite runs all compat test cases for a single historical version.
func runCompatSuite(t *testing.T, version string) {
t.Helper()

// Build or retrieve the old binary.
oldBinary := buildCompatBinary(t, version)
t.Logf("Using compat binary for %s: %s", version, oldBinary)

lntest.MaxBlocksMinedPerTest = 250

logDir := node.GetLogDir()
netName := miner.HarnessNetParams.Name
for _, dir := range []string{".minerlogs", ".backendlogs"} {
path := fmt.Sprintf("%s/%s/%s", logDir, dir, netName)
require.NoError(t, os.MkdirAll(path, 0750))
}

ctx, cancel := context.WithCancel(context.Background())
defer cancel()

m := miner.NewMiner(ctx, t)
require.NoError(t, m.SetUp(true, 50))
require.NoError(t, m.Client.NotifyNewTransactions(false))
t.Cleanup(func() { m.Stop() })

numBlocks := miner.HarnessNetParams.MinerConfirmationWindow * 2
m.GenerateBlocks(numBlocks)

chainBackend, cleanup, err := lntest.NewBackend(
m.P2PAddress(), miner.HarnessNetParams,
)
require.NoError(t, err)
defer func() {
require.NoError(t, cleanup())
}()
require.NoError(t, chainBackend.ConnectMiner())

feeService := lntest.NewFeeService(t)
feeService.SetFeeRate(chainfee.FeePerKwFloor, 1)
require.NoError(t, feeService.Start())
t.Cleanup(func() {
require.NoError(t, feeService.Stop())
})

net := itest.NewIntegratedNetworkHarness(
t, "../tapd-integrated-itest", chainBackend,
miner.HarnessNetParams,
)
net.Miner = m
net.FeeServiceURL = feeService.URL()
net.FeeService = feeService
defer net.TearDown()

// Store the old binary path in the harness so test cases can
// retrieve it. We use an environment variable as a simple
// side channel.
t.Setenv("COMPAT_OLD_BINARY", oldBinary)
Comment on lines +154 to +157
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

medium

Using environment variables to pass data between test setup and test cases can be brittle. A more robust approach would be to extend the ccHarnessTest struct to include the oldBinary path. This would make the data flow explicit and avoid potential issues with parallel test execution if not handled carefully, even though t.Setenv provides some protection.

For example:

// in this file
ht := &ccHarnessTest{
    t:          t1,
    testCase:   tc,
    lndHarness: net,
    oldBinary:  oldBinary, // new field
}

// in test files that need it
oldBinaryPath := ht.oldBinary
if oldBinaryPath != "" {
    // use NewNodeWithBinary
} else {
    // use NewNode
}

This would require modifying the ccHarnessTest struct and how it's used in the test cases, but it leads to cleaner and more maintainable test code.


for _, tc := range compatTestCases {
tc := tc
success := t.Run(tc.name, func(t1 *testing.T) {
ht := &ccHarnessTest{
t: t1,
testCase: tc,
lndHarness: net,
}
ctxt, cancel := context.WithTimeout(
ctx, 10*time.Minute,
)
defer cancel()

tc.test(ctxt, net, ht)
})

net.TearDown()

if !success {
t.Logf("Failure time: %v", time.Now().Format(
"2006-01-02 15:04:05.000",
))
return
}
}
}
47 changes: 47 additions & 0 deletions itest/integrated_harness.go
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,53 @@ func (h *IntegratedNetworkHarness) NewNode(name string,
return n
}

// NewNodeWithBinary creates, starts, and returns a new IntegratedNode that
// runs the specified binary instead of the harness's default binary. This is
// used for backward compatibility testing where some nodes run an older
// release while others run the current build.
func (h *IntegratedNetworkHarness) NewNodeWithBinary(name, binaryPath string,
extraLndArgs, extraTapdArgs []string) *IntegratedNode {

h.t.Helper()

chainArgs := h.chainBackend.GenArgs()
lndArgs := append(chainArgs, extraLndArgs...)

if h.FeeServiceURL != "" {
lndArgs = append(lndArgs, "--fee.url="+h.FeeServiceURL)
}

n := NewIntegratedNode(
h.t, name, binaryPath, h.netParams, lndArgs, extraTapdArgs,
)
n.Start()

h.activeNodes[name] = n

return n
}

// UpgradeNode stops the given node and restarts it using the harness's
// default (current) binary, preserving its data directories. This simulates
// a node operator upgrading their software mid-operation.
func (h *IntegratedNetworkHarness) UpgradeNode(
node *IntegratedNode) {

h.t.Helper()

node.Stop()

// Switch the binary path to the harness's current build.
node.Cfg.BinaryPath = h.binary

// Remove the ready file so Start() waits for the new instance.
if node.readyFile != "" {
_ = os.Remove(node.readyFile)
}

node.Start()
}

// TearDown stops all active nodes managed by this harness.
func (h *IntegratedNetworkHarness) TearDown() {
h.t.Helper()
Expand Down
Loading
Loading