diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md index 66ed7136..a6d279b5 100644 --- a/.github/pull_request_template.md +++ b/.github/pull_request_template.md @@ -1,2 +1,3 @@ #### Pull Request Checklist -- [ ] Update `release_notes.md` if your PR contains major features, breaking changes or bugfixes +- [ ] Add an entry to `docs/release-notes/release-notes-next.md`, or apply the + `no-changelog` label (required by CI) diff --git a/.github/workflows/gateway.yml b/.github/workflows/gateway.yml new file mode 100644 index 00000000..1462b01f --- /dev/null +++ b/.github/workflows/gateway.yml @@ -0,0 +1,66 @@ +name: gateway + +# Opt-in code-review bot. Triggered by a `/gateway ` comment on a PR +# (e.g. `/gateway review`); review/approve commands are gated to maintainers. +# Comment-commands only — no pull_request triggers — so fork PRs (which receive +# no secrets) never spawn failing runs. v0.5.0 added the +# pull_request_review_comment trigger: /gateway dismiss, promote, and explain +# now also work as replies on a finding's inline thread (finding id inferred +# from the thread when omitted). Also a comment event — same fork-PR safety +# profile as issue_comment. +# +# Thin shim: the public lightninglabs/gateway-action mints an App token and +# checks out the private gateway runtime at execution time. The runtime stays +# private; only this entry point is public. + +on: + issue_comment: + types: [created] + pull_request_review_comment: + types: [created] + +permissions: + # The action mints an App installation token internally; the GITHUB_TOKEN + # handed to this shim is unused, so we minimise it. + contents: read + +jobs: + review: + # issue_comment fires for all issues and every PR comment. Filter to PR + # comments that look like a /gateway command so unrelated comments don't + # spin up a no-op runner. `contains` (not `startsWith`) because the runtime + # accepts the command at column 0 of any line, including multi-line bodies. + if: >- + ${{ + (github.event_name == 'issue_comment' + && github.event.issue.pull_request != null + && contains(github.event.comment.body, '/gateway')) || + (github.event_name == 'pull_request_review_comment' + && contains(github.event.comment.body, '/gateway')) + }} + runs-on: ubuntu-latest + timeout-minutes: 15 + env: + GATEWAY_REVIEW_MODE: multi + steps: + - uses: lightninglabs/gateway-action@334a8455ee316e40668ae3ac85249150c62704ec # v0.6.0 + with: + # Pin the private runtime to an immutable commit (matches the action + # SHA-pin above) so runtime upgrades go through a loop PR, not a moved + # tag. Without this, runtime_ref defaults to the v0.6.0 tag. + runtime_ref: 75f6e67deac362bdcfc10d10629ddcf69c0e2615 # gateway v0.6.0 + event_name: ${{ github.event_name }} + event_action: ${{ github.event.action }} + repo: ${{ github.repository }} + pr_number: ${{ github.event.issue.number || github.event.pull_request.number }} + actor: ${{ github.event.sender.login }} + comment_body: ${{ github.event.comment.body }} + comment_id: ${{ github.event.comment.id }} + comment_in_reply_to: ${{ github.event.comment.in_reply_to_id }} + # installation_id intentionally omitted: as of gateway v0.4.4 the + # runtime resolves the App installation covering this repo from + # app_id/private_key, so a hardcoded (and easily wrong-org) id is no + # longer needed. + app_id: ${{ secrets.GATEWAY_APP_ID }} + private_key: ${{ secrets.GATEWAY_PRIVATE_KEY }} + claude_code_oauth_token: ${{ secrets.CLAUDE_CODE_OAUTH_TOKEN }} diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 945c42f7..3250faf5 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -24,6 +24,37 @@ env: LITD_ITEST_BRANCH: master jobs: + ######################## + # commit message lint + ######################## + commit-message: + name: Commit Message + runs-on: ubuntu-latest + steps: + - name: git checkout + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Lint commit messages + env: + EVENT_NAME: ${{ github.event_name }} + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + BEFORE_SHA: ${{ github.event.before }} + GITHUB_SHA: ${{ github.sha }} + run: | + if [ "$EVENT_NAME" = "pull_request" ]; then + range="$BASE_SHA..$HEAD_SHA" + elif [ "$EVENT_NAME" = "push" ] && + [ "$BEFORE_SHA" != "0000000000000000000000000000000000000000" ]; then + range="$BEFORE_SHA..$GITHUB_SHA" + else + range="$(git rev-parse HEAD^ 2>/dev/null)..HEAD" + fi + + make commitmsg-lint range="$range" + ######################## # RPC compile and check ######################## @@ -124,6 +155,9 @@ jobs: - name: check run: make docs-check + - name: check generated FSM diagrams + run: make fsm-check + ######################## # run unit-test sqlite3 race ######################## diff --git a/.github/workflows/release-notes.yml b/.github/workflows/release-notes.yml new file mode 100644 index 00000000..818ccbf4 --- /dev/null +++ b/.github/workflows/release-notes.yml @@ -0,0 +1,58 @@ +name: Release Notes + +on: + pull_request: + branches: + - "*" + types: + - opened + - reopened + - synchronize + - labeled + - unlabeled + +permissions: + contents: read + +defaults: + run: + shell: bash + +jobs: + release-notes: + name: Release Notes + runs-on: ubuntu-latest + steps: + - name: Skip release notes check + if: contains(github.event.pull_request.labels.*.name, 'no-changelog') + run: echo "Release notes check skipped by the no-changelog label." + + - name: git checkout + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-changelog') }} + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check for a release note + if: ${{ !contains(github.event.pull_request.labels.*.name, 'no-changelog') }} + env: + BASE_SHA: ${{ github.event.pull_request.base.sha }} + HEAD_SHA: ${{ github.event.pull_request.head.sha }} + RELEASE_NOTES_FILE: docs/release-notes/release-notes-next.md + run: | + if git diff --unified=0 "$BASE_SHA...$HEAD_SHA" -- \ + "$RELEASE_NOTES_FILE" | + awk ' + /^@@/ { in_hunk = 1; next } + in_hunk && /^\+/ { + line = substr($0, 2) + if (line ~ /[^[:space:]]/) found = 1 + } + END { exit found ? 0 : 1 } + ' + then + exit 0 + fi + + echo "::error file=$RELEASE_NOTES_FILE::Add a release note or apply the no-changelog label." + exit 1 diff --git a/.golangci.yml b/.golangci.yml index 914e5789..329dcde2 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -3,7 +3,7 @@ run: go: "1.26" # timeout for analysis - timeout: 4m + timeout: 6m linters: default: all @@ -55,14 +55,16 @@ linters: - wsl_v5 - noinlineerr settings: + staticcheck: + checks: + - all + - -QF* + - -ST* gosec: excludes: - G402 - G306 - G115 - staticcheck: - checks: - - -SA1019 tagliatelle: case: rules: diff --git a/AGENTS.md b/AGENTS.md index 52600b56..22f10a06 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -51,3 +51,33 @@ The project is a client daemon (`loopd`) that connects to a user's `lnd` node an * **Cryptography:** Extensively uses Taproot and MuSig2 for efficiency, privacy, and complex spending conditions, especially in the `instantout` and `staticaddr` features. * **Labeling (`labels/`):** A utility to create and validate labels for swaps, which helps distinguish between user-initiated and automated swaps (e.g., `[reserved]: autoloop-out`). * **Assets (`assets/`):** Contains logic for interacting with `tapd` (Taproot Assets Protocol Daemon), allowing Loop to facilitate swaps involving assets other than Bitcoin. + +**5. Minimum `lnd` Version (`loopd/run.go`):** + +`loopd` enforces a minimum `lnd` version at startup through +`LoopMinRequiredLndVersion` in `loopd/run.go` (handed to `lndclient` as +`CheckVersion`). This is a hard gate: loopd refuses to start against an older +`lnd` node. + +**Maintenance rule:** whenever you start using an `lnd` gRPC method or message +field that does not exist in older `lnd`, bump `LoopMinRequiredLndVersion` to the +`lnd` release that introduced that API, and record which API drove the bump in the +comment above the variable. Pin it to the *real* floor of the APIs the client uses +— do **not** just track the `go.mod` dependency. Historically this value only +tracked `go.mod` and drifted out of sync with the APIs actually called (it sat at +0.17.0 while the client already depended on 0.18.4 APIs). To find the introducing +release, grep the field/method across `lnd` version tags, e.g. +`git grep -- `. As of the current floor (**v0.18.4-beta**) +the binding dependencies are the asset loop-out fields +`routerrpc.SendPaymentRequest.first_hop_custom_records` and +`lnrpc.Route.custom_channel_data` (lnd v0.18.4-beta), plus the sweep-batcher fee +floor `walletrpc.EstimateFeeResponse.min_relay_fee_sat_per_kw` (lnd v0.18.3-beta). + +**6. Pull Request Release Notes:** + +Every pull request must either add at least one non-empty line to +`docs/release-notes/release-notes-next.md` or carry the `no-changelog` label. +Before finishing any change intended for a pull request, add a concise entry +under the appropriate section (`New Features`, `Breaking Changes`, `Bug Fixes`, +or `Maintenance`). For a change that does not warrant a release note, ensure the +pull request uses the `no-changelog` label instead. This is enforced by CI. diff --git a/Makefile b/Makefile index a4a12d1b..1caeef2d 100644 --- a/Makefile +++ b/Makefile @@ -174,6 +174,43 @@ endif cd tools/ && GOPROXY=direct $(GOMOD) tidy if test -n "$$(git status --porcelain)"; then echo "Running go mod tidy changes go.mod/go.sum"; git status; git diff; exit 1; fi +commitmsg-lint: + @$(call print, "Linting commit message(s).") + @if [ -n "$(range)" ]; then \ + ./scripts/commit_message.py lint --range "$(range)"; \ + elif [ -n "$(commit)" ]; then \ + ./scripts/commit_message.py lint --commit "$(commit)"; \ + elif [ -n "$(file)" ]; then \ + ./scripts/commit_message.py lint --file "$(file)"; \ + else \ + ./scripts/commit_message.py lint --commit HEAD; \ + fi + +commitmsg-fmt: + @$(call print, "Formatting commit message.") + @if [ -n "$(file)" ]; then \ + if [ "$(inplace)" = "1" ]; then \ + ./scripts/commit_message.py fmt --file "$(file)" --in-place \ + $(if $(filter 1,$(decode)),--decode-escaped-newlines,); \ + else \ + ./scripts/commit_message.py fmt --file "$(file)" \ + $(if $(filter 1,$(decode)),--decode-escaped-newlines,); \ + fi; \ + elif [ -n "$(commit)" ]; then \ + ./scripts/commit_message.py fmt --commit "$(commit)" \ + $(if $(filter 1,$(decode)),--decode-escaped-newlines,); \ + else \ + echo "Error: provide file= or commit="; \ + exit 1; \ + fi + +commitmsg-reword: + @$(call print, "Rewording commit with formatted message.") + @./scripts/commit_message.py reword \ + --commit "$(if $(commit),$(commit),HEAD)" \ + $(if $(filter 1,$(decode)),--decode-escaped-newlines,) \ + $(if $(filter 1,$(dryrun)),--dry-run,) + sqlc: @$(call print, "Generating sql models and queries in Go") ./scripts/gen_sqlc_docker.sh @@ -194,4 +231,20 @@ docs-check: docs fsm: @$(call print, "Generating state machine docs") ./scripts/fsm-generate.sh; -.PHONY: fsm + +FSM_FILES := \ + fsm/example_fsm.md \ + instantout/fsm.md \ + instantout/reservation/fsm.md \ + staticaddr/deposit/fsm.md \ + staticaddr/loopin/fsm.md + +fsm-check: fsm + @$(call print, "Verifying generated state machine docs") + if test -n "$$(git status --porcelain -- $(FSM_FILES))"; then \ + echo "Generated FSM diagrams are not up-to-date!"; \ + git status --porcelain -- $(FSM_FILES); \ + git diff -- $(FSM_FILES); \ + exit 1; \ + fi +.PHONY: fsm fsm-check diff --git a/README.md b/README.md index 0bba28ef..e90f858e 100644 --- a/README.md +++ b/README.md @@ -133,4 +133,4 @@ go install ./... ## Reproducible builds If you want to build release files yourself, follow -[the guide](./docs/release.md). +[the guide](./docs/reproducible_release.md). diff --git a/assets/client.go b/assets/client.go index c224f333..7eef98b0 100644 --- a/assets/client.go +++ b/assets/client.go @@ -4,14 +4,15 @@ import ( "context" "encoding/hex" "fmt" + "math" + "math/big" "os" + "path/filepath" "sync" "time" "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/taproot-assets/rfqmath" - "github.com/lightninglabs/taproot-assets/rpcutils" - "github.com/lightninglabs/taproot-assets/tapcfg" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" @@ -49,13 +50,24 @@ type TapdConfig struct { // DefaultTapdConfig returns a default configuration to connect to a taproot // assets daemon. func DefaultTapdConfig() *TapdConfig { - defaultConf := tapcfg.DefaultConfig() + return DefaultTapdConfigForNetwork( + btcutil.AppDataDir("tapd", false), "mainnet", + ) +} + +// DefaultTapdConfigForNetwork returns the default tapd configuration rooted in +// defaultTapdDir for the given Bitcoin network. Passing the directory +// explicitly keeps path construction testable without accessing the user's +// real tapd data. +func DefaultTapdConfigForNetwork(defaultTapdDir, network string) *TapdConfig { return &TapdConfig{ - Activate: false, - Host: "localhost:10029", - MacaroonPath: defaultConf.RpcConf.MacaroonPath, - TLSPath: defaultConf.RpcConf.TLSCertPath, - RFQtimeout: defaultRfqTimeout, + Activate: false, + Host: "localhost:10029", + MacaroonPath: filepath.Join( + defaultTapdDir, "data", network, "admin.macaroon", + ), + TLSPath: filepath.Join(defaultTapdDir, "tls.cert"), + RFQtimeout: defaultRfqTimeout, } } @@ -67,14 +79,19 @@ type TapdClient struct { rfqrpc.RfqClient universerpc.UniverseClient - cfg *TapdConfig - assetNameCache map[string]string - assetNameMutex sync.Mutex - cc *grpc.ClientConn + rfqTimeoutSeconds uint32 + assetNameCache map[string]string + assetNameMutex sync.RWMutex + cc *grpc.ClientConn } // NewTapdClient returns a new taproot assets client. func NewTapdClient(config *TapdConfig) (*TapdClient, error) { + rfqTimeoutSeconds, err := getRfqTimeoutSeconds(config.RFQtimeout) + if err != nil { + return nil, err + } + // Create the client connection to the server. conn, err := getClientConn(config) if err != nil { @@ -85,7 +102,7 @@ func NewTapdClient(config *TapdConfig) (*TapdClient, error) { client := &TapdClient{ assetNameCache: make(map[string]string), cc: conn, - cfg: config, + rfqTimeoutSeconds: rfqTimeoutSeconds, TaprootAssetsClient: taprpc.NewTaprootAssetsClient(conn), TaprootAssetChannelsClient: tapchannelrpc.NewTaprootAssetChannelsClient(conn), PriceOracleClient: priceoraclerpc.NewPriceOracleClient(conn), @@ -128,7 +145,7 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context, PeerPubKey: peerPubkey, PaymentMaxAmt: uint64(paymentMaxAmt), Expiry: uint64(expiry), - TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()), + TimeoutSeconds: c.rfqTimeoutSeconds, }) if err != nil { return nil, err @@ -141,21 +158,26 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context, rfq.GetRejectedQuote()) } - if rfq.GetAcceptedQuote() != nil { - return rfq.GetAcceptedQuote(), nil + acceptedQuote := rfq.GetAcceptedQuote() + if acceptedQuote == nil { + return nil, fmt.Errorf("no accepted quote") } - return nil, fmt.Errorf("no accepted quote") + _, err = unmarshalAssetRate(acceptedQuote.BidAssetRate) + if err != nil { + return nil, fmt.Errorf("invalid accepted quote asset rate: %w", + err) + } + + return acceptedQuote, nil } // GetAssetName returns the human-readable name of the asset. func (c *TapdClient) GetAssetName(ctx context.Context, assetId []byte) (string, error) { - c.assetNameMutex.Lock() - defer c.assetNameMutex.Unlock() assetIdStr := hex.EncodeToString(assetId) - if name, ok := c.assetNameCache[assetIdStr]; ok { + if name, ok := c.getCachedAssetName(assetIdStr); ok { return name, nil } @@ -181,11 +203,28 @@ func (c *TapdClient) GetAssetName(ctx context.Context, assetName = assetStats.AssetStats[0].Asset.AssetName } - c.assetNameCache[assetIdStr] = assetName + c.cacheAssetName(assetIdStr, assetName) return assetName, nil } +// getCachedAssetName returns an asset name from the cache. +func (c *TapdClient) getCachedAssetName(assetID string) (string, bool) { + c.assetNameMutex.RLock() + defer c.assetNameMutex.RUnlock() + + name, ok := c.assetNameCache[assetID] + return name, ok +} + +// cacheAssetName adds an asset name to the cache. +func (c *TapdClient) cacheAssetName(assetID, name string) { + c.assetNameMutex.Lock() + defer c.assetNameMutex.Unlock() + + c.assetNameCache[assetID] = name +} + // GetAssetPrice returns the price of an asset in satoshis. NOTE: this currently // uses the rfq process for the asset price. A future implementation should // use a price oracle to not spam a peer. @@ -209,7 +248,7 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string, }, PaymentMaxAmt: uint64(msatAmt), Expiry: uint64(rfqExpiry), - TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()), + TimeoutSeconds: c.rfqTimeoutSeconds, PeerPubKey: peerPubkey, }) if err != nil { @@ -243,7 +282,7 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string, func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) ( btcutil.Amount, error) { - rateFP, err := rpcutils.UnmarshalRfqFixedPoint(assetRate) + rateFP, err := unmarshalAssetRate(assetRate) if err != nil { return 0, fmt.Errorf("cannot unmarshal asset rate: %w", err) } @@ -255,6 +294,33 @@ func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) ( return msatAmt.ToSatoshis(), nil } +// unmarshalAssetRate validates and converts an RPC asset rate to the fixed +// point representation used for RFQ arithmetic. +func unmarshalAssetRate(assetRate *rfqrpc.FixedPoint) ( + *rfqmath.BigIntFixedPoint, error) { + + if assetRate == nil { + return nil, fmt.Errorf("asset rate cannot be nil") + } + if assetRate.Scale > math.MaxUint8 { + return nil, fmt.Errorf("scale value overflow: %v", assetRate.Scale) + } + + coefficient, ok := new(big.Int).SetString(assetRate.Coefficient, 10) + if !ok { + return nil, fmt.Errorf("invalid asset rate coefficient: %q", + assetRate.Coefficient) + } + if coefficient.Sign() <= 0 { + return nil, fmt.Errorf("asset rate coefficient must be positive") + } + + return &rfqmath.BigIntFixedPoint{ + Coefficient: rfqmath.NewBigInt(coefficient), + Scale: uint8(assetRate.Scale), + }, nil +} + // getPaymentMaxAmount returns the milisat amount we are willing to pay for the // payment. func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( @@ -277,6 +343,26 @@ func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( ) } +// getRfqTimeoutSeconds converts the configured RFQ timeout to the whole +// seconds accepted by tapd. Fractional seconds are rounded up so tapd's +// timeout is never shorter than the configured duration. +func getRfqTimeoutSeconds(timeout time.Duration) (uint32, error) { + if timeout <= 0 { + return 0, fmt.Errorf("RFQ timeout must be greater than zero") + } + + seconds := timeout / time.Second + if timeout%time.Second != 0 { + seconds++ + } + if seconds > time.Duration(math.MaxUint32) { + return 0, fmt.Errorf("RFQ timeout exceeds maximum of %v seconds", + uint64(math.MaxUint32)) + } + + return uint32(seconds), nil +} + func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) { // Load the specified TLS certificate and build transport credentials. creds, err := credentials.NewClientTLSFromFile(config.TLSPath, "") diff --git a/assets/client_test.go b/assets/client_test.go index 6b791aaf..5941ae44 100644 --- a/assets/client_test.go +++ b/assets/client_test.go @@ -1,14 +1,269 @@ package assets import ( + "context" + "encoding/hex" + "encoding/pem" + "math" + "net/http" + "net/http/httptest" + "os" + "path/filepath" "testing" + "time" "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" + "github.com/lightninglabs/taproot-assets/taprpc/universerpc" "github.com/lightningnetwork/lnd/lnwire" "github.com/stretchr/testify/require" + "google.golang.org/grpc" + "gopkg.in/macaroon.v2" ) +type blockingUniverseClient struct { + universerpc.UniverseClient + + queryStarted chan struct{} + releaseQuery chan struct{} +} + +func (b *blockingUniverseClient) QueryAssetStats(context.Context, + *universerpc.AssetStatsQuery, ...grpc.CallOption) ( + *universerpc.UniverseAssetStats, error) { + + close(b.queryStarted) + <-b.releaseQuery + + return &universerpc.UniverseAssetStats{ + AssetStats: []*universerpc.AssetStatsSnapshot{ + { + Asset: &universerpc.AssetStatsAsset{ + AssetName: "queried asset", + }, + }, + }, + }, nil +} + +type staticRfqClient struct { + rfqrpc.RfqClient + + response *rfqrpc.AddAssetSellOrderResponse +} + +func (s *staticRfqClient) AddAssetSellOrder(context.Context, + *rfqrpc.AddAssetSellOrderRequest, ...grpc.CallOption) ( + *rfqrpc.AddAssetSellOrderResponse, error) { + + return s.response, nil +} + +// TestDefaultTapdConfig tests that the default tapd connection paths match +// tapd's mainnet defaults. +func TestDefaultTapdConfig(t *testing.T) { + defaultTapdDir := btcutil.AppDataDir("tapd", false) + config := DefaultTapdConfig() + + require.Equal(t, filepath.Join( + defaultTapdDir, "data", "mainnet", "admin.macaroon", + ), config.MacaroonPath) + require.Equal( + t, filepath.Join(defaultTapdDir, "tls.cert"), config.TLSPath, + ) +} + +// TestTapdConfigClientConn tests that the default tapd file layout can be used +// to construct a client connection. +func TestTapdConfigClientConn(t *testing.T) { + // Use an isolated tapd root so the test never reads from or writes to a + // user's real tapd data directory. + defaultTapdDir := t.TempDir() + network := "regtest" + macaroonPath := filepath.Join( + defaultTapdDir, "data", network, "admin.macaroon", + ) + require.NoError(t, os.MkdirAll(filepath.Dir(macaroonPath), 0o700)) + + // NewTapdClient parses the configured TLS certificate before creating + // its gRPC client. An httptest server provides a valid certificate + // without requiring a running tapd instance. + tlsServer := httptest.NewTLSServer(http.NotFoundHandler()) + t.Cleanup(tlsServer.Close) + cert := tlsServer.Certificate() + certBytes := pem.EncodeToMemory(&pem.Block{ + Type: "CERTIFICATE", Bytes: cert.Raw, + }) + require.NoError(t, os.WriteFile( + filepath.Join(defaultTapdDir, "tls.cert"), certBytes, 0o600, + )) + + // Store a valid serialized macaroon at tapd's production path. This + // ensures connection setup tests the path itself rather than failing on + // malformed credentials. + mac, err := macaroon.New( + []byte("root-key"), []byte("id"), "tapd", + macaroon.LatestVersion, + ) + require.NoError(t, err) + macBytes, err := mac.MarshalBinary() + require.NoError(t, err) + require.NoError(t, os.WriteFile(macaroonPath, macBytes, 0o600)) + + // grpc.NewClient connects lazily, so constructing the tapd client verifies + // that both credentials can be loaded and parsed without needing a live + // tapd server. + config := DefaultTapdConfigForNetwork(defaultTapdDir, network) + client, err := NewTapdClient(config) + require.NoError(t, err) + t.Cleanup(func() { + require.NoError(t, client.cc.Close()) + }) + + require.Equal(t, macaroonPath, config.MacaroonPath) + require.Equal( + t, filepath.Join(defaultTapdDir, "tls.cert"), config.TLSPath, + ) +} + +// TestGetAssetNameCachedLookupNotBlocked verifies that a slow universe query +// for one asset does not prevent another caller from reading a cached name. +func TestGetAssetNameCachedLookupNotBlocked(t *testing.T) { + const cachedName = "cached asset" + + cachedAssetID := []byte{1} + queryStarted := make(chan struct{}) + releaseQuery := make(chan struct{}) + client := &TapdClient{ + UniverseClient: &blockingUniverseClient{ + queryStarted: queryStarted, + releaseQuery: releaseQuery, + }, + assetNameCache: map[string]string{ + hex.EncodeToString(cachedAssetID): cachedName, + }, + } + + queryResult := make(chan error, 1) + go func() { + _, err := client.GetAssetName(context.Background(), []byte{2}) + queryResult <- err + }() + + select { + case <-queryStarted: + case <-time.After(time.Second): + t.Fatal("universe query did not start") + } + + type nameResult struct { + name string + err error + } + cachedResult := make(chan nameResult, 1) + go func() { + name, err := client.GetAssetName( + context.Background(), cachedAssetID, + ) + cachedResult <- nameResult{name: name, err: err} + }() + + select { + case result := <-cachedResult: + require.NoError(t, result.err) + require.Equal(t, cachedName, result.name) + case <-time.After(time.Second): + close(releaseQuery) + t.Fatal("cached lookup blocked behind universe query") + } + + close(releaseQuery) + require.NoError(t, <-queryResult) +} + +// TestGetRfqForAssetValidatesRate verifies that malformed accepted quote rates +// are rejected before they reach downstream RFQ arithmetic. +func TestGetRfqForAssetValidatesRate(t *testing.T) { + tests := []struct { + name string + assetRate *rfqrpc.FixedPoint + expectError bool + }{ + { + name: "valid", + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "100000", Scale: 0, + }, + }, + { + name: "nil", + assetRate: nil, + expectError: true, + }, + { + name: "malformed coefficient", + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "not-a-number", Scale: 0, + }, + expectError: true, + }, + { + name: "zero coefficient", + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "0", Scale: 0, + }, + expectError: true, + }, + { + name: "negative coefficient", + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "-1", Scale: 0, + }, + expectError: true, + }, + { + name: "scale overflow", + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "1", Scale: 256, + }, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + acceptedQuote := &rfqrpc.PeerAcceptedSellQuote{ + BidAssetRate: test.assetRate, + } + acceptedResponse := + &rfqrpc.AddAssetSellOrderResponse_AcceptedQuote{ + AcceptedQuote: acceptedQuote, + } + client := &TapdClient{ + RfqClient: &staticRfqClient{ + response: &rfqrpc.AddAssetSellOrderResponse{ + Response: acceptedResponse, + }, + }, + rfqTimeoutSeconds: 60, + } + + quote, err := client.GetRfqForAsset( + context.Background(), 1000, []byte{1}, []byte{2}, + time.Now().Add(time.Minute).Unix(), 1, + ) + if test.expectError { + require.Error(t, err) + require.Nil(t, quote) + return + } + + require.NoError(t, err) + require.Same(t, acceptedQuote, quote) + }) + } +} + func TestGetPaymentMaxAmount(t *testing.T) { tests := []struct { satAmount btcutil.Amount @@ -68,6 +323,62 @@ func TestGetPaymentMaxAmount(t *testing.T) { } } +// TestGetRfqTimeoutSeconds verifies that configured durations are safely +// converted to tapd's whole-second timeout field. +func TestGetRfqTimeoutSeconds(t *testing.T) { + tests := []struct { + name string + timeout time.Duration + expectedSeconds uint32 + expectError bool + }{ + { + name: "whole seconds", + timeout: 60 * time.Second, + expectedSeconds: 60, + }, + { + name: "sub-second rounded up", + timeout: time.Millisecond, + expectedSeconds: 1, + }, + { + name: "fractional second rounded up", + timeout: time.Second + time.Nanosecond, + expectedSeconds: 2, + }, + { + name: "zero", + timeout: 0, + expectError: true, + }, + { + name: "negative", + timeout: -time.Second, + expectError: true, + }, + { + name: "overflow", + timeout: time.Duration(math.MaxUint32)*time.Second + + time.Nanosecond, + expectError: true, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + seconds, err := getRfqTimeoutSeconds(test.timeout) + if test.expectError { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, test.expectedSeconds, seconds) + }) + } +} + func TestGetSatsFromAssetAmt(t *testing.T) { tests := []struct { assetAmt uint64 @@ -93,6 +404,39 @@ func TestGetSatsFromAssetAmt(t *testing.T) { expected: btcutil.Amount(0), expectError: false, }, + { + assetAmt: 1000, + assetRate: nil, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "not-a-number", Scale: 0, + }, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "0", Scale: 0, + }, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "-1", Scale: 0, + }, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "1", Scale: 256, + }, + expectError: true, + }, } for _, test := range tests { diff --git a/cmd/loop/instantout.go b/cmd/loop/instantout.go index 9783f005..3cbcda08 100644 --- a/cmd/loop/instantout.go +++ b/cmd/loop/instantout.go @@ -183,6 +183,8 @@ func instantOut(ctx context.Context, cmd *cli.Command) error { fmt.Println("Starting instant swap out") + maxSwapFee := quote.ServiceFeeSat + // Now we can request the instant out swap. instantOutRes, err := client.InstantOut( ctx, @@ -190,6 +192,9 @@ func instantOut(ctx context.Context, cmd *cli.Command) error { ReservationIds: selectedReservations, OutgoingChanSet: outgoingChanSet, DestAddr: cmd.String("addr"), + MaxSwapFee: &looprpc.InstantOutRequest_MaxSwapFeeSat{ + MaxSwapFeeSat: maxSwapFee, + }, }, ) if err != nil { diff --git a/cmd/loop/loopin.go b/cmd/loop/loopin.go index 0f1401d2..d0512040 100644 --- a/cmd/loop/loopin.go +++ b/cmd/loop/loopin.go @@ -207,7 +207,7 @@ func loopIn(ctx context.Context, cmd *cli.Command) error { } fmt.Printf("Swap initiated\n") - fmt.Printf("ID: %v\n", resp.Id) + fmt.Printf("ID: %x\n", resp.IdBytes) if resp.HtlcAddressP2Tr != "" { fmt.Printf("HTLC address (P2TR): %v\n", resp.HtlcAddressP2Tr) diff --git a/cmd/loop/main.go b/cmd/loop/main.go index 113404fd..127634d2 100644 --- a/cmd/loop/main.go +++ b/cmd/loop/main.go @@ -6,7 +6,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "os" "os/signal" "path/filepath" @@ -561,10 +560,7 @@ func parseAmt(text string) (btcutil.Amount, error) { func logSwap(swap *looprpc.SwapStatus) { // If our swap failed, we add our failure reason to the state. - swapState := fmt.Sprintf("%v", swap.State) - if swap.State == looprpc.SwapState_FAILED { - swapState = fmt.Sprintf("%v (%v)", swapState, swap.FailureReason) - } + swapState := monitorSwapState(swap) if swap.Type == looprpc.SwapType_LOOP_OUT { fmt.Printf("%v %v %v %v - %v", @@ -586,10 +582,32 @@ func logSwap(swap *looprpc.SwapStatus) { } } - if swap.State != looprpc.SwapState_INITIATED && - swap.State != looprpc.SwapState_HTLC_PUBLISHED && - swap.State != looprpc.SwapState_PREIMAGE_REVEALED { + showCost := shouldShowSwapCost(swap.GetState()) + if swap.Type == looprpc.SwapType_STATIC_LOOP_IN { + staticState := swap.GetStaticLoopInState() + // Static loop-ins key cost visibility off the dedicated FSM state, not + // the generic SwapState lifecycle used by traditional swaps. + switch staticState { + case looprpc.StaticAddressLoopInSwapState_INIT_HTLC, + looprpc.StaticAddressLoopInSwapState_SIGN_HTLC_TX, + looprpc.StaticAddressLoopInSwapState_MONITOR_INVOICE_HTLC_TX, + looprpc.StaticAddressLoopInSwapState_SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT, + looprpc.StaticAddressLoopInSwapState_MONITOR_HTLC_TIMEOUT_SWEEP, + looprpc.StaticAddressLoopInSwapState_UNLOCK_DEPOSITS: + showCost = false + + case looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED, + looprpc.StaticAddressLoopInSwapState_HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT, + looprpc.StaticAddressLoopInSwapState_SUCCEEDED, + looprpc.StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED, + looprpc.StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP: + + showCost = true + } + } + + if showCost { fmt.Printf(" (cost: server %v, onchain %v, offchain %v)", swap.CostServer, swap.CostOnchain, swap.CostOffchain, ) @@ -598,6 +616,38 @@ func logSwap(swap *looprpc.SwapStatus) { fmt.Println() } +// monitorSwapState returns the static loop-in FSM label for static swaps and +// the shared swap-state label for all others. +func monitorSwapState(swap *looprpc.SwapStatus) string { + if swap.Type == looprpc.SwapType_STATIC_LOOP_IN { + return swap.GetStaticLoopInState().String() + } + + return genericMonitorSwapState(swap) +} + +// shouldShowSwapCost reports whether a swap's generic state is terminal enough +// to include the persisted cost summary in monitor output. +func shouldShowSwapCost(loopState looprpc.SwapState) bool { + return loopState != looprpc.SwapState_INITIATED && + loopState != looprpc.SwapState_HTLC_PUBLISHED && + loopState != looprpc.SwapState_PREIMAGE_REVEALED +} + +// genericMonitorSwapState formats the shared swap-state label and failure +// reason used by non-static swaps. +func genericMonitorSwapState(swap *looprpc.SwapStatus) string { + loopState := swap.GetState() + swapState := fmt.Sprintf("%v", loopState) + if loopState == looprpc.SwapState_FAILED { + swapState = fmt.Sprintf( + "%v (%v)", swapState, swap.FailureReason, + ) + } + + return swapState +} + // getClientConn dials the loopd gRPC server with TLS and macaroon auth. func getClientConn(address, tlsCertPath, macaroonPath string) (daemonConn, func(), error) { @@ -646,7 +696,7 @@ func getClientConn(address, tlsCertPath, macaroonPath string) (daemonConn, // gRPC dial options from it. func readMacaroon(macPath string) (grpc.DialOption, error) { // Load the specified macaroon file. - macBytes, err := ioutil.ReadFile(macPath) + macBytes, err := os.ReadFile(macPath) if err != nil { return nil, fmt.Errorf("unable to read macaroon path : %v", err) } diff --git a/cmd/loop/monitor_test.go b/cmd/loop/monitor_test.go new file mode 100644 index 00000000..9ed4841e --- /dev/null +++ b/cmd/loop/monitor_test.go @@ -0,0 +1,275 @@ +package main + +import ( + "bytes" + "io" + "os" + "strings" + "testing" + "time" + + "github.com/lightninglabs/loop/looprpc" + "github.com/stretchr/testify/require" +) + +// TestMonitorSwapStateKeepsRegularLoopInFailureReason preserves the generic +// failure suffix for regular loop-in swaps so terminal errors stay visible. +func TestMonitorSwapStateKeepsRegularLoopInFailureReason(t *testing.T) { + swap := &looprpc.SwapStatus{ + Type: looprpc.SwapType_LOOP_IN, + State: looprpc.SwapState_FAILED, + FailureReason: looprpc. + FailureReason_FAILURE_REASON_TIMEOUT, + } + + got := monitorSwapState(swap) + require.Equal(t, "FAILED (FAILURE_REASON_TIMEOUT)", got) +} + +// TestMonitorSwapStateLabelsStaticLoopInStages locks the precise static loop-in +// stage names so monitor output stays stable as the FSM evolves. +func TestMonitorSwapStateLabelsStaticLoopInStages(t *testing.T) { + tests := []struct { + name string + staticState looprpc.StaticAddressLoopInSwapState + want string + }{ + { + name: "init htlc", + staticState: looprpc.StaticAddressLoopInSwapState_INIT_HTLC, + want: "INIT_HTLC", + }, + { + name: "sign htlc", + staticState: looprpc. + StaticAddressLoopInSwapState_SIGN_HTLC_TX, + want: "SIGN_HTLC_TX", + }, + { + name: "monitor invoice and htlc", + staticState: looprpc. + StaticAddressLoopInSwapState_MONITOR_INVOICE_HTLC_TX, + want: "MONITOR_INVOICE_HTLC_TX", + }, + { + name: "unlock deposits", + staticState: looprpc. + StaticAddressLoopInSwapState_UNLOCK_DEPOSITS, + want: "UNLOCK_DEPOSITS", + }, + { + name: "payment received", + staticState: looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED, + want: "PAYMENT_RECEIVED", + }, + { + name: "timeout sweep", + staticState: looprpc. + StaticAddressLoopInSwapState_SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT, + want: "SWEEP_STATIC_ADDRESS_HTLC_TIMEOUT", + }, + { + name: "monitor timeout sweep", + staticState: looprpc. + StaticAddressLoopInSwapState_MONITOR_HTLC_TIMEOUT_SWEEP, + want: "MONITOR_HTLC_TIMEOUT_SWEEP", + }, + { + name: "timeout swept", + staticState: looprpc. + StaticAddressLoopInSwapState_HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT, + want: "HTLC_STATIC_ADDRESS_TIMEOUT_SWEPT", + }, + { + name: "success", + staticState: looprpc.StaticAddressLoopInSwapState_SUCCEEDED, + want: "SUCCEEDED", + }, + { + name: "succeeded transitioning failed", + staticState: looprpc. + StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED, + want: "SUCCEEDED_TRANSITIONING_FAILED", + }, + { + name: "failed", + staticState: looprpc. + StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP, + want: "FAILED_STATIC_ADDRESS_SWAP", + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + swap := &looprpc.SwapStatus{ + Type: looprpc.SwapType_STATIC_LOOP_IN, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: test.staticState, + }, + } + + got := monitorSwapState(swap) + require.Equal(t, test.want, got) + }) + } + + initSwap := &looprpc.SwapStatus{ + Type: looprpc.SwapType_STATIC_LOOP_IN, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_INIT_HTLC, + }, + } + signSwap := &looprpc.SwapStatus{ + Type: looprpc.SwapType_STATIC_LOOP_IN, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_SIGN_HTLC_TX, + }, + } + require.NotEqual(t, monitorSwapState(initSwap), monitorSwapState(signSwap)) +} + +// TestMonitorSwapStateLabelsUnknownStaticLoopIn ensures the absent static +// oneof maps to the UNKNOWN static loop-in label. +func TestMonitorSwapStateLabelsUnknownStaticLoopIn(t *testing.T) { + swap := &looprpc.SwapStatus{ + Type: looprpc.SwapType_STATIC_LOOP_IN, + } + + got := monitorSwapState(swap) + require.Equal(t, "UNKNOWN_STATIC_ADDRESS_SWAP_STATE", got) +} + +// TestLogSwapHidesStaticLoopInCostForInFlightState proves in-flight static FSM +// state suppresses cost even with generic SUCCESS set deliberately. +func TestLogSwapHidesStaticLoopInCostForInFlightState(t *testing.T) { + swap := &looprpc.SwapStatus{ + LastUpdateTime: time.Unix(1, 0).UnixNano(), + Type: looprpc.SwapType_STATIC_LOOP_IN, + State: looprpc.SwapState_SUCCESS, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_INIT_HTLC, + }, + Amt: 50_000, + CostServer: 11, + CostOnchain: 22, + CostOffchain: 33, + HtlcAddressP2Wsh: "bc1qstaticinflighttestaddress", + } + + output := captureStdout(t, func() { + logSwap(swap) + }) + + require.Contains(t, output, "STATIC_LOOP_IN INIT_HTLC 0.00050000 BTC") + require.NotContains(t, output, "(cost:") +} + +// TestLogSwapHidesCostForUnknownStaticLoopInState ensures unknown static +// states print the UNKNOWN label without a cost summary. +func TestLogSwapHidesCostForUnknownStaticLoopInState(t *testing.T) { + swap := &looprpc.SwapStatus{ + LastUpdateTime: time.Unix(3, 0).UnixNano(), + Type: looprpc.SwapType_STATIC_LOOP_IN, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE, + }, + Amt: 50_000, + CostServer: 11, + CostOnchain: 22, + CostOffchain: 33, + HtlcAddressP2Wsh: "bc1qunknownstaticterminaltestaddress", + } + + output := captureStdout(t, func() { + logSwap(swap) + }) + + require.Contains( + t, output, "STATIC_LOOP_IN UNKNOWN_STATIC_ADDRESS_SWAP_STATE 0.00050000 BTC", + ) + require.NotContains(t, output, "(cost:") +} + +// TestLogSwapShowsStaticLoopInCostForTerminalState preserves terminal cost +// output, including the P2WSH address, once the static loop-in is done. +func TestLogSwapShowsStaticLoopInCostForTerminalState(t *testing.T) { + swap := &looprpc.SwapStatus{ + LastUpdateTime: time.Unix(2, 0).UnixNano(), + Type: looprpc.SwapType_STATIC_LOOP_IN, + State: looprpc.SwapState_INITIATED, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_SUCCEEDED, + }, + Amt: 50_000, + CostServer: 11, + CostOnchain: 22, + CostOffchain: 33, + HtlcAddressP2Wsh: "bc1qstaticterminaltestaddress", + } + + output := captureStdout(t, func() { + logSwap(swap) + }) + + require.Contains( + t, output, + "STATIC_LOOP_IN SUCCEEDED 0.00050000 BTC - P2WSH: bc1qstaticterminaltestaddress", + ) + require.Contains( + t, output, + "(cost: server 11, onchain 22, offchain 33)", + ) +} + +// TestLogSwapDisplaysStaticLoopInTypeAndStage ensures the monitor output names +// the static loop-in type and active FSM stage instead of collapsing them. +func TestLogSwapDisplaysStaticLoopInTypeAndStage(t *testing.T) { + swap := &looprpc.SwapStatus{ + LastUpdateTime: time.Unix(1, 0).UnixNano(), + Type: looprpc.SwapType_STATIC_LOOP_IN, + StaticLoopInStateOptional: &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: looprpc. + StaticAddressLoopInSwapState_SIGN_HTLC_TX, + }, + Amt: 50_000, + HtlcAddressP2Wsh: "tb1q5cyxnuxmeuwuvkwfem96llyxf8duyshm56t8k8", + } + + output := captureStdout(t, func() { + logSwap(swap) + }) + + require.Contains( + t, output, "STATIC_LOOP_IN SIGN_HTLC_TX 0.00050000 BTC", + ) + require.Contains( + t, output, "P2WSH: tb1q5cyxnuxmeuwuvkwfem96llyxf8duyshm56t8k8", + ) + require.NotContains(t, output, "STATIC_LOOP_IN INIT_HTLC") +} + +func captureStdout(t *testing.T, fn func()) string { + t.Helper() + + originalStdout := os.Stdout + reader, writer, err := os.Pipe() + require.NoError(t, err) + os.Stdout = writer + + fn() + + require.NoError(t, writer.Close()) + os.Stdout = originalStdout + + var buf bytes.Buffer + _, err = io.Copy(&buf, reader) + require.NoError(t, err) + require.NoError(t, reader.Close()) + + return strings.TrimSpace(buf.String()) +} diff --git a/cmd/loop/openchannel.go b/cmd/loop/openchannel.go index fd31b744..98cc2452 100644 --- a/cmd/loop/openchannel.go +++ b/cmd/loop/openchannel.go @@ -7,19 +7,17 @@ import ( "strconv" "github.com/lightninglabs/loop/looprpc" - lndcommands "github.com/lightningnetwork/lnd/cmd/commands" + "github.com/lightningnetwork/lnd" "github.com/lightningnetwork/lnd/lnrpc" "github.com/urfave/cli/v3" ) const ( - defaultUtxoMinConf = 1 -) - -var ( + defaultUtxoMinConf = 1 channelTypeTweakless = "tweakless" channelTypeAnchors = "anchors" - channelTypeSimpleTaproot = "taproot" + channelTypeSimpleTaproot = "simple-taproot" + channelTypeTaproot = "taproot" ) var openChannelCommand = &cli.Command{ @@ -137,9 +135,9 @@ var openChannelCommand = &cli.Command{ &cli.StringFlag{ Name: "channel_type", Usage: fmt.Sprintf("(optional) the type of channel to "+ - "propose to the remote peer (%q, %q, %q)", + "propose to the remote peer (%q, %q, %q, %q)", channelTypeTweakless, channelTypeAnchors, - channelTypeSimpleTaproot), + channelTypeSimpleTaproot, channelTypeTaproot), }, &cli.BoolFlag{ Name: "zero_conf", @@ -160,11 +158,11 @@ var openChannelCommand = &cli.Command{ }, &cli.StringFlag{ Name: "memo", - Usage: `(optional) a note-to-self containing some useful - information about the channel. This is stored - locally only, and is purely for reference. It - has no bearing on the channel's operation. Max - allowed length is 500 characters`, + Usage: "(optional) a note-to-self containing some useful " + + "information about the channel. This is stored " + + "locally only, and is purely for reference. It " + + "has no bearing on the channel's operation. Max " + + "allowed length is 500 characters", }, &cli.BoolFlag{ Name: "fundmax", @@ -271,7 +269,7 @@ func openChannel(ctx context.Context, cmd *cli.Command) error { if cmd.IsSet("utxo") { utxos := cmd.StringSlice("utxo") - outpoints, err := lndcommands.UtxosToOutpoints(utxos) + outpoints, err := lnd.UtxosToOutpoints(utxos) if err != nil { return fmt.Errorf("unable to decode utxos: %w", err) } @@ -322,6 +320,7 @@ func openChannel(ctx context.Context, cmd *cli.Command) error { switch channelType { case "": break + case channelTypeTweakless: req.CommitmentType = lnrpc.CommitmentType_STATIC_REMOTE_KEY @@ -330,6 +329,10 @@ func openChannel(ctx context.Context, cmd *cli.Command) error { case channelTypeSimpleTaproot: req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT + + case channelTypeTaproot: + req.CommitmentType = lnrpc.CommitmentType_TAPROOT + default: return fmt.Errorf("unsupported channel type %v", channelType) } diff --git a/cmd/loop/session_replay_test.go b/cmd/loop/session_replay_test.go index 34fe55ed..43d548ab 100644 --- a/cmd/loop/session_replay_test.go +++ b/cmd/loop/session_replay_test.go @@ -1193,7 +1193,7 @@ var rfc3339TimestampRegex = regexp.MustCompile( // timeStringTimestampRegex matches time.String-style timestamps embedded in // CLI output. var timeStringTimestampRegex = regexp.MustCompile( - `\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{4} [A-Z]{2,5}`, + `\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2} [+-]\d{4} (?:[A-Z]{2,5}|[+-]\d{2}(?:\d{2})?)`, ) // normalizeTimestamps rewrites embedded timestamps to UTC to avoid @@ -1214,7 +1214,14 @@ func normalizeTimestamps(text string) string { // Normalize time.String timestamps next. timeReplacer := func(ts string) string { - parsed, err := time.Parse("2006-01-02 15:04:05 -0700 MST", ts) + zoneNameIndex := strings.LastIndex(ts, " ") + if zoneNameIndex <= 0 { + return ts + } + + parsed, err := time.Parse( + "2006-01-02 15:04:05 -0700", ts[:zoneNameIndex], + ) if err != nil { return ts } @@ -1229,6 +1236,41 @@ func normalizeTimestamps(text string) string { return text } +// TestNormalizeTimestamps verifies that timestamp normalization handles the +// timestamp formats emitted by CLI commands in different local time zones. +func TestNormalizeTimestamps(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + text string + want string + }{ + { + name: "rfc3339", + text: "updated: 2026-01-26T01:28:06-05:00\n", + want: "updated: 2026-01-26T06:28:06Z\n", + }, + { + name: "alphabetic time string zone", + text: "deadline: 2026-01-26 01:28:06 -0500 EST\n", + want: "deadline: 2026-01-26 06:28:06 +0000 UTC\n", + }, + { + name: "numeric time string zone", + text: "deadline: 2026-01-26 03:28:06 -0300 -03\n", + want: "deadline: 2026-01-26 06:28:06 +0000 UTC\n", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + got := normalizeTimestamps(testCase.text) + require.Equal(t, testCase.want, got) + }) + } +} + // TestCloneCommandForReplayResetsFlagState verifies cloned commands reset flag // state. func TestCloneCommandForReplayResetsFlagState(t *testing.T) { diff --git a/cmd/loop/staticaddr.go b/cmd/loop/staticaddr.go index 372368a0..c973a002 100644 --- a/cmd/loop/staticaddr.go +++ b/cmd/loop/staticaddr.go @@ -4,14 +4,17 @@ import ( "context" "errors" "fmt" + "sort" + "strings" "github.com/lightninglabs/loop/labels" "github.com/lightninglabs/loop/looprpc" - "github.com/lightninglabs/loop/staticaddr/deposit" "github.com/lightninglabs/loop/staticaddr/loopin" "github.com/lightninglabs/loop/swapserverrpc" - lndcommands "github.com/lightningnetwork/lnd/cmd/commands" + "github.com/lightningnetwork/lnd" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnrpc" + "github.com/lightningnetwork/lnd/lnwallet" "github.com/lightningnetwork/lnd/routing/route" "github.com/urfave/cli/v3" ) @@ -192,7 +195,7 @@ func withdraw(ctx context.Context, cmd *cli.Command) error { case isAllSelected: case isUtxoSelected: utxos := cmd.StringSlice("utxo") - outpoints, err = lndcommands.UtxosToOutpoints(utxos) + outpoints, err = lnd.UtxosToOutpoints(utxos) if err != nil { return err } @@ -553,11 +556,7 @@ func staticAddressLoopIn(ctx context.Context, cmd *cli.Command) error { allDeposits := depositList.FilteredDeposits if len(allDeposits) == 0 { - errString := fmt.Sprintf("no confirmed deposits available, "+ - "deposits need at least %v confirmations", - deposit.MinConfs) - - return errors.New(errString) + return errors.New("no deposited outputs available") } var depositOutpoints []string @@ -614,6 +613,28 @@ func staticAddressLoopIn(ctx context.Context, cmd *cli.Command) error { return err } + // Warn the user if any selected deposits have fewer than 6 + // confirmations, as the swap payment won't be received immediately + // for those. + summary, err := client.GetStaticAddressSummary( + ctx, &looprpc.StaticAddressSummaryRequest{}, + ) + if err != nil { + return err + } + + depositsToCheck := warningDepositOutpoints( + allDeposits, depositOutpoints, autoSelectDepositsForQuote, + quoteReq.Amt, + ) + warning := lowConfDepositWarning( + allDeposits, depositsToCheck, + int64(summary.RelativeExpiryBlocks), + ) + if warning != "" { + fmt.Println(warning) + } + if !(cmd.Bool("force") || cmd.Bool("f")) { err = displayInDetails(quoteReq, quote, cmd.Bool("verbose")) if err != nil { @@ -669,6 +690,162 @@ func depositsToOutpoints(deposits []*looprpc.Deposit) []string { return outpoints } +var warningSelectionDustLimit = int64(lnwallet.DustLimitForSize(input.P2TRSize)) + +// warningDepositOutpoints returns the deposit outpoints to check for +// low-confirmation warnings. +func warningDepositOutpoints(allDeposits []*looprpc.Deposit, + selectedOutpoints []string, autoSelect bool, targetAmount int64) []string { + + if !autoSelect { + return selectedOutpoints + } + + return autoSelectedWarningOutpoints(allDeposits, targetAmount) +} + +// autoSelectedWarningOutpoints returns the outpoints selected by the same +// ordering used for automatic static loop-in deposit selection. +func autoSelectedWarningOutpoints(allDeposits []*looprpc.Deposit, + targetAmount int64) []string { + + if targetAmount <= 0 { + return nil + } + + // KEEP IN SYNC with staticaddr/loopin.SelectDeposits. + deposits := filterSwappableWarningDeposits(allDeposits) + sort.Slice(deposits, func(i, j int) bool { + iConfirmed := deposits[i].ConfirmationHeight > 0 + jConfirmed := deposits[j].ConfirmationHeight > 0 + if iConfirmed != jConfirmed { + return iConfirmed + } + + if deposits[i].Value == deposits[j].Value { + return deposits[i].BlocksUntilExpiry < + deposits[j].BlocksUntilExpiry + } + + return deposits[i].Value > deposits[j].Value + }) + + selectedOutpoints := make([]string, 0, len(deposits)) + var selectedAmount int64 + for _, deposit := range deposits { + selectedOutpoints = append(selectedOutpoints, deposit.Outpoint) + selectedAmount += deposit.Value + if selectedAmount == targetAmount { + return selectedOutpoints + } + + if selectedAmount > targetAmount && + selectedAmount-targetAmount >= warningSelectionDustLimit { + + return selectedOutpoints + } + } + + return nil +} + +// filterSwappableWarningDeposits filters deposits for CLI warning selection. +func filterSwappableWarningDeposits( + allDeposits []*looprpc.Deposit) []*looprpc.Deposit { + + swappable := make([]*looprpc.Deposit, 0, len(allDeposits)) + minBlocksUntilExpiry := int64( + loopin.DefaultLoopInOnChainCltvDelta + loopin.DepositHtlcDelta, + ) + for _, deposit := range allDeposits { + // Unconfirmed deposits remain swappable because their CSV timeout has + // not started yet. This mirrors loopin.IsSwappable. + if deposit.ConfirmationHeight > 0 && + deposit.BlocksUntilExpiry < minBlocksUntilExpiry { + + continue + } + + swappable = append(swappable, deposit) + } + + return swappable +} + +// conservativeWarningConfs is the highest default confirmation tier used by +// the server's dynamic confirmation-risk policy. +// +// The CLI does not currently know the server's exact policy, so we use this +// conservative threshold for warnings without promising immediate execution. +const conservativeWarningConfs = 6 + +// lowConfDepositWarning checks the selected deposits against a conservative +// confirmation threshold and returns a warning string if any are found. +func lowConfDepositWarning(allDeposits []*looprpc.Deposit, + selectedOutpoints []string, csvExpiry int64) string { + + depositMap := make(map[string]*looprpc.Deposit, len(allDeposits)) + for _, d := range allDeposits { + depositMap[d.Outpoint] = d + } + + var lowConfEntries []string + for _, op := range selectedOutpoints { + d, ok := depositMap[op] + if !ok { + continue + } + + var confs int64 + switch { + case d.ConfirmationHeight <= 0: + confs = 0 + + case csvExpiry > 0: + // For confirmed deposits we can compute + // confirmations as CSVExpiry - BlocksUntilExpiry + 1. + confs = csvExpiry - d.BlocksUntilExpiry + 1 + + default: + // Can't determine confirmations without the CSV expiry. + continue + } + + if confs >= conservativeWarningConfs { + continue + } + + if confs == 0 { + lowConfEntries = append( + lowConfEntries, + fmt.Sprintf(" - %s (unconfirmed)", op), + ) + } else { + lowConfEntries = append( + lowConfEntries, + fmt.Sprintf( + " - %s (%d confirmations)", op, + confs, + ), + ) + } + } + + if len(lowConfEntries) == 0 { + return "" + } + + return fmt.Sprintf( + "\nWARNING: The following deposits are below the "+ + "conservative %d-confirmation threshold:\n%s\n"+ + "The swap payment for these deposits may wait for "+ + "more confirmations depending on the server's "+ + "confirmation-risk policy.\n", + conservativeWarningConfs, + strings.Join(lowConfEntries, "\n"), + ) +} + func displayNewAddressWarning() error { fmt.Printf("\nWARNING: Be aware that loosing your l402.token file in " + ".loop under your home directory will take your ability to " + diff --git a/cmd/loop/staticaddr_test.go b/cmd/loop/staticaddr_test.go new file mode 100644 index 00000000..2cc88ad6 --- /dev/null +++ b/cmd/loop/staticaddr_test.go @@ -0,0 +1,220 @@ +package main + +import ( + "strings" + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/staticaddr/loopin" + "github.com/stretchr/testify/require" +) + +// TestLowConfDepositWarningConfirmedOnly verifies confirmed deposits below the +// conservative warning threshold are included in the warning text. +func TestLowConfDepositWarningConfirmedOnly(t *testing.T) { + t.Parallel() + + deposits := []*looprpc.Deposit{ + { + Outpoint: "confirmed-low", + ConfirmationHeight: 100, + BlocksUntilExpiry: 140, + }, + { + Outpoint: "confirmed-high", + ConfirmationHeight: 95, + BlocksUntilExpiry: 139, + }, + } + + warning := lowConfDepositWarning( + deposits, []string{"confirmed-low", "confirmed-high"}, 144, + ) + + require.Contains(t, warning, "confirmed-low (5 confirmations)") + require.NotContains(t, warning, "confirmed-high") +} + +// TestLowConfDepositWarningUnconfirmed verifies unconfirmed deposits get a +// warning that the swap may wait for confirmation-risk acceptance. +func TestLowConfDepositWarningUnconfirmed(t *testing.T) { + t.Parallel() + + deposits := []*looprpc.Deposit{ + { + Outpoint: "mempool", + ConfirmationHeight: 0, + BlocksUntilExpiry: 144, + }, + } + + warning := lowConfDepositWarning(deposits, []string{"mempool"}, 144) + + require.Contains(t, warning, "mempool (unconfirmed)") + require.True( + t, + strings.Contains( + warning, + "conservative 6-confirmation threshold", + ), + ) + require.NotContains(t, warning, "executed immediately") +} + +// TestWarningDepositOutpointsAutoSelectPrefersConfirmed verifies automatic +// warning selection keeps the loop-in preference for confirmed outputs. +func TestWarningDepositOutpointsAutoSelectPrefersConfirmed(t *testing.T) { + t.Parallel() + + const csvExpiry = 1100 + + deposits := []*looprpc.Deposit{ + { + Outpoint: "mempool-large", + Value: 2_000_000, + ConfirmationHeight: 0, + BlocksUntilExpiry: csvExpiry, + }, + { + Outpoint: "confirmed", + Value: 1_500_000, + ConfirmationHeight: 100, + BlocksUntilExpiry: csvExpiry - 5, + }, + } + + selected := warningDepositOutpoints(deposits, nil, true, 1_000_000) + + require.Equal(t, []string{"confirmed"}, selected) + require.Empty(t, lowConfDepositWarning(deposits, selected, csvExpiry)) +} + +// TestWarningDepositOutpointsAutoSelectIncludesNeededUnconfirmed verifies the +// warning path includes mempool deposits when they are needed for the target. +func TestWarningDepositOutpointsAutoSelectIncludesNeededUnconfirmed(t *testing.T) { + t.Parallel() + + const csvExpiry = 1100 + + deposits := []*looprpc.Deposit{ + { + Outpoint: "confirmed-small", + Value: 500_000, + ConfirmationHeight: 100, + BlocksUntilExpiry: csvExpiry - 5, + }, + { + Outpoint: "mempool-large", + Value: 2_000_000, + ConfirmationHeight: 0, + BlocksUntilExpiry: csvExpiry, + }, + } + + selected := warningDepositOutpoints(deposits, nil, true, 1_000_000) + + require.Equal( + t, []string{"confirmed-small", "mempool-large"}, selected, + ) + + warning := lowConfDepositWarning(deposits, selected, csvExpiry) + require.Contains(t, warning, "mempool-large (unconfirmed)") + require.NotContains(t, warning, "confirmed-small") +} + +// TestWarningDepositSelectionMatchesLoopInSelection verifies CLI warning +// selection matches the loop-in selector. +func TestWarningDepositSelectionMatchesLoopInSelection(t *testing.T) { + t.Parallel() + + const ( + blockHeight = uint32(10_000) + csvExpiry = uint32(1_200) + targetAmount = int64(2_500_000) + ) + + type fixture struct { + name string + value int64 + confirmationHeight int64 + } + + fixtures := []fixture{ + { + name: "mempool-huge", + value: 3_000_000, + confirmationHeight: 0, + }, + { + name: "confirmed-later-expiry", + value: 2_000_000, + confirmationHeight: 9_900, + }, + { + name: "confirmed-earlier-expiry", + value: 2_000_000, + confirmationHeight: 9_890, + }, + { + name: "confirmed-small", + value: 600_000, + confirmationHeight: 9_900, + }, + { + name: "confirmed-too-close-to-expiry", + value: 5_000_000, + confirmationHeight: 9_849, + }, + } + + rpcDeposits := make([]*looprpc.Deposit, 0, len(fixtures)) + loopInDeposits := make([]*deposit.Deposit, 0, len(fixtures)) + for idx, fixture := range fixtures { + hash := chainhash.Hash{byte(idx + 1)} + outpoint := wire.OutPoint{ + Hash: hash, + Index: uint32(idx), + } + + blocksUntilExpiry := int64(0) + if fixture.confirmationHeight > 0 { + blocksUntilExpiry = fixture.confirmationHeight + + int64(csvExpiry) - int64(blockHeight) + } + + rpcDeposits = append(rpcDeposits, &looprpc.Deposit{ + Outpoint: outpoint.String(), + Value: fixture.value, + ConfirmationHeight: fixture.confirmationHeight, + BlocksUntilExpiry: blocksUntilExpiry, + }) + loopInDeposits = append(loopInDeposits, &deposit.Deposit{ + OutPoint: outpoint, + Value: btcutil.Amount(fixture.value), + ConfirmationHeight: fixture.confirmationHeight, + }) + } + + cliSelected := autoSelectedWarningOutpoints( + rpcDeposits, targetAmount, + ) + + loopInSelected, err := loopin.SelectDeposits( + btcutil.Amount(targetAmount), loopInDeposits, csvExpiry, + blockHeight, + ) + require.NoError(t, err) + + loopInSelectedOutpoints := make([]string, 0, len(loopInSelected)) + for _, selected := range loopInSelected { + loopInSelectedOutpoints = append( + loopInSelectedOutpoints, selected.OutPoint.String(), + ) + } + + require.Equal(t, loopInSelectedOutpoints, cliSelected) +} diff --git a/cmd/loop/swaps.go b/cmd/loop/swaps.go index 9323f6ce..419522b6 100644 --- a/cmd/loop/swaps.go +++ b/cmd/loop/swaps.go @@ -15,9 +15,10 @@ import ( var listSwapsCommand = &cli.Command{ Name: "listswaps", - Usage: "list all swaps in the local database", - Description: "Allows the user to get a list of all swaps that are " + - "currently stored in the database", + Usage: "list traditional Loop In and Loop Out swaps", + Description: "Lists traditional Loop In and Loop Out swaps that are " + + "currently stored in the database. Static address loop-ins are not " + + "included; use `loop static listswaps` to view them.", Action: listSwaps, Flags: []cli.Flag{ &cli.BoolFlag{ @@ -128,10 +129,12 @@ func listSwaps(ctx context.Context, cmd *cli.Command) error { var swapInfoCommand = &cli.Command{ Name: "swapinfo", - Usage: "show the status of a swap", + Usage: "show the status of a traditional swap", ArgsUsage: "id", - Description: "Allows the user to get the status of a single swap " + - "currently stored in the database", + Description: "Shows the status of a traditional Loop In or Loop Out " + + "swap currently stored in the database. Static address loop-ins " + + "must be viewed with `loop static listswaps`; there is no generic " + + "per-swap static lookup command.", Flags: []cli.Flag{ &cli.Uint64Flag{ Name: "id", diff --git a/cmd/loop/testdata/sessions/AGENTS.md b/cmd/loop/testdata/sessions/AGENTS.md index d4b27293..b5a69573 100644 --- a/cmd/loop/testdata/sessions/AGENTS.md +++ b/cmd/loop/testdata/sessions/AGENTS.md @@ -71,7 +71,7 @@ Base URL: `http://127.0.0.1:12345` | `quote/` | `loop quote out` (success + verbose), `loop quote in` (help + verbose), `loop quote out` (help), `loop quote in` (deposit_outpoint success), `loop quote in` (positional + last_hop) | | `static/` | `loop static withdraw` (no selection error), `loop static withdraw` (invalid utxo), `loop static withdraw` (all success), `loop static withdraw` (utxo + dest_addr success), `loop static listwithdrawals`, `loop static listswaps` | | `static-autoloop/` | `loop setparams --loopinsource static-address` (success + no-experimental error), `loop getparams` (static-address loop-in source), `loop suggestswaps` (static loop-in suggestion) | -| `static-loop-in/` | `loop static new`, `loop static` (help), `loop static listunspent` (incl alias), `loop static listdeposits`, `loop static summary`, `loop static in` (multiple args/flags cases), `loop static in` (duplicate outpoints), `loop static in` (positional low amount error), `loop static in` (positional + last_hop + payment_timeout), `loop static in` (all cancel) | +| `static-loop-in/` | `loop static new`, `loop static` (help), `loop static listunspent` (incl alias), `loop static listdeposits`, `loop static summary`, `loop static in` (multiple args/flags cases), `loop static in` (duplicate outpoints), `loop static in` (positional low amount error), `loop static in` (positional + last_hop + payment_timeout), `loop static in` (all cancel), `loop static in` (explicit and automatically selected low-confirmation warnings) | | `static-filters/` | `loop static listdeposits --filter ...` for each state (deposited/withdrawing/withdrawn/looping_in/looped_in/publish_expired_deposit/sweep_htlc_timeout/htlc_timeout_swept/wait_for_expiry_sweep/expired/failed) | | `swaps/` | `loop listswaps` (success + conflicting filters + loop_out_only filters + loop_in_only), `loop swapinfo` (success + invalid id + id flag errors), `loop abandonswap` (help + invalid id + success) | diff --git a/cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json b/cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json new file mode 100644 index 00000000..451d34f3 --- /dev/null +++ b/cmd/loop/testdata/sessions/basic-swaps/04_loop-monitor-static-loop-in.json @@ -0,0 +1,267 @@ +{ + "metadata": { + "args": [ + "loop", + "monitor", + "--network", + "regtest" + ], + "env": {}, + "version": "0.33.3-beta commit= commit_hash=", + "run_error": "recv: rpc error: code = Canceled desc = context canceled", + "duration": 25407490958, + "clock_start_unix": 1784150305 + }, + "events": [ + { + "time_ms": 13, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "send", + "message_type": "looprpc.MonitorRequest", + "payload": {} + } + }, + { + "time_ms": 13, + "kind": "stdout", + "data": { + "lines": [ + "Note: offchain cost may report as 0 after loopd restart during swap\n" + ] + } + }, + { + "time_ms": 16479, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "recv", + "message_type": "looprpc.SwapStatus", + "payload": { + "amt": "500000", + "id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826", + "id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=", + "type": "STATIC_LOOP_IN", + "state": "INITIATED", + "static_loop_in_state": "INIT_HTLC", + "failure_reason": "FAILURE_REASON_NONE", + "initiation_time": "1784150320647961000", + "last_update_time": "1784150321494895000", + "htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2tr": "", + "cost_server": "0", + "cost_onchain": "0", + "cost_offchain": "0", + "last_hop": "", + "outgoing_chan_set": [], + "label": "", + "asset_info": null + } + } + }, + { + "time_ms": 16479, + "kind": "stdout", + "data": { + "lines": [ + "2026-07-15T18:18:41-03:00 STATIC_LOOP_IN INIT_HTLC 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv\n" + ] + } + }, + { + "time_ms": 16482, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "recv", + "message_type": "looprpc.SwapStatus", + "payload": { + "amt": "500000", + "id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826", + "id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=", + "type": "STATIC_LOOP_IN", + "state": "INITIATED", + "static_loop_in_state": "SIGN_HTLC_TX", + "failure_reason": "FAILURE_REASON_NONE", + "initiation_time": "1784150320647961000", + "last_update_time": "1784150321499639000", + "htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2tr": "", + "cost_server": "0", + "cost_onchain": "0", + "cost_offchain": "0", + "last_hop": "", + "outgoing_chan_set": [], + "label": "", + "asset_info": null + } + } + }, + { + "time_ms": 16482, + "kind": "stdout", + "data": { + "lines": [ + "2026-07-15T18:18:41-03:00 STATIC_LOOP_IN SIGN_HTLC_TX 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv\n" + ] + } + }, + { + "time_ms": 16530, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "recv", + "message_type": "looprpc.SwapStatus", + "payload": { + "amt": "500000", + "id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826", + "id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=", + "type": "STATIC_LOOP_IN", + "state": "INITIATED", + "static_loop_in_state": "MONITOR_INVOICE_HTLC_TX", + "failure_reason": "FAILURE_REASON_NONE", + "initiation_time": "1784150320647961000", + "last_update_time": "1784150321548450000", + "htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2tr": "", + "cost_server": "0", + "cost_onchain": "0", + "cost_offchain": "0", + "last_hop": "", + "outgoing_chan_set": [], + "label": "", + "asset_info": null + } + } + }, + { + "time_ms": 16530, + "kind": "stdout", + "data": { + "lines": [ + "2026-07-15T18:18:41-03:00 STATIC_LOOP_IN MONITOR_INVOICE_HTLC_TX 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv\n" + ] + } + }, + { + "time_ms": 17027, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "recv", + "message_type": "looprpc.SwapStatus", + "payload": { + "amt": "500000", + "id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826", + "id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=", + "type": "STATIC_LOOP_IN", + "state": "INITIATED", + "static_loop_in_state": "PAYMENT_RECEIVED", + "failure_reason": "FAILURE_REASON_NONE", + "initiation_time": "1784150320647961000", + "last_update_time": "1784150322044521000", + "htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2tr": "", + "cost_server": "1828", + "cost_onchain": "0", + "cost_offchain": "0", + "last_hop": "", + "outgoing_chan_set": [], + "label": "", + "asset_info": null + } + } + }, + { + "time_ms": 17027, + "kind": "stdout", + "data": { + "lines": [ + "2026-07-15T18:18:42-03:00 STATIC_LOOP_IN PAYMENT_RECEIVED 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv (cost: server 1828, onchain 0, offchain 0)\n" + ] + } + }, + { + "time_ms": 17036, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "recv", + "message_type": "looprpc.SwapStatus", + "payload": { + "amt": "500000", + "id": "bec3777d45337f652d96136e5714ed90a5d4f7cda87b5842110ecc38c9a65826", + "id_bytes": "vsN3fUUzf2UtlhNuVxTtkKXU982oe1hCEQ7MOMmmWCY=", + "type": "STATIC_LOOP_IN", + "state": "INITIATED", + "static_loop_in_state": "SUCCEEDED", + "failure_reason": "FAILURE_REASON_NONE", + "initiation_time": "1784150320647961000", + "last_update_time": "1784150322052572000", + "htlc_address": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2wsh": "bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv", + "htlc_address_p2tr": "", + "cost_server": "1828", + "cost_onchain": "0", + "cost_offchain": "0", + "last_hop": "", + "outgoing_chan_set": [], + "label": "", + "asset_info": null + } + } + }, + { + "time_ms": 17036, + "kind": "stdout", + "data": { + "lines": [ + "2026-07-15T18:18:42-03:00 STATIC_LOOP_IN SUCCEEDED 0.00500000 BTC - P2WSH: bcrt1qjsl6tmv2vf3p5wwv28jh6xwettds934k02c3l5w6zyjsyrsd3lfskgdjmv (cost: server 1828, onchain 0, offchain 0)\n" + ] + } + }, + { + "time_ms": 25406, + "kind": "signal", + "data": { + "signal": "interrupt" + } + }, + { + "time_ms": 25407, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/Monitor", + "event": "error", + "error": "rpc error: code = Canceled desc = context canceled", + "status": { + "code": 1, + "message": "context canceled" + } + } + }, + { + "time_ms": 25407, + "kind": "stderr", + "data": { + "lines": [ + "[loop] recv: rpc error: code = Canceled desc = context canceled\n" + ] + } + }, + { + "time_ms": 25407, + "kind": "exit", + "data": { + "run_error": "recv: rpc error: code = Canceled desc = context canceled" + } + } + ] +} diff --git a/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json b/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json index 6dae20b1..257f27c7 100644 --- a/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json +++ b/cmd/loop/testdata/sessions/instantout/02_loop-instantout.json @@ -139,7 +139,8 @@ "Mu65fbhayEtRzougKLBnoeRN8f+tEM1+O9QuNvUIfbI=" ], "outgoing_chan_set": [], - "dest_addr": "" + "dest_addr": "", + "max_swap_fee_sat": "4800" } } }, diff --git a/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json b/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json index 205ae43e..07038f8e 100644 --- a/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json +++ b/cmd/loop/testdata/sessions/instantout/07_loop-instantout-channel.json @@ -148,7 +148,8 @@ "outgoing_chan_set": [ "125344325763072" ], - "dest_addr": "" + "dest_addr": "", + "max_swap_fee_sat": "3200" } } }, diff --git a/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json b/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json index 9a6d103b..89f13c31 100644 --- a/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json +++ b/cmd/loop/testdata/sessions/instantout/08_loop-instantout-select-index.json @@ -162,7 +162,8 @@ "cSfKVONNmsK9+p4Uc5nc3ZtE+37uOODHeq1vprhh/x4=" ], "outgoing_chan_set": [], - "dest_addr": "" + "dest_addr": "", + "max_swap_fee_sat": "1600" } } }, diff --git a/cmd/loop/testdata/sessions/static-loop-in/15_loop-static-in.json b/cmd/loop/testdata/sessions/static-loop-in/15_loop-static-in.json index ea600c31..5acb8ddf 100644 --- a/cmd/loop/testdata/sessions/static-loop-in/15_loop-static-in.json +++ b/cmd/loop/testdata/sessions/static-loop-in/15_loop-static-in.json @@ -91,6 +91,37 @@ } } }, + { + "time_ms": 65, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "request", + "message_type": "looprpc.StaticAddressSummaryRequest", + "payload": {} + } + }, + { + "time_ms": 65, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "response", + "message_type": "looprpc.StaticAddressSummaryResponse", + "payload": { + "static_address": "bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw", + "relative_expiry_blocks": "14400", + "total_num_deposits": 1, + "value_unconfirmed_satoshis": "0", + "value_deposited_satoshis": "2500000", + "value_expired_satoshis": "0", + "value_withdrawn_satoshis": "0", + "value_looped_in_satoshis": "0", + "value_htlc_timeout_sweeps_satoshis": "0", + "value_channels_opened": "0" + } + } + }, { "time_ms": 65, "kind": "stdout", diff --git a/cmd/loop/testdata/sessions/static-loop-in/18_loop-static-in-positional-payment-timeout.json b/cmd/loop/testdata/sessions/static-loop-in/18_loop-static-in-positional-payment-timeout.json index 5a6b7c1c..b997322b 100644 --- a/cmd/loop/testdata/sessions/static-loop-in/18_loop-static-in-positional-payment-timeout.json +++ b/cmd/loop/testdata/sessions/static-loop-in/18_loop-static-in-positional-payment-timeout.json @@ -94,6 +94,37 @@ } } }, + { + "time_ms": 500, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "request", + "message_type": "looprpc.StaticAddressSummaryRequest", + "payload": {} + } + }, + { + "time_ms": 500, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "response", + "message_type": "looprpc.StaticAddressSummaryResponse", + "payload": { + "static_address": "bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw", + "relative_expiry_blocks": "14400", + "total_num_deposits": 1, + "value_unconfirmed_satoshis": "0", + "value_deposited_satoshis": "500000", + "value_expired_satoshis": "0", + "value_withdrawn_satoshis": "0", + "value_looped_in_satoshis": "0", + "value_htlc_timeout_sweeps_satoshis": "0", + "value_channels_opened": "0" + } + } + }, { "time_ms": 500, "kind": "grpc", diff --git a/cmd/loop/testdata/sessions/static-loop-in/19_loop-static-in-all-cancel.json b/cmd/loop/testdata/sessions/static-loop-in/19_loop-static-in-all-cancel.json index 073bcb3b..d562f2ef 100644 --- a/cmd/loop/testdata/sessions/static-loop-in/19_loop-static-in-all-cancel.json +++ b/cmd/loop/testdata/sessions/static-loop-in/19_loop-static-in-all-cancel.json @@ -99,6 +99,37 @@ } } }, + { + "time_ms": 446, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "request", + "message_type": "looprpc.StaticAddressSummaryRequest", + "payload": {} + } + }, + { + "time_ms": 446, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "response", + "message_type": "looprpc.StaticAddressSummaryResponse", + "payload": { + "static_address": "bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw", + "relative_expiry_blocks": "14400", + "total_num_deposits": 1, + "value_unconfirmed_satoshis": "0", + "value_deposited_satoshis": "500000", + "value_expired_satoshis": "0", + "value_withdrawn_satoshis": "0", + "value_looped_in_satoshis": "0", + "value_htlc_timeout_sweeps_satoshis": "0", + "value_channels_opened": "0" + } + } + }, { "time_ms": 446, "kind": "stdout", diff --git a/cmd/loop/testdata/sessions/static-loop-in/23_loop-static-in-max-swap-fee-both.json b/cmd/loop/testdata/sessions/static-loop-in/23_loop-static-in-max-swap-fee-both.json index 329b8e5b..02b5a3ac 100644 --- a/cmd/loop/testdata/sessions/static-loop-in/23_loop-static-in-max-swap-fee-both.json +++ b/cmd/loop/testdata/sessions/static-loop-in/23_loop-static-in-max-swap-fee-both.json @@ -127,6 +127,37 @@ } } }, + { + "time_ms": 50, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "request", + "message_type": "looprpc.StaticAddressSummaryRequest", + "payload": {} + } + }, + { + "time_ms": 50, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "response", + "message_type": "looprpc.StaticAddressSummaryResponse", + "payload": { + "static_address": "bcrt1p604kzzh28764kkw45yps48weergwljggamhhe7tqfglzjzang6cs43f2m2", + "relative_expiry_blocks": "14400", + "total_num_deposits": 4, + "value_unconfirmed_satoshis": "0", + "value_deposited_satoshis": "2546150", + "value_expired_satoshis": "0", + "value_withdrawn_satoshis": "0", + "value_looped_in_satoshis": "0", + "value_htlc_timeout_sweeps_satoshis": "0", + "value_channels_opened": "0" + } + } + }, { "time_ms": 50, "kind": "grpc", diff --git a/cmd/loop/testdata/sessions/static-loop-in/24_loop-static-in-max-swap-fee-sat-success.json b/cmd/loop/testdata/sessions/static-loop-in/24_loop-static-in-max-swap-fee-sat-success.json index e108e966..f2994fde 100644 --- a/cmd/loop/testdata/sessions/static-loop-in/24_loop-static-in-max-swap-fee-sat-success.json +++ b/cmd/loop/testdata/sessions/static-loop-in/24_loop-static-in-max-swap-fee-sat-success.json @@ -118,6 +118,37 @@ } } }, + { + "time_ms": 45, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "request", + "message_type": "looprpc.StaticAddressSummaryRequest", + "payload": {} + } + }, + { + "time_ms": 45, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "response", + "message_type": "looprpc.StaticAddressSummaryResponse", + "payload": { + "static_address": "bcrt1p604kzzh28764kkw45yps48weergwljggamhhe7tqfglzjzang6cs43f2m2", + "relative_expiry_blocks": "14400", + "total_num_deposits": 3, + "value_unconfirmed_satoshis": "0", + "value_deposited_satoshis": "2046150", + "value_expired_satoshis": "0", + "value_withdrawn_satoshis": "0", + "value_looped_in_satoshis": "0", + "value_htlc_timeout_sweeps_satoshis": "0", + "value_channels_opened": "0" + } + } + }, { "time_ms": 45, "kind": "grpc", diff --git a/cmd/loop/testdata/sessions/static-loop-in/25_loop-static-in-low-conf-utxo.json b/cmd/loop/testdata/sessions/static-loop-in/25_loop-static-in-low-conf-utxo.json new file mode 100644 index 00000000..3093fb9d --- /dev/null +++ b/cmd/loop/testdata/sessions/static-loop-in/25_loop-static-in-low-conf-utxo.json @@ -0,0 +1,242 @@ +{ + "metadata": { + "args": [ + "loop", + "static", + "in", + "--amt", + "500000", + "--utxo", + "188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0", + "--network", + "regtest" + ], + "env": {}, + "version": "0.31.7-beta commit=vbump-lndclient-70-g352a68cd43f1976a937faaf76041bc078fdd16f6 commit_hash=352a68cd43f1976a937faaf76041bc078fdd16f6", + "duration": 2826015164, + "clock_start_unix": 1769407086 + }, + "events": [ + { + "time_ms": 3, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/ListStaticAddressDeposits", + "event": "request", + "message_type": "looprpc.ListStaticAddressDepositsRequest", + "payload": { + "state_filter": "DEPOSITED", + "outpoints": [] + } + } + }, + { + "time_ms": 22, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/ListStaticAddressDeposits", + "event": "response", + "message_type": "looprpc.ListStaticAddressDepositsResponse", + "payload": { + "filtered_deposits": [ + { + "id": "6mq78FccC6ghF66fIIZhTqzqiykT3AVEtwwA3ng1PnE=", + "state": "DEPOSITED", + "outpoint": "188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0", + "value": "2500000", + "confirmation_height": "131", + "blocks_until_expiry": "14396", + "swap_hash": "" + } + ] + } + } + }, + { + "time_ms": 22, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "500000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "", + "loop_in_route_hints": [], + "private": false, + "deposit_outpoints": [ + "188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0" + ], + "asset_info": null, + "auto_select_deposits": false, + "fast": false + } + } + }, + { + "time_ms": 65, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "response", + "message_type": "looprpc.InQuoteResponse", + "payload": { + "swap_fee_sat": "1824", + "htlc_publish_fee_sat": "0", + "cltv_delta": 0, + "conf_target": 0, + "quoted_amt": "500000" + } + } + }, + { + "time_ms": 65, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "request", + "message_type": "looprpc.StaticAddressSummaryRequest", + "payload": {} + } + }, + { + "time_ms": 65, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "response", + "message_type": "looprpc.StaticAddressSummaryResponse", + "payload": { + "static_address": "bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw", + "relative_expiry_blocks": "14400", + "total_num_deposits": 1, + "value_unconfirmed_satoshis": "0", + "value_deposited_satoshis": "2500000", + "value_expired_satoshis": "0", + "value_withdrawn_satoshis": "0", + "value_looped_in_satoshis": "0", + "value_htlc_timeout_sweeps_satoshis": "0", + "value_channels_opened": "0" + } + } + }, + { + "time_ms": 65, + "kind": "stdout", + "data": { + "lines": [ + "\n", + "WARNING: The following deposits are below the conservative 6-confirmation threshold:\n", + " - 188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0 (5 confirmations)\n", + "The swap payment for these deposits may wait for more confirmations depending on the server's confirmation-risk policy.\n", + "\n", + "Previously deposited on-chain: 500000 sat\n", + "Receive off-chain: 498176 sat\n", + "Estimated total fee: 1824 sat\n", + "\n", + "CONTINUE SWAP? (y/n): {\n", + " \"amount\": \"2500000\",\n", + " \"change\": \"2000000\",\n", + " \"fast\": false,\n", + " \"htlc_cltv\": 1136,\n", + " \"initiation_height\": 136,\n", + " \"initiator\": \"loop-cli\",\n", + " \"label\": \"\",\n", + " \"max_swap_fee_satoshis\": \"1824\",\n", + " \"payment_timeout_seconds\": 60,\n", + " \"protocol_version\": \"V0\",\n", + " \"quoted_swap_fee_satoshis\": \"1824\",\n", + " \"state\": \"SignHtlcTx\",\n", + " \"swap_amount\": \"500000\",\n", + " \"swap_hash\": \"9f19fb5042a5de6da2f1ce183c5e224fd7802db408e9afdd598ee8174b2bce3f\",\n", + " \"used_deposits\": [\n", + " {\n", + " \"blocks_until_expiry\": \"14396\",\n", + " \"confirmation_height\": \"131\",\n", + " \"id\": \"ea6abbf0571c0ba82117ae9f2086614eacea8b2913dc0544b70c00de78353e71\",\n", + " \"outpoint\": \"188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0\",\n", + " \"state\": \"LOOPING_IN\",\n", + " \"swap_hash\": \"\",\n", + " \"value\": \"2500000\"\n", + " }\n", + " ]\n", + "}\n" + ] + } + }, + { + "time_ms": 2358, + "kind": "stdin", + "data": { + "text": "y\n" + } + }, + { + "time_ms": 2358, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticAddressLoopIn", + "event": "request", + "message_type": "looprpc.StaticAddressLoopInRequest", + "payload": { + "outpoints": [ + "188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0" + ], + "max_swap_fee_satoshis": "1824", + "last_hop": "", + "label": "", + "initiator": "loop-cli", + "route_hints": [], + "private": false, + "payment_timeout_seconds": 60, + "amount": "500000", + "fast": false + } + } + }, + { + "time_ms": 2824, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticAddressLoopIn", + "event": "response", + "message_type": "looprpc.StaticAddressLoopInResponse", + "payload": { + "swap_hash": "nxn7UEKl3m2i8c4YPF4iT9eALbQI6a/dWY7oF0srzj8=", + "state": "SignHtlcTx", + "amount": "2500000", + "htlc_cltv": 1136, + "quoted_swap_fee_satoshis": "1824", + "max_swap_fee_satoshis": "1824", + "initiation_height": 136, + "protocol_version": "V0", + "label": "", + "initiator": "loop-cli", + "payment_timeout_seconds": 60, + "used_deposits": [ + { + "id": "6mq78FccC6ghF66fIIZhTqzqiykT3AVEtwwA3ng1PnE=", + "state": "LOOPING_IN", + "outpoint": "188f55042e49cfa9942cc1f8e216c5e8679a7036e9ee6449d0fcc6c6b81561be:0", + "value": "2500000", + "confirmation_height": "131", + "blocks_until_expiry": "14396", + "swap_hash": "" + } + ], + "swap_amount": "500000", + "change": "2000000", + "fast": false + } + } + }, + { + "time_ms": 2826, + "kind": "exit", + "data": {} + } + ] +} diff --git a/cmd/loop/testdata/sessions/static-loop-in/26_loop-static-in-auto-unconfirmed.json b/cmd/loop/testdata/sessions/static-loop-in/26_loop-static-in-auto-unconfirmed.json new file mode 100644 index 00000000..dae0c0a0 --- /dev/null +++ b/cmd/loop/testdata/sessions/static-loop-in/26_loop-static-in-auto-unconfirmed.json @@ -0,0 +1,231 @@ +{ + "metadata": { + "args": [ + "/home/user/bin/loop", + "static", + "in", + "--network", + "regtest", + "500000", + "--payment_timeout", + "30s", + "--last_hop", + "0271d6e29301159d9e1cc5d3983479a51f3b3c0c682eda7f16aa1f47dfe09b22f7", + "--force" + ], + "env": { + "HOME": "/home/user" + }, + "version": "0.31.7-beta commit=v0.31.7-beta-28-g6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845 commit_hash=6d8ddfc59ddc2dcfd1a9b4e4b3c53a9cf15dd845", + "duration": 1078774451, + "clock_start_unix": 1769407086 + }, + "events": [ + { + "time_ms": 4, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/ListStaticAddressDeposits", + "event": "request", + "message_type": "looprpc.ListStaticAddressDepositsRequest", + "payload": { + "state_filter": "DEPOSITED", + "outpoints": [] + } + } + }, + { + "time_ms": 179, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/ListStaticAddressDeposits", + "event": "response", + "message_type": "looprpc.ListStaticAddressDepositsResponse", + "payload": { + "filtered_deposits": [ + { + "id": "j71tovlF3ikFqn+pOGB0TZOH00ZEhDYOluRnpR3jvJ0=", + "state": "DEPOSITED", + "outpoint": "9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0", + "value": "500000", + "confirmation_height": "0", + "blocks_until_expiry": "14400", + "swap_hash": "" + } + ] + } + } + }, + { + "time_ms": 180, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "request", + "message_type": "looprpc.QuoteRequest", + "payload": { + "amt": "500000", + "conf_target": 0, + "external_htlc": false, + "swap_publication_deadline": "0", + "loop_in_last_hop": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3", + "loop_in_route_hints": [], + "private": false, + "deposit_outpoints": [], + "asset_info": null, + "auto_select_deposits": true, + "fast": false + } + } + }, + { + "time_ms": 500, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetLoopInQuote", + "event": "response", + "message_type": "looprpc.InQuoteResponse", + "payload": { + "swap_fee_sat": "1824", + "htlc_publish_fee_sat": "0", + "cltv_delta": 0, + "conf_target": 0, + "quoted_amt": "500000" + } + } + }, + { + "time_ms": 500, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "request", + "message_type": "looprpc.StaticAddressSummaryRequest", + "payload": {} + } + }, + { + "time_ms": 500, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/GetStaticAddressSummary", + "event": "response", + "message_type": "looprpc.StaticAddressSummaryResponse", + "payload": { + "static_address": "bcrt1pfu9g59aqtxd39653f76y4c8z7r3t9tmcvrvhl57a3dgj3epdwxdqcd9fpw", + "relative_expiry_blocks": "14400", + "total_num_deposits": 1, + "value_unconfirmed_satoshis": "500000", + "value_deposited_satoshis": "0", + "value_expired_satoshis": "0", + "value_withdrawn_satoshis": "0", + "value_looped_in_satoshis": "0", + "value_htlc_timeout_sweeps_satoshis": "0", + "value_channels_opened": "0" + } + } + }, + { + "time_ms": 500, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticAddressLoopIn", + "event": "request", + "message_type": "looprpc.StaticAddressLoopInRequest", + "payload": { + "outpoints": [], + "max_swap_fee_satoshis": "1824", + "last_hop": "AnHW4pMBFZ2eHMXTmDR5pR87PAxoLtp/FqofR9/gmyL3", + "label": "", + "initiator": "loop-cli", + "route_hints": [], + "private": false, + "payment_timeout_seconds": 30, + "amount": "500000", + "fast": false + } + } + }, + { + "time_ms": 1078, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticAddressLoopIn", + "event": "response", + "message_type": "looprpc.StaticAddressLoopInResponse", + "payload": { + "swap_hash": "hDAjN0JANkGTlqt5ZN14uFsaSBqfHbc9tc3e5XwkQ+c=", + "state": "SignHtlcTx", + "amount": "500000", + "htlc_cltv": 1165, + "quoted_swap_fee_satoshis": "1824", + "max_swap_fee_satoshis": "1824", + "initiation_height": 165, + "protocol_version": "V0", + "label": "", + "initiator": "loop-cli", + "payment_timeout_seconds": 30, + "used_deposits": [ + { + "id": "j71tovlF3ikFqn+pOGB0TZOH00ZEhDYOluRnpR3jvJ0=", + "state": "LOOPING_IN", + "outpoint": "9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0", + "value": "500000", + "confirmation_height": "0", + "blocks_until_expiry": "14400", + "swap_hash": "" + } + ], + "swap_amount": "500000", + "change": "0", + "fast": false + } + } + }, + { + "time_ms": 1078, + "kind": "stdout", + "data": { + "lines": [ + "\n", + "WARNING: The following deposits are below the conservative 6-confirmation threshold:\n", + " - 9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0 (unconfirmed)\n", + "The swap payment for these deposits may wait for more confirmations depending on the server's confirmation-risk policy.\n", + "\n", + "{\n", + " \"amount\": \"500000\",\n", + " \"change\": \"0\",\n", + " \"fast\": false,\n", + " \"htlc_cltv\": 1165,\n", + " \"initiation_height\": 165,\n", + " \"initiator\": \"loop-cli\",\n", + " \"label\": \"\",\n", + " \"max_swap_fee_satoshis\": \"1824\",\n", + " \"payment_timeout_seconds\": 30,\n", + " \"protocol_version\": \"V0\",\n", + " \"quoted_swap_fee_satoshis\": \"1824\",\n", + " \"state\": \"SignHtlcTx\",\n", + " \"swap_amount\": \"500000\",\n", + " \"swap_hash\": \"84302337424036419396ab7964dd78b85b1a481a9f1db73db5cddee57c2443e7\",\n", + " \"used_deposits\": [\n", + " {\n", + " \"blocks_until_expiry\": \"14400\",\n", + " \"confirmation_height\": \"0\",\n", + " \"id\": \"8fbd6da2f945de2905aa7fa93860744d9387d3464484360e96e467a51de3bc9d\",\n", + " \"outpoint\": \"9fa0d5dd5348794aa0541dd2729497f0907890606d044e1c4757bdc848f38df8:0\",\n", + " \"state\": \"LOOPING_IN\",\n", + " \"swap_hash\": \"\",\n", + " \"value\": \"500000\"\n", + " }\n", + " ]\n", + "}\n" + ] + } + }, + { + "time_ms": 1078, + "kind": "exit", + "data": {} + } + ] +} diff --git a/cmd/loop/testdata/sessions/static-openchannel/01_loop-static-openchannel-help.json b/cmd/loop/testdata/sessions/static-openchannel/01_loop-static-openchannel-help.json index 3861060d..11582280 100644 --- a/cmd/loop/testdata/sessions/static-openchannel/01_loop-static-openchannel-help.json +++ b/cmd/loop/testdata/sessions/static-openchannel/01_loop-static-openchannel-help.json @@ -67,15 +67,11 @@ " --max_local_csv uint (optional) the maximum number of blocks that we will allow the remote peer to require we wait before accessing our funds in the case of a unilateral close. (default: 0)\n", " --close_address string (optional) an address to enforce payout of our funds to on cooperative close. Note that if this value is set on channel open, you will *not* be able to cooperatively close to a different address.\n", " --remote_max_value_in_flight_msat uint (optional) the maximum value in msat that can be pending within the channel at any given time (default: 0)\n", - " --channel_type string (optional) the type of channel to propose to the remote peer (\"tweakless\", \"anchors\", \"taproot\")\n", + " --channel_type string (optional) the type of channel to propose to the remote peer (\"tweakless\", \"anchors\", \"simple-taproot\", \"taproot\")\n", " --zero_conf (optional) whether a zero-conf channel open should be attempted. (default: false)\n", " --scid_alias (optional) whether a scid-alias channel type should be negotiated. (default: false)\n", " --remote_reserve_sats uint (optional) the minimum number of satoshis we require the remote node to keep as a direct payment. If not specified, a default of 1% of the channel capacity will be used. (default: 0)\n", - " --memo string (optional) a note-to-self containing some useful\n", - " information about the channel. This is stored\n", - " locally only, and is purely for reference. It\n", - " has no bearing on the channel's operation. Max\n", - " allowed length is 500 characters\n", + " --memo string (optional) a note-to-self containing some useful information about the channel. This is stored locally only, and is purely for reference. It has no bearing on the channel's operation. Max allowed length is 500 characters\n", " --fundmax if set, the wallet will attempt to commit the maximum possible local amount to the channel. This must not be set at the same time as local_amt (default: false)\n", " --utxo string [ --utxo string ] a utxo specified as outpoint(tx:idx) which will be used to fund a channel. This flag can be repeatedly used to fund a channel with a selection of utxos. The selected funds can either be entirely spent by specifying the fundmax flag or partially by selecting a fraction of the sum of the outpoints in local_amt\n", " --help, -h show help\n", diff --git a/cmd/loop/testdata/sessions/static-openchannel/07_loop-static-openchannel-taproot-success.json b/cmd/loop/testdata/sessions/static-openchannel/07_loop-static-openchannel-taproot-success.json new file mode 100644 index 00000000..2eb70154 --- /dev/null +++ b/cmd/loop/testdata/sessions/static-openchannel/07_loop-static-openchannel-taproot-success.json @@ -0,0 +1,104 @@ +{ + "metadata": { + "args": [ + "/home/user/bin/loop", + "--rpcserver=localhost:11010", + "--loopdir=/redacted/loop", + "--tlscertpath=/redacted/loop/regtest/tls.cert", + "--macaroonpath=/redacted/loop/regtest/loop.macaroon", + "static", + "openchannel", + "--node_key", + "03465f68fd39358667678f8353a31e0d99475e3fd2fb4e58daf7dbfabe04c011f4", + "--fundmax", + "--utxo", + "89f6fd2ee96445c6e48278e7eaaca7de8e342904f20609ce72c0d27afb443e2d:1", + "--channel_type", + "taproot", + "--private", + "--network", + "regtest" + ], + "env": {}, + "version": "0.33.2-beta commit=v0.33.2-beta-bump-lnd-21-a-7-g21019e684ed06e2267382f473dfeaafb81a33310 commit_hash=21019e684ed06e2267382f473dfeaafb81a33310", + "duration": 295879956, + "clock_start_unix": 1782002722 + }, + "events": [ + { + "time_ms": 2, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticOpenChannel", + "event": "request", + "message_type": "looprpc.StaticOpenChannelRequest", + "payload": { + "open_channel_request": { + "sat_per_vbyte": "0", + "node_pubkey": "A0ZfaP05NYZnZ4+DU6MeDZlHXj/S+05Y2vfb+r4EwBH0", + "node_pubkey_string": "", + "local_funding_amount": "0", + "push_sat": "0", + "target_conf": 0, + "sat_per_byte": "0", + "private": true, + "min_htlc_msat": "0", + "remote_csv_delay": 0, + "min_confs": 1, + "spend_unconfirmed": false, + "close_address": "", + "funding_shim": null, + "remote_max_value_in_flight_msat": "0", + "remote_max_htlcs": 0, + "max_local_csv": 0, + "commitment_type": "TAPROOT", + "zero_conf": false, + "scid_alias": false, + "base_fee": "0", + "fee_rate": "0", + "use_base_fee": false, + "use_fee_rate": false, + "remote_chan_reserve_sat": "0", + "fund_max": true, + "memo": "", + "outpoints": [ + { + "txid_bytes": "", + "txid_str": "89f6fd2ee96445c6e48278e7eaaca7de8e342904f20609ce72c0d27afb443e2d", + "output_index": 1 + } + ] + } + } + } + }, + { + "time_ms": 295, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticOpenChannel", + "event": "response", + "message_type": "looprpc.StaticOpenChannelResponse", + "payload": { + "channel_open_outpoint": "302a41b53946494306e415bdf4724e88b2cf2b37ad2f36dc075db00cc6499e9f:0" + } + } + }, + { + "time_ms": 295, + "kind": "stdout", + "data": { + "lines": [ + "{\n", + " \"channel_open_outpoint\": \"302a41b53946494306e415bdf4724e88b2cf2b37ad2f36dc075db00cc6499e9f:0\"\n", + "}\n" + ] + } + }, + { + "time_ms": 295, + "kind": "exit", + "data": {} + } + ] +} diff --git a/cmd/loop/testdata/sessions/static-openchannel/08_loop-static-openchannel-taproot-unsupported-lnd.json b/cmd/loop/testdata/sessions/static-openchannel/08_loop-static-openchannel-taproot-unsupported-lnd.json new file mode 100644 index 00000000..3315aa85 --- /dev/null +++ b/cmd/loop/testdata/sessions/static-openchannel/08_loop-static-openchannel-taproot-unsupported-lnd.json @@ -0,0 +1,106 @@ +{ + "metadata": { + "args": [ + "/home/user/bin/loop", + "--rpcserver=localhost:11010", + "--loopdir=/redacted/loop", + "--tlscertpath=/redacted/loop/regtest/tls.cert", + "--macaroonpath=/redacted/loop/regtest/loop.macaroon", + "static", + "openchannel", + "--node_key", + "026eca9330cc3e589505c4a240ea1f7f551e5765b1aae9040c6667141da657eae6", + "--fundmax", + "--utxo", + "dc942b3252ded50abc30ef4e8614d392ab0f091acab75fdd09e18a07f6ac36e6:0", + "--channel_type", + "taproot", + "--private", + "--network", + "regtest" + ], + "env": {}, + "version": "0.33.3-beta commit= commit_hash=", + "run_error": "rpc error: code = Unknown desc = channel_type=taproot is not supported by the connected lnd; update LND to v0.21.0-beta or later to use this channel type: got error from server: rpc error: code = Unknown desc = unhandled request channel type 7", + "duration": 39639744, + "clock_start_unix": 1782158339 + }, + "events": [ + { + "time_ms": 7, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticOpenChannel", + "event": "request", + "message_type": "looprpc.StaticOpenChannelRequest", + "payload": { + "open_channel_request": { + "sat_per_vbyte": "0", + "node_pubkey": "Am7KkzDMPliVBcSiQOoff1UeV2WxqukEDGZnFB2mV+rm", + "node_pubkey_string": "", + "local_funding_amount": "0", + "push_sat": "0", + "target_conf": 0, + "sat_per_byte": "0", + "private": true, + "min_htlc_msat": "0", + "remote_csv_delay": 0, + "min_confs": 1, + "spend_unconfirmed": false, + "close_address": "", + "funding_shim": null, + "remote_max_value_in_flight_msat": "0", + "remote_max_htlcs": 0, + "max_local_csv": 0, + "commitment_type": "TAPROOT", + "zero_conf": false, + "scid_alias": false, + "base_fee": "0", + "fee_rate": "0", + "use_base_fee": false, + "use_fee_rate": false, + "remote_chan_reserve_sat": "0", + "fund_max": true, + "memo": "", + "outpoints": [ + { + "txid_bytes": "", + "txid_str": "dc942b3252ded50abc30ef4e8614d392ab0f091acab75fdd09e18a07f6ac36e6", + "output_index": 0 + } + ] + } + } + } + }, + { + "time_ms": 39, + "kind": "grpc", + "data": { + "method": "/looprpc.SwapClient/StaticOpenChannel", + "event": "error", + "error": "rpc error: code = Unknown desc = channel_type=taproot is not supported by the connected lnd; update LND to v0.21.0-beta or later to use this channel type: got error from server: rpc error: code = Unknown desc = unhandled request channel type 7", + "status": { + "code": 2, + "message": "channel_type=taproot is not supported by the connected lnd; update LND to v0.21.0-beta or later to use this channel type: got error from server: rpc error: code = Unknown desc = unhandled request channel type 7" + } + } + }, + { + "time_ms": 39, + "kind": "stderr", + "data": { + "lines": [ + "[loop] rpc error: code = Unknown desc = channel_type=taproot is not supported by the connected lnd; update LND to v0.21.0-beta or later to use this channel type: got error from server: rpc error: code = Unknown desc = unhandled request channel type 7\n" + ] + } + }, + { + "time_ms": 39, + "kind": "exit", + "data": { + "run_error": "rpc error: code = Unknown desc = channel_type=taproot is not supported by the connected lnd; update LND to v0.21.0-beta or later to use this channel type: got error from server: rpc error: code = Unknown desc = unhandled request channel type 7" + } + } + ] +} diff --git a/cost_migration.go b/cost_migration.go index 14fa2c87..d99ef58b 100644 --- a/cost_migration.go +++ b/cost_migration.go @@ -142,6 +142,7 @@ func MigrateLoopOutCosts(ctx context.Context, lnd lndclient.LndServices, ctx, lndclient.ListPaymentsRequest{ Offset: offset, MaxPayments: uint64(paymentBatchSize), + OmitHops: true, }, ) if err != nil { diff --git a/cost_migration_test.go b/cost_migration_test.go index 082895f2..abdc80d3 100644 --- a/cost_migration_test.go +++ b/cost_migration_test.go @@ -163,6 +163,11 @@ func TestCostMigration(t *testing.T) { // Now we can run the migration. err = MigrateLoopOutCosts(context.Background(), lnd.LndServices, 1, store) require.NoError(t, err) + listPaymentsRequests := lnd.ListPaymentsRequestsSnapshot() + require.NotEmpty(t, listPaymentsRequests) + for _, req := range listPaymentsRequests { + require.True(t, req.OmitHops) + } // Finally check that the swap cost has been updated correctly. swap, err := store.FetchLoopOutSwap( diff --git a/docs/commit-tooling.md b/docs/commit-tooling.md new file mode 100644 index 00000000..d4746f8b --- /dev/null +++ b/docs/commit-tooling.md @@ -0,0 +1,30 @@ +# Commit Message Tooling + +Use `scripts/commit_message.py` to lint, format, and safely reword commit +messages. The script enforces subject/body wrapping (`69`/`72`), keeps real +newlines, and preserves markdown-like body structure (lists, quotes, fenced +blocks, trailers). + +## Common Workflows + +```bash +# Lint the current commit message +make commitmsg-lint commit=HEAD + +# Lint a commit range +make commitmsg-lint range=upstream/master..HEAD + +# Format a message file in place +make commitmsg-fmt file=/tmp/msg inplace=1 + +# Reword a commit from formatted output +make commitmsg-reword commit= + +# Preview a reword without rewriting history +make commitmsg-reword commit= dryrun=1 +``` + +## Literal Newlines + +If a message was created with literal `\n` sequences, use `decode=1` with +`commitmsg-fmt` or `commitmsg-reword` to convert them to real line breaks. diff --git a/docs/loop.1 b/docs/loop.1 index 108243b5..1e21905b 100644 --- a/docs/loop.1 +++ b/docs/loop.1 @@ -2,18 +2,13 @@ .TH loop 1 .SH NAME -.PP -loop - control plane for your loopd +loop \- control plane for your loopd .SH SYNOPSIS -.PP loop -.PP -.RS - -.nf +.EX [--help|-h] [--loopdir]=[value] [--macaroonpath]=[value] @@ -21,25 +16,17 @@ loop [--rpcserver]=[value] [--tlscertpath]=[value] [--version|-v] - -.fi -.RE +.EE .PP \fBUsage\fP: -.PP -.RS - -.nf +.EX loop [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...] - -.fi -.RE +.EE .SH GLOBAL OPTIONS -.PP \fB--help, -h\fP: show help .PP @@ -63,7 +50,6 @@ loop [GLOBAL OPTIONS] [command [COMMAND OPTIONS]] [ARGUMENTS...] .SH COMMANDS .SH out -.PP perform an off-chain to on-chain swap (looping out) .PP @@ -115,7 +101,6 @@ perform an off-chain to on-chain swap (looping out) \fB--verbose, -v\fP: show expanded details .SH in -.PP perform an on-chain to off-chain swap (loop in) .PP @@ -149,28 +134,24 @@ perform an on-chain to off-chain swap (loop in) \fB--verbose, -v\fP: show expanded details .SH terms -.PP Display the current swap terms imposed by the server. .PP \fB--help, -h\fP: show help .SH monitor -.PP monitor progress of any active swaps .PP \fB--help, -h\fP: show help .SH quote -.PP get a quote for the cost of a swap .PP \fB--help, -h\fP: show help .SS in -.PP get a quote for the cost of a loop in swap .PP @@ -195,7 +176,6 @@ get a quote for the cost of a loop in swap \fB--verbose, -v\fP: show expanded details .SS out -.PP get a quote for the cost of a loop out swap .PP @@ -211,22 +191,19 @@ get a quote for the cost of a loop out swap \fB--verbose, -v\fP: show expanded details .SH listauth -.PP list all L402 tokens .PP \fB--help, -h\fP: show help .SH fetchl402 -.PP fetches a new L402 authentication token from the server .PP \fB--help, -h\fP: show help .SH listswaps -.PP -list all swaps in the local database +list traditional Loop In and Loop Out swaps .PP \fB--channel\fP="": the comma-separated list of short channel IDs of the channels to loop out @@ -256,8 +233,7 @@ list all swaps in the local database \fB--start_time_ns\fP="": Unix timestamp in nanoseconds to select swaps initiated after this time (default: 0) .SH swapinfo -.PP -show the status of a swap +show the status of a traditional swap .PP \fB--help, -h\fP: show help @@ -266,14 +242,12 @@ show the status of a swap \fB--id\fP="": the ID of the swap (default: 0) .SH getparams -.PP show liquidity manager parameters .PP \fB--help, -h\fP: show help .SH setrule -.PP set liquidity manager rule for a channel/peer .PP @@ -292,14 +266,12 @@ set liquidity manager rule for a channel/peer \fB--type\fP="": the type of swap to perform, set to 'out' for acquiring inbound liquidity or 'in' for acquiring outbound liquidity. (default: out) .SH suggestswaps -.PP show a list of suggested swaps .PP \fB--help, -h\fP: show help .SH setparams -.PP update the parameters set for the liquidity manager .PP @@ -390,14 +362,12 @@ update the parameters set for the liquidity manager \fB--sweeplimit\fP="": the limit placed on our estimated sweep fee in sat/vByte. (default: 0) .SH getinfo -.PP show general information about the loop daemon .PP \fB--help, -h\fP: show help .SH abandonswap -.PP abandon a swap with a given swap hash .PP @@ -407,21 +377,18 @@ abandon a swap with a given swap hash \fB--i_know_what_i_am_doing\fP: Specify this flag if you made sure that you read and understood the following consequence of applying this command. .SH reservations, r -.PP manage reservations .PP \fB--help, -h\fP: show help .SS list, l -.PP list all reservations .PP \fB--help, -h\fP: show help .SH instantout -.PP perform an instant off-chain to on-chain swap (looping out) .PP @@ -434,14 +401,12 @@ perform an instant off-chain to on-chain swap (looping out) \fB--help, -h\fP: show help .SH listinstantouts -.PP list all instant out swaps .PP \fB--help, -h\fP: show help .SH stop -.PP stop the loop daemon .PP @@ -451,21 +416,18 @@ stop the loop daemon \fB--wait\fP: wait until loopd fully shuts down .SH static, s -.PP perform on-chain to off-chain swaps using static addresses. .PP \fB--help, -h\fP: show help .SS new, n -.PP Create a new static loop in address. .PP \fB--help, -h\fP: show help .SS listunspent, l -.PP List unspent static address outputs. .PP @@ -478,7 +440,6 @@ List unspent static address outputs. \fB--min_confs\fP="": The minimum amount of confirmations an output should have to be listed. (default: 0) .SS listdeposits -.PP Displays static address deposits. A filter can be applied to only show deposits in a specific state. .PP @@ -497,27 +458,24 @@ htlc_timeout_swept wait_for_expiry_sweep expired failed -. +\&. .PP \fB--help, -h\fP: show help .SS listwithdrawals -.PP Display a summary of past withdrawals. .PP \fB--help, -h\fP: show help .SS listswaps -.PP Shows a list of static address swaps. .PP \fB--help, -h\fP: show help .SS withdraw, w -.PP Withdraw from static address deposits. .PP @@ -539,14 +497,12 @@ Withdraw from static address deposits. \fB--utxo\fP="": specify utxos as outpoints(tx:idx) which willbe withdrawn. (default: []) .SS summary, s -.PP Display a summary of static address related information. .PP \fB--help, -h\fP: show help .SS in -.PP Loop in funds from static address deposits. .PP @@ -592,14 +548,13 @@ Loop in funds from static address deposits. \fB--verbose, -v\fP: show expanded details .SS openchannel -.PP Open a channel to an existing peer. .PP \fB--base_fee_msat\fP="": the base fee in milli-satoshis that will be charged for each forwarded HTLC, regardless of payment size (default: 0) .PP -\fB--channel_type\fP="": (optional) the type of channel to propose to the remote peer ("tweakless", "anchors", "taproot") +\fB--channel_type\fP="": (optional) the type of channel to propose to the remote peer ("tweakless", "anchors", "simple-taproot", "taproot") .PP \fB--close_address\fP="": (optional) an address to enforce payout of our funds to on cooperative close. Note that if this value is set on channel open, you will \fInot\fP be able to cooperatively close to a different address. @@ -620,11 +575,7 @@ Open a channel to an existing peer. \fB--max_local_csv\fP="": (optional) the maximum number of blocks that we will allow the remote peer to require we wait before accessing our funds in the case of a unilateral close. (default: 0) .PP -\fB--memo\fP="": (optional) a note-to-self containing some useful - information about the channel. This is stored - locally only, and is purely for reference. It - has no bearing on the channel's operation. Max - allowed length is 500 characters +\fB--memo\fP="": (optional) a note-to-self containing some useful information about the channel. This is stored locally only, and is purely for reference. It has no bearing on the channel's operation. Max allowed length is 500 characters .PP \fB--min_htlc_msat\fP="": (optional) the minimum value we will require for incoming HTLCs on the channel (default: 0) diff --git a/docs/loop.md b/docs/loop.md index 28bf8c0c..316cebdc 100644 --- a/docs/loop.md +++ b/docs/loop.md @@ -235,9 +235,9 @@ The following flags are supported: ### `listswaps` command -list all swaps in the local database. +list traditional Loop In and Loop Out swaps. -Allows the user to get a list of all swaps that are currently stored in the database. +Lists traditional Loop In and Loop Out swaps that are currently stored in the database. Static address loop-ins are not included; use `loop static listswaps` to view them. Usage: @@ -261,9 +261,9 @@ The following flags are supported: ### `swapinfo` command -show the status of a swap. +show the status of a traditional swap. -Allows the user to get the status of a single swap currently stored in the database. +Shows the status of a traditional Loop In or Loop Out swap currently stored in the database. Static address loop-ins must be viewed with `loop static listswaps`; there is no generic per-swap static lookup command. Usage: @@ -715,11 +715,11 @@ The following flags are supported: | `--max_local_csv="…"` | (optional) the maximum number of blocks that we will allow the remote peer to require we wait before accessing our funds in the case of a unilateral close | uint | `0` | | `--close_address="…"` | (optional) an address to enforce payout of our funds to on cooperative close. Note that if this value is set on channel open, you will *not* be able to cooperatively close to a different address | string | | `--remote_max_value_in_flight_msat="…"` | (optional) the maximum value in msat that can be pending within the channel at any given time | uint | `0` | -| `--channel_type="…"` | (optional) the type of channel to propose to the remote peer ("tweakless", "anchors", "taproot") | string | +| `--channel_type="…"` | (optional) the type of channel to propose to the remote peer ("tweakless", "anchors", "simple-taproot", "taproot") | string | | `--zero_conf` | (optional) whether a zero-conf channel open should be attempted | bool | `false` | | `--scid_alias` | (optional) whether a scid-alias channel type should be negotiated | bool | `false` | | `--remote_reserve_sats="…"` | (optional) the minimum number of satoshis we require the remote node to keep as a direct payment. If not specified, a default of 1% of the channel capacity will be used | uint | `0` | -| `--memo="…"` | (optional) a note-to-self containing some useful information about the channel. This is stored locally only, and is purely for reference. It has no bearing on the channel's operation. Max allowed length is 500 characters | string | +| `--memo="…"` | (optional) a note-to-self containing some useful information about the channel. This is stored locally only, and is purely for reference. It has no bearing on the channel's operation. Max allowed length is 500 characters | string | | `--fundmax` | if set, the wallet will attempt to commit the maximum possible local amount to the channel. This must not be set at the same time as local_amt | bool | `false` | | `--utxo="…"` | a utxo specified as outpoint(tx:idx) which will be used to fund a channel. This flag can be repeatedly used to fund a channel with a selection of utxos. The selected funds can either be entirely spent by specifying the fundmax flag or partially by selecting a fraction of the sum of the outpoints in local_amt | string | `[]` | | `--help` (`-h`) | show help | bool | `false` | diff --git a/docs/release-notes/README.md b/docs/release-notes/README.md new file mode 100644 index 00000000..a23154fb --- /dev/null +++ b/docs/release-notes/README.md @@ -0,0 +1,113 @@ +# Loop Client Release Notes + +Release notes are listed in chronological order and include every published +GitHub release. Previous and next links connect adjacent files in this index. +Contributor lists are derived from non-merge commit authors and co-authors +between consecutive official release tags. + +Add user-facing changes to [release-notes-next.md](release-notes-next.md). +When preparing a release, use +[release-notes-template.md](release-notes-template.md) to turn those notes into +a versioned file, then reset the next-release sections. + +| Release notes | Date | Highlights | +| --- | --- | --- | +| [v0.1-alpha](release-notes-0.1.md) | 2019-03-21 | Initial Loop Out release | +| [v0.1.1-alpha](release-notes-0.1.1.md) | 2019-04-17 | Added testnet Loop In | +| [v0.1.2-alpha](release-notes-0.1.2.md) | 2019-05-03 | Fixed macaroon, testnet, and routing fee handling | +| [v0.1.3-beta](release-notes-0.1.3-beta.md) | 2019-05-31 | Published the v0.1.3 beta tag with Docker and accounting updates | +| [v0.1.3-alpha](release-notes-0.1.3.md) | 2019-05-31 | Added Docker support and accounting improvements | +| [v0.2-alpha](release-notes-0.2.md) | 2019-06-26 | Introduced Loop In | +| [v0.2.0-alpha](release-notes-0.2.0.md) | 2019-06-26 | Aliased v0.2-alpha without source changes | +| [v0.2.1-alpha](release-notes-0.2.1.md) | 2019-07-25 | Added configurable Loop Out sweep confirmation targets | +| [v0.2.2-alpha](release-notes-0.2.2.md) | 2019-07-31 | Improved external Loop Out validation | +| [v0.2.3-alpha](release-notes-0.2.3.md) | 2019-10-03 | Fixed REST parity, fee estimation, and confirmation targets | +| [v0.2.4-alpha](release-notes-0.2.4.md) | 2019-10-11 | Prepared flexible swap fees and timing | +| [v0.3.0-alpha](release-notes-0.3.0.md) | 2019-11-21 | Added delayed, batchable Loop Out execution | +| [v0.3.1-alpha](release-notes-0.3.1.md) | 2020-01-15 | Added fee controls for Loop Out and fast quotes | +| [v0.4.0-rc1.beta](release-notes-0.4.0-rc1.md) | 2020-01-24 | Previewed authenticated requests and fixed Loop In timeouts | +| [v0.4.0-beta](release-notes-0.4.0.md) | 2020-02-04 | Added authenticated requests and REST monitoring | +| [v0.4.1-beta](release-notes-0.4.1.md) | 2020-02-11 | Added REST CORS controls and fixed delayed quote errors | +| [v0.5.0-beta](release-notes-0.5.0.md) | 2020-03-05 | Added Loop In last-hop selection | +| [v0.5.1-beta](release-notes-0.5.1.md) | 2020-03-15 | Improved Loop In quotes and fee sanity checks | +| [v0.6.0-beta](release-notes-0.6.0.md) | 2020-04-30 | Added LND v0.10 support and Loop In confirmation targets | +| [v0.6.1-beta](release-notes-0.6.1.md) | 2020-05-12 | Added native SegWit Loop In HTLCs and fixed multipart limits | +| [v0.6.2-beta](release-notes-0.6.2.md) | 2020-05-12 | Published native SegWit Loop In and multipart-limit fixes | +| [v0.6.3-beta](release-notes-0.6.3.md) | 2020-06-04 | Restored multi-channel Loop Out with LND v0.10.1 | +| [v0.6.4-beta](release-notes-0.6.4.md) | 2020-06-12 | Improved Loop Out restart behavior and liquidity speed | +| [v0.6.5-beta](release-notes-0.6.5.md) | 2020-07-02 | Added human-readable server messages | +| [v0.7.0-beta](release-notes-0.7.0.md) | 2020-07-21 | Added longer confirmation targets and server status subscriptions | +| [v0.8.0-beta](release-notes-0.8.0.md) | 2020-08-11 | Improved swap failure reporting and confirmation controls | +| [v0.8.1-beta](release-notes-0.8.1.md) | 2020-08-28 | Reduced Loop Out routing and sweep fees | +| [v0.9.0-beta](release-notes-0.9.0.md) | 2020-09-10 | Introduced liquidity management and TLS | +| [v0.10.0-beta](release-notes-0.10.0.md) | 2020-10-13 | Added multipath Loop In and fee-aware swap suggestions | +| [v0.11.0-beta](release-notes-0.11.0.md) | 2020-10-27 | Introduced Autoloop and transaction labels | +| [v0.11.1-beta](release-notes-0.11.1.md) | 2020-11-12 | Added swap initiator metadata | +| [v0.11.2-beta](release-notes-0.11.2.md) | 2020-12-08 | Added configurable Autoloop swap size limits | +| [v0.11.3-beta](release-notes-0.11.3.md) | 2021-02-09 | Added locked-LND startup waiting and single macaroon support | +| [v0.11.4-beta](release-notes-0.11.4.md) | 2021-02-11 | Defaulted to LND's admin macaroon path | +| [v0.12.0-beta](release-notes-0.12.0.md) | 2021-03-06 | Added per-peer Autoloop rules | +| [v0.12.1-beta](release-notes-0.12.1.md) | 2021-03-26 | Added verbose quote and swap output | +| [v0.12.2-beta](release-notes-0.12.2.md) | 2021-04-29 | Retried failed LSAT payments | +| [v0.13.0-beta](release-notes-0.13.0.md) | 2021-05-19 | Raised the minimum LND version to v0.11.1-beta | +| [v0.14.0-beta](release-notes-0.14.0.md) | 2021-06-01 | Reported Loop Out routing failures to the server | +| [v0.14.1-beta](release-notes-0.14.1.md) | 2021-06-09 | Removed protobuf startup warnings | +| [v0.14.2-beta](release-notes-0.14.2.md) | 2021-07-20 | Fixed Python gRPC client connectivity | +| [v0.15.0-beta](release-notes-0.15.0.md) | 2021-08-03 | Added Loop In quote probing | +| [v0.15.1-beta](release-notes-0.15.1.md) | 2021-11-18 | Updated to LND v0.14 and added WASM RPC stubs | +| [v0.16.0-beta](release-notes-0.16.0.md) | 2021-12-14 | Added private Loop In route hints | +| [v0.17.0-beta](release-notes-0.17.0.md) | 2022-02-07 | Added Loop In support to Autoloop | +| [v0.18.0-beta](release-notes-0.18.0.md) | 2022-03-30 | Added optional routing plugins | +| [v0.19.1-beta](release-notes-0.19.1.md) | 2022-06-09 | Persisted liquidity parameters | +| [v0.20.0-beta](release-notes-0.20.0.md) | 2022-07-20 | Added experimental P2TR HTLCs and MuSig2 sweeps | +| [v0.20.1-beta](release-notes-0.20.1.md) | 2022-08-01 | Improved sweep transaction logging | +| [v0.20.2-beta](release-notes-0.20.2.md) | 2022-10-26 | Raised the minimum LND version for Taproot support | +| [v0.21.0-beta](release-notes-0.21.0.md) | 2022-12-16 | Added Autoloop destination addresses | +| [v0.22.0-beta](release-notes-0.22.0.md) | 2023-03-29 | Added recurring Autoloop budgets | +| [v0.22.1-beta](release-notes-0.22.1.md) | 2023-04-24 | Corrected the HTLC script version for legacy swaps | +| [v0.23.0-beta](release-notes-0.23.0.md) | 2023-04-25 | Made MuSig2 the default swap protocol | +| [v0.24.0-beta](release-notes-0.24.0.md) | 2023-05-24 | Added Easy Autoloop | +| [v0.24.1-beta](release-notes-0.24.1.md) | 2023-05-26 | Added REST bindings for GetInfo | +| [v0.25.0-beta](release-notes-0.25.0.md) | 2023-07-03 | Added SQL stores and Easy Autoloop fee fixes | +| [v0.25.1-beta](release-notes-0.25.1.md) | 2023-07-04 | Removed Windows 386 release builds | +| [v0.25.2-beta](release-notes-0.25.2.md) | 2023-07-04 | Removed unsupported release platforms | +| [v0.26.0-beta](release-notes-0.26.0.md) | 2023-07-28 | Added xpub-backed Loop Out destinations | +| [v0.26.1-beta](release-notes-0.26.1.md) | 2023-08-08 | Recorded swap initiators and repaired faulty timestamps | +| [v0.26.2-beta](release-notes-0.26.2.md) | 2023-08-09 | Migrated faulty Loop Out timestamps | +| [v0.26.3-beta](release-notes-0.26.3.md) | 2023-09-18 | Added the FSM foundation and hardened Loop In settlement | +| [v0.26.4-beta](release-notes-0.26.4.md) | 2023-10-04 | Added Easy Autoloop destinations and Apple Silicon builds | +| [v0.26.5-beta](release-notes-0.26.5.md) | 2023-10-31 | Fixed leap-year parsing and refactored database startup | +| [v0.26.6-beta](release-notes-0.26.6.md) | 2023-11-28 | Added Loop In abandonment and specific balance failures | +| [v0.27.0-beta](release-notes-0.27.0.md) | 2024-01-30 | Added batched Loop Out sweeps | +| [v0.27.1-beta](release-notes-0.27.1.md) | 2024-02-14 | Automatically recovered incorrectly funded external Loop Ins | +| [v0.28.0-beta](release-notes-0.28.0.md) | 2024-03-05 | Expanded Instant Out listing, destinations, and fee accounting | +| [v0.28.1-beta](release-notes-0.28.1.md) | 2024-04-16 | Routed Loop Out prepayments over selected channels | +| [v0.28.2-beta](release-notes-0.28.2.md) | 2024-05-25 | Renamed LSAT options and APIs to L402 | +| [v0.28.3-beta](release-notes-0.28.3.md) | 2024-06-03 | Corrected stored Loop Out costs and batch restoration | +| [v0.28.4-beta](release-notes-0.28.4.md) | 2024-06-04 | Fixed pending-swap cost migration and added RPC timeouts | +| [v0.28.5-beta](release-notes-0.28.5.md) | 2024-06-06 | Paginated cost-migration payment lookups | +| [v0.28.6-beta](release-notes-0.28.6.md) | 2024-07-11 | Expanded sweep-batcher signing and fee controls | +| [v0.28.7-beta](release-notes-0.28.7.md) | 2024-08-08 | Improved sweep selection and stored swap metadata | +| [v0.28.8-beta](release-notes-0.28.8.md) | 2024-10-21 | Introduced the notification manager and mixed sweep batches | +| [v0.28.9-beta](release-notes-0.28.9.md) | 2024-10-30 | Added L402 retrieval and FSM context propagation | +| [v0.29.0-beta](release-notes-0.29.0.md) | 2024-12-18 | Persisted static address Loop In mode | +| [v0.29.1-beta](release-notes-0.29.1.md) | 2025-03-18 | Added Taproot Asset Loop Outs and partial static withdrawals | +| [v0.30.0-beta](release-notes-0.30.0.md) | 2025-03-27 | Added Taproot Asset Easy Autoloop and hardened recovery | +| [v0.31.0-beta](release-notes-0.31.0.md) | 2025-04-25 | Added filtering and pagination to loop listswaps | +| [v0.31.1-beta](release-notes-0.31.1.md) | 2025-05-05 | Changed Autoloop to use a slow publication deadline | +| [v0.31.2-beta](release-notes-0.31.2.md) | 2025-06-18 | Corrected commit hash and version reporting | +| [v0.31.3-beta](release-notes-0.31.3.md) | 2025-09-25 | Added partial static Loop Ins and hardened static address recovery | +| [v0.31.4-beta](release-notes-0.31.4.md) | 2025-10-16 | Added fast static Loop Ins and reproducible releases | +| [v0.31.5-beta](release-notes-0.31.5.md) | 2025-10-31 | Added swap resumption and notification versioning | +| [v0.31.5-beta-lnd0.20](release-notes-0.31.5-lnd0.20.md) | 2025-10-31 | Updated LND 0.20 compatibility and module version metadata | +| [v0.31.6-beta](release-notes-0.31.6.md) | 2025-11-26 | Added Easy Autoloop peer exclusions and loop stop | +| [v0.31.7-beta](release-notes-0.31.7.md) | 2025-11-26 | Bumped the release version after v0.31.6 | +| [v0.31.8-beta](release-notes-0.31.8.md) | 2026-02-03 | Added manual HTLC sweeps and PSBT static withdrawals | +| [v0.32.0-beta](release-notes-0.32.0.md) | 2026-03-02 | Opened channels directly from static address deposits | +| [v0.32.1-beta](release-notes-0.32.1.md) | 2026-03-02 | Updated reproducible releases to Go 1.26 | +| [v0.33.0-beta](release-notes-0.33.0.md) | 2026-04-08 | Added static Loop In fee caps and lifecycle hardening | +| [v0.33.1-beta](release-notes-0.33.1.md) | 2026-05-26 | Added Static Address Autoloop and CLI session tests | +| [v0.33.2-beta](release-notes-0.33.2.md) | 2026-06-08 | Exposed static swap details and hardened MuSig2 inputs | +| [v0.33.3-beta](release-notes-0.33.3.md) | 2026-06-21 | Raised the LND floor and updated the LND dependency | +| [v0.34.0-beta](release-notes-0.34.0.md) | 2026-07-23 | Tracked low-confirmation deposits and production Taproot channels | +| [Next release](release-notes-next.md) | Unreleased | No changes yet | diff --git a/docs/release-notes/release-notes-0.1.1.md b/docs/release-notes/release-notes-0.1.1.md new file mode 100644 index 00000000..92665ea6 --- /dev/null +++ b/docs/release-notes/release-notes-0.1.1.md @@ -0,0 +1,148 @@ +# Loop Client Release Notes + +- **Release date:** 2019-04-17 +- **Release page:** + [v0.1.1-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.1.1-alpha) +- **Previous release:** [v0.1-alpha](release-notes-0.1.md) +- **Next release:** [v0.1.2-alpha](release-notes-0.1.2.md) + +#### New Features + +This is a minor release of the Lightning Loop Go client. + +Includes changes to: +- Add testnet support for Loop In (on-chain to off-chain swaps) +- Add amt as a keyword argument + +The Loop In implementation can be kicked off using the `loop in ` command: +``` +⛰ loop in -h +NAME: + loop in - perform an on-chain to off-chain swap (loop in) + +USAGE: + loop in [command options] amt + +DESCRIPTION: + + Send the amount in satoshis specified by the amt argument off-chain. + +OPTIONS: + --amt value the amount in satoshis to loop in (default: 0) + --external expect htlc to be published externally +``` + +The `--external` flag is of note as it allows the HTLC to be paid by a wallet +other then `lnd`. This allows for users to do things like top-off channel from +_another wallet_, or _withdrawal_ funds from an exchange directly into one's +channel. It can also be used to obtain an address to give to someone else, which +once paid and Looped In, is credited to your channel! + +#### Breaking Changes + +#### Bug Fixes + +- Fix monitoring exception + +#### Maintenance + +- Documentation updates + +**Verifying the Release** + +In order to verify the release, you'll need to have `gpg` or `gpg2` installed on +your system. Once you've obtained a copy (and hopefully verified that as well), +you'll first need to import the keys that have signed this release if you +haven't done so already: +``` +curl https://keybase.io/roasbeef/pgp_keys.asc | gpg --import +``` + +Once you have his PGP key you can verify the release (assuming +`manifest-v0.1.1-alpha.txt` and `manifest-v0.1.1-alpha.txt.sig` are in the +current directory) with: +``` +gpg --verify manifest-v0.1.1-alpha.txt +``` + +You should see the following if the verification was successful: +``` +gpg: assuming signed data in 'manifest-v0.1.1-alpha.txt' +gpg: Signature made Wed Apr 17 15:20:49 2019 PDT +gpg: using RSA key F8037E70C12C7A263C032508CE58F7F8E20FD9A2 +gpg: Good signature from "Olaoluwa Osuntokun " [ultimate] +``` + +That will verify the signature on the main manifest page which ensures integrity +and authenticity of the binaries you've downloaded locally. Next, depending on +your operating system you should then re-calculate the `sha256` sum of the +binary, and compare that with the following hashes (which are included in the +manifest file): +``` +d3c3db1bed3ff0fb125514d9072d621492ff2b86f4a90200e93686052f18f5bd loop-darwin-386-v0.1.1-alpha.tar.gz +ebf461a604b681d8a5c82bae14a0620a38a9f65604ca4f532aedb44ddc7c4ea0 loop-darwin-amd64-v0.1.1-alpha.tar.gz +bc83eb2eefb91054858c2a9164fdf245848f2f861d01526261e65dbd2d8237e0 loop-dragonfly-amd64-v0.1.1-alpha.tar.gz +ece2bb8e4c477627a4485f809e7f0d1c88893e01ca7e33363784c392875f00b6 loop-freebsd-386-v0.1.1-alpha.tar.gz +496941befd46331a4551ff9ff60fe4dc1a49434c1593960f3ed53a255aca0783 loop-freebsd-amd64-v0.1.1-alpha.tar.gz +6e21b065178d9616e78a63bacdca4401f8ed36a82dbdfb0d915099af29ae29da loop-freebsd-arm-v0.1.1-alpha.tar.gz +3264e23ffe8994bd374c961f4973eaa6288e4dce0531967de85b23b342518824 loop-linux-386-v0.1.1-alpha.tar.gz +476f26083febb42fab15fa263048cf8bddcc86671aeaa98e1b74f5c422ebaee4 loop-linux-amd64-v0.1.1-alpha.tar.gz +9f13fb6cc9df51501e4c9560da96d96a9d0f385e600eb77fdbfdb97fb731a4a6 loop-linux-arm64-v0.1.1-alpha.tar.gz +53faaf225412c4f9dc16d437252f666243dba13f9135f07886ec019e5b109ebb loop-linux-armv6-v0.1.1-alpha.tar.gz +4e8c04d1585476a8efb02805181dde80be0a1b45816d24f004924b6756298dd5 loop-linux-armv7-v0.1.1-alpha.tar.gz +a21b05379615a6d7d4de68c3edb2f5a341f23dd0f4895b646b5272e6b99e98d2 loop-linux-mips64-v0.1.1-alpha.tar.gz +047c2a9e62b477db7eb8c4f88a5bda68bcdd10bb396d4ba66ff34328a7b9e6d8 loop-linux-mips64le-v0.1.1-alpha.tar.gz +bd25a16e27fb3140be8ae62a8df1076a80ceac443420738d6261f0c375e60bb5 loop-linux-ppc64-v0.1.1-alpha.tar.gz +ef1a37d85c8ae1eb7fba34a66613344042f26867ab838017b3995ef4d7d8cd95 loop-netbsd-386-v0.1.1-alpha.tar.gz +1962c2b174bc401c5362ca1f7282e8129b6958dae08a1fd0248de6505d4b6c4c loop-netbsd-amd64-v0.1.1-alpha.tar.gz +be27b8c84fac5c0e492aa7b395e3b344f8e0c7170525610e2df51238f8fea7de loop-openbsd-386-v0.1.1-alpha.tar.gz +31ce199daf1af10f328af3b5d8b9cfcefc980eabffd6b91481ddf8a5d2b1e235 loop-openbsd-amd64-v0.1.1-alpha.tar.gz +406d5989ec935faf85dbdaccb88a659ba88ff35ca111b71bf769871f20139ccd loop-source-v0.1.1-alpha.tar.gz +61395e480f7e1cecb9b945a7cad4a0b81186767474da5520dee48b6e89191f9b loop-windows-386-v0.1.1-alpha.zip +01848825834fb4fc1c13841ffdfdc1b59b4f352d350d5b7ffbb3252885f0330d loop-windows-amd64-v0.1.1-alpha.zip +2f0fcaca1479d04b49e85e0a3b7f70dd430cf675daaaa4b77104a83747bba3e0 vendor.tar.gz +``` + +One can use the `shasum -a 256 ` tool in order to re-compute the +`sha256` hash of the target binary for your operating system. The produced hash +should be compared with the hashes listed above and they should match *exactly*. + +Finally, you can also verify the _tag_ itself with the following command: +``` +git verify-tag v0.1.1-alpha +``` + +**Building the Contained Release** + +With this new version of `loop`, we've modified our release process to ensure +the bundled release is now _fully self contained_. As a result, with only the +attached payload with this release, users will be able to rebuild the target +release themselves without having to fetch any of the dependancies. Note that at +this stage, binaries aren't yet fully reproducible (even with `go modules` ). +This is due to the fact that by default, +[Go will include the full directory path where the binary was built in the binary itself](https://github.com/golang/go/issues/16860). +As a result, unless your file system exactly mirrors the machine used to build +the binary, you'll get a different binary, as it includes artifacts from your +local file system. This will be fixed in `go1.13`, and before then we may modify +our release system to do this automatically. + +In order to re-build from scratch, assuming that `vendor.tar.gz` and +`loop-source-v0.1.1-alpha.tar.gz` are in the current directory: +``` +tar -xvzf vendor.tar.gz +tar -xvzf loop-source-v0.1.1-alpha.tar.gz +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.1.1-alpha" ./cmd/loop +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.1.1-alpha" ./cmd/loopd +``` + +The `-mod=vendor` flag tells the `go build` command that it doesn't need to +fetch the dependencies, and instead, they're all enclosed in the local vendor +directory. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- conscott +- Francisco Calderón +- Joost Jager +- Olaoluwa Osuntokun diff --git a/docs/release-notes/release-notes-0.1.2.md b/docs/release-notes/release-notes-0.1.2.md new file mode 100644 index 00000000..08a90b60 --- /dev/null +++ b/docs/release-notes/release-notes-0.1.2.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2019-05-03 +- **Release page:** + [v0.1.2-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.1.2-alpha) +- **Previous release:** [v0.1.1-alpha](release-notes-0.1.1.md) +- **Next release:** [v0.1.3-beta](release-notes-0.1.3-beta.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +Patch release for Loop client with fixes: + +- No more need to regenerate macaroons +- Fixes issue that required specifying testnet remote service URL directly +- Reduce max routing fee + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Joost Jager +- Olaoluwa Osuntokun diff --git a/docs/release-notes/release-notes-0.1.3-beta.md b/docs/release-notes/release-notes-0.1.3-beta.md new file mode 100644 index 00000000..22b107cf --- /dev/null +++ b/docs/release-notes/release-notes-0.1.3-beta.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2019-05-31 +- **Release page:** + [v0.1.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.1.3-beta) +- **Previous release:** [v0.1.2-alpha](release-notes-0.1.2.md) +- **Next release:** [v0.1.3-alpha](release-notes-0.1.3.md) + +#### New Features + +* Add Docker integration. + [PR #46](https://github.com/lightninglabs/loop/pull/46) +* Persist completed swap costs and expose them through the RPC interface. + [PR #55](https://github.com/lightninglabs/loop/pull/55) + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Increase the maximum Lightning gRPC receive size to accommodate larger + responses. + [PR #56](https://github.com/lightninglabs/loop/pull/56) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Geoff Taylor +- Joost Jager +- Valentine Wallace diff --git a/docs/release-notes/release-notes-0.1.3.md b/docs/release-notes/release-notes-0.1.3.md new file mode 100644 index 00000000..4f90c987 --- /dev/null +++ b/docs/release-notes/release-notes-0.1.3.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2019-05-31 +- **Release page:** + [v0.1.3-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.1.3-alpha) +- **Previous release:** [v0.1.3-beta](release-notes-0.1.3-beta.md) +- **Next release:** [v0.2-alpha](release-notes-0.2.md) + +#### New Features + +Minor changes: + +- Add Docker support +- Improve after-the-fact accounting + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Joost Jager +- Olaoluwa Osuntokun +- Valentine Wallace diff --git a/docs/release-notes/release-notes-0.1.md b/docs/release-notes/release-notes-0.1.md new file mode 100644 index 00000000..2b943411 --- /dev/null +++ b/docs/release-notes/release-notes-0.1.md @@ -0,0 +1,170 @@ +# Loop Client Release Notes + +- **Release date:** 2019-03-21 +- **Release page:** + [v0.1-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.1-alpha) +- **Previous release:** None +- **Next release:** [v0.1.1-alpha](release-notes-0.1.1.md) + +#### New Features + +This is the first major release of Lightning Loop! This release is the first of +many planned, and includes the initial base functionality for the system. +[Check out our blog post for a high level overview on how the Loop service works](https://blog.lightning.engineering/posts/2019/03/20/loop.html). +With this new release, users can use Loop Out to free up inbound receiving +bandwidth in their channels and also send coins on chain from their existing +channel. + +The Lightning Loop client software is similar to `lnd`. There's the primary +daemon `loopd`, and its command-line interface `loop`. +[Check out the instructions in the `README` to get started](https://github.com/lightninglabs/loop/blob/master/README.md). +The `loop` command is very simple and at initial release has the following +commands: +``` +⛰ loop -h +NAME: + loop - control plane for your loopd + +USAGE: + loop [global options] command [command options] [arguments...] + +VERSION: + 0.1.0-alpha commit=v0.1-alpha + +COMMANDS: + out perform an off-chain to on-chain swap (looping out) + terms Display the current swap terms imposed by the server. + monitor monitor progress of any active swaps + quote get a quote for the cost of a swap + help, h Shows a list of commands or help for one command + +GLOBAL OPTIONS: + --loopd value loopd daemon address host:port (default: "localhost:11010") + --help, -h show help + --version, -v print the version +``` + +You'll most frequently be using `loop out`. A sample run looks something like: +``` +loop out 500000 --addr=bc1qnrhawnjr7fn3gd09s8gltzzjdfsxsrm2k64ze7 +``` + +By specifying the optional `addr` flag, I tell `loopd` that I'd like to have the +funds sent to that target address. This lets one do cool things like send +directly from your channel into cold storage, or even deposit directly into an +exchange or another wallet under one's control. + +Once the loop has been initiated, you can use `loop monitor` to check on its +status. + +The other primary way to +[interact with Lightning Loop is via the gRPC API](https://lightning.engineering/loop). +This lets existing lapps, services, and businesses drive Loop programmatically +in a similar fashion to how they interact with `lnd` today. We'll have more +example uses cases, documentation, and explainers in the near future. + +Loop there it is!! ⚡️🔁 + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +**Verifying the Release** + +In order to verify the release, you'll need to have `gpg` or `gpg2` installed on +your system. Once you've obtained a copy (and hopefully verified that as well), +you'll first need to import the keys that have signed this release if you +haven't done so already: +``` +curl https://keybase.io/roasbeef/pgp_keys.asc | gpg --import +``` + +Once you have his PGP key you can verify the release (assuming +`manifest-v0.1-alpha.txt` and `manifest-v0.1-alpha.txt.sig` are in the current +directory) with: +``` +gpg --verify manifest-v0.1-alpha.txt +``` + +You should see the following if the verification was successful: +``` +gpg: assuming signed data in 'manifest-v0.1-alpha.txt' +gpg: Signature made Wed Mar 20 20:22:49 2019 PDT +gpg: using RSA key F8037E70C12C7A263C032508CE58F7F8E20FD9A2 +gpg: Good signature from "Olaoluwa Osuntokun " [ultimate] +``` + +That will verify the signature on the main manifest page which ensures integrity +and authenticity of the binaries you've downloaded locally. Next, depending on +your operating system you should then re-calculate the `sha256` sum of the +binary, and compare that with the following hashes (which are included in the +manifest file): +``` +5c63d870d007d4dd9d0480a0ebc1fee83f1099acac12a4bdd121d3882b772bfb loop-darwin-386-v0.1-alpha.tar.gz +98fc299e107c89df4875af0f2fe46957b228321036c60661507ecdbb9ba1a56a loop-darwin-amd64-v0.1-alpha.tar.gz +761c5af296e62b2eaa9421e9d36a258e9f435a2bc53df33b950b41d3a9eef89d loop-dragonfly-amd64-v0.1-alpha.tar.gz +269997eba3daebeeac7edb82739b3406df3e52b3345c61124c60b145b9080c0f loop-freebsd-386-v0.1-alpha.tar.gz +38fb7a32c9c36f80cac9a47e448338dedde53554f569454202236d278654eadf loop-freebsd-amd64-v0.1-alpha.tar.gz +7226a8c5171c6e10adf754d1c3acd81dee84497d170877981ea441108368fee7 loop-freebsd-arm-v0.1-alpha.tar.gz +e07552fd11967743262a678060369f5fbe17bcde3c4ee6cc21196618248afb9c loop-linux-386-v0.1-alpha.tar.gz +816a7050c91652a8e5b881e939c1bc70e06fe23e8309ac80ef5d38fda06969b7 loop-linux-amd64-v0.1-alpha.tar.gz +75a9ce3f605359735d39d9a0fb5037676c6ac29a70d7db5bc75fc1e5e3fcc465 loop-linux-arm64-v0.1-alpha.tar.gz +b7c7668c90f1d67689a9e557346e32bf10c169ace220cd507ae4ee63f64b12d1 loop-linux-armv6-v0.1-alpha.tar.gz +8aad519ff927643c35968bc9eb4e91a5a1f063ebf60e50b12c05f745d3602540 loop-linux-armv7-v0.1-alpha.tar.gz +5b3be8f2ab4acff7e854a794ecde59abd7feed3002bb13177f89c1cd1c9419e7 loop-linux-mips64-v0.1-alpha.tar.gz +aa3da5049eb2d411912aba8386dcc241f9db2b41dd4571ddbea80cf07f3fbc20 loop-linux-mips64le-v0.1-alpha.tar.gz +1a6a8e8e93df6c02eac1e61d4e9f3a518bc2c2a096deb0b77f0e5a6c0afb553d loop-linux-ppc64-v0.1-alpha.tar.gz +85e4f4e32783aad5e7e53125ca4c2efecaf997b24e81e54db0cc380cdf876cf9 loop-netbsd-386-v0.1-alpha.tar.gz +8896df3499e9287177b5eacb2fc6b964e6dec698807f49582cbccbd3b7088878 loop-netbsd-amd64-v0.1-alpha.tar.gz +133b069ad151adeac9c62596ba507f9ed425cfe6670a03e2e5dea6316e522f18 loop-openbsd-386-v0.1-alpha.tar.gz +a9d1f80d34f698450d413844ff1a4536a4377faa6debd7c16bcfde94964cd628 loop-openbsd-amd64-v0.1-alpha.tar.gz +94c35abdcafeebb17f19850b5e88031926116cce8bd22cdf56d905716a0063d8 loop-source-v0.1-alpha.tar.gz +5cd5171f469e639ece6b37a2ff35bae3dac6d1b0a9490ed9e3f90e4f1fcadb97 loop-windows-386-v0.1-alpha.zip +b139416e426868b060ea6fd28ba840398a6e45ad492eac50297462d4391bc0ce loop-windows-amd64-v0.1-alpha.zip +e2ab0d8f7ec07468ea46e551f17521799147a01f48a3f2c5b1c2d62efd9512a5 vendor.tar.gz +``` + +One can use the `shasum -a 256 ` tool in order to re-compute the +`sha256` hash of the target binary for your operating system. The produced hash +should be compared with the hashes listed above and they should match *exactly*. + +Finally, you can also verify the _tag_ itself with the following command: +``` +git verify-tag v0.1-alpha +``` + +**Building the Contained Release** + +With this new version of `loop`, we've modified our release process to ensure +the bundled release is now _fully self contained_. As a result, with only the +attached payload with this release, users will be able to rebuild the target +release themselves without having to fetch any of the dependancies. Note that at +this stage, binaries aren't yet fully reproducible (even with `go modules` ). +This is due to the fact that by default, +[Go will include the full directory path where the binary was built in the binary itself](https://github.com/golang/go/issues/16860). +As a result, unless your file system exactly mirrors the machine used to build +the binary, you'll get a different binary, as it includes artifacts from your +local file system. This will be fixed in `go1.13`, and before then we may modify +our release system to do this automatically. + +In order to re-build from scratch, assuming that `vendor.tar.gz` and +`loop-source-v0.1-alpha.tar.gz` are in the current directory: +``` +tar -xvzf vendor.tar.gz +tar -xvzf loop-source-v0.1-alpha.tar.gz +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.1-alpha" ./cmd/loop +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.1-alpha" ./cmd/loopd +``` + +The `-mod=vendor` flag tells the `go build` command that it doesn't need to +fetch the dependencies, and instead, they're all enclosed in the local vendor +directory. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Joost Jager +- Olaoluwa Osuntokun +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.10.0.md b/docs/release-notes/release-notes-0.10.0.md new file mode 100644 index 00000000..a7f72675 --- /dev/null +++ b/docs/release-notes/release-notes-0.10.0.md @@ -0,0 +1,56 @@ +# Loop Client Release Notes + +- **Release date:** 2020-10-13 +- **Release page:** + [v0.10.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.10.0-beta) +- **Previous release:** [v0.9.0-beta](release-notes-0.9.0.md) +- **Next release:** [v0.11.0-beta](release-notes-0.11.0.md) + +#### New Features + +* Multi-path payment has been enabled for Loop In. This means that it is now + possible to replenish multiple channels via a single Loop In request and a + single on-chain htlc. This has to potential to greatly reduce chain fee costs. + Note that it is not yet possible to select specific peers to loop in through. +* The daemon now sends a user agent string with each swap. This allows + developers to identify their fork or custom implementation of the loop client. + +**Updated Swap Suggestions** + +* The swap suggestions endpoint has been updated to be fee-aware. Swaps that + exceed the fee limits set by the liquidity manager will no longer be suggested + (see `getparams` for the current limits, and use `setparams` to update these + values). +* Swap suggestions are now aware of ongoing and previously failed swaps. They + will not suggest swaps for channels that are currently being utilized for + swaps, and will not suggest any swaps if a swap that is not limited to a + specific peer or channel is ongoing. If a channel was part of a failed swap + within the last 24H, it will be excluded from our swap suggestions (this value + is configurable). +* The `debug` logging level is recommended if using this feature. + +#### Breaking Changes + +* Macaroon authentication has been enabled for the `loopd` gRPC and REST + connections. This makes it possible for the loop API to be exposed safely over + the internet as unauthorized access is now prevented. + +The daemon will write a default `loop.macaroon` in its main directory. For +mainnet this file will be picked up automatically by the `loop` CLI tool. For +testnet you need to specify the `--network=testnet` flag. +[More information about TLS and macaroons.](../../README.md#authentication-and-transport-security) + +* The `setparm` loopcli endpoint is renamed to `setrule` because this endpoint + is only used for setting liqudity rules (parameters can be set using the new + `setparams` endpoint). + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Joost Jager +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.11.0.md b/docs/release-notes/release-notes-0.11.0.md new file mode 100644 index 00000000..804e9019 --- /dev/null +++ b/docs/release-notes/release-notes-0.11.0.md @@ -0,0 +1,37 @@ +# Loop Client Release Notes + +- **Release date:** 2020-10-27 +- **Release page:** + [v0.11.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.11.0-beta) +- **Previous release:** [v0.10.0-beta](release-notes-0.10.0.md) +- **Next release:** [v0.11.1-beta](release-notes-0.11.1.md) + +#### New Features + +* The loop client now labels all its on-chain transactions to make them easily + identifiable in `lnd` 's `listchaintxns` output. + +**Introducing Autoloop** + +* This release includes support for opt-in automatic dispatch of loop out swaps, + based on the output of the `Suggestions` endpoint. +* To enable the autolooper, the following command can be used: + `loop setparams --autoout=true --autobudget={budget in sats} --budgetstart={start time for budget}` +* Automatically dispatched swaps are identified in the output of the `ListSwaps` + with the label `[reserved]: autoloop-out`. +* If autoloop is not enabled, the client will log the actions that the + autolooper would have taken if it was enabled, and the `Suggestions` endpoint + can be used to view the exact set of swaps that the autolooper would make if + enabled. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.11.1.md b/docs/release-notes/release-notes-0.11.1.md new file mode 100644 index 00000000..5d6ff9ee --- /dev/null +++ b/docs/release-notes/release-notes-0.11.1.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2020-11-12 +- **Release page:** + [v0.11.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.11.1-beta) +- **Previous release:** [v0.11.0-beta](release-notes-0.11.0.md) +- **Next release:** [v0.11.2-beta](release-notes-0.11.2.md) + +#### New Features + +* When requesting a swap, a new `initiator` field can be set on the gRPC/REST + interface that is appended to the user agent string that is sent to the server + to give information about Loop usage. The initiator field is meant for user + interfaces to add their name to give the full picture of the binary used + (`loopd`, LiT) and the method/interface used for triggering the swap (`loop` + CLI, autolooper, LiT UI, other 3rd party UI). + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.11.2.md b/docs/release-notes/release-notes-0.11.2.md new file mode 100644 index 00000000..717e93df --- /dev/null +++ b/docs/release-notes/release-notes-0.11.2.md @@ -0,0 +1,35 @@ +# Loop Client Release Notes + +- **Release date:** 2020-12-08 +- **Release page:** + [v0.11.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.11.2-beta) +- **Previous release:** [v0.11.1-beta](release-notes-0.11.1.md) +- **Next release:** [v0.11.3-beta](release-notes-0.11.3.md) + +#### New Features + +**Autoloop Swap Size** + +* Autoloop can now be configured with custom swap size limits. Previously, + autoloop would use the minimum/maximum swap amount set by the server (exposed + by the `loop terms` command) to decide on swap size. +* Setting a custom minimum swap amount is particularly useful for clients that + would like to perform fewer, larger swaps to save on fees. The trade-off when + setting a large minimum amount is that autoloop will wait until your channel + is at least the minimum amount below its incoming threshold amount before + executing a swap, which may result in channels staying under the threshold for + longer. +* These values can be set using the following command: + `loop setparams --minamt={minimum in sats} --maxamt={maximum in sats}`. +* The values set must fall within the limits set by the loop server. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen diff --git a/docs/release-notes/release-notes-0.11.3.md b/docs/release-notes/release-notes-0.11.3.md new file mode 100644 index 00000000..05e378ef --- /dev/null +++ b/docs/release-notes/release-notes-0.11.3.md @@ -0,0 +1,50 @@ +# Loop Client Release Notes + +- **Release date:** 2021-02-09 +- **Release page:** + [v0.11.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.11.3-beta) +- **Previous release:** [v0.11.2-beta](release-notes-0.11.2.md) +- **Next release:** [v0.11.4-beta](release-notes-0.11.4.md) + +#### New Features + +* If lnd is locked when the loop client starts up, it will wait for lnd to be + unlocked. Previous versions would exit with an error. +* Loop will no longer need all `lnd` subserver macaroons to be present in the + `--lnd.macaroondir`. Instead the new `--lnd.macaroonpath` option can be + pointed to a single macaroon, for example the `admin.macaroon` or a custom + baked one with the exact permissions needed for Loop. If the now deprecated + flag/option `--lnd.macaroondir` is used, it will fall back to use only the + `admin.macaroon` from that directory. +* The rules used for autoloop have been relaxed to allow autoloop to dispatch + swaps even if there are manually initiated swaps that are not limited to a + single channel in progress. This change was made to allow autoloop to coexist + with manual swaps. +* The `SuggestSwaps` endpoint has been updated to include reasons that indicate + why the Autolooper is not currently dispatching swaps for the set of rules + that the client is configured with. See the + [autoloop documentation](../autoloop.md) for a detailed explanations of + these reasons. + +#### Breaking Changes + +* The `AutoOut`, `AutoOutBudgetSat` and `AutoOutBudgetStartSec` fields in the + `LiquidityParameters` message used in the experimental autoloop API have been + renamed to `Autoloop`, `AutoloopBudgetSat` and `AutoloopBudgetStartSec`. +* The `autoout` flag for enabling automatic dispatch of loop out swaps has been + renamed to `autoloop` so that it can cover loop out and loop in. +* The `SuggestSwaps` rpc call will now fail with a `FailedPrecondition` grpc + error code if no rules are configured for the autolooper. Previously the rpc + would return an empty response. + +#### Bug Fixes + +- Fixed compile time compatibility with `lnd v0.12.0-beta`. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.11.4.md b/docs/release-notes/release-notes-0.11.4.md new file mode 100644 index 00000000..893e6c1b --- /dev/null +++ b/docs/release-notes/release-notes-0.11.4.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2021-02-11 +- **Release page:** + [v0.11.4-beta](https://github.com/lightninglabs/loop/releases/tag/v0.11.4-beta) +- **Previous release:** [v0.11.3-beta](release-notes-0.11.3.md) +- **Next release:** [v0.12.0-beta](release-notes-0.12.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Default for `--lnd.macaroonpath` is set to the default path for the LND + `admin.macaroon` (https://github.com/lightninglabs/loop/issues/336) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath diff --git a/docs/release-notes/release-notes-0.12.0.md b/docs/release-notes/release-notes-0.12.0.md new file mode 100644 index 00000000..d8bc1014 --- /dev/null +++ b/docs/release-notes/release-notes-0.12.0.md @@ -0,0 +1,40 @@ +# Loop Client Release Notes + +- **Release date:** 2021-03-06 +- **Release page:** + [v0.12.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.12.0-beta) +- **Previous release:** [v0.11.4-beta](release-notes-0.11.4.md) +- **Next release:** [v0.12.1-beta](release-notes-0.12.1.md) + +#### New Features + +* Autoloop can now be configured on a per-peer basis, rather than only on an + individual channel level. This change allows desired liquidity thresholds to + be set for an individual peer, rather than a specific channel, and leverages + multi-loop-out to more efficiently manage liquidity. To configure peer-level + rules, provide the 'setrule' command with the peer's pubkey. +* Autoloop's fee API has been simplified to allow setting a single percentage + which will be used to limit total swap fees to a percentage of the amount + being swapped, the default budget has been updated to reflect this. Use + `loop setparams --feepercent={percentage}` to update this value. This fee + setting has been updated to the default for autoloop. +* The default confirmation target for automated loop out swap sweeps has been + increased to 100 blocks. This change will not affect the time it takes to + acquire inbound liquidity, but will decrease the cost of swaps. + +#### Breaking Changes + +#### Bug Fixes + +* The loop dockerfile has been updated to use the `make` command so that the + latest commit hash of the code being run will be included in `loopd`. +* A bug where loop in on-chain fees were not recorded properly has been + addressed. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- rockstardev diff --git a/docs/release-notes/release-notes-0.12.1.md b/docs/release-notes/release-notes-0.12.1.md new file mode 100644 index 00000000..e7a14a34 --- /dev/null +++ b/docs/release-notes/release-notes-0.12.1.md @@ -0,0 +1,40 @@ +# Loop Client Release Notes + +- **Release date:** 2021-03-26 +- **Release page:** + [v0.12.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.12.1-beta) +- **Previous release:** [v0.12.0-beta](release-notes-0.12.0.md) +- **Next release:** [v0.12.2-beta](release-notes-0.12.2.md) + +#### New Features + +* A new flag, `--verbose`, or `-v`, is added to `loop in`, `loop out` and + `loop quote`. Responses from these commands are also updated to provide more + verbose info, giving users a more intuitive view about money paid on/off-chain + and fees incurred. Use `loop in -v`, `loop out -v`, `loop quote in -v` or + `loop quote out -v` to view the details. +* A stripped down version of the Loop server is now provided as a + [Docker image](https://hub.docker.com/r/lightninglabs/loopserver). A quick + start script and example `docker-compose` environment as well as + [documentation on how to use the `regtest` Loop server](https://github.com/lightninglabs/loop/blob/master/regtest/README.md) + was added too. + +#### Breaking Changes + +#### Bug Fixes + +* A bug that would not list autoloop rules set on a per-peer basis when they + were excluded due to insufficient budget, or the number of swaps in flight has + been corrected. These rules will now be included in the output of + `suggestswaps` with other autoloop peer rules. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Elle Mouton +- Justin O'Brien +- Oliver Gugger +- Yong Yu diff --git a/docs/release-notes/release-notes-0.12.2.md b/docs/release-notes/release-notes-0.12.2.md new file mode 100644 index 00000000..a12757e7 --- /dev/null +++ b/docs/release-notes/release-notes-0.12.2.md @@ -0,0 +1,29 @@ +# Loop Client Release Notes + +- **Release date:** 2021-04-29 +- **Release page:** + [v0.12.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.12.2-beta) +- **Previous release:** [v0.12.1-beta](release-notes-0.12.1.md) +- **Next release:** [v0.13.0-beta](release-notes-0.13.0.md) + +#### New Features + +- If the payment for an LSAT fails, it is now automatically re-tried. + +#### Breaking Changes + +#### Bug Fixes + + - Instead of just blocking for forever without any apparent reason if another + Loop daemon process is already running, we now exit with an error after 5 + seconds if acquiring the unique lock on the Loop `bbolt` DB fails. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Elle Mouton +- Oliver Gugger +- Yong Yu diff --git a/docs/release-notes/release-notes-0.13.0.md b/docs/release-notes/release-notes-0.13.0.md new file mode 100644 index 00000000..749afcb4 --- /dev/null +++ b/docs/release-notes/release-notes-0.13.0.md @@ -0,0 +1,25 @@ +# Loop Client Release Notes + +- **Release date:** 2021-05-19 +- **Release page:** + [v0.13.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.13.0-beta) +- **Previous release:** [v0.12.2-beta](release-notes-0.12.2.md) +- **Next release:** [v0.14.0-beta](release-notes-0.14.0.md) + +#### New Features + +#### Breaking Changes + +A breaking change is required due to poold changes. + +- Bumped the minimum required version of `lnd` to `v0.11.1-beta`. + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Carla Kirk-Cohen +- Oliver Gugger +- Yong Yu diff --git a/docs/release-notes/release-notes-0.14.0.md b/docs/release-notes/release-notes-0.14.0.md new file mode 100644 index 00000000..d36aead4 --- /dev/null +++ b/docs/release-notes/release-notes-0.14.0.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2021-06-01 +- **Release page:** + [v0.14.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.14.0-beta) +- **Previous release:** [v0.13.0-beta](release-notes-0.13.0.md) +- **Next release:** [v0.14.1-beta](release-notes-0.14.1.md) + +#### New Features + +- The loopd client reports off-chain routing failures for loop out swaps if it + cannot find a route to the server for the swap's prepay or invoice payment. + This allows the server to release accepted invoices, if there are any, + earlier, reducing the amount of time that funds are held off-chain. If the + swap failed on one of the loop server's channels, it will report failure + location of its off-chain failure. If the failure occurred outside of the loop + server's infrastructure, a generic failure will be used so that no information + about the client's position in the network is leaked. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Maurice Poirrier diff --git a/docs/release-notes/release-notes-0.14.1.md b/docs/release-notes/release-notes-0.14.1.md new file mode 100644 index 00000000..80e7102c --- /dev/null +++ b/docs/release-notes/release-notes-0.14.1.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2021-06-09 +- **Release page:** + [v0.14.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.14.1-beta) +- **Previous release:** [v0.14.0-beta](release-notes-0.14.0.md) +- **Next release:** [v0.14.2-beta](release-notes-0.14.2.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* A protobuf warning that was being logged on `loopd` startup and `loop` cli + calls has been addressed. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen diff --git a/docs/release-notes/release-notes-0.14.2.md b/docs/release-notes/release-notes-0.14.2.md new file mode 100644 index 00000000..985a2b1e --- /dev/null +++ b/docs/release-notes/release-notes-0.14.2.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2021-07-20 +- **Release page:** + [v0.14.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.14.2-beta) +- **Previous release:** [v0.14.1-beta](release-notes-0.14.1.md) +- **Next release:** [v0.15.0-beta](release-notes-0.15.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +- Certain versions of the Python gRPC library + [weren't able to connect](https://github.com/grpc/grpc/issues/23172) to + `loopd` 's gRPC interface, getting the `missing selected ALPN property` error. + A server side fix was introduced to get rid of that error message. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Olaoluwa Osuntokun +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.15.0.md b/docs/release-notes/release-notes-0.15.0.md new file mode 100644 index 00000000..13939592 --- /dev/null +++ b/docs/release-notes/release-notes-0.15.0.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2021-08-03 +- **Release page:** + [v0.15.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.15.0-beta) +- **Previous release:** [v0.14.2-beta](release-notes-0.14.2.md) +- **Next release:** [v0.15.1-beta](release-notes-0.15.1.md) + +#### New Features + +* Loop-in quote now asks the server to optionally probe the client to test + inbound liquidity. The server may use this information to give more accurate + quotes. + +#### Breaking Changes + +#### Bug Fixes + +* Grpc error codes returned by the swap server when swap initiation fails are + now surfaced to the client. Previously these error codes would be returned as + a string. + +#### Maintenance + +* Updated compile time dependencies of `lnd`, `grpc-gateway`, `protobuf` and + `grpc`. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Oliver Gugger +- Yong Yu diff --git a/docs/release-notes/release-notes-0.15.1.md b/docs/release-notes/release-notes-0.15.1.md new file mode 100644 index 00000000..feafa720 --- /dev/null +++ b/docs/release-notes/release-notes-0.15.1.md @@ -0,0 +1,41 @@ +# Loop Client Release Notes + +- **Release date:** 2021-11-18 +- **Release page:** + [v0.15.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.15.1-beta) +- **Previous release:** [v0.15.0-beta](release-notes-0.15.0.md) +- **Next release:** [v0.16.0-beta](release-notes-0.16.0.md) + +#### New Features + +* Add JSON client stubs for using the Loop RPC interface from WebAssembly. + [PR #411](https://github.com/lightninglabs/loop/pull/411) +* Export `NewListenerConfig` so callers can construct custom listeners. + [PR #432](https://github.com/lightninglabs/loop/pull/432) + +#### Breaking Changes + +#### Bug Fixes + +* Improve errors for explicitly configured files. + [PR #413](https://github.com/lightninglabs/loop/pull/413) +* Create the default macaroon only when required by the active configuration. + [PR #428](https://github.com/lightninglabs/loop/pull/428) + +#### Maintenance + +* Refactor Autoloop swap construction behind a swap-builder interface. + [PR #418](https://github.com/lightninglabs/loop/pull/418) +* Update `lndclient` and the compile-time LND dependency for LND v0.14. + [PR #425](https://github.com/lightninglabs/loop/pull/425) + [PR #426](https://github.com/lightninglabs/loop/pull/426) + [PR #438](https://github.com/lightninglabs/loop/pull/438) + +#### Contributors (Alphabetical Order) + +- Carla Kirk-Cohen +- Harsha Goli +- Martin Habovštiak +- Oliver Gugger +- Turtle +- Yong Yu diff --git a/docs/release-notes/release-notes-0.16.0.md b/docs/release-notes/release-notes-0.16.0.md new file mode 100644 index 00000000..265937bf --- /dev/null +++ b/docs/release-notes/release-notes-0.16.0.md @@ -0,0 +1,41 @@ +# Loop Client Release Notes + +- **Release date:** 2021-12-14 +- **Release page:** + [v0.16.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.16.0-beta) +- **Previous release:** [v0.15.1-beta](release-notes-0.15.1.md) +- **Next release:** [v0.17.0-beta](release-notes-0.17.0.md) + +#### New Features + +* `--private` flag is now available on the `loop in` command, meaning users can + now loop in to private nodes! This was implemented in + [#415](https://github.com/lightninglabs/loop/pull/415) and has an + implementation of LND's hop hints creation feature for the time being. The + flag is also available in `loop quote in` as an extension. +* `--route_hints` has also been added on the `loop in` and `loop quote in` cli + commands, and was also include in + [#415](https://github.com/lightninglabs/loop/pull/415). While the `--private` + flag autogenerates routehints to assist the payer (Lightning Labs), + `--route_hints` allows the user to feed their own crafted versions if they so + please. + +#### Breaking Changes + +#### Bug Fixes + +* Fixed issue where loop assumes the mainnet location for lnd.macaroonpath + regardless of passed network parameters. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Harsha Goli +- Martin Habovštiak +- Oliver Gugger +- Turtle +- Yong Yu diff --git a/docs/release-notes/release-notes-0.17.0.md b/docs/release-notes/release-notes-0.17.0.md new file mode 100644 index 00000000..6b99d1a4 --- /dev/null +++ b/docs/release-notes/release-notes-0.17.0.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2022-02-07 +- **Release page:** + [v0.17.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.17.0-beta) +- **Previous release:** [v0.16.0-beta](release-notes-0.16.0.md) +- **Next release:** [v0.18.0-beta](release-notes-0.18.0.md) + +#### New Features + +* Loop in functionality has been added to AutoLoop. This feature can be enabled + to acquire outgoing capacity on your node automatically, using + `loop setrule --type=in`. At present, autoloop can only be set to loop out + *or* loop in, and cannot manage liquidity in both directions. + +* Use LND's hop hint selector when doing private loop-ins. + +#### Breaking Changes + +#### Bug Fixes + +* Close local databases when loopd daemon is stopped programmatically. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Elle Mouton +- Suhail Saqan diff --git a/docs/release-notes/release-notes-0.18.0.md b/docs/release-notes/release-notes-0.18.0.md new file mode 100644 index 00000000..8f6a094d --- /dev/null +++ b/docs/release-notes/release-notes-0.18.0.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2022-03-30 +- **Release page:** + [v0.18.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.18.0-beta) +- **Previous release:** [v0.17.0-beta](release-notes-0.17.0.md) +- **Next release:** [v0.19.1-beta](release-notes-0.19.1.md) + +#### New Features + +* Loop client now supports optional routing plugins to improve off-chain payment + reliability. One such plugin that the client implements will gradually prefer + increasingly more expensive routes in case payments using cheap routes time + out. Note that with this addition the minimum required LND version is LND + 0.14.2-beta. + +#### Breaking Changes + +#### Bug Fixes + +* Loop now supports being hooked up to a remote signing pair of `lnd` nodes, as + long as `lnd` is `v0.14.3-beta` or later. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Harsha Goli +- Marnix +- Oliver Gugger +- Yong Yu diff --git a/docs/release-notes/release-notes-0.19.1.md b/docs/release-notes/release-notes-0.19.1.md new file mode 100644 index 00000000..6d4e3800 --- /dev/null +++ b/docs/release-notes/release-notes-0.19.1.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2022-06-09 +- **Release page:** + [v0.19.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.19.1-beta) +- **Previous release:** [v0.18.0-beta](release-notes-0.18.0.md) +- **Next release:** [v0.20.0-beta](release-notes-0.20.0.md) + +#### New Features + +* User-specified liquidity parameters are persisted on disk to enable the + liquidity manager's config to survive after a restart. + +#### Breaking Changes + +#### Bug Fixes + +* The `SuggestSwaps` rpc now returns the correct peer pubkeys in the + disqualified list. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Alex Miller +- Andras Banki-Horvath +- Carsten Otto +- Harsha Goli +- Oliver Gugger +- sputn1ck +- Yong Yu diff --git a/docs/release-notes/release-notes-0.2.0.md b/docs/release-notes/release-notes-0.2.0.md new file mode 100644 index 00000000..bdc485a6 --- /dev/null +++ b/docs/release-notes/release-notes-0.2.0.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2019-06-26 +- **Release page:** + [v0.2.0-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2.0-alpha) +- **Previous release:** [v0.2-alpha](release-notes-0.2.md) +- **Next release:** [v0.2.1-alpha](release-notes-0.2.1.md) + +#### New Features + +The `v0.2.0-alpha` and `v0.2-alpha` tags point to the same source commit. This +tag does not introduce additional source changes. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +No additional contributors; this tag points to the same commit as +`v0.2-alpha`. diff --git a/docs/release-notes/release-notes-0.2.1.md b/docs/release-notes/release-notes-0.2.1.md new file mode 100644 index 00000000..741cc044 --- /dev/null +++ b/docs/release-notes/release-notes-0.2.1.md @@ -0,0 +1,38 @@ +# Loop Client Release Notes + +- **Release date:** 2019-07-25 +- **Release page:** + [v0.2.1-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2.1-alpha) +- **Previous release:** [v0.2.0-alpha](release-notes-0.2.0.md) +- **Next release:** [v0.2.2-alpha](release-notes-0.2.2.md) + +#### New Features + +This release includes a new option to specify the sweep confirmation target for +Loop Out. + +``` +--conf_target=block_count +``` + +This will delay the finality of the swap in favor of a potential reduction in +chain fee expenditure. + +If you are using this feature, please note that it does become increasingly +important that you do not allow `loopd` to terminate before the swap is final, +otherwise the timeout pathway may activate in the HTLC. + +Because of the finality problem, the target will only be used at the start of +the swap preimage reveal time period and is not a guaranteed final sweep fee +rate. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.2.2.md b/docs/release-notes/release-notes-0.2.2.md new file mode 100644 index 00000000..6b9f5bb7 --- /dev/null +++ b/docs/release-notes/release-notes-0.2.2.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2019-07-31 +- **Release page:** + [v0.2.2-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2.2-alpha) +- **Previous release:** [v0.2.1-alpha](release-notes-0.2.1.md) +- **Next release:** [v0.2.3-alpha](release-notes-0.2.3.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +This release includes some important fixes for Loop Out. + +- Improved support for selecting an external Loop Out address +- Better validation for executing a Loop Out swap + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Bjorn Olav Jalborg +- Joost Jager diff --git a/docs/release-notes/release-notes-0.2.3.md b/docs/release-notes/release-notes-0.2.3.md new file mode 100644 index 00000000..ad1a4153 --- /dev/null +++ b/docs/release-notes/release-notes-0.2.3.md @@ -0,0 +1,37 @@ +# Loop Client Release Notes + +- **Release date:** 2019-10-03 +- **Release page:** + [v0.2.3-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2.3-alpha) +- **Previous release:** [v0.2.2-alpha](release-notes-0.2.2.md) +- **Next release:** [v0.2.4-alpha](release-notes-0.2.4.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +This patch release fixes the following issues: + +- Max confirmations now respects a longer confirmation target more reliably +- REST API now matches the gRPC API more closely +- Fix issue encountered when using external address with Loop In + +For more details, see: + +- https://github.com/lightninglabs/loop/pull/74 - REST API Update +- https://github.com/lightninglabs/loop/pull/86 - Loop In Fee Estimation Failure +- https://github.com/lightninglabs/loop/pull/89 - Max Confirmation Target Issue + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Nigel Christian +- Olaoluwa Osuntokun +- Oliver Gugger +- saguywalker +- William O'Beirne +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.2.4.md b/docs/release-notes/release-notes-0.2.4.md new file mode 100644 index 00000000..06ffa69a --- /dev/null +++ b/docs/release-notes/release-notes-0.2.4.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2019-10-11 +- **Release page:** + [v0.2.4-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2.4-alpha) +- **Previous release:** [v0.2.3-alpha](release-notes-0.2.3.md) +- **Next release:** [v0.3.0-alpha](release-notes-0.3.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +This patch release includes: + +- Preparatory changes for upcoming server changes related to swap fees and + timing. + +In the future, the Loop service will allow more flexibility around extensions to +the swap timing in order to get a lower swap fee. + +In preparation, the API will be adjusted to provide for multiple fee rates at +different preferred swap wait times. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Johan T. Halseth +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.2.md b/docs/release-notes/release-notes-0.2.md new file mode 100644 index 00000000..9db683a4 --- /dev/null +++ b/docs/release-notes/release-notes-0.2.md @@ -0,0 +1,153 @@ +# Loop Client Release Notes + +- **Release date:** 2019-06-26 +- **Release page:** + [v0.2-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.2-alpha) +- **Previous release:** [v0.1.3-alpha](release-notes-0.1.3.md) +- **Next release:** [v0.2.0-alpha](release-notes-0.2.0.md) + +#### New Features + +This is the first second release of Lightning Loop! This new release enables +**Loop In** on mainnet, and also exposes some additional code used to build the +`loop` daemon as an external package that others can use. +[Check out our latest blog post for more details on Loop In, and the future of Lightning Loop](https://blog.lightning.engineering/announcement/2019/06/25/loop-in.html). + +**Loop In** + +At a high-level a Loop In swap can be used to: + * Refilling depleted channels with funds from cold-wallets or exchange + withdrawals + * Servicing off-chain Lightning withdrawals using on-chain payments, with no + funds in channels required + +#### Breaking Changes + +#### Bug Fixes + + * As a failsafe payment method that can be used when channel liquidity along a + route is insufficient + +A new command has been added to the `loop` cli command to allow users to drive +Loop In swaps: +``` +NAME: + loop in - perform an on-chain to off-chain swap (loop in) + +USAGE: + loop in [command options] amt + +DESCRIPTION: + + Send the amount in satoshis specified by the amt argument off-chain. + +OPTIONS: + --amt value the amount in satoshis to loop in (default: 0) + --external expect htlc to be published externally +``` + +The `--external` argument allows the on-chain HTLC transacting to be published +externally. This allows for a number of use cases like using this address to +withdraw from an exchange into your Lightning channel! + +[Additionally, the gRPC API (and REST!) docs have also been updated](https://lightning.engineering/loop). + +Loop there it is!! ⚡️🔁 + +#### Maintenance + +**Verifying the Release** + +In order to verify the release, you'll need to have `gpg` or `gpg2` installed on +your system. Once you've obtained a copy (and hopefully verified that as well), +you'll first need to import the keys that have signed this release if you +haven't done so already: +``` +curl https://keybase.io/roasbeef/pgp_keys.asc | gpg --import +``` + +Once you have his PGP key you can verify the release (assuming +`manifest-v0.2-alpha.txt` and `manifest-v0.2-alpha.txt.sig` are in the current +directory) with: +``` +gpg --verify manifest-v0.2-alpha.txt +``` + +You should see the following if the verification was successful: +``` +gpg: assuming signed data in 'manifest-v0.2-alpha.txt' +gpg: Signature made Wed Jun 26 09:56:58 2019 PDT +gpg: using RSA key F8037E70C12C7A263C032508CE58F7F8E20FD9A2 +gpg: Good signature from "Olaoluwa Osuntokun " [ultimate] +``` + +That will verify the signature on the main manifest page which ensures integrity +and authenticity of the binaries you've downloaded locally. Next, depending on +your operating system you should then re-calculate the `sha256` sum of the +binary, and compare that with the following hashes (which are included in the +manifest file): +``` +de97a2e33fcce9650911f8976ed9089d061a1d5fb374bdfd3be0d6101871585d loop-darwin-386-v0.2-alpha.tar.gz +666a5910757cb15dd2a62ac009b31359a97f4f1c83803d4028d10d23db6ee587 loop-darwin-amd64-v0.2-alpha.tar.gz +4a06d43e48e7537975a92d25ac187b78a74426976ffd36bb1efaeb83bae7aa4f loop-dragonfly-amd64-v0.2-alpha.tar.gz +f6cf356db4060d8c90b91dc1d8547472b255ceb8aae2197f531a177a285f60fc loop-freebsd-386-v0.2-alpha.tar.gz +74fdb8d3a8e0a91cfeabe5e06f2be386a68e4524fa42d9c042b9a2030499a76d loop-freebsd-amd64-v0.2-alpha.tar.gz +d500f36eefc1e1ee63b8259d4eefcd64f34b295781956559bd52389b5a37a98b loop-freebsd-arm-v0.2-alpha.tar.gz +d2d01f9a8d1483173c5f1618477ff1929e8aa5e322849091d7b86694fedbf0bb loop-linux-386-v0.2-alpha.tar.gz +da92445ec5e754da3f379e85310d697f3609a17e6639d1251a479a09fc0c16e1 loop-linux-amd64-v0.2-alpha.tar.gz +04edae2cbdb6943b70b13cbc678901d26f4f06767f60f591f7c7d791c8b5925f loop-linux-arm64-v0.2-alpha.tar.gz +5fae0185f8849364af9b998609d002f5e226d3e937dabfd56d1474415f6555db loop-linux-armv6-v0.2-alpha.tar.gz +931a43c8a936db0a3a6a3d099b67d953b69344259bfa7d37faac985341ff27e9 loop-linux-armv7-v0.2-alpha.tar.gz +757ae30b349f8fc209f7df20e1b6010814d0dd279a3717b8c7ba5cb7a092103b loop-linux-mips64-v0.2-alpha.tar.gz +900c8ce735ac94f1d35c179e4387cc9292f261be685eb57b865d772bd668d8ec loop-linux-mips64le-v0.2-alpha.tar.gz +aeb3d2f882405308827974515ea5d652d5e6d72188165ab315dbbb4c7402bc3f loop-linux-ppc64-v0.2-alpha.tar.gz +0d072fd9ed648de679f46ee959a3aef1ae9e025d756140e432df2a51bfe57221 loop-netbsd-386-v0.2-alpha.tar.gz +f26cedf2e8a38b175b47a6b6101edab99033e4776c6de21306cdc793e6dc0b0b loop-netbsd-amd64-v0.2-alpha.tar.gz +3ea2bb1482c78d2bb45358c3de7961670b5ed64a231ec9a2dc2b6ba13a3e76a6 loop-openbsd-386-v0.2-alpha.tar.gz +7a67ae9805beb8e43bd257e8355f8c471bbc740b1b2f1bbf8f834d50312299fe loop-openbsd-amd64-v0.2-alpha.tar.gz +8f15d5c5f56bd19952361ee27b1821306d634bbe8799e52ab36a09f8c151ff6d loop-source-v0.2-alpha.tar.gz +99de0a0e0464a5d7910e6ec4c5cdc881db278bf1bbb6095d5aec7c7a033aca0f loop-windows-386-v0.2-alpha.zip +fb1e4dca3728a2608dce560385bf6e07cb319d27f4d7cfeb0a4aaf00b1cc2a4c loop-windows-amd64-v0.2-alpha.zip +47b208033931c22fd6b1b23b723d49e18359709cb6b29a3f2623e6f80179bfbf vendor.tar.gz +``` + +One can use the `shasum -a 256 ` tool in order to re-compute the +`sha256` hash of the target binary for your operating system. The produced hash +should be compared with the hashes listed above and they should match *exactly*. + +Finally, you can also verify the _tag_ itself with the following command: +``` +git verify-tag v0.2-alpha +``` + +**Building the Contained Release** + +With this new version of `loop`, we've modified our release process to ensure +the bundled release is now _fully self contained_. As a result, with only the +attached payload with this release, users will be able to rebuild the target +release themselves without having to fetch any of the dependancies. Note that at +this stage, binaries aren't yet fully reproducible (even with `go modules` ). +This is due to the fact that by default, +[Go will include the full directory path where the binary was built in the binary itself](https://github.com/golang/go/issues/16860). +As a result, unless your file system exactly mirrors the machine used to build +the binary, you'll get a different binary, as it includes artifacts from your +local file system. This will be fixed in `go1.13`, and before then we may modify +our release system to do this automatically. + +In order to re-build from scratch, assuming that `vendor.tar.gz` and +`loop-source-v0.2-alpha.tar.gz` are in the current directory: +``` +tar -xvzf vendor.tar.gz +tar -xvzf loop-source-v0.2-alpha.tar.gz +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.2-alpha" ./cmd/loop +GO111MODULE=on go install -v -mod=vendor -ldflags "-X github.com/lightninglabs/loop.Commit=v0.2-alpha" ./cmd/loopd +``` + +The `-mod=vendor` flag tells the `go build` command that it doesn't need to +fetch the dependencies, and instead, they're all enclosed in the local vendor +directory. + +#### Contributors (Alphabetical Order) + +No additional contributors; this tag points to the same commit as +`v0.1.3-alpha`. diff --git a/docs/release-notes/release-notes-0.20.0.md b/docs/release-notes/release-notes-0.20.0.md new file mode 100644 index 00000000..b08ac6f2 --- /dev/null +++ b/docs/release-notes/release-notes-0.20.0.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2022-07-20 +- **Release page:** + [v0.20.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.20.0-beta) +- **Previous release:** [v0.19.1-beta](release-notes-0.19.1.md) +- **Next release:** [v0.20.1-beta](release-notes-0.20.1.md) + +#### New Features + +* P2TR HTLCs and privacy preserving and cheaper MuSig2 loopout sweeps are now + supported as an experimental feature when running `loopd` with + the`--experimental` flag. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Elle Mouton +- Oliver Gugger +- sputn1ck diff --git a/docs/release-notes/release-notes-0.20.1.md b/docs/release-notes/release-notes-0.20.1.md new file mode 100644 index 00000000..14eff49d --- /dev/null +++ b/docs/release-notes/release-notes-0.20.1.md @@ -0,0 +1,22 @@ +# Loop Client Release Notes + +- **Release date:** 2022-08-01 +- **Release page:** + [v0.20.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.20.1-beta) +- **Previous release:** [v0.20.0-beta](release-notes-0.20.0.md) +- **Next release:** [v0.20.2-beta](release-notes-0.20.2.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +- Improve logging for sweep transactions + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath diff --git a/docs/release-notes/release-notes-0.20.2.md b/docs/release-notes/release-notes-0.20.2.md new file mode 100644 index 00000000..16196c79 --- /dev/null +++ b/docs/release-notes/release-notes-0.20.2.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2022-10-26 +- **Release page:** + [v0.20.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.20.2-beta) +- **Previous release:** [v0.20.1-beta](release-notes-0.20.1.md) +- **Next release:** [v0.21.0-beta](release-notes-0.21.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Bump minimum LND version to v0.15.1-beta to avoid failed swaps due to the + chain backend not properly supporting taproot (fixed in: + https://github.com/lightningnetwork/lnd/pull/6798). + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Evan Kaloudis +- sputn1ck diff --git a/docs/release-notes/release-notes-0.21.0.md b/docs/release-notes/release-notes-0.21.0.md new file mode 100644 index 00000000..e10f5121 --- /dev/null +++ b/docs/release-notes/release-notes-0.21.0.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2022-12-16 +- **Release page:** + [v0.21.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.21.0-beta) +- **Previous release:** [v0.20.2-beta](release-notes-0.20.2.md) +- **Next release:** [v0.22.0-beta](release-notes-0.22.0.md) + +#### New Features + +* Autoloop now has a new parameter named `destaddr` which if set to a valid + bitcoin address will direct all funds from automatically dispatched loop outs + towards that address. + +#### Breaking Changes + +* Listing loop-in swaps will not display old, nested segwit swap htlc addresses + correctly anymore since support for these htlc types is removed from the code + base. + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- George Tsagkarelis +- sputn1ck diff --git a/docs/release-notes/release-notes-0.22.0.md b/docs/release-notes/release-notes-0.22.0.md new file mode 100644 index 00000000..b72b804a --- /dev/null +++ b/docs/release-notes/release-notes-0.22.0.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2023-03-29 +- **Release page:** + [v0.22.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.22.0-beta) +- **Previous release:** [v0.21.0-beta](release-notes-0.21.0.md) +- **Next release:** [v0.22.1-beta](release-notes-0.22.1.md) + +#### New Features + +* Autoloop now uses a recurring budget. Users can now specify `autobudget` and + `autobudgetrefreshperiod` to specify the amount of the budget and the period + over which it will refresh. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- George Tsagkarelis +- sputn1ck diff --git a/docs/release-notes/release-notes-0.22.1.md b/docs/release-notes/release-notes-0.22.1.md new file mode 100644 index 00000000..84695064 --- /dev/null +++ b/docs/release-notes/release-notes-0.22.1.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2023-04-24 +- **Release page:** + [v0.22.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.22.1-beta) +- **Previous release:** [v0.22.0-beta](release-notes-0.22.0.md) +- **Next release:** [v0.23.0-beta](release-notes-0.23.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Correct the HTLC script version used when resuming very old swaps. + [PR #571](https://github.com/lightninglabs/loop/pull/571) + +#### Maintenance + +* Update the Go Docker image and remove the local `swapserverrpc` module + replacement. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.23.0.md b/docs/release-notes/release-notes-0.23.0.md new file mode 100644 index 00000000..6759b3f8 --- /dev/null +++ b/docs/release-notes/release-notes-0.23.0.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2023-04-25 +- **Release page:** + [v0.23.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.23.0-beta) +- **Previous release:** [v0.22.1-beta](release-notes-0.22.1.md) +- **Next release:** [v0.24.0-beta](release-notes-0.24.0.md) + +#### New Features + +* Set musig2 to be the default swap protocol. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- George Tsagkarelis +- sputn1ck diff --git a/docs/release-notes/release-notes-0.24.0.md b/docs/release-notes/release-notes-0.24.0.md new file mode 100644 index 00000000..fbcd426c --- /dev/null +++ b/docs/release-notes/release-notes-0.24.0.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2023-05-24 +- **Release page:** + [v0.24.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.24.0-beta) +- **Previous release:** [v0.23.0-beta](release-notes-0.23.0.md) +- **Next release:** [v0.24.1-beta](release-notes-0.24.1.md) + +#### New Features + +- Easy Autoloop: a new mode for autoloop which requires the user to only set a + single target balance. Autoloop will start dispatching loop outs + whenever the total channel balance of the node exceeds that target. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- George Tsagkarelis +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.24.1.md b/docs/release-notes/release-notes-0.24.1.md new file mode 100644 index 00000000..0ff93d5a --- /dev/null +++ b/docs/release-notes/release-notes-0.24.1.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2023-05-26 +- **Release page:** + [v0.24.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.24.1-beta) +- **Previous release:** [v0.24.0-beta](release-notes-0.24.0.md) +- **Next release:** [v0.25.0-beta](release-notes-0.25.0.md) + +#### New Features + +* Add REST bindings for the `GetInfo` RPC. + [PR #588](https://github.com/lightninglabs/loop/pull/588) + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Slyghtning diff --git a/docs/release-notes/release-notes-0.25.0.md b/docs/release-notes/release-notes-0.25.0.md new file mode 100644 index 00000000..65d49424 --- /dev/null +++ b/docs/release-notes/release-notes-0.25.0.md @@ -0,0 +1,42 @@ +# Loop Client Release Notes + +- **Release date:** 2023-07-03 +- **Release page:** + [v0.25.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.25.0-beta) +- **Previous release:** [v0.24.1-beta](release-notes-0.24.1.md) +- **Next release:** [v0.25.1-beta](release-notes-0.25.1.md) + +#### New Features + +* Add SQLite and PostgreSQL stores, including migration from an existing bbolt + database. + [PR #585](https://github.com/lightninglabs/loop/pull/585) +* Expose the L402 token identifier through the gRPC interface. + [PR #601](https://github.com/lightninglabs/loop/pull/601) +* Distinguish swaps initiated by Easy Autoloop in their labels. + [PR #591](https://github.com/lightninglabs/loop/pull/591) + +#### Breaking Changes + +#### Bug Fixes + +* Respect the configured fee-PPM limit in Easy Autoloop. + [PR #595](https://github.com/lightninglabs/loop/pull/595) +* Read the Autoloop enabled flag directly from the active liquidity + parameters. + [PR #590](https://github.com/lightninglabs/loop/pull/590) + +#### Maintenance + +* Correct Autoloop documentation and harden its tests. + [PR #593](https://github.com/lightninglabs/loop/pull/593) + [PR #594](https://github.com/lightninglabs/loop/pull/594) + [PR #596](https://github.com/lightninglabs/loop/pull/596) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- George Tsagkarelis +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.25.1.md b/docs/release-notes/release-notes-0.25.1.md new file mode 100644 index 00000000..340ccb56 --- /dev/null +++ b/docs/release-notes/release-notes-0.25.1.md @@ -0,0 +1,25 @@ +# Loop Client Release Notes + +- **Release date:** 2023-07-04 +- **Release page:** + [v0.25.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.25.1-beta) +- **Previous release:** [v0.25.0-beta](release-notes-0.25.0.md) +- **Next release:** [v0.25.2-beta](release-notes-0.25.2.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Stop publishing Windows 386 release artifacts. + [PR #606](https://github.com/lightninglabs/loop/pull/606) +* Bump the release version to v0.25.1-beta. + [PR #607](https://github.com/lightninglabs/loop/pull/607) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath diff --git a/docs/release-notes/release-notes-0.25.2.md b/docs/release-notes/release-notes-0.25.2.md new file mode 100644 index 00000000..dcffee50 --- /dev/null +++ b/docs/release-notes/release-notes-0.25.2.md @@ -0,0 +1,25 @@ +# Loop Client Release Notes + +- **Release date:** 2023-07-04 +- **Release page:** + [v0.25.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.25.2-beta) +- **Previous release:** [v0.25.1-beta](release-notes-0.25.1.md) +- **Next release:** [v0.26.0-beta](release-notes-0.26.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Remove unsupported release platforms, including Windows 386. + [PR #608](https://github.com/lightninglabs/loop/pull/608) +* Bump the release version to v0.25.2-beta. + [PR #609](https://github.com/lightninglabs/loop/pull/609) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath diff --git a/docs/release-notes/release-notes-0.26.0.md b/docs/release-notes/release-notes-0.26.0.md new file mode 100644 index 00000000..422a90d2 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.0.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2023-07-28 +- **Release page:** + [v0.26.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.0-beta) +- **Previous release:** [v0.25.2-beta](release-notes-0.25.2.md) +- **Next release:** [v0.26.1-beta](release-notes-0.26.1.md) + +#### New Features + +This new feature enables loop out and autoloop out operations to sweep htlcs to +addresses generated from an extened public key. A precondition for using the +loop client in this fashion is onboarding a xpub account in the backing lnd +instance, e.g. +`lncli wallet accounts import xpub... my_loop_account --address_type p2tr --master_key_fingerprint 0df50`. +Loop outs can then be instructed to sweep to a new derived address from the +specified account, e.g: +`loop out --amt 10000000 --account my_loop_account --address_type p2tr`. To use +this functionality with autoloop out one has to set the backing lnd account and +address type via liquidity parameters for autoloop, e.g. +`loop --network regtest setparams --autoloop=true --account=my_loop_account --account_addr_type=p2tr...` + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- George Tsagkarelis +- Guillermo Caracuel +- Slyghtning diff --git a/docs/release-notes/release-notes-0.26.1.md b/docs/release-notes/release-notes-0.26.1.md new file mode 100644 index 00000000..5bfdd804 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.1.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2023-08-08 +- **Release page:** + [v0.26.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.1-beta) +- **Previous release:** [v0.26.0-beta](release-notes-0.26.0.md) +- **Next release:** [v0.26.2-beta](release-notes-0.26.2.md) + +#### New Features + +* Record the initiator associated with each swap. + [PR #600](https://github.com/lightninglabs/loop/pull/600) + +#### Breaking Changes + +#### Bug Fixes + +* Repair faulty Loop Out timestamps on startup. + [PR #617](https://github.com/lightninglabs/loop/pull/617) + +#### Maintenance + +* Bump the release version to v0.26.1-beta. + [PR #618](https://github.com/lightninglabs/loop/pull/618) + +#### Contributors (Alphabetical Order) + +- Andras Banki-Horvath +- George Tsagkarelis +- sputn1ck diff --git a/docs/release-notes/release-notes-0.26.2.md b/docs/release-notes/release-notes-0.26.2.md new file mode 100644 index 00000000..23a73169 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.2.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2023-08-09 +- **Release page:** + [v0.26.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.2-beta) +- **Previous release:** [v0.26.1-beta](release-notes-0.26.1.md) +- **Next release:** [v0.26.3-beta](release-notes-0.26.3.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Add a SQL migration for faulty timestamps that may have appeared for users + who were looping out through LiT v0.10.4-alpha. + [PR #619](https://github.com/lightninglabs/loop/pull/619) + +#### Maintenance + +* Bump the release version to v0.26.2-beta. + [PR #620](https://github.com/lightninglabs/loop/pull/620) + +#### Contributors (Alphabetical Order) + +- Andras Banki-Horvath +- sputn1ck diff --git a/docs/release-notes/release-notes-0.26.3.md b/docs/release-notes/release-notes-0.26.3.md new file mode 100644 index 00000000..6a1bd693 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.3.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2023-09-18 +- **Release page:** + [v0.26.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.3-beta) +- **Previous release:** [v0.26.2-beta](release-notes-0.26.2.md) +- **Next release:** [v0.26.4-beta](release-notes-0.26.4.md) + +#### New Features + +* Add the reusable finite state machine module. + [PR #631](https://github.com/lightninglabs/loop/pull/631) + +#### Breaking Changes + +#### Bug Fixes + +* Treat an already-settled Loop In invoice as a successful settlement. + [PR #636](https://github.com/lightninglabs/loop/pull/636) +* Correct SQL time parsing and the faulty-year migration, and skip bbolt + migration when the SQLite database already exists. + [PR #627](https://github.com/lightninglabs/loop/pull/627) + [PR #630](https://github.com/lightninglabs/loop/pull/630) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Liongrass +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.26.4.md b/docs/release-notes/release-notes-0.26.4.md new file mode 100644 index 00000000..cd023afd --- /dev/null +++ b/docs/release-notes/release-notes-0.26.4.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2023-10-04 +- **Release page:** + [v0.26.4-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.4-beta) +- **Previous release:** [v0.26.3-beta](release-notes-0.26.3.md) +- **Next release:** [v0.26.5-beta](release-notes-0.26.5.md) + +#### New Features + +* Add destination-address support to Easy Autoloop. + [PR #644](https://github.com/lightninglabs/loop/pull/644) +* Add Apple Silicon as a release target and provide a sample `loopd` + configuration. + [PR #643](https://github.com/lightninglabs/loop/pull/643) + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Update the compile-time LND dependency to v0.17.0-beta. + [PR #641](https://github.com/lightninglabs/loop/pull/641) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- dstadulis +- Oliver Gugger +- Slyghtning diff --git a/docs/release-notes/release-notes-0.26.5.md b/docs/release-notes/release-notes-0.26.5.md new file mode 100644 index 00000000..07b7aee6 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.5.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2023-10-31 +- **Release page:** + [v0.26.5-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.5-beta) +- **Previous release:** [v0.26.4-beta](release-notes-0.26.4.md) +- **Next release:** [v0.26.6-beta](release-notes-0.26.6.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Correct leap-year handling in database timestamp parsing. + [PR #657](https://github.com/lightninglabs/loop/pull/657) + +#### Maintenance + +* Refactor database initialization and update security-sensitive dependencies. + [PR #649](https://github.com/lightninglabs/loop/pull/649) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.26.6.md b/docs/release-notes/release-notes-0.26.6.md new file mode 100644 index 00000000..2045aa46 --- /dev/null +++ b/docs/release-notes/release-notes-0.26.6.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2023-11-28 +- **Release page:** + [v0.26.6-beta](https://github.com/lightninglabs/loop/releases/tag/v0.26.6-beta) +- **Previous release:** [v0.26.5-beta](release-notes-0.26.5.md) +- **Next release:** [v0.27.0-beta](release-notes-0.27.0.md) + +#### New Features + +* Add RPC and CLI support for abandoning Loop In swaps. + [PR #661](https://github.com/lightninglabs/loop/pull/661) + +#### Breaking Changes + +#### Bug Fixes + +* Finalize Loop Ins that fail because confirmed wallet funds are insufficient + and expose a specific failure reason. + [PR #665](https://github.com/lightninglabs/loop/pull/665) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Slyghtning diff --git a/docs/release-notes/release-notes-0.27.0.md b/docs/release-notes/release-notes-0.27.0.md new file mode 100644 index 00000000..a31dd32b --- /dev/null +++ b/docs/release-notes/release-notes-0.27.0.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2024-01-30 +- **Release page:** + [v0.27.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.27.0-beta) +- **Previous release:** [v0.26.6-beta](release-notes-0.26.6.md) +- **Next release:** [v0.27.1-beta](release-notes-0.27.1.md) + +#### New Features + +* Sweep Batcher: A new sub-system was added that handles all the loopout sweeps. + Successful loopout HTLCs will no longer be swept back to the wallet via + individual transactions but will instead form a single transaction that holds + multiple inputs and pays to a single output. This will significantly reduce + chain fee costs as it's using less block space by directly consolidating all + the htlcs to a single address. Loopouts that pay to non-wallet addresses will + still use individual transactions as their output cannot be mutated. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- bitcoin-lightning +- elbandi +- George Tsagkarelis +- GoodDaisy +- Mohamed Awnallah +- shuoer86 +- sputn1ck diff --git a/docs/release-notes/release-notes-0.27.1.md b/docs/release-notes/release-notes-0.27.1.md new file mode 100644 index 00000000..825ff31d --- /dev/null +++ b/docs/release-notes/release-notes-0.27.1.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2024-02-14 +- **Release page:** + [v0.27.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.27.1-beta) +- **Previous release:** [v0.27.0-beta](release-notes-0.27.0.md) +- **Next release:** [v0.28.0-beta](release-notes-0.28.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +This release adds automatic sweeping of incorrectly deposited amounts to +external loop in addresses. Previously, a mismatch in the contract amount and +the actually deposited amount required external tools to recover the client +funds. With this release the client automatically sweeps the funds back to the +wallet upon contract expiry. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.28.0.md b/docs/release-notes/release-notes-0.28.0.md new file mode 100644 index 00000000..c8f093c4 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.0.md @@ -0,0 +1,35 @@ +# Loop Client Release Notes + +- **Release date:** 2024-03-05 +- **Release page:** + [v0.28.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.0-beta) +- **Previous release:** [v0.27.1-beta](release-notes-0.27.1.md) +- **Next release:** [v0.28.1-beta](release-notes-0.28.1.md) + +#### New Features + +* Add RPC and CLI support for listing Instant Out swaps. + [PR #708](https://github.com/lightninglabs/loop/pull/708) +* Allow Instant Out funds to be sent to a custom destination and improve the + command-line flow. + [PR #709](https://github.com/lightninglabs/loop/pull/709) + [PR #710](https://github.com/lightninglabs/loop/pull/710) + +#### Breaking Changes + +#### Bug Fixes + +* Correct per-sweep on-chain fee accounting and improve displayed server costs. + [PR #694](https://github.com/lightninglabs/loop/pull/694) + +#### Maintenance + +* Increase SQLite durability. + [PR #712](https://github.com/lightninglabs/loop/pull/712) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.28.1.md b/docs/release-notes/release-notes-0.28.1.md new file mode 100644 index 00000000..f386287d --- /dev/null +++ b/docs/release-notes/release-notes-0.28.1.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2024-04-16 +- **Release page:** + [v0.28.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.1-beta) +- **Previous release:** [v0.28.0-beta](release-notes-0.28.0.md) +- **Next release:** [v0.28.2-beta](release-notes-0.28.2.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Route the Loop Out prepayment over the selected outgoing channel set and + populate the correct HTLC in swap updates. + [PR #727](https://github.com/lightninglabs/loop/pull/727) + +#### Maintenance + +* Update SQLite, SQLC, PostgreSQL, and Docker dependencies and cache Docker + builds. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Slyghtning diff --git a/docs/release-notes/release-notes-0.28.2.md b/docs/release-notes/release-notes-0.28.2.md new file mode 100644 index 00000000..93784f22 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.2.md @@ -0,0 +1,36 @@ +# Loop Client Release Notes + +- **Release date:** 2024-05-25 +- **Release page:** + [v0.28.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.2-beta) +- **Previous release:** [v0.28.1-beta](release-notes-0.28.1.md) +- **Next release:** [v0.28.3-beta](release-notes-0.28.3.md) + +#### New Features + +#### Breaking Changes + +In loopd.conf file `maxlsatcost` and `maxlsatfee` were renamed to `maxl402cost` +and `maxl402fee` accordingly. Old versions of the options are still recognized +for backward compatibility, but a deprecation warning is printed. Users are +encouraged to change the options to new names if they have been changed locally. + +The path in looprpc "/v1/lsat/tokens" was renamed to "/v1/l402/tokens" and the +corresponding method was renamed from `GetLsatTokens` to `GetL402Tokens`. New +`loop` binary won't work with old `loopd`, because `loop listauth` is now +calling `GetL402Tokens` method, which does not exist in previous `loopd` binary. +Old `loop` binary works with new `loopd`, since `loopd` provides a wrapper for +`GetLsatTokens` which calls `GetL402Tokens`; the wrapper logs a warning and it +will be removed in a couple of releases. HTTP endpoint "/v1/l402/tokens" is now +an additional binding for API "/v1/lsat/tokens", so it still works. + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- Slyghtning diff --git a/docs/release-notes/release-notes-0.28.3.md b/docs/release-notes/release-notes-0.28.3.md new file mode 100644 index 00000000..dc713e46 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.3.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2024-06-03 +- **Release page:** + [v0.28.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.3-beta) +- **Previous release:** [v0.28.2-beta](release-notes-0.28.2.md) +- **Next release:** [v0.28.4-beta](release-notes-0.28.4.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Migrate incorrectly stored negative Loop Out costs and account for the + prepayment amount correctly. + [PR #764](https://github.com/lightninglabs/loop/pull/764) +* Restore sweep-batcher swaps from the Loop database and preserve their + confirmation targets. + [PR #761](https://github.com/lightninglabs/loop/pull/761) + +#### Maintenance + +* Update the compile-time LND dependency to v0.18.0-beta. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- Slyghtning diff --git a/docs/release-notes/release-notes-0.28.4.md b/docs/release-notes/release-notes-0.28.4.md new file mode 100644 index 00000000..5bc6f81d --- /dev/null +++ b/docs/release-notes/release-notes-0.28.4.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2024-06-04 +- **Release page:** + [v0.28.4-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.4-beta) +- **Previous release:** [v0.28.3-beta](release-notes-0.28.3.md) +- **Next release:** [v0.28.5-beta](release-notes-0.28.5.md) + +#### New Features + +* Make the LND RPC timeout configurable. + [PR #771](https://github.com/lightninglabs/loop/pull/771) + +#### Breaking Changes + +#### Bug Fixes + +* Correct the cost migration for pending swaps. + [PR #771](https://github.com/lightninglabs/loop/pull/771) + +#### Maintenance + +* Factor out a sweep-batcher implementation that does not depend on the Loop + database and update LND to v0.18.0-beta.1. + [PR #766](https://github.com/lightninglabs/loop/pull/766) + [PR #770](https://github.com/lightninglabs/loop/pull/770) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- sputn1ck diff --git a/docs/release-notes/release-notes-0.28.5.md b/docs/release-notes/release-notes-0.28.5.md new file mode 100644 index 00000000..513b551a --- /dev/null +++ b/docs/release-notes/release-notes-0.28.5.md @@ -0,0 +1,24 @@ +# Loop Client Release Notes + +- **Release date:** 2024-06-06 +- **Release page:** + [v0.28.5-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.5-beta) +- **Previous release:** [v0.28.4-beta](release-notes-0.28.4.md) +- **Next release:** [v0.28.6-beta](release-notes-0.28.6.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +* Paginate LND payment lookups while migrating stored Loop Out costs. + [PR #773](https://github.com/lightninglabs/loop/pull/773) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- jinjingroad diff --git a/docs/release-notes/release-notes-0.28.6.md b/docs/release-notes/release-notes-0.28.6.md new file mode 100644 index 00000000..7025a1f6 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.6.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2024-07-11 +- **Release page:** + [v0.28.6-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.6-beta) +- **Previous release:** [v0.28.5-beta](release-notes-0.28.5.md) +- **Next release:** [v0.28.7-beta](release-notes-0.28.7.md) + +#### New Features + +* Add sweep-batcher options for custom MuSig2 signing and disabling fee + bumping. + [PR #783](https://github.com/lightninglabs/loop/pull/783) + [PR #784](https://github.com/lightninglabs/loop/pull/784) +* Track a minimum fee rate for sweeps and log transaction weight. + [PR #785](https://github.com/lightninglabs/loop/pull/785) + +#### Breaking Changes + +#### Bug Fixes + +* Fix sweep-batcher shutdown and database transaction races. + [PR #779](https://github.com/lightninglabs/loop/pull/779) + [PR #780](https://github.com/lightninglabs/loop/pull/780) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- chengehe +- Slyghtning diff --git a/docs/release-notes/release-notes-0.28.7.md b/docs/release-notes/release-notes-0.28.7.md new file mode 100644 index 00000000..da77ab61 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.7.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2024-08-08 +- **Release page:** + [v0.28.7-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.7-beta) +- **Previous release:** [v0.28.6-beta](release-notes-0.28.6.md) +- **Next release:** [v0.28.8-beta](release-notes-0.28.8.md) + +#### New Features + +* Return the last hop when fetching stored swaps. + [PR #806](https://github.com/lightninglabs/loop/pull/806) +* Add greedy sweep-batch selection. + [PR #787](https://github.com/lightninglabs/loop/pull/787) + +#### Breaking Changes + +#### Bug Fixes + +* Correct command path handling, sweep fee estimates, and Instant Out SQL + network configuration. + [PR #803](https://github.com/lightninglabs/loop/pull/803) + [PR #793](https://github.com/lightninglabs/loop/pull/793) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- crystalstall +- longxiangqiao +- Slyghtning +- yingshanghuangqiao diff --git a/docs/release-notes/release-notes-0.28.8.md b/docs/release-notes/release-notes-0.28.8.md new file mode 100644 index 00000000..e1007636 --- /dev/null +++ b/docs/release-notes/release-notes-0.28.8.md @@ -0,0 +1,41 @@ +# Loop Client Release Notes + +- **Release date:** 2024-10-21 +- **Release page:** + [v0.28.8-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.8-beta) +- **Previous release:** [v0.28.7-beta](release-notes-0.28.7.md) +- **Next release:** [v0.28.9-beta](release-notes-0.28.9.md) + +#### New Features + +* Add a persistent notification manager and generalize the server notification + stream. + [PR #826](https://github.com/lightninglabs/loop/pull/826) +* Add mixed sweep batches, configurable publication delays, transaction labels, + and a maximum batch size. + [PR #791](https://github.com/lightninglabs/loop/pull/791) + [PR #801](https://github.com/lightninglabs/loop/pull/801) + [PR #809](https://github.com/lightninglabs/loop/pull/809) + [PR #813](https://github.com/lightninglabs/loop/pull/813) + +#### Breaking Changes + +#### Bug Fixes + +* Prevent sweep-batcher fee rates from decreasing and improve publication error + handling. + [PR #815](https://github.com/lightninglabs/loop/pull/815) + [PR #833](https://github.com/lightninglabs/loop/pull/833) + +#### Maintenance + +* Publish `looprpc` as a separately versioned Go module. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- drawdrop +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.28.9.md b/docs/release-notes/release-notes-0.28.9.md new file mode 100644 index 00000000..e48e7d5c --- /dev/null +++ b/docs/release-notes/release-notes-0.28.9.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2024-10-30 +- **Release page:** + [v0.28.9-beta](https://github.com/lightninglabs/loop/releases/tag/v0.28.9-beta) +- **Previous release:** [v0.28.8-beta](release-notes-0.28.8.md) +- **Next release:** [v0.29.0-beta](release-notes-0.29.0.md) + +#### New Features + +* Add the `FetchL402` RPC and `loop fetchl402` command. + [PR #841](https://github.com/lightninglabs/loop/pull/841) +* Propagate event contexts through the FSM, reservation, and Instant Out + packages. + [PR #839](https://github.com/lightninglabs/loop/pull/839) + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Update the compile-time LND dependency to v0.18.4-beta. + [PR #828](https://github.com/lightninglabs/loop/pull/828) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Oliver Gugger +- sputn1ck diff --git a/docs/release-notes/release-notes-0.29.0.md b/docs/release-notes/release-notes-0.29.0.md new file mode 100644 index 00000000..e2229fec --- /dev/null +++ b/docs/release-notes/release-notes-0.29.0.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2024-12-18 +- **Release page:** + [v0.29.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.29.0-beta) +- **Previous release:** [v0.28.9-beta](release-notes-0.28.9.md) +- **Next release:** [v0.29.1-beta](release-notes-0.29.1.md) + +#### New Features + +Add support for persisting address loop in mode +https://docs.lightning.engineering/lightning-network-tools/loop/static-loop-in-addresses + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- pinglanlu +- Slyghtning +- sputn1ck +- ziggie diff --git a/docs/release-notes/release-notes-0.29.1.md b/docs/release-notes/release-notes-0.29.1.md new file mode 100644 index 00000000..5129fb44 --- /dev/null +++ b/docs/release-notes/release-notes-0.29.1.md @@ -0,0 +1,38 @@ +# Loop Client Release Notes + +- **Release date:** 2025-03-18 +- **Release page:** + [v0.29.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.29.1-beta) +- **Previous release:** [v0.29.0-beta](release-notes-0.29.0.md) +- **Next release:** [v0.30.0-beta](release-notes-0.30.0.md) + +#### New Features + +* Add Taproot Asset Loop Outs, including asset-aware quotes, accounting, and + command-line options. + [PR #872](https://github.com/lightninglabs/loop/pull/872) + [PR #876](https://github.com/lightninglabs/loop/pull/876) +* Allow a specific amount to be withdrawn from static address deposits. + [PR #860](https://github.com/lightninglabs/loop/pull/860) + +#### Breaking Changes + +#### Bug Fixes + +* Fix Loop Out fee estimation when the confirmation target is one. + [PR #899](https://github.com/lightninglabs/loop/pull/899) +* Add notification backoff and pending-L402 handling to avoid busy looping. + [PR #879](https://github.com/lightninglabs/loop/pull/879) + +#### Maintenance + +* Update the compile-time LND dependency to v0.19.0. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- Oliver Gugger +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.3.0.md b/docs/release-notes/release-notes-0.3.0.md new file mode 100644 index 00000000..cda614a1 --- /dev/null +++ b/docs/release-notes/release-notes-0.3.0.md @@ -0,0 +1,39 @@ +# Loop Client Release Notes + +- **Release date:** 2019-11-21 +- **Release page:** + [v0.3.0-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.3.0-alpha) +- **Previous release:** [v0.2.4-alpha](release-notes-0.2.4.md) +- **Next release:** [v0.3.1-alpha](release-notes-0.3.1.md) + +#### New Features + +This minor version release includes: + +- Change the default behavior of Loop Out swaps to use the new Loop Out delay +- New `--fast` flag to execute Loop Out swaps immediately +- Add delay configuration to the Loop Out client API + +The new delay option is related to an on-going effort to minimize the on-chain +footprint of Lightning Loop, which will deliver increased privacy and lower +chain fees. + +If multiple execution delayed Loop Outs are present at the time of Loop Out +on-chain funding, the Loop service will now batch those funding outputs together +into a single transaction, reducing the number of change outputs and required +inputs used for each swap. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Corey Phillips +- Johan T. Halseth +- Joost Jager +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.3.1.md b/docs/release-notes/release-notes-0.3.1.md new file mode 100644 index 00000000..275f89c3 --- /dev/null +++ b/docs/release-notes/release-notes-0.3.1.md @@ -0,0 +1,37 @@ +# Loop Client Release Notes + +- **Release date:** 2020-01-15 +- **Release page:** + [v0.3.1-alpha](https://github.com/lightninglabs/loop/releases/tag/v0.3.1-alpha) +- **Previous release:** [v0.3.0-alpha](release-notes-0.3.0.md) +- **Next release:** [v0.4.0-rc1.beta](release-notes-0.4.0-rc1.md) + +#### New Features + +In this patch version release: + +- `loop out` now supports an optional `--max_swap_routing_fee` flag to specify a + max routing fee +- `loop quote` now supports an optional `--fast` flag to specify a quote for an + immediate swap + +This version is recommended for all users to improve the fidelity of fee +estimation. In a previous update the swap quote may have returned a higher +estimated fee than would have been ultimately paid. + +Max swap routing fee is also recommended for users who want to adjust the fee +ceiling of their Loop Out Lightning Network fees. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- FreeThinker +- Johan T. Halseth +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.30.0.md b/docs/release-notes/release-notes-0.30.0.md new file mode 100644 index 00000000..446b9627 --- /dev/null +++ b/docs/release-notes/release-notes-0.30.0.md @@ -0,0 +1,35 @@ +# Loop Client Release Notes + +- **Release date:** 2025-03-27 +- **Release page:** + [v0.30.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.30.0-beta) +- **Previous release:** [v0.29.1-beta](release-notes-0.29.1.md) +- **Next release:** [v0.31.0-beta](release-notes-0.31.0.md) + +#### New Features + +* Add Taproot Asset support to Easy Autoloop Loop Outs. + [PR #886](https://github.com/lightninglabs/loop/pull/886) + +#### Breaking Changes + +#### Bug Fixes + +* Make static address deposit recovery non-concurrent and correct deposit FSM + shutdown and locking behavior. + [PR #908](https://github.com/lightninglabs/loop/pull/908) +* Fix data races across Loop Out payment, reservations, static addresses, + logging, and sweep batching. + [PR #890](https://github.com/lightninglabs/loop/pull/890) + [PR #902](https://github.com/lightninglabs/loop/pull/902) + +#### Maintenance + +* Run unit tests with the Go race detector. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.31.0.md b/docs/release-notes/release-notes-0.31.0.md new file mode 100644 index 00000000..ff847787 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.0.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2025-04-25 +- **Release page:** + [v0.31.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.0-beta) +- **Previous release:** [v0.30.0-beta](release-notes-0.30.0.md) +- **Next release:** [v0.31.1-beta](release-notes-0.31.1.md) + +#### New Features + +* [Enhance](https://github.com/lightninglabs/loop/pull/912) the `loop listswaps` + command by improving the ability to filter the response. Use + `--start_timestamp_ns` to return only swaps after that timestamp. Use + `--max_swaps` to limit total swap outputs. Paging is enabled using the + `next_start_time` field in the response. + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- Guillermo Caracuel +- Sam Korn +- Slyghtning diff --git a/docs/release-notes/release-notes-0.31.1.md b/docs/release-notes/release-notes-0.31.1.md new file mode 100644 index 00000000..b1a9313f --- /dev/null +++ b/docs/release-notes/release-notes-0.31.1.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2025-05-05 +- **Release page:** + [v0.31.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.1-beta) +- **Previous release:** [v0.31.0-beta](release-notes-0.31.0.md) +- **Next release:** [v0.31.2-beta](release-notes-0.31.2.md) + +#### New Features + +#### Breaking Changes + +* Autoloop now defaults to a slow publication deadline (30 minutes after + initiation) to reduce fees. To revert to the previous behavior, users can run + `loop setparams --fast`, which causes swap HTLCs to be published immediately + after initiation. + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev +- ffranr +- Oliver Gugger +- Slyghtning +- sputn1ck diff --git a/docs/release-notes/release-notes-0.31.2.md b/docs/release-notes/release-notes-0.31.2.md new file mode 100644 index 00000000..bb0e37b4 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.2.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2025-06-18 +- **Release page:** + [v0.31.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.2-beta) +- **Previous release:** [v0.31.1-beta](release-notes-0.31.1.md) +- **Next release:** [v0.31.3-beta](release-notes-0.31.3.md) + +#### New Features + +#### Breaking Changes + +* The content of the `commit_hash` field of the `GetInfo` response has been + updated so that it contains the Git commit hash the Loop binary build was + based on. If the build had uncommited changes, this field will contain the + most recent commit hash, suffixed by "-dirty". +* The `Commit` part of the `--version` command output has been updated to + contain the most recent git commit tag. + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- Oliver Gugger +- Slyghtning +- Viktor Tigerström diff --git a/docs/release-notes/release-notes-0.31.3.md b/docs/release-notes/release-notes-0.31.3.md new file mode 100644 index 00000000..4b54d0f1 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.3.md @@ -0,0 +1,61 @@ +# Loop Client Release Notes + +- **Release date:** 2025-09-25 +- **Release page:** + [v0.31.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.3-beta) +- **Previous release:** [v0.31.2-beta](release-notes-0.31.2.md) +- **Next release:** [v0.31.4-beta](release-notes-0.31.4.md) + +#### New Features + +* Allow static address Loop Ins to swap an amount smaller than the selected + deposits and expose arbitrary static swap amounts through the server RPC. + [PR #887](https://github.com/lightninglabs/loop/pull/887) + [PR #951](https://github.com/lightninglabs/loop/pull/951) +* Add deposit indices, outpoint-based deposit lookups, and CLI output for + deposits and their swap hashes. + [PR #959](https://github.com/lightninglabs/loop/pull/959) + [PR #990](https://github.com/lightninglabs/loop/pull/990) + [PR #991](https://github.com/lightninglabs/loop/pull/991) +* Add sweep-batcher change outputs. + [PR #976](https://github.com/lightninglabs/loop/pull/976) + +#### Breaking Changes + +#### Bug Fixes + +* Harden static address deposit recovery, withdrawal handling, block polling, + expiry tracking, and deposit availability filtering. + [PR #954](https://github.com/lightninglabs/loop/pull/954) + [PR #956](https://github.com/lightninglabs/loop/pull/956) + [PR #995](https://github.com/lightninglabs/loop/pull/995) + [PR #996](https://github.com/lightninglabs/loop/pull/996) + [PR #1003](https://github.com/lightninglabs/loop/pull/1003) + [PR #1004](https://github.com/lightninglabs/loop/pull/1004) +* Avoid creating a Loop In timeout transaction after its invoice has already + been paid. + [PR #998](https://github.com/lightninglabs/loop/pull/998) +* Fix sweep-batcher reorg detection and presigned-sweep handling. + [PR #952](https://github.com/lightninglabs/loop/pull/952) + [PR #975](https://github.com/lightninglabs/loop/pull/975) +* Avoid a startup panic when the Taproot Asset client is unavailable. + [PR #970](https://github.com/lightninglabs/loop/pull/970) + +#### Maintenance + +* Update LND through v0.19.3, Taproot Assets through v0.6.1, Go, and lint + tooling. + [PR #985](https://github.com/lightninglabs/loop/pull/985) + [PR #989](https://github.com/lightninglabs/loop/pull/989) + [PR #993](https://github.com/lightninglabs/loop/pull/993) + [PR #1001](https://github.com/lightninglabs/loop/pull/1001) +* Build static release binaries and fix release-version matching. + [PR #965](https://github.com/lightninglabs/loop/pull/965) + [PR #978](https://github.com/lightninglabs/loop/pull/978) + +#### Contributors (Alphabetical Order) + +- Andras Banki-Horvath +- Boris Nagaev +- George Tsagkarelis +- Slyghtning diff --git a/docs/release-notes/release-notes-0.31.4.md b/docs/release-notes/release-notes-0.31.4.md new file mode 100644 index 00000000..142786b1 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.4.md @@ -0,0 +1,38 @@ +# Loop Client Release Notes + +- **Release date:** 2025-10-16 +- **Release page:** + [v0.31.4-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.4-beta) +- **Previous release:** [v0.31.3-beta](release-notes-0.31.3.md) +- **Next release:** [v0.31.5-beta](release-notes-0.31.5.md) + +#### New Features + +* Add fast publication support to traditional and static Loop Ins. + [PR #1009](https://github.com/lightninglabs/loop/pull/1009) +* Add reproducible host and Docker release builds. + [PR #1007](https://github.com/lightninglabs/loop/pull/1007) + +#### Breaking Changes + +* Migrate the command-line client to `urfave/cli/v3`. + [PR #1013](https://github.com/lightninglabs/loop/pull/1013) + +#### Bug Fixes + +* Correct sweep-batcher change-output fee accounting and publish fee-rate + calculation. + [PR #1020](https://github.com/lightninglabs/loop/pull/1020) +* Harden static deposit recovery, withdrawal, and swap-state handling. + +#### Maintenance + +* Presign sweep transactions in parallel. + [PR #1022](https://github.com/lightninglabs/loop/pull/1022) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- George Tsagkarelis +- Slyghtning diff --git a/docs/release-notes/release-notes-0.31.5-lnd0.20.md b/docs/release-notes/release-notes-0.31.5-lnd0.20.md new file mode 100644 index 00000000..b41906a7 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.5-lnd0.20.md @@ -0,0 +1,26 @@ +# Loop Client Release Notes + +- **Release date:** 2025-10-31 +- **Release page:** + [v0.31.5-beta-lnd0.20](https://github.com/lightninglabs/loop/releases/tag/v0.31.5-beta-lnd0.20) +- **Previous release:** [v0.31.5-beta](release-notes-0.31.5.md) +- **Next release:** [v0.31.6-beta](release-notes-0.31.6.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Restore the LND v0.20 release dependency set, including the corresponding + `lndclient` and Taproot Assets versions. +* Restore CLI documentation generation and related release tooling that had + been removed by the dependency downgrade. +* Correct the `looprpc` and `swapserverrpc` module versions used by this + compatibility release. + +#### Contributors (Alphabetical Order) + +- sputn1ck diff --git a/docs/release-notes/release-notes-0.31.5.md b/docs/release-notes/release-notes-0.31.5.md new file mode 100644 index 00000000..cea88b6f --- /dev/null +++ b/docs/release-notes/release-notes-0.31.5.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2025-10-31 +- **Release page:** + [v0.31.5-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.5-beta) +- **Previous release:** [v0.31.4-beta](release-notes-0.31.4.md) +- **Next release:** + [v0.31.5-beta-lnd0.20](release-notes-0.31.5-lnd0.20.md) + +#### New Features + +* Add notification versioning and a manager for resuming swaps. + [PR #1029](https://github.com/lightninglabs/loop/pull/1029) +* Add Instant Out and static address RPCs to `client.yaml`. + [PR #1026](https://github.com/lightninglabs/loop/pull/1026) + +#### Breaking Changes + +* Standardize static address amount flags on `--amt`. + [PR #1023](https://github.com/lightninglabs/loop/pull/1023) + +#### Bug Fixes + +* Avoid an Instant Out startup crash when experimental features are disabled. + [PR #1030](https://github.com/lightninglabs/loop/pull/1030) + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Boris Nagaev diff --git a/docs/release-notes/release-notes-0.31.6.md b/docs/release-notes/release-notes-0.31.6.md new file mode 100644 index 00000000..1324a083 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.6.md @@ -0,0 +1,42 @@ +# Loop Client Release Notes + +- **Release date:** 2025-11-26 +- **Release page:** + [v0.31.6-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.6-beta) +- **Previous release:** + [v0.31.5-beta-lnd0.20](release-notes-0.31.5-lnd0.20.md) +- **Next release:** [v0.31.7-beta](release-notes-0.31.7.md) + +#### New Features + +* Add Easy Autoloop peer exclusion and an option to include all peers. + [PR #1016](https://github.com/lightninglabs/loop/pull/1016) +* Add the `loop stop` command. + [PR #1048](https://github.com/lightninglabs/loop/pull/1048) + +#### Breaking Changes + +#### Bug Fixes + +* Wait for the chain notifier and a positive block height during startup. + [PR #1046](https://github.com/lightninglabs/loop/pull/1046) +* Correct a sweep-batcher race while re-adding sweeps during shutdown. + [PR #1042](https://github.com/lightninglabs/loop/pull/1042) + +#### Maintenance + +* Update LND to v0.20.0-beta and Taproot Assets to v0.7.0. + [PR #1045](https://github.com/lightninglabs/loop/pull/1045) +* Update `lndclient`, Go, and security-sensitive dependencies. + [PR #1034](https://github.com/lightninglabs/loop/pull/1034) + [PR #1041](https://github.com/lightninglabs/loop/pull/1041) + [PR #1047](https://github.com/lightninglabs/loop/pull/1047) + [PR #1049](https://github.com/lightninglabs/loop/pull/1049) +* Add Lightning Terminal integration tests to CI. + [PR #1050](https://github.com/lightninglabs/loop/pull/1050) + +#### Contributors (Alphabetical Order) + +- Boris Nagaev +- ffranr +- Slyghtning diff --git a/docs/release-notes/release-notes-0.31.7.md b/docs/release-notes/release-notes-0.31.7.md new file mode 100644 index 00000000..57064af3 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.7.md @@ -0,0 +1,22 @@ +# Loop Client Release Notes + +- **Release date:** 2025-11-26 +- **Release page:** + [v0.31.7-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.7-beta) +- **Previous release:** [v0.31.6-beta](release-notes-0.31.6.md) +- **Next release:** [v0.31.8-beta](release-notes-0.31.8.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Bump the release version to v0.31.7-beta. + [PR #1051](https://github.com/lightninglabs/loop/pull/1051) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth diff --git a/docs/release-notes/release-notes-0.31.8.md b/docs/release-notes/release-notes-0.31.8.md new file mode 100644 index 00000000..a4b432c6 --- /dev/null +++ b/docs/release-notes/release-notes-0.31.8.md @@ -0,0 +1,38 @@ +# Loop Client Release Notes + +- **Release date:** 2026-02-03 +- **Release page:** + [v0.31.8-beta](https://github.com/lightninglabs/loop/releases/tag/v0.31.8-beta) +- **Previous release:** [v0.31.7-beta](release-notes-0.31.7.md) +- **Next release:** [v0.32.0-beta](release-notes-0.32.0.md) + +#### New Features + +* Add the `SweepHtlc` RPC and `loop out sweephtlc` command for manually + sweeping a Loop Out HTLC. + [PR #1066](https://github.com/lightninglabs/loop/pull/1066) +* Add PSBT-based static address withdrawals. + [PR #1043](https://github.com/lightninglabs/loop/pull/1043) + +#### Breaking Changes + +* Deprecate the previous server-driven static withdrawal RPC in favor of the + PSBT-based endpoint. + [PR #1043](https://github.com/lightninglabs/loop/pull/1043) + +#### Bug Fixes + +* Re-register static address HTLC notifications after confirmation errors. +* Persist confirmed sweep batches atomically. + [PR #1044](https://github.com/lightninglabs/loop/pull/1044) + +#### Maintenance + +* Replace deprecated gRPC dial helpers and remove unused client code. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- Slyghtning +- Viktor Torstensson diff --git a/docs/release-notes/release-notes-0.32.0.md b/docs/release-notes/release-notes-0.32.0.md new file mode 100644 index 00000000..80f5cf78 --- /dev/null +++ b/docs/release-notes/release-notes-0.32.0.md @@ -0,0 +1,42 @@ +# Loop Client Release Notes + +- **Release date:** 2026-03-02 +- **Release page:** + [v0.32.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.32.0-beta) +- **Previous release:** [v0.31.8-beta](release-notes-0.31.8.md) +- **Next release:** [v0.32.1-beta](release-notes-0.32.1.md) + +#### New Features + +Static Address Open Channel — This release introduces the ability to open +Lightning channels directly from static address deposits. Instead of performing +a loop-in to get funds into your Lightning node and then opening a channel as +separate steps, you can now combine both into a single operation using the new +loop openchannel command. + + - Consolidate deposits into channels: If you have one or more static address + deposits sitting on-chain, you can open a channel to any peer using those + funds directly, skipping the loop-in swap and its associated fees. + - Flexible funding: Use the --utxo flag to select specific deposit outpoints, + or use --fundmax to sweep all selected UTXOs into a single channel. + - Full channel configuration: The command supports the same options as lncli + openchannel — channel type (tweakless, anchors, taproot), private channels, + push amounts, fee rates, zero-conf, and more. + +Examples: + +```shell +loop openchannel --node_key --local_amt 1000000 +loop openchannel --node_key --utxo txid:0 --fundmax +``` + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Slyghtning diff --git a/docs/release-notes/release-notes-0.32.1.md b/docs/release-notes/release-notes-0.32.1.md new file mode 100644 index 00000000..4fcaee65 --- /dev/null +++ b/docs/release-notes/release-notes-0.32.1.md @@ -0,0 +1,23 @@ +# Loop Client Release Notes + +- **Release date:** 2026-03-02 +- **Release page:** + [v0.32.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.32.1-beta) +- **Previous release:** [v0.32.0-beta](release-notes-0.32.0.md) +- **Next release:** [v0.33.0-beta](release-notes-0.33.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +* Update release builds to Go 1.26.0. + [PR #1083](https://github.com/lightninglabs/loop/pull/1083) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev diff --git a/docs/release-notes/release-notes-0.33.0.md b/docs/release-notes/release-notes-0.33.0.md new file mode 100644 index 00000000..08ce105a --- /dev/null +++ b/docs/release-notes/release-notes-0.33.0.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2026-04-08 +- **Release page:** + [v0.33.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.33.0-beta) +- **Previous release:** [v0.32.1-beta](release-notes-0.32.1.md) +- **Next release:** [v0.33.1-beta](release-notes-0.33.1.md) + +#### New Features + +* Add maximum swap-fee controls to static address Loop Ins. + [PR #1114](https://github.com/lightninglabs/loop/pull/1114) + +#### Breaking Changes + +#### Bug Fixes + +* Harden static address startup, withdrawal notification registration, Loop In + recovery, HTLC timeout handling, and sweep output validation. + [PR #1078](https://github.com/lightninglabs/loop/pull/1078) +* Ensure cleanup operations use live contexts and fix a Loop Out test goroutine + leak. + +#### Maintenance + +* Regenerate the command-line reference and update build dependencies. + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- Slyghtning diff --git a/docs/release-notes/release-notes-0.33.1.md b/docs/release-notes/release-notes-0.33.1.md new file mode 100644 index 00000000..596236cc --- /dev/null +++ b/docs/release-notes/release-notes-0.33.1.md @@ -0,0 +1,35 @@ +# Loop Client Release Notes + +- **Release date:** 2026-05-26 +- **Release page:** + [v0.33.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.33.1-beta) +- **Previous release:** [v0.33.0-beta](release-notes-0.33.0.md) +- **Next release:** [v0.33.2-beta](release-notes-0.33.2.md) + +#### New Features + +* Add Static Address Loop Ins to Autoloop planning and execution. + [PR #1119](https://github.com/lightninglabs/loop/pull/1119) + +#### Breaking Changes + +#### Bug Fixes + +* Validate static address server parameters and prevent panics when displaying + malformed or missing Instant Out reservation identifiers. + [PR #1137](https://github.com/lightninglabs/loop/pull/1137) +* Harden sweep-batcher shutdown and route-hint preservation. + +#### Maintenance + +* Add record-and-replay integration tests for CLI sessions. + [PR #1072](https://github.com/lightninglabs/loop/pull/1072) +* Add the project security policy. + +#### Contributors (Alphabetical Order) + +- 0xfandom +- Alex Bosworth +- Boris Nagaev +- Olexandr88 +- Slyghtning diff --git a/docs/release-notes/release-notes-0.33.2.md b/docs/release-notes/release-notes-0.33.2.md new file mode 100644 index 00000000..84e8ff44 --- /dev/null +++ b/docs/release-notes/release-notes-0.33.2.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2026-06-08 +- **Release page:** + [v0.33.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.33.2-beta) +- **Previous release:** [v0.33.1-beta](release-notes-0.33.1.md) +- **Next release:** [v0.33.3-beta](release-notes-0.33.3.md) + +#### New Features + +* Expose static address swap timing and cost details through the RPC and CLI. + [PR #1150](https://github.com/lightninglabs/loop/pull/1150) +* Handle server notifications that a static address HTLC has confirmed. + [PR #1120](https://github.com/lightninglabs/loop/pull/1120) + +#### Breaking Changes + +#### Bug Fixes + +* Reject malformed server keys and MuSig2 signing data. + [PR #1148](https://github.com/lightninglabs/loop/pull/1148) +* Recover confirmed HTLCs through the direct sweep path and reuse their stored + destinations. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev +- Slyghtning diff --git a/docs/release-notes/release-notes-0.33.3.md b/docs/release-notes/release-notes-0.33.3.md new file mode 100644 index 00000000..53abbffa --- /dev/null +++ b/docs/release-notes/release-notes-0.33.3.md @@ -0,0 +1,25 @@ +# Loop Client Release Notes + +- **Release date:** 2026-06-21 +- **Release page:** + [v0.33.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.33.3-beta) +- **Previous release:** [v0.33.2-beta](release-notes-0.33.2.md) +- **Next release:** [v0.34.0-beta](release-notes-0.34.0.md) + +#### New Features + +#### Breaking Changes + +* Raise the minimum supported LND version to v0.18.4-beta. + +#### Bug Fixes + +#### Maintenance + +* Update the compile-time LND dependency to v0.21. + [PR #1153](https://github.com/lightninglabs/loop/pull/1153) + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Boris Nagaev diff --git a/docs/release-notes/release-notes-0.34.0.md b/docs/release-notes/release-notes-0.34.0.md new file mode 100644 index 00000000..19fa587f --- /dev/null +++ b/docs/release-notes/release-notes-0.34.0.md @@ -0,0 +1,82 @@ +# Loop Client Release Notes + +- **Release date:** 2026-07-23 +- **Release page:** + [v0.34.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.34.0-beta) +- **Previous release:** [v0.33.3-beta](release-notes-0.33.3.md) +- **Next release:** [Next release](release-notes-next.md) + +#### New Features + +* Static address deposits are now tracked and shown as soon as they appear in + the wallet, including while they are still in the mempool. Static loop-ins can + select low-confirmation deposits, with a CLI warning that payment may wait for + more confirmations under the server's confirmation-risk policy. Withdrawals + and channel opens continue to require confirmed deposits. The selected risk + decision and payment deadline are persisted across restarts. + [PR #1141](https://github.com/lightninglabs/loop/pull/1141) +* `loop openchannel` now supports LND's production `TAPROOT` commitment type + while retaining explicit support for the legacy `SIMPLE_TAPROOT` type. + [PR #1156](https://github.com/lightninglabs/loop/pull/1156) + +#### Breaking Changes + +* `loop openchannel --channel_type=taproot` now requests LND's production + `TAPROOT` commitment type, which was introduced in LND v0.21. Users who want + the previous legacy `SIMPLE_TAPROOT` behavior must now specify + `--channel_type=simple-taproot`. + [PR #1156](https://github.com/lightninglabs/loop/pull/1156) + +#### Bug Fixes + +* Static address deposit tracking now reconciles against the wallet, preserves + deposit identity across RBF replacements, hides replaced or unavailable + deposits from normal listings, verifies inputs before HTLC signing, and + rejects duplicate outpoints. + [PR #1141](https://github.com/lightninglabs/loop/pull/1141) + [PR #1161](https://github.com/lightninglabs/loop/pull/1161) +* Static loop-in lifecycle handling has been hardened across initialization + failures, invoice cancellation, HTLC timeouts, shutdown, and restart. Failed + swaps remain visible, deposits stay locked while an HTLC timeout path is + active, resumable states are preserved, and failed deposit transitions are + retried instead of advancing prematurely. + [PR #1154](https://github.com/lightninglabs/loop/pull/1154) + [PR #1161](https://github.com/lightninglabs/loop/pull/1161) + [PR #1141](https://github.com/lightninglabs/loop/pull/1141) +* Slow notification subscribers no longer block the notification manager. + Required notifications are delivered through ordered per-subscriber queues, + while optional reservation notifications remain best-effort. + [PR #1154](https://github.com/lightninglabs/loop/pull/1154) +* Standard loop-in abandonment and timeout handling now classifies + already-settled invoice errors correctly, reports unexpected invoice + cancellation failures, and no longer appends a misleading `` value to + abandonment errors. + [PR #1177](https://github.com/lightninglabs/loop/pull/1177) + [commit 8716a517](https://github.com/lightninglabs/loop/commit/8716a517224e1aaf53d9d27d9f7358e9de980fc4) +* The default `tapd` admin macaroon path now follows the configured Bitcoin + network while continuing to honor explicit path overrides. + [commit 37882541](https://github.com/lightninglabs/loop/commit/378825414b4d5df04ff9afbe725cf048c63df9e9) + +#### Maintenance + +* The Loop Out cost migration now omits payment-hop data that it does not use, + reducing LND response size and query cost for nodes with large payment + histories. Deprecation checks were also enabled in CI. + [PR #1156](https://github.com/lightninglabs/loop/pull/1156) +* Dependencies were updated to address security alerts, PostgreSQL error + handling was migrated to `pgx` v5, and applicable module Go versions were + raised to Go 1.25.12. + [PR #1175](https://github.com/lightninglabs/loop/pull/1175) +* Static loop-in fee validation now logs the HTLC weight, fee rates, computed + fees, and configured caps to make fee-guard failures easier to diagnose. + [PR #1158](https://github.com/lightninglabs/loop/pull/1158) +* FSM diagram generation is now deterministic. Static address deposit and + loop-in diagrams are generated and checked in CI to prevent stale diagrams. + [PR #1179](https://github.com/lightninglabs/loop/pull/1179) + +#### Contributors (Alphabetical Order) + +- Boris Nagaev +- Chanda Chewe +- Gustavo Stingelin +- Slyghtning diff --git a/docs/release-notes/release-notes-0.4.0-rc1.md b/docs/release-notes/release-notes-0.4.0-rc1.md new file mode 100644 index 00000000..5230fd92 --- /dev/null +++ b/docs/release-notes/release-notes-0.4.0-rc1.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2020-01-24 +- **Release page:** + [v0.4.0-rc1.beta](https://github.com/lightninglabs/loop/releases/tag/v0.4.0-rc1.beta) +- **Previous release:** [v0.3.1-alpha](release-notes-0.3.1.md) +- **Next release:** [v0.4.0-beta](release-notes-0.4.0.md) + +#### New Features + +#### Breaking Changes + +This is a release candidate for the minor version update to v0.4.0 + +In this minor version release candidate: + +- Default ports are switched to authenticated port +- Requests now require payment for an auth token + +#### Bug Fixes + +- Loop In timeouts are fixed for overfunded cases + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Joost Jager +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.4.0.md b/docs/release-notes/release-notes-0.4.0.md new file mode 100644 index 00000000..261fdb0c --- /dev/null +++ b/docs/release-notes/release-notes-0.4.0.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2020-02-04 +- **Release page:** + [v0.4.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.4.0-beta) +- **Previous release:** [v0.4.0-rc1.beta](release-notes-0.4.0-rc1.md) +- **Next release:** [v0.4.1-beta](release-notes-0.4.1.md) + +#### New Features + +- REST endpoints are now available for monitoring the client daemon + +Upgrading is recommended for all users + +#### Breaking Changes + +In this update: + +- Default ports are switched to the authenticated port +- Swap requests will now require payment for an auth token + +#### Bug Fixes + +- Loop In refund timeouts are now fixed for overfunded cases + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.4.1.md b/docs/release-notes/release-notes-0.4.1.md new file mode 100644 index 00000000..1621f215 --- /dev/null +++ b/docs/release-notes/release-notes-0.4.1.md @@ -0,0 +1,31 @@ +# Loop Client Release Notes + +- **Release date:** 2020-02-11 +- **Release page:** + [v0.4.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.4.1-beta) +- **Previous release:** [v0.4.0-beta](release-notes-0.4.0.md) +- **Next release:** [v0.5.0-beta](release-notes-0.5.0.md) + +#### New Features + +In this patch release: + +- REST clients can now + [adjust their CORS origin headers](https://github.com/lightninglabs/loop/pull/148) + +#### Breaking Changes + +#### Bug Fixes + +- [An error was fixed](https://github.com/lightninglabs/loop/pull/149) when + getting price quote with delayed conf target but fast execution + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Joost Jager +- Olaoluwa Osuntokun +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.5.0.md b/docs/release-notes/release-notes-0.5.0.md new file mode 100644 index 00000000..896228e9 --- /dev/null +++ b/docs/release-notes/release-notes-0.5.0.md @@ -0,0 +1,28 @@ +# Loop Client Release Notes + +- **Release date:** 2020-03-05 +- **Release page:** + [v0.5.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.5.0-beta) +- **Previous release:** [v0.4.1-beta](release-notes-0.4.1.md) +- **Next release:** [v0.5.1-beta](release-notes-0.5.1.md) + +#### New Features + +In this minor release: + +- Add support for specifying the last hop of a Loop In + +#### Breaking Changes + +- Non-working REST streaming status endpoint is removed + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Joost Jager +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.5.1.md b/docs/release-notes/release-notes-0.5.1.md new file mode 100644 index 00000000..aefee8d2 --- /dev/null +++ b/docs/release-notes/release-notes-0.5.1.md @@ -0,0 +1,27 @@ +# Loop Client Release Notes + +- **Release date:** 2020-03-15 +- **Release page:** + [v0.5.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.5.1-beta) +- **Previous release:** [v0.5.0-beta](release-notes-0.5.0.md) +- **Next release:** [v0.6.0-beta](release-notes-0.6.0.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +In this patch release: + +- Allow calling for a Loop In quote when there are insufficient funds in the + wallet +- Increase the level of the sanity check on miner fee maximum to account for + high fee variance + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.6.0.md b/docs/release-notes/release-notes-0.6.0.md new file mode 100644 index 00000000..d00ebc9e --- /dev/null +++ b/docs/release-notes/release-notes-0.6.0.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2020-04-30 +- **Release page:** + [v0.6.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.0-beta) +- **Previous release:** [v0.5.1-beta](release-notes-0.5.1.md) +- **Next release:** [v0.6.1-beta](release-notes-0.6.1.md) + +#### New Features + +- Loop In configuration target can now be specified to reduce on-chain fee costs +- Loop Out now uses LND 0.10.0 upgraded payment APIs to allow for improved + payment success + +#### Breaking Changes + +This release is designed to support LND 0.10.0 - please make sure you have +updated your node + +In this minor release: + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Joost Jager +- Oliver Gugger +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.6.1.md b/docs/release-notes/release-notes-0.6.1.md new file mode 100644 index 00000000..f7716b8b --- /dev/null +++ b/docs/release-notes/release-notes-0.6.1.md @@ -0,0 +1,34 @@ +# Loop Client Release Notes + +- **Release date:** 2020-05-12 +- **Release page:** + [v0.6.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.1-beta) +- **Previous release:** [v0.6.0-beta](release-notes-0.6.0.md) +- **Next release:** [v0.6.2-beta](release-notes-0.6.2.md) + +#### New Features + +* Add native SegWit P2WSH addresses for Loop In HTLCs while retaining support + for externally funded nested SegWit addresses. + [PR #184](https://github.com/lightninglabs/loop/pull/184) + +#### Breaking Changes + +#### Bug Fixes + +* Return the correct HTLC address in the Loop In swap response. + [PR #198](https://github.com/lightninglabs/loop/pull/198) +* Enforce the configured maximum number of payment parts for multi-channel + Loop Outs. + [PR #196](https://github.com/lightninglabs/loop/pull/196) + +#### Maintenance + +* Configure CI to clone the full repository history. + [PR #192](https://github.com/lightninglabs/loop/pull/192) + +#### Contributors (Alphabetical Order) + +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Joost Jager diff --git a/docs/release-notes/release-notes-0.6.2.md b/docs/release-notes/release-notes-0.6.2.md new file mode 100644 index 00000000..12c9e390 --- /dev/null +++ b/docs/release-notes/release-notes-0.6.2.md @@ -0,0 +1,35 @@ +# Loop Client Release Notes + +- **Release date:** 2020-05-12 +- **Release page:** + [v0.6.2-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.2-beta) +- **Previous release:** [v0.6.1-beta](release-notes-0.6.1.md) +- **Next release:** [v0.6.3-beta](release-notes-0.6.3.md) + +#### New Features + +* Switch Loop In to use native SegWit P2WSH HTLC addresses. Externally funded + Loop Ins can use either nested SegWit or native SegWit addresses. + [PR #184](https://github.com/lightninglabs/loop/pull/184) + +#### Breaking Changes + +#### Bug Fixes + +* Fix the maximum number of payment parts for Multi-Loop Out so that the + configured limit takes effect. + [PR #196](https://github.com/lightninglabs/loop/pull/196) + +#### Maintenance + +* Apply follow-up version updates for v0.6.1-beta and v0.6.2-beta. + [PR #199](https://github.com/lightninglabs/loop/pull/199) + [PR #200](https://github.com/lightninglabs/loop/pull/200) + +The user-facing changes highlighted in the published v0.6.2-beta release were +already present in the v0.6.1-beta tag history. The tag-to-tag source delta for +v0.6.2-beta contains the version metadata updates above. + +#### Contributors (Alphabetical Order) + +- Joost Jager diff --git a/docs/release-notes/release-notes-0.6.3.md b/docs/release-notes/release-notes-0.6.3.md new file mode 100644 index 00000000..a89ba49b --- /dev/null +++ b/docs/release-notes/release-notes-0.6.3.md @@ -0,0 +1,29 @@ +# Loop Client Release Notes + +- **Release date:** 2020-06-04 +- **Release page:** + [v0.6.3-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.3-beta) +- **Previous release:** [v0.6.2-beta](release-notes-0.6.2.md) +- **Next release:** [v0.6.4-beta](release-notes-0.6.4.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +In this patch release: + +- Loop Out using the LND v0.10.1 release as the backing LND can now select + multiple channels to Loop Out + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Joost Jager +- Olaoluwa Osuntokun +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.6.4.md b/docs/release-notes/release-notes-0.6.4.md new file mode 100644 index 00000000..a020023c --- /dev/null +++ b/docs/release-notes/release-notes-0.6.4.md @@ -0,0 +1,32 @@ +# Loop Client Release Notes + +- **Release date:** 2020-06-12 +- **Release page:** + [v0.6.4-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.4-beta) +- **Previous release:** [v0.6.3-beta](release-notes-0.6.3.md) +- **Next release:** [v0.6.5-beta](release-notes-0.6.5.md) + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +In this patch release: + +- A loopd error will now terminate with exit code 1 instead of exit code 0 to + assist auto-restarting +- Loop Out will now increase inbound liquidity more quickly in situations where + low chain fees were specified + +If you have time to wait for the funds to return to you on-chain, consider +increasing `--conf_target` from the default of 6 when running the `loop out` +command to achieve fee savings at the cost of slower swap finality. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Oliver Gugger diff --git a/docs/release-notes/release-notes-0.6.5.md b/docs/release-notes/release-notes-0.6.5.md new file mode 100644 index 00000000..2be491dc --- /dev/null +++ b/docs/release-notes/release-notes-0.6.5.md @@ -0,0 +1,29 @@ +# Loop Client Release Notes + +- **Release date:** 2020-07-02 +- **Release page:** + [v0.6.5-beta](https://github.com/lightninglabs/loop/releases/tag/v0.6.5-beta) +- **Previous release:** [v0.6.4-beta](release-notes-0.6.4.md) +- **Next release:** [v0.7.0-beta](release-notes-0.7.0.md) + +#### New Features + +In this patch release: + +* Added support for a human-readable message from the server. The message is + received after initiation of a swap and printed to the log. It is also exposed + as a field on the client RPC interface. + +#### Breaking Changes + +#### Bug Fixes + +* Several bugs fixes and security improvements. + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Carla Kirk-Cohen +- Joost Jager +- Wilmer Paulino diff --git a/docs/release-notes/release-notes-0.7.0.md b/docs/release-notes/release-notes-0.7.0.md new file mode 100644 index 00000000..c1c907ae --- /dev/null +++ b/docs/release-notes/release-notes-0.7.0.md @@ -0,0 +1,29 @@ +# Loop Client Release Notes + +- **Release date:** 2020-07-21 +- **Release page:** + [v0.7.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.7.0-beta) +- **Previous release:** [v0.6.5-beta](release-notes-0.6.5.md) +- **Next release:** [v0.8.0-beta](release-notes-0.8.0.md) + +#### New Features + +In this minor release: + +- Loop Out confirmation targets can now be much longer, resulting in potentially + much cheaper Loop Out on-chain costs +- The Loop server has been upgraded to supply a state subscription service to + supply indications of server-side swap status + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen +- Gabriel Comte +- Joost Jager diff --git a/docs/release-notes/release-notes-0.8.0.md b/docs/release-notes/release-notes-0.8.0.md new file mode 100644 index 00000000..53ab1bbe --- /dev/null +++ b/docs/release-notes/release-notes-0.8.0.md @@ -0,0 +1,30 @@ +# Loop Client Release Notes + +- **Release date:** 2020-08-11 +- **Release page:** + [v0.8.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.8.0-beta) +- **Previous release:** [v0.7.0-beta](release-notes-0.7.0.md) +- **Next release:** [v0.8.1-beta](release-notes-0.8.1.md) + +#### New Features + +In this minor release: + +- The new LND 0.11.0 chain transaction labeling feature is used to record in the + LND db which txs are Loop-related +- You can now specify the number of confirmations to wait before considering the + swap service's on-chain send to you to be confirmed + +#### Breaking Changes + +#### Bug Fixes + +- More details into swap failure causes are surfaced to help better understand + how to fix issues encountered with swap execution + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Carla Kirk-Cohen diff --git a/docs/release-notes/release-notes-0.8.1.md b/docs/release-notes/release-notes-0.8.1.md new file mode 100644 index 00000000..e9984db7 --- /dev/null +++ b/docs/release-notes/release-notes-0.8.1.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +- **Release date:** 2020-08-28 +- **Release page:** + [v0.8.1-beta](https://github.com/lightninglabs/loop/releases/tag/v0.8.1-beta) +- **Previous release:** [v0.8.0-beta](release-notes-0.8.0.md) +- **Next release:** [v0.9.0-beta](release-notes-0.9.0.md) + +#### New Features + +In this patch release: + +- The Loop Out pathfinding timeout is increased to help discover lower fee + routes for larger amounts +- The Loop Out default sweep confirmation target is extended to help lower + on-chain fees for Loop Outs +- Support is added to choose your own directory for Loop's database and log + files + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +- Preparatory support is included for an upcoming update to the on-chain script + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Kartik Shah diff --git a/docs/release-notes/release-notes-0.9.0.md b/docs/release-notes/release-notes-0.9.0.md new file mode 100644 index 00000000..5d67f1aa --- /dev/null +++ b/docs/release-notes/release-notes-0.9.0.md @@ -0,0 +1,40 @@ +# Loop Client Release Notes + +- **Release date:** 2020-09-10 +- **Release page:** + [v0.9.0-beta](https://github.com/lightninglabs/loop/releases/tag/v0.9.0-beta) +- **Previous release:** [v0.8.1-beta](release-notes-0.8.1.md) +- **Next release:** [v0.10.0-beta](release-notes-0.10.0.md) + +#### New Features + +- A liquidity management system is introduced to drive automated Loop actions + based on desired liquidity + - Fully automated Loop actions are not yet included, this release introduces + suggested actions that can be executed using the API, or in the future can + be directed to be executed automatically. +- A rewrite of the on-chain script is introduced that is a bit cheaper on-chain + in the successful case + +#### Breaking Changes + +In this patch release: + +- TLS encryption is introduced and is now required to communicate with the Loop + daemon API +- Support is discontinued in binary releases for Darwin 386 (32 bit Mac) + +#### Bug Fixes + +- The Docker image Go version is increased to Go 1.13 + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Alex Bosworth +- Andras Banki-Horvath +- Carla Kirk-Cohen +- Joost Jager +- Oliver Gugger +- Tom Dickman diff --git a/docs/release-notes/release-notes-next.md b/docs/release-notes/release-notes-next.md new file mode 100644 index 00000000..1f889227 --- /dev/null +++ b/docs/release-notes/release-notes-next.md @@ -0,0 +1,33 @@ +# Loop Client Release Notes + +#### New Features + +#### Breaking Changes + +* Instant Out and reservation RPCs now require the `loop:out` permission. + Operators using custom scoped macaroons must rebake them before calling + `ListReservations`, `InstantOut`, `InstantOutQuote`, or `ListInstantOuts`. + [PR #1194](https://github.com/lightninglabs/loop/pull/1194) + +#### Bug Fixes + +* Loop Out requests now account for channel reserves when checking outbound + capacity, preventing swaps from starting when their off-chain payment cannot + be funded. + +* Improved Instant Out and reservation validation, lifecycle cleanup, recovery + timing, fee limits, and macaroon permissions. + [PR #1194](https://github.com/lightninglabs/loop/pull/1194) + +* Taproot Asset Loop Out handling now validates RFQ timeouts and asset rates, + keeps cached asset-name lookups responsive during slow `tapd` queries, and + closes `tapd` connections cleanly during shutdown and startup failures. + [PR #1189](https://github.com/lightninglabs/loop/pull/1189) + +#### Maintenance + +* Added a CI gate and repository agent guidance requiring every pull request to + include a non-empty entry in the next release notes unless it carries the + `no-changelog` label. + +#### Contributors (Alphabetical Order) diff --git a/docs/release-notes/release-notes-template.md b/docs/release-notes/release-notes-template.md new file mode 100644 index 00000000..f7c15829 --- /dev/null +++ b/docs/release-notes/release-notes-template.md @@ -0,0 +1,19 @@ +# Loop Client Release Notes + +- **Release date:** YYYY-MM-DD +- **Release page:** + [vX.Y.Z-beta](https://github.com/lightninglabs/loop/releases/tag/vX.Y.Z-beta) +- **Previous release:** [vX.Y.W-beta](release-notes-X.Y.W.md) +- **Next release:** None + +#### New Features + +#### Breaking Changes + +#### Bug Fixes + +#### Maintenance + +#### Contributors (Alphabetical Order) + +- Contributor Name diff --git a/docs/release.md b/docs/reproducible_release.md similarity index 100% rename from docs/release.md rename to docs/reproducible_release.md diff --git a/fsm/example_fsm.md b/fsm/example_fsm.md index 882d3404..7de06445 100644 --- a/fsm/example_fsm.md +++ b/fsm/example_fsm.md @@ -2,11 +2,11 @@ stateDiagram-v2 [*] --> InitFSM: OnRequestStuff InitFSM -InitFSM --> StuffSentOut: OnStuffSentOut InitFSM --> StuffFailed: OnError +InitFSM --> StuffSentOut: OnStuffSentOut StuffFailed StuffSentOut -StuffSentOut --> StuffSuccess: OnStuffSuccess StuffSentOut --> StuffFailed: OnError +StuffSentOut --> StuffSuccess: OnStuffSuccess StuffSuccess ``` \ No newline at end of file diff --git a/fsm/stateparser/stateparser.go b/fsm/stateparser/stateparser.go index cafedd7d..edbadb10 100644 --- a/fsm/stateparser/stateparser.go +++ b/fsm/stateparser/stateparser.go @@ -12,8 +12,12 @@ import ( "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/instantout" "github.com/lightninglabs/loop/instantout/reservation" + "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/staticaddr/loopin" ) +var errInvalidFSMSelector = errors.New("missing or unknown fsm selector") + func main() { if err := run(); err != nil { fmt.Println(err) @@ -35,35 +39,43 @@ func run() error { return err } - switch *stateMachine { + states, err := getStates(*stateMachine) + if err != nil { + return err + } + + return writeMermaidFile(fp, states) +} + +func getStates(stateMachine string) (fsm.States, error) { + switch stateMachine { case "example": exampleFSM := &fsm.ExampleFSM{} - err = writeMermaidFile(fp, exampleFSM.GetStates()) - if err != nil { - return err - } + return exampleFSM.GetStates(), nil case "reservation": reservationFSM := &reservation.FSM{} - err = writeMermaidFile(fp, reservationFSM.GetServerInitiatedReservationStates()) - if err != nil { - return err - } + return reservationFSM.GetServerInitiatedReservationStates(), nil case "instantout": - instantout := &instantout.FSM{} - err = writeMermaidFile(fp, instantout.GetV1ReservationStates()) - if err != nil { - return err - } + instantOutFSM := &instantout.FSM{} + return instantOutFSM.GetV1ReservationStates(), nil + + case "staticaddr-deposit": + depositFSM := &deposit.FSM{} + return depositFSM.DepositStatesV0(), nil + + case "staticaddr-loopin": + loopInFSM := &loopin.FSM{} + return loopInFSM.LoopInStatesV0(), nil default: - fmt.Println("Missing or wrong argument: fsm must be one of:") - fmt.Println("\treservations") - fmt.Println("\texample") + return nil, fmt.Errorf( + "%w %q; supported selectors: example, instantout, "+ + "reservation, staticaddr-deposit, staticaddr-loopin", + errInvalidFSMSelector, stateMachine, + ) } - - return nil } func writeMermaidFile(filename string, states fsm.States) error { @@ -86,8 +98,11 @@ func writeMermaidFile(filename string, states fsm.States) error { state = "[*]" } // write transitions - for edge, target := range edges.Transitions { - fmt.Fprintf(&b, "%s --> %s: %s\n", state, target, edge) + for _, edge := range sortedTransitionKeys(edges.Transitions) { + fmt.Fprintf( + &b, "%s --> %s: %s\n", state, + edges.Transitions[fsm.EventType(edge)], edge, + ) } } @@ -110,3 +125,14 @@ func sortedKeys(m fsm.States) []string { sort.Strings(keys) return keys } + +func sortedTransitionKeys(m fsm.Transitions) []string { + keys := make([]string, len(m)) + i := 0 + for k := range m { + keys[i] = string(k) + i++ + } + sort.Strings(keys) + return keys +} diff --git a/go.mod b/go.mod index 34191986..4a32aa7d 100644 --- a/go.mod +++ b/go.mod @@ -1,34 +1,33 @@ module github.com/lightninglabs/loop require ( - github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 - github.com/btcsuite/btcd/btcec/v2 v2.3.4 - github.com/btcsuite/btcd/btcutil v1.1.5 + github.com/btcsuite/btcd v0.25.1-0.20260310163610-1c55c7c18179 + github.com/btcsuite/btcd/btcec/v2 v2.3.6 + github.com/btcsuite/btcd/btcutil v1.1.6 github.com/btcsuite/btcd/btcutil/psbt v1.1.10 github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b - github.com/btcsuite/btcwallet v0.16.17 + github.com/btcsuite/btcwallet v0.16.18 github.com/btcsuite/btcwallet/wtxmgr v1.5.6 github.com/davecgh/go-spew v1.1.1 - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 github.com/fortytw2/leaktest v1.3.0 github.com/golang-migrate/migrate/v4 v4.17.0 github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 - github.com/jackc/pgconn v1.14.3 github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 github.com/jackc/pgx/v5 v5.9.2 - github.com/jessevdk/go-flags v1.4.0 + github.com/jessevdk/go-flags v1.6.1 github.com/lib/pq v1.10.9 - github.com/lightninglabs/aperture v0.3.13-beta - github.com/lightninglabs/lndclient v0.20.0-8 + github.com/lightninglabs/aperture v0.4.0 + github.com/lightninglabs/lndclient v0.21.0-2 github.com/lightninglabs/loop/looprpc v1.0.7 github.com/lightninglabs/loop/swapserverrpc v1.0.14 - github.com/lightninglabs/taproot-assets v0.7.0 - github.com/lightninglabs/taproot-assets/taprpc v1.0.11 - github.com/lightningnetwork/lnd v0.20.1-beta + github.com/lightninglabs/taproot-assets v0.8.0 + github.com/lightninglabs/taproot-assets/taprpc v1.1.0 + github.com/lightningnetwork/lnd v0.21.0-beta github.com/lightningnetwork/lnd/cert v1.2.2 github.com/lightningnetwork/lnd/clock v1.1.1 - github.com/lightningnetwork/lnd/queue v1.1.1 + github.com/lightningnetwork/lnd/queue v1.2.0 github.com/lightningnetwork/lnd/ticker v1.1.1 github.com/lightningnetwork/lnd/tlv v1.3.2 github.com/lightningnetwork/lnd/tor v1.1.6 @@ -37,8 +36,8 @@ require ( github.com/urfave/cli-docs/v3 v3.1.1-0.20251020101624-bec07369b4f6 github.com/urfave/cli/v3 v3.4.1 go.etcd.io/bbolt v1.4.3 - golang.org/x/sync v0.19.0 - google.golang.org/grpc v1.79.3 + golang.org/x/sync v0.20.0 + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 gopkg.in/macaroon-bakery.v2 v2.3.0 gopkg.in/macaroon.v2 v2.1.0 @@ -49,7 +48,6 @@ require ( dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c // indirect - github.com/Masterminds/semver/v3 v3.3.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e // indirect github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 // indirect @@ -57,9 +55,9 @@ require ( github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/siphash v1.0.1 // indirect - github.com/andybalholm/brotli v1.0.4 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c // indirect + github.com/btcsuite/btcd/v2transport v1.0.1 // indirect + github.com/btcsuite/btclog v1.0.0 // indirect github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 // indirect github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5 // indirect @@ -67,7 +65,6 @@ require ( 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/caddyserver/certmagic v0.17.2 // indirect github.com/cenkalti/backoff/v4 v4.2.1 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/continuity v0.3.0 // indirect @@ -76,12 +73,12 @@ require ( github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect - github.com/cpuguy83/go-md2man/v2 v2.0.2 // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect + github.com/cpuguy83/go-md2man/v2 v2.0.5 // indirect + github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect github.com/decred/dcrd/lru v1.1.2 // indirect github.com/distribution/reference v0.6.0 // indirect - github.com/docker/cli v29.2.0+incompatible // indirect - github.com/docker/go-connections v0.6.0 // indirect + github.com/docker/cli v29.4.1+incompatible // indirect + github.com/docker/go-connections v0.7.0 // indirect github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect @@ -100,49 +97,36 @@ require ( github.com/google/uuid v1.6.0 // indirect github.com/gorilla/websocket v1.5.0 // indirect github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0-rc.0 // indirect - github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3 // indirect github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 // indirect github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgtype v1.14.4 // indirect - github.com/jackc/pgx/v4 v4.18.3 // indirect - github.com/jackc/puddle v1.3.0 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect github.com/jackpal/gateway v1.0.5 // indirect github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad // indirect - github.com/jedib0t/go-pretty/v6 v6.2.7 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/jrick/logrotate v1.1.2 // indirect github.com/json-iterator/go v1.1.12 // indirect github.com/kkdai/bstream v1.0.0 // indirect github.com/klauspost/compress v1.17.9 // indirect - github.com/klauspost/cpuid/v2 v2.2.7 // indirect - github.com/libdns/libdns v0.2.1 // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect - github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.4-0.20250610182311-2f1d46ef18b7 // indirect - github.com/lightninglabs/neutrino v0.16.1 // indirect - github.com/lightninglabs/neutrino/cache v1.1.2 // indirect - github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 // indirect + github.com/lightninglabs/neutrino v0.17.1 // indirect + github.com/lightninglabs/neutrino/cache v1.1.3 // indirect + github.com/lightningnetwork/lightning-onion v1.3.0 // indirect + github.com/lightningnetwork/lnd/actor v0.0.6 // indirect github.com/lightningnetwork/lnd/fn/v2 v2.0.9 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect - github.com/lightningnetwork/lnd/kvdb v1.4.16 // indirect - github.com/lightningnetwork/lnd/sqldb v1.0.12-0.20260113193010-8565d12e40b1 // indirect + github.com/lightningnetwork/lnd/kvdb v1.5.1 // indirect + github.com/lightningnetwork/lnd/sqldb v1.0.13 // indirect github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 // indirect github.com/mattn/go-isatty v0.0.20 // indirect - github.com/mattn/go-runewidth v0.0.13 // indirect github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 // indirect - github.com/mholt/acmez v1.0.4 // indirect github.com/miekg/dns v1.1.50 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect - github.com/moby/moby/api v1.53.0 // indirect - github.com/moby/moby/client v0.2.2 // indirect + github.com/moby/moby/api v1.54.2 // indirect + github.com/moby/moby/client v0.4.1 // indirect github.com/moby/sys/user v0.3.0 // indirect github.com/moby/term v0.5.2 // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect @@ -150,24 +134,22 @@ require ( github.com/ncruces/go-strftime v0.1.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/opencontainers/runc v1.2.8 // indirect + github.com/opencontainers/runc v1.3.6 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.14.0 // indirect github.com/prometheus/client_model v0.3.0 // indirect github.com/prometheus/common v0.37.0 // indirect github.com/prometheus/procfs v0.8.0 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect - github.com/rivo/uniseg v0.2.0 // indirect github.com/rogpeppe/fastuuid v1.2.0 // indirect github.com/russross/blackfriday/v2 v2.1.0 // indirect github.com/sirupsen/logrus v1.9.3 // indirect github.com/soheilhy/cmux v0.1.5 // indirect - github.com/spf13/pflag v1.0.6 // indirect + github.com/spf13/pflag v1.0.9 // indirect github.com/stretchr/objx v0.5.2 // 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/tv42/zbase32 v0.0.0-20160707012821-501572607d02 // indirect - github.com/urfave/cli v1.22.14 // indirect github.com/xeipuuv/gojsonpointer v0.0.0-20180127040702-4e3ac2762d5f // indirect github.com/xeipuuv/gojsonreference v0.0.0-20180127040603-bd5ef7bd5415 // indirect github.com/xeipuuv/gojsonschema v1.2.0 // indirect @@ -195,18 +177,18 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.24.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/mod v0.30.0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 // indirect + golang.org/x/mod v0.35.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.11.0 // indirect - golang.org/x/tools v0.39.0 // indirect + golang.org/x/tools v0.44.0 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect @@ -224,7 +206,7 @@ replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-d // We are using a fork of the migration library with custom functionality that // did not yet make it into the upstream repository. -replace github.com/golang-migrate/migrate/v4 => github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2 +replace github.com/golang-migrate/migrate/v4 => github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2.0.20251211093704-71c1eef09789 replace lukechampine.com/uint128 => github.com/lukechampine/uint128 v1.2.0 @@ -242,4 +224,15 @@ replace gonum.org/v1/plot => github.com/gonum/plot v0.10.1 // checking later if the domain reappears and replace can be removed. replace dario.cat/mergo => github.com/darccio/mergo v1.0.1 -go 1.25.5 +// The go.augendre.info vanity import endpoints are currently unavailable, +// but these tags still declare the original module paths. +replace go.augendre.info/arangolint => github.com/Crocmagnon/arangolint v0.4.0 + +replace go.augendre.info/fatcontext => github.com/Crocmagnon/fatcontext v0.9.0 + +go 1.25.12 + +// This sqldb revision contains lnd's pgx v5 migration, which removes the +// vulnerable legacy pgx v4/pgproto3 dependency chain. Remove this replacement +// once the required lnd version includes an sqldb release with that migration. +replace github.com/lightningnetwork/lnd/sqldb v1.0.13 => github.com/lightningnetwork/lnd/sqldb v1.0.13-0.20260514041430-e9b422f78581 diff --git a/go.sum b/go.sum index 13a8613a..9ba8984a 100644 --- a/go.sum +++ b/go.sum @@ -3,7 +3,6 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= @@ -14,598 +13,30 @@ cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKV cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.78.0/go.mod h1:QjdrLG0uq+YwhjoVOLsS1t7TW8fs36kLs4XO5R5ECHg= -cloud.google.com/go v0.79.0/go.mod h1:3bzgcEeQlzbuEAYu4mrWhKqWjmpprinYgKJLgKHnbb8= -cloud.google.com/go v0.81.0/go.mod h1:mk/AM35KwGk/Nm2YSeZbxXdrNK3KZOYHmLkOqC2V6E0= -cloud.google.com/go v0.83.0/go.mod h1:Z7MJUsANfY0pYPdw0lbnivPx4/vhy/e2FEkSkF7vAVY= -cloud.google.com/go v0.84.0/go.mod h1:RazrYuxIK6Kb7YrzzhPoLmCVzl7Sup4NrbKPg8KHSUM= -cloud.google.com/go v0.87.0/go.mod h1:TpDYlFy7vuLzZMMZ+B6iRiELaY7z/gJPaqbMx6mlWcY= -cloud.google.com/go v0.90.0/go.mod h1:kRX0mNRHe0e2rC6oNakvwQqzyDmg57xJ+SZU1eT2aDQ= -cloud.google.com/go v0.93.3/go.mod h1:8utlLll2EF5XMAV15woO4lSbWQlk8rer9aLOfLh7+YI= -cloud.google.com/go v0.94.1/go.mod h1:qAlAugsXlC+JWO+Bke5vCtc9ONxjQT3drlTTnAplMW4= -cloud.google.com/go v0.97.0/go.mod h1:GF7l59pYBVlXQIBLx3a761cZ41F9bBH3JUlihCt2Udc= -cloud.google.com/go v0.99.0/go.mod h1:w0Xx2nLzqWJPuozYQX+hFfCSI8WioryfRDzkoI/Y2ZA= -cloud.google.com/go v0.100.1/go.mod h1:fs4QogzfH5n2pBXBP9vRiU+eCny7lD2vmFZy79Iuw1U= -cloud.google.com/go v0.100.2/go.mod h1:4Xra9TjzAeYHrl5+oeLlzbM2k3mjVhZh4UqTZ//w99A= -cloud.google.com/go v0.102.0/go.mod h1:oWcCzKlqJ5zgHQt9YsaeTY9KzIvjyy0ArmiBUgpQ+nc= -cloud.google.com/go v0.102.1/go.mod h1:XZ77E9qnTEnrgEOvr4xzfdX5TRo7fB4T2F4O6+34hIU= -cloud.google.com/go v0.104.0/go.mod h1:OO6xxXdJyvuJPcEPBLN9BJPD+jep5G1+2U5B5gkRYtA= -cloud.google.com/go v0.105.0/go.mod h1:PrLgOJNe5nfE9UMxKxgXj4mD3voiP+YQ6gdt6KMFOKM= -cloud.google.com/go v0.107.0/go.mod h1:wpc2eNrD7hXUTy8EKS10jkxpZBjASrORK7goS+3YX2I= -cloud.google.com/go v0.110.0/go.mod h1:SJnCLqQ0FCFGSZMUNUf84MV3Aia54kn7pi8st7tMzaY= -cloud.google.com/go/accessapproval v1.4.0/go.mod h1:zybIuC3KpDOvotz59lFe5qxRZx6C75OtwbisN56xYB4= -cloud.google.com/go/accessapproval v1.5.0/go.mod h1:HFy3tuiGvMdcd/u+Cu5b9NkO1pEICJ46IR82PoUdplw= -cloud.google.com/go/accessapproval v1.6.0/go.mod h1:R0EiYnwV5fsRFiKZkPHr6mwyk2wxUJ30nL4j2pcFY2E= -cloud.google.com/go/accesscontextmanager v1.3.0/go.mod h1:TgCBehyr5gNMz7ZaH9xubp+CE8dkrszb4oK9CWyvD4o= -cloud.google.com/go/accesscontextmanager v1.4.0/go.mod h1:/Kjh7BBu/Gh83sv+K60vN9QE5NJcd80sU33vIe2IFPE= -cloud.google.com/go/accesscontextmanager v1.6.0/go.mod h1:8XCvZWfYw3K/ji0iVnp+6pu7huxoQTLmxAbVjbloTtM= -cloud.google.com/go/accesscontextmanager v1.7.0/go.mod h1:CEGLewx8dwa33aDAZQujl7Dx+uYhS0eay198wB/VumQ= -cloud.google.com/go/aiplatform v1.22.0/go.mod h1:ig5Nct50bZlzV6NvKaTwmplLLddFx0YReh9WfTO5jKw= -cloud.google.com/go/aiplatform v1.24.0/go.mod h1:67UUvRBKG6GTayHKV8DBv2RtR1t93YRu5B1P3x99mYY= -cloud.google.com/go/aiplatform v1.27.0/go.mod h1:Bvxqtl40l0WImSb04d0hXFU7gDOiq9jQmorivIiWcKg= -cloud.google.com/go/aiplatform v1.35.0/go.mod h1:7MFT/vCaOyZT/4IIFfxH4ErVg/4ku6lKv3w0+tFTgXQ= -cloud.google.com/go/aiplatform v1.36.1/go.mod h1:WTm12vJRPARNvJ+v6P52RDHCNe4AhvjcIZ/9/RRHy/k= -cloud.google.com/go/aiplatform v1.37.0/go.mod h1:IU2Cv29Lv9oCn/9LkFiiuKfwrRTq+QQMbW+hPCxJGZw= -cloud.google.com/go/analytics v0.11.0/go.mod h1:DjEWCu41bVbYcKyvlws9Er60YE4a//bK6mnhWvQeFNI= -cloud.google.com/go/analytics v0.12.0/go.mod h1:gkfj9h6XRf9+TS4bmuhPEShsh3hH8PAZzm/41OOhQd4= -cloud.google.com/go/analytics v0.17.0/go.mod h1:WXFa3WSym4IZ+JiKmavYdJwGG/CvpqiqczmL59bTD9M= -cloud.google.com/go/analytics v0.18.0/go.mod h1:ZkeHGQlcIPkw0R/GW+boWHhCOR43xz9RN/jn7WcqfIE= -cloud.google.com/go/analytics v0.19.0/go.mod h1:k8liqf5/HCnOUkbawNtrWWc+UAzyDlW89doe8TtoDsE= -cloud.google.com/go/apigateway v1.3.0/go.mod h1:89Z8Bhpmxu6AmUxuVRg/ECRGReEdiP3vQtk4Z1J9rJk= -cloud.google.com/go/apigateway v1.4.0/go.mod h1:pHVY9MKGaH9PQ3pJ4YLzoj6U5FUDeDFBllIz7WmzJoc= -cloud.google.com/go/apigateway v1.5.0/go.mod h1:GpnZR3Q4rR7LVu5951qfXPJCHquZt02jf7xQx7kpqN8= -cloud.google.com/go/apigeeconnect v1.3.0/go.mod h1:G/AwXFAKo0gIXkPTVfZDd2qA1TxBXJ3MgMRBQkIi9jc= -cloud.google.com/go/apigeeconnect v1.4.0/go.mod h1:kV4NwOKqjvt2JYR0AoIWo2QGfoRtn/pkS3QlHp0Ni04= -cloud.google.com/go/apigeeconnect v1.5.0/go.mod h1:KFaCqvBRU6idyhSNyn3vlHXc8VMDJdRmwDF6JyFRqZ8= -cloud.google.com/go/apigeeregistry v0.4.0/go.mod h1:EUG4PGcsZvxOXAdyEghIdXwAEi/4MEaoqLMLDMIwKXY= -cloud.google.com/go/apigeeregistry v0.5.0/go.mod h1:YR5+s0BVNZfVOUkMa5pAR2xGd0A473vA5M7j247o1wM= -cloud.google.com/go/apigeeregistry v0.6.0/go.mod h1:BFNzW7yQVLZ3yj0TKcwzb8n25CFBri51GVGOEUcgQsc= -cloud.google.com/go/apikeys v0.4.0/go.mod h1:XATS/yqZbaBK0HOssf+ALHp8jAlNHUgyfprvNcBIszU= -cloud.google.com/go/apikeys v0.5.0/go.mod h1:5aQfwY4D+ewMMWScd3hm2en3hCj+BROlyrt3ytS7KLI= -cloud.google.com/go/apikeys v0.6.0/go.mod h1:kbpXu5upyiAlGkKrJgQl8A0rKNNJ7dQ377pdroRSSi8= -cloud.google.com/go/appengine v1.4.0/go.mod h1:CS2NhuBuDXM9f+qscZ6V86m1MIIqPj3WC/UoEuR1Sno= -cloud.google.com/go/appengine v1.5.0/go.mod h1:TfasSozdkFI0zeoxW3PTBLiNqRmzraodCWatWI9Dmak= -cloud.google.com/go/appengine v1.6.0/go.mod h1:hg6i0J/BD2cKmDJbaFSYHFyZkgBEfQrDg/X0V5fJn84= -cloud.google.com/go/appengine v1.7.0/go.mod h1:eZqpbHFCqRGa2aCdope7eC0SWLV1j0neb/QnMJVWx6A= -cloud.google.com/go/appengine v1.7.1/go.mod h1:IHLToyb/3fKutRysUlFO0BPt5j7RiQ45nrzEJmKTo6E= -cloud.google.com/go/area120 v0.5.0/go.mod h1:DE/n4mp+iqVyvxHN41Vf1CR602GiHQjFPusMFW6bGR4= -cloud.google.com/go/area120 v0.6.0/go.mod h1:39yFJqWVgm0UZqWTOdqkLhjoC7uFfgXRC8g/ZegeAh0= -cloud.google.com/go/area120 v0.7.0/go.mod h1:a3+8EUD1SX5RUcCs3MY5YasiO1z6yLiNLRiFrykbynY= -cloud.google.com/go/area120 v0.7.1/go.mod h1:j84i4E1RboTWjKtZVWXPqvK5VHQFJRF2c1Nm69pWm9k= -cloud.google.com/go/artifactregistry v1.6.0/go.mod h1:IYt0oBPSAGYj/kprzsBjZ/4LnG/zOcHyFHjWPCi6SAQ= -cloud.google.com/go/artifactregistry v1.7.0/go.mod h1:mqTOFOnGZx8EtSqK/ZWcsm/4U8B77rbcLP6ruDU2Ixk= -cloud.google.com/go/artifactregistry v1.8.0/go.mod h1:w3GQXkJX8hiKN0v+at4b0qotwijQbYUqF2GWkZzAhC0= -cloud.google.com/go/artifactregistry v1.9.0/go.mod h1:2K2RqvA2CYvAeARHRkLDhMDJ3OXy26h3XW+3/Jh2uYc= -cloud.google.com/go/artifactregistry v1.11.1/go.mod h1:lLYghw+Itq9SONbCa1YWBoWs1nOucMH0pwXN1rOBZFI= -cloud.google.com/go/artifactregistry v1.11.2/go.mod h1:nLZns771ZGAwVLzTX/7Al6R9ehma4WUEhZGWV6CeQNQ= -cloud.google.com/go/artifactregistry v1.12.0/go.mod h1:o6P3MIvtzTOnmvGagO9v/rOjjA0HmhJ+/6KAXrmYDCI= -cloud.google.com/go/artifactregistry v1.13.0/go.mod h1:uy/LNfoOIivepGhooAUpL1i30Hgee3Cu0l4VTWHUC08= -cloud.google.com/go/asset v1.5.0/go.mod h1:5mfs8UvcM5wHhqtSv8J1CtxxaQq3AdBxxQi2jGW/K4o= -cloud.google.com/go/asset v1.7.0/go.mod h1:YbENsRK4+xTiL+Ofoj5Ckf+O17kJtgp3Y3nn4uzZz5s= -cloud.google.com/go/asset v1.8.0/go.mod h1:mUNGKhiqIdbr8X7KNayoYvyc4HbbFO9URsjbytpUaW0= -cloud.google.com/go/asset v1.9.0/go.mod h1:83MOE6jEJBMqFKadM9NLRcs80Gdw76qGuHn8m3h8oHQ= -cloud.google.com/go/asset v1.10.0/go.mod h1:pLz7uokL80qKhzKr4xXGvBQXnzHn5evJAEAtZiIb0wY= -cloud.google.com/go/asset v1.11.1/go.mod h1:fSwLhbRvC9p9CXQHJ3BgFeQNM4c9x10lqlrdEUYXlJo= -cloud.google.com/go/asset v1.12.0/go.mod h1:h9/sFOa4eDIyKmH6QMpm4eUK3pDojWnUhTgJlk762Hg= -cloud.google.com/go/asset v1.13.0/go.mod h1:WQAMyYek/b7NBpYq/K4KJWcRqzoalEsxz/t/dTk4THw= -cloud.google.com/go/assuredworkloads v1.5.0/go.mod h1:n8HOZ6pff6re5KYfBXcFvSViQjDwxFkAkmUFffJRbbY= -cloud.google.com/go/assuredworkloads v1.6.0/go.mod h1:yo2YOk37Yc89Rsd5QMVECvjaMKymF9OP+QXWlKXUkXw= -cloud.google.com/go/assuredworkloads v1.7.0/go.mod h1:z/736/oNmtGAyU47reJgGN+KVoYoxeLBoj4XkKYscNI= -cloud.google.com/go/assuredworkloads v1.8.0/go.mod h1:AsX2cqyNCOvEQC8RMPnoc0yEarXQk6WEKkxYfL6kGIo= -cloud.google.com/go/assuredworkloads v1.9.0/go.mod h1:kFuI1P78bplYtT77Tb1hi0FMxM0vVpRC7VVoJC3ZoT0= -cloud.google.com/go/assuredworkloads v1.10.0/go.mod h1:kwdUQuXcedVdsIaKgKTp9t0UJkE5+PAVNhdQm4ZVq2E= -cloud.google.com/go/automl v1.5.0/go.mod h1:34EjfoFGMZ5sgJ9EoLsRtdPSNZLcfflJR39VbVNS2M0= -cloud.google.com/go/automl v1.6.0/go.mod h1:ugf8a6Fx+zP0D59WLhqgTDsQI9w07o64uf/Is3Nh5p8= -cloud.google.com/go/automl v1.7.0/go.mod h1:RL9MYCCsJEOmt0Wf3z9uzG0a7adTT1fe+aObgSpkCt8= -cloud.google.com/go/automl v1.8.0/go.mod h1:xWx7G/aPEe/NP+qzYXktoBSDfjO+vnKMGgsApGJJquM= -cloud.google.com/go/automl v1.12.0/go.mod h1:tWDcHDp86aMIuHmyvjuKeeHEGq76lD7ZqfGLN6B0NuU= -cloud.google.com/go/baremetalsolution v0.3.0/go.mod h1:XOrocE+pvK1xFfleEnShBlNAXf+j5blPPxrhjKgnIFc= -cloud.google.com/go/baremetalsolution v0.4.0/go.mod h1:BymplhAadOO/eBa7KewQ0Ppg4A4Wplbn+PsFKRLo0uI= -cloud.google.com/go/baremetalsolution v0.5.0/go.mod h1:dXGxEkmR9BMwxhzBhV0AioD0ULBmuLZI8CdwalUxuss= -cloud.google.com/go/batch v0.3.0/go.mod h1:TR18ZoAekj1GuirsUsR1ZTKN3FC/4UDnScjT8NXImFE= -cloud.google.com/go/batch v0.4.0/go.mod h1:WZkHnP43R/QCGQsZ+0JyG4i79ranE2u8xvjq/9+STPE= -cloud.google.com/go/batch v0.7.0/go.mod h1:vLZN95s6teRUqRQ4s3RLDsH8PvboqBK+rn1oevL159g= -cloud.google.com/go/beyondcorp v0.2.0/go.mod h1:TB7Bd+EEtcw9PCPQhCJtJGjk/7TC6ckmnSFS+xwTfm4= -cloud.google.com/go/beyondcorp v0.3.0/go.mod h1:E5U5lcrcXMsCuoDNyGrpyTm/hn7ne941Jz2vmksAxW8= -cloud.google.com/go/beyondcorp v0.4.0/go.mod h1:3ApA0mbhHx6YImmuubf5pyW8srKnCEPON32/5hj+RmM= -cloud.google.com/go/beyondcorp v0.5.0/go.mod h1:uFqj9X+dSfrheVp7ssLTaRHd2EHqSL4QZmH4e8WXGGU= cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/bigquery v1.42.0/go.mod h1:8dRTJxhtG+vwBKzE5OseQn/hiydoQN3EedCaOdYmxRA= -cloud.google.com/go/bigquery v1.43.0/go.mod h1:ZMQcXHsl+xmU1z36G2jNGZmKp9zNY5BUua5wDgmNCfw= -cloud.google.com/go/bigquery v1.44.0/go.mod h1:0Y33VqXTEsbamHJvJHdFmtqHvMIY28aK1+dFsvaChGc= -cloud.google.com/go/bigquery v1.47.0/go.mod h1:sA9XOgy0A8vQK9+MWhEQTY6Tix87M/ZurWFIxmF9I/E= -cloud.google.com/go/bigquery v1.48.0/go.mod h1:QAwSz+ipNgfL5jxiaK7weyOhzdoAy1zFm0Nf1fysJac= -cloud.google.com/go/bigquery v1.49.0/go.mod h1:Sv8hMmTFFYBlt/ftw2uN6dFdQPzBlREY9yBh7Oy7/4Q= -cloud.google.com/go/bigquery v1.50.0/go.mod h1:YrleYEh2pSEbgTBZYMJ5SuSr0ML3ypjRB1zgf7pvQLU= -cloud.google.com/go/billing v1.4.0/go.mod h1:g9IdKBEFlItS8bTtlrZdVLWSSdSyFUZKXNS02zKMOZY= -cloud.google.com/go/billing v1.5.0/go.mod h1:mztb1tBc3QekhjSgmpf/CV4LzWXLzCArwpLmP2Gm88s= -cloud.google.com/go/billing v1.6.0/go.mod h1:WoXzguj+BeHXPbKfNWkqVtDdzORazmCjraY+vrxcyvI= -cloud.google.com/go/billing v1.7.0/go.mod h1:q457N3Hbj9lYwwRbnlD7vUpyjq6u5U1RAOArInEiD5Y= -cloud.google.com/go/billing v1.12.0/go.mod h1:yKrZio/eu+okO/2McZEbch17O5CB5NpZhhXG6Z766ss= -cloud.google.com/go/billing v1.13.0/go.mod h1:7kB2W9Xf98hP9Sr12KfECgfGclsH3CQR0R08tnRlRbc= -cloud.google.com/go/binaryauthorization v1.1.0/go.mod h1:xwnoWu3Y84jbuHa0zd526MJYmtnVXn0syOjaJgy4+dM= -cloud.google.com/go/binaryauthorization v1.2.0/go.mod h1:86WKkJHtRcv5ViNABtYMhhNWRrD1Vpi//uKEy7aYEfI= -cloud.google.com/go/binaryauthorization v1.3.0/go.mod h1:lRZbKgjDIIQvzYQS1p99A7/U1JqvqeZg0wiI5tp6tg0= -cloud.google.com/go/binaryauthorization v1.4.0/go.mod h1:tsSPQrBd77VLplV70GUhBf/Zm3FsKmgSqgm4UmiDItk= -cloud.google.com/go/binaryauthorization v1.5.0/go.mod h1:OSe4OU1nN/VswXKRBmciKpo9LulY41gch5c68htf3/Q= -cloud.google.com/go/certificatemanager v1.3.0/go.mod h1:n6twGDvcUBFu9uBgt4eYvvf3sQ6My8jADcOVwHmzadg= -cloud.google.com/go/certificatemanager v1.4.0/go.mod h1:vowpercVFyqs8ABSmrdV+GiFf2H/ch3KyudYQEMM590= -cloud.google.com/go/certificatemanager v1.6.0/go.mod h1:3Hh64rCKjRAX8dXgRAyOcY5vQ/fE1sh8o+Mdd6KPgY8= -cloud.google.com/go/channel v1.8.0/go.mod h1:W5SwCXDJsq/rg3tn3oG0LOxpAo6IMxNa09ngphpSlnk= -cloud.google.com/go/channel v1.9.0/go.mod h1:jcu05W0my9Vx4mt3/rEHpfxc9eKi9XwsdDL8yBMbKUk= -cloud.google.com/go/channel v1.11.0/go.mod h1:IdtI0uWGqhEeatSB62VOoJ8FSUhJ9/+iGkJVqp74CGE= -cloud.google.com/go/channel v1.12.0/go.mod h1:VkxCGKASi4Cq7TbXxlaBezonAYpp1GCnKMY6tnMQnLU= -cloud.google.com/go/cloudbuild v1.3.0/go.mod h1:WequR4ULxlqvMsjDEEEFnOG5ZSRSgWOywXYDb1vPE6U= -cloud.google.com/go/cloudbuild v1.4.0/go.mod h1:5Qwa40LHiOXmz3386FrjrYM93rM/hdRr7b53sySrTqA= -cloud.google.com/go/cloudbuild v1.6.0/go.mod h1:UIbc/w9QCbH12xX+ezUsgblrWv+Cv4Tw83GiSMHOn9M= -cloud.google.com/go/cloudbuild v1.7.0/go.mod h1:zb5tWh2XI6lR9zQmsm1VRA+7OCuve5d8S+zJUul8KTg= -cloud.google.com/go/cloudbuild v1.9.0/go.mod h1:qK1d7s4QlO0VwfYn5YuClDGg2hfmLZEb4wQGAbIgL1s= -cloud.google.com/go/clouddms v1.3.0/go.mod h1:oK6XsCDdW4Ib3jCCBugx+gVjevp2TMXFtgxvPSee3OM= -cloud.google.com/go/clouddms v1.4.0/go.mod h1:Eh7sUGCC+aKry14O1NRljhjyrr0NFC0G2cjwX0cByRk= -cloud.google.com/go/clouddms v1.5.0/go.mod h1:QSxQnhikCLUw13iAbffF2CZxAER3xDGNHjsTAkQJcQA= -cloud.google.com/go/cloudtasks v1.5.0/go.mod h1:fD92REy1x5woxkKEkLdvavGnPJGEn8Uic9nWuLzqCpY= -cloud.google.com/go/cloudtasks v1.6.0/go.mod h1:C6Io+sxuke9/KNRkbQpihnW93SWDU3uXt92nu85HkYI= -cloud.google.com/go/cloudtasks v1.7.0/go.mod h1:ImsfdYWwlWNJbdgPIIGJWC+gemEGTBK/SunNQQNCAb4= -cloud.google.com/go/cloudtasks v1.8.0/go.mod h1:gQXUIwCSOI4yPVK7DgTVFiiP0ZW/eQkydWzwVMdHxrI= -cloud.google.com/go/cloudtasks v1.9.0/go.mod h1:w+EyLsVkLWHcOaqNEyvcKAsWp9p29dL6uL9Nst1cI7Y= -cloud.google.com/go/cloudtasks v1.10.0/go.mod h1:NDSoTLkZ3+vExFEWu2UJV1arUyzVDAiZtdWcsUyNwBs= -cloud.google.com/go/compute v0.1.0/go.mod h1:GAesmwr110a34z04OlxYkATPBEfVhkymfTBXtfbBFow= -cloud.google.com/go/compute v1.3.0/go.mod h1:cCZiE1NHEtai4wiufUhW8I8S1JKkAnhnQJWM7YD99wM= -cloud.google.com/go/compute v1.5.0/go.mod h1:9SMHyhJlzhlkJqrPAc839t2BZFTSk6Jdj6mkzQJeu0M= -cloud.google.com/go/compute v1.6.0/go.mod h1:T29tfhtVbq1wvAPo0E3+7vhgmkOYeXjhFvz/FMzPu0s= -cloud.google.com/go/compute v1.6.1/go.mod h1:g85FgpzFvNULZ+S8AYq87axRKuf2Kh7deLqV/jJ3thU= -cloud.google.com/go/compute v1.7.0/go.mod h1:435lt8av5oL9P3fv1OEzSbSUe+ybHXGMPQHHZWZxy9U= -cloud.google.com/go/compute v1.10.0/go.mod h1:ER5CLbMxl90o2jtNbGSbtfOpQKR0t15FOtRsugnLrlU= -cloud.google.com/go/compute v1.12.0/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.12.1/go.mod h1:e8yNOBcBONZU1vJKCvCoDw/4JQsA0dpM4x/6PIIOocU= -cloud.google.com/go/compute v1.13.0/go.mod h1:5aPTS0cUNMIc1CE546K+Th6weJUNQErARyZtRXDJ8GE= -cloud.google.com/go/compute v1.14.0/go.mod h1:YfLtxrj9sU4Yxv+sXzZkyPjEyPBZfXHUvjxega5vAdo= -cloud.google.com/go/compute v1.15.1/go.mod h1:bjjoF/NtFUrkD/urWfdHaKuOPDR5nWIs63rR+SXhcpA= -cloud.google.com/go/compute v1.18.0/go.mod h1:1X7yHxec2Ga+Ss6jPyjxRxpu2uu7PLgsOVXvgU0yacs= -cloud.google.com/go/compute v1.19.0/go.mod h1:rikpw2y+UMidAe9tISo04EHNOIf42RLYF/q8Bs93scU= -cloud.google.com/go/compute v1.19.1/go.mod h1:6ylj3a05WF8leseCdIf77NK0g1ey+nj5IKd5/kvShxE= -cloud.google.com/go/compute/metadata v0.1.0/go.mod h1:Z1VN+bulIf6bt4P/C37K4DyZYZEXYonfTBHHFPO/4UU= -cloud.google.com/go/compute/metadata v0.2.0/go.mod h1:zFmK7XCadkQkj6TtorcaGlCW1hT1fIilQDwofLpJ20k= -cloud.google.com/go/compute/metadata v0.2.1/go.mod h1:jgHgmJd2RKBGzXqF5LR2EZMGxBkeanZ9wwa75XHJgOM= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/contactcenterinsights v1.3.0/go.mod h1:Eu2oemoePuEFc/xKFPjbTuPSj0fYJcPls9TFlPNnHHY= -cloud.google.com/go/contactcenterinsights v1.4.0/go.mod h1:L2YzkGbPsv+vMQMCADxJoT9YiTTnSEd6fEvCeHTYVck= -cloud.google.com/go/contactcenterinsights v1.6.0/go.mod h1:IIDlT6CLcDoyv79kDv8iWxMSTZhLxSCofVV5W6YFM/w= -cloud.google.com/go/container v1.6.0/go.mod h1:Xazp7GjJSeUYo688S+6J5V+n/t+G5sKBTFkKNudGRxg= -cloud.google.com/go/container v1.7.0/go.mod h1:Dp5AHtmothHGX3DwwIHPgq45Y8KmNsgN3amoYfxVkLo= -cloud.google.com/go/container v1.13.1/go.mod h1:6wgbMPeQRw9rSnKBCAJXnds3Pzj03C4JHamr8asWKy4= -cloud.google.com/go/container v1.14.0/go.mod h1:3AoJMPhHfLDxLvrlVWaK57IXzaPnLaZq63WX59aQBfM= -cloud.google.com/go/container v1.15.0/go.mod h1:ft+9S0WGjAyjDggg5S06DXj+fHJICWg8L7isCQe9pQA= -cloud.google.com/go/containeranalysis v0.5.1/go.mod h1:1D92jd8gRR/c0fGMlymRgxWD3Qw9C1ff6/T7mLgVL8I= -cloud.google.com/go/containeranalysis v0.6.0/go.mod h1:HEJoiEIu+lEXM+k7+qLCci0h33lX3ZqoYFdmPcoO7s4= -cloud.google.com/go/containeranalysis v0.7.0/go.mod h1:9aUL+/vZ55P2CXfuZjS4UjQ9AgXoSw8Ts6lemfmxBxI= -cloud.google.com/go/containeranalysis v0.9.0/go.mod h1:orbOANbwk5Ejoom+s+DUCTTJ7IBdBQJDcSylAx/on9s= -cloud.google.com/go/datacatalog v1.3.0/go.mod h1:g9svFY6tuR+j+hrTw3J2dNcmI0dzmSiyOzm8kpLq0a0= -cloud.google.com/go/datacatalog v1.5.0/go.mod h1:M7GPLNQeLfWqeIm3iuiruhPzkt65+Bx8dAKvScX8jvs= -cloud.google.com/go/datacatalog v1.6.0/go.mod h1:+aEyF8JKg+uXcIdAmmaMUmZ3q1b/lKLtXCmXdnc0lbc= -cloud.google.com/go/datacatalog v1.7.0/go.mod h1:9mEl4AuDYWw81UGc41HonIHH7/sn52H0/tc8f8ZbZIE= -cloud.google.com/go/datacatalog v1.8.0/go.mod h1:KYuoVOv9BM8EYz/4eMFxrr4DUKhGIOXxZoKYF5wdISM= -cloud.google.com/go/datacatalog v1.8.1/go.mod h1:RJ58z4rMp3gvETA465Vg+ag8BGgBdnRPEMMSTr5Uv+M= -cloud.google.com/go/datacatalog v1.12.0/go.mod h1:CWae8rFkfp6LzLumKOnmVh4+Zle4A3NXLzVJ1d1mRm0= -cloud.google.com/go/datacatalog v1.13.0/go.mod h1:E4Rj9a5ZtAxcQJlEBTLgMTphfP11/lNaAshpoBgemX8= -cloud.google.com/go/dataflow v0.6.0/go.mod h1:9QwV89cGoxjjSR9/r7eFDqqjtvbKxAK2BaYU6PVk9UM= -cloud.google.com/go/dataflow v0.7.0/go.mod h1:PX526vb4ijFMesO1o202EaUmouZKBpjHsTlCtB4parQ= -cloud.google.com/go/dataflow v0.8.0/go.mod h1:Rcf5YgTKPtQyYz8bLYhFoIV/vP39eL7fWNcSOyFfLJE= -cloud.google.com/go/dataform v0.3.0/go.mod h1:cj8uNliRlHpa6L3yVhDOBrUXH+BPAO1+KFMQQNSThKo= -cloud.google.com/go/dataform v0.4.0/go.mod h1:fwV6Y4Ty2yIFL89huYlEkwUPtS7YZinZbzzj5S9FzCE= -cloud.google.com/go/dataform v0.5.0/go.mod h1:GFUYRe8IBa2hcomWplodVmUx/iTL0FrsauObOM3Ipr0= -cloud.google.com/go/dataform v0.6.0/go.mod h1:QPflImQy33e29VuapFdf19oPbE4aYTJxr31OAPV+ulA= -cloud.google.com/go/dataform v0.7.0/go.mod h1:7NulqnVozfHvWUBpMDfKMUESr+85aJsC/2O0o3jWPDE= -cloud.google.com/go/datafusion v1.4.0/go.mod h1:1Zb6VN+W6ALo85cXnM1IKiPw+yQMKMhB9TsTSRDo/38= -cloud.google.com/go/datafusion v1.5.0/go.mod h1:Kz+l1FGHB0J+4XF2fud96WMmRiq/wj8N9u007vyXZ2w= -cloud.google.com/go/datafusion v1.6.0/go.mod h1:WBsMF8F1RhSXvVM8rCV3AeyWVxcC2xY6vith3iw3S+8= -cloud.google.com/go/datalabeling v0.5.0/go.mod h1:TGcJ0G2NzcsXSE/97yWjIZO0bXj0KbVlINXMG9ud42I= -cloud.google.com/go/datalabeling v0.6.0/go.mod h1:WqdISuk/+WIGeMkpw/1q7bK/tFEZxsrFJOJdY2bXvTQ= -cloud.google.com/go/datalabeling v0.7.0/go.mod h1:WPQb1y08RJbmpM3ww0CSUAGweL0SxByuW2E+FU+wXcM= -cloud.google.com/go/dataplex v1.3.0/go.mod h1:hQuRtDg+fCiFgC8j0zV222HvzFQdRd+SVX8gdmFcZzA= -cloud.google.com/go/dataplex v1.4.0/go.mod h1:X51GfLXEMVJ6UN47ESVqvlsRplbLhcsAt0kZCCKsU0A= -cloud.google.com/go/dataplex v1.5.2/go.mod h1:cVMgQHsmfRoI5KFYq4JtIBEUbYwc3c7tXmIDhRmNNVQ= -cloud.google.com/go/dataplex v1.6.0/go.mod h1:bMsomC/aEJOSpHXdFKFGQ1b0TDPIeL28nJObeO1ppRs= -cloud.google.com/go/dataproc v1.7.0/go.mod h1:CKAlMjII9H90RXaMpSxQ8EU6dQx6iAYNPcYPOkSbi8s= -cloud.google.com/go/dataproc v1.8.0/go.mod h1:5OW+zNAH0pMpw14JVrPONsxMQYMBqJuzORhIBfBn9uI= -cloud.google.com/go/dataproc v1.12.0/go.mod h1:zrF3aX0uV3ikkMz6z4uBbIKyhRITnxvr4i3IjKsKrw4= -cloud.google.com/go/dataqna v0.5.0/go.mod h1:90Hyk596ft3zUQ8NkFfvICSIfHFh1Bc7C4cK3vbhkeo= -cloud.google.com/go/dataqna v0.6.0/go.mod h1:1lqNpM7rqNLVgWBJyk5NF6Uen2PHym0jtVJonplVsDA= -cloud.google.com/go/dataqna v0.7.0/go.mod h1:Lx9OcIIeqCrw1a6KdO3/5KMP1wAmTc0slZWwP12Qq3c= cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/datastore v1.10.0/go.mod h1:PC5UzAmDEkAmkfaknstTYbNpgE49HAgW2J1gcgUfmdM= -cloud.google.com/go/datastore v1.11.0/go.mod h1:TvGxBIHCS50u8jzG+AW/ppf87v1of8nwzFNgEZU1D3c= -cloud.google.com/go/datastream v1.2.0/go.mod h1:i/uTP8/fZwgATHS/XFu0TcNUhuA0twZxxQ3EyCUQMwo= -cloud.google.com/go/datastream v1.3.0/go.mod h1:cqlOX8xlyYF/uxhiKn6Hbv6WjwPPuI9W2M9SAXwaLLQ= -cloud.google.com/go/datastream v1.4.0/go.mod h1:h9dpzScPhDTs5noEMQVWP8Wx8AFBRyS0s8KWPx/9r0g= -cloud.google.com/go/datastream v1.5.0/go.mod h1:6TZMMNPwjUqZHBKPQ1wwXpb0d5VDVPl2/XoS5yi88q4= -cloud.google.com/go/datastream v1.6.0/go.mod h1:6LQSuswqLa7S4rPAOZFVjHIG3wJIjZcZrw8JDEDJuIs= -cloud.google.com/go/datastream v1.7.0/go.mod h1:uxVRMm2elUSPuh65IbZpzJNMbuzkcvu5CjMqVIUHrww= -cloud.google.com/go/deploy v1.4.0/go.mod h1:5Xghikd4VrmMLNaF6FiRFDlHb59VM59YoDQnOUdsH/c= -cloud.google.com/go/deploy v1.5.0/go.mod h1:ffgdD0B89tToyW/U/D2eL0jN2+IEV/3EMuXHA0l4r+s= -cloud.google.com/go/deploy v1.6.0/go.mod h1:f9PTHehG/DjCom3QH0cntOVRm93uGBDt2vKzAPwpXQI= -cloud.google.com/go/deploy v1.8.0/go.mod h1:z3myEJnA/2wnB4sgjqdMfgxCA0EqC3RBTNcVPs93mtQ= -cloud.google.com/go/dialogflow v1.15.0/go.mod h1:HbHDWs33WOGJgn6rfzBW1Kv807BE3O1+xGbn59zZWI4= -cloud.google.com/go/dialogflow v1.16.1/go.mod h1:po6LlzGfK+smoSmTBnbkIZY2w8ffjz/RcGSS+sh1el0= -cloud.google.com/go/dialogflow v1.17.0/go.mod h1:YNP09C/kXA1aZdBgC/VtXX74G/TKn7XVCcVumTflA+8= -cloud.google.com/go/dialogflow v1.18.0/go.mod h1:trO7Zu5YdyEuR+BhSNOqJezyFQ3aUzz0njv7sMx/iek= -cloud.google.com/go/dialogflow v1.19.0/go.mod h1:JVmlG1TwykZDtxtTXujec4tQ+D8SBFMoosgy+6Gn0s0= -cloud.google.com/go/dialogflow v1.29.0/go.mod h1:b+2bzMe+k1s9V+F2jbJwpHPzrnIyHihAdRFMtn2WXuM= -cloud.google.com/go/dialogflow v1.31.0/go.mod h1:cuoUccuL1Z+HADhyIA7dci3N5zUssgpBJmCzI6fNRB4= -cloud.google.com/go/dialogflow v1.32.0/go.mod h1:jG9TRJl8CKrDhMEcvfcfFkkpp8ZhgPz3sBGmAUYJ2qE= -cloud.google.com/go/dlp v1.6.0/go.mod h1:9eyB2xIhpU0sVwUixfBubDoRwP+GjeUoxxeueZmqvmM= -cloud.google.com/go/dlp v1.7.0/go.mod h1:68ak9vCiMBjbasxeVD17hVPxDEck+ExiHavX8kiHG+Q= -cloud.google.com/go/dlp v1.9.0/go.mod h1:qdgmqgTyReTz5/YNSSuueR8pl7hO0o9bQ39ZhtgkWp4= -cloud.google.com/go/documentai v1.7.0/go.mod h1:lJvftZB5NRiFSX4moiye1SMxHx0Bc3x1+p9e/RfXYiU= -cloud.google.com/go/documentai v1.8.0/go.mod h1:xGHNEB7CtsnySCNrCFdCyyMz44RhFEEX2Q7UD0c5IhU= -cloud.google.com/go/documentai v1.9.0/go.mod h1:FS5485S8R00U10GhgBC0aNGrJxBP8ZVpEeJ7PQDZd6k= -cloud.google.com/go/documentai v1.10.0/go.mod h1:vod47hKQIPeCfN2QS/jULIvQTugbmdc0ZvxxfQY1bg4= -cloud.google.com/go/documentai v1.16.0/go.mod h1:o0o0DLTEZ+YnJZ+J4wNfTxmDVyrkzFvttBXXtYRMHkM= -cloud.google.com/go/documentai v1.18.0/go.mod h1:F6CK6iUH8J81FehpskRmhLq/3VlwQvb7TvwOceQ2tbs= -cloud.google.com/go/domains v0.6.0/go.mod h1:T9Rz3GasrpYk6mEGHh4rymIhjlnIuB4ofT1wTxDeT4Y= -cloud.google.com/go/domains v0.7.0/go.mod h1:PtZeqS1xjnXuRPKE/88Iru/LdfoRyEHYA9nFQf4UKpg= -cloud.google.com/go/domains v0.8.0/go.mod h1:M9i3MMDzGFXsydri9/vW+EWz9sWb4I6WyHqdlAk0idE= -cloud.google.com/go/edgecontainer v0.1.0/go.mod h1:WgkZ9tp10bFxqO8BLPqv2LlfmQF1X8lZqwW4r1BTajk= -cloud.google.com/go/edgecontainer v0.2.0/go.mod h1:RTmLijy+lGpQ7BXuTDa4C4ssxyXT34NIuHIgKuP4s5w= -cloud.google.com/go/edgecontainer v0.3.0/go.mod h1:FLDpP4nykgwwIfcLt6zInhprzw0lEi2P1fjO6Ie0qbc= -cloud.google.com/go/edgecontainer v1.0.0/go.mod h1:cttArqZpBB2q58W/upSG++ooo6EsblxDIolxa3jSjbY= -cloud.google.com/go/errorreporting v0.3.0/go.mod h1:xsP2yaAp+OAW4OIm60An2bbLpqIhKXdWR/tawvl7QzU= -cloud.google.com/go/essentialcontacts v1.3.0/go.mod h1:r+OnHa5jfj90qIfZDO/VztSFqbQan7HV75p8sA+mdGI= -cloud.google.com/go/essentialcontacts v1.4.0/go.mod h1:8tRldvHYsmnBCHdFpvU+GL75oWiBKl80BiqlFh9tp+8= -cloud.google.com/go/essentialcontacts v1.5.0/go.mod h1:ay29Z4zODTuwliK7SnX8E86aUF2CTzdNtvv42niCX0M= -cloud.google.com/go/eventarc v1.7.0/go.mod h1:6ctpF3zTnaQCxUjHUdcfgcA1A2T309+omHZth7gDfmc= -cloud.google.com/go/eventarc v1.8.0/go.mod h1:imbzxkyAU4ubfsaKYdQg04WS1NvncblHEup4kvF+4gw= -cloud.google.com/go/eventarc v1.10.0/go.mod h1:u3R35tmZ9HvswGRBnF48IlYgYeBcPUCjkr4BTdem2Kw= -cloud.google.com/go/eventarc v1.11.0/go.mod h1:PyUjsUKPWoRBCHeOxZd/lbOOjahV41icXyUY5kSTvVY= -cloud.google.com/go/filestore v1.3.0/go.mod h1:+qbvHGvXU1HaKX2nD0WEPo92TP/8AQuCVEBXNY9z0+w= -cloud.google.com/go/filestore v1.4.0/go.mod h1:PaG5oDfo9r224f8OYXURtAsY+Fbyq/bLYoINEK8XQAI= -cloud.google.com/go/filestore v1.5.0/go.mod h1:FqBXDWBp4YLHqRnVGveOkHDf8svj9r5+mUDLupOWEDs= -cloud.google.com/go/filestore v1.6.0/go.mod h1:di5unNuss/qfZTw2U9nhFqo8/ZDSc466dre85Kydllg= -cloud.google.com/go/firestore v1.9.0/go.mod h1:HMkjKHNTtRyZNiMzu7YAsLr9K3X2udY2AMwDaMEQiiE= -cloud.google.com/go/functions v1.6.0/go.mod h1:3H1UA3qiIPRWD7PeZKLvHZ9SaQhR26XIJcC0A5GbvAk= -cloud.google.com/go/functions v1.7.0/go.mod h1:+d+QBcWM+RsrgZfV9xo6KfA1GlzJfxcfZcRPEhDDfzg= -cloud.google.com/go/functions v1.8.0/go.mod h1:RTZ4/HsQjIqIYP9a9YPbU+QFoQsAlYgrwOXJWHn1POY= -cloud.google.com/go/functions v1.9.0/go.mod h1:Y+Dz8yGguzO3PpIjhLTbnqV1CWmgQ5UwtlpzoyquQ08= -cloud.google.com/go/functions v1.10.0/go.mod h1:0D3hEOe3DbEvCXtYOZHQZmD+SzYsi1YbI7dGvHfldXw= -cloud.google.com/go/functions v1.12.0/go.mod h1:AXWGrF3e2C/5ehvwYo/GH6O5s09tOPksiKhz+hH8WkA= -cloud.google.com/go/functions v1.13.0/go.mod h1:EU4O007sQm6Ef/PwRsI8N2umygGqPBS/IZQKBQBcJ3c= -cloud.google.com/go/gaming v1.5.0/go.mod h1:ol7rGcxP/qHTRQE/RO4bxkXq+Fix0j6D4LFPzYTIrDM= -cloud.google.com/go/gaming v1.6.0/go.mod h1:YMU1GEvA39Qt3zWGyAVA9bpYz/yAhTvaQ1t2sK4KPUA= -cloud.google.com/go/gaming v1.7.0/go.mod h1:LrB8U7MHdGgFG851iHAfqUdLcKBdQ55hzXy9xBJz0+w= -cloud.google.com/go/gaming v1.8.0/go.mod h1:xAqjS8b7jAVW0KFYeRUxngo9My3f33kFmua++Pi+ggM= -cloud.google.com/go/gaming v1.9.0/go.mod h1:Fc7kEmCObylSWLO334NcO+O9QMDyz+TKC4v1D7X+Bc0= -cloud.google.com/go/gkebackup v0.2.0/go.mod h1:XKvv/4LfG829/B8B7xRkk8zRrOEbKtEam6yNfuQNH60= -cloud.google.com/go/gkebackup v0.3.0/go.mod h1:n/E671i1aOQvUxT541aTkCwExO/bTer2HDlj4TsBRAo= -cloud.google.com/go/gkebackup v0.4.0/go.mod h1:byAyBGUwYGEEww7xsbnUTBHIYcOPy/PgUWUtOeRm9Vg= -cloud.google.com/go/gkeconnect v0.5.0/go.mod h1:c5lsNAg5EwAy7fkqX/+goqFsU1Da/jQFqArp+wGNr/o= -cloud.google.com/go/gkeconnect v0.6.0/go.mod h1:Mln67KyU/sHJEBY8kFZ0xTeyPtzbq9StAVvEULYK16A= -cloud.google.com/go/gkeconnect v0.7.0/go.mod h1:SNfmVqPkaEi3bF/B3CNZOAYPYdg7sU+obZ+QTky2Myw= -cloud.google.com/go/gkehub v0.9.0/go.mod h1:WYHN6WG8w9bXU0hqNxt8rm5uxnk8IH+lPY9J2TV7BK0= -cloud.google.com/go/gkehub v0.10.0/go.mod h1:UIPwxI0DsrpsVoWpLB0stwKCP+WFVG9+y977wO+hBH0= -cloud.google.com/go/gkehub v0.11.0/go.mod h1:JOWHlmN+GHyIbuWQPl47/C2RFhnFKH38jH9Ascu3n0E= -cloud.google.com/go/gkehub v0.12.0/go.mod h1:djiIwwzTTBrF5NaXCGv3mf7klpEMcST17VBTVVDcuaw= -cloud.google.com/go/gkemulticloud v0.3.0/go.mod h1:7orzy7O0S+5kq95e4Hpn7RysVA7dPs8W/GgfUtsPbrA= -cloud.google.com/go/gkemulticloud v0.4.0/go.mod h1:E9gxVBnseLWCk24ch+P9+B2CoDFJZTyIgLKSalC7tuI= -cloud.google.com/go/gkemulticloud v0.5.0/go.mod h1:W0JDkiyi3Tqh0TJr//y19wyb1yf8llHVto2Htf2Ja3Y= -cloud.google.com/go/grafeas v0.2.0/go.mod h1:KhxgtF2hb0P191HlY5besjYm6MqTSTj3LSI+M+ByZHc= -cloud.google.com/go/gsuiteaddons v1.3.0/go.mod h1:EUNK/J1lZEZO8yPtykKxLXI6JSVN2rg9bN8SXOa0bgM= -cloud.google.com/go/gsuiteaddons v1.4.0/go.mod h1:rZK5I8hht7u7HxFQcFei0+AtfS9uSushomRlg+3ua1o= -cloud.google.com/go/gsuiteaddons v1.5.0/go.mod h1:TFCClYLd64Eaa12sFVmUyG62tk4mdIsI7pAnSXRkcFo= -cloud.google.com/go/iam v0.1.0/go.mod h1:vcUNEa0pEm0qRVpmWepWaFMIAI8/hjB9mO8rNCJtF6c= -cloud.google.com/go/iam v0.3.0/go.mod h1:XzJPvDayI+9zsASAFO68Hk07u3z+f+JrT2xXNdp4bnY= -cloud.google.com/go/iam v0.5.0/go.mod h1:wPU9Vt0P4UmCux7mqtRu6jcpPAb74cP1fh50J3QpkUc= -cloud.google.com/go/iam v0.6.0/go.mod h1:+1AH33ueBne5MzYccyMHtEKqLE4/kJOibtffMHDMFMc= -cloud.google.com/go/iam v0.7.0/go.mod h1:H5Br8wRaDGNc8XP3keLc4unfUUZeyH3Sfl9XpQEYOeg= -cloud.google.com/go/iam v0.8.0/go.mod h1:lga0/y3iH6CX7sYqypWJ33hf7kkfXJag67naqGESjkE= -cloud.google.com/go/iam v0.11.0/go.mod h1:9PiLDanza5D+oWFZiH1uG+RnRCfEGKoyl6yo4cgWZGY= -cloud.google.com/go/iam v0.12.0/go.mod h1:knyHGviacl11zrtZUoDuYpDgLjvr28sLQaG0YB2GYAY= -cloud.google.com/go/iam v0.13.0/go.mod h1:ljOg+rcNfzZ5d6f1nAUJ8ZIxOaZUVoS14bKCtaLZ/D0= -cloud.google.com/go/iap v1.4.0/go.mod h1:RGFwRJdihTINIe4wZ2iCP0zF/qu18ZwyKxrhMhygBEc= -cloud.google.com/go/iap v1.5.0/go.mod h1:UH/CGgKd4KyohZL5Pt0jSKE4m3FR51qg6FKQ/z/Ix9A= -cloud.google.com/go/iap v1.6.0/go.mod h1:NSuvI9C/j7UdjGjIde7t7HBz+QTwBcapPE07+sSRcLk= -cloud.google.com/go/iap v1.7.0/go.mod h1:beqQx56T9O1G1yNPph+spKpNibDlYIiIixiqsQXxLIo= -cloud.google.com/go/iap v1.7.1/go.mod h1:WapEwPc7ZxGt2jFGB/C/bm+hP0Y6NXzOYGjpPnmMS74= -cloud.google.com/go/ids v1.1.0/go.mod h1:WIuwCaYVOzHIj2OhN9HAwvW+DBdmUAdcWlFxRl+KubM= -cloud.google.com/go/ids v1.2.0/go.mod h1:5WXvp4n25S0rA/mQWAg1YEEBBq6/s+7ml1RDCW1IrcY= -cloud.google.com/go/ids v1.3.0/go.mod h1:JBdTYwANikFKaDP6LtW5JAi4gubs57SVNQjemdt6xV4= -cloud.google.com/go/iot v1.3.0/go.mod h1:r7RGh2B61+B8oz0AGE+J72AhA0G7tdXItODWsaA2oLs= -cloud.google.com/go/iot v1.4.0/go.mod h1:dIDxPOn0UvNDUMD8Ger7FIaTuvMkj+aGk94RPP0iV+g= -cloud.google.com/go/iot v1.5.0/go.mod h1:mpz5259PDl3XJthEmh9+ap0affn/MqNSP4My77Qql9o= -cloud.google.com/go/iot v1.6.0/go.mod h1:IqdAsmE2cTYYNO1Fvjfzo9po179rAtJeVGUvkLN3rLE= -cloud.google.com/go/kms v1.4.0/go.mod h1:fajBHndQ+6ubNw6Ss2sSd+SWvjL26RNo/dr7uxsnnOA= -cloud.google.com/go/kms v1.5.0/go.mod h1:QJS2YY0eJGBg3mnDfuaCyLauWwBJiHRboYxJ++1xJNg= -cloud.google.com/go/kms v1.6.0/go.mod h1:Jjy850yySiasBUDi6KFUwUv2n1+o7QZFyuUJg6OgjA0= -cloud.google.com/go/kms v1.8.0/go.mod h1:4xFEhYFqvW+4VMELtZyxomGSYtSQKzM178ylFW4jMAg= -cloud.google.com/go/kms v1.9.0/go.mod h1:qb1tPTgfF9RQP8e1wq4cLFErVuTJv7UsSC915J8dh3w= -cloud.google.com/go/kms v1.10.0/go.mod h1:ng3KTUtQQU9bPX3+QGLsflZIHlkbn8amFAMY63m8d24= -cloud.google.com/go/kms v1.10.1/go.mod h1:rIWk/TryCkR59GMC3YtHtXeLzd634lBbKenvyySAyYI= -cloud.google.com/go/language v1.4.0/go.mod h1:F9dRpNFQmJbkaop6g0JhSBXCNlO90e1KWx5iDdxbWic= -cloud.google.com/go/language v1.6.0/go.mod h1:6dJ8t3B+lUYfStgls25GusK04NLh3eDLQnWM3mdEbhI= -cloud.google.com/go/language v1.7.0/go.mod h1:DJ6dYN/W+SQOjF8e1hLQXMF21AkH2w9wiPzPCJa2MIE= -cloud.google.com/go/language v1.8.0/go.mod h1:qYPVHf7SPoNNiCL2Dr0FfEFNil1qi3pQEyygwpgVKB8= -cloud.google.com/go/language v1.9.0/go.mod h1:Ns15WooPM5Ad/5no/0n81yUetis74g3zrbeJBE+ptUY= -cloud.google.com/go/lifesciences v0.5.0/go.mod h1:3oIKy8ycWGPUyZDR/8RNnTOYevhaMLqh5vLUXs9zvT8= -cloud.google.com/go/lifesciences v0.6.0/go.mod h1:ddj6tSX/7BOnhxCSd3ZcETvtNr8NZ6t/iPhY2Tyfu08= -cloud.google.com/go/lifesciences v0.8.0/go.mod h1:lFxiEOMqII6XggGbOnKiyZ7IBwoIqA84ClvoezaA/bo= -cloud.google.com/go/logging v1.6.1/go.mod h1:5ZO0mHHbvm8gEmeEUHrmDlTDSu5imF6MUP9OfilNXBw= -cloud.google.com/go/logging v1.7.0/go.mod h1:3xjP2CjkM3ZkO73aj4ASA5wRPGGCRrPIAeNqVNkzY8M= -cloud.google.com/go/longrunning v0.1.1/go.mod h1:UUFxuDWkv22EuY93jjmDMFT5GPQKeFVJBIF6QlTqdsE= -cloud.google.com/go/longrunning v0.3.0/go.mod h1:qth9Y41RRSUE69rDcOn6DdK3HfQfsUI0YSmW3iIlLJc= -cloud.google.com/go/longrunning v0.4.1/go.mod h1:4iWDqhBZ70CvZ6BfETbvam3T8FMvLK+eFj0E6AaRQTo= -cloud.google.com/go/managedidentities v1.3.0/go.mod h1:UzlW3cBOiPrzucO5qWkNkh0w33KFtBJU281hacNvsdE= -cloud.google.com/go/managedidentities v1.4.0/go.mod h1:NWSBYbEMgqmbZsLIyKvxrYbtqOsxY1ZrGM+9RgDqInM= -cloud.google.com/go/managedidentities v1.5.0/go.mod h1:+dWcZ0JlUmpuxpIDfyP5pP5y0bLdRwOS4Lp7gMni/LA= -cloud.google.com/go/maps v0.1.0/go.mod h1:BQM97WGyfw9FWEmQMpZ5T6cpovXXSd1cGmFma94eubI= -cloud.google.com/go/maps v0.6.0/go.mod h1:o6DAMMfb+aINHz/p/jbcY+mYeXBoZoxTfdSQ8VAJaCw= -cloud.google.com/go/maps v0.7.0/go.mod h1:3GnvVl3cqeSvgMcpRlQidXsPYuDGQ8naBis7MVzpXsY= -cloud.google.com/go/mediatranslation v0.5.0/go.mod h1:jGPUhGTybqsPQn91pNXw0xVHfuJ3leR1wj37oU3y1f4= -cloud.google.com/go/mediatranslation v0.6.0/go.mod h1:hHdBCTYNigsBxshbznuIMFNe5QXEowAuNmmC7h8pu5w= -cloud.google.com/go/mediatranslation v0.7.0/go.mod h1:LCnB/gZr90ONOIQLgSXagp8XUW1ODs2UmUMvcgMfI2I= -cloud.google.com/go/memcache v1.4.0/go.mod h1:rTOfiGZtJX1AaFUrOgsMHX5kAzaTQ8azHiuDoTPzNsE= -cloud.google.com/go/memcache v1.5.0/go.mod h1:dk3fCK7dVo0cUU2c36jKb4VqKPS22BTkf81Xq617aWM= -cloud.google.com/go/memcache v1.6.0/go.mod h1:XS5xB0eQZdHtTuTF9Hf8eJkKtR3pVRCcvJwtm68T3rA= -cloud.google.com/go/memcache v1.7.0/go.mod h1:ywMKfjWhNtkQTxrWxCkCFkoPjLHPW6A7WOTVI8xy3LY= -cloud.google.com/go/memcache v1.9.0/go.mod h1:8oEyzXCu+zo9RzlEaEjHl4KkgjlNDaXbCQeQWlzNFJM= -cloud.google.com/go/metastore v1.5.0/go.mod h1:2ZNrDcQwghfdtCwJ33nM0+GrBGlVuh8rakL3vdPY3XY= -cloud.google.com/go/metastore v1.6.0/go.mod h1:6cyQTls8CWXzk45G55x57DVQ9gWg7RiH65+YgPsNh9s= -cloud.google.com/go/metastore v1.7.0/go.mod h1:s45D0B4IlsINu87/AsWiEVYbLaIMeUSoxlKKDqBGFS8= -cloud.google.com/go/metastore v1.8.0/go.mod h1:zHiMc4ZUpBiM7twCIFQmJ9JMEkDSyZS9U12uf7wHqSI= -cloud.google.com/go/metastore v1.10.0/go.mod h1:fPEnH3g4JJAk+gMRnrAnoqyv2lpUCqJPWOodSaf45Eo= -cloud.google.com/go/monitoring v1.7.0/go.mod h1:HpYse6kkGo//7p6sT0wsIC6IBDET0RhIsnmlA53dvEk= -cloud.google.com/go/monitoring v1.8.0/go.mod h1:E7PtoMJ1kQXWxPjB6mv2fhC5/15jInuulFdYYtlcvT4= -cloud.google.com/go/monitoring v1.12.0/go.mod h1:yx8Jj2fZNEkL/GYZyTLS4ZtZEZN8WtDEiEqG4kLK50w= -cloud.google.com/go/monitoring v1.13.0/go.mod h1:k2yMBAB1H9JT/QETjNkgdCGD9bPF712XiLTVr+cBrpw= -cloud.google.com/go/networkconnectivity v1.4.0/go.mod h1:nOl7YL8odKyAOtzNX73/M5/mGZgqqMeryi6UPZTk/rA= -cloud.google.com/go/networkconnectivity v1.5.0/go.mod h1:3GzqJx7uhtlM3kln0+x5wyFvuVH1pIBJjhCpjzSt75o= -cloud.google.com/go/networkconnectivity v1.6.0/go.mod h1:OJOoEXW+0LAxHh89nXd64uGG+FbQoeH8DtxCHVOMlaM= -cloud.google.com/go/networkconnectivity v1.7.0/go.mod h1:RMuSbkdbPwNMQjB5HBWD5MpTBnNm39iAVpC3TmsExt8= -cloud.google.com/go/networkconnectivity v1.10.0/go.mod h1:UP4O4sWXJG13AqrTdQCD9TnLGEbtNRqjuaaA7bNjF5E= -cloud.google.com/go/networkconnectivity v1.11.0/go.mod h1:iWmDD4QF16VCDLXUqvyspJjIEtBR/4zq5hwnY2X3scM= -cloud.google.com/go/networkmanagement v1.4.0/go.mod h1:Q9mdLLRn60AsOrPc8rs8iNV6OHXaGcDdsIQe1ohekq8= -cloud.google.com/go/networkmanagement v1.5.0/go.mod h1:ZnOeZ/evzUdUsnvRt792H0uYEnHQEMaz+REhhzJRcf4= -cloud.google.com/go/networkmanagement v1.6.0/go.mod h1:5pKPqyXjB/sgtvB5xqOemumoQNB7y95Q7S+4rjSOPYY= -cloud.google.com/go/networksecurity v0.5.0/go.mod h1:xS6fOCoqpVC5zx15Z/MqkfDwH4+m/61A3ODiDV1xmiQ= -cloud.google.com/go/networksecurity v0.6.0/go.mod h1:Q5fjhTr9WMI5mbpRYEbiexTzROf7ZbDzvzCrNl14nyU= -cloud.google.com/go/networksecurity v0.7.0/go.mod h1:mAnzoxx/8TBSyXEeESMy9OOYwo1v+gZ5eMRnsT5bC8k= -cloud.google.com/go/networksecurity v0.8.0/go.mod h1:B78DkqsxFG5zRSVuwYFRZ9Xz8IcQ5iECsNrPn74hKHU= -cloud.google.com/go/notebooks v1.2.0/go.mod h1:9+wtppMfVPUeJ8fIWPOq1UnATHISkGXGqTkxeieQ6UY= -cloud.google.com/go/notebooks v1.3.0/go.mod h1:bFR5lj07DtCPC7YAAJ//vHskFBxA5JzYlH68kXVdk34= -cloud.google.com/go/notebooks v1.4.0/go.mod h1:4QPMngcwmgb6uw7Po99B2xv5ufVoIQ7nOGDyL4P8AgA= -cloud.google.com/go/notebooks v1.5.0/go.mod h1:q8mwhnP9aR8Hpfnrc5iN5IBhrXUy8S2vuYs+kBJ/gu0= -cloud.google.com/go/notebooks v1.7.0/go.mod h1:PVlaDGfJgj1fl1S3dUwhFMXFgfYGhYQt2164xOMONmE= -cloud.google.com/go/notebooks v1.8.0/go.mod h1:Lq6dYKOYOWUCTvw5t2q1gp1lAp0zxAxRycayS0iJcqQ= -cloud.google.com/go/optimization v1.1.0/go.mod h1:5po+wfvX5AQlPznyVEZjGJTMr4+CAkJf2XSTQOOl9l4= -cloud.google.com/go/optimization v1.2.0/go.mod h1:Lr7SOHdRDENsh+WXVmQhQTrzdu9ybg0NecjHidBq6xs= -cloud.google.com/go/optimization v1.3.1/go.mod h1:IvUSefKiwd1a5p0RgHDbWCIbDFgKuEdB+fPPuP0IDLI= -cloud.google.com/go/orchestration v1.3.0/go.mod h1:Sj5tq/JpWiB//X/q3Ngwdl5K7B7Y0KZ7bfv0wL6fqVA= -cloud.google.com/go/orchestration v1.4.0/go.mod h1:6W5NLFWs2TlniBphAViZEVhrXRSMgUGDfW7vrWKvsBk= -cloud.google.com/go/orchestration v1.6.0/go.mod h1:M62Bevp7pkxStDfFfTuCOaXgaaqRAga1yKyoMtEoWPQ= -cloud.google.com/go/orgpolicy v1.4.0/go.mod h1:xrSLIV4RePWmP9P3tBl8S93lTmlAxjm06NSm2UTmKvE= -cloud.google.com/go/orgpolicy v1.5.0/go.mod h1:hZEc5q3wzwXJaKrsx5+Ewg0u1LxJ51nNFlext7Tanwc= -cloud.google.com/go/orgpolicy v1.10.0/go.mod h1:w1fo8b7rRqlXlIJbVhOMPrwVljyuW5mqssvBtU18ONc= -cloud.google.com/go/osconfig v1.7.0/go.mod h1:oVHeCeZELfJP7XLxcBGTMBvRO+1nQ5tFG9VQTmYS2Fs= -cloud.google.com/go/osconfig v1.8.0/go.mod h1:EQqZLu5w5XA7eKizepumcvWx+m8mJUhEwiPqWiZeEdg= -cloud.google.com/go/osconfig v1.9.0/go.mod h1:Yx+IeIZJ3bdWmzbQU4fxNl8xsZ4amB+dygAwFPlvnNo= -cloud.google.com/go/osconfig v1.10.0/go.mod h1:uMhCzqC5I8zfD9zDEAfvgVhDS8oIjySWh+l4WK6GnWw= -cloud.google.com/go/osconfig v1.11.0/go.mod h1:aDICxrur2ogRd9zY5ytBLV89KEgT2MKB2L/n6x1ooPw= -cloud.google.com/go/oslogin v1.4.0/go.mod h1:YdgMXWRaElXz/lDk1Na6Fh5orF7gvmJ0FGLIs9LId4E= -cloud.google.com/go/oslogin v1.5.0/go.mod h1:D260Qj11W2qx/HVF29zBg+0fd6YCSjSqLUkY/qEenQU= -cloud.google.com/go/oslogin v1.6.0/go.mod h1:zOJ1O3+dTU8WPlGEkFSh7qeHPPSoxrcMbbK1Nm2iX70= -cloud.google.com/go/oslogin v1.7.0/go.mod h1:e04SN0xO1UNJ1M5GP0vzVBFicIe4O53FOfcixIqTyXo= -cloud.google.com/go/oslogin v1.9.0/go.mod h1:HNavntnH8nzrn8JCTT5fj18FuJLFJc4NaZJtBnQtKFs= -cloud.google.com/go/phishingprotection v0.5.0/go.mod h1:Y3HZknsK9bc9dMi+oE8Bim0lczMU6hrX0UpADuMefr0= -cloud.google.com/go/phishingprotection v0.6.0/go.mod h1:9Y3LBLgy0kDTcYET8ZH3bq/7qni15yVUoAxiFxnlSUA= -cloud.google.com/go/phishingprotection v0.7.0/go.mod h1:8qJI4QKHoda/sb/7/YmMQ2omRLSLYSu9bU0EKCNI+Lk= -cloud.google.com/go/policytroubleshooter v1.3.0/go.mod h1:qy0+VwANja+kKrjlQuOzmlvscn4RNsAc0e15GGqfMxg= -cloud.google.com/go/policytroubleshooter v1.4.0/go.mod h1:DZT4BcRw3QoO8ota9xw/LKtPa8lKeCByYeKTIf/vxdE= -cloud.google.com/go/policytroubleshooter v1.5.0/go.mod h1:Rz1WfV+1oIpPdN2VvvuboLVRsB1Hclg3CKQ53j9l8vw= -cloud.google.com/go/policytroubleshooter v1.6.0/go.mod h1:zYqaPTsmfvpjm5ULxAyD/lINQxJ0DDsnWOP/GZ7xzBc= -cloud.google.com/go/privatecatalog v0.5.0/go.mod h1:XgosMUvvPyxDjAVNDYxJ7wBW8//hLDDYmnsNcMGq1K0= -cloud.google.com/go/privatecatalog v0.6.0/go.mod h1:i/fbkZR0hLN29eEWiiwue8Pb+GforiEIBnV9yrRUOKI= -cloud.google.com/go/privatecatalog v0.7.0/go.mod h1:2s5ssIFO69F5csTXcwBP7NPFTZvps26xGzvQ2PQaBYg= -cloud.google.com/go/privatecatalog v0.8.0/go.mod h1:nQ6pfaegeDAq/Q5lrfCQzQLhubPiZhSaNhIgfJlnIXs= cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/pubsub v1.26.0/go.mod h1:QgBH3U/jdJy/ftjPhTkyXNj543Tin1pRYcdcPRnFIRI= -cloud.google.com/go/pubsub v1.27.1/go.mod h1:hQN39ymbV9geqBnfQq6Xf63yNhUAhv9CZhzp5O6qsW0= -cloud.google.com/go/pubsub v1.28.0/go.mod h1:vuXFpwaVoIPQMGXqRyUQigu/AX1S3IWugR9xznmcXX8= -cloud.google.com/go/pubsub v1.30.0/go.mod h1:qWi1OPS0B+b5L+Sg6Gmc9zD1Y+HaM0MdUr7LsupY1P4= -cloud.google.com/go/pubsublite v1.5.0/go.mod h1:xapqNQ1CuLfGi23Yda/9l4bBCKz/wC3KIJ5gKcxveZg= -cloud.google.com/go/pubsublite v1.6.0/go.mod h1:1eFCS0U11xlOuMFV/0iBqw3zP12kddMeCbj/F3FSj9k= -cloud.google.com/go/pubsublite v1.7.0/go.mod h1:8hVMwRXfDfvGm3fahVbtDbiLePT3gpoiJYJY+vxWxVM= -cloud.google.com/go/recaptchaenterprise v1.3.1/go.mod h1:OdD+q+y4XGeAlxRaMn1Y7/GveP6zmq76byL6tjPE7d4= -cloud.google.com/go/recaptchaenterprise/v2 v2.1.0/go.mod h1:w9yVqajwroDNTfGuhmOjPDN//rZGySaf6PtFVcSCa7o= -cloud.google.com/go/recaptchaenterprise/v2 v2.2.0/go.mod h1:/Zu5jisWGeERrd5HnlS3EUGb/D335f9k51B/FVil0jk= -cloud.google.com/go/recaptchaenterprise/v2 v2.3.0/go.mod h1:O9LwGCjrhGHBQET5CA7dd5NwwNQUErSgEDit1DLNTdo= -cloud.google.com/go/recaptchaenterprise/v2 v2.4.0/go.mod h1:Am3LHfOuBstrLrNCBrlI5sbwx9LBg3te2N6hGvHn2mE= -cloud.google.com/go/recaptchaenterprise/v2 v2.5.0/go.mod h1:O8LzcHXN3rz0j+LBC91jrwI3R+1ZSZEWrfL7XHgNo9U= -cloud.google.com/go/recaptchaenterprise/v2 v2.6.0/go.mod h1:RPauz9jeLtB3JVzg6nCbe12qNoaa8pXc4d/YukAmcnA= -cloud.google.com/go/recaptchaenterprise/v2 v2.7.0/go.mod h1:19wVj/fs5RtYtynAPJdDTb69oW0vNHYDBTbB4NvMD9c= -cloud.google.com/go/recommendationengine v0.5.0/go.mod h1:E5756pJcVFeVgaQv3WNpImkFP8a+RptV6dDLGPILjvg= -cloud.google.com/go/recommendationengine v0.6.0/go.mod h1:08mq2umu9oIqc7tDy8sx+MNJdLG0fUi3vaSVbztHgJ4= -cloud.google.com/go/recommendationengine v0.7.0/go.mod h1:1reUcE3GIu6MeBz/h5xZJqNLuuVjNg1lmWMPyjatzac= -cloud.google.com/go/recommender v1.5.0/go.mod h1:jdoeiBIVrJe9gQjwd759ecLJbxCDED4A6p+mqoqDvTg= -cloud.google.com/go/recommender v1.6.0/go.mod h1:+yETpm25mcoiECKh9DEScGzIRyDKpZ0cEhWGo+8bo+c= -cloud.google.com/go/recommender v1.7.0/go.mod h1:XLHs/W+T8olwlGOgfQenXBTbIseGclClff6lhFVe9Bs= -cloud.google.com/go/recommender v1.8.0/go.mod h1:PkjXrTT05BFKwxaUxQmtIlrtj0kph108r02ZZQ5FE70= -cloud.google.com/go/recommender v1.9.0/go.mod h1:PnSsnZY7q+VL1uax2JWkt/UegHssxjUVVCrX52CuEmQ= -cloud.google.com/go/redis v1.7.0/go.mod h1:V3x5Jq1jzUcg+UNsRvdmsfuFnit1cfe3Z/PGyq/lm4Y= -cloud.google.com/go/redis v1.8.0/go.mod h1:Fm2szCDavWzBk2cDKxrkmWBqoCiL1+Ctwq7EyqBCA/A= -cloud.google.com/go/redis v1.9.0/go.mod h1:HMYQuajvb2D0LvMgZmLDZW8V5aOC/WxstZHiy4g8OiA= -cloud.google.com/go/redis v1.10.0/go.mod h1:ThJf3mMBQtW18JzGgh41/Wld6vnDDc/F/F35UolRZPM= -cloud.google.com/go/redis v1.11.0/go.mod h1:/X6eicana+BWcUda5PpwZC48o37SiFVTFSs0fWAJ7uQ= -cloud.google.com/go/resourcemanager v1.3.0/go.mod h1:bAtrTjZQFJkiWTPDb1WBjzvc6/kifjj4QBYuKCCoqKA= -cloud.google.com/go/resourcemanager v1.4.0/go.mod h1:MwxuzkumyTX7/a3n37gmsT3py7LIXwrShilPh3P1tR0= -cloud.google.com/go/resourcemanager v1.5.0/go.mod h1:eQoXNAiAvCf5PXxWxXjhKQoTMaUSNrEfg+6qdf/wots= -cloud.google.com/go/resourcemanager v1.6.0/go.mod h1:YcpXGRs8fDzcUl1Xw8uOVmI8JEadvhRIkoXXUNVYcVo= -cloud.google.com/go/resourcemanager v1.7.0/go.mod h1:HlD3m6+bwhzj9XCouqmeiGuni95NTrExfhoSrkC/3EI= -cloud.google.com/go/resourcesettings v1.3.0/go.mod h1:lzew8VfESA5DQ8gdlHwMrqZs1S9V87v3oCnKCWoOuQU= -cloud.google.com/go/resourcesettings v1.4.0/go.mod h1:ldiH9IJpcrlC3VSuCGvjR5of/ezRrOxFtpJoJo5SmXg= -cloud.google.com/go/resourcesettings v1.5.0/go.mod h1:+xJF7QSG6undsQDfsCJyqWXyBwUoJLhetkRMDRnIoXA= -cloud.google.com/go/retail v1.8.0/go.mod h1:QblKS8waDmNUhghY2TI9O3JLlFk8jybHeV4BF19FrE4= -cloud.google.com/go/retail v1.9.0/go.mod h1:g6jb6mKuCS1QKnH/dpu7isX253absFl6iE92nHwlBUY= -cloud.google.com/go/retail v1.10.0/go.mod h1:2gDk9HsL4HMS4oZwz6daui2/jmKvqShXKQuB2RZ+cCc= -cloud.google.com/go/retail v1.11.0/go.mod h1:MBLk1NaWPmh6iVFSz9MeKG/Psyd7TAgm6y/9L2B4x9Y= -cloud.google.com/go/retail v1.12.0/go.mod h1:UMkelN/0Z8XvKymXFbD4EhFJlYKRx1FGhQkVPU5kF14= -cloud.google.com/go/run v0.2.0/go.mod h1:CNtKsTA1sDcnqqIFR3Pb5Tq0usWxJJvsWOCPldRU3Do= -cloud.google.com/go/run v0.3.0/go.mod h1:TuyY1+taHxTjrD0ZFk2iAR+xyOXEA0ztb7U3UNA0zBo= -cloud.google.com/go/run v0.8.0/go.mod h1:VniEnuBwqjigv0A7ONfQUaEItaiCRVujlMqerPPiktM= -cloud.google.com/go/run v0.9.0/go.mod h1:Wwu+/vvg8Y+JUApMwEDfVfhetv30hCG4ZwDR/IXl2Qg= -cloud.google.com/go/scheduler v1.4.0/go.mod h1:drcJBmxF3aqZJRhmkHQ9b3uSSpQoltBPGPxGAWROx6s= -cloud.google.com/go/scheduler v1.5.0/go.mod h1:ri073ym49NW3AfT6DZi21vLZrG07GXr5p3H1KxN5QlI= -cloud.google.com/go/scheduler v1.6.0/go.mod h1:SgeKVM7MIwPn3BqtcBntpLyrIJftQISRrYB5ZtT+KOk= -cloud.google.com/go/scheduler v1.7.0/go.mod h1:jyCiBqWW956uBjjPMMuX09n3x37mtyPJegEWKxRsn44= -cloud.google.com/go/scheduler v1.8.0/go.mod h1:TCET+Y5Gp1YgHT8py4nlg2Sew8nUHMqcpousDgXJVQc= -cloud.google.com/go/scheduler v1.9.0/go.mod h1:yexg5t+KSmqu+njTIh3b7oYPheFtBWGcbVUYF1GGMIc= -cloud.google.com/go/secretmanager v1.6.0/go.mod h1:awVa/OXF6IiyaU1wQ34inzQNc4ISIDIrId8qE5QGgKA= -cloud.google.com/go/secretmanager v1.8.0/go.mod h1:hnVgi/bN5MYHd3Gt0SPuTPPp5ENina1/LxM+2W9U9J4= -cloud.google.com/go/secretmanager v1.9.0/go.mod h1:b71qH2l1yHmWQHt9LC80akm86mX8AL6X1MA01dW8ht4= -cloud.google.com/go/secretmanager v1.10.0/go.mod h1:MfnrdvKMPNra9aZtQFvBcvRU54hbPD8/HayQdlUgJpU= -cloud.google.com/go/security v1.5.0/go.mod h1:lgxGdyOKKjHL4YG3/YwIL2zLqMFCKs0UbQwgyZmfJl4= -cloud.google.com/go/security v1.7.0/go.mod h1:mZklORHl6Bg7CNnnjLH//0UlAlaXqiG7Lb9PsPXLfD0= -cloud.google.com/go/security v1.8.0/go.mod h1:hAQOwgmaHhztFhiQ41CjDODdWP0+AE1B3sX4OFlq+GU= -cloud.google.com/go/security v1.9.0/go.mod h1:6Ta1bO8LXI89nZnmnsZGp9lVoVWXqsVbIq/t9dzI+2Q= -cloud.google.com/go/security v1.10.0/go.mod h1:QtOMZByJVlibUT2h9afNDWRZ1G96gVywH8T5GUSb9IA= -cloud.google.com/go/security v1.12.0/go.mod h1:rV6EhrpbNHrrxqlvW0BWAIawFWq3X90SduMJdFwtLB8= -cloud.google.com/go/security v1.13.0/go.mod h1:Q1Nvxl1PAgmeW0y3HTt54JYIvUdtcpYKVfIB8AOMZ+0= -cloud.google.com/go/securitycenter v1.13.0/go.mod h1:cv5qNAqjY84FCN6Y9z28WlkKXyWsgLO832YiWwkCWcU= -cloud.google.com/go/securitycenter v1.14.0/go.mod h1:gZLAhtyKv85n52XYWt6RmeBdydyxfPeTrpToDPw4Auc= -cloud.google.com/go/securitycenter v1.15.0/go.mod h1:PeKJ0t8MoFmmXLXWm41JidyzI3PJjd8sXWaVqg43WWk= -cloud.google.com/go/securitycenter v1.16.0/go.mod h1:Q9GMaLQFUD+5ZTabrbujNWLtSLZIZF7SAR0wWECrjdk= -cloud.google.com/go/securitycenter v1.18.1/go.mod h1:0/25gAzCM/9OL9vVx4ChPeM/+DlfGQJDwBy/UC8AKK0= -cloud.google.com/go/securitycenter v1.19.0/go.mod h1:LVLmSg8ZkkyaNy4u7HCIshAngSQ8EcIRREP3xBnyfag= -cloud.google.com/go/servicecontrol v1.4.0/go.mod h1:o0hUSJ1TXJAmi/7fLJAedOovnujSEvjKCAFNXPQ1RaU= -cloud.google.com/go/servicecontrol v1.5.0/go.mod h1:qM0CnXHhyqKVuiZnGKrIurvVImCs8gmqWsDoqe9sU1s= -cloud.google.com/go/servicecontrol v1.10.0/go.mod h1:pQvyvSRh7YzUF2efw7H87V92mxU8FnFDawMClGCNuAA= -cloud.google.com/go/servicecontrol v1.11.0/go.mod h1:kFmTzYzTUIuZs0ycVqRHNaNhgR+UMUpw9n02l/pY+mc= -cloud.google.com/go/servicecontrol v1.11.1/go.mod h1:aSnNNlwEFBY+PWGQ2DoM0JJ/QUXqV5/ZD9DOLB7SnUk= -cloud.google.com/go/servicedirectory v1.4.0/go.mod h1:gH1MUaZCgtP7qQiI+F+A+OpeKF/HQWgtAddhTbhL2bs= -cloud.google.com/go/servicedirectory v1.5.0/go.mod h1:QMKFL0NUySbpZJ1UZs3oFAmdvVxhhxB6eJ/Vlp73dfg= -cloud.google.com/go/servicedirectory v1.6.0/go.mod h1:pUlbnWsLH9c13yGkxCmfumWEPjsRs1RlmJ4pqiNjVL4= -cloud.google.com/go/servicedirectory v1.7.0/go.mod h1:5p/U5oyvgYGYejufvxhgwjL8UVXjkuw7q5XcG10wx1U= -cloud.google.com/go/servicedirectory v1.8.0/go.mod h1:srXodfhY1GFIPvltunswqXpVxFPpZjf8nkKQT7XcXaY= -cloud.google.com/go/servicedirectory v1.9.0/go.mod h1:29je5JjiygNYlmsGz8k6o+OZ8vd4f//bQLtvzkPPT/s= -cloud.google.com/go/servicemanagement v1.4.0/go.mod h1:d8t8MDbezI7Z2R1O/wu8oTggo3BI2GKYbdG4y/SJTco= -cloud.google.com/go/servicemanagement v1.5.0/go.mod h1:XGaCRe57kfqu4+lRxaFEAuqmjzF0r+gWHjWqKqBvKFo= -cloud.google.com/go/servicemanagement v1.6.0/go.mod h1:aWns7EeeCOtGEX4OvZUWCCJONRZeFKiptqKf1D0l/Jc= -cloud.google.com/go/servicemanagement v1.8.0/go.mod h1:MSS2TDlIEQD/fzsSGfCdJItQveu9NXnUniTrq/L8LK4= -cloud.google.com/go/serviceusage v1.3.0/go.mod h1:Hya1cozXM4SeSKTAgGXgj97GlqUvF5JaoXacR1JTP/E= -cloud.google.com/go/serviceusage v1.4.0/go.mod h1:SB4yxXSaYVuUBYUml6qklyONXNLt83U0Rb+CXyhjEeU= -cloud.google.com/go/serviceusage v1.5.0/go.mod h1:w8U1JvqUqwJNPEOTQjrMHkw3IaIFLoLsPLvsE3xueec= -cloud.google.com/go/serviceusage v1.6.0/go.mod h1:R5wwQcbOWsyuOfbP9tGdAnCAc6B9DRwPG1xtWMDeuPA= -cloud.google.com/go/shell v1.3.0/go.mod h1:VZ9HmRjZBsjLGXusm7K5Q5lzzByZmJHf1d0IWHEN5X4= -cloud.google.com/go/shell v1.4.0/go.mod h1:HDxPzZf3GkDdhExzD/gs8Grqk+dmYcEjGShZgYa9URw= -cloud.google.com/go/shell v1.6.0/go.mod h1:oHO8QACS90luWgxP3N9iZVuEiSF84zNyLytb+qE2f9A= -cloud.google.com/go/spanner v1.41.0/go.mod h1:MLYDBJR/dY4Wt7ZaMIQ7rXOTLjYrmxLE/5ve9vFfWos= -cloud.google.com/go/spanner v1.44.0/go.mod h1:G8XIgYdOK+Fbcpbs7p2fiprDw4CaZX63whnSMLVBxjk= -cloud.google.com/go/spanner v1.45.0/go.mod h1:FIws5LowYz8YAE1J8fOS7DJup8ff7xJeetWEo5REA2M= -cloud.google.com/go/speech v1.6.0/go.mod h1:79tcr4FHCimOp56lwC01xnt/WPJZc4v3gzyT7FoBkCM= -cloud.google.com/go/speech v1.7.0/go.mod h1:KptqL+BAQIhMsj1kOP2la5DSEEerPDuOP/2mmkhHhZQ= -cloud.google.com/go/speech v1.8.0/go.mod h1:9bYIl1/tjsAnMgKGHKmBZzXKEkGgtU+MpdDPTE9f7y0= -cloud.google.com/go/speech v1.9.0/go.mod h1:xQ0jTcmnRFFM2RfX/U+rk6FQNUF6DQlydUSyoooSpco= -cloud.google.com/go/speech v1.14.1/go.mod h1:gEosVRPJ9waG7zqqnsHpYTOoAS4KouMRLDFMekpJ0J0= -cloud.google.com/go/speech v1.15.0/go.mod h1:y6oH7GhqCaZANH7+Oe0BhgIogsNInLlz542tg3VqeYI= cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.22.1/go.mod h1:S8N1cAStu7BOeFfE8KAQzmyyLkK8p/vmRq6kuBTW58Y= -cloud.google.com/go/storage v1.23.0/go.mod h1:vOEEDNFnciUMhBeT6hsJIn3ieU5cFRmzeLgDvXzfIXc= -cloud.google.com/go/storage v1.27.0/go.mod h1:x9DOL8TK/ygDUMieqwfhdpQryTeEkhGKMi80i/iqR2s= -cloud.google.com/go/storage v1.28.1/go.mod h1:Qnisd4CqDdo6BGs2AD5LLnEsmSQ80wQ5ogcBBKhU86Y= -cloud.google.com/go/storage v1.29.0/go.mod h1:4puEjyTKnku6gfKoTfNOU/W+a9JyuVNxjpS5GBrB8h4= -cloud.google.com/go/storagetransfer v1.5.0/go.mod h1:dxNzUopWy7RQevYFHewchb29POFv3/AaBgnhqzqiK0w= -cloud.google.com/go/storagetransfer v1.6.0/go.mod h1:y77xm4CQV/ZhFZH75PLEXY0ROiS7Gh6pSKrM8dJyg6I= -cloud.google.com/go/storagetransfer v1.7.0/go.mod h1:8Giuj1QNb1kfLAiWM1bN6dHzfdlDAVC9rv9abHot2W4= -cloud.google.com/go/storagetransfer v1.8.0/go.mod h1:JpegsHHU1eXg7lMHkvf+KE5XDJ7EQu0GwNJbbVGanEw= -cloud.google.com/go/talent v1.1.0/go.mod h1:Vl4pt9jiHKvOgF9KoZo6Kob9oV4lwd/ZD5Cto54zDRw= -cloud.google.com/go/talent v1.2.0/go.mod h1:MoNF9bhFQbiJ6eFD3uSsg0uBALw4n4gaCaEjBw9zo8g= -cloud.google.com/go/talent v1.3.0/go.mod h1:CmcxwJ/PKfRgd1pBjQgU6W3YBwiewmUzQYH5HHmSCmM= -cloud.google.com/go/talent v1.4.0/go.mod h1:ezFtAgVuRf8jRsvyE6EwmbTK5LKciD4KVnHuDEFmOOA= -cloud.google.com/go/talent v1.5.0/go.mod h1:G+ODMj9bsasAEJkQSzO2uHQWXHHXUomArjWQQYkqK6c= -cloud.google.com/go/texttospeech v1.4.0/go.mod h1:FX8HQHA6sEpJ7rCMSfXuzBcysDAuWusNNNvN9FELDd8= -cloud.google.com/go/texttospeech v1.5.0/go.mod h1:oKPLhR4n4ZdQqWKURdwxMy0uiTS1xU161C8W57Wkea4= -cloud.google.com/go/texttospeech v1.6.0/go.mod h1:YmwmFT8pj1aBblQOI3TfKmwibnsfvhIBzPXcW4EBovc= -cloud.google.com/go/tpu v1.3.0/go.mod h1:aJIManG0o20tfDQlRIej44FcwGGl/cD0oiRyMKG19IQ= -cloud.google.com/go/tpu v1.4.0/go.mod h1:mjZaX8p0VBgllCzF6wcU2ovUXN9TONFLd7iz227X2Xg= -cloud.google.com/go/tpu v1.5.0/go.mod h1:8zVo1rYDFuW2l4yZVY0R0fb/v44xLh3llq7RuV61fPM= -cloud.google.com/go/trace v1.3.0/go.mod h1:FFUE83d9Ca57C+K8rDl/Ih8LwOzWIV1krKgxg6N0G28= -cloud.google.com/go/trace v1.4.0/go.mod h1:UG0v8UBqzusp+z63o7FK74SdFE+AXpCLdFb1rshXG+Y= -cloud.google.com/go/trace v1.8.0/go.mod h1:zH7vcsbAhklH8hWFig58HvxcxyQbaIqMarMg9hn5ECA= -cloud.google.com/go/trace v1.9.0/go.mod h1:lOQqpE5IaWY0Ixg7/r2SjixMuc6lfTFeO4QGM4dQWOk= -cloud.google.com/go/translate v1.3.0/go.mod h1:gzMUwRjvOqj5i69y/LYLd8RrNQk+hOmIXTi9+nb3Djs= -cloud.google.com/go/translate v1.4.0/go.mod h1:06Dn/ppvLD6WvA5Rhdp029IX2Mi3Mn7fpMRLPvXT5Wg= -cloud.google.com/go/translate v1.5.0/go.mod h1:29YDSYveqqpA1CQFD7NQuP49xymq17RXNaUDdc0mNu0= -cloud.google.com/go/translate v1.6.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/translate v1.7.0/go.mod h1:lMGRudH1pu7I3n3PETiOB2507gf3HnfLV8qlkHZEyos= -cloud.google.com/go/video v1.8.0/go.mod h1:sTzKFc0bUSByE8Yoh8X0mn8bMymItVGPfTuUBUyRgxk= -cloud.google.com/go/video v1.9.0/go.mod h1:0RhNKFRF5v92f8dQt0yhaHrEuH95m068JYOvLZYnJSw= -cloud.google.com/go/video v1.12.0/go.mod h1:MLQew95eTuaNDEGriQdcYn0dTwf9oWiA4uYebxM5kdg= -cloud.google.com/go/video v1.13.0/go.mod h1:ulzkYlYgCp15N2AokzKjy7MQ9ejuynOJdf1tR5lGthk= -cloud.google.com/go/video v1.14.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/video v1.15.0/go.mod h1:SkgaXwT+lIIAKqWAJfktHT/RbgjSuY6DobxEp0C5yTQ= -cloud.google.com/go/videointelligence v1.6.0/go.mod h1:w0DIDlVRKtwPCn/C4iwZIJdvC69yInhW0cfi+p546uU= -cloud.google.com/go/videointelligence v1.7.0/go.mod h1:k8pI/1wAhjznARtVT9U1llUaFNPh7muw8QyOUpavru4= -cloud.google.com/go/videointelligence v1.8.0/go.mod h1:dIcCn4gVDdS7yte/w+koiXn5dWVplOZkE+xwG9FgK+M= -cloud.google.com/go/videointelligence v1.9.0/go.mod h1:29lVRMPDYHikk3v8EdPSaL8Ku+eMzDljjuvRs105XoU= -cloud.google.com/go/videointelligence v1.10.0/go.mod h1:LHZngX1liVtUhZvi2uNS0VQuOzNi2TkY1OakiuoUOjU= -cloud.google.com/go/vision v1.2.0/go.mod h1:SmNwgObm5DpFBme2xpyOyasvBc1aPdjvMk2bBk0tKD0= -cloud.google.com/go/vision/v2 v2.2.0/go.mod h1:uCdV4PpN1S0jyCyq8sIM42v2Y6zOLkZs+4R9LrGYwFo= -cloud.google.com/go/vision/v2 v2.3.0/go.mod h1:UO61abBx9QRMFkNBbf1D8B1LXdS2cGiiCRx0vSpZoUo= -cloud.google.com/go/vision/v2 v2.4.0/go.mod h1:VtI579ll9RpVTrdKdkMzckdnwMyX2JILb+MhPqRbPsY= -cloud.google.com/go/vision/v2 v2.5.0/go.mod h1:MmaezXOOE+IWa+cS7OhRRLK2cNv1ZL98zhqFFZaaH2E= -cloud.google.com/go/vision/v2 v2.6.0/go.mod h1:158Hes0MvOS9Z/bDMSFpjwsUrZ5fPrdwuyyvKSGAGMY= -cloud.google.com/go/vision/v2 v2.7.0/go.mod h1:H89VysHy21avemp6xcf9b9JvZHVehWbET0uT/bcuY/0= -cloud.google.com/go/vmmigration v1.2.0/go.mod h1:IRf0o7myyWFSmVR1ItrBSFLFD/rJkfDCUTO4vLlJvsE= -cloud.google.com/go/vmmigration v1.3.0/go.mod h1:oGJ6ZgGPQOFdjHuocGcLqX4lc98YQ7Ygq8YQwHh9A7g= -cloud.google.com/go/vmmigration v1.5.0/go.mod h1:E4YQ8q7/4W9gobHjQg4JJSgXXSgY21nA5r8swQV+Xxc= -cloud.google.com/go/vmmigration v1.6.0/go.mod h1:bopQ/g4z+8qXzichC7GW1w2MjbErL54rk3/C843CjfY= -cloud.google.com/go/vmwareengine v0.1.0/go.mod h1:RsdNEf/8UDvKllXhMz5J40XxDrNJNN4sagiox+OI208= -cloud.google.com/go/vmwareengine v0.2.2/go.mod h1:sKdctNJxb3KLZkE/6Oui94iw/xs9PRNC2wnNLXsHvH8= -cloud.google.com/go/vmwareengine v0.3.0/go.mod h1:wvoyMvNWdIzxMYSpH/R7y2h5h3WFkx6d+1TIsP39WGY= -cloud.google.com/go/vpcaccess v1.4.0/go.mod h1:aQHVbTWDYUR1EbTApSVvMq1EnT57ppDmQzZ3imqIk4w= -cloud.google.com/go/vpcaccess v1.5.0/go.mod h1:drmg4HLk9NkZpGfCmZ3Tz0Bwnm2+DKqViEpeEpOq0m8= -cloud.google.com/go/vpcaccess v1.6.0/go.mod h1:wX2ILaNhe7TlVa4vC5xce1bCnqE3AeH27RV31lnmZes= -cloud.google.com/go/webrisk v1.4.0/go.mod h1:Hn8X6Zr+ziE2aNd8SliSDWpEnSS1u4R9+xXZmFiHmGE= -cloud.google.com/go/webrisk v1.5.0/go.mod h1:iPG6fr52Tv7sGk0H6qUFzmL3HHZev1htXuWDEEsqMTg= -cloud.google.com/go/webrisk v1.6.0/go.mod h1:65sW9V9rOosnc9ZY7A7jsy1zoHS5W9IAXv6dGqhMQMc= -cloud.google.com/go/webrisk v1.7.0/go.mod h1:mVMHgEYH0r337nmt1JyLthzMr6YxwN1aAIEc2fTcq7A= -cloud.google.com/go/webrisk v1.8.0/go.mod h1:oJPDuamzHXgUc+b8SiHRcVInZQuybnvEW72PqTc7sSg= -cloud.google.com/go/websecurityscanner v1.3.0/go.mod h1:uImdKm2wyeXQevQJXeh8Uun/Ym1VqworNDlBXQevGMo= -cloud.google.com/go/websecurityscanner v1.4.0/go.mod h1:ebit/Fp0a+FWu5j4JOmJEV8S8CzdTkAS77oDsiSqYWQ= -cloud.google.com/go/websecurityscanner v1.5.0/go.mod h1:Y6xdCPy81yi0SQnDY1xdNTNpfY1oAgXUlcfN3B3eSng= -cloud.google.com/go/workflows v1.6.0/go.mod h1:6t9F5h/unJz41YqfBmqSASJSXccBLtD1Vwf+KmJENM0= -cloud.google.com/go/workflows v1.7.0/go.mod h1:JhSrZuVZWuiDfKEFxU0/F1PQjmpnpcoISEXH2bcHC3M= -cloud.google.com/go/workflows v1.8.0/go.mod h1:ysGhmEajwZxGn1OhGOGKsTXc5PyxOc0vfKf5Af+to4M= -cloud.google.com/go/workflows v1.9.0/go.mod h1:ZGkj1aFIOd9c8Gerkjjq7OW7I5+l6cSvT3ujaO/WwSA= -cloud.google.com/go/workflows v1.10.0/go.mod h1:fZ8LmRmZQWacon9UCX1r/g/DfAXx5VcPALq2CxzdePw= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -gioui.org v0.0.0-20210308172011-57750fc8a0a6/go.mod h1:RSH6KIUZ0p2xy5zHDxgAM4zumjgTw83q2ge/PI+yyw8= -git.sr.ht/~sbinet/gg v0.3.1/go.mod h1:KGYtlADtqsqANL9ueOFkWymvzUvLMQllU5Ixo+8v3pc= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c h1:pxW6RcqyfI9/kWtOwnv/G+AzdKuy2ZrqINhenH4HyNs= github.com/BurntSushi/toml v1.4.1-0.20240526193622-a339e1f7089c/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/JohnCGriffin/overflow v0.0.0-20211019200055-46fa312c352c/go.mod h1:X0CRv0ky0k6m906ixxpzmDRLvX58TFUKS2eePweuyxk= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= -github.com/Masterminds/semver/v3 v3.3.0 h1:B8LGeaivUe71a5qox1ICM/JLl0NqZSW5CHyL+hmvYS0= -github.com/Masterminds/semver/v3 v3.3.0/go.mod h1:4V+yj/TJE1HU9XfppCwVMZq3I84lprf4nC11bSS5beM= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e h1:n+DcnTNkQnHlwpsrHoQtkrJIO7CBx029fw6oR4vIob4= @@ -614,62 +45,55 @@ github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82 h1:MG93+PZYs9 github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82/go.mod h1:GbuBk21JqF+driLX3XtJYNZjGa45YDoa9IqCTzNSfEc= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5/go.mod h1:lmUJ/7eu/Q8D7ML55dXQrVaamCz2vxCfdQBasLZfHKk= -github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344 h1:cDVUiFo+npB0ZASqnw4q90ylaVAbnYyx0JYqK4YcGok= github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344/go.mod h1:9pIqrY6SXNL8vjRQE5Hd/OL5GyK/9MrGUWs87z/eFfk= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY= github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA= github.com/aead/siphash v1.0.1 h1:FwHfE/T45KPKYuuSAKyyvE+oPWcaQ+CUmFW0bPlM+kg= github.com/aead/siphash v1.0.1/go.mod h1:Nywa3cDsYNNK3gaciGTWPwHt0wlpNV15vwmswBAUSII= -github.com/ajstarks/deck v0.0.0-20200831202436-30c9fc6549a9/go.mod h1:JynElWSGnm/4RlzPXRlREEwqTHAN3T56Bv2ITsFT3gY= -github.com/ajstarks/deck/generate v0.0.0-20210309230005-c3f852c02e19/go.mod h1:T13YZdzov6OU0A1+RfKZiZN9ca6VeKdBdyDV+BY97Tk= -github.com/ajstarks/svgo v0.0.0-20211024235047-1546f124cd8b/go.mod h1:1KcenG0jGWcpt8ov532z81sp/kMMUG485J2InIOyADM= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= -github.com/apache/arrow/go/v10 v10.0.1/go.mod h1:YvhnlEePVnBS4+0z3fhPfUy7W1Ikj0Ih0vcRo/gZ1M0= -github.com/apache/arrow/go/v11 v11.0.0/go.mod h1:Eg5OsL5H+e299f7u5ssuXsuHQVEGC4xei5aX110hRiI= -github.com/apache/thrift v0.16.0/go.mod h1:PHK3hniurgQaNMZYaCLEqXKsYK8upmhPbmdP2FXSqgU= github.com/benbjohnson/clock v1.1.0 h1:Q92kusRqC1XV2MjkWETPvjJVqKetz1OzxZB7mHJLju8= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+CedLV8= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/boombuler/barcode v1.0.0/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= -github.com/boombuler/barcode v1.0.1/go.mod h1:paBWMcWSl3LHKBqUq+rly7CNSldXjb2rDl3JlRe0mD8= github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= -github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 h1:8n9k3I7e8DkpdQ5YAP4j8ly/LSsbe6qX9vmVbrUGvVw= -github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6/go.mod h1:OmM4kFtB0klaG/ZqT86rQiyw/1iyXlJgc3UHClPhhbs= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd v0.25.1-0.20260310163610-1c55c7c18179 h1:yJOTxkbxxtuSFrErMqYRvqZLfWggHssioBiWebkV9yo= +github.com/btcsuite/btcd v0.25.1-0.20260310163610-1c55c7c18179/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= -github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.6 h1:IzlsEr9olcSRKB/n7c4351F3xHKxS2lma+1UFGCYd4E= +github.com/btcsuite/btcd/btcec/v2 v2.3.6/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= -github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= +github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= +github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= github.com/btcsuite/btcd/btcutil/psbt v1.1.10 h1:TC1zhxhFfhnGqoPjsrlEpoqzh+9TPOHrCgnPR47Mj9I= github.com/btcsuite/btcd/btcutil/psbt v1.1.10/go.mod h1:ehBEvU91lxSlXtA+zZz3iFYx7Yq9eqnKx4/kSrnsvMY= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/v2transport v1.0.1 h1:pIyyyBCPwd087K3Wdb/9tIvUubAQdzTJghjPgzTQVsE= +github.com/btcsuite/btcd/v2transport v1.0.1/go.mod h1:N6H0HGSElVVJKntzaYHYVbW71DtWDLMw2yhwVRO3ZOE= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzgaNgrCPsFWd7cGYNpeFUgd9ZIgyM0= -github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= +github.com/btcsuite/btclog v1.0.0 h1:sEkpKJMmfGiyZjADwEIgB1NSwMyfdD1FB8v6+w1T0Ns= +github.com/btcsuite/btclog v1.0.0/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b h1:MQ+Q6sDy37V1wP1Yu79A5KqJutolqUGwA99UZWQDWZM= github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcwallet v0.16.17 h1:1N6lHznRdcjDopBvcofxaIHknArkJ/EcVKgLKfGL4Dg= -github.com/btcsuite/btcwallet v0.16.17/go.mod h1:YO+W745BAH8n/Rpgj68QsLR6eLlgM4W2do4RejT0buo= +github.com/btcsuite/btcwallet v0.16.18 h1:6h0kMxij4igPu35jOPAWZbn22ceOC4me4L3jj8Za6Zk= +github.com/btcsuite/btcwallet v0.16.18/go.mod h1:4TTru0cgIPbCZpY4aRfAVwX87zrQw4GXM8MH6+A5xZw= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 h1:Rr0njWI3r341nhSPesKQ2JF+ugDSzdPoeckS75SeDZk= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5/go.mod h1:+tXJ3Ym0nlQc/iHSwW1qzjmPs3ev+UVWMbGgfV1OZqU= github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 h1:YEO+Lx1ZJJAtdRrjuhXjWrYsmAk26wLTlNzxt2q0lhk= @@ -691,17 +115,11 @@ github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 h1:R8vQdOQdZ9Y3 github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792/go.mod h1:ghJtEyQwv5/p4Mg4C0fgbePVuGr935/5ddU9Z3TmDRY= github.com/btcsuite/winsvc v1.0.0 h1:J9B4L7e3oqhXOcm+2IuNApwzQec85lE+QaikUcCs+dk= github.com/btcsuite/winsvc v1.0.0/go.mod h1:jsenWakMcC0zFBFurPLEAyrnc/teJEM1O46fmI40EZs= -github.com/caddyserver/certmagic v0.17.2 h1:o30seC1T/dBqBCNNGNHWwj2i5/I/FMjBbTAhjADP3nE= -github.com/caddyserver/certmagic v0.17.2/go.mod h1:ouWUuC490GOLJzkyN35eXfV8bSbwMwSf4bdhkIxtdQE= github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.3.0/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/census-instrumentation/opencensus-proto v0.4.1/go.mod h1:4T9NM4+4Vw91VeyqjLS6ao50K5bOcLKN6Q42XnYaRYw= -github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.1.2/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= @@ -709,20 +127,6 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20210930031921-04548b0d99d4/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/udpa/go v0.0.0-20220112060539-c52dc94e7fbe/go.mod h1:6pvJx4me5XPnfI9Z40ddWsdw2W/uZgQLFXToKeRcDiI= -github.com/cncf/xds/go v0.0.0-20210312221358-fbca930ec8ed/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20220314180256-7f1daf1720fc/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230105202645-06c439db220b/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cncf/xds/go v0.0.0-20230607035331-e9ce68804cb4/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= @@ -733,15 +137,12 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf h1:iW4rZ826su+pqaw19uhpSCzhj44qo35pNgKFGqzDKkU= github.com/coreos/go-systemd v0.0.0-20191104093116-d3cd4ed1dbcf/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/cpuguy83/go-md2man/v2 v2.0.2 h1:p1EgwI/C7NhT0JmVkwCD2ZBK8j4aeHQX2pMHHBfMQ6w= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -752,11 +153,11 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/decred/dcrd/lru v1.1.2 h1:KdCzlkxppuoIDGEvCGah1fZRicrDH36IipvlB1ROkFY= github.com/decred/dcrd/lru v1.1.2/go.mod h1:gEdCVgXs1/YoBvFWt7Scgknbhwik3FgVSzlnCcXL2N8= @@ -764,38 +165,24 @@ github.com/dhui/dktest v0.4.5 h1:uUfYBIVREmj/Rw6MvgmqNAYzTiKOHJak+enB5Di73MM= github.com/dhui/dktest v0.4.5/go.mod h1:tmcyeHDKagvlDrz7gDKq4UAJOLIfVZYkfD5OnHDwcCo= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= -github.com/docker/cli v29.2.0+incompatible h1:9oBd9+YM7rxjZLfyMGxjraKBKE4/nVyvVfN4qNl9XRM= -github.com/docker/cli v29.2.0+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= +github.com/docker/cli v29.4.1+incompatible h1:02RT8QqqwtGRn+6SYypv8IUEbD/ltY6sfKCJIoUcGzk= +github.com/docker/cli v29.4.1+incompatible/go.mod h1:JLrzqnKDaYBop7H2jaqPtU4hHvMKP+vjCwu2uszcLI8= github.com/docker/docker v28.1.1+incompatible h1:49M11BFLsVO1gxY9UX9p/zwkE/rswggs8AdFmXQw51I= github.com/docker/docker v28.1.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= -github.com/docker/go-connections v0.6.0 h1:LlMG9azAe1TqfR7sO+NJttz1gy6KO7VJBh+pMmjSD94= -github.com/docker/go-connections v0.6.0/go.mod h1:AahvXYshr6JgfUJGdDCs2b5EZG/vmaMAntpSFH5BFKE= +github.com/docker/go-connections v0.7.0 h1:6SsRfJddP22WMrCkj19x9WKjEDTB+ahsdiGYf0mN39c= +github.com/docker/go-connections v0.7.0/go.mod h1:no1qkHdjq7kLMGUXYAduOhYPSJxxvgWBh7ogVvptn3Q= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= -github.com/docopt/docopt-go v0.0.0-20180111231733-ee0de3bc6815/go.mod h1:WwZ+bS3ebgob9U8Nd0kOddGdZWjyMGR8Wziv+TBNwSE= -github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY= github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210217033140-668b12f5399d/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/go-control-plane v0.9.9-0.20210512163311-63b5d3c536b0/go.mod h1:hliV/p42l8fGbc6Y9bQ70uLwIvmJyVE5k4iMKlh8wCQ= -github.com/envoyproxy/go-control-plane v0.9.10-0.20210907150352-cf90f659a021/go.mod h1:AFq3mo9L8Lqqiid3OhADV3RfLJnjiw63cSpi+fDTRC0= -github.com/envoyproxy/go-control-plane v0.10.2-0.20220325020618-49ff273808a1/go.mod h1:KJwIaB5Mv44NWtYuAOFCVOjcI94vtpEz2JU/D2v6IjE= -github.com/envoyproxy/go-control-plane v0.10.3/go.mod h1:fJJn/j26vwOu972OllsvAgJJM//w9BV6Fxbg2LuVd34= -github.com/envoyproxy/go-control-plane v0.11.1-0.20230524094728-9239064ad72f/go.mod h1:sfYdkwUW4BA3PbKjySwjJy+O4Pu0h62rlqCMHNk+K+Q= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/envoyproxy/protoc-gen-validate v0.6.7/go.mod h1:dyJXwwfPK2VSqiB9Klm1J6romD608Ba7Hij42vrOBCo= -github.com/envoyproxy/protoc-gen-validate v0.9.1/go.mod h1:OKNgG7TCp5pF4d6XftA0++PMirau2/yoOwVac3AbF2w= -github.com/envoyproxy/protoc-gen-validate v0.10.1/go.mod h1:DRjgyB0I43LtJapqN6NiRwroiAU2PaFuvk/vjgh61ss= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/fergusstrange/embedded-postgres v1.25.0 h1:sa+k2Ycrtz40eCRPOzI7Ry7TtkWXXJ+YRsxpKMDhxK0= github.com/fergusstrange/embedded-postgres v1.25.0/go.mod h1:t/MLs0h9ukYM6FSt99R7InCHs1nW0ordoVCcnzmpTYw= -github.com/fogleman/gg v1.3.0/go.mod h1:R/bRT+9gY/C5z7JzPU0zXsXHKM4/ayA+zqcVNZzPa1k= github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= github.com/frankban/quicktest v1.0.0/go.mod h1:R98jIehRai+d1/3Hv2//jOVCTJhW1VBavT6B6CuGq2k= @@ -809,10 +196,6 @@ github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4 github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= -github.com/go-fonts/dejavu v0.1.0/go.mod h1:4Wt4I4OU2Nq9asgDCteaAaWZOV24E+0/Pwo0gppep4g= -github.com/go-fonts/latin-modern v0.2.0/go.mod h1:rQVLdDMK+mK1xscDwsqM5J8U2jrRa3T0ecnM9pNujks= -github.com/go-fonts/liberation v0.2.0/go.mod h1:K6qoJYypsmfVjWg8KOVDQhLc8UDgIK2HYqyqAO9z7GY= -github.com/go-fonts/stix v0.1.0/go.mod h1:w/c1f0ldAUlJmLBvlbkvVXLAD+tAMqobIIQpmnUIzUY= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= @@ -820,7 +203,6 @@ github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2 github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-kit/log v0.2.0/go.mod h1:NwTd00d/i8cPZ3xOwwiv2PO5MOcx78fFErGNcVmBjv0= -github.com/go-latex/latex v0.0.0-20210823091927-c0d11ff05a81/go.mod h1:SX0U8uGpxhq9o2S/CELCSUxEWWAuoCUcVCQWv7G2OCk= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= @@ -832,27 +214,18 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= github.com/go-macaroon-bakery/macaroonpb v1.0.0 h1:It9exBaRMZ9iix1iJ6gwzfwsDE6ExNuwtAJ9e09v6XE= github.com/go-macaroon-bakery/macaroonpb v1.0.0/go.mod h1:UzrGOcbiwTXISFP2XDLDPjfhMINZa+fX/7A2lMd31zc= -github.com/go-pdf/fpdf v0.5.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= -github.com/go-pdf/fpdf v0.6.0/go.mod h1:HzcnA+A23uwogo0tp9yU+l3V+KXhiESpt1PMayhOh5M= github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= -github.com/goccy/go-json v0.9.11/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= -github.com/gofrs/uuid v4.2.0+incompatible h1:yyYWMnhkhrKwwr8gAOcOCYxOOscHgDS9yZgBrnJfGa0= -github.com/gofrs/uuid v4.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= -github.com/golang/freetype v0.0.0-20170609003504-e2365dfdc4a0/go.mod h1:E/TSTwGwJL78qG/PmXZO1EjYhfJinVAhrmmHX6Z8B9k= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/glog v1.0.0/go.mod h1:EWib/APOK0SL3dFbYqvxE3UYd8E6s1ouQ7iEp/0LWV4= -github.com/golang/glog v1.1.0/go.mod h1:pfYeQZ3JWZoXTV5sFc986z3HTpwQs9At6P4ImfuP3NQ= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -863,8 +236,6 @@ github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/mock v1.5.0/go.mod h1:CWnOUgYIOo4TcNZ0wHX3YZCqsaM1I1Jvs6v3mP3KVu8= -github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= @@ -876,46 +247,31 @@ github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QD github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= -github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM= github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/gonum/gonum v0.11.0 h1:Lffpf1Aq8WeVUJTbfiKHlOVlFm+c021iePrlwvE5woI= github.com/gonum/gonum v0.11.0/go.mod h1:fSG4YDCxxUZQJ7rKsQrj0gMOg00Il0Z96/qMA4bVQhA= -github.com/gonum/plot v0.10.1/go.mod h1:VZW5OlhkL1mysU9vaqNHnsy86inf6Ot+jB3r+BczCEo= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4= github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA= -github.com/google/flatbuffers v2.0.8+incompatible/go.mod h1:1AeVuKshWv4vARoZatz6mlQ0JxURH0Kv5+zNeJKJCa8= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= github.com/google/go-cmp v0.2.1-0.20190312032427-6f77996f0c42/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE= -github.com/google/go-cmp v0.5.8/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.2.1/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= @@ -923,55 +279,24 @@ github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hf github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210122040257-d980be63207e/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210226084205-cbba55b83ad5/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.3.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.0.0-20220520183353-fd19c99a87aa/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.1.0/go.mod h1:17drOmN3MwGY7t0e+Ei9b45FFGA3fBs3x36SsCg1hq8= -github.com/googleapis/enterprise-certificate-proxy v0.2.0/go.mod h1:8C0jb7/mgJe/9KK8Lm7X9ctZC2t60YyIpYEI16jx0Qg= -github.com/googleapis/enterprise-certificate-proxy v0.2.1/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= -github.com/googleapis/enterprise-certificate-proxy v0.2.3/go.mod h1:AwSRAtLfXpU5Nm3pW+v7rGDHp09LsPtGY9MduiEsR9k= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.1.0/go.mod h1:Q3nei7sK6ybPYH7twZdmQpAd1MKb7pfu6SK+H1/DsU0= -github.com/googleapis/gax-go/v2 v2.1.1/go.mod h1:hddJymUZASv3XPyGkUpKj8pPO47Rmb0eJc8R6ouapiM= -github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/OthfcblKl4IGNaM= -github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= -github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= -github.com/googleapis/gax-go/v2 v2.5.1/go.mod h1:h6B0KMMFNtI2ddbGJn3T3ZbwkeT6yqEF02fYlzkUCyo= -github.com/googleapis/gax-go/v2 v2.6.0/go.mod h1:1mjbznJAPHFpesgE5ucqfYEscaz5kMdcIDwU/6+DDoY= -github.com/googleapis/gax-go/v2 v2.7.0/go.mod h1:TEop28CZZQ2y+c0VxMUmu1lV+fQx57QpBWsYpwqHJx8= -github.com/googleapis/gax-go/v2 v2.7.1/go.mod h1:4orTrqY6hXxxaUL4LHIPl6lGo8vAE38/qKbhSAKP6QI= -github.com/googleapis/go-type-adapters v1.0.0/go.mod h1:zHW75FOG2aur7gAO2B+MLby+cLsWGBF62rFAi7WjWO4= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0 h1:UH//fgunKIs4JdUbpDl1VZCDaL56wXCB/5+wF6uHfaI= github.com/grpc-ecosystem/go-grpc-middleware v1.4.0/go.mod h1:g5qyo/la0ALbONm6Vbp88Yd8NsDy6rZz+RcrMPxvld8= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0-rc.0 h1:mdLirNAJBxnGgyB6pjZLcs6ue/6eZGBui6gXspfq4ks= -github.com/grpc-ecosystem/go-grpc-middleware/providers/prometheus v1.0.0-rc.0/go.mod h1:kdXbOySqcQeTxiqglW7aahTmWZy3Pgi6SYL36yvKeyA= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3 h1:o95KDiV/b1xdkumY5YbLR0/n2+wBxUpgf3HgfKgTyLI= -github.com/grpc-ecosystem/go-grpc-middleware/v2 v2.0.0-rc.3/go.mod h1:hTxjzRcX49ogbTGVJ1sM5mz5s+SSgiGIyL3jjPxl32E= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0 h1:Ovs26xHkKqVztRpIrF/92BcuyuQ/YW4NSIpoGtfXNho= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= github.com/grpc-ecosystem/grpc-gateway v1.16.0 h1:gmcG1KaJ57LophUzW0Hy8NmPhnMZb4M0+kPpLofRdBo= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.7.0/go.mod h1:hgWBS7lorOAVIJEQMi4ZsPv9hVvWI6+ch50m39Pf2Ks= -github.com/grpc-ecosystem/grpc-gateway/v2 v2.11.3/go.mod h1:o//XUCC/F+yRGJoPO/VU0GSB0f8Nhgmxx0VIRUvaC0w= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 h1:bkypFPDjIYGfCYD5mRBvpqxfYX1YCS1PXdKYWi8FsN0= github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0/go.mod h1:P+Lt/0by1T8bfcF3z737NnSbmxQAppXMRziHUxPOC8k= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= @@ -982,76 +307,33 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/iancoleman/strcase v0.2.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47ZCWhYzw7ho= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= -github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0 h1:eHK/5clGOatcjX3oWGBO/MpxpbHzSwud5EWTSCI+MX0= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jackpal/gateway v1.0.5 h1:qzXWUJfuMdlLMtt0a3Dgt+xkWQiA5itDEITVJtuSwMc= github.com/jackpal/gateway v1.0.5/go.mod h1:lTpwd4ACLXmpyiCTRtfiNyVnUmqT9RivzCDQetPfnjA= github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad h1:heFfj7z0pGsNCekUlsFhO2jstxO4b5iQ665LjwM5mDc= github.com/jackpal/go-nat-pmp v0.0.0-20170405195558-28a68d0c24ad/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= -github.com/jedib0t/go-pretty/v6 v6.2.7 h1:4823Lult/tJ0VI1PgW3aSKw59pMWQ6Kzv9b3Bj6MwY0= -github.com/jedib0t/go-pretty/v6 v6.2.7/go.mod h1:FMkOpgGD3EZ91cW8g/96RfxoV7bdeJyzXPYgz1L1ln0= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -1072,68 +354,54 @@ github.com/juju/schema v1.0.0/go.mod h1:Y+ThzXpUJ0E7NYYocAbuvJ7vTivXfrof/IfRPq/0 github.com/juju/webbrowser v0.0.0-20160309143629-54b8c57083b4/go.mod h1:G6PCelgkM6cuvyD10iYJsjLBsSadVXtJ+nBxFAxE2BU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= -github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= -github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51/go.mod h1:CzGEWj7cYgsdH8dAjBGEr58BoE7ScuLd+fwFZ44+/x8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kkdai/bstream v0.0.0-20161212061736-f391b8402d23/go.mod h1:J+Gs4SYgM6CZQHDETBtE9HaSEkGmuNXF86RwHhHUvq4= github.com/kkdai/bstream v1.0.0 h1:Se5gHwgp2VT2uHfDrkbbgbgEvV9cimLELwrPJctSjg8= github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+NVZZA= -github.com/klauspost/asmfmt v1.3.2/go.mod h1:AG8TuvYojzulgDAMCnYn50l/5QV3Bs/tp6j0HLHbNSE= -github.com/klauspost/compress v1.15.9/go.mod h1:PhcZ0MbTNciWF3rruxRgKxI5NkcHHrHUDtV4Yw2GlzU= github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= -github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= -github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM= -github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= -github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis= -github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40= -github.com/lightninglabs/aperture v0.3.13-beta h1:0Slj0FS67O687z4pIQZgZt5oavSoZYa3NniuyNxBzFs= -github.com/lightninglabs/aperture v0.3.13-beta/go.mod h1:QWBweJKRn8C5GJ+SmsTygr3lIZWYODLZ9n57wuWvw8I= +github.com/lightninglabs/aperture v0.4.0 h1:GxI7vypu8srbbGHiDgM2M/vSE8xj0LZzj3ASLC6crXY= +github.com/lightninglabs/aperture v0.4.0/go.mod h1:cQY0FwDqm6oBILlXWJC8toJssaH9F1qXVwghqSesyJI= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf/go.mod h1:vxmQPeIQxPf6Jf9rM8R+B4rKBqLA2AjttNxkFBL2Plk= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.4-0.20250610182311-2f1d46ef18b7 h1:373o5lNr1udAdhcf5+zq/0dYpRtkvYLl8Lk6wG7I0DY= github.com/lightninglabs/lightning-node-connect/hashmailrpc v1.0.4-0.20250610182311-2f1d46ef18b7/go.mod h1:bDnEKRN1u13NFBuy/C+bFLhxA5bfd3clT25y76QY0AM= -github.com/lightninglabs/lndclient v0.20.0-8 h1:xymEVZjHcFoszZsJy3jyPNErY+YBCkxLDRV5ohynry4= -github.com/lightninglabs/lndclient v0.20.0-8/go.mod h1:AQTlloQUUK6OW6j9YRiA/7Sy09PXlyVxsvPo5bW0L6A= -github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2 h1:eFjp1dIB2BhhQp/THKrjLdlYuPugO9UU4kDqu91OX/Q= -github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= -github.com/lightninglabs/neutrino v0.16.1 h1:5Kz4ToxncEVkpKC6fwUjXKtFKJhuxlG3sBB3MdJTJjs= -github.com/lightninglabs/neutrino v0.16.1/go.mod h1:L+5UAccpUdyM7yDgmQySgixf7xmwBgJtOfs/IP26jCs= -github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g= -github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo= +github.com/lightninglabs/lndclient v0.21.0-2 h1:gA1utFMoKV6OZfHpYAQ1TX0wVs5cAtgTORRVavHR/ls= +github.com/lightninglabs/lndclient v0.21.0-2/go.mod h1:RUIcfPr82HrvZr3pu9f8nbD5v6VFbm+KgExqNNp5bE4= +github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2.0.20251211093704-71c1eef09789 h1:7kX7vUgHUazAHcCJ6uzBDa4/2MEGEbMEfa01GtfqmTQ= +github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2.0.20251211093704-71c1eef09789/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= +github.com/lightninglabs/neutrino v0.17.1 h1:lNhgq7ix/N81R6oATroP/kHMzH1qzVVF2dEGcTlN2t4= +github.com/lightninglabs/neutrino v0.17.1/go.mod h1:tcwCgRTGWcaua0L/xzdwllW8eslHDbux4XkiYsivvHE= +github.com/lightninglabs/neutrino/cache v1.1.3 h1:rgnabC41W+XaPuBTQrdeFjFCCAVKh1yctAgmb3Se9zA= +github.com/lightninglabs/neutrino/cache v1.1.3/go.mod h1:qxkJb+pUxR5p84jl5uIGFCR4dGdFkhNUwMSxw3EUWls= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9Z6CpKxl13mS48idsu6F+cEZf0lkyiV+Dq9g= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -github.com/lightninglabs/taproot-assets v0.7.0 h1:oD7NrwTYqt14yKAZj9jtIunl02ayVWZSJMuTC1weURY= -github.com/lightninglabs/taproot-assets v0.7.0/go.mod h1:KPVXin+YtpGUGYdkcDh2l1pclKqWu6blW+rXAHTK0WI= -github.com/lightninglabs/taproot-assets/taprpc v1.0.11 h1:8P6+M3GtKEhXnB+Du3uyR5Be049FMVouHvxRKdKrgH4= -github.com/lightninglabs/taproot-assets/taprpc v1.0.11/go.mod h1:DZn+0c9/PHEKisJLSqNdyH3BVJmwl8mFLe04y++/FlI= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 h1:6D3LrdagJweLLdFm1JNodZsBk6iU4TTsBBFLQ4yiXfI= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9/go.mod h1:EDqJ3MuZIbMq0QI1czTIKDJ/GS8S14RXPwapHw8cw6w= -github.com/lightningnetwork/lnd v0.20.1-beta h1:wDMNgks5uST1CY+WwjIZ4+McPMMFpr2pIIGJp7ytDI4= -github.com/lightningnetwork/lnd v0.20.1-beta/go.mod h1:oIKh9EqE1sJJpQPq9ZCMFc4Ot287NrotZ1oZn0zUI+M= +github.com/lightninglabs/taproot-assets v0.8.0 h1:8Mky342/f5hbVm94Owj0YveIdKuTasJwFB6uqAa5/gc= +github.com/lightninglabs/taproot-assets v0.8.0/go.mod h1:SEoMeNzpENVUPnvuqllkDhl7QvNE74+Dtb/dnkbrygg= +github.com/lightninglabs/taproot-assets/taprpc v1.1.0 h1:Oum7ddGygrEaT+NHqpaQI8U6pV5jJUH4hvhezl5y00k= +github.com/lightninglabs/taproot-assets/taprpc v1.1.0/go.mod h1:X7XP753o8xCgjVI2mRu1Tvpyk3k4uybGDMFhpF6IRxI= +github.com/lightningnetwork/lightning-onion v1.3.0 h1:FqILgHjD6euc/Muo1VOzZ4+XDPuFnw6EYROBq0rR/5c= +github.com/lightningnetwork/lightning-onion v1.3.0/go.mod h1:nP85zMHG7c0si/eHBbSQpuDCtnIXfSvFrK3tW6YWzmU= +github.com/lightningnetwork/lnd v0.21.0-beta h1:bDP5UH15E7DVGTztsmBPQLqgyilq5EXDrglvQFmRc3U= +github.com/lightningnetwork/lnd v0.21.0-beta/go.mod h1:HcKq9DyxbVEZXuR28TIyGbIIgAjANCxI+N6dqOnRBAA= +github.com/lightningnetwork/lnd/actor v0.0.6 h1:Ge8N2wivARG+27qJBwTlB0vwsypStZYZy8vk4Zl38sU= +github.com/lightningnetwork/lnd/actor v0.0.6/go.mod h1:YAsoniSbY/cAM9HTVNfZLvt7RI6swDxy6wzPspTcMZg= github.com/lightningnetwork/lnd/cert v1.2.2 h1:71YK6hogeJtxSxw2teq3eGeuy4rHGKcFf0d0Uy4qBjI= github.com/lightningnetwork/lnd/cert v1.2.2/go.mod h1:jQmFn/Ez4zhDgq2hnYSw8r35bqGVxViXhX6Cd7HXM6U= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= @@ -1142,12 +410,12 @@ github.com/lightningnetwork/lnd/fn/v2 v2.0.9 h1:ZytG4ltPac/sCyg1EJDn10RGzPIDJeye github.com/lightningnetwork/lnd/fn/v2 v2.0.9/go.mod h1:aPUJHJ31S+Lgoo8I5SxDIjnmeCifqujaiTXKZqpav3w= github.com/lightningnetwork/lnd/healthcheck v1.2.6 h1:1sWhqr93GdkWy4+6U7JxBfcyZIE78MhIHTJZfPx7qqI= github.com/lightningnetwork/lnd/healthcheck v1.2.6/go.mod h1:Mu02um4CWY/zdTOvFje7WJgJcHyX2zq/FG3MhOAiGaQ= -github.com/lightningnetwork/lnd/kvdb v1.4.16 h1:9BZgWdDfjmHRHLS97cz39bVuBAqMc4/p3HX1xtUdbDI= -github.com/lightningnetwork/lnd/kvdb v1.4.16/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= -github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= -github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= -github.com/lightningnetwork/lnd/sqldb v1.0.12-0.20260113193010-8565d12e40b1 h1:PkEppKL17cZh0Dr9h/T9BEVJUbd/p2tjJ/x8ffG3R0M= -github.com/lightningnetwork/lnd/sqldb v1.0.12-0.20260113193010-8565d12e40b1/go.mod h1:tB2jlqu79TIOR9uhAZOmPxpVFUhB2s+oxKnqRRL1oc0= +github.com/lightningnetwork/lnd/kvdb v1.5.1 h1:OG5cDbqggxiCFKAJbSPw0PfQovi+0odCZAAU5r5b+ho= +github.com/lightningnetwork/lnd/kvdb v1.5.1/go.mod h1:5lubXYoXHDBWBYKmC+2we7qgkjjz0cEZ/5QSQkxQSec= +github.com/lightningnetwork/lnd/queue v1.2.0 h1:sSrn+u84OLuOT/F+xGxgg8VfknXeIZEAFQoMH6BL60s= +github.com/lightningnetwork/lnd/queue v1.2.0/go.mod h1:qLNP0L3B7piRGvDyhAyJKic4xTt+Mw4D7mWrQeuAwxY= +github.com/lightningnetwork/lnd/sqldb v1.0.13-0.20260514041430-e9b422f78581 h1:s0M+mUHRSlBZuz0qxKdr77oKDFff3Hdg5oysJ31BjiU= +github.com/lightningnetwork/lnd/sqldb v1.0.13-0.20260514041430-e9b422f78581/go.mod h1:PQvrB+2SlYuyAoe0ac/SUIMZ9rP73jYlanIN8O7/eA0= github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM= github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= github.com/lightningnetwork/lnd/tlv v1.3.2 h1:MO4FCk7F4k5xPMqVZF6Nb/kOpxlwPrUQpYjmyKny5s0= @@ -1157,36 +425,19 @@ github.com/lightningnetwork/lnd/tor v1.1.6/go.mod h1:qSRB8llhAK+a6kaTPWOLLXSZc6H github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY= github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= -github.com/lukechampine/uint128 v1.2.0/go.mod h1:c4eWIwlEGaxC/+H1VguhU4PHXNWDCDMUlWdIWl2j1gk= -github.com/lyft/protoc-gen-star v0.6.0/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star v0.6.1/go.mod h1:TGAoBVkt8w7MPG72TrKIu85MIdXwDuzJYeZuUPFPNwA= -github.com/lyft/protoc-gen-star/v2 v2.0.1/go.mod h1:RcCdONR2ScXaYnQC5tUzxzlpA3WVYF7/opLeUgcQs/o= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= -github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= -github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= -github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= -github.com/mattn/go-sqlite3 v1.14.14/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369 h1:I0XW9+e1XWDxdcEniV4rQAIOPUGDq67JSCiRCgGCZLI= github.com/matttproud/golang_protobuf_extensions v1.0.2-0.20181231171920-c182affec369/go.mod h1:BSXmuO+STAnVfrANrmjBb36TMTDstsz7MSK+HVaYKv4= -github.com/mholt/acmez v1.0.4 h1:N3cE4Pek+dSolbsofIkAYz6H1d3pE+2G0os7QHslf80= -github.com/mholt/acmez v1.0.4/go.mod h1:qFGLZ4u+ehWINeJZjzPlsnjJBCPAADWTcIqE/7DAYQY= github.com/miekg/dns v1.1.50 h1:DQUfb9uc6smULcREF09Uc+/Gd46YWqJd5DbpPE9xkcA= github.com/miekg/dns v1.1.50/go.mod h1:e3IlAVfNqAllflbibAZEWOXOQ+Ynzk/dDozDxY7XnME= -github.com/minio/asm2plan9s v0.0.0-20200509001527-cdd76441f9d8/go.mod h1:mC1jAcsrzbxHt8iiaC+zU4b1ylILSosueou12R++wfY= -github.com/minio/c2goasm v0.0.0-20190812172519-36a3d3bbc4f3/go.mod h1:RagcQ7I8IeTMnF8JTXieKnO4Z6JCsikNEzj0DwauVzE= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= -github.com/moby/moby/api v1.53.0 h1:PihqG1ncw4W+8mZs69jlwGXdaYBeb5brF6BL7mPIS/w= -github.com/moby/moby/api v1.53.0/go.mod h1:8mb+ReTlisw4pS6BRzCMts5M49W5M7bKt1cJy/YbAqc= -github.com/moby/moby/client v0.2.2 h1:Pt4hRMCAIlyjL3cr8M5TrXCwKzguebPAc2do2ur7dEM= -github.com/moby/moby/client v0.2.2/go.mod h1:2EkIPVNCqR05CMIzL1mfA07t0HvVUUOl85pasRz/GmQ= +github.com/moby/moby/api v1.54.2 h1:wiat9QAhnDQjA7wk1kh/TqHz2I1uUA7M7t9SAl/JNXg= +github.com/moby/moby/api v1.54.2/go.mod h1:+RQ6wluLwtYaTd1WnPLykIDPekkuyD/ROWQClE83pzs= +github.com/moby/moby/client v0.4.1 h1:DMQgisVoMkmMs7fp3ROSdiBnoAu8+vo3GggFl06M/wY= +github.com/moby/moby/client v0.4.1/go.mod h1:z52C9O2POPOsnxZAy//WtKcQ32P+jT/NGeXu/7nfjGQ= github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= github.com/moby/term v0.5.2 h1:6qk3FJAFDs6i/q3W/pQ97SX192qKfZgGjCQqfCJkgzQ= @@ -1223,23 +474,15 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q= -github.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI= +github.com/opencontainers/runc v1.3.6 h1:SLGIymCtsk80iNPWgbc8dtjI30r+5mTVV+4dN8/17Sk= +github.com/opencontainers/runc v1.3.6/go.mod h1:o1wyv76EDlTkcf0KTFgN8bMWLPvgF/HfX709lDv+rr4= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= -github.com/phpdave11/gofpdf v1.4.2/go.mod h1:zpO6xFn9yxo3YLyMvW8HcKWVdbNqgIfOOp2dXMnm1mY= -github.com/phpdave11/gofpdi v1.0.12/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/phpdave11/gofpdi v1.0.13/go.mod h1:vBmVV0Do6hSBHC8uKUQ71JGW+ZGQq74llk/7bXwjDoI= -github.com/pierrec/lz4/v4 v4.1.15/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4= -github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= -github.com/pkg/sftp v1.10.1/go.mod h1:lYOWFsE0bwd1+KfKJaKeuokY15vzFx25BLbzYYoAxZI= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= @@ -1268,48 +511,27 @@ github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1 github.com/prometheus/procfs v0.7.3/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= github.com/prometheus/procfs v0.8.0 h1:ODq8ZFEaYeCaZOJlZZdJA2AbQR98dSHSM1KW/You5mo= github.com/prometheus/procfs v0.8.0/go.mod h1:z7EfXMXOkbkqb9IINtpCn86r/to3BnA0uaxHdg830/4= -github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE= github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo= -github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= -github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= -github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= -github.com/ruudk/golang-pdf417 v0.0.0-20201230142125-a7e3863a1245/go.mod h1:pQAZKsJ8yyVxGRWYNEm9oFB8ieLgKFnamEyDmSA0BRk= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= -github.com/shopspring/decimal v1.3.1 h1:2Usl1nmF/WZucqkFZhnfFYxxxu8LG21F6nPQBE5gKV8= -github.com/shopspring/decimal v1.3.1/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= -github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= -github.com/spf13/afero v1.3.3/go.mod h1:5KUK8ByomD5Ti5Artl0RtHeI5pTF7MIDuXL3yY520V4= -github.com/spf13/afero v1.6.0/go.mod h1:Ai8FlHk4v/PARR026UzYexafAt9roJ7LcLMAmO6Z93I= -github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= -github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= -github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= +github.com/spf13/pflag v1.0.9 h1:9exaQaMOCwffKiiiYk6/BndUBv+iRViNW+4lEMi0PvY= +github.com/spf13/pflag v1.0.9/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -1317,13 +539,9 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= @@ -1333,8 +551,6 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 h1:uruHq4 github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02 h1:tcJ6OjwOMvExLlzrAVZute09ocAGa7KqOON60++Gz4E= github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02/go.mod h1:tHlrkM198S068ZqfrO6S8HsoJq2bF3ETfTL+kt4tInY= -github.com/urfave/cli v1.22.14 h1:ebbhrRiGK2i4naQJr+1Xj92HXZCrK7MsyTS/ob3HnAk= -github.com/urfave/cli v1.22.14/go.mod h1:X0eDS6pD6Exaclxm99NJ3FiCDRED7vIHpx2mDOHLvkA= github.com/urfave/cli-docs/v3 v3.1.1-0.20251020101624-bec07369b4f6 h1:cm0BrTu3Q0CNo+vB5kErCcVMZqD2D1z7y3YVIiBlr+o= github.com/urfave/cli-docs/v3 v3.1.1-0.20251020101624-bec07369b4f6/go.mod h1:59d+5Hz1h6GSGJ10cvcEkbIe3j233t4XDqI72UIx7to= github.com/urfave/cli/v3 v3.4.1 h1:1M9UOCy5bLmGnuu1yn3t3CB4rG79Rtoxuv1sPhnm6qM= @@ -1354,11 +570,6 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.1/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zeebo/assert v1.3.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0= -github.com/zeebo/xxh3 v1.0.2/go.mod h1:5NWz9Sef7zIDm2JHfFlcQvNekmcEl9ekUZQQKCYaDcA= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec h1:FpfFs4EhNehiVfzQttTuxanPIT43FtkkCFypIod8LHo= gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec/go.mod h1:BZ1RAoRPbCxum9Grlv5aeksu2H8BiKehBYooU2LFiOQ= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= @@ -1382,9 +593,6 @@ go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.49.0 h1:4Pp6oUg3+e/6M4C0A/3kJ2VYa++dsWVTtGgLVj5xtHg= @@ -1405,33 +613,17 @@ go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfC go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= -go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.opentelemetry.io/proto/otlp v0.15.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= -go.opentelemetry.io/proto/otlp v0.19.0/go.mod h1:H7XAot3MsfNsj7EXtrA2q5xSNQ10UqI405h3+duxN4U= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= -go.uber.org/goleak v1.1.11/go.mod h1:cwTWslyiVhfpKIDGSZEM2HlOvcqm+tG4zioyIeLoqMQ= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.18.1/go.mod h1:xg/QME4nWcxGxrpdeYfq7UvYrLh66cuVKdrbD1XF/NI= -go.uber.org/zap v1.21.0/go.mod h1:wjWOCqI0f2ZZrJF/UufIOkiC8ii6tm1iqIsLo76RfJw= go.uber.org/zap v1.24.0 h1:FiJd5l1UOLj0wCgbSE0rwwXHzEdAZS6hiiSnxJN/D60= go.uber.org/zap v1.24.0/go.mod h1:2kMP+WWQ8aoFoedH3T2sq6iJ2yDWpHbP0f6MQbS9Gkg= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -1441,47 +633,26 @@ golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191002040644-a1355ae1e2c3/go.mod h1:NOZ3BPKG0ec/BKJQgnvsSFpcKLM5xXVWnvZS97DWHgE= golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20220827204233-334a2380cb91/go.mod h1:cyybsKvd6eL0RnXn6p/Grxp8F5bW7iYuBgsNCOHpMYE= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= -golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= +golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 h1:3yiSh9fhy5/RhCSntf4Sy0Tnx50DmMpQ4MQdKKk4yg4= +golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200119044424-58c23975cae1/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200430140353-33d19683fad8/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20200618115811-c13761719519/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20201208152932-35266b937fa6/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/image v0.0.0-20210607152325-775e3b0c77b9/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20210628002857-a66eb6448b8d/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20211028202545-6944b10bf410/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= -golang.org/x/image v0.0.0-20220302094943-723b81ca9867/go.mod h1:023OzeP/+EPmXeapQh35lcL3II3LrY8Ic+EFFKVhULM= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -1492,8 +663,6 @@ golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHl golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20210508222113-6edffad5e616/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= @@ -1502,17 +671,9 @@ golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzB golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.5.0/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.7.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.9.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -1530,7 +691,6 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -1548,75 +708,21 @@ golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20200813134508-3edf25e44fcc/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLdyRGr576XBO4/greRjx4P4O3yc= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= -golang.org/x/net v0.0.0-20210423184538-5f58ad60dda6/go.mod h1:OJAsFXCWl8Ukc7SiCT/9KSuxbyM7479/AVlXFRxuMCk= -golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210726213435-c6fcb2dbf985/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210813160813-60bc85c4be6d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20211015210444-4f30a5c0130f/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220607020251-c690dde0001d/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220617184016-355a448f1bc9/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220624214902-1bab6f366d9e/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220630215102-69896b714898/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.0.0-20220909164309-bea034e7d591/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221012135044-0b7e1fb9d458/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.0.0-20221014081412-f15817d10f9b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.4.0/go.mod h1:MBQ8lrhLObU/6UmLb4fmbmk5OcyYmqtbGd/9yIeKjEE= -golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210220000619-9bb904979d93/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210313182246-cd4f82c27b84/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210413134643-5e61552d6c78/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20210514164344-f6687ab2804c/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210628180205-a41e5a781914/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210805134026-6f1e6394065a/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210819190943-2bc19b11175f/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20211104180415-d3ed0bb246c8/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= golang.org/x/oauth2 v0.0.0-20220223155221-ee480838109b/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220309155454-6242fa91716a/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220411215720-9780585627b5/go.mod h1:DAh4E804XQdzx2j+YRIaUnCqCV2RuMz24cGBJ5QYIrc= -golang.org/x/oauth2 v0.0.0-20220608161450-d0670ef3b1eb/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220622183110-fd043fe589d2/go.mod h1:jaDAt6Dkxork7LmZnYtzbRWj0W47D86a3TGe0YHBvmE= -golang.org/x/oauth2 v0.0.0-20220822191816-0ebed06d0094/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20220909003341-f21342109be1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221006150949-b44042a4b9c1/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.0.0-20221014153046-6fdb5e3db783/go.mod h1:h4gKUeWbJ4rQPri7E0u6Gs4e9Ri2zaLxzw5DI5XGrYg= -golang.org/x/oauth2 v0.4.0/go.mod h1:RznEsdpjGAINPTOF0UH/t+xJ75L18YO3Ho6Pyn+uRec= -golang.org/x/oauth2 v0.5.0/go.mod h1:9/XBHVqLaWO3/BRHs5jbpYCnOZVjj5V0ndyaAM7KB4I= -golang.org/x/oauth2 v0.6.0/go.mod h1:ycmewcwgD4Rpr3eZJLSB4Kyyljb3qDh40vJ8STE5HKw= -golang.org/x/oauth2 v0.7.0/go.mod h1:hPLQkd9LyjfXTiRohC/41GhcFqxisoUQ99sCUOHO9x4= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -1628,20 +734,13 @@ golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220601150217-0de741cfad7f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220819030929-7fc1605a5dde/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220929204114-8fcdb60fdcc0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= -golang.org/x/sys v0.0.0-20180816055513-1c9583448a9c/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1652,17 +751,14 @@ golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190804053845-51ab0e2deafa/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -1679,104 +775,39 @@ golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200625212154-ddb9806d33ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200814200057-3d37ad5750ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210220050731-9a76102bfb43/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210304124612-50617c2ba197/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210305230114-8fe3ee5dd75b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210315160823-c6e025ad8005/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210320140829-1e4c9ba3b0c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210330210617-4fbd30eecc44/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210514084401-e8d321eab015/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210603125802-9665404d3644/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210816183151-1e6c022a8912/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211007075335-d3039528d8ac/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211019181941-9d821ace8654/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211124211545-fe61309f8881/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20211210111614-af8b64212486/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220209214540-3681064d5158/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220227234510-4e6760a101f9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220328115105-d36c6a25d886/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220412211240-33da011f77ad/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220502124256-b6088ccd6cba/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220503163025-988cb79eb6c6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220610221304-9f5ed59c137d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220615213510-4f61da869c0c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220624220833-87e55d714810/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220728004956-3c1f35247d10/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220829200755-d48e67d00261/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.3.0/go.mod h1:q750SLmJuPmVoN1blW3UFBPREJfb1KmY3vwxfr+nFDA= -golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.5.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20220922220347-f3bd1da661af/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.1.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -1787,19 +818,14 @@ golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3 golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190927191325-030b2cf1153e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -1808,7 +834,6 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -1828,38 +853,14 @@ golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roY golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201124115921-2c860bdd6e78/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.4/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.6-0.20210726203631-07bc1bf47fb2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= -golang.org/x/tools v0.1.9/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.3.0/go.mod h1:/rWhSS2+zyEVwoJf8YAX6L2f0ntZ7Kn/mGgAWcipA5k= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.7.0/go.mod h1:4pg6aUX35JBAogB10C9AtvVL+qowtN4pT3CGSQex14s= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220609144429-65e65417b02f/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -1876,54 +877,12 @@ google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0M google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.41.0/go.mod h1:RkxM5lITDfTzmyKFPt+wGrCJbVfniCr2ool8kTBzRTU= -google.golang.org/api v0.43.0/go.mod h1:nQsDGjRXMo4lvh5hP0TKqF244gqhGcr/YSIykhUk/94= -google.golang.org/api v0.47.0/go.mod h1:Wbvgpq1HddcWVtzsVLyfLp8lDg6AA241LmgIL59tHXo= -google.golang.org/api v0.48.0/go.mod h1:71Pr1vy+TAZRPkPs/xlCf5SsU8WjuAWv1Pfjbtukyy4= -google.golang.org/api v0.50.0/go.mod h1:4bNT5pAuq5ji4SRZm+5QIkjny9JAyVD/3gaSihNefaw= -google.golang.org/api v0.51.0/go.mod h1:t4HdrdoNgyN5cbEfm7Lum0lcLDLiise1F8qDKX00sOU= -google.golang.org/api v0.54.0/go.mod h1:7C4bFFOvVDGXjfDTAsgGwDgAxRDeQ4X8NvUedIt6z3k= -google.golang.org/api v0.55.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.56.0/go.mod h1:38yMfeP1kfjsl8isn0tliTjIb1rJXcQi4UXlbqivdVE= -google.golang.org/api v0.57.0/go.mod h1:dVPlbZyBo2/OjBpmvNdpn2GRm6rPy75jyU7bmhdrMgI= -google.golang.org/api v0.61.0/go.mod h1:xQRti5UdCmoCEqFxcz93fTl338AVqDgyaDRuOZ3hg9I= -google.golang.org/api v0.63.0/go.mod h1:gs4ij2ffTRXwuzzgJl/56BdwJaA194ijkfn++9tDuPo= -google.golang.org/api v0.67.0/go.mod h1:ShHKP8E60yPsKNw/w8w+VYaj9H6buA5UqDp8dhbQZ6g= -google.golang.org/api v0.70.0/go.mod h1:Bs4ZM2HGifEvXwd50TtW70ovgJffJYw2oRCOFU/SkfA= -google.golang.org/api v0.71.0/go.mod h1:4PyU6e6JogV1f9eA4voyrTY2batOLdgZ5qZ5HOCc4j8= -google.golang.org/api v0.74.0/go.mod h1:ZpfMZOVRMywNyvJFeqL9HRWBgAuRfSjJFpe9QtRRyDs= -google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.77.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69ljA= -google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= -google.golang.org/api v0.80.0/go.mod h1:xY3nI94gbvBrE0J6NHXhxOmW97HG7Khjkku6AFB3Hyg= -google.golang.org/api v0.84.0/go.mod h1:NTsGnUFJMYROtiquksZHBWtHfeMC7iYthki7Eq3pa8o= -google.golang.org/api v0.85.0/go.mod h1:AqZf8Ep9uZ2pyTvgL+x0D3Zt0eoT9b5E8fmzfu6FO2g= -google.golang.org/api v0.90.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.93.0/go.mod h1:+Sem1dnrKlrXMR/X0bPnMWyluQe4RsNoYfmNLhOIkzw= -google.golang.org/api v0.95.0/go.mod h1:eADj+UBuxkh5zlrSntJghuNeg8HwQ1w5lTKkuqaETEI= -google.golang.org/api v0.96.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.97.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.98.0/go.mod h1:w7wJQLTM+wvQpNf5JyEcBoxK0RH7EDrh/L4qfsuJ13s= -google.golang.org/api v0.99.0/go.mod h1:1YOf74vkVndF7pG6hIHuINsM7eWwpVTAfNMNiL91A08= -google.golang.org/api v0.100.0/go.mod h1:ZE3Z2+ZOr87Rx7dqFsdRQkRBk36kDtp/h+QpHbB7a70= -google.golang.org/api v0.102.0/go.mod h1:3VFl6/fzoA+qNuS1N1/VfXY4LjoXN/wzeIp7TweWwGo= -google.golang.org/api v0.103.0/go.mod h1:hGtW6nK1AC+d9si/UBhw8Xli+QMOf6xyNAyJw4qU9w0= -google.golang.org/api v0.106.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.107.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.108.0/go.mod h1:2Ts0XTHNVWxypznxWOYUeI4g3WdP9Pk2Qk58+a/O9MY= -google.golang.org/api v0.110.0/go.mod h1:7FC4Vvx1Mooxh8C5HWjzZHcavuS2f6pmJpZx60ca7iI= -google.golang.org/api v0.111.0/go.mod h1:qtFHvU9mhgTJegR31csQ+rwxyUTHOKFqCKWp1J0fdw0= -google.golang.org/api v0.114.0/go.mod h1:ifYI2ZsFK6/uGddGfAD5BMxlnkBqCmqHSDUVi45N5Yg= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= @@ -1950,120 +909,16 @@ google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200806141610-86f49bd18e98/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210310155132-4ce2db91004e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210319143718-93e7006c17a6/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210329143202-679c6ae281ee/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210402141018-6c239bbf2bb1/go.mod h1:9lPAdzaEmUacj36I+k7YKbEc5CXzPIeORRgDAUOu28A= -google.golang.org/genproto v0.0.0-20210513213006-bf773b8c8384/go.mod h1:P3QM42oQyzQSnHPnZ/vqoCdDmzH28fzWByN9asMeM8A= -google.golang.org/genproto v0.0.0-20210602131652-f16073e35f0c/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210604141403-392c879c8b08/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210608205507-b6d2f5bf0d7d/go.mod h1:UODoCrxHCcBojKKwX1terBiRUaqAsFqJiF615XL43r0= -google.golang.org/genproto v0.0.0-20210624195500-8bfb893ecb84/go.mod h1:SzzZ/N+nwJDaO1kznhnlzqS8ocJICar6hYhVyhi++24= -google.golang.org/genproto v0.0.0-20210713002101-d411969a0d9a/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210716133855-ce7ef5c701ea/go.mod h1:AxrInvYm1dci+enl5hChSFPOmmUF1+uAa/UsgNRWd7k= -google.golang.org/genproto v0.0.0-20210728212813-7823e685a01f/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210805201207-89edb61ffb67/go.mod h1:ob2IJxKrgPT52GcgX759i1sleT07tiKowYBGbczaW48= -google.golang.org/genproto v0.0.0-20210813162853-db860fec028c/go.mod h1:cFeNkxwySK631ADgubI+/XFU/xp8FD5KIVV4rj8UC5w= -google.golang.org/genproto v0.0.0-20210821163610-241b8fcbd6c8/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210828152312-66f60bf46e71/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210831024726-fe130286e0e2/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210903162649-d08c68adba83/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210909211513-a8c4777a87af/go.mod h1:eFjDcFEctNawg4eG61bRv87N7iHBWyVhJu7u1kqDUXY= -google.golang.org/genproto v0.0.0-20210924002016-3dee208752a0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211118181313-81c1377c94b1/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211206160659-862468c7d6e0/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211208223120-3a66f561d7aa/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20211221195035-429b39de9b1c/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220126215142-9970aeb2e350/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220207164111-0872dc986b00/go.mod h1:5CzLGKJ67TSI2B9POpiiyGha0AjJvZIUgRMt1dSmuhc= -google.golang.org/genproto v0.0.0-20220218161850-94dd64e39d7c/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220222213610-43724f9ea8cf/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220304144024-325a89244dc8/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220310185008-1973136f34c6/go.mod h1:kGP+zUP2Ddo0ayMi4YuN7C3WZyJvGLZRh8Z5wnAqvEI= -google.golang.org/genproto v0.0.0-20220324131243-acbaeb5b85eb/go.mod h1:hAL49I2IFola2sVEjAn7MEwsja0xp51I0tlGAf9hz4E= -google.golang.org/genproto v0.0.0-20220329172620-7be39ac1afc7/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220407144326-9054f6ed7bac/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220413183235-5e96e2839df9/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220414192740-2d67ff6cf2b4/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= -google.golang.org/genproto v0.0.0-20220502173005-c8bf987b8c21/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220518221133-4f43b3371335/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220523171625-347a074981d8/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220608133413-ed9918b62aac/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220616135557-88e70c0c3a90/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220617124728-180714bec0ad/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220624142145-8cd45d7dbd1f/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220628213854-d9e0b6570c03/go.mod h1:KEWEmljWE5zPzLBa/oHl6DaEt9LmfH6WtH1OHIvleBA= -google.golang.org/genproto v0.0.0-20220722212130-b98a9ff5e252/go.mod h1:GkXuJDJ6aQ7lnJcRF+SJVgFdQhypqgl3LB1C9vabdRE= -google.golang.org/genproto v0.0.0-20220801145646-83ce21fca29f/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= -google.golang.org/genproto v0.0.0-20220815135757-37a418bb8959/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220817144833-d7fd3f11b9b1/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829144015-23454907ede3/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220829175752-36a9c930ecbf/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= -google.golang.org/genproto v0.0.0-20220913154956-18f8339a66a5/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220914142337-ca0e39ece12f/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220915135415-7fd63a7952de/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220916172020-2692e8806bfa/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220919141832-68c03719ef51/go.mod h1:0Nb8Qy+Sk5eDzHnzlStwW3itdNaWoZA5XeSG+R3JHSo= -google.golang.org/genproto v0.0.0-20220920201722-2b89144ce006/go.mod h1:ht8XFiar2npT/g4vkk7O0WYS1sHOHbdujxbEp7CJWbw= -google.golang.org/genproto v0.0.0-20220926165614-551eb538f295/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20220926220553-6981cbe3cfce/go.mod h1:woMGP53BroOrRY3xTxlbr8Y3eB/nzAvvFM83q7kG2OI= -google.golang.org/genproto v0.0.0-20221010155953-15ba04fc1c0e/go.mod h1:3526vdqwhZAwq4wsRUaVG555sVgsNmIjRtO7t/JH29U= -google.golang.org/genproto v0.0.0-20221014173430-6e2ab493f96b/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221014213838-99cd37c6964a/go.mod h1:1vXfmgAz9N9Jx0QA82PqRVauvCz1SGSz739p0f183jM= -google.golang.org/genproto v0.0.0-20221024153911-1573dae28c9c/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221024183307-1bc688fe9f3e/go.mod h1:9qHF0xnpdSfF6knlcsnpzUu5y+rpwgbvsyGAZPBMg4s= -google.golang.org/genproto v0.0.0-20221027153422-115e99e71e1c/go.mod h1:CGI5F/G+E5bKwmfYo09AXuVN4dD894kIKUFmVbP2/Fo= -google.golang.org/genproto v0.0.0-20221109142239-94d6d90a7d66/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221114212237-e4508ebdbee1/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221117204609-8f9c96812029/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221118155620-16455021b5e6/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201164419-0e50fba7f41c/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221201204527-e3fa12d562f3/go.mod h1:rZS5c/ZVYMaOGBfO68GWtjOw/eLaZM1X6iVtgjZ+EWg= -google.golang.org/genproto v0.0.0-20221202195650-67e5cbc046fd/go.mod h1:cTsE614GARnxrLsqKREzmNYJACSWWpAWdNMwnD7c2BE= -google.golang.org/genproto v0.0.0-20221227171554-f9683d7f8bef/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230110181048-76db0878b65f/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230112194545-e10362b5ecf9/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230113154510-dbe35b8444a5/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230123190316-2c411cf9d197/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230124163310-31e0e69b6fc2/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230125152338-dcaf20b6aeaa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230127162408-596548ed4efa/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230209215440-0dfe4f8abfcc/go.mod h1:RGgjbofJ8xD9Sq1VVhDM1Vok1vRONV+rg+CjzG4SZKM= -google.golang.org/genproto v0.0.0-20230216225411-c8e22ba71e44/go.mod h1:8B0gmkoRebU8ukX6HP+4wrVQUY1+6PkQ44BSyIlflHA= -google.golang.org/genproto v0.0.0-20230222225845-10f96fb3dbec/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= -google.golang.org/genproto v0.0.0-20230303212802-e74f57abe488/go.mod h1:TvhZT5f700eVlTNwND1xoEZQeWTB2RY/65kplwl/bFA= -google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230320184635-7606e756e683/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s= -google.golang.org/genproto v0.0.0-20230323212658-478b75c54725/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230330154414-c0448cd141ea/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230331144136-dcfb400f0633/go.mod h1:UUQDJDOlWu4KYeJZffbWgBkS1YFobzKbLVfK69pe0Ak= -google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= @@ -2076,39 +931,9 @@ google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKa google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.36.1/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.37.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.37.1/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.38.0/go.mod h1:NREThFqKR1f3iQ6oBuvc5LadQuXVGo9rkm5ZGrQdJfM= -google.golang.org/grpc v1.39.0/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.39.1/go.mod h1:PImNr+rS9TWYb2O4/emRugxiyHZ5JyHW5F+RPnDzfrE= -google.golang.org/grpc v1.40.0/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.40.1/go.mod h1:ogyxbiOoUXAkP+4+xa6PZSE9DZgIHtSpzjDTB9KAK34= -google.golang.org/grpc v1.42.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ590SU= -google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= -google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.47.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.50.1/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= -google.golang.org/grpc v1.51.0/go.mod h1:wgNDFcnuBGmxLKI/qn4T+m5BtEBYXJPvibbUPsAIPww= -google.golang.org/grpc v1.52.3/go.mod h1:pu6fVzoFb+NBYNAvQL08ic+lvB2IojljRYuun5vorUY= -google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= -google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g= -google.golang.org/grpc v1.56.3/go.mod h1:I9bI3vqKfayGqPUAwGdOSu7kt6oIJLixfffKrpXqQ9s= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= -google.golang.org/grpc/examples v0.0.0-20210424002626-9572fd6faeae/go.mod h1:Ly7ZA/ARzg8fnPU9TyZIxoz33sEUuWX7txiqs8lPTgE= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -2121,7 +946,6 @@ gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/httprequest.v1 v1.2.0/go.mod h1:T61ZUaJLpMnzvoJDO03ZD8yRXD4nZzBeDoW5e9sffjg= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= gopkg.in/macaroon-bakery.v2 v2.3.0 h1:b40knPgPTke1QLTE8BSYeH7+R/hiIozB1A8CTLYN0Ic= gopkg.in/macaroon-bakery.v2 v2.3.0/go.mod h1:/8YhtPARXeRzbpEPLmRB66+gQE8/pzBBkWwg7Vz/guc= @@ -2154,66 +978,33 @@ honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWh honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.1.3/go.mod h1:NgwopIslSNH47DimFoV78dnkksY2EFtX0ajyb3K/las= -modernc.org/cc/v3 v3.36.0/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.2/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= -modernc.org/cc/v3 v3.36.3/go.mod h1:NFUHyPn4ekoC/JHeZFfZurN6ixxawE1BnVonP/oahEI= modernc.org/cc/v4 v4.21.4 h1:3Be/Rdo1fpr8GrQ7IVw9OHtplU4gWbb+wNgeoBMmGLQ= modernc.org/cc/v4 v4.21.4/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= -modernc.org/ccgo/v3 v3.0.0-20220428102840-41399a37e894/go.mod h1:eI31LL8EwEBKPpNpA4bU1/i+sKOwOrQy8D87zWUcRZc= -modernc.org/ccgo/v3 v3.0.0-20220430103911-bc99d88307be/go.mod h1:bwdAnOoaIt8Ax9YdWGjxWsdkPcZyRPHqrOvJxaKAKGw= -modernc.org/ccgo/v3 v3.16.4/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.6/go.mod h1:tGtX0gE9Jn7hdZFeU88slbTh1UtCYKusWOoCJuvkWsQ= -modernc.org/ccgo/v3 v3.16.8/go.mod h1:zNjwkizS+fIFDrDjIAgBSCLkWbJuHF+ar3QRn+Z9aws= -modernc.org/ccgo/v3 v3.16.9/go.mod h1:zNMzC9A9xeNUepy6KuZBbugn3c0Mc9TeiJO4lgvkJDo= modernc.org/ccgo/v4 v4.19.2 h1:lwQZgvboKD0jBwdaeVCTouxhxAyN6iawF3STraAal8Y= modernc.org/ccgo/v4 v4.19.2/go.mod h1:ysS3mxiMV38XGRTTcgo0DQTeTmAO4oCmJl1nX9VFI3s= -modernc.org/ccorpus v1.11.6/go.mod h1:2gEUTrWqdpH2pXsmTM1ZkjeSrUWDpjMu2T6m29L/ErQ= modernc.org/fileutil v1.3.0 h1:gQ5SIzK3H9kdfai/5x41oQiKValumqNTDXMvKo62HvE= modernc.org/fileutil v1.3.0/go.mod h1:XatxS8fZi3pS8/hKG2GH/ArUogfxjpEKs3Ku3aK4JyQ= modernc.org/gc/v2 v2.4.1 h1:9cNzOqPyMJBvrUipmynX0ZohMhcxPtMccYgGOJdOiBw= modernc.org/gc/v2 v2.4.1/go.mod h1:wzN5dK1AzVGoH6XOzc3YZ+ey/jPgYHLuVckd62P0GYU= -modernc.org/httpfs v1.0.6/go.mod h1:7dosgurJGp0sPaRanU53W4xZYKh14wfzX420oZADeHM= -modernc.org/libc v0.0.0-20220428101251-2d5f3daf273b/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.16.0/go.mod h1:N4LD6DBE9cf+Dzf9buBlzVJndKr/iJHG97vGLHYnb5A= -modernc.org/libc v1.16.1/go.mod h1:JjJE0eu4yeK7tab2n4S1w8tlWd9MxXLRzheaRnAKymU= -modernc.org/libc v1.16.17/go.mod h1:hYIV5VZczAmGZAnG15Vdngn5HSF5cSkbvfz2B7GRuVU= -modernc.org/libc v1.16.19/go.mod h1:p7Mg4+koNjc8jkqwcoFBJx7tXkpj00G77X7A72jXPXA= -modernc.org/libc v1.17.0/go.mod h1:XsgLldpP4aWlPlsjqKRdHPqCxCjISdHfM/yeWC5GyW0= -modernc.org/libc v1.17.1/go.mod h1:FZ23b+8LjxZs7XtFMbSzL/EhPxNbfZbErxEHc7cbD9s= modernc.org/libc v1.55.3 h1:AzcW1mhlPNrRtjS5sS+eW2ISCgSOLLNyFzRh/V3Qj/U= modernc.org/libc v1.55.3/go.mod h1:qFXepLhz+JjFThQ4kzwzOjA/y/artDeg+pcYnY+Q83w= -modernc.org/mathutil v1.2.2/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.4.1/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= -modernc.org/mathutil v1.5.0/go.mod h1:mZW8CKdRPY1v87qxC/wUdX5O1qDzXMP5TH3wjfpga6E= modernc.org/mathutil v1.6.0 h1:fRe9+AmYlaej+64JsEEhoWuAYBkOtQiMEU7n/XgfYi4= modernc.org/mathutil v1.6.0/go.mod h1:Ui5Q9q1TR2gFm0AQRqQUaBWFLAhQpCwNcuhBOSedWPo= -modernc.org/memory v1.1.1/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.0/go.mod h1:/0wo5ibyrQiaoUoH7f9D8dnglAmILJ5/cxZlRECf+Nw= -modernc.org/memory v1.2.1/go.mod h1:PkUhL0Mugw21sHPeskwZW4D6VscE/GQJOnIpCnW6pSU= modernc.org/memory v1.8.0 h1:IqGTL6eFMaDZZhEWwcREgeMXYwmW83LYW8cROZYkg+E= modernc.org/memory v1.8.0/go.mod h1:XPZ936zp5OMKGWPqbD3JShgd/ZoQ7899TUuQqxY+peU= -modernc.org/opt v0.1.1/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/opt v0.1.3 h1:3XOZf2yznlhC+ibLltsDGzABUGVx8J6pnFMS3E4dcq4= modernc.org/opt v0.1.3/go.mod h1:WdSiB5evDcignE70guQKxYUl14mgWtbClRi5wmkkTX0= modernc.org/sortutil v1.2.0 h1:jQiD3PfS2REGJNzNCMMaLSp/wdMNieTbKX920Cqdgqc= modernc.org/sortutil v1.2.0/go.mod h1:TKU2s7kJMf1AE84OoiGppNHJwvB753OYfNl2WRb++Ss= -modernc.org/sqlite v1.18.1/go.mod h1:6ho+Gow7oX5V+OiOQ6Tr4xeqbx13UZ6t+Fw9IRUG4d4= modernc.org/sqlite v1.34.5 h1:Bb6SR13/fjp15jt70CL4f18JIN7p7dnMExd+UFnF15g= modernc.org/sqlite v1.34.5/go.mod h1:YLuNmX9NKs8wRNK2ko1LW1NGYcc9FkBO69JOt1AR9JE= -modernc.org/strutil v1.1.1/go.mod h1:DE+MQQ/hjKBZS2zNInV5hhcipt5rLPWkmpbGeW5mmdw= -modernc.org/strutil v1.1.3/go.mod h1:MEHNA7PdEnEwLvspRMtWTNnp2nnyvMfkimT1NKNAGbw= modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA= modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0= -modernc.org/tcl v1.13.1/go.mod h1:XOLfOwzhkljL4itZkK6T72ckMgvj0BDsnKNdZVUOecw= -modernc.org/token v1.0.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y= modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM= -modernc.org/z v1.5.1/go.mod h1:eWFB510QWW5Th9YGZT81s+LwvaAs3Q2yr4sP0rmLkv8= pgregory.net/rapid v1.2.0 h1:keKAYRcjm+e1F0oAuU5F5+YPAWcyxNNRK2wud503Gnk= pgregory.net/rapid v1.2.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= sigs.k8s.io/yaml v1.2.0 h1:kr/MCeFWJWTwyaHoR9c8EjH9OumOmoF9YGiZd7lFm/Q= diff --git a/instantout/actions.go b/instantout/actions.go index d1c405fd..329add5e 100644 --- a/instantout/actions.go +++ b/instantout/actions.go @@ -20,6 +20,7 @@ import ( "github.com/lightningnetwork/lnd/lnrpc/walletrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" + "github.com/lightningnetwork/lnd/lnwire" ) const ( @@ -51,6 +52,10 @@ const ( // htlcExpiryDelta is the delta in blocks we require between the htlc // expiry and reservation expiry. htlcExpiryDelta = int32(40) + + // htlcRecoverySafetyDelta leaves one urgent confirmation target for + // the HTLC and another for its preimage sweep after recovery. + htlcRecoverySafetyDelta = 2 * urgentConfTarget ) // InitInstantOutCtx contains the context for the InitInstantOutAction. @@ -61,8 +66,12 @@ type InitInstantOutCtx struct { outgoingChanSet loopdb.ChannelSet protocolVersion ProtocolVersion sweepAddress btcutil.Address + maxSwapFee *btcutil.Amount } +// RecoverInstantOutCtx marks an action as being resumed after restart. +type RecoverInstantOutCtx struct{} + // InitInstantOutAction is the first action that is executed when the instant // out FSM is started. It will send the instant out request to the server. func (f *FSM) InitInstantOutAction(ctx context.Context, @@ -78,7 +87,7 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, } var ( - reservationAmt uint64 + reservationAmt btcutil.Amount reservationIds = make([][]byte, 0, len(initCtx.reservations)) reservations = make( []*reservation.Reservation, 0, len(initCtx.reservations), @@ -99,7 +108,7 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, "locked", reservationId)) } - reservationAmt += uint64(res.Value) + reservationAmt += res.Value reservationIds = append(reservationIds, resId[:]) reservations = append(reservations, res) @@ -161,6 +170,11 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, return f.HandleError(fmt.Errorf("invalid swap invoice hash: "+ "expected %x got %x", preimage.Hash(), payReq.Hash)) } + if err := validateInstantOutInvoiceAmount( + payReq.Value, reservationAmt, initCtx.maxSwapFee, + ); err != nil { + return f.HandleError(err) + } serverPubkey, err := btcec.ParsePubKey(instantOutResponse.SenderKey) if err != nil { return f.HandleError(err) @@ -179,6 +193,11 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, } // Now we can create the instant out. + var maxSwapFee btcutil.Amount + if initCtx.maxSwapFee != nil { + maxSwapFee = *initCtx.maxSwapFee + } + instantOut := &InstantOut{ SwapHash: swapHash, swapPreimage: preimage, @@ -188,7 +207,8 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, CltvExpiry: initCtx.cltvExpiry, clientPubkey: keyRes.PubKey, serverPubkey: serverPubkey, - Value: btcutil.Amount(reservationAmt), + Value: reservationAmt, + MaxSwapFee: maxSwapFee, htlcFeeRate: feeRate, swapInvoice: instantOutResponse.SwapInvoice, Reservations: reservations, @@ -206,6 +226,37 @@ func (f *FSM) InitInstantOutAction(ctx context.Context, return OnInit } +// validateInstantOutInvoiceAmount verifies that the server invoice doesn't +// charge more than the client-approved swap fee. Sub-satoshi fees are rounded +// up so the cap cannot be bypassed with millisatoshi precision. +func validateInstantOutInvoiceAmount(invoiceAmount lnwire.MilliSatoshi, + swapAmount btcutil.Amount, maxSwapFee *btcutil.Amount) error { + + // Omitting the cap preserves the behavior of clients that predate this + // field. In-tree callers set it explicitly after accepting a quote. + if maxSwapFee == nil { + return nil + } + + if *maxSwapFee < 0 { + return fmt.Errorf("maximum swap fee must not be negative") + } + + swapAmountMsat := lnwire.NewMSatFromSatoshis(swapAmount) + if invoiceAmount <= swapAmountMsat { + return nil + } + + swapFeeMsat := invoiceAmount - swapAmountMsat + swapFeeSat := btcutil.Amount((int64(swapFeeMsat)-1)/1000 + 1) + if swapFeeSat > *maxSwapFee { + return fmt.Errorf("instant out swap fee %d exceeds maximum %d", + swapFeeSat, *maxSwapFee) + } + + return nil +} + // PollPaymentAcceptedAction locks the reservations, sends the payment to the // server and polls the server for the payment status. func (f *FSM) PollPaymentAcceptedAction(ctx context.Context, @@ -293,6 +344,15 @@ func (f *FSM) BuildHTLCAction(ctx context.Context, } f.htlcMusig2Sessions = htlcSessions + defer func() { + err := cleanupMuSig2Sessions( + ctx, f.cfg.Signer, f.htlcMusig2Sessions, + ) + if err != nil { + f.Errorf("unable to clean up HTLC MuSig2 sessions: %v", err) + } + f.htlcMusig2Sessions = nil + }() // Send the server the client nonces. htlcInitRes, err := f.cfg.InstantOutClient.InitHtlcSig( @@ -373,6 +433,46 @@ func (f *FSM) BuildHTLCAction(ctx context.Context, func (f *FSM) PushPreimageAction(ctx context.Context, eventCtx fsm.EventContext) fsm.EventType { + // A recovered swap may have been offline long enough that the server's + // reservation timeout is now close. Fall back to the already finalized + // HTLC instead of revealing the preimage without enough time to publish + // that safety transaction. + if _, ok := eventCtx.(*RecoverInstantOutCtx); ok { + info, err := f.cfg.LndClient.GetInfo(ctx) + if err != nil { + f.LastActionError = fmt.Errorf( + "unable to get recovery chain height: %w", err, + ) + + return OnErrorPublishHtlc + } + + currentHeight := int64(info.BlockHeight) + minHtlcExpiry := currentHeight + + int64(htlcRecoverySafetyDelta) + if int64(f.InstantOut.CltvExpiry) < minHtlcExpiry { + f.LastActionError = fmt.Errorf("instant out HTLC expires at "+ + "height %d, before recovery safety height %d", + f.InstantOut.CltvExpiry, minHtlcExpiry) + + return OnErrorPublishHtlc + } + + minReservationExpiry := currentHeight + + int64(htlcExpiryDelta) + for _, res := range f.InstantOut.Reservations { + if int64(res.Expiry) >= minReservationExpiry { + continue + } + + f.LastActionError = fmt.Errorf("reservation %x expires at "+ + "height %d, before recovery safety height %d", + res.ID, res.Expiry, minReservationExpiry) + + return OnErrorPublishHtlc + } + } + // First we'll create the musig2 context. coopSessions, coopClientNonces, err := f.InstantOut.createMusig2Session( ctx, f.cfg.Signer, @@ -382,6 +482,15 @@ func (f *FSM) PushPreimageAction(ctx context.Context, } f.sweeplessSweepSessions = coopSessions + defer func() { + err := cleanupMuSig2Sessions( + ctx, f.cfg.Signer, f.sweeplessSweepSessions, + ) + if err != nil { + f.Errorf("unable to clean up sweep MuSig2 sessions: %v", err) + } + f.sweeplessSweepSessions = nil + }() // Get the feerate for the coop sweep. feeRate, err := f.cfg.Wallet.EstimateFeeRate(ctx, normalConfTarget) @@ -617,20 +726,23 @@ func (f *FSM) WaitForHtlcSweepConfirmedAction(ctx context.Context, // handleErrorAndUnlockReservations handles an error and unlocks the // reservations. func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context, - err error) fsm.EventType { + actionErr error) fsm.EventType { // We might get here from a canceled context, we create a new context // with a timeout to unlock the reservations. - ctx, cancel := context.WithTimeout(ctx, time.Second*30) + cleanupCtx, cancel := context.WithTimeout( + context.WithoutCancel(ctx), time.Second*30, + ) defer cancel() // Unlock the reservations. + var unlockErr error for _, reservation := range f.InstantOut.Reservations { err := f.cfg.ReservationManager.UnlockReservation( - ctx, reservation.ID, + cleanupCtx, reservation.ID, ) if err != nil { f.Errorf("error unlocking reservation: %v", err) - return f.HandleError(err) + unlockErr = errors.Join(unlockErr, err) } } @@ -638,10 +750,12 @@ func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context, // release the reservations. This can be done in a goroutine as we // wan't to fail the fsm early. go func() { - ctx, cancel := context.WithTimeout(ctx, time.Second*30) + cancelCtx, cancel := context.WithTimeout( + context.WithoutCancel(ctx), time.Second*30, + ) defer cancel() _, cancelErr := f.cfg.InstantOutClient.CancelInstantSwap( - ctx, &swapserverrpc.CancelInstantSwapRequest{ + cancelCtx, &swapserverrpc.CancelInstantSwapRequest{ SwapHash: f.InstantOut.SwapHash[:], }, ) @@ -652,7 +766,13 @@ func (f *FSM) handleErrorAndUnlockReservations(ctx context.Context, } }() - return f.HandleError(err) + // Preserve the action failure when cleanup also fails. If cleanup was + // the only failure, report it to the state machine. + if actionErr != nil { + return f.HandleError(actionErr) + } + + return f.HandleError(unlockErr) } func getMaxRoutingFee(amt btcutil.Amount) btcutil.Amount { diff --git a/instantout/cleanup_test.go b/instantout/cleanup_test.go new file mode 100644 index 00000000..45640079 --- /dev/null +++ b/instantout/cleanup_test.go @@ -0,0 +1,77 @@ +package instantout + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/instantout/reservation" + "github.com/lightninglabs/loop/swapserverrpc" + "github.com/stretchr/testify/require" + "google.golang.org/grpc" +) + +type cleanupTestReservationManager struct { + ReservationManager + + unlockErr error +} + +func (m *cleanupTestReservationManager) UnlockReservation(context.Context, + reservation.ID) error { + + return m.unlockErr +} + +type cleanupTestInstantOutClient struct { + swapserverrpc.InstantSwapServerClient + + canceled chan struct{} +} + +func (c *cleanupTestInstantOutClient) CancelInstantSwap(context.Context, + *swapserverrpc.CancelInstantSwapRequest, ...grpc.CallOption) ( + *swapserverrpc.CancelInstantSwapResponse, error) { + + close(c.canceled) + return &swapserverrpc.CancelInstantSwapResponse{}, nil +} + +// TestCleanupPreservesActionError verifies that an unlock failure doesn't +// replace the action failure or prevent the cancellation notification. +func TestCleanupPreservesActionError(t *testing.T) { + actionErr := errors.New("action failed") + cancelClient := &cleanupTestInstantOutClient{ + canceled: make(chan struct{}), + } + instantOutFSM := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + ReservationManager: &cleanupTestReservationManager{ + unlockErr: errors.New("unlock failed"), + }, + InstantOutClient: cancelClient, + }, + InstantOut: &InstantOut{ + Reservations: []*reservation.Reservation{ + {ID: reservation.ID{1}}, + }, + }, + } + + event := instantOutFSM.handleErrorAndUnlockReservations( + t.Context(), actionErr, + ) + require.Equal(t, fsm.OnError, event) + require.ErrorIs(t, instantOutFSM.LastActionError, actionErr) + require.Eventually(t, func() bool { + select { + case <-cancelClient.canceled: + return true + default: + return false + } + }, time.Second, time.Millisecond) +} diff --git a/instantout/fsm.md b/instantout/fsm.md index 4b90fb27..a709aa68 100644 --- a/instantout/fsm.md +++ b/instantout/fsm.md @@ -2,35 +2,41 @@ stateDiagram-v2 [*] --> Init: OnStart BuildHtlc +BuildHtlc --> InstantOutFailed: OnError BuildHtlc --> PushPreimage: OnHtlcSigReceived -BuildHtlc --> InstantFailedOutFailed: OnError -BuildHtlc --> InstantFailedOutFailed: OnRecover +BuildHtlc --> InstantOutFailed: OnRecover FailedHtlcSweep +FailedHtlcSweep --> PublishHtlcSweep: OnRecover +FinishedHtlcPreimageSweep FinishedSweeplessSweep Init +Init --> InstantOutFailed: OnError Init --> SendPaymentAndPollAccepted: OnInit -Init --> InstantFailedOutFailed: OnError -Init --> InstantFailedOutFailed: OnRecover -InstantFailedOutFailed +Init --> InstantOutFailed: OnRecover +InstantOutFailed PublishHtlc PublishHtlc --> FailedHtlcSweep: OnError +PublishHtlc --> PublishHtlcSweep: OnHtlcPublished PublishHtlc --> PublishHtlc: OnRecover -PublishHtlc --> WaitForHtlcSweepConfirmed: OnHtlcBroadcasted +PublishHtlcSweep +PublishHtlcSweep --> FailedHtlcSweep: OnError +PublishHtlcSweep --> WaitForHtlcSweepConfirmed: OnHtlcSweepPublished +PublishHtlcSweep --> PublishHtlcSweep: OnRecover PushPreimage +PushPreimage --> InstantOutFailed: OnError +PushPreimage --> PublishHtlc: OnErrorPublishHtlc PushPreimage --> PushPreimage: OnRecover PushPreimage --> WaitForSweeplessSweepConfirmed: OnSweeplessSweepPublished -PushPreimage --> InstantFailedOutFailed: OnError -PushPreimage --> PublishHtlc: OnErrorPublishHtlc SendPaymentAndPollAccepted +SendPaymentAndPollAccepted --> InstantOutFailed: OnError SendPaymentAndPollAccepted --> BuildHtlc: OnPaymentAccepted -SendPaymentAndPollAccepted --> InstantFailedOutFailed: OnError -SendPaymentAndPollAccepted --> InstantFailedOutFailed: OnRecover +SendPaymentAndPollAccepted --> InstantOutFailed: OnRecover WaitForHtlcSweepConfirmed +WaitForHtlcSweepConfirmed --> FailedHtlcSweep: OnError WaitForHtlcSweepConfirmed --> FinishedHtlcPreimageSweep: OnHtlcSwept WaitForHtlcSweepConfirmed --> WaitForHtlcSweepConfirmed: OnRecover -WaitForHtlcSweepConfirmed --> FailedHtlcSweep: OnError WaitForSweeplessSweepConfirmed -WaitForSweeplessSweepConfirmed --> FinishedSweeplessSweep: OnSweeplessSweepConfirmed -WaitForSweeplessSweepConfirmed --> WaitForSweeplessSweepConfirmed: OnRecover WaitForSweeplessSweepConfirmed --> PublishHtlc: OnError +WaitForSweeplessSweepConfirmed --> WaitForSweeplessSweepConfirmed: OnRecover +WaitForSweeplessSweepConfirmed --> FinishedSweeplessSweep: OnSweeplessSweepConfirmed ``` \ No newline at end of file diff --git a/instantout/instantout.go b/instantout/instantout.go index f8c89eb0..c700ee91 100644 --- a/instantout/instantout.go +++ b/instantout/instantout.go @@ -5,6 +5,7 @@ import ( "errors" "fmt" "reflect" + "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" @@ -25,6 +26,8 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" ) +const muSig2CleanupTimeout = 5 * time.Second + // InstantOut holds the necessary information to execute an instant out swap. type InstantOut struct { // SwapHash is the hash of the swap. @@ -57,6 +60,9 @@ type InstantOut struct { // Value is the amount that is swapped. Value btcutil.Amount + // MaxSwapFee is the maximum off-chain swap fee accepted by the client. + MaxSwapFee btcutil.Amount + // keyLocator is the key locator that is used for the swap. keyLocator keychain.KeyLocator @@ -112,7 +118,10 @@ func (i *InstantOut) createMusig2Session(ctx context.Context, for idx, reservation := range i.Reservations { session, err := reservation.Musig2CreateSession(ctx, signer) if err != nil { - return nil, nil, err + cleanupErr := cleanupMuSig2Sessions( + ctx, signer, musig2Sessions[:idx], + ) + return nil, nil, errors.Join(err, cleanupErr) } musig2Sessions[idx] = session @@ -122,6 +131,32 @@ func (i *InstantOut) createMusig2Session(ctx context.Context, return musig2Sessions, clientNonces, nil } +// cleanupMuSig2Sessions removes completed or abandoned MuSig2 sessions from +// lnd. Cleanup uses a bounded context that survives cancellation of the swap +// action that created the sessions. +func cleanupMuSig2Sessions(ctx context.Context, signer lndclient.SignerClient, + sessions []*input.MuSig2SessionInfo) error { + + cleanupCtx, cancel := context.WithTimeout( + context.WithoutCancel(ctx), muSig2CleanupTimeout, + ) + defer cancel() + + var cleanupErr error + for _, session := range sessions { + if session == nil { + continue + } + + err := signer.MuSig2Cleanup(cleanupCtx, session.SessionID) + if err != nil { + cleanupErr = errors.Join(cleanupErr, err) + } + } + + return cleanupErr +} + // getInputReservations returns the input reservations for the instant out. func (i *InstantOut) getInputReservations() (InputReservations, error) { if len(i.Reservations) == 0 { @@ -263,12 +298,31 @@ func (i *InstantOut) signMusig2Tx(ctx context.Context, if err != nil { return nil, err } + if tx == nil { + return nil, errors.New("transaction is nil") + } + if len(tx.TxIn) != len(inputs) { + return nil, fmt.Errorf("invalid number of transaction inputs: "+ + "expected %d, got %d", len(inputs), len(tx.TxIn)) + } + if len(musig2sessions) != len(inputs) { + return nil, fmt.Errorf("invalid number of MuSig2 sessions: "+ + "expected %d, got %d", len(inputs), len(musig2sessions)) + } + if len(counterPartyNonces) != len(inputs) { + return nil, fmt.Errorf("invalid number of server nonces: "+ + "expected %d, got %d", len(inputs), len(counterPartyNonces)) + } prevOutFetcher := inputs.GetPrevoutFetcher() sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) sigs := make([][]byte, len(inputs)) for idx, reservation := range inputs { + if musig2sessions[idx] == nil { + return nil, fmt.Errorf("MuSig2 session %d is nil", idx) + } + if !reflect.DeepEqual(tx.TxIn[idx].PreviousOutPoint, reservation.Outpoint) { @@ -329,8 +383,30 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, if err != nil { return nil, err } + if tx == nil { + return nil, errors.New("transaction is nil") + } + if len(tx.TxIn) != len(inputs) { + return nil, fmt.Errorf("invalid number of transaction inputs: "+ + "expected %d, got %d", len(inputs), len(tx.TxIn)) + } + if len(musig2Sessions) != len(inputs) { + return nil, fmt.Errorf("invalid number of MuSig2 sessions: "+ + "expected %d, got %d", len(inputs), len(musig2Sessions)) + } + if len(serverSigs) != len(inputs) { + return nil, fmt.Errorf("invalid number of server signatures: "+ + "expected %d, got %d", len(inputs), len(serverSigs)) + } + + prevOutFetcher := inputs.GetPrevoutFetcher() + sigHashes := txscript.NewTxSigHashes(tx, prevOutFetcher) for idx := range inputs { + if musig2Sessions[idx] == nil { + return nil, fmt.Errorf("MuSig2 session %d is nil", idx) + } + haveAllSigs, finalSig, err := signer.MuSig2CombineSig( ctx, musig2Sessions[idx].SessionID, [][]byte{serverSigs[idx]}, @@ -343,7 +419,26 @@ func (i *InstantOut) finalizeMusig2Transaction(ctx context.Context, return nil, fmt.Errorf("missing sigs") } + // lnd removes a MuSig2 session automatically once all signatures + // have been combined. Clear the local entry so the caller's deferred + // cleanup only targets sessions abandoned on an error path. + musig2Sessions[idx] = nil + tx.TxIn[idx].Witness = wire.TxWitness{finalSig} + + vm, err := txscript.NewEngine( + inputs[idx].PkScript, tx, idx, + txscript.StandardVerifyFlags, nil, sigHashes, + int64(inputs[idx].Value), prevOutFetcher, + ) + if err != nil { + return nil, fmt.Errorf("unable to verify final MuSig2 "+ + "signature for input %d: %w", idx, err) + } + if err := vm.Execute(); err != nil { + return nil, fmt.Errorf("invalid final MuSig2 signature "+ + "for input %d: %w", idx, err) + } } return tx, nil diff --git a/instantout/instantout_test.go b/instantout/instantout_test.go new file mode 100644 index 00000000..66301a22 --- /dev/null +++ b/instantout/instantout_test.go @@ -0,0 +1,261 @@ +package instantout + +import ( + "context" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/instantout/reservation" + "github.com/lightningnetwork/lnd/input" + "github.com/lightningnetwork/lnd/lnwire" + "github.com/stretchr/testify/require" +) + +type invalidFinalSigSigner struct { + lndclient.SignerClient +} + +func (s *invalidFinalSigSigner) MuSig2CombineSig(context.Context, [32]byte, + [][]byte) (bool, []byte, error) { + + return true, make([]byte, 64), nil +} + +type cleanupTrackingSigner struct { + lndclient.SignerClient + + cleaned [][32]byte +} + +type fixedHeightLightningClient struct { + lndclient.LightningClient + + height uint32 + err error +} + +func (c *fixedHeightLightningClient) GetInfo(context.Context) ( + *lndclient.Info, error) { + + if c.err != nil { + return nil, c.err + } + + return &lndclient.Info{BlockHeight: c.height}, nil +} + +func (s *cleanupTrackingSigner) MuSig2Cleanup(_ context.Context, + sessionID [32]byte) error { + + s.cleaned = append(s.cleaned, sessionID) + return nil +} + +// TestMuSig2VectorLengthValidation verifies that malformed server-controlled +// vectors are rejected before they can be indexed. +func TestMuSig2VectorLengthValidation(t *testing.T) { + _, pubKey := btcec.PrivKeyFromBytes([]byte{1}) + instantOut := &InstantOut{ + Reservations: []*reservation.Reservation{ + { + ClientPubkey: pubKey, + ServerPubkey: pubKey, + Value: btcutil.Amount(100_000), + Expiry: 200, + Outpoint: &wire.OutPoint{}, + }, + }, + } + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{}) + sessions := []*input.MuSig2SessionInfo{{}} + + require.NotPanics(t, func() { + _, err := instantOut.signMusig2Tx( + context.Background(), nil, tx, sessions, nil, + ) + require.ErrorContains(t, err, "server nonces") + }) + + require.NotPanics(t, func() { + _, err := instantOut.finalizeMusig2Transaction( + context.Background(), nil, sessions, tx, nil, + ) + require.ErrorContains(t, err, "server signatures") + }) +} + +// TestFinalizeMuSig2TransactionVerifiesSignature verifies that a combined +// signature is validated locally before the transaction can be used as the +// instant-out safety net. +func TestFinalizeMuSig2TransactionVerifiesSignature(t *testing.T) { + _, pubKey := btcec.PrivKeyFromBytes([]byte{1}) + res := &reservation.Reservation{ + ClientPubkey: pubKey, + ServerPubkey: pubKey, + Value: btcutil.Amount(100_000), + Expiry: 200, + Outpoint: &wire.OutPoint{}, + } + instantOut := &InstantOut{ + Reservations: []*reservation.Reservation{res}, + } + tx := wire.NewMsgTx(2) + tx.AddTxIn(&wire.TxIn{PreviousOutPoint: *res.Outpoint}) + tx.AddTxOut(&wire.TxOut{Value: 90_000}) + + sessions := []*input.MuSig2SessionInfo{{}} + _, err := instantOut.finalizeMusig2Transaction( + context.Background(), &invalidFinalSigSigner{}, + sessions, tx, [][]byte{{1}}, + ) + require.ErrorContains(t, err, "invalid final MuSig2 signature") + require.Nil(t, sessions[0]) +} + +// TestCleanupMuSig2Sessions verifies that all allocated sessions are released +// while nil entries from partial session creation are skipped. +func TestCleanupMuSig2Sessions(t *testing.T) { + firstID := [32]byte{1} + secondID := [32]byte{2} + signer := &cleanupTrackingSigner{} + + err := cleanupMuSig2Sessions( + t.Context(), signer, []*input.MuSig2SessionInfo{ + {SessionID: firstID}, nil, {SessionID: secondID}, + }, + ) + require.NoError(t, err) + require.Equal(t, [][32]byte{firstID, secondID}, signer.cleaned) +} + +// TestPushPreimageRejectsExpiringReservation verifies that recovery takes the +// on-chain fallback before revealing the preimage when a reservation is too +// close to its server-controlled timeout. +func TestPushPreimageRejectsExpiringReservation(t *testing.T) { + instantOutFSM := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + LndClient: &fixedHeightLightningClient{height: 100}, + }, + InstantOut: &InstantOut{ + CltvExpiry: 200, + Reservations: []*reservation.Reservation{ + { + ID: reservation.ID{1}, + Expiry: 139, + }, + }, + }, + } + + event := instantOutFSM.PushPreimageAction( + t.Context(), &RecoverInstantOutCtx{}, + ) + require.Equal(t, OnErrorPublishHtlc, event) + require.ErrorContains( + t, instantOutFSM.LastActionError, "before recovery safety height", + ) +} + +// TestPushPreimageRejectsExpiringHtlc verifies that recovery uses a fresh +// chain height and leaves time to confirm both fallback transactions. +func TestPushPreimageRejectsExpiringHtlc(t *testing.T) { + instantOutFSM := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + LndClient: &fixedHeightLightningClient{height: 100}, + }, + InstantOut: &InstantOut{ + CltvExpiry: 105, + Reservations: []*reservation.Reservation{ + { + ID: reservation.ID{1}, + Expiry: 200, + }, + }, + }, + } + + event := instantOutFSM.PushPreimageAction( + t.Context(), &RecoverInstantOutCtx{}, + ) + require.Equal(t, OnErrorPublishHtlc, event) + require.ErrorContains( + t, instantOutFSM.LastActionError, + "instant out HTLC expires at height 105", + ) +} + +// TestValidateInstantOutInvoiceAmount verifies enforcement of the fee cap at +// millisatoshi precision. +func TestValidateInstantOutInvoiceAmount(t *testing.T) { + const swapAmount = btcutil.Amount(100_000) + + maxSwapFee := btcutil.Amount(200) + zeroSwapFee := btcutil.Amount(0) + negativeSwapFee := btcutil.Amount(-1) + + tests := []struct { + name string + invoiceAmount lnwire.MilliSatoshi + maxSwapFee *btcutil.Amount + expectErr bool + }{ + { + name: "exact fee cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount + maxSwapFee, + ), + maxSwapFee: &maxSwapFee, + }, + { + name: "one millisatoshi over fee cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount+maxSwapFee, + ) + 1, + maxSwapFee: &maxSwapFee, + expectErr: true, + }, + { + name: "discounted invoice", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount - 1, + ), + maxSwapFee: &zeroSwapFee, + }, + { + name: "negative cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount, + ), + maxSwapFee: &negativeSwapFee, + expectErr: true, + }, + { + name: "omitted cap", + invoiceAmount: lnwire.NewMSatFromSatoshis( + swapAmount + maxSwapFee + 1, + ), + maxSwapFee: nil, + }, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + err := validateInstantOutInvoiceAmount( + tc.invoiceAmount, swapAmount, tc.maxSwapFee, + ) + if tc.expectErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + }) + } +} diff --git a/instantout/manager.go b/instantout/manager.go index 37ccb681..e9ecb352 100644 --- a/instantout/manager.go +++ b/instantout/manager.go @@ -20,6 +20,20 @@ var ( ErrSwapDoesNotExist = errors.New("swap does not exist") ) +type newInstantOutOptions struct { + maxSwapFee *btcutil.Amount +} + +// NewInstantOutOption customizes an instant out request. +type NewInstantOutOption func(*newInstantOutOptions) + +// WithMaxSwapFee limits the off-chain fee accepted for an instant out. +func WithMaxSwapFee(maxSwapFee btcutil.Amount) NewInstantOutOption { + return func(options *newInstantOutOptions) { + options.maxSwapFee = &maxSwapFee + } +} + // Manager manages the instantout state machines. type Manager struct { sync.Mutex @@ -119,8 +133,9 @@ func (m *Manager) recoverInstantOuts(ctx context.Context) error { // As SendEvent can block, we'll start a goroutine to process // the event. + recoverCtx := &RecoverInstantOutCtx{} go func() { - err := instantOutFSM.SendEvent(ctx, OnRecover, nil) + err := instantOutFSM.SendEvent(ctx, OnRecover, recoverCtx) if err != nil { log.Errorf("FSM %v Error sending recover "+ "event %v, state: %v", @@ -135,7 +150,21 @@ func (m *Manager) recoverInstantOuts(ctx context.Context) error { // NewInstantOut creates a new instantout. func (m *Manager) NewInstantOut(ctx context.Context, - reservations []reservation.ID, sweepAddress string) (*FSM, error) { + reservations []reservation.ID, sweepAddress string, + options ...NewInstantOutOption) (*FSM, error) { + + requestOptions := &newInstantOutOptions{} + for _, option := range options { + if option != nil { + option(requestOptions) + } + } + + if requestOptions.maxSwapFee != nil && + *requestOptions.maxSwapFee < 0 { + + return nil, fmt.Errorf("maximum swap fee must not be negative") + } var ( sweepAddr btcutil.Address @@ -158,6 +187,7 @@ func (m *Manager) NewInstantOut(ctx context.Context, initationHeight: m.currentHeight, protocolVersion: CurrentProtocolVersion(), sweepAddress: sweepAddr, + maxSwapFee: requestOptions.maxSwapFee, } instantOut, err := NewFSM(m.cfg, ProtocolVersionFullReservation) diff --git a/instantout/reservation/actions_test.go b/instantout/reservation/actions_test.go index 40e6509b..643b8ce3 100644 --- a/instantout/reservation/actions_test.go +++ b/instantout/reservation/actions_test.go @@ -203,6 +203,7 @@ func TestSubscribeToConfirmationAction(t *testing.T) { blockHeight int32 blockErr error sendTxConf bool + outputValue btcutil.Amount confErr error expectedEvent fsm.EventType }{ @@ -210,8 +211,15 @@ func TestSubscribeToConfirmationAction(t *testing.T) { name: "success", blockHeight: 0, sendTxConf: true, + outputValue: defaultValue, expectedEvent: OnConfirmed, }, + { + name: "reservation value mismatch", + sendTxConf: true, + outputValue: defaultValue - 1, + expectedEvent: fsm.OnError, + }, { name: "expired", blockHeight: 100, @@ -273,7 +281,7 @@ func TestSubscribeToConfirmationAction(t *testing.T) { TxIn: []*wire.TxIn{}, TxOut: []*wire.TxOut{ { - Value: int64(defaultValue), + Value: int64(tc.outputValue), PkScript: pkScript, }, }, diff --git a/instantout/reservation/reservation_fsm.md b/instantout/reservation/fsm.md similarity index 57% rename from instantout/reservation/reservation_fsm.md rename to instantout/reservation/fsm.md index 1eefb5c6..6a48d367 100644 --- a/instantout/reservation/reservation_fsm.md +++ b/instantout/reservation/fsm.md @@ -2,20 +2,28 @@ stateDiagram-v2 [*] --> Init: OnServerRequest Confirmed -Confirmed --> SpendBroadcasted: OnSpendBroadcasted -Confirmed --> TimedOut: OnTimedOut +Confirmed --> Confirmed: OnError +Confirmed --> Locked: OnLocked Confirmed --> Confirmed: OnRecover +Confirmed --> Spent: OnSpent +Confirmed --> TimedOut: OnTimedOut Failed Init Init --> WaitForConfirmation: OnBroadcast -Init --> Failed: OnRecover Init --> Failed: OnError -SpendBroadcasted -SpendBroadcasted --> SpendConfirmed: OnSpendConfirmed -SpendConfirmed +Init --> Failed: OnRecover +Locked +Locked --> Locked: OnError +Locked --> Locked: OnRecover +Locked --> Spent: OnSpent +Locked --> TimedOut: OnTimedOut +Locked --> Confirmed: OnUnlocked +Spent +Spent --> Spent: OnSpent TimedOut +TimedOut --> TimedOut: OnTimedOut WaitForConfirmation -WaitForConfirmation --> WaitForConfirmation: OnRecover WaitForConfirmation --> Confirmed: OnConfirmed +WaitForConfirmation --> WaitForConfirmation: OnRecover WaitForConfirmation --> TimedOut: OnTimedOut ``` \ No newline at end of file diff --git a/instantout/reservation/interfaces.go b/instantout/reservation/interfaces.go index 04bf830d..23658d5a 100644 --- a/instantout/reservation/interfaces.go +++ b/instantout/reservation/interfaces.go @@ -8,14 +8,18 @@ import ( ) var ( - ErrReservationAlreadyExists = fmt.Errorf("reservation already exists") - ErrReservationNotFound = fmt.Errorf("reservation not found") + ErrReservationAlreadyExists = fmt.Errorf("reservation already exists") + ErrReservationNotFound = fmt.Errorf("reservation not found") + ErrTooManyActiveReservations = fmt.Errorf( + "too many active reservations", + ) ) const ( - KeyFamily = int32(42068) - DefaultConfTarget = int32(3) - IdLength = 32 + KeyFamily = int32(42068) + DefaultConfTarget = int32(3) + IdLength = 32 + maxActiveReservations = 1000 ) // Store is the interface that stores the reservations. diff --git a/instantout/reservation/manager.go b/instantout/reservation/manager.go index 600febfe..bccdfa1e 100644 --- a/instantout/reservation/manager.go +++ b/instantout/reservation/manager.go @@ -2,6 +2,7 @@ package reservation import ( "context" + "errors" "fmt" "strings" "sync" @@ -13,6 +14,11 @@ import ( reservationrpc "github.com/lightninglabs/loop/swapserverrpc" ) +var ( + reservationStateWaitTimeout = 5 * time.Second + reservationStatePollDelay = time.Second +) + // Manager manages the reservation state machines. type Manager struct { sync.Mutex @@ -25,6 +31,28 @@ type Manager struct { activeReservations map[ID]*FSM } +// finalStateObserver removes a reservation FSM from the active set once it +// reaches a terminal state. +type finalStateObserver struct { + manager *Manager + id ID + fsm *FSM +} + +// Notify implements the fsm.Observer interface. +func (o *finalStateObserver) Notify(notification fsm.Notification) { + if !isFinalState(notification.NextState) { + return + } + + o.manager.Lock() + defer o.manager.Unlock() + + if o.manager.activeReservations[o.id] == o.fsm { + delete(o.manager.activeReservations, o.id) + } +} + // NewManager creates a new reservation manager. func NewManager(cfg *Config) *Manager { return &Manager{ @@ -80,7 +108,8 @@ func (m *Manager) Run(ctx context.Context, height int32, runCtx, uint32(currentHeight), reservationRes, ) if err != nil { - return err + log.Errorf("Unable to create reservation %x: %v", + reservationRes.ReservationId, err) } case err := <-newBlockErrChan: @@ -110,16 +139,41 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, return nil, err } + _, err = m.cfg.Store.GetReservation(ctx, reservationID) + switch { + case err == nil: + return nil, ErrReservationAlreadyExists + + case !errors.Is(err, ErrReservationNotFound): + return nil, err + } + // Create the reservation state machine. We need to pass in the runCtx // of the reservation manager so that the state machine will keep on // running even if the grpc conte reservationFSM := NewFSM(m.cfg) - // Add the reservation to the active reservations map. + // Add the reservation to the active reservations map. Check the map while + // holding the lock as concurrent callers may both have completed the store + // lookup above. m.Lock() + if _, ok := m.activeReservations[reservationID]; ok { + m.Unlock() + return nil, ErrReservationAlreadyExists + } + if len(m.activeReservations) >= maxActiveReservations { + m.Unlock() + return nil, ErrTooManyActiveReservations + } m.activeReservations[reservationID] = reservationFSM m.Unlock() + reservationFSM.RegisterObserver(&finalStateObserver{ + manager: m, + id: reservationID, + fsm: reservationFSM, + }) + initContext := &InitReservationContext{ reservationID: reservationID, serverPubkey: serverKey, @@ -130,17 +184,19 @@ func (m *Manager) newReservation(ctx context.Context, currentHeight uint32, // Send the init event to the state machine. go func() { - err = reservationFSM.SendEvent(ctx, OnServerRequest, initContext) - if err != nil { - log.Errorf("Error sending init event: %v", err) + sendErr := reservationFSM.SendEvent( + ctx, OnServerRequest, initContext, + ) + if sendErr != nil { + log.Errorf("Error sending init event: %v", sendErr) } }() // We'll now wait for the reservation to be in the state where it is // waiting to be confirmed. err = reservationFSM.DefaultObserver.WaitForState( - ctx, 5*time.Second, WaitForConfirmation, - fsm.WithWaitForStateOption(time.Second), + ctx, reservationStateWaitTimeout, WaitForConfirmation, + fsm.WithWaitForStateOption(reservationStatePollDelay), ) if err != nil { if reservationFSM.LastActionError != nil { @@ -173,7 +229,14 @@ func (m *Manager) RecoverReservations(ctx context.Context) error { reservationFSM := NewFSMFromReservation(m.cfg, reservation) + m.Lock() m.activeReservations[reservation.ID] = reservationFSM + m.Unlock() + reservationFSM.RegisterObserver(&finalStateObserver{ + manager: m, + id: reservation.ID, + fsm: reservationFSM, + }) // As SendEvent can block, we'll start a goroutine to process // the event. @@ -211,7 +274,7 @@ func (m *Manager) LockReservation(ctx context.Context, id ID) error { m.Unlock() if !ok { - return fmt.Errorf("reservation not found") + return ErrReservationNotFound } // Try to send the lock event to the reservation. @@ -231,7 +294,20 @@ func (m *Manager) UnlockReservation(ctx context.Context, id ID) error { m.Unlock() if !ok { - return fmt.Errorf("reservation not found") + storedReservation, err := m.cfg.Store.GetReservation(ctx, id) + if err != nil { + return err + } + + // Terminal reservations are removed from the active set. Treat an + // unlock after that removal as idempotent, while still surfacing a + // missing active FSM for reservations that should be running. + if isFinalState(storedReservation.State) { + return nil + } + + return fmt.Errorf("%w: reservation %x is in state %v", + ErrReservationNotFound, id, storedReservation.State) } // Try to send the unlock event to the reservation. diff --git a/instantout/reservation/manager_test.go b/instantout/reservation/manager_test.go index 79455750..af014926 100644 --- a/instantout/reservation/manager_test.go +++ b/instantout/reservation/manager_test.go @@ -12,6 +12,7 @@ import ( "github.com/lightninglabs/loop/swapserverrpc" "github.com/lightninglabs/loop/test" "github.com/lightningnetwork/lnd/chainntnfs" + "github.com/lightningnetwork/lnd/keychain" "github.com/stretchr/testify/mock" "github.com/stretchr/testify/require" ) @@ -57,6 +58,7 @@ func TestManager(t *testing.T) { confTx := &wire.MsgTx{ TxOut: []*wire.TxOut{ { + Value: int64(defaultValue), PkScript: pkScript, }, }, @@ -97,6 +99,245 @@ func TestManager(t *testing.T) { // We'll now expect the reservation to be expired. err = reservationFSM.DefaultObserver.WaitForState(ctxb, 5*time.Second, Spent) require.NoError(t, err) + + testContext.manager.Lock() + _, ok := testContext.manager.activeReservations[defaultReservationId] + testContext.manager.Unlock() + require.False(t, ok) +} + +// TestManagerContinuesAfterInvalidNotification verifies that a malformed +// server notification doesn't stop the reservation manager from processing +// later notifications. +func TestManagerContinuesAfterInvalidNotification(t *testing.T) { + testContext := newManagerTestContext(t) + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + initChan := make(chan struct{}) + errChan := make(chan error, 1) + go func() { + errChan <- testContext.manager.Run( + ctx, testContext.mockLnd.Height, initChan, + ) + }() + + <-initChan + + // A malformed ID is rejected by newReservation. The manager should log + // the error and continue processing the stream. + testContext.reservationNotificationChan <- &swapserverrpc.ServerReservationNotification{ + ReservationId: []byte{1}, + } + + testContext.reservationNotificationChan <- &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + } + + select { + case <-testContext.mockLnd.RegisterConfChannel: + case err := <-errChan: + require.NoError(t, err) + t.Fatal("reservation manager stopped after malformed notification") + case <-time.After(5 * time.Second): + t.Fatal("valid reservation notification was not processed") + } + + cancel() + require.NoError(t, <-errChan) +} + +// TestManagerRejectsDuplicateReservation verifies that a duplicate server +// notification cannot replace the active FSM for an existing reservation. +func TestManagerRejectsDuplicateReservation(t *testing.T) { + testContext := newManagerTestContext(t) + ctx := t.Context() + req := &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + } + + firstFSM, err := testContext.manager.newReservation( + ctx, uint32(testContext.mockLnd.Height), req, + ) + require.NoError(t, err) + + secondFSM, err := testContext.manager.newReservation( + ctx, uint32(testContext.mockLnd.Height), req, + ) + require.ErrorIs(t, err, ErrReservationAlreadyExists) + require.Nil(t, secondFSM) + require.Same( + t, firstFSM, + testContext.manager.activeReservations[defaultReservationId], + ) +} + +// TestManagerLimitsActiveReservations verifies that server notifications +// cannot grow the active FSM set without bound. +func TestManagerLimitsActiveReservations(t *testing.T) { + testContext := newManagerTestContext(t) + + for i := range maxActiveReservations { + var id ID + id[0] = byte(i) + id[1] = byte(i >> 8) + testContext.manager.activeReservations[id] = NewFSM( + testContext.manager.cfg, + ) + } + + reservationFSM, err := testContext.manager.newReservation( + t.Context(), uint32(testContext.mockLnd.Height), + &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + }, + ) + require.ErrorIs(t, err, ErrTooManyActiveReservations) + require.Nil(t, reservationFSM) + require.Len( + t, testContext.manager.activeReservations, + maxActiveReservations, + ) +} + +// TestManagerKeepsReservationAfterWaitTimeout verifies that a caller-side +// wait timeout doesn't evict a reservation FSM that is still initializing. +func TestManagerKeepsReservationAfterWaitTimeout(t *testing.T) { + testContext := newManagerTestContext(t) + + originalWaitTimeout := reservationStateWaitTimeout + originalPollDelay := reservationStatePollDelay + reservationStateWaitTimeout = 20 * time.Millisecond + reservationStatePollDelay = time.Millisecond + t.Cleanup(func() { + reservationStateWaitTimeout = originalWaitTimeout + reservationStatePollDelay = originalPollDelay + }) + + releaseOpen := make(chan struct{}) + testContext.mockReservationClient.ExpectedCalls = nil + testContext.mockReservationClient.On( + "OpenReservation", mock.Anything, mock.Anything, mock.Anything, + ).Run(func(mock.Arguments) { + <-releaseOpen + }).Return( + &swapserverrpc.ServerOpenReservationResponse{}, nil, + ) + + reservationFSM, err := testContext.manager.newReservation( + t.Context(), uint32(testContext.mockLnd.Height), + &swapserverrpc.ServerReservationNotification{ + ReservationId: defaultReservationId[:], + Value: uint64(defaultValue), + ServerKey: defaultPubkeyBytes, + Expiry: uint32(testContext.mockLnd.Height) + + defaultExpiry, + }, + ) + require.Error(t, err) + require.Nil(t, reservationFSM) + + testContext.manager.Lock() + activeFSM := testContext.manager.activeReservations[defaultReservationId] + testContext.manager.Unlock() + require.NotNil(t, activeFSM) + + close(releaseOpen) + require.NoError(t, activeFSM.DefaultObserver.WaitForState( + t.Context(), 5*time.Second, WaitForConfirmation, + )) +} + +// TestManagerRecoversAllPersistedReservations verifies that the cap applied to +// new notifications doesn't prevent the manager from resuming obligations +// already recorded in the database. The terminal transitions also exercise +// concurrent observer-driven removal from the active map. +func TestManagerRecoversAllPersistedReservations(t *testing.T) { + reservations := make([]*Reservation, maxActiveReservations+1) + for i := range reservations { + reservations[i] = &Reservation{ + ID: ID{ + byte(i), byte(i >> 8), byte(i >> 16), + }, + State: Init, + ProtocolVersion: ProtocolVersionServerInitiated, + } + } + + manager := NewManager(&Config{ + Store: &recoveryStore{reservations: reservations}, + }) + require.NoError(t, manager.RecoverReservations(t.Context())) + require.Eventually(t, func() bool { + manager.Lock() + defer manager.Unlock() + + return len(manager.activeReservations) == 0 + }, 5*time.Second, time.Millisecond) +} + +// TestUnlockTerminalReservationIsIdempotent verifies that cleanup can safely +// race with terminal-state eviction without masking the original swap result. +func TestUnlockTerminalReservationIsIdempotent(t *testing.T) { + testContext := newManagerTestContext(t) + storedReservation := &Reservation{ + ID: defaultReservationId, + State: Init, + ClientPubkey: defaultPubkey, + ServerPubkey: defaultPubkey, + Value: defaultValue, + Expiry: defaultExpiry, + ProtocolVersion: ProtocolVersionServerInitiated, + KeyLocator: keychain.KeyLocator{ + Family: keychain.KeyFamily(KeyFamily), + Index: 1, + }, + } + + require.NoError(t, testContext.manager.cfg.Store.CreateReservation( + t.Context(), storedReservation, + )) + storedReservation.State = TimedOut + require.NoError(t, testContext.manager.cfg.Store.UpdateReservation( + t.Context(), storedReservation, + )) + + require.NoError(t, testContext.manager.UnlockReservation( + t.Context(), defaultReservationId, + )) + require.ErrorIs(t, testContext.manager.UnlockReservation( + t.Context(), ID{1}, + ), ErrReservationNotFound) +} + +type recoveryStore struct { + Store + + reservations []*Reservation +} + +func (s *recoveryStore) ListReservations(context.Context) ([]*Reservation, + error) { + + return s.reservations, nil +} + +func (s *recoveryStore) UpdateReservation(context.Context, + *Reservation) error { + + return nil } // ManagerTestContext is a helper struct that contains all the necessary diff --git a/instantout/reservation/reservation.go b/instantout/reservation/reservation.go index 5a167d2e..8b83ae33 100644 --- a/instantout/reservation/reservation.go +++ b/instantout/reservation/reservation.go @@ -142,8 +142,14 @@ func (r *Reservation) findReservationOutput(tx *wire.MsgTx) (*wire.OutPoint, return nil, err } + var foundScript bool for i, txOut := range tx.TxOut { if bytes.Equal(txOut.PkScript, pkScript) { + foundScript = true + if txOut.Value != int64(r.Value) { + continue + } + return &wire.OutPoint{ Hash: tx.TxHash(), Index: uint32(i), @@ -151,6 +157,11 @@ func (r *Reservation) findReservationOutput(tx *wire.MsgTx) (*wire.OutPoint, } } + if foundScript { + return nil, fmt.Errorf("reservation output value mismatch: "+ + "expected %d", r.Value) + } + return nil, errors.New("reservation output not found") } diff --git a/instantout/reservation/store.go b/instantout/reservation/store.go index 117d02c6..72613f9c 100644 --- a/instantout/reservation/store.go +++ b/instantout/reservation/store.go @@ -180,6 +180,10 @@ func (r *SQLStore) GetReservation(ctx context.Context, return nil }) if err != nil { + if errors.Is(err, sql.ErrNoRows) { + return nil, ErrReservationNotFound + } + return nil, err } diff --git a/instantout/store.go b/instantout/store.go index 25d7fe70..0e2a5066 100644 --- a/instantout/store.go +++ b/instantout/store.go @@ -104,7 +104,7 @@ func (s *SQLStore) CreateInstantLoopOut(ctx context.Context, AmountRequested: int64(instantOut.Value), CltvExpiry: instantOut.CltvExpiry, MaxMinerFee: 0, - MaxSwapFee: 0, + MaxSwapFee: int64(instantOut.MaxSwapFee), InitiationHeight: instantOut.initiationHeight, ProtocolVersion: int32(instantOut.protocolVersion), Label: "", @@ -368,6 +368,7 @@ func (s *SQLStore) sqlInstantOutToInstantOut(ctx context.Context, protocolVersion: ProtocolVersion(row.ProtocolVersion), initiationHeight: row.InitiationHeight, Value: btcutil.Amount(row.AmountRequested), + MaxSwapFee: btcutil.Amount(row.MaxSwapFee), keyLocator: keychain.KeyLocator{ Family: keychain.KeyFamily(row.ClientKeyFamily), Index: uint32(row.ClientKeyIndex), diff --git a/interface.go b/interface.go index 10a86bfa..4b8623bf 100644 --- a/interface.go +++ b/interface.go @@ -4,6 +4,7 @@ import ( "time" "github.com/btcsuite/btcd/btcutil" + "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/loopdb" "github.com/lightninglabs/loop/swap" "github.com/lightninglabs/taproot-assets/rfqmath" @@ -476,8 +477,12 @@ type SwapInfoKit struct { LastUpdateTime time.Time } -// SwapInfo exposes common info fields for loop in and loop out swaps. +// SwapInfo exposes common info fields for traditional swaps and static address +// loop-ins. type SwapInfo struct { + // SwapStateData.State is authoritative for swap.TypeIn and swap.TypeOut. + // For swap.TypeStaticAddressLoopIn, StaticAddressLoopInState is + // authoritative and State remains its zero value, loopdb.StateInitiated. loopdb.SwapStateData loopdb.SwapContract @@ -488,9 +493,14 @@ type SwapInfo struct { // SwapHash stores the swap preimage hash. SwapHash lntypes.Hash - // SwapType describes whether this is a loop in or loop out swap. + // SwapType describes the kind of swap. SwapType swap.Type + // StaticAddressLoopInState stores the precise static address loop-in FSM + // state when SwapType is swap.TypeStaticAddressLoopIn. For traditional + // swaps, it remains the fsm.StateType zero value, fsm.EmptyState. + StaticAddressLoopInState fsm.StateType + // HtlcAddressP2WSH stores the address of the P2WSH (native segwit) // swap htlc. This is used for both loop-in and loop-out. HtlcAddressP2WSH btcutil.Address diff --git a/liquidity/parameters.go b/liquidity/parameters.go index 44c555db..3b365cc0 100644 --- a/liquidity/parameters.go +++ b/liquidity/parameters.go @@ -400,9 +400,16 @@ func rpcToFee(req *clientrpc.LiquidityParameters) (FeeLimit, error) { // rpcToRule switches on rpc rule type to convert to our rule interface. func rpcToRule(rule *clientrpc.LiquidityRule) (*SwapRule, error) { - swapType := swap.TypeOut - if rule.SwapType == clientrpc.SwapType_LOOP_IN { + var swapType swap.Type + switch rule.SwapType { + case clientrpc.SwapType_LOOP_OUT: + swapType = swap.TypeOut + + case clientrpc.SwapType_LOOP_IN: swapType = swap.TypeIn + + default: + return nil, fmt.Errorf("unknown swap type: %v", rule.SwapType) } switch rule.Type { @@ -559,8 +566,11 @@ func RpcToParameters(req *clientrpc.LiquidityParameters) (*Parameters, req.AutoloopBudgetSat != 0 { params.AutoFeeRefreshPeriod = InfiniteDuration + // Keep reading the legacy start field so old stored + // liquidity parameters migrate to the refresh-period model. + budgetStartSec := req.AutoloopBudgetStartSec //nolint:staticcheck params.AutoloopBudgetLastRefresh = time.Unix( - int64(req.AutoloopBudgetStartSec), 0) + int64(budgetStartSec), 0) } for _, rule := range req.Rules { diff --git a/liquidity/parameters_test.go b/liquidity/parameters_test.go index 0e54dbf1..7af73c0d 100644 --- a/liquidity/parameters_test.go +++ b/liquidity/parameters_test.go @@ -3,9 +3,62 @@ package liquidity import ( "testing" + clientrpc "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/loop/swap" "github.com/stretchr/testify/require" ) +// TestRPCToRuleSwapType verifies RPC swap type conversion. +func TestRPCToRuleSwapType(t *testing.T) { + tests := []struct { + name string + swapType clientrpc.SwapType + wantType swap.Type + wantErr bool + }{ + { + name: "loop out", + swapType: clientrpc.SwapType_LOOP_OUT, + wantType: swap.TypeOut, + }, + { + name: "loop in", + swapType: clientrpc.SwapType_LOOP_IN, + wantType: swap.TypeIn, + }, + { + name: "static loop in rejected", + swapType: clientrpc.SwapType_STATIC_LOOP_IN, + wantErr: true, + }, + { + name: "unknown swap type rejected", + swapType: clientrpc.SwapType(99), + wantErr: true, + }, + } + + for _, testCase := range tests { + t.Run(testCase.name, func(t *testing.T) { + rpcRule := &clientrpc.LiquidityRule{ + Type: clientrpc.LiquidityRuleType_THRESHOLD, + IncomingThreshold: 10, + OutgoingThreshold: 20, + SwapType: testCase.swapType, + } + + got, err := rpcToRule(rpcRule) + if testCase.wantErr { + require.Error(t, err) + return + } + + require.NoError(t, err) + require.Equal(t, testCase.wantType, got.Type) + }) + } +} + // TestValidateRestrictions tests validating client restrictions against a set // of server restrictions. func TestValidateRestrictions(t *testing.T) { diff --git a/loopd/config.go b/loopd/config.go index 0e0e065e..bacfc45a 100644 --- a/loopd/config.go +++ b/loopd/config.go @@ -335,6 +335,14 @@ func Validate(cfg *Config) error { ) } + // If the user doesn't specify Tapd.MacaroonPath, reassemble it with + // the configured Bitcoin network. + if cfg.Tapd.MacaroonPath == assets.DefaultTapdConfig().MacaroonPath { + cfg.Tapd.MacaroonPath = assets.DefaultTapdConfigForNetwork( + btcutil.AppDataDir("tapd", false), cfg.Network, + ).MacaroonPath + } + // We'll also update the database file location as well, if it wasn't // set. if cfg.Sqlite.DatabaseFileName == defaultSqliteDatabasePath { diff --git a/loopd/config_test.go b/loopd/config_test.go new file mode 100644 index 00000000..87eda5fa --- /dev/null +++ b/loopd/config_test.go @@ -0,0 +1,51 @@ +package loopd + +import ( + "path/filepath" + "testing" + + "github.com/btcsuite/btcd/btcutil" + "github.com/lightninglabs/loop/assets" + "github.com/stretchr/testify/require" +) + +// TestValidateTapdMacaroonPath tests that validation updates the default tapd +// macaroon path for the configured network without changing an explicit path. +func TestValidateTapdMacaroonPath(t *testing.T) { + customPath := filepath.Join(t.TempDir(), "custom.macaroon") + defaultConfig := assets.DefaultTapdConfig() + regtestConfig := assets.DefaultTapdConfigForNetwork( + btcutil.AppDataDir("tapd", false), "regtest", + ) + + tests := []struct { + name string + macaroonPath string + expectedPath string + }{ + { + name: "default path", + macaroonPath: defaultConfig.MacaroonPath, + expectedPath: regtestConfig.MacaroonPath, + }, + { + name: "explicit path", + macaroonPath: customPath, + expectedPath: customPath, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + cfg := DefaultConfig() + cfg.Network = "regtest" + cfg.LoopDir = t.TempDir() + cfg.Tapd.MacaroonPath = test.macaroonPath + + require.NoError(t, Validate(&cfg)) + require.Equal( + t, test.expectedPath, cfg.Tapd.MacaroonPath, + ) + }) + } +} diff --git a/loopd/daemon.go b/loopd/daemon.go index 241d62e0..319709d8 100644 --- a/loopd/daemon.go +++ b/loopd/daemon.go @@ -33,7 +33,7 @@ import ( "github.com/lightningnetwork/lnd/clock" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/macaroons" - "go.etcd.io/bbolt" + bbolterrors "go.etcd.io/bbolt/errors" "google.golang.org/grpc" "google.golang.org/protobuf/encoding/protojson" "gopkg.in/macaroon-bakery.v2/bakery" @@ -110,6 +110,10 @@ type Daemon struct { macaroonService *lndclient.MacaroonService } +// staticLoopInStatusChanBuffer keeps the shared swap-status fanout from +// stalling static loop-in FSM progress during transient subscriber gaps. +const staticLoopInStatusChanBuffer = 20 + // New creates a new instance of the loop client daemon. func New(config *Config, lisCfg *ListenerCfg) *Daemon { return &Daemon{ @@ -154,6 +158,15 @@ func (d *Daemon) Start() error { if err != nil { return err } + + defer func() { + if err == nil || d.assetClient == nil { + return + } + + d.assetClient.Close() + d.assetClient = nil + }() } // With lnd connected, initialize everything else, such as the swap @@ -161,7 +174,7 @@ func (d *Daemon) Start() error { // and error handlers. If this fails, then nothing has been started yet, // and we can just return the error. err = d.initialize(true) - if errors.Is(err, bbolt.ErrTimeout) { + if errors.Is(err, bbolterrors.ErrTimeout) { // We're trying to be started as a standalone Loop daemon, most // likely LiT is already running and blocking the DB return fmt.Errorf("%v: make sure no other loop daemon process "+ @@ -174,15 +187,15 @@ func (d *Daemon) Start() error { // If we get here, we already have started several goroutines. So if // anything goes wrong now, we need to cleanly shut down again. - startErr := d.startWebServers() - if startErr != nil { - errorf("Error while starting daemon: %v", startErr) + err = d.startWebServers() + if err != nil { + errorf("Error while starting daemon: %v", err) d.Stop() stopErr := <-d.ErrChan if stopErr != nil { errorf("Error while stopping daemon: %v", stopErr) } - return startErr + return err } return nil @@ -211,7 +224,7 @@ func (d *Daemon) StartAsSubserver(lndGrpc *lndclient.GrpcLndServices, // handlers. If this fails, then nothing has been started yet, and we // can just return the error. err := d.initialize(withMacaroonService) - if errors.Is(err, bbolt.ErrTimeout) { + if errors.Is(err, bbolterrors.ErrTimeout) { // We're trying to be started inside LiT so there most likely is // another standalone Loop process blocking the DB. return fmt.Errorf("%v: make sure no other loop daemon "+ @@ -555,10 +568,30 @@ func (d *Daemon) initialize(withMacaroonService bool) error { } } + // Static address loop-in store setup is needed by the notification + // manager so confirmation-risk decisions are durable before fan-out. + staticAddressLoopInStore := loopin.NewSqlStore( + loopdb.NewTypedStore[loopin.Querier](baseDb), + clock.NewDefaultClock(), d.lnd.ChainParams, + ) + // Start the notification manager. notificationCfg := ¬ifications.Config{ Client: loop_swaprpc.NewSwapServerClient(swapClient.Conn), CurrentToken: swapClient.L402Store.CurrentToken, + PersistStaticLoopInRiskDecision: func(ctx context.Context, + swapHash lntypes.Hash, accepted bool) error { + + decision := loopin.ConfirmationRiskDecisionRejected + if accepted { + decision = loopin.ConfirmationRiskDecisionAccepted + } + + return staticAddressLoopInStore. + RecordStaticAddressRiskDecision( + ctx, swapHash, decision, + ) + }, } notificationManager := notifications.NewManager(notificationCfg) @@ -661,12 +694,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { LightningClient: d.lnd.Client, } openChannelManager = openchannel.NewManager(openChannelCfg) - - // Static address loop-in manager setup. - staticAddressLoopInStore := loopin.NewSqlStore( - loopdb.NewTypedStore[loopin.Querier](baseDb), - clock.NewDefaultClock(), d.lnd.ChainParams, - ) + statusChan := make(chan loop.SwapInfo, staticLoopInStatusChanBuffer) // Run the deposit swap hash migration. err = loopin.MigrateDepositSwapHash( @@ -687,11 +715,17 @@ func (d *Daemon) initialize(withMacaroonService bool) error { return err } + statusUpdater := &staticLoopInStatusUpdater{ + statusChan: statusChan, + mainCtx: d.mainCtx, + chainParams: d.lnd.ChainParams, + } staticLoopInManager, err = loopin.NewManager(&loopin.Config{ Server: staticAddressClient, QuoteGetter: swapClient.Server, LndClient: d.lnd.Client, + TxOutChecker: loopin.NewLndTxOutChecker(d.lnd.Client), InvoicesClient: d.lnd.Invoices, NodePubkey: d.lnd.NodePubkey, AddressManager: staticAddressManager, @@ -703,6 +737,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { ChainParams: d.lnd.ChainParams, Signer: d.lnd.Signer, ValidateLoopInContract: loop.ValidateLoopInContract, + SendUpdate: statusUpdater.sendUpdate, MaxStaticAddrHtlcFeePercentage: d.cfg.MaxStaticAddrHtlcFeePercentage, MaxStaticAddrHtlcBackupFeePercentage: d.cfg.MaxStaticAddrHtlcBackupFeePercentage, }, blockHeight) @@ -768,7 +803,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { lnd: &d.lnd.LndServices, swaps: make(map[lntypes.Hash]loop.SwapInfo), subscribers: make(map[int]chan<- any), - statusChan: make(chan loop.SwapInfo), + statusChan: statusChan, mainCtx: d.mainCtx, reservationManager: reservationManager, instantOutManager: instantOutManager, @@ -994,7 +1029,7 @@ func (d *Daemon) initialize(withMacaroonService bool) error { d.wg.Go(func() { infof("Starting static address open channel manager") err := openChannelManager.Run(d.mainCtx) - if err != nil && !errors.Is(context.Canceled, err) { + if err != nil && !errors.Is(err, context.Canceled) { d.internalErrChan <- err } infof("Static address open channel manager stopped") @@ -1119,6 +1154,10 @@ func (d *Daemon) stop() { if d.clientCleanup != nil { d.clientCleanup() } + if d.assetClient != nil { + d.assetClient.Close() + d.assetClient = nil + } // Everything should be shutting down now, wait for completion. d.wg.Wait() diff --git a/loopd/run.go b/loopd/run.go index 0735d29a..e33fea8e 100644 --- a/loopd/run.go +++ b/loopd/run.go @@ -24,10 +24,24 @@ var ( // LoopMinRequiredLndVersion is the minimum required version of lnd that // is compatible with the current version of the loop client. Also all // listed build tags/subservers need to be enabled. + // + // IMPORTANT: bump this whenever the client starts using an lnd RPC + // method or message field that does not exist in older lnd, to the lnd + // release that introduced that API (see the maintenance note in + // AGENTS.md). The current floor of v0.18.4-beta is set by the highest + // such dependency the client has today: + // - routerrpc.SendPaymentRequest.first_hop_custom_records and + // lnrpc.Route.custom_channel_data, used by asset loop outs + // (loopout.go): both added in lnd v0.18.4-beta. + // - walletrpc.EstimateFeeResponse.min_relay_fee_sat_per_kw, read by + // the sweep batcher fee floor via lndclient WalletKit.MinRelayFee + // (sweepbatcher/, loopd/sweep_htlc.go): added in lnd v0.18.3-beta. + // On older lnd this field silently decodes to 0, disabling the + // sweeper's min-relay fee floor. LoopMinRequiredLndVersion = &verrpc.Version{ AppMajor: 0, - AppMinor: 17, - AppPatch: 0, + AppMinor: 18, + AppPatch: 4, BuildTags: []string{ "signrpc", "walletrpc", "chainrpc", "invoicesrpc", }, diff --git a/loopd/static_loopin_status_updater.go b/loopd/static_loopin_status_updater.go new file mode 100644 index 00000000..aa0592cb --- /dev/null +++ b/loopd/static_loopin_status_updater.go @@ -0,0 +1,47 @@ +package loopd + +import ( + "context" + "fmt" + + "github.com/btcsuite/btcd/chaincfg" + "github.com/lightninglabs/loop" + "github.com/lightninglabs/loop/staticaddr/loopin" +) + +// staticLoopInStatusUpdater publishes static-address loop-in status updates to +// the client-facing swap stream. +type staticLoopInStatusUpdater struct { + // statusChan sends updates to the client-facing swap stream. + statusChan chan<- loop.SwapInfo + + // mainCtx is canceled when loopd is shutting down. + mainCtx context.Context + + // chainParams are used to reconstruct the static loop-in HTLC address. + chainParams *chaincfg.Params +} + +// sendUpdate converts the persisted static-address loop-in into swap info and +// forwards it to the status stream unless either context is canceled. +func (u *staticLoopInStatusUpdater) sendUpdate(ctx context.Context, + swp *loopin.StaticAddressLoopIn) error { + + info, err := staticAddressLoopInSwapInfoWithChainParams( + swp, u.chainParams, + ) + if err != nil { + return fmt.Errorf("unable to notify static loop-in update: %w", err) + } + + select { + case u.statusChan <- *info: + return nil + + case <-ctx.Done(): + return ctx.Err() + + case <-u.mainCtx.Done(): + return u.mainCtx.Err() + } +} diff --git a/loopd/swapclient_server.go b/loopd/swapclient_server.go index bed6efa8..497fe3a8 100644 --- a/loopd/swapclient_server.go +++ b/loopd/swapclient_server.go @@ -96,7 +96,7 @@ type swapClientServer struct { reservationManager *reservation.Manager instantOutManager *instantout.Manager staticAddressManager *address.Manager - depositManager *deposit.Manager + depositManager staticAddressDepositManager withdrawalManager *withdraw.Manager staticLoopInManager *loopin.Manager openChannelManager *openchannel.Manager @@ -112,6 +112,31 @@ type swapClientServer struct { stopDaemon func() } +// staticAddressDepositManager is the deposit manager behavior required by the +// RPC server. +type staticAddressDepositManager interface { + // EnsureDepositsFresh reconciles tracked deposits with lnd's current + // wallet view before user-facing deposit selection. + EnsureDepositsFresh(context.Context) error + + // GetActiveDepositsInState returns active deposits that are currently in + // the requested state. + GetActiveDepositsInState(fsm.StateType) ([]*deposit.Deposit, error) + + // DepositsForOutpoints returns known deposit records for the requested + // outpoints, optionally skipping unknown outpoints. + DepositsForOutpoints(context.Context, []string, bool) ( + []*deposit.Deposit, error) + + // GetVisibleDeposits returns deposits that should be shown in normal + // user-facing views. + GetVisibleDeposits(context.Context) ([]*deposit.Deposit, error) + + // GetAllDeposits returns all known deposit records, including historical + // records that are no longer user-visible. + GetAllDeposits(context.Context) ([]*deposit.Deposit, error) +} + // LoopOut initiates a loop out swap with the given parameters. The call returns // after the swap has been set up with the swap server. From that point onwards, // progress can be tracked via the LoopOutStatus stream that is returned from @@ -251,6 +276,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context, req.AssetSwapRfqId = in.AssetRfqInfo.SwapRfqId } + // Keep accepting the deprecated single-channel field for older clients. switch { case in.LoopOutChannel != 0 && len(in.OutgoingChanSet) > 0: // nolint:staticcheck return nil, errors.New("loop_out_channel and outgoing_" + @@ -273,7 +299,7 @@ func (s *swapClientServer) LoopOut(ctx context.Context, resp := &looprpc.SwapResponse{ Id: info.SwapHash.String(), IdBytes: info.SwapHash[:], - HtlcAddress: htlcAddress, + HtlcAddress: htlcAddress, //nolint:staticcheck ServerMessage: info.ServerMessage, } @@ -385,6 +411,8 @@ func (s *swapClientServer) marshallSwap(ctx context.Context, } var swapType looprpc.SwapType + staticLoopInState := looprpc. + StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE var ( htlcAddress string htlcAddressP2TR string @@ -411,6 +439,27 @@ func (s *swapClientServer) marshallSwap(ctx context.Context, lastHop = loopSwap.LastHop[:] } + case swap.TypeStaticAddressLoopIn: + // Static loop-ins surface their precise FSM state through the + // optional oneof and keep the reconstructed HTLC P2WSH address, + // not the reusable static address. + swapType = looprpc.SwapType_STATIC_LOOP_IN + staticLoopInState = toClientStaticAddressLoopInState( + loopSwap.StaticAddressLoopInState, + ) + + if loopSwap.HtlcAddressP2WSH == nil { + return nil, errors.New( + "missing static address loop-in P2WSH HTLC address", + ) + } + htlcAddressP2WSH = loopSwap.HtlcAddressP2WSH.EncodeAddress() + htlcAddress = htlcAddressP2WSH + + if loopSwap.LastHop != nil { + lastHop = loopSwap.LastHop[:] + } + case swap.TypeOut: swapType = looprpc.SwapType_LOOP_OUT if loopSwap.HtlcAddressP2WSH != nil { @@ -452,7 +501,7 @@ func (s *swapClientServer) marshallSwap(ctx context.Context, return nil, errors.New("unknown swap type") } - return &looprpc.SwapStatus{ + rpcSwap := &looprpc.SwapStatus{ Amt: int64(loopSwap.AmountRequested), Id: loopSwap.SwapHash.String(), IdBytes: loopSwap.SwapHash[:], @@ -471,7 +520,15 @@ func (s *swapClientServer) marshallSwap(ctx context.Context, LastHop: lastHop, OutgoingChanSet: outGoingChanSet, AssetInfo: assetInfo, - }, nil + } + if swapType == looprpc.SwapType_STATIC_LOOP_IN { + rpcSwap.StaticLoopInStateOptional = + &looprpc.SwapStatus_StaticLoopInState{ + StaticLoopInState: staticLoopInState, + } + } + + return rpcSwap, nil } // Monitor will return a stream of swap updates for currently active swaps. @@ -492,27 +549,28 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest, // Start a notification queue for this subscriber. queue := queue.NewConcurrentQueue(20) queue.Start() + ctx := server.Context() - // Add this subscriber to the global subscriber list. Also create a - // snapshot of all pending and completed swaps within the lock, to - // prevent subscribers from receiving duplicate updates. s.swapsLock.Lock() id := s.nextSubscriberID s.nextSubscriberID++ s.subscribers[id] = queue.ChanIn() - - var pendingSwaps, completedSwaps []loop.SwapInfo - for _, swap := range s.swaps { - if swap.State.Type() == loopdb.StateTypePending { - pendingSwaps = append(pendingSwaps, swap) - } else { - completedSwaps = append(completedSwaps, swap) - } - } - + pendingSwaps, completedSwaps := s.monitorCachedSwaps() s.swapsLock.Unlock() + err := s.appendStaticAddressLoopInMonitorSnapshot( + ctx, &pendingSwaps, &completedSwaps, + ) + if err != nil { + s.swapsLock.Lock() + delete(s.subscribers, id) + s.swapsLock.Unlock() + queue.Stop() + + return err + } + defer func() { s.swapsLock.Lock() delete(s.subscribers, id) @@ -542,6 +600,13 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest, ) }) + // Static-address loop-in updates can arrive from both the initial snapshot + // and the live queue. Build a high-water mark from the snapshot so we can + // suppress stale duplicate snapshot items without dropping newer live ones. + staticSnapshotHighWater := staticAddressLoopInMonitorHighWater( + filteredSwaps, + ) + // Return swaps to caller. for _, swap := range filteredSwaps { if err := send(swap); err != nil { @@ -559,6 +624,13 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest, } swap := queueItem.(loop.SwapInfo) + if isInitialStaticAddressLoopInStale( + staticSnapshotHighWater, swap, + ) { + + continue + } + if err := send(swap); err != nil { return err } @@ -574,6 +646,102 @@ func (s *swapClientServer) Monitor(in *looprpc.MonitorRequest, } } +// staticAddressLoopInMonitorHighWater records the latest snapshot item for each +// static-address loop-in swap hash. +func staticAddressLoopInMonitorHighWater( + swaps []loop.SwapInfo) map[lntypes.Hash]staticAddressLoopInHighWater { + + highWater := make(map[lntypes.Hash]staticAddressLoopInHighWater) + for _, swp := range swaps { + if swp.SwapType != swap.TypeStaticAddressLoopIn { + continue + } + + current, ok := highWater[swp.SwapHash] + if !ok || swp.LastUpdate.After(current.lastUpdate) { + highWater[swp.SwapHash] = staticAddressLoopInHighWater{ + lastUpdate: swp.LastUpdate, + staticState: swp.StaticAddressLoopInState, + } + } + } + + return highWater +} + +// staticAddressLoopInHighWater stores the most recent snapshot timestamp and +// state for one static-address loop-in swap. +type staticAddressLoopInHighWater struct { + lastUpdate time.Time + staticState fsm.StateType +} + +// isInitialStaticAddressLoopInStale reports whether a live static-address +// loop-in update is older than the snapshot copy already sent, or equal to it +// with the same static FSM state. +func isInitialStaticAddressLoopInStale( + highWater map[lntypes.Hash]staticAddressLoopInHighWater, + swp loop.SwapInfo) bool { + + if swp.SwapType != swap.TypeStaticAddressLoopIn { + return false + } + current, ok := highWater[swp.SwapHash] + if !ok { + return false + } + + if swp.LastUpdate.Before(current.lastUpdate) { + return true + } + if swp.LastUpdate.After(current.lastUpdate) { + return false + } + + // Equal timestamps can race the initial DB snapshot, so match state too. + return swp.StaticAddressLoopInState == current.staticState +} + +// monitorCachedSwaps returns the current in-memory swaps split into pending and +// completed slices for monitor snapshot construction. +func (s *swapClientServer) monitorCachedSwaps() ([]loop.SwapInfo, + []loop.SwapInfo) { + + var pendingSwaps, completedSwaps []loop.SwapInfo + for _, swap := range s.swaps { + if swap.State.Type() == loopdb.StateTypePending { + pendingSwaps = append(pendingSwaps, swap) + } else { + completedSwaps = append(completedSwaps, swap) + } + } + + return pendingSwaps, completedSwaps +} + +// appendStaticAddressLoopInMonitorSnapshot appends the current static-address +// loop-in swaps to the monitor snapshot. +func (s *swapClientServer) appendStaticAddressLoopInMonitorSnapshot( + ctx context.Context, pendingSwaps, completedSwaps *[]loop.SwapInfo) error { + + staticSwaps, err := s.staticAddressLoopInSwapInfos(ctx) + if err != nil { + return err + } + for _, swap := range staticSwaps { + if slices.Contains( + loopin.FinalStates, swap.StaticAddressLoopInState, + ) { + + *completedSwaps = append(*completedSwaps, *swap) + } else { + *pendingSwaps = append(*pendingSwaps, *swap) + } + } + + return nil +} + // ListSwaps returns a list of all currently known swaps and their current // status. func (s *swapClientServer) ListSwaps(ctx context.Context, @@ -728,10 +896,13 @@ func (s *swapClientServer) SwapInfo(ctx context.Context, // Just return the server's in-memory cache here too as we also want to // return temporary failures to the client. + s.swapsLock.Lock() swp, ok := s.swaps[swapHash] + s.swapsLock.Unlock() if !ok { return nil, fmt.Errorf("swap with hash %s not found", req.Id) } + return s.marshallSwap(ctx, &swp) } @@ -920,6 +1091,12 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context, // number of deposits to quote for. numDeposits := 0 if autoSelectDeposits { + err = s.depositManager.EnsureDepositsFresh(ctx) + if err != nil { + return nil, fmt.Errorf("unable to refresh deposits: %w", + err) + } + deposits, err := s.depositManager.GetActiveDepositsInState( deposit.Deposited, ) @@ -954,6 +1131,12 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context, numDeposits = len(selectedDeposits) } else if len(req.DepositOutpoints) > 0 { + err = s.depositManager.EnsureDepositsFresh(ctx) + if err != nil { + return nil, fmt.Errorf("unable to refresh deposits: %w", + err) + } + // If deposits are selected, we need to retrieve them to // calculate the total value which we request a quote for. depositList, err := s.ListStaticAddressDeposits( @@ -976,15 +1159,24 @@ func (s *swapClientServer) GetLoopInQuote(ctx context.Context, return nil, fmt.Errorf("expected %d deposits, got %d", len(req.DepositOutpoints), len(depositList.FilteredDeposits)) - } else { - numDeposits = len(depositList.FilteredDeposits) } + numDeposits = len(depositList.FilteredDeposits) // In case we quote for deposits, we send the server both the // selected value and the number of deposits. This is so the // server can probe the selected value and calculate the per // input fee. for _, deposit := range depositList.FilteredDeposits { + // ListStaticAddressDeposits only returns deposits that are visible + // in the manager's live view. For a manual quote we additionally + // require the current state to be Deposited so stale client-side + // outpoint selection fails early instead of making it to swap + // initiation. + if deposit.State != looprpc.DepositState_DEPOSITED { + return nil, fmt.Errorf("deposit %s is not "+ + "currently available", deposit.Outpoint) + } + totalDepositAmount += btcutil.Amount( deposit.Value, ) @@ -1182,11 +1374,11 @@ func (s *swapClientServer) LoopIn(ctx context.Context, if loopdb.CurrentProtocolVersion() < loopdb.ProtocolVersionHtlcV3 { p2wshAddr := swapInfo.HtlcAddressP2WSH.String() - response.HtlcAddress = p2wshAddr + response.HtlcAddress = p2wshAddr //nolint:staticcheck response.HtlcAddressP2Wsh = p2wshAddr } else { p2trAddr := swapInfo.HtlcAddressP2TR.String() - response.HtlcAddress = p2trAddr + response.HtlcAddress = p2trAddr //nolint:staticcheck response.HtlcAddressP2Tr = p2trAddr } @@ -1571,8 +1763,15 @@ func (s *swapClientServer) InstantOut(ctx context.Context, reservationIds[i] = resId } + var options []instantout.NewInstantOutOption + if req.GetMaxSwapFee() != nil { + options = append(options, instantout.WithMaxSwapFee( + btcutil.Amount(req.GetMaxSwapFeeSat()), + )) + } + instantOutFsm, err := s.instantOutManager.NewInstantOut( - ctx, reservationIds, req.DestAddr, + ctx, reservationIds, req.DestAddr, options..., ) if err != nil { return nil, err @@ -1692,58 +1891,40 @@ func (s *swapClientServer) ListUnspentDeposits(ctx context.Context, } // ListUnspentRaw returns the unspent wallet view of the backing lnd - // wallet. It might be that deposits show up there that are actually - // not spendable because they already have been used but not yet spent - // by the server. We filter out such deposits here. + // wallet. Static loop-in initiation requires an active deposit record, + // so only deposits that are both wallet-visible and tracked as + // Deposited are returned here. var ( outpoints []string isUnspent = make(map[wire.OutPoint]struct{}) ) - // Keep track of confirmed outpoints that we need to check against our - // database. - confirmedToCheck := make(map[wire.OutPoint]struct{}) - for _, utxo := range utxos { - if utxo.Confirmations < deposit.MinConfs { - // Unconfirmed deposits are always available. - isUnspent[utxo.OutPoint] = struct{}{} - } else { - // Confirmed deposits need to be checked. - outpoints = append(outpoints, utxo.OutPoint.String()) - confirmedToCheck[utxo.OutPoint] = struct{}{} - } + outpoints = append(outpoints, utxo.OutPoint.String()) + } + + err = s.depositManager.EnsureDepositsFresh(ctx) + if err != nil { + return nil, err } // Check the spent status of the deposits by looking at their states. - ignoreUnknownOutpoints := false + ignoreUnknownOutpoints := true deposits, err := s.depositManager.DepositsForOutpoints( ctx, outpoints, ignoreUnknownOutpoints, ) if err != nil { return nil, err } + for _, d := range deposits { - // A nil deposit means we don't have a record for it. We'll - // handle this case after the loop. if d == nil { continue } - // If the deposit is in the "Deposited" state, it's available. if d.IsInState(deposit.Deposited) { isUnspent[d.OutPoint] = struct{}{} } - - // We have a record for this deposit, so we no longer need to - // check it. - delete(confirmedToCheck, d.OutPoint) - } - - // Any remaining outpoints in confirmedToCheck are ones that lnd knows - // about but we don't. These are new, unspent deposits. - for op := range confirmedToCheck { - isUnspent[op] = struct{}{} } // Prepare the list of unspent deposits for the rpc response. @@ -1783,6 +1964,12 @@ func (s *swapClientServer) WithdrawDeposits(ctx context.Context, return nil, fmt.Errorf("must select either all or some utxos") case isAllSelected: + err = s.depositManager.EnsureDepositsFresh(ctx) + if err != nil { + return nil, fmt.Errorf("unable to refresh deposits: %w", + err) + } + deposits, err := s.depositManager.GetActiveDepositsInState( deposit.Deposited, ) @@ -1790,8 +1977,9 @@ func (s *swapClientServer) WithdrawDeposits(ctx context.Context, return nil, err } - for _, d := range deposits { - outpoints = append(outpoints, d.OutPoint) + outpoints, err = withdrawAllDepositOutpoints(deposits) + if err != nil { + return nil, err } case isUtxoSelected: @@ -1814,6 +2002,25 @@ func (s *swapClientServer) WithdrawDeposits(ctx context.Context, }, err } +// withdrawAllDepositOutpoints returns all deposit outpoints for an `all` +// withdrawal request. The request must fail if any deposited output is still +// unconfirmed because `all` should not silently downgrade to a subset. +func withdrawAllDepositOutpoints(deposits []*deposit.Deposit) ([]wire.OutPoint, + error) { + + outpoints := make([]wire.OutPoint, 0, len(deposits)) + for _, d := range deposits { + if d.GetConfirmationHeight() <= 0 { + return nil, fmt.Errorf("can't withdraw all deposits while " + + "some deposits are unconfirmed") + } + + outpoints = append(outpoints, d.OutPoint) + } + + return outpoints, nil +} + // ListStaticAddressDeposits returns a list of all sufficiently confirmed // deposits behind the static address and displays properties like value, // state or blocks til expiry. @@ -1829,7 +2036,7 @@ func (s *swapClientServer) ListStaticAddressDeposits(ctx context.Context, "outpoints") } - allDeposits, err := s.depositManager.GetAllDeposits(ctx) + allDeposits, err := s.depositManager.GetVisibleDeposits(ctx) if err != nil { return nil, err } @@ -1895,7 +2102,7 @@ func (s *swapClientServer) ListStaticAddressWithdrawals(ctx context.Context, Id: d.ID[:], Outpoint: d.OutPoint.String(), Value: int64(d.Value), - ConfirmationHeight: d.ConfirmationHeight, + ConfirmationHeight: d.GetConfirmationHeight(), State: toClientDepositState( d.GetState(), ), @@ -1986,16 +2193,18 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context, protoDeposits = make([]*looprpc.Deposit, 0, len(ds)) for _, d := range ds { state := toClientDepositState(d.GetState()) - blocksUntilExpiry := d.ConfirmationHeight + - int64(addrParams.Expiry) - - int64(lndInfo.BlockHeight) + confirmationHeight := d.GetConfirmationHeight() + blocksUntilExpiry := depositBlocksUntilExpiry( + confirmationHeight, addrParams.Expiry, + int64(lndInfo.BlockHeight), + ) pd := &looprpc.Deposit{ Id: d.ID[:], State: state, Outpoint: d.OutPoint.String(), Value: int64(d.Value), - ConfirmationHeight: d.ConfirmationHeight, + ConfirmationHeight: confirmationHeight, SwapHash: d.SwapHash[:], BlocksUntilExpiry: blocksUntilExpiry, } @@ -2032,6 +2241,8 @@ func (s *swapClientServer) ListStaticAddressSwaps(ctx context.Context, }, nil } +// staticAddressLoopInTimestamp converts a non-zero timestamp to Unix nano +// form and preserves zero timestamps as zero. func staticAddressLoopInTimestamp(t time.Time) int64 { if t.IsZero() { return 0 @@ -2057,6 +2268,141 @@ func staticAddressLoopInSwapServerCost(swp *loopin.StaticAddressLoopIn) int64 { } } +// staticAddressLoopInSwapInfos loads the static-address loop-in manager swaps +// and converts them to client-facing swap info records. +func (s *swapClientServer) staticAddressLoopInSwapInfos( + ctx context.Context) ([]*loop.SwapInfo, error) { + + if s.staticLoopInManager == nil { + return nil, nil + } + + staticSwaps, err := s.staticLoopInManager.GetAllSwaps(ctx) + if err != nil { + return nil, err + } + + swapInfos := make([]*loop.SwapInfo, 0, len(staticSwaps)) + for _, swp := range staticSwaps { + if swp == nil { + continue + } + + swapInfo, err := s.staticAddressLoopInSwapInfo(ctx, swp) + if err != nil { + return nil, err + } + swapInfos = append(swapInfos, swapInfo) + } + + return swapInfos, nil +} + +// staticAddressLoopInSwapInfo converts one static-address loop-in into swap +// info using the daemon's current chain parameters. +func (s *swapClientServer) staticAddressLoopInSwapInfo(_ context.Context, + swp *loopin.StaticAddressLoopIn) (*loop.SwapInfo, error) { + + chainParams, err := s.network.ChainParams() + if err != nil { + return nil, fmt.Errorf("error getting chain params") + } + + return staticAddressLoopInSwapInfoWithChainParams(swp, chainParams) +} + +// staticAddressLoopInSwapInfoWithChainParams converts one static-address +// loop-in into swap info, including its reconstructed V2 P2WSH HTLC address. +func staticAddressLoopInSwapInfoWithChainParams( + swp *loopin.StaticAddressLoopIn, + chainParams *chaincfg.Params) (*loop.SwapInfo, error) { + + htlcAddress, err := staticAddressLoopInHtlcAddress(swp, chainParams) + if err != nil { + return nil, err + } + + var lastHop *route.Vertex + if len(swp.LastHop) > 0 { + vertex, err := route.NewVertexFromBytes(swp.LastHop) + if err != nil { + return nil, err + } + lastHop = &vertex + } + + amount := swp.TotalDepositAmount() + if swp.SelectedAmount > 0 { + amount = swp.SelectedAmount + } + + lastUpdate := swp.LastUpdateTime + if lastUpdate.IsZero() { + lastUpdate = swp.InitiationTime + } + + return &loop.SwapInfo{ + SwapStateData: loopdb.SwapStateData{ + // Mirror ListStaticAddressSwaps by reporting only the persisted + // client-visible server cost. On-chain and off-chain costs stay + // zero until static loop-ins persist real fee data. + Cost: loopdb.SwapCost{ + Server: btcutil.Amount( + staticAddressLoopInSwapServerCost(swp), + ), + }, + }, + SwapContract: loopdb.SwapContract{ + AmountRequested: amount, + CltvExpiry: swp.HtlcCltvExpiry, + MaxSwapFee: swp.MaxSwapFee, + InitiationTime: swp.InitiationTime, + Label: swp.Label, + ProtocolVersion: loopdb.ProtocolVersion( + swp.ProtocolVersion, + ), + }, + LastUpdate: lastUpdate, + SwapHash: swp.SwapHash, + SwapType: swap.TypeStaticAddressLoopIn, + StaticAddressLoopInState: swp.GetState(), + HtlcAddressP2WSH: htlcAddress, + LastHop: lastHop, + }, nil +} + +// staticAddressLoopInHtlcAddress reconstructs the V2 P2WSH HTLC address from +// the static-address loop-in's client and server keys. +func staticAddressLoopInHtlcAddress(swp *loopin.StaticAddressLoopIn, + chainParams *chaincfg.Params) (btcutil.Address, error) { + + if swp.ClientPubkey == nil { + return nil, errors.New("missing static address loop-in client HTLC key") + } + if swp.ServerPubkey == nil { + return nil, errors.New("missing static address loop-in server HTLC key") + } + + htlc, err := swap.NewHtlcV2( + swp.HtlcCltvExpiry, pubkeyTo33ByteSlice(swp.ClientPubkey), + pubkeyTo33ByteSlice(swp.ServerPubkey), swp.SwapHash, chainParams, + ) + if err != nil { + return nil, fmt.Errorf("construct static address loop-in HTLC: %w", err) + } + + return htlc.Address, nil +} + +// pubkeyTo33ByteSlice converts a compressed public key to a fixed 33-byte +// array. +func pubkeyTo33ByteSlice(pubkey *btcec.PublicKey) [33]byte { + var pubkeyBytes [33]byte + copy(pubkeyBytes[:], pubkey.SerializeCompressed()) + + return pubkeyBytes +} + // GetStaticAddressSummary returns a summary of static address-related // information. Amongst deposits and withdrawals and their total values, it also // includes a list of detailed deposit information filtered by their state. @@ -2064,7 +2410,7 @@ func (s *swapClientServer) GetStaticAddressSummary(ctx context.Context, _ *looprpc.StaticAddressSummaryRequest) ( *looprpc.StaticAddressSummaryResponse, error) { - allDeposits, err := s.depositManager.GetAllDeposits(ctx) + allDeposits, err := s.depositManager.GetVisibleDeposits(ctx) if err != nil { return nil, err } @@ -2080,23 +2426,16 @@ func (s *swapClientServer) GetStaticAddressSummary(ctx context.Context, htlcTimeoutSwept int64 ) - // Value unconfirmed. - utxos, err := s.staticAddressManager.ListUnspent( - ctx, 0, deposit.MinConfs-1, - ) - if err != nil { - return nil, err - } - for _, u := range utxos { - valueUnconfirmed += int64(u.Value) - } - - // Confirmed total values by category. + // Total values by category. for _, d := range allDeposits { value := int64(d.Value) switch d.GetState() { case deposit.Deposited: - valueDeposited += value + if d.GetConfirmationHeight() <= 0 { + valueUnconfirmed += value + } else { + valueDeposited += value + } case deposit.Expired: valueExpired += value @@ -2246,13 +2585,27 @@ func (s *swapClientServer) populateBlocksUntilExpiry(ctx context.Context, return err } for i := range len(deposits) { - deposits[i].BlocksUntilExpiry = - deposits[i].ConfirmationHeight + - int64(params.Expiry) - bestBlockHeight + deposits[i].BlocksUntilExpiry = depositBlocksUntilExpiry( + deposits[i].ConfirmationHeight, params.Expiry, + bestBlockHeight, + ) } return nil } +// depositBlocksUntilExpiry returns the remaining blocks until a deposit +// expires. Unconfirmed deposits return the full CSV value because the timeout +// has not started yet. +func depositBlocksUntilExpiry(confirmationHeight int64, expiry uint32, + bestBlockHeight int64) int64 { + + if confirmationHeight <= 0 { + return int64(expiry) + } + + return confirmationHeight + int64(expiry) - bestBlockHeight +} + // StaticOpenChannel initiates an open channel request using static address // deposits. func (s *swapClientServer) StaticOpenChannel(ctx context.Context, @@ -2303,7 +2656,7 @@ func filter(deposits []*deposit.Deposit, f filterFunc) []*looprpc.Deposit { ), Outpoint: outpoint, Value: int64(d.Value), - ConfirmationHeight: d.ConfirmationHeight, + ConfirmationHeight: d.GetConfirmationHeight(), SwapHash: swapHash, } @@ -2356,6 +2709,8 @@ func toClientDepositState(state fsm.StateType) looprpc.DepositState { } } +// toClientStaticAddressLoopInState maps the static-address loop-in FSM state +// to the RPC enum exposed to clients. func toClientStaticAddressLoopInState( state fsm.StateType) looprpc.StaticAddressLoopInSwapState { @@ -2508,7 +2863,12 @@ func (s *swapClientServer) processStatusUpdates(mainCtx context.Context) { // subscribers about the changes. case swp := <-s.statusChan: s.swapsLock.Lock() - s.swaps[swp.SwapHash] = swp + // Static loop-ins are broadcast to monitor subscribers, but they + // stay out of the legacy swap cache so ListSwaps and SwapInfo remain + // traditional-swap views. + if swp.SwapType != swap.TypeStaticAddressLoopIn { + s.swaps[swp.SwapHash] = swp + } for _, subscriber := range s.subscribers { select { @@ -2660,6 +3020,13 @@ func validateLoopOutRequest(ctx context.Context, lnd lndclient.LightningClient, // the amount requested, the maximum possible routing fees, // the available channel set and the fact that equal splitting is // used for MPP. + // + // TODO: Also account for the quoted server fee and the concurrent prepay + // payment. The CLI and autoloop obtain the fee and prepay amounts from a + // quote, but this RPC only carries maximum limits, which direct callers may + // set higher than the quoted amounts. Treating those limits as exact can + // reject a viable swap, while the actual routing fees are only known when + // the invoices are paid. requiredBalance := btcutil.Amount(req.Amt + req.MaxSwapRoutingFee) isRoutable, _ := hasBandwidth(activeChannelSet, requiredBalance, int(maxParts)) @@ -2694,11 +3061,24 @@ func hasBandwidth(channels []lndclient.ChannelInfo, amt btcutil.Amount, localBalances := make([]btcutil.Amount, len(channels)) var totalBandwidth btcutil.Amount for i, channel := range channels { - tracef("Channel %v: local=%v remote=%v", channel.ChannelID, - channel.LocalBalance, channel.RemoteBalance) + localBalance := channel.LocalBalance + var reserve btcutil.Amount + if channel.LocalConstraints != nil { + reserve = channel.LocalConstraints.Reserve + } - localBalances[i] = channel.LocalBalance - totalBandwidth += channel.LocalBalance + if reserve >= localBalance { + localBalance = 0 + } else { + localBalance -= reserve + } + + tracef("Channel %v: local=%v reserve=%v available=%v "+ + "remote=%v", channel.ChannelID, channel.LocalBalance, + reserve, localBalance, channel.RemoteBalance) + + localBalances[i] = localBalance + totalBandwidth += localBalance } tracef("Total bandwidth: %v", totalBandwidth) @@ -2720,8 +3100,6 @@ func hasBandwidth(channels []lndclient.ChannelInfo, amt btcutil.Amount, paid := false for i := range len(localBalances) { - // TODO(hieblmi): Consider channel reserves because the - // channel can't send its full local balance. if localBalances[i] >= split { tracef("len(shards)=%v: Local channel "+ "balance %v can pay %v sats", diff --git a/loopd/swapclient_server_deposit_test.go b/loopd/swapclient_server_deposit_test.go new file mode 100644 index 00000000..3d4ce065 --- /dev/null +++ b/loopd/swapclient_server_deposit_test.go @@ -0,0 +1,86 @@ +package loopd + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/staticaddr/deposit" +) + +// TestDepositBlocksUntilExpiry checks blocks-until-expiry handling for +// confirmed and unconfirmed deposits. +func TestDepositBlocksUntilExpiry(t *testing.T) { + t.Run("unconfirmed", func(t *testing.T) { + if blocks := depositBlocksUntilExpiry(0, 144, 500); blocks != 144 { + t.Fatalf("expected 144 blocks for unconfirmed deposit, got %d", + blocks) + } + }) + + t.Run("confirmed", func(t *testing.T) { + if blocks := depositBlocksUntilExpiry(450, 144, 500); blocks != 94 { + t.Fatalf("expected 94 blocks until expiry, got %d", + blocks) + } + }) +} + +// TestWithdrawAllDepositOutpoints checks `all` withdrawal handling for +// confirmed and unconfirmed deposits. +func TestWithdrawAllDepositOutpoints(t *testing.T) { + t.Run("rejects unconfirmed", func(t *testing.T) { + deposits := []*deposit.Deposit{ + { + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{1}, + Index: 1, + }, + }, + { + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{2}, + Index: 2, + }, + ConfirmationHeight: 123, + }, + } + + _, err := withdrawAllDepositOutpoints(deposits) + if err == nil { + t.Fatal("expected unconfirmed deposit to fail all withdrawal") + } + }) + + t.Run("returns all confirmed", func(t *testing.T) { + first := wire.OutPoint{ + Hash: chainhash.Hash{3}, + Index: 3, + } + second := wire.OutPoint{ + Hash: chainhash.Hash{4}, + Index: 4, + } + deposits := []*deposit.Deposit{ + { + OutPoint: first, + ConfirmationHeight: 123, + }, + { + OutPoint: second, + ConfirmationHeight: 124, + }, + } + + outpoints, err := withdrawAllDepositOutpoints(deposits) + if err != nil { + t.Fatalf("expected confirmed deposits to succeed: %v", err) + } + if len(outpoints) != 2 { + t.Fatalf("expected 2 outpoints, got %d", len(outpoints)) + } + if outpoints[0] != first || outpoints[1] != second { + t.Fatal("expected all confirmed outpoints to remain selected") + } + }) +} diff --git a/loopd/swapclient_server_staticaddr_test.go b/loopd/swapclient_server_staticaddr_test.go new file mode 100644 index 00000000..bb4cc01c --- /dev/null +++ b/loopd/swapclient_server_staticaddr_test.go @@ -0,0 +1,238 @@ +package loopd + +import ( + "context" + "testing" + + "github.com/btcsuite/btcd/btcec/v2" + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/btcsuite/btclog/v2" + "github.com/lightninglabs/loop/looprpc" + "github.com/lightninglabs/loop/staticaddr/address" + "github.com/lightninglabs/loop/staticaddr/deposit" + "github.com/lightninglabs/loop/staticaddr/script" + mock_lnd "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/stretchr/testify/require" +) + +type staticAddrDepositStore struct { + allDeposits []*deposit.Deposit + byOutpoint map[string]*deposit.Deposit +} + +// CreateDeposit implements deposit.Store for static address server tests. +func (s *staticAddrDepositStore) CreateDeposit(context.Context, + *deposit.Deposit) error { + + return nil +} + +// UpdateDeposit implements deposit.Store for static address server tests. +func (s *staticAddrDepositStore) UpdateDeposit(context.Context, + *deposit.Deposit) error { + + return nil +} + +// GetDeposit implements deposit.Store for static address server tests. +func (s *staticAddrDepositStore) GetDeposit(context.Context, + deposit.ID) (*deposit.Deposit, error) { + + return nil, nil +} + +// DepositForOutpoint returns the deposit for the requested outpoint. +func (s *staticAddrDepositStore) DepositForOutpoint(_ context.Context, + outpoint string) (*deposit.Deposit, error) { + + if deposit, ok := s.byOutpoint[outpoint]; ok { + return deposit, nil + } + + return nil, deposit.ErrDepositNotFound +} + +// AllDeposits returns all deposits seeded into the test store. +func (s *staticAddrDepositStore) AllDeposits(context.Context) ( + []*deposit.Deposit, error) { + + return s.allDeposits, nil +} + +type staticAddrTestAddressManager struct{} + +func (s *staticAddrTestAddressManager) GetStaticAddressParameters( + context.Context) (*script.Parameters, error) { + + return nil, nil +} + +func (s *staticAddrTestAddressManager) GetStaticAddress( + context.Context) (*script.StaticAddress, error) { + + return nil, nil +} + +func (s *staticAddrTestAddressManager) ListUnspent(context.Context, + int32, int32) ([]*lnwallet.Utxo, error) { + + return nil, nil +} + +func (s *staticAddrTestAddressManager) GetTaprootAddress( + *btcec.PublicKey, *btcec.PublicKey, int64) (*btcutil.AddressTaproot, + error) { + + return nil, nil +} + +// newTestDepositManager creates a deposit manager backed by seeded deposits. +func newTestDepositManager( + deposits ...*deposit.Deposit) *deposit.Manager { + + byOutpoint := make(map[string]*deposit.Deposit, len(deposits)) + for _, deposit := range deposits { + byOutpoint[deposit.OutPoint.String()] = deposit + } + + return deposit.NewManager(&deposit.ManagerConfig{ + AddressManager: &staticAddrTestAddressManager{}, + Store: &staticAddrDepositStore{ + allDeposits: deposits, + byOutpoint: byOutpoint, + }, + }) +} + +// newTestStaticAddressContext creates static address test dependencies. +func newTestStaticAddressContext(t *testing.T) (*address.Manager, + *mock_lnd.LndMockServices) { + + t.Helper() + + mock := mock_lnd.NewMockLnd() + _, client := mock_lnd.CreateKey(1) + _, server := mock_lnd.CreateKey(2) + + addrStore := &mockAddressStore{ + params: []*script.Parameters{{ + ClientPubkey: client, + ServerPubkey: server, + Expiry: 10, + PkScript: []byte("pkscript"), + }}, + } + + addrMgr, err := address.NewManager(&address.ManagerConfig{ + Store: addrStore, + WalletKit: mock.WalletKit, + ChainParams: mock.ChainParams, + }, 1) + require.NoError(t, err) + + return addrMgr, mock +} + +// TestListStaticAddressDepositsReturnsVisibleDeposits verifies normal deposit +// listings include visible deposit records. +func TestListStaticAddressDepositsReturnsVisibleDeposits(t *testing.T) { + t.Parallel() + + available := &deposit.Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{2}, + Index: 2, + }, + } + available.SetState(deposit.Deposited) + + addrMgr, lnd := newTestStaticAddressContext(t) + server := &swapClientServer{ + depositManager: newTestDepositManager(available), + staticAddressManager: addrMgr, + lnd: &lnd.LndServices, + } + + resp, err := server.ListStaticAddressDeposits( + context.Background(), &looprpc.ListStaticAddressDepositsRequest{}, + ) + require.NoError(t, err) + require.Len(t, resp.FilteredDeposits, 1) + require.Equal( + t, available.OutPoint.String(), + resp.FilteredDeposits[0].Outpoint, + ) +} + +// TestGetStaticAddressSummaryTotalsDeposits verifies visible deposits are +// included in static address summary totals. +func TestGetStaticAddressSummaryTotalsDeposits(t *testing.T) { + t.Parallel() + + unconfirmed := &deposit.Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{4}, + Index: 4, + }, + Value: btcutil.Amount(2_000), + ConfirmationHeight: 0, + } + unconfirmed.SetState(deposit.Deposited) + + confirmed := &deposit.Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{5}, + Index: 5, + }, + Value: btcutil.Amount(3_000), + ConfirmationHeight: 123, + } + confirmed.SetState(deposit.Deposited) + + addrMgr, _ := newTestStaticAddressContext(t) + server := &swapClientServer{ + depositManager: newTestDepositManager( + unconfirmed, confirmed, + ), + staticAddressManager: addrMgr, + } + + resp, err := server.GetStaticAddressSummary( + context.Background(), &looprpc.StaticAddressSummaryRequest{}, + ) + require.NoError(t, err) + require.EqualValues(t, 2, resp.TotalNumDeposits) + require.EqualValues(t, 2_000, resp.ValueUnconfirmedSatoshis) + require.EqualValues(t, 3_000, resp.ValueDepositedSatoshis) +} + +// TestGetLoopInQuoteRejectsUnavailableSelectedDeposit verifies manual quote +// requests fail for selected deposits that are no longer available. +func TestGetLoopInQuoteRejectsUnavailableSelectedDeposit(t *testing.T) { + t.Parallel() + setLogger(btclog.Disabled) + + locked := &deposit.Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{6}, + Index: 6, + }, + Value: btcutil.Amount(5_000), + } + locked.SetState(deposit.LoopingIn) + + addrMgr, lnd := newTestStaticAddressContext(t) + server := &swapClientServer{ + depositManager: newTestDepositManager(locked), + staticAddressManager: addrMgr, + lnd: &lnd.LndServices, + } + + _, err := server.GetLoopInQuote(context.Background(), &looprpc.QuoteRequest{ + DepositOutpoints: []string{locked.OutPoint.String()}, + }) + require.ErrorContains(t, err, "is not currently available") +} diff --git a/loopd/swapclient_server_test.go b/loopd/swapclient_server_test.go index 0857bbf2..1171389c 100644 --- a/loopd/swapclient_server_test.go +++ b/loopd/swapclient_server_test.go @@ -2,10 +2,12 @@ package loopd import ( "context" + "fmt" "os" "testing" "time" + "github.com/btcsuite/btcd/btcec/v2/schnorr" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/chaincfg" "github.com/btcsuite/btcd/chaincfg/chainhash" @@ -24,6 +26,7 @@ import ( "github.com/lightninglabs/loop/staticaddr/script" "github.com/lightninglabs/loop/swap" mock_lnd "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/input" "github.com/lightningnetwork/lnd/lnrpc/invoicesrpc" "github.com/lightningnetwork/lnd/lntypes" "github.com/lightningnetwork/lnd/lnwallet" @@ -31,6 +34,7 @@ import ( "github.com/lightningnetwork/lnd/routing/route" "github.com/stretchr/testify/require" "google.golang.org/grpc/codes" + "google.golang.org/grpc/metadata" "google.golang.org/grpc/status" ) @@ -500,10 +504,554 @@ func TestListStaticAddressSwapsPopulatesTimingAndCosts(t *testing.T) { ) } +// TestStaticAddressLoopInMarshallUsesStaticTypeAndP2WSH protects the RPC +// mapping invariant that static loop-ins expose their static type, static +// state, and P2WSH HTLC address without leaking a taproot HTLC address. +func TestStaticAddressLoopInMarshallUsesStaticTypeAndP2WSH(t *testing.T) { + server := &swapClientServer{} + loopSwap := &loop.SwapInfo{ + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateInitiated, + }, + SwapContract: loopdb.SwapContract{ + InitiationTime: time.Now(), + }, + LastUpdate: time.Now(), + SwapHash: lntypes.Hash{1}, + SwapType: swap.TypeStaticAddressLoopIn, + StaticAddressLoopInState: loopin.SignHtlcTx, + HtlcAddressP2WSH: testnetAddr, + } + + rpcSwap, err := server.marshallSwap(t.Context(), loopSwap) + require.NoError(t, err) + require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, rpcSwap.Type) + require.Equal( + t, looprpc.StaticAddressLoopInSwapState_SIGN_HTLC_TX, + rpcSwap.GetStaticLoopInState(), + ) + require.Equal(t, looprpc.SwapState_INITIATED, rpcSwap.State) + require.Equal(t, testnetAddr.EncodeAddress(), rpcSwap.HtlcAddressP2Wsh) + require.Empty(t, rpcSwap.HtlcAddressP2Tr) +} + +// TestStaticAddressLoopInMarshallFailuresLeaveLegacyFieldsDefault asserts that +// static loop-in failures keep default legacy fields while preserving the +// precise static state. +func TestStaticAddressLoopInMarshallFailuresLeaveLegacyFieldsDefault( + t *testing.T) { + + tests := []struct { + name string + state fsm.StateType + wantStaticState looprpc.StaticAddressLoopInSwapState + }{ + { + name: "failed", + state: loopin.Failed, + wantStaticState: looprpc. + StaticAddressLoopInSwapState_FAILED_STATIC_ADDRESS_SWAP, + }, + { + name: "succeeded transitioning failed", + state: loopin.SucceededTransitioningFailed, + wantStaticState: looprpc. + StaticAddressLoopInSwapState_SUCCEEDED_TRANSITIONING_FAILED, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + server, staticLoopIn := newGenericStaticLoopInServer(t) + staticLoopIn.SetState(test.state) + loopSwap, err := server.staticAddressLoopInSwapInfo( + t.Context(), staticLoopIn, + ) + require.NoError(t, err) + + rpcSwap, err := server.marshallSwap(t.Context(), loopSwap) + + require.NoError(t, err) + require.Equal(t, looprpc.SwapState_INITIATED, rpcSwap.State) + require.Equal( + t, looprpc.FailureReason_FAILURE_REASON_NONE, + rpcSwap.FailureReason, + ) + require.Equal( + t, test.wantStaticState, + rpcSwap.GetStaticLoopInState(), + ) + }) + } +} + +// TestStaticAddressLoopInMarshallRejectsMissingHtlcAddress protects the +// fail-closed HTLC-address invariant for static loop-ins missing the P2WSH +// address required by the client-facing RPC representation. +func TestStaticAddressLoopInMarshallRejectsMissingHtlcAddress(t *testing.T) { + _, taprootAddress := newTestStaticAddressParams(t) + server := &swapClientServer{} + loopSwap := &loop.SwapInfo{ + SwapStateData: loopdb.SwapStateData{ + State: loopdb.StateInitiated, + }, + SwapContract: loopdb.SwapContract{ + InitiationTime: time.Now(), + }, + LastUpdate: time.Now(), + SwapHash: lntypes.Hash{1}, + SwapType: swap.TypeStaticAddressLoopIn, + StaticAddressLoopInState: loopin.SignHtlcTx, + HtlcAddressP2TR: taprootAddress, + } + + _, err := server.marshallSwap(t.Context(), loopSwap) + require.ErrorContains(t, err, "missing static address loop-in P2WSH HTLC address") +} + +// TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing protects the +// HTLC-address construction invariant that missing cooperative keys must not +// produce monitorable swap info. +func TestStaticAddressLoopInSwapInfoFailsClosedWhenHtlcKeysMissing(t *testing.T) { + server, staticLoopIn := newGenericStaticLoopInServer(t) + staticLoopIn.ClientPubkey = nil + + _, err := server.staticAddressLoopInSwapInfo(t.Context(), staticLoopIn) + require.ErrorContains( + t, err, "missing static address loop-in client HTLC key", + ) +} + +// TestMonitorSnapshotIncludesStaticAddressLoopIns protects the monitor snapshot +// invariant that pending static loop-ins are included alongside cached generic +// swaps with their static state and swap-specific HTLC address. +func TestMonitorSnapshotIncludesStaticAddressLoopIns(t *testing.T) { + ctx := t.Context() + server, staticLoopIn := newGenericStaticLoopInServer(t) + + pendingSwaps, completedSwaps := server.monitorCachedSwaps() + err := server.appendStaticAddressLoopInMonitorSnapshot( + ctx, &pendingSwaps, &completedSwaps, + ) + require.NoError(t, err) + require.Empty(t, completedSwaps) + require.Len(t, pendingSwaps, 1) + require.Equal(t, staticLoopIn.SwapHash, pendingSwaps[0].SwapHash) + require.Equal(t, swap.TypeStaticAddressLoopIn, pendingSwaps[0].SwapType) + require.Equal( + t, staticLoopIn.GetState(), + pendingSwaps[0].StaticAddressLoopInState, + ) + assertStaticLoopInUsesSwapHtlcAddress(t, staticLoopIn, pendingSwaps[0]) +} + +// TestMonitorSnapshotIncludesFinalStaticAddressLoopIns protects the monitor +// snapshot invariant that exact final static loop-in states are completed swaps. +func TestMonitorSnapshotIncludesFinalStaticAddressLoopIns(t *testing.T) { + server, staticLoopIn := newGenericStaticLoopInServer(t) + staticLoopIn.SetState(loopin.Succeeded) + + pendingSwaps, completedSwaps := server.monitorCachedSwaps() + err := server.appendStaticAddressLoopInMonitorSnapshot( + t.Context(), &pendingSwaps, &completedSwaps, + ) + + require.NoError(t, err) + require.Empty(t, pendingSwaps) + require.Len(t, completedSwaps, 1) +} + +// TestStaticLoopInStatusUpdaterUsesSwapHtlcAddress protects the live-update +// invariant that static loop-in status events derive the HTLC address from the +// swap, not from reusable static address parameters. +func TestStaticLoopInStatusUpdaterUsesSwapHtlcAddress(t *testing.T) { + ctx := t.Context() + _, staticLoopIn := newGenericStaticLoopInServer(t) + staticLoopIn.AddressParams = nil + statusChan := make(chan loop.SwapInfo, 1) + updater := &staticLoopInStatusUpdater{ + statusChan: statusChan, + mainCtx: ctx, + chainParams: &chaincfg.TestNet3Params, + } + + err := updater.sendUpdate(ctx, staticLoopIn) + require.NoError(t, err) + swapInfo := <-statusChan + assertStaticLoopInUsesSwapHtlcAddress(t, staticLoopIn, swapInfo) +} + +// TestMonitorSuppressesStaticAddressLoopInSnapshotLiveDuplicate protects the +// monitor race invariant that live static loop-in updates arriving during the +// initial snapshot are deduplicated without dropping newer progress. +func TestMonitorSuppressesStaticAddressLoopInSnapshotLiveDuplicate(t *testing.T) { + logger := btclog.NewSLogger( + btclog.NewDefaultHandler(os.Stdout), + ) + setLogger(logger.SubSystem(Subsystem)) + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + mainCtx, mainCancel := context.WithCancel(t.Context()) + defer mainCancel() + server, staticLoopIn, store := newGenericStaticLoopInServerWithStore(t) + server.statusChan = make(chan loop.SwapInfo) + server.subscribers = make(map[int]chan<- any) + server.mainCtx = mainCtx + + snapshotStarted := make(chan struct{}, 1) + releaseSnapshot := make(chan struct{}) + store.beforeGet = func() { + select { + case snapshotStarted <- struct{}{}: + default: + } + } + store.waitGet = releaseSnapshot + + go server.processStatusUpdates(mainCtx) + + monitorServer := &testMonitorServer{ + ctx: ctx, + sent: make(chan *looprpc.SwapStatus, 3), + } + errChan := make(chan error, 1) + go func() { + errChan <- server.Monitor(&looprpc.MonitorRequest{}, monitorServer) + }() + + select { + case <-snapshotStarted: + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + + staticUpdate, err := server.staticAddressLoopInSwapInfo(ctx, staticLoopIn) + require.NoError(t, err) + staleUpdate := *staticUpdate + staleUpdate.State = loopdb.StateInitiated + staleUpdate.LastUpdate = staticUpdate.LastUpdate.Add(-time.Second) + server.statusChan <- staleUpdate + + server.statusChan <- *staticUpdate + close(releaseSnapshot) + + first := receiveMonitorUpdate(t, ctx, monitorServer.sent) + require.Equal(t, staticLoopIn.SwapHash[:], first.IdBytes) + require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, first.Type) + require.Equal( + t, looprpc.StaticAddressLoopInSwapState_PAYMENT_RECEIVED, + first.GetStaticLoopInState(), + ) + + nextUpdate := *staticUpdate + nextUpdate.State = loopdb.StateSuccess + nextUpdate.StaticAddressLoopInState = loopin.Succeeded + nextUpdate.LastUpdate = staticUpdate.LastUpdate.Add(time.Second) + server.statusChan <- nextUpdate + + second := receiveMonitorUpdate(t, ctx, monitorServer.sent) + require.Equal(t, staticLoopIn.SwapHash[:], second.IdBytes) + require.Equal(t, looprpc.SwapType_STATIC_LOOP_IN, second.Type) + require.Equal( + t, looprpc.StaticAddressLoopInSwapState_SUCCEEDED, + second.GetStaticLoopInState(), + ) + + cancel() + require.NoError(t, <-errChan) +} + +// TestStaticAddressLoopInHighWaterSuppressesExactDuplicate protects the +// high-water dedup invariant that an initial live update identical to the +// snapshot is treated as stale. +func TestStaticAddressLoopInHighWaterSuppressesExactDuplicate(t *testing.T) { + swapHash := lntypes.Hash{1, 2, 3} + lastUpdate := time.Unix(100, 0).UTC() + snapshot := loop.SwapInfo{ + SwapHash: swapHash, + SwapType: swap.TypeStaticAddressLoopIn, + LastUpdate: lastUpdate, + StaticAddressLoopInState: loopin.PaymentReceived, + } + highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{ + snapshot, + }) + + isStale := isInitialStaticAddressLoopInStale(highWater, snapshot) + + require.True(t, isStale) +} + +// TestStaticAddressLoopInHighWaterKeepsSameTimeDifferentState protects the +// high-water timing invariant that equal timestamps do not hide a distinct +// static loop-in state transition. +func TestStaticAddressLoopInHighWaterKeepsSameTimeDifferentState(t *testing.T) { + swapHash := lntypes.Hash{1, 2, 3} + lastUpdate := time.Unix(100, 0).UTC() + snapshot := loop.SwapInfo{ + SwapHash: swapHash, + SwapType: swap.TypeStaticAddressLoopIn, + LastUpdate: lastUpdate, + StaticAddressLoopInState: loopin.PaymentReceived, + } + liveUpdate := snapshot + liveUpdate.StaticAddressLoopInState = loopin.Succeeded + highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{ + snapshot, + }) + + isStale := isInitialStaticAddressLoopInStale(highWater, liveUpdate) + + require.False(t, isStale) +} + +// TestStaticAddressLoopInHighWaterSuppressesOlderStaticOnly protects the +// high-water cache invariant that stale suppression applies only to static +// loop-ins and cannot filter generic swap updates. +func TestStaticAddressLoopInHighWaterSuppressesOlderStaticOnly(t *testing.T) { + swapHash := lntypes.Hash{1, 2, 3} + lastUpdate := time.Unix(100, 0).UTC() + snapshot := loop.SwapInfo{ + SwapHash: swapHash, + SwapType: swap.TypeStaticAddressLoopIn, + LastUpdate: lastUpdate, + StaticAddressLoopInState: loopin.PaymentReceived, + } + highWater := staticAddressLoopInMonitorHighWater([]loop.SwapInfo{ + snapshot, + }) + olderStatic := snapshot + olderStatic.LastUpdate = lastUpdate.Add(-time.Second) + olderStatic.StaticAddressLoopInState = loopin.Succeeded + nonStatic := olderStatic + nonStatic.SwapType = swap.TypeOut + + staticStale := isInitialStaticAddressLoopInStale(highWater, olderStatic) + nonStaticStale := isInitialStaticAddressLoopInStale( + highWater, nonStatic, + ) + + require.True(t, staticStale) + require.False(t, nonStaticStale) +} + +// TestStaticAddressLoopInStatusUpdateDoesNotEnterGenericSwapCache protects the +// cache isolation invariant that static loop-in live updates reach subscribers +// without entering the generic swap cache. +func TestStaticAddressLoopInStatusUpdateDoesNotEnterGenericSwapCache(t *testing.T) { + ctx := t.Context() + server, staticLoopIn := newGenericStaticLoopInServer(t) + server.statusChan = make(chan loop.SwapInfo) + updates := make(chan any, 1) + server.subscribers = map[int]chan<- any{0: updates} + mainCtx, cancel := context.WithCancel(ctx) + defer cancel() + go server.processStatusUpdates(mainCtx) + + staticUpdate, err := server.staticAddressLoopInSwapInfo(ctx, staticLoopIn) + require.NoError(t, err) + server.statusChan <- *staticUpdate + + select { + case update := <-updates: + require.Equal(t, staticLoopIn.SwapHash, update.(loop.SwapInfo).SwapHash) + + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + + server.swapsLock.Lock() + _, cached := server.swaps[staticLoopIn.SwapHash] + server.swapsLock.Unlock() + require.False(t, cached) +} + +func newGenericStaticLoopInServer(t *testing.T) (*swapClientServer, + *loopin.StaticAddressLoopIn) { + + server, staticLoopIn, _ := newGenericStaticLoopInServerWithStore(t) + + return server, staticLoopIn +} + +func newTestStaticAddressParams(t *testing.T) (*script.Parameters, + *btcutil.AddressTaproot) { + + t.Helper() + + const staticAddressExpiry = uint32(25) + + _, staticClientPubkey := mock_lnd.CreateKey(12) + _, staticServerPubkey := mock_lnd.CreateKey(13) + staticAddress, err := script.NewStaticAddress( + input.MuSig2Version100RC2, int64(staticAddressExpiry), + staticClientPubkey, staticServerPubkey, + ) + require.NoError(t, err) + + staticPkScript, err := staticAddress.StaticAddressScript() + require.NoError(t, err) + + taprootAddress, err := btcutil.NewAddressTaproot( + schnorr.SerializePubKey(staticAddress.TaprootKey), + &chaincfg.TestNet3Params, + ) + require.NoError(t, err) + + return &script.Parameters{ + ClientPubkey: staticClientPubkey, + ServerPubkey: staticServerPubkey, + Expiry: staticAddressExpiry, + PkScript: staticPkScript, + }, taprootAddress +} + +func newGenericStaticLoopInServerWithStore(t *testing.T) (*swapClientServer, + *loopin.StaticAddressLoopIn, *mockStaticAddressLoopInStore) { + + t.Helper() + + _, clientPubkey := mock_lnd.CreateKey(10) + _, serverPubkey := mock_lnd.CreateKey(11) + addressParams, _ := newTestStaticAddressParams(t) + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{12, 13, 14}, + Index: 2, + } + staticDeposit := &deposit.Deposit{ + OutPoint: depositOutpoint, + Value: 51_000, + } + lastHop := route.Vertex{7, 8, 9} + + staticLoopIn := &loopin.StaticAddressLoopIn{ + SwapHash: lntypes.Hash{1, 2, 3}, + HtlcCltvExpiry: 700, + InitiationTime: time.Unix(100, 0).UTC(), + LastUpdateTime: time.Unix(200, 0).UTC(), + Label: "static-loop-in", + ClientPubkey: clientPubkey, + ServerPubkey: serverPubkey, + LastHop: lastHop[:], + QuotedSwapFee: 1_111, + SelectedAmount: 50_000, + DepositOutpoints: []string{depositOutpoint.String()}, + Deposits: []*deposit.Deposit{staticDeposit}, + AddressParams: addressParams, + } + staticLoopIn.SetState(loopin.PaymentReceived) + + depositStore := &mockDepositStore{ + byOutpoint: map[string]*deposit.Deposit{ + depositOutpoint.String(): staticDeposit, + }, + } + loopInStore := &mockStaticAddressLoopInStore{ + swaps: []*loopin.StaticAddressLoopIn{staticLoopIn}, + } + staticLoopInManager, err := loopin.NewManager(&loopin.Config{ + Store: loopInStore, + DepositManager: deposit.NewManager(&deposit.ManagerConfig{ + Store: depositStore, + }), + }, 1) + require.NoError(t, err) + + return &swapClientServer{ + network: lndclient.NetworkTestnet, + swaps: make(map[lntypes.Hash]loop.SwapInfo), + staticLoopInManager: staticLoopInManager, + }, staticLoopIn, loopInStore +} + +// assertStaticLoopInUsesSwapHtlcAddress verifies the static loop-in uses the +// swap HTLC P2WSH address expected by the fixture. +func assertStaticLoopInUsesSwapHtlcAddress(t *testing.T, + staticLoopIn *loopin.StaticAddressLoopIn, swapInfo loop.SwapInfo) { + + t.Helper() + + expectedAddress, err := staticAddressLoopInHtlcAddress( + staticLoopIn, &chaincfg.TestNet3Params, + ) + require.NoError(t, err) + require.Nil(t, swapInfo.HtlcAddressP2TR) + require.NotNil(t, swapInfo.HtlcAddressP2WSH) + require.Equal( + t, expectedAddress.EncodeAddress(), + swapInfo.HtlcAddressP2WSH.EncodeAddress(), + ) +} + +// testMonitorServer implements the monitor stream interface for tests. +type testMonitorServer struct { + ctx context.Context + sent chan *looprpc.SwapStatus +} + +// Send forwards monitor updates to the test channel until the context is canceled. +func (s *testMonitorServer) Send(swapStatus *looprpc.SwapStatus) error { + select { + case s.sent <- swapStatus: + return nil + + case <-s.ctx.Done(): + return s.ctx.Err() + } +} + +// SetHeader is a no-op stub that satisfies the monitor stream interface in tests. +func (s *testMonitorServer) SetHeader(metadata.MD) error { + return nil +} + +// SendHeader is a no-op stub that satisfies the monitor stream interface in tests. +func (s *testMonitorServer) SendHeader(metadata.MD) error { + return nil +} + +// SetTrailer is a no-op stub that satisfies the monitor stream interface in tests. +func (s *testMonitorServer) SetTrailer(metadata.MD) {} + +// Context returns the stream context used by the test monitor server. +func (s *testMonitorServer) Context() context.Context { + return s.ctx +} + +// SendMsg is a no-op stub that satisfies the monitor stream interface in tests. +func (s *testMonitorServer) SendMsg(any) error { + return nil +} + +// RecvMsg is a no-op stub that satisfies the monitor stream interface in tests. +func (s *testMonitorServer) RecvMsg(any) error { + return nil +} + +// receiveMonitorUpdate waits for a monitor update or fails if the context is canceled. +func receiveMonitorUpdate(t *testing.T, ctx context.Context, + updates <-chan *looprpc.SwapStatus) *looprpc.SwapStatus { + + t.Helper() + + select { + case update := <-updates: + return update + + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } + + return nil +} + // mockStaticAddressLoopInStore is a minimal in-memory loop-in store for RPC // response mapping tests. type mockStaticAddressLoopInStore struct { - swaps []*loopin.StaticAddressLoopIn + swaps []*loopin.StaticAddressLoopIn + beforeGet func() + waitGet <-chan struct{} } // CreateLoopIn satisfies the static loop-in store interface. @@ -522,9 +1070,20 @@ func (s *mockStaticAddressLoopInStore) UpdateLoopIn(_ context.Context, // GetStaticAddressLoopInSwapsByStates returns the configured loop-ins. func (s *mockStaticAddressLoopInStore) GetStaticAddressLoopInSwapsByStates( - _ context.Context, _ []fsm.StateType) ([]*loopin.StaticAddressLoopIn, + ctx context.Context, _ []fsm.StateType) ([]*loopin.StaticAddressLoopIn, error) { + if s.beforeGet != nil { + s.beforeGet() + } + if s.waitGet != nil { + select { + case <-s.waitGet: + case <-ctx.Done(): + return nil, ctx.Err() + } + } + return s.swaps, nil } @@ -535,6 +1094,14 @@ func (s *mockStaticAddressLoopInStore) IsStored(_ context.Context, return false, nil } +// RecordStaticAddressRiskDecision satisfies the static loop-in store interface. +func (s *mockStaticAddressLoopInStore) RecordStaticAddressRiskDecision( + _ context.Context, _ lntypes.Hash, + _ loopin.ConfirmationRiskDecision) error { + + return nil +} + // GetLoopInByHash returns the configured loop-in with the given hash. func (s *mockStaticAddressLoopInStore) GetLoopInByHash(_ context.Context, swapHash lntypes.Hash) (*loopin.StaticAddressLoopIn, error) { @@ -811,6 +1378,50 @@ func TestValidateLoopOutRequest(t *testing.T) { err: errBalanceTooLow, expectedTarget: 0, }, + { + name: "channel reserve leaves balance one sat short", + chain: chaincfg.MainNetParams, + destAddr: mainnetAddr, + label: "label ok", + confTarget: 2, + channels: []lndclient.ChannelInfo{ + { + Active: true, + ChannelID: chanID2.ToUint64(), + LocalBalance: 10100, + LocalConstraints: &lndclient.ChannelConstraints{ + Reserve: 100, + }, + }, + }, + amount: 10000, + maxRoutingFee: 1, + maxParts: 1, + err: errBalanceTooLow, + expectedTarget: 0, + }, + { + name: "channel reserve leaves exact balance", + chain: chaincfg.MainNetParams, + destAddr: mainnetAddr, + label: "label ok", + confTarget: 2, + channels: []lndclient.ChannelInfo{ + { + Active: true, + ChannelID: chanID2.ToUint64(), + LocalBalance: 10101, + LocalConstraints: &lndclient.ChannelConstraints{ + Reserve: 100, + }, + }, + }, + amount: 10000, + maxRoutingFee: 1, + maxParts: 1, + err: nil, + expectedTarget: 2, + }, { name: "can split between channels", chain: chaincfg.MainNetParams, @@ -1321,7 +1932,7 @@ func (s *mockDepositStore) DepositForOutpoint(_ context.Context, if d, ok := s.byOutpoint[outpoint]; ok { return d, nil } - return nil, nil + return nil, deposit.ErrDepositNotFound } func (s *mockDepositStore) AllDeposits(_ context.Context) ([]*deposit.Deposit, @@ -1335,6 +1946,90 @@ func (s *mockDepositStore) AllDeposits(_ context.Context) ([]*deposit.Deposit, return deposits, nil } +// listUnspentDepositManager backs ListUnspentDeposits tests without requiring +// the full deposit manager event loop. +type listUnspentDepositManager struct { + byOutpoint map[string]*deposit.Deposit + + ensureDepositsFreshCalls int + onEnsureDepositsFresh func(*listUnspentDepositManager) +} + +func (m *listUnspentDepositManager) EnsureDepositsFresh( + context.Context) error { + + m.ensureDepositsFreshCalls++ + if m.onEnsureDepositsFresh != nil { + m.onEnsureDepositsFresh(m) + } + + return nil +} + +func (m *listUnspentDepositManager) GetActiveDepositsInState( + state fsm.StateType) ([]*deposit.Deposit, error) { + + deposits := make([]*deposit.Deposit, 0, len(m.byOutpoint)) + for _, d := range m.byOutpoint { + if !d.IsInState(state) { + continue + } + + deposits = append(deposits, d) + } + + return deposits, nil +} + +func (m *listUnspentDepositManager) DepositsForOutpoints( + _ context.Context, outpoints []string, ignoreUnknown bool) ( + []*deposit.Deposit, error) { + + deposits := make([]*deposit.Deposit, 0, len(outpoints)) + seen := make(map[string]struct{}, len(outpoints)) + for i, outpoint := range outpoints { + if _, ok := seen[outpoint]; ok { + return nil, fmt.Errorf("duplicate outpoint %s "+ + "at index %d", outpoint, i) + } + seen[outpoint] = struct{}{} + + d, ok := m.byOutpoint[outpoint] + if !ok { + if ignoreUnknown { + continue + } + + return nil, deposit.ErrDepositNotFound + } + + deposits = append(deposits, d) + } + + return deposits, nil +} + +func (m *listUnspentDepositManager) GetVisibleDeposits( + context.Context) ([]*deposit.Deposit, error) { + + return m.allDeposits(), nil +} + +func (m *listUnspentDepositManager) GetAllDeposits( + context.Context) ([]*deposit.Deposit, error) { + + return m.allDeposits(), nil +} + +func (m *listUnspentDepositManager) allDeposits() []*deposit.Deposit { + deposits := make([]*deposit.Deposit, 0, len(m.byOutpoint)) + for _, d := range m.byOutpoint { + deposits = append(deposits, d) + } + + return deposits +} + // TestListUnspentDeposits tests filtering behavior of ListUnspentDeposits. func TestListUnspentDeposits(t *testing.T) { ctx := context.Background() @@ -1376,39 +2071,41 @@ func TestListUnspentDeposits(t *testing.T) { } } - minConfs := int64(deposit.MinConfs) - utxoBelow := makeUtxo(0, minConfs-1) // always included - utxoAt := makeUtxo(1, minConfs) // included only if Deposited - utxoAbove1 := makeUtxo(2, minConfs+1) - utxoAbove2 := makeUtxo(3, minConfs+2) + utxoUnknown := makeUtxo(0, 0) + utxoDeposited := makeUtxo(1, 1) + utxoWithdrawn := makeUtxo(2, 2) + utxoLoopingIn := makeUtxo(3, 5) + utxoConfirmedUnknown := makeUtxo(4, 3) // Helper to build the deposit manager with specific states. buildDepositMgr := func( - states map[wire.OutPoint]fsm.StateType) *deposit.Manager { + states map[wire.OutPoint]fsm.StateType) *listUnspentDepositManager { - store := &mockDepositStore{ + depMgr := &listUnspentDepositManager{ byOutpoint: make(map[string]*deposit.Deposit), } for op, state := range states { d := &deposit.Deposit{OutPoint: op} d.SetState(state) - store.byOutpoint[op.String()] = d + depMgr.byOutpoint[op.String()] = d } - return deposit.NewManager(&deposit.ManagerConfig{Store: store}) + return depMgr } - // Include below-min-conf and >=min with Deposited; exclude others. - t.Run("below min conf always, Deposited included, others excluded", + // Only known Deposited records are available. Unknown deposits and + // known non-Deposited states are excluded. + t.Run("only known Deposited included", func(t *testing.T) { mock.SetListUnspent([]*lnwallet.Utxo{ - utxoBelow, utxoAt, utxoAbove1, utxoAbove2, + utxoUnknown, utxoDeposited, utxoWithdrawn, + utxoLoopingIn, }) depMgr := buildDepositMgr(map[wire.OutPoint]fsm.StateType{ - utxoAt.OutPoint: deposit.Deposited, - utxoAbove1.OutPoint: deposit.Withdrawn, - utxoAbove2.OutPoint: deposit.LoopingIn, + utxoDeposited.OutPoint: deposit.Deposited, + utxoWithdrawn.OutPoint: deposit.Withdrawn, + utxoLoopingIn.OutPoint: deposit.LoopingIn, }) server := &swapClientServer{ @@ -1420,9 +2117,10 @@ func TestListUnspentDeposits(t *testing.T) { ctx, &looprpc.ListUnspentDepositsRequest{}, ) require.NoError(t, err) + require.Equal(t, 1, depMgr.ensureDepositsFreshCalls) - // Expect utxoBelow and utxoAt only. - require.Len(t, resp.Utxos, 2) + // Expect the Deposited utxo only. + require.Len(t, resp.Utxos, 1) got := map[string]struct{}{} for _, u := range resp.Utxos { got[u.Outpoint] = struct{}{} @@ -1430,25 +2128,23 @@ func TestListUnspentDeposits(t *testing.T) { // same across utxos. require.NotEmpty(t, u.StaticAddress) } - _, ok1 := got[utxoBelow.OutPoint.String()] - _, ok2 := got[utxoAt.OutPoint.String()] - require.True(t, ok1) - require.True(t, ok2) + _, ok := got[utxoDeposited.OutPoint.String()] + require.True(t, ok) }) - // Swap states, now include utxoBelow and utxoAbove1. - t.Run("Deposited on >=min included; non-Deposited excluded", + // Confirmation depth no longer changes availability; state does. + t.Run("availability ignores conf depth once deposit state is known", func(t *testing.T) { mock.SetListUnspent( []*lnwallet.Utxo{ - utxoBelow, utxoAt, utxoAbove1, - utxoAbove2, + utxoUnknown, utxoDeposited, + utxoWithdrawn, utxoLoopingIn, }) depMgr := buildDepositMgr(map[wire.OutPoint]fsm.StateType{ - utxoAt.OutPoint: deposit.Withdrawn, - utxoAbove1.OutPoint: deposit.Deposited, - utxoAbove2.OutPoint: deposit.Withdrawn, + utxoDeposited.OutPoint: deposit.Deposited, + utxoWithdrawn.OutPoint: deposit.Withdrawn, + utxoLoopingIn.OutPoint: deposit.LoopingIn, }) server := &swapClientServer{ @@ -1460,26 +2156,32 @@ func TestListUnspentDeposits(t *testing.T) { ctx, &looprpc.ListUnspentDepositsRequest{}, ) require.NoError(t, err) + require.Equal(t, 1, depMgr.ensureDepositsFreshCalls) - require.Len(t, resp.Utxos, 2) + require.Len(t, resp.Utxos, 1) got := map[string]struct{}{} for _, u := range resp.Utxos { got[u.Outpoint] = struct{}{} } - _, ok1 := got[utxoBelow.OutPoint.String()] - _, ok2 := got[utxoAbove1.OutPoint.String()] - require.True(t, ok1) - require.True(t, ok2) + _, ok := got[utxoDeposited.OutPoint.String()] + require.True(t, ok) }) - // Confirmed UTXO not present in store should be included. - t.Run("confirmed utxo not in store is included", func(t *testing.T) { - // Only return a confirmed UTXO from lnd and make sure the - // deposit manager/store doesn't know about it. - mock.SetListUnspent([]*lnwallet.Utxo{utxoAbove2}) + // A wallet-visible UTXO reconciled by EnsureDepositsFresh should be + // returned in the same ListUnspentDeposits call. + t.Run("freshly reconciled wallet utxo is included", func(t *testing.T) { + mock.SetListUnspent([]*lnwallet.Utxo{utxoConfirmedUnknown}) - // Empty store (no states for any outpoint). depMgr := buildDepositMgr(map[wire.OutPoint]fsm.StateType{}) + depMgr.onEnsureDepositsFresh = func( + m *listUnspentDepositManager) { + + d := &deposit.Deposit{ + OutPoint: utxoConfirmedUnknown.OutPoint, + } + d.SetState(deposit.Deposited) + m.byOutpoint[d.OutPoint.String()] = d + } server := &swapClientServer{ staticAddressManager: addrMgr, @@ -1490,13 +2192,12 @@ func TestListUnspentDeposits(t *testing.T) { ctx, &looprpc.ListUnspentDepositsRequest{}, ) require.NoError(t, err) + require.Equal(t, 1, depMgr.ensureDepositsFreshCalls) - // We expect the confirmed UTXO to be included even though it - // doesn't exist in the store yet. require.Len(t, resp.Utxos, 1) require.Equal( - t, utxoAbove2.OutPoint.String(), resp.Utxos[0].Outpoint, + t, utxoConfirmedUnknown.OutPoint.String(), + resp.Utxos[0].Outpoint, ) - require.NotEmpty(t, resp.Utxos[0].StaticAddress) }) } diff --git a/loopd/view.go b/loopd/view.go index 73b401d8..86b9604a 100644 --- a/loopd/view.go +++ b/loopd/view.go @@ -44,6 +44,7 @@ func view(config *Config, lisCfg *ListenerCfg) error { if err != nil { return err } + defer assetClient.Close() } swapClient, cleanup, err := getClient( diff --git a/loopdb/migration_04_updates_test.go b/loopdb/migration_04_updates_test.go index 37f05466..f0e85f49 100644 --- a/loopdb/migration_04_updates_test.go +++ b/loopdb/migration_04_updates_test.go @@ -2,8 +2,6 @@ package loopdb import ( "context" - "io/ioutil" - "os" "path/filepath" "testing" @@ -48,9 +46,7 @@ func TestMigrationUpdates(t *testing.T) { ctxb := context.Background() // Restore a legacy database. - tempDirName, err := ioutil.TempDir("", "clientstore") - require.NoError(t, err) - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() tempPath := filepath.Join(tempDirName, dbFileName) db, err := bbolt.Open(tempPath, 0600, nil) diff --git a/loopdb/sqlc/migrations/000021_static_loopin_risk_decision.down.sql b/loopdb/sqlc/migrations/000021_static_loopin_risk_decision.down.sql new file mode 100644 index 00000000..3a7313c1 --- /dev/null +++ b/loopdb/sqlc/migrations/000021_static_loopin_risk_decision.down.sql @@ -0,0 +1,3 @@ +-- Drop confirmation-risk decision fields from static address loop-ins. +ALTER TABLE static_address_swaps DROP COLUMN confirmation_risk_decision; +ALTER TABLE static_address_swaps DROP COLUMN confirmation_risk_decision_time; diff --git a/loopdb/sqlc/migrations/000021_static_loopin_risk_decision.up.sql b/loopdb/sqlc/migrations/000021_static_loopin_risk_decision.up.sql new file mode 100644 index 00000000..7117a156 --- /dev/null +++ b/loopdb/sqlc/migrations/000021_static_loopin_risk_decision.up.sql @@ -0,0 +1,15 @@ +-- confirmation_risk_decision records the server's confirmation-risk decision +-- for a static address loop-in. Possible values are: +-- - '': no decision has been received yet; +-- - 'accepted': the server accepted waiting for the low-confirmation +-- deposits, which starts or reconstructs the payment deadline; +-- - 'rejected': the server stopped waiting for the low-confirmation deposits +-- before paying the invoice. +-- Once rejected, a later accepted update is ignored. +ALTER TABLE static_address_swaps ADD COLUMN confirmation_risk_decision TEXT NOT NULL DEFAULT ''; + +-- confirmation_risk_decision_time records when loopd received and persisted +-- the server's decision, so payment deadlines can be reconstructed after +-- restart. Same-decision replays preserve the original timestamp; changing +-- from accepted to rejected updates it. +ALTER TABLE static_address_swaps ADD COLUMN confirmation_risk_decision_time TIMESTAMP; diff --git a/loopdb/sqlc/models.go b/loopdb/sqlc/models.go index 34d8a277..78a75d04 100644 --- a/loopdb/sqlc/models.go +++ b/loopdb/sqlc/models.go @@ -137,18 +137,20 @@ type StaticAddress struct { } type StaticAddressSwap struct { - ID int32 - SwapHash []byte - SwapInvoice string - LastHop []byte - PaymentTimeoutSeconds int32 - QuotedSwapFeeSatoshis int64 - DepositOutpoints string - HtlcTxFeeRateSatKw int64 - HtlcTimeoutSweepTxID sql.NullString - HtlcTimeoutSweepAddress string - SelectedAmount int64 - Fast bool + ID int32 + SwapHash []byte + SwapInvoice string + LastHop []byte + PaymentTimeoutSeconds int32 + QuotedSwapFeeSatoshis int64 + DepositOutpoints string + HtlcTxFeeRateSatKw int64 + HtlcTimeoutSweepTxID sql.NullString + HtlcTimeoutSweepAddress string + SelectedAmount int64 + Fast bool + ConfirmationRiskDecision string + ConfirmationRiskDecisionTime sql.NullTime } type StaticAddressSwapUpdate struct { diff --git a/loopdb/sqlc/querier.go b/loopdb/sqlc/querier.go index 2d8dc137..ba3c35eb 100644 --- a/loopdb/sqlc/querier.go +++ b/loopdb/sqlc/querier.go @@ -67,6 +67,7 @@ type Querier interface { MapDepositToSwap(ctx context.Context, arg MapDepositToSwapParams) error OverrideSelectedSwapAmount(ctx context.Context, arg OverrideSelectedSwapAmountParams) error OverrideSwapCosts(ctx context.Context, arg OverrideSwapCostsParams) error + RecordStaticAddressRiskDecision(ctx context.Context, arg RecordStaticAddressRiskDecisionParams) error SwapHashForDepositID(ctx context.Context, depositID []byte) ([]byte, error) UpdateBatch(ctx context.Context, arg UpdateBatchParams) error UpdateDeposit(ctx context.Context, arg UpdateDepositParams) error diff --git a/loopdb/sqlc/queries/static_address_loopin.sql b/loopdb/sqlc/queries/static_address_loopin.sql index 7ee326a2..b4fca5d4 100644 --- a/loopdb/sqlc/queries/static_address_loopin.sql +++ b/loopdb/sqlc/queries/static_address_loopin.sql @@ -33,6 +33,22 @@ SET WHERE swap_hash = $1; +-- name: RecordStaticAddressRiskDecision :exec +UPDATE static_address_swaps +SET + confirmation_risk_decision = $2, + confirmation_risk_decision_time = CASE + WHEN confirmation_risk_decision = $2 THEN + COALESCE(confirmation_risk_decision_time, $3) + ELSE $3 + END +WHERE + swap_hash = $1 + AND NOT ( + confirmation_risk_decision = 'rejected' + AND $2 = 'accepted' + ); + -- name: InsertStaticAddressMetaUpdate :exec INSERT INTO static_address_swap_updates ( swap_hash, @@ -147,7 +163,3 @@ WHERE d.swap_hash = $1; - - - - diff --git a/loopdb/sqlc/static_address_loopin.sql.go b/loopdb/sqlc/static_address_loopin.sql.go index 87949cb3..31934016 100644 --- a/loopdb/sqlc/static_address_loopin.sql.go +++ b/loopdb/sqlc/static_address_loopin.sql.go @@ -153,7 +153,7 @@ func (q *Queries) GetLoopInSwapUpdates(ctx context.Context, swapHash []byte) ([] const getStaticAddressLoopInSwap = `-- name: GetStaticAddressLoopInSwap :one SELECT swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label, - static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, + static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time, htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index FROM swaps @@ -166,36 +166,38 @@ WHERE ` type GetStaticAddressLoopInSwapRow struct { - ID int32 - SwapHash []byte - Preimage []byte - InitiationTime time.Time - AmountRequested int64 - CltvExpiry int32 - MaxMinerFee int64 - MaxSwapFee int64 - InitiationHeight int32 - ProtocolVersion int32 - Label string - ID_2 int32 - SwapHash_2 []byte - SwapInvoice string - LastHop []byte - PaymentTimeoutSeconds int32 - QuotedSwapFeeSatoshis int64 - DepositOutpoints string - HtlcTxFeeRateSatKw int64 - HtlcTimeoutSweepTxID sql.NullString - HtlcTimeoutSweepAddress string - SelectedAmount int64 - Fast bool - SwapHash_3 []byte - SenderScriptPubkey []byte - ReceiverScriptPubkey []byte - SenderInternalPubkey []byte - ReceiverInternalPubkey []byte - ClientKeyFamily int32 - ClientKeyIndex int32 + ID int32 + SwapHash []byte + Preimage []byte + InitiationTime time.Time + AmountRequested int64 + CltvExpiry int32 + MaxMinerFee int64 + MaxSwapFee int64 + InitiationHeight int32 + ProtocolVersion int32 + Label string + ID_2 int32 + SwapHash_2 []byte + SwapInvoice string + LastHop []byte + PaymentTimeoutSeconds int32 + QuotedSwapFeeSatoshis int64 + DepositOutpoints string + HtlcTxFeeRateSatKw int64 + HtlcTimeoutSweepTxID sql.NullString + HtlcTimeoutSweepAddress string + SelectedAmount int64 + Fast bool + ConfirmationRiskDecision string + ConfirmationRiskDecisionTime sql.NullTime + SwapHash_3 []byte + SenderScriptPubkey []byte + ReceiverScriptPubkey []byte + SenderInternalPubkey []byte + ReceiverInternalPubkey []byte + ClientKeyFamily int32 + ClientKeyIndex int32 } func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byte) (GetStaticAddressLoopInSwapRow, error) { @@ -225,6 +227,8 @@ func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byt &i.HtlcTimeoutSweepAddress, &i.SelectedAmount, &i.Fast, + &i.ConfirmationRiskDecision, + &i.ConfirmationRiskDecisionTime, &i.SwapHash_3, &i.SenderScriptPubkey, &i.ReceiverScriptPubkey, @@ -239,7 +243,7 @@ func (q *Queries) GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byt const getStaticAddressLoopInSwapsByStates = `-- name: GetStaticAddressLoopInSwapsByStates :many SELECT swaps.id, swaps.swap_hash, swaps.preimage, swaps.initiation_time, swaps.amount_requested, swaps.cltv_expiry, swaps.max_miner_fee, swaps.max_swap_fee, swaps.initiation_height, swaps.protocol_version, swaps.label, - static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, + static_address_swaps.id, static_address_swaps.swap_hash, static_address_swaps.swap_invoice, static_address_swaps.last_hop, static_address_swaps.payment_timeout_seconds, static_address_swaps.quoted_swap_fee_satoshis, static_address_swaps.deposit_outpoints, static_address_swaps.htlc_tx_fee_rate_sat_kw, static_address_swaps.htlc_timeout_sweep_tx_id, static_address_swaps.htlc_timeout_sweep_address, static_address_swaps.selected_amount, static_address_swaps.fast, static_address_swaps.confirmation_risk_decision, static_address_swaps.confirmation_risk_decision_time, htlc_keys.swap_hash, htlc_keys.sender_script_pubkey, htlc_keys.receiver_script_pubkey, htlc_keys.sender_internal_pubkey, htlc_keys.receiver_internal_pubkey, htlc_keys.client_key_family, htlc_keys.client_key_index FROM swaps @@ -263,36 +267,38 @@ ORDER BY ` type GetStaticAddressLoopInSwapsByStatesRow struct { - ID int32 - SwapHash []byte - Preimage []byte - InitiationTime time.Time - AmountRequested int64 - CltvExpiry int32 - MaxMinerFee int64 - MaxSwapFee int64 - InitiationHeight int32 - ProtocolVersion int32 - Label string - ID_2 int32 - SwapHash_2 []byte - SwapInvoice string - LastHop []byte - PaymentTimeoutSeconds int32 - QuotedSwapFeeSatoshis int64 - DepositOutpoints string - HtlcTxFeeRateSatKw int64 - HtlcTimeoutSweepTxID sql.NullString - HtlcTimeoutSweepAddress string - SelectedAmount int64 - Fast bool - SwapHash_3 []byte - SenderScriptPubkey []byte - ReceiverScriptPubkey []byte - SenderInternalPubkey []byte - ReceiverInternalPubkey []byte - ClientKeyFamily int32 - ClientKeyIndex int32 + ID int32 + SwapHash []byte + Preimage []byte + InitiationTime time.Time + AmountRequested int64 + CltvExpiry int32 + MaxMinerFee int64 + MaxSwapFee int64 + InitiationHeight int32 + ProtocolVersion int32 + Label string + ID_2 int32 + SwapHash_2 []byte + SwapInvoice string + LastHop []byte + PaymentTimeoutSeconds int32 + QuotedSwapFeeSatoshis int64 + DepositOutpoints string + HtlcTxFeeRateSatKw int64 + HtlcTimeoutSweepTxID sql.NullString + HtlcTimeoutSweepAddress string + SelectedAmount int64 + Fast bool + ConfirmationRiskDecision string + ConfirmationRiskDecisionTime sql.NullTime + SwapHash_3 []byte + SenderScriptPubkey []byte + ReceiverScriptPubkey []byte + SenderInternalPubkey []byte + ReceiverInternalPubkey []byte + ClientKeyFamily int32 + ClientKeyIndex int32 } func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dollar_1 sql.NullString) ([]GetStaticAddressLoopInSwapsByStatesRow, error) { @@ -328,6 +334,8 @@ func (q *Queries) GetStaticAddressLoopInSwapsByStates(ctx context.Context, dolla &i.HtlcTimeoutSweepAddress, &i.SelectedAmount, &i.Fast, + &i.ConfirmationRiskDecision, + &i.ConfirmationRiskDecisionTime, &i.SwapHash_3, &i.SenderScriptPubkey, &i.ReceiverScriptPubkey, @@ -482,6 +490,34 @@ func (q *Queries) OverrideSelectedSwapAmount(ctx context.Context, arg OverrideSe return err } +const recordStaticAddressRiskDecision = `-- name: RecordStaticAddressRiskDecision :exec +UPDATE static_address_swaps +SET + confirmation_risk_decision = $2, + confirmation_risk_decision_time = CASE + WHEN confirmation_risk_decision = $2 THEN + COALESCE(confirmation_risk_decision_time, $3) + ELSE $3 + END +WHERE + swap_hash = $1 + AND NOT ( + confirmation_risk_decision = 'rejected' + AND $2 = 'accepted' + ) +` + +type RecordStaticAddressRiskDecisionParams struct { + SwapHash []byte + ConfirmationRiskDecision string + ConfirmationRiskDecisionTime sql.NullTime +} + +func (q *Queries) RecordStaticAddressRiskDecision(ctx context.Context, arg RecordStaticAddressRiskDecisionParams) error { + _, err := q.db.ExecContext(ctx, recordStaticAddressRiskDecision, arg.SwapHash, arg.ConfirmationRiskDecision, arg.ConfirmationRiskDecisionTime) + return err +} + const swapHashForDepositID = `-- name: SwapHashForDepositID :one SELECT swap_hash diff --git a/loopdb/sqlerrors.go b/loopdb/sqlerrors.go index 696323bf..be5e0870 100644 --- a/loopdb/sqlerrors.go +++ b/loopdb/sqlerrors.go @@ -4,8 +4,8 @@ import ( "errors" "fmt" - "github.com/jackc/pgconn" "github.com/jackc/pgerrcode" + "github.com/jackc/pgx/v5/pgconn" "modernc.org/sqlite" sqlite3 "modernc.org/sqlite/lib" ) diff --git a/loopdb/store.go b/loopdb/store.go index fe71c19c..1fec57eb 100644 --- a/loopdb/store.go +++ b/loopdb/store.go @@ -15,6 +15,7 @@ import ( "github.com/btcsuite/btcd/chaincfg/chainhash" "github.com/lightningnetwork/lnd/lntypes" "go.etcd.io/bbolt" + bbolterrors "go.etcd.io/bbolt/errors" ) var ( @@ -197,9 +198,9 @@ func NewBoltSwapStore(dbPath string, chainParams *chaincfg.Params) ( bdb, err := bboltOpen(path, 0600, &bbolt.Options{ Timeout: DefaultLoopDBTimeout, }) - if errors.Is(err, bbolt.ErrTimeout) { + if errors.Is(err, bbolterrors.ErrTimeout) { return nil, fmt.Errorf("%w: couldn't obtain exclusive lock on "+ - "%s, timed out after %v", bbolt.ErrTimeout, path, + "%s, timed out after %v", bbolterrors.ErrTimeout, path, DefaultLoopDBTimeout) } if err != nil { diff --git a/loopdb/store_test.go b/loopdb/store_test.go index 4a61062b..ec9feb2a 100644 --- a/loopdb/store_test.go +++ b/loopdb/store_test.go @@ -4,7 +4,6 @@ import ( "context" "crypto/sha256" "fmt" - "io/ioutil" "os" "path/filepath" "testing" @@ -18,6 +17,7 @@ import ( "github.com/lightningnetwork/lnd/routing/route" "github.com/stretchr/testify/require" "go.etcd.io/bbolt" + bbolterrors "go.etcd.io/bbolt/errors" ) var ( @@ -62,7 +62,7 @@ func TestNewBoltSwapStoreTimeout(t *testing.T) { bboltOpen = origOpen }) - wrappedErr := fmt.Errorf("wrapped: %w", bbolt.ErrTimeout) + wrappedErr := fmt.Errorf("wrapped: %w", bbolterrors.ErrTimeout) bboltOpen = func(path string, mode os.FileMode, options *bbolt.Options) (*bbolt.DB, error) { @@ -74,7 +74,7 @@ func TestNewBoltSwapStoreTimeout(t *testing.T) { store, err := NewBoltSwapStore(tempDir, &chaincfg.MainNetParams) require.Nil(t, store) - require.ErrorIs(t, err, bbolt.ErrTimeout) + require.ErrorIs(t, err, bbolterrors.ErrTimeout) require.ErrorContains(t, err, "couldn't obtain exclusive lock") } @@ -142,10 +142,7 @@ func TestLoopOutStore(t *testing.T) { // testLoopOutStore tests the basic functionality of the current bbolt // swap store for specific swap parameters. func testLoopOutStore(t *testing.T, pendingSwap *LoopOutContract) { - tempDirName, err := ioutil.TempDir("", "clientstore") - require.NoError(t, err) - - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams) require.NoError(t, err) @@ -284,9 +281,7 @@ func TestLoopInStore(t *testing.T) { } func testLoopInStore(t *testing.T, pendingSwap LoopInContract) { - tempDirName, err := ioutil.TempDir("", "clientstore") - require.NoError(t, err) - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams) require.NoError(t, err) @@ -366,11 +361,7 @@ func testLoopInStore(t *testing.T, pendingSwap LoopInContract) { // TestVersionNew tests that a new database is initialized with the current // version. func TestVersionNew(t *testing.T) { - tempDirName, err := ioutil.TempDir("", "clientstore") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() store, err := NewBoltSwapStore(tempDirName, &chaincfg.MainNetParams) if err != nil { @@ -390,11 +381,7 @@ func TestVersionNew(t *testing.T) { // TestVersionMigrated tests that an existing version zero database is migrated // to the latest version. func TestVersionMigrated(t *testing.T) { - tempDirName, err := ioutil.TempDir("", "clientstore") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() createVersionZeroDb(t, tempDirName) @@ -459,11 +446,7 @@ func TestLegacyOutgoingChannel(t *testing.T) { } // Restore a legacy database. - tempDirName, err := ioutil.TempDir("", "clientstore") - if err != nil { - t.Fatal(err) - } - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() tempPath := filepath.Join(tempDirName, dbFileName) db, err := bbolt.Open(tempPath, 0600, nil) @@ -498,9 +481,7 @@ func TestLegacyOutgoingChannel(t *testing.T) { // TestLiquidityParams checks that reading and writing to liquidty bucket are // as expected. func TestLiquidityParams(t *testing.T) { - tempDirName, err := ioutil.TempDir("", "clientstore") - require.NoError(t, err, "failed to db") - defer os.RemoveAll(tempDirName) + tempDirName := t.TempDir() ctxb := context.Background() diff --git a/loopin.go b/loopin.go index ae6802c8..c047aefc 100644 --- a/loopin.go +++ b/loopin.go @@ -27,6 +27,8 @@ import ( "github.com/lightningnetwork/lnd/lnwallet/chainfee" "github.com/lightningnetwork/lnd/lnwire" "github.com/lightningnetwork/lnd/routing/route" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var ( @@ -58,6 +60,25 @@ var ( ErrSwapFinalized = errors.New("swap is in a final state") ) +// isInvoiceAlreadySettledError reports whether err indicates that an invoice +// cancellation failed because the invoice was already settled. If lnd returns +// the sentinel from an RPC handler, gRPC transports it as an Unknown status +// with the sentinel's error text. +func isInvoiceAlreadySettledError(err error) bool { + if err == nil { + return false + } + + if errors.Is(err, invpkg.ErrInvoiceAlreadySettled) { + return true + } + + rpcStatus, ok := status.FromError(err) + return ok && + rpcStatus.Code() == codes.Unknown && + rpcStatus.Message() == invpkg.ErrInvoiceAlreadySettled.Error() +} + // loopInSwap contains all the in-memory state related to a pending loop in // swap. type loopInSwap struct { @@ -1087,7 +1108,7 @@ func (s *loopInSwap) processHtlcSpend(ctx context.Context, // already settled. This means that the server didn't succeed in // sweeping the htlc after paying the invoice. err := s.lnd.Invoices.CancelInvoice(ctx, s.hash) - if err != nil && err != invpkg.ErrInvoiceAlreadySettled { + if err != nil && !isInvoiceAlreadySettledError(err) { return err } } @@ -1174,13 +1195,17 @@ func (s *loopInSwap) setStateAbandoned(ctx context.Context) error { return err } - // If the invoice is already settled or canceled, this is a nop. - _ = s.lnd.Invoices.CancelInvoice(ctx, s.hash) + // Cancel the invoice so the server can no longer settle it. If the + // invoice is already settled we ignore the error, matching the + // behaviour of the timeout path. Any other unexpected error is logged + // but does not prevent the abandon from completing. + err = s.lnd.Invoices.CancelInvoice(ctx, s.hash) + if err != nil && !isInvoiceAlreadySettledError(err) { + s.log.Warnf("Failed to cancel invoice for abandoned swap: %v", + err) + } - return fmt.Errorf("swap hash "+ - "abandoned by client, "+ - "swap ID: %v, %v", - s.hash, err) + return fmt.Errorf("swap hash abandoned by client, swap ID: %v", s.hash) } // persistAndAnnounceState updates the swap state on disk and sends out an diff --git a/loopin_test.go b/loopin_test.go index a1a09584..93d019da 100644 --- a/loopin_test.go +++ b/loopin_test.go @@ -20,6 +20,8 @@ import ( "github.com/lightningnetwork/lnd/routing/route" "github.com/lightningnetwork/lnd/zpay32" "github.com/stretchr/testify/require" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" ) var ( @@ -44,6 +46,142 @@ type probeInvoicesMock struct { cancelCtxErr chan error } +// cancelErrorInvoicesMock is an InvoicesClient that returns a configured +// cancellation error. +type cancelErrorInvoicesMock struct { + lndclient.InvoicesClient + + err error +} + +// CancelInvoice returns the cancellation error configured on the mock. +func (c *cancelErrorInvoicesMock) CancelInvoice(context.Context, + lntypes.Hash) error { + + return c.err +} + +// TestIsInvoiceAlreadySettledError verifies the local and gRPC error forms +// recognized by the already-settled classifier. +func TestIsInvoiceAlreadySettledError(t *testing.T) { + t.Parallel() + + testCases := []struct { + name string + err error + expected bool + }{ + { + name: "sentinel", + err: invpkg.ErrInvoiceAlreadySettled, + expected: true, + }, + { + name: "wrapped sentinel", + err: fmt.Errorf( + "cancel invoice: %w", + invpkg.ErrInvoiceAlreadySettled, + ), + expected: true, + }, + { + name: "grpc representation", + err: status.Error( + codes.Unknown, + invpkg.ErrInvoiceAlreadySettled.Error(), + ), + expected: true, + }, + { + name: "different grpc status", + err: status.Error( + codes.FailedPrecondition, + invpkg.ErrInvoiceAlreadySettled.Error(), + ), + }, + { + name: "different error", + err: fmt.Errorf("cancel invoice failed"), + }, + { + name: "nil", + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + require.Equal( + t, testCase.expected, + isInvoiceAlreadySettledError(testCase.err), + ) + }) + } +} + +// TestProcessHtlcSpendIgnoresGRPCAlreadySettled verifies that the timeout path +// completes normally when invoice cancellation reports an already-settled +// invoice. +func TestProcessHtlcSpendIgnoresGRPCAlreadySettled(t *testing.T) { + defer test.Guard(t)() + + // Initialize a loop-in so the test uses the same contract and HTLC + // scripts as the production flow. + testCtx := newLoopInTestContext(t) + cfg := newSwapConfig( + &testCtx.lnd.LndServices, testCtx.store, testCtx.server, nil, + clock.NewTestClock(time.Unix(123, 0)), + ) + + initResult, err := newLoopInSwap( + context.Background(), cfg, 600, &testLoopInRequest, + ) + require.NoError(t, err) + testCtx.store.AssertLoopInStored() + + // Return the gRPC status observed when lnd transports its + // ErrInvoiceAlreadySettled sentinel across the RPC boundary. + cfg.lnd.Invoices = &cancelErrorInvoicesMock{ + err: status.Error( + codes.Unknown, + invpkg.ErrInvoiceAlreadySettled.Error(), + ), + } + + // Confirmation processing normally selects the HTLC version that was + // found on chain. Select the initialized version explicitly because this + // test calls processHtlcSpend directly. + if initResult.swap.htlcP2TR != nil { + initResult.swap.htlc = initResult.swap.htlcP2TR + } else { + initResult.swap.htlc = initResult.swap.htlcP2WSH + } + require.NotNil(t, initResult.swap.htlc) + + // Construct a timeout spend so processHtlcSpend takes the invoice + // cancellation branch. + timeoutWitness, err := initResult.swap.htlc.GenTimeoutWitness([]byte{1}) + require.NoError(t, err) + + timeoutTx := &wire.MsgTx{ + TxIn: []*wire.TxIn{ + { + Witness: timeoutWitness, + }, + }, + } + + // The already-settled status must be suppressed while the swap still + // transitions to its terminal timeout state. + err = initResult.swap.processHtlcSpend( + context.Background(), &chainntnfs.SpendDetail{ + SpendingTx: timeoutTx, + SpenderInputIndex: 0, + }, 100, + ) + require.NoError(t, err) + require.Equal(t, loopdb.StateFailTimeout, initResult.swap.state) +} + // SubscribeSingleInvoice returns the mock's preconfigured channels. func (p *probeInvoicesMock) SubscribeSingleInvoice(_ context.Context, _ lntypes.Hash) (<-chan lndclient.InvoiceUpdate, <-chan error, error) { @@ -127,9 +265,8 @@ func TestLoopInSuccess(t *testing.T) { } // TestLoopInSwapInvoiceRouteHintsMatchProbe asserts that explicit route hints -// are preserved on both loop-in invoices. The probe invoice already keeps the -// requested hints, while the swap invoice currently loses them via the -// lndclient AddInvoice wrapper. +// are preserved on both loop-in invoices so the swap invoice matches the probe +// invoice. func TestLoopInSwapInvoiceRouteHintsMatchProbe(t *testing.T) { t.Parallel() @@ -655,12 +792,6 @@ func testLoopInResume(t *testing.T, state loopdb.SwapState, expired bool, defer func() { require.NoError(t, <-errChan) - select { - case <-ctx.lnd.SendPaymentChannel: - t.Fatal("unexpected payment sent") - default: - } - select { case <-ctx.lnd.SendOutputsChannel: t.Fatal("unexpected tx published") diff --git a/looprpc/client.pb.go b/looprpc/client.pb.go index 3305bfcd..04b35f1e 100644 --- a/looprpc/client.pb.go +++ b/looprpc/client.pb.go @@ -80,6 +80,8 @@ const ( SwapType_LOOP_OUT SwapType = 0 // LOOP_IN indicates a loop in swap (on-chain to off-chain) SwapType_LOOP_IN SwapType = 1 + // STATIC_LOOP_IN indicates a static address loop in swap. + SwapType_STATIC_LOOP_IN SwapType = 2 ) // Enum value maps for SwapType. @@ -87,10 +89,12 @@ var ( SwapType_name = map[int32]string{ 0: "LOOP_OUT", 1: "LOOP_IN", + 2: "STATIC_LOOP_IN", } SwapType_value = map[string]int32{ - "LOOP_OUT": 0, - "LOOP_IN": 1, + "LOOP_OUT": 0, + "LOOP_IN": 1, + "STATIC_LOOP_IN": 2, } ) @@ -1474,8 +1478,12 @@ type SwapStatus struct { IdBytes []byte `protobuf:"bytes,11,opt,name=id_bytes,json=idBytes,proto3" json:"id_bytes,omitempty"` // The type of the swap. Type SwapType `protobuf:"varint,3,opt,name=type,proto3,enum=looprpc.SwapType" json:"type,omitempty"` - // State the swap is currently in, see State enum. + // Generic loop-in/loop-out state for swaps. State SwapState `protobuf:"varint,4,opt,name=state,proto3,enum=looprpc.SwapState" json:"state,omitempty"` + // Types that are valid to be assigned to StaticLoopInStateOptional: + // + // *SwapStatus_StaticLoopInState + StaticLoopInStateOptional isSwapStatus_StaticLoopInStateOptional `protobuf_oneof:"static_loop_in_state_optional"` // A failure reason for the swap, only set if the swap has failed. FailureReason FailureReason `protobuf:"varint,14,opt,name=failure_reason,json=failureReason,proto3,enum=looprpc.FailureReason" json:"failure_reason,omitempty"` // Initiation time of the swap. @@ -1578,6 +1586,22 @@ func (x *SwapStatus) GetState() SwapState { return SwapState_INITIATED } +func (x *SwapStatus) GetStaticLoopInStateOptional() isSwapStatus_StaticLoopInStateOptional { + if x != nil { + return x.StaticLoopInStateOptional + } + return nil +} + +func (x *SwapStatus) GetStaticLoopInState() StaticAddressLoopInSwapState { + if x != nil { + if x, ok := x.StaticLoopInStateOptional.(*SwapStatus_StaticLoopInState); ok { + return x.StaticLoopInState + } + } + return StaticAddressLoopInSwapState_UNKNOWN_STATIC_ADDRESS_SWAP_STATE +} + func (x *SwapStatus) GetFailureReason() FailureReason { if x != nil { return x.FailureReason @@ -1670,6 +1694,17 @@ func (x *SwapStatus) GetAssetInfo() *AssetLoopOutInfo { return nil } +type isSwapStatus_StaticLoopInStateOptional interface { + isSwapStatus_StaticLoopInStateOptional() +} + +type SwapStatus_StaticLoopInState struct { + // Static address loop-in FSM state when type is STATIC_LOOP_IN. + StaticLoopInState StaticAddressLoopInSwapState `protobuf:"varint,20,opt,name=static_loop_in_state,json=staticLoopInState,proto3,enum=looprpc.StaticAddressLoopInSwapState,oneof"` +} + +func (*SwapStatus_StaticLoopInState) isSwapStatus_StaticLoopInStateOptional() {} + type ListSwapsRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // Optional filter to only return swaps that match the filter. @@ -4432,7 +4467,11 @@ type InstantOutRequest struct { OutgoingChanSet []uint64 `protobuf:"varint,2,rep,packed,name=outgoing_chan_set,json=outgoingChanSet,proto3" json:"outgoing_chan_set,omitempty"` // An optional address to sweep the onchain funds to. If not set, the funds // will be swept to the wallet's internal address. - DestAddr string `protobuf:"bytes,3,opt,name=dest_addr,json=destAddr,proto3" json:"dest_addr,omitempty"` + DestAddr string `protobuf:"bytes,3,opt,name=dest_addr,json=destAddr,proto3" json:"dest_addr,omitempty"` + // Types that are valid to be assigned to MaxSwapFee: + // + // *InstantOutRequest_MaxSwapFeeSat + MaxSwapFee isInstantOutRequest_MaxSwapFee `protobuf_oneof:"max_swap_fee"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } @@ -4488,6 +4527,36 @@ func (x *InstantOutRequest) GetDestAddr() string { return "" } +func (x *InstantOutRequest) GetMaxSwapFee() isInstantOutRequest_MaxSwapFee { + if x != nil { + return x.MaxSwapFee + } + return nil +} + +func (x *InstantOutRequest) GetMaxSwapFeeSat() int64 { + if x != nil { + if x, ok := x.MaxSwapFee.(*InstantOutRequest_MaxSwapFeeSat); ok { + return x.MaxSwapFeeSat + } + } + return 0 +} + +type isInstantOutRequest_MaxSwapFee interface { + isInstantOutRequest_MaxSwapFee() +} + +type InstantOutRequest_MaxSwapFeeSat struct { + // The maximum off-chain swap fee that may be charged for the swap. If + // this field is omitted, no fee cap is applied for compatibility with + // clients that predate this field. An explicitly set value of zero + // rejects any positive swap fee. + MaxSwapFeeSat int64 `protobuf:"varint,4,opt,name=max_swap_fee_sat,json=maxSwapFeeSat,proto3,oneof"` +} + +func (*InstantOutRequest_MaxSwapFeeSat) isInstantOutRequest_MaxSwapFee() {} + type InstantOutResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The hash of the swap preimage. @@ -6704,14 +6773,15 @@ const file_client_proto_rawDesc = "" + "\x12htlc_address_p2wsh\x18\x05 \x01(\tR\x10htlcAddressP2wsh\x12*\n" + "\x11htlc_address_p2tr\x18\a \x01(\tR\x0fhtlcAddressP2tr\x12%\n" + "\x0eserver_message\x18\x06 \x01(\tR\rserverMessageJ\x04\b\x04\x10\x05\"\x10\n" + - "\x0eMonitorRequest\"\xb1\x05\n" + + "\x0eMonitorRequest\"\xac\x06\n" + "\n" + "SwapStatus\x12\x10\n" + "\x03amt\x18\x01 \x01(\x03R\x03amt\x12\x12\n" + "\x02id\x18\x02 \x01(\tB\x02\x18\x01R\x02id\x12\x19\n" + "\bid_bytes\x18\v \x01(\fR\aidBytes\x12%\n" + "\x04type\x18\x03 \x01(\x0e2\x11.looprpc.SwapTypeR\x04type\x12(\n" + - "\x05state\x18\x04 \x01(\x0e2\x12.looprpc.SwapStateR\x05state\x12=\n" + + "\x05state\x18\x04 \x01(\x0e2\x12.looprpc.SwapStateR\x05state\x12X\n" + + "\x14static_loop_in_state\x18\x14 \x01(\x0e2%.looprpc.StaticAddressLoopInSwapStateH\x00R\x11staticLoopInState\x12=\n" + "\x0efailure_reason\x18\x0e \x01(\x0e2\x16.looprpc.FailureReasonR\rfailureReason\x12'\n" + "\x0finitiation_time\x18\x05 \x01(\x03R\x0einitiationTime\x12(\n" + "\x10last_update_time\x18\x06 \x01(\x03R\x0elastUpdateTime\x12%\n" + @@ -6727,7 +6797,8 @@ const file_client_proto_rawDesc = "" + "\x11outgoing_chan_set\x18\x11 \x03(\x04R\x0foutgoingChanSet\x12\x14\n" + "\x05label\x18\x0f \x01(\tR\x05label\x128\n" + "\n" + - "asset_info\x18\x13 \x01(\v2\x19.looprpc.AssetLoopOutInfoR\tassetInfo\"s\n" + + "asset_info\x18\x13 \x01(\v2\x19.looprpc.AssetLoopOutInfoR\tassetInfoB\x1f\n" + + "\x1dstatic_loop_in_state_optional\"s\n" + "\x10ListSwapsRequest\x12B\n" + "\x10list_swap_filter\x18\x01 \x01(\v2\x18.looprpc.ListSwapsFilterR\x0elistSwapFilter\x12\x1b\n" + "\tmax_swaps\x18\x02 \x01(\x04R\bmaxSwaps\"\xf1\x02\n" + @@ -6927,11 +6998,13 @@ const file_client_proto_rawDesc = "" + "\x06amount\x18\x03 \x01(\x04R\x06amount\x12\x13\n" + "\x05tx_id\x18\x04 \x01(\tR\x04txId\x12\x12\n" + "\x04vout\x18\x05 \x01(\rR\x04vout\x12\x16\n" + - "\x06expiry\x18\x06 \x01(\rR\x06expiry\"\x85\x01\n" + + "\x06expiry\x18\x06 \x01(\rR\x06expiry\"\xc0\x01\n" + "\x11InstantOutRequest\x12'\n" + "\x0freservation_ids\x18\x01 \x03(\fR\x0ereservationIds\x12*\n" + "\x11outgoing_chan_set\x18\x02 \x03(\x04R\x0foutgoingChanSet\x12\x1b\n" + - "\tdest_addr\x18\x03 \x01(\tR\bdestAddr\"t\n" + + "\tdest_addr\x18\x03 \x01(\tR\bdestAddr\x12)\n" + + "\x10max_swap_fee_sat\x18\x04 \x01(\x03H\x00R\rmaxSwapFeeSatB\x0e\n" + + "\fmax_swap_fee\"t\n" + "\x12InstantOutResponse\x12(\n" + "\x10instant_out_hash\x18\x01 \x01(\fR\x0einstantOutHash\x12\x1e\n" + "\vsweep_tx_id\x18\x02 \x01(\tR\tsweepTxId\x12\x14\n" + @@ -7088,10 +7161,11 @@ const file_client_proto_rawDesc = "" + "\x13asset_cost_offchain\x18\x03 \x01(\x04R\x11assetCostOffchain*;\n" + "\vAddressType\x12\x18\n" + "\x14ADDRESS_TYPE_UNKNOWN\x10\x00\x12\x12\n" + - "\x0eTAPROOT_PUBKEY\x10\x01*%\n" + + "\x0eTAPROOT_PUBKEY\x10\x01*9\n" + "\bSwapType\x12\f\n" + "\bLOOP_OUT\x10\x00\x12\v\n" + - "\aLOOP_IN\x10\x01*s\n" + + "\aLOOP_IN\x10\x01\x12\x12\n" + + "\x0eSTATIC_LOOP_IN\x10\x02*s\n" + "\tSwapState\x12\r\n" + "\tINITIATED\x10\x00\x12\x15\n" + "\x11PREIMAGE_REVEALED\x10\x01\x12\x12\n" + @@ -7321,121 +7395,122 @@ var file_client_proto_depIdxs = []int32{ 91, // 4: looprpc.LoopInRequest.route_hints:type_name -> looprpc.RouteHint 1, // 5: looprpc.SwapStatus.type:type_name -> looprpc.SwapType 2, // 6: looprpc.SwapStatus.state:type_name -> looprpc.SwapState - 3, // 7: looprpc.SwapStatus.failure_reason:type_name -> looprpc.FailureReason - 88, // 8: looprpc.SwapStatus.asset_info:type_name -> looprpc.AssetLoopOutInfo - 20, // 9: looprpc.ListSwapsRequest.list_swap_filter:type_name -> looprpc.ListSwapsFilter - 9, // 10: looprpc.ListSwapsFilter.swap_type:type_name -> looprpc.ListSwapsFilter.SwapTypeFilter - 18, // 11: looprpc.ListSwapsResponse.swaps:type_name -> looprpc.SwapStatus - 24, // 12: looprpc.SweepHtlcResponse.not_requested:type_name -> looprpc.PublishNotRequested - 25, // 13: looprpc.SweepHtlcResponse.published:type_name -> looprpc.PublishSucceeded - 26, // 14: looprpc.SweepHtlcResponse.failed:type_name -> looprpc.PublishFailed - 91, // 15: looprpc.QuoteRequest.loop_in_route_hints:type_name -> looprpc.RouteHint - 85, // 16: looprpc.QuoteRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest - 86, // 17: looprpc.OutQuoteResponse.asset_rfq_info:type_name -> looprpc.AssetRfqInfo - 91, // 18: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint - 40, // 19: looprpc.TokensResponse.tokens:type_name -> looprpc.L402Token - 41, // 20: looprpc.GetInfoResponse.loop_out_stats:type_name -> looprpc.LoopStats - 41, // 21: looprpc.GetInfoResponse.loop_in_stats:type_name -> looprpc.LoopStats - 47, // 22: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule - 0, // 23: looprpc.LiquidityParameters.account_addr_type:type_name -> looprpc.AddressType - 89, // 24: looprpc.LiquidityParameters.easy_asset_params:type_name -> looprpc.LiquidityParameters.EasyAssetParamsEntry - 4, // 25: looprpc.LiquidityParameters.loop_in_source:type_name -> looprpc.LoopInSource - 1, // 26: looprpc.LiquidityRule.swap_type:type_name -> looprpc.SwapType - 5, // 27: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType - 45, // 28: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters - 6, // 29: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason - 14, // 30: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest - 15, // 31: looprpc.SuggestSwapsResponse.loop_in:type_name -> looprpc.LoopInRequest - 83, // 32: looprpc.SuggestSwapsResponse.static_loop_in:type_name -> looprpc.StaticAddressLoopInRequest - 51, // 33: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified - 57, // 34: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation - 64, // 35: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut - 69, // 36: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo - 92, // 37: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint - 7, // 38: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState - 80, // 39: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit - 81, // 40: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal - 82, // 41: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap - 7, // 42: looprpc.Deposit.state:type_name -> looprpc.DepositState - 80, // 43: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit - 8, // 44: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState - 80, // 45: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit - 91, // 46: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint - 80, // 47: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit - 87, // 48: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint - 87, // 49: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint - 46, // 50: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams - 14, // 51: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest - 15, // 52: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest - 17, // 53: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest - 19, // 54: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest - 22, // 55: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest - 27, // 56: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest - 53, // 57: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest - 28, // 58: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest - 31, // 59: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest - 28, // 60: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest - 31, // 61: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest - 34, // 62: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest - 36, // 63: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest - 36, // 64: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest - 38, // 65: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest - 42, // 66: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest - 12, // 67: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest - 44, // 68: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest - 48, // 69: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest - 50, // 70: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest - 55, // 71: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest - 58, // 72: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest - 60, // 73: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest - 62, // 74: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest - 65, // 75: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest - 67, // 76: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest - 70, // 77: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest - 72, // 78: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest - 74, // 79: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest - 76, // 80: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest - 78, // 81: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest - 83, // 82: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest - 10, // 83: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest - 16, // 84: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse - 16, // 85: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse - 18, // 86: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus - 21, // 87: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse - 23, // 88: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse - 18, // 89: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus - 54, // 90: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse - 30, // 91: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse - 33, // 92: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse - 29, // 93: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse - 32, // 94: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse - 35, // 95: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse - 37, // 96: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse - 37, // 97: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse - 39, // 98: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse - 43, // 99: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse - 13, // 100: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse - 45, // 101: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters - 49, // 102: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse - 52, // 103: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse - 56, // 104: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse - 59, // 105: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse - 61, // 106: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse - 63, // 107: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse - 66, // 108: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse - 68, // 109: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse - 71, // 110: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse - 73, // 111: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse - 75, // 112: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse - 77, // 113: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse - 79, // 114: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse - 84, // 115: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse - 11, // 116: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse - 84, // [84:117] is the sub-list for method output_type - 51, // [51:84] is the sub-list for method input_type - 51, // [51:51] is the sub-list for extension type_name - 51, // [51:51] is the sub-list for extension extendee - 0, // [0:51] is the sub-list for field type_name + 8, // 7: looprpc.SwapStatus.static_loop_in_state:type_name -> looprpc.StaticAddressLoopInSwapState + 3, // 8: looprpc.SwapStatus.failure_reason:type_name -> looprpc.FailureReason + 88, // 9: looprpc.SwapStatus.asset_info:type_name -> looprpc.AssetLoopOutInfo + 20, // 10: looprpc.ListSwapsRequest.list_swap_filter:type_name -> looprpc.ListSwapsFilter + 9, // 11: looprpc.ListSwapsFilter.swap_type:type_name -> looprpc.ListSwapsFilter.SwapTypeFilter + 18, // 12: looprpc.ListSwapsResponse.swaps:type_name -> looprpc.SwapStatus + 24, // 13: looprpc.SweepHtlcResponse.not_requested:type_name -> looprpc.PublishNotRequested + 25, // 14: looprpc.SweepHtlcResponse.published:type_name -> looprpc.PublishSucceeded + 26, // 15: looprpc.SweepHtlcResponse.failed:type_name -> looprpc.PublishFailed + 91, // 16: looprpc.QuoteRequest.loop_in_route_hints:type_name -> looprpc.RouteHint + 85, // 17: looprpc.QuoteRequest.asset_info:type_name -> looprpc.AssetLoopOutRequest + 86, // 18: looprpc.OutQuoteResponse.asset_rfq_info:type_name -> looprpc.AssetRfqInfo + 91, // 19: looprpc.ProbeRequest.route_hints:type_name -> looprpc.RouteHint + 40, // 20: looprpc.TokensResponse.tokens:type_name -> looprpc.L402Token + 41, // 21: looprpc.GetInfoResponse.loop_out_stats:type_name -> looprpc.LoopStats + 41, // 22: looprpc.GetInfoResponse.loop_in_stats:type_name -> looprpc.LoopStats + 47, // 23: looprpc.LiquidityParameters.rules:type_name -> looprpc.LiquidityRule + 0, // 24: looprpc.LiquidityParameters.account_addr_type:type_name -> looprpc.AddressType + 89, // 25: looprpc.LiquidityParameters.easy_asset_params:type_name -> looprpc.LiquidityParameters.EasyAssetParamsEntry + 4, // 26: looprpc.LiquidityParameters.loop_in_source:type_name -> looprpc.LoopInSource + 1, // 27: looprpc.LiquidityRule.swap_type:type_name -> looprpc.SwapType + 5, // 28: looprpc.LiquidityRule.type:type_name -> looprpc.LiquidityRuleType + 45, // 29: looprpc.SetLiquidityParamsRequest.parameters:type_name -> looprpc.LiquidityParameters + 6, // 30: looprpc.Disqualified.reason:type_name -> looprpc.AutoReason + 14, // 31: looprpc.SuggestSwapsResponse.loop_out:type_name -> looprpc.LoopOutRequest + 15, // 32: looprpc.SuggestSwapsResponse.loop_in:type_name -> looprpc.LoopInRequest + 83, // 33: looprpc.SuggestSwapsResponse.static_loop_in:type_name -> looprpc.StaticAddressLoopInRequest + 51, // 34: looprpc.SuggestSwapsResponse.disqualified:type_name -> looprpc.Disqualified + 57, // 35: looprpc.ListReservationsResponse.reservations:type_name -> looprpc.ClientReservation + 64, // 36: looprpc.ListInstantOutsResponse.swaps:type_name -> looprpc.InstantOut + 69, // 37: looprpc.ListUnspentDepositsResponse.utxos:type_name -> looprpc.Utxo + 92, // 38: looprpc.WithdrawDepositsRequest.outpoints:type_name -> lnrpc.OutPoint + 7, // 39: looprpc.ListStaticAddressDepositsRequest.state_filter:type_name -> looprpc.DepositState + 80, // 40: looprpc.ListStaticAddressDepositsResponse.filtered_deposits:type_name -> looprpc.Deposit + 81, // 41: looprpc.ListStaticAddressWithdrawalResponse.withdrawals:type_name -> looprpc.StaticAddressWithdrawal + 82, // 42: looprpc.ListStaticAddressSwapsResponse.swaps:type_name -> looprpc.StaticAddressLoopInSwap + 7, // 43: looprpc.Deposit.state:type_name -> looprpc.DepositState + 80, // 44: looprpc.StaticAddressWithdrawal.deposits:type_name -> looprpc.Deposit + 8, // 45: looprpc.StaticAddressLoopInSwap.state:type_name -> looprpc.StaticAddressLoopInSwapState + 80, // 46: looprpc.StaticAddressLoopInSwap.deposits:type_name -> looprpc.Deposit + 91, // 47: looprpc.StaticAddressLoopInRequest.route_hints:type_name -> looprpc.RouteHint + 80, // 48: looprpc.StaticAddressLoopInResponse.used_deposits:type_name -> looprpc.Deposit + 87, // 49: looprpc.AssetRfqInfo.prepay_asset_rate:type_name -> looprpc.FixedPoint + 87, // 50: looprpc.AssetRfqInfo.swap_asset_rate:type_name -> looprpc.FixedPoint + 46, // 51: looprpc.LiquidityParameters.EasyAssetParamsEntry.value:type_name -> looprpc.EasyAssetAutoloopParams + 14, // 52: looprpc.SwapClient.LoopOut:input_type -> looprpc.LoopOutRequest + 15, // 53: looprpc.SwapClient.LoopIn:input_type -> looprpc.LoopInRequest + 17, // 54: looprpc.SwapClient.Monitor:input_type -> looprpc.MonitorRequest + 19, // 55: looprpc.SwapClient.ListSwaps:input_type -> looprpc.ListSwapsRequest + 22, // 56: looprpc.SwapClient.SweepHtlc:input_type -> looprpc.SweepHtlcRequest + 27, // 57: looprpc.SwapClient.SwapInfo:input_type -> looprpc.SwapInfoRequest + 53, // 58: looprpc.SwapClient.AbandonSwap:input_type -> looprpc.AbandonSwapRequest + 28, // 59: looprpc.SwapClient.LoopOutTerms:input_type -> looprpc.TermsRequest + 31, // 60: looprpc.SwapClient.LoopOutQuote:input_type -> looprpc.QuoteRequest + 28, // 61: looprpc.SwapClient.GetLoopInTerms:input_type -> looprpc.TermsRequest + 31, // 62: looprpc.SwapClient.GetLoopInQuote:input_type -> looprpc.QuoteRequest + 34, // 63: looprpc.SwapClient.Probe:input_type -> looprpc.ProbeRequest + 36, // 64: looprpc.SwapClient.GetL402Tokens:input_type -> looprpc.TokensRequest + 36, // 65: looprpc.SwapClient.GetLsatTokens:input_type -> looprpc.TokensRequest + 38, // 66: looprpc.SwapClient.FetchL402Token:input_type -> looprpc.FetchL402TokenRequest + 42, // 67: looprpc.SwapClient.GetInfo:input_type -> looprpc.GetInfoRequest + 12, // 68: looprpc.SwapClient.StopDaemon:input_type -> looprpc.StopDaemonRequest + 44, // 69: looprpc.SwapClient.GetLiquidityParams:input_type -> looprpc.GetLiquidityParamsRequest + 48, // 70: looprpc.SwapClient.SetLiquidityParams:input_type -> looprpc.SetLiquidityParamsRequest + 50, // 71: looprpc.SwapClient.SuggestSwaps:input_type -> looprpc.SuggestSwapsRequest + 55, // 72: looprpc.SwapClient.ListReservations:input_type -> looprpc.ListReservationsRequest + 58, // 73: looprpc.SwapClient.InstantOut:input_type -> looprpc.InstantOutRequest + 60, // 74: looprpc.SwapClient.InstantOutQuote:input_type -> looprpc.InstantOutQuoteRequest + 62, // 75: looprpc.SwapClient.ListInstantOuts:input_type -> looprpc.ListInstantOutsRequest + 65, // 76: looprpc.SwapClient.NewStaticAddress:input_type -> looprpc.NewStaticAddressRequest + 67, // 77: looprpc.SwapClient.ListUnspentDeposits:input_type -> looprpc.ListUnspentDepositsRequest + 70, // 78: looprpc.SwapClient.WithdrawDeposits:input_type -> looprpc.WithdrawDepositsRequest + 72, // 79: looprpc.SwapClient.ListStaticAddressDeposits:input_type -> looprpc.ListStaticAddressDepositsRequest + 74, // 80: looprpc.SwapClient.ListStaticAddressWithdrawals:input_type -> looprpc.ListStaticAddressWithdrawalRequest + 76, // 81: looprpc.SwapClient.ListStaticAddressSwaps:input_type -> looprpc.ListStaticAddressSwapsRequest + 78, // 82: looprpc.SwapClient.GetStaticAddressSummary:input_type -> looprpc.StaticAddressSummaryRequest + 83, // 83: looprpc.SwapClient.StaticAddressLoopIn:input_type -> looprpc.StaticAddressLoopInRequest + 10, // 84: looprpc.SwapClient.StaticOpenChannel:input_type -> looprpc.StaticOpenChannelRequest + 16, // 85: looprpc.SwapClient.LoopOut:output_type -> looprpc.SwapResponse + 16, // 86: looprpc.SwapClient.LoopIn:output_type -> looprpc.SwapResponse + 18, // 87: looprpc.SwapClient.Monitor:output_type -> looprpc.SwapStatus + 21, // 88: looprpc.SwapClient.ListSwaps:output_type -> looprpc.ListSwapsResponse + 23, // 89: looprpc.SwapClient.SweepHtlc:output_type -> looprpc.SweepHtlcResponse + 18, // 90: looprpc.SwapClient.SwapInfo:output_type -> looprpc.SwapStatus + 54, // 91: looprpc.SwapClient.AbandonSwap:output_type -> looprpc.AbandonSwapResponse + 30, // 92: looprpc.SwapClient.LoopOutTerms:output_type -> looprpc.OutTermsResponse + 33, // 93: looprpc.SwapClient.LoopOutQuote:output_type -> looprpc.OutQuoteResponse + 29, // 94: looprpc.SwapClient.GetLoopInTerms:output_type -> looprpc.InTermsResponse + 32, // 95: looprpc.SwapClient.GetLoopInQuote:output_type -> looprpc.InQuoteResponse + 35, // 96: looprpc.SwapClient.Probe:output_type -> looprpc.ProbeResponse + 37, // 97: looprpc.SwapClient.GetL402Tokens:output_type -> looprpc.TokensResponse + 37, // 98: looprpc.SwapClient.GetLsatTokens:output_type -> looprpc.TokensResponse + 39, // 99: looprpc.SwapClient.FetchL402Token:output_type -> looprpc.FetchL402TokenResponse + 43, // 100: looprpc.SwapClient.GetInfo:output_type -> looprpc.GetInfoResponse + 13, // 101: looprpc.SwapClient.StopDaemon:output_type -> looprpc.StopDaemonResponse + 45, // 102: looprpc.SwapClient.GetLiquidityParams:output_type -> looprpc.LiquidityParameters + 49, // 103: looprpc.SwapClient.SetLiquidityParams:output_type -> looprpc.SetLiquidityParamsResponse + 52, // 104: looprpc.SwapClient.SuggestSwaps:output_type -> looprpc.SuggestSwapsResponse + 56, // 105: looprpc.SwapClient.ListReservations:output_type -> looprpc.ListReservationsResponse + 59, // 106: looprpc.SwapClient.InstantOut:output_type -> looprpc.InstantOutResponse + 61, // 107: looprpc.SwapClient.InstantOutQuote:output_type -> looprpc.InstantOutQuoteResponse + 63, // 108: looprpc.SwapClient.ListInstantOuts:output_type -> looprpc.ListInstantOutsResponse + 66, // 109: looprpc.SwapClient.NewStaticAddress:output_type -> looprpc.NewStaticAddressResponse + 68, // 110: looprpc.SwapClient.ListUnspentDeposits:output_type -> looprpc.ListUnspentDepositsResponse + 71, // 111: looprpc.SwapClient.WithdrawDeposits:output_type -> looprpc.WithdrawDepositsResponse + 73, // 112: looprpc.SwapClient.ListStaticAddressDeposits:output_type -> looprpc.ListStaticAddressDepositsResponse + 75, // 113: looprpc.SwapClient.ListStaticAddressWithdrawals:output_type -> looprpc.ListStaticAddressWithdrawalResponse + 77, // 114: looprpc.SwapClient.ListStaticAddressSwaps:output_type -> looprpc.ListStaticAddressSwapsResponse + 79, // 115: looprpc.SwapClient.GetStaticAddressSummary:output_type -> looprpc.StaticAddressSummaryResponse + 84, // 116: looprpc.SwapClient.StaticAddressLoopIn:output_type -> looprpc.StaticAddressLoopInResponse + 11, // 117: looprpc.SwapClient.StaticOpenChannel:output_type -> looprpc.StaticOpenChannelResponse + 85, // [85:118] is the sub-list for method output_type + 52, // [52:85] is the sub-list for method input_type + 52, // [52:52] is the sub-list for extension type_name + 52, // [52:52] is the sub-list for extension extendee + 0, // [0:52] is the sub-list for field type_name } func init() { file_client_proto_init() } @@ -7443,11 +7518,17 @@ func file_client_proto_init() { if File_client_proto != nil { return } + file_client_proto_msgTypes[8].OneofWrappers = []any{ + (*SwapStatus_StaticLoopInState)(nil), + } file_client_proto_msgTypes[13].OneofWrappers = []any{ (*SweepHtlcResponse_NotRequested)(nil), (*SweepHtlcResponse_Published)(nil), (*SweepHtlcResponse_Failed)(nil), } + file_client_proto_msgTypes[48].OneofWrappers = []any{ + (*InstantOutRequest_MaxSwapFeeSat)(nil), + } type x struct{} out := protoimpl.TypeBuilder{ File: protoimpl.DescBuilder{ diff --git a/looprpc/client.proto b/looprpc/client.proto index 3ae690dc..f2a592f1 100644 --- a/looprpc/client.proto +++ b/looprpc/client.proto @@ -546,10 +546,17 @@ message SwapStatus { SwapType type = 3; /* - State the swap is currently in, see State enum. + Generic loop-in/loop-out state for swaps. */ SwapState state = 4; + oneof static_loop_in_state_optional { + /* + Static address loop-in FSM state when type is STATIC_LOOP_IN. + */ + StaticAddressLoopInSwapState static_loop_in_state = 20; + } + /* A failure reason for the swap, only set if the swap has failed. */ @@ -608,6 +615,9 @@ enum SwapType { // LOOP_IN indicates a loop in swap (on-chain to off-chain) LOOP_IN = 1; + + // STATIC_LOOP_IN indicates a static address loop in swap. + STATIC_LOOP_IN = 2; } enum SwapState { @@ -1675,6 +1685,16 @@ message InstantOutRequest { will be swept to the wallet's internal address. */ string dest_addr = 3; + + oneof max_swap_fee { + /* + The maximum off-chain swap fee that may be charged for the swap. If + this field is omitted, no fee cap is applied for compatibility with + clients that predate this field. An explicitly set value of zero + rejects any positive swap fee. + */ + int64 max_swap_fee_sat = 4; + } } message InstantOutResponse { diff --git a/looprpc/client.swagger.json b/looprpc/client.swagger.json index 0cdbc1ef..57d6dfd6 100644 --- a/looprpc/client.swagger.json +++ b/looprpc/client.swagger.json @@ -1212,11 +1212,13 @@ "STATIC_REMOTE_KEY", "ANCHORS", "SCRIPT_ENFORCED_LEASE", + "TAPROOT", + "SIMPLE_TAPROOT_FINAL", "SIMPLE_TAPROOT", "SIMPLE_TAPROOT_OVERLAY" ], "default": "UNKNOWN_COMMITMENT_TYPE", - "description": " - UNKNOWN_COMMITMENT_TYPE: Returned when the commitment type isn't known or unavailable.\n - LEGACY: A channel using the legacy commitment format having tweaked to_remote\nkeys.\n - STATIC_REMOTE_KEY: A channel that uses the modern commitment format where the key in the\noutput of the remote party does not change each state. This makes back\nup and recovery easier as when the channel is closed, the funds go\ndirectly to that key.\n - ANCHORS: A channel that uses a commitment format that has anchor outputs on the\ncommitments, allowing fee bumping after a force close transaction has\nbeen broadcast.\n - SCRIPT_ENFORCED_LEASE: A channel that uses a commitment type that builds upon the anchors\ncommitment format, but in addition requires a CLTV clause to spend outputs\npaying to the channel initiator. This is intended for use on leased channels\nto guarantee that the channel initiator has no incentives to close a leased\nchannel before its maturity date.\n - SIMPLE_TAPROOT: A channel that uses musig2 for the funding output, and the new tapscript\nfeatures where relevant.\n - SIMPLE_TAPROOT_OVERLAY: Identical to the SIMPLE_TAPROOT channel type, but with extra functionality.\nThis channel type also commits to additional meta data in the tapscript\nleaves for the scripts in a channel." + "description": " - UNKNOWN_COMMITMENT_TYPE: Returned when the commitment type isn't known or unavailable.\n - LEGACY: A channel using the legacy commitment format having tweaked to_remote\nkeys.\n - STATIC_REMOTE_KEY: A channel that uses the modern commitment format where the key in the\noutput of the remote party does not change each state. This makes back\nup and recovery easier as when the channel is closed, the funds go\ndirectly to that key.\n - ANCHORS: A channel that uses a commitment format that has anchor outputs on the\ncommitments, allowing fee bumping after a force close transaction has\nbeen broadcast.\n - SCRIPT_ENFORCED_LEASE: A channel that uses a commitment type that builds upon the anchors\ncommitment format, but in addition requires a CLTV clause to spend outputs\npaying to the channel initiator. This is intended for use on leased channels\nto guarantee that the channel initiator has no incentives to close a leased\nchannel before its maturity date.\n - TAPROOT: The production taproot channel type that uses musig2 for the funding\noutput and the new tapscript features, with final scripts and feature\nbits 80/81. This is the recommended taproot variant; new integrations\nshould select this enum value.\n - SIMPLE_TAPROOT_FINAL: Deprecated alias for TAPROOT, preserved so existing clients that select\nthe production taproot channel type by its historic name continue to\ncompile and serialize against the same wire value.\n - SIMPLE_TAPROOT: A legacy taproot channel type that uses musig2 for the funding output and\nthe new tapscript features, but with development scripts and the staging\nfeature bits. Retained for compatibility with peers that have not upgraded\nto TAPROOT; new integrations should prefer TAPROOT.\n - SIMPLE_TAPROOT_OVERLAY: Identical to the SIMPLE_TAPROOT channel type, but with extra functionality.\nThis channel type also commits to additional meta data in the tapscript\nleaves for the scripts in a channel." }, "lnrpcFundingShim": { "type": "object", @@ -1915,6 +1917,11 @@ "dest_addr": { "type": "string", "description": "An optional address to sweep the onchain funds to. If not set, the funds\nwill be swept to the wallet's internal address." + }, + "max_swap_fee_sat": { + "type": "string", + "format": "int64", + "description": "The maximum off-chain swap fee that may be charged for the swap. If\nthis field is omitted, no fee cap is applied for compatibility with\nclients that predate this field. An explicitly set value of zero\nrejects any positive swap fee." } } }, @@ -3059,7 +3066,11 @@ }, "state": { "$ref": "#/definitions/looprpcSwapState", - "description": "State the swap is currently in, see State enum." + "description": "Generic loop-in/loop-out state for swaps." + }, + "static_loop_in_state": { + "$ref": "#/definitions/looprpcStaticAddressLoopInSwapState", + "description": "Static address loop-in FSM state when type is STATIC_LOOP_IN." }, "failure_reason": { "$ref": "#/definitions/looprpcFailureReason", @@ -3129,10 +3140,11 @@ "type": "string", "enum": [ "LOOP_OUT", - "LOOP_IN" + "LOOP_IN", + "STATIC_LOOP_IN" ], "default": "LOOP_OUT", - "title": "- LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)\n - LOOP_IN: LOOP_IN indicates a loop in swap (on-chain to off-chain)" + "description": " - LOOP_OUT: LOOP_OUT indicates an loop out swap (off-chain to on-chain)\n - LOOP_IN: LOOP_IN indicates a loop in swap (on-chain to off-chain)\n - STATIC_LOOP_IN: STATIC_LOOP_IN indicates a static address loop in swap." }, "looprpcSweepHtlcRequest": { "type": "object", diff --git a/looprpc/client_test.go b/looprpc/client_test.go new file mode 100644 index 00000000..4a19c68f --- /dev/null +++ b/looprpc/client_test.go @@ -0,0 +1,32 @@ +package looprpc + +import ( + "testing" + + "google.golang.org/protobuf/proto" +) + +// TestInstantOutMaxSwapFeePresence verifies that an omitted fee cap remains +// distinguishable from an explicitly encoded zero while retaining the scalar +// field's original wire representation. +func TestInstantOutMaxSwapFeePresence(t *testing.T) { + request := &InstantOutRequest{} + if err := proto.Unmarshal(nil, request); err != nil { + t.Fatalf("unable to unmarshal omitted cap: %v", err) + } + if request.GetMaxSwapFee() != nil { + t.Fatal("omitted cap unexpectedly has presence") + } + + // Field four, encoded as a varint with value zero. This is the same wire + // representation used before the field gained presence semantics. + if err := proto.Unmarshal([]byte{0x20, 0x00}, request); err != nil { + t.Fatalf("unable to unmarshal explicit zero cap: %v", err) + } + if request.GetMaxSwapFee() == nil { + t.Fatal("explicit zero cap lost presence") + } + if request.GetMaxSwapFeeSat() != 0 { + t.Fatalf("expected zero cap, got %d", request.GetMaxSwapFeeSat()) + } +} diff --git a/looprpc/go.mod b/looprpc/go.mod index 43145d33..16a04cad 100644 --- a/looprpc/go.mod +++ b/looprpc/go.mod @@ -1,12 +1,12 @@ module github.com/lightninglabs/loop/looprpc -go 1.25.5 +go 1.25.12 require ( github.com/grpc-ecosystem/grpc-gateway/v2 v2.20.0 github.com/lightninglabs/loop/swapserverrpc v1.0.14 - github.com/lightningnetwork/lnd v0.20.1-beta - google.golang.org/grpc v1.79.3 + github.com/lightningnetwork/lnd v0.21.0-beta + google.golang.org/grpc v1.82.1 google.golang.org/protobuf v1.36.11 gopkg.in/macaroon-bakery.v2 v2.3.0 ) @@ -14,20 +14,21 @@ require ( require ( dario.cat/mergo v1.0.1 // indirect github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c // indirect - github.com/BurntSushi/toml v1.3.2 // indirect + github.com/BurntSushi/toml v1.4.0 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 // indirect github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect github.com/aead/siphash v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect - github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 // indirect - github.com/btcsuite/btcd/btcec/v2 v2.3.4 // indirect - github.com/btcsuite/btcd/btcutil v1.1.5 // indirect - github.com/btcsuite/btcd/btcutil/psbt v1.1.8 // indirect + github.com/btcsuite/btcd v0.25.1-0.20260310163610-1c55c7c18179 // indirect + github.com/btcsuite/btcd/btcec/v2 v2.3.6 // indirect + github.com/btcsuite/btcd/btcutil v1.1.6 // indirect + github.com/btcsuite/btcd/btcutil/psbt v1.1.10 // indirect github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 // indirect - github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c // indirect + github.com/btcsuite/btcd/v2transport v1.0.1 // indirect + github.com/btcsuite/btclog v1.0.0 // indirect github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b // indirect - github.com/btcsuite/btcwallet v0.16.17 // indirect + github.com/btcsuite/btcwallet v0.16.18 // indirect github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 // indirect github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 // indirect github.com/btcsuite/btcwallet/wallet/txsizes v1.2.5 // indirect @@ -44,8 +45,8 @@ require ( github.com/coreos/go-semver v0.3.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect github.com/davecgh/go-spew v1.1.1 // indirect - github.com/decred/dcrd/crypto/blake256 v1.0.1 // indirect - github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 // indirect + github.com/decred/dcrd/crypto/blake256 v1.1.0 // indirect + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/decred/dcrd/lru v1.1.2 // indirect github.com/distribution/reference v0.6.0 // indirect github.com/docker/cli v29.2.0+incompatible // indirect @@ -73,18 +74,12 @@ require ( github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/go-multierror v1.1.1 // indirect github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect - github.com/jackc/chunkreader/v2 v2.0.1 // indirect - github.com/jackc/pgconn v1.14.3 // indirect github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 // indirect - github.com/jackc/pgio v1.0.0 // indirect github.com/jackc/pgpassfile v1.0.0 // indirect - github.com/jackc/pgproto3/v2 v2.3.3 // indirect github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect - github.com/jackc/pgtype v1.14.4 // indirect - github.com/jackc/pgx/v4 v4.18.3 // indirect github.com/jackc/pgx/v5 v5.9.2 // indirect github.com/jackc/puddle/v2 v2.2.2 // indirect - github.com/jessevdk/go-flags v1.4.0 // indirect + github.com/jessevdk/go-flags v1.6.1 // indirect github.com/jonboulle/clockwork v0.2.2 // indirect github.com/jrick/logrotate v1.1.2 // indirect github.com/json-iterator/go v1.1.12 // indirect @@ -92,15 +87,15 @@ require ( github.com/klauspost/compress v1.17.9 // indirect github.com/lib/pq v1.10.9 // indirect github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect - github.com/lightninglabs/neutrino v0.16.1 // indirect - github.com/lightninglabs/neutrino/cache v1.1.2 // indirect - github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 // indirect + github.com/lightninglabs/neutrino v0.17.1 // indirect + github.com/lightninglabs/neutrino/cache v1.1.3 // indirect + github.com/lightningnetwork/lightning-onion v1.3.0 // indirect github.com/lightningnetwork/lnd/clock v1.1.1 // indirect github.com/lightningnetwork/lnd/fn/v2 v2.0.9 // indirect github.com/lightningnetwork/lnd/healthcheck v1.2.6 // indirect - github.com/lightningnetwork/lnd/kvdb v1.4.16 // indirect - github.com/lightningnetwork/lnd/queue v1.1.1 // indirect - github.com/lightningnetwork/lnd/sqldb v1.0.12-0.20260113193010-8565d12e40b1 // indirect + github.com/lightningnetwork/lnd/kvdb v1.5.1 // indirect + github.com/lightningnetwork/lnd/queue v1.2.0 // indirect + github.com/lightningnetwork/lnd/sqldb v1.0.13 // indirect github.com/lightningnetwork/lnd/ticker v1.1.1 // indirect github.com/lightningnetwork/lnd/tlv v1.3.2 // indirect github.com/lightningnetwork/lnd/tor v1.1.6 // indirect @@ -118,7 +113,7 @@ require ( github.com/ncruces/go-strftime v0.1.9 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/image-spec v1.1.1 // indirect - github.com/opencontainers/runc v1.2.8 // indirect + github.com/opencontainers/runc v1.3.6 // indirect github.com/ory/dockertest/v3 v3.10.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v1.11.1 // indirect @@ -161,17 +156,17 @@ require ( go.uber.org/multierr v1.6.0 // indirect go.uber.org/zap v1.17.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect - golang.org/x/crypto v0.46.0 // indirect - golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sync v0.19.0 // indirect - golang.org/x/sys v0.42.0 // indirect - golang.org/x/term v0.38.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/crypto v0.52.0 // indirect + golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sync v0.20.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/term v0.43.0 // indirect + golang.org/x/text v0.37.0 // indirect golang.org/x/time v0.11.0 // indirect google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect + google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 // indirect gopkg.in/errgo.v1 v1.0.1 // indirect gopkg.in/macaroon.v2 v2.1.0 // indirect gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect @@ -203,3 +198,8 @@ replace lukechampine.com/uint128 => github.com/lukechampine/uint128 v1.2.0 // the replaced domain disappeared and package moved to new location. Worth // checking later if the domain reappears and replace can be removed. replace dario.cat/mergo => github.com/darccio/mergo v1.0.1 + +// This sqldb revision contains lnd's pgx v5 migration, which removes the +// vulnerable legacy pgx v4/pgproto3 dependency chain. Remove this replacement +// once the required lnd version includes an sqldb release with that migration. +replace github.com/lightningnetwork/lnd/sqldb v1.0.13 => github.com/lightningnetwork/lnd/sqldb v1.0.13-0.20260514041430-e9b422f78581 diff --git a/looprpc/go.sum b/looprpc/go.sum index cc2a46e8..e5499c1a 100644 --- a/looprpc/go.sum +++ b/looprpc/go.sum @@ -3,10 +3,8 @@ cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMT github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c h1:udKWzYgxTojEKWjV8V+WSxDXJ4NFATAsZjh8iIbsQIg= github.com/Azure/go-ansiterm v0.0.0-20250102033503-faa5f7b0171c/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/Masterminds/semver/v3 v3.1.1 h1:hLg3sBzpNErnxhQtUy/mmLR2I9foDujNK030IGemrRc= -github.com/Masterminds/semver/v3 v3.1.1/go.mod h1:VPu/7SZ7ePZ3QOrcuXROw5FAcLl4a0cBrbBpGY/8hQs= +github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0= +github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Nvveen/Gotty v0.0.0-20120604004816-cd527374f1e5 h1:TngWCqHvy9oXAN6lEVMRuU21PR1EtLVZJmdB18Gu3Rw= @@ -28,30 +26,34 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ= github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M= github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A= -github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6 h1:8n9k3I7e8DkpdQ5YAP4j8ly/LSsbe6qX9vmVbrUGvVw= -github.com/btcsuite/btcd v0.24.3-0.20250318170759-4f4ea81776d6/go.mod h1:OmM4kFtB0klaG/ZqT86rQiyw/1iyXlJgc3UHClPhhbs= +github.com/btcsuite/btcd v0.24.2/go.mod h1:5C8ChTkl5ejr3WHj8tkQSCmydiMEPB0ZhQhehpq7Dgg= +github.com/btcsuite/btcd v0.25.1-0.20260310163610-1c55c7c18179 h1:yJOTxkbxxtuSFrErMqYRvqZLfWggHssioBiWebkV9yo= +github.com/btcsuite/btcd v0.25.1-0.20260310163610-1c55c7c18179/go.mod h1:qbPE+pEiR9643E1s1xu57awsRhlCIm1ZIi6FfeRA4KE= github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA= github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE= -github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ= -github.com/btcsuite/btcd/btcec/v2 v2.3.4/go.mod h1:zYzJ8etWJQIv1Ogk7OzpWjowwOdXY1W/17j2MW85J04= +github.com/btcsuite/btcd/btcec/v2 v2.3.6 h1:IzlsEr9olcSRKB/n7c4351F3xHKxS2lma+1UFGCYd4E= +github.com/btcsuite/btcd/btcec/v2 v2.3.6/go.mod h1:m22FrOAiuxl/tht9wIqAoGHcbnCCaPWyauO8y2LGGtQ= github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A= github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE= -github.com/btcsuite/btcd/btcutil v1.1.5 h1:+wER79R5670vs/ZusMTF1yTcRYE5GUsFbdjdisflzM8= github.com/btcsuite/btcd/btcutil v1.1.5/go.mod h1:PSZZ4UitpLBWzxGd5VGOrLnmOjtPP/a6HaFo12zMs00= -github.com/btcsuite/btcd/btcutil/psbt v1.1.8 h1:4voqtT8UppT7nmKQkXV+T9K8UyQjKOn2z/ycpmJK8wg= -github.com/btcsuite/btcd/btcutil/psbt v1.1.8/go.mod h1:kA6FLH/JfUx++j9pYU0pyu+Z8XGBQuuTmuKYUf6q7/U= +github.com/btcsuite/btcd/btcutil v1.1.6 h1:zFL2+c3Lb9gEgqKNzowKUPQNb8jV7v5Oaodi/AYFd6c= +github.com/btcsuite/btcd/btcutil v1.1.6/go.mod h1:9dFymx8HpuLqBnsPELrImQeTQfKBQqzqGbbV3jK55aE= +github.com/btcsuite/btcd/btcutil/psbt v1.1.10 h1:TC1zhxhFfhnGqoPjsrlEpoqzh+9TPOHrCgnPR47Mj9I= +github.com/btcsuite/btcd/btcutil/psbt v1.1.10/go.mod h1:ehBEvU91lxSlXtA+zZz3iFYx7Yq9eqnKx4/kSrnsvMY= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0 h1:59Kx4K6lzOW5w6nFlA0v5+lk/6sjybR934QNHSJZPTQ= github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0/go.mod h1:7SFka0XMvUgj3hfZtydOrQY2mwhPclbT2snogU7SQQc= +github.com/btcsuite/btcd/v2transport v1.0.1 h1:pIyyyBCPwd087K3Wdb/9tIvUubAQdzTJghjPgzTQVsE= +github.com/btcsuite/btcd/v2transport v1.0.1/go.mod h1:N6H0HGSElVVJKntzaYHYVbW71DtWDLMw2yhwVRO3ZOE= github.com/btcsuite/btclog v0.0.0-20170628155309-84c8d2346e9f/go.mod h1:TdznJufoqS23FtqVCzL0ZqgP5MqXbb4fg/WgDys70nA= -github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c h1:4HxD1lBUGUddhzgaNgrCPsFWd7cGYNpeFUgd9ZIgyM0= -github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= +github.com/btcsuite/btclog v1.0.0 h1:sEkpKJMmfGiyZjADwEIgB1NSwMyfdD1FB8v6+w1T0Ns= +github.com/btcsuite/btclog v1.0.0/go.mod h1:w7xnGOhwT3lmrS4H3b/D1XAXxvh+tbhUm8xeHN2y3TQ= github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b h1:MQ+Q6sDy37V1wP1Yu79A5KqJutolqUGwA99UZWQDWZM= github.com/btcsuite/btclog/v2 v2.0.1-0.20250728225537-6090e87c6c5b/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE= github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg= -github.com/btcsuite/btcwallet v0.16.17 h1:1N6lHznRdcjDopBvcofxaIHknArkJ/EcVKgLKfGL4Dg= -github.com/btcsuite/btcwallet v0.16.17/go.mod h1:YO+W745BAH8n/Rpgj68QsLR6eLlgM4W2do4RejT0buo= +github.com/btcsuite/btcwallet v0.16.18 h1:6h0kMxij4igPu35jOPAWZbn22ceOC4me4L3jj8Za6Zk= +github.com/btcsuite/btcwallet v0.16.18/go.mod h1:4TTru0cgIPbCZpY4aRfAVwX87zrQw4GXM8MH6+A5xZw= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 h1:Rr0njWI3r341nhSPesKQ2JF+ugDSzdPoeckS75SeDZk= github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5/go.mod h1:+tXJ3Ym0nlQc/iHSwW1qzjmPs3ev+UVWMbGgfV1OZqU= github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 h1:YEO+Lx1ZJJAtdRrjuhXjWrYsmAk26wLTlNzxt2q0lhk= @@ -81,8 +83,6 @@ github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UF github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cockroachdb/apd v1.1.0 h1:3LFP3629v+1aKXU5Q37mxmRxX/pIu1nijXydLShEq5I= -github.com/cockroachdb/apd v1.1.0/go.mod h1:8Sl8LxpKi29FqWXR16WEFZRNSz3SoPzUzeMeY4+DwBQ= github.com/cockroachdb/datadriven v1.0.2 h1:H9MtNqVoVhvd9nCBwOyDjUEdZCREqbIdCJD93PBm/jA= github.com/cockroachdb/datadriven v1.0.2/go.mod h1:a9RdTaap04u637JoCzcUoIcDmvwSUtcUFtT/C3kJlTU= github.com/containerd/continuity v0.3.0 h1:nisirsYROK15TAMVukJOUyGJjz4BNQJBVsNvAXZJ/eg= @@ -93,11 +93,8 @@ github.com/containerd/errdefs/pkg v0.3.0 h1:9IKJ06FvyNlexW690DXuQNx2KA2cUJXx151X github.com/containerd/errdefs/pkg v0.3.0/go.mod h1:NJw6s9HwNuRhnjJhM7pylWwMyAkmCQvQ4GpJHEqRLVk= github.com/coreos/go-semver v0.3.0 h1:wkHLiw0WNATZnSG7epLsujiMCgPAc9xhjJ4tgnAxmfM= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= -github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= -github.com/coreos/go-systemd v0.0.0-20190719114852-fd7a80b32e1f/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.5.0 h1:RrqgGjYQKalulkV8NGVIfkXQf6YYmOyiJKk8iXXhfZs= github.com/coreos/go-systemd/v22 v22.5.0/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= -github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/creack/pty v1.1.24 h1:bJrF4RRfyJnbTJqzRLHzcGaZK1NeM5kTC9jGgovnR1s= github.com/creack/pty v1.1.24/go.mod h1:08sCNb52WyoAwi2QDyzUCTgcvVFhUzewun7wtTfvcwE= @@ -108,11 +105,11 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/decred/dcrd/crypto/blake256 v1.0.0/go.mod h1:sQl2p6Y26YV+ZOcSTP6thNdn47hh8kt6rqSlvmrXFAc= -github.com/decred/dcrd/crypto/blake256 v1.0.1 h1:7PltbUIQB7u/FfZ39+DGa/ShuMyJ5ilcvdfma9wOH6Y= -github.com/decred/dcrd/crypto/blake256 v1.0.1/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.0.1/go.mod h1:hyedUtir6IdtD/7lIxGeCxkaw7y45JueMRL4DIyJDKs= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0 h1:rpfIENRNNilwHwZeG5+P150SMrnNEcHYvcCuK6dPZSg= -github.com/decred/dcrd/dcrec/secp256k1/v4 v4.3.0/go.mod h1:v57UDF4pDQJcEfFUCRop3lJL149eHGSe9Jvczhzjo/0= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/lru v1.0.0/go.mod h1:mxKOwFd7lFjN2GZYsiz/ecgqR6kkYAl+0pz0tEMk218= github.com/decred/dcrd/lru v1.1.2 h1:KdCzlkxppuoIDGEvCGah1fZRicrDH36IipvlB1ROkFY= github.com/decred/dcrd/lru v1.1.2/go.mod h1:gEdCVgXs1/YoBvFWt7Scgknbhwik3FgVSzlnCcXL2N8= @@ -167,8 +164,6 @@ github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/me github.com/go-viper/mapstructure/v2 v2.4.0 h1:EBsztssimR/CONLSZZ04E8qAkxNYq4Qp9LvH92wZUgs= github.com/go-viper/mapstructure/v2 v2.4.0/go.mod h1:oJDH3BJKyqBA2TXFhDsKDGDTlndYOZ6rGS0BRZIxGhM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= -github.com/gofrs/uuid v4.0.0+incompatible h1:1SD/1F5pU8p29ybwgQSwpQk+mwdRrXCYuPhW6m+TnJw= -github.com/gofrs/uuid v4.0.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= @@ -202,7 +197,6 @@ github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd h1:gbpYu9NMq8jhDVbvlGkMFWCjLFlqqEZjEmObmhUy6Vo= github.com/google/pprof v0.0.0-20240409012703-83162a5b38cd/go.mod h1:kf6iHlnVGwgKolg33glAes7Yg/8iWP8ukqeldJSO7jw= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= @@ -226,66 +220,20 @@ github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9 github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k= github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= -github.com/jackc/chunkreader v1.0.0/go.mod h1:RT6O25fNZIuasFJRyZ4R/Y2BbhasbmZXF9QQ7T3kePo= -github.com/jackc/chunkreader/v2 v2.0.0/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/chunkreader/v2 v2.0.1 h1:i+RDz65UE+mmpjTfyz0MoVTnzeYxroil2G82ki7MGG8= -github.com/jackc/chunkreader/v2 v2.0.1/go.mod h1:odVSm741yZoC3dpHEUXIqA9tQRhFrgOHwnPIn9lDKlk= -github.com/jackc/pgconn v0.0.0-20190420214824-7e0022ef6ba3/go.mod h1:jkELnwuX+w9qN5YIfX0fl88Ehu4XC3keFuOJJk9pcnA= -github.com/jackc/pgconn v0.0.0-20190824142844-760dd75542eb/go.mod h1:lLjNuW/+OfW9/pnVKPazfWOgNfH2aPem8YQ7ilXGvJE= -github.com/jackc/pgconn v0.0.0-20190831204454-2fabfa3c18b7/go.mod h1:ZJKsE/KZfsUgOEh9hBm+xYTstcNHg7UPMVJqRfQxq4s= -github.com/jackc/pgconn v1.8.0/go.mod h1:1C2Pb36bGIP9QHGBYCjnyhqu7Rv3sGshaQUvmfGIB/o= -github.com/jackc/pgconn v1.9.0/go.mod h1:YctiPyvzfU11JFxoXokUOOKQXQmDMoJL9vJzHH8/2JY= -github.com/jackc/pgconn v1.9.1-0.20210724152538-d89c8390a530/go.mod h1:4z2w8XhRbP1hYxkpTuBjTS3ne3J48K83+u0zoyvg2pI= -github.com/jackc/pgconn v1.14.3 h1:bVoTr12EGANZz66nZPkMInAV/KHD2TxH9npjXXgiB3w= -github.com/jackc/pgconn v1.14.3/go.mod h1:RZbme4uasqzybK2RK5c65VsHxoyaml09lx3tXOcO/VM= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438 h1:Dj0L5fhJ9F82ZJyVOmBx6msDp/kfd1t9GRfny/mfJA0= github.com/jackc/pgerrcode v0.0.0-20240316143900-6e2875d9b438/go.mod h1:a/s9Lp5W7n/DD0VrVoyJ00FbP2ytTPDVOivvn2bMlds= -github.com/jackc/pgio v1.0.0 h1:g12B9UwVnzGhueNavwioyEEpAmqMe1E/BN9ES+8ovkE= -github.com/jackc/pgio v1.0.0/go.mod h1:oP+2QK2wFfUWgr+gxjoBH9KGBb31Eio69xUb0w5bYf8= -github.com/jackc/pgmock v0.0.0-20190831213851-13a1b77aafa2/go.mod h1:fGZlG77KXmcq05nJLRkk0+p82V8B8Dw8KN2/V9c/OAE= -github.com/jackc/pgmock v0.0.0-20201204152224-4fe30f7445fd/go.mod h1:hrBW0Enj2AZTNpt/7Y5rr2xe/9Mn757Wtb2xeBzPv2c= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65 h1:DadwsjnMwFjfWc9y5Wi/+Zz7xoE5ALHsRQlOctkOiHc= -github.com/jackc/pgmock v0.0.0-20210724152146-4ad1a8207f65/go.mod h1:5R2h2EEX+qri8jOWMbJCtaPWkrrNc7OHwsp2TCqp7ak= github.com/jackc/pgpassfile v1.0.0 h1:/6Hmqy13Ss2zCq62VdNG8tM1wchn8zjSGOBJ6icpsIM= github.com/jackc/pgpassfile v1.0.0/go.mod h1:CEx0iS5ambNFdcRtxPj5JhEz+xB6uRky5eyVu/W2HEg= -github.com/jackc/pgproto3 v1.1.0/go.mod h1:eR5FA3leWg7p9aeAqi37XOTgTIbkABlvcPB3E5rlc78= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190420180111-c116219b62db/go.mod h1:bhq50y+xrl9n5mRYyCBFKkpRVTLYJVWeCc+mEAI3yXA= -github.com/jackc/pgproto3/v2 v2.0.0-alpha1.0.20190609003834-432c2951c711/go.mod h1:uH0AWtUmuShn0bcesswc4aBTWGvw0cAxIJp+6OB//Wg= -github.com/jackc/pgproto3/v2 v2.0.0-rc3/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.0-rc3.0.20190831210041-4c03ce451f29/go.mod h1:ryONWYqW6dqSg1Lw6vXNMXoBJhpzvWKnT95C46ckYeM= -github.com/jackc/pgproto3/v2 v2.0.6/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.1.1/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgproto3/v2 v2.3.3 h1:1HLSx5H+tXR9pW3in3zaztoEwQYRC9SQaYUHjTSUOag= -github.com/jackc/pgproto3/v2 v2.3.3/go.mod h1:WfJCnwN3HIg9Ish/j3sgWXnAfK8A9Y0bwXYU5xKaEdA= -github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b/go.mod h1:vsD4gTJCa9TptPL8sPkXrLZ+hDuNrZCnj29CQpr4X1E= -github.com/jackc/pgservicefile v0.0.0-20221227161230-091c0ba34f0a/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 h1:iCEnooe7UlwOQYpKFhBabPMi4aNAfoODPEFNiAnClxo= github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761/go.mod h1:5TJZWKEWniPve33vlWYSoGYefn3gLQRzjfDlhSJ9ZKM= -github.com/jackc/pgtype v0.0.0-20190421001408-4ed0de4755e0/go.mod h1:hdSHsc1V01CGwFsrv11mJRHWJ6aifDLfdV3aVjFF0zg= -github.com/jackc/pgtype v0.0.0-20190824184912-ab885b375b90/go.mod h1:KcahbBH1nCMSo2DXpzsoWOAfFkdEtEJpPbVLq8eE+mc= -github.com/jackc/pgtype v0.0.0-20190828014616-a8802b16cc59/go.mod h1:MWlu30kVJrUS8lot6TQqcg7mtthZ9T0EoIBFiJcmcyw= -github.com/jackc/pgtype v1.8.1-0.20210724151600-32e20a603178/go.mod h1:C516IlIV9NKqfsMCXTdChteoXmwgUceqaLfjg2e3NlM= -github.com/jackc/pgtype v1.14.0/go.mod h1:LUMuVrfsFfdKGLw+AFFVv6KtHOFMwRgDDzBt76IqCA4= -github.com/jackc/pgtype v1.14.4 h1:fKuNiCumbKTAIxQwXfB/nsrnkEI6bPJrrSiMKgbJ2j8= -github.com/jackc/pgtype v1.14.4/go.mod h1:aKeozOde08iifGosdJpz9MBZonJOUJxqNpPBcMJTlVA= -github.com/jackc/pgx/v4 v4.0.0-20190420224344-cc3461e65d96/go.mod h1:mdxmSJJuR08CZQyj1PVQBHy9XOp5p8/SHH6a0psbY9Y= -github.com/jackc/pgx/v4 v4.0.0-20190421002000-1b8f0016e912/go.mod h1:no/Y67Jkk/9WuGR0JG/JseM9irFbnEPbuWV2EELPNuM= -github.com/jackc/pgx/v4 v4.0.0-pre1.0.20190824185557-6972a5742186/go.mod h1:X+GQnOEnf1dqHGpw7JmHqHc1NxDoalibchSk9/RWuDc= -github.com/jackc/pgx/v4 v4.12.1-0.20210724153913-640aa07df17c/go.mod h1:1QD0+tgSXP7iUjYm9C1NxKhny7lq6ee99u/z+IHFcgs= -github.com/jackc/pgx/v4 v4.18.2/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= -github.com/jackc/pgx/v4 v4.18.3 h1:dE2/TrEsGX3RBprb3qryqSV9Y60iZN1C6i8IrmW9/BA= -github.com/jackc/pgx/v4 v4.18.3/go.mod h1:Ey4Oru5tH5sB6tV7hDmfWFahwF15Eb7DNXlRKx2CkVw= github.com/jackc/pgx/v5 v5.9.2 h1:3ZhOzMWnR4yJ+RW1XImIPsD1aNSz4T4fyP7zlQb56hw= github.com/jackc/pgx/v5 v5.9.2/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4= -github.com/jackc/puddle v0.0.0-20190413234325-e4ced69a3a2b/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v0.0.0-20190608224051-11cab39313c9/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.1.3/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= -github.com/jackc/puddle v1.3.0/go.mod h1:m4B5Dj62Y0fbyuIc15OsIqK0+JU8nkqQjsgx7dvjSWk= github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo= github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4= github.com/jessevdk/go-flags v0.0.0-20141203071132-1679536dcc89/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= -github.com/jessevdk/go-flags v1.4.0 h1:4IU2WS7AumrZ/40jfhf4QVDMsQwqA7VEHozFRrGARJA= github.com/jessevdk/go-flags v1.4.0/go.mod h1:4FA24M0QyGHXBuZZK/XkWh8h0e1EYbRYJSGM75WSRxI= +github.com/jessevdk/go-flags v1.6.1 h1:Cvu5U8UGrLay1rZfv/zP7iLpSHGUZ/Ou68T0iX1bBK4= +github.com/jessevdk/go-flags v1.6.1/go.mod h1:Mk8T1hIAWpOiJiHa9rJASDK2UGWji0EuPGBnNLMooyc= github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ= github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= @@ -312,7 +260,6 @@ github.com/kkdai/bstream v1.0.0/go.mod h1:FDnDOHt5Yx4p3FaHcioFT0QjDOtgUpvjeZqAs+ github.com/klauspost/compress v1.17.9 h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA= github.com/klauspost/compress v1.17.9/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw= github.com/konsorten/go-windows-terminal-sequences v1.0.1/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= -github.com/konsorten/go-windows-terminal-sequences v1.0.2/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/konsorten/go-windows-terminal-sequences v1.0.3/go.mod h1:T0+1ngSBFLxvqU3pZ+m/2kptfBszLMUkC4ZK/EgS/cQ= github.com/kr/logfmt v0.0.0-20140226030751-b84e30acd515/go.mod h1:+0opPa2QZZtGFBFZlji/RkVcI2GknAs/DXo4wKdlNEc= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= @@ -320,15 +267,11 @@ github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfn github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/pty v1.1.8/go.mod h1:O1sed60cT9XZ5uDucP5qwvh+TE3NnUj51EiZO/lmSfw= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.1.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= github.com/lib/pq v1.3.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.2/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf h1:HZKvJUHlcXI/f/O0Avg7t8sqkPo78HFzjmeYFl6DPnc= @@ -337,28 +280,30 @@ github.com/lightninglabs/loop/swapserverrpc v1.0.14 h1:0+UrC2oNFsWYqGZjmU+Fkcn8i github.com/lightninglabs/loop/swapserverrpc v1.0.14/go.mod h1:HDRyzFOZeX0e1P9f9RSFE7FzE5u6Eta0hPqx5W7Wp24= github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2 h1:eFjp1dIB2BhhQp/THKrjLdlYuPugO9UU4kDqu91OX/Q= github.com/lightninglabs/migrate/v4 v4.18.2-9023d66a-fork-pr-2/go.mod h1:99BKpIi6ruaaXRM1A77eqZ+FWPQ3cfRa+ZVy5bmWMaY= -github.com/lightninglabs/neutrino v0.16.1 h1:5Kz4ToxncEVkpKC6fwUjXKtFKJhuxlG3sBB3MdJTJjs= -github.com/lightninglabs/neutrino v0.16.1/go.mod h1:L+5UAccpUdyM7yDgmQySgixf7xmwBgJtOfs/IP26jCs= -github.com/lightninglabs/neutrino/cache v1.1.2 h1:C9DY/DAPaPxbFC+xNNEI/z1SJY9GS3shmlu5hIQ798g= -github.com/lightninglabs/neutrino/cache v1.1.2/go.mod h1:XJNcgdOw1LQnanGjw8Vj44CvguYA25IMKjWFZczwZuo= +github.com/lightninglabs/neutrino v0.17.1 h1:lNhgq7ix/N81R6oATroP/kHMzH1qzVVF2dEGcTlN2t4= +github.com/lightninglabs/neutrino v0.17.1/go.mod h1:tcwCgRTGWcaua0L/xzdwllW8eslHDbux4XkiYsivvHE= +github.com/lightninglabs/neutrino/cache v1.1.3 h1:rgnabC41W+XaPuBTQrdeFjFCCAVKh1yctAgmb3Se9zA= +github.com/lightninglabs/neutrino/cache v1.1.3/go.mod h1:qxkJb+pUxR5p84jl5uIGFCR4dGdFkhNUwMSxw3EUWls= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display h1:w7FM5LH9Z6CpKxl13mS48idsu6F+cEZf0lkyiV+Dq9g= github.com/lightninglabs/protobuf-go-hex-display v1.34.2-hex-display/go.mod h1:qYOHts0dSfpeUzUFpOMr/WGzszTmLH+DiWniOlNbLDw= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9 h1:6D3LrdagJweLLdFm1JNodZsBk6iU4TTsBBFLQ4yiXfI= -github.com/lightningnetwork/lightning-onion v1.2.1-0.20240815225420-8b40adf04ab9/go.mod h1:EDqJ3MuZIbMq0QI1czTIKDJ/GS8S14RXPwapHw8cw6w= -github.com/lightningnetwork/lnd v0.20.1-beta h1:wDMNgks5uST1CY+WwjIZ4+McPMMFpr2pIIGJp7ytDI4= -github.com/lightningnetwork/lnd v0.20.1-beta/go.mod h1:oIKh9EqE1sJJpQPq9ZCMFc4Ot287NrotZ1oZn0zUI+M= +github.com/lightningnetwork/lightning-onion v1.3.0 h1:FqILgHjD6euc/Muo1VOzZ4+XDPuFnw6EYROBq0rR/5c= +github.com/lightningnetwork/lightning-onion v1.3.0/go.mod h1:nP85zMHG7c0si/eHBbSQpuDCtnIXfSvFrK3tW6YWzmU= +github.com/lightningnetwork/lnd v0.21.0-beta h1:bDP5UH15E7DVGTztsmBPQLqgyilq5EXDrglvQFmRc3U= +github.com/lightningnetwork/lnd v0.21.0-beta/go.mod h1:HcKq9DyxbVEZXuR28TIyGbIIgAjANCxI+N6dqOnRBAA= +github.com/lightningnetwork/lnd/actor v0.0.6 h1:Ge8N2wivARG+27qJBwTlB0vwsypStZYZy8vk4Zl38sU= +github.com/lightningnetwork/lnd/actor v0.0.6/go.mod h1:YAsoniSbY/cAM9HTVNfZLvt7RI6swDxy6wzPspTcMZg= github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsDIBjgjWdZgA0= github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ= github.com/lightningnetwork/lnd/fn/v2 v2.0.9 h1:ZytG4ltPac/sCyg1EJDn10RGzPIDJeyennUMRdOw7Y8= github.com/lightningnetwork/lnd/fn/v2 v2.0.9/go.mod h1:aPUJHJ31S+Lgoo8I5SxDIjnmeCifqujaiTXKZqpav3w= github.com/lightningnetwork/lnd/healthcheck v1.2.6 h1:1sWhqr93GdkWy4+6U7JxBfcyZIE78MhIHTJZfPx7qqI= github.com/lightningnetwork/lnd/healthcheck v1.2.6/go.mod h1:Mu02um4CWY/zdTOvFje7WJgJcHyX2zq/FG3MhOAiGaQ= -github.com/lightningnetwork/lnd/kvdb v1.4.16 h1:9BZgWdDfjmHRHLS97cz39bVuBAqMc4/p3HX1xtUdbDI= -github.com/lightningnetwork/lnd/kvdb v1.4.16/go.mod h1:HW+bvwkxNaopkz3oIgBV6NEnV4jCEZCACFUcNg4xSjM= -github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI= -github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4= -github.com/lightningnetwork/lnd/sqldb v1.0.12-0.20260113193010-8565d12e40b1 h1:PkEppKL17cZh0Dr9h/T9BEVJUbd/p2tjJ/x8ffG3R0M= -github.com/lightningnetwork/lnd/sqldb v1.0.12-0.20260113193010-8565d12e40b1/go.mod h1:tB2jlqu79TIOR9uhAZOmPxpVFUhB2s+oxKnqRRL1oc0= +github.com/lightningnetwork/lnd/kvdb v1.5.1 h1:OG5cDbqggxiCFKAJbSPw0PfQovi+0odCZAAU5r5b+ho= +github.com/lightningnetwork/lnd/kvdb v1.5.1/go.mod h1:5lubXYoXHDBWBYKmC+2we7qgkjjz0cEZ/5QSQkxQSec= +github.com/lightningnetwork/lnd/queue v1.2.0 h1:sSrn+u84OLuOT/F+xGxgg8VfknXeIZEAFQoMH6BL60s= +github.com/lightningnetwork/lnd/queue v1.2.0/go.mod h1:qLNP0L3B7piRGvDyhAyJKic4xTt+Mw4D7mWrQeuAwxY= +github.com/lightningnetwork/lnd/sqldb v1.0.13-0.20260514041430-e9b422f78581 h1:s0M+mUHRSlBZuz0qxKdr77oKDFff3Hdg5oysJ31BjiU= +github.com/lightningnetwork/lnd/sqldb v1.0.13-0.20260514041430-e9b422f78581/go.mod h1:PQvrB+2SlYuyAoe0ac/SUIMZ9rP73jYlanIN8O7/eA0= github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM= github.com/lightningnetwork/lnd/ticker v1.1.1/go.mod h1:waPTRAAcwtu7Ji3+3k+u/xH5GHovTsCoSVpho0KDvdA= github.com/lightningnetwork/lnd/tlv v1.3.2 h1:MO4FCk7F4k5xPMqVZF6Nb/kOpxlwPrUQpYjmyKny5s0= @@ -368,11 +313,6 @@ github.com/lightningnetwork/lnd/tor v1.1.6/go.mod h1:qSRB8llhAK+a6kaTPWOLLXSZc6H github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796 h1:sjOGyegMIhvgfq5oaue6Td+hxZuf3tDC8lAPrFldqFw= github.com/ltcsuite/ltcd v0.0.0-20190101042124-f37f8bf35796/go.mod h1:3p7ZTf9V1sNPI5H8P3NkTFF4LuwMdPl2DodF60qAKqY= github.com/ltcsuite/ltcutil v0.0.0-20181217130922-17f3b04680b6/go.mod h1:8Vg/LTOO0KYa/vlHWJ6XZAevPQThGH5sufO0Hrou/lA= -github.com/mattn/go-colorable v0.1.1/go.mod h1:FuOcm+DKB9mbwrcAfNl7/TZVBZ6rcnceauSikq3lYCQ= -github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= -github.com/mattn/go-isatty v0.0.5/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.7/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= -github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= @@ -421,8 +361,8 @@ github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8 github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= github.com/opencontainers/image-spec v1.1.1 h1:y0fUlFfIZhPF1W537XOLg0/fcx6zcHCJwooC2xJA040= github.com/opencontainers/image-spec v1.1.1/go.mod h1:qpqAh3Dmcf36wStyyWU+kCeDgrGnAve2nCC8+7h8Q0M= -github.com/opencontainers/runc v1.2.8 h1:RnEICeDReapbZ5lZEgHvj7E9Q3Eex9toYmaGBsbvU5Q= -github.com/opencontainers/runc v1.2.8/go.mod h1:cC0YkmZcuvr+rtBZ6T7NBoVbMGNAdLa/21vIElJDOzI= +github.com/opencontainers/runc v1.3.6 h1:SLGIymCtsk80iNPWgbc8dtjI30r+5mTVV+4dN8/17Sk= +github.com/opencontainers/runc v1.3.6/go.mod h1:o1wyv76EDlTkcf0KTFgN8bMWLPvgF/HfX709lDv+rr4= github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= github.com/ory/dockertest/v3 v3.10.0 h1:4K3z2VMe8Woe++invjaTB7VRyQXQy5UY+loujO4aNE4= github.com/ory/dockertest/v3 v3.10.0/go.mod h1:nr57ZbRWMqfsdGdFNLHz5jjNdDb7VVFnzAeW1n5N1Lg= @@ -456,18 +396,9 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0 h1:Ppwyp6VYCF1nvBTXL3trRso7mXMlRrw9ooo375wvi2s= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= -github.com/rs/xid v1.2.1/go.mod h1:+uKXf+4Djp6Md1KODXJxgGQPKngRmWyn10oCKFzNHOQ= -github.com/rs/zerolog v1.13.0/go.mod h1:YbFCdg8HfsridGWAh22vktObvhZbQsZXe4/zB0OKkWU= -github.com/rs/zerolog v1.15.0/go.mod h1:xYTKnLHcpfU2225ny5qZjxnj9NvkumZYjJHlAThCjNc= -github.com/satori/go.uuid v1.2.0/go.mod h1:dA0hQrYB0VpLJoorglMZABFdXlWrHn1NEOzdhQKdks0= -github.com/shopspring/decimal v0.0.0-20180709203117-cd690d0c9e24/go.mod h1:M+9NzErvs504Cn4c5DxATwIqPbtswREoFCre64PpcG4= -github.com/shopspring/decimal v1.2.0 h1:abSATXmQEYyShuxI4/vyW3tV1MrKAJzCZ/0zLUXYbsQ= -github.com/shopspring/decimal v1.2.0/go.mod h1:DKyhrW/HYNuLGql+MJL6WCR6knT2jwCFRcu2hWCYk4o= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= -github.com/sirupsen/logrus v1.4.1/go.mod h1:ni0Sbl8bgC9z8RoU9G6nDWqqs/fq4eDPysMBDgk/93Q= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= @@ -478,7 +409,6 @@ github.com/spf13/pflag v1.0.6 h1:jFzHGLGAlb3ruxLB8MhbI6A8+AQX/2eW4qeyNZXNp2o= github.com/spf13/pflag v1.0.6/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY= @@ -486,11 +416,10 @@ github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/ github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= @@ -509,8 +438,6 @@ github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 h1:eY9dn8+vbi4tKz5 github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -github.com/zenazn/goji v0.9.0/go.mod h1:7S9M489iMyHBNxwZnk9/EHS098H4/F6TATF2mIxtB1Q= go.etcd.io/bbolt v1.4.3 h1:dEadXpI6G79deX5prL3QRNP6JB8UxVkqo4UPnHaNXJo= go.etcd.io/bbolt v1.4.3/go.mod h1:tKQlpPaYCVFctUIgFKFnAlvbmB3tpy1vkTnDWohtc0E= go.etcd.io/etcd/api/v3 v3.5.12 h1:W4sw5ZoU2Juc9gBWuLk5U6fHfNVyY1WC5g9uiXZio/c= @@ -549,23 +476,15 @@ go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09 go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= go.opentelemetry.io/proto/otlp v1.3.1 h1:TrMUixzpM0yuc/znrFTP9MMRh8trP93mkCiDVeXrui0= go.opentelemetry.io/proto/otlp v1.3.1/go.mod h1:0X1WI4de4ZsLrrJNLAQbFeLCm3T7yBkR0XqQ7niQU+8= -go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= -go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= -go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= -go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= -go.uber.org/multierr v1.5.0/go.mod h1:FeouvMocqHpRaaGuG9EjoKcStLC43Zu/fmqdUMPcKYU= go.uber.org/multierr v1.6.0 h1:y6IPFStTAIT5Ytl7/XYmHvzXQ7S3g/IeZW9hyZ5thw4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= -go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= -go.uber.org/zap v1.9.1/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= -go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0 h1:MTjgFu6ZLKvY6Pvaqk97GlxNBuMpV4Hy/3P6tRGlI2U= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= go.yaml.in/yaml/v3 v3.0.4 h1:tfq32ie2Jv2UxXFdLJdh3jXuOzWiL1fo0bu/FbuKpbc= @@ -575,34 +494,20 @@ golang.org/x/crypto v0.0.0-20180723164146-c126467f60eb/go.mod h1:6SG95UA2DQfeDnf golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190404164418-38d8ce5564a5/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190411191339-88737f569e3a/go.mod h1:WFFai1msRO1wXaEeE5yQxYXgSfI8pQAWXbQop6sCtWE= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190820162420-60c769a6c586/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20201203163018-be400aefbc4c/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I= -golang.org/x/crypto v0.0.0-20210616213533-5ff15b29337e/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= -golang.org/x/crypto v0.20.0/go.mod h1:Xwo95rrVNIoSMx9wa1JroENMToLWn3RNVrTBpLHgZPQ= -golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= -golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= +golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988= +golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8 h1:aAcj0Da7eBAtrTp03QXWvm88pSyOt+UgdZw2BFZ+lEw= -golang.org/x/exp v0.0.0-20240325151524-a685a6edb6d8/go.mod h1:CQ1k9gNrJ50XIzaKCRR2hssIjF07kZFEiieALBM/ARQ= +golang.org/x/exp v0.0.0-20250811191247-51f88131bc50 h1:3yiSh9fhy5/RhCSntf4Sy0Tnx50DmMpQ4MQdKKk4yg4= +golang.org/x/exp v0.0.0-20250811191247-51f88131bc50/go.mod h1:rT6SFzZ7oxADUDx58pcaKFTcZ+inxAa9fTrYx/uVYwg= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= -golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= +golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM= +golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU= golang.org/x/net v0.0.0-20150829230318-ea47fc708ee3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= @@ -615,7 +520,6 @@ golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200520004742-59133d7f0dd7/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= @@ -624,12 +528,8 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg= -golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -641,27 +541,20 @@ golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= -golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4= +golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190403152447-81d4e9dc473e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190813064441-fde4db37ae7a/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190904154756-749cb33beabd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200116001909-b77594299b42/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200519105757-fe76b779f299/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200615200032-f1bc736245b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -672,36 +565,19 @@ golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210303074136-134d130e1a04/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo= -golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= -golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo= -golang.org/x/term v0.17.0/go.mod h1:lLRBjIVuehSbZlaOtGMbcMncT+aqLLLmKrsjNrUguwk= -golang.org/x/term v0.38.0 h1:PQ5pkm/rLO6HnxFR7N2lJHOZX6Kez5Y1gDSJla6jo7Q= -golang.org/x/term v0.38.0/go.mod h1:bSEAKrOT1W+VSu9TSCMtoGEOUcKxOKgl3LE5QEF/xVg= +golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4= +golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= golang.org/x/time v0.11.0 h1:/bpjEDfN9tkoN/ryeYHnv5hcMlc8ncjMcM4XBk5NWV0= golang.org/x/time v0.11.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= @@ -709,22 +585,12 @@ golang.org/x/tools v0.0.0-20181008205924-a2b3f7f249e9/go.mod h1:n7NCudcB/nEzxVGm golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425163242-31fd60d6bfdc/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190823170909-c4a336ef6a2f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200619180055-7c47624df98f/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= -golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= -golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20190513163551-3ee3066db522/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c= +golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= @@ -737,18 +603,18 @@ google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfG google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9 h1:9+tzLLstTlPTRyJTh+ah5wIMsBW5c4tQwGTN3thOW9Y= google.golang.org/genproto v0.0.0-20240213162025-012b6fc9bca9/go.mod h1:mqHbVIp48Muh7Ywss/AD6I5kNVKZMmAa/QEW58Gxp2s= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= -google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww= -google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478 h1:yQugLulqltosq0B/f8l4w9VryjV+N/5gcW0jQ3N8Qec= +google.golang.org/genproto/googleapis/api v0.0.0-20260414002931-afd174a4e478/go.mod h1:C6ADNqOxbgdUUeRTU+LCHDPB9ttAMCTff6auwCVa4uc= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478 h1:RmoJA1ujG+/lRGNfUnOMfhCy5EipVMyvUE+KNbPbTlw= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260414002931-afd174a4e478/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -758,10 +624,8 @@ gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EV gopkg.in/errgo.v1 v1.0.0/go.mod h1:CxwszS/Xz1C49Ucd2i6Zil5UToP1EmyrFhKaMVbg1mk= gopkg.in/errgo.v1 v1.0.1 h1:oQFRXzZ7CkBGdm1XZm/EbQYaYNNEElNBOd09M6cqNso= gopkg.in/errgo.v1 v1.0.1/go.mod h1:3NjfXwocQRYAPTq4/fzX+CwUhPRcR/azYRhj8G+LqMo= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/httprequest.v1 v1.2.0/go.mod h1:T61ZUaJLpMnzvoJDO03ZD8yRXD4nZzBeDoW5e9sffjg= -gopkg.in/inconshreveable/log15.v2 v2.0.0-20180818164646-67afb5ed74ec/go.mod h1:aPpfJ7XW+gOuirDoZ8gHhLh3kZ1B08FtV2bbmy7Jv3s= gopkg.in/juju/environschema.v1 v1.0.0/go.mod h1:WTgU3KXKCVoO9bMmG/4KHzoaRvLeoxfjArpgd1MGWFA= gopkg.in/macaroon-bakery.v2 v2.3.0 h1:b40knPgPTke1QLTE8BSYeH7+R/hiIozB1A8CTLYN0Ic= gopkg.in/macaroon-bakery.v2 v2.3.0/go.mod h1:/8YhtPARXeRzbpEPLmRB66+gQE8/pzBBkWwg7Vz/guc= @@ -789,7 +653,6 @@ gotest.tools/v3 v3.5.2 h1:7koQfIKdy+I8UTetycgUqXWSDwpgv193Ka+qRsmBY8Q= gotest.tools/v3 v3.5.2/go.mod h1:LtdLGcnqToBH83WByAAi/wiwSFCArdFIUV/xxN4pcjA= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= modernc.org/cc/v4 v4.20.0 h1:45Or8mQfbUqJOG9WaxvlFYOAQO0lQ5RvqBcFCXngjxk= modernc.org/cc/v4 v4.20.0/go.mod h1:HM7VJTZbUCR3rV8EYBi9wxnJ0ZBRiGE5OeGXNA0IsLQ= modernc.org/ccgo/v4 v4.16.0 h1:ofwORa6vx2FMm0916/CkZjpFPSR70VwTjUCe2Eg5BnA= diff --git a/looprpc/perms.go b/looprpc/perms.go index d646f667..9187920a 100644 --- a/looprpc/perms.go +++ b/looprpc/perms.go @@ -177,18 +177,30 @@ var RequiredPermissions = map[string][]bakery.Op{ "/looprpc.SwapClient/ListReservations": {{ Entity: "swap", Action: "read", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/InstantOut": {{ Entity: "swap", Action: "execute", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/InstantOutQuote": {{ Entity: "swap", Action: "read", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/ListInstantOuts": {{ Entity: "swap", Action: "read", + }, { + Entity: "loop", + Action: "out", }}, "/looprpc.SwapClient/StopDaemon": {{ Entity: "loop", diff --git a/notifications/manager.go b/notifications/manager.go index 3eda5d65..eb47c1ed 100644 --- a/notifications/manager.go +++ b/notifications/manager.go @@ -26,6 +26,14 @@ const ( // static loop in sweep requests. NotificationTypeStaticLoopInSweepRequest + // NotificationTypeStaticLoopInRiskAccepted is the notification type for + // static loop in confirmation risk acceptance. + NotificationTypeStaticLoopInRiskAccepted + + // NotificationTypeStaticLoopInRiskRejected is the notification type for + // static loop in confirmation risk rejection. + NotificationTypeStaticLoopInRiskRejected + // NotificationTypeUnfinishedSwap is the notification type for unfinished // swap notifications. NotificationTypeUnfinishedSwap @@ -45,6 +53,10 @@ const ( // htlc-confirmed subscriber before dropping the notification. htlcConfirmedSubscriberSendTimeout = 200 * time.Millisecond + // defaultMaxQueuedNotifications is the default number of notifications + // we queue per subscriber before dropping new notifications. + defaultMaxQueuedNotifications = 1024 + // current_version is the current version of the notification listener. current_version = swapserverrpc.SubscribeNotificationsRequest_V1 ) @@ -72,6 +84,17 @@ type Config struct { // MinAliveConnTime is the minimum time that the connection to the // server needs to be alive before we consider it a successful. MinAliveConnTime time.Duration + + // MaxQueuedNotifications is the maximum number of notifications that + // can wait in each subscriber's delivery queue. + MaxQueuedNotifications int + + // PersistStaticLoopInRiskDecision durably records static loop-in + // confirmation-risk decisions. If this fails, the notification is still + // cached and forwarded so a later subscriber can process it after the swap + // row exists. + PersistStaticLoopInRiskDecision func(context.Context, lntypes.Hash, + bool) error } // Manager is a manager for notifications that the swap server sends to the @@ -83,7 +106,26 @@ type Manager struct { hasL402 bool + // subscribers holds active notification subscribers by notification + // type. It is guarded by the Manager mutex. subscribers map[NotificationType][]subscriber + + // staticLoopInRiskAccepted caches accepted risk decisions by swap hash + // so a later matching subscriber can receive a previously delivered + // server decision. + staticLoopInRiskAccepted map[lntypes.Hash]*swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification + + // staticLoopInRiskRejected caches rejected risk decisions by swap hash + // so a later matching subscriber can receive a previously delivered + // server decision. + staticLoopInRiskRejected map[lntypes.Hash]*swapserverrpc. + ServerStaticLoopInRiskRejectedNotification + + // staticLoopInRiskPersisted records whether the cached risk decision for + // a swap hash was durably persisted. Unpersisted decisions remain cached + // after subscriber cancellation so they can be replayed. + staticLoopInRiskPersisted map[lntypes.Hash]bool } // NewManager creates a new notification manager. @@ -92,16 +134,149 @@ func NewManager(cfg *Config) *Manager { if cfg.MinAliveConnTime == 0 { cfg.MinAliveConnTime = defaultMinAliveConnTime } + if cfg.MaxQueuedNotifications <= 0 { + cfg.MaxQueuedNotifications = defaultMaxQueuedNotifications + } return &Manager{ cfg: cfg, subscribers: make(map[NotificationType][]subscriber), + staticLoopInRiskAccepted: make( + map[lntypes.Hash]*swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification, + ), + staticLoopInRiskRejected: make( + map[lntypes.Hash]*swapserverrpc. + ServerStaticLoopInRiskRejectedNotification, + ), + staticLoopInRiskPersisted: make(map[lntypes.Hash]bool), } } type subscriber struct { subCtx context.Context recvChan any + swapHash *lntypes.Hash + enqueue func(any) +} + +// newNotificationQueue creates a per-subscriber FIFO delivery function. +func newNotificationQueue[T any](ctx context.Context, + recvChan chan T, maxPending int) func(any) { + + type queue struct { + sync.Mutex + + pending []T + notify chan struct{} + closed bool + } + + q := &queue{ + notify: make(chan struct{}, 1), + } + + closeQueue := func() { + q.Lock() + q.closed = true + q.pending = nil + q.Unlock() + } + + go func() { + defer close(recvChan) + defer closeQueue() + + for { + select { + case <-ctx.Done(): + return + default: + } + + q.Lock() + if len(q.pending) == 0 { + q.Unlock() + + select { + case <-q.notify: + continue + + case <-ctx.Done(): + return + } + } + + ntfn := q.pending[0] + var zero T + q.pending[0] = zero + q.pending = q.pending[1:] + q.Unlock() + + select { + case recvChan <- ntfn: + case <-ctx.Done(): + return + } + } + }() + + return func(ntfn any) { + typedNtfn, ok := ntfn.(T) + if !ok { + log.Warnf("unexpected notification type %T", ntfn) + return + } + + q.Lock() + if q.closed { + q.Unlock() + return + } + if len(q.pending) >= maxPending { + q.Unlock() + log.Warnf("dropping notification for slow subscriber: "+ + "queue depth %d reached", maxPending) + return + } + + q.pending = append(q.pending, typedNtfn) + q.Unlock() + + select { + case q.notify <- struct{}{}: + default: + } + } +} + +// queueNotification queues or synchronously sends a must-deliver notification. +func queueNotification[T any](sub subscriber, recvChan chan T, ntfn T) { + if sub.enqueue != nil { + sub.enqueue(ntfn) + return + } + + log.Warnf("subscriber has no notification queue, falling back to " + + "blocking send") + + select { + case recvChan <- ntfn: + case <-sub.subCtx.Done(): + } +} + +// dropNotification sends a best-effort notification to a subscriber. +func dropNotification[T any](sub subscriber, recvChan chan T, ntfn T, + description string) { + + select { + case recvChan <- ntfn: + case <-sub.subCtx.Done(): + default: + log.Debugf("Dropping %s notification for slow subscriber", + description) + } } // SubscribeReservations subscribes to the reservation notifications. @@ -136,6 +311,9 @@ func (m *Manager) SubscribeStaticLoopInSweepRequests(ctx context.Context, sub := subscriber{ subCtx: ctx, recvChan: notifChan, + enqueue: newNotificationQueue( + ctx, notifChan, m.cfg.MaxQueuedNotifications, + ), } m.addSubscriber(NotificationTypeStaticLoopInSweepRequest, sub) @@ -145,12 +323,73 @@ func (m *Manager) SubscribeStaticLoopInSweepRequests(ctx context.Context, NotificationTypeStaticLoopInSweepRequest, sub, ) + }) + + return notifChan +} + +func subscribeStaticLoopInRiskDecision[T any](m *Manager, ctx context.Context, + swapHash lntypes.Hash, notifType NotificationType, + notifications map[lntypes.Hash]T) <-chan T { + + notifChan := make(chan T, 1) + sub := subscriber{ + subCtx: ctx, + recvChan: notifChan, + swapHash: &swapHash, + } + + m.Lock() + m.subscribers[notifType] = append(m.subscribers[notifType], sub) + if ntfn, ok := notifications[swapHash]; ok { + notifChan <- ntfn + if m.staticLoopInRiskPersisted[swapHash] { + delete(notifications, swapHash) + delete(m.staticLoopInRiskPersisted, swapHash) + } + } + m.Unlock() + + context.AfterFunc(ctx, func() { + m.removeSubscriber(notifType, sub) + m.Lock() + if _, ok := notifications[swapHash]; ok && + m.staticLoopInRiskPersisted[swapHash] { + + delete(notifications, swapHash) + delete(m.staticLoopInRiskPersisted, swapHash) + } + m.Unlock() close(notifChan) }) return notifChan } +// SubscribeStaticLoopInRiskAccepted subscribes to static loop in risk accepted +// notifications. +func (m *Manager) SubscribeStaticLoopInRiskAccepted(ctx context.Context, + swapHash lntypes.Hash, +) <-chan *swapserverrpc.ServerStaticLoopInRiskAcceptedNotification { + + return subscribeStaticLoopInRiskDecision( + m, ctx, swapHash, NotificationTypeStaticLoopInRiskAccepted, + m.staticLoopInRiskAccepted, + ) +} + +// SubscribeStaticLoopInRiskRejected subscribes to static loop in risk rejected +// notifications. +func (m *Manager) SubscribeStaticLoopInRiskRejected(ctx context.Context, + swapHash lntypes.Hash, +) <-chan *swapserverrpc.ServerStaticLoopInRiskRejectedNotification { + + return subscribeStaticLoopInRiskDecision( + m, ctx, swapHash, NotificationTypeStaticLoopInRiskRejected, + m.staticLoopInRiskRejected, + ) +} + // SubscribeUnfinishedSwaps subscribes to the unfinished swap notifications. func (m *Manager) SubscribeUnfinishedSwaps(ctx context.Context, ) <-chan *swapserverrpc.ServerUnfinishedSwapNotification { @@ -161,12 +400,14 @@ func (m *Manager) SubscribeUnfinishedSwaps(ctx context.Context, sub := subscriber{ subCtx: ctx, recvChan: notifChan, + enqueue: newNotificationQueue( + ctx, notifChan, m.cfg.MaxQueuedNotifications, + ), } m.addSubscriber(NotificationTypeUnfinishedSwap, sub) context.AfterFunc(ctx, func() { m.removeSubscriber(NotificationTypeUnfinishedSwap, sub) - close(notifChan) }) return notifChan @@ -306,7 +547,7 @@ func (m *Manager) subscribeNotifications(ctx context.Context) error { notification, err := notifStream.Recv() if err == nil && notification != nil { log.Tracef("Received notification: %v", notification) - m.handleNotification(notification) + m.handleNotification(ctx, notification) continue } @@ -316,9 +557,73 @@ func (m *Manager) subscribeNotifications(ctx context.Context) error { } } +// staticLoopInRiskDecisionName returns the log label for a risk decision. +func staticLoopInRiskDecisionName(accepted bool) string { + if accepted { + return "accepted" + } + + return "rejected" +} + +// handleStaticLoopInRiskDecision persists, caches, and forwards a risk +// decision notification to the matching subscriber. +func (m *Manager) handleStaticLoopInRiskDecision(ctx context.Context, + swapHashBytes []byte, accepted bool, notifType NotificationType, + cacheDecision func(lntypes.Hash, bool), + notifySubscriber func(subscriber)) { + + decision := staticLoopInRiskDecisionName(accepted) + persisted := m.cfg.PersistStaticLoopInRiskDecision == nil + + var ( + swapHash lntypes.Hash + hasSwapHash bool + ) + if swapHashBytes != nil { + hash, err := lntypes.MakeHash(swapHashBytes) + if err != nil { + log.Warnf("Received invalid static loop in risk "+ + "%s notification: %v", decision, err) + } else { + swapHash = hash + hasSwapHash = true + } + } + + if hasSwapHash && m.cfg.PersistStaticLoopInRiskDecision != nil { + err := m.cfg.PersistStaticLoopInRiskDecision( + ctx, swapHash, accepted, + ) + if err != nil { + log.Errorf("Unable to persist static loop in risk "+ + "%s notification: %v", decision, err) + } else { + persisted = true + } + } + + m.Lock() + defer m.Unlock() + + if hasSwapHash { + cacheDecision(swapHash, persisted) + } + + for _, sub := range m.subscribers[notifType] { + if !hasSwapHash || sub.swapHash == nil || + *sub.swapHash != swapHash { + + continue + } + + notifySubscriber(sub) + } +} + // handleNotification handles an incoming notification from the server, // forwarding it to the appropriate subscribers. -func (m *Manager) handleNotification(ntfn *swapserverrpc. +func (m *Manager) handleNotification(ctx context.Context, ntfn *swapserverrpc. SubscribeNotificationsResponse) { switch ntfn.Notification.(type) { @@ -332,7 +637,13 @@ func (m *Manager) handleNotification(ntfn *swapserverrpc. recvChan := sub.recvChan.(chan *swapserverrpc. ServerReservationNotification) - recvChan <- reservationNtfn + select { + case recvChan <- reservationNtfn: + case <-sub.subCtx.Done(): + default: + log.Debugf("Dropping reservation " + + "notification for slow subscriber") + } } case *swapserverrpc.SubscribeNotificationsResponse_StaticLoopInSweep: // nolint: lll // We'll forward the static loop in sweep request to all @@ -345,9 +656,65 @@ func (m *Manager) handleNotification(ntfn *swapserverrpc. recvChan := sub.recvChan.(chan *swapserverrpc. ServerStaticLoopInSweepNotification) - recvChan <- staticLoopInSweepRequestNtfn + queueNotification(sub, recvChan, staticLoopInSweepRequestNtfn) } + case *swapserverrpc.SubscribeNotificationsResponse_StaticLoopInRiskAccepted: // nolint: lll + // We'll forward the static loop in risk accepted notification to the + // subscriber for the matching swap. + riskAcceptedNtfn := ntfn.GetStaticLoopInRiskAccepted() + var swapHashBytes []byte + if riskAcceptedNtfn != nil { + swapHashBytes = riskAcceptedNtfn.SwapHash + } + + m.handleStaticLoopInRiskDecision( + ctx, swapHashBytes, true, + NotificationTypeStaticLoopInRiskAccepted, + func(swapHash lntypes.Hash, persisted bool) { + m.staticLoopInRiskAccepted[swapHash] = + riskAcceptedNtfn + m.staticLoopInRiskPersisted[swapHash] = persisted + delete(m.staticLoopInRiskRejected, swapHash) + }, + func(sub subscriber) { + recvChan := sub.recvChan.(chan *swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification) + dropNotification( + sub, recvChan, riskAcceptedNtfn, + "static loop in risk accepted", + ) + }, + ) + + case *swapserverrpc.SubscribeNotificationsResponse_StaticLoopInRiskRejected: // nolint: lll + // We'll forward the static loop in risk rejected notification to the + // subscriber for the matching swap. + riskRejectedNtfn := ntfn.GetStaticLoopInRiskRejected() + var swapHashBytes []byte + if riskRejectedNtfn != nil { + swapHashBytes = riskRejectedNtfn.SwapHash + } + + m.handleStaticLoopInRiskDecision( + ctx, swapHashBytes, false, + NotificationTypeStaticLoopInRiskRejected, + func(swapHash lntypes.Hash, persisted bool) { + m.staticLoopInRiskRejected[swapHash] = + riskRejectedNtfn + m.staticLoopInRiskPersisted[swapHash] = persisted + delete(m.staticLoopInRiskAccepted, swapHash) + }, + func(sub subscriber) { + recvChan := sub.recvChan.(chan *swapserverrpc. + ServerStaticLoopInRiskRejectedNotification) + dropNotification( + sub, recvChan, riskRejectedNtfn, + "static loop in risk rejected", + ) + }, + ) + case *swapserverrpc.SubscribeNotificationsResponse_UnfinishedSwap: // nolint: lll // We'll forward the unfinished swap notification to all // subscribers. @@ -359,7 +726,7 @@ func (m *Manager) handleNotification(ntfn *swapserverrpc. recvChan := sub.recvChan.(chan *swapserverrpc. ServerUnfinishedSwapNotification) - recvChan <- unfinishedSwapNtfn + queueNotification(sub, recvChan, unfinishedSwapNtfn) } case *swapserverrpc.SubscribeNotificationsResponse_HtlcConfirmed: @@ -403,7 +770,7 @@ func (m *Manager) removeSubscriber(notifType NotificationType, sub subscriber) { subs := m.subscribers[notifType] newSubs := make([]subscriber, 0, len(subs)) for _, s := range subs { - if s != sub { + if s.recvChan != sub.recvChan { newSubs = append(newSubs, s) } } diff --git a/notifications/manager_test.go b/notifications/manager_test.go index 44300d7a..59ecab78 100644 --- a/notifications/manager_test.go +++ b/notifications/manager_test.go @@ -20,7 +20,7 @@ import ( var ( testReservationId = []byte{0x01, 0x02} - testReservationId2 = []byte{0x01, 0x02} + testReservationId2 = []byte{0x03, 0x04} ) // mockNotificationsClient implements the NotificationsClient interface for testing. @@ -190,6 +190,698 @@ func getTestNotification(resId []byte) *swapserverrpc.SubscribeNotificationsResp } } +// unfinishedSwapNotification builds an unfinished swap notification. +func unfinishedSwapNotification( + swapHash lntypes.Hash) *swapserverrpc.SubscribeNotificationsResponse { + + return &swapserverrpc.SubscribeNotificationsResponse{ + Notification: &swapserverrpc. + SubscribeNotificationsResponse_UnfinishedSwap{ + UnfinishedSwap: &swapserverrpc. + ServerUnfinishedSwapNotification{ + SwapHash: swapHash[:], + }, + }, + } +} + +// staticLoopInSweepNotification builds a static loop-in sweep notification. +func staticLoopInSweepNotification( + swapHash lntypes.Hash) *swapserverrpc.SubscribeNotificationsResponse { + + return &swapserverrpc.SubscribeNotificationsResponse{ + Notification: &swapserverrpc. + SubscribeNotificationsResponse_StaticLoopInSweep{ + StaticLoopInSweep: &swapserverrpc. + ServerStaticLoopInSweepNotification{ + SwapHash: swapHash[:], + }, + }, + } +} + +// staticLoopInRiskAcceptedNotification builds a risk accepted notification. +func staticLoopInRiskAcceptedNotification( + swapHash lntypes.Hash) *swapserverrpc.SubscribeNotificationsResponse { + + return &swapserverrpc.SubscribeNotificationsResponse{ + Notification: &swapserverrpc. + SubscribeNotificationsResponse_StaticLoopInRiskAccepted{ + StaticLoopInRiskAccepted: &swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification{ + SwapHash: swapHash[:], + }, + }, + } +} + +// staticLoopInRiskRejectedNotification builds a risk rejected notification. +func staticLoopInRiskRejectedNotification( + swapHash lntypes.Hash) *swapserverrpc.SubscribeNotificationsResponse { + + return &swapserverrpc.SubscribeNotificationsResponse{ + Notification: &swapserverrpc. + SubscribeNotificationsResponse_StaticLoopInRiskRejected{ + StaticLoopInRiskRejected: &swapserverrpc. + ServerStaticLoopInRiskRejectedNotification{ + SwapHash: swapHash[:], + }, + }, + } +} + +type staticLoopInRiskNotification interface { + GetSwapHash() []byte +} + +// assertStaticLoopInRiskNotificationSwapScoped checks swap-scoped fanout. +func assertStaticLoopInRiskNotificationSwapScoped[ + T staticLoopInRiskNotification](t *testing.T, + subscribe func(*Manager, context.Context, lntypes.Hash) <-chan T, + notification func(lntypes.Hash) *swapserverrpc. + SubscribeNotificationsResponse, label string, + swapHashA, swapHashB lntypes.Hash) { + + t.Helper() + + mgr := NewManager(&Config{}) + + subCtx, subCancel := context.WithCancel(t.Context()) + defer subCancel() + + subChanA := subscribe(mgr, subCtx, swapHashA) + subChanB := subscribe(mgr, subCtx, swapHashB) + + mgr.handleNotification(t.Context(), notification(swapHashA)) + + select { + case received := <-subChanA: + require.Equal(t, swapHashA[:], received.GetSwapHash()) + + case <-time.After(time.Second): + t.Fatalf("did not receive first swap risk %s notification", + label) + } + + select { + case received := <-subChanB: + t.Fatalf("second swap received wrong notification: %x", + received.GetSwapHash()) + + default: + } + + mgr.handleNotification(t.Context(), notification(swapHashB)) + + select { + case received := <-subChanB: + require.Equal(t, swapHashB[:], received.GetSwapHash()) + + case <-time.After(time.Second): + t.Fatalf("did not receive second swap risk %s notification", + label) + } +} + +// TestManager_SlowReservationSubscriberDoesNotBlock tests that a reservation +// subscriber with a full notification channel does not block delivery to other +// subscribers. Reservation notifications are best-effort, so slow subscribers +// drop new notifications instead of queueing them. +func TestManager_SlowReservationSubscriberDoesNotBlock(t *testing.T) { + t.Parallel() + + mgr := NewManager(&Config{}) + + slowCtx, slowCancel := context.WithCancel(t.Context()) + defer slowCancel() + slowChan := mgr.SubscribeReservations(slowCtx) + + fastCtx, fastCancel := context.WithCancel(t.Context()) + defer fastCancel() + fastChan := mgr.SubscribeReservations(fastCtx) + + firstNotif := getTestNotification(testReservationId) + mgr.handleNotification(t.Context(), firstNotif) + + received := <-fastChan + require.Equal(t, testReservationId, received.ReservationId) + + secondNotif := getTestNotification(testReservationId2) + done := make(chan struct{}) + go func() { + mgr.handleNotification(t.Context(), secondNotif) + close(done) + }() + + require.Eventually(t, func() bool { + select { + case <-done: + return true + default: + return false + } + }, time.Second, 10*time.Millisecond) + + select { + case received = <-fastChan: + require.Equal(t, testReservationId2, received.ReservationId) + + case <-time.After(time.Second): + t.Fatal("fast subscriber did not receive notification") + } + + require.Len(t, slowChan, 1) + + select { + case received = <-slowChan: + require.Equal(t, testReservationId, received.ReservationId) + + case <-time.After(time.Second): + t.Fatal("slow subscriber did not receive first notification") + } + + select { + case received = <-slowChan: + t.Fatalf("slow subscriber received dropped notification %x", + received.ReservationId) + + case <-time.After(50 * time.Millisecond): + } +} + +// TestManager_UnfinishedSwapNotificationWaitsForSubscriber verifies that +// unfinished swap recovery notifications are not dropped when the local +// subscriber is briefly behind. +func TestManager_UnfinishedSwapNotificationWaitsForSubscriber(t *testing.T) { + t.Parallel() + + assertQueuedSwapHashNotifications( + t, + func(mgr *Manager, ctx context.Context) <-chan *swapserverrpc. + ServerUnfinishedSwapNotification { + + return mgr.SubscribeUnfinishedSwaps(ctx) + }, + unfinishedSwapNotification, + func(ntfn *swapserverrpc.ServerUnfinishedSwapNotification) []byte { + return ntfn.SwapHash + }, + lntypes.Hash{0x02, 0x03}, lntypes.Hash{0x04, 0x05}, + "did not receive first unfinished swap notification", + "second unfinished swap notification was dropped", + ) +} + +// TestManager_StaticLoopInSweepNotificationQueuesForSlowSubscriber verifies +// that a full static-loop-in sweep subscriber channel does not block the global +// notification receive loop. +func TestManager_StaticLoopInSweepNotificationQueuesForSlowSubscriber( + t *testing.T) { + + t.Parallel() + + assertQueuedSwapHashNotifications( + t, + func(mgr *Manager, ctx context.Context) <-chan *swapserverrpc. + ServerStaticLoopInSweepNotification { + + return mgr.SubscribeStaticLoopInSweepRequests(ctx) + }, + staticLoopInSweepNotification, + func(ntfn *swapserverrpc.ServerStaticLoopInSweepNotification) []byte { + return ntfn.SwapHash + }, + lntypes.Hash{0x12, 0x13}, lntypes.Hash{0x14, 0x15}, + "did not receive first sweep notification", + "second sweep notification was not queued", + ) +} + +// TestManager_QueuedNotificationChannelClosesOnCancel verifies that queued +// subscribers own their channel shutdown even when delivery is blocked. +func TestManager_QueuedNotificationChannelClosesOnCancel(t *testing.T) { + t.Parallel() + + mgr := NewManager(&Config{}) + + subCtx, subCancel := context.WithCancel(t.Context()) + subChan := mgr.SubscribeUnfinishedSwaps(subCtx) + + swapHashA := lntypes.Hash{0x21, 0x22} + mgr.handleNotification(t.Context(), unfinishedSwapNotification(swapHashA)) + + require.Eventually(t, func() bool { + return len(subChan) == 1 + }, time.Second, 10*time.Millisecond) + + swapHashB := lntypes.Hash{0x23, 0x24} + done := make(chan struct{}) + go func() { + mgr.handleNotification(t.Context(), unfinishedSwapNotification(swapHashB)) + close(done) + }() + + require.Eventually(t, func() bool { + select { + case <-done: + return true + default: + return false + } + }, time.Second, 10*time.Millisecond) + + subCancel() + + select { + case received, ok := <-subChan: + require.True(t, ok) + require.Equal(t, swapHashA[:], received.SwapHash) + + case <-time.After(time.Second): + t.Fatal("first unfinished swap notification was not delivered") + } + + require.Eventually(t, func() bool { + select { + case _, ok := <-subChan: + return !ok + default: + return false + } + }, time.Second, 10*time.Millisecond) +} + +// TestNotificationQueueDropsAtCapacity checks the queue's explicit drop policy +// once a subscriber reaches its configured backlog limit. +func TestNotificationQueueDropsAtCapacity(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + recvChan := make(chan int, 1) + enqueue := newNotificationQueue(ctx, recvChan, 0) + + enqueue(1) + + select { + case ntfn := <-recvChan: + t.Fatalf("received dropped notification %d", ntfn) + + case <-time.After(50 * time.Millisecond): + } +} + +// assertQueuedSwapHashNotifications checks queued delivery for swap hashes. +func assertQueuedSwapHashNotifications[T any](t *testing.T, + subscribe func(*Manager, context.Context) <-chan T, + notification func(lntypes.Hash) *swapserverrpc. + SubscribeNotificationsResponse, + swapHash func(T) []byte, swapHashA, swapHashB lntypes.Hash, + firstFailureMsg, secondFailureMsg string) { + + t.Helper() + + mgr := NewManager(&Config{}) + + subCtx, subCancel := context.WithCancel(t.Context()) + defer subCancel() + + subChan := subscribe(mgr, subCtx) + + mgr.handleNotification(t.Context(), notification(swapHashA)) + + done := make(chan struct{}) + go func() { + mgr.handleNotification(t.Context(), notification(swapHashB)) + close(done) + }() + + require.Eventually(t, func() bool { + select { + case <-done: + return true + default: + return false + } + }, time.Second, 10*time.Millisecond) + + select { + case received := <-subChan: + require.Equal(t, swapHashA[:], swapHash(received)) + + case <-time.After(time.Second): + t.Fatal(firstFailureMsg) + } + + select { + case received := <-subChan: + require.Equal(t, swapHashB[:], swapHash(received)) + + case <-time.After(time.Second): + t.Fatal(secondFailureMsg) + } +} + +// TestManager_StaticLoopInRiskAcceptedNotification tests that the Manager +// forwards static loop in risk accepted notifications to subscribers. +func TestManager_StaticLoopInRiskAcceptedNotification(t *testing.T) { + t.Parallel() + + mgr := NewManager(&Config{}) + + subCtx, subCancel := context.WithCancel(t.Context()) + defer subCancel() + + swapHash := lntypes.Hash{0x04, 0x05} + + subChan := mgr.SubscribeStaticLoopInRiskAccepted(subCtx, swapHash) + + mgr.handleNotification( + t.Context(), + &swapserverrpc.SubscribeNotificationsResponse{ + Notification: &swapserverrpc. + SubscribeNotificationsResponse_StaticLoopInRiskAccepted{ + StaticLoopInRiskAccepted: &swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification{ + SwapHash: swapHash[:], + }, + }, + }, + ) + + select { + case received := <-subChan: + require.Equal(t, swapHash[:], received.SwapHash) + + case <-time.After(time.Second): + t.Fatal("did not receive risk accepted notification") + } +} + +// TestManager_StaticLoopInRiskDecisionPersists verifies that risk decisions are +// handed to the durable callback before they are treated as delivered. +func TestManager_StaticLoopInRiskDecisionPersists(t *testing.T) { + t.Parallel() + + type persistedDecision struct { + swapHash lntypes.Hash + accepted bool + } + + persisted := make(chan persistedDecision, 2) + mgr := NewManager(&Config{ + PersistStaticLoopInRiskDecision: func(_ context.Context, + swapHash lntypes.Hash, accepted bool) error { + + persisted <- persistedDecision{ + swapHash: swapHash, + accepted: accepted, + } + + return nil + }, + }) + + acceptedHash := lntypes.Hash{0x16, 0x17} + rejectedHash := lntypes.Hash{0x18, 0x19} + + mgr.handleNotification( + t.Context(), staticLoopInRiskAcceptedNotification(acceptedHash), + ) + mgr.handleNotification( + t.Context(), staticLoopInRiskRejectedNotification(rejectedHash), + ) + + select { + case decision := <-persisted: + require.Equal(t, acceptedHash, decision.swapHash) + require.True(t, decision.accepted) + + case <-time.After(time.Second): + t.Fatal("accepted risk decision was not persisted") + } + + select { + case decision := <-persisted: + require.Equal(t, rejectedHash, decision.swapHash) + require.False(t, decision.accepted) + + case <-time.After(time.Second): + t.Fatal("rejected risk decision was not persisted") + } +} + +// TestManager_StaticLoopInRiskDecisionReplayOnPersistFailure verifies that an +// early risk notification is still cached if the swap row does not exist yet. +func TestManager_StaticLoopInRiskDecisionReplayOnPersistFailure(t *testing.T) { + t.Parallel() + + swapHash := lntypes.Hash{0x1a, 0x1b} + mgr := NewManager(&Config{ + PersistStaticLoopInRiskDecision: func(_ context.Context, + _ lntypes.Hash, _ bool) error { + + return errors.New("swap not stored yet") + }, + }) + + mgr.handleNotification( + t.Context(), staticLoopInRiskAcceptedNotification(swapHash), + ) + + subCtx, subCancel := context.WithCancel(t.Context()) + defer subCancel() + + subChan := mgr.SubscribeStaticLoopInRiskAccepted(subCtx, swapHash) + + select { + case received := <-subChan: + require.Equal(t, swapHash[:], received.SwapHash) + + case <-time.After(time.Second): + t.Fatal("did not replay risk notification after persist failure") + } +} + +// TestManager_StaticLoopInRiskDecisionReplaysAfterSubscriberCancel verifies that +// a non-persisted risk decision remains replayable if the subscriber is canceled +// before the FSM has a chance to process it. +func TestManager_StaticLoopInRiskDecisionReplaysAfterSubscriberCancel( + t *testing.T) { + + t.Parallel() + + assertStaticLoopInRiskDecisionReplaysAfterSubscriberCancel( + t, + (*Manager).SubscribeStaticLoopInRiskAccepted, + staticLoopInRiskAcceptedNotification, + ) + assertStaticLoopInRiskDecisionReplaysAfterSubscriberCancel( + t, + (*Manager).SubscribeStaticLoopInRiskRejected, + staticLoopInRiskRejectedNotification, + ) +} + +func assertStaticLoopInRiskDecisionReplaysAfterSubscriberCancel[ + T staticLoopInRiskNotification](t *testing.T, + subscribe func(*Manager, context.Context, lntypes.Hash) <-chan T, + notification func(lntypes.Hash) *swapserverrpc. + SubscribeNotificationsResponse) { + + t.Helper() + + swapHash := lntypes.Hash{0x2a, 0x2b} + mgr := NewManager(&Config{ + PersistStaticLoopInRiskDecision: func(_ context.Context, + _ lntypes.Hash, _ bool) error { + + return errors.New("swap not stored yet") + }, + }) + + subCtx, subCancel := context.WithCancel(t.Context()) + subChan := subscribe(mgr, subCtx, swapHash) + + mgr.handleNotification(t.Context(), notification(swapHash)) + + require.Eventually(t, func() bool { + return len(subChan) == 1 + }, time.Second, 10*time.Millisecond) + + subCancel() + + select { + case <-subChan: + + case <-time.After(time.Second): + t.Fatal("risk decision notification was not delivered before " + + "cancel") + } + + select { + case _, ok := <-subChan: + require.False(t, ok) + + case <-time.After(time.Second): + t.Fatal("risk decision subscription did not close after cancel") + } + + replayCtx, replayCancel := context.WithCancel(t.Context()) + defer replayCancel() + + replayChan := subscribe(mgr, replayCtx, swapHash) + select { + case received := <-replayChan: + require.Equal(t, swapHash[:], received.GetSwapHash()) + + case <-time.After(time.Second): + t.Fatal("cached risk decision was lost after subscriber " + + "cancellation") + } +} + +// TestManager_StaticLoopInRiskAcceptedNotificationSwapScoped verifies that a +// notification for one swap does not occupy another swap's subscriber channel. +func TestManager_StaticLoopInRiskAcceptedNotificationSwapScoped(t *testing.T) { + t.Parallel() + + assertStaticLoopInRiskNotificationSwapScoped( + t, func(m *Manager, ctx context.Context, + swapHash lntypes.Hash) <-chan *swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification { + + return m.SubscribeStaticLoopInRiskAccepted(ctx, swapHash) + }, staticLoopInRiskAcceptedNotification, "accepted", + lntypes.Hash{0x04, 0x05}, lntypes.Hash{0x06, 0x07}, + ) +} + +// TestManager_StaticLoopInRiskAcceptedNotificationReplay tests that the Manager +// replays a risk accepted notification that arrives before the swap-specific +// subscriber is registered. +func TestManager_StaticLoopInRiskAcceptedNotificationReplay(t *testing.T) { + t.Parallel() + + mgr := NewManager(&Config{}) + + swapHash := lntypes.Hash{0x06, 0x07} + mgr.handleNotification( + t.Context(), + &swapserverrpc.SubscribeNotificationsResponse{ + Notification: &swapserverrpc. + SubscribeNotificationsResponse_StaticLoopInRiskAccepted{ + StaticLoopInRiskAccepted: &swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification{ + SwapHash: swapHash[:], + }, + }, + }, + ) + + subCtx, subCancel := context.WithCancel(t.Context()) + defer subCancel() + + subChan := mgr.SubscribeStaticLoopInRiskAccepted(subCtx, swapHash) + + select { + case received := <-subChan: + require.Equal(t, swapHash[:], received.SwapHash) + + case <-time.After(time.Second): + t.Fatal("did not replay risk accepted notification") + } +} + +// TestManager_StaticLoopInRiskRejectedNotification tests that the Manager +// forwards static loop in risk rejected notifications to subscribers. +func TestManager_StaticLoopInRiskRejectedNotification(t *testing.T) { + t.Parallel() + + mgr := NewManager(&Config{}) + + subCtx, subCancel := context.WithCancel(t.Context()) + defer subCancel() + + swapHash := lntypes.Hash{0x08, 0x09} + + subChan := mgr.SubscribeStaticLoopInRiskRejected(subCtx, swapHash) + + mgr.handleNotification( + t.Context(), + &swapserverrpc.SubscribeNotificationsResponse{ + Notification: &swapserverrpc. + SubscribeNotificationsResponse_StaticLoopInRiskRejected{ + StaticLoopInRiskRejected: &swapserverrpc. + ServerStaticLoopInRiskRejectedNotification{ + SwapHash: swapHash[:], + }, + }, + }, + ) + + select { + case received := <-subChan: + require.Equal(t, swapHash[:], received.SwapHash) + + case <-time.After(time.Second): + t.Fatal("did not receive risk rejected notification") + } +} + +// TestManager_StaticLoopInRiskRejectedNotificationSwapScoped verifies that a +// notification for one swap does not occupy another swap's subscriber channel. +func TestManager_StaticLoopInRiskRejectedNotificationSwapScoped(t *testing.T) { + t.Parallel() + + assertStaticLoopInRiskNotificationSwapScoped( + t, func(m *Manager, ctx context.Context, + swapHash lntypes.Hash) <-chan *swapserverrpc. + ServerStaticLoopInRiskRejectedNotification { + + return m.SubscribeStaticLoopInRiskRejected(ctx, swapHash) + }, staticLoopInRiskRejectedNotification, "rejected", + lntypes.Hash{0x08, 0x09}, lntypes.Hash{0x0a, 0x0b}, + ) +} + +// TestManager_StaticLoopInRiskRejectedNotificationReplay tests that the Manager +// replays a risk rejected notification that arrives before the swap-specific +// subscriber is registered. +func TestManager_StaticLoopInRiskRejectedNotificationReplay(t *testing.T) { + t.Parallel() + + mgr := NewManager(&Config{}) + + swapHash := lntypes.Hash{0x0a, 0x0b} + mgr.handleNotification( + t.Context(), + &swapserverrpc.SubscribeNotificationsResponse{ + Notification: &swapserverrpc. + SubscribeNotificationsResponse_StaticLoopInRiskRejected{ + StaticLoopInRiskRejected: &swapserverrpc. + ServerStaticLoopInRiskRejectedNotification{ + SwapHash: swapHash[:], + }, + }, + }, + ) + + subCtx, subCancel := context.WithCancel(t.Context()) + defer subCancel() + + subChan := mgr.SubscribeStaticLoopInRiskRejected(subCtx, swapHash) + + select { + case received := <-subChan: + require.Equal(t, swapHash[:], received.SwapHash) + + case <-time.After(time.Second): + t.Fatal("did not replay risk rejected notification") + } +} + // TestManager_Backoff verifies that repeated failures in // subscribeNotifications cause the Manager to space out subscription attempts // via a predictable incremental backoff. diff --git a/release_notes.md b/release_notes.md deleted file mode 100644 index 38afc603..00000000 --- a/release_notes.md +++ /dev/null @@ -1,23 +0,0 @@ -# Loop Client Release Notes -This file tracks release notes for the loop client. - -### Developers: -* When new features are added to the repo, a short description of the feature should be added under the "Next Release" heading. -* This should be done in the same PR as the change so that our release notes stay in sync! - -### Release Manager: -* All of the items under the "Next Release" heading should be included in the release notes. -* As part of the PR that bumps the client version, cut everything below the 'Next Release' heading. -* These notes can either be pasted in a temporary doc, or you can get them from the PR diff once it is merged. -* The notes are just a guideline as to the changes that have been made since the last release, they can be updated. -* Once the version bump PR is merged and tagged, add the release notes to the tag on GitHub. - -## Next release - -#### New Features - -#### Breaking Changes - -#### Bug Fixes - -#### Maintenance diff --git a/scripts/commit_message.py b/scripts/commit_message.py new file mode 100755 index 00000000..d99e0135 --- /dev/null +++ b/scripts/commit_message.py @@ -0,0 +1,960 @@ +#!/usr/bin/env python3 + +""" +commit_message lints, formats, and rewrites Git commit messages. + +Goals: +- Enforce a commit subject in ": " form. +- Wrap subjects and bodies using repository defaults (69/72 columns). +- Preserve markdown-like body structure: + - Paragraph breaks. + - Bullet/numbered list item indentation. + - Fenced and indented code blocks. + - Git trailers. +- Support linting existing commit ranges and rewording commits in-place. +""" + +from __future__ import annotations + +import argparse +import os +import re +import subprocess +import sys +import tempfile +import textwrap +from dataclasses import dataclass +from pathlib import Path +from typing import Iterable + + +DEFAULT_SUBJECT_WIDTH = 69 +DEFAULT_BODY_WIDTH = 72 + +SUBJECT_PATTERN = re.compile( + r"^(?P[A-Za-z0-9_][A-Za-z0-9+_.\-/]*): (?P.+)$" +) +LIST_ITEM_PATTERN = re.compile( + r"^(?P\s*)(?P[-+*]|\d+[.)])(?P\s+)(?P.*)$" +) +TRAILER_PATTERN = re.compile(r"^[A-Za-z0-9-]+:\s+\S.*$") +FENCE_PATTERN = re.compile(r"^\s*(```|~~~)") +QUOTE_PATTERN = re.compile(r"^(?P\s*)>\s?(?P.*)$") +NEWLINE_ESCAPE_PATTERN = re.compile(r"\\n") + + +@dataclass +class LintIssue: + """LintIssue captures one formatting violation.""" + + line: int + message: str + + +@dataclass +class LintResult: + """LintResult stores issues and whether a message passed lint.""" + + issues: list[LintIssue] + + def ok(self) -> bool: + return len(self.issues) == 0 + + +def run_git(args: list[str]) -> str: + """run_git executes git and returns trimmed stdout.""" + + return subprocess.check_output( + ["git", *args], + stderr=subprocess.STDOUT, + text=True, + ).rstrip("\n") + + +def get_commit_message(rev: str) -> str: + """get_commit_message returns the full commit message for a revision.""" + + return run_git(["show", "-s", "--format=%B", rev]) + + +def commit_subject(rev: str) -> str: + """commit_subject returns only the subject line for a revision.""" + + return run_git(["show", "-s", "--format=%s", rev]) + + +def commit_is_merge(rev: str) -> bool: + """commit_is_merge reports whether rev has more than one parent.""" + + parents = run_git(["rev-list", "--parents", "-n", "1", rev]).split() + return len(parents) > 2 + + +def split_subject_body(message: str) -> tuple[str, list[str]]: + """ + split_subject_body returns (subject, body_lines) from a commit message. + + The first line is always treated as subject. Any following lines are body. + """ + + lines = message.splitlines() + if not lines: + return "", [] + return lines[0], lines[1:] + + +def _count_columns(text: str) -> int: + """_count_columns counts characters for width checks.""" + + return len(text) + + +def _is_list_item(line: str) -> bool: + """_is_list_item checks whether a line starts a markdown-like list item.""" + + return LIST_ITEM_PATTERN.match(line) is not None + + +def _is_trailer(line: str) -> bool: + """_is_trailer checks whether a line is a git trailer.""" + + return TRAILER_PATTERN.match(line) is not None + + +def _is_indented_code(line: str) -> bool: + """_is_indented_code checks whether a line looks like indented code.""" + + if "\t" in line[:1]: + return True + + spaces = len(line) - len(line.lstrip(" ")) + return spaces >= 4 and line.strip() != "" + + +def _is_quote_line(line: str) -> bool: + """_is_quote_line checks whether a line is markdown quote text.""" + + return QUOTE_PATTERN.match(line) is not None + + +def _normalize_body_leading_blank(body_lines: list[str]) -> list[str]: + """ + _normalize_body_leading_blank removes body-leading blank lines. + + The formatter emits exactly one blank separator between subject and body. + """ + + idx = 0 + while idx < len(body_lines) and body_lines[idx].strip() == "": + idx += 1 + return body_lines[idx:] + + +def _normalize_body_trailing_blank(body_lines: list[str]) -> list[str]: + """_normalize_body_trailing_blank trims trailing blank lines.""" + + idx = len(body_lines) + while idx > 0 and body_lines[idx - 1].strip() == "": + idx -= 1 + return body_lines[:idx] + + +def _collapse_redundant_blank_lines(lines: list[str]) -> list[str]: + """_collapse_redundant_blank_lines keeps at most one consecutive blank.""" + + out: list[str] = [] + blank = False + + for line in lines: + is_blank = line.strip() == "" + if is_blank and blank: + continue + out.append(line) + blank = is_blank + + return out + + +def _decode_escaped_newlines(lines: list[str]) -> list[str]: + """ + _decode_escaped_newlines expands literal "\\n" sequences into real lines. + + This targets the common shell-escaping mistake from `git commit -m`. + """ + + out: list[str] = [] + split_re = re.compile(r"(? list[str]: + """wrap_text wraps a paragraph without splitting long tokens.""" + + wrapper = textwrap.TextWrapper( + width=width, + initial_indent=initial_indent, + subsequent_indent=subsequent_indent, + break_long_words=False, + break_on_hyphens=False, + replace_whitespace=True, + drop_whitespace=True, + ) + + wrapped = wrapper.wrap(text) + return wrapped if wrapped else [initial_indent.rstrip()] + + +def _consume_paragraph(body: list[str], start: int) -> tuple[int, list[str]]: + """ + _consume_paragraph consumes plain paragraph lines from body[start:]. + + It stops before blank lines and before other structured blocks. + """ + + parts: list[str] = [] + i = start + + while i < len(body): + line = body[i] + if line.strip() == "": + break + if FENCE_PATTERN.match(line): + break + if _is_list_item(line): + break + if _is_indented_code(line): + break + if _is_trailer(line): + break + if _is_quote_line(line): + break + parts.append(line.strip()) + i += 1 + + return i, parts + + +def _format_paragraph(parts: list[str], width: int) -> list[str]: + """_format_paragraph normalizes spacing and wraps plain text paragraphs.""" + + text = " ".join(part for part in parts if part) + if not text: + return [] + return wrap_text(text, width) + + +def _consume_quote_block( + body: list[str], + start: int, + width: int, +) -> tuple[int, list[str]]: + """_consume_quote_block consumes consecutive markdown quote lines.""" + + i = start + quoted: list[str] = [] + indent = "" + + while i < len(body): + line = body[i] + if line.strip() == "": + break + match = QUOTE_PATTERN.match(line) + if not match: + break + indent = match.group("indent") + quoted.append(match.group("text").strip()) + i += 1 + + text = " ".join(fragment for fragment in quoted if fragment) + if not text: + return i, [f"{indent}>"] + + prefix = f"{indent}> " + return i, wrap_text( + text, + width, + initial_indent=prefix, + subsequent_indent=prefix, + ) + + +def _consume_list_item( + body: list[str], + start: int, + width: int, +) -> tuple[int, list[str]]: + """ + _consume_list_item consumes a single list item with continuation lines. + + Continuation lines are treated as part of the item until a new peer list + item starts or a blank line is reached. + """ + + first = body[start] + match = LIST_ITEM_PATTERN.match(first) + if match is None: + return start + 1, [first.rstrip()] + + indent = match.group("indent") + marker = match.group("marker") + content = [match.group("text").strip()] + i = start + 1 + + while i < len(body): + line = body[i] + if line.strip() == "": + break + next_match = LIST_ITEM_PATTERN.match(line) + if next_match is not None and len(next_match.group("indent")) <= len(indent): + break + if FENCE_PATTERN.match(line): + break + if _is_trailer(line): + break + content.append(line.strip()) + i += 1 + + text = " ".join(part for part in content if part) + prefix = f"{indent}{marker} " + continuation = " " * len(prefix) + + if not text: + return i, [prefix.rstrip()] + + return i, wrap_text( + text, + width, + initial_indent=prefix, + subsequent_indent=continuation, + ) + + +def _consume_fence_block(body: list[str], start: int) -> tuple[int, list[str]]: + """_consume_fence_block preserves fenced code blocks verbatim.""" + + lines: list[str] = [] + i = start + open_line = body[i].rstrip() + lines.append(open_line) + i += 1 + + while i < len(body): + line = body[i].rstrip() + lines.append(line) + if FENCE_PATTERN.match(line): + i += 1 + break + i += 1 + + return i, lines + + +def _consume_indented_block(body: list[str], start: int) -> tuple[int, list[str]]: + """_consume_indented_block preserves indented code blocks verbatim.""" + + lines: list[str] = [] + i = start + + while i < len(body): + line = body[i] + if line.strip() == "": + lines.append("") + i += 1 + continue + if _is_indented_code(line): + lines.append(line.rstrip()) + i += 1 + continue + break + + while lines and lines[-1] == "": + lines.pop() + + return i, lines + + +def format_body(body_lines: list[str], body_width: int) -> list[str]: + """format_body returns a wrapped body while preserving markdown structure.""" + + body = _normalize_body_leading_blank(body_lines) + body = _normalize_body_trailing_blank(body) + + if not body: + return [] + + out: list[str] = [] + i = 0 + while i < len(body): + line = body[i] + + if line.strip() == "": + out.append("") + i += 1 + continue + + if FENCE_PATTERN.match(line): + i, block = _consume_fence_block(body, i) + out.extend(block) + continue + + if _is_indented_code(line): + i, block = _consume_indented_block(body, i) + out.extend(block) + continue + + if _is_list_item(line): + while i < len(body): + current = body[i] + if current.strip() == "": + break + if not _is_list_item(current): + break + i, item = _consume_list_item(body, i, body_width) + out.extend(item) + continue + + if _is_quote_line(line): + i, quoted = _consume_quote_block(body, i, body_width) + out.extend(quoted) + continue + + if _is_trailer(line): + out.append(line.rstrip()) + i += 1 + continue + + i, para = _consume_paragraph(body, i) + out.extend(_format_paragraph(para, body_width)) + + return _collapse_redundant_blank_lines(out) + + +def format_message( + message: str, + subject_width: int = DEFAULT_SUBJECT_WIDTH, + body_width: int = DEFAULT_BODY_WIDTH, + decode_escaped_newlines: bool = False, +) -> str: + """format_message normalizes spacing and wraps subject/body.""" + + subject, body_lines = split_subject_body(message) + subject = subject.strip() + if decode_escaped_newlines: + body_lines = _decode_escaped_newlines(body_lines) + + match = SUBJECT_PATTERN.match(subject) + if match is None: + wrapped_subject = wrap_text(subject, subject_width) + subject_out = " ".join(wrapped_subject).strip() + else: + pkg = match.group("pkg") + summary = match.group("summary").strip() + prefix = f"{pkg}: " + available = max(10, subject_width - len(prefix)) + summary_wrapped = wrap_text(summary, available) + subject_out = prefix + " ".join( + line.strip() for line in summary_wrapped if line.strip() + ) + + body_lines = format_body(body_lines, body_width) + + if not body_lines: + return f"{subject_out}\n" + + return f"{subject_out}\n\n" + "\n".join(body_lines) + "\n" + + +def lint_message( + message: str, + subject_width: int = DEFAULT_SUBJECT_WIDTH, + body_width: int = DEFAULT_BODY_WIDTH, +) -> LintResult: + """lint_message validates message structure and line widths.""" + + issues: list[LintIssue] = [] + subject, body_lines = split_subject_body(message) + + if not subject: + issues.append(LintIssue(1, "empty commit message subject")) + return LintResult(issues) + + if _count_columns(subject) > subject_width: + issues.append( + LintIssue( + 1, + f"subject length {_count_columns(subject)} exceeds " + f"{subject_width} chars", + ) + ) + + match = SUBJECT_PATTERN.match(subject) + if match is None: + issues.append( + LintIssue( + 1, + 'subject must match ": "', + ) + ) + else: + summary = match.group("summary") + if summary.endswith("."): + issues.append( + LintIssue(1, "subject summary should not end with period") + ) + + if body_lines: + if body_lines[0].strip() != "": + issues.append( + LintIssue( + 2, + "body must be separated from subject by one blank line", + ) + ) + else: + leading_blanks = 0 + while ( + leading_blanks < len(body_lines) + and body_lines[leading_blanks].strip() == "" + ): + leading_blanks += 1 + if leading_blanks > 1 and leading_blanks < len(body_lines): + issues.append( + LintIssue( + 2, + "use exactly one blank line between subject and body", + ) + ) + + in_fence = False + for idx, line in enumerate(body_lines, start=2): + if NEWLINE_ESCAPE_PATTERN.search(line): + issues.append( + LintIssue( + idx, + r'found literal "\n"; use real newlines in commit body', + ) + ) + + if FENCE_PATTERN.match(line): + in_fence = not in_fence + continue + + if in_fence: + continue + + if line.strip() == "": + continue + + if _is_indented_code(line): + continue + if _is_trailer(line): + continue + + width = _count_columns(line) + if width > body_width: + issues.append( + LintIssue( + idx, + f"body line length {width} exceeds {body_width} chars", + ) + ) + + return LintResult(issues) + + +def read_input_message(args: argparse.Namespace) -> str: + """read_input_message reads commit message input from file/stdin/commit.""" + + if args.file: + return Path(args.file).read_text(encoding="utf-8") + + if args.commit: + return get_commit_message(args.commit) + + if not sys.stdin.isatty(): + return sys.stdin.read() + + raise ValueError("no input provided: use --file, --commit, or stdin") + + +def write_output_message(args: argparse.Namespace, message: str) -> None: + """write_output_message writes formatted output to stdout or file.""" + + if args.in_place and args.file: + Path(args.file).write_text(message, encoding="utf-8") + return + + sys.stdout.write(message) + + +def _lint_one( + label: str, + message: str, + subject_width: int, + body_width: int, +) -> bool: + """_lint_one lints one message and prints issues. Returns success.""" + + result = lint_message( + message, + subject_width=subject_width, + body_width=body_width, + ) + if result.ok(): + print(f"{label}: OK") + return True + + print(f"{label}: FAIL") + for issue in result.issues: + print(f" L{issue.line}: {issue.message}") + return False + + +def _collect_revs(range_expr: str, include_merges: bool) -> list[str]: + """_collect_revs returns commits in a revision range.""" + + args = ["rev-list", "--reverse", range_expr] + if not include_merges: + args.insert(1, "--no-merges") + out = run_git(args) + return [line for line in out.splitlines() if line.strip()] + + +def lint_command(args: argparse.Namespace) -> int: + """lint_command runs linting for a message source or commit range.""" + + all_ok = True + + if args.range: + revs = _collect_revs(args.range, include_merges=args.include_merges) + if not revs: + print(f"no commits found in range: {args.range}") + return 0 + + for rev in revs: + if not args.include_merges and commit_is_merge(rev): + continue + subject = commit_subject(rev) + label = f"{rev[:7]} {subject}" + all_ok &= _lint_one( + label, + get_commit_message(rev), + args.subject_width, + args.body_width, + ) + + return 0 if all_ok else 1 + + message = read_input_message(args) + label = args.file or args.commit or "" + ok = _lint_one(label, message, args.subject_width, args.body_width) + return 0 if ok else 1 + + +def fmt_command(args: argparse.Namespace) -> int: + """fmt_command formats a message from file/stdin/commit.""" + + message = read_input_message(args) + formatted = format_message( + message, + subject_width=args.subject_width, + body_width=args.body_width, + decode_escaped_newlines=args.decode_escaped_newlines, + ) + + if args.check: + if message == formatted: + return 0 + print("message is not properly formatted") + return 1 + + write_output_message(args, formatted) + return 0 + + +def _assert_clean_worktree() -> None: + """_assert_clean_worktree ensures there are no staged or unstaged changes.""" + + status = run_git(["status", "--porcelain"]) + if status.strip(): + raise ValueError( + "worktree is not clean; commit rewriting requires a clean state" + ) + + +def _reword_head(new_message: str, no_verify: bool) -> None: + """_reword_head amends HEAD with new_message.""" + + with tempfile.NamedTemporaryFile("w", delete=False) as tmp: + tmp.write(new_message) + tmp_path = tmp.name + + try: + cmd = ["git", "commit", "--amend", "-F", tmp_path] + if no_verify: + cmd.append("--no-verify") + subprocess.check_call(cmd) + finally: + Path(tmp_path).unlink(missing_ok=True) + + +def _reword_non_head(target_rev: str, new_message: str, no_verify: bool) -> None: + """_reword_non_head rewrites a non-HEAD commit via non-interactive rebase.""" + + full = run_git(["rev-parse", target_rev]) + head = run_git(["rev-parse", "HEAD"]) + if full == head: + _reword_head(new_message, no_verify=no_verify) + return + + merge_count = run_git(["rev-list", "--count", "--merges", f"{full}..HEAD"]) + if int(merge_count) > 0: + raise ValueError( + "range from target commit to HEAD contains merge commits; " + "automatic reword is disabled for this case" + ) + + is_ancestor = subprocess.call( + ["git", "merge-base", "--is-ancestor", full, "HEAD"], + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + ) + if is_ancestor != 0: + raise ValueError(f"target commit {target_rev} is not an ancestor of HEAD") + + parent = run_git(["rev-parse", f"{full}^"]) + + with tempfile.TemporaryDirectory() as tmpdir: + tmp = Path(tmpdir) + msg_file = tmp / "new-message.txt" + seq_editor = tmp / "sequence-editor.py" + msg_editor = tmp / "message-editor.py" + + msg_file.write_text(new_message, encoding="utf-8") + seq_editor.write_text( + textwrap.dedent( + """\ + #!/usr/bin/env python3 + import os + import pathlib + import re + import sys + + target = os.environ["COMMIT_REWORD_TARGET"] + todo_path = pathlib.Path(sys.argv[1]) + lines = todo_path.read_text(encoding="utf-8").splitlines() + + out = [] + replaced = False + for line in lines: + match = re.match(r"^(pick|p)\\s+([0-9a-f]+)(\\s+.*)$", line) + if match and not replaced: + commit = match.group(2) + if target.startswith(commit) or commit.startswith(target): + out.append(f"reword {commit}{match.group(3)}") + replaced = True + continue + out.append(line) + + if not replaced: + raise SystemExit( + f"could not find commit {target} in rebase todo" + ) + + todo_path.write_text( + "\\n".join(out) + "\\n", + encoding="utf-8", + ) + """ + ), + encoding="utf-8", + ) + msg_editor.write_text( + textwrap.dedent( + """\ + #!/usr/bin/env python3 + import pathlib + import shutil + import sys + + src = pathlib.Path(__import__("os").environ["COMMIT_REWORD_FILE"]) + dst = pathlib.Path(sys.argv[1]) + shutil.copyfile(src, dst) + """ + ), + encoding="utf-8", + ) + seq_editor.chmod(0o755) + msg_editor.chmod(0o755) + + env = os.environ.copy() + env["COMMIT_REWORD_TARGET"] = full + env["COMMIT_REWORD_FILE"] = str(msg_file) + env["GIT_SEQUENCE_EDITOR"] = str(seq_editor) + env["GIT_EDITOR"] = str(msg_editor) + + cmd = ["git", "rebase", "-i", parent] + if no_verify: + cmd.append("--no-verify") + + subprocess.check_call(cmd, env=env) + + +def reword_command(args: argparse.Namespace) -> int: + """reword_command rewrites a commit message using formatted content.""" + + original = get_commit_message(args.commit) + formatted = format_message( + original, + subject_width=args.subject_width, + body_width=args.body_width, + decode_escaped_newlines=args.decode_escaped_newlines, + ) + + if original == formatted and not args.force: + print("commit message already formatted; nothing to do") + return 0 + + if args.dry_run: + print(formatted, end="") + return 0 + + _assert_clean_worktree() + + try: + _reword_non_head( + args.commit, + formatted, + no_verify=args.no_verify, + ) + except subprocess.CalledProcessError as err: + print(f"git command failed with exit status {err.returncode}") + return err.returncode or 1 + except ValueError as err: + print(f"error: {err}") + return 1 + + print(f"reworded commit {args.commit} with formatted message") + return 0 + + +def build_parser() -> argparse.ArgumentParser: + """build_parser creates the CLI parser.""" + + parser = argparse.ArgumentParser( + description=( + "Lint, format, and reword commit messages with markdown-aware " + "body wrapping." + ) + ) + parser.add_argument( + "--subject-width", + type=int, + default=DEFAULT_SUBJECT_WIDTH, + help=f"max subject width (default: {DEFAULT_SUBJECT_WIDTH})", + ) + parser.add_argument( + "--body-width", + type=int, + default=DEFAULT_BODY_WIDTH, + help=f"max body width (default: {DEFAULT_BODY_WIDTH})", + ) + + sub = parser.add_subparsers(dest="cmd", required=True) + + lint = sub.add_parser("lint", help="lint a message or commit range") + lint.add_argument("--file", help="path to commit message file") + lint.add_argument("--commit", help="commit revision to lint") + lint.add_argument( + "--range", + help="lint all commits in revision range (example: origin/main..HEAD)", + ) + lint.add_argument( + "--include-merges", + action="store_true", + help="include merge commits when linting --range", + ) + lint.set_defaults(func=lint_command) + + fmt = sub.add_parser("fmt", help="format a message from file/stdin/commit") + fmt.add_argument("--file", help="path to commit message file") + fmt.add_argument("--commit", help="commit revision to read as input") + fmt.add_argument( + "--in-place", + action="store_true", + help="write formatted output back to --file", + ) + fmt.add_argument( + "--check", + action="store_true", + help="exit non-zero if formatting changes would be applied", + ) + fmt.add_argument( + "--decode-escaped-newlines", + action="store_true", + help='decode literal "\\n" body sequences before formatting', + ) + fmt.set_defaults(func=fmt_command) + + reword = sub.add_parser( + "reword", + help="reword an existing commit with its formatted message", + ) + reword.add_argument( + "--commit", + default="HEAD", + help="commit revision to reword (default: HEAD)", + ) + reword.add_argument( + "--force", + action="store_true", + help="rewrite even if message is already formatted", + ) + reword.add_argument( + "--no-verify", + action="store_true", + help="pass --no-verify to git amend/rebase reword operations", + ) + reword.add_argument( + "--decode-escaped-newlines", + action="store_true", + help='decode literal "\\n" body sequences before rewording', + ) + reword.add_argument( + "--dry-run", + action="store_true", + help="print the formatted message instead of rewriting commits", + ) + reword.set_defaults(func=reword_command) + + return parser + + +def main(argv: Iterable[str] | None = None) -> int: + """main parses arguments and dispatches subcommands.""" + + parser = build_parser() + args = parser.parse_args(argv) + + try: + return int(args.func(args)) + except ValueError as err: + print(f"error: {err}") + return 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/scripts/fsm-generate.sh b/scripts/fsm-generate.sh index 4ab2d74c..5d2ae1c1 100755 --- a/scripts/fsm-generate.sh +++ b/scripts/fsm-generate.sh @@ -1,4 +1,8 @@ #!/usr/bin/env bash +set -euo pipefail + go run ./fsm/stateparser/stateparser.go --out ./fsm/example_fsm.md --fsm example -go run ./fsm/stateparser/stateparser.go --out ./reservation/reservation_fsm.md --fsm reservation -go run ./fsm/stateparser/stateparser.go --out ./instantout/fsm.md --fsm instantout \ No newline at end of file +go run ./fsm/stateparser/stateparser.go --out ./instantout/reservation/fsm.md --fsm reservation +go run ./fsm/stateparser/stateparser.go --out ./instantout/fsm.md --fsm instantout +go run ./fsm/stateparser/stateparser.go --out ./staticaddr/deposit/fsm.md --fsm staticaddr-deposit +go run ./fsm/stateparser/stateparser.go --out ./staticaddr/loopin/fsm.md --fsm staticaddr-loopin diff --git a/staticaddr/address/manager_test.go b/staticaddr/address/manager_test.go index 5881bf84..b7bbf79a 100644 --- a/staticaddr/address/manager_test.go +++ b/staticaddr/address/manager_test.go @@ -66,6 +66,10 @@ func (m *mockStaticAddressClient) PushStaticAddressHtlcSigs(ctx context.Context, args.Error(1) } +// ServerWithdrawDeposits implements the deprecated RPC required by the +// generated client interface. Production code uses ServerPsbtWithdrawDeposits. +// +//nolint:staticcheck func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context, in *swapserverrpc.ServerWithdrawRequest, opts ...grpc.CallOption) (*swapserverrpc.ServerWithdrawResponse, diff --git a/staticaddr/deposit/actions.go b/staticaddr/deposit/actions.go index 362417e7..77560eb2 100644 --- a/staticaddr/deposit/actions.go +++ b/staticaddr/deposit/actions.go @@ -139,7 +139,7 @@ func (f *FSM) WaitForExpirySweepAction(ctx context.Context, spendChan, errSpendChan, err := f.cfg.ChainNotifier.RegisterConfirmationsNtfn( //nolint:lll ctx, txID, f.deposit.TimeOutSweepPkScript, DefaultConfTarget, - int32(f.deposit.ConfirmationHeight), + int32(f.deposit.GetConfirmationHeight()), ) if err != nil { return f.HandleError(err) @@ -161,14 +161,25 @@ func (f *FSM) WaitForExpirySweepAction(ctx context.Context, // FinalizeDepositAction is the final action after a withdrawal. It signals to // the manager that the deposit has been swept and the FSM can be removed. -func (f *FSM) FinalizeDepositAction(ctx context.Context, +func (f *FSM) FinalizeDepositAction(_ context.Context, _ fsm.EventContext) fsm.EventType { - select { - case <-ctx.Done(): - return fsm.OnError + outpoint := f.deposit.OutPoint - case f.finalizedDepositChan <- f.deposit.OutPoint: - return fsm.NoOp - } + // The finalization notification only tells the manager to remove the + // deposit from its active set. Send it asynchronously so a busy manager + // loop can't stall withdrawal confirmation while deposit locks are held. + go func() { + select { + case <-f.quitChan: + // The deposit is already in a final state. If shutdown wins + // this race, startup recovery will skip it instead of + // re-adding it to the active set. + return + + case f.finalizedDepositChan <- outpoint: + } + }() + + return fsm.NoOp } diff --git a/staticaddr/deposit/actions_test.go b/staticaddr/deposit/actions_test.go new file mode 100644 index 00000000..8c021121 --- /dev/null +++ b/staticaddr/deposit/actions_test.go @@ -0,0 +1,135 @@ +package deposit + +import ( + "context" + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/fsm" + "github.com/stretchr/testify/require" +) + +// TestFinalizeDepositActionDoesNotBlock ensures the final cleanup notification +// does not block the withdrawal completion path while the manager loop is busy. +func TestFinalizeDepositActionDoesNotBlock(t *testing.T) { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{1}, + Index: 1, + } + + depositFSM := &FSM{ + deposit: &Deposit{ + OutPoint: outpoint, + }, + quitChan: make(chan struct{}), + finalizedDepositChan: make(chan wire.OutPoint), + } + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- depositFSM.FinalizeDepositAction(ctx, nil) + }() + + select { + case result := <-resultChan: + require.Equal(t, fsm.NoOp, result) + + case <-time.After(100 * time.Millisecond): + t.Fatal("FinalizeDepositAction blocked on manager cleanup") + } + + select { + case gotOutpoint := <-depositFSM.finalizedDepositChan: + require.Equal(t, outpoint, gotOutpoint) + + case <-time.After(time.Second): + t.Fatal("finalization cleanup notification was not delivered") + } +} + +// TestFinalizeDepositActionIgnoresRequestCancellation ensures the cleanup +// notification is tied to the FSM lifetime, not the caller's request context. +func TestFinalizeDepositActionIgnoresRequestCancellation(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + quitChan := make(chan struct{}) + defer close(quitChan) + + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{2}, + Index: 2, + } + + depositFSM := &FSM{ + deposit: &Deposit{ + OutPoint: outpoint, + }, + quitChan: quitChan, + finalizedDepositChan: make(chan wire.OutPoint), + } + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- depositFSM.FinalizeDepositAction(ctx, nil) + }() + + select { + case result := <-resultChan: + require.Equal(t, fsm.NoOp, result) + + case <-time.After(100 * time.Millisecond): + t.Fatal("FinalizeDepositAction blocked on manager cleanup") + } + + cancel() + + select { + case gotOutpoint := <-depositFSM.finalizedDepositChan: + require.Equal(t, outpoint, gotOutpoint) + + case <-time.After(time.Second): + t.Fatal("finalization cleanup notification was dropped after " + + "request cancellation") + } +} + +// TestFinalizeDepositActionIgnoresCanceledContext ensures the final cleanup +// notification is still queued even if the caller's context is already done. +func TestFinalizeDepositActionIgnoresCanceledContext(t *testing.T) { + ctx, cancel := context.WithCancel(context.Background()) + cancel() + + quitChan := make(chan struct{}) + defer close(quitChan) + + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{3}, + Index: 3, + } + + depositFSM := &FSM{ + deposit: &Deposit{ + OutPoint: outpoint, + }, + quitChan: quitChan, + finalizedDepositChan: make(chan wire.OutPoint), + } + + result := depositFSM.FinalizeDepositAction(ctx, nil) + require.Equal(t, fsm.NoOp, result) + + select { + case gotOutpoint := <-depositFSM.finalizedDepositChan: + require.Equal(t, outpoint, gotOutpoint) + + case <-time.After(time.Second): + t.Fatal("finalization cleanup notification was dropped for " + + "an already-canceled request context") + } +} diff --git a/staticaddr/deposit/deposit.go b/staticaddr/deposit/deposit.go index 4cb64bc9..d63cc4b7 100644 --- a/staticaddr/deposit/deposit.go +++ b/staticaddr/deposit/deposit.go @@ -29,6 +29,13 @@ func (r *ID) FromByteSlice(b []byte) error { // Deposit bundles an utxo at a static address together with manager-relevant // data. +// +// Lock order: if both Manager.mu and a Deposit lock are needed, acquire +// Manager.mu before Deposit.Lock. Never acquire Manager.mu while holding a +// Deposit lock. +// +// The state and ConfirmationHeight fields are mutable and protected by the +// deposit lock. type Deposit struct { sync.Mutex @@ -45,7 +52,8 @@ type Deposit struct { Value btcutil.Amount // ConfirmationHeight is the absolute height at which the deposit was - // first confirmed. + // first confirmed. A value of zero means the deposit is still + // unconfirmed. ConfirmationHeight int64 // TimeOutSweepPkScript is the pk script that is used to sweep the @@ -69,6 +77,12 @@ func (d *Deposit) IsInFinalState() bool { d.Lock() defer d.Unlock() + return d.isInFinalStateNoLock() +} + +// isInFinalStateNoLock returns true if the deposit is final without acquiring +// the deposit lock. +func (d *Deposit) isInFinalStateNoLock() bool { return d.state == Expired || d.state == Withdrawn || d.state == LoopedIn || d.state == HtlcTimeoutSwept || d.state == ChannelPublished @@ -78,6 +92,10 @@ func (d *Deposit) IsExpired(currentHeight, expiry uint32) bool { d.Lock() defer d.Unlock() + if d.ConfirmationHeight <= 0 { + return false + } + return currentHeight >= uint32(d.ConfirmationHeight)+expiry } @@ -88,6 +106,10 @@ func (d *Deposit) GetState() fsm.StateType { return d.state } +func (d *Deposit) getStateNoLock() fsm.StateType { + return d.state +} + func (d *Deposit) SetState(state fsm.StateType) { d.Lock() defer d.Unlock() @@ -95,7 +117,7 @@ func (d *Deposit) SetState(state fsm.StateType) { d.state = state } -func (d *Deposit) SetStateNoLock(state fsm.StateType) { +func (d *Deposit) setStateNoLock(state fsm.StateType) { d.state = state } @@ -106,10 +128,30 @@ func (d *Deposit) IsInState(state fsm.StateType) bool { return d.state == state } -func (d *Deposit) IsInStateNoLock(state fsm.StateType) bool { +func (d *Deposit) isInStateNoLock(state fsm.StateType) bool { return d.state == state } +// IsInStateNoLock returns whether the deposit is in the given state without +// acquiring the deposit lock. +func (d *Deposit) IsInStateNoLock(state fsm.StateType) bool { + return d.isInStateNoLock(state) +} + +// GetConfirmationHeight returns the deposit confirmation height. +func (d *Deposit) GetConfirmationHeight() int64 { + d.Lock() + defer d.Unlock() + + return d.ConfirmationHeight +} + +// GetConfirmationHeightNoLock returns the deposit confirmation height without +// acquiring the deposit lock. +func (d *Deposit) GetConfirmationHeightNoLock() int64 { + return d.ConfirmationHeight +} + // GetRandomDepositID generates a random deposit ID. func GetRandomDepositID() (ID, error) { var id ID diff --git a/staticaddr/deposit/deposit_test.go b/staticaddr/deposit/deposit_test.go new file mode 100644 index 00000000..3215e116 --- /dev/null +++ b/staticaddr/deposit/deposit_test.go @@ -0,0 +1,17 @@ +package deposit + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +// TestDepositIsExpiredUnconfirmed verifies that unconfirmed deposits do not +// expire because their CSV timeout has not started yet. +func TestDepositIsExpiredUnconfirmed(t *testing.T) { + t.Parallel() + + d := &Deposit{} + + require.False(t, d.IsExpired(1_000, 144)) +} diff --git a/staticaddr/deposit/fsm.go b/staticaddr/deposit/fsm.go index 6dadd127..c5bb85c3 100644 --- a/staticaddr/deposit/fsm.go +++ b/staticaddr/deposit/fsm.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "sync" "github.com/btcsuite/btcd/txscript" "github.com/btcsuite/btcd/wire" @@ -41,8 +42,8 @@ var ( // States. var ( - // Deposited signals that funds at a static address have reached the - // confirmation height. + // Deposited signals that funds at a static address have been detected + // and are available to the client. Deposited = fsm.StateType("Deposited") // Withdrawing signals that the withdrawal transaction has been @@ -92,8 +93,8 @@ var ( // Events. var ( // OnStart is sent to the fsm once the deposit outpoint has been - // sufficiently confirmed. It transitions the fsm into the Deposited - // state from where we can trigger a withdrawal, a loopin or an expiry. + // detected. It transitions the fsm into the Deposited state from where + // we can trigger a withdrawal, a loopin or an expiry. OnStart = fsm.EventType("OnStart") // OnWithdrawInitiated is sent to the fsm when a withdrawal has been @@ -160,6 +161,12 @@ type FSM struct { blockNtfnChan chan uint32 + // stopChan requests shutdown of the block notification loop. + stopChan chan struct{} + + // stopOnce ensures Stop is idempotent. + stopOnce sync.Once + // quitChan stops after the FSM stops consuming blockNtfnChan. quitChan chan struct{} @@ -191,6 +198,7 @@ func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig, params: params, address: address, blockNtfnChan: make(chan uint32), + stopChan: make(chan struct{}), quitChan: make(chan struct{}), finalizedDepositChan: finalizedDepositChan, } @@ -226,6 +234,9 @@ func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig, ctx, currentHeight, ) + case <-fsm.stopChan: + return + case <-ctx.Done(): return } @@ -235,12 +246,27 @@ func NewFSM(ctx context.Context, deposit *Deposit, cfg *ManagerConfig, return depoFsm, nil } +// Stop requests shutdown of the FSM's block notification loop. +func (f *FSM) Stop() { + if f == nil || f.stopChan == nil { + return + } + + f.stopOnce.Do(func() { + close(f.stopChan) + }) +} + // handleBlockNotification inspects the current block height and sends the // OnExpiry event to publish the expiry sweep transaction if the deposit timed // out, or it republishes the expiry sweep transaction if it was not yet swept. func (f *FSM) handleBlockNotification(ctx context.Context, currentHeight uint32) { + if f.deposit.IsInFinalState() { + return + } + // If the deposit is expired but not yet sufficiently confirmed, we // republish the expiry sweep transaction. if f.deposit.IsExpired(currentHeight, f.params.Expiry) { @@ -353,6 +379,11 @@ func (f *FSM) DepositStatesV0() fsm.States { // still pending, we publish the expiry sweep. OnExpiry: PublishExpirySweep, + // If the server publishes the HTLC without + // paying us, we need to keep the deposit locked + // until the HTLC timeout path can be swept. + OnSweepingHtlcTimeout: SweepHtlcTimeout, + OnLoopInInitiated: LoopingIn, OnRecover: LoopingIn, @@ -362,7 +393,7 @@ func (f *FSM) DepositStatesV0() fsm.States { }, LoopedIn: fsm.State{ Transitions: fsm.Transitions{ - OnExpiry: Expired, + OnExpiry: LoopedIn, }, Action: f.FinalizeDepositAction, }, @@ -381,7 +412,7 @@ func (f *FSM) DepositStatesV0() fsm.States { }, Withdrawn: fsm.State{ Transitions: fsm.Transitions{ - OnExpiry: Expired, + OnExpiry: Withdrawn, OnWithdrawn: Withdrawn, }, Action: f.FinalizeDepositAction, @@ -421,17 +452,14 @@ func (f *FSM) updateDeposit(ctx context.Context, return } - type checkStateFunc func(state fsm.StateType) bool - type setStateFunc func(state fsm.StateType) - checkFunc := checkStateFunc(f.deposit.IsInState) - setFunc := setStateFunc(f.deposit.SetState) - if _, ok := lockedEvents[notification.Event]; ok { - checkFunc = f.deposit.IsInStateNoLock - setFunc = f.deposit.SetStateNoLock + _, alreadyLocked := lockedEvents[notification.Event] + if !alreadyLocked { + f.deposit.Lock() + defer f.deposit.Unlock() } - setFunc(notification.NextState) - if isUpdateSkipped(notification, checkFunc) { + f.deposit.setStateNoLock(notification.NextState) + if isUpdateSkipped(notification, f.deposit.isInStateNoLock) { return } diff --git a/staticaddr/deposit/fsm.md b/staticaddr/deposit/fsm.md new file mode 100644 index 00000000..9a804c6c --- /dev/null +++ b/staticaddr/deposit/fsm.md @@ -0,0 +1,52 @@ +```mermaid +stateDiagram-v2 +[*] --> Deposited: OnStart +ChannelPublished +ChannelPublished --> ChannelPublished: OnExpiry +Deposited +Deposited --> Deposited: OnError +Deposited --> PublishExpirySweep: OnExpiry +Deposited --> LoopingIn: OnLoopInInitiated +Deposited --> OpeningChannel: OnOpeningChannel +Deposited --> Deposited: OnRecover +Deposited --> SweepHtlcTimeout: OnSweepingHtlcTimeout +Deposited --> Withdrawing: OnWithdrawInitiated +Expired +Expired --> Expired: OnExpiry +HtlcTimeoutSwept +HtlcTimeoutSwept --> HtlcTimeoutSwept: OnExpiry +LoopedIn +LoopedIn --> LoopedIn: OnExpiry +LoopingIn +LoopingIn --> Deposited: OnError +LoopingIn --> PublishExpirySweep: OnExpiry +LoopingIn --> LoopingIn: OnLoopInInitiated +LoopingIn --> LoopedIn: OnLoopedIn +LoopingIn --> LoopingIn: OnRecover +LoopingIn --> SweepHtlcTimeout: OnSweepingHtlcTimeout +OpeningChannel +OpeningChannel --> ChannelPublished: OnChannelPublished +OpeningChannel --> Deposited: OnError +OpeningChannel --> OpeningChannel: OnExpiry +OpeningChannel --> OpeningChannel: OnRecover +PublishExpirySweep +PublishExpirySweep --> Deposited: OnError +PublishExpirySweep --> WaitForExpirySweep: OnExpiryPublished +PublishExpirySweep --> PublishExpirySweep: OnRecover +SweepHtlcTimeout +SweepHtlcTimeout --> HtlcTimeoutSwept: OnHtlcTimeoutSwept +SweepHtlcTimeout --> SweepHtlcTimeout: OnRecover +WaitForExpirySweep +WaitForExpirySweep --> Deposited: OnError +WaitForExpirySweep --> Expired: OnExpirySwept +WaitForExpirySweep --> PublishExpirySweep: OnRecover +Withdrawing +Withdrawing --> Deposited: OnError +Withdrawing --> Withdrawing: OnExpiry +Withdrawing --> Withdrawing: OnRecover +Withdrawing --> Withdrawing: OnWithdrawInitiated +Withdrawing --> Withdrawn: OnWithdrawn +Withdrawn +Withdrawn --> Withdrawn: OnExpiry +Withdrawn --> Withdrawn: OnWithdrawn +``` \ No newline at end of file diff --git a/staticaddr/deposit/fsm_test.go b/staticaddr/deposit/fsm_test.go new file mode 100644 index 00000000..f174ad1d --- /dev/null +++ b/staticaddr/deposit/fsm_test.go @@ -0,0 +1,159 @@ +package deposit + +import ( + "testing" + "time" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// TestHandleBlockNotificationIgnoresFinalStates verifies that a block-driven +// expiry notification cannot mutate deposits that already reached a final +// state but have not yet been removed from the manager's active set. +func TestHandleBlockNotificationIgnoresFinalStates(t *testing.T) { + t.Parallel() + + finalStates := []fsm.StateType{ + Expired, + Withdrawn, + LoopedIn, + HtlcTimeoutSwept, + ChannelPublished, + } + + for i, state := range finalStates { + t.Run(string(state), func(t *testing.T) { + t.Parallel() + + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{byte(i + 1)}, + Index: uint32(i), + } + deposit := &Deposit{ + OutPoint: outpoint, + ConfirmationHeight: 1, + } + deposit.SetState(state) + + depositFSM := &FSM{ + cfg: &ManagerConfig{ + Store: new(mockStore), + }, + deposit: deposit, + params: &script.Parameters{Expiry: 1}, + quitChan: make(chan struct{}), + finalizedDepositChan: make(chan wire.OutPoint, 1), + } + depositFSM.StateMachine = fsm.NewStateMachineWithState( + depositFSM.DepositStatesV0(), state, + DefaultObserverSize, + ) + depositFSM.ActionEntryFunc = depositFSM.updateDeposit + + depositFSM.handleBlockNotification(t.Context(), 3) + + require.Never(t, func() bool { + return deposit.GetState() != state + }, 100*time.Millisecond, 10*time.Millisecond) + + select { + case finalized := <-depositFSM.finalizedDepositChan: + t.Fatalf("unexpected finalization for %v", finalized) + + default: + } + }) + } +} + +// TestFinalStatesIgnoreQueuedExpiry verifies that a queued OnExpiry event cannot +// overwrite a deposit that already reached a final state. +func TestFinalStatesIgnoreQueuedExpiry(t *testing.T) { + t.Parallel() + + finalStates := []fsm.StateType{ + Expired, + Withdrawn, + LoopedIn, + HtlcTimeoutSwept, + ChannelPublished, + } + + for i, state := range finalStates { + t.Run(string(state), func(t *testing.T) { + t.Parallel() + + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{byte(i + 1)}, + Index: uint32(i), + } + deposit := &Deposit{ + OutPoint: outpoint, + } + deposit.SetState(state) + + depositFSM := &FSM{ + cfg: &ManagerConfig{ + Store: new(mockStore), + }, + deposit: deposit, + quitChan: make(chan struct{}), + finalizedDepositChan: make(chan wire.OutPoint, 1), + } + depositFSM.StateMachine = fsm.NewStateMachineWithState( + depositFSM.DepositStatesV0(), state, + DefaultObserverSize, + ) + depositFSM.ActionEntryFunc = depositFSM.updateDeposit + + err := depositFSM.SendEvent(t.Context(), OnExpiry, nil) + require.NoError(t, err) + require.Equal(t, state, deposit.GetState()) + }) + } +} + +// TestLoopingInTransitionsToSweepHtlcTimeout verifies that a deposit selected +// by a loop-in can be moved into the timeout sweep state if the server confirms +// the HTLC without paying the invoice. +func TestLoopingInTransitionsToSweepHtlcTimeout(t *testing.T) { + t.Parallel() + + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{9}, + Index: 9, + } + deposit := &Deposit{ + OutPoint: outpoint, + } + deposit.SetState(LoopingIn) + + store := new(mockStore) + store.On( + "UpdateDeposit", mock.Anything, mock.Anything, + ).Return(nil).Once() + + depositFSM := &FSM{ + cfg: &ManagerConfig{ + Store: store, + }, + deposit: deposit, + params: &script.Parameters{Expiry: 1}, + } + depositFSM.StateMachine = fsm.NewStateMachineWithState( + depositFSM.DepositStatesV0(), LoopingIn, DefaultObserverSize, + ) + depositFSM.ActionEntryFunc = depositFSM.updateDeposit + + err := depositFSM.SendEvent( + t.Context(), OnSweepingHtlcTimeout, nil, + ) + require.NoError(t, err) + require.Equal(t, SweepHtlcTimeout, deposit.GetState()) + store.AssertExpectations(t) +} diff --git a/staticaddr/deposit/manager.go b/staticaddr/deposit/manager.go index af882030..61fc8e76 100644 --- a/staticaddr/deposit/manager.go +++ b/staticaddr/deposit/manager.go @@ -6,6 +6,7 @@ import ( "fmt" "sort" "sync" + "sync/atomic" "time" "github.com/btcsuite/btcd/txscript" @@ -17,9 +18,8 @@ import ( ) const ( - // MinConfs is the minimum number of confirmations we require for a - // deposit to be considered available for loop-ins, coop-spends and - // timeouts. + // MinConfs is the legacy minimum confirmation target deposits had to + // reach before they were considered ready to be used for swaps. MinConfs = 6 // MaxConfs is unset since we don't require a max number of @@ -58,12 +58,21 @@ type ManagerConfig struct { } // Manager manages the address state machines. +// +// Lock order: if both Manager.mu and a Deposit lock are needed, acquire +// Manager.mu before Deposit.Lock. Never acquire Manager.mu while holding a +// Deposit lock. Multiple deposits must be locked with lockDeposits, which +// canonicalizes lock order by outpoint. type Manager struct { cfg *ManagerConfig // mu guards access to the activeDeposits map. mu sync.Mutex + // reconcileMu serializes deposit reconciliation so new deposits are + // discovered and retained exactly once per outpoint. + reconcileMu sync.Mutex + // activeDeposits contains all the active static address outputs. activeDeposits map[wire.OutPoint]*FSM @@ -77,6 +86,9 @@ type Manager struct { // been finalized. The manager will adjust its internal state and flush // finalized deposits from its memory. finalizedDepositChan chan wire.OutPoint + + // currentHeight stores the currently best known block height. + currentHeight atomic.Uint32 } // NewManager creates a new deposit manager. @@ -98,6 +110,19 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error { return err } + var startupHeight uint32 + select { + case height := <-newBlockChan: + startupHeight = uint32(height) + m.currentHeight.Store(startupHeight) + + case err = <-newBlockErrChan: + return err + + case <-ctx.Done(): + return ctx.Err() + } + // Recover previous deposits and static address parameters from the DB. err = m.recoverDeposits(ctx) if err != nil { @@ -111,6 +136,14 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error { err = m.reconcileDeposits(ctx) if err != nil { log.Errorf("unable to reconcile deposits: %v", err) + } else { + // The startup height was consumed before recovered deposit FSMs + // existed. Replay it so already-expired recovered deposits can act + // immediately, but only after their wallet view is fresh. + err = m.notifyActiveDeposits(ctx, startupHeight) + if err != nil { + return err + } } // Start the deposit notifier. @@ -123,32 +156,23 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error { for { select { case height := <-newBlockChan: - // Inform all active deposits about a new block arrival. - m.mu.Lock() - activeDeposits := make([]*FSM, 0, len(m.activeDeposits)) - for _, fsm := range m.activeDeposits { - activeDeposits = append(activeDeposits, fsm) + m.currentHeight.Store(uint32(height)) + + err := m.reconcileDeposits(ctx) + if err != nil { + log.Errorf("unable to reconcile deposits: %v", err) + continue } - m.mu.Unlock() - for _, fsm := range activeDeposits { - select { - case fsm.blockNtfnChan <- uint32(height): - - case <-fsm.quitChan: - continue - - case <-ctx.Done(): - return ctx.Err() - } + err = m.notifyActiveDeposits(ctx, uint32(height)) + if err != nil { + return err } case outpoint := <-m.finalizedDepositChan: // If deposits notify us about their finalization, flush // the finalized deposit from memory. - m.mu.Lock() - delete(m.activeDeposits, outpoint) - m.mu.Unlock() + m.removeActiveDeposit(outpoint) case err = <-newBlockErrChan: return err @@ -159,6 +183,33 @@ func (m *Manager) Run(ctx context.Context, initChan chan struct{}) error { } } +// notifyActiveDeposits informs all active deposit FSMs about a new block +// height. +func (m *Manager) notifyActiveDeposits(ctx context.Context, + height uint32) error { + + m.mu.Lock() + activeDeposits := make([]*FSM, 0, len(m.activeDeposits)) + for _, fsm := range m.activeDeposits { + activeDeposits = append(activeDeposits, fsm) + } + m.mu.Unlock() + + for _, fsm := range activeDeposits { + select { + case fsm.blockNtfnChan <- height: + + case <-fsm.quitChan: + continue + + case <-ctx.Done(): + return ctx.Err() + } + } + + return nil +} + // recoverDeposits recovers static address parameters, previous deposits and // state machines from the database and starts the deposit notifier. func (m *Manager) recoverDeposits(ctx context.Context) error { @@ -207,8 +258,10 @@ func (m *Manager) recoverDeposits(ctx context.Context) error { return nil } -// pollDeposits polls new deposits to our static address and notifies the -// manager's event loop about them. +// pollDeposits periodically polls for new deposits to our static address. This +// complements the block-driven reconciliation in the main event loop: while new +// blocks trigger reconcileDeposits to promptly detect confirmations, the ticker +// here catches deposits that appear in the mempool between blocks. func (m *Manager) pollDeposits(ctx context.Context) { log.Debugf("Waiting for new static address deposits...") @@ -231,20 +284,44 @@ func (m *Manager) pollDeposits(ctx context.Context) { }() } +// EnsureDepositsFresh reconciles the cached active deposit set with lnd's +// current wallet view. Spending paths call this before selecting deposits so +// stale persisted records are not treated as live funds. This can happen when +// an unconfirmed funding transaction is replaced, a confirmed deposit is +// reorged out, or the output was spent outside the active manager path. +func (m *Manager) EnsureDepositsFresh(ctx context.Context) error { + return m.reconcileDeposits(ctx) +} + // reconcileDeposits fetches all spends to our static addresses from our lnd // wallet and matches it against the deposits in our memory that we've seen so // far. It picks the newly identified deposits and starts a state machine per // deposit to track its progress. func (m *Manager) reconcileDeposits(ctx context.Context) error { + m.reconcileMu.Lock() + defer m.reconcileMu.Unlock() + log.Tracef("Reconciling new deposits...") utxos, err := m.cfg.AddressManager.ListUnspent( - ctx, MinConfs, MaxConfs, + ctx, 0, MaxConfs, ) if err != nil { return fmt.Errorf("unable to list new deposits: %w", err) } + currentHeight := m.currentHeight.Load() + err = m.updateDepositConfirmations(ctx, utxos, currentHeight) + if err != nil { + return fmt.Errorf("unable to update deposit "+ + "confirmations: %w", err) + } + + err = m.syncActiveDeposits(ctx, utxos) + if err != nil { + return fmt.Errorf("unable to sync active deposits: %w", err) + } + newDeposits := m.filterNewDeposits(utxos) if len(newDeposits) == 0 { log.Tracef("No new deposits...") @@ -252,7 +329,7 @@ func (m *Manager) reconcileDeposits(ctx context.Context) error { } for _, utxo := range newDeposits { - deposit, err := m.createNewDeposit(ctx, utxo) + deposit, err := m.createNewDeposit(ctx, utxo, currentHeight) if err != nil { return fmt.Errorf("unable to retain new deposit: %w", err) @@ -272,9 +349,11 @@ func (m *Manager) reconcileDeposits(ctx context.Context) error { // createNewDeposit transforms the wallet utxo into a deposit struct and stores // it in our database and manager memory. func (m *Manager) createNewDeposit(ctx context.Context, - utxo *lnwallet.Utxo) (*Deposit, error) { + utxo *lnwallet.Utxo, currentHeight uint32) (*Deposit, error) { - blockHeight, err := m.getBlockHeight(ctx, utxo) + confirmationHeight, err := confirmationHeightForUtxo( + currentHeight, utxo, + ) if err != nil { return nil, err } @@ -302,7 +381,7 @@ func (m *Manager) createNewDeposit(ctx context.Context, state: Deposited, OutPoint: utxo.OutPoint, Value: utxo.Value, - ConfirmationHeight: int64(blockHeight), + ConfirmationHeight: confirmationHeight, TimeOutSweepPkScript: timeoutSweepPkScript, } @@ -318,37 +397,167 @@ func (m *Manager) createNewDeposit(ctx context.Context, return deposit, nil } -// getBlockHeight retrieves the block height of a given utxo. -func (m *Manager) getBlockHeight(ctx context.Context, - utxo *lnwallet.Utxo) (uint32, error) { +// confirmationHeightForUtxo derives the first confirmation height of a wallet +// UTXO from the manager's current block height. Unconfirmed UTXOs return 0. +func confirmationHeightForUtxo(currentHeight uint32, + utxo *lnwallet.Utxo) (int64, error) { - addressParams, err := m.cfg.AddressManager.GetStaticAddressParameters( - ctx, - ) - if err != nil { - return 0, fmt.Errorf("couldn't get confirmation height for "+ - "deposit, %w", err) + if utxo.Confirmations <= 0 { + return 0, nil } - notifChan, errChan, err := - m.cfg.ChainNotifier.RegisterConfirmationsNtfn( - ctx, &utxo.OutPoint.Hash, addressParams.PkScript, - MinConfs, addressParams.InitiationHeight, + if currentHeight == 0 { + return 0, errors.New("current block height unavailable") + } + + firstConfirmationHeight := int64(currentHeight) - utxo.Confirmations + 1 + if firstConfirmationHeight <= 0 { + return 0, fmt.Errorf("invalid confirmation height %d for %v "+ + "with current height %d and %d confirmations", + firstConfirmationHeight, utxo.OutPoint, currentHeight, + utxo.Confirmations) + } + + return firstConfirmationHeight, nil +} + +// updateDepositConfirmations syncs first confirmation heights for deposits that +// are visible in lnd's wallet view. +func (m *Manager) updateDepositConfirmations(ctx context.Context, + utxos []*lnwallet.Utxo, currentHeight uint32) error { + + for _, utxo := range utxos { + m.mu.Lock() + deposit, ok := m.deposits[utxo.OutPoint] + m.mu.Unlock() + if !ok { + continue + } + + err := func() error { + deposit.Lock() + defer deposit.Unlock() + + previousConfirmationHeight := deposit.ConfirmationHeight + + confirmationHeight, err := confirmationHeightForUtxo( + currentHeight, utxo, + ) + if err != nil { + return err + } + + if deposit.ConfirmationHeight == confirmationHeight { + return nil + } + + deposit.ConfirmationHeight = confirmationHeight + + err = m.cfg.Store.UpdateDeposit(ctx, deposit) + if err != nil { + deposit.ConfirmationHeight = previousConfirmationHeight + + return err + } + + return nil + }() + if err != nil { + return err + } + } + + return nil +} + +// syncActiveDeposits reconciles the live active set with lnd's current wallet +// view. Known Deposited records that are visible but inactive become active +// again, and active Deposited records that are no longer wallet-visible are +// removed from the live set. The DB record is left untouched as historical +// evidence that the outpoint was once detected. +func (m *Manager) syncActiveDeposits(ctx context.Context, + utxos []*lnwallet.Utxo) error { + + currentUtxos := make(map[wire.OutPoint]struct{}, len(utxos)) + for _, utxo := range utxos { + currentUtxos[utxo.OutPoint] = struct{}{} + } + + type deactivatedDeposit struct { + outpoint wire.OutPoint + fsm *FSM + } + + toActivate := make([]*Deposit, 0, len(utxos)) + var toDeactivate []deactivatedDeposit + func() { + m.mu.Lock() + defer m.mu.Unlock() + toDeactivate = make( + []deactivatedDeposit, 0, len(m.activeDeposits), ) - if err != nil { - return 0, err + + for _, utxo := range utxos { + deposit, ok := m.deposits[utxo.OutPoint] + if !ok { + continue + } + + if _, active := m.activeDeposits[utxo.OutPoint]; active { + continue + } + + if !deposit.IsInState(Deposited) { + continue + } + + toActivate = append(toActivate, deposit) + } + + for outpoint, fsm := range m.activeDeposits { + if _, ok := currentUtxos[outpoint]; ok { + continue + } + + if fsm == nil || fsm.deposit == nil { + continue + } + + if !fsm.deposit.IsInState(Deposited) { + continue + } + + delete(m.activeDeposits, outpoint) + toDeactivate = append(toDeactivate, deactivatedDeposit{ + outpoint: outpoint, + fsm: fsm, + }) + } + }() + + for _, deactivated := range toDeactivate { + deactivated.fsm.Stop() + + log.Infof("Removed vanished deposit %v from active set", + deactivated.outpoint) } - select { - case tx := <-notifChan: - return tx.BlockHeight, nil + for _, deposit := range toActivate { + if !deposit.IsInState(Deposited) { + continue + } - case err := <-errChan: - return 0, err + err := m.startDepositFsm(ctx, deposit) + if err != nil { + m.removeActiveDeposit(deposit.OutPoint) - case <-ctx.Done(): - return 0, ctx.Err() + return err + } + + log.Infof("Reactivated visible deposit %v", deposit.OutPoint) } + + return nil } // filterNewDeposits filters the given utxos for new deposits that we haven't @@ -412,12 +621,12 @@ func (m *Manager) GetActiveDepositsInState(stateFilter fsm.StateType) ( deposits = append(deposits, fsm.deposit) } - lockDeposits(deposits) - defer unlockDeposits(deposits) + lockedDeposits := lockDeposits(deposits) + defer unlockDeposits(lockedDeposits) filteredDeposits := make([]*Deposit, 0, len(deposits)) for _, d := range deposits { - if !d.IsInStateNoLock(stateFilter) { + if !d.isInStateNoLock(stateFilter) { continue } @@ -425,8 +634,8 @@ func (m *Manager) GetActiveDepositsInState(stateFilter fsm.StateType) ( } sort.Slice(filteredDeposits, func(i, j int) bool { - return filteredDeposits[i].ConfirmationHeight < - filteredDeposits[j].ConfirmationHeight + return filteredDeposits[i].GetConfirmationHeightNoLock() < + filteredDeposits[j].GetConfirmationHeightNoLock() }) return filteredDeposits, nil @@ -439,6 +648,10 @@ func (m *Manager) GetActiveDepositsInState(stateFilter fsm.StateType) ( func (m *Manager) AllOutpointsActiveDeposits(outpoints []wire.OutPoint, targetState fsm.StateType) ([]*Deposit, bool) { + if CheckDuplicates(outpoints) != nil { + return nil, false + } + m.mu.Lock() defer m.mu.Unlock() @@ -453,10 +666,10 @@ func (m *Manager) AllOutpointsActiveDeposits(outpoints []wire.OutPoint, return deposits, true } - lockDeposits(deposits) - defer unlockDeposits(deposits) + lockedDeposits := lockDeposits(deposits) + defer unlockDeposits(lockedDeposits) for _, d := range deposits { - if !d.IsInStateNoLock(targetState) { + if !d.isInStateNoLock(targetState) { return nil, false } } @@ -495,8 +708,15 @@ func (m *Manager) TransitionDeposits(ctx context.Context, deposits []*Deposit, outpoints := make([]wire.OutPoint, len(deposits)) for i, d := range deposits { + if d == nil { + return fmt.Errorf("nil deposit at index %d", i) + } + outpoints[i] = d.OutPoint } + if err := CheckDuplicates(outpoints); err != nil { + return fmt.Errorf("duplicate deposit outpoint: %w", err) + } m.mu.Lock() stateMachines, _ := m.toActiveDeposits(&outpoints) @@ -506,8 +726,16 @@ func (m *Manager) TransitionDeposits(ctx context.Context, deposits []*Deposit, return fmt.Errorf("deposits not found in active deposits") } - lockDeposits(deposits) - defer unlockDeposits(deposits) + lockedDeposits := lockDeposits(deposits) + defer unlockDeposits(lockedDeposits) + for _, deposit := range deposits { + if deposit.isInFinalStateNoLock() { + return fmt.Errorf("deposit %v is no longer active in "+ + "state %v", deposit.OutPoint, + deposit.getStateNoLock()) + } + } + for _, sm := range stateMachines { err := sm.SendEvent(ctx, event, nil) if err != nil { @@ -525,25 +753,84 @@ func (m *Manager) TransitionDeposits(ctx context.Context, deposits []*Deposit, return nil } -func lockDeposits(deposits []*Deposit) { - for _, d := range deposits { +// lockDeposits locks deposits in canonical outpoint order and returns the +// ordered slice that must be passed to unlockDeposits. +func lockDeposits(deposits []*Deposit) []*Deposit { + lockedDeposits := append([]*Deposit(nil), deposits...) + sort.Slice(lockedDeposits, func(i, j int) bool { + return lockedDeposits[i].OutPoint.String() < + lockedDeposits[j].OutPoint.String() + }) + + for _, d := range lockedDeposits { d.Lock() } + + return lockedDeposits } +// unlockDeposits unlocks deposits in reverse lock order. func unlockDeposits(deposits []*Deposit) { - for _, d := range deposits { + for i := len(deposits) - 1; i >= 0; i-- { + d := deposits[i] d.Unlock() } } -// GetAllDeposits returns all active deposits. +// removeActiveDeposit removes and stops the FSM for an active outpoint. +func (m *Manager) removeActiveDeposit(outpoint wire.OutPoint) { + m.mu.Lock() + fsm, ok := m.activeDeposits[outpoint] + if ok { + delete(m.activeDeposits, outpoint) + } + m.mu.Unlock() + + if ok { + fsm.Stop() + } +} + +// GetAllDeposits returns all known deposits from the database. func (m *Manager) GetAllDeposits(ctx context.Context) ([]*Deposit, error) { return m.cfg.Store.AllDeposits(ctx) } +// GetVisibleDeposits returns deposits that should be exposed through normal +// user-facing views. The database can contain historical Deposited rows whose +// outpoints are no longer present in lnd's current wallet view, for example +// after replacement or reorg. Once the manager has recovered its live cache, +// plain Deposited records are only visible while their outpoint is in the +// active set. +func (m *Manager) GetVisibleDeposits(ctx context.Context) ([]*Deposit, error) { + deposits, err := m.cfg.Store.AllDeposits(ctx) + if err != nil { + return nil, err + } + + m.mu.Lock() + defer m.mu.Unlock() + + liveCacheReady := len(m.deposits) > 0 + filtered := make([]*Deposit, 0, len(deposits)) + for _, d := range deposits { + if liveCacheReady && d.IsInState(Deposited) { + if _, ok := m.activeDeposits[d.OutPoint]; !ok { + continue + } + } + + filtered = append(filtered, d) + } + + return filtered, nil +} + // UpdateDeposit overrides all fields of the deposit with given ID in the store. func (m *Manager) UpdateDeposit(ctx context.Context, d *Deposit) error { + d.Lock() + defer d.Unlock() + return m.cfg.Store.UpdateDeposit(ctx, d) } diff --git a/staticaddr/deposit/manager_height_test.go b/staticaddr/deposit/manager_height_test.go new file mode 100644 index 00000000..78ad27bb --- /dev/null +++ b/staticaddr/deposit/manager_height_test.go @@ -0,0 +1,39 @@ +package deposit + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/stretchr/testify/require" +) + +// TestConfirmationHeightForUtxo verifies confirmation heights are derived from +// the current block height and wallet confirmation count. +func TestConfirmationHeightForUtxo(t *testing.T) { + t.Run("unconfirmed", func(t *testing.T) { + height, err := confirmationHeightForUtxo(0, &lnwallet.Utxo{}) + require.NoError(t, err) + require.Zero(t, height) + }) + + t.Run("confirmed", func(t *testing.T) { + height, err := confirmationHeightForUtxo(101, &lnwallet.Utxo{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{1}, + Index: 2, + }, + Confirmations: 6, + }) + require.NoError(t, err) + require.EqualValues(t, 96, height) + }) + + t.Run("invalid current height", func(t *testing.T) { + _, err := confirmationHeightForUtxo(2, &lnwallet.Utxo{ + Confirmations: 6, + }) + require.ErrorContains(t, err, "invalid confirmation height") + }) +} diff --git a/staticaddr/deposit/manager_reconcile_test.go b/staticaddr/deposit/manager_reconcile_test.go new file mode 100644 index 00000000..15b2f0a6 --- /dev/null +++ b/staticaddr/deposit/manager_reconcile_test.go @@ -0,0 +1,735 @@ +package deposit + +import ( + "context" + "errors" + "strings" + "sync" + "sync/atomic" + "testing" + "time" + + "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/loop/fsm" + "github.com/lightninglabs/loop/staticaddr/script" + "github.com/lightninglabs/loop/staticaddr/version" + "github.com/lightninglabs/loop/test" + "github.com/lightningnetwork/lnd/lnwallet" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" +) + +// TestReconcileDepositsSerialized verifies reconciliation is serialized across +// concurrent callers. +func TestReconcileDepositsSerialized(t *testing.T) { + ctx := context.Background() + mockLnd := test.NewMockLnd() + utxo := &lnwallet.Utxo{ + AddressType: lnwallet.TaprootPubkey, + Value: btcutil.Amount(100_000), + Confirmations: 0, + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{1}, + Index: 1, + }, + } + + mockAddressManager := new(mockAddressManager) + mockAddressManager.On( + "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), + ).Return([]*lnwallet.Utxo{utxo}, nil) + mockAddressManager.On( + "GetStaticAddressParameters", mock.Anything, + ).Return((*script.Parameters)(nil), errors.New("fsm init failed")) + + mockStore := new(mockStore) + var createCalls atomic.Int32 + createEntered := make(chan struct{}) + releaseCreate := make(chan struct{}) + mockStore.On( + "CreateDeposit", mock.Anything, mock.Anything, + ).Return(nil).Run(func(mock.Arguments) { + if createCalls.Add(1) == 1 { + close(createEntered) + } + + <-releaseCreate + }) + + manager := NewManager(&ManagerConfig{ + AddressManager: mockAddressManager, + Store: mockStore, + WalletKit: mockLnd.WalletKit, + Signer: mockLnd.Signer, + }) + + var wg sync.WaitGroup + wg.Add(2) + + errs := make(chan error, 2) + go func() { + defer wg.Done() + errs <- manager.reconcileDeposits(ctx) + }() + + <-createEntered + + go func() { + defer wg.Done() + errs <- manager.reconcileDeposits(ctx) + }() + + time.Sleep(100 * time.Millisecond) + close(releaseCreate) + wg.Wait() + close(errs) + + var gotErrs []error + for err := range errs { + gotErrs = append(gotErrs, err) + } + + require.EqualValues(t, 1, createCalls.Load()) + require.Len(t, manager.deposits, 1) + require.Empty(t, manager.activeDeposits) + require.Len(t, gotErrs, 2) + + var errCount int + for _, err := range gotErrs { + if err == nil { + continue + } + + errCount++ + errMsg := err.Error() + require.True( + t, + strings.Contains( + errMsg, "unable to start new deposit FSM", + ) || strings.Contains( + errMsg, "unable to sync active deposits", + ), + "unexpected error: %v", err, + ) + } + require.Equal(t, 2, errCount) +} + +// TestReconcileConfirmedDepositUsesCurrentHeight verifies confirmation heights +// are derived from the manager's current block height. +func TestReconcileConfirmedDepositUsesCurrentHeight(t *testing.T) { + ctx := context.Background() + mockLnd := test.NewMockLnd() + utxo := &lnwallet.Utxo{ + AddressType: lnwallet.TaprootPubkey, + Value: btcutil.Amount(100_000), + Confirmations: 3, + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{8}, + Index: 1, + }, + } + + mockAddressManager := new(mockAddressManager) + mockAddressManager.On( + "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), + ).Return([]*lnwallet.Utxo{utxo}, nil) + mockAddressManager.On( + "GetStaticAddressParameters", mock.Anything, + ).Return((*script.Parameters)(nil), errors.New("fsm init failed")) + + mockStore := new(mockStore) + mockStore.On( + "CreateDeposit", mock.Anything, mock.Anything, + ).Return(nil).Run(func(args mock.Arguments) { + createdDeposit := args.Get(1).(*Deposit) + require.EqualValues(t, 98, createdDeposit.ConfirmationHeight) + }) + + manager := NewManager(&ManagerConfig{ + AddressManager: mockAddressManager, + Store: mockStore, + WalletKit: mockLnd.WalletKit, + Signer: mockLnd.Signer, + }) + manager.currentHeight.Store(100) + + err := manager.reconcileDeposits(ctx) + require.ErrorContains(t, err, "unable to start new deposit FSM") +} + +// TestUpdateDepositConfirmationsResetsReorgedDeposit verifies that a deposit +// which remains wallet-visible but loses confirmations has its confirmation +// height reset. This can happen if a confirmed transaction is reorged back into +// the mempool. +func TestUpdateDepositConfirmationsResetsReorgedDeposit(t *testing.T) { + ctx := context.Background() + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{7}, + Index: 2, + } + + deposit := &Deposit{ + OutPoint: outpoint, + ConfirmationHeight: 99, + } + deposit.SetState(Deposited) + + utxo := &lnwallet.Utxo{ + OutPoint: outpoint, + Confirmations: 0, + } + + mockStore := new(mockStore) + mockStore.On( + "UpdateDeposit", mock.Anything, mock.Anything, + ).Return(nil).Run(func(args mock.Arguments) { + updatedDeposit := args.Get(1).(*Deposit) + require.Zero(t, updatedDeposit.ConfirmationHeight) + }) + + manager := NewManager(&ManagerConfig{ + Store: mockStore, + }) + manager.deposits[outpoint] = deposit + + err := manager.updateDepositConfirmations(ctx, []*lnwallet.Utxo{utxo}, 0) + require.NoError(t, err) + require.Zero(t, deposit.ConfirmationHeight) + mockStore.AssertExpectations(t) +} + +// TestUpdateDepositConfirmationsRecomputesPositiveHeight verifies that a +// deposit which is confirmed again at a different height after a reorg does +// not retain its stale, positive confirmation height. +func TestUpdateDepositConfirmationsRecomputesPositiveHeight(t *testing.T) { + ctx := context.Background() + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{10}, + Index: 3, + } + + deposit := &Deposit{ + OutPoint: outpoint, + ConfirmationHeight: 100, + } + deposit.SetState(Deposited) + + utxo := &lnwallet.Utxo{ + OutPoint: outpoint, + Confirmations: 2, + } + + mockStore := new(mockStore) + mockStore.On( + "UpdateDeposit", mock.Anything, mock.Anything, + ).Return(nil).Run(func(args mock.Arguments) { + updatedDeposit := args.Get(1).(*Deposit) + require.EqualValues(t, 109, updatedDeposit.ConfirmationHeight) + }) + + manager := NewManager(&ManagerConfig{ + Store: mockStore, + }) + manager.deposits[outpoint] = deposit + + err := manager.updateDepositConfirmations( + ctx, []*lnwallet.Utxo{utxo}, 110, + ) + require.NoError(t, err) + require.EqualValues(t, 109, deposit.ConfirmationHeight) + mockStore.AssertExpectations(t) +} + +// TestReconcileDepositsDeactivatesVanishedUnconfirmedDeposit verifies that a +// missing wallet outpoint is removed from the live active set without mutating +// its historical DB state. +func TestReconcileDepositsDeactivatesVanishedUnconfirmedDeposit(t *testing.T) { + ctx := t.Context() + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{2}, + Index: 7, + } + + deposit := &Deposit{ + OutPoint: outpoint, + } + deposit.SetState(Deposited) + + mockAddressManager := new(mockAddressManager) + mockAddressManager.On( + "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), + ).Return([]*lnwallet.Utxo{}, nil) + + manager := NewManager(&ManagerConfig{ + AddressManager: mockAddressManager, + Store: new(mockStore), + }) + manager.deposits[outpoint] = deposit + fsm := &FSM{ + deposit: deposit, + stopChan: make(chan struct{}), + quitChan: make(chan struct{}), + } + go func() { + <-fsm.stopChan + close(fsm.quitChan) + }() + manager.activeDeposits[outpoint] = fsm + + require.NoError(t, manager.reconcileDeposits(ctx)) + require.Equal(t, Deposited, deposit.GetState()) + require.Empty(t, manager.activeDeposits) + select { + case <-fsm.quitChan: + + case <-time.After(time.Second): + t.Fatal("fsm did not stop after deposit vanished") + } +} + +// TestReconcileDepositsDeactivatesVanishedConfirmedDeposit verifies that a +// previously confirmed deposit is also removed from the live active set if it +// vanishes from the wallet view. +func TestReconcileDepositsDeactivatesVanishedConfirmedDeposit(t *testing.T) { + ctx := context.Background() + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{9}, + Index: 4, + } + + deposit := &Deposit{ + OutPoint: outpoint, + ConfirmationHeight: 123, + } + deposit.SetState(Deposited) + + mockAddressManager := new(mockAddressManager) + mockAddressManager.On( + "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), + ).Return([]*lnwallet.Utxo{}, nil) + + manager := NewManager(&ManagerConfig{ + AddressManager: mockAddressManager, + Store: new(mockStore), + }) + manager.deposits[outpoint] = deposit + fsm := &FSM{ + deposit: deposit, + stopChan: make(chan struct{}), + quitChan: make(chan struct{}), + } + go func() { + <-fsm.stopChan + close(fsm.quitChan) + }() + manager.activeDeposits[outpoint] = fsm + + require.NoError(t, manager.reconcileDeposits(ctx)) + require.Equal(t, Deposited, deposit.GetState()) + require.EqualValues(t, 123, deposit.ConfirmationHeight) + require.Empty(t, manager.activeDeposits) + select { + case <-fsm.quitChan: + + case <-time.After(time.Second): + t.Fatal("fsm did not stop after confirmed deposit vanished") + } +} + +// TestAllOutpointsActiveDepositsRejectsDuplicateOutpoints verifies that a +// duplicated selection is rejected before the manager tries to lock the same +// deposit twice. +func TestAllOutpointsActiveDepositsRejectsDuplicateOutpoints(t *testing.T) { + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{12}, + Index: 6, + } + + deposit := &Deposit{ + OutPoint: outpoint, + } + deposit.SetState(Deposited) + + manager := NewManager(&ManagerConfig{}) + manager.deposits[outpoint] = deposit + manager.activeDeposits[outpoint] = &FSM{ + deposit: deposit, + } + + deposits, ok := manager.AllOutpointsActiveDeposits( + []wire.OutPoint{outpoint, outpoint}, Deposited, + ) + require.False(t, ok) + require.Nil(t, deposits) +} + +// TestTransitionDepositsRejectsDuplicateOutpoints verifies that transition +// callers cannot deadlock the manager by passing the same deposit twice. +func TestTransitionDepositsRejectsDuplicateOutpoints(t *testing.T) { + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{13}, + Index: 6, + } + + deposit := &Deposit{ + OutPoint: outpoint, + } + deposit.SetState(Deposited) + + manager := NewManager(&ManagerConfig{}) + err := manager.TransitionDeposits( + t.Context(), []*Deposit{deposit, deposit}, OnLoopInInitiated, + LoopingIn, + ) + require.ErrorContains(t, err, "duplicate deposit outpoint") + require.Equal(t, Deposited, deposit.GetState()) +} + +// TestLockDepositsCanonicalizesOutpoints verifies that lockDeposits takes a +// canonical copy of the caller's slice so overlapping multi-deposit operations +// cannot lock deposits in conflicting request orders. +func TestLockDepositsCanonicalizesOutpoints(t *testing.T) { + depositA := &Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{1}, + Index: 0, + }, + } + depositB := &Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{2}, + Index: 0, + }, + } + + deposits := []*Deposit{depositB, depositA} + lockedDeposits := lockDeposits(deposits) + defer unlockDeposits(lockedDeposits) + + require.Equal(t, []*Deposit{depositA, depositB}, lockedDeposits) + require.Equal(t, []*Deposit{depositB, depositA}, deposits) +} + +// TestLockDepositsAllowsReversedConcurrentRequests exercises the reviewer +// case where overlapping callers request the same deposits in opposite orders. +func TestLockDepositsAllowsReversedConcurrentRequests(t *testing.T) { + depositA := &Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{3}, + Index: 0, + }, + } + depositB := &Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{4}, + Index: 0, + }, + } + + start := make(chan struct{}) + done := make(chan struct{}, 2) + lockAndUnlock := func(deposits []*Deposit) { + <-start + + for range 100 { + lockedDeposits := lockDeposits(deposits) + unlockDeposits(lockedDeposits) + } + + done <- struct{}{} + } + + go lockAndUnlock([]*Deposit{depositA, depositB}) + go lockAndUnlock([]*Deposit{depositB, depositA}) + + close(start) + + for range 2 { + select { + case <-done: + + case <-time.After(time.Second): + t.Fatal("reversed deposit lock requests deadlocked") + } + } +} + +// TestReconcileDepositsReactivatesReappearedDeposit verifies that the same +// outpoint can become active again if lnd reports it after a prior wallet-view +// miss. +func TestReconcileDepositsReactivatesReappearedDeposit(t *testing.T) { + ctx := context.Background() + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{3}, + Index: 5, + } + + deposit := &Deposit{ + OutPoint: outpoint, + Value: btcutil.Amount(100_000), + ConfirmationHeight: 77, + } + deposit.SetState(Deposited) + + utxo := &lnwallet.Utxo{ + OutPoint: outpoint, + Value: deposit.Value, + Confirmations: 0, + } + + mockAddressManager := new(mockAddressManager) + mockAddressManager.On( + "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), + ).Return([]*lnwallet.Utxo{utxo}, nil) + mockAddressManager.On( + "GetStaticAddressParameters", mock.Anything, + ).Return(&script.Parameters{ + ProtocolVersion: version.ProtocolVersion_V0, + }, nil) + mockAddressManager.On( + "GetStaticAddress", mock.Anything, + ).Return((*script.StaticAddress)(nil), nil) + + mockStore := new(mockStore) + var updateStates []fsm.StateType + mockStore.On( + "UpdateDeposit", mock.Anything, mock.Anything, + ).Return(nil).Run(func(args mock.Arguments) { + updatedDeposit := args.Get(1).(*Deposit) + updateStates = append(updateStates, updatedDeposit.state) + if updatedDeposit.isInStateNoLock(Deposited) { + require.Zero(t, updatedDeposit.ConfirmationHeight) + } + }) + + manager := NewManager(&ManagerConfig{ + AddressManager: mockAddressManager, + Store: mockStore, + }) + manager.deposits[outpoint] = deposit + + // Reconciliation should reactivate the existing record instead of + // creating a second deposit entry for the same outpoint. + require.NoError(t, manager.reconcileDeposits(ctx)) + require.Equal(t, Deposited, deposit.GetState()) + require.Zero(t, deposit.ConfirmationHeight) + require.Len(t, manager.activeDeposits, 1) + require.Equal(t, []fsm.StateType{Deposited}, updateStates) +} + +// TestReconcileDepositsKeepsInactiveOnFSMStartFailure verifies that a failed +// reactivation does not leave memory saying a deposit is active without an FSM. +func TestReconcileDepositsKeepsInactiveOnFSMStartFailure(t *testing.T) { + ctx := context.Background() + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{11}, + Index: 5, + } + + deposit := &Deposit{ + OutPoint: outpoint, + Value: btcutil.Amount(100_000), + ConfirmationHeight: 77, + } + deposit.SetState(Deposited) + + utxo := &lnwallet.Utxo{ + OutPoint: outpoint, + Value: deposit.Value, + Confirmations: 0, + } + + mockAddressManager := new(mockAddressManager) + mockAddressManager.On( + "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), + ).Return([]*lnwallet.Utxo{utxo}, nil) + mockAddressManager.On( + "GetStaticAddressParameters", mock.Anything, + ).Return((*script.Parameters)(nil), errors.New("fsm init failed")) + + var ( + updateStates []fsm.StateType + updateHeights []int64 + ) + mockStore := new(mockStore) + mockStore.On( + "UpdateDeposit", mock.Anything, mock.Anything, + ).Return(nil).Run(func(args mock.Arguments) { + updatedDeposit := args.Get(1).(*Deposit) + updateStates = append(updateStates, updatedDeposit.state) + updateHeights = append( + updateHeights, updatedDeposit.ConfirmationHeight, + ) + }) + + manager := NewManager(&ManagerConfig{ + AddressManager: mockAddressManager, + Store: mockStore, + }) + manager.deposits[outpoint] = deposit + + err := manager.reconcileDeposits(ctx) + require.ErrorContains(t, err, "unable to sync active deposits") + require.Equal(t, Deposited, deposit.GetState()) + require.Zero(t, deposit.ConfirmationHeight) + require.Empty(t, manager.activeDeposits) + require.Equal(t, []fsm.StateType{Deposited}, updateStates) + require.EqualValues(t, []int64{0}, updateHeights) +} + +// TestReconcileDepositsDeactivatesBeforeActivationFailure verifies that a +// failed reactivation of one visible deposit does not leave another vanished +// deposit in the live active set. +func TestReconcileDepositsDeactivatesBeforeActivationFailure(t *testing.T) { + ctx := context.Background() + visibleOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{21}, + Index: 5, + } + vanishedOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{22}, + Index: 6, + } + + visibleDeposit := &Deposit{ + OutPoint: visibleOutpoint, + Value: btcutil.Amount(100_000), + } + visibleDeposit.SetState(Deposited) + + vanishedDeposit := &Deposit{ + OutPoint: vanishedOutpoint, + Value: btcutil.Amount(100_000), + } + vanishedDeposit.SetState(Deposited) + + utxo := &lnwallet.Utxo{ + OutPoint: visibleOutpoint, + Value: visibleDeposit.Value, + Confirmations: 0, + } + + mockAddressManager := new(mockAddressManager) + mockAddressManager.On( + "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), + ).Return([]*lnwallet.Utxo{utxo}, nil) + mockAddressManager.On( + "GetStaticAddressParameters", mock.Anything, + ).Return((*script.Parameters)(nil), errors.New("fsm init failed")) + + manager := NewManager(&ManagerConfig{ + AddressManager: mockAddressManager, + Store: new(mockStore), + }) + manager.deposits[visibleOutpoint] = visibleDeposit + manager.deposits[vanishedOutpoint] = vanishedDeposit + + vanishedFsm := &FSM{ + deposit: vanishedDeposit, + stopChan: make(chan struct{}), + quitChan: make(chan struct{}), + } + go func() { + <-vanishedFsm.stopChan + close(vanishedFsm.quitChan) + }() + manager.activeDeposits[vanishedOutpoint] = vanishedFsm + + err := manager.reconcileDeposits(ctx) + require.ErrorContains(t, err, "unable to sync active deposits") + require.Empty(t, manager.activeDeposits) + + select { + case <-vanishedFsm.quitChan: + + case <-time.After(time.Second): + t.Fatal("vanished deposit fsm did not stop") + } +} + +// TestReconcileReplacementDepositCreatesNewDeposit ensures that a replacement +// UTXO is retained as a new deposit while an in-flight deposit remains tied to +// the outpoint selected by a loop-in. +func TestReconcileReplacementDepositCreatesNewDeposit(t *testing.T) { + ctx := context.Background() + mockLnd := test.NewMockLnd() + oldOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{4}, + Index: 8, + } + newOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{5}, + Index: 9, + } + + depositID, err := GetRandomDepositID() + require.NoError(t, err) + + deposit := &Deposit{ + ID: depositID, + OutPoint: oldOutpoint, + Value: btcutil.Amount(100_000), + } + deposit.SetState(LoopingIn) + + utxo := &lnwallet.Utxo{ + OutPoint: newOutpoint, + Value: deposit.Value, + Confirmations: 0, + } + + mockAddressManager := new(mockAddressManager) + mockAddressManager.On( + "ListUnspent", mock.Anything, int32(0), int32(MaxConfs), + ).Return([]*lnwallet.Utxo{utxo}, nil) + mockAddressManager.On( + "GetStaticAddressParameters", mock.Anything, + ).Return(&script.Parameters{ + ProtocolVersion: version.ProtocolVersion_V0, + }, nil) + mockAddressManager.On( + "GetStaticAddress", mock.Anything, + ).Return((*script.StaticAddress)(nil), nil) + + mockStore := new(mockStore) + var createdDeposit *Deposit + mockStore.On( + "CreateDeposit", mock.Anything, mock.Anything, + ).Return(nil).Run(func(args mock.Arguments) { + createdDeposit = args.Get(1).(*Deposit) + }) + + manager := NewManager(&ManagerConfig{ + AddressManager: mockAddressManager, + Store: mockStore, + WalletKit: mockLnd.WalletKit, + Signer: mockLnd.Signer, + }) + manager.deposits[oldOutpoint] = deposit + fsm := &FSM{} + manager.activeDeposits[oldOutpoint] = fsm + + require.NoError(t, manager.reconcileDeposits(ctx)) + + require.Same(t, deposit, manager.deposits[oldOutpoint]) + require.Equal(t, oldOutpoint, deposit.OutPoint) + require.Equal(t, LoopingIn, deposit.GetState()) + + replacement, ok := manager.deposits[newOutpoint] + require.True(t, ok) + require.Same(t, createdDeposit, replacement) + require.NotEqual(t, depositID, replacement.ID) + require.Equal(t, newOutpoint, replacement.OutPoint) + require.Equal(t, Deposited, replacement.GetState()) + require.Zero(t, replacement.ConfirmationHeight) + + require.Same(t, fsm, manager.activeDeposits[oldOutpoint]) + require.NotSame(t, fsm, manager.activeDeposits[newOutpoint]) + + mockStore.AssertNotCalled( + t, "UpdateDeposit", mock.Anything, mock.Anything, + ) +} diff --git a/staticaddr/deposit/manager_test.go b/staticaddr/deposit/manager_test.go index 0ab79302..78663f9a 100644 --- a/staticaddr/deposit/manager_test.go +++ b/staticaddr/deposit/manager_test.go @@ -3,6 +3,7 @@ package deposit import ( "context" "encoding/hex" + "errors" "testing" "time" @@ -71,6 +72,10 @@ func (m *mockStaticAddressClient) PushStaticAddressHtlcSigs(ctx context.Context, args.Error(1) } +// ServerWithdrawDeposits implements the deprecated RPC required by the +// generated client interface. Production code uses ServerPsbtWithdrawDeposits. +// +//nolint:staticcheck func (m *mockStaticAddressClient) ServerWithdrawDeposits(ctx context.Context, in *swapserverrpc.ServerWithdrawRequest, opts ...grpc.CallOption) (*swapserverrpc.ServerWithdrawResponse, @@ -129,11 +134,30 @@ func (m *mockAddressManager) ListUnspent(ctx context.Context, minConfs, maxConfs int32) ([]*lnwallet.Utxo, error) { args := m.Called(ctx, minConfs, maxConfs) + if listUnspent, ok := args.Get(0).(func() []*lnwallet.Utxo); ok { + return listUnspent(), args.Error(1) + } return args.Get(0).([]*lnwallet.Utxo), args.Error(1) } +// listUnspentOverride delegates all address-manager methods except +// ListUnspent to another implementation. +type listUnspentOverride struct { + AddressManager + + listUnspent func(context.Context, int32, int32) ([]*lnwallet.Utxo, + error) +} + +// ListUnspent calls the override's ListUnspent implementation. +func (l *listUnspentOverride) ListUnspent(ctx context.Context, + minConfs, maxConfs int32) ([]*lnwallet.Utxo, error) { + + return l.listUnspent(ctx, minConfs, maxConfs) +} + func (m *mockAddressManager) GetTaprootAddress(clientPubkey, serverPubkey *btcec.PublicKey, expiry int64) (*btcutil.AddressTaproot, error) { @@ -233,6 +257,10 @@ func TestManager(t *testing.T) { runErrChan <- testContext.manager.Run(ctx, initChan) }() + // Send an initial block so the manager can proceed past its startup + // block wait. + testContext.blockChan <- int32(defaultDepositConfirmations) + // Ensure that the manager has been initialized. select { case <-initChan: @@ -303,6 +331,156 @@ func TestManager(t *testing.T) { } } +// TestManagerReplaysStartupBlockToRecoveredDeposits verifies that the initial +// block epoch consumed during startup is delivered to recovered deposit FSMs. +func TestManagerReplaysStartupBlockToRecoveredDeposits(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + const defaultTimeout = 30 * time.Second + + testContext := newManagerTestContext(t) + + initChan := make(chan struct{}) + runErrChan := make(chan error, 1) + go func() { + runErrChan <- testContext.manager.Run(ctx, initChan) + }() + + // Send only the startup block at the recovered deposit's expiry height. + testContext.blockChan <- int32( + defaultDepositConfirmations + defaultExpiry, + ) + + select { + case <-initChan: + + case err := <-runErrChan: + require.NoError(t, err, "manager failed to start") + + case <-time.After(defaultTimeout): + t.Fatal("manager timed out starting") + } + + select { + case <-testContext.mockLnd.SignOutputRawChannel: + + case <-time.After(defaultTimeout): + t.Fatal("did not receive sign request") + } + + select { + case <-testContext.mockLnd.TxPublishChannel: + + case <-time.After(defaultTimeout): + t.Fatal("did not receive published expiry tx") + } + + cancel() + select { + case err := <-runErrChan: + require.ErrorIs(t, err, context.Canceled) + + case <-time.After(defaultTimeout): + t.Fatal("manager did not stop") + } +} + +// TestManagerSkipsExpiryNotificationOnReconcileFailure verifies that deposit +// FSMs cannot make an expiry decision from stale confirmation data when wallet +// reconciliation fails at startup or while processing a later block. +func TestManagerSkipsExpiryNotificationOnReconcileFailure(t *testing.T) { + testCases := []struct { + name string + startupHeight int32 + blockHeight int32 + }{ + { + name: "startup", + startupHeight: int32( + defaultDepositConfirmations + defaultExpiry, + ), + }, + { + name: "block", + startupHeight: int32(defaultDepositConfirmations), + blockHeight: int32( + defaultDepositConfirmations + defaultExpiry, + ), + }, + } + + for _, testCase := range testCases { + t.Run(testCase.name, func(t *testing.T) { + ctx, cancel := context.WithCancel(t.Context()) + defer cancel() + + testContext := newManagerTestContext(t) + baseAddressManager := testContext.mockAddressManager + var listUnspentCalls int + testContext.manager.cfg.AddressManager = + &listUnspentOverride{ + AddressManager: baseAddressManager, + listUnspent: func(ctx context.Context, + minConfs, maxConfs int32) ( + []*lnwallet.Utxo, error) { + + listUnspentCalls++ + if testCase.blockHeight != 0 && + listUnspentCalls == 1 { + + return baseAddressManager.ListUnspent( + ctx, minConfs, maxConfs, + ) + } + + return nil, errors.New( + "injected reconciliation failure", + ) + }, + } + + initChan := make(chan struct{}) + runErrChan := make(chan error, 1) + go func() { + runErrChan <- testContext.manager.Run(ctx, initChan) + }() + + testContext.blockChan <- testCase.startupHeight + + select { + case <-initChan: + + case err := <-runErrChan: + require.NoError(t, err, "manager failed to start") + + case <-time.After(time.Second): + t.Fatal("manager timed out starting") + } + + if testCase.blockHeight != 0 { + testContext.blockChan <- testCase.blockHeight + } + + select { + case <-testContext.mockLnd.SignOutputRawChannel: + t.Fatal("expiry sweep signed with stale deposit data") + + case <-time.After(200 * time.Millisecond): + } + + cancel() + select { + case err := <-runErrChan: + require.ErrorIs(t, err, context.Canceled) + + case <-time.After(time.Second): + t.Fatal("manager did not stop") + } + }) + } +} + // ManagerTestContext is a helper struct that contains all the necessary // components to test the reservation manager. type ManagerTestContext struct { @@ -362,6 +540,7 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext { "UpdateDeposit", mock.Anything, mock.Anything, ).Return(nil) + var manager *Manager mockAddressManager.On( "GetStaticAddressParameters", mock.Anything, ).Return(&script.Parameters{ @@ -370,7 +549,19 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext { mockAddressManager.On( "ListUnspent", mock.Anything, mock.Anything, mock.Anything, - ).Return([]*lnwallet.Utxo{utxo}, nil) + ).Return(func() []*lnwallet.Utxo { + currentUtxo := *utxo + currentHeight := manager.currentHeight.Load() + if currentHeight < defaultDepositConfirmations { + currentUtxo.Confirmations = 0 + } else { + currentUtxo.Confirmations = int64( + currentHeight - defaultDepositConfirmations + 1, + ) + } + + return []*lnwallet.Utxo{¤tUtxo} + }, nil) // Define the expected return values for the mocks. mockChainNotifier.On( @@ -390,7 +581,7 @@ func newManagerTestContext(t *testing.T) *ManagerTestContext { Signer: mockLnd.Signer, } - manager := NewManager(cfg) + manager = NewManager(cfg) testContext := &ManagerTestContext{ manager: manager, diff --git a/staticaddr/deposit/outpoint.go b/staticaddr/deposit/outpoint.go new file mode 100644 index 00000000..64a3a701 --- /dev/null +++ b/staticaddr/deposit/outpoint.go @@ -0,0 +1,21 @@ +package deposit + +import ( + "fmt" + + "github.com/btcsuite/btcd/wire" +) + +// CheckDuplicates returns an error if the outpoint list contains duplicates. +func CheckDuplicates(outpoints []wire.OutPoint) error { + seen := make(map[wire.OutPoint]struct{}, len(outpoints)) + for _, outpoint := range outpoints { + if _, ok := seen[outpoint]; ok { + return fmt.Errorf("duplicate outpoint %v", outpoint) + } + + seen[outpoint] = struct{}{} + } + + return nil +} diff --git a/staticaddr/deposit/outpoint_test.go b/staticaddr/deposit/outpoint_test.go new file mode 100644 index 00000000..fff86458 --- /dev/null +++ b/staticaddr/deposit/outpoint_test.go @@ -0,0 +1,40 @@ +package deposit + +import ( + "testing" + + "github.com/btcsuite/btcd/chaincfg/chainhash" + "github.com/btcsuite/btcd/wire" + "github.com/stretchr/testify/require" +) + +func TestCheckDuplicates(t *testing.T) { + duplicate := wire.OutPoint{ + Hash: chainhash.Hash{1}, + Index: 2, + } + + outpoints := []wire.OutPoint{{ + Hash: chainhash.Hash{3}, + Index: 4, + }, duplicate, { + Hash: chainhash.Hash{5}, + Index: 6, + }, duplicate} + + err := CheckDuplicates(outpoints) + require.ErrorContains(t, err, "duplicate outpoint") + require.ErrorContains(t, err, duplicate.String()) +} + +func TestCheckDuplicatesNoDuplicate(t *testing.T) { + outpoints := []wire.OutPoint{{ + Hash: chainhash.Hash{1}, + Index: 2, + }, { + Hash: chainhash.Hash{3}, + Index: 4, + }} + + require.NoError(t, CheckDuplicates(outpoints)) +} diff --git a/staticaddr/deposit/sql_store.go b/staticaddr/deposit/sql_store.go index d9f4249f..a49550e5 100644 --- a/staticaddr/deposit/sql_store.go +++ b/staticaddr/deposit/sql_store.go @@ -47,7 +47,7 @@ func (s *SqlStore) CreateDeposit(ctx context.Context, deposit *Deposit) error { TxHash: deposit.Hash[:], OutIndex: int32(deposit.Index), Amount: int64(deposit.Value), - ConfirmationHeight: deposit.ConfirmationHeight, + ConfirmationHeight: deposit.GetConfirmationHeight(), TimeoutSweepPkScript: deposit.TimeOutSweepPkScript, } @@ -69,11 +69,15 @@ func (s *SqlStore) CreateDeposit(ctx context.Context, deposit *Deposit) error { } // UpdateDeposit updates the deposit in the database. +// +// Callers that pass a live deposit must hold the deposit lock while calling +// this method. The deposit FSM already does this for state transitions, and +// Manager.UpdateDeposit wraps external callers with the same lock. func (s *SqlStore) UpdateDeposit(ctx context.Context, deposit *Deposit) error { insertUpdateArgs := sqlc.InsertDepositUpdateParams{ DepositID: deposit.ID[:], UpdateTimestamp: s.clock.Now().UTC(), - UpdateState: string(deposit.state), + UpdateState: string(deposit.getStateNoLock()), } var ( @@ -83,7 +87,7 @@ func (s *SqlStore) UpdateDeposit(ctx context.Context, deposit *Deposit) error { Valid: true, } confirmationHeight = sql.NullInt64{ - Int64: deposit.ConfirmationHeight, + Int64: deposit.GetConfirmationHeightNoLock(), } ) diff --git a/staticaddr/loopin/actions.go b/staticaddr/loopin/actions.go index 70a27811..3dc1c029 100644 --- a/staticaddr/loopin/actions.go +++ b/staticaddr/loopin/actions.go @@ -12,6 +12,7 @@ import ( "github.com/btcsuite/btcd/btcec/v2/schnorr/musig2" "github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/txscript" + "github.com/btcsuite/btcd/wire" "github.com/btcsuite/btcwallet/chain" "github.com/lightninglabs/lndclient" "github.com/lightninglabs/loop" @@ -36,6 +37,10 @@ const ( defaultConfTarget = 3 DefaultPaymentTimeoutSeconds = 60 + + defaultInvoiceCleanupTimeout = 5 * time.Second + + monitorRetryDelay = time.Second ) var ( @@ -57,6 +62,27 @@ var ( func (f *FSM) InitHtlcAction(ctx context.Context, _ fsm.EventContext) fsm.EventType { + var event fsm.EventType + invoiceNeedsCleanup := false + defer func() { + // If we created the private invoice but failed before persisting the + // swap, cancel it so retries do not accumulate orphan invoices. + if !invoiceNeedsCleanup || event != fsm.OnError { + return + } + + if err := f.cancelSwapInvoice(); err != nil { + f.Warnf("unable to clean up invoice for swap %v: %v", + f.loopIn.SwapHash, err) + } + }() + + returnError := func(err error) fsm.EventType { + event = f.HandleError(err) + + return event + } + // Lock the deposits and transition them to the LoopingIn state. err := f.cfg.DepositManager.TransitionDeposits( ctx, f.loopIn.Deposits, deposit.OnLoopInInitiated, @@ -65,20 +91,21 @@ func (f *FSM) InitHtlcAction(ctx context.Context, if err != nil { err = fmt.Errorf("unable to loop-in deposits: %w", err) - return f.HandleError(err) + return returnError(err) } // Calculate the swap invoice amount. The server needs to pay us the // swap amount minus the fees that the server charges for the swap. The // swap amount is either the total value of the selected deposits, or // the selected amount if a specific amount was requested. - swapAmount := f.loopIn.TotalDepositAmount() + totalDepositAmount := f.loopIn.TotalDepositAmount() + swapAmount := totalDepositAmount + var changeAmount btcutil.Amount var hasChange bool if f.loopIn.SelectedAmount > 0 { swapAmount = f.loopIn.SelectedAmount - remainingAmount := f.loopIn.TotalDepositAmount() - swapAmount - hasChange = remainingAmount > 0 && remainingAmount < - f.loopIn.TotalDepositAmount() + changeAmount = totalDepositAmount - swapAmount + hasChange = changeAmount > 0 && changeAmount < totalDepositAmount } swapInvoiceAmt := swapAmount - f.loopIn.QuotedSwapFee @@ -88,7 +115,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context, err = fmt.Errorf("unable to create random swap preimage: %w", err) - return f.HandleError(err) + return returnError(err) } f.loopIn.SwapPreimage = swapPreimage f.loopIn.SwapHash = swapPreimage.Hash() @@ -100,7 +127,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context, if err != nil { err = fmt.Errorf("unable to derive client htlc key: %w", err) - return f.HandleError(err) + return returnError(err) } f.loopIn.ClientPubkey = keyDesc.PubKey f.loopIn.HtlcKeyLocator = keyDesc.KeyLocator @@ -119,10 +146,14 @@ func (f *FSM) InitHtlcAction(ctx context.Context, if err != nil { err = fmt.Errorf("unable to create swap invoice: %w", err) - return f.HandleError(err) + return returnError(err) } f.loopIn.SwapInvoice = swapInvoice + // From here until CreateLoopIn succeeds, any error path would otherwise + // leave behind a live invoice with no persisted swap to recover it. + invoiceNeedsCleanup = true + f.loopIn.ProtocolVersion = version.AddressProtocolVersion( version.CurrentRPCProtocolVersion(), ) @@ -149,7 +180,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context, err = fmt.Errorf("unable to initiate the loop-in with the "+ "server: %w", err) - return f.HandleError(err) + return returnError(err) } // Pushing empty sigs signals the server that we abandoned the swap @@ -171,7 +202,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context, pushEmptySigs() err = fmt.Errorf("unable to parse server pubkey: %w", err) - return f.HandleError(err) + return returnError(err) } f.loopIn.ServerPubkey = serverPubkey @@ -185,7 +216,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context, err = fmt.Errorf("server response parameters are outside "+ "our allowed range: %w", err) - return f.HandleError(err) + return returnError(err) } f.loopIn.HtlcCltvExpiry = loopInResp.HtlcExpiry @@ -194,7 +225,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context, pushEmptySigs() err = fmt.Errorf("unable to convert server nonces: %w", err) - return f.HandleError(err) + return returnError(err) } f.htlcServerNoncesHighFee, err = toNonces( loopInResp.HighFeeHtlcInfo.Nonces, @@ -202,7 +233,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context, if err != nil { pushEmptySigs() - return f.HandleError(err) + return returnError(err) } f.htlcServerNoncesExtremelyHighFee, err = toNonces( loopInResp.ExtremeFeeHtlcInfo.Nonces, @@ -210,7 +241,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context, if err != nil { pushEmptySigs() - return f.HandleError(err) + return returnError(err) } // We need to defend against the server setting high fees for the htlc @@ -223,46 +254,64 @@ func (f *FSM) InitHtlcAction(ctx context.Context, maxHtlcTxBackupFee := btcutil.Amount(amt * f.cfg.MaxStaticAddrHtlcBackupFeePercentage) + htlcWeight := f.loopIn.htlcWeight(hasChange) feeRate := chainfee.SatPerKWeight(loopInResp.StandardHtlcInfo.FeeRate) - fee := feeRate.FeeForWeight(f.loopIn.htlcWeight(hasChange)) + fee := feeRate.FeeForWeight(htlcWeight) + highFeeRate := chainfee.SatPerKWeight(loopInResp.HighFeeHtlcInfo.FeeRate) + highFee := highFeeRate.FeeForWeight(htlcWeight) + extremelyHighFeeRate := chainfee.SatPerKWeight( + loopInResp.ExtremeFeeHtlcInfo.FeeRate, + ) + extremelyHighFee := extremelyHighFeeRate.FeeForWeight(htlcWeight) + + f.Debugf("htlc fee validation: "+ + "deposit_count=%d, total_deposit=%v, "+ + "swap_amount=%v, change_amount=%v, has_change=%v, "+ + "htlc_weight=%v, standard_fee_rate=%v, standard_fee=%v, "+ + "high_fee_rate=%v, high_fee=%v, extreme_fee_rate=%v, "+ + "extreme_fee=%v, max_fee=%v, max_backup_fee=%v", + len(f.loopIn.Deposits), totalDepositAmount, swapAmount, + changeAmount, hasChange, htlcWeight, feeRate, fee, highFeeRate, + highFee, extremelyHighFeeRate, extremelyHighFee, maxHtlcTxFee, + maxHtlcTxBackupFee) + if fee > maxHtlcTxFee { // Abort the swap by pushing empty sigs to the server. pushEmptySigs() - log.Errorf("server htlc tx fee is higher than the configured "+ - "allowed maximum: %v > %v", fee, maxHtlcTxFee) + f.Errorf("server standard htlc tx fee is higher than the "+ + "configured allowed maximum: %v > %v "+ + "(fee_rate=%v, weight=%v)", + fee, maxHtlcTxFee, feeRate, htlcWeight) - return f.HandleError(ErrFeeTooHigh) + return returnError(ErrFeeTooHigh) } f.loopIn.HtlcTxFeeRate = feeRate - highFeeRate := chainfee.SatPerKWeight(loopInResp.HighFeeHtlcInfo.FeeRate) - fee = highFeeRate.FeeForWeight(f.loopIn.htlcWeight(hasChange)) - if fee > maxHtlcTxBackupFee { + if highFee > maxHtlcTxBackupFee { // Abort the swap by pushing empty sigs to the server. pushEmptySigs() - log.Errorf("server htlc backup tx fee is higher than the "+ - "configured allowed maximum: %v > %v", fee, - maxHtlcTxBackupFee) + f.Errorf("server high-fee htlc backup tx fee is higher "+ + "than the configured allowed maximum: %v > %v "+ + "(fee_rate=%v, weight=%v)", + highFee, maxHtlcTxBackupFee, highFeeRate, htlcWeight) - return f.HandleError(ErrFeeTooHigh) + return returnError(ErrFeeTooHigh) } f.loopIn.HtlcTxHighFeeRate = highFeeRate - extremelyHighFeeRate := chainfee.SatPerKWeight( - loopInResp.ExtremeFeeHtlcInfo.FeeRate, - ) - fee = extremelyHighFeeRate.FeeForWeight(f.loopIn.htlcWeight(hasChange)) - if fee > maxHtlcTxBackupFee { + if extremelyHighFee > maxHtlcTxBackupFee { // Abort the swap by pushing empty sigs to the server. pushEmptySigs() - log.Errorf("server htlc backup tx fee is higher than the "+ - "configured allowed maximum: %v > %v", fee, - maxHtlcTxBackupFee) + f.Errorf("server extreme-fee htlc backup tx fee is "+ + "higher than the configured allowed maximum: %v > %v "+ + "(fee_rate=%v, weight=%v)", + extremelyHighFee, maxHtlcTxBackupFee, + extremelyHighFeeRate, htlcWeight) - return f.HandleError(ErrFeeTooHigh) + return returnError(ErrFeeTooHigh) } f.loopIn.HtlcTxExtremelyHighFeeRate = extremelyHighFeeRate @@ -276,7 +325,7 @@ func (f *FSM) InitHtlcAction(ctx context.Context, err = fmt.Errorf("unable to derive htlc timeout sweep "+ "address: %w", err) - return f.HandleError(err) + return returnError(err) } f.loopIn.HtlcTimeoutSweepAddress = sweepAddress @@ -286,10 +335,221 @@ func (f *FSM) InitHtlcAction(ctx context.Context, pushEmptySigs() err = fmt.Errorf("unable to store loop-in in db: %w", err) - return f.HandleError(err) + return returnError(err) } - return OnHtlcInitiated + // Once the swap is stored, restart/recovery code owns invoice lifecycle. + invoiceNeedsCleanup = false + + err = f.sendUpdate(ctx) + if err != nil { + f.Errorf("Error sending loop-in update: %v", err) + } + + event = OnHtlcInitiated + + return event +} + +// cancelSwapInvoice cancels the current swap invoice using a detached, +// timeout-limited context. Callers that must not proceed while the invoice may +// still be payable can use the returned error to retry. +func (f *FSM) cancelSwapInvoice() error { + if f.loopIn.SwapHash == (lntypes.Hash{}) { + return nil + } + + cleanupCtx, cancel := context.WithTimeout( + context.Background(), defaultInvoiceCleanupTimeout, + ) + defer cancel() + + err := f.cfg.InvoicesClient.CancelInvoice(cleanupCtx, f.loopIn.SwapHash) + return err +} + +// handleInvoiceUpdate applies the monitor state's invoice-update semantics and +// reports whether the update produced a terminal event. +func (f *FSM) handleInvoiceUpdate(update lndclient.InvoiceUpdate) ( + fsm.EventType, bool) { + + switch update.State { + case invoices.ContractOpen: + return fsm.NoOp, false + + case invoices.ContractAccepted: + return fsm.NoOp, false + + case invoices.ContractSettled: + f.Debugf("received off-chain payment update %v", update.State) + return OnPaymentReceived, true + + case invoices.ContractCanceled: + // If the invoice was canceled we only log here since we still need + // to monitor until the htlc timed out. + log.Warnf("invoice for swap hash %v canceled", f.loopIn.SwapHash) + return fsm.NoOp, false + + default: + // An unknown state is not evidence that the invoice can no longer + // settle. Keep monitoring rather than leaving the deposits available + // for reuse. + f.Warnf("unexpected invoice state %v for swap hash %v", + update.State, f.loopIn.SwapHash) + + return fsm.NoOp, false + } +} + +// selectedDepositConfirmationHeights returns current confirmation heights for +// the original deposit outpoints selected by this loop-in. +func selectedDepositConfirmationHeights( + loopIn *StaticAddressLoopIn) map[string]int64 { + + confirmations := make(map[string]int64, len(loopIn.Deposits)) + outpoints := make(map[string]struct{}, len(loopIn.DepositOutpoints)) + for _, outpoint := range loopIn.DepositOutpoints { + outpoints[outpoint] = struct{}{} + } + + for _, d := range loopIn.Deposits { + if d == nil { + continue + } + + outpoint := d.OutPoint.String() + confirmationHeight := d.GetConfirmationHeight() + + if _, ok := outpoints[outpoint]; !ok { + continue + } + + confirmations[outpoint] = confirmationHeight + } + + return confirmations +} + +// refreshSelectedDeposits reloads the loop-in's selected deposits from the +// deposit manager/store so recovery does not rely on stale deposit snapshots. +func (f *FSM) refreshSelectedDeposits(ctx context.Context) error { + if f.cfg.DepositManager == nil || len(f.loopIn.DepositOutpoints) == 0 { + return nil + } + + err := f.cfg.DepositManager.EnsureDepositsFresh(ctx) + if err != nil { + return fmt.Errorf("unable to refresh deposit wallet view: %w", err) + } + + const ignoreUnknownOutpoints = false + deposits, err := f.cfg.DepositManager.DepositsForOutpoints( + ctx, f.loopIn.DepositOutpoints, ignoreUnknownOutpoints, + ) + if err != nil { + return err + } + + if len(deposits) != len(f.loopIn.DepositOutpoints) { + return fmt.Errorf("expected %d selected deposits, got %d", + len(f.loopIn.DepositOutpoints), len(deposits)) + } + + f.loopIn.Deposits = deposits + + return nil +} + +// legacyMinConfsReached returns true once every original deposit is confirmed +// and the youngest original deposit has reached the legacy confirmation target. +func legacyMinConfsReached(outpoints []string, + confirmationHeights map[string]int64, currentHeight int32) bool { + + if currentHeight <= 0 || len(outpoints) == 0 { + return false + } + + youngestConfirmation := int64(0) + for _, outpoint := range outpoints { + confirmationHeight, ok := confirmationHeights[outpoint] + if !ok || confirmationHeight <= 0 { + return false + } + + if confirmationHeight > youngestConfirmation { + youngestConfirmation = confirmationHeight + } + } + + return int64(currentHeight) >= youngestConfirmation+deposit.MinConfs-1 +} + +// shouldStartLegacyConfirmationFallback reports whether the local MinConfs +// payment deadline fallback should be armed at the current block height. +// +// The primary path starts the deadline from a server risk-accepted notification. +// This fallback preserves the legacy client-side MinConfs behavior when no risk +// decision has been observed locally: once every original deposit reaches +// MinConfs, the client treats that as enough confirmation-risk clearance to +// start the payment window. The selected deposits are refreshed first so +// recovered swaps do not depend on stale in-memory deposit snapshots. +func (f *FSM) shouldStartLegacyConfirmationFallback(ctx context.Context, + currentHeight int32) bool { + + err := f.refreshSelectedDeposits(ctx) + if err != nil { + f.Warnf("unable to refresh selected deposits for legacy "+ + "confirmation fallback: %v", err) + + return false + } + + depositConfirmationHeights := selectedDepositConfirmationHeights( + f.loopIn, + ) + + return legacyMinConfsReached( + f.loopIn.DepositOutpoints, depositConfirmationHeights, + currentHeight, + ) +} + +// originalDepositOutpointUnavailable checks the original selected deposit +// outpoints against the chain backend's UTXO view. +func (f *FSM) originalDepositOutpointUnavailable(ctx context.Context) ( + bool, error) { + + if f.cfg.TxOutChecker == nil { + return false, nil + } + + if len(f.loopIn.DepositOutpoints) == 0 { + return false, nil + } + + outpoints := make([]wire.OutPoint, len(f.loopIn.DepositOutpoints)) + for i, outpointStr := range f.loopIn.DepositOutpoints { + outpoint, err := wire.NewOutPointFromString(outpointStr) + if err != nil { + return false, fmt.Errorf("invalid deposit outpoint %q: %w", + outpointStr, err) + } + + outpoints[i] = *outpoint + } + + txOuts, err := f.cfg.TxOutChecker.GetTxOuts(ctx, outpoints) + if err != nil { + return false, fmt.Errorf("unable to get txouts: %w", err) + } + + for _, outpoint := range outpoints { + if txOuts[outpoint] == nil { + return true, nil + } + } + + return false, nil } // SignHtlcTxAction is called if the htlc was initialized and the server @@ -300,6 +560,21 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context, var err error + outpointUnavailable, err := f.originalDepositOutpointUnavailable(ctx) + if err != nil { + return f.HandleError(err) + } + if outpointUnavailable { + err = errors.New("original deposit outpoint no longer available") + f.Warnf("%v, canceling swap invoice", err) + if cancelErr := f.cancelSwapInvoice(); cancelErr != nil { + f.Warnf("unable to cancel invoice for swap %v: %v", + f.loopIn.SwapHash, cancelErr) + } + + return f.HandleError(err) + } + f.loopIn.AddressParams, err = f.cfg.AddressManager.GetStaticAddressParameters(ctx) @@ -317,6 +592,11 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context, return f.HandleError(err) } + err = f.checkDepositsAvailable(ctx) + if err != nil { + return f.HandleError(err) + } + // Create a musig2 session for each deposit and different htlc tx fee // rates. createSession := staticutil.CreateMusig2Sessions @@ -430,6 +710,68 @@ func (f *FSM) SignHtlcTxAction(ctx context.Context, return OnHtlcTxSigned } +// checkDepositsAvailable verifies that all loop-in deposits are still available +// before the client signs the HTLC transaction. +func (f *FSM) checkDepositsAvailable(ctx context.Context) error { + outpoints, err := f.validateSigningDepositOutpoints() + if err != nil { + return err + } + + if f.cfg.TxOutChecker == nil { + return nil + } + + txOuts, err := f.cfg.TxOutChecker.GetTxOuts(ctx, outpoints) + if err != nil { + return fmt.Errorf("unable to check deposits: %w", err) + } + + for _, outpoint := range outpoints { + if txOuts[outpoint] == nil { + return fmt.Errorf("deposit %v is no longer available", + outpoint) + } + } + + return nil +} + +// validateSigningDepositOutpoints verifies that the current deposit rows match +// the server-side outpoint snapshot before signing the HTLC transaction. +func (f *FSM) validateSigningDepositOutpoints() ([]wire.OutPoint, error) { + currentOutpoints := f.loopIn.Outpoints() + if len(f.loopIn.DepositOutpoints) == 0 { + return currentOutpoints, nil + } + + if len(f.loopIn.DepositOutpoints) != len(currentOutpoints) { + return nil, fmt.Errorf("deposit outpoint snapshot has %d "+ + "outpoints, current deposits have %d", + len(f.loopIn.DepositOutpoints), len(currentOutpoints)) + } + + snapshotOutpoints := make( + []wire.OutPoint, len(f.loopIn.DepositOutpoints), + ) + for i, snapshot := range f.loopIn.DepositOutpoints { + outpoint, err := wire.NewOutPointFromString(snapshot) + if err != nil { + return nil, fmt.Errorf("unable to parse deposit "+ + "outpoint snapshot %q: %w", snapshot, err) + } + + snapshotOutpoints[i] = *outpoint + if *outpoint != currentOutpoints[i] { + return nil, fmt.Errorf("deposit outpoint snapshot "+ + "mismatch at index %d: snapshot %v, "+ + "current %v", i, outpoint, currentOutpoints[i]) + } + } + + return snapshotOutpoints, nil +} + // cleanUpSessions releases allocated memory of the musig2 sessions. func (f *FSM) cleanUpSessions(ctx context.Context, sessions []*input.MuSig2SessionInfo) { @@ -454,6 +796,29 @@ func (f *FSM) cleanUpSessions(ctx context.Context, func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context, _ fsm.EventContext) fsm.EventType { + retryMonitor := func(err error) fsm.EventType { + if ctx.Err() != nil { + return fsm.NoOp + } + + f.Errorf("monitoring failed: %v, retrying", err) + + invoice, lookupErr := f.cfg.LndClient.LookupInvoice( + ctx, f.loopIn.SwapHash, + ) + if lookupErr == nil && invoice.State == invoices.ContractSettled { + return OnPaymentReceived + } + + select { + case <-time.After(monitorRetryDelay): + return OnRecover + + case <-ctx.Done(): + return fsm.NoOp + } + } + // Subscribe to the state of the swap invoice. If upon restart recovery, // we land here and observe that the invoice is already canceled, it can // only be the case where a user-provided payment timeout was hit, the @@ -469,21 +834,27 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context, subscribeCtx, f.loopIn.SwapHash, ) if err != nil { + if ctx.Err() != nil { + return fsm.NoOp + } + err = fmt.Errorf("unable to subscribe to swap "+ "invoice: %w", err) - return f.HandleError(err) + return retryMonitor(err) } htlc, err := f.loopIn.getHtlc(f.cfg.ChainParams) if err != nil { err = fmt.Errorf("unable to get htlc: %w", err) - return f.HandleError(err) + return retryMonitor(err) } // Subscribe to htlc tx confirmation. reorgChan := make(chan struct{}, 1) + // registerHtlcConf registers for the HTLC transaction confirmation using + // the current reorg channel. registerHtlcConf := func() (chan *chainntnfs.TxConfirmation, chan error, error) { @@ -496,82 +867,296 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context, htlcConfChan, htlcErrConfChan, err := registerHtlcConf() if err != nil { + if ctx.Err() != nil { + return fsm.NoOp + } + err = fmt.Errorf("unable to monitor htlc tx confirmation: %w", err) - return f.HandleError(err) + return retryMonitor(err) } // Subscribe to new blocks. registerBlocks := f.cfg.ChainNotifier.RegisterBlockEpochNtfn blockChan, blockChanErr, err := registerBlocks(ctx) if err != nil { - err = fmt.Errorf("unable to subscribe to new blocks: %w", err) - - return f.HandleError(err) - } - - htlcConfirmed := false - - invoice, err := f.cfg.LndClient.LookupInvoice(ctx, f.loopIn.SwapHash) - if err != nil { - err = fmt.Errorf("unable to look up invoice by swap hash: %w", - err) - - return f.HandleError(err) - } - - // Create the swap payment timeout timer. If it runs out we cancel the - // invoice, but keep monitoring the htlc confirmation. - // If the invoice was canceled, e.g. before a restart, we don't need to - // set a new deadline. - var deadlineChan <-chan time.Time - if invoice.State != invoices.ContractCanceled { - // If the invoice is still live we set the timeout to the - // remaining payment time. If too much time has elapsed, e.g. - // after a restart, we set the timeout to 0 to cancel the - // invoice and unlock the deposits immediately. - remainingTimeSeconds := f.loopIn.RemainingPaymentTimeSeconds() - - // If the invoice isn't cancelled yet and the payment timeout - // elapsed, we set the timeout to 0 to cancel the invoice and - // unlock the deposits immediately. Otherwise, we start the - // timer with the remaining seconds to timeout. - timeout := time.Duration(0) * time.Second - if remainingTimeSeconds > 0 { - timeout = time.Duration(remainingTimeSeconds) * - time.Second + if ctx.Err() != nil { + return fsm.NoOp } - deadlineChan = time.NewTimer(timeout).C - } else { + err = fmt.Errorf("unable to subscribe to new blocks: %w", err) + + return retryMonitor(err) + } + + // The watcher keeps notification normalization and timestamp restoration + // outside of the swap-state handling below. + riskWatcher := newConfirmationRiskWatcher( + f.cfg, f.loopIn.SwapHash, f.Warnf, + ) + riskUpdateChan, cancelRiskNotificationSubscriptions := + riskWatcher.subscribe(ctx) + defer cancelRiskNotificationSubscriptions() + + // Look up the current invoice state after registering subscriptions so + // recovery can resume the payment deadline from the latest known state. + invoice, err := f.cfg.LndClient.LookupInvoice(ctx, f.loopIn.SwapHash) + if err != nil { + if ctx.Err() != nil { + return fsm.NoOp + } + + // A failed lookup leaves the invoice state unknown. The active + // subscription can still provide an authoritative update, so keep + // monitoring and, most importantly, keep the deposits locked. + f.Warnf("unable to look up invoice by swap hash: %v", err) + invoice = &lndclient.Invoice{} + } + + // A settled invoice always takes precedence over a recovered risk + // rejection or an elapsed payment deadline. + if invoice.State == invoices.ContractSettled { + return OnPaymentReceived + } + + invoiceCanceledForNonPayment := invoice.State == invoices.ContractCanceled + if invoiceCanceledForNonPayment { // If the invoice was canceled previously we end our // subscription to invoice updates. cancelInvoiceSubscription() } - cancelInvoice := func() { - f.Errorf("timeout waiting for invoice to be " + - "paid, canceling invoice") + // Create the swap payment timeout timer after the server confirms + // confirmation risk was accepted. If a server does not support risk + // notifications, fall back after the legacy deposit confirmation depth. + var ( + deadlineChan <-chan time.Time + deadlineTimer *time.Timer + deadlineStarted bool + ) + // Stop the payment deadline timer when leaving the monitor action. + defer func() { + if deadlineTimer != nil { + deadlineTimer.Stop() + } + }() - // Cancel the lndclient invoice subscription. - cancelInvoiceSubscription() + // depositsInState reports whether all selected deposits are currently + // in the requested state. + depositsInState := func(state fsm.StateType) bool { + if len(f.loopIn.Deposits) == 0 { + return false + } - err = f.cfg.InvoicesClient.CancelInvoice(ctx, f.loopIn.SwapHash) - if err != nil { - f.Warnf("unable to cancel invoice "+ - "for swap hash: %v", err) + for _, d := range f.loopIn.Deposits { + if d == nil { + return false + } + + if !d.IsInState(state) { + return false + } + } + + return true + } + + // startPaymentDeadline arms the server payment timeout from the decision + // time when one is available. + startPaymentDeadline := func(reason string, startedAt time.Time) { + if deadlineStarted || invoice.State == invoices.ContractCanceled { + return + } + + timeout := f.loopIn.PaymentTimeoutDuration() + if !startedAt.IsZero() { + timeout -= time.Since(startedAt) + if timeout < 0 { + timeout = 0 + } + } + + f.Infof("starting payment deadline after %s", reason) + deadlineTimer = time.NewTimer(timeout) + deadlineChan = deadlineTimer.C + deadlineStarted = true + } + + depositsLockedForHtlcTimeout := depositsInState( + deposit.SweepHtlcTimeout, + ) + + // transitionDepositsToHtlcTimeout locks deposits into timeout sweeping once + // the HTLC is confirmed and the invoice cannot be paid. + transitionDepositsToHtlcTimeout := func(reason string) error { + if depositsLockedForHtlcTimeout || + depositsInState(deposit.SweepHtlcTimeout) { + + depositsLockedForHtlcTimeout = true + return nil + } + + depositsToTransition := make( + []*deposit.Deposit, 0, len(f.loopIn.Deposits), + ) + for _, d := range f.loopIn.Deposits { + if d != nil && d.IsInState(deposit.SweepHtlcTimeout) { + continue + } + + depositsToTransition = append(depositsToTransition, d) + } + + transitionErr := f.cfg.DepositManager.TransitionDeposits( + ctx, depositsToTransition, + deposit.OnSweepingHtlcTimeout, + deposit.SweepHtlcTimeout, + ) + if transitionErr != nil { + // WaitForState can report cancellation after the deposit FSMs + // already reached the target state. Do not turn that shutdown + // error into success: the monitor must return NoOp and remain + // recoverable instead of advancing the loop-in FSM. + if ctx.Err() != nil { + return ctx.Err() + } + + // The transition can return an error after every deposit + // already reached the target state. Treat that as + // success, but never advance with a partial transition. + if depositsInState(deposit.SweepHtlcTimeout) { + depositsLockedForHtlcTimeout = true + + return nil + } + + return fmt.Errorf("unable to transition deposits to the htlc "+ + "timeout sweeping state after %s: %w", + reason, transitionErr) + } + if !depositsInState(deposit.SweepHtlcTimeout) { + return fmt.Errorf("not all deposits reached the htlc timeout "+ + "sweeping state after %s", reason) + } + + depositsLockedForHtlcTimeout = true + + return nil + } + + // startLegacyFallback starts the payment deadline once the old local + // minimum-confirmation rule has been satisfied. + startLegacyFallback := func(reason string, currentHeight int32) { + if deadlineStarted || invoice.State == invoices.ContractCanceled || + f.loopIn.ConfirmationRiskDecision != + ConfirmationRiskDecisionNone { + + return + } + + if f.shouldStartLegacyConfirmationFallback(ctx, currentHeight) { + decisionTime, ok := riskWatcher.durableDecisionTime( + ctx, ConfirmationRiskDecisionAccepted, + ) + if !ok { + return + } + + f.loopIn.ConfirmationRiskDecision = + ConfirmationRiskDecisionAccepted + f.loopIn.ConfirmationRiskDecisionTime = decisionTime + startPaymentDeadline(reason, decisionTime) } } + // cancelInvoice only marks the invoice canceled after lnd acknowledges + // the request or the lookup/subscription already observed that state. + // Failures recover the monitor state without releasing deposits. + cancelInvoice := func(reason string) (fsm.EventType, bool) { + if invoice.State != invoices.ContractCanceled { + f.Errorf("%s, canceling invoice", reason) + if err := f.cancelSwapInvoice(); err != nil { + return retryMonitor(err), false + } + } + + cancelInvoiceSubscription() + invoice.State = invoices.ContractCanceled + invoiceCanceledForNonPayment = true + + return fsm.NoOp, true + } + + // handleRiskRejected records a server rejection and only exits through + // the generic error path once the invoice can no longer settle. + handleRiskRejected := func(reason string, + decisionTime time.Time) fsm.EventType { + + f.loopIn.ConfirmationRiskDecision = + ConfirmationRiskDecisionRejected + f.loopIn.ConfirmationRiskDecisionTime = decisionTime + + event, canceled := cancelInvoice( + "server rejected confirmation risk wait after " + reason, + ) + if !canceled { + return event + } + + return f.HandleError(fmt.Errorf( + "server rejected confirmation risk wait after %s", reason, + )) + } + + switch f.loopIn.ConfirmationRiskDecision { + case ConfirmationRiskDecisionAccepted: + startPaymentDeadline( + "recovered risk accepted notification", + f.loopIn.ConfirmationRiskDecisionTime, + ) + + case ConfirmationRiskDecisionRejected: + decisionTime := riskWatcher.decisionTime( + ctx, ConfirmationRiskDecisionRejected, + ) + + return handleRiskRejected( + "recovered risk rejection", decisionTime, + ) + } + + info, err := f.cfg.LndClient.GetInfo(ctx) + if err != nil { + f.Warnf("unable to query current height for legacy confirmation "+ + "fallback: %v", err) + } else { + startLegacyFallback( + "legacy confirmation fallback", int32(info.BlockHeight), + ) + } + + htlcConfirmed := false for { select { case <-htlcConfChan: f.Infof("htlc tx confirmed") htlcConfirmed = true + if invoiceCanceledForNonPayment { + err = transitionDepositsToHtlcTimeout( + "htlc confirmation after invoice cancellation", + ) + if err != nil { + return retryMonitor(err) + } + } case err = <-htlcErrConfChan: + if ctx.Err() != nil { + return fsm.NoOp + } + f.Errorf("htlc tx conf chan error, re-registering: "+ "%v", err) @@ -582,10 +1167,14 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context, // Re-register for htlc confirmation. htlcConfChan, htlcErrConfChan, err = registerHtlcConf() if err != nil { + if ctx.Err() != nil { + return fsm.NoOp + } + err = fmt.Errorf("unable to re-register for "+ "htlc tx confirmation: %w", err) - return f.HandleError(err) + return retryMonitor(err) } case <-reorgChan: @@ -596,26 +1185,75 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context, htlcConfChan, htlcErrConfChan, err = registerHtlcConf() if err != nil { + if ctx.Err() != nil { + return fsm.NoOp + } + err = fmt.Errorf("unable to monitor htlc tx "+ "confirmation: %v", err) - return f.HandleError(err) + return retryMonitor(err) } case <-deadlineChan: - // If the server didn't pay the invoice on time, we - // cancel the invoice and keep monitoring the htlc tx - // confirmation. We also need to unlock the deposits to - // re-enable them for loop-ins and withdrawals. - cancelInvoice() + deadlineChan = nil - event := f.UnlockDepositsAction(ctx, nil) - if event != fsm.OnError { - f.Errorf("unable to unlock deposits after " + - "payment deadline") + // If the server didn't pay the invoice on time, we cancel + // it and keep monitoring the htlc tx. Confirmed HTLC + // deposits remain locked for timeout sweeping. + event, canceled := cancelInvoice( + "timeout waiting for invoice to be paid", + ) + if !canceled { + return event + } + if htlcConfirmed { + err = transitionDepositsToHtlcTimeout( + "payment deadline", + ) + if err != nil { + return retryMonitor(err) + } + + continue + } + + err = f.unlockDeposits(ctx) + if err != nil { + return retryMonitor(fmt.Errorf("unable to unlock deposits "+ + "after payment deadline: %w", err)) + } + + case riskUpdate, ok := <-riskUpdateChan: + if !ok { + riskUpdateChan = nil + continue + } + + decisionTime := riskWatcher.decisionTime( + ctx, riskUpdate.decision, + ) + f.loopIn.ConfirmationRiskDecision = riskUpdate.decision + f.loopIn.ConfirmationRiskDecisionTime = decisionTime + + switch riskUpdate.decision { + case ConfirmationRiskDecisionAccepted: + startPaymentDeadline( + riskUpdate.reason, + f.loopIn.ConfirmationRiskDecisionTime, + ) + + case ConfirmationRiskDecisionRejected: + return handleRiskRejected( + riskUpdate.reason, decisionTime, + ) } case currentHeight := <-blockChan: + startLegacyFallback( + "legacy confirmation fallback", currentHeight, + ) + // If the htlc is confirmed but blockChan fires before // htlcConfChan, we would wrongfully assume that the // htlc tx was not confirmed which would lead to @@ -633,10 +1271,10 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context, f.Infof("htlc timed out at block height %v", currentHeight) - - // If the timeout path opened up we consider the swap - // failed and cancel the invoice. - cancelInvoice() + event, canceled := cancelInvoice("htlc timed out") + if !canceled { + return event + } if !htlcConfirmed { f.Infof("swap timed out, htlc not confirmed") @@ -644,63 +1282,79 @@ func (f *FSM) MonitorInvoiceAndHtlcTxAction(ctx context.Context, // If the htlc hasn't confirmed but the timeout // path opened up, and we didn't receive the // swap payment, we consider the swap attempt to - // be failed. We cancelled the invoice, but - // don't need to unlock the deposits because - // that happened when the payment deadline was - // reached. + // be failed. Now that the invoice is canceled and + // the HTLC can no longer confirm, its deposits can be + // made available again. + err = f.unlockDeposits(ctx) + if err != nil { + return retryMonitor(fmt.Errorf("unable to unlock "+ + "deposits after htlc timeout: %w", err)) + } + return OnSwapTimedOut } // If the htlc has confirmed and the timeout path has // opened up we sweep the funds back to us. - err = f.cfg.DepositManager.TransitionDeposits( - ctx, f.loopIn.Deposits, - deposit.OnSweepingHtlcTimeout, - deposit.SweepHtlcTimeout, - ) + err = transitionDepositsToHtlcTimeout("htlc timeout") if err != nil { - log.Errorf("unable to transition "+ - "deposits to the htlc timeout "+ - "sweeping state: %v", err) + return retryMonitor(err) } return OnSweepHtlcTimeout case err = <-blockChanErr: - f.Errorf("block subscription error: %v", err) - - return f.HandleError(err) - - case update := <-invoiceUpdateChan: - switch update.State { - case invoices.ContractOpen: - case invoices.ContractAccepted: - case invoices.ContractSettled: - f.Debugf("received off-chain payment update "+ - "%v", update.State) - - return OnPaymentReceived - - case invoices.ContractCanceled: - // If the invoice was canceled we only log here - // since we still need to monitor until the htlc - // timed out. - log.Warnf("invoice for swap hash %v canceled", - f.loopIn.SwapHash) - - default: - err = fmt.Errorf("unexpected invoice state %v "+ - "for swap hash %v canceled", - update.State, f.loopIn.SwapHash) - - return f.HandleError(err) + if ctx.Err() != nil { + return fsm.NoOp } - case err = <-invoiceErrChan: - f.Errorf("invoice subscription error: %v", err) + f.Errorf("block subscription error: %v", err) + + return retryMonitor(err) + + case update, ok := <-invoiceUpdateChan: + if !ok { + if !invoiceCanceledForNonPayment { + return retryMonitor(errors.New( + "invoice update subscription closed", + )) + } + + invoiceUpdateChan = nil + continue + } + + if event, done := f.handleInvoiceUpdate(update); done { + return event + } + + invoice.State = update.State + if update.State == invoices.ContractCanceled { + invoiceCanceledForNonPayment = true + } + + case err, ok := <-invoiceErrChan: + if !ok { + if !invoiceCanceledForNonPayment { + return retryMonitor(errors.New( + "invoice error subscription closed", + )) + } + + invoiceErrChan = nil + continue + } + + if ctx.Err() != nil { + return fsm.NoOp + } + + return retryMonitor(fmt.Errorf( + "invoice subscription error: %w", err, + )) case <-ctx.Done(): - return f.HandleError(ctx.Err()) + return fsm.NoOp } } } @@ -726,10 +1380,10 @@ func (f *FSM) SweepHtlcTimeoutAction(ctx context.Context, select { // The context is cancelled when the server is shutting - // down. In that case we give up broadcasting attempts - // and return an error. + // down. Keep the current state so recovery resumes + // broadcasting attempts after restart. case <-ctx.Done(): - return f.HandleError(ctx.Err()) + return fsm.NoOp case <-time.After(htlcTimeoutSweepRetryDelay): } @@ -763,6 +1417,10 @@ func (f *FSM) MonitorHtlcTimeoutSweepAction(ctx context.Context, ) if err != nil { + if ctx.Err() != nil { + return fsm.NoOp + } + err = fmt.Errorf("unable to register to the htlc timeout "+ "sweep tx: %w", err) @@ -772,6 +1430,10 @@ func (f *FSM) MonitorHtlcTimeoutSweepAction(ctx context.Context, for { select { case err := <-errChan: + if ctx.Err() != nil { + return fsm.NoOp + } + return f.HandleError(err) case conf := <-htlcTimeoutTxidChan: @@ -795,7 +1457,7 @@ func (f *FSM) MonitorHtlcTimeoutSweepAction(ctx context.Context, return OnHtlcTimeoutSwept case <-ctx.Done(): - return f.HandleError(ctx.Err()) + return fsm.NoOp } } } @@ -824,18 +1486,30 @@ func (f *FSM) PaymentReceivedAction(ctx context.Context, func (f *FSM) UnlockDepositsAction(ctx context.Context, _ fsm.EventContext) fsm.EventType { - err := f.cfg.DepositManager.TransitionDeposits( - ctx, f.loopIn.Deposits, fsm.OnError, deposit.Deposited, - ) - if err != nil { - err = fmt.Errorf("unable to unlock deposits: %w", err) + if err := f.cancelSwapInvoice(); err != nil { + f.Warnf("unable to cancel invoice for swap %v: %v", + f.loopIn.SwapHash, err) + } + err := f.unlockDeposits(ctx) + if err != nil { return f.HandleError(err) } return fsm.OnError } +func (f *FSM) unlockDeposits(ctx context.Context) error { + err := f.cfg.DepositManager.TransitionDeposits( + ctx, f.loopIn.Deposits, fsm.OnError, deposit.Deposited, + ) + if err != nil { + return fmt.Errorf("unable to unlock deposits: %w", err) + } + + return nil +} + // createAndPublishHtlcTimeoutSweepTx creates and publishes the htlc timeout // sweep transaction. func (f *FSM) createAndPublishHtlcTimeoutSweepTx(ctx context.Context) error { diff --git a/staticaddr/loopin/actions_test.go b/staticaddr/loopin/actions_test.go index 2543c752..afc0085d 100644 --- a/staticaddr/loopin/actions_test.go +++ b/staticaddr/loopin/actions_test.go @@ -3,6 +3,7 @@ package loopin import ( "context" "errors" + "fmt" "testing" "time" @@ -24,12 +25,382 @@ import ( "google.golang.org/grpc" ) +const testTimeout = 5 * time.Second + +// TestHandleInvoiceUpdate verifies that invoice state updates map to the +// monitor events expected by the static address loop-in FSM. +func TestHandleInvoiceUpdate(t *testing.T) { + t.Parallel() + + swapHash := lntypes.Hash{1, 2, 3} + tests := []struct { + name string + state invoices.ContractState + event fsm.EventType + done bool + errString string + }{ + { + name: "open", + state: invoices.ContractOpen, + event: fsm.NoOp, + }, + { + name: "accepted", + state: invoices.ContractAccepted, + event: fsm.NoOp, + }, + { + name: "settled", + state: invoices.ContractSettled, + event: OnPaymentReceived, + done: true, + }, + { + name: "canceled", + state: invoices.ContractCanceled, + event: fsm.NoOp, + }, + { + name: "unexpected", + state: invoices.ContractState(99), + event: fsm.NoOp, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + f := &FSM{ + StateMachine: &fsm.StateMachine{}, + loopIn: &StaticAddressLoopIn{ + SwapHash: swapHash, + }, + } + + event, done := f.handleInvoiceUpdate( + lndclient.InvoiceUpdate{ + Invoice: lndclient.Invoice{ + State: test.state, + }, + }, + ) + require.Equal(t, test.event, event) + require.Equal(t, test.done, done) + + if test.errString == "" { + require.Nil(t, f.LastActionError) + } else { + require.ErrorContains( + t, f.LastActionError, test.errString, + ) + require.ErrorContains( + t, f.LastActionError, fmt.Sprint(swapHash), + ) + } + }) + } +} + +// TestMonitorInvoiceSettledWinsOverRecoveredRiskRejection verifies that an +// authoritative settled state takes precedence over a persisted server risk +// rejection during recovery. +func TestMonitorInvoiceSettledWinsOverRecoveredRiskRejection(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + swapHash := lntypes.Hash{1, 2, 5} + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractSettled, + }) + + f, depositMgr := newInvoiceMonitorTestFSM( + t, ctx, mockLnd, swapHash, ConfirmationRiskDecisionRejected, + mockLnd.LndServices.Invoices, + ) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + select { + case event := <-resultChan: + require.Equal(t, OnPaymentReceived, event) + + case <-ctx.Done(): + t.Fatalf("monitor action did not exit: %v", ctx.Err()) + } + + require.Empty(t, depositMgr.transitions) + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("settled invoice was canceled: %v", hash) + + default: + } +} + +// TestMonitorInvoiceCancelErrorKeepsMonitoring verifies that cancellation +// failures neither unlock deposits nor stop invoice monitoring. Recovery +// rechecks the authoritative invoice state, and a later settlement wins. +func TestMonitorInvoiceCancelErrorKeepsMonitoring(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + swapHash := lntypes.Hash{1, 2, 6} + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + cancelCalls := make(chan lntypes.Hash, 2) + releaseCancel := make(chan struct{}) + invoicesClient := &failingCancelInvoices{ + InvoicesClient: mockLnd.LndServices.Invoices, + cancelCalls: cancelCalls, + release: releaseCancel, + err: errors.New("invoice backend unavailable"), + } + f, depositMgr := newInvoiceMonitorTestFSM( + t, ctx, mockLnd, swapHash, ConfirmationRiskDecisionRejected, + invoicesClient, + ) + f.ActionEntryFunc = nil + + resultChan := make(chan error, 1) + go func() { + resultChan <- f.SendEvent(ctx, OnRecover, nil) + }() + waitForMonitorSubscriptions(t, ctx, mockLnd) + + select { + case hash := <-cancelCalls: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("cancellation attempt not received: %v", ctx.Err()) + } + + select { + case transition := <-depositMgr.transitionChan: + t.Fatalf("deposits unlocked after cancellation error: %v", + transition) + + default: + } + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractSettled, + }) + close(releaseCancel) + + select { + case err := <-resultChan: + require.NoError(t, err) + + case <-ctx.Done(): + t.Fatalf("monitor action did not exit: %v", ctx.Err()) + } + + require.Equal(t, []fsm.StateType{deposit.LoopedIn}, depositMgr.states) +} + +// TestMonitorInvoiceUnknownStateKeepsMonitoring verifies that a failed lookup +// and an unknown subscription state do not release deposits. A subsequent +// authoritative settlement still completes the swap. +func TestMonitorInvoiceUnknownStateKeepsMonitoring(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + swapHash := lntypes.Hash{1, 2, 7} + f, depositMgr := newInvoiceMonitorTestFSM( + t, ctx, mockLnd, swapHash, ConfirmationRiskDecisionNone, + mockLnd.LndServices.Invoices, + ) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + var invoiceSub *test.SingleInvoiceSubscription + select { + case invoiceSub = <-mockLnd.SingleInvoiceSubcribeChannel: + case <-ctx.Done(): + t.Fatalf("invoice subscription not registered: %v", ctx.Err()) + } + + select { + case <-mockLnd.RegisterConfChannel: + case <-ctx.Done(): + t.Fatalf("htlc conf registration not received: %v", ctx.Err()) + } + + invoiceSub.Update <- lndclient.InvoiceUpdate{ + Invoice: lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractState(99), + }, + } + + select { + case event := <-resultChan: + t.Fatalf("unknown invoice state ended monitor with %v", event) + + case transition := <-depositMgr.transitionChan: + t.Fatalf("unknown invoice state unlocked deposits: %v", transition) + + case <-time.After(100 * time.Millisecond): + } + + invoiceSub.Update <- lndclient.InvoiceUpdate{ + Invoice: lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractSettled, + }, + } + + select { + case event := <-resultChan: + require.Equal(t, OnPaymentReceived, event) + + case <-ctx.Done(): + t.Fatalf("monitor action did not exit: %v", ctx.Err()) + } + + require.Empty(t, depositMgr.transitions) +} + +// TestMonitorInvoiceSetupFailureRecoversSettledInvoice verifies that a +// transient subscription failure cannot route an already-settled swap through +// deposit cleanup before the authoritative invoice lookup runs. +func TestMonitorInvoiceSetupFailureRecoversSettledInvoice(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + swapHash := lntypes.Hash{1, 2, 8} + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractSettled, + }) + invoicesClient := &flakySubscribeInvoices{ + InvoicesClient: mockLnd.LndServices.Invoices, + err: errors.New("invoice backend unavailable"), + } + f, depositMgr := newInvoiceMonitorTestFSM( + t, ctx, mockLnd, swapHash, ConfirmationRiskDecisionRejected, + invoicesClient, + ) + f.ActionEntryFunc = nil + + resultChan := make(chan error, 1) + go func() { + resultChan <- f.SendEvent(ctx, OnRecover, nil) + }() + + select { + case err := <-resultChan: + require.NoError(t, err) + + case <-ctx.Done(): + t.Fatalf("monitor did not recover: %v", ctx.Err()) + } + + require.Equal(t, 1, invoicesClient.subscribeCalls) + require.Equal(t, []fsm.StateType{deposit.LoopedIn}, depositMgr.states) + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("settled invoice was canceled: %v", hash) + + default: + } +} + +// TestMonitorInvoiceStreamErrorRecoversSettlement verifies that a dead invoice +// stream checks the latest invoice state before entering recovery. +func TestMonitorInvoiceStreamErrorRecoversSettlement(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + swapHash := lntypes.Hash{1, 2, 9} + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + f, depositMgr := newInvoiceMonitorTestFSM( + t, ctx, mockLnd, swapHash, ConfirmationRiskDecisionNone, + mockLnd.LndServices.Invoices, + ) + lookupStarted := make(chan struct{}) + releaseLookup := make(chan struct{}) + f.cfg.LndClient = &firstLookupBarrier{ + LightningClient: mockLnd.Client, + lookupStarted: lookupStarted, + release: releaseLookup, + } + f.ActionEntryFunc = nil + + resultChan := make(chan error, 1) + go func() { + resultChan <- f.SendEvent(ctx, OnRecover, nil) + }() + + var invoiceSub *test.SingleInvoiceSubscription + select { + case invoiceSub = <-mockLnd.SingleInvoiceSubcribeChannel: + case <-ctx.Done(): + t.Fatalf("invoice subscription not registered: %v", ctx.Err()) + } + select { + case <-mockLnd.RegisterConfChannel: + case <-ctx.Done(): + t.Fatalf("htlc conf registration not received: %v", ctx.Err()) + } + select { + case <-lookupStarted: + case <-ctx.Done(): + t.Fatalf("initial invoice lookup not received: %v", ctx.Err()) + } + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractSettled, + }) + close(releaseLookup) + select { + case invoiceSub.Err <- errors.New("invoice stream failed"): + case <-ctx.Done(): + t.Fatalf("invoice stream error was not consumed: %v", ctx.Err()) + } + + select { + case err := <-resultChan: + require.NoError(t, err) + + case <-ctx.Done(): + t.Fatalf("monitor did not recover: %v", ctx.Err()) + } + + require.Equal(t, []fsm.StateType{deposit.LoopedIn}, depositMgr.states) +} + // TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr ensures that an error from // the HTLC confirmation subscription triggers a re-registration. Without the // regression fix, only the initial registration would be performed and the // test would time out waiting for the second one. func TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr(t *testing.T) { - ctx, cancel := context.WithTimeout(t.Context(), 5*time.Second) + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) defer cancel() mockLnd := test.NewMockLnd() @@ -45,7 +416,7 @@ func TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr(t *testing.T) { SwapHash: swapHash, HtlcCltvExpiry: 2_000, InitiationHeight: uint32(mockLnd.Height), - InitiationTime: time.Now(), + InitiationTime: time.Now().Add(-time.Hour), ProtocolVersion: version.ProtocolVersion_V0, ClientPubkey: clientKey.PubKey(), ServerPubkey: serverKey.PubKey(), @@ -54,10 +425,10 @@ func TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr(t *testing.T) { loopIn.SetState(MonitorInvoiceAndHtlcTx) // Seed the mock invoice store so LookupInvoice succeeds. - mockLnd.Invoices[swapHash] = &lndclient.Invoice{ + mockLnd.SetInvoice(&lndclient.Invoice{ Hash: swapHash, State: invoices.ContractOpen, - } + }) cfg := &Config{ AddressManager: &mockAddressManager{ @@ -128,9 +499,260 @@ func TestMonitorInvoiceAndHtlcTxReRegistersOnConfErr(t *testing.T) { } } +// TestMonitorInvoiceAndHtlcTxNoOpOnShutdown ensures that a shutdown while the +// client is monitoring an HTLC-signed loop-in keeps the swap resumable instead +// of entering the generic unlock path. +func TestMonitorInvoiceAndHtlcTxNoOpOnShutdown(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + runCtx, stop := context.WithCancel(ctx) + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{4, 5, 6} + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 3_600, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.Invoices[swapHash] = &lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + } + + depositMgr := &recordingDepositManager{} + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: depositMgr, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + } + + f, err := NewFSM(runCtx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(runCtx, nil) + }() + + select { + case <-mockLnd.SingleInvoiceSubcribeChannel: + case <-ctx.Done(): + t.Fatalf("invoice subscription not registered: %v", ctx.Err()) + } + + select { + case <-mockLnd.RegisterConfChannel: + case <-ctx.Done(): + t.Fatalf("htlc conf registration not received: %v", ctx.Err()) + } + + stop() + + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-ctx.Done(): + t.Fatalf("monitor action did not exit: %v", ctx.Err()) + } + + require.Nil(t, f.LastActionError) + require.Empty(t, depositMgr.transitions) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice canceled on shutdown: %v", hash) + + default: + } +} + +// TestSweepHtlcTimeoutActionNoOpOnShutdown ensures that a shutdown during +// timeout sweep publication keeps the FSM in the same state so it can resume +// after restart. +func TestSweepHtlcTimeoutActionNoOpOnShutdown(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithCancel(t.Context()) + cancel() + + mockLnd := test.NewMockLnd() + f := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + LndClient: mockLnd.Client, + WalletKit: mockLnd.WalletKit, + }, + loopIn: &StaticAddressLoopIn{}, + } + + event := f.SweepHtlcTimeoutAction(ctx, nil) + require.Equal(t, fsm.NoOp, event) + require.Nil(t, f.LastActionError) +} + +// TestMonitorHtlcTimeoutSweepActionNoOpOnShutdown ensures that a shutdown +// while waiting for the timeout sweep confirmation keeps the FSM resumable. +func TestMonitorHtlcTimeoutSweepActionNoOpOnShutdown(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + sweepAddr, err := mockLnd.WalletKit.NextAddr(ctx, "", 0, false) + require.NoError(t, err) + + f := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + ChainNotifier: mockLnd.ChainNotifier, + }, + loopIn: &StaticAddressLoopIn{ + HtlcTimeoutSweepAddress: sweepAddr, + InitiationHeight: uint32(mockLnd.Height), + }, + } + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorHtlcTimeoutSweepAction(ctx, nil) + }() + + select { + case <-mockLnd.RegisterConfChannel: + case <-ctx.Done(): + t.Fatalf("timeout sweep conf registration not received: %v", + ctx.Err()) + } + + cancel() + + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + require.Nil(t, f.LastActionError) + + case <-time.After(testTimeout): + t.Fatal("timeout sweep monitor did not return") + } +} + +// TestMonitorInvoiceAndHtlcTxShutdownDoesNotUnlock verifies that daemon +// shutdown exits the monitor action without treating the swap as failed. +func TestMonitorInvoiceAndHtlcTxShutdownDoesNotUnlock(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + runCtx, stop := context.WithCancel(ctx) + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{1, 2, 4} + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now().Add(-time.Hour), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 3_600, + Deposits: []*deposit.Deposit{{ + Value: 200_000, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + depositMgr := &recordingDepositManager{ + transitionChan: make(chan depositTransition, 1), + } + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: depositMgr, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + } + + f, err := NewFSM(runCtx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(runCtx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + stop() + + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-ctx.Done(): + t.Fatalf("monitor action did not exit: %v", ctx.Err()) + } + + require.NoError(t, f.LastActionError) + + select { + case transition := <-depositMgr.transitionChan: + t.Fatalf("deposit transition on shutdown: %v", transition) + + default: + } + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice canceled on shutdown: %v", hash) + + default: + } +} + // TestInitHtlcActionPreservesRouteHints asserts that static-address loop-in // propagates explicit route hints into the encoded swap invoice sent to the -// server. This currently fails because lndclient.AddInvoice drops route hints. +// server. func TestInitHtlcActionPreservesRouteHints(t *testing.T) { t.Parallel() @@ -191,6 +813,133 @@ func TestInitHtlcActionPreservesRouteHints(t *testing.T) { test.RequireRouteHintsEqual(t, loopIn.RouteHints, routeHints) } +func TestSignHtlcTxActionChecksDepositAvailability(t *testing.T) { + dep := &deposit.Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x77}, + Index: 2, + }, + Value: 200_000, + } + checker := &recordingTxOutChecker{} + + f := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + TxOutChecker: checker, + }, + loopIn: &StaticAddressLoopIn{ + Deposits: []*deposit.Deposit{dep}, + }, + } + + event := f.SignHtlcTxAction(t.Context(), nil) + require.Equal(t, fsm.OnError, event) + require.ErrorContains( + t, f.LastActionError, "deposit "+ + dep.OutPoint.String()+" is no longer available", + ) + require.Equal(t, [][]wire.OutPoint{{dep.OutPoint}}, checker.outpoints) +} + +func TestCheckDepositsAvailableRejectsDivergentDepositOutpoints( + t *testing.T) { + + currentOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{0x89}, + Index: 1, + } + snapshotOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{0x88}, + Index: 0, + } + checker := &recordingTxOutChecker{} + + f := &FSM{ + cfg: &Config{ + TxOutChecker: checker, + }, + loopIn: &StaticAddressLoopIn{ + Deposits: []*deposit.Deposit{{ + OutPoint: currentOutpoint, + Value: 200_000, + }}, + DepositOutpoints: []string{snapshotOutpoint.String()}, + }, + } + + err := f.checkDepositsAvailable(t.Context()) + require.ErrorContains(t, err, "deposit outpoint snapshot mismatch") + require.Empty(t, checker.outpoints) +} + +// TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence protects the +// persistence-first invariant: once the loop-in is stored, a later status +// update failure must not roll back the action or state transition. +func TestInitHtlcActionIgnoresSendUpdateErrorAfterPersistence(t *testing.T) { + mockLnd := test.NewMockLnd() + _, serverKey := test.CreateKey(22) + + server := &mockStaticAddressServer{ + response: testStaticAddressLoopInResponse( + serverKey.SerializeCompressed(), + ), + } + + dep := &deposit.Deposit{ + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{2}, + Index: 0, + }, + Value: 500_000, + } + + loopIn := &StaticAddressLoopIn{ + Deposits: []*deposit.Deposit{dep}, + DepositOutpoints: []string{dep.OutPoint.String()}, + SelectedAmount: dep.Value, + QuotedSwapFee: 1_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + PaymentTimeoutSeconds: 3_600, + } + + sendUpdateErr := errors.New("status channel blocked") + sendUpdateCalled := false + f := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + Server: server, + DepositManager: &noopDepositManager{}, + LndClient: mockLnd.Client, + WalletKit: mockLnd.WalletKit, + ChainParams: mockLnd.ChainParams, + Store: &mockStore{}, + ValidateLoopInContract: testValidateLoopInContract, + MaxStaticAddrHtlcFeePercentage: 1, + MaxStaticAddrHtlcBackupFeePercentage: 1, + SendUpdate: func(context.Context, + *StaticAddressLoopIn) error { + + sendUpdateCalled = true + + return sendUpdateErr + }, + }, + loopIn: loopIn, + } + + event := f.InitHtlcAction(t.Context(), nil) + require.Equal(t, OnHtlcInitiated, event) + require.Nil(t, f.LastActionError) + require.True(t, sendUpdateCalled) +} + // mockStaticAddressServer captures static-address loop-in requests in tests. type mockStaticAddressServer struct { swapserverrpc.StaticAddressServerClient @@ -270,6 +1019,2308 @@ func testValidateLoopInContract(_ int32, _ int32) error { return nil } +// TestMonitorInvoiceAndHtlcTxLocksConfirmedHtlcAtDeadline verifies that the +// payment timeout starts on risk acceptance and keeps confirmed HTLC deposits +// locked for timeout sweeping. +func TestMonitorInvoiceAndHtlcTxLocksConfirmedHtlcAtDeadline(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{4, 5, 6} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{7}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now().Add(-time.Hour), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 1, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{{ + OutPoint: depositOutpoint, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + notificationMgr := &mockNotificationManager{ + riskAccepted: make( + chan *swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification, 1, + ), + } + depositMgr := &recordingDepositManager{ + transitionChan: make(chan depositTransition, 1), + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: depositMgr, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + NotificationManager: notificationMgr, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + select { + case <-mockLnd.SingleInvoiceSubcribeChannel: + case <-ctx.Done(): + t.Fatalf("invoice subscription not registered: %v", ctx.Err()) + } + + var confRegistration *test.ConfRegistration + select { + case confRegistration = <-mockLnd.RegisterConfChannel: + case <-ctx.Done(): + t.Fatalf("htlc conf registration not received: %v", ctx.Err()) + } + confRegistration.ConfChan <- nil + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice canceled before risk acceptance: %v", hash) + + case <-time.After(200 * time.Millisecond): + } + + notificationMgr.riskAccepted <- &swapserverrpc.ServerStaticLoopInRiskAcceptedNotification{ + SwapHash: swapHash[:], + } + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice canceled immediately after risk acceptance: %v", + hash) + + case <-time.After(200 * time.Millisecond): + } + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + select { + case transition := <-depositMgr.transitionChan: + require.Equal(t, []*deposit.Deposit{ + loopIn.Deposits[0], + }, transition.deposits) + require.Equal(t, deposit.OnSweepingHtlcTimeout, transition.event) + require.Equal(t, deposit.SweepHtlcTimeout, transition.state) + + case <-ctx.Done(): + t.Fatalf("deposits were not locked for timeout sweeping: %v", + ctx.Err()) + } + + cancel() + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxIgnoresWrongHashRiskNotifications verifies that +// risk notifications for another swap do not start the payment deadline or +// persist a decision through the monitor action. +func TestMonitorInvoiceAndHtlcTxIgnoresWrongHashRiskNotifications( + t *testing.T) { + + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{4, 5, 8} + otherHash := lntypes.Hash{8, 5, 4} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{9}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 3_600, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{{ + OutPoint: depositOutpoint, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + notificationMgr := &mockNotificationManager{ + riskAccepted: make( + chan *swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification, 2, + ), + riskRejected: make( + chan *swapserverrpc. + ServerStaticLoopInRiskRejectedNotification, 1, + ), + } + store := &recordingRiskStore{ + mockStore: &mockStore{ + loopIns: map[lntypes.Hash]*StaticAddressLoopIn{ + swapHash: {}, + }, + }, + decisions: make(chan ConfirmationRiskDecision, 1), + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: &noopDepositManager{}, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + NotificationManager: notificationMgr, + Store: store, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + notificationMgr.riskAccepted <- &swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification{ + SwapHash: otherHash[:], + } + notificationMgr.riskRejected <- &swapserverrpc. + ServerStaticLoopInRiskRejectedNotification{ + SwapHash: otherHash[:], + } + + select { + case decision := <-store.decisions: + t.Fatalf("persisted wrong-hash risk decision: %v", decision) + + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("canceled invoice for wrong-hash risk decision: %v", hash) + + case event := <-resultChan: + t.Fatalf("monitor action exited after wrong-hash risk decision: %v", + event) + + case <-time.After(200 * time.Millisecond): + } + + notificationMgr.riskAccepted <- &swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification{ + SwapHash: swapHash[:], + } + + select { + case decision := <-store.decisions: + require.Equal(t, ConfirmationRiskDecisionAccepted, decision) + + case <-ctx.Done(): + t.Fatalf("risk decision was not persisted: %v", ctx.Err()) + } + + cancel() + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxUsesPersistedAcceptedRiskTime verifies that live +// risk notifications use the durable receipt time, not the local channel +// receive time, when reconstructing the payment deadline. +func TestMonitorInvoiceAndHtlcTxUsesPersistedAcceptedRiskTime(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{4, 5, 7} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{8}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 1, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{{ + OutPoint: depositOutpoint, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + notificationMgr := &mockNotificationManager{ + riskAccepted: make( + chan *swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification, 1, + ), + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: &noopDepositManager{}, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + NotificationManager: notificationMgr, + Store: &mockStore{ + loopIns: map[lntypes.Hash]*StaticAddressLoopIn{ + swapHash: { + ConfirmationRiskDecision: ConfirmationRiskDecisionAccepted, + ConfirmationRiskDecisionTime: time.Now().Add( + -time.Minute, + ), + }, + }, + }, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice canceled before risk acceptance: %v", hash) + + case <-time.After(200 * time.Millisecond): + } + + notificationMgr.riskAccepted <- &swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification{ + SwapHash: swapHash[:], + } + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + cancel() + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxPersistsReplayedRiskAccepted verifies that a risk +// notification replayed after the swap row exists is written back to the store. +func TestMonitorInvoiceAndHtlcTxPersistsReplayedRiskAccepted(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{5, 6, 10} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{14}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 3_600, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{{ + OutPoint: depositOutpoint, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + notificationMgr := &mockNotificationManager{ + riskAccepted: make( + chan *swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification, 1, + ), + } + store := &recordingRiskStore{ + mockStore: &mockStore{ + loopIns: map[lntypes.Hash]*StaticAddressLoopIn{ + swapHash: {}, + }, + }, + decisions: make(chan ConfirmationRiskDecision, 1), + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: &noopDepositManager{}, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + NotificationManager: notificationMgr, + Store: store, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + notificationMgr.riskAccepted <- &swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification{ + SwapHash: swapHash[:], + } + + select { + case decision := <-store.decisions: + require.Equal(t, ConfirmationRiskDecisionAccepted, decision) + + case <-ctx.Done(): + t.Fatalf("risk decision was not persisted: %v", ctx.Err()) + } + + stored := store.loopIns[swapHash] + require.Equal(t, ConfirmationRiskDecisionAccepted, + stored.ConfirmationRiskDecision) + require.False(t, stored.ConfirmationRiskDecisionTime.IsZero()) + + cancel() + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxPersistsRiskRejected verifies that a server-side +// confirmation risk rejection is persisted and exits through the generic error +// path so the FSM unlocks deposits. +func TestMonitorInvoiceAndHtlcTxPersistsRiskRejected(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{5, 6, 7} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{9}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: mockLnd.Height, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 3_600, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{{ + OutPoint: depositOutpoint, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + notificationMgr := &mockNotificationManager{ + riskRejected: make( + chan *swapserverrpc. + ServerStaticLoopInRiskRejectedNotification, 1, + ), + } + + store := &recordingRiskStore{ + mockStore: &mockStore{ + loopIns: map[lntypes.Hash]*StaticAddressLoopIn{ + swapHash: {}, + }, + }, + decisions: make(chan ConfirmationRiskDecision, 1), + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: &noopDepositManager{}, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + NotificationManager: notificationMgr, + Store: store, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + notificationMgr.riskRejected <- &swapserverrpc.ServerStaticLoopInRiskRejectedNotification{ // nolint: lll + SwapHash: swapHash[:], + } + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + select { + case decision := <-store.decisions: + require.Equal(t, ConfirmationRiskDecisionRejected, decision) + + case <-ctx.Done(): + t.Fatalf("risk decision was not persisted: %v", ctx.Err()) + } + + stored := store.loopIns[swapHash] + require.Equal(t, ConfirmationRiskDecisionRejected, + stored.ConfirmationRiskDecision) + require.False(t, stored.ConfirmationRiskDecisionTime.IsZero()) + + select { + case event := <-resultChan: + require.Equal(t, fsm.OnError, event) + require.ErrorContains( + t, f.LastActionError, + "server rejected confirmation risk wait", + ) + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxRecoversAcceptedRiskDecision verifies that a +// persisted risk acceptance restarts the payment deadline with elapsed time +// preserved after restart. +func TestMonitorInvoiceAndHtlcTxRecoversAcceptedRiskDecision(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{5, 6, 8} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{12}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 1, + ConfirmationRiskDecision: ConfirmationRiskDecisionAccepted, + ConfirmationRiskDecisionTime: time.Now().Add(-time.Minute), + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{{ + OutPoint: depositOutpoint, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + depositMgr := &recordingDepositManager{ + transitionChan: make(chan depositTransition, 1), + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: depositMgr, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + select { + case transition := <-depositMgr.transitionChan: + require.Equal(t, fsm.OnError, transition.event) + require.Equal(t, deposit.Deposited, transition.state) + + case <-ctx.Done(): + t.Fatalf("deposits were not unlocked: %v", ctx.Err()) + } + + cancel() + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxRecoversRejectedRiskDecision verifies that a +// persisted risk rejection still cancels after restart and exits through the +// generic error path so the FSM unlocks deposits. +func TestMonitorInvoiceAndHtlcTxRecoversRejectedRiskDecision(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{5, 6, 9} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{13}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: mockLnd.Height, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 3_600, + ConfirmationRiskDecision: ConfirmationRiskDecisionRejected, + ConfirmationRiskDecisionTime: time.Now(), + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{{ + OutPoint: depositOutpoint, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + depositMgr := &recordingDepositManager{ + transitionChan: make(chan depositTransition, 1), + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: depositMgr, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + select { + case event := <-resultChan: + require.Equal(t, fsm.OnError, event) + require.ErrorContains( + t, f.LastActionError, + "server rejected confirmation risk wait", + ) + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxDoesNotCancelWhenOriginalOutpointVanishes +// verifies that once the monitor state is reached, a missing original deposit +// outpoint does not cancel the invoice. After HTLC signatures are handed to the +// server, the outpoint can disappear because the server published the expected +// HTLC transaction. +func TestMonitorInvoiceAndHtlcTxDoesNotCancelWhenOriginalOutpointVanishes( + t *testing.T) { + + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{5, 7, 9} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{10}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 3_600, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{{ + OutPoint: depositOutpoint, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + txOutChecker := &recordingTxOutChecker{} + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: &noopDepositManager{}, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + TxOutChecker: txOutChecker, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice should not have been canceled: %v", hash) + + case <-time.After(200 * time.Millisecond): + } + + cancel() + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } + + require.Empty(t, txOutChecker.outpoints) +} + +// TestMonitorInvoiceAndHtlcTxDoesNotCancelAcceptedInvoiceForMissingOutpoint +// verifies that the outpoint-vanished fallback is only active before payment +// has started. Once the invoice is accepted, the original deposit may disappear +// because the server has moved forward with the swap. +func TestMonitorInvoiceAndHtlcTxDoesNotCancelAcceptedInvoiceForMissingOutpoint( + t *testing.T) { + + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{6, 8, 10} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{11}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 3_600, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{{ + OutPoint: depositOutpoint, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractAccepted, + }) + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: &noopDepositManager{}, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + TxOutChecker: &recordingTxOutChecker{}, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice should not have been canceled: %v", hash) + + case <-time.After(200 * time.Millisecond): + } + + cancel() + select { + case <-resultChan: + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxStartsDeadlineAtLegacyMinConfs verifies that the +// monitor action preserves the legacy payment deadline fallback when no risk +// decision has been observed locally. +func TestMonitorInvoiceAndHtlcTxStartsDeadlineAtLegacyMinConfs(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{7, 8, 9} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{8}, + Index: 0, + } + depositRecord := &deposit.Deposit{ + OutPoint: depositOutpoint, + } + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 1, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{depositRecord}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: &noopDepositManager{ + deposits: []*deposit.Deposit{depositRecord}, + }, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice canceled before deposit confirmation: %v", hash) + + case <-time.After(200 * time.Millisecond): + } + + confirmationHeight := int64(mockLnd.Height) - deposit.MinConfs + 1 + depositRecord.Lock() + depositRecord.ConfirmationHeight = confirmationHeight + depositRecord.Unlock() + + require.NoError(t, mockLnd.NotifyHeight(mockLnd.Height)) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice canceled immediately after deposit "+ + "confirmation: %v", hash) + + case <-time.After(200 * time.Millisecond): + } + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + cancel() + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxStartsLegacyFallbackWithNotificationManager +// verifies that the legacy payment deadline fallback still applies when the +// notification manager is configured but no risk decision has been observed. +func TestMonitorInvoiceAndHtlcTxStartsLegacyFallbackWithNotificationManager( + t *testing.T) { + + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{7, 8, 10} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{9}, + Index: 0, + } + depositRecord := &deposit.Deposit{ + OutPoint: depositOutpoint, + } + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now().Add(-time.Hour), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 1, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{depositRecord}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + notificationMgr := &mockNotificationManager{ + riskAccepted: make( + chan *swapserverrpc.ServerStaticLoopInRiskAcceptedNotification, + 1, + ), + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: &noopDepositManager{ + deposits: []*deposit.Deposit{depositRecord}, + }, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + NotificationManager: notificationMgr, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + confirmationHeight := int64(mockLnd.Height) - deposit.MinConfs + 1 + depositRecord.Lock() + depositRecord.ConfirmationHeight = confirmationHeight + depositRecord.Unlock() + + require.NoError(t, mockLnd.NotifyHeight(mockLnd.Height)) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice canceled before payment deadline: %v", hash) + + case <-time.After(200 * time.Millisecond): + } + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + cancel() + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxStartsLegacyFallbackAtCurrentHeight verifies that +// recovery can arm the legacy payment deadline without waiting for a later +// block notification. +func TestMonitorInvoiceAndHtlcTxStartsLegacyFallbackAtCurrentHeight( + t *testing.T) { + + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{7, 8, 12} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{13}, + Index: 0, + } + staleDeposit := &deposit.Deposit{ + OutPoint: depositOutpoint, + ConfirmationHeight: 0, + } + confirmationHeight := int64(mockLnd.Height) - deposit.MinConfs + 1 + freshDeposit := &deposit.Deposit{ + OutPoint: depositOutpoint, + ConfirmationHeight: confirmationHeight, + } + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now().Add(-time.Hour), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 1, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{staleDeposit}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + store := &recordingRiskStore{ + mockStore: &mockStore{ + loopIns: map[lntypes.Hash]*StaticAddressLoopIn{ + swapHash: {}, + }, + }, + decisions: make(chan ConfirmationRiskDecision, 1), + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: &silentBlockChainNotifier{ + ChainNotifierClient: mockLnd.ChainNotifier, + }, + DepositManager: &noopDepositManager{ + deposits: []*deposit.Deposit{freshDeposit}, + }, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + Store: store, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + select { + case decision := <-store.decisions: + require.Equal(t, ConfirmationRiskDecisionAccepted, decision) + + case <-ctx.Done(): + t.Fatalf("legacy fallback decision was not persisted: %v", + ctx.Err()) + } + require.Equal(t, ConfirmationRiskDecisionAccepted, + store.loopIns[swapHash].ConfirmationRiskDecision) + require.False(t, + store.loopIns[swapHash].ConfirmationRiskDecisionTime.IsZero()) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice canceled before payment deadline: %v", hash) + + case <-time.After(200 * time.Millisecond): + } + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + cancel() + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestMonitorInvoiceAndHtlcTxRefreshesDepositsForLegacyFallback verifies that a +// recovered monitor state does not rely on stale selected-deposit snapshots when +// deciding whether the legacy payment deadline fallback has opened. +func TestMonitorInvoiceAndHtlcTxRefreshesDepositsForLegacyFallback(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{7, 8, 11} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{12}, + Index: 0, + } + staleDeposit := &deposit.Deposit{ + OutPoint: depositOutpoint, + ConfirmationHeight: 0, + } + confirmationHeight := int64(mockLnd.Height) - deposit.MinConfs + 1 + freshDeposit := &deposit.Deposit{ + OutPoint: depositOutpoint, + ConfirmationHeight: confirmationHeight, + } + type depositLookup struct { + outpoints []string + ignoreUnknown bool + } + depositLookups := make(chan depositLookup, 1) + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: 2_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now().Add(-time.Hour), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 1, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{staleDeposit}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: &noopDepositManager{ + deposits: []*deposit.Deposit{freshDeposit}, + depositsForOutpoints: func(outpoints []string, + ignoreUnknown bool) { + + select { + case depositLookups <- depositLookup{ + outpoints: append( + []string(nil), outpoints..., + ), + ignoreUnknown: ignoreUnknown, + }: + default: + } + }, + }, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + require.NoError(t, mockLnd.NotifyHeight(mockLnd.Height)) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice canceled immediately after deposit "+ + "confirmation: %v", hash) + + case <-time.After(200 * time.Millisecond): + } + + select { + case lookup := <-depositLookups: + require.Equal(t, []string{ + depositOutpoint.String(), + }, lookup.outpoints) + require.False(t, lookup.ignoreUnknown) + + case <-ctx.Done(): + t.Fatalf("deposit refresh was not called: %v", ctx.Err()) + } + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + cancel() + select { + case event := <-resultChan: + require.Equal(t, fsm.NoOp, event) + + case <-time.After(time.Second): + t.Fatal("monitor action did not exit") + } +} + +// TestLegacyConfirmationFallbackStopsOnFreshnessFailure verifies that MinConfs +// is not evaluated from cached deposits when the wallet reconciliation fails. +func TestLegacyConfirmationFallbackStopsOnFreshnessFailure(t *testing.T) { + outpoint := wire.OutPoint{ + Hash: chainhash.Hash{14}, + Index: 0, + } + lookupCalled := false + depositManager := &noopDepositManager{ + deposits: []*deposit.Deposit{{ + OutPoint: outpoint, + ConfirmationHeight: 1, + }}, + ensureFreshErr: errors.New("wallet unavailable"), + depositsForOutpoints: func([]string, bool) { + lookupCalled = true + }, + } + f := &FSM{ + cfg: &Config{ + DepositManager: depositManager, + }, + loopIn: &StaticAddressLoopIn{ + DepositOutpoints: []string{outpoint.String()}, + }, + } + + reached := f.shouldStartLegacyConfirmationFallback( + t.Context(), deposit.MinConfs, + ) + require.False(t, reached) + require.False(t, lookupCalled) +} + +// TestMonitorInvoiceAndHtlcTxUnlocksOnHtlcTimeoutWithoutDeadline verifies that +// deposits are unlocked even if the payment deadline never started before the +// HTLC timeout path opened. +func TestMonitorInvoiceAndHtlcTxUnlocksOnHtlcTimeoutWithoutDeadline( + t *testing.T) { + + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + swapHash := lntypes.Hash{10, 11, 12} + depositOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{10}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: mockLnd.Height, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 3_600, + DepositOutpoints: []string{ + depositOutpoint.String(), + }, + Deposits: []*deposit.Deposit{{ + OutPoint: depositOutpoint, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + depositMgr := &recordingDepositManager{} + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: depositMgr, + InvoicesClient: mockLnd.LndServices.Invoices, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + + require.NoError(t, mockLnd.NotifyHeight(mockLnd.Height+1)) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + select { + case event := <-resultChan: + require.Equal(t, OnSwapTimedOut, event) + + case <-ctx.Done(): + t.Fatalf("monitor action did not exit: %v", ctx.Err()) + } + + require.Equal(t, []fsm.EventType{fsm.OnError}, depositMgr.events) + require.Equal(t, []fsm.StateType{deposit.Deposited}, depositMgr.states) +} + +// TestMonitorInvoiceAndHtlcTxDoesNotAdvanceWhenTimeoutDepositTransitionFails +// verifies that a failed deposit timeout transition keeps the loop-in in its +// recoverable monitor state. +func TestMonitorInvoiceAndHtlcTxDoesNotAdvanceWhenTimeoutDepositTransitionFails( + t *testing.T) { + + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + swapHash := lntypes.Hash{20, 21, 22} + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractCanceled, + }) + + f, depositMgr := newInvoiceMonitorTestFSM( + t, ctx, mockLnd, swapHash, ConfirmationRiskDecisionNone, + mockLnd.LndServices.Invoices, + ) + f.loopIn.HtlcCltvExpiry = mockLnd.Height + depositMgr.err = errors.New("transition failed") + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + select { + case <-mockLnd.SingleInvoiceSubcribeChannel: + case <-ctx.Done(): + t.Fatalf("invoice subscription not registered: %v", ctx.Err()) + } + + var confRegistration *test.ConfRegistration + select { + case confRegistration = <-mockLnd.RegisterConfChannel: + case <-ctx.Done(): + t.Fatalf("htlc conf registration not received: %v", ctx.Err()) + } + + confRegistration.ConfChan <- nil + + select { + case transition := <-depositMgr.transitionChan: + require.Equal(t, deposit.OnSweepingHtlcTimeout, transition.event) + require.Equal(t, deposit.SweepHtlcTimeout, transition.state) + + case <-ctx.Done(): + t.Fatalf("deposit timeout transition was not attempted: %v", + ctx.Err()) + } + + require.NoError(t, mockLnd.NotifyHeight(mockLnd.Height+1)) + + select { + case event := <-resultChan: + require.Equal(t, OnRecover, event) + + case <-ctx.Done(): + t.Fatalf("monitor action did not exit: %v", ctx.Err()) + } +} + +// TestMonitorInvoiceAndHtlcTxRetriesOnlyPendingTimeoutDeposits verifies that a +// retry after a partial timeout transition skips deposits that already reached +// the target state. +func TestMonitorInvoiceAndHtlcTxRetriesOnlyPendingTimeoutDeposits(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + swapHash := lntypes.Hash{26, 27, 28} + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractCanceled, + }) + + f, depositMgr := newInvoiceMonitorTestFSM( + t, ctx, mockLnd, swapHash, ConfirmationRiskDecisionNone, + mockLnd.LndServices.Invoices, + ) + f.loopIn.HtlcCltvExpiry = mockLnd.Height + + firstDeposit := f.loopIn.Deposits[0] + secondDeposit := &deposit.Deposit{Value: 300_000} + firstDeposit.SetState(deposit.LoopingIn) + secondDeposit.SetState(deposit.LoopingIn) + f.loopIn.Deposits = append(f.loopIn.Deposits, secondDeposit) + + attempts := 0 + depositMgr.transition = func(deposits []*deposit.Deposit, + _ fsm.EventType, state fsm.StateType) error { + + attempts++ + if attempts == 1 { + deposits[0].SetState(state) + + return errors.New("partial transition") + } + for _, d := range deposits { + d.SetState(state) + } + + return nil + } + depositMgr.transitionChan = make(chan depositTransition, 2) + + runMonitor := func(runCtx context.Context) chan fsm.EventType { + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(runCtx, nil) + }() + + select { + case <-mockLnd.SingleInvoiceSubcribeChannel: + case <-runCtx.Done(): + t.Fatalf("invoice subscription not registered: %v", + runCtx.Err()) + } + + var confRegistration *test.ConfRegistration + select { + case confRegistration = <-mockLnd.RegisterConfChannel: + case <-runCtx.Done(): + t.Fatalf("htlc conf registration not received: %v", + runCtx.Err()) + } + confRegistration.ConfChan <- nil + + return resultChan + } + + firstCtx, cancelFirst := context.WithCancel(ctx) + firstResult := runMonitor(firstCtx) + select { + case event := <-firstResult: + require.Equal(t, OnRecover, event) + case <-ctx.Done(): + t.Fatalf("first monitor attempt did not exit: %v", ctx.Err()) + } + cancelFirst() + firstTransition := <-depositMgr.transitionChan + + require.True(t, firstDeposit.IsInState(deposit.SweepHtlcTimeout)) + require.True(t, secondDeposit.IsInState(deposit.LoopingIn)) + + secondCtx, cancelSecond := context.WithCancel(ctx) + defer cancelSecond() + secondResult := runMonitor(secondCtx) + secondTransition := <-depositMgr.transitionChan + require.NoError(t, mockLnd.NotifyHeight(mockLnd.Height+1)) + + select { + case event := <-secondResult: + require.Equal(t, OnSweepHtlcTimeout, event) + case <-ctx.Done(): + t.Fatalf("second monitor attempt did not exit: %v", ctx.Err()) + } + + require.Equal(t, []*deposit.Deposit{ + firstDeposit, secondDeposit, + }, firstTransition.deposits) + require.Equal(t, []*deposit.Deposit{ + secondDeposit, + }, secondTransition.deposits) + require.True(t, firstDeposit.IsInState(deposit.SweepHtlcTimeout)) + require.True(t, secondDeposit.IsInState(deposit.SweepHtlcTimeout)) +} + +// TestMonitorInvoiceAndHtlcTxDoesNotFailWhenTimeoutUnlockFails verifies that a +// failed unlock does not make the loop-in terminal while deposits remain +// locked. +func TestMonitorInvoiceAndHtlcTxDoesNotFailWhenTimeoutUnlockFails(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + swapHash := lntypes.Hash{23, 24, 25} + mockLnd.SetInvoice(&lndclient.Invoice{ + Hash: swapHash, + State: invoices.ContractOpen, + }) + + f, depositMgr := newInvoiceMonitorTestFSM( + t, ctx, mockLnd, swapHash, ConfirmationRiskDecisionNone, + mockLnd.LndServices.Invoices, + ) + f.loopIn.HtlcCltvExpiry = mockLnd.Height + depositMgr.err = errors.New("transition failed") + + resultChan := make(chan fsm.EventType, 1) + go func() { + resultChan <- f.MonitorInvoiceAndHtlcTxAction(ctx, nil) + }() + + waitForMonitorSubscriptions(t, ctx, mockLnd) + require.NoError(t, mockLnd.NotifyHeight(mockLnd.Height+1)) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + select { + case event := <-resultChan: + require.Equal(t, OnRecover, event) + + case <-ctx.Done(): + t.Fatalf("monitor action did not exit: %v", ctx.Err()) + } + + require.Equal(t, []fsm.EventType{fsm.OnError}, depositMgr.events) + require.Equal(t, []fsm.StateType{deposit.Deposited}, depositMgr.states) +} + +// waitForMonitorSubscriptions waits until invoice and HTLC watchers are active. +func waitForMonitorSubscriptions(t *testing.T, ctx context.Context, + mockLnd *test.LndMockServices) { + + t.Helper() + + select { + case <-mockLnd.SingleInvoiceSubcribeChannel: + case <-ctx.Done(): + t.Fatalf("invoice subscription not registered: %v", ctx.Err()) + } + + select { + case <-mockLnd.RegisterConfChannel: + case <-ctx.Done(): + t.Fatalf("htlc conf registration not received: %v", ctx.Err()) + } +} + +// newInvoiceMonitorTestFSM creates the minimal monitor-state setup shared by +// invoice precedence and cancellation tests. +func newInvoiceMonitorTestFSM(t *testing.T, ctx context.Context, + mockLnd *test.LndMockServices, swapHash lntypes.Hash, + decision ConfirmationRiskDecision, + invoicesClient lndclient.InvoicesClient) (*FSM, *recordingDepositManager) { + + t.Helper() + + clientKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + HtlcCltvExpiry: mockLnd.Height + 1_000, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + ProtocolVersion: version.ProtocolVersion_V0, + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + PaymentTimeoutSeconds: 3_600, + ConfirmationRiskDecision: decision, + ConfirmationRiskDecisionTime: time.Now(), + Deposits: []*deposit.Deposit{{ + Value: 200_000, + }}, + } + loopIn.SetState(MonitorInvoiceAndHtlcTx) + + depositMgr := &recordingDepositManager{ + transitionChan: make(chan depositTransition, 1), + } + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ClientPubkey: clientKey.PubKey(), + ServerPubkey: serverKey.PubKey(), + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + ChainNotifier: mockLnd.ChainNotifier, + DepositManager: depositMgr, + InvoicesClient: invoicesClient, + LndClient: mockLnd.Client, + ChainParams: mockLnd.ChainParams, + } + + f, err := NewFSM(ctx, loopIn, cfg, true) + require.NoError(t, err) + + return f, depositMgr +} + +// failingCancelInvoices records cancellation attempts and returns a configured +// error after its release channel is closed. +type failingCancelInvoices struct { + lndclient.InvoicesClient + + cancelCalls chan lntypes.Hash + release chan struct{} + err error +} + +// flakySubscribeInvoices counts subscription attempts and returns a configured +// subscription error. +type flakySubscribeInvoices struct { + lndclient.InvoicesClient + + subscribeCalls int + err error +} + +// firstLookupBarrier blocks the first invoice lookup until its release channel +// is closed. +type firstLookupBarrier struct { + lndclient.LightningClient + + lookupStarted chan struct{} + release chan struct{} + firstLookup bool +} + +func (f *firstLookupBarrier) LookupInvoice(ctx context.Context, + hash lntypes.Hash) (*lndclient.Invoice, error) { + + invoice, err := f.LightningClient.LookupInvoice(ctx, hash) + if f.firstLookup { + return invoice, err + } + + f.firstLookup = true + close(f.lookupStarted) + select { + case <-f.release: + return invoice, err + + case <-ctx.Done(): + return nil, ctx.Err() + } +} + +func (f *flakySubscribeInvoices) SubscribeSingleInvoice(ctx context.Context, + hash lntypes.Hash) (<-chan lndclient.InvoiceUpdate, <-chan error, error) { + + f.subscribeCalls++ + if f.subscribeCalls == 1 { + return nil, nil, f.err + } + + return f.InvoicesClient.SubscribeSingleInvoice(ctx, hash) +} + +func (f *failingCancelInvoices) CancelInvoice(ctx context.Context, + hash lntypes.Hash) error { + + select { + case f.cancelCalls <- hash: + case <-ctx.Done(): + return ctx.Err() + } + + if f.release != nil { + select { + case <-f.release: + case <-ctx.Done(): + return ctx.Err() + } + } + + return f.err +} + +// TestOriginalDepositOutpointUnavailableRequiresMissingTxOut verifies that a +// present txout does not trigger the RBF cancellation path. +func TestOriginalDepositOutpointUnavailableRequiresMissingTxOut(t *testing.T) { + originalOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{1}, + Index: 0, + } + + txOutChecker := &recordingTxOutChecker{ + txOuts: map[wire.OutPoint]*wire.TxOut{ + originalOutpoint: {Value: 10_000}, + }, + } + f := &FSM{ + cfg: &Config{ + TxOutChecker: txOutChecker, + }, + loopIn: &StaticAddressLoopIn{ + DepositOutpoints: []string{originalOutpoint.String()}, + }, + } + + unavailable, err := f.originalDepositOutpointUnavailable(t.Context()) + require.NoError(t, err) + require.False(t, unavailable) + require.Equal(t, [][]wire.OutPoint{{originalOutpoint}}, + txOutChecker.outpoints) +} + +// TestSignHtlcTxActionCancelsWhenOriginalOutpointUnavailable verifies that a +// pending loop-in is canceled before HTLC signing if GetTxOuts reports that +// one of the originally selected outpoints is gone. +func TestSignHtlcTxActionCancelsWhenOriginalOutpointUnavailable(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + swapHash := lntypes.Hash{9, 8, 7} + originalOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{1}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + DepositOutpoints: []string{originalOutpoint.String()}, + } + + txOutChecker := &recordingTxOutChecker{} + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + InvoicesClient: mockLnd.LndServices.Invoices, + TxOutChecker: txOutChecker, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + event := f.SignHtlcTxAction(ctx, nil) + require.Equal(t, fsm.OnError, event) + require.ErrorContains( + t, f.LastActionError, "original deposit outpoint no longer available", + ) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + require.Equal(t, [][]wire.OutPoint{{originalOutpoint}}, + txOutChecker.outpoints) +} + +// TestSignHtlcTxActionDoesNotCancelOnTxOutLookupError verifies that lookup +// failures are treated as errors, but do not cancel the invoice. The invoice is +// only canceled when GetTxOuts omits an original outpoint. +func TestSignHtlcTxActionDoesNotCancelOnTxOutLookupError(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + swapHash := lntypes.Hash{9, 8, 6} + originalOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{3}, + Index: 0, + } + + loopIn := &StaticAddressLoopIn{ + SwapHash: swapHash, + DepositOutpoints: []string{originalOutpoint.String()}, + } + + txOutChecker := &recordingTxOutChecker{ + err: errors.New("backend unavailable"), + } + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + InvoicesClient: mockLnd.LndServices.Invoices, + TxOutChecker: txOutChecker, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + event := f.SignHtlcTxAction(ctx, nil) + require.Equal(t, fsm.OnError, event) + require.ErrorContains( + t, f.LastActionError, "unable to get txout", + ) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + t.Fatalf("invoice should not have been canceled: %x", hash) + default: + } +} + +// TestInitHtlcActionCancelsInvoiceOnServerError verifies that an invoice +// created before a server-side rejection is canceled immediately. +func TestInitHtlcActionCancelsInvoiceOnServerError(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + + loopIn := &StaticAddressLoopIn{ + Deposits: []*deposit.Deposit{{ + Value: 200_000, + }}, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + PaymentTimeoutSeconds: DefaultPaymentTimeoutSeconds, + ProtocolVersion: version.ProtocolVersion_V0, + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + DepositManager: &noopDepositManager{}, + WalletKit: mockLnd.WalletKit, + LndClient: mockLnd.Client, + InvoicesClient: mockLnd.LndServices.Invoices, + Server: &initHtlcTestServer{ + loopInErr: errors.New("server rejected swap"), + }, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + // The init step should fail and synchronously trigger deferred invoice + // cleanup. + event := f.InitHtlcAction(ctx, nil) + require.Equal(t, fsm.OnError, event) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, loopIn.SwapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } +} + +// TestInitHtlcActionCancelsInvoiceOnFeeGuardFailure verifies that the early +// fee guard also cancels the pre-created invoice before returning an error. +func TestInitHtlcActionCancelsInvoiceOnFeeGuardFailure(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + serverKey, err := btcec.NewPrivateKey() + require.NoError(t, err) + + loopIn := &StaticAddressLoopIn{ + Deposits: []*deposit.Deposit{{ + Value: 200_000, + }}, + InitiationHeight: uint32(mockLnd.Height), + InitiationTime: time.Now(), + PaymentTimeoutSeconds: DefaultPaymentTimeoutSeconds, + ProtocolVersion: version.ProtocolVersion_V0, + } + + cfg := &Config{ + AddressManager: &mockAddressManager{ + params: &script.Parameters{ + ProtocolVersion: version.ProtocolVersion_V0, + }, + }, + DepositManager: &noopDepositManager{}, + WalletKit: mockLnd.WalletKit, + LndClient: mockLnd.Client, + InvoicesClient: mockLnd.LndServices.Invoices, + Server: &initHtlcTestServer{ + loopInResp: &swapserverrpc.ServerStaticAddressLoopInResponse{ + HtlcServerPubKey: serverKey.PubKey(). + SerializeCompressed(), + HtlcExpiry: mockLnd.Height + + DefaultLoopInOnChainCltvDelta, + StandardHtlcInfo: &swapserverrpc.ServerHtlcSigningInfo{ + FeeRate: 1_000_000, + }, + HighFeeHtlcInfo: &swapserverrpc.ServerHtlcSigningInfo{}, + ExtremeFeeHtlcInfo: &swapserverrpc. + ServerHtlcSigningInfo{}, + }, + }, + ValidateLoopInContract: func(int32, int32) error { + return nil + }, + MaxStaticAddrHtlcFeePercentage: 0, + MaxStaticAddrHtlcBackupFeePercentage: 1, + } + + f, err := NewFSM(ctx, loopIn, cfg, false) + require.NoError(t, err) + + // The fee guard runs before persistence, so the deferred cleanup must + // cancel the invoice on this error path as well. + event := f.InitHtlcAction(ctx, nil) + require.Equal(t, fsm.OnError, event) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, loopIn.SwapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } +} + +// TestUnlockDepositsActionCancelsInvoice verifies that stored swaps that enter +// the generic error unlock path also clean up their swap invoice. +func TestUnlockDepositsActionCancelsInvoice(t *testing.T) { + ctx, cancel := context.WithTimeout(t.Context(), testTimeout) + defer cancel() + + mockLnd := test.NewMockLnd() + dep := &deposit.Deposit{ + Value: 200_000, + } + swapHash := lntypes.Hash{0x44, 0x55} + depositMgr := &recordingDepositManager{} + + f := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + DepositManager: depositMgr, + InvoicesClient: mockLnd.LndServices.Invoices, + }, + loopIn: &StaticAddressLoopIn{ + SwapHash: swapHash, + SwapInvoice: "lnbc1test", + Deposits: []*deposit.Deposit{dep}, + }, + } + + event := f.UnlockDepositsAction(ctx, nil) + require.Equal(t, fsm.OnError, event) + require.NoError(t, f.LastActionError) + + select { + case hash := <-mockLnd.FailInvoiceChannel: + require.Equal(t, swapHash, hash) + + case <-ctx.Done(): + t.Fatalf("invoice was not canceled: %v", ctx.Err()) + } + + require.Len(t, depositMgr.transitions, 1) + require.Equal(t, []*deposit.Deposit{dep}, depositMgr.transitions[0].deposits) + require.Equal(t, fsm.OnError, depositMgr.transitions[0].event) + require.Equal(t, deposit.Deposited, depositMgr.transitions[0].state) +} + +// TestUnlockDepositsActionReportsTransitionError ensures the unlock path +// preserves the real deposit transition failure for callers that need to log it. +func TestUnlockDepositsActionReportsTransitionError(t *testing.T) { + depositMgr := &recordingDepositManager{ + err: errors.New("transition failed"), + } + f := &FSM{ + StateMachine: &fsm.StateMachine{}, + cfg: &Config{ + DepositManager: depositMgr, + }, + loopIn: &StaticAddressLoopIn{ + Deposits: []*deposit.Deposit{{Value: 200_000}}, + }, + } + + event := f.UnlockDepositsAction(t.Context(), nil) + require.Equal(t, fsm.OnError, event) + require.ErrorContains( + t, f.LastActionError, "unable to unlock deposits", + ) + require.ErrorContains(t, f.LastActionError, "transition failed") +} + // mockAddressManager is a minimal AddressManager implementation used by the // test FSM setup. type mockAddressManager struct { @@ -291,7 +3342,17 @@ func (m *mockAddressManager) GetStaticAddress(_ context.Context) ( } // noopDepositManager is a stub DepositManager used to satisfy FSM config. -type noopDepositManager struct{} +type noopDepositManager struct { + deposits []*deposit.Deposit + depositsForOutpoints func([]string, bool) + depositErr error + ensureFreshErr error +} + +// EnsureDepositsFresh implements DepositManager with a no-op. +func (n *noopDepositManager) EnsureDepositsFresh(context.Context) error { + return n.ensureFreshErr +} // GetAllDeposits implements DepositManager with a no-op. func (n *noopDepositManager) GetAllDeposits(_ context.Context) ( @@ -315,10 +3376,14 @@ func (n *noopDepositManager) TransitionDeposits(context.Context, } // DepositsForOutpoints implements DepositManager with a no-op. -func (n *noopDepositManager) DepositsForOutpoints(context.Context, []string, - bool) ([]*deposit.Deposit, error) { +func (n *noopDepositManager) DepositsForOutpoints(_ context.Context, + outpoints []string, ignoreUnknown bool) ([]*deposit.Deposit, error) { - return nil, nil + if n.depositsForOutpoints != nil { + n.depositsForOutpoints(outpoints, ignoreUnknown) + } + + return n.deposits, n.depositErr } // GetActiveDepositsInState implements DepositManager with a no-op. @@ -327,3 +3392,182 @@ func (n *noopDepositManager) GetActiveDepositsInState(fsm.StateType) ( return nil, nil } + +type depositTransition struct { + deposits []*deposit.Deposit + event fsm.EventType + state fsm.StateType +} + +type recordingDepositManager struct { + noopDepositManager + + err error + errs []error + transition func([]*deposit.Deposit, fsm.EventType, fsm.StateType) error + transitions []depositTransition + + transitionChan chan depositTransition + events []fsm.EventType + states []fsm.StateType +} + +// TransitionDeposits records the transition and returns the configured error. +func (r *recordingDepositManager) TransitionDeposits(_ context.Context, + deposits []*deposit.Deposit, event fsm.EventType, + state fsm.StateType) error { + + transition := depositTransition{ + deposits: deposits, + event: event, + state: state, + } + + r.transitions = append(r.transitions, transition) + r.events = append(r.events, event) + r.states = append(r.states, state) + + if r.transitionChan != nil { + r.transitionChan <- transition + } + switch { + case r.transition != nil: + if err := r.transition(deposits, event, state); err != nil { + return err + } + + case len(r.errs) > 0: + err := r.errs[0] + r.errs = r.errs[1:] + if err != nil { + return err + } + + case r.err != nil: + return r.err + } + + for _, d := range deposits { + if d != nil { + d.SetState(state) + } + } + + return nil +} + +type recordingRiskStore struct { + *mockStore + + decisions chan ConfirmationRiskDecision +} + +// RecordStaticAddressRiskDecision records a risk decision in the mock store. +func (s *recordingRiskStore) RecordStaticAddressRiskDecision( + _ context.Context, swapHash lntypes.Hash, + decision ConfirmationRiskDecision) error { + + loopIn, ok := s.loopIns[swapHash] + if !ok { + return ErrLoopInNotFound + } + + loopIn.ConfirmationRiskDecision = decision + loopIn.ConfirmationRiskDecisionTime = time.Now() + + select { + case s.decisions <- decision: + default: + } + + return nil +} + +// mockNotificationManager allows tests to push server notifications directly to +// monitor actions. +type mockNotificationManager struct { + riskAccepted chan *swapserverrpc.ServerStaticLoopInRiskAcceptedNotification + riskRejected chan *swapserverrpc.ServerStaticLoopInRiskRejectedNotification +} + +type silentBlockChainNotifier struct { + lndclient.ChainNotifierClient +} + +// RegisterBlockEpochNtfn implements ChainNotifierClient without delivering an +// initial block. Tests use it to assert current-height recovery behavior without +// relying on a block notification. +func (s *silentBlockChainNotifier) RegisterBlockEpochNtfn(context.Context) ( + chan int32, chan error, error) { + + return make(chan int32), make(chan error), nil +} + +// SubscribeStaticLoopInSweepRequests implements NotificationManager. +func (m *mockNotificationManager) SubscribeStaticLoopInSweepRequests( + context.Context) <-chan *swapserverrpc.ServerStaticLoopInSweepNotification { + + return make(chan *swapserverrpc.ServerStaticLoopInSweepNotification) +} + +// SubscribeStaticLoopInRiskAccepted implements NotificationManager. +func (m *mockNotificationManager) SubscribeStaticLoopInRiskAccepted( + context.Context, lntypes.Hash, +) <-chan *swapserverrpc.ServerStaticLoopInRiskAcceptedNotification { + + return m.riskAccepted +} + +// SubscribeStaticLoopInRiskRejected implements NotificationManager. +func (m *mockNotificationManager) SubscribeStaticLoopInRiskRejected( + context.Context, lntypes.Hash, +) <-chan *swapserverrpc.ServerStaticLoopInRiskRejectedNotification { + + return m.riskRejected +} + +type recordingTxOutChecker struct { + outpoints [][]wire.OutPoint + txOuts map[wire.OutPoint]*wire.TxOut + err error +} + +// GetTxOuts records the request and returns the configured available outputs. +func (r *recordingTxOutChecker) GetTxOuts(_ context.Context, + outpoints []wire.OutPoint) (map[wire.OutPoint]*wire.TxOut, error) { + + r.outpoints = append( + r.outpoints, append([]wire.OutPoint(nil), outpoints...), + ) + if r.err != nil { + return nil, r.err + } + + return r.txOuts, nil +} + +// initHtlcTestServer lets InitHtlcAction tests inject a deterministic server +// response without standing up the full gRPC client. +type initHtlcTestServer struct { + swapserverrpc.StaticAddressServerClient + + loopInResp *swapserverrpc.ServerStaticAddressLoopInResponse + loopInErr error +} + +// ServerStaticAddressLoopIn returns the canned response configured by the test. +func (s *initHtlcTestServer) ServerStaticAddressLoopIn(context.Context, + *swapserverrpc.ServerStaticAddressLoopInRequest, ...grpc.CallOption, +) (*swapserverrpc.ServerStaticAddressLoopInResponse, error) { + + return s.loopInResp, s.loopInErr +} + +// PushStaticAddressHtlcSigs accepts the abandonment signal used by error-path +// tests without adding additional assertions. +func (s *initHtlcTestServer) PushStaticAddressHtlcSigs(context.Context, + *swapserverrpc.PushStaticAddressHtlcSigsRequest, ...grpc.CallOption, +) (*swapserverrpc.PushStaticAddressHtlcSigsResponse, error) { + + return &swapserverrpc.PushStaticAddressHtlcSigsResponse{}, nil +} diff --git a/staticaddr/loopin/autoloop.go b/staticaddr/loopin/autoloop.go index 337d73e1..0bfddaa4 100644 --- a/staticaddr/loopin/autoloop.go +++ b/staticaddr/loopin/autoloop.go @@ -30,6 +30,11 @@ func (m *Manager) PrepareAutoloopLoopIn(ctx context.Context, return nil, 0, false, ErrNoAutoloopCandidate } + err := m.cfg.DepositManager.EnsureDepositsFresh(ctx) + if err != nil { + return nil, 0, false, err + } + allDeposits, err := m.cfg.DepositManager.GetActiveDepositsInState( deposit.Deposited, ) diff --git a/staticaddr/loopin/autoloop_dp.go b/staticaddr/loopin/autoloop_dp.go index c605c43e..86e67fd1 100644 --- a/staticaddr/loopin/autoloop_dp.go +++ b/staticaddr/loopin/autoloop_dp.go @@ -234,9 +234,9 @@ func filterAutoloopCandidateDeposits(maxAmount btcutil.Amount, continue } + confirmationHeight := candidateDeposit.GetConfirmationHeight() swappable := IsSwappable( - uint32(candidateDeposit.ConfirmationHeight), - blockHeight, csvExpiry, + uint32(confirmationHeight), blockHeight, csvExpiry, ) if !swappable { continue @@ -246,8 +246,9 @@ func filterAutoloopCandidateDeposits(maxAmount btcutil.Amount, continue } - residualLife := candidateDeposit.ConfirmationHeight + - int64(csvExpiry) - int64(blockHeight) + residualLife := int64(blocksUntilDepositExpiry( + uint32(confirmationHeight), blockHeight, csvExpiry, + )) eligibleDeposits = append( eligibleDeposits, autoloopCandidateDeposit{ diff --git a/staticaddr/loopin/autoloop_dp_test.go b/staticaddr/loopin/autoloop_dp_test.go index c1253670..bf197ad7 100644 --- a/staticaddr/loopin/autoloop_dp_test.go +++ b/staticaddr/loopin/autoloop_dp_test.go @@ -80,6 +80,28 @@ func TestSelectNoChangeDepositsWithMemoryBudget(t *testing.T) { } } +// TestSelectNoChangeDepositsPrefersConfirmedTie verifies unconfirmed deposits +// are not treated as earlier-expiring than confirmed deposits. Their CSV timer +// has not started yet, so a same-value confirmed deposit should win the expiry +// tie-break. +func TestSelectNoChangeDepositsPrefersConfirmedTie(t *testing.T) { + t.Parallel() + + unconfirmed := makeDeposit(34, 0, 5_000, 0) + confirmed := makeDeposit(35, 0, 5_000, 200) + + deposits, err := selectNoChangeDeposits( + 5_000, 5_000, []*deposit.Deposit{ + unconfirmed, confirmed, + }, 1_000, 100, nil, + ) + require.NoError(t, err) + require.Equal( + t, []string{confirmed.OutPoint.String()}, + depositOutpoints(deposits), + ) +} + // TestAutoloopDPSizing verifies the bucket sizing math. These cases are easier // to understand directly than by inferring the step from a larger selector // behavior test. diff --git a/staticaddr/loopin/fsm.go b/staticaddr/loopin/fsm.go index ea6e610f..eb53dada 100644 --- a/staticaddr/loopin/fsm.go +++ b/staticaddr/loopin/fsm.go @@ -273,6 +273,22 @@ func (f *FSM) updateLoopIn(ctx context.Context, notification fsm.Notification) { return } + + err = f.sendUpdate(ctx) + if err != nil { + f.Errorf("Error sending loop-in update: %v", err) + } +} + +// sendUpdate publishes the latest loop-in state after it has been persisted. +// The callback must remain lightweight because it runs synchronously with FSM +// state transitions. +func (f *FSM) sendUpdate(ctx context.Context) error { + if f.cfg.SendUpdate == nil { + return nil + } + + return f.cfg.SendUpdate(ctx, f.loopIn) } // isUpdateSkipped returns true if the loop-in should not be updated for the diff --git a/staticaddr/loopin/fsm.md b/staticaddr/loopin/fsm.md new file mode 100644 index 00000000..19b22435 --- /dev/null +++ b/staticaddr/loopin/fsm.md @@ -0,0 +1,37 @@ +```mermaid +stateDiagram-v2 +[*] --> InitHtlcTx: OnInitHtlc +Failed +HtlcTimeoutSwept +InitHtlcTx +InitHtlcTx --> UnlockDeposits: OnError +InitHtlcTx --> SignHtlcTx: OnHtlcInitiated +InitHtlcTx --> UnlockDeposits: OnRecover +MonitorHtlcTimeoutSweep +MonitorHtlcTimeoutSweep --> Failed: OnError +MonitorHtlcTimeoutSweep --> HtlcTimeoutSwept: OnHtlcTimeoutSwept +MonitorHtlcTimeoutSweep --> MonitorHtlcTimeoutSweep: OnRecover +MonitorInvoiceAndHtlcTx +MonitorInvoiceAndHtlcTx --> UnlockDeposits: OnError +MonitorInvoiceAndHtlcTx --> PaymentReceived: OnPaymentReceived +MonitorInvoiceAndHtlcTx --> MonitorInvoiceAndHtlcTx: OnRecover +MonitorInvoiceAndHtlcTx --> Failed: OnSwapTimedOut +MonitorInvoiceAndHtlcTx --> SweepHtlcTimeout: OnSweepHtlcTimeout +PaymentReceived +PaymentReceived --> SucceededTransitioningFailed: OnError +PaymentReceived --> Succeeded: OnRecover +PaymentReceived --> Succeeded: OnSucceeded +SignHtlcTx +SignHtlcTx --> UnlockDeposits: OnError +SignHtlcTx --> MonitorInvoiceAndHtlcTx: OnHtlcTxSigned +SignHtlcTx --> UnlockDeposits: OnRecover +Succeeded +SucceededTransitioningFailed +SweepHtlcTimeout +SweepHtlcTimeout --> Failed: OnError +SweepHtlcTimeout --> MonitorHtlcTimeoutSweep: OnHtlcTimeoutSweepPublished +SweepHtlcTimeout --> SweepHtlcTimeout: OnRecover +UnlockDeposits +UnlockDeposits --> Failed: OnError +UnlockDeposits --> UnlockDeposits: OnRecover +``` \ No newline at end of file diff --git a/staticaddr/loopin/interface.go b/staticaddr/loopin/interface.go index c4bbb2b7..d54355a7 100644 --- a/staticaddr/loopin/interface.go +++ b/staticaddr/loopin/interface.go @@ -4,6 +4,7 @@ import ( "context" "github.com/btcsuite/btcd/btcutil" + "github.com/btcsuite/btcd/wire" "github.com/lightninglabs/loop" "github.com/lightninglabs/loop/fsm" "github.com/lightninglabs/loop/staticaddr/deposit" @@ -44,6 +45,9 @@ type AddressManager interface { // DepositManager handles the interaction of loop-ins with deposits. type DepositManager interface { + // EnsureDepositsFresh reconciles active deposits with the wallet view. + EnsureDepositsFresh(ctx context.Context) error + // GetAllDeposits returns all known deposits from the database store. GetAllDeposits(ctx context.Context) ([]*deposit.Deposit, error) @@ -87,6 +91,11 @@ type StaticAddressLoopInStore interface { // IsStored checks if the loop-in is already stored in the database. IsStored(ctx context.Context, swapHash lntypes.Hash) (bool, error) + // RecordStaticAddressRiskDecision persists the server's + // confirmation-risk decision for the loop-in identified by swapHash. + RecordStaticAddressRiskDecision(ctx context.Context, + swapHash lntypes.Hash, decision ConfirmationRiskDecision) error + // GetLoopInByHash returns the loop-in swap with the given hash. GetLoopInByHash(ctx context.Context, swapHash lntypes.Hash) ( *StaticAddressLoopIn, error) @@ -105,10 +114,33 @@ type QuoteGetter interface { numDeposits uint32, fast bool) (*loop.LoopInQuote, error) } +// TxOutChecker checks whether outpoints are still available in the chain +// backend's UTXO view. +type TxOutChecker interface { + // GetTxOuts returns entries for the requested outpoints that are + // available and unspent. Missing entries are unavailable or spent. + GetTxOuts(ctx context.Context, outpoints []wire.OutPoint) ( + map[wire.OutPoint]*wire.TxOut, error) +} + type NotificationManager interface { // SubscribeStaticLoopInSweepRequests subscribes to the static loop in // sweep requests. These are sent by the server to the client to request // a sweep of a static loop in that has been finished. SubscribeStaticLoopInSweepRequests(ctx context.Context, ) <-chan *swapserverrpc.ServerStaticLoopInSweepNotification + + // SubscribeStaticLoopInRiskAccepted subscribes to static loop in risk + // accepted notifications. These are sent by the server after the selected + // deposits are accepted by confirmation risk tracking. + SubscribeStaticLoopInRiskAccepted( + ctx context.Context, swapHash lntypes.Hash, + ) <-chan *swapserverrpc.ServerStaticLoopInRiskAcceptedNotification + + // SubscribeStaticLoopInRiskRejected subscribes to static loop in risk + // rejected notifications. These are sent by the server if it aborts the + // confirmation risk wait before payment. + SubscribeStaticLoopInRiskRejected( + ctx context.Context, swapHash lntypes.Hash, + ) <-chan *swapserverrpc.ServerStaticLoopInRiskRejectedNotification } diff --git a/staticaddr/loopin/loopin.go b/staticaddr/loopin/loopin.go index 0be2ffe4..7fcc3ff9 100644 --- a/staticaddr/loopin/loopin.go +++ b/staticaddr/loopin/loopin.go @@ -31,6 +31,23 @@ import ( "github.com/lightningnetwork/lnd/zpay32" ) +// ConfirmationRiskDecision records the server's decision on whether it accepts +// waiting for low-confirmation deposits before paying a static loop-in invoice. +type ConfirmationRiskDecision string + +const ( + // ConfirmationRiskDecisionNone means no risk decision has been received. + ConfirmationRiskDecisionNone ConfirmationRiskDecision = "" + + // ConfirmationRiskDecisionAccepted means the server accepted waiting for + // deposit confirmations and the payment deadline has started. + ConfirmationRiskDecisionAccepted ConfirmationRiskDecision = "accepted" + + // ConfirmationRiskDecisionRejected means the server stopped waiting for + // deposit confirmations before paying the invoice. + ConfirmationRiskDecisionRejected ConfirmationRiskDecision = "rejected" +) + // StaticAddressLoopIn represents the in-memory loop-in information. type StaticAddressLoopIn struct { // SwapHash is the hashed preimage of the swap invoice. It represents @@ -93,8 +110,6 @@ type StaticAddressLoopIn struct { // The outpoints in the format txid:vout that are part of the loop-in // swap. - // TODO(hieblmi): Replace this with a getter method that fetches the - // outpoints from the deposits. DepositOutpoints []string // SelectedAmount is the amount that the user selected for the swap. If @@ -109,6 +124,15 @@ type StaticAddressLoopIn struct { // LastUpdateTime is the timestamp of the latest persisted state update. LastUpdateTime time.Time + // ConfirmationRiskDecision records the server's persisted decision on + // low-confirmation deposit risk. + ConfirmationRiskDecision ConfirmationRiskDecision + + // ConfirmationRiskDecisionTime is when loopd persisted the server risk + // decision. It is used to reconstruct payment-deadline timeouts after + // restart. + ConfirmationRiskDecisionTime time.Time + // state is the current state of the swap. state fsm.StateType @@ -469,12 +493,28 @@ func (l *StaticAddressLoopIn) TotalDepositAmount() btcutil.Amount { // RemainingPaymentTimeSeconds returns the remaining time in seconds until the // payment timeout is reached. The remaining time is calculated from the -// initiation time of the swap. If more than the swaps configured payment +// initiation time of the swap. If more than the swap's configured payment // timeout has passed, the remaining time will be negative. func (l *StaticAddressLoopIn) RemainingPaymentTimeSeconds() int64 { - elapsedSinceInitiation := time.Since(l.InitiationTime).Seconds() + deadline := l.InitiationTime.Add(l.PaymentTimeoutDuration()) - return int64(l.PaymentTimeoutSeconds) - int64(elapsedSinceInitiation) + return int64(time.Until(deadline).Seconds()) +} + +// PaymentTimeoutDuration returns the configured payment timeout duration, +// falling back to the default if the swap predates the persisted timeout field. +func (l *StaticAddressLoopIn) PaymentTimeoutDuration() time.Duration { + return time.Duration(l.paymentTimeoutSeconds()) * time.Second +} + +// paymentTimeoutSeconds returns the configured timeout in seconds. +func (l *StaticAddressLoopIn) paymentTimeoutSeconds() int64 { + timeoutSeconds := int64(l.PaymentTimeoutSeconds) + if timeoutSeconds == 0 { + timeoutSeconds = int64(DefaultPaymentTimeoutSeconds) + } + + return timeoutSeconds } // Outpoints returns the wire outpoints of the deposits. diff --git a/staticaddr/loopin/loopin_test.go b/staticaddr/loopin/loopin_test.go index d4be020f..8b0892e6 100644 --- a/staticaddr/loopin/loopin_test.go +++ b/staticaddr/loopin/loopin_test.go @@ -147,6 +147,42 @@ func TestCreateHtlcSweepTxSweepValue(t *testing.T) { "the HTLC output") } +// TestPaymentTimeoutDuration verifies that zero timeout values fall back to the +// default payment timeout duration. +func TestPaymentTimeoutDuration(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + paymentTimeoutSeconds uint32 + expected time.Duration + }{ + { + name: "default", + expected: time.Duration(DefaultPaymentTimeoutSeconds) * time.Second, + }, + { + name: "configured", + paymentTimeoutSeconds: 42, + expected: 42 * time.Second, + }, + } + + for _, test := range tests { + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + loopIn := &StaticAddressLoopIn{ + PaymentTimeoutSeconds: test.paymentTimeoutSeconds, + } + + require.Equal( + t, test.expected, loopIn.PaymentTimeoutDuration(), + ) + }) + } +} + // newStaticAddress creates a StaticAddress for testing. func newStaticAddress(clientKey, serverKey *btcec.PublicKey, csvExpiry int64) (*script.StaticAddress, error) { diff --git a/staticaddr/loopin/manager.go b/staticaddr/loopin/manager.go index a76cbba3..47a447fc 100644 --- a/staticaddr/loopin/manager.go +++ b/staticaddr/loopin/manager.go @@ -4,6 +4,7 @@ import ( "bytes" "context" "fmt" + "math" "slices" "sort" "sync/atomic" @@ -56,6 +57,10 @@ type Config struct { // LndClient is used to add invoices and select hop hints. LndClient lndclient.LightningClient + // TxOutChecker checks that selected deposits are still available before + // the client gives the server HTLC signatures. + TxOutChecker TxOutChecker + // InvoicesClient is used to subscribe to invoice settlements and // cancel invoices. InvoicesClient lndclient.InvoicesClient @@ -93,6 +98,9 @@ type Config struct { // request. ValidateLoopInContract ValidateLoopInContract + // SendUpdate publishes a loop-in status update after it is persisted. + SendUpdate func(context.Context, *StaticAddressLoopIn) error + // MaxStaticAddrHtlcFeePercentage is the percentage of the swap amount // that we allow the server to charge for the htlc transaction. // Although highly unlikely, this is a defense against the server @@ -533,17 +541,15 @@ func (m *Manager) recoverLoopIns(ctx context.Context) error { for _, loopIn := range pendingLoopIns { log.Debugf("Recovering loopIn %x", loopIn.SwapHash[:]) - // Retrieve all deposits regardless of deposit state. If any of - // the deposits is not active in the in-mem map of the deposits - // manager we log it, but continue to recover the loop-in. - var allActive bool - loopIn.Deposits, allActive = - m.cfg.DepositManager.AllStringOutpointsActiveDeposits( - loopIn.DepositOutpoints, fsm.EmptyState, - ) - + // Retrieve all deposits regardless of deposit state. If all + // deposits are active in the in-mem map of the deposits manager, + // use those active instances. Otherwise, keep the store's + // swap_hash/deposit-id reconstruction and continue recovery. + activeDeposits, allActive := m.activeDepositsForLoopIn(loopIn) if !allActive { log.Errorf("one or more deposits are not active") + } else { + loopIn.Deposits = activeDeposits } loopIn.AddressParams, err = @@ -627,6 +633,11 @@ func (m *Manager) initiateLoopIn(ctx context.Context, selectedDeposits []*deposit.Deposit ) + err = m.cfg.DepositManager.EnsureDepositsFresh(ctx) + if err != nil { + return nil, fmt.Errorf("unable to refresh deposits: %w", err) + } + // Determine which deposits to use for the loop-in swap. If none are // selected by the client, we will coin-select them based on the amount. switch { @@ -754,8 +765,12 @@ func (m *Manager) initiateLoopIn(ctx context.Context, } swap := &StaticAddressLoopIn{ - SelectedAmount: req.SelectedAmount, - DepositOutpoints: selectedOutpoints, + SelectedAmount: req.SelectedAmount, + // Copy into a nil slice so the swap owns a stable snapshot + // instead of aliasing the caller's selectedOutpoints slice. + DepositOutpoints: append( + []string(nil), selectedOutpoints..., + ), Deposits: selectedDeposits, Label: req.Label, Initiator: req.Initiator, @@ -814,42 +829,36 @@ func (m *Manager) startLoopInFsm(ctx context.Context, func (m *Manager) GetAllSwaps(ctx context.Context) ([]*StaticAddressLoopIn, error) { - swaps, err := m.cfg.Store.GetStaticAddressLoopInSwapsByStates( + return m.cfg.Store.GetStaticAddressLoopInSwapsByStates( ctx, AllStates, ) - if err != nil { - return nil, err - } - - allDeposits, err := m.cfg.DepositManager.GetAllDeposits(ctx) - if err != nil { - return nil, err - } - - var depositLookup = make(map[string]*deposit.Deposit) - for i, d := range allDeposits { - depositLookup[d.OutPoint.String()] = allDeposits[i] - } - - for i, s := range swaps { - var deposits []*deposit.Deposit - for _, outpoint := range s.DepositOutpoints { - if d, ok := depositLookup[outpoint]; ok { - deposits = append(deposits, d) - } - } - - swaps[i].Deposits = deposits - } - - return swaps, nil } -// SelectDeposits sorts the deposits by amount in descending order, then by -// blocks-until-expiry in ascending order. It then selects the deposits that -// are needed to cover the amount requested without leaving a dust change. It -// returns an error if the sum of deposits minus dust is less than the requested -// amount. +// activeDepositsForLoopIn returns the active deposit instances for a loop-in +// using the current deposit outpoints reconstructed by the store. The stored +// deposit outpoint snapshots remain the original swap inputs and are not the +// source of truth for current deposit rows. +func (m *Manager) activeDepositsForLoopIn(loopIn *StaticAddressLoopIn) ( + []*deposit.Deposit, bool) { + + outpoints := loopIn.DepositOutpoints + if len(loopIn.Deposits) > 0 { + outpoints = make([]string, 0, len(loopIn.Deposits)) + for _, d := range loopIn.Deposits { + outpoints = append(outpoints, d.OutPoint.String()) + } + } + + return m.cfg.DepositManager.AllStringOutpointsActiveDeposits( + outpoints, fsm.EmptyState, + ) +} + +// SelectDeposits sorts deposits by confirmation status first, then by amount in +// descending order, then by blocks-until-expiry in ascending order. It then +// selects the deposits that are needed to cover the amount requested without +// leaving a dust change. It returns an error if the sum of deposits minus dust +// is less than the requested amount. func SelectDeposits(targetAmount btcutil.Amount, unfilteredDeposits []*deposit.Deposit, csvExpiry uint32, blockHeight uint32) ([]*deposit.Deposit, error) { @@ -857,8 +866,9 @@ func SelectDeposits(targetAmount btcutil.Amount, // Filter out deposits that are too close to expiry to be swapped. var deposits []*deposit.Deposit for _, d := range unfilteredDeposits { + confirmationHeight := d.GetConfirmationHeight() if !IsSwappable( - uint32(d.ConfirmationHeight), blockHeight, csvExpiry, + uint32(confirmationHeight), blockHeight, csvExpiry, ) { log.Debugf("Skipping deposit %s as it expires before "+ @@ -870,14 +880,27 @@ func SelectDeposits(targetAmount btcutil.Amount, deposits = append(deposits, d) } - // Sort the deposits by amount in descending order, then by - // blocks-until-expiry in ascending order. + // Sort confirmed deposits ahead of unconfirmed ones so auto-selection + // prefers deposits the server can accept immediately. Within each group + // we prefer larger deposits, then earlier expiries. sort.Slice(deposits, func(i, j int) bool { + iConfirmationHeight := deposits[i].GetConfirmationHeight() + jConfirmationHeight := deposits[j].GetConfirmationHeight() + iConfirmed := iConfirmationHeight > 0 + jConfirmed := jConfirmationHeight > 0 + if iConfirmed != jConfirmed { + return iConfirmed + } + if deposits[i].Value == deposits[j].Value { - iExp := uint32(deposits[i].ConfirmationHeight) + - csvExpiry - blockHeight - jExp := uint32(deposits[j].ConfirmationHeight) + - csvExpiry - blockHeight + iExp := blocksUntilDepositExpiry( + uint32(iConfirmationHeight), blockHeight, + csvExpiry, + ) + jExp := blocksUntilDepositExpiry( + uint32(jConfirmationHeight), blockHeight, + csvExpiry, + ) return iExp < jExp } @@ -909,20 +932,33 @@ func SelectDeposits(targetAmount btcutil.Amount, // IsSwappable checks if a deposit is swappable. It returns true if the deposit // is not expired and the htlc is not too close to expiry. func IsSwappable(confirmationHeight, blockHeight, csvExpiry uint32) bool { - // The deposit expiry height is the confirmation height plus the csv - // expiry. - depositExpiryHeight := confirmationHeight + csvExpiry - - // The htlc expiry height is the current height plus the htlc - // cltv delta. - htlcExpiryHeight := blockHeight + DefaultLoopInOnChainCltvDelta - - // Ensure that the deposit doesn't expire before the htlc. - if depositExpiryHeight < htlcExpiryHeight+DepositHtlcDelta { - return false + if confirmationHeight == 0 { + return true } - return true + // The deposit expiry height is the confirmation height plus the csv + // expiry. + return blocksUntilDepositExpiry( + confirmationHeight, blockHeight, csvExpiry, + ) >= DefaultLoopInOnChainCltvDelta+DepositHtlcDelta +} + +// blocksUntilDepositExpiry returns the remaining number of blocks until a +// deposit expires. Unconfirmed deposits return MaxUint32 because their CSV has +// not started yet. +func blocksUntilDepositExpiry(confirmationHeight, blockHeight, + csvExpiry uint32) uint32 { + + if confirmationHeight == 0 { + return math.MaxUint32 + } + + depositExpiryHeight := confirmationHeight + csvExpiry + if depositExpiryHeight <= blockHeight { + return 0 + } + + return depositExpiryHeight - blockHeight } // DeduceSwapAmount calculates the swap amount based on the selected amount and diff --git a/staticaddr/loopin/manager_test.go b/staticaddr/loopin/manager_test.go index 85178a05..9fa8587c 100644 --- a/staticaddr/loopin/manager_test.go +++ b/staticaddr/loopin/manager_test.go @@ -81,6 +81,27 @@ func TestSelectDeposits(t *testing.T) { expected: []*deposit.Deposit{d3}, expectedErr: "", }, + { + name: "prefer confirmed deposit over larger unconfirmed one", + deposits: []*deposit.Deposit{ + { + Value: 2_000_000, + ConfirmationHeight: 0, + }, + { + Value: 1_500_000, + ConfirmationHeight: 5_004, + }, + }, + targetValue: 1_000_000, + expected: []*deposit.Deposit{ + { + Value: 1_500_000, + ConfirmationHeight: 5_004, + }, + }, + expectedErr: "", + }, { name: "single deposit insufficient by 1", deposits: []*deposit.Deposit{d1}, @@ -224,6 +245,45 @@ func TestInitiateLoopInAllowsReservedAutoloopLabel(t *testing.T) { require.Equal(t, selectedDeposit.Value, quoteGetter.amount) } +// TestUpdateLoopInSendsUpdateAfterSuccessfulStoreUpdate protects the +// notification contract: after a successful database update, the manager must +// publish the stored loop-in state to listeners. +func TestUpdateLoopInSendsUpdateAfterSuccessfulStoreUpdate(t *testing.T) { + ctx := t.Context() + swapHash := lntypes.Hash{1, 2, 3} + updates := make(chan *StaticAddressLoopIn, 1) + loopIn := &StaticAddressLoopIn{SwapHash: swapHash} + loopIn.SetState(SignHtlcTx) + + loopInFsm := &FSM{ + cfg: &Config{ + Store: &mockStore{stored: true}, + SendUpdate: func(_ context.Context, + updated *StaticAddressLoopIn) error { + + updates <- updated + + return nil + }, + }, + loopIn: loopIn, + } + + loopInFsm.updateLoopIn(ctx, fsm.Notification{ + PreviousState: SignHtlcTx, + NextState: MonitorInvoiceAndHtlcTx, + }) + + select { + case updated := <-updates: + require.Equal(t, swapHash, updated.SwapHash) + require.Equal(t, MonitorInvoiceAndHtlcTx, updated.GetState()) + + case <-ctx.Done(): + t.Fatal(ctx.Err()) + } +} + // TestHandleLoopInSweepReqRejectsInvalidServerNonce ensures that a malformed // MuSig2 nonce returned by the server is rejected before it reaches the signer. func TestHandleLoopInSweepReqRejectsInvalidServerNonce(t *testing.T) { @@ -298,6 +358,71 @@ func TestHandleLoopInSweepReqRejectsInvalidServerNonce(t *testing.T) { require.ErrorContains(t, err, depOutpoint) } +// TestActiveDepositsForLoopInUsesCurrentDepositOutpoints verifies that +// recovery checks the current deposit outpoints reconstructed by the store +// rather than the original outpoint snapshot persisted on the swap. +func TestActiveDepositsForLoopInUsesCurrentDepositOutpoints(t *testing.T) { + oldOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{0xaa}, + Index: 0, + } + currentDeposit := makeDeposit(0xbb, 1, 10_000, 42) + + manager := &Manager{ + cfg: &Config{ + DepositManager: &mockDepositManager{ + byOutpoint: map[string]*deposit.Deposit{ + currentDeposit.OutPoint.String(): currentDeposit, + }, + }, + }, + } + + deposits, allActive := manager.activeDepositsForLoopIn( + &StaticAddressLoopIn{ + DepositOutpoints: []string{oldOutpoint.String()}, + Deposits: []*deposit.Deposit{currentDeposit}, + }, + ) + require.True(t, allActive) + require.Equal(t, []*deposit.Deposit{currentDeposit}, deposits) +} + +// TestGetAllSwapsPreservesStoreDeposits verifies that list responses keep the +// store's swap_hash/deposit-id reconstruction even when DepositOutpoints is an +// original input snapshot and the deposit's current outpoint has changed. +func TestGetAllSwapsPreservesStoreDeposits(t *testing.T) { + oldOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{0xcc}, + Index: 0, + } + currentDeposit := makeDeposit(0xdd, 1, 10_000, 42) + swap := &StaticAddressLoopIn{ + DepositOutpoints: []string{oldOutpoint.String()}, + Deposits: []*deposit.Deposit{currentDeposit}, + } + + manager := &Manager{ + cfg: &Config{ + Store: &mockStore{ + swaps: []*StaticAddressLoopIn{swap}, + }, + }, + } + + swaps, err := manager.GetAllSwaps(t.Context()) + require.NoError(t, err) + require.Len(t, swaps, 1) + require.Equal(t, []string{oldOutpoint.String()}, swaps[0].DepositOutpoints) + require.Equal(t, []*deposit.Deposit{currentDeposit}, swaps[0].Deposits) +} + +// TestIsSwappableUnconfirmed checks that an unconfirmed deposit is considered +// swappable because its CSV timeout has not started yet. +func TestIsSwappableUnconfirmed(t *testing.T) { + require.True(t, IsSwappable(0, 5000, 1000)) +} + // mockDepositManager implements DepositManager for tests. type mockDepositManager struct { // activeDeposits is the set returned by GetActiveDepositsInState. @@ -307,6 +432,10 @@ type mockDepositManager struct { byOutpoint map[string]*deposit.Deposit } +func (m *mockDepositManager) EnsureDepositsFresh(context.Context) error { + return nil +} + func (m *mockDepositManager) GetAllDeposits(_ context.Context) ( []*deposit.Deposit, error) { @@ -316,7 +445,7 @@ func (m *mockDepositManager) GetAllDeposits(_ context.Context) ( func (m *mockDepositManager) AllStringOutpointsActiveDeposits(outpoints []string, state fsm.StateType) ([]*deposit.Deposit, bool) { - if state != deposit.Deposited { + if state != deposit.Deposited && state != fsm.EmptyState { return nil, false } @@ -408,8 +537,10 @@ func (m *mockQuoteGetter) GetLoopInQuote(_ context.Context, // mockStore implements StaticAddressLoopInStore for tests. type mockStore struct { + swaps []*StaticAddressLoopIn loopIns map[lntypes.Hash]*StaticAddressLoopIn mapIDs map[lntypes.Hash][]deposit.ID + stored bool } func (s *mockStore) CreateLoopIn(_ context.Context, @@ -427,10 +558,17 @@ func (s *mockStore) UpdateLoopIn(_ context.Context, func (s *mockStore) GetStaticAddressLoopInSwapsByStates(_ context.Context, _ []fsm.StateType) ([]*StaticAddressLoopIn, error) { - return nil, nil + return s.swaps, nil } func (s *mockStore) IsStored(_ context.Context, _ lntypes.Hash) (bool, error) { - return false, nil + return s.stored, nil +} + +// RecordStaticAddressRiskDecision implements Store for manager tests. +func (s *mockStore) RecordStaticAddressRiskDecision(context.Context, + lntypes.Hash, ConfirmationRiskDecision) error { + + return nil } func (s *mockStore) GetLoopInByHash(_ context.Context, diff --git a/staticaddr/loopin/risk_watcher.go b/staticaddr/loopin/risk_watcher.go new file mode 100644 index 00000000..6df9a745 --- /dev/null +++ b/staticaddr/loopin/risk_watcher.go @@ -0,0 +1,196 @@ +package loopin + +import ( + "bytes" + "context" + "time" + + "github.com/lightningnetwork/lnd/lntypes" +) + +// confirmationRiskUpdate is the normalized result of a server confirmation-risk +// notification. +type confirmationRiskUpdate struct { + decision ConfirmationRiskDecision + reason string +} + +// confirmationRiskWatcher normalizes static loop-in confirmation risk +// notifications and restores the durable decision timestamp. +type confirmationRiskWatcher struct { + swapHash lntypes.Hash + store StaticAddressLoopInStore + notificationManager NotificationManager + logWarnf func(string, ...any) +} + +// newConfirmationRiskWatcher creates a helper that handles confirmation-risk +// notification plumbing for a single static loop-in swap. +func newConfirmationRiskWatcher(cfg *Config, swapHash lntypes.Hash, + warnf func(string, ...any)) *confirmationRiskWatcher { + + return &confirmationRiskWatcher{ + swapHash: swapHash, + store: cfg.Store, + notificationManager: cfg.NotificationManager, + logWarnf: warnf, + } +} + +// warnf logs through the FSM-scoped logger when one is available. +func (w *confirmationRiskWatcher) warnf(format string, args ...any) { + if w.logWarnf != nil { + w.logWarnf(format, args...) + + return + } + + log.Warnf(format, args...) +} + +// subscribe subscribes to accepted and rejected confirmation-risk notifications +// and emits normalized updates for the watcher's swap hash. +func (w *confirmationRiskWatcher) subscribe(ctx context.Context) ( + <-chan confirmationRiskUpdate, func()) { + + if w.notificationManager == nil { + return nil, func() {} + } + + notificationCtx, cancel := context.WithCancel(ctx) + riskAcceptedChan := w.notificationManager.SubscribeStaticLoopInRiskAccepted( + notificationCtx, w.swapHash, + ) + riskRejectedChan := w.notificationManager.SubscribeStaticLoopInRiskRejected( + notificationCtx, w.swapHash, + ) + + riskUpdates := make(chan confirmationRiskUpdate, 1) + go func() { + defer close(riskUpdates) + + for { + select { + case riskAccepted, ok := <-riskAcceptedChan: + if !ok { + riskAcceptedChan = nil + continue + } + + if riskAccepted == nil || !bytes.Equal( + riskAccepted.SwapHash, w.swapHash[:], + ) { + + continue + } + + update := confirmationRiskUpdate{ + decision: ConfirmationRiskDecisionAccepted, + reason: "risk accepted notification", + } + select { + case riskUpdates <- update: + case <-notificationCtx.Done(): + return + } + + case riskRejected, ok := <-riskRejectedChan: + if !ok { + riskRejectedChan = nil + continue + } + + if riskRejected == nil || !bytes.Equal( + riskRejected.SwapHash, w.swapHash[:], + ) { + + continue + } + + update := confirmationRiskUpdate{ + decision: ConfirmationRiskDecisionRejected, + reason: "risk rejection", + } + select { + case riskUpdates <- update: + case <-notificationCtx.Done(): + return + } + + case <-notificationCtx.Done(): + return + } + } + }() + + return riskUpdates, cancel +} + +// durableDecisionTime returns the durable decision timestamp, recording the +// decision first if the notification was replayed before it could be persisted. +// The bool is false when a configured store could not durably record or reload +// the decision. +func (w *confirmationRiskWatcher) durableDecisionTime(ctx context.Context, + decision ConfirmationRiskDecision) (time.Time, bool) { + + now := time.Now() + if w.store == nil { + return now, true + } + + storedLoopIn, err := w.store.GetLoopInByHash(ctx, w.swapHash) + if err != nil { + w.warnf("unable to reload persisted risk decision for swap %v: %v", + w.swapHash, err) + + return time.Time{}, false + } + + if storedLoopIn == nil { + return time.Time{}, false + } + + hasPersistedDecision := + storedLoopIn.ConfirmationRiskDecision == decision && + !storedLoopIn.ConfirmationRiskDecisionTime.IsZero() + + if !hasPersistedDecision { + err = w.store.RecordStaticAddressRiskDecision( + ctx, w.swapHash, decision, + ) + if err != nil { + w.warnf("unable to persist replayed risk decision for "+ + "swap %v: %v", w.swapHash, err) + + return time.Time{}, false + } + + storedLoopIn, err = w.store.GetLoopInByHash(ctx, w.swapHash) + if err != nil { + w.warnf("unable to reload persisted risk decision for "+ + "swap %v: %v", w.swapHash, err) + + return time.Time{}, false + } + if storedLoopIn == nil || + storedLoopIn.ConfirmationRiskDecision != decision || + storedLoopIn.ConfirmationRiskDecisionTime.IsZero() { + + return time.Time{}, false + } + } + + return storedLoopIn.ConfirmationRiskDecisionTime, true +} + +// decisionTime retains the best-effort behavior used for server notifications. +func (w *confirmationRiskWatcher) decisionTime(ctx context.Context, + decision ConfirmationRiskDecision) time.Time { + + decisionTime, ok := w.durableDecisionTime(ctx, decision) + if !ok { + return time.Now() + } + + return decisionTime +} diff --git a/staticaddr/loopin/risk_watcher_test.go b/staticaddr/loopin/risk_watcher_test.go new file mode 100644 index 00000000..4f907615 --- /dev/null +++ b/staticaddr/loopin/risk_watcher_test.go @@ -0,0 +1,121 @@ +package loopin + +import ( + "context" + "testing" + "time" + + "github.com/lightninglabs/loop/swapserverrpc" + "github.com/lightningnetwork/lnd/lntypes" + "github.com/stretchr/testify/require" +) + +// TestConfirmationRiskWatcherSubscribeFiltersSwapHash verifies that the watcher +// only emits normalized decisions for the swap it was created for. +func TestConfirmationRiskWatcherSubscribeFiltersSwapHash(t *testing.T) { + t.Parallel() + + ctx, cancel := context.WithTimeout(t.Context(), time.Second) + defer cancel() + + swapHash := lntypes.Hash{1, 2, 3} + otherHash := lntypes.Hash{3, 2, 1} + notificationMgr := &mockNotificationManager{ + riskAccepted: make( + chan *swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification, 1, + ), + riskRejected: make( + chan *swapserverrpc. + ServerStaticLoopInRiskRejectedNotification, 1, + ), + } + + watcher := newConfirmationRiskWatcher( + &Config{NotificationManager: notificationMgr}, swapHash, + t.Logf, + ) + updates, stop := watcher.subscribe(ctx) + defer stop() + + notificationMgr.riskAccepted <- &swapserverrpc. + ServerStaticLoopInRiskAcceptedNotification{ + SwapHash: otherHash[:], + } + + select { + case update := <-updates: + t.Fatalf("received wrong-hash risk update: %v", update) + + case <-time.After(100 * time.Millisecond): + } + + notificationMgr.riskRejected <- &swapserverrpc. + ServerStaticLoopInRiskRejectedNotification{ + SwapHash: swapHash[:], + } + + select { + case update := <-updates: + require.Equal(t, ConfirmationRiskDecisionRejected, + update.decision) + require.Equal(t, "risk rejection", update.reason) + + case <-ctx.Done(): + t.Fatalf("risk update not received: %v", ctx.Err()) + } +} + +// TestConfirmationRiskWatcherDecisionTimeRestoration verifies that the watcher +// preserves existing persisted decision timestamps and records missing ones. +func TestConfirmationRiskWatcherDecisionTimeRestoration(t *testing.T) { + t.Parallel() + + ctx := t.Context() + swapHash := lntypes.Hash{4, 5, 6} + decisionTime := time.Unix(123, 0).UTC() + store := &recordingRiskStore{ + mockStore: &mockStore{ + loopIns: map[lntypes.Hash]*StaticAddressLoopIn{ + swapHash: { + ConfirmationRiskDecision: ConfirmationRiskDecisionAccepted, + ConfirmationRiskDecisionTime: decisionTime, + }, + }, + }, + decisions: make(chan ConfirmationRiskDecision, 1), + } + + watcher := newConfirmationRiskWatcher(&Config{Store: store}, swapHash, + t.Logf) + restoredTime := watcher.decisionTime( + ctx, ConfirmationRiskDecisionAccepted, + ) + require.True(t, restoredTime.Equal(decisionTime)) + + select { + case decision := <-store.decisions: + t.Fatalf("persisted already-recorded decision: %v", decision) + + default: + } + + store.loopIns[swapHash] = &StaticAddressLoopIn{} + recordedTime := watcher.decisionTime( + ctx, ConfirmationRiskDecisionRejected, + ) + require.False(t, recordedTime.IsZero()) + + select { + case decision := <-store.decisions: + require.Equal(t, ConfirmationRiskDecisionRejected, decision) + + case <-time.After(time.Second): + t.Fatal("missing risk decision was not persisted") + } + require.Equal(t, ConfirmationRiskDecisionRejected, + store.loopIns[swapHash].ConfirmationRiskDecision) + require.True(t, recordedTime.Equal( + store.loopIns[swapHash].ConfirmationRiskDecisionTime, + )) +} diff --git a/staticaddr/loopin/sql_store.go b/staticaddr/loopin/sql_store.go index d06c5c18..9dc2a084 100644 --- a/staticaddr/loopin/sql_store.go +++ b/staticaddr/loopin/sql_store.go @@ -5,6 +5,7 @@ import ( "database/sql" "errors" "strings" + "time" "github.com/btcsuite/btcd/btcec/v2" "github.com/btcsuite/btcd/btcutil" @@ -23,10 +24,19 @@ import ( const OutpointSeparator = ";" +// sqlStoreUpdateTime returns the PostgreSQL-compatible timestamp precision used +// for persisted loop-in update metadata. +func sqlStoreUpdateTime(clock clock.Clock) time.Time { + return clock.Now().Truncate(time.Microsecond) +} + var ( // ErrInvalidOutpoint is returned when an outpoint contains the outpoint // separator. ErrInvalidOutpoint = errors.New("outpoint contains outpoint separator") + + // ErrLoopInNotFound is returned when a loop-in swap is not stored. + ErrLoopInNotFound = errors.New("static address loop-in not found") ) // Querier is the interface that contains all the queries generated by sqlc for @@ -51,13 +61,18 @@ type Querier interface { UpdateStaticAddressLoopIn(ctx context.Context, arg sqlc.UpdateStaticAddressLoopInParams) error + // RecordStaticAddressRiskDecision stores the server's confirmation-risk + // decision for a loop-in swap. + RecordStaticAddressRiskDecision(ctx context.Context, + arg sqlc.RecordStaticAddressRiskDecisionParams) error + // GetStaticAddressLoopInSwap retrieves a loop-in swap by its swap hash. GetStaticAddressLoopInSwap(ctx context.Context, swapHash []byte) (sqlc.GetStaticAddressLoopInSwapRow, error) // GetStaticAddressLoopInSwapsByStates retrieves all swaps with the - // given states. The states string is an input for the IN primitive in - // sqlite, hence the format needs to be '{State1,State2,...}'. + // given states. The states string is comma-separated so the query can + // match complete state names by wrapping it with comma sentinels. GetStaticAddressLoopInSwapsByStates(ctx context.Context, states sql.NullString) ([]sqlc.GetStaticAddressLoopInSwapsByStatesRow, error) @@ -203,7 +218,7 @@ func (s *SqlStore) GetStaticAddressLoopInSwapsByStates(ctx context.Context, } func toJointStringStates(states []fsm.StateType) string { - return "{" + strings.Join(toStrings(states), ",") + "}" + return strings.Join(toStrings(states), ",") } func toStrings(states []fsm.StateType) []string { @@ -280,13 +295,14 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context, Fast: loopIn.Fast, } + updateTime := sqlStoreUpdateTime(s.clock) updateArgs := sqlc.InsertStaticAddressMetaUpdateParams{ SwapHash: loopIn.SwapHash[:], - UpdateTimestamp: s.clock.Now(), + UpdateTimestamp: updateTime, UpdateState: string(loopIn.GetState()), } - return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), + err := s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), func(q Querier) error { err := q.InsertSwap(ctx, swapArgs) if err != nil { @@ -323,6 +339,13 @@ func (s *SqlStore) CreateLoopIn(ctx context.Context, return q.InsertStaticAddressMetaUpdate(ctx, updateArgs) }, ) + if err != nil { + return err + } + + loopIn.LastUpdateTime = updateTime + + return nil } // UpdateLoopIn updates the loop-in in the database. @@ -343,13 +366,14 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context, }, } + updateTime := sqlStoreUpdateTime(s.clock) updateArgs := sqlc.InsertStaticAddressMetaUpdateParams{ SwapHash: loopIn.SwapHash[:], UpdateState: string(loopIn.GetState()), - UpdateTimestamp: s.clock.Now(), + UpdateTimestamp: updateTime, } - return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), + err := s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), func(q Querier) error { err := q.UpdateStaticAddressLoopIn(ctx, updateParams) if err != nil { @@ -359,6 +383,50 @@ func (s *SqlStore) UpdateLoopIn(ctx context.Context, return q.InsertStaticAddressMetaUpdate(ctx, updateArgs) }, ) + if err != nil { + return err + } + + loopIn.LastUpdateTime = updateTime + + return nil +} + +// RecordStaticAddressRiskDecision stores the server's confirmation-risk +// decision for a static address loop-in. The timestamp is written by the store +// so recovery can reconstruct the remaining payment deadline from one durable +// clock source. +func (s *SqlStore) RecordStaticAddressRiskDecision(ctx context.Context, + swapHash lntypes.Hash, decision ConfirmationRiskDecision) error { + + if decision != ConfirmationRiskDecisionAccepted && + decision != ConfirmationRiskDecisionRejected { + + return errors.New("unknown confirmation risk decision") + } + + params := sqlc.RecordStaticAddressRiskDecisionParams{ + SwapHash: swapHash[:], + ConfirmationRiskDecision: string(decision), + ConfirmationRiskDecisionTime: sql.NullTime{ + Time: s.clock.Now(), + Valid: true, + }, + } + + return s.baseDB.ExecTx(ctx, loopdb.NewSqlWriteOpts(), + func(q Querier) error { + stored, err := q.IsStored(ctx, swapHash[:]) + if err != nil { + return err + } + if !stored { + return ErrLoopInNotFound + } + + return q.RecordStaticAddressRiskDecision(ctx, params) + }, + ) } func (s *SqlStore) BatchUpdateSelectedSwapAmounts(ctx context.Context, @@ -507,9 +575,12 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, } } - depositOutpoints := strings.Split( - swap.DepositOutpoints, OutpointSeparator, - ) + var depositOutpoints []string + if swap.DepositOutpoints != "" { + depositOutpoints = strings.Split( + swap.DepositOutpoints, OutpointSeparator, + ) + } timeoutAddressString := swap.HtlcTimeoutSweepAddress var timeoutAddress btcutil.Address @@ -555,6 +626,7 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, depositList = append(depositList, deposit) } + depositList = orderDepositsBySnapshot(depositList, depositOutpoints) loopIn := &StaticAddressLoopIn{ SwapHash: swapHash, @@ -580,6 +652,9 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, DepositOutpoints: depositOutpoints, SelectedAmount: btcutil.Amount(swap.SelectedAmount), Fast: swap.Fast, + ConfirmationRiskDecision: ConfirmationRiskDecision( + swap.ConfirmationRiskDecision, + ), HtlcTxFeeRate: chainfee.SatPerKWeight( swap.HtlcTxFeeRateSatKw, ), @@ -587,6 +662,10 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, HtlcTimeoutSweepTxHash: htlcTimeoutSweepTxHash, Deposits: depositList, } + if swap.ConfirmationRiskDecisionTime.Valid { + loopIn.ConfirmationRiskDecisionTime = + swap.ConfirmationRiskDecisionTime.Time + } if len(updates) > 0 { lastUpdate := updates[len(updates)-1] @@ -596,3 +675,37 @@ func toStaticAddressLoopIn(_ context.Context, network *chaincfg.Params, return loopIn, nil } + +// orderDepositsBySnapshot returns deposits ordered by the stored outpoint +// snapshot when the current deposit rows still match that snapshot. If any +// snapshot outpoint no longer maps to a current deposit row, recovery keeps the +// store reconstruction untouched so callers can handle the divergence. +func orderDepositsBySnapshot(deposits []*deposit.Deposit, + depositOutpoints []string) []*deposit.Deposit { + + if len(deposits) != len(depositOutpoints) { + return deposits + } + + byOutpoint := make(map[string]*deposit.Deposit, len(deposits)) + for _, d := range deposits { + outpoint := d.OutPoint.String() + if _, ok := byOutpoint[outpoint]; ok { + return deposits + } + + byOutpoint[outpoint] = d + } + + orderedDeposits := make([]*deposit.Deposit, len(depositOutpoints)) + for i, outpoint := range depositOutpoints { + d, ok := byOutpoint[outpoint] + if !ok { + return deposits + } + + orderedDeposits[i] = d + } + + return orderedDeposits +} diff --git a/staticaddr/loopin/sql_store_test.go b/staticaddr/loopin/sql_store_test.go index 1e30081d..c08d940f 100644 --- a/staticaddr/loopin/sql_store_test.go +++ b/staticaddr/loopin/sql_store_test.go @@ -41,8 +41,10 @@ func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) { } loopingDepositID := newID() + timeoutDepositID := newID() loopedInDepositID := newID() - d1, d2 := &deposit.Deposit{ + failedDepositID := newID() + d1, d2, d3, d4 := &deposit.Deposit{ ID: loopingDepositID, OutPoint: wire.OutPoint{ Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d}, @@ -54,7 +56,7 @@ func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) { }, }, &deposit.Deposit{ - ID: loopedInDepositID, + ID: timeoutDepositID, OutPoint: wire.OutPoint{ Hash: chainhash.Hash{0x2a, 0x2b, 0x3c, 0x4e}, Index: 1, @@ -63,29 +65,67 @@ func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) { TimeOutSweepPkScript: []byte{ 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, }, + }, + &deposit.Deposit{ + ID: loopedInDepositID, + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x3a, 0x2b, 0x3c, 0x4e}, + Index: 2, + }, + Value: btcutil.Amount(300_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4f, + }, + }, + &deposit.Deposit{ + ID: failedDepositID, + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x4a, 0x2b, 0x3c, 0x4e}, + Index: 3, + }, + Value: btcutil.Amount(400_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x50, + }, } err := depositStore.CreateDeposit(ctxb, d1) require.NoError(t, err) err = depositStore.CreateDeposit(ctxb, d2) require.NoError(t, err) + err = depositStore.CreateDeposit(ctxb, d3) + require.NoError(t, err) + err = depositStore.CreateDeposit(ctxb, d4) + require.NoError(t, err) // Add two updates per deposit, expect the last to be retrieved. d1.SetState(deposit.Deposited) d2.SetState(deposit.Deposited) + d3.SetState(deposit.Deposited) + d4.SetState(deposit.Deposited) err = depositStore.UpdateDeposit(ctxb, d1) require.NoError(t, err) err = depositStore.UpdateDeposit(ctxb, d2) require.NoError(t, err) + err = depositStore.UpdateDeposit(ctxb, d3) + require.NoError(t, err) + err = depositStore.UpdateDeposit(ctxb, d4) + require.NoError(t, err) d1.SetState(deposit.LoopingIn) - d2.SetState(deposit.LoopedIn) + d2.SetState(deposit.HtlcTimeoutSwept) + d3.SetState(deposit.LoopedIn) + d4.SetState(deposit.Deposited) err = depositStore.UpdateDeposit(ctxb, d1) require.NoError(t, err) err = depositStore.UpdateDeposit(ctxb, d2) require.NoError(t, err) + err = depositStore.UpdateDeposit(ctxb, d3) + require.NoError(t, err) + err = depositStore.UpdateDeposit(ctxb, d4) + require.NoError(t, err) _, clientPubKey := test.CreateKey(1) _, serverPubKey := test.CreateKey(2) @@ -108,13 +148,30 @@ func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) { err = swapStore.CreateLoopIn(ctxb, &swapPending) require.NoError(t, err) + // Create htlc-timeout-swept swap. HtlcTimeoutSwept is the first final + // state, so this exercises the state-list query boundary. + swapHashTimeoutSwept := lntypes.Hash{0x4, 0x2, 0x3, 0x5} + swapTimeoutSwept := StaticAddressLoopIn{ + SwapHash: swapHashTimeoutSwept, + SwapPreimage: lntypes.Preimage{0x4, 0x2, 0x3, 0x5}, + DepositOutpoints: []string{d2.OutPoint.String()}, + Deposits: []*deposit.Deposit{d2}, + ClientPubkey: clientPubKey, + ServerPubkey: serverPubKey, + HtlcTimeoutSweepAddress: addr, + } + swapTimeoutSwept.SetState(HtlcTimeoutSwept) + + err = swapStore.CreateLoopIn(ctxb, &swapTimeoutSwept) + require.NoError(t, err) + // Create succeeded swap. swapHashSucceeded := lntypes.Hash{0x2, 0x2, 0x3, 0x5} swapSucceeded := StaticAddressLoopIn{ SwapHash: swapHashSucceeded, SwapPreimage: lntypes.Preimage{0x2, 0x2, 0x3, 0x5}, - DepositOutpoints: []string{d2.OutPoint.String()}, - Deposits: []*deposit.Deposit{d2}, + DepositOutpoints: []string{d3.OutPoint.String()}, + Deposits: []*deposit.Deposit{d3}, ClientPubkey: clientPubKey, ServerPubkey: serverPubKey, HtlcTimeoutSweepAddress: addr, @@ -124,6 +181,23 @@ func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) { err = swapStore.CreateLoopIn(ctxb, &swapSucceeded) require.NoError(t, err) + // Create failed swap. Failed is the last final state, so this + // exercises the state-list query boundary. + swapHashFailed := lntypes.Hash{0x3, 0x2, 0x3, 0x5} + swapFailed := StaticAddressLoopIn{ + SwapHash: swapHashFailed, + SwapPreimage: lntypes.Preimage{0x3, 0x2, 0x3, 0x5}, + DepositOutpoints: []string{d4.OutPoint.String()}, + Deposits: []*deposit.Deposit{d4}, + ClientPubkey: clientPubKey, + ServerPubkey: serverPubKey, + HtlcTimeoutSweepAddress: addr, + } + swapFailed.SetState(Failed) + + err = swapStore.CreateLoopIn(ctxb, &swapFailed) + require.NoError(t, err) + pendingSwaps, err := swapStore.GetStaticAddressLoopInSwapsByStates(ctxb, PendingStates) require.NoError(t, err) @@ -142,16 +216,33 @@ func TestGetStaticAddressLoopInSwapsByStates(t *testing.T) { finalizedSwaps, err := swapStore.GetStaticAddressLoopInSwapsByStates(ctxb, FinalStates) require.NoError(t, err) - require.Len(t, finalizedSwaps, 1) - require.Equal(t, swapHashSucceeded, finalizedSwaps[0].SwapHash) - require.Equal(t, []string{d2.OutPoint.String()}, finalizedSwaps[0].DepositOutpoints) - require.Equal(t, Succeeded, finalizedSwaps[0].GetState()) + require.Len(t, finalizedSwaps, 3) + finalizedByState := make(map[string]*StaticAddressLoopIn) + for _, swap := range finalizedSwaps { + finalizedByState[string(swap.GetState())] = swap + } - finalizedDeposits := finalizedSwaps[0].Deposits + timeoutSweptSwap := finalizedByState[string(HtlcTimeoutSwept)] + require.NotNil(t, timeoutSweptSwap) + require.Equal(t, swapHashTimeoutSwept, timeoutSweptSwap.SwapHash) + require.Equal(t, HtlcTimeoutSwept, timeoutSweptSwap.GetState()) + + succeededSwap := finalizedByState[string(Succeeded)] + require.NotNil(t, succeededSwap) + require.Equal(t, swapHashSucceeded, succeededSwap.SwapHash) + require.Equal(t, []string{d3.OutPoint.String()}, succeededSwap.DepositOutpoints) + require.Equal(t, Succeeded, succeededSwap.GetState()) + + failedSwap := finalizedByState[string(Failed)] + require.NotNil(t, failedSwap) + require.Equal(t, swapHashFailed, failedSwap.SwapHash) + require.Equal(t, Failed, failedSwap.GetState()) + + finalizedDeposits := succeededSwap.Deposits require.Len(t, finalizedDeposits, 1) - require.Equal(t, d2.ID, finalizedDeposits[0].ID) - require.Equal(t, d2.OutPoint, finalizedDeposits[0].OutPoint) - require.Equal(t, d2.Value, finalizedDeposits[0].Value) + require.Equal(t, d3.ID, finalizedDeposits[0].ID) + require.Equal(t, d3.OutPoint, finalizedDeposits[0].OutPoint) + require.Equal(t, d3.Value, finalizedDeposits[0].Value) require.Equal(t, deposit.LoopedIn, finalizedDeposits[0].GetState()) } @@ -161,7 +252,9 @@ func TestCreateLoopIn(t *testing.T) { // Set up test context objects. ctx := t.Context() testDb := loopdb.NewTestDB(t) - testClock := clock.NewTestClock(time.Now()) + createTime := time.Unix(1_717_171_717, 123_456_789).UTC() + expectedCreateTime := createTime.Truncate(time.Microsecond) + testClock := clock.NewTestClock(createTime) defer testDb.Close() depositStore := deposit.NewSqlStore(testDb.BaseDB) @@ -234,6 +327,7 @@ func TestCreateLoopIn(t *testing.T) { err = swapStore.CreateLoopIn(ctx, &swapPending) require.NoError(t, err) + require.Equal(t, expectedCreateTime, swapPending.LastUpdateTime) depositIDs, err := swapStore.DepositIDsForSwapHash( ctx, swapHashPending, @@ -258,6 +352,84 @@ func TestCreateLoopIn(t *testing.T) { require.Equal(t, []string{d1.OutPoint.String(), d2.OutPoint.String()}, swap.DepositOutpoints) require.Equal(t, SignHtlcTx, swap.GetState()) + require.Equal(t, swapPending.LastUpdateTime, swap.LastUpdateTime) + require.Equal( + t, ConfirmationRiskDecisionNone, + swap.ConfirmationRiskDecision, + ) + + decisionTime := time.Unix(123, 0).UTC() + testClock.SetTime(decisionTime) + err = swapStore.RecordStaticAddressRiskDecision( + ctx, swapHashPending, ConfirmationRiskDecisionAccepted, + ) + require.NoError(t, err) + + swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending) + require.NoError(t, err) + require.Equal( + t, ConfirmationRiskDecisionAccepted, + swap.ConfirmationRiskDecision, + ) + require.True(t, swap.ConfirmationRiskDecisionTime.Equal(decisionTime)) + + // Replaying the same decision must retain its original deadline anchor. + laterDecisionTime := decisionTime.Add(time.Hour) + testClock.SetTime(laterDecisionTime) + err = swapStore.RecordStaticAddressRiskDecision( + ctx, swapHashPending, ConfirmationRiskDecisionAccepted, + ) + require.NoError(t, err) + + swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending) + require.NoError(t, err) + require.Equal( + t, ConfirmationRiskDecisionAccepted, + swap.ConfirmationRiskDecision, + ) + require.True(t, swap.ConfirmationRiskDecisionTime.Equal(decisionTime)) + + // A different decision is a new event and receives a new timestamp. + rejectedDecisionTime := laterDecisionTime.Add(time.Hour) + testClock.SetTime(rejectedDecisionTime) + err = swapStore.RecordStaticAddressRiskDecision( + ctx, swapHashPending, ConfirmationRiskDecisionRejected, + ) + require.NoError(t, err) + + swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending) + require.NoError(t, err) + require.Equal( + t, ConfirmationRiskDecisionRejected, + swap.ConfirmationRiskDecision, + ) + require.True(t, swap.ConfirmationRiskDecisionTime.Equal( + rejectedDecisionTime, + )) + + // Rejected is terminal: a racing synthetic acceptance must not replace + // the server's rejection or move its deadline anchor. + testClock.SetTime(rejectedDecisionTime.Add(time.Hour)) + err = swapStore.RecordStaticAddressRiskDecision( + ctx, swapHashPending, ConfirmationRiskDecisionAccepted, + ) + require.NoError(t, err) + + swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending) + require.NoError(t, err) + require.Equal( + t, ConfirmationRiskDecisionRejected, + swap.ConfirmationRiskDecision, + ) + require.True(t, swap.ConfirmationRiskDecisionTime.Equal( + rejectedDecisionTime, + )) + + err = swapStore.RecordStaticAddressRiskDecision( + ctx, lntypes.Hash{0x9, 0x9, 0x9}, + ConfirmationRiskDecisionRejected, + ) + require.ErrorIs(t, err, ErrLoopInNotFound) require.Len(t, swap.Deposits, 2) @@ -277,12 +449,170 @@ func TestCreateLoopIn(t *testing.T) { err = swapStore.UpdateLoopIn(ctx, &swapPending) require.NoError(t, err) + require.Equal( + t, updateTime.Truncate(time.Microsecond), + swapPending.LastUpdateTime, + ) swap, err = swapStore.GetLoopInByHash(ctx, swapHashPending) require.NoError(t, err) require.Equal(t, Succeeded, swap.GetState()) - require.WithinDuration( - t, updateTime.UTC(), swap.LastUpdateTime.UTC(), - time.Microsecond, - ) + require.Equal(t, swapPending.LastUpdateTime, swap.LastUpdateTime) +} + +// TestGetLoopInByHashOrdersDepositsBySnapshot ensures recovered deposits are +// ordered by the stored swap input snapshot, which is the signing order shared +// with the server. +func TestGetLoopInByHashOrdersDepositsBySnapshot(t *testing.T) { + ctx := context.Background() + testDb := loopdb.NewTestDB(t) + testClock := clock.NewTestClock(time.Now()) + defer testDb.Close() + + depositStore := deposit.NewSqlStore(testDb.BaseDB) + swapStore := NewSqlStore( + loopdb.NewTypedStore[Querier](testDb), testClock, + &chaincfg.RegressionNetParams, + ) + + newID := func() deposit.ID { + did, err := deposit.GetRandomDepositID() + require.NoError(t, err) + + return did + } + + d1 := &deposit.Deposit{ + ID: newID(), + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x11}, + Index: 0, + }, + Value: 100_000, + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41, + }, + } + d2 := &deposit.Deposit{ + ID: newID(), + OutPoint: wire.OutPoint{ + Hash: chainhash.Hash{0x22}, + Index: 1, + }, + Value: 200_000, + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x4d, + }, + } + + require.NoError(t, depositStore.CreateDeposit(ctx, d1)) + require.NoError(t, depositStore.CreateDeposit(ctx, d2)) + + d1.SetState(deposit.LoopingIn) + d2.SetState(deposit.LoopingIn) + require.NoError(t, depositStore.UpdateDeposit(ctx, d1)) + require.NoError(t, depositStore.UpdateDeposit(ctx, d2)) + + _, clientPubKey := test.CreateKey(1) + _, serverPubKey := test.CreateKey(2) + addr, err := btcutil.DecodeAddress(P2wkhAddr, nil) + require.NoError(t, err) + + swapHash := lntypes.Hash{0x1, 0x2, 0x3, 0x4} + swap := StaticAddressLoopIn{ + SwapHash: swapHash, + SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4}, + DepositOutpoints: []string{ + d2.OutPoint.String(), d1.OutPoint.String(), + }, + Deposits: []*deposit.Deposit{d2, d1}, + ClientPubkey: clientPubKey, + ServerPubkey: serverPubKey, + HtlcTimeoutSweepAddress: addr, + } + swap.SetState(SignHtlcTx) + + require.NoError(t, swapStore.CreateLoopIn(ctx, &swap)) + + storedSwap, err := swapStore.GetLoopInByHash(ctx, swapHash) + require.NoError(t, err) + require.Equal(t, []string{ + d2.OutPoint.String(), d1.OutPoint.String(), + }, storedSwap.DepositOutpoints) + require.Len(t, storedSwap.Deposits, 2) + require.Equal(t, d2.ID, storedSwap.Deposits[0].ID) + require.Equal(t, d1.ID, storedSwap.Deposits[1].ID) +} + +// TestGetLoopInByHashPreservesStoredDepositOutpoints ensures recovered loop-ins +// keep the original outpoint snapshot stored when the swap was created. +func TestGetLoopInByHashPreservesStoredDepositOutpoints(t *testing.T) { + ctxb := context.Background() + testDb := loopdb.NewTestDB(t) + testClock := clock.NewTestClock(time.Now()) + defer testDb.Close() + + depositStore := deposit.NewSqlStore(testDb.BaseDB) + swapStore := NewSqlStore( + loopdb.NewTypedStore[Querier](testDb), testClock, + &chaincfg.RegressionNetParams, + ) + + depositID, err := deposit.GetRandomDepositID() + require.NoError(t, err) + + oldOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{0x1a, 0x2b, 0x3c, 0x4d}, + Index: 0, + } + currentOutpoint := wire.OutPoint{ + Hash: chainhash.Hash{0x5a, 0x6b, 0x7c, 0x8d}, + Index: 1, + } + + d := &deposit.Deposit{ + ID: depositID, + OutPoint: oldOutpoint, + Value: btcutil.Amount(100_000), + TimeOutSweepPkScript: []byte{ + 0x00, 0x14, 0x1a, 0x2b, 0x3c, 0x41, + }, + } + require.NoError(t, depositStore.CreateDeposit(ctxb, d)) + + d.SetState(deposit.LoopingIn) + require.NoError(t, depositStore.UpdateDeposit(ctxb, d)) + + _, clientPubKey := test.CreateKey(1) + _, serverPubKey := test.CreateKey(2) + addr, err := btcutil.DecodeAddress(P2wkhAddr, nil) + require.NoError(t, err) + + swapHash := lntypes.Hash{0x1, 0x2, 0x3, 0x4} + swap := StaticAddressLoopIn{ + SwapHash: swapHash, + SwapPreimage: lntypes.Preimage{0x1, 0x2, 0x3, 0x4}, + DepositOutpoints: []string{oldOutpoint.String()}, + Deposits: []*deposit.Deposit{d}, + ClientPubkey: clientPubKey, + ServerPubkey: serverPubKey, + HtlcTimeoutSweepAddress: addr, + } + swap.SetState(SignHtlcTx) + + require.NoError(t, swapStore.CreateLoopIn(ctxb, &swap)) + + d.OutPoint = currentOutpoint + d.ConfirmationHeight = 42 + require.NoError(t, depositStore.UpdateDeposit(ctxb, d)) + + storedSwap, err := swapStore.GetLoopInByHash(ctxb, swapHash) + require.NoError(t, err) + require.Equal( + t, []string{oldOutpoint.String()}, + storedSwap.DepositOutpoints, + ) + require.Len(t, storedSwap.Deposits, 1) + require.Equal(t, currentOutpoint, storedSwap.Deposits[0].OutPoint) + require.Equal(t, int64(42), storedSwap.Deposits[0].ConfirmationHeight) } diff --git a/staticaddr/loopin/txout_checker.go b/staticaddr/loopin/txout_checker.go new file mode 100644 index 00000000..da14be7c --- /dev/null +++ b/staticaddr/loopin/txout_checker.go @@ -0,0 +1,79 @@ +package loopin + +import ( + "context" + + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" +) + +// lndTxOutChecker checks outpoint availability using lnd's wallet transaction +// view. It omits outputs already spent by a wallet-known transaction. +type lndTxOutChecker struct { + client lndclient.LightningClient +} + +// NewLndTxOutChecker creates a TxOutChecker backed by lnd. +func NewLndTxOutChecker(client lndclient.LightningClient) TxOutChecker { + return &lndTxOutChecker{ + client: client, + } +} + +// GetTxOuts returns all requested tx outputs that lnd's transaction view still +// reports as unspent. +func (c *lndTxOutChecker) GetTxOuts(ctx context.Context, + outpoints []wire.OutPoint) (map[wire.OutPoint]*wire.TxOut, error) { + + outpointByString := make(map[string]wire.OutPoint, len(outpoints)) + outpointsByHash := make(map[string][]wire.OutPoint, len(outpoints)) + for _, outpoint := range outpoints { + outpointByString[outpoint.String()] = outpoint + outpointsByHash[outpoint.Hash.String()] = append( + outpointsByHash[outpoint.Hash.String()], outpoint, + ) + } + + // We need lnd's wallet transaction view rather than only the funding + // transaction: a matching previous outpoint tells us the deposit has + // already been spent by a wallet-known transaction. Use endHeight=-1 so + // lnd includes unconfirmed transactions and mempool spends. + txs, err := c.client.ListTransactions(ctx, 0, -1) + if err != nil { + return nil, err + } + + txOuts := make(map[wire.OutPoint]*wire.TxOut, len(outpoints)) + spent := make(map[wire.OutPoint]struct{}, len(outpoints)) + for _, tx := range txs { + for _, prevOutpoint := range tx.PreviousOutpoints { + outpoint, ok := outpointByString[prevOutpoint.GetOutpoint()] + if ok { + spent[outpoint] = struct{}{} + } + } + + if tx.Tx == nil { + continue + } + + txHash := tx.TxHash + if txHash == "" { + txHash = tx.Tx.TxHash().String() + } + + for _, outpoint := range outpointsByHash[txHash] { + if int(outpoint.Index) >= len(tx.Tx.TxOut) { + continue + } + + txOuts[outpoint] = tx.Tx.TxOut[outpoint.Index] + } + } + + for outpoint := range spent { + delete(txOuts, outpoint) + } + + return txOuts, nil +} diff --git a/staticaddr/loopin/txout_checker_test.go b/staticaddr/loopin/txout_checker_test.go new file mode 100644 index 00000000..491d9ce9 --- /dev/null +++ b/staticaddr/loopin/txout_checker_test.go @@ -0,0 +1,112 @@ +package loopin + +import ( + "context" + "errors" + "testing" + + "github.com/btcsuite/btcd/wire" + "github.com/lightninglabs/lndclient" + "github.com/lightningnetwork/lnd/lnrpc" + "github.com/stretchr/testify/require" +) + +func TestLndTxOutChecker(t *testing.T) { + fundingTx := wire.NewMsgTx(2) + fundingTx.AddTxOut(wire.NewTxOut(1000, []byte{0x01})) + fundingTx.AddTxOut(wire.NewTxOut(2000, []byte{0x02})) + + outpoint := wire.OutPoint{ + Hash: fundingTx.TxHash(), + Index: 1, + } + + t.Run("returns live tx outputs", func(t *testing.T) { + otherOutpoint := wire.OutPoint{ + Hash: fundingTx.TxHash(), + Index: 0, + } + client := &mockTxListLightningClient{ + txs: []lndclient.Transaction{{ + Tx: fundingTx, + }}, + } + + checker := NewLndTxOutChecker(client) + txOuts, err := checker.GetTxOuts( + t.Context(), []wire.OutPoint{outpoint, otherOutpoint}, + ) + require.NoError(t, err) + require.Equal(t, fundingTx.TxOut[outpoint.Index], txOuts[outpoint]) + require.Equal( + t, fundingTx.TxOut[otherOutpoint.Index], + txOuts[otherOutpoint], + ) + require.Equal(t, []txListCall{{ + startHeight: 0, + endHeight: -1, + }}, client.calls) + }) + + t.Run("returns nil for known spend", func(t *testing.T) { + client := &mockTxListLightningClient{ + txs: []lndclient.Transaction{{ + Tx: fundingTx, + }, { + PreviousOutpoints: []*lnrpc.PreviousOutPoint{{ + Outpoint: outpoint.String(), + }}, + }}, + } + + checker := NewLndTxOutChecker(client) + txOuts, err := checker.GetTxOuts( + t.Context(), []wire.OutPoint{outpoint}, + ) + require.NoError(t, err) + require.Nil(t, txOuts[outpoint]) + require.Equal(t, []txListCall{{ + startHeight: 0, + endHeight: -1, + }}, client.calls) + }) + + t.Run("returns error", func(t *testing.T) { + expectedErr := errors.New("list transactions failed") + client := &mockTxListLightningClient{ + err: expectedErr, + } + + checker := NewLndTxOutChecker(client) + txOuts, err := checker.GetTxOuts( + t.Context(), []wire.OutPoint{outpoint}, + ) + require.ErrorIs(t, err, expectedErr) + require.Nil(t, txOuts) + }) +} + +type txListCall struct { + startHeight int32 + endHeight int32 +} + +type mockTxListLightningClient struct { + lndclient.LightningClient + + txs []lndclient.Transaction + err error + calls []txListCall +} + +func (m *mockTxListLightningClient) ListTransactions(_ context.Context, + startHeight, endHeight int32, _ ...lndclient.ListTransactionsOption) ( + []lndclient.Transaction, error) { + + m.calls = append(m.calls, txListCall{ + startHeight: startHeight, + endHeight: endHeight, + }) + + return m.txs, m.err +} diff --git a/staticaddr/openchannel/interface.go b/staticaddr/openchannel/interface.go index 73010a2b..ee6ed31a 100644 --- a/staticaddr/openchannel/interface.go +++ b/staticaddr/openchannel/interface.go @@ -12,6 +12,9 @@ import ( ) type DepositManager interface { + // EnsureDepositsFresh reconciles active deposits with the wallet view. + EnsureDepositsFresh(ctx context.Context) error + // AllOutpointsActiveDeposits returns all deposits that are in the // given state. If the state filter is fsm.StateTypeNone, all deposits // are returned. diff --git a/staticaddr/openchannel/manager.go b/staticaddr/openchannel/manager.go index 2274ce50..d06d1217 100644 --- a/staticaddr/openchannel/manager.go +++ b/staticaddr/openchannel/manager.go @@ -267,11 +267,6 @@ func (m *Manager) OpenChannel(ctx context.Context, ).FeePerKWeight() } - // There are three ways in which we select deposits to open a channel - // with. 1.) The user manually selects the deposits. 2.) The user only - // selects a local channel amount in which case we coin-select deposits - // to cover for it. 3.) The user selects the fundmax flag, in which case - // we select all deposits to fund the channel. if len(req.Outpoints) > 0 { // Ensure that the deposits are in a state in which they are // available for a channel open. @@ -284,13 +279,14 @@ func (m *Manager) OpenChannel(ctx context.Context, // Check for duplicate outpoints which would lead to fee // miscalculation and an invalid PSBT with the same input // listed twice. - seen := make(map[wire.OutPoint]struct{}, len(outpoints)) - for _, op := range outpoints { - if _, ok := seen[op]; ok { - return nil, fmt.Errorf("duplicate outpoint "+ - "%v in request", op) - } - seen[op] = struct{}{} + if err := deposit.CheckDuplicates(outpoints); err != nil { + return nil, fmt.Errorf("%w in request", err) + } + + err = m.cfg.DepositManager.EnsureDepositsFresh(ctx) + if err != nil { + return nil, fmt.Errorf("unable to refresh deposits: %w", + err) } deposits, allActive = @@ -301,6 +297,12 @@ func (m *Manager) OpenChannel(ctx context.Context, return nil, ErrOpeningChannelUnavailableDeposits } } else { + err = m.cfg.DepositManager.EnsureDepositsFresh(ctx) + if err != nil { + return nil, fmt.Errorf("unable to refresh deposits: %w", + err) + } + // We have to select the deposits that are used to fund the // channel. deposits, err = m.cfg.DepositManager.GetActiveDepositsInState( @@ -310,6 +312,12 @@ func (m *Manager) OpenChannel(ctx context.Context, return nil, err } + // Automatic channel funding must ignore mempool deposits because + // they cannot yet be used as funding inputs. + deposits = filterConfirmedDeposits(deposits) + + // If a local funding amount is set, coin-select deposits to + // cover it. Otherwise fundmax uses all available deposits. if req.LocalFundingAmount != 0 { deposits, err = staticutil.SelectDeposits( deposits, req.LocalFundingAmount, @@ -319,9 +327,14 @@ func (m *Manager) OpenChannel(ctx context.Context, return nil, fmt.Errorf("error selecting "+ "deposits: %w", err) } - } else { - // The fundmax flag is set, hence we select all deposits - // for funding the channel. + } + } + + for _, d := range deposits { + // Deposited now includes mempool outputs for static loop-ins, but + // channel opens still require the deposit input to be confirmed. + if d.GetConfirmationHeight() <= 0 { + return nil, ErrOpeningChannelUnavailableDeposits } } @@ -369,6 +382,7 @@ func (m *Manager) OpenChannel(ctx context.Context, if err == nil { return chanOutpoint, nil } + err = maybeWrapTaprootUnsupportedError(reqClone, err) log.Infof("error opening channel: %v", err) @@ -399,6 +413,22 @@ func (m *Manager) OpenChannel(ctx context.Context, return nil, err } +// filterConfirmedDeposits filters the given deposits and returns only those +// that have a positive confirmation height, i.e. deposits that have been +// confirmed on-chain. +func filterConfirmedDeposits(deposits []*deposit.Deposit) []*deposit.Deposit { + confirmed := make([]*deposit.Deposit, 0, len(deposits)) + for _, d := range deposits { + if d.GetConfirmationHeight() <= 0 { + continue + } + + confirmed = append(confirmed, d) + } + + return confirmed +} + // openChannelPsbt starts an interactive channel open protocol that uses a // partially signed bitcoin transaction (PSBT) to fund the channel output. The // protocol involves several steps between the loop client and the server: @@ -620,7 +650,7 @@ func (m *Manager) openChannelPsbt(ctx context.Context, "address: %w", err) } - //nolint:ll + //nolint:lll signedTx, unsignedPsbt, err := m.cfg.WithdrawalManager.CreateFinalizedWithdrawalTx( ctx, deposits, channelFundingAddress, feeRate, fundingAmount, req.CommitmentType, @@ -773,6 +803,9 @@ func resolveCommitmentType(commitmentType lnrpc.CommitmentType) ( case lnrpc.CommitmentType_SIMPLE_TAPROOT: return lnrpc.CommitmentType_SIMPLE_TAPROOT, nil + case lnrpc.CommitmentType_TAPROOT: + return lnrpc.CommitmentType_TAPROOT, nil + default: return lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE, fmt.Errorf( "unsupported commitment type %v", commitmentType, @@ -780,6 +813,30 @@ func resolveCommitmentType(commitmentType lnrpc.CommitmentType) ( } } +// maybeWrapTaprootUnsupportedError turns lnd's generic unknown channel type +// error into a user-actionable message for Loop's production taproot channel +// type. +func maybeWrapTaprootUnsupportedError(req *lnrpc.OpenChannelRequest, + err error) error { + + if err == nil || req.CommitmentType != lnrpc.CommitmentType_TAPROOT { + return err + } + + errMsg := strings.ToLower(err.Error()) + switch { + case strings.Contains(errMsg, "unhandled request channel type"), + strings.Contains(errMsg, "unknown channel type"), + strings.Contains(errMsg, "unsupported channel type"): + + return fmt.Errorf("channel_type=taproot is not supported "+ + "by the connected lnd; update LND to v0.21.0-beta "+ + "or later to use this channel type: %w", err) + } + + return err +} + // checkPsbtFlags make sure a request to open a channel doesn't set any // parameters that are incompatible with the PSBT funding flow. func checkPsbtFlags(req *lnrpc.OpenChannelRequest) error { diff --git a/staticaddr/openchannel/manager_test.go b/staticaddr/openchannel/manager_test.go index f408da16..d46c8c00 100644 --- a/staticaddr/openchannel/manager_test.go +++ b/staticaddr/openchannel/manager_test.go @@ -29,12 +29,17 @@ type transitionCall struct { } type mockDepositManager struct { + activeDeposits []*deposit.Deposit openingDeposits []*deposit.Deposit getErr error transitionErrs map[fsm.EventType]error calls []transitionCall } +func (m *mockDepositManager) EnsureDepositsFresh(context.Context) error { + return nil +} + func (m *mockDepositManager) AllOutpointsActiveDeposits([]wire.OutPoint, fsm.StateType) ([]*deposit.Deposit, bool) { @@ -44,15 +49,19 @@ func (m *mockDepositManager) AllOutpointsActiveDeposits([]wire.OutPoint, func (m *mockDepositManager) GetActiveDepositsInState(stateFilter fsm.StateType) ( []*deposit.Deposit, error) { - if stateFilter != deposit.OpeningChannel { - return nil, nil + switch stateFilter { + case deposit.Deposited: + return m.activeDeposits, nil + + case deposit.OpeningChannel: + if m.getErr != nil { + return nil, m.getErr + } + + return m.openingDeposits, nil } - if m.getErr != nil { - return nil, m.getErr - } - - return m.openingDeposits, nil + return nil, nil } func (m *mockDepositManager) TransitionDeposits(_ context.Context, @@ -464,6 +473,97 @@ func TestOpenChannelDuplicateOutpoints(t *testing.T) { require.ErrorContains(t, err, "duplicate outpoint") } +// TestOpenChannelSkipsUnconfirmedAutoSelection verifies that automatic coin +// selection ignores mempool deposits and keeps using confirmed ones. +func TestOpenChannelSkipsUnconfirmedAutoSelection(t *testing.T) { + t.Parallel() + + confirmedA := &deposit.Deposit{ + OutPoint: testOutPoint(1), + Value: 160_000, + ConfirmationHeight: 10, + } + confirmedB := &deposit.Deposit{ + OutPoint: testOutPoint(2), + Value: 140_000, + ConfirmationHeight: 11, + } + unconfirmed := &deposit.Deposit{ + OutPoint: testOutPoint(3), + Value: 500_000, + } + + depositManager := &mockDepositManager{ + activeDeposits: []*deposit.Deposit{ + unconfirmed, confirmedA, confirmedB, + }, + transitionErrs: map[fsm.EventType]error{ + deposit.OnOpeningChannel: errors.New("stop after selection"), + }, + } + manager := &Manager{ + cfg: &Config{ + DepositManager: depositManager, + }, + } + + req := &lnrpc.OpenChannelRequest{ + NodePubkey: make([]byte, 33), + LocalFundingAmount: 100_000, + SatPerVbyte: 10, + } + + _, err := manager.OpenChannel(context.Background(), req) + require.ErrorContains(t, err, "stop after selection") + require.Len(t, depositManager.calls, 1) + require.Equal(t, deposit.OnOpeningChannel, depositManager.calls[0].event) + require.NotContains(t, depositManager.calls[0].outpoints, unconfirmed.OutPoint) +} + +// TestOpenChannelFundMaxSkipsUnconfirmed verifies that fundmax only locks +// confirmed deposits. +func TestOpenChannelFundMaxSkipsUnconfirmed(t *testing.T) { + t.Parallel() + + confirmed := &deposit.Deposit{ + OutPoint: testOutPoint(1), + Value: 200_000, + ConfirmationHeight: 10, + } + unconfirmed := &deposit.Deposit{ + OutPoint: testOutPoint(2), + Value: 300_000, + } + + depositManager := &mockDepositManager{ + activeDeposits: []*deposit.Deposit{ + unconfirmed, confirmed, + }, + transitionErrs: map[fsm.EventType]error{ + deposit.OnOpeningChannel: errors.New("stop after selection"), + }, + } + manager := &Manager{ + cfg: &Config{ + DepositManager: depositManager, + }, + } + + req := &lnrpc.OpenChannelRequest{ + NodePubkey: make([]byte, 33), + FundMax: true, + SatPerVbyte: 10, + } + + _, err := manager.OpenChannel(context.Background(), req) + require.ErrorContains(t, err, "stop after selection") + require.Len(t, depositManager.calls, 1) + require.Equal( + t, []wire.OutPoint{confirmed.OutPoint}, + depositManager.calls[0].outpoints, + ) +} + // TestValidateInitialPsbtFlags verifies that request fields incompatible with // PSBT funding are rejected early, before any deposits are locked. func TestValidateInitialPsbtFlags(t *testing.T) { @@ -573,6 +673,11 @@ func TestResolveCommitmentType(t *testing.T) { commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT, expectedType: lnrpc.CommitmentType_SIMPLE_TAPROOT, }, + { + name: "production taproot supported", + commitmentType: lnrpc.CommitmentType_TAPROOT, + expectedType: lnrpc.CommitmentType_TAPROOT, + }, { name: "legacy rejected", commitmentType: lnrpc.CommitmentType_LEGACY, @@ -596,6 +701,33 @@ func TestResolveCommitmentType(t *testing.T) { } } +// TestMaybeWrapTaprootUnsupportedError verifies that generic old-lnd channel +// type rejections become actionable for Loop users selecting production +// taproot channels. +func TestMaybeWrapTaprootUnsupportedError(t *testing.T) { + t.Parallel() + + baseErr := errors.New("got error from server: rpc error: " + + "code = Unknown desc = unhandled request channel type 7") + req := &lnrpc.OpenChannelRequest{ + CommitmentType: lnrpc.CommitmentType_TAPROOT, + } + + err := maybeWrapTaprootUnsupportedError(req, baseErr) + require.ErrorContains( + t, err, "channel_type=taproot is not supported", + ) + require.ErrorContains( + t, err, "update LND to v0.21.0-beta or later", + ) + require.ErrorIs(t, err, baseErr) + + req.CommitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT + err = maybeWrapTaprootUnsupportedError(req, baseErr) + require.ErrorIs(t, err, baseErr) + require.NotContains(t, err.Error(), "update LND") +} + // --------------------------------------------------------------------------- // Mock types for PSBT channel open flow tests. // --------------------------------------------------------------------------- diff --git a/staticaddr/staticutil/utils.go b/staticaddr/staticutil/utils.go index daf97c46..a2509333 100644 --- a/staticaddr/staticutil/utils.go +++ b/staticaddr/staticutil/utils.go @@ -24,20 +24,21 @@ import ( func ToPrevOuts(deposits []*deposit.Deposit, pkScript []byte) (map[wire.OutPoint]*wire.TxOut, error) { + outpoints := make([]wire.OutPoint, len(deposits)) + for i, d := range deposits { + outpoints[i] = d.OutPoint + } + if err := deposit.CheckDuplicates(outpoints); err != nil { + return nil, err + } + prevOuts := make(map[wire.OutPoint]*wire.TxOut, len(deposits)) - for _, d := range deposits { - outpoint := wire.OutPoint{ - Hash: d.Hash, - Index: d.Index, - } + for i, d := range deposits { + outpoint := outpoints[i] txOut := &wire.TxOut{ Value: int64(d.Value), PkScript: pkScript, } - if _, ok := prevOuts[outpoint]; ok { - return nil, fmt.Errorf("duplicate outpoint %v", - outpoint) - } prevOuts[outpoint] = txOut } @@ -232,8 +233,10 @@ func estimateFee(numInputs int, feeRate chainfee.SatPerKWeight, // Add the funding output based on commitment type. switch commitmentType { - case lnrpc.CommitmentType_SIMPLE_TAPROOT: + case lnrpc.CommitmentType_SIMPLE_TAPROOT, + lnrpc.CommitmentType_TAPROOT: we.AddP2TROutput() + default: we.AddP2WSHOutput() } diff --git a/staticaddr/staticutil/utils_test.go b/staticaddr/staticutil/utils_test.go index d0ab8b1f..ae68b489 100644 --- a/staticaddr/staticutil/utils_test.go +++ b/staticaddr/staticutil/utils_test.go @@ -271,9 +271,6 @@ func TestSelectDeposits(t *testing.T) { // High fee rate: 100 sat/vbyte = 25000 sat/kw. highFeeRate := chainfee.SatPerKVByte(100_000).FeePerKWeight() - anchors := lnrpc.CommitmentType_ANCHORS - taproot := lnrpc.CommitmentType_SIMPLE_TAPROOT - tests := []struct { name string deposits []*deposit.Deposit @@ -290,7 +287,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(1_000, 2_000), amount: 1_000_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantErr: "insufficient funds", }, { @@ -298,7 +295,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(100_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantErr: "insufficient funds", }, { @@ -310,7 +307,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(51_000), amount: 50_000, feeRate: highFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantErr: "insufficient funds", }, { @@ -327,7 +324,7 @@ func TestSelectDeposits(t *testing.T) { ), amount: 400_000, feeRate: highFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 2, validate: func(t *testing.T, selected []*deposit.Deposit) { require.Equal( @@ -345,7 +342,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(500_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 1, }, { @@ -353,7 +350,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(60_000, 60_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 2, }, { @@ -361,7 +358,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(10_000, 200_000, 50_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 1, validate: func(t *testing.T, selected []*deposit.Deposit) { // Should pick the 200k deposit. @@ -388,13 +385,13 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(35_500, 35_500, 10_000), amount: 50_000, feeRate: highFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 3, validate: func(t *testing.T, selected []*deposit.Deposit) { total := depositSum(selected) fee := estimateFee( len(selected), highFeeRate, - anchors, + lnrpc.CommitmentType_ANCHORS, ) require.GreaterOrEqual( t, total, @@ -407,7 +404,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(40_000, 40_000, 40_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 3, }, { @@ -415,7 +412,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(100_000, 50_000), amount: 99_000, feeRate: 0, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 1, validate: func(t *testing.T, selected []*deposit.Deposit) { // With zero fee, 100k covers 99k + 0 + dust. @@ -430,12 +427,12 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(200_000, 100_000, 50_000), amount: 100_000, feeRate: highFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, validate: func(t *testing.T, selected []*deposit.Deposit) { total := depositSum(selected) fee := estimateFee( len(selected), highFeeRate, - anchors, + lnrpc.CommitmentType_ANCHORS, ) require.GreaterOrEqual( t, total, @@ -448,7 +445,15 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(500_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: taproot, + commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT, + wantCount: 1, + }, + { + name: "production taproot commitment type", + deposits: makeDeposits(500_000), + amount: 100_000, + feeRate: lowFeeRate, + commitmentType: lnrpc.CommitmentType_TAPROOT, wantCount: 1, }, { @@ -459,12 +464,12 @@ func TestSelectDeposits(t *testing.T) { ), amount: 50_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, validate: func(t *testing.T, selected []*deposit.Deposit) { total := depositSum(selected) fee := estimateFee( len(selected), lowFeeRate, - anchors, + lnrpc.CommitmentType_ANCHORS, ) require.GreaterOrEqual( t, total, @@ -484,12 +489,12 @@ func TestSelectDeposits(t *testing.T) { ), amount: 150_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, validate: func(t *testing.T, selected []*deposit.Deposit) { total := depositSum(selected) fee := estimateFee( len(selected), lowFeeRate, - anchors, + lnrpc.CommitmentType_ANCHORS, ) // Core invariant: selected amount covers // requested amount + fee + dust. @@ -506,7 +511,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(10_000, 20_000, 300_000), amount: 100_000, feeRate: lowFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 1, validate: func(t *testing.T, selected []*deposit.Deposit) { require.Equal( @@ -525,7 +530,7 @@ func TestSelectDeposits(t *testing.T) { deposits: makeDeposits(60_000, 60_000), amount: 50_000, feeRate: highFeeRate, - commitmentType: anchors, + commitmentType: lnrpc.CommitmentType_ANCHORS, wantCount: 2, }, } diff --git a/staticaddr/withdraw/funding_values_test.go b/staticaddr/withdraw/funding_values_test.go index 97e5fd14..67c16a1a 100644 --- a/staticaddr/withdraw/funding_values_test.go +++ b/staticaddr/withdraw/funding_values_test.go @@ -235,6 +235,11 @@ func TestCalculateWithdrawalTxValuesCommitmentTypeParity(t *testing.T) { commitmentType: lnrpc.CommitmentType_SIMPLE_TAPROOT, addr: taprootAddr, }, + { + name: "production taproot and p2tr", + commitmentType: lnrpc.CommitmentType_TAPROOT, + addr: taprootAddr, + }, } selectedAmounts := []btcutil.Amount{ diff --git a/staticaddr/withdraw/interface.go b/staticaddr/withdraw/interface.go index ff878488..0f32697a 100644 --- a/staticaddr/withdraw/interface.go +++ b/staticaddr/withdraw/interface.go @@ -21,14 +21,24 @@ type AddressManager interface { } type DepositManager interface { + // EnsureDepositsFresh reconciles active deposits with the wallet view. + EnsureDepositsFresh(ctx context.Context) error + + // GetActiveDepositsInState returns all active deposits in the given + // state. GetActiveDepositsInState(stateFilter fsm.StateType) ([]*deposit.Deposit, error) + // AllOutpointsActiveDeposits returns all active deposits referenced by + // the outpoints if every deposit is active and in the given state. AllOutpointsActiveDeposits(outpoints []wire.OutPoint, stateFilter fsm.StateType) ([]*deposit.Deposit, bool) + // TransitionDeposits transitions the deposits with the given event and + // waits until they reach the expected final state. TransitionDeposits(ctx context.Context, deposits []*deposit.Deposit, event fsm.EventType, expectedFinalState fsm.StateType) error + // UpdateDeposit persists the current deposit fields. UpdateDeposit(ctx context.Context, d *deposit.Deposit) error } diff --git a/staticaddr/withdraw/manager.go b/staticaddr/withdraw/manager.go index 6cd94149..3a7927a4 100644 --- a/staticaddr/withdraw/manager.go +++ b/staticaddr/withdraw/manager.go @@ -320,6 +320,11 @@ func (m *Manager) WithdrawDeposits(ctx context.Context, allWithdrawing bool ) + err := m.cfg.DepositManager.EnsureDepositsFresh(ctx) + if err != nil { + return "", "", fmt.Errorf("unable to refresh deposits: %w", err) + } + // Ensure that the deposits are in a state in which they can be // withdrawn. deposits, allDeposited = m.cfg.DepositManager.AllOutpointsActiveDeposits( @@ -381,10 +386,16 @@ func (m *Manager) WithdrawDeposits(ctx context.Context, } } - var ( - withdrawalAddress btcutil.Address - err error - ) + for _, d := range deposits { + // Deposited now includes mempool outputs for static loop-ins, but + // withdrawals still require the deposit input to be confirmed. + if d.GetConfirmationHeight() <= 0 { + return "", "", fmt.Errorf("can't withdraw, " + + "unconfirmed deposits can't be withdrawn") + } + } + + var withdrawalAddress btcutil.Address // Check if the user provided an address to withdraw to. If not, we'll // generate a new address for them. @@ -669,7 +680,7 @@ func (m *Manager) handleWithdrawal(ctx context.Context, d := deposits[0] spentChan, errChan, err := m.cfg.ChainNotifier.RegisterSpendNtfn( ctx, &d.OutPoint, addrParams.PkScript, - int32(d.ConfirmationHeight), + int32(d.GetConfirmationHeight()), ) if err != nil { return fmt.Errorf("unable to register spend ntfn: %w", err) @@ -1108,7 +1119,8 @@ func WithdrawalTxWeight(numInputs int, sweepAddress btcutil.Address, if commitmentType != lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE { switch commitmentType { - case lnrpc.CommitmentType_SIMPLE_TAPROOT: + case lnrpc.CommitmentType_SIMPLE_TAPROOT, + lnrpc.CommitmentType_TAPROOT: weightEstimator.AddP2TROutput() default: diff --git a/swap/type.go b/swap/type.go index 20942c74..b4558e15 100644 --- a/swap/type.go +++ b/swap/type.go @@ -9,10 +9,13 @@ const ( // TypeOut is a loop out swap. TypeOut + + // TypeStaticAddressLoopIn is a static-address loop-in swap. + TypeStaticAddressLoopIn ) -// IsOut returns true if the swap is a loop out swap, false if it is a loop in -// swap. +// IsOut returns true only if the swap is TypeOut; TypeIn and +// TypeStaticAddressLoopIn both return false. func (t Type) IsOut() bool { return t == TypeOut } @@ -23,6 +26,8 @@ func (t Type) String() string { return "In" case TypeOut: return "Out" + case TypeStaticAddressLoopIn: + return "StaticAddressLoopIn" default: return "Unknown" } diff --git a/swapserverrpc/common.pb.go b/swapserverrpc/common.pb.go index ae84e1a4..0ae8bb21 100644 --- a/swapserverrpc/common.pb.go +++ b/swapserverrpc/common.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v3.21.12 // source: common.proto diff --git a/swapserverrpc/go.mod b/swapserverrpc/go.mod index d21a93d4..2b71eea3 100644 --- a/swapserverrpc/go.mod +++ b/swapserverrpc/go.mod @@ -1,15 +1,15 @@ module github.com/lightninglabs/loop/swapserverrpc require ( - google.golang.org/grpc v1.79.3 - google.golang.org/protobuf v1.36.10 + google.golang.org/grpc v1.82.1 + google.golang.org/protobuf v1.36.11 ) require ( golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 // indirect - golang.org/x/net v0.48.0 // indirect - golang.org/x/sys v0.39.0 // indirect - golang.org/x/text v0.32.0 // indirect + golang.org/x/net v0.55.0 // indirect + golang.org/x/sys v0.45.0 // indirect + golang.org/x/text v0.37.0 // indirect google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 // indirect ) @@ -17,4 +17,4 @@ require ( // "go mod check" failures in CI. replace gonum.org/v1/gonum => github.com/gonum/gonum v0.11.0 -go 1.25.5 +go 1.25.12 diff --git a/swapserverrpc/go.sum b/swapserverrpc/go.sum index 4749ce5c..2c6e462d 100644 --- a/swapserverrpc/go.sum +++ b/swapserverrpc/go.sum @@ -14,27 +14,27 @@ github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= -go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= -go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= -go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= -go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= -go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= -go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= -go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= -go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= -go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= -go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I= +go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0= +go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM= +go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY= +go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg= +go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg= +go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw= +go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A= +go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A= +go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0 h1:e66Fs6Z+fZTbFBAxKfP3PALWBtpfqks2bwGcexMxgtk= golang.org/x/exp v0.0.0-20240909161429-701f63a606c0/go.mod h1:2TbTHSBQa924w8M6Xs1QcRcFwyucIwBGpK1p2f1YFFY= -golang.org/x/net v0.48.0 h1:zyQRTTrjc33Lhh0fBgT/H3oZq9WuvRR5gPC70xpDiQU= -golang.org/x/net v0.48.0/go.mod h1:+ndRgGjkh8FGtu1w1FGbEC31if4VrNVMuKTgcAAnQRY= -golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= -golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= -golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= -golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= +golang.org/x/net v0.55.0 h1:bcvxaJn3e1U6InsFWt1JUq1aSjnRxLzT2rtD2KfkDF8= +golang.org/x/net v0.55.0/go.mod h1:L5U2KuzuOe1lY7Z+aWVIKK6qEeJXnXV9yzGA+WCHJww= +golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY= +golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw= +golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc= +golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1 h1:KpwkzHKEF7B9Zxg18WzOa7djJ+Ha5DzthMyZYQfEn2A= google.golang.org/genproto v0.0.0-20230410155749-daa745c078e1/go.mod h1:nKE/iIaLqn2bQwXBg8f1g2Ylh6r5MN5CmZvuzZCgsCU= -google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE= -google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= -google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= -google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +google.golang.org/grpc v1.82.1 h1:NnAxzGRA0677vCa4BUkOAnO5+FfQqVl9iUXeD0IqcGE= +google.golang.org/grpc v1.82.1/go.mod h1:yzTZ1TB1Z3SG+LIYaI+WiE8D5+PZ3ArnrSp8zF3+/ZA= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= diff --git a/swapserverrpc/instantout.pb.go b/swapserverrpc/instantout.pb.go index 4593f002..96cd8e70 100644 --- a/swapserverrpc/instantout.pb.go +++ b/swapserverrpc/instantout.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v3.21.12 // source: instantout.proto diff --git a/swapserverrpc/reservation.pb.go b/swapserverrpc/reservation.pb.go index 3293ee47..a1f73b43 100644 --- a/swapserverrpc/reservation.pb.go +++ b/swapserverrpc/reservation.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v3.21.12 // source: reservation.proto diff --git a/swapserverrpc/server.pb.go b/swapserverrpc/server.pb.go index cbca725e..c2b1d440 100644 --- a/swapserverrpc/server.pb.go +++ b/swapserverrpc/server.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v3.21.12 // source: server.proto diff --git a/swapserverrpc/staticaddr.pb.go b/swapserverrpc/staticaddr.pb.go index 77ad9799..5dc4f49a 100644 --- a/swapserverrpc/staticaddr.pb.go +++ b/swapserverrpc/staticaddr.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.10 +// protoc-gen-go v1.36.11 // protoc v3.21.12 // source: staticaddr.proto @@ -387,8 +387,12 @@ type ServerPsbtWithdrawRequest struct { WithdrawalPsbt []byte `protobuf:"bytes,1,opt,name=withdrawal_psbt,json=withdrawalPsbt,proto3" json:"withdrawal_psbt,omitempty"` // The map of deposit txid:idx to the nonce used by the client. DepositToNonces map[string][]byte `protobuf:"bytes,2,rep,name=deposit_to_nonces,json=depositToNonces,proto3" json:"deposit_to_nonces,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` - unknownFields protoimpl.UnknownFields - sizeCache protoimpl.SizeCache + // The map of deposit txid:idx to the static address descriptor that was + // used to derive the deposit output. The server combines the client key + // with the L402's server pubkey and expiry to validate each input. + DepositToClientPubkeys map[string]*StaticAddressDescriptor `protobuf:"bytes,3,rep,name=deposit_to_client_pubkeys,json=depositToClientPubkeys,proto3" json:"deposit_to_client_pubkeys,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache } func (x *ServerPsbtWithdrawRequest) Reset() { @@ -435,6 +439,13 @@ func (x *ServerPsbtWithdrawRequest) GetDepositToNonces() map[string][]byte { return nil } +func (x *ServerPsbtWithdrawRequest) GetDepositToClientPubkeys() map[string]*StaticAddressDescriptor { + if x != nil { + return x.DepositToClientPubkeys + } + return nil +} + type ServerPsbtWithdrawResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The txid of the psbt that the client wants to push the sigs for. @@ -544,6 +555,115 @@ func (x *ServerPsbtWithdrawSigningInfo) GetSig() []byte { return nil } +type StaticAddressDescriptor struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The client's secp256k1 public key serialized in 33-byte compressed SEC1 + // format. This is not the 32-byte x-only BIP-340 encoding. + Pubkey []byte `protobuf:"bytes,1,opt,name=pubkey,proto3" json:"pubkey,omitempty"` + // The expected output script for the static address. + PkScript []byte `protobuf:"bytes,2,opt,name=pk_script,json=pkScript,proto3" json:"pk_script,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StaticAddressDescriptor) Reset() { + *x = StaticAddressDescriptor{} + mi := &file_staticaddr_proto_msgTypes[8] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StaticAddressDescriptor) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StaticAddressDescriptor) ProtoMessage() {} + +func (x *StaticAddressDescriptor) ProtoReflect() protoreflect.Message { + mi := &file_staticaddr_proto_msgTypes[8] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StaticAddressDescriptor.ProtoReflect.Descriptor instead. +func (*StaticAddressDescriptor) Descriptor() ([]byte, []int) { + return file_staticaddr_proto_rawDescGZIP(), []int{8} +} + +func (x *StaticAddressDescriptor) GetPubkey() []byte { + if x != nil { + return x.Pubkey + } + return nil +} + +func (x *StaticAddressDescriptor) GetPkScript() []byte { + if x != nil { + return x.PkScript + } + return nil +} + +type StaticAddressChangeOutput struct { + state protoimpl.MessageState `protogen:"open.v1"` + // The descriptor for the static address change output. + StaticAddress *StaticAddressDescriptor `protobuf:"bytes,1,opt,name=static_address,json=staticAddress,proto3" json:"static_address,omitempty"` + // The expected change amount in satoshis. + Amount int64 `protobuf:"varint,2,opt,name=amount,proto3" json:"amount,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *StaticAddressChangeOutput) Reset() { + *x = StaticAddressChangeOutput{} + mi := &file_staticaddr_proto_msgTypes[9] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *StaticAddressChangeOutput) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*StaticAddressChangeOutput) ProtoMessage() {} + +func (x *StaticAddressChangeOutput) ProtoReflect() protoreflect.Message { + mi := &file_staticaddr_proto_msgTypes[9] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use StaticAddressChangeOutput.ProtoReflect.Descriptor instead. +func (*StaticAddressChangeOutput) Descriptor() ([]byte, []int) { + return file_staticaddr_proto_rawDescGZIP(), []int{9} +} + +func (x *StaticAddressChangeOutput) GetStaticAddress() *StaticAddressDescriptor { + if x != nil { + return x.StaticAddress + } + return nil +} + +func (x *StaticAddressChangeOutput) GetAmount() int64 { + if x != nil { + return x.Amount + } + return 0 +} + type ServerStaticAddressLoopInRequest struct { state protoimpl.MessageState `protogen:"open.v1"` // The client's public key for the htlc output. @@ -589,14 +709,21 @@ type ServerStaticAddressLoopInRequest struct { Amount uint64 `protobuf:"varint,9,opt,name=amount,proto3" json:"amount,omitempty"` // If set, request the server to use fast publication behavior for this // swap. - Fast bool `protobuf:"varint,10,opt,name=fast,proto3" json:"fast,omitempty"` + Fast bool `protobuf:"varint,10,opt,name=fast,proto3" json:"fast,omitempty"` + // The map of deposit txid:idx to the static address descriptor that was + // used to derive the deposit output. The server combines the client key + // with the L402's server pubkey and expiry to validate each input. + DepositToClientPubkeys map[string]*StaticAddressDescriptor `protobuf:"bytes,11,rep,name=deposit_to_client_pubkeys,json=depositToClientPubkeys,proto3" json:"deposit_to_client_pubkeys,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"` + // Optional change output metadata for fractional loop-ins that return + // funds to a newly generated static address. + ChangeOutput *StaticAddressChangeOutput `protobuf:"bytes,12,opt,name=change_output,json=changeOutput,proto3" json:"change_output,omitempty"` unknownFields protoimpl.UnknownFields sizeCache protoimpl.SizeCache } func (x *ServerStaticAddressLoopInRequest) Reset() { *x = ServerStaticAddressLoopInRequest{} - mi := &file_staticaddr_proto_msgTypes[8] + mi := &file_staticaddr_proto_msgTypes[10] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -608,7 +735,7 @@ func (x *ServerStaticAddressLoopInRequest) String() string { func (*ServerStaticAddressLoopInRequest) ProtoMessage() {} func (x *ServerStaticAddressLoopInRequest) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[8] + mi := &file_staticaddr_proto_msgTypes[10] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -621,7 +748,7 @@ func (x *ServerStaticAddressLoopInRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerStaticAddressLoopInRequest.ProtoReflect.Descriptor instead. func (*ServerStaticAddressLoopInRequest) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{8} + return file_staticaddr_proto_rawDescGZIP(), []int{10} } func (x *ServerStaticAddressLoopInRequest) GetHtlcClientPubKey() []byte { @@ -694,6 +821,20 @@ func (x *ServerStaticAddressLoopInRequest) GetFast() bool { return false } +func (x *ServerStaticAddressLoopInRequest) GetDepositToClientPubkeys() map[string]*StaticAddressDescriptor { + if x != nil { + return x.DepositToClientPubkeys + } + return nil +} + +func (x *ServerStaticAddressLoopInRequest) GetChangeOutput() *StaticAddressChangeOutput { + if x != nil { + return x.ChangeOutput + } + return nil +} + type ServerStaticAddressLoopInResponse struct { state protoimpl.MessageState `protogen:"open.v1"` // The server's public key for the htlc output. @@ -715,7 +856,7 @@ type ServerStaticAddressLoopInResponse struct { func (x *ServerStaticAddressLoopInResponse) Reset() { *x = ServerStaticAddressLoopInResponse{} - mi := &file_staticaddr_proto_msgTypes[9] + mi := &file_staticaddr_proto_msgTypes[11] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -727,7 +868,7 @@ func (x *ServerStaticAddressLoopInResponse) String() string { func (*ServerStaticAddressLoopInResponse) ProtoMessage() {} func (x *ServerStaticAddressLoopInResponse) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[9] + mi := &file_staticaddr_proto_msgTypes[11] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -740,7 +881,7 @@ func (x *ServerStaticAddressLoopInResponse) ProtoReflect() protoreflect.Message // Deprecated: Use ServerStaticAddressLoopInResponse.ProtoReflect.Descriptor instead. func (*ServerStaticAddressLoopInResponse) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{9} + return file_staticaddr_proto_rawDescGZIP(), []int{11} } func (x *ServerStaticAddressLoopInResponse) GetHtlcServerPubKey() []byte { @@ -790,7 +931,7 @@ type ServerHtlcSigningInfo struct { func (x *ServerHtlcSigningInfo) Reset() { *x = ServerHtlcSigningInfo{} - mi := &file_staticaddr_proto_msgTypes[10] + mi := &file_staticaddr_proto_msgTypes[12] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -802,7 +943,7 @@ func (x *ServerHtlcSigningInfo) String() string { func (*ServerHtlcSigningInfo) ProtoMessage() {} func (x *ServerHtlcSigningInfo) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[10] + mi := &file_staticaddr_proto_msgTypes[12] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -815,7 +956,7 @@ func (x *ServerHtlcSigningInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ServerHtlcSigningInfo.ProtoReflect.Descriptor instead. func (*ServerHtlcSigningInfo) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{10} + return file_staticaddr_proto_rawDescGZIP(), []int{12} } func (x *ServerHtlcSigningInfo) GetNonces() [][]byte { @@ -848,7 +989,7 @@ type PushStaticAddressHtlcSigsRequest struct { func (x *PushStaticAddressHtlcSigsRequest) Reset() { *x = PushStaticAddressHtlcSigsRequest{} - mi := &file_staticaddr_proto_msgTypes[11] + mi := &file_staticaddr_proto_msgTypes[13] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -860,7 +1001,7 @@ func (x *PushStaticAddressHtlcSigsRequest) String() string { func (*PushStaticAddressHtlcSigsRequest) ProtoMessage() {} func (x *PushStaticAddressHtlcSigsRequest) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[11] + mi := &file_staticaddr_proto_msgTypes[13] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -873,7 +1014,7 @@ func (x *PushStaticAddressHtlcSigsRequest) ProtoReflect() protoreflect.Message { // Deprecated: Use PushStaticAddressHtlcSigsRequest.ProtoReflect.Descriptor instead. func (*PushStaticAddressHtlcSigsRequest) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{11} + return file_staticaddr_proto_rawDescGZIP(), []int{13} } func (x *PushStaticAddressHtlcSigsRequest) GetSwapHash() []byte { @@ -916,7 +1057,7 @@ type ClientHtlcSigningInfo struct { func (x *ClientHtlcSigningInfo) Reset() { *x = ClientHtlcSigningInfo{} - mi := &file_staticaddr_proto_msgTypes[12] + mi := &file_staticaddr_proto_msgTypes[14] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -928,7 +1069,7 @@ func (x *ClientHtlcSigningInfo) String() string { func (*ClientHtlcSigningInfo) ProtoMessage() {} func (x *ClientHtlcSigningInfo) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[12] + mi := &file_staticaddr_proto_msgTypes[14] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -941,7 +1082,7 @@ func (x *ClientHtlcSigningInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ClientHtlcSigningInfo.ProtoReflect.Descriptor instead. func (*ClientHtlcSigningInfo) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{12} + return file_staticaddr_proto_rawDescGZIP(), []int{14} } func (x *ClientHtlcSigningInfo) GetNonces() [][]byte { @@ -966,7 +1107,7 @@ type PushStaticAddressHtlcSigsResponse struct { func (x *PushStaticAddressHtlcSigsResponse) Reset() { *x = PushStaticAddressHtlcSigsResponse{} - mi := &file_staticaddr_proto_msgTypes[13] + mi := &file_staticaddr_proto_msgTypes[15] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -978,7 +1119,7 @@ func (x *PushStaticAddressHtlcSigsResponse) String() string { func (*PushStaticAddressHtlcSigsResponse) ProtoMessage() {} func (x *PushStaticAddressHtlcSigsResponse) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[13] + mi := &file_staticaddr_proto_msgTypes[15] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -991,7 +1132,7 @@ func (x *PushStaticAddressHtlcSigsResponse) ProtoReflect() protoreflect.Message // Deprecated: Use PushStaticAddressHtlcSigsResponse.ProtoReflect.Descriptor instead. func (*PushStaticAddressHtlcSigsResponse) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{13} + return file_staticaddr_proto_rawDescGZIP(), []int{15} } type PushStaticAddressSweeplessSigsRequest struct { @@ -1013,7 +1154,7 @@ type PushStaticAddressSweeplessSigsRequest struct { func (x *PushStaticAddressSweeplessSigsRequest) Reset() { *x = PushStaticAddressSweeplessSigsRequest{} - mi := &file_staticaddr_proto_msgTypes[14] + mi := &file_staticaddr_proto_msgTypes[16] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1025,7 +1166,7 @@ func (x *PushStaticAddressSweeplessSigsRequest) String() string { func (*PushStaticAddressSweeplessSigsRequest) ProtoMessage() {} func (x *PushStaticAddressSweeplessSigsRequest) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[14] + mi := &file_staticaddr_proto_msgTypes[16] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1038,7 +1179,7 @@ func (x *PushStaticAddressSweeplessSigsRequest) ProtoReflect() protoreflect.Mess // Deprecated: Use PushStaticAddressSweeplessSigsRequest.ProtoReflect.Descriptor instead. func (*PushStaticAddressSweeplessSigsRequest) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{14} + return file_staticaddr_proto_rawDescGZIP(), []int{16} } func (x *PushStaticAddressSweeplessSigsRequest) GetSwapHash() []byte { @@ -1082,7 +1223,7 @@ type ClientSweeplessSigningInfo struct { func (x *ClientSweeplessSigningInfo) Reset() { *x = ClientSweeplessSigningInfo{} - mi := &file_staticaddr_proto_msgTypes[15] + mi := &file_staticaddr_proto_msgTypes[17] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1094,7 +1235,7 @@ func (x *ClientSweeplessSigningInfo) String() string { func (*ClientSweeplessSigningInfo) ProtoMessage() {} func (x *ClientSweeplessSigningInfo) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[15] + mi := &file_staticaddr_proto_msgTypes[17] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1107,7 +1248,7 @@ func (x *ClientSweeplessSigningInfo) ProtoReflect() protoreflect.Message { // Deprecated: Use ClientSweeplessSigningInfo.ProtoReflect.Descriptor instead. func (*ClientSweeplessSigningInfo) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{15} + return file_staticaddr_proto_rawDescGZIP(), []int{17} } func (x *ClientSweeplessSigningInfo) GetNonce() []byte { @@ -1132,7 +1273,7 @@ type PushStaticAddressSweeplessSigsResponse struct { func (x *PushStaticAddressSweeplessSigsResponse) Reset() { *x = PushStaticAddressSweeplessSigsResponse{} - mi := &file_staticaddr_proto_msgTypes[16] + mi := &file_staticaddr_proto_msgTypes[18] ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) ms.StoreMessageInfo(mi) } @@ -1144,7 +1285,7 @@ func (x *PushStaticAddressSweeplessSigsResponse) String() string { func (*PushStaticAddressSweeplessSigsResponse) ProtoMessage() {} func (x *PushStaticAddressSweeplessSigsResponse) ProtoReflect() protoreflect.Message { - mi := &file_staticaddr_proto_msgTypes[16] + mi := &file_staticaddr_proto_msgTypes[18] if x != nil { ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) if ms.LoadMessageInfo() == nil { @@ -1157,7 +1298,7 @@ func (x *PushStaticAddressSweeplessSigsResponse) ProtoReflect() protoreflect.Mes // Deprecated: Use PushStaticAddressSweeplessSigsResponse.ProtoReflect.Descriptor instead. func (*PushStaticAddressSweeplessSigsResponse) Descriptor() ([]byte, []int) { - return file_staticaddr_proto_rawDescGZIP(), []int{16} + return file_staticaddr_proto_rawDescGZIP(), []int{18} } var File_staticaddr_proto protoreflect.FileDescriptor @@ -1184,13 +1325,17 @@ const file_staticaddr_proto_rawDesc = "" + "\rchange_amount\x18\x06 \x01(\x03R\fchangeAmount:\x02\x18\x01\"m\n" + "\x16ServerWithdrawResponse\x12*\n" + "\x11musig2_sweep_sigs\x18\x01 \x03(\fR\x0fmusig2SweepSigs\x12#\n" + - "\rserver_nonces\x18\x02 \x03(\fR\fserverNonces:\x02\x18\x01\"\xed\x01\n" + + "\rserver_nonces\x18\x02 \x03(\fR\fserverNonces:\x02\x18\x01\"\xd5\x03\n" + "\x19ServerPsbtWithdrawRequest\x12'\n" + "\x0fwithdrawal_psbt\x18\x01 \x01(\fR\x0ewithdrawalPsbt\x12c\n" + - "\x11deposit_to_nonces\x18\x02 \x03(\v27.looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntryR\x0fdepositToNonces\x1aB\n" + + "\x11deposit_to_nonces\x18\x02 \x03(\v27.looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntryR\x0fdepositToNonces\x12y\n" + + "\x19deposit_to_client_pubkeys\x18\x03 \x03(\v2>.looprpc.ServerPsbtWithdrawRequest.DepositToClientPubkeysEntryR\x16depositToClientPubkeys\x1aB\n" + "\x14DepositToNoncesEntry\x12\x10\n" + "\x03key\x18\x01 \x01(\tR\x03key\x12\x14\n" + - "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\"\xf1\x01\n" + + "\x05value\x18\x02 \x01(\fR\x05value:\x028\x01\x1ak\n" + + "\x1bDepositToClientPubkeysEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x126\n" + + "\x05value\x18\x02 \x01(\v2 .looprpc.StaticAddressDescriptorR\x05value:\x028\x01\"\xf1\x01\n" + "\x1aServerPsbtWithdrawResponse\x12\x12\n" + "\x04txid\x18\x01 \x01(\fR\x04txid\x12W\n" + "\fsigning_info\x18\x02 \x03(\v24.looprpc.ServerPsbtWithdrawResponse.SigningInfoEntryR\vsigningInfo\x1af\n" + @@ -1199,7 +1344,13 @@ const file_staticaddr_proto_rawDesc = "" + "\x05value\x18\x02 \x01(\v2&.looprpc.ServerPsbtWithdrawSigningInfoR\x05value:\x028\x01\"G\n" + "\x1dServerPsbtWithdrawSigningInfo\x12\x14\n" + "\x05nonce\x18\x01 \x01(\fR\x05nonce\x12\x10\n" + - "\x03sig\x18\x02 \x01(\fR\x03sig\"\xae\x03\n" + + "\x03sig\x18\x02 \x01(\fR\x03sig\"N\n" + + "\x17StaticAddressDescriptor\x12\x16\n" + + "\x06pubkey\x18\x01 \x01(\fR\x06pubkey\x12\x1b\n" + + "\tpk_script\x18\x02 \x01(\fR\bpkScript\"|\n" + + "\x19StaticAddressChangeOutput\x12G\n" + + "\x0estatic_address\x18\x01 \x01(\v2 .looprpc.StaticAddressDescriptorR\rstaticAddress\x12\x16\n" + + "\x06amount\x18\x02 \x01(\x03R\x06amount\"\xe7\x05\n" + " ServerStaticAddressLoopInRequest\x12-\n" + "\x13htlc_client_pub_key\x18\x01 \x01(\fR\x10htlcClientPubKey\x12\x1b\n" + "\tswap_hash\x18\x02 \x01(\fR\bswapHash\x12+\n" + @@ -1212,7 +1363,12 @@ const file_staticaddr_proto_rawDesc = "" + "\x17payment_timeout_seconds\x18\b \x01(\rR\x15paymentTimeoutSeconds\x12\x16\n" + "\x06amount\x18\t \x01(\x04R\x06amount\x12\x12\n" + "\x04fast\x18\n" + - " \x01(\bR\x04fast\"\xe1\x02\n" + + " \x01(\bR\x04fast\x12\x80\x01\n" + + "\x19deposit_to_client_pubkeys\x18\v \x03(\v2E.looprpc.ServerStaticAddressLoopInRequest.DepositToClientPubkeysEntryR\x16depositToClientPubkeys\x12G\n" + + "\rchange_output\x18\f \x01(\v2\".looprpc.StaticAddressChangeOutputR\fchangeOutput\x1ak\n" + + "\x1bDepositToClientPubkeysEntry\x12\x10\n" + + "\x03key\x18\x01 \x01(\tR\x03key\x126\n" + + "\x05value\x18\x02 \x01(\v2 .looprpc.StaticAddressDescriptorR\x05value:\x028\x01\"\xe1\x02\n" + "!ServerStaticAddressLoopInResponse\x12-\n" + "\x13htlc_server_pub_key\x18\x01 \x01(\fR\x10htlcServerPubKey\x12\x1f\n" + "\vhtlc_expiry\x18\x02 \x01(\x05R\n" + @@ -1267,7 +1423,7 @@ func file_staticaddr_proto_rawDescGZIP() []byte { } var file_staticaddr_proto_enumTypes = make([]protoimpl.EnumInfo, 1) -var file_staticaddr_proto_msgTypes = make([]protoimpl.MessageInfo, 20) +var file_staticaddr_proto_msgTypes = make([]protoimpl.MessageInfo, 24) var file_staticaddr_proto_goTypes = []any{ (StaticAddressProtocolVersion)(0), // 0: looprpc.StaticAddressProtocolVersion (*ServerNewAddressRequest)(nil), // 1: looprpc.ServerNewAddressRequest @@ -1278,53 +1434,63 @@ var file_staticaddr_proto_goTypes = []any{ (*ServerPsbtWithdrawRequest)(nil), // 6: looprpc.ServerPsbtWithdrawRequest (*ServerPsbtWithdrawResponse)(nil), // 7: looprpc.ServerPsbtWithdrawResponse (*ServerPsbtWithdrawSigningInfo)(nil), // 8: looprpc.ServerPsbtWithdrawSigningInfo - (*ServerStaticAddressLoopInRequest)(nil), // 9: looprpc.ServerStaticAddressLoopInRequest - (*ServerStaticAddressLoopInResponse)(nil), // 10: looprpc.ServerStaticAddressLoopInResponse - (*ServerHtlcSigningInfo)(nil), // 11: looprpc.ServerHtlcSigningInfo - (*PushStaticAddressHtlcSigsRequest)(nil), // 12: looprpc.PushStaticAddressHtlcSigsRequest - (*ClientHtlcSigningInfo)(nil), // 13: looprpc.ClientHtlcSigningInfo - (*PushStaticAddressHtlcSigsResponse)(nil), // 14: looprpc.PushStaticAddressHtlcSigsResponse - (*PushStaticAddressSweeplessSigsRequest)(nil), // 15: looprpc.PushStaticAddressSweeplessSigsRequest - (*ClientSweeplessSigningInfo)(nil), // 16: looprpc.ClientSweeplessSigningInfo - (*PushStaticAddressSweeplessSigsResponse)(nil), // 17: looprpc.PushStaticAddressSweeplessSigsResponse - nil, // 18: looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntry - nil, // 19: looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry - nil, // 20: looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry - (*PrevoutInfo)(nil), // 21: looprpc.PrevoutInfo + (*StaticAddressDescriptor)(nil), // 9: looprpc.StaticAddressDescriptor + (*StaticAddressChangeOutput)(nil), // 10: looprpc.StaticAddressChangeOutput + (*ServerStaticAddressLoopInRequest)(nil), // 11: looprpc.ServerStaticAddressLoopInRequest + (*ServerStaticAddressLoopInResponse)(nil), // 12: looprpc.ServerStaticAddressLoopInResponse + (*ServerHtlcSigningInfo)(nil), // 13: looprpc.ServerHtlcSigningInfo + (*PushStaticAddressHtlcSigsRequest)(nil), // 14: looprpc.PushStaticAddressHtlcSigsRequest + (*ClientHtlcSigningInfo)(nil), // 15: looprpc.ClientHtlcSigningInfo + (*PushStaticAddressHtlcSigsResponse)(nil), // 16: looprpc.PushStaticAddressHtlcSigsResponse + (*PushStaticAddressSweeplessSigsRequest)(nil), // 17: looprpc.PushStaticAddressSweeplessSigsRequest + (*ClientSweeplessSigningInfo)(nil), // 18: looprpc.ClientSweeplessSigningInfo + (*PushStaticAddressSweeplessSigsResponse)(nil), // 19: looprpc.PushStaticAddressSweeplessSigsResponse + nil, // 20: looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntry + nil, // 21: looprpc.ServerPsbtWithdrawRequest.DepositToClientPubkeysEntry + nil, // 22: looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry + nil, // 23: looprpc.ServerStaticAddressLoopInRequest.DepositToClientPubkeysEntry + nil, // 24: looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry + (*PrevoutInfo)(nil), // 25: looprpc.PrevoutInfo } var file_staticaddr_proto_depIdxs = []int32{ 0, // 0: looprpc.ServerNewAddressRequest.protocol_version:type_name -> looprpc.StaticAddressProtocolVersion 3, // 1: looprpc.ServerNewAddressResponse.params:type_name -> looprpc.ServerAddressParameters - 21, // 2: looprpc.ServerWithdrawRequest.outpoints:type_name -> looprpc.PrevoutInfo - 18, // 3: looprpc.ServerPsbtWithdrawRequest.deposit_to_nonces:type_name -> looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntry - 19, // 4: looprpc.ServerPsbtWithdrawResponse.signing_info:type_name -> looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry - 0, // 5: looprpc.ServerStaticAddressLoopInRequest.protocol_version:type_name -> looprpc.StaticAddressProtocolVersion - 11, // 6: looprpc.ServerStaticAddressLoopInResponse.standard_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo - 11, // 7: looprpc.ServerStaticAddressLoopInResponse.high_fee_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo - 11, // 8: looprpc.ServerStaticAddressLoopInResponse.extreme_fee_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo - 13, // 9: looprpc.PushStaticAddressHtlcSigsRequest.standard_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo - 13, // 10: looprpc.PushStaticAddressHtlcSigsRequest.high_fee_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo - 13, // 11: looprpc.PushStaticAddressHtlcSigsRequest.extreme_fee_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo - 20, // 12: looprpc.PushStaticAddressSweeplessSigsRequest.signing_info:type_name -> looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry - 8, // 13: looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry.value:type_name -> looprpc.ServerPsbtWithdrawSigningInfo - 16, // 14: looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry.value:type_name -> looprpc.ClientSweeplessSigningInfo - 1, // 15: looprpc.StaticAddressServer.ServerNewAddress:input_type -> looprpc.ServerNewAddressRequest - 4, // 16: looprpc.StaticAddressServer.ServerWithdrawDeposits:input_type -> looprpc.ServerWithdrawRequest - 6, // 17: looprpc.StaticAddressServer.ServerPsbtWithdrawDeposits:input_type -> looprpc.ServerPsbtWithdrawRequest - 9, // 18: looprpc.StaticAddressServer.ServerStaticAddressLoopIn:input_type -> looprpc.ServerStaticAddressLoopInRequest - 12, // 19: looprpc.StaticAddressServer.PushStaticAddressHtlcSigs:input_type -> looprpc.PushStaticAddressHtlcSigsRequest - 15, // 20: looprpc.StaticAddressServer.PushStaticAddressSweeplessSigs:input_type -> looprpc.PushStaticAddressSweeplessSigsRequest - 2, // 21: looprpc.StaticAddressServer.ServerNewAddress:output_type -> looprpc.ServerNewAddressResponse - 5, // 22: looprpc.StaticAddressServer.ServerWithdrawDeposits:output_type -> looprpc.ServerWithdrawResponse - 7, // 23: looprpc.StaticAddressServer.ServerPsbtWithdrawDeposits:output_type -> looprpc.ServerPsbtWithdrawResponse - 10, // 24: looprpc.StaticAddressServer.ServerStaticAddressLoopIn:output_type -> looprpc.ServerStaticAddressLoopInResponse - 14, // 25: looprpc.StaticAddressServer.PushStaticAddressHtlcSigs:output_type -> looprpc.PushStaticAddressHtlcSigsResponse - 17, // 26: looprpc.StaticAddressServer.PushStaticAddressSweeplessSigs:output_type -> looprpc.PushStaticAddressSweeplessSigsResponse - 21, // [21:27] is the sub-list for method output_type - 15, // [15:21] is the sub-list for method input_type - 15, // [15:15] is the sub-list for extension type_name - 15, // [15:15] is the sub-list for extension extendee - 0, // [0:15] is the sub-list for field type_name + 25, // 2: looprpc.ServerWithdrawRequest.outpoints:type_name -> looprpc.PrevoutInfo + 20, // 3: looprpc.ServerPsbtWithdrawRequest.deposit_to_nonces:type_name -> looprpc.ServerPsbtWithdrawRequest.DepositToNoncesEntry + 21, // 4: looprpc.ServerPsbtWithdrawRequest.deposit_to_client_pubkeys:type_name -> looprpc.ServerPsbtWithdrawRequest.DepositToClientPubkeysEntry + 22, // 5: looprpc.ServerPsbtWithdrawResponse.signing_info:type_name -> looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry + 9, // 6: looprpc.StaticAddressChangeOutput.static_address:type_name -> looprpc.StaticAddressDescriptor + 0, // 7: looprpc.ServerStaticAddressLoopInRequest.protocol_version:type_name -> looprpc.StaticAddressProtocolVersion + 23, // 8: looprpc.ServerStaticAddressLoopInRequest.deposit_to_client_pubkeys:type_name -> looprpc.ServerStaticAddressLoopInRequest.DepositToClientPubkeysEntry + 10, // 9: looprpc.ServerStaticAddressLoopInRequest.change_output:type_name -> looprpc.StaticAddressChangeOutput + 13, // 10: looprpc.ServerStaticAddressLoopInResponse.standard_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo + 13, // 11: looprpc.ServerStaticAddressLoopInResponse.high_fee_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo + 13, // 12: looprpc.ServerStaticAddressLoopInResponse.extreme_fee_htlc_info:type_name -> looprpc.ServerHtlcSigningInfo + 15, // 13: looprpc.PushStaticAddressHtlcSigsRequest.standard_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo + 15, // 14: looprpc.PushStaticAddressHtlcSigsRequest.high_fee_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo + 15, // 15: looprpc.PushStaticAddressHtlcSigsRequest.extreme_fee_htlc_info:type_name -> looprpc.ClientHtlcSigningInfo + 24, // 16: looprpc.PushStaticAddressSweeplessSigsRequest.signing_info:type_name -> looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry + 9, // 17: looprpc.ServerPsbtWithdrawRequest.DepositToClientPubkeysEntry.value:type_name -> looprpc.StaticAddressDescriptor + 8, // 18: looprpc.ServerPsbtWithdrawResponse.SigningInfoEntry.value:type_name -> looprpc.ServerPsbtWithdrawSigningInfo + 9, // 19: looprpc.ServerStaticAddressLoopInRequest.DepositToClientPubkeysEntry.value:type_name -> looprpc.StaticAddressDescriptor + 18, // 20: looprpc.PushStaticAddressSweeplessSigsRequest.SigningInfoEntry.value:type_name -> looprpc.ClientSweeplessSigningInfo + 1, // 21: looprpc.StaticAddressServer.ServerNewAddress:input_type -> looprpc.ServerNewAddressRequest + 4, // 22: looprpc.StaticAddressServer.ServerWithdrawDeposits:input_type -> looprpc.ServerWithdrawRequest + 6, // 23: looprpc.StaticAddressServer.ServerPsbtWithdrawDeposits:input_type -> looprpc.ServerPsbtWithdrawRequest + 11, // 24: looprpc.StaticAddressServer.ServerStaticAddressLoopIn:input_type -> looprpc.ServerStaticAddressLoopInRequest + 14, // 25: looprpc.StaticAddressServer.PushStaticAddressHtlcSigs:input_type -> looprpc.PushStaticAddressHtlcSigsRequest + 17, // 26: looprpc.StaticAddressServer.PushStaticAddressSweeplessSigs:input_type -> looprpc.PushStaticAddressSweeplessSigsRequest + 2, // 27: looprpc.StaticAddressServer.ServerNewAddress:output_type -> looprpc.ServerNewAddressResponse + 5, // 28: looprpc.StaticAddressServer.ServerWithdrawDeposits:output_type -> looprpc.ServerWithdrawResponse + 7, // 29: looprpc.StaticAddressServer.ServerPsbtWithdrawDeposits:output_type -> looprpc.ServerPsbtWithdrawResponse + 12, // 30: looprpc.StaticAddressServer.ServerStaticAddressLoopIn:output_type -> looprpc.ServerStaticAddressLoopInResponse + 16, // 31: looprpc.StaticAddressServer.PushStaticAddressHtlcSigs:output_type -> looprpc.PushStaticAddressHtlcSigsResponse + 19, // 32: looprpc.StaticAddressServer.PushStaticAddressSweeplessSigs:output_type -> looprpc.PushStaticAddressSweeplessSigsResponse + 27, // [27:33] is the sub-list for method output_type + 21, // [21:27] is the sub-list for method input_type + 21, // [21:21] is the sub-list for extension type_name + 21, // [21:21] is the sub-list for extension extendee + 0, // [0:21] is the sub-list for field type_name } func init() { file_staticaddr_proto_init() } @@ -1339,7 +1505,7 @@ func file_staticaddr_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: unsafe.Slice(unsafe.StringData(file_staticaddr_proto_rawDesc), len(file_staticaddr_proto_rawDesc)), NumEnums: 1, - NumMessages: 20, + NumMessages: 24, NumExtensions: 0, NumServices: 1, }, diff --git a/swapserverrpc/staticaddr.proto b/swapserverrpc/staticaddr.proto index b590f4d4..9618d013 100644 --- a/swapserverrpc/staticaddr.proto +++ b/swapserverrpc/staticaddr.proto @@ -124,6 +124,11 @@ message ServerPsbtWithdrawRequest { // The map of deposit txid:idx to the nonce used by the client. map deposit_to_nonces = 2; + + // The map of deposit txid:idx to the static address descriptor that was + // used to derive the deposit output. The server combines the client key + // with the L402's server pubkey and expiry to validate each input. + map deposit_to_client_pubkeys = 3; } message ServerPsbtWithdrawResponse { @@ -143,6 +148,23 @@ message ServerPsbtWithdrawSigningInfo { bytes sig = 2; } +message StaticAddressDescriptor { + // The client's secp256k1 public key serialized in 33-byte compressed SEC1 + // format. This is not the 32-byte x-only BIP-340 encoding. + bytes pubkey = 1; + + // The expected output script for the static address. + bytes pk_script = 2; +} + +message StaticAddressChangeOutput { + // The descriptor for the static address change output. + StaticAddressDescriptor static_address = 1; + + // The expected change amount in satoshis. + int64 amount = 2; +} + message ServerStaticAddressLoopInRequest { // The client's public key for the htlc output. bytes htlc_client_pub_key = 1; @@ -196,6 +218,15 @@ message ServerStaticAddressLoopInRequest { // If set, request the server to use fast publication behavior for this // swap. bool fast = 10; + + // The map of deposit txid:idx to the static address descriptor that was + // used to derive the deposit output. The server combines the client key + // with the L402's server pubkey and expiry to validate each input. + map deposit_to_client_pubkeys = 11; + + // Optional change output metadata for fractional loop-ins that return + // funds to a newly generated static address. + StaticAddressChangeOutput change_output = 12; } message ServerStaticAddressLoopInResponse { diff --git a/test/lightning_client_mock.go b/test/lightning_client_mock.go index fe4198a1..350fb59a 100644 --- a/test/lightning_client_mock.go +++ b/test/lightning_client_mock.go @@ -29,21 +29,6 @@ type mockLightningClient struct { wg sync.WaitGroup } -// PayInvoice pays an invoice. -func (h *mockLightningClient) PayInvoice(ctx context.Context, invoice string, - maxFee btcutil.Amount, - outgoingChannel *uint64) chan lndclient.PaymentResult { - - done := make(chan lndclient.PaymentResult, 1) - - h.lnd.SendPaymentChannel <- PaymentChannelMessage{ - PaymentRequest: invoice, - Done: done, - } - - return done -} - // DecodePaymentRequest returns a non-nil payment request. func (h *mockLightningClient) DecodePaymentRequest(_ context.Context, _ string) (*lndclient.PaymentRequest, error) { @@ -170,7 +155,9 @@ func (h *mockLightningClient) LookupInvoice(_ context.Context, return nil, fmt.Errorf("invoice: %x not found", hash) } - return inv, nil + invoiceCopy := *inv + + return &invoiceCopy, nil } // ListTransactions returns all known transactions of the backing lnd node. @@ -281,6 +268,11 @@ func (h *mockLightningClient) ListPayments(_ context.Context, req lndclient.ListPaymentsRequest) (*lndclient.ListPaymentsResponse, error) { + h.lnd.lock.Lock() + defer h.lnd.lock.Unlock() + + h.lnd.ListPaymentsRequests = append(h.lnd.ListPaymentsRequests, req) + if req.Offset >= uint64(len(h.lnd.Payments)) { return &lndclient.ListPaymentsResponse{}, nil } @@ -288,7 +280,8 @@ func (h *mockLightningClient) ListPayments(_ context.Context, lastIndexOffset := req.Offset + req.MaxPayments lastIndexOffset = min(lastIndexOffset, uint64(len(h.lnd.Payments))) - result := h.lnd.Payments[req.Offset:lastIndexOffset] + result := make([]lndclient.Payment, lastIndexOffset-req.Offset) + copy(result, h.lnd.Payments[req.Offset:lastIndexOffset]) return &lndclient.ListPaymentsResponse{ Payments: result, diff --git a/test/lnd_services_mock.go b/test/lnd_services_mock.go index 4fe5d9b5..a41a3dbc 100644 --- a/test/lnd_services_mock.go +++ b/test/lnd_services_mock.go @@ -48,7 +48,6 @@ func NewMockLnd() *LndMockServices { ChainParams: &chaincfg.TestNet3Params, Versioner: versioner, }, - SendPaymentChannel: make(chan PaymentChannelMessage), ConfChannel: make(chan *chainntnfs.TxConfirmation), RegisterConfChannel: make(chan *ConfRegistration), RegisterSpendChannel: make(chan *SpendRegistration), @@ -95,12 +94,6 @@ func NewMockLnd() *LndMockServices { return &lnd } -// PaymentChannelMessage is the data that passed through SendPaymentChannel. -type PaymentChannelMessage struct { - PaymentRequest string - Done chan lndclient.PaymentResult -} - // TrackPaymentMessage is the data that passed through TrackPaymentChannel. type TrackPaymentMessage struct { Hash lntypes.Hash @@ -138,7 +131,6 @@ type PublishHandler func(ctx context.Context, tx *wire.MsgTx, type LndMockServices struct { lndclient.LndServices - SendPaymentChannel chan PaymentChannelMessage SpendChannel chan *chainntnfs.SpendDetail TxPublishChannel chan *wire.MsgTx SendOutputsChannel chan wire.MsgTx @@ -170,12 +162,13 @@ type LndMockServices struct { // keyed by hash string. Invoices map[lntypes.Hash]*lndclient.Invoice - Channels []lndclient.ChannelInfo - ChannelEdges map[uint64]*lndclient.ChannelEdge - ClosedChannels []lndclient.ClosedChannel - ForwardingEvents []lndclient.ForwardingEvent - Payments []lndclient.Payment - MissionControlState []lndclient.MissionControlEntry + Channels []lndclient.ChannelInfo + ChannelEdges map[uint64]*lndclient.ChannelEdge + ClosedChannels []lndclient.ClosedChannel + ForwardingEvents []lndclient.ForwardingEvent + Payments []lndclient.Payment + ListPaymentsRequests []lndclient.ListPaymentsRequest + MissionControlState []lndclient.MissionControlEntry WaitForFinished func() @@ -193,6 +186,20 @@ func (s *LndMockServices) EpochSubscribers() int32 { return int32(len(s.blockHeightListeners)) } +// ListPaymentsRequestsSnapshot returns a copy of all ListPayments requests +// recorded by the mock. +func (s *LndMockServices) ListPaymentsRequestsSnapshot() []lndclient.ListPaymentsRequest { + s.lock.Lock() + defer s.lock.Unlock() + + requests := make( + []lndclient.ListPaymentsRequest, len(s.ListPaymentsRequests), + ) + copy(requests, s.ListPaymentsRequests) + + return requests +} + // NotifyHeight notifies a new block height. func (s *LndMockServices) NotifyHeight(height int32) error { s.lock.Lock() @@ -218,15 +225,18 @@ func (s *LndMockServices) AddTx(tx *wire.MsgTx) { s.lock.Unlock() } +// SetInvoice stores a copy of the given invoice in the mock invoice store. +func (s *LndMockServices) SetInvoice(invoice *lndclient.Invoice) { + s.lock.Lock() + defer s.lock.Unlock() + + invoiceCopy := *invoice + s.Invoices[invoice.Hash] = &invoiceCopy +} + // IsDone checks whether all channels have been fully emptied. If not this may // indicate unexpected behaviour of the code under test. func (s *LndMockServices) IsDone() error { - select { - case <-s.SendPaymentChannel: - return errors.New("SendPaymentChannel not empty") - default: - } - select { case <-s.SpendChannel: return errors.New("SpendChannel not empty") diff --git a/tools/go.mod b/tools/go.mod index 819df12f..1f2d73ff 100644 --- a/tools/go.mod +++ b/tools/go.mod @@ -222,3 +222,9 @@ require ( go.augendre.info/fatcontext v0.9.0 // indirect go.yaml.in/yaml/v3 v3.0.4 // indirect ) + +// The go.augendre.info vanity import endpoints are currently unavailable, +// but these tags still declare the original module paths. +replace go.augendre.info/arangolint => github.com/Crocmagnon/arangolint v0.4.0 + +replace go.augendre.info/fatcontext => github.com/Crocmagnon/fatcontext v0.9.0 diff --git a/tools/go.sum b/tools/go.sum index 243eb20e..b04c04cd 100644 --- a/tools/go.sum +++ b/tools/go.sum @@ -61,6 +61,10 @@ github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03 github.com/BurntSushi/toml v1.6.0 h1:dRaEfpa2VI55EwlIW72hMRHdWouJeRF7TPYhI+AUQjk= github.com/BurntSushi/toml v1.6.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= +github.com/Crocmagnon/arangolint v0.4.0 h1:hqwqcPrdvYouEmwLxT9lZGY5/Zwn25m5WvLa+JDxYhc= +github.com/Crocmagnon/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= +github.com/Crocmagnon/fatcontext v0.9.0 h1:uCKrygUTja+hDpqURwbOCpIOqdeywN196fUA+/9r9lU= +github.com/Crocmagnon/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= github.com/Djarvur/go-err113 v0.1.1 h1:eHfopDqXRwAi+YmCUas75ZE0+hoBHJ2GQNLYRSxao4g= github.com/Djarvur/go-err113 v0.1.1/go.mod h1:IaWJdYFLg76t2ihfflPZnM1LIQszWOsFDh2hhhAVF6k= github.com/Masterminds/semver/v3 v3.4.0 h1:Zog+i5UMtVoCU8oKka5P7i9q9HgrJeGzI9SA1Xbatp0= @@ -622,10 +626,6 @@ go-simpler.org/musttag v0.14.0 h1:XGySZATqQYSEV3/YTy+iX+aofbZZllJaqwFWs+RTtSo= go-simpler.org/musttag v0.14.0/go.mod h1:uP8EymctQjJ4Z1kUnjX0u2l60WfUdQxCwSNKzE1JEOE= go-simpler.org/sloglint v0.11.1 h1:xRbPepLT/MHPTCA6TS/wNfZrDzkGvCCqUv4Bdwc3H7s= go-simpler.org/sloglint v0.11.1/go.mod h1:2PowwiCOK8mjiF+0KGifVOT8ZsCNiFzvfyJeJOIt8MQ= -go.augendre.info/arangolint v0.4.0 h1:xSCZjRoS93nXazBSg5d0OGCi9APPLNMmmLrC995tR50= -go.augendre.info/arangolint v0.4.0/go.mod h1:l+f/b4plABuFISuKnTGD4RioXiCCgghv2xqst/xOvAA= -go.augendre.info/fatcontext v0.9.0 h1:Gt5jGD4Zcj8CDMVzjOJITlSb9cEch54hjRRlN3qDojE= -go.augendre.info/fatcontext v0.9.0/go.mod h1:L94brOAT1OOUNue6ph/2HnwxoNlds9aXDF2FcUntbNw= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= diff --git a/version.go b/version.go index f7848515..88d2cc4b 100644 --- a/version.go +++ b/version.go @@ -35,10 +35,11 @@ const semanticAlphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqr // These constants define the application version and follow the semantic // versioning 2.0.0 spec (http://semver.org/). const ( - // Note: please update release_notes.md when you change these values. + // Note: please update docs/release-notes/release-notes-next.md when you + // change these values. appMajor uint = 0 - appMinor uint = 33 - appPatch uint = 2 + appMinor uint = 34 + appPatch uint = 0 // appPreRelease MUST only contain characters from semanticAlphabet per // the semantic versioning spec.