multi: inject clocks into swap lifecycle and update tests

Inject clock.Clock into swapConfig so that swap initiation times
are deterministic and testable. Update all swap tests to use
TestClock with a fixed time, and assert that InitiationTime
matches the clock value.
This commit is contained in:
Slyghtning 2026-03-13 08:58:56 +01:00
parent 66a17f47e6
commit b702aebbf4
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
8 changed files with 63 additions and 19 deletions

View file

@ -22,6 +22,7 @@ import (
"github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/utils" "github.com/lightninglabs/loop/utils"
"github.com/lightninglabs/taproot-assets/rpcutils" "github.com/lightninglabs/taproot-assets/rpcutils"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
"google.golang.org/grpc" "google.golang.org/grpc"
@ -485,7 +486,10 @@ func (s *Client) Run(ctx context.Context, statusChan chan<- SwapInfo) error {
func (s *Client) resumeSwaps(ctx context.Context, func (s *Client) resumeSwaps(ctx context.Context,
loopOutSwaps []*loopdb.LoopOut, loopInSwaps []*loopdb.LoopIn) { loopOutSwaps []*loopdb.LoopOut, loopInSwaps []*loopdb.LoopIn) {
swapCfg := newSwapConfig(s.lndServices, s.Store, s.Server, s.AssetClient) swapCfg := newSwapConfig(
s.lndServices, s.Store, s.Server, s.AssetClient,
clock.NewDefaultClock(),
)
for _, pend := range loopOutSwaps { for _, pend := range loopOutSwaps {
if pend.State().State.Type() != loopdb.StateTypePending { if pend.State().State.Type() != loopdb.StateTypePending {
@ -578,6 +582,7 @@ func (s *Client) LoopOut(globalCtx context.Context,
// Create a new swap object for this swap. // Create a new swap object for this swap.
swapCfg := newSwapConfig( swapCfg := newSwapConfig(
s.lndServices, s.Store, s.Server, s.AssetClient, s.lndServices, s.Store, s.Server, s.AssetClient,
clock.NewDefaultClock(),
) )
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(
@ -759,7 +764,10 @@ func (s *Client) LoopIn(globalCtx context.Context,
// Create a new swap object for this swap. // Create a new swap object for this swap.
initiationHeight := s.executor.height() initiationHeight := s.executor.height()
swapCfg := newSwapConfig(s.lndServices, s.Store, s.Server, s.AssetClient) swapCfg := newSwapConfig(
s.lndServices, s.Store, s.Server, s.AssetClient,
clock.NewDefaultClock(),
)
initResult, err := newLoopInSwap( initResult, err := newLoopInSwap(
globalCtx, swapCfg, initiationHeight, request, globalCtx, swapCfg, initiationHeight, request,
) )

View file

@ -9,6 +9,7 @@ import (
"github.com/lightninglabs/lndclient" "github.com/lightninglabs/lndclient"
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -25,6 +26,7 @@ func TestCalculateLoopOutCost(t *testing.T) {
lnd: &lnd.LndServices, lnd: &lnd.LndServices,
store: store, store: store,
server: server, server: server,
clock: clock.NewTestClock(time.Unix(123, 0)),
} }
height := int32(600) height := int32(600)
@ -118,6 +120,7 @@ func TestCostMigration(t *testing.T) {
lnd: &lnd.LndServices, lnd: &lnd.LndServices,
store: store, store: store,
server: server, server: server,
clock: clock.NewTestClock(time.Unix(123, 0)),
} }
height := int32(600) height := int32(600)

View file

@ -1005,7 +1005,8 @@ func (d *Daemon) initialize(withMacaroonService bool) error {
} }
loop.Resume( loop.Resume(
d.mainCtx, notificationManager, swapClient.Store, d.impl.Conn, d.lnd, d.mainCtx, notificationManager, swapClient.Store,
d.impl.Conn, d.lnd, clock.NewDefaultClock(),
) )
// Last, start our internal error handler. This will return exactly one // Last, start our internal error handler. This will return exactly one

View file

@ -7,7 +7,6 @@ import (
"errors" "errors"
"fmt" "fmt"
"sync" "sync"
"time"
"github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
@ -264,7 +263,7 @@ func newLoopInSwap(globalCtx context.Context, cfg *swapConfig,
// Instantiate a struct that contains all required data to start the // Instantiate a struct that contains all required data to start the
// swap. // swap.
initiationTime := time.Now() initiationTime := cfg.clock.Now()
contract := loopdb.LoopInContract{ contract := loopdb.LoopInContract{
HtlcConfTarget: request.HtlcConfTarget, HtlcConfTarget: request.HtlcConfTarget,
@ -837,7 +836,7 @@ func (s *loopInSwap) publishOnChainHtlc(ctx context.Context) (bool, error) {
// the fee for publishing the htlc. // the fee for publishing the htlc.
s.cost.Onchain = fee s.cost.Onchain = fee
s.lastUpdateTime = time.Now() s.lastUpdateTime = s.clock.Now()
if err := s.persistState(ctx); err != nil { if err := s.persistState(ctx); err != nil {
return false, fmt.Errorf("persist htlc tx: %v", err) return false, fmt.Errorf("persist htlc tx: %v", err)
} }
@ -1210,7 +1209,7 @@ func (s *loopInSwap) persistState(ctx context.Context) error {
// setState updates the swap state and last update timestamp. // setState updates the swap state and last update timestamp.
func (s *loopInSwap) setState(state loopdb.SwapState) { func (s *loopInSwap) setState(state loopdb.SwapState) {
s.lastUpdateTime = time.Now() s.lastUpdateTime = s.clock.Now()
s.state = state s.state = state
} }

View file

@ -13,6 +13,7 @@ import (
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
"github.com/lightninglabs/loop/utils" "github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/clock"
invpkg "github.com/lightningnetwork/lnd/invoices" invpkg "github.com/lightningnetwork/lnd/invoices"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/routing/route"
@ -129,8 +130,12 @@ func testLoopInSuccess(t *testing.T) {
ctx := newLoopInTestContext(t) ctx := newLoopInTestContext(t)
height := int32(600) height := int32(600)
startTime := time.Unix(123, 0)
cfg := newSwapConfig(&ctx.lnd.LndServices, ctx.store, ctx.server, nil) cfg := newSwapConfig(
&ctx.lnd.LndServices, ctx.store, ctx.server, nil,
clock.NewTestClock(startTime),
)
expectedLastHop := &route.Vertex{0x02} expectedLastHop := &route.Vertex{0x02}
@ -144,6 +149,7 @@ func testLoopInSuccess(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
inSwap := initResult.swap inSwap := initResult.swap
require.Equal(t, startTime, inSwap.InitiationTime)
ctx.store.AssertLoopInStored() ctx.store.AssertLoopInStored()
@ -279,7 +285,10 @@ func testLoopInTimeout(t *testing.T, externalValue int64) {
height := int32(600) height := int32(600)
cfg := newSwapConfig(&ctx.lnd.LndServices, ctx.store, ctx.server, nil) cfg := newSwapConfig(
&ctx.lnd.LndServices, ctx.store, ctx.server, nil,
clock.NewTestClock(time.Unix(123, 0)),
)
req := testLoopInRequest req := testLoopInRequest
if externalValue != 0 { if externalValue != 0 {
@ -490,7 +499,10 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool,
ctxb := context.Background() ctxb := context.Background()
ctx := newLoopInTestContext(t) ctx := newLoopInTestContext(t)
cfg := newSwapConfig(&ctx.lnd.LndServices, ctx.store, ctx.server, nil) cfg := newSwapConfig(
&ctx.lnd.LndServices, ctx.store, ctx.server, nil,
clock.NewTestClock(time.Unix(123, 0)),
)
// Create sender and receiver keys. // Create sender and receiver keys.
_, senderPubKey := test.CreateKey(1) _, senderPubKey := test.CreateKey(1)
@ -845,7 +857,10 @@ func advanceToPublishedHtlc(t *testing.T, ctx *loopInTestContext) SwapInfo {
func startNewLoopIn(t *testing.T, ctx *loopInTestContext, height int32) ( func startNewLoopIn(t *testing.T, ctx *loopInTestContext, height int32) (
*swapConfig, *loopInSwap, error) { *swapConfig, *loopInSwap, error) {
cfg := newSwapConfig(&ctx.lnd.LndServices, ctx.store, ctx.server, nil) cfg := newSwapConfig(
&ctx.lnd.LndServices, ctx.store, ctx.server, nil,
clock.NewTestClock(time.Unix(123, 0)),
)
req := &testLoopInRequest req := &testLoopInRequest

View file

@ -25,6 +25,7 @@ import (
"github.com/lightninglabs/taproot-assets/fn" "github.com/lightninglabs/taproot-assets/fn"
"github.com/lightninglabs/taproot-assets/rfqmsg" "github.com/lightninglabs/taproot-assets/rfqmsg"
"github.com/lightningnetwork/lnd/chainntnfs" "github.com/lightningnetwork/lnd/chainntnfs"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
paymentsdb "github.com/lightningnetwork/lnd/payments/db" paymentsdb "github.com/lightningnetwork/lnd/payments/db"
@ -192,7 +193,7 @@ func newLoopOutSwap(globalCtx context.Context, cfg *swapConfig,
// Instantiate a struct that contains all required data to start the // Instantiate a struct that contains all required data to start the
// swap. // swap.
initiationTime := time.Now() initiationTime := cfg.clock.Now()
contract := loopdb.LoopOutContract{ contract := loopdb.LoopOutContract{
SwapInvoice: swapResp.swapInvoice, SwapInvoice: swapResp.swapInvoice,
@ -633,7 +634,7 @@ func (s *loopOutSwap) executeSwap(globalCtx context.Context) error {
// persistState updates the swap state and sends out an update notification. // persistState updates the swap state and sends out an update notification.
func (s *loopOutSwap) persistState(ctx context.Context) error { func (s *loopOutSwap) persistState(ctx context.Context) error {
updateTime := time.Now() updateTime := s.clock.Now()
s.lastUpdateTime = updateTime s.lastUpdateTime = updateTime
@ -864,12 +865,12 @@ func (s *loopOutSwap) payInvoiceAsync(ctx context.Context,
payCtx, cancel := context.WithCancel(ctx) payCtx, cancel := context.WithCancel(ctx)
defer cancel() defer cancel()
start := time.Now() start := s.clock.Now()
paymentStatus, attempts, err := s.sendPaymentWithRetry( paymentStatus, attempts, err := s.sendPaymentWithRetry(
payCtx, hash, &req, maxRetries, routingPlugin, pluginType, payCtx, hash, &req, maxRetries, routingPlugin, pluginType,
) )
dt := time.Since(start) dt := s.clock.Now().Sub(start)
paymentSuccess := err == nil && paymentSuccess := err == nil &&
paymentStatus.State == lnrpc.Payment_SUCCEEDED paymentStatus.State == lnrpc.Payment_SUCCEEDED
@ -1543,6 +1544,7 @@ type resumeManager struct {
swapStore loopdb.SwapStore swapStore loopdb.SwapStore
swapClient swapserverrpc.SwapServerClient swapClient swapserverrpc.SwapServerClient
lnd *lndclient.GrpcLndServices lnd *lndclient.GrpcLndServices
clock clock.Clock
reqChan chan *swapserverrpc.ServerUnfinishedSwapNotification reqChan chan *swapserverrpc.ServerUnfinishedSwapNotification
} }
@ -1552,13 +1554,14 @@ type resumeManager struct {
func Resume(ctx context.Context, ntfnManager NotificationManager, func Resume(ctx context.Context, ntfnManager NotificationManager,
swapStore loopdb.SwapStore, swapStore loopdb.SwapStore,
swapClientConn *grpc.ClientConn, swapClientConn *grpc.ClientConn,
lnd *lndclient.GrpcLndServices) { lnd *lndclient.GrpcLndServices, clock clock.Clock) {
resumeManager := &resumeManager{ resumeManager := &resumeManager{
ntfnManager: ntfnManager, ntfnManager: ntfnManager,
swapStore: swapStore, swapStore: swapStore,
swapClient: swapserverrpc.NewSwapServerClient(swapClientConn), swapClient: swapserverrpc.NewSwapServerClient(swapClientConn),
lnd: lnd, lnd: lnd,
clock: clock,
reqChan: make(chan *swapserverrpc.ServerUnfinishedSwapNotification, 1), reqChan: make(chan *swapserverrpc.ServerUnfinishedSwapNotification, 1),
} }
go resumeManager.start(ctx) go resumeManager.start(ctx)
@ -1717,7 +1720,7 @@ func (m *resumeManager) resumeLoopOutPayment(ctx context.Context,
cost.Server = payResp.Value.ToSatoshis() - amtRequested cost.Server = payResp.Value.ToSatoshis() - amtRequested
cost.Offchain = payResp.Fee.ToSatoshis() cost.Offchain = payResp.Fee.ToSatoshis()
// Payment succeeded. // Payment succeeded.
updateTime := time.Now() updateTime := m.clock.Now()
// Update state in store. // Update state in store.
err = m.swapStore.UpdateLoopOut( err = m.swapStore.UpdateLoopOut(

View file

@ -18,6 +18,7 @@ import (
"github.com/lightninglabs/loop/sweep" "github.com/lightninglabs/loop/sweep"
"github.com/lightninglabs/loop/sweepbatcher" "github.com/lightninglabs/loop/sweepbatcher"
"github.com/lightninglabs/loop/test" "github.com/lightninglabs/loop/test"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lnrpc" "github.com/lightningnetwork/lnd/lnrpc"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
"github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwallet/chainfee"
@ -57,11 +58,13 @@ func testLoopOutPaymentParameters(t *testing.T) {
} }
height := int32(600) height := int32(600)
startTime := time.Unix(123, 0)
cfg := &swapConfig{ cfg := &swapConfig{
lnd: &lnd.LndServices, lnd: &lnd.LndServices,
store: store, store: store,
server: server, server: server,
clock: clock.NewTestClock(startTime),
} }
sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices} sweeper := &sweep.Sweeper{Lnd: &lnd.LndServices}
@ -82,6 +85,7 @@ func testLoopOutPaymentParameters(t *testing.T) {
) )
require.NoError(t, err) require.NoError(t, err)
swap := initResult.swap swap := initResult.swap
require.Equal(t, startTime, swap.InitiationTime)
// Execute the swap in its own goroutine. // Execute the swap in its own goroutine.
errChan := make(chan error) errChan := make(chan error)
@ -193,7 +197,10 @@ func testLateHtlcPublish(t *testing.T) {
height := int32(600) height := int32(600)
cfg := newSwapConfig(&lnd.LndServices, store, server, nil) cfg := newSwapConfig(
&lnd.LndServices, store, server, nil,
clock.NewTestClock(time.Unix(123, 0)),
)
testRequest.Expiry = height + testLoopOutMinOnChainCltvDelta testRequest.Expiry = height + testLoopOutMinOnChainCltvDelta
@ -296,6 +303,7 @@ func testCustomSweepConfTarget(t *testing.T) {
cfg := newSwapConfig( cfg := newSwapConfig(
&lnd.LndServices, loopdb.NewStoreMock(t), server, nil, &lnd.LndServices, loopdb.NewStoreMock(t), server, nil,
clock.NewTestClock(time.Unix(123, 0)),
) )
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(
@ -539,6 +547,7 @@ func testPreimagePush(t *testing.T) {
cfg := newSwapConfig( cfg := newSwapConfig(
&lnd.LndServices, loopdb.NewStoreMock(t), server, nil, &lnd.LndServices, loopdb.NewStoreMock(t), server, nil,
clock.NewTestClock(time.Unix(123, 0)),
) )
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(
@ -797,6 +806,7 @@ func testFailedOffChainCancelation(t *testing.T) {
cfg := newSwapConfig( cfg := newSwapConfig(
&lnd.LndServices, loopdb.NewStoreMock(t), server, nil, &lnd.LndServices, loopdb.NewStoreMock(t), server, nil,
clock.NewTestClock(time.Unix(123, 0)),
) )
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(
@ -951,6 +961,7 @@ func TestLoopOutMuSig2Sweep(t *testing.T) {
cfg := newSwapConfig( cfg := newSwapConfig(
&lnd.LndServices, loopdb.NewStoreMock(t), server, nil, &lnd.LndServices, loopdb.NewStoreMock(t), server, nil,
clock.NewTestClock(time.Unix(123, 0)),
) )
initResult, err := newLoopOutSwap( initResult, err := newLoopOutSwap(

View file

@ -9,6 +9,7 @@ import (
"github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/loopdb"
"github.com/lightninglabs/loop/swap" "github.com/lightninglabs/loop/swap"
"github.com/lightninglabs/loop/utils" "github.com/lightninglabs/loop/utils"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lntypes"
) )
@ -83,15 +84,18 @@ type swapConfig struct {
store loopdb.SwapStore store loopdb.SwapStore
server swapServerClient server swapServerClient
assets *assets.TapdClient assets *assets.TapdClient
clock clock.Clock
} }
func newSwapConfig(lnd *lndclient.LndServices, store loopdb.SwapStore, func newSwapConfig(lnd *lndclient.LndServices, store loopdb.SwapStore,
server swapServerClient, assets *assets.TapdClient) *swapConfig { server swapServerClient, assets *assets.TapdClient,
clock clock.Clock) *swapConfig {
return &swapConfig{ return &swapConfig{
lnd: lnd, lnd: lnd,
store: store, store: store,
server: server, server: server,
assets: assets, assets: assets,
clock: clock,
} }
} }