Compare commits
1 Commits
master
...
basedon-te
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9a6e22ecbe |
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@ -21,7 +21,7 @@ defaults:
|
||||
shell: bash
|
||||
|
||||
env:
|
||||
BITCOIN_VERSION: "28"
|
||||
BITCOIN_VERSION: "27"
|
||||
|
||||
TRANCHES: 8
|
||||
|
||||
@ -31,7 +31,7 @@ env:
|
||||
# /dev.Dockerfile
|
||||
# /make/builder.Dockerfile
|
||||
# /.github/workflows/release.yml
|
||||
GO_VERSION: 1.22.6
|
||||
GO_VERSION: 1.22.5
|
||||
|
||||
jobs:
|
||||
########################
|
||||
|
||||
3
.github/workflows/release.yaml
vendored
3
.github/workflows/release.yaml
vendored
@ -11,11 +11,12 @@ defaults:
|
||||
|
||||
env:
|
||||
# If you change this value, please change it in the following files as well:
|
||||
# /.travis.yml
|
||||
# /Dockerfile
|
||||
# /dev.Dockerfile
|
||||
# /make/builder.Dockerfile
|
||||
# /.github/workflows/main.yml
|
||||
GO_VERSION: 1.22.6
|
||||
GO_VERSION: 1.22.5
|
||||
|
||||
jobs:
|
||||
main:
|
||||
|
||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -66,7 +66,6 @@ profile.tmp
|
||||
.DS_Store
|
||||
|
||||
.vscode
|
||||
*.code-workspace
|
||||
|
||||
# Coverage test
|
||||
coverage.txt
|
||||
|
||||
@ -1,8 +1,18 @@
|
||||
run:
|
||||
go: "1.22.6"
|
||||
# timeout for analysis
|
||||
deadline: 10m
|
||||
|
||||
# Abort after 10 minutes.
|
||||
timeout: 10m
|
||||
# Skip autogenerated files for mobile and gRPC as well as copied code for
|
||||
# internal use.
|
||||
skip-files:
|
||||
- "mobile\\/.*generated\\.go"
|
||||
- "\\.pb\\.go$"
|
||||
- "\\.pb\\.gw\\.go$"
|
||||
- "internal\\/musig2v040"
|
||||
|
||||
skip-dirs:
|
||||
- channeldb/migration_01_to_11
|
||||
- channeldb/migration/lnwire21
|
||||
|
||||
build-tags:
|
||||
- autopilotrpc
|
||||
@ -47,6 +57,7 @@ linters-settings:
|
||||
- G306 # Poor file permissions used when writing to a new file.
|
||||
|
||||
staticcheck:
|
||||
go: "1.22.5"
|
||||
checks: ["-SA1019"]
|
||||
|
||||
lll:
|
||||
@ -122,15 +133,25 @@ linters:
|
||||
- gochecknoinits
|
||||
|
||||
# Deprecated linters. See https://golangci-lint.run/usage/linters/.
|
||||
- interfacer
|
||||
- golint
|
||||
- maligned
|
||||
- scopelint
|
||||
- exhaustivestruct
|
||||
- bodyclose
|
||||
- contextcheck
|
||||
- nilerr
|
||||
- noctx
|
||||
- rowserrcheck
|
||||
- sqlclosecheck
|
||||
- structcheck
|
||||
- tparallel
|
||||
- unparam
|
||||
- wastedassign
|
||||
- ifshort
|
||||
- varcheck
|
||||
- deadcode
|
||||
- nosnakecase
|
||||
|
||||
|
||||
# Disable gofumpt as it has weird behavior regarding formatting multiple
|
||||
@ -170,7 +191,7 @@ linters:
|
||||
- wrapcheck
|
||||
|
||||
# Allow dynamic errors.
|
||||
- err113
|
||||
- goerr113
|
||||
|
||||
# We use ErrXXX instead.
|
||||
- errname
|
||||
@ -186,41 +207,15 @@ linters:
|
||||
# The linter is too aggressive and doesn't add much value since reviewers
|
||||
# will also catch magic numbers that make sense to extract.
|
||||
- gomnd
|
||||
- mnd
|
||||
|
||||
# Some of the tests cannot be parallelized. On the other hand, we don't
|
||||
# gain much performance with this check so we disable it for now until
|
||||
# unit tests become our CI bottleneck.
|
||||
# Some of the tests cannot be parallelized. On the other hand, we don't
|
||||
# gain much performance with this check so we disable it for now until
|
||||
# unit tests become our CI bottleneck.
|
||||
- paralleltest
|
||||
|
||||
# New linters that we haven't had time to address yet.
|
||||
- testifylint
|
||||
- perfsprint
|
||||
- inamedparam
|
||||
- copyloopvar
|
||||
- tagalign
|
||||
- protogetter
|
||||
- revive
|
||||
- depguard
|
||||
- gosmopolitan
|
||||
- intrange
|
||||
|
||||
|
||||
issues:
|
||||
# Only show newly introduced problems.
|
||||
new-from-rev: 77c7f776d5cbf9e147edc81d65ae5ba177a684e5
|
||||
|
||||
# Skip autogenerated files for mobile and gRPC as well as copied code for
|
||||
# internal use.
|
||||
skip-files:
|
||||
- "mobile\\/.*generated\\.go"
|
||||
- "\\.pb\\.go$"
|
||||
- "\\.pb\\.gw\\.go$"
|
||||
- "internal\\/musig2v040"
|
||||
|
||||
skip-dirs:
|
||||
- channeldb/migration_01_to_11
|
||||
- channeldb/migration/lnwire21
|
||||
new-from-rev: 8c66353e4c02329abdacb5a8df29998035ec2e24
|
||||
|
||||
exclude-rules:
|
||||
# Exclude gosec from running for tests so that tests with weak randomness
|
||||
@ -261,8 +256,8 @@ issues:
|
||||
- forbidigo
|
||||
- godot
|
||||
|
||||
# Allow fmt.Printf() in commands.
|
||||
- path: cmd/commands/*
|
||||
# Allow fmt.Printf() in lncli.
|
||||
- path: cmd/lncli/*
|
||||
linters:
|
||||
- forbidigo
|
||||
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# /make/builder.Dockerfile
|
||||
# /.github/workflows/main.yml
|
||||
# /.github/workflows/release.yml
|
||||
FROM golang:1.22.6-alpine as builder
|
||||
FROM golang:1.22.5-alpine as builder
|
||||
|
||||
# Force Go to use the cgo based DNS resolver. This is required to ensure DNS
|
||||
# queries required to connect to linked containers succeed.
|
||||
|
||||
2
Makefile
2
Makefile
@ -43,7 +43,7 @@ endif
|
||||
# GO_VERSION is the Go version used for the release build, docker files, and
|
||||
# GitHub Actions. This is the reference version for the project. All other Go
|
||||
# versions are checked against this version.
|
||||
GO_VERSION = 1.22.6
|
||||
GO_VERSION = 1.22.5
|
||||
|
||||
GOBUILD := $(LOOPVARFIX) go build -v
|
||||
GOINSTALL := $(LOOPVARFIX) go install -v
|
||||
|
||||
153
README.md
153
README.md
@ -1,89 +1,98 @@
|
||||
# BTCPayServer LND
|
||||
## Lightning Network Daemon
|
||||
|
||||
This repository is used to build LND Docker container images that are distributed with BTCPayServer by default.
|
||||
[](https://github.com/lightningnetwork/lnd/actions/workflows/release.yaml)
|
||||
[](https://github.com/lightningnetwork/lnd/blob/master/LICENSE)
|
||||
[](https://web.libera.chat/#lnd)
|
||||
[](https://godoc.org/github.com/lightningnetwork/lnd)
|
||||
[](https://goreportcard.com/report/github.com/lightningnetwork/lnd)
|
||||
|
||||
Docker images are published to https://hub.docker.com/r/btcpayserver/lnd/
|
||||
<img src="logo.png">
|
||||
|
||||
Versions:
|
||||
- [0.18.3-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.18.3-beta/images/sha256-513ddd55a5af44a14e27110ee14cb28f1c7a69205bcaa2fba4e66275c1f725e5?context=repo)
|
||||
- Includes 0.28.7-beta Loop
|
||||
- [Fix for lnd unlock password \n problem](https://github.com/btcpayserver/lnd/pull/7)
|
||||
- [0.18.1-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.18.1-beta/images/sha256-5fbfa76a218ab59bf9206485f4c0c071a525f9f0906255a5672054741d043b79?context=repo)
|
||||
- Includes 0.28.5-beta Loop
|
||||
- [0.18.0-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.18.0-beta/images/sha256-e6043dddf0bdbd5c740e882447c441b37f87f2c736ebb08747a4aff5e100d9bf?context=repo)
|
||||
- Includes 0.28.2-beta Loop
|
||||
- [0.17.4-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.4-beta/images/sha256-b62ecff5ca71d37f9b4846f35b4d86ddc4faa3fc1dd0618ae9221d99f47708bd?context=explore)
|
||||
- Includes 0.26.6-beta Loop
|
||||
- [0.17.3-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.3-beta/images/sha256-141611de6c062835e9513dd1ec4155c779d7a7b55258eb1fe06e228b0835fa56?context=repo)
|
||||
- Includes 0.26.6-beta Loop
|
||||
- [0.17.2-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.2-beta/images/sha256-936767369b703a67daf6db6a008a3b53c15f407d29a7ad2327a0de28f5951b30?context=explore)
|
||||
- Includes 0.26.5-beta Loop
|
||||
- [0.17.1-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.1-beta/images/sha256-b5c106136bd33a422463c736a1db8bd3541f95ac6f277dae86ab2a01b0c3445a?context=explore)
|
||||
- Includes 0.26.5-beta Loop
|
||||
- [0.17.0-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.0-beta/images/sha256-58b98f983cd786bcb4d48ea8586144cafd44d58dc3018e26bfbfcf875f495368?context=explore)
|
||||
- Includes 0.26.4-beta Loop
|
||||
- [0.17.0-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.0-beta/images/sha256-58b98f983cd786bcb4d48ea8586144cafd44d58dc3018e26bfbfcf875f495368?context=explore)
|
||||
- Includes 0.26.4-beta Loop
|
||||
- [0.16.4-beta-1](https://hub.docker.com/layers/btcpayserver/lnd/v0.16.4-beta-1/images/sha256-9dd204b62d6c892485b3dd8a76e8f48545ceda5702c9d47329ba4bcbc535a8b4?context=explore)
|
||||
- [0.16.3-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.16.3-beta/images/sha256-9ff34769378cfca18664c7d1da3747e7ad7fb7f38a9a7b82a3d4f85e5bfef7bf?context=explore)
|
||||
- [0.16.2-beta-1](https://hub.docker.com/layers/btcpayserver/lnd/v0.16.2-beta-1/images/sha256-bfff9de84a0a4af9d643ff555125358861b70374976b970cc00d1e7fc44ed520?context=explore)
|
||||
- [0.16.1-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.16.0-beta/images/sha256-f0eb70c20691aaa2ffc34fd5bd6c284299c84e96152cda5e46882a3aa4a3c6a2?context=explore)
|
||||
- [0.16.0-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.16.0-beta/images/sha256-f0eb70c20691aaa2ffc34fd5bd6c284299c84e96152cda5e46882a3aa4a3c6a2?context=explore)
|
||||
- [0.15.4-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.15.4-beta-1/images/sha256-cadbbff93cf36146e24fa4f32170b4b9d278a2e1acfdc50470790a94506ee9c3?context=explore)
|
||||
- [Other versions are tagged](https://github.com/btcpayserver/lnd/tags), but obsoleted and not supported.
|
||||
- All LND versions prior to 0.15.4 contain a consensus bug that prevents them from properly parsing transactions with more than 500,000 witness items per input (https://github.com/btcsuite/btcd/issues/1906)
|
||||
- All LND versions prior to 0.15.2 contain a bug that prevents them from properly parsing Taproot transactions with script size over 11000 bytes (https://github.com/lightningnetwork/lnd/issues/7002)
|
||||
- LND version 0.14.0-beta shipped with check that made it incompatable with c-lightning and eclair (https://github.com/lightningnetwork/lnd/issues/5890)
|
||||
- All LND versions prior to 0.13.3 contain specification-level vulnerability (https://lists.linuxfoundation.org/pipermail/lightning-dev/2021-October/003257.html)
|
||||
- All LND versions prior to 0.7 contain critical vulnerability (https://lists.linuxfoundation.org/pipermail/lightning-dev/2019-September/002174.html)
|
||||
The Lightning Network Daemon (`lnd`) - is a complete implementation of a
|
||||
[Lightning Network](https://lightning.network) node. `lnd` has several pluggable back-end
|
||||
chain services including [`btcd`](https://github.com/btcsuite/btcd) (a
|
||||
full-node), [`bitcoind`](https://github.com/bitcoin/bitcoin), and
|
||||
[`neutrino`](https://github.com/lightninglabs/neutrino) (a new experimental light client). The project's codebase uses the
|
||||
[btcsuite](https://github.com/btcsuite/) set of Bitcoin libraries, and also
|
||||
exports a large set of isolated re-usable Lightning Network related libraries
|
||||
within it. In the current state `lnd` is capable of:
|
||||
* Creating channels.
|
||||
* Closing channels.
|
||||
* Completely managing all channel states (including the exceptional ones!).
|
||||
* Maintaining a fully authenticated+validated channel graph.
|
||||
* Performing path finding within the network, passively forwarding incoming payments.
|
||||
* Sending outgoing [onion-encrypted payments](https://github.com/lightningnetwork/lightning-onion)
|
||||
through the network.
|
||||
* Updating advertised fee schedules.
|
||||
* Automatic channel management ([`autopilot`](https://github.com/lightningnetwork/lnd/tree/master/autopilot)).
|
||||
|
||||
Each version is marked with appropriate `basedon-vX.X.X-beta` tags. We are using `basedon` prefix in order not to conflict with LND tags from source repository.
|
||||
## Lightning Network Specification Compliance
|
||||
`lnd` _fully_ conforms to the [Lightning Network specification
|
||||
(BOLTs)](https://github.com/lightningnetwork/lightning-rfc). BOLT stands for:
|
||||
Basis of Lightning Technology. The specifications are currently being drafted
|
||||
by several groups of implementers based around the world including the
|
||||
developers of `lnd`. The set of specification documents as well as our
|
||||
implementation of the specification are still a work-in-progress. With that
|
||||
said, the current status of `lnd`'s BOLT compliance is:
|
||||
|
||||
## Updating LND version in BTCPay Server
|
||||
- [X] BOLT 1: Base Protocol
|
||||
- [X] BOLT 2: Peer Protocol for Channel Management
|
||||
- [X] BOLT 3: Bitcoin Transaction and Script Formats
|
||||
- [X] BOLT 4: Onion Routing Protocol
|
||||
- [X] BOLT 5: Recommendations for On-chain Transaction Handling
|
||||
- [X] BOLT 7: P2P Node and Channel Discovery
|
||||
- [X] BOLT 8: Encrypted and Authenticated Transport
|
||||
- [X] BOLT 9: Assigned Feature Flags
|
||||
- [X] BOLT 10: DNS Bootstrap and Assisted Node Location
|
||||
- [X] BOLT 11: Invoice Protocol for Lightning Payments
|
||||
|
||||
1. **Update https://github.com/btcpayserver/lnd**
|
||||
## Developer Resources
|
||||
|
||||
a) Go to https://github.com/lightningnetwork/lnd/releases and find the commit on which we should add our resources.
|
||||
b) Checkout a new branch for that commit, usually in the format of `lnd/v0.18.3-beta`.
|
||||
c) Cherry-pick the `Adding BtcPayServer related files and resources` commit. [Example commit](https://github.com/btcpayserver/lnd/commit/ae4bb33c6a3db8b7cc01d18fdf46e600ead9bed4).
|
||||
d) Tag it with the `basedon-v*` prefix name and push it. For v0.18.1, the tag name was `basedon-v0.18.3-beta`.
|
||||
i. Before you push the tag to CircleCI to build and publish image to Docker Hub, you can test if building the image works locally.
|
||||
ii. You can do this for linuxamd64 for example by using command `docker build --pull -t local-lnd:test_version -f linuxamd64.Dockerfile .`
|
||||
e) The build process will start (it [matches on tag format](.circleci/config.yml#L11)). Here is [an example CircleCI build](https://app.circleci.com/pipelines/github/btcpayserver/lnd/202/workflows/b90b5888-c0b8-4207-860e-a63ce21077af).
|
||||
f) The resulting image will be published to Docker Hub. Example [Docker Hub image for v0.18.3](https://hub.docker.com/layers/btcpayserver/lnd/v0.18.3-beta/images/sha256-513ddd55a5af44a14e27110ee14cb28f1c7a69205bcaa2fba4e66275c1f725e5?context=repo).
|
||||
The daemon has been designed to be as developer friendly as possible in order
|
||||
to facilitate application development on top of `lnd`. Two primary RPC
|
||||
interfaces are exported: an HTTP REST API, and a [gRPC](https://grpc.io/)
|
||||
service. The exported APIs are not yet stable, so be warned: they may change
|
||||
drastically in the near future.
|
||||
|
||||
Occasionally, there are problems with:
|
||||
- Versioning of base Docker images used for building Go binaries. You may need to bump that base image - [example commit](https://github.com/btcpayserver/lnd/commit/c841954c515a9d067c24987291316b093b91c2f2).
|
||||
- [Updating Loop](https://github.com/lightninglabs/loop) as part of the package, which needs to happen occasionally. [Example bump of Loop version](https://github.com/btcpayserver/lnd/commit/b3aecc7ac58280ef662e39ba99461573a30fe79a
|
||||
An automatically generated set of documentation for the RPC APIs can be found
|
||||
at [api.lightning.community](https://api.lightning.community). A set of developer
|
||||
resources including guides, articles, example applications and community resources can be found at:
|
||||
[docs.lightning.engineering](https://docs.lightning.engineering).
|
||||
|
||||
3. **Update https://github.com/btcpayserver/BTCPayServer.Lightning**
|
||||
Finally, we also have an active
|
||||
[Slack](https://lightning.engineering/slack.html) where protocol developers, application developers, testers and users gather to
|
||||
discuss various aspects of `lnd` and also Lightning in general.
|
||||
|
||||
Now we need to update the dependency in our Lightning library project. This library has tests, so we will know if something is broken.
|
||||
## Installation
|
||||
In order to build from source, please see [the installation
|
||||
instructions](docs/INSTALL.md).
|
||||
|
||||
a) Modify the `docker-compose.yml` file to reference the new LND version. [Example](https://github.com/btcpayserver/BTCPayServer.Lightning/pull/162/commits/413784ef9b2a8e7aa0496eb91f792ff0086c0ef7).
|
||||
b) Checkout a new branch for that commit, usually in the format of `feat/lnd-0.18.1`.
|
||||
c) Title the commit `Bumping LND to 0.18.1-beta`.
|
||||
d) Open a pull request and reference Docker Hub and Tag. [Example PR](https://github.com/btcpayserver/BTCPayServer.Lightning/pull/162).
|
||||
e) Once tests pass, you can merge it.
|
||||
## Docker
|
||||
To run lnd from Docker, please see the main [Docker instructions](docs/DOCKER.md)
|
||||
|
||||
4. **Update https://github.com/btcpayserver/btcpayserver**
|
||||
## IRC
|
||||
* irc.libera.chat
|
||||
* channel #lnd
|
||||
* [webchat](https://web.libera.chat/#lnd)
|
||||
|
||||
This will give access to LND to the whole dev team and allow for further testing on their dev machines if everything works as expected.
|
||||
## Safety
|
||||
|
||||
a) Modify 4 `docker-compose.yml` files in `BTCPayServer.Tests`. [Example pull request to emulate](https://github.com/btcpayserver/btcpayserver/pull/6795).
|
||||
b) When you open the PR, include the version and link to the BTCPayServer.Lightning PR.
|
||||
c) Once tests pass, you can merge it.
|
||||
When operating a mainnet `lnd` node, please refer to our [operational safety
|
||||
guidelines](docs/safety.md). It is important to note that `lnd` is still
|
||||
**beta** software and that ignoring these operational guidelines can lead to
|
||||
loss of funds.
|
||||
|
||||
5. **Update https://github.com/btcpayserver/btcpayserver-docker**
|
||||
## Security
|
||||
|
||||
a) Now that everything is prepared, open a PR in the btcpayserver-docker repository to allow these changes to propagate to everyone. [Example pull request](https://github.com/btcpayserver/btcpayserver-docker/pull/911).
|
||||
b) Open the PR in DRAFT mode and tag @NicolasDorier and @Pavlenex as reviewers. They typically handle releases, and once they test that the LND version update works on their server, they can ACK the update and merge it as part of the release process.
|
||||
The developers of `lnd` take security _very_ seriously. The disclosure of
|
||||
security vulnerabilities helps us secure the health of `lnd`, privacy of our
|
||||
users, and also the health of the Lightning Network as a whole. If you find
|
||||
any issues regarding security or privacy, please disclose the information
|
||||
responsibly by sending an email to security at lightning dot engineering,
|
||||
preferably encrypted using our designated PGP key
|
||||
(`91FE464CD75101DA6B6BAB60555C6465E5BCB3AF`) which can be found
|
||||
[here](https://gist.githubusercontent.com/Roasbeef/6fb5b52886183239e4aa558f83d085d3/raw/5fa96010af201628bcfa61e9309d9b13d23d220f/security@lightning.engineering).
|
||||
|
||||
## Source repository
|
||||
|
||||
https://github.com/lightningnetwork/lnd
|
||||
|
||||
## Links
|
||||
* [BTCPayServer main repo](https://github.com/btcpayserver/btcpayserver)
|
||||
* [BTCPayServer-Docker repo](https://github.com/btcpayserver/btcpayserver-docker)
|
||||
* [BTCPayServer.Lightning](https://github.com/btcpayserver/BTCPayServer.Lightning)
|
||||
## Further reading
|
||||
* [Step-by-step send payment guide with docker](https://github.com/lightningnetwork/lnd/tree/master/docker)
|
||||
* [Contribution guide](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md)
|
||||
|
||||
@ -5,22 +5,11 @@ import (
|
||||
"fmt"
|
||||
"sync"
|
||||
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"golang.org/x/exp/maps"
|
||||
)
|
||||
|
||||
// UpdateLinkAliases is a function type for a function that locates the active
|
||||
// link that matches the given shortID and triggers an update based on the
|
||||
// latest values of the alias manager.
|
||||
type UpdateLinkAliases func(shortID lnwire.ShortChannelID) error
|
||||
|
||||
// ScidAliasMap is a map from a base short channel ID to a set of alias short
|
||||
// channel IDs.
|
||||
type ScidAliasMap map[lnwire.ShortChannelID][]lnwire.ShortChannelID
|
||||
|
||||
var (
|
||||
// aliasBucket stores aliases as keys and their base SCIDs as values.
|
||||
// This is used to populate the maps that the Manager uses. The keys
|
||||
@ -58,18 +47,17 @@ var (
|
||||
// operations.
|
||||
byteOrder = binary.BigEndian
|
||||
|
||||
// AliasStartBlockHeight is the starting block height of the alias
|
||||
// range.
|
||||
AliasStartBlockHeight uint32 = 16_000_000
|
||||
// startBlockHeight is the starting block height of the alias range.
|
||||
startingBlockHeight = 16_000_000
|
||||
|
||||
// AliasEndBlockHeight is the ending block height of the alias range.
|
||||
AliasEndBlockHeight uint32 = 16_250_000
|
||||
// endBlockHeight is the ending block height of the alias range.
|
||||
endBlockHeight = 16_250_000
|
||||
|
||||
// StartingAlias is the first alias ShortChannelID that will get
|
||||
// assigned by RequestAlias. The starting BlockHeight is chosen so that
|
||||
// legitimate SCIDs in integration tests aren't mistaken for an alias.
|
||||
StartingAlias = lnwire.ShortChannelID{
|
||||
BlockHeight: AliasStartBlockHeight,
|
||||
BlockHeight: uint32(startingBlockHeight),
|
||||
TxIndex: 0,
|
||||
TxPosition: 0,
|
||||
}
|
||||
@ -80,10 +68,6 @@ var (
|
||||
// errNoPeerAlias is returned when the peer's alias for a given
|
||||
// channel is not found.
|
||||
errNoPeerAlias = fmt.Errorf("no peer alias found")
|
||||
|
||||
// ErrAliasNotFound is returned when the alias is not found and can't
|
||||
// be mapped to a base SCID.
|
||||
ErrAliasNotFound = fmt.Errorf("alias not found")
|
||||
)
|
||||
|
||||
// Manager is a struct that handles aliases for LND. It has an underlying
|
||||
@ -93,14 +77,10 @@ var (
|
||||
type Manager struct {
|
||||
backend kvdb.Backend
|
||||
|
||||
// linkAliasUpdater is a function used by the alias manager to
|
||||
// facilitate live update of aliases in other subsystems.
|
||||
linkAliasUpdater UpdateLinkAliases
|
||||
|
||||
// baseToSet is a mapping from the "base" SCID to the set of aliases
|
||||
// for this channel. This mapping includes all channels that
|
||||
// negotiated the option-scid-alias feature bit.
|
||||
baseToSet ScidAliasMap
|
||||
baseToSet map[lnwire.ShortChannelID][]lnwire.ShortChannelID
|
||||
|
||||
// aliasToBase is a mapping that maps all aliases for a given channel
|
||||
// to its base SCID. This is only used for channels that have
|
||||
@ -118,15 +98,9 @@ type Manager struct {
|
||||
}
|
||||
|
||||
// NewManager initializes an alias Manager from the passed database backend.
|
||||
func NewManager(db kvdb.Backend, linkAliasUpdater UpdateLinkAliases) (*Manager,
|
||||
error) {
|
||||
|
||||
m := &Manager{
|
||||
backend: db,
|
||||
baseToSet: make(ScidAliasMap),
|
||||
linkAliasUpdater: linkAliasUpdater,
|
||||
}
|
||||
|
||||
func NewManager(db kvdb.Backend) (*Manager, error) {
|
||||
m := &Manager{backend: db}
|
||||
m.baseToSet = make(map[lnwire.ShortChannelID][]lnwire.ShortChannelID)
|
||||
m.aliasToBase = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID)
|
||||
m.peerAlias = make(map[lnwire.ChannelID]lnwire.ShortChannelID)
|
||||
|
||||
@ -241,22 +215,12 @@ func (m *Manager) populateMaps() error {
|
||||
// AddLocalAlias adds a database mapping from the passed alias to the passed
|
||||
// base SCID. The gossip boolean marks whether or not to create a mapping
|
||||
// that the gossiper will use. It is set to false for the upgrade path where
|
||||
// the feature-bit is toggled on and there are existing channels. The linkUpdate
|
||||
// flag is used to signal whether this function should also trigger an update
|
||||
// on the htlcswitch scid alias maps.
|
||||
// the feature-bit is toggled on and there are existing channels.
|
||||
func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID,
|
||||
gossip, linkUpdate bool) error {
|
||||
gossip bool) error {
|
||||
|
||||
// We need to lock the manager for the whole duration of this method,
|
||||
// except for the very last part where we call the link updater. In
|
||||
// order for us to safely use a defer _and_ still be able to manually
|
||||
// unlock, we use a sync.Once.
|
||||
m.Lock()
|
||||
unlockOnce := sync.Once{}
|
||||
unlock := func() {
|
||||
unlockOnce.Do(m.Unlock)
|
||||
}
|
||||
defer unlock()
|
||||
defer m.Unlock()
|
||||
|
||||
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
|
||||
// If the caller does not want to allow the alias to be used
|
||||
@ -306,18 +270,6 @@ func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID,
|
||||
m.aliasToBase[alias] = baseScid
|
||||
}
|
||||
|
||||
// We definitely need to unlock the Manager before calling the link
|
||||
// updater. If we don't, we'll deadlock. We use a sync.Once to ensure
|
||||
// that we only unlock once.
|
||||
unlock()
|
||||
|
||||
// Finally, we trigger a htlcswitch update if the flag is set, in order
|
||||
// for any future htlc that references the added alias to be properly
|
||||
// routed.
|
||||
if linkUpdate {
|
||||
return m.linkAliasUpdater(baseScid)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -388,74 +340,6 @@ func (m *Manager) DeleteSixConfs(baseScid lnwire.ShortChannelID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// DeleteLocalAlias removes a mapping from the database and the Manager's maps.
|
||||
func (m *Manager) DeleteLocalAlias(alias,
|
||||
baseScid lnwire.ShortChannelID) error {
|
||||
|
||||
// We need to lock the manager for the whole duration of this method,
|
||||
// except for the very last part where we call the link updater. In
|
||||
// order for us to safely use a defer _and_ still be able to manually
|
||||
// unlock, we use a sync.Once.
|
||||
m.Lock()
|
||||
unlockOnce := sync.Once{}
|
||||
unlock := func() {
|
||||
unlockOnce.Do(m.Unlock)
|
||||
}
|
||||
defer unlock()
|
||||
|
||||
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
|
||||
aliasToBaseBucket, err := tx.CreateTopLevelBucket(aliasBucket)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var aliasBytes [8]byte
|
||||
byteOrder.PutUint64(aliasBytes[:], alias.ToUint64())
|
||||
|
||||
// If the user attempts to delete an alias that doesn't exist,
|
||||
// we'll want to inform them about it and not just do nothing.
|
||||
if aliasToBaseBucket.Get(aliasBytes[:]) == nil {
|
||||
return ErrAliasNotFound
|
||||
}
|
||||
|
||||
return aliasToBaseBucket.Delete(aliasBytes[:])
|
||||
}, func() {})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Now that the database state has been updated, we'll delete the
|
||||
// mapping from the Manager's maps.
|
||||
aliasSet, ok := m.baseToSet[baseScid]
|
||||
if !ok {
|
||||
return ErrAliasNotFound
|
||||
}
|
||||
|
||||
// We'll filter the alias set and remove the alias from it.
|
||||
aliasSet = fn.Filter(func(a lnwire.ShortChannelID) bool {
|
||||
return a.ToUint64() != alias.ToUint64()
|
||||
}, aliasSet)
|
||||
|
||||
// If the alias set is empty, we'll delete the base SCID from the
|
||||
// baseToSet map.
|
||||
if len(aliasSet) == 0 {
|
||||
delete(m.baseToSet, baseScid)
|
||||
} else {
|
||||
m.baseToSet[baseScid] = aliasSet
|
||||
}
|
||||
|
||||
// Finally, we'll delete the aliasToBase mapping from the Manager's
|
||||
// cache (but this is only set if we gossip the alias).
|
||||
delete(m.aliasToBase, alias)
|
||||
|
||||
// We definitely need to unlock the Manager before calling the link
|
||||
// updater. If we don't, we'll deadlock. We use a sync.Once to ensure
|
||||
// that we only unlock once.
|
||||
unlock()
|
||||
|
||||
return m.linkAliasUpdater(baseScid)
|
||||
}
|
||||
|
||||
// PutPeerAlias stores the peer's alias SCID once we learn of it in the
|
||||
// channel_ready message.
|
||||
func (m *Manager) PutPeerAlias(chanID lnwire.ChannelID,
|
||||
@ -508,19 +392,6 @@ func (m *Manager) GetPeerAlias(chanID lnwire.ChannelID) (lnwire.ShortChannelID,
|
||||
func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
|
||||
var nextAlias lnwire.ShortChannelID
|
||||
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
// haveAlias returns true if the passed alias is already assigned to a
|
||||
// channel in the baseToSet map.
|
||||
haveAlias := func(maybeNextAlias lnwire.ShortChannelID) bool {
|
||||
return fn.Any(func(aliasList []lnwire.ShortChannelID) bool {
|
||||
return fn.Any(func(alias lnwire.ShortChannelID) bool {
|
||||
return alias == maybeNextAlias
|
||||
}, aliasList)
|
||||
}, maps.Values(m.baseToSet))
|
||||
}
|
||||
|
||||
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
|
||||
bucket, err := tx.CreateTopLevelBucket(aliasAllocBucket)
|
||||
if err != nil {
|
||||
@ -533,29 +404,6 @@ func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
|
||||
// StartingAlias to it.
|
||||
nextAlias = StartingAlias
|
||||
|
||||
// If the very first alias is already assigned, we'll
|
||||
// keep incrementing until we find an unassigned alias.
|
||||
// This is to avoid collision with custom added SCID
|
||||
// aliases that fall into the same range as the ones we
|
||||
// generate here monotonically. Those custom SCIDs are
|
||||
// stored in a different bucket, but we can just check
|
||||
// the in-memory map for simplicity.
|
||||
for {
|
||||
if !haveAlias(nextAlias) {
|
||||
break
|
||||
}
|
||||
|
||||
nextAlias = getNextScid(nextAlias)
|
||||
|
||||
// Abort if we've reached the end of the range.
|
||||
if nextAlias.BlockHeight >=
|
||||
AliasEndBlockHeight {
|
||||
|
||||
return fmt.Errorf("range for custom " +
|
||||
"aliases exhausted")
|
||||
}
|
||||
}
|
||||
|
||||
var scratch [8]byte
|
||||
byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())
|
||||
return bucket.Put(lastAliasKey, scratch[:])
|
||||
@ -570,26 +418,6 @@ func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
|
||||
)
|
||||
nextAlias = getNextScid(lastScid)
|
||||
|
||||
// If the next alias is already assigned, we'll keep
|
||||
// incrementing until we find an unassigned alias. This is to
|
||||
// avoid collision with custom added SCID aliases that fall into
|
||||
// the same range as the ones we generate here monotonically.
|
||||
// Those custom SCIDs are stored in a different bucket, but we
|
||||
// can just check the in-memory map for simplicity.
|
||||
for {
|
||||
if !haveAlias(nextAlias) {
|
||||
break
|
||||
}
|
||||
|
||||
nextAlias = getNextScid(nextAlias)
|
||||
|
||||
// Abort if we've reached the end of the range.
|
||||
if nextAlias.BlockHeight >= AliasEndBlockHeight {
|
||||
return fmt.Errorf("range for custom " +
|
||||
"aliases exhausted")
|
||||
}
|
||||
}
|
||||
|
||||
var scratch [8]byte
|
||||
byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())
|
||||
return bucket.Put(lastAliasKey, scratch[:])
|
||||
@ -605,11 +433,11 @@ func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
|
||||
|
||||
// ListAliases returns a carbon copy of baseToSet. This is used by the rpc
|
||||
// layer.
|
||||
func (m *Manager) ListAliases() ScidAliasMap {
|
||||
func (m *Manager) ListAliases() map[lnwire.ShortChannelID][]lnwire.ShortChannelID {
|
||||
m.RLock()
|
||||
defer m.RUnlock()
|
||||
|
||||
baseCopy := make(ScidAliasMap)
|
||||
baseCopy := make(map[lnwire.ShortChannelID][]lnwire.ShortChannelID)
|
||||
|
||||
for k, v := range m.baseToSet {
|
||||
setCopy := make([]lnwire.ShortChannelID, len(v))
|
||||
@ -668,10 +496,10 @@ func getNextScid(last lnwire.ShortChannelID) lnwire.ShortChannelID {
|
||||
|
||||
// IsAlias returns true if the passed SCID is an alias. The function determines
|
||||
// this by looking at the BlockHeight. If the BlockHeight is greater than
|
||||
// AliasStartBlockHeight and less than AliasEndBlockHeight, then it is an alias
|
||||
// startingBlockHeight and less than endBlockHeight, then it is an alias
|
||||
// assigned by RequestAlias. These bounds only apply to aliases we generate.
|
||||
// Our peers are free to use any range they choose.
|
||||
func IsAlias(scid lnwire.ShortChannelID) bool {
|
||||
return scid.BlockHeight >= AliasStartBlockHeight &&
|
||||
scid.BlockHeight < AliasEndBlockHeight
|
||||
return scid.BlockHeight >= uint32(startingBlockHeight) &&
|
||||
scid.BlockHeight < uint32(endBlockHeight)
|
||||
}
|
||||
|
||||
@ -23,11 +23,7 @@ func TestAliasStorePeerAlias(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
linkUpdater := func(shortID lnwire.ShortChannelID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
aliasStore, err := NewManager(db, linkUpdater)
|
||||
aliasStore, err := NewManager(db)
|
||||
require.NoError(t, err)
|
||||
|
||||
var chanID1 [32]byte
|
||||
@ -56,11 +52,7 @@ func TestAliasStoreRequest(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
linkUpdater := func(shortID lnwire.ShortChannelID) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
aliasStore, err := NewManager(db, linkUpdater)
|
||||
aliasStore, err := NewManager(db)
|
||||
require.NoError(t, err)
|
||||
|
||||
// We'll assert that the very first alias we receive is StartingAlias.
|
||||
@ -76,118 +68,6 @@ func TestAliasStoreRequest(t *testing.T) {
|
||||
require.Equal(t, nextAlias, alias2)
|
||||
}
|
||||
|
||||
// TestAliasLifecycle tests that the aliases can be created and deleted.
|
||||
func TestAliasLifecycle(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Create the backend database and use this to create the aliasStore.
|
||||
dbPath := filepath.Join(t.TempDir(), "testdb")
|
||||
db, err := kvdb.Create(
|
||||
kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
|
||||
)
|
||||
require.NoError(t, err)
|
||||
defer db.Close()
|
||||
|
||||
updateChan := make(chan struct{}, 1)
|
||||
|
||||
linkUpdater := func(shortID lnwire.ShortChannelID) error {
|
||||
updateChan <- struct{}{}
|
||||
return nil
|
||||
}
|
||||
|
||||
aliasStore, err := NewManager(db, linkUpdater)
|
||||
require.NoError(t, err)
|
||||
|
||||
const (
|
||||
base = uint64(123123123)
|
||||
alias = uint64(456456456)
|
||||
)
|
||||
|
||||
// Parse the aliases and base to short channel ID format.
|
||||
baseScid := lnwire.NewShortChanIDFromInt(base)
|
||||
aliasScid := lnwire.NewShortChanIDFromInt(alias)
|
||||
aliasScid2 := lnwire.NewShortChanIDFromInt(alias + 1)
|
||||
|
||||
// Add the first alias.
|
||||
err = aliasStore.AddLocalAlias(aliasScid, baseScid, false, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The link updater should be called.
|
||||
<-updateChan
|
||||
|
||||
// Query the aliases and verify the results.
|
||||
aliasList := aliasStore.GetAliases(baseScid)
|
||||
require.Len(t, aliasList, 1)
|
||||
require.Contains(t, aliasList, aliasScid)
|
||||
|
||||
// Add the second alias.
|
||||
err = aliasStore.AddLocalAlias(aliasScid2, baseScid, false, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The link updater should be called.
|
||||
<-updateChan
|
||||
|
||||
// Query the aliases and verify the results.
|
||||
aliasList = aliasStore.GetAliases(baseScid)
|
||||
require.Len(t, aliasList, 2)
|
||||
require.Contains(t, aliasList, aliasScid)
|
||||
require.Contains(t, aliasList, aliasScid2)
|
||||
|
||||
// Delete the first alias.
|
||||
err = aliasStore.DeleteLocalAlias(aliasScid, baseScid)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The link updater should be called.
|
||||
<-updateChan
|
||||
|
||||
// We expect to get an error if we attempt to delete the same alias
|
||||
// again.
|
||||
err = aliasStore.DeleteLocalAlias(aliasScid, baseScid)
|
||||
require.ErrorIs(t, err, ErrAliasNotFound)
|
||||
|
||||
// The link updater should _not_ be called.
|
||||
select {
|
||||
case <-updateChan:
|
||||
t.Fatal("link alias updater should not have been called")
|
||||
default:
|
||||
}
|
||||
|
||||
// Query the aliases and verify that first one doesn't exist anymore.
|
||||
aliasList = aliasStore.GetAliases(baseScid)
|
||||
require.Len(t, aliasList, 1)
|
||||
require.Contains(t, aliasList, aliasScid2)
|
||||
require.NotContains(t, aliasList, aliasScid)
|
||||
|
||||
// Delete the second alias.
|
||||
err = aliasStore.DeleteLocalAlias(aliasScid2, baseScid)
|
||||
require.NoError(t, err)
|
||||
|
||||
// The link updater should be called.
|
||||
<-updateChan
|
||||
|
||||
// Query the aliases and verify that none exists.
|
||||
aliasList = aliasStore.GetAliases(baseScid)
|
||||
require.Len(t, aliasList, 0)
|
||||
|
||||
// We now request an alias generated by the aliasStore. This should give
|
||||
// the first from the pre-defined list of allocated aliases.
|
||||
firstRequested, err := aliasStore.RequestAlias()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, StartingAlias, firstRequested)
|
||||
|
||||
// We now manually add the next alias from the range as a custom alias.
|
||||
secondAlias := getNextScid(firstRequested)
|
||||
err = aliasStore.AddLocalAlias(secondAlias, baseScid, false, true)
|
||||
require.NoError(t, err)
|
||||
|
||||
// When we now request another alias from the allocation list, we expect
|
||||
// the third one (tx position 2) to be returned.
|
||||
thirdRequested, err := aliasStore.RequestAlias()
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, getNextScid(secondAlias), thirdRequested)
|
||||
require.EqualValues(t, 2, thirdRequested.TxPosition)
|
||||
}
|
||||
|
||||
// TestGetNextScid tests that given a current lnwire.ShortChannelID,
|
||||
// getNextScid returns the expected alias to use next.
|
||||
func TestGetNextScid(t *testing.T) {
|
||||
@ -200,7 +80,7 @@ func TestGetNextScid(t *testing.T) {
|
||||
name: "starting alias",
|
||||
current: StartingAlias,
|
||||
expected: lnwire.ShortChannelID{
|
||||
BlockHeight: AliasStartBlockHeight,
|
||||
BlockHeight: uint32(startingBlockHeight),
|
||||
TxIndex: 0,
|
||||
TxPosition: 1,
|
||||
},
|
||||
|
||||
@ -43,7 +43,7 @@ const (
|
||||
AppMinor uint = 18
|
||||
|
||||
// AppPatch defines the application patch for this binary.
|
||||
AppPatch uint = 4
|
||||
AppPatch uint = 3
|
||||
|
||||
// AppPreRelease MUST only contain characters from semanticAlphabet per
|
||||
// the semantic versioning spec.
|
||||
|
||||
@ -24,7 +24,6 @@ import (
|
||||
"github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
@ -64,14 +63,6 @@ type Config struct {
|
||||
// state.
|
||||
ChanStateDB *channeldb.ChannelStateDB
|
||||
|
||||
// AuxLeafStore is an optional store that can be used to store auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||
|
||||
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||
|
||||
// BlockCache is the main cache for storing block information.
|
||||
BlockCache *blockcache.BlockCache
|
||||
|
||||
|
||||
@ -356,30 +356,6 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
|
||||
):
|
||||
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT
|
||||
|
||||
case channelFeatures.OnlyContains(
|
||||
lnwire.SimpleTaprootOverlayChansRequired,
|
||||
lnwire.ZeroConfRequired,
|
||||
lnwire.ScidAliasRequired,
|
||||
):
|
||||
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
|
||||
|
||||
case channelFeatures.OnlyContains(
|
||||
lnwire.SimpleTaprootOverlayChansRequired,
|
||||
lnwire.ZeroConfRequired,
|
||||
):
|
||||
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
|
||||
|
||||
case channelFeatures.OnlyContains(
|
||||
lnwire.SimpleTaprootOverlayChansRequired,
|
||||
lnwire.ScidAliasRequired,
|
||||
):
|
||||
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
|
||||
|
||||
case channelFeatures.OnlyContains(
|
||||
lnwire.SimpleTaprootOverlayChansRequired,
|
||||
):
|
||||
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
|
||||
|
||||
case channelFeatures.OnlyContains(
|
||||
lnwire.StaticRemoteKeyRequired,
|
||||
):
|
||||
|
||||
@ -226,109 +226,28 @@ const (
|
||||
// A tlv type definition used to serialize an outpoint's indexStatus
|
||||
// for use in the outpoint index.
|
||||
indexStatusType tlv.Type = 0
|
||||
|
||||
// A tlv type definition used to serialize and deserialize a KeyLocator
|
||||
// from the database.
|
||||
keyLocType tlv.Type = 1
|
||||
|
||||
// A tlv type used to serialize and deserialize the
|
||||
// `InitialLocalBalance` field.
|
||||
initialLocalBalanceType tlv.Type = 2
|
||||
|
||||
// A tlv type used to serialize and deserialize the
|
||||
// `InitialRemoteBalance` field.
|
||||
initialRemoteBalanceType tlv.Type = 3
|
||||
|
||||
// A tlv type definition used to serialize and deserialize the
|
||||
// confirmed ShortChannelID for a zero-conf channel.
|
||||
realScidType tlv.Type = 4
|
||||
|
||||
// A tlv type definition used to serialize and deserialize the
|
||||
// Memo for the channel channel.
|
||||
channelMemoType tlv.Type = 5
|
||||
)
|
||||
|
||||
// openChannelTlvData houses the new data fields that are stored for each
|
||||
// channel in a TLV stream within the root bucket. This is stored as a TLV
|
||||
// stream appended to the existing hard-coded fields in the channel's root
|
||||
// bucket. New fields being added to the channel state should be added here.
|
||||
//
|
||||
// NOTE: This struct is used for serialization purposes only and its fields
|
||||
// should be accessed via the OpenChannel struct while in memory.
|
||||
type openChannelTlvData struct {
|
||||
// revokeKeyLoc is the key locator for the revocation key.
|
||||
revokeKeyLoc tlv.RecordT[tlv.TlvType1, keyLocRecord]
|
||||
|
||||
// initialLocalBalance is the initial local balance of the channel.
|
||||
initialLocalBalance tlv.RecordT[tlv.TlvType2, uint64]
|
||||
|
||||
// initialRemoteBalance is the initial remote balance of the channel.
|
||||
initialRemoteBalance tlv.RecordT[tlv.TlvType3, uint64]
|
||||
|
||||
// realScid is the real short channel ID of the channel corresponding to
|
||||
// the on-chain outpoint.
|
||||
realScid tlv.RecordT[tlv.TlvType4, lnwire.ShortChannelID]
|
||||
|
||||
// memo is an optional text field that gives context to the user about
|
||||
// the channel.
|
||||
memo tlv.OptionalRecordT[tlv.TlvType5, []byte]
|
||||
|
||||
// tapscriptRoot is the optional Tapscript root the channel funding
|
||||
// output commits to.
|
||||
tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte]
|
||||
|
||||
// customBlob is an optional TLV encoded blob of data representing
|
||||
// custom channel funding information.
|
||||
customBlob tlv.OptionalRecordT[tlv.TlvType7, tlv.Blob]
|
||||
}
|
||||
|
||||
// encode serializes the openChannelTlvData to the given io.Writer.
|
||||
func (c *openChannelTlvData) encode(w io.Writer) error {
|
||||
tlvRecords := []tlv.Record{
|
||||
c.revokeKeyLoc.Record(),
|
||||
c.initialLocalBalance.Record(),
|
||||
c.initialRemoteBalance.Record(),
|
||||
c.realScid.Record(),
|
||||
}
|
||||
c.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) {
|
||||
tlvRecords = append(tlvRecords, memo.Record())
|
||||
})
|
||||
c.tapscriptRoot.WhenSome(
|
||||
func(root tlv.RecordT[tlv.TlvType6, [32]byte]) {
|
||||
tlvRecords = append(tlvRecords, root.Record())
|
||||
},
|
||||
)
|
||||
c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType7, tlv.Blob]) {
|
||||
tlvRecords = append(tlvRecords, blob.Record())
|
||||
})
|
||||
|
||||
// Create the tlv stream.
|
||||
tlvStream, err := tlv.NewStream(tlvRecords...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tlvStream.Encode(w)
|
||||
}
|
||||
|
||||
// decode deserializes the openChannelTlvData from the given io.Reader.
|
||||
func (c *openChannelTlvData) decode(r io.Reader) error {
|
||||
memo := c.memo.Zero()
|
||||
tapscriptRoot := c.tapscriptRoot.Zero()
|
||||
blob := c.customBlob.Zero()
|
||||
|
||||
// Create the tlv stream.
|
||||
tlvStream, err := tlv.NewStream(
|
||||
c.revokeKeyLoc.Record(),
|
||||
c.initialLocalBalance.Record(),
|
||||
c.initialRemoteBalance.Record(),
|
||||
c.realScid.Record(),
|
||||
memo.Record(),
|
||||
tapscriptRoot.Record(),
|
||||
blob.Record(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tlvs, err := tlvStream.DecodeWithParsedTypes(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := tlvs[memo.TlvType()]; ok {
|
||||
c.memo = tlv.SomeRecordT(memo)
|
||||
}
|
||||
if _, ok := tlvs[tapscriptRoot.TlvType()]; ok {
|
||||
c.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot)
|
||||
}
|
||||
if _, ok := tlvs[c.customBlob.TlvType()]; ok {
|
||||
c.customBlob = tlv.SomeRecordT(blob)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// indexStatus is an enum-like type that describes what state the
|
||||
// outpoint is in. Currently only two possible values.
|
||||
type indexStatus uint8
|
||||
@ -406,11 +325,6 @@ const (
|
||||
// SimpleTaprootFeatureBit indicates that the simple-taproot-chans
|
||||
// feature bit was negotiated during the lifetime of the channel.
|
||||
SimpleTaprootFeatureBit ChannelType = 1 << 10
|
||||
|
||||
// TapscriptRootBit indicates that this is a MuSig2 channel with a top
|
||||
// level tapscript commitment. This MUST be set along with the
|
||||
// SimpleTaprootFeatureBit.
|
||||
TapscriptRootBit ChannelType = 1 << 11
|
||||
)
|
||||
|
||||
// IsSingleFunder returns true if the channel type if one of the known single
|
||||
@ -481,12 +395,6 @@ func (c ChannelType) IsTaproot() bool {
|
||||
return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit
|
||||
}
|
||||
|
||||
// HasTapscriptRoot returns true if the channel is using a top level tapscript
|
||||
// root commitment.
|
||||
func (c ChannelType) HasTapscriptRoot() bool {
|
||||
return c&TapscriptRootBit == TapscriptRootBit
|
||||
}
|
||||
|
||||
// ChannelStateBounds are the parameters from OpenChannel and AcceptChannel
|
||||
// that are responsible for providing bounds on the state space of the abstract
|
||||
// channel state. These values must be remembered for normal channel operation
|
||||
@ -588,53 +496,6 @@ type ChannelConfig struct {
|
||||
HtlcBasePoint keychain.KeyDescriptor
|
||||
}
|
||||
|
||||
// commitTlvData stores all the optional data that may be stored as a TLV stream
|
||||
// at the _end_ of the normal serialized commit on disk.
|
||||
type commitTlvData struct {
|
||||
// customBlob is a custom blob that may store extra data for custom
|
||||
// channels.
|
||||
customBlob tlv.OptionalRecordT[tlv.TlvType1, tlv.Blob]
|
||||
}
|
||||
|
||||
// encode encodes the aux data into the passed io.Writer.
|
||||
func (c *commitTlvData) encode(w io.Writer) error {
|
||||
var tlvRecords []tlv.Record
|
||||
c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType1, tlv.Blob]) {
|
||||
tlvRecords = append(tlvRecords, blob.Record())
|
||||
})
|
||||
|
||||
// Create the tlv stream.
|
||||
tlvStream, err := tlv.NewStream(tlvRecords...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return tlvStream.Encode(w)
|
||||
}
|
||||
|
||||
// decode attempts to decode the aux data from the passed io.Reader.
|
||||
func (c *commitTlvData) decode(r io.Reader) error {
|
||||
blob := c.customBlob.Zero()
|
||||
|
||||
tlvStream, err := tlv.NewStream(
|
||||
blob.Record(),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tlvs, err := tlvStream.DecodeWithParsedTypes(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, ok := tlvs[c.customBlob.TlvType()]; ok {
|
||||
c.customBlob = tlv.SomeRecordT(blob)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChannelCommitment is a snapshot of the commitment state at a particular
|
||||
// point in the commitment chain. With each state transition, a snapshot of the
|
||||
// current state along with all non-settled HTLCs are recorded. These snapshots
|
||||
@ -701,11 +562,6 @@ type ChannelCommitment struct {
|
||||
// able by us.
|
||||
CommitTx *wire.MsgTx
|
||||
|
||||
// CustomBlob is an optional blob that can be used to store information
|
||||
// specific to a custom channel type. This may track some custom
|
||||
// specific state for this given commitment.
|
||||
CustomBlob fn.Option[tlv.Blob]
|
||||
|
||||
// CommitSig is one half of the signature required to fully complete
|
||||
// the script for the commitment transaction above. This is the
|
||||
// signature signed by the remote party for our version of the
|
||||
@ -715,26 +571,9 @@ type ChannelCommitment struct {
|
||||
// Htlcs is the set of HTLC's that are pending at this particular
|
||||
// commitment height.
|
||||
Htlcs []HTLC
|
||||
}
|
||||
|
||||
// amendTlvData updates the channel with the given auxiliary TLV data.
|
||||
func (c *ChannelCommitment) amendTlvData(auxData commitTlvData) {
|
||||
auxData.customBlob.WhenSomeV(func(blob tlv.Blob) {
|
||||
c.CustomBlob = fn.Some(blob)
|
||||
})
|
||||
}
|
||||
|
||||
// extractTlvData creates a new commitTlvData from the given commitment.
|
||||
func (c *ChannelCommitment) extractTlvData() commitTlvData {
|
||||
var auxData commitTlvData
|
||||
|
||||
c.CustomBlob.WhenSome(func(blob tlv.Blob) {
|
||||
auxData.customBlob = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType1](blob),
|
||||
)
|
||||
})
|
||||
|
||||
return auxData
|
||||
// TODO(roasbeef): pending commit pointer?
|
||||
// * lets just walk through
|
||||
}
|
||||
|
||||
// ChannelStatus is a bit vector used to indicate whether an OpenChannel is in
|
||||
@ -1028,16 +867,6 @@ type OpenChannel struct {
|
||||
// channel that will be useful to our future selves.
|
||||
Memo []byte
|
||||
|
||||
// TapscriptRoot is an optional tapscript root used to derive the MuSig2
|
||||
// funding output.
|
||||
TapscriptRoot fn.Option[chainhash.Hash]
|
||||
|
||||
// CustomBlob is an optional blob that can be used to store information
|
||||
// specific to a custom channel type. This information is only created
|
||||
// at channel funding time, and after wards is to be considered
|
||||
// immutable.
|
||||
CustomBlob fn.Option[tlv.Blob]
|
||||
|
||||
// TODO(roasbeef): eww
|
||||
Db *ChannelStateDB
|
||||
|
||||
@ -1196,64 +1025,6 @@ func (c *OpenChannel) SetBroadcastHeight(height uint32) {
|
||||
c.FundingBroadcastHeight = height
|
||||
}
|
||||
|
||||
// amendTlvData updates the channel with the given auxiliary TLV data.
|
||||
func (c *OpenChannel) amendTlvData(auxData openChannelTlvData) {
|
||||
c.RevocationKeyLocator = auxData.revokeKeyLoc.Val.KeyLocator
|
||||
c.InitialLocalBalance = lnwire.MilliSatoshi(
|
||||
auxData.initialLocalBalance.Val,
|
||||
)
|
||||
c.InitialRemoteBalance = lnwire.MilliSatoshi(
|
||||
auxData.initialRemoteBalance.Val,
|
||||
)
|
||||
c.confirmedScid = auxData.realScid.Val
|
||||
|
||||
auxData.memo.WhenSomeV(func(memo []byte) {
|
||||
c.Memo = memo
|
||||
})
|
||||
auxData.tapscriptRoot.WhenSomeV(func(h [32]byte) {
|
||||
c.TapscriptRoot = fn.Some[chainhash.Hash](h)
|
||||
})
|
||||
auxData.customBlob.WhenSomeV(func(blob tlv.Blob) {
|
||||
c.CustomBlob = fn.Some(blob)
|
||||
})
|
||||
}
|
||||
|
||||
// extractTlvData creates a new openChannelTlvData from the given channel.
|
||||
func (c *OpenChannel) extractTlvData() openChannelTlvData {
|
||||
auxData := openChannelTlvData{
|
||||
revokeKeyLoc: tlv.NewRecordT[tlv.TlvType1](
|
||||
keyLocRecord{c.RevocationKeyLocator},
|
||||
),
|
||||
initialLocalBalance: tlv.NewPrimitiveRecord[tlv.TlvType2](
|
||||
uint64(c.InitialLocalBalance),
|
||||
),
|
||||
initialRemoteBalance: tlv.NewPrimitiveRecord[tlv.TlvType3](
|
||||
uint64(c.InitialRemoteBalance),
|
||||
),
|
||||
realScid: tlv.NewRecordT[tlv.TlvType4](
|
||||
c.confirmedScid,
|
||||
),
|
||||
}
|
||||
|
||||
if len(c.Memo) != 0 {
|
||||
auxData.memo = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType5](c.Memo),
|
||||
)
|
||||
}
|
||||
c.TapscriptRoot.WhenSome(func(h chainhash.Hash) {
|
||||
auxData.tapscriptRoot = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType6, [32]byte](h),
|
||||
)
|
||||
})
|
||||
c.CustomBlob.WhenSome(func(blob tlv.Blob) {
|
||||
auxData.customBlob = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType7](blob),
|
||||
)
|
||||
})
|
||||
|
||||
return auxData
|
||||
}
|
||||
|
||||
// Refresh updates the in-memory channel state using the latest state observed
|
||||
// on disk.
|
||||
func (c *OpenChannel) Refresh() error {
|
||||
@ -2580,12 +2351,6 @@ type HTLC struct {
|
||||
// HTLC. It is stored in the ExtraData field, which is used to store
|
||||
// a TLV stream of additional information associated with the HTLC.
|
||||
BlindingPoint lnwire.BlindingPointRecord
|
||||
|
||||
// CustomRecords is a set of custom TLV records that are associated with
|
||||
// this HTLC. These records are used to store additional information
|
||||
// about the HTLC that is not part of the standard HTLC fields. This
|
||||
// field is encoded within the ExtraData field.
|
||||
CustomRecords lnwire.CustomRecords
|
||||
}
|
||||
|
||||
// serializeExtraData encodes a TLV stream of extra data to be stored with a
|
||||
@ -2604,11 +2369,6 @@ func (h *HTLC) serializeExtraData() error {
|
||||
records = append(records, &b)
|
||||
})
|
||||
|
||||
records, err := h.CustomRecords.ExtendRecordProducers(records)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return h.ExtraData.PackRecords(records...)
|
||||
}
|
||||
|
||||
@ -2630,19 +2390,8 @@ func (h *HTLC) deserializeExtraData() error {
|
||||
|
||||
if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil {
|
||||
h.BlindingPoint = tlv.SomeRecordT(blindingPoint)
|
||||
|
||||
// Remove the entry from the TLV map. Anything left in the map
|
||||
// will be included in the custom records field.
|
||||
delete(tlvMap, h.BlindingPoint.TlvType())
|
||||
}
|
||||
|
||||
// Set the custom records field to the remaining TLV records.
|
||||
customRecords, err := lnwire.NewCustomRecords(tlvMap)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
h.CustomRecords = customRecords
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -2780,8 +2529,6 @@ func (h *HTLC) Copy() HTLC {
|
||||
copy(clone.Signature[:], h.Signature)
|
||||
copy(clone.RHash[:], h.RHash[:])
|
||||
copy(clone.ExtraData, h.ExtraData)
|
||||
clone.BlindingPoint = h.BlindingPoint
|
||||
clone.CustomRecords = h.CustomRecords.Copy()
|
||||
|
||||
return clone
|
||||
}
|
||||
@ -2943,14 +2690,6 @@ func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { // nolint: dupl
|
||||
}
|
||||
}
|
||||
|
||||
// We'll also encode the commit aux data stream here. We do this here
|
||||
// rather than above (at the call to serializeChanCommit), to ensure
|
||||
// backwards compat for reads to existing non-custom channels.
|
||||
auxData := diff.Commitment.extractTlvData()
|
||||
if err := auxData.encode(w); err != nil {
|
||||
return fmt.Errorf("unable to write aux data: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -3011,17 +2750,6 @@ func deserializeCommitDiff(r io.Reader) (*CommitDiff, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// As a final step, we'll read out any aux commit data that we have at
|
||||
// the end of this byte stream. We do this here to ensure backward
|
||||
// compatibility, as otherwise we risk erroneously reading into the
|
||||
// wrong field.
|
||||
var auxData commitTlvData
|
||||
if err := auxData.decode(r); err != nil {
|
||||
return nil, fmt.Errorf("unable to decode aux data: %w", err)
|
||||
}
|
||||
|
||||
d.Commitment.amendTlvData(auxData)
|
||||
|
||||
return &d, nil
|
||||
}
|
||||
|
||||
@ -4000,13 +3728,6 @@ func (c *OpenChannel) Snapshot() *ChannelSnapshot {
|
||||
},
|
||||
}
|
||||
|
||||
localCommit.CustomBlob.WhenSome(func(blob tlv.Blob) {
|
||||
blobCopy := make([]byte, len(blob))
|
||||
copy(blobCopy, blob)
|
||||
|
||||
snapshot.ChannelCommitment.CustomBlob = fn.Some(blobCopy)
|
||||
})
|
||||
|
||||
// Copy over the current set of HTLCs to ensure the caller can't mutate
|
||||
// our internal state.
|
||||
snapshot.Htlcs = make([]HTLC, len(localCommit.Htlcs))
|
||||
@ -4309,9 +4030,32 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error {
|
||||
return err
|
||||
}
|
||||
|
||||
auxData := channel.extractTlvData()
|
||||
if err := auxData.encode(&w); err != nil {
|
||||
return fmt.Errorf("unable to encode aux data: %w", err)
|
||||
// Convert balance fields into uint64.
|
||||
localBalance := uint64(channel.InitialLocalBalance)
|
||||
remoteBalance := uint64(channel.InitialRemoteBalance)
|
||||
|
||||
// Create the tlv stream.
|
||||
tlvStream, err := tlv.NewStream(
|
||||
// Write the RevocationKeyLocator as the first entry in a tlv
|
||||
// stream.
|
||||
MakeKeyLocRecord(
|
||||
keyLocType, &channel.RevocationKeyLocator,
|
||||
),
|
||||
tlv.MakePrimitiveRecord(
|
||||
initialLocalBalanceType, &localBalance,
|
||||
),
|
||||
tlv.MakePrimitiveRecord(
|
||||
initialRemoteBalanceType, &remoteBalance,
|
||||
),
|
||||
MakeScidRecord(realScidType, &channel.confirmedScid),
|
||||
tlv.MakePrimitiveRecord(channelMemoType, &channel.Memo),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tlvStream.Encode(&w); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil {
|
||||
@ -4398,12 +4142,6 @@ func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment,
|
||||
return err
|
||||
}
|
||||
|
||||
// Before we write to disk, we'll also write our aux data as well.
|
||||
auxData := c.extractTlvData()
|
||||
if err := auxData.encode(&b); err != nil {
|
||||
return fmt.Errorf("unable to write aux data: %w", err)
|
||||
}
|
||||
|
||||
return chanBucket.Put(commitKey, b.Bytes())
|
||||
}
|
||||
|
||||
@ -4506,14 +4244,45 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error {
|
||||
}
|
||||
}
|
||||
|
||||
var auxData openChannelTlvData
|
||||
if err := auxData.decode(r); err != nil {
|
||||
return fmt.Errorf("unable to decode aux data: %w", err)
|
||||
// Create balance fields in uint64, and Memo field as byte slice.
|
||||
var (
|
||||
localBalance uint64
|
||||
remoteBalance uint64
|
||||
memo []byte
|
||||
)
|
||||
|
||||
// Create the tlv stream.
|
||||
tlvStream, err := tlv.NewStream(
|
||||
// Write the RevocationKeyLocator as the first entry in a tlv
|
||||
// stream.
|
||||
MakeKeyLocRecord(
|
||||
keyLocType, &channel.RevocationKeyLocator,
|
||||
),
|
||||
tlv.MakePrimitiveRecord(
|
||||
initialLocalBalanceType, &localBalance,
|
||||
),
|
||||
tlv.MakePrimitiveRecord(
|
||||
initialRemoteBalanceType, &remoteBalance,
|
||||
),
|
||||
MakeScidRecord(realScidType, &channel.confirmedScid),
|
||||
tlv.MakePrimitiveRecord(channelMemoType, &memo),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Assign all the relevant fields from the aux data into the actual
|
||||
// open channel.
|
||||
channel.amendTlvData(auxData)
|
||||
if err := tlvStream.Decode(r); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Attach the balance fields.
|
||||
channel.InitialLocalBalance = lnwire.MilliSatoshi(localBalance)
|
||||
channel.InitialRemoteBalance = lnwire.MilliSatoshi(remoteBalance)
|
||||
|
||||
// Attach the memo field if non-empty.
|
||||
if len(memo) > 0 {
|
||||
channel.Memo = memo
|
||||
}
|
||||
|
||||
channel.Packager = NewChannelPackager(channel.ShortChannelID)
|
||||
|
||||
@ -4549,9 +4318,7 @@ func deserializeChanCommit(r io.Reader) (ChannelCommitment, error) {
|
||||
return c, nil
|
||||
}
|
||||
|
||||
func fetchChanCommitment(chanBucket kvdb.RBucket,
|
||||
local bool) (ChannelCommitment, error) {
|
||||
|
||||
func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment, error) {
|
||||
var commitKey []byte
|
||||
if local {
|
||||
commitKey = append(chanCommitmentKey, byte(0x00))
|
||||
@ -4565,23 +4332,7 @@ func fetchChanCommitment(chanBucket kvdb.RBucket,
|
||||
}
|
||||
|
||||
r := bytes.NewReader(commitBytes)
|
||||
chanCommit, err := deserializeChanCommit(r)
|
||||
if err != nil {
|
||||
return ChannelCommitment{}, fmt.Errorf("unable to decode "+
|
||||
"chan commit: %w", err)
|
||||
}
|
||||
|
||||
// We'll also check to see if we have any aux data stored as the end of
|
||||
// the stream.
|
||||
var auxData commitTlvData
|
||||
if err := auxData.decode(r); err != nil {
|
||||
return ChannelCommitment{}, fmt.Errorf("unable to decode "+
|
||||
"chan aux data: %w", err)
|
||||
}
|
||||
|
||||
chanCommit.amendTlvData(auxData)
|
||||
|
||||
return chanCommit, nil
|
||||
return deserializeChanCommit(r)
|
||||
}
|
||||
|
||||
func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error {
|
||||
@ -4689,25 +4440,6 @@ func deleteThawHeight(chanBucket kvdb.RwBucket) error {
|
||||
return chanBucket.Delete(frozenChanKey)
|
||||
}
|
||||
|
||||
// keyLocRecord is a wrapper struct around keychain.KeyLocator to implement the
|
||||
// tlv.RecordProducer interface.
|
||||
type keyLocRecord struct {
|
||||
keychain.KeyLocator
|
||||
}
|
||||
|
||||
// Record creates a Record out of a KeyLocator using the passed Type and the
|
||||
// EKeyLocator and DKeyLocator functions. The size will always be 8 as
|
||||
// KeyFamily is uint32 and the Index is uint32.
|
||||
//
|
||||
// NOTE: This is part of the tlv.RecordProducer interface.
|
||||
func (k *keyLocRecord) Record() tlv.Record {
|
||||
// Note that we set the type here as zero, as when used with a
|
||||
// tlv.RecordT, the type param will be used as the type.
|
||||
return tlv.MakeStaticRecord(
|
||||
0, &k.KeyLocator, 8, EKeyLocator, DKeyLocator,
|
||||
)
|
||||
}
|
||||
|
||||
// EKeyLocator is an encoder for keychain.KeyLocator.
|
||||
func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error {
|
||||
if v, ok := val.(*keychain.KeyLocator); ok {
|
||||
@ -4736,6 +4468,22 @@ func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
|
||||
return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8)
|
||||
}
|
||||
|
||||
// MakeKeyLocRecord creates a Record out of a KeyLocator using the passed
|
||||
// Type and the EKeyLocator and DKeyLocator functions. The size will always be
|
||||
// 8 as KeyFamily is uint32 and the Index is uint32.
|
||||
func MakeKeyLocRecord(typ tlv.Type, keyLoc *keychain.KeyLocator) tlv.Record {
|
||||
return tlv.MakeStaticRecord(typ, keyLoc, 8, EKeyLocator, DKeyLocator)
|
||||
}
|
||||
|
||||
// MakeScidRecord creates a Record out of a ShortChannelID using the passed
|
||||
// Type and the EShortChannelID and DShortChannelID functions. The size will
|
||||
// always be 8 for the ShortChannelID.
|
||||
func MakeScidRecord(typ tlv.Type, scid *lnwire.ShortChannelID) tlv.Record {
|
||||
return tlv.MakeStaticRecord(
|
||||
typ, scid, 8, lnwire.EShortChannelID, lnwire.DShortChannelID,
|
||||
)
|
||||
}
|
||||
|
||||
// ShutdownInfo contains various info about the shutdown initiation of a
|
||||
// channel.
|
||||
type ShutdownInfo struct {
|
||||
|
||||
@ -17,7 +17,6 @@ import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lnmock"
|
||||
@ -174,7 +173,7 @@ func fundingPointOption(chanPoint wire.OutPoint) testChannelOption {
|
||||
}
|
||||
|
||||
// channelIDOption is an option which sets the short channel ID of the channel.
|
||||
func channelIDOption(chanID lnwire.ShortChannelID) testChannelOption {
|
||||
var channelIDOption = func(chanID lnwire.ShortChannelID) testChannelOption {
|
||||
return func(params *testChannelParams) {
|
||||
params.channel.ShortChannelID = chanID
|
||||
}
|
||||
@ -327,9 +326,6 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
|
||||
uniqueOutputIndex.Add(1)
|
||||
op := wire.OutPoint{Hash: key, Index: uniqueOutputIndex.Load()}
|
||||
|
||||
var tapscriptRoot chainhash.Hash
|
||||
copy(tapscriptRoot[:], bytes.Repeat([]byte{1}, 32))
|
||||
|
||||
return &OpenChannel{
|
||||
ChanType: SingleFunderBit | FrozenBit,
|
||||
ChainHash: key,
|
||||
@ -351,7 +347,6 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
|
||||
FeePerKw: btcutil.Amount(5000),
|
||||
CommitTx: channels.TestFundingTx,
|
||||
CommitSig: bytes.Repeat([]byte{1}, 71),
|
||||
CustomBlob: fn.Some([]byte{1, 2, 3}),
|
||||
},
|
||||
RemoteCommitment: ChannelCommitment{
|
||||
CommitHeight: 0,
|
||||
@ -361,7 +356,6 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
|
||||
FeePerKw: btcutil.Amount(5000),
|
||||
CommitTx: channels.TestFundingTx,
|
||||
CommitSig: bytes.Repeat([]byte{1}, 71),
|
||||
CustomBlob: fn.Some([]byte{4, 5, 6}),
|
||||
},
|
||||
NumConfsRequired: 4,
|
||||
RemoteCurrentRevocation: privKey.PubKey(),
|
||||
@ -374,9 +368,6 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
|
||||
ThawHeight: uint32(defaultPendingHeight),
|
||||
InitialLocalBalance: lnwire.MilliSatoshi(9000),
|
||||
InitialRemoteBalance: lnwire.MilliSatoshi(3000),
|
||||
Memo: []byte("test"),
|
||||
TapscriptRoot: fn.Some(tapscriptRoot),
|
||||
CustomBlob: fn.Some([]byte{1, 2, 3}),
|
||||
}
|
||||
}
|
||||
|
||||
@ -584,32 +575,24 @@ func assertCommitmentEqual(t *testing.T, a, b *ChannelCommitment) {
|
||||
func assertRevocationLogEntryEqual(t *testing.T, c *ChannelCommitment,
|
||||
r *RevocationLog) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
// Check the common fields.
|
||||
require.EqualValues(
|
||||
t, r.CommitTxHash.Val, c.CommitTx.TxHash(), "CommitTx mismatch",
|
||||
t, r.CommitTxHash, c.CommitTx.TxHash(), "CommitTx mismatch",
|
||||
)
|
||||
|
||||
// Now check the common fields from the HTLCs.
|
||||
require.Equal(t, len(r.HTLCEntries), len(c.Htlcs), "HTLCs len mismatch")
|
||||
for i, rHtlc := range r.HTLCEntries {
|
||||
cHtlc := c.Htlcs[i]
|
||||
require.Equal(t, rHtlc.RHash.Val[:], cHtlc.RHash[:], "RHash")
|
||||
require.Equal(
|
||||
t, rHtlc.Amt.Val.Int(), cHtlc.Amt.ToSatoshis(), "Amt",
|
||||
)
|
||||
require.Equal(
|
||||
t, rHtlc.RefundTimeout.Val, cHtlc.RefundTimeout,
|
||||
"RefundTimeout",
|
||||
)
|
||||
require.EqualValues(
|
||||
t, rHtlc.OutputIndex.Val, cHtlc.OutputIndex,
|
||||
"OutputIndex",
|
||||
)
|
||||
require.Equal(
|
||||
t, rHtlc.Incoming.Val, cHtlc.Incoming, "Incoming",
|
||||
)
|
||||
require.Equal(t, rHtlc.RHash, cHtlc.RHash, "RHash mismatch")
|
||||
require.Equal(t, rHtlc.Amt, cHtlc.Amt.ToSatoshis(),
|
||||
"Amt mismatch")
|
||||
require.Equal(t, rHtlc.RefundTimeout, cHtlc.RefundTimeout,
|
||||
"RefundTimeout mismatch")
|
||||
require.EqualValues(t, rHtlc.OutputIndex, cHtlc.OutputIndex,
|
||||
"OutputIndex mismatch")
|
||||
require.Equal(t, rHtlc.Incoming, cHtlc.Incoming,
|
||||
"Incoming mismatch")
|
||||
}
|
||||
}
|
||||
|
||||
@ -674,7 +657,6 @@ func TestChannelStateTransition(t *testing.T) {
|
||||
CommitTx: newTx,
|
||||
CommitSig: newSig,
|
||||
Htlcs: htlcs,
|
||||
CustomBlob: fn.Some([]byte{4, 5, 6}),
|
||||
}
|
||||
|
||||
// First update the local node's broadcastable state and also add a
|
||||
@ -712,14 +694,9 @@ func TestChannelStateTransition(t *testing.T) {
|
||||
// have been updated.
|
||||
updatedChannel, err := cdb.FetchOpenChannels(channel.IdentityPub)
|
||||
require.NoError(t, err, "unable to fetch updated channel")
|
||||
|
||||
assertCommitmentEqual(
|
||||
t, &commitment, &updatedChannel[0].LocalCommitment,
|
||||
)
|
||||
|
||||
assertCommitmentEqual(t, &commitment, &updatedChannel[0].LocalCommitment)
|
||||
numDiskUpdates, err := updatedChannel[0].CommitmentHeight()
|
||||
require.NoError(t, err, "unable to read commitment height from disk")
|
||||
|
||||
if numDiskUpdates != uint64(commitment.CommitHeight) {
|
||||
t.Fatalf("num disk updates doesn't match: %v vs %v",
|
||||
numDiskUpdates, commitment.CommitHeight)
|
||||
@ -822,10 +799,10 @@ func TestChannelStateTransition(t *testing.T) {
|
||||
|
||||
// Check the output indexes are saved as expected.
|
||||
require.EqualValues(
|
||||
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex.Val,
|
||||
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex,
|
||||
)
|
||||
require.EqualValues(
|
||||
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex.Val,
|
||||
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex,
|
||||
)
|
||||
|
||||
// The two deltas (the original vs the on-disk version) should
|
||||
@ -867,10 +844,10 @@ func TestChannelStateTransition(t *testing.T) {
|
||||
|
||||
// Check the output indexes are saved as expected.
|
||||
require.EqualValues(
|
||||
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex.Val,
|
||||
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex,
|
||||
)
|
||||
require.EqualValues(
|
||||
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex.Val,
|
||||
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex,
|
||||
)
|
||||
|
||||
assertRevocationLogEntryEqual(t, &oldRemoteCommit, prevCommit)
|
||||
@ -1665,24 +1642,6 @@ func TestHTLCsExtraData(t *testing.T) {
|
||||
),
|
||||
}
|
||||
|
||||
// Custom channel data htlc with a blinding point.
|
||||
customDataHTLC := HTLC{
|
||||
Signature: testSig.Serialize(),
|
||||
Incoming: false,
|
||||
Amt: 10,
|
||||
RHash: key,
|
||||
RefundTimeout: 1,
|
||||
OnionBlob: lnmock.MockOnion(),
|
||||
BlindingPoint: tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
|
||||
pubKey,
|
||||
),
|
||||
),
|
||||
CustomRecords: map[uint64][]byte{
|
||||
uint64(lnwire.MinCustomRecordsTlvType + 3): {1, 2, 3},
|
||||
},
|
||||
}
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
htlcs []HTLC
|
||||
@ -1704,7 +1663,6 @@ func TestHTLCsExtraData(t *testing.T) {
|
||||
mockHtlc,
|
||||
blindingPointHTLC,
|
||||
mockHtlc,
|
||||
customDataHTLC,
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
@ -286,27 +286,6 @@ func NewFwdPkg(source lnwire.ShortChannelID, height uint64,
|
||||
}
|
||||
}
|
||||
|
||||
// SourceRef is a convenience method that returns an AddRef to this forwarding
|
||||
// package for the index in the argument. It is the caller's responsibility
|
||||
// to ensure that the index is in bounds.
|
||||
func (f *FwdPkg) SourceRef(i uint16) AddRef {
|
||||
return AddRef{
|
||||
Height: f.Height,
|
||||
Index: i,
|
||||
}
|
||||
}
|
||||
|
||||
// DestRef is a convenience method that returns a SettleFailRef to this
|
||||
// forwarding package for the index in the argument. It is the caller's
|
||||
// responsibility to ensure that the index is in bounds.
|
||||
func (f *FwdPkg) DestRef(i uint16) SettleFailRef {
|
||||
return SettleFailRef{
|
||||
Source: f.Source,
|
||||
Height: f.Height,
|
||||
Index: i,
|
||||
}
|
||||
}
|
||||
|
||||
// ID returns an unique identifier for this package, used to ensure that sphinx
|
||||
// replay processing of this batch is idempotent.
|
||||
func (f *FwdPkg) ID() []byte {
|
||||
|
||||
@ -2382,7 +2382,7 @@ func TestStressTestChannelGraphAPI(t *testing.T) {
|
||||
methodsMu.Unlock()
|
||||
|
||||
err := fn()
|
||||
require.NoErrorf(t, err, name)
|
||||
require.NoErrorf(t, err, fmt.Sprintf(name))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ package migration_01_to_11
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
@ -153,7 +154,12 @@ func signDigestCompact(hash []byte) ([]byte, error) {
|
||||
privKey, _ := btcec.PrivKeyFromBytes(testPrivKeyBytes)
|
||||
|
||||
// ecdsa.SignCompact returns a pubkey-recoverable signature
|
||||
return ecdsa.SignCompact(privKey, hash, isCompressedKey), nil
|
||||
sig, err := ecdsa.SignCompact(privKey, hash, isCompressedKey)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("can't sign the hash: %w", err)
|
||||
}
|
||||
|
||||
return sig, nil
|
||||
}
|
||||
|
||||
// getPayReq creates a payment request for the given net.
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
)
|
||||
|
||||
// ChannelEdgeInfo represents a fully authenticated channel along with all its
|
||||
@ -63,11 +62,6 @@ type ChannelEdgeInfo struct {
|
||||
// the value output in the outpoint that created this channel.
|
||||
Capacity btcutil.Amount
|
||||
|
||||
// TapscriptRoot is the optional Merkle root of the tapscript tree if
|
||||
// this channel is a taproot channel that also commits to a tapscript
|
||||
// tree (custom channel).
|
||||
TapscriptRoot fn.Option[chainhash.Hash]
|
||||
|
||||
// ExtraOpaqueData is the set of data that was appended to this
|
||||
// message, some of which we may not actually know how to iterate or
|
||||
// parse. By holding onto this data, we ensure that we're able to
|
||||
|
||||
@ -195,11 +195,6 @@ type PaymentCreationInfo struct {
|
||||
|
||||
// PaymentRequest is the full payment request, if any.
|
||||
PaymentRequest []byte
|
||||
|
||||
// FirstHopCustomRecords are the TLV records that are to be sent to the
|
||||
// first hop of this payment. These records will be transmitted via the
|
||||
// wire message only and therefore do not affect the onion payload size.
|
||||
FirstHopCustomRecords lnwire.CustomRecords
|
||||
}
|
||||
|
||||
// htlcBucketKey creates a composite key from prefix and id where the result is
|
||||
@ -1015,21 +1010,10 @@ func serializePaymentCreationInfo(w io.Writer, c *PaymentCreationInfo) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Any remaining bytes are TLV encoded records. Currently, these are
|
||||
// only the custom records provided by the user to be sent to the first
|
||||
// hop. But this can easily be extended with further records by merging
|
||||
// the records into a single TLV stream.
|
||||
err := c.FirstHopCustomRecords.SerializeTo(w)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func deserializePaymentCreationInfo(r io.Reader) (*PaymentCreationInfo,
|
||||
error) {
|
||||
|
||||
func deserializePaymentCreationInfo(r io.Reader) (*PaymentCreationInfo, error) {
|
||||
var scratch [8]byte
|
||||
|
||||
c := &PaymentCreationInfo{}
|
||||
@ -1062,15 +1046,6 @@ func deserializePaymentCreationInfo(r io.Reader) (*PaymentCreationInfo,
|
||||
}
|
||||
c.PaymentRequest = payReq
|
||||
|
||||
// Any remaining bytes are TLV encoded records. Currently, these are
|
||||
// only the custom records provided by the user to be sent to the first
|
||||
// hop. But this can easily be extended with further records by merging
|
||||
// the records into a single TLV stream.
|
||||
c.FirstHopCustomRecords, err = lnwire.ParseCustomRecordsFrom(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return c, nil
|
||||
}
|
||||
|
||||
@ -1096,25 +1071,6 @@ func serializeHTLCAttemptInfo(w io.Writer, a *HTLCAttemptInfo) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// Merge the fixed/known records together with the custom records to
|
||||
// serialize them as a single blob. We can't do this in SerializeRoute
|
||||
// because we're in the middle of the byte stream there. We can only do
|
||||
// TLV serialization at the end of the stream, since EOF is allowed for
|
||||
// a stream if no more data is expected.
|
||||
producers := []tlv.RecordProducer{
|
||||
&a.Route.FirstHopAmount,
|
||||
}
|
||||
tlvData, err := lnwire.MergeAndEncode(
|
||||
producers, nil, a.Route.FirstHopWireCustomRecords,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if _, err := w.Write(tlvData); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1152,22 +1108,6 @@ func deserializeHTLCAttemptInfo(r io.Reader) (*HTLCAttemptInfo, error) {
|
||||
|
||||
a.Hash = &hash
|
||||
|
||||
// Read any remaining data (if any) and parse it into the known records
|
||||
// and custom records.
|
||||
extraData, err := io.ReadAll(r)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
customRecords, _, _, err := lnwire.ParseAndExtractCustomRecords(
|
||||
extraData, &a.Route.FirstHopAmount,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
a.Route.FirstHopWireCustomRecords = customRecords
|
||||
|
||||
return a, nil
|
||||
}
|
||||
|
||||
@ -1433,8 +1373,6 @@ func SerializeRoute(w io.Writer, r route.Route) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Any new/extra TLV data is encoded in serializeHTLCAttemptInfo!
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1468,7 +1406,5 @@ func DeserializeRoute(r io.Reader) (route.Route, error) {
|
||||
}
|
||||
rt.Hops = hops
|
||||
|
||||
// Any new/extra TLV data is decoded in deserializeHTLCAttemptInfo!
|
||||
|
||||
return rt, nil
|
||||
}
|
||||
|
||||
@ -13,10 +13,8 @@ import (
|
||||
"github.com/davecgh/go-spew/spew"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
"github.com/lightningnetwork/lnd/record"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
@ -110,7 +108,7 @@ func makeFakeInfo() (*PaymentCreationInfo, *HTLCAttemptInfo) {
|
||||
// Use single second precision to avoid false positive test
|
||||
// failures due to the monotonic time component.
|
||||
CreationTime: time.Unix(time.Now().Unix(), 0),
|
||||
PaymentRequest: []byte("test"),
|
||||
PaymentRequest: []byte(""),
|
||||
}
|
||||
|
||||
a := NewHtlcAttempt(
|
||||
@ -126,64 +124,51 @@ func TestSentPaymentSerialization(t *testing.T) {
|
||||
c, s := makeFakeInfo()
|
||||
|
||||
var b bytes.Buffer
|
||||
require.NoError(t, serializePaymentCreationInfo(&b, c), "serialize")
|
||||
|
||||
// Assert the length of the serialized creation info is as expected,
|
||||
// without any custom records.
|
||||
baseLength := 32 + 8 + 8 + 4 + len(c.PaymentRequest)
|
||||
require.Len(t, b.Bytes(), baseLength)
|
||||
if err := serializePaymentCreationInfo(&b, c); err != nil {
|
||||
t.Fatalf("unable to serialize creation info: %v", err)
|
||||
}
|
||||
|
||||
newCreationInfo, err := deserializePaymentCreationInfo(&b)
|
||||
require.NoError(t, err, "deserialize")
|
||||
require.Equal(t, c, newCreationInfo)
|
||||
require.NoError(t, err, "unable to deserialize creation info")
|
||||
|
||||
b.Reset()
|
||||
|
||||
// Now we add some custom records to the creation info and serialize it
|
||||
// again.
|
||||
c.FirstHopCustomRecords = lnwire.CustomRecords{
|
||||
lnwire.MinCustomRecordsTlvType: []byte{1, 2, 3},
|
||||
if !reflect.DeepEqual(c, newCreationInfo) {
|
||||
t.Fatalf("Payments do not match after "+
|
||||
"serialization/deserialization %v vs %v",
|
||||
spew.Sdump(c), spew.Sdump(newCreationInfo),
|
||||
)
|
||||
}
|
||||
require.NoError(t, serializePaymentCreationInfo(&b, c), "serialize")
|
||||
|
||||
newCreationInfo, err = deserializePaymentCreationInfo(&b)
|
||||
require.NoError(t, err, "deserialize")
|
||||
require.Equal(t, c, newCreationInfo)
|
||||
|
||||
b.Reset()
|
||||
require.NoError(t, serializeHTLCAttemptInfo(&b, s), "serialize")
|
||||
if err := serializeHTLCAttemptInfo(&b, s); err != nil {
|
||||
t.Fatalf("unable to serialize info: %v", err)
|
||||
}
|
||||
|
||||
newWireInfo, err := deserializeHTLCAttemptInfo(&b)
|
||||
require.NoError(t, err, "deserialize")
|
||||
require.NoError(t, err, "unable to deserialize info")
|
||||
newWireInfo.AttemptID = s.AttemptID
|
||||
|
||||
// First we verify all the records match up properly.
|
||||
require.Equal(t, s.Route, newWireInfo.Route)
|
||||
|
||||
// We now add the new fields and custom records to the route and
|
||||
// serialize it again.
|
||||
b.Reset()
|
||||
s.Route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0](
|
||||
tlv.NewBigSizeT(lnwire.MilliSatoshi(1234)),
|
||||
)
|
||||
s.Route.FirstHopWireCustomRecords = lnwire.CustomRecords{
|
||||
lnwire.MinCustomRecordsTlvType + 3: []byte{4, 5, 6},
|
||||
// First we verify all the records match up porperly, as they aren't
|
||||
// able to be properly compared using reflect.DeepEqual.
|
||||
err = assertRouteEqual(&s.Route, &newWireInfo.Route)
|
||||
if err != nil {
|
||||
t.Fatalf("Routes do not match after "+
|
||||
"serialization/deserialization: %v", err)
|
||||
}
|
||||
require.NoError(t, serializeHTLCAttemptInfo(&b, s), "serialize")
|
||||
|
||||
newWireInfo, err = deserializeHTLCAttemptInfo(&b)
|
||||
require.NoError(t, err, "deserialize")
|
||||
require.Equal(t, s.Route, newWireInfo.Route)
|
||||
|
||||
// Clear routes to allow DeepEqual to compare the remaining fields.
|
||||
newWireInfo.Route = route.Route{}
|
||||
s.Route = route.Route{}
|
||||
newWireInfo.AttemptID = s.AttemptID
|
||||
|
||||
// Call session key method to set our cached session key so we can use
|
||||
// DeepEqual, and assert that our key equals the original key.
|
||||
require.Equal(t, s.cachedSessionKey, newWireInfo.SessionKey())
|
||||
|
||||
require.Equal(t, s, newWireInfo)
|
||||
if !reflect.DeepEqual(s, newWireInfo) {
|
||||
t.Fatalf("Payments do not match after "+
|
||||
"serialization/deserialization %v vs %v",
|
||||
spew.Sdump(s), spew.Sdump(newWireInfo),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// assertRouteEquals compares to routes for equality and returns an error if
|
||||
|
||||
@ -7,7 +7,6 @@ import (
|
||||
"math"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
@ -17,15 +16,16 @@ import (
|
||||
const (
|
||||
// OutputIndexEmpty is used when the output index doesn't exist.
|
||||
OutputIndexEmpty = math.MaxUint16
|
||||
)
|
||||
|
||||
type (
|
||||
// BigSizeAmount is a type alias for a TLV record of a btcutil.Amount.
|
||||
BigSizeAmount = tlv.BigSizeT[btcutil.Amount]
|
||||
|
||||
// BigSizeMilliSatoshi is a type alias for a TLV record of a
|
||||
// lnwire.MilliSatoshi.
|
||||
BigSizeMilliSatoshi = tlv.BigSizeT[lnwire.MilliSatoshi]
|
||||
// A set of tlv type definitions used to serialize the body of
|
||||
// revocation logs to the database.
|
||||
//
|
||||
// NOTE: A migration should be added whenever this list changes.
|
||||
revLogOurOutputIndexType tlv.Type = 0
|
||||
revLogTheirOutputIndexType tlv.Type = 1
|
||||
revLogCommitTxHashType tlv.Type = 2
|
||||
revLogOurBalanceType tlv.Type = 3
|
||||
revLogTheirBalanceType tlv.Type = 4
|
||||
)
|
||||
|
||||
var (
|
||||
@ -54,74 +54,6 @@ var (
|
||||
ErrOutputIndexTooBig = errors.New("output index is over uint16")
|
||||
)
|
||||
|
||||
// SparsePayHash is a type alias for a 32 byte array, which when serialized is
|
||||
// able to save some space by not including an empty payment hash on disk.
|
||||
type SparsePayHash [32]byte
|
||||
|
||||
// NewSparsePayHash creates a new SparsePayHash from a 32 byte array.
|
||||
func NewSparsePayHash(rHash [32]byte) SparsePayHash {
|
||||
return SparsePayHash(rHash)
|
||||
}
|
||||
|
||||
// Record returns a tlv record for the SparsePayHash.
|
||||
func (s *SparsePayHash) Record() tlv.Record {
|
||||
// We use a zero for the type here, as this'll be used along with the
|
||||
// RecordT type.
|
||||
return tlv.MakeDynamicRecord(
|
||||
0, s, s.hashLen,
|
||||
sparseHashEncoder, sparseHashDecoder,
|
||||
)
|
||||
}
|
||||
|
||||
// hashLen is used by MakeDynamicRecord to return the size of the RHash.
|
||||
//
|
||||
// NOTE: for zero hash, we return a length 0.
|
||||
func (s *SparsePayHash) hashLen() uint64 {
|
||||
if bytes.Equal(s[:], lntypes.ZeroHash[:]) {
|
||||
return 0
|
||||
}
|
||||
|
||||
return 32
|
||||
}
|
||||
|
||||
// sparseHashEncoder is the customized encoder which skips encoding the empty
|
||||
// hash.
|
||||
func sparseHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
|
||||
v, ok := val.(*SparsePayHash)
|
||||
if !ok {
|
||||
return tlv.NewTypeForEncodingErr(val, "SparsePayHash")
|
||||
}
|
||||
|
||||
// If the value is an empty hash, we will skip encoding it.
|
||||
if bytes.Equal(v[:], lntypes.ZeroHash[:]) {
|
||||
return nil
|
||||
}
|
||||
|
||||
vArray := (*[32]byte)(v)
|
||||
|
||||
return tlv.EBytes32(w, vArray, buf)
|
||||
}
|
||||
|
||||
// sparseHashDecoder is the customized decoder which skips decoding the empty
|
||||
// hash.
|
||||
func sparseHashDecoder(r io.Reader, val interface{}, buf *[8]byte,
|
||||
l uint64) error {
|
||||
|
||||
v, ok := val.(*SparsePayHash)
|
||||
if !ok {
|
||||
return tlv.NewTypeForEncodingErr(val, "SparsePayHash")
|
||||
}
|
||||
|
||||
// If the length is zero, we will skip encoding the empty hash.
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
vArray := (*[32]byte)(v)
|
||||
|
||||
return tlv.DBytes32(r, vArray, buf, 32)
|
||||
}
|
||||
|
||||
// HTLCEntry specifies the minimal info needed to be stored on disk for ALL the
|
||||
// historical HTLCs, which is useful for constructing RevocationLog when a
|
||||
// breach is detected.
|
||||
@ -140,90 +72,116 @@ func sparseHashDecoder(r io.Reader, val interface{}, buf *[8]byte,
|
||||
// made into tlv records without further conversion.
|
||||
type HTLCEntry struct {
|
||||
// RHash is the payment hash of the HTLC.
|
||||
RHash tlv.RecordT[tlv.TlvType0, SparsePayHash]
|
||||
RHash [32]byte
|
||||
|
||||
// RefundTimeout is the absolute timeout on the HTLC that the sender
|
||||
// must wait before reclaiming the funds in limbo.
|
||||
RefundTimeout tlv.RecordT[tlv.TlvType1, uint32]
|
||||
RefundTimeout uint32
|
||||
|
||||
// OutputIndex is the output index for this particular HTLC output
|
||||
// within the commitment transaction.
|
||||
//
|
||||
// NOTE: we use uint16 instead of int32 here to save us 2 bytes, which
|
||||
// gives us a max number of HTLCs of 65K.
|
||||
OutputIndex tlv.RecordT[tlv.TlvType2, uint16]
|
||||
OutputIndex uint16
|
||||
|
||||
// Incoming denotes whether we're the receiver or the sender of this
|
||||
// HTLC.
|
||||
Incoming tlv.RecordT[tlv.TlvType3, bool]
|
||||
//
|
||||
// NOTE: this field is the memory representation of the field
|
||||
// incomingUint.
|
||||
Incoming bool
|
||||
|
||||
// Amt is the amount of satoshis this HTLC escrows.
|
||||
Amt tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]]
|
||||
//
|
||||
// NOTE: this field is the memory representation of the field amtUint.
|
||||
Amt btcutil.Amount
|
||||
|
||||
// CustomBlob is an optional blob that can be used to store information
|
||||
// specific to revocation handling for a custom channel type.
|
||||
CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob]
|
||||
// amtTlv is the uint64 format of Amt. This field is created so we can
|
||||
// easily make it into a tlv record and save it to disk.
|
||||
//
|
||||
// NOTE: we keep this field for accounting purpose only. If the disk
|
||||
// space becomes an issue, we could delete this field to save us extra
|
||||
// 8 bytes.
|
||||
amtTlv uint64
|
||||
|
||||
// HtlcIndex is the index of the HTLC in the channel.
|
||||
HtlcIndex tlv.OptionalRecordT[tlv.TlvType6, uint16]
|
||||
// incomingTlv is the uint8 format of Incoming. This field is created
|
||||
// so we can easily make it into a tlv record and save it to disk.
|
||||
incomingTlv uint8
|
||||
}
|
||||
|
||||
// RHashLen is used by MakeDynamicRecord to return the size of the RHash.
|
||||
//
|
||||
// NOTE: for zero hash, we return a length 0.
|
||||
func (h *HTLCEntry) RHashLen() uint64 {
|
||||
if h.RHash == lntypes.ZeroHash {
|
||||
return 0
|
||||
}
|
||||
return 32
|
||||
}
|
||||
|
||||
// RHashEncoder is the customized encoder which skips encoding the empty hash.
|
||||
func RHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
|
||||
v, ok := val.(*[32]byte)
|
||||
if !ok {
|
||||
return tlv.NewTypeForEncodingErr(val, "RHash")
|
||||
}
|
||||
|
||||
// If the value is an empty hash, we will skip encoding it.
|
||||
if *v == lntypes.ZeroHash {
|
||||
return nil
|
||||
}
|
||||
|
||||
return tlv.EBytes32(w, v, buf)
|
||||
}
|
||||
|
||||
// RHashDecoder is the customized decoder which skips decoding the empty hash.
|
||||
func RHashDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
|
||||
v, ok := val.(*[32]byte)
|
||||
if !ok {
|
||||
return tlv.NewTypeForEncodingErr(val, "RHash")
|
||||
}
|
||||
|
||||
// If the length is zero, we will skip encoding the empty hash.
|
||||
if l == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
return tlv.DBytes32(r, v, buf, 32)
|
||||
}
|
||||
|
||||
// toTlvStream converts an HTLCEntry record into a tlv representation.
|
||||
func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) {
|
||||
records := []tlv.Record{
|
||||
h.RHash.Record(),
|
||||
h.RefundTimeout.Record(),
|
||||
h.OutputIndex.Record(),
|
||||
h.Incoming.Record(),
|
||||
h.Amt.Record(),
|
||||
}
|
||||
const (
|
||||
// A set of tlv type definitions used to serialize htlc entries
|
||||
// to the database. We define it here instead of the head of
|
||||
// the file to avoid naming conflicts.
|
||||
//
|
||||
// NOTE: A migration should be added whenever this list
|
||||
// changes.
|
||||
rHashType tlv.Type = 0
|
||||
refundTimeoutType tlv.Type = 1
|
||||
outputIndexType tlv.Type = 2
|
||||
incomingType tlv.Type = 3
|
||||
amtType tlv.Type = 4
|
||||
)
|
||||
|
||||
h.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) {
|
||||
records = append(records, r.Record())
|
||||
})
|
||||
|
||||
h.HtlcIndex.WhenSome(func(r tlv.RecordT[tlv.TlvType6, uint16]) {
|
||||
records = append(records, r.Record())
|
||||
})
|
||||
|
||||
tlv.SortRecords(records)
|
||||
|
||||
return tlv.NewStream(records...)
|
||||
}
|
||||
|
||||
// NewHTLCEntryFromHTLC creates a new HTLCEntry from an HTLC.
|
||||
func NewHTLCEntryFromHTLC(htlc HTLC) (*HTLCEntry, error) {
|
||||
h := &HTLCEntry{
|
||||
RHash: tlv.NewRecordT[tlv.TlvType0](
|
||||
NewSparsePayHash(htlc.RHash),
|
||||
return tlv.NewStream(
|
||||
tlv.MakeDynamicRecord(
|
||||
rHashType, &h.RHash, h.RHashLen,
|
||||
RHashEncoder, RHashDecoder,
|
||||
),
|
||||
RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1](
|
||||
htlc.RefundTimeout,
|
||||
tlv.MakePrimitiveRecord(
|
||||
refundTimeoutType, &h.RefundTimeout,
|
||||
),
|
||||
OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2](
|
||||
uint16(htlc.OutputIndex),
|
||||
tlv.MakePrimitiveRecord(
|
||||
outputIndexType, &h.OutputIndex,
|
||||
),
|
||||
Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](htlc.Incoming),
|
||||
Amt: tlv.NewRecordT[tlv.TlvType4](
|
||||
tlv.NewBigSizeT(htlc.Amt.ToSatoshis()),
|
||||
),
|
||||
HtlcIndex: tlv.SomeRecordT(tlv.NewPrimitiveRecord[tlv.TlvType6](
|
||||
uint16(htlc.HtlcIndex),
|
||||
)),
|
||||
}
|
||||
|
||||
if len(htlc.CustomRecords) != 0 {
|
||||
blob, err := htlc.CustomRecords.Serialize()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
h.CustomBlob = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob),
|
||||
)
|
||||
}
|
||||
|
||||
return h, nil
|
||||
tlv.MakePrimitiveRecord(incomingType, &h.incomingTlv),
|
||||
// We will save 3 bytes if the amount is less or equal to
|
||||
// 4,294,967,295 msat, or roughly 0.043 bitcoin.
|
||||
tlv.MakeBigSizeRecord(amtType, &h.amtTlv),
|
||||
)
|
||||
}
|
||||
|
||||
// RevocationLog stores the info needed to construct a breach retribution. Its
|
||||
@ -233,15 +191,15 @@ func NewHTLCEntryFromHTLC(htlc HTLC) (*HTLCEntry, error) {
|
||||
type RevocationLog struct {
|
||||
// OurOutputIndex specifies our output index in this commitment. In a
|
||||
// remote commitment transaction, this is the to remote output index.
|
||||
OurOutputIndex tlv.RecordT[tlv.TlvType0, uint16]
|
||||
OurOutputIndex uint16
|
||||
|
||||
// TheirOutputIndex specifies their output index in this commitment. In
|
||||
// a remote commitment transaction, this is the to local output index.
|
||||
TheirOutputIndex tlv.RecordT[tlv.TlvType1, uint16]
|
||||
TheirOutputIndex uint16
|
||||
|
||||
// CommitTxHash is the hash of the latest version of the commitment
|
||||
// state, broadcast able by us.
|
||||
CommitTxHash tlv.RecordT[tlv.TlvType2, [32]byte]
|
||||
CommitTxHash [32]byte
|
||||
|
||||
// HTLCEntries is the set of HTLCEntry's that are pending at this
|
||||
// particular commitment height.
|
||||
@ -251,65 +209,21 @@ type RevocationLog struct {
|
||||
// directly spendable by us. In other words, it is the value of the
|
||||
// to_remote output on the remote parties' commitment transaction.
|
||||
//
|
||||
// NOTE: this is an option so that it is clear if the value is zero or
|
||||
// NOTE: this is a pointer so that it is clear if the value is zero or
|
||||
// nil. Since migration 30 of the channeldb initially did not include
|
||||
// this field, it could be the case that the field is not present for
|
||||
// all revocation logs.
|
||||
OurBalance tlv.OptionalRecordT[tlv.TlvType3, BigSizeMilliSatoshi]
|
||||
OurBalance *lnwire.MilliSatoshi
|
||||
|
||||
// TheirBalance is the current available balance within the channel
|
||||
// directly spendable by the remote node. In other words, it is the
|
||||
// value of the to_local output on the remote parties' commitment.
|
||||
//
|
||||
// NOTE: this is an option so that it is clear if the value is zero or
|
||||
// NOTE: this is a pointer so that it is clear if the value is zero or
|
||||
// nil. Since migration 30 of the channeldb initially did not include
|
||||
// this field, it could be the case that the field is not present for
|
||||
// all revocation logs.
|
||||
TheirBalance tlv.OptionalRecordT[tlv.TlvType4, BigSizeMilliSatoshi]
|
||||
|
||||
// CustomBlob is an optional blob that can be used to store information
|
||||
// specific to a custom channel type. This information is only created
|
||||
// at channel funding time, and after wards is to be considered
|
||||
// immutable.
|
||||
CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob]
|
||||
}
|
||||
|
||||
// NewRevocationLog creates a new RevocationLog from the given parameters.
|
||||
func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16,
|
||||
commitHash [32]byte, ourBalance,
|
||||
theirBalance fn.Option[lnwire.MilliSatoshi], htlcs []*HTLCEntry,
|
||||
customBlob fn.Option[tlv.Blob]) RevocationLog {
|
||||
|
||||
rl := RevocationLog{
|
||||
OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0](
|
||||
ourOutputIndex,
|
||||
),
|
||||
TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1](
|
||||
theirOutputIndex,
|
||||
),
|
||||
CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2](commitHash),
|
||||
HTLCEntries: htlcs,
|
||||
}
|
||||
|
||||
ourBalance.WhenSome(func(balance lnwire.MilliSatoshi) {
|
||||
rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3](
|
||||
tlv.NewBigSizeT(balance),
|
||||
))
|
||||
})
|
||||
|
||||
theirBalance.WhenSome(func(balance lnwire.MilliSatoshi) {
|
||||
rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4](
|
||||
tlv.NewBigSizeT(balance),
|
||||
))
|
||||
})
|
||||
|
||||
customBlob.WhenSome(func(blob tlv.Blob) {
|
||||
rl.CustomBlob = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob),
|
||||
)
|
||||
})
|
||||
|
||||
return rl
|
||||
TheirBalance *lnwire.MilliSatoshi
|
||||
}
|
||||
|
||||
// putRevocationLog uses the fields `CommitTx` and `Htlcs` from a
|
||||
@ -328,32 +242,15 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment,
|
||||
}
|
||||
|
||||
rl := &RevocationLog{
|
||||
OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0](
|
||||
uint16(ourOutputIndex),
|
||||
),
|
||||
TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1](
|
||||
uint16(theirOutputIndex),
|
||||
),
|
||||
CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2, [32]byte](
|
||||
commit.CommitTx.TxHash(),
|
||||
),
|
||||
HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)),
|
||||
OurOutputIndex: uint16(ourOutputIndex),
|
||||
TheirOutputIndex: uint16(theirOutputIndex),
|
||||
CommitTxHash: commit.CommitTx.TxHash(),
|
||||
HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)),
|
||||
}
|
||||
|
||||
commit.CustomBlob.WhenSome(func(blob tlv.Blob) {
|
||||
rl.CustomBlob = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob),
|
||||
)
|
||||
})
|
||||
|
||||
if !noAmtData {
|
||||
rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3](
|
||||
tlv.NewBigSizeT(commit.LocalBalance),
|
||||
))
|
||||
|
||||
rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4](
|
||||
tlv.NewBigSizeT(commit.RemoteBalance),
|
||||
))
|
||||
rl.OurBalance = &commit.LocalBalance
|
||||
rl.TheirBalance = &commit.RemoteBalance
|
||||
}
|
||||
|
||||
for _, htlc := range commit.Htlcs {
|
||||
@ -368,9 +265,12 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment,
|
||||
return ErrOutputIndexTooBig
|
||||
}
|
||||
|
||||
entry, err := NewHTLCEntryFromHTLC(htlc)
|
||||
if err != nil {
|
||||
return err
|
||||
entry := &HTLCEntry{
|
||||
RHash: htlc.RHash,
|
||||
RefundTimeout: htlc.RefundTimeout,
|
||||
Incoming: htlc.Incoming,
|
||||
OutputIndex: uint16(htlc.OutputIndex),
|
||||
Amt: htlc.Amt.ToSatoshis(),
|
||||
}
|
||||
rl.HTLCEntries = append(rl.HTLCEntries, entry)
|
||||
}
|
||||
@ -406,27 +306,31 @@ func fetchRevocationLog(log kvdb.RBucket,
|
||||
func serializeRevocationLog(w io.Writer, rl *RevocationLog) error {
|
||||
// Add the tlv records for all non-optional fields.
|
||||
records := []tlv.Record{
|
||||
rl.OurOutputIndex.Record(),
|
||||
rl.TheirOutputIndex.Record(),
|
||||
rl.CommitTxHash.Record(),
|
||||
tlv.MakePrimitiveRecord(
|
||||
revLogOurOutputIndexType, &rl.OurOutputIndex,
|
||||
),
|
||||
tlv.MakePrimitiveRecord(
|
||||
revLogTheirOutputIndexType, &rl.TheirOutputIndex,
|
||||
),
|
||||
tlv.MakePrimitiveRecord(
|
||||
revLogCommitTxHashType, &rl.CommitTxHash,
|
||||
),
|
||||
}
|
||||
|
||||
// Now we add any optional fields that are non-nil.
|
||||
rl.OurBalance.WhenSome(
|
||||
func(r tlv.RecordT[tlv.TlvType3, BigSizeMilliSatoshi]) {
|
||||
records = append(records, r.Record())
|
||||
},
|
||||
)
|
||||
if rl.OurBalance != nil {
|
||||
lb := uint64(*rl.OurBalance)
|
||||
records = append(records, tlv.MakeBigSizeRecord(
|
||||
revLogOurBalanceType, &lb,
|
||||
))
|
||||
}
|
||||
|
||||
rl.TheirBalance.WhenSome(
|
||||
func(r tlv.RecordT[tlv.TlvType4, BigSizeMilliSatoshi]) {
|
||||
records = append(records, r.Record())
|
||||
},
|
||||
)
|
||||
|
||||
rl.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) {
|
||||
records = append(records, r.Record())
|
||||
})
|
||||
if rl.TheirBalance != nil {
|
||||
rb := uint64(*rl.TheirBalance)
|
||||
records = append(records, tlv.MakeBigSizeRecord(
|
||||
revLogTheirBalanceType, &rb,
|
||||
))
|
||||
}
|
||||
|
||||
// Create the tlv stream.
|
||||
tlvStream, err := tlv.NewStream(records...)
|
||||
@ -447,6 +351,14 @@ func serializeRevocationLog(w io.Writer, rl *RevocationLog) error {
|
||||
// format.
|
||||
func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error {
|
||||
for _, htlc := range htlcs {
|
||||
// Patch the incomingTlv field.
|
||||
if htlc.Incoming {
|
||||
htlc.incomingTlv = 1
|
||||
}
|
||||
|
||||
// Patch the amtTlv field.
|
||||
htlc.amtTlv = uint64(htlc.Amt)
|
||||
|
||||
// Create the tlv stream.
|
||||
tlvStream, err := htlc.toTlvStream()
|
||||
if err != nil {
|
||||
@ -464,20 +376,27 @@ func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error {
|
||||
|
||||
// deserializeRevocationLog deserializes a RevocationLog based on tlv format.
|
||||
func deserializeRevocationLog(r io.Reader) (RevocationLog, error) {
|
||||
var rl RevocationLog
|
||||
|
||||
ourBalance := rl.OurBalance.Zero()
|
||||
theirBalance := rl.TheirBalance.Zero()
|
||||
customBlob := rl.CustomBlob.Zero()
|
||||
var (
|
||||
rl RevocationLog
|
||||
ourBalance uint64
|
||||
theirBalance uint64
|
||||
)
|
||||
|
||||
// Create the tlv stream.
|
||||
tlvStream, err := tlv.NewStream(
|
||||
rl.OurOutputIndex.Record(),
|
||||
rl.TheirOutputIndex.Record(),
|
||||
rl.CommitTxHash.Record(),
|
||||
ourBalance.Record(),
|
||||
theirBalance.Record(),
|
||||
customBlob.Record(),
|
||||
tlv.MakePrimitiveRecord(
|
||||
revLogOurOutputIndexType, &rl.OurOutputIndex,
|
||||
),
|
||||
tlv.MakePrimitiveRecord(
|
||||
revLogTheirOutputIndexType, &rl.TheirOutputIndex,
|
||||
),
|
||||
tlv.MakePrimitiveRecord(
|
||||
revLogCommitTxHashType, &rl.CommitTxHash,
|
||||
),
|
||||
tlv.MakeBigSizeRecord(revLogOurBalanceType, &ourBalance),
|
||||
tlv.MakeBigSizeRecord(
|
||||
revLogTheirBalanceType, &theirBalance,
|
||||
),
|
||||
)
|
||||
if err != nil {
|
||||
return rl, err
|
||||
@ -489,16 +408,14 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) {
|
||||
return rl, err
|
||||
}
|
||||
|
||||
if t, ok := parsedTypes[ourBalance.TlvType()]; ok && t == nil {
|
||||
rl.OurBalance = tlv.SomeRecordT(ourBalance)
|
||||
if t, ok := parsedTypes[revLogOurBalanceType]; ok && t == nil {
|
||||
lb := lnwire.MilliSatoshi(ourBalance)
|
||||
rl.OurBalance = &lb
|
||||
}
|
||||
|
||||
if t, ok := parsedTypes[theirBalance.TlvType()]; ok && t == nil {
|
||||
rl.TheirBalance = tlv.SomeRecordT(theirBalance)
|
||||
}
|
||||
|
||||
if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil {
|
||||
rl.CustomBlob = tlv.SomeRecordT(customBlob)
|
||||
if t, ok := parsedTypes[revLogTheirBalanceType]; ok && t == nil {
|
||||
rb := lnwire.MilliSatoshi(theirBalance)
|
||||
rl.TheirBalance = &rb
|
||||
}
|
||||
|
||||
// Read the HTLC entries.
|
||||
@ -515,28 +432,14 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) {
|
||||
for {
|
||||
var htlc HTLCEntry
|
||||
|
||||
customBlob := htlc.CustomBlob.Zero()
|
||||
htlcIndex := htlc.HtlcIndex.Zero()
|
||||
|
||||
// Create the tlv stream.
|
||||
records := []tlv.Record{
|
||||
htlc.RHash.Record(),
|
||||
htlc.RefundTimeout.Record(),
|
||||
htlc.OutputIndex.Record(),
|
||||
htlc.Incoming.Record(),
|
||||
htlc.Amt.Record(),
|
||||
customBlob.Record(),
|
||||
htlcIndex.Record(),
|
||||
}
|
||||
|
||||
tlvStream, err := tlv.NewStream(records...)
|
||||
tlvStream, err := htlc.toTlvStream()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Read the HTLC entry.
|
||||
parsedTypes, err := readTlvStream(r, tlvStream)
|
||||
if err != nil {
|
||||
if _, err := readTlvStream(r, tlvStream); err != nil {
|
||||
// We've reached the end when hitting an EOF.
|
||||
if err == io.ErrUnexpectedEOF {
|
||||
break
|
||||
@ -544,13 +447,13 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil {
|
||||
htlc.CustomBlob = tlv.SomeRecordT(customBlob)
|
||||
// Patch the Incoming field.
|
||||
if htlc.incomingTlv == 1 {
|
||||
htlc.Incoming = true
|
||||
}
|
||||
|
||||
if t, ok := parsedTypes[htlcIndex.TlvType()]; ok && t == nil {
|
||||
htlc.HtlcIndex = tlv.SomeRecordT(htlcIndex)
|
||||
}
|
||||
// Patch the Amt field.
|
||||
htlc.Amt = btcutil.Amount(htlc.amtTlv)
|
||||
|
||||
// Append the entry.
|
||||
htlcs = append(htlcs, &htlc)
|
||||
@ -566,7 +469,6 @@ func writeTlvStream(w io.Writer, s *tlv.Stream) error {
|
||||
if err := s.Encode(&b); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Write the stream's length as a varint.
|
||||
err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{})
|
||||
if err != nil {
|
||||
|
||||
@ -8,7 +8,6 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lntest/channels"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
@ -34,38 +33,17 @@ var (
|
||||
0xff, // value = 255
|
||||
}
|
||||
|
||||
customRecords = lnwire.CustomRecords{
|
||||
lnwire.MinCustomRecordsTlvType + 1: []byte("custom data"),
|
||||
}
|
||||
|
||||
blobBytes = []byte{
|
||||
// Corresponds to the encoded version of the above custom
|
||||
// records.
|
||||
0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73, 0x74,
|
||||
0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
|
||||
}
|
||||
|
||||
testHTLCEntry = HTLCEntry{
|
||||
RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1, uint32](
|
||||
740_000,
|
||||
),
|
||||
OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2, uint16](
|
||||
10,
|
||||
),
|
||||
Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](true),
|
||||
Amt: tlv.NewRecordT[tlv.TlvType4](
|
||||
tlv.NewBigSizeT(btcutil.Amount(1_000_000)),
|
||||
),
|
||||
CustomBlob: tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType5](blobBytes),
|
||||
),
|
||||
HtlcIndex: tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType6, uint16](0x33),
|
||||
),
|
||||
RefundTimeout: 740_000,
|
||||
OutputIndex: 10,
|
||||
Incoming: true,
|
||||
Amt: 1000_000,
|
||||
amtTlv: 1000_000,
|
||||
incomingTlv: 1,
|
||||
}
|
||||
testHTLCEntryBytes = []byte{
|
||||
// Body length 45.
|
||||
0x2d,
|
||||
// Body length 23.
|
||||
0x16,
|
||||
// Rhash tlv.
|
||||
0x0, 0x0,
|
||||
// RefundTimeout tlv.
|
||||
@ -76,45 +54,6 @@ var (
|
||||
0x3, 0x1, 0x1,
|
||||
// Amt tlv.
|
||||
0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40,
|
||||
// Custom blob tlv.
|
||||
0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73,
|
||||
0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
|
||||
// HLTC index tlv.
|
||||
0x6, 0x2, 0x0, 0x33,
|
||||
}
|
||||
|
||||
testHTLCEntryHash = HTLCEntry{
|
||||
RHash: tlv.NewPrimitiveRecord[tlv.TlvType0](NewSparsePayHash(
|
||||
[32]byte{0x33, 0x44, 0x55},
|
||||
)),
|
||||
RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1, uint32](
|
||||
740_000,
|
||||
),
|
||||
OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2, uint16](
|
||||
10,
|
||||
),
|
||||
Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](true),
|
||||
Amt: tlv.NewRecordT[tlv.TlvType4](
|
||||
tlv.NewBigSizeT(btcutil.Amount(1_000_000)),
|
||||
),
|
||||
}
|
||||
testHTLCEntryHashBytes = []byte{
|
||||
// Body length 54.
|
||||
0x36,
|
||||
// Rhash tlv.
|
||||
0x0, 0x20,
|
||||
0x33, 0x44, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||
// RefundTimeout tlv.
|
||||
0x1, 0x4, 0x0, 0xb, 0x4a, 0xa0,
|
||||
// OutputIndex tlv.
|
||||
0x2, 0x2, 0x0, 0xa,
|
||||
// Incoming tlv.
|
||||
0x3, 0x1, 0x1,
|
||||
// Amt tlv.
|
||||
0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40,
|
||||
}
|
||||
|
||||
localBalance = lnwire.MilliSatoshi(9000)
|
||||
@ -129,29 +68,24 @@ var (
|
||||
CommitTx: channels.TestFundingTx,
|
||||
CommitSig: bytes.Repeat([]byte{1}, 71),
|
||||
Htlcs: []HTLC{{
|
||||
RefundTimeout: testHTLCEntry.RefundTimeout.Val,
|
||||
OutputIndex: int32(testHTLCEntry.OutputIndex.Val),
|
||||
HtlcIndex: uint64(
|
||||
testHTLCEntry.HtlcIndex.ValOpt().
|
||||
UnsafeFromSome(),
|
||||
),
|
||||
Incoming: testHTLCEntry.Incoming.Val,
|
||||
RefundTimeout: testHTLCEntry.RefundTimeout,
|
||||
OutputIndex: int32(testHTLCEntry.OutputIndex),
|
||||
Incoming: testHTLCEntry.Incoming,
|
||||
Amt: lnwire.NewMSatFromSatoshis(
|
||||
testHTLCEntry.Amt.Val.Int(),
|
||||
testHTLCEntry.Amt,
|
||||
),
|
||||
CustomRecords: customRecords,
|
||||
}},
|
||||
CustomBlob: fn.Some(blobBytes),
|
||||
}
|
||||
|
||||
testRevocationLogNoAmts = NewRevocationLog(
|
||||
0, 1, testChannelCommit.CommitTx.TxHash(),
|
||||
fn.None[lnwire.MilliSatoshi](), fn.None[lnwire.MilliSatoshi](),
|
||||
[]*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes),
|
||||
)
|
||||
testRevocationLogNoAmts = RevocationLog{
|
||||
OurOutputIndex: 0,
|
||||
TheirOutputIndex: 1,
|
||||
CommitTxHash: testChannelCommit.CommitTx.TxHash(),
|
||||
HTLCEntries: []*HTLCEntry{&testHTLCEntry},
|
||||
}
|
||||
testRevocationLogNoAmtsBytes = []byte{
|
||||
// Body length 61.
|
||||
0x3d,
|
||||
// Body length 42.
|
||||
0x2a,
|
||||
// OurOutputIndex tlv.
|
||||
0x0, 0x2, 0x0, 0x0,
|
||||
// TheirOutputIndex tlv.
|
||||
@ -162,19 +96,19 @@ var (
|
||||
0x6e, 0x60, 0x29, 0x23, 0x1d, 0x5e, 0xc5, 0xe6,
|
||||
0xbd, 0xf7, 0xd3, 0x9b, 0x16, 0x7d, 0x0, 0xff,
|
||||
0xc8, 0x22, 0x51, 0xb1, 0x5b, 0xa0, 0xbf, 0xd,
|
||||
// Custom blob tlv.
|
||||
0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73,
|
||||
0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
|
||||
}
|
||||
|
||||
testRevocationLogWithAmts = NewRevocationLog(
|
||||
0, 1, testChannelCommit.CommitTx.TxHash(),
|
||||
fn.Some(localBalance), fn.Some(remoteBalance),
|
||||
[]*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes),
|
||||
)
|
||||
testRevocationLogWithAmts = RevocationLog{
|
||||
OurOutputIndex: 0,
|
||||
TheirOutputIndex: 1,
|
||||
CommitTxHash: testChannelCommit.CommitTx.TxHash(),
|
||||
HTLCEntries: []*HTLCEntry{&testHTLCEntry},
|
||||
OurBalance: &localBalance,
|
||||
TheirBalance: &remoteBalance,
|
||||
}
|
||||
testRevocationLogWithAmtsBytes = []byte{
|
||||
// Body length 71.
|
||||
0x47,
|
||||
// Body length 52.
|
||||
0x34,
|
||||
// OurOutputIndex tlv.
|
||||
0x0, 0x2, 0x0, 0x0,
|
||||
// TheirOutputIndex tlv.
|
||||
@ -189,9 +123,6 @@ var (
|
||||
0x3, 0x3, 0xfd, 0x23, 0x28,
|
||||
// Remote Balance.
|
||||
0x4, 0x3, 0xfd, 0x0b, 0xb8,
|
||||
// Custom blob tlv.
|
||||
0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73,
|
||||
0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
|
||||
}
|
||||
)
|
||||
|
||||
@ -262,6 +193,11 @@ func TestSerializeHTLCEntriesEmptyRHash(t *testing.T) {
|
||||
// Copy the testHTLCEntry.
|
||||
entry := testHTLCEntry
|
||||
|
||||
// Set the internal fields to empty values so we can test the bytes are
|
||||
// padded.
|
||||
entry.incomingTlv = 0
|
||||
entry.amtTlv = 0
|
||||
|
||||
// Write the tlv stream.
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := serializeHTLCEntries(buf, []*HTLCEntry{&entry})
|
||||
@ -271,21 +207,6 @@ func TestSerializeHTLCEntriesEmptyRHash(t *testing.T) {
|
||||
require.Equal(t, testHTLCEntryBytes, buf.Bytes())
|
||||
}
|
||||
|
||||
func TestSerializeHTLCEntriesWithRHash(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Copy the testHTLCEntry.
|
||||
entry := testHTLCEntryHash
|
||||
|
||||
// Write the tlv stream.
|
||||
buf := bytes.NewBuffer([]byte{})
|
||||
err := serializeHTLCEntries(buf, []*HTLCEntry{&entry})
|
||||
require.NoError(t, err)
|
||||
|
||||
// Check the bytes are read as expected.
|
||||
require.Equal(t, testHTLCEntryHashBytes, buf.Bytes())
|
||||
}
|
||||
|
||||
func TestSerializeHTLCEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
@ -294,7 +215,7 @@ func TestSerializeHTLCEntries(t *testing.T) {
|
||||
|
||||
// Create a fake rHash.
|
||||
rHashBytes := bytes.Repeat([]byte{10}, 32)
|
||||
copy(entry.RHash.Val[:], rHashBytes)
|
||||
copy(entry.RHash[:], rHashBytes)
|
||||
|
||||
// Construct the serialized bytes.
|
||||
//
|
||||
@ -303,7 +224,7 @@ func TestSerializeHTLCEntries(t *testing.T) {
|
||||
partialBytes := testHTLCEntryBytes[3:]
|
||||
|
||||
// Write the total length and RHash tlv.
|
||||
expectedBytes := []byte{0x4d, 0x0, 0x20}
|
||||
expectedBytes := []byte{0x36, 0x0, 0x20}
|
||||
expectedBytes = append(expectedBytes, rHashBytes...)
|
||||
|
||||
// Append the rest.
|
||||
@ -348,7 +269,7 @@ func TestSerializeAndDeserializeRevLog(t *testing.T) {
|
||||
t, &test.revLog, test.revLogBytes,
|
||||
)
|
||||
|
||||
testDeserializeRevocationLog(
|
||||
testDerializeRevocationLog(
|
||||
t, &test.revLog, test.revLogBytes,
|
||||
)
|
||||
})
|
||||
@ -372,7 +293,7 @@ func testSerializeRevocationLog(t *testing.T, rl *RevocationLog,
|
||||
require.Equal(t, revLogBytes, buf.Bytes()[:bodyIndex])
|
||||
}
|
||||
|
||||
func testDeserializeRevocationLog(t *testing.T, revLog *RevocationLog,
|
||||
func testDerializeRevocationLog(t *testing.T, revLog *RevocationLog,
|
||||
revLogBytes []byte) {
|
||||
|
||||
// Construct the full bytes.
|
||||
@ -388,7 +309,7 @@ func testDeserializeRevocationLog(t *testing.T, revLog *RevocationLog,
|
||||
require.Equal(t, *revLog, rl)
|
||||
}
|
||||
|
||||
func TestDeserializeHTLCEntriesEmptyRHash(t *testing.T) {
|
||||
func TestDerializeHTLCEntriesEmptyRHash(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Read the tlv stream.
|
||||
@ -401,7 +322,7 @@ func TestDeserializeHTLCEntriesEmptyRHash(t *testing.T) {
|
||||
require.Equal(t, &testHTLCEntry, htlcs[0])
|
||||
}
|
||||
|
||||
func TestDeserializeHTLCEntries(t *testing.T) {
|
||||
func TestDerializeHTLCEntries(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// Copy the testHTLCEntry.
|
||||
@ -409,7 +330,7 @@ func TestDeserializeHTLCEntries(t *testing.T) {
|
||||
|
||||
// Create a fake rHash.
|
||||
rHashBytes := bytes.Repeat([]byte{10}, 32)
|
||||
copy(entry.RHash.Val[:], rHashBytes)
|
||||
copy(entry.RHash[:], rHashBytes)
|
||||
|
||||
// Construct the serialized bytes.
|
||||
//
|
||||
@ -418,7 +339,7 @@ func TestDeserializeHTLCEntries(t *testing.T) {
|
||||
partialBytes := testHTLCEntryBytes[3:]
|
||||
|
||||
// Write the total length and RHash tlv.
|
||||
testBytes := append([]byte{0x4d, 0x0, 0x20}, rHashBytes...)
|
||||
testBytes := append([]byte{0x36, 0x0, 0x20}, rHashBytes...)
|
||||
|
||||
// Append the rest.
|
||||
testBytes = append(testBytes, partialBytes...)
|
||||
@ -477,11 +398,11 @@ func TestDeleteLogBucket(t *testing.T) {
|
||||
|
||||
err = kvdb.Update(backend, func(tx kvdb.RwTx) error {
|
||||
// Create the buckets.
|
||||
chanBucket, _, err := createTestRevocationLogBuckets(tx)
|
||||
chanBucket, _, err := createTestRevocatoinLogBuckets(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create the buckets again should give us an error.
|
||||
_, _, err = createTestRevocationLogBuckets(tx)
|
||||
_, _, err = createTestRevocatoinLogBuckets(tx)
|
||||
require.ErrorIs(t, err, kvdb.ErrBucketExists)
|
||||
|
||||
// Delete both buckets.
|
||||
@ -489,7 +410,7 @@ func TestDeleteLogBucket(t *testing.T) {
|
||||
require.NoError(t, err)
|
||||
|
||||
// Create the buckets again should give us NO error.
|
||||
_, _, err = createTestRevocationLogBuckets(tx)
|
||||
_, _, err = createTestRevocatoinLogBuckets(tx)
|
||||
return err
|
||||
}, func() {})
|
||||
require.NoError(t, err)
|
||||
@ -595,7 +516,7 @@ func TestPutRevocationLog(t *testing.T) {
|
||||
// Construct the testing db transaction.
|
||||
dbTx := func(tx kvdb.RwTx) (RevocationLog, error) {
|
||||
// Create the buckets.
|
||||
_, bucket, err := createTestRevocationLogBuckets(tx)
|
||||
_, bucket, err := createTestRevocatoinLogBuckets(tx)
|
||||
require.NoError(t, err)
|
||||
|
||||
// Save the log.
|
||||
@ -765,7 +686,7 @@ func TestFetchRevocationLogCompatible(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func createTestRevocationLogBuckets(tx kvdb.RwTx) (kvdb.RwBucket,
|
||||
func createTestRevocatoinLogBuckets(tx kvdb.RwTx) (kvdb.RwBucket,
|
||||
kvdb.RwBucket, error) {
|
||||
|
||||
chanBucket, err := tx.CreateTopLevelBucket(openChannelBucket)
|
||||
|
||||
@ -1,601 +0,0 @@
|
||||
// Copyright (c) 2013-2017 The btcsuite developers
|
||||
// Copyright (c) 2015-2016 The Decred developers
|
||||
// Copyright (C) 2015-2024 The Lightning Network Developers
|
||||
|
||||
package commands
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/lightningnetwork/lnd"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/tor"
|
||||
"github.com/urfave/cli"
|
||||
"golang.org/x/term"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDataDir = "data"
|
||||
defaultChainSubDir = "chain"
|
||||
defaultTLSCertFilename = "tls.cert"
|
||||
defaultMacaroonFilename = "admin.macaroon"
|
||||
defaultRPCPort = "10009"
|
||||
defaultRPCHostPort = "localhost:" + defaultRPCPort
|
||||
|
||||
envVarRPCServer = "LNCLI_RPCSERVER"
|
||||
envVarLNDDir = "LNCLI_LNDDIR"
|
||||
envVarSOCKSProxy = "LNCLI_SOCKSPROXY"
|
||||
envVarTLSCertPath = "LNCLI_TLSCERTPATH"
|
||||
envVarChain = "LNCLI_CHAIN"
|
||||
envVarNetwork = "LNCLI_NETWORK"
|
||||
envVarMacaroonPath = "LNCLI_MACAROONPATH"
|
||||
envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT"
|
||||
envVarMacaroonIP = "LNCLI_MACAROONIP"
|
||||
envVarProfile = "LNCLI_PROFILE"
|
||||
envVarMacFromJar = "LNCLI_MACFROMJAR"
|
||||
)
|
||||
|
||||
var (
|
||||
DefaultLndDir = btcutil.AppDataDir("lnd", false)
|
||||
defaultTLSCertPath = filepath.Join(
|
||||
DefaultLndDir, defaultTLSCertFilename,
|
||||
)
|
||||
|
||||
// maxMsgRecvSize is the largest message our client will receive. We
|
||||
// set this to 200MiB atm.
|
||||
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize)
|
||||
)
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintf(os.Stderr, "[lncli] %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient,
|
||||
func()) {
|
||||
|
||||
conn := getClientConn(ctx, true)
|
||||
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return lnrpc.NewWalletUnlockerClient(conn), cleanUp
|
||||
}
|
||||
|
||||
func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) {
|
||||
conn := getClientConn(ctx, true)
|
||||
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return lnrpc.NewStateClient(conn), cleanUp
|
||||
}
|
||||
|
||||
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
||||
conn := getClientConn(ctx, false)
|
||||
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return lnrpc.NewLightningClient(conn), cleanUp
|
||||
}
|
||||
|
||||
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
||||
// First, we'll get the selected stored profile or an ephemeral one
|
||||
// created from the global options in the CLI context.
|
||||
profile, err := getGlobalOptions(ctx, skipMacaroons)
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("could not load global options: %w", err))
|
||||
}
|
||||
|
||||
// Create a dial options array.
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithUnaryInterceptor(
|
||||
addMetadataUnaryInterceptor(profile.Metadata),
|
||||
),
|
||||
grpc.WithStreamInterceptor(
|
||||
addMetaDataStreamInterceptor(profile.Metadata),
|
||||
),
|
||||
}
|
||||
|
||||
if profile.Insecure {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
} else {
|
||||
// Load the specified TLS certificate.
|
||||
certPool, err := profile.cert()
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("could not create cert pool: %w", err))
|
||||
}
|
||||
|
||||
// Build transport credentials from the certificate pool. If
|
||||
// there is no certificate pool, we expect the server to use a
|
||||
// non-self-signed certificate such as a certificate obtained
|
||||
// from Let's Encrypt.
|
||||
var creds credentials.TransportCredentials
|
||||
if certPool != nil {
|
||||
creds = credentials.NewClientTLSFromCert(certPool, "")
|
||||
} else {
|
||||
// Fallback to the system pool. Using an empty tls
|
||||
// config is an alternative to x509.SystemCertPool().
|
||||
// That call is not supported on Windows.
|
||||
creds = credentials.NewTLS(&tls.Config{})
|
||||
}
|
||||
|
||||
opts = append(opts, grpc.WithTransportCredentials(creds))
|
||||
}
|
||||
|
||||
// Only process macaroon credentials if --no-macaroons isn't set and
|
||||
// if we're not skipping macaroon processing.
|
||||
if !profile.NoMacaroons && !skipMacaroons {
|
||||
// Find out which macaroon to load.
|
||||
macName := profile.Macaroons.Default
|
||||
if ctx.GlobalIsSet("macfromjar") {
|
||||
macName = ctx.GlobalString("macfromjar")
|
||||
}
|
||||
var macEntry *macaroonEntry
|
||||
for _, entry := range profile.Macaroons.Jar {
|
||||
if entry.Name == macName {
|
||||
macEntry = entry
|
||||
break
|
||||
}
|
||||
}
|
||||
if macEntry == nil {
|
||||
fatal(fmt.Errorf("macaroon with name '%s' not found "+
|
||||
"in profile", macName))
|
||||
}
|
||||
|
||||
// Get and possibly decrypt the specified macaroon.
|
||||
//
|
||||
// TODO(guggero): Make it possible to cache the password so we
|
||||
// don't need to ask for it every time.
|
||||
mac, err := macEntry.loadMacaroon(readPassword)
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("could not load macaroon: %w", err))
|
||||
}
|
||||
|
||||
macConstraints := []macaroons.Constraint{
|
||||
// We add a time-based constraint to prevent replay of
|
||||
// the macaroon. It's good for 60 seconds by default to
|
||||
// make up for any discrepancy between client and server
|
||||
// clocks, but leaking the macaroon before it becomes
|
||||
// invalid makes it possible for an attacker to reuse
|
||||
// the macaroon. In addition, the validity time of the
|
||||
// macaroon is extended by the time the server clock is
|
||||
// behind the client clock, or shortened by the time the
|
||||
// server clock is ahead of the client clock (or invalid
|
||||
// altogether if, in the latter case, this time is more
|
||||
// than 60 seconds).
|
||||
// TODO(aakselrod): add better anti-replay protection.
|
||||
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
|
||||
|
||||
// Lock macaroon down to a specific IP address.
|
||||
macaroons.IPLockConstraint(profile.Macaroons.IP),
|
||||
|
||||
// ... Add more constraints if needed.
|
||||
}
|
||||
|
||||
// Apply constraints to the macaroon.
|
||||
constrainedMac, err := macaroons.AddConstraints(
|
||||
mac, macConstraints...,
|
||||
)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
// Now we append the macaroon credentials to the dial options.
|
||||
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("error cloning mac: %w", err))
|
||||
}
|
||||
opts = append(opts, grpc.WithPerRPCCredentials(cred))
|
||||
}
|
||||
|
||||
// If a socksproxy server is specified we use a tor dialer
|
||||
// to connect to the grpc server.
|
||||
if ctx.GlobalIsSet("socksproxy") {
|
||||
socksProxy := ctx.GlobalString("socksproxy")
|
||||
torDialer := func(_ context.Context, addr string) (net.Conn,
|
||||
error) {
|
||||
|
||||
return tor.Dial(
|
||||
addr, socksProxy, false, false,
|
||||
tor.DefaultConnTimeout,
|
||||
)
|
||||
}
|
||||
opts = append(opts, grpc.WithContextDialer(torDialer))
|
||||
} else {
|
||||
// We need to use a custom dialer so we can also connect to
|
||||
// unix sockets and not just TCP addresses.
|
||||
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
|
||||
opts = append(opts, grpc.WithContextDialer(genericDialer))
|
||||
}
|
||||
|
||||
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
|
||||
|
||||
conn, err := grpc.Dial(profile.RPCServer, opts...)
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("unable to connect to RPC server: %w", err))
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// addMetadataUnaryInterceptor returns a grpc client side interceptor that
|
||||
// appends any key-value metadata strings to the outgoing context of a grpc
|
||||
// unary call.
|
||||
func addMetadataUnaryInterceptor(
|
||||
md map[string]string) grpc.UnaryClientInterceptor {
|
||||
|
||||
return func(ctx context.Context, method string, req, reply interface{},
|
||||
cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
|
||||
opts ...grpc.CallOption) error {
|
||||
|
||||
outCtx := contextWithMetadata(ctx, md)
|
||||
return invoker(outCtx, method, req, reply, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// addMetaDataStreamInterceptor returns a grpc client side interceptor that
|
||||
// appends any key-value metadata strings to the outgoing context of a grpc
|
||||
// stream call.
|
||||
func addMetaDataStreamInterceptor(
|
||||
md map[string]string) grpc.StreamClientInterceptor {
|
||||
|
||||
return func(ctx context.Context, desc *grpc.StreamDesc,
|
||||
cc *grpc.ClientConn, method string, streamer grpc.Streamer,
|
||||
opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
|
||||
outCtx := contextWithMetadata(ctx, md)
|
||||
return streamer(outCtx, desc, cc, method, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// contextWithMetaData appends the given metadata key-value pairs to the given
|
||||
// context.
|
||||
func contextWithMetadata(ctx context.Context,
|
||||
md map[string]string) context.Context {
|
||||
|
||||
kvPairs := make([]string, 0, 2*len(md))
|
||||
for k, v := range md {
|
||||
kvPairs = append(kvPairs, k, v)
|
||||
}
|
||||
|
||||
return metadata.AppendToOutgoingContext(ctx, kvPairs...)
|
||||
}
|
||||
|
||||
// extractPathArgs parses the TLS certificate and macaroon paths from the
|
||||
// command.
|
||||
func extractPathArgs(ctx *cli.Context) (string, string, error) {
|
||||
network := strings.ToLower(ctx.GlobalString("network"))
|
||||
switch network {
|
||||
case "mainnet", "testnet", "regtest", "simnet", "signet":
|
||||
default:
|
||||
return "", "", fmt.Errorf("unknown network: %v", network)
|
||||
}
|
||||
|
||||
// We'll now fetch the lnddir so we can make a decision on how to
|
||||
// properly read the macaroons (if needed) and also the cert. This will
|
||||
// either be the default, or will have been overwritten by the end
|
||||
// user.
|
||||
lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir"))
|
||||
|
||||
// If the macaroon path as been manually provided, then we'll only
|
||||
// target the specified file.
|
||||
var macPath string
|
||||
if ctx.GlobalString("macaroonpath") != "" {
|
||||
macPath = lncfg.CleanAndExpandPath(ctx.GlobalString(
|
||||
"macaroonpath",
|
||||
))
|
||||
} else {
|
||||
// Otherwise, we'll go into the path:
|
||||
// lnddir/data/chain/<chain>/<network> in order to fetch the
|
||||
// macaroon that we need.
|
||||
macPath = filepath.Join(
|
||||
lndDir, defaultDataDir, defaultChainSubDir,
|
||||
lnd.BitcoinChainName, network, defaultMacaroonFilename,
|
||||
)
|
||||
}
|
||||
|
||||
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath"))
|
||||
|
||||
// If a custom lnd directory was set, we'll also check if custom paths
|
||||
// for the TLS cert and macaroon file were set as well. If not, we'll
|
||||
// override their paths so they can be found within the custom lnd
|
||||
// directory set. This allows us to set a custom lnd directory, along
|
||||
// with custom paths to the TLS cert and macaroon file.
|
||||
if lndDir != DefaultLndDir {
|
||||
tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
|
||||
}
|
||||
|
||||
return tlsCertPath, macPath, nil
|
||||
}
|
||||
|
||||
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
|
||||
// or flag b can be set, but not both. It returns the name of the flag or an
|
||||
// error.
|
||||
func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) {
|
||||
if ctx.IsSet(a) && ctx.IsSet(b) {
|
||||
return "", fmt.Errorf(
|
||||
"either %s or %s should be set, but not both", a, b,
|
||||
)
|
||||
}
|
||||
|
||||
if ctx.IsSet(a) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func Main() {
|
||||
app := cli.NewApp()
|
||||
app.Name = "lncli"
|
||||
app.Version = build.Version() + " commit=" + build.Commit
|
||||
app.Usage = "control plane for your Lightning Network Daemon (lnd)"
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "rpcserver",
|
||||
Value: defaultRPCHostPort,
|
||||
Usage: "The host:port of LN daemon.",
|
||||
EnvVar: envVarRPCServer,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "lnddir",
|
||||
Value: DefaultLndDir,
|
||||
Usage: "The path to lnd's base directory.",
|
||||
TakesFile: true,
|
||||
EnvVar: envVarLNDDir,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "socksproxy",
|
||||
Usage: "The host:port of a SOCKS proxy through " +
|
||||
"which all connections to the LN " +
|
||||
"daemon will be established over.",
|
||||
EnvVar: envVarSOCKSProxy,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "tlscertpath",
|
||||
Value: defaultTLSCertPath,
|
||||
Usage: "The path to lnd's TLS certificate.",
|
||||
TakesFile: true,
|
||||
EnvVar: envVarTLSCertPath,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "chain, c",
|
||||
Usage: "The chain lnd is running on, e.g. bitcoin.",
|
||||
Value: "bitcoin",
|
||||
EnvVar: envVarChain,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "network, n",
|
||||
Usage: "The network lnd is running on, e.g. mainnet, " +
|
||||
"testnet, etc.",
|
||||
Value: "mainnet",
|
||||
EnvVar: envVarNetwork,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-macaroons",
|
||||
Usage: "Disable macaroon authentication.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "macaroonpath",
|
||||
Usage: "The path to macaroon file.",
|
||||
TakesFile: true,
|
||||
EnvVar: envVarMacaroonPath,
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "macaroontimeout",
|
||||
Value: 60,
|
||||
Usage: "Anti-replay macaroon validity time in " +
|
||||
"seconds.",
|
||||
EnvVar: envVarMacaroonTimeout,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "macaroonip",
|
||||
Usage: "If set, lock macaroon to specific IP address.",
|
||||
EnvVar: envVarMacaroonIP,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "profile, p",
|
||||
Usage: "Instead of reading settings from command " +
|
||||
"line parameters or using the default " +
|
||||
"profile, use a specific profile. If " +
|
||||
"a default profile is set, this flag can be " +
|
||||
"set to an empty string to disable reading " +
|
||||
"values from the profiles file.",
|
||||
EnvVar: envVarProfile,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "macfromjar",
|
||||
Usage: "Use this macaroon from the profile's " +
|
||||
"macaroon jar instead of the default one. " +
|
||||
"Can only be used if profiles are defined.",
|
||||
EnvVar: envVarMacFromJar,
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "metadata",
|
||||
Usage: "This flag can be used to specify a key-value " +
|
||||
"pair that should be appended to the " +
|
||||
"outgoing context before the request is sent " +
|
||||
"to lnd. This flag may be specified multiple " +
|
||||
"times. The format is: \"key:value\".",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "insecure",
|
||||
Usage: "Connect to the rpc server without TLS " +
|
||||
"authentication",
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
app.Commands = []cli.Command{
|
||||
createCommand,
|
||||
createWatchOnlyCommand,
|
||||
unlockCommand,
|
||||
changePasswordCommand,
|
||||
newAddressCommand,
|
||||
estimateFeeCommand,
|
||||
sendManyCommand,
|
||||
sendCoinsCommand,
|
||||
listUnspentCommand,
|
||||
connectCommand,
|
||||
disconnectCommand,
|
||||
openChannelCommand,
|
||||
batchOpenChannelCommand,
|
||||
closeChannelCommand,
|
||||
closeAllChannelsCommand,
|
||||
abandonChannelCommand,
|
||||
listPeersCommand,
|
||||
walletBalanceCommand,
|
||||
ChannelBalanceCommand,
|
||||
getInfoCommand,
|
||||
getDebugInfoCommand,
|
||||
encryptDebugPackageCommand,
|
||||
decryptDebugPackageCommand,
|
||||
getRecoveryInfoCommand,
|
||||
pendingChannelsCommand,
|
||||
SendPaymentCommand,
|
||||
payInvoiceCommand,
|
||||
sendToRouteCommand,
|
||||
AddInvoiceCommand,
|
||||
lookupInvoiceCommand,
|
||||
listInvoicesCommand,
|
||||
ListChannelsCommand,
|
||||
closedChannelsCommand,
|
||||
listPaymentsCommand,
|
||||
describeGraphCommand,
|
||||
getNodeMetricsCommand,
|
||||
getChanInfoCommand,
|
||||
getNodeInfoCommand,
|
||||
queryRoutesCommand,
|
||||
getNetworkInfoCommand,
|
||||
debugLevelCommand,
|
||||
decodePayReqCommand,
|
||||
listChainTxnsCommand,
|
||||
stopCommand,
|
||||
signMessageCommand,
|
||||
verifyMessageCommand,
|
||||
feeReportCommand,
|
||||
updateChannelPolicyCommand,
|
||||
forwardingHistoryCommand,
|
||||
exportChanBackupCommand,
|
||||
verifyChanBackupCommand,
|
||||
restoreChanBackupCommand,
|
||||
bakeMacaroonCommand,
|
||||
listMacaroonIDsCommand,
|
||||
deleteMacaroonIDCommand,
|
||||
listPermissionsCommand,
|
||||
printMacaroonCommand,
|
||||
constrainMacaroonCommand,
|
||||
trackPaymentCommand,
|
||||
versionCommand,
|
||||
profileSubCommand,
|
||||
getStateCommand,
|
||||
deletePaymentsCommand,
|
||||
sendCustomCommand,
|
||||
subscribeCustomCommand,
|
||||
fishCompletionCommand,
|
||||
listAliasesCommand,
|
||||
estimateRouteFeeCommand,
|
||||
generateManPageCommand,
|
||||
}
|
||||
|
||||
// Add any extra commands determined by build flags.
|
||||
app.Commands = append(app.Commands, autopilotCommands()...)
|
||||
app.Commands = append(app.Commands, invoicesCommands()...)
|
||||
app.Commands = append(app.Commands, neutrinoCommands()...)
|
||||
app.Commands = append(app.Commands, routerCommands()...)
|
||||
app.Commands = append(app.Commands, walletCommands()...)
|
||||
app.Commands = append(app.Commands, watchtowerCommands()...)
|
||||
app.Commands = append(app.Commands, wtclientCommands()...)
|
||||
app.Commands = append(app.Commands, devCommands()...)
|
||||
app.Commands = append(app.Commands, peersCommands()...)
|
||||
app.Commands = append(app.Commands, chainCommands()...)
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// readPassword reads a password from the terminal. This requires there to be an
|
||||
// actual TTY so passing in a password from stdin won't work.
|
||||
func readPassword(text string) ([]byte, error) {
|
||||
fmt.Print(text)
|
||||
|
||||
// The variable syscall.Stdin is of a different type in the Windows API
|
||||
// that's why we need the explicit cast. And of course the linter
|
||||
// doesn't like it either.
|
||||
pw, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
|
||||
fmt.Println()
|
||||
|
||||
return pw, err
|
||||
}
|
||||
|
||||
// networkParams parses the global network flag into a chaincfg.Params.
|
||||
func networkParams(ctx *cli.Context) (*chaincfg.Params, error) {
|
||||
network := strings.ToLower(ctx.GlobalString("network"))
|
||||
switch network {
|
||||
case "mainnet":
|
||||
return &chaincfg.MainNetParams, nil
|
||||
|
||||
case "testnet":
|
||||
return &chaincfg.TestNet3Params, nil
|
||||
|
||||
case "regtest":
|
||||
return &chaincfg.RegressionNetParams, nil
|
||||
|
||||
case "simnet":
|
||||
return &chaincfg.SimNetParams, nil
|
||||
|
||||
case "signet":
|
||||
return &chaincfg.SigNetParams, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown network: %v", network)
|
||||
}
|
||||
}
|
||||
|
||||
// parseCoinSelectionStrategy parses a coin selection strategy string
|
||||
// from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type.
|
||||
func parseCoinSelectionStrategy(ctx *cli.Context) (
|
||||
lnrpc.CoinSelectionStrategy, error) {
|
||||
|
||||
strategy := ctx.String(coinSelectionStrategyFlag.Name)
|
||||
if !ctx.IsSet(coinSelectionStrategyFlag.Name) {
|
||||
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
||||
nil
|
||||
}
|
||||
|
||||
switch strategy {
|
||||
case "global-config":
|
||||
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
||||
nil
|
||||
|
||||
case "largest":
|
||||
return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil
|
||||
|
||||
case "random":
|
||||
return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil
|
||||
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown coin selection strategy "+
|
||||
"%v", strategy)
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
@ -42,7 +42,7 @@ func parseTime(s string, base time.Time) (uint64, error) {
|
||||
|
||||
var lightningPrefix = "lightning:"
|
||||
|
||||
// StripPrefix removes accidentally copied 'lightning:' prefix.
|
||||
func StripPrefix(s string) string {
|
||||
// stripPrefix removes accidentally copied 'lightning:' prefix.
|
||||
func stripPrefix(s string) string {
|
||||
return strings.TrimSpace(strings.TrimPrefix(s, lightningPrefix))
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"testing"
|
||||
@ -111,7 +111,7 @@ func TestStripPrefix(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
for _, test := range stripPrefixTests {
|
||||
actual := StripPrefix(test.in)
|
||||
actual := stripPrefix(test.in)
|
||||
require.Equal(t, test.expected, actual)
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build autopilotrpc
|
||||
// +build autopilotrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build !autopilotrpc
|
||||
// +build !autopilotrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build chainrpc
|
||||
// +build chainrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build !chainrpc
|
||||
// +build !chainrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@ -9,7 +9,7 @@ import (
|
||||
"github.com/urfave/cli"
|
||||
)
|
||||
|
||||
var AddInvoiceCommand = cli.Command{
|
||||
var addInvoiceCommand = cli.Command{
|
||||
Name: "addinvoice",
|
||||
Category: "Invoices",
|
||||
Usage: "Add a new invoice.",
|
||||
@ -408,7 +408,7 @@ func decodePayReq(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
resp, err := client.DecodePayReq(ctxc, &lnrpc.PayReqString{
|
||||
PayReq: StripPrefix(payreq),
|
||||
PayReq: stripPrefix(payreq),
|
||||
})
|
||||
if err != nil {
|
||||
return err
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -265,7 +265,6 @@ func setCfg(ctx *cli.Context) error {
|
||||
Config: mcCfg.Config,
|
||||
},
|
||||
)
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
@ -367,6 +366,5 @@ func resetMissionControl(ctx *cli.Context) error {
|
||||
|
||||
req := &routerrpc.ResetMissionControlRequest{}
|
||||
_, err := client.ResetMissionControl(ctxc, req)
|
||||
|
||||
return err
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -25,7 +25,6 @@ import (
|
||||
"github.com/lightningnetwork/lnd/record"
|
||||
"github.com/lightningnetwork/lnd/routing/route"
|
||||
"github.com/urfave/cli"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -153,8 +152,8 @@ var (
|
||||
}
|
||||
)
|
||||
|
||||
// PaymentFlags returns common flags for sendpayment and payinvoice.
|
||||
func PaymentFlags() []cli.Flag {
|
||||
// paymentFlags returns common flags for sendpayment and payinvoice.
|
||||
func paymentFlags() []cli.Flag {
|
||||
return []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "pay_req",
|
||||
@ -203,7 +202,7 @@ func PaymentFlags() []cli.Flag {
|
||||
}
|
||||
}
|
||||
|
||||
var SendPaymentCommand = cli.Command{
|
||||
var sendPaymentCommand = cli.Command{
|
||||
Name: "sendpayment",
|
||||
Category: "Payments",
|
||||
Usage: "Send a payment over lightning.",
|
||||
@ -227,7 +226,7 @@ var SendPaymentCommand = cli.Command{
|
||||
`,
|
||||
ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
|
||||
"--pay_req=R [--pay_addr=H]",
|
||||
Flags: append(PaymentFlags(),
|
||||
Flags: append(paymentFlags(),
|
||||
cli.StringFlag{
|
||||
Name: "dest, d",
|
||||
Usage: "the compressed identity pubkey of the " +
|
||||
@ -254,7 +253,7 @@ var SendPaymentCommand = cli.Command{
|
||||
Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
|
||||
},
|
||||
),
|
||||
Action: SendPayment,
|
||||
Action: sendPayment,
|
||||
}
|
||||
|
||||
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
|
||||
@ -325,23 +324,20 @@ func parsePayAddr(ctx *cli.Context, args cli.Args) ([]byte, error) {
|
||||
return payAddr, nil
|
||||
}
|
||||
|
||||
func SendPayment(ctx *cli.Context) error {
|
||||
func sendPayment(ctx *cli.Context) error {
|
||||
// Show command help if no arguments provided
|
||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||
_ = cli.ShowCommandHelp(ctx, "sendpayment")
|
||||
return nil
|
||||
}
|
||||
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
// If a payment request was provided, we can exit early since all of the
|
||||
// details of the payment are encoded within the request.
|
||||
if ctx.IsSet("pay_req") {
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: StripPrefix(ctx.String("pay_req")),
|
||||
PaymentRequest: stripPrefix(ctx.String("pay_req")),
|
||||
Amt: ctx.Int64("amt"),
|
||||
DestCustomRecords: make(map[uint64][]byte),
|
||||
Amp: ctx.Bool(ampFlag.Name),
|
||||
@ -361,9 +357,7 @@ func SendPayment(ctx *cli.Context) error {
|
||||
|
||||
req.PaymentAddr = payAddr
|
||||
|
||||
return SendPaymentRequest(
|
||||
ctx, req, conn, conn, routerRPCSendPayment,
|
||||
)
|
||||
return sendPaymentRequest(ctx, req)
|
||||
}
|
||||
|
||||
var (
|
||||
@ -472,29 +466,19 @@ func SendPayment(ctx *cli.Context) error {
|
||||
|
||||
req.PaymentAddr = payAddr
|
||||
|
||||
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
|
||||
return sendPaymentRequest(ctx, req)
|
||||
}
|
||||
|
||||
// SendPaymentFn is a function type that abstracts the SendPaymentV2 call of the
|
||||
// router client.
|
||||
type SendPaymentFn func(ctx context.Context, payConn grpc.ClientConnInterface,
|
||||
req *routerrpc.SendPaymentRequest) (PaymentResultStream, error)
|
||||
|
||||
// routerRPCSendPayment is the default implementation of the SendPaymentFn type
|
||||
// that uses the lnd routerrpc.SendPaymentV2 call.
|
||||
func routerRPCSendPayment(ctx context.Context, payConn grpc.ClientConnInterface,
|
||||
req *routerrpc.SendPaymentRequest) (PaymentResultStream, error) {
|
||||
|
||||
return routerrpc.NewRouterClient(payConn).SendPaymentV2(ctx, req)
|
||||
}
|
||||
|
||||
func SendPaymentRequest(ctx *cli.Context, req *routerrpc.SendPaymentRequest,
|
||||
lnConn, paymentConn grpc.ClientConnInterface,
|
||||
callSendPayment SendPaymentFn) error {
|
||||
func sendPaymentRequest(ctx *cli.Context,
|
||||
req *routerrpc.SendPaymentRequest) error {
|
||||
|
||||
ctxc := getContext()
|
||||
|
||||
lnClient := lnrpc.NewLightningClient(lnConn)
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
client := lnrpc.NewLightningClient(conn)
|
||||
routerClient := routerrpc.NewRouterClient(conn)
|
||||
|
||||
outChan := ctx.Int64Slice("outgoing_chan_id")
|
||||
if len(outChan) != 0 {
|
||||
@ -574,7 +558,7 @@ func SendPaymentRequest(ctx *cli.Context, req *routerrpc.SendPaymentRequest,
|
||||
if req.PaymentRequest != "" {
|
||||
// Decode payment request to find out the amount.
|
||||
decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
|
||||
decodeResp, err := lnClient.DecodePayReq(ctxc, decodeReq)
|
||||
decodeResp, err := client.DecodePayReq(ctxc, decodeReq)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -618,12 +602,14 @@ func SendPaymentRequest(ctx *cli.Context, req *routerrpc.SendPaymentRequest,
|
||||
printJSON := ctx.Bool(jsonFlag.Name)
|
||||
req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
|
||||
|
||||
stream, err := callSendPayment(ctxc, paymentConn, req)
|
||||
stream, err := routerClient.SendPaymentV2(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
finalState, err := PrintLivePayment(ctxc, stream, lnClient, printJSON)
|
||||
finalState, err := printLivePayment(
|
||||
ctxc, stream, client, printJSON,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -681,29 +667,24 @@ func trackPayment(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
client := lnrpc.NewLightningClient(conn)
|
||||
_, err = PrintLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
|
||||
_, err = printLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
|
||||
return err
|
||||
}
|
||||
|
||||
// PaymentResultStream is an interface that abstracts the Recv method of the
|
||||
// SendPaymentV2 or TrackPaymentV2 client stream.
|
||||
type PaymentResultStream interface {
|
||||
Recv() (*lnrpc.Payment, error)
|
||||
}
|
||||
|
||||
// PrintLivePayment receives payment updates from the given stream and either
|
||||
// printLivePayment receives payment updates from the given stream and either
|
||||
// outputs them as json or as a more user-friendly formatted table. The table
|
||||
// option uses terminal control codes to rewrite the output. This call
|
||||
// terminates when the payment reaches a final state.
|
||||
func PrintLivePayment(ctxc context.Context, stream PaymentResultStream,
|
||||
lnClient lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
|
||||
func printLivePayment(ctxc context.Context,
|
||||
stream routerrpc.Router_TrackPaymentV2Client,
|
||||
client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
|
||||
|
||||
// Terminal escape codes aren't supported on Windows, fall back to json.
|
||||
if !json && runtime.GOOS == "windows" {
|
||||
json = true
|
||||
}
|
||||
|
||||
aliases := newAliasCache(lnClient)
|
||||
aliases := newAliasCache(client)
|
||||
|
||||
first := true
|
||||
var lastLineCount int
|
||||
@ -725,17 +706,17 @@ func PrintLivePayment(ctxc context.Context, stream PaymentResultStream,
|
||||
// Write raw json to stdout.
|
||||
printRespJSON(payment)
|
||||
} else {
|
||||
resultTable := formatPayment(ctxc, payment, aliases)
|
||||
table := formatPayment(ctxc, payment, aliases)
|
||||
|
||||
// Clear all previously written lines and print the
|
||||
// updated table.
|
||||
clearLines(lastLineCount)
|
||||
fmt.Print(resultTable)
|
||||
fmt.Print(table)
|
||||
|
||||
// Store the number of lines written for the next update
|
||||
// pass.
|
||||
lastLineCount = 0
|
||||
for _, b := range resultTable {
|
||||
for _, b := range table {
|
||||
if b == '\n' {
|
||||
lastLineCount++
|
||||
}
|
||||
@ -893,7 +874,7 @@ var payInvoiceCommand = cli.Command{
|
||||
This command is a shortcut for 'sendpayment --pay_req='.
|
||||
`,
|
||||
ArgsUsage: "pay_req",
|
||||
Flags: append(PaymentFlags(),
|
||||
Flags: append(paymentFlags(),
|
||||
cli.Int64Flag{
|
||||
Name: "amt",
|
||||
Usage: "(optional) number of satoshis to fulfill the " +
|
||||
@ -904,9 +885,6 @@ var payInvoiceCommand = cli.Command{
|
||||
}
|
||||
|
||||
func payInvoice(ctx *cli.Context) error {
|
||||
conn := getClientConn(ctx, false)
|
||||
defer conn.Close()
|
||||
|
||||
args := ctx.Args()
|
||||
|
||||
var payReq string
|
||||
@ -920,14 +898,14 @@ func payInvoice(ctx *cli.Context) error {
|
||||
}
|
||||
|
||||
req := &routerrpc.SendPaymentRequest{
|
||||
PaymentRequest: StripPrefix(payReq),
|
||||
PaymentRequest: stripPrefix(payReq),
|
||||
Amt: ctx.Int64("amt"),
|
||||
DestCustomRecords: make(map[uint64][]byte),
|
||||
Amp: ctx.Bool(ampFlag.Name),
|
||||
Cancelable: ctx.Bool(cancelableFlag.Name),
|
||||
}
|
||||
|
||||
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
|
||||
return sendPaymentRequest(ctx, req)
|
||||
}
|
||||
|
||||
var sendToRouteCommand = cli.Command{
|
||||
@ -1922,7 +1900,7 @@ func estimateRouteFee(ctx *cli.Context) error {
|
||||
req.AmtSat = amtSat
|
||||
|
||||
case ctx.IsSet("pay_req"):
|
||||
req.PaymentRequest = StripPrefix(ctx.String("pay_req"))
|
||||
req.PaymentRequest = stripPrefix(ctx.String("pay_req"))
|
||||
if ctx.IsSet("timeout") {
|
||||
req.Timeout = uint32(ctx.Duration("timeout").Seconds())
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"errors"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
@ -11,7 +11,6 @@ import (
|
||||
"io"
|
||||
"math"
|
||||
"os"
|
||||
"regexp"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
@ -42,49 +41,8 @@ const (
|
||||
defaultUtxoMinConf = 1
|
||||
)
|
||||
|
||||
var (
|
||||
errBadChanPoint = errors.New(
|
||||
"expecting chan_point to be in format of: txid:index",
|
||||
)
|
||||
|
||||
customDataPattern = regexp.MustCompile(
|
||||
`"custom_channel_data":\s*"([0-9a-f]+)"`,
|
||||
)
|
||||
)
|
||||
|
||||
// replaceCustomData replaces the custom channel data hex string with the
|
||||
// decoded custom channel data in the JSON response.
|
||||
func replaceCustomData(jsonBytes []byte) []byte {
|
||||
// If there's nothing to replace, return the original JSON.
|
||||
if !customDataPattern.Match(jsonBytes) {
|
||||
return jsonBytes
|
||||
}
|
||||
|
||||
replacedBytes := customDataPattern.ReplaceAllFunc(
|
||||
jsonBytes, func(match []byte) []byte {
|
||||
encoded := customDataPattern.FindStringSubmatch(
|
||||
string(match),
|
||||
)[1]
|
||||
decoded, err := hex.DecodeString(encoded)
|
||||
if err != nil {
|
||||
return match
|
||||
}
|
||||
|
||||
return []byte("\"custom_channel_data\":" +
|
||||
string(decoded))
|
||||
},
|
||||
)
|
||||
|
||||
var buf bytes.Buffer
|
||||
err := json.Indent(&buf, replacedBytes, "", " ")
|
||||
if err != nil {
|
||||
// If we can't indent the JSON, it likely means the replacement
|
||||
// data wasn't correct, so we return the original JSON.
|
||||
return jsonBytes
|
||||
}
|
||||
|
||||
return buf.Bytes()
|
||||
}
|
||||
var errBadChanPoint = errors.New("expecting chan_point to be in format of: " +
|
||||
"txid:index")
|
||||
|
||||
func getContext() context.Context {
|
||||
shutdownInterceptor, err := signal.Intercept()
|
||||
@ -108,9 +66,9 @@ func printJSON(resp interface{}) {
|
||||
}
|
||||
|
||||
var out bytes.Buffer
|
||||
_ = json.Indent(&out, b, "", " ")
|
||||
_, _ = out.WriteString("\n")
|
||||
_, _ = out.WriteTo(os.Stdout)
|
||||
json.Indent(&out, b, "", "\t")
|
||||
out.WriteString("\n")
|
||||
out.WriteTo(os.Stdout)
|
||||
}
|
||||
|
||||
func printRespJSON(resp proto.Message) {
|
||||
@ -120,9 +78,7 @@ func printRespJSON(resp proto.Message) {
|
||||
return
|
||||
}
|
||||
|
||||
jsonBytesReplaced := replaceCustomData(jsonBytes)
|
||||
|
||||
fmt.Printf("%s\n", jsonBytesReplaced)
|
||||
fmt.Printf("%s\n", jsonBytes)
|
||||
}
|
||||
|
||||
// actionDecorator is used to add additional information and error handling
|
||||
@ -1486,15 +1442,15 @@ func walletBalance(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ChannelBalanceCommand = cli.Command{
|
||||
var channelBalanceCommand = cli.Command{
|
||||
Name: "channelbalance",
|
||||
Category: "Channels",
|
||||
Usage: "Returns the sum of the total available channel balance across " +
|
||||
"all open channels.",
|
||||
Action: actionDecorator(ChannelBalance),
|
||||
Action: actionDecorator(channelBalance),
|
||||
}
|
||||
|
||||
func ChannelBalance(ctx *cli.Context) error {
|
||||
func channelBalance(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
@ -1619,7 +1575,7 @@ func pendingChannels(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var ListChannelsCommand = cli.Command{
|
||||
var listChannelsCommand = cli.Command{
|
||||
Name: "listchannels",
|
||||
Category: "Channels",
|
||||
Usage: "List all open channels.",
|
||||
@ -1652,7 +1608,7 @@ var ListChannelsCommand = cli.Command{
|
||||
"order to improve performance",
|
||||
},
|
||||
},
|
||||
Action: actionDecorator(ListChannels),
|
||||
Action: actionDecorator(listChannels),
|
||||
}
|
||||
|
||||
var listAliasesCommand = cli.Command{
|
||||
@ -1660,10 +1616,10 @@ var listAliasesCommand = cli.Command{
|
||||
Category: "Channels",
|
||||
Usage: "List all aliases.",
|
||||
Flags: []cli.Flag{},
|
||||
Action: actionDecorator(listAliases),
|
||||
Action: actionDecorator(listaliases),
|
||||
}
|
||||
|
||||
func listAliases(ctx *cli.Context) error {
|
||||
func listaliases(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
@ -1680,7 +1636,7 @@ func listAliases(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ListChannels(ctx *cli.Context) error {
|
||||
func listChannels(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
client, cleanUp := getClient(ctx)
|
||||
defer cleanUp()
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@ -120,74 +120,3 @@ func TestParseTimeLockDelta(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestReplaceCustomData tests that hex encoded custom data can be formatted as
|
||||
// JSON in the console output.
|
||||
func TestReplaceCustomData(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := []struct {
|
||||
name string
|
||||
data string
|
||||
replaceData string
|
||||
expected string
|
||||
}{
|
||||
{
|
||||
name: "no replacement necessary",
|
||||
data: "foo",
|
||||
expected: "foo",
|
||||
},
|
||||
{
|
||||
name: "valid json with replacement",
|
||||
data: "{\"foo\":\"bar\",\"custom_channel_data\":\"" +
|
||||
hex.EncodeToString([]byte(
|
||||
"{\"bar\":\"baz\"}",
|
||||
)) + "\"}",
|
||||
expected: `{
|
||||
"foo": "bar",
|
||||
"custom_channel_data": {
|
||||
"bar": "baz"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "valid json with replacement and space",
|
||||
data: "{\"foo\":\"bar\",\"custom_channel_data\": \"" +
|
||||
hex.EncodeToString([]byte(
|
||||
"{\"bar\":\"baz\"}",
|
||||
)) + "\"}",
|
||||
expected: `{
|
||||
"foo": "bar",
|
||||
"custom_channel_data": {
|
||||
"bar": "baz"
|
||||
}
|
||||
}`,
|
||||
},
|
||||
{
|
||||
name: "doesn't match pattern, returned identical",
|
||||
data: "this ain't even json, and no custom data " +
|
||||
"either",
|
||||
expected: "this ain't even json, and no custom data " +
|
||||
"either",
|
||||
},
|
||||
{
|
||||
name: "invalid json",
|
||||
data: "this ain't json, " +
|
||||
"\"custom_channel_data\":\"a\"",
|
||||
expected: "this ain't json, " +
|
||||
"\"custom_channel_data\":\"a\"",
|
||||
},
|
||||
{
|
||||
name: "valid json, invalid hex, just formatted",
|
||||
data: "{\"custom_channel_data\":\"f\"}",
|
||||
expected: "{\n \"custom_channel_data\": \"f\"\n}",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
result := replaceCustomData([]byte(tc.data))
|
||||
require.Equal(t, tc.expected, string(result))
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build dev
|
||||
// +build dev
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build !dev
|
||||
// +build !dev
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build invoicesrpc
|
||||
// +build invoicesrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build !invoicesrpc
|
||||
// +build !invoicesrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@ -1,11 +1,594 @@
|
||||
// Copyright (c) 2013-2017 The btcsuite developers
|
||||
// Copyright (c) 2015-2016 The Decred developers
|
||||
// Copyright (C) 2015-2024 The Lightning Network Developers
|
||||
// Copyright (C) 2015-2022 The Lightning Network Developers
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/lightningnetwork/lnd/cmd/commands"
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/btcsuite/btcd/btcutil"
|
||||
"github.com/btcsuite/btcd/chaincfg"
|
||||
"github.com/lightningnetwork/lnd"
|
||||
"github.com/lightningnetwork/lnd/build"
|
||||
"github.com/lightningnetwork/lnd/lncfg"
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/tor"
|
||||
"github.com/urfave/cli"
|
||||
"golang.org/x/term"
|
||||
"google.golang.org/grpc"
|
||||
"google.golang.org/grpc/credentials"
|
||||
"google.golang.org/grpc/metadata"
|
||||
)
|
||||
|
||||
const (
|
||||
defaultDataDir = "data"
|
||||
defaultChainSubDir = "chain"
|
||||
defaultTLSCertFilename = "tls.cert"
|
||||
defaultMacaroonFilename = "admin.macaroon"
|
||||
defaultRPCPort = "10009"
|
||||
defaultRPCHostPort = "localhost:" + defaultRPCPort
|
||||
|
||||
envVarRPCServer = "LNCLI_RPCSERVER"
|
||||
envVarLNDDir = "LNCLI_LNDDIR"
|
||||
envVarSOCKSProxy = "LNCLI_SOCKSPROXY"
|
||||
envVarTLSCertPath = "LNCLI_TLSCERTPATH"
|
||||
envVarChain = "LNCLI_CHAIN"
|
||||
envVarNetwork = "LNCLI_NETWORK"
|
||||
envVarMacaroonPath = "LNCLI_MACAROONPATH"
|
||||
envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT"
|
||||
envVarMacaroonIP = "LNCLI_MACAROONIP"
|
||||
envVarProfile = "LNCLI_PROFILE"
|
||||
envVarMacFromJar = "LNCLI_MACFROMJAR"
|
||||
)
|
||||
|
||||
var (
|
||||
defaultLndDir = btcutil.AppDataDir("lnd", false)
|
||||
defaultTLSCertPath = filepath.Join(defaultLndDir, defaultTLSCertFilename)
|
||||
|
||||
// maxMsgRecvSize is the largest message our client will receive. We
|
||||
// set this to 200MiB atm.
|
||||
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize)
|
||||
)
|
||||
|
||||
func fatal(err error) {
|
||||
fmt.Fprintf(os.Stderr, "[lncli] %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func()) {
|
||||
conn := getClientConn(ctx, true)
|
||||
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return lnrpc.NewWalletUnlockerClient(conn), cleanUp
|
||||
}
|
||||
|
||||
func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) {
|
||||
conn := getClientConn(ctx, true)
|
||||
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return lnrpc.NewStateClient(conn), cleanUp
|
||||
}
|
||||
|
||||
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
||||
conn := getClientConn(ctx, false)
|
||||
|
||||
cleanUp := func() {
|
||||
conn.Close()
|
||||
}
|
||||
|
||||
return lnrpc.NewLightningClient(conn), cleanUp
|
||||
}
|
||||
|
||||
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
||||
// First, we'll get the selected stored profile or an ephemeral one
|
||||
// created from the global options in the CLI context.
|
||||
profile, err := getGlobalOptions(ctx, skipMacaroons)
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("could not load global options: %w", err))
|
||||
}
|
||||
|
||||
// Create a dial options array.
|
||||
opts := []grpc.DialOption{
|
||||
grpc.WithUnaryInterceptor(
|
||||
addMetadataUnaryInterceptor(profile.Metadata),
|
||||
),
|
||||
grpc.WithStreamInterceptor(
|
||||
addMetaDataStreamInterceptor(profile.Metadata),
|
||||
),
|
||||
}
|
||||
|
||||
if profile.Insecure {
|
||||
opts = append(opts, grpc.WithInsecure())
|
||||
} else {
|
||||
// Load the specified TLS certificate.
|
||||
certPool, err := profile.cert()
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("could not create cert pool: %w", err))
|
||||
}
|
||||
|
||||
// Build transport credentials from the certificate pool. If
|
||||
// there is no certificate pool, we expect the server to use a
|
||||
// non-self-signed certificate such as a certificate obtained
|
||||
// from Let's Encrypt.
|
||||
var creds credentials.TransportCredentials
|
||||
if certPool != nil {
|
||||
creds = credentials.NewClientTLSFromCert(certPool, "")
|
||||
} else {
|
||||
// Fallback to the system pool. Using an empty tls
|
||||
// config is an alternative to x509.SystemCertPool().
|
||||
// That call is not supported on Windows.
|
||||
creds = credentials.NewTLS(&tls.Config{})
|
||||
}
|
||||
|
||||
opts = append(opts, grpc.WithTransportCredentials(creds))
|
||||
}
|
||||
|
||||
// Only process macaroon credentials if --no-macaroons isn't set and
|
||||
// if we're not skipping macaroon processing.
|
||||
if !profile.NoMacaroons && !skipMacaroons {
|
||||
// Find out which macaroon to load.
|
||||
macName := profile.Macaroons.Default
|
||||
if ctx.GlobalIsSet("macfromjar") {
|
||||
macName = ctx.GlobalString("macfromjar")
|
||||
}
|
||||
var macEntry *macaroonEntry
|
||||
for _, entry := range profile.Macaroons.Jar {
|
||||
if entry.Name == macName {
|
||||
macEntry = entry
|
||||
break
|
||||
}
|
||||
}
|
||||
if macEntry == nil {
|
||||
fatal(fmt.Errorf("macaroon with name '%s' not found "+
|
||||
"in profile", macName))
|
||||
}
|
||||
|
||||
// Get and possibly decrypt the specified macaroon.
|
||||
//
|
||||
// TODO(guggero): Make it possible to cache the password so we
|
||||
// don't need to ask for it every time.
|
||||
mac, err := macEntry.loadMacaroon(readPassword)
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("could not load macaroon: %w", err))
|
||||
}
|
||||
|
||||
macConstraints := []macaroons.Constraint{
|
||||
// We add a time-based constraint to prevent replay of
|
||||
// the macaroon. It's good for 60 seconds by default to
|
||||
// make up for any discrepancy between client and server
|
||||
// clocks, but leaking the macaroon before it becomes
|
||||
// invalid makes it possible for an attacker to reuse
|
||||
// the macaroon. In addition, the validity time of the
|
||||
// macaroon is extended by the time the server clock is
|
||||
// behind the client clock, or shortened by the time the
|
||||
// server clock is ahead of the client clock (or invalid
|
||||
// altogether if, in the latter case, this time is more
|
||||
// than 60 seconds).
|
||||
// TODO(aakselrod): add better anti-replay protection.
|
||||
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
|
||||
|
||||
// Lock macaroon down to a specific IP address.
|
||||
macaroons.IPLockConstraint(profile.Macaroons.IP),
|
||||
|
||||
// ... Add more constraints if needed.
|
||||
}
|
||||
|
||||
// Apply constraints to the macaroon.
|
||||
constrainedMac, err := macaroons.AddConstraints(
|
||||
mac, macConstraints...,
|
||||
)
|
||||
if err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
|
||||
// Now we append the macaroon credentials to the dial options.
|
||||
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("error cloning mac: %w", err))
|
||||
}
|
||||
opts = append(opts, grpc.WithPerRPCCredentials(cred))
|
||||
}
|
||||
|
||||
// If a socksproxy server is specified we use a tor dialer
|
||||
// to connect to the grpc server.
|
||||
if ctx.GlobalIsSet("socksproxy") {
|
||||
socksProxy := ctx.GlobalString("socksproxy")
|
||||
torDialer := func(_ context.Context, addr string) (net.Conn,
|
||||
error) {
|
||||
|
||||
return tor.Dial(
|
||||
addr, socksProxy, false, false,
|
||||
tor.DefaultConnTimeout,
|
||||
)
|
||||
}
|
||||
opts = append(opts, grpc.WithContextDialer(torDialer))
|
||||
} else {
|
||||
// We need to use a custom dialer so we can also connect to
|
||||
// unix sockets and not just TCP addresses.
|
||||
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
|
||||
opts = append(opts, grpc.WithContextDialer(genericDialer))
|
||||
}
|
||||
|
||||
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
|
||||
|
||||
conn, err := grpc.Dial(profile.RPCServer, opts...)
|
||||
if err != nil {
|
||||
fatal(fmt.Errorf("unable to connect to RPC server: %w", err))
|
||||
}
|
||||
|
||||
return conn
|
||||
}
|
||||
|
||||
// addMetadataUnaryInterceptor returns a grpc client side interceptor that
|
||||
// appends any key-value metadata strings to the outgoing context of a grpc
|
||||
// unary call.
|
||||
func addMetadataUnaryInterceptor(
|
||||
md map[string]string) grpc.UnaryClientInterceptor {
|
||||
|
||||
return func(ctx context.Context, method string, req, reply interface{},
|
||||
cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
|
||||
opts ...grpc.CallOption) error {
|
||||
|
||||
outCtx := contextWithMetadata(ctx, md)
|
||||
return invoker(outCtx, method, req, reply, cc, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// addMetaDataStreamInterceptor returns a grpc client side interceptor that
|
||||
// appends any key-value metadata strings to the outgoing context of a grpc
|
||||
// stream call.
|
||||
func addMetaDataStreamInterceptor(
|
||||
md map[string]string) grpc.StreamClientInterceptor {
|
||||
|
||||
return func(ctx context.Context, desc *grpc.StreamDesc,
|
||||
cc *grpc.ClientConn, method string, streamer grpc.Streamer,
|
||||
opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||
|
||||
outCtx := contextWithMetadata(ctx, md)
|
||||
return streamer(outCtx, desc, cc, method, opts...)
|
||||
}
|
||||
}
|
||||
|
||||
// contextWithMetaData appends the given metadata key-value pairs to the given
|
||||
// context.
|
||||
func contextWithMetadata(ctx context.Context,
|
||||
md map[string]string) context.Context {
|
||||
|
||||
kvPairs := make([]string, 0, 2*len(md))
|
||||
for k, v := range md {
|
||||
kvPairs = append(kvPairs, k, v)
|
||||
}
|
||||
|
||||
return metadata.AppendToOutgoingContext(ctx, kvPairs...)
|
||||
}
|
||||
|
||||
// extractPathArgs parses the TLS certificate and macaroon paths from the
|
||||
// command.
|
||||
func extractPathArgs(ctx *cli.Context) (string, string, error) {
|
||||
network := strings.ToLower(ctx.GlobalString("network"))
|
||||
switch network {
|
||||
case "mainnet", "testnet", "regtest", "simnet", "signet":
|
||||
default:
|
||||
return "", "", fmt.Errorf("unknown network: %v", network)
|
||||
}
|
||||
|
||||
// We'll now fetch the lnddir so we can make a decision on how to
|
||||
// properly read the macaroons (if needed) and also the cert. This will
|
||||
// either be the default, or will have been overwritten by the end
|
||||
// user.
|
||||
lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir"))
|
||||
|
||||
// If the macaroon path as been manually provided, then we'll only
|
||||
// target the specified file.
|
||||
var macPath string
|
||||
if ctx.GlobalString("macaroonpath") != "" {
|
||||
macPath = lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath"))
|
||||
} else {
|
||||
// Otherwise, we'll go into the path:
|
||||
// lnddir/data/chain/<chain>/<network> in order to fetch the
|
||||
// macaroon that we need.
|
||||
macPath = filepath.Join(
|
||||
lndDir, defaultDataDir, defaultChainSubDir,
|
||||
lnd.BitcoinChainName, network, defaultMacaroonFilename,
|
||||
)
|
||||
}
|
||||
|
||||
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath"))
|
||||
|
||||
// If a custom lnd directory was set, we'll also check if custom paths
|
||||
// for the TLS cert and macaroon file were set as well. If not, we'll
|
||||
// override their paths so they can be found within the custom lnd
|
||||
// directory set. This allows us to set a custom lnd directory, along
|
||||
// with custom paths to the TLS cert and macaroon file.
|
||||
if lndDir != defaultLndDir {
|
||||
tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
|
||||
}
|
||||
|
||||
return tlsCertPath, macPath, nil
|
||||
}
|
||||
|
||||
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
|
||||
// or flag b can be set, but not both. It returns the name of the flag or an
|
||||
// error.
|
||||
func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) {
|
||||
if ctx.IsSet(a) && ctx.IsSet(b) {
|
||||
return "", fmt.Errorf(
|
||||
"either %s or %s should be set, but not both", a, b,
|
||||
)
|
||||
}
|
||||
|
||||
if ctx.IsSet(a) {
|
||||
return a, nil
|
||||
}
|
||||
|
||||
return b, nil
|
||||
}
|
||||
|
||||
func main() {
|
||||
commands.Main()
|
||||
app := cli.NewApp()
|
||||
app.Name = "lncli"
|
||||
app.Version = build.Version() + " commit=" + build.Commit
|
||||
app.Usage = "control plane for your Lightning Network Daemon (lnd)"
|
||||
app.Flags = []cli.Flag{
|
||||
cli.StringFlag{
|
||||
Name: "rpcserver",
|
||||
Value: defaultRPCHostPort,
|
||||
Usage: "The host:port of LN daemon.",
|
||||
EnvVar: envVarRPCServer,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "lnddir",
|
||||
Value: defaultLndDir,
|
||||
Usage: "The path to lnd's base directory.",
|
||||
TakesFile: true,
|
||||
EnvVar: envVarLNDDir,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "socksproxy",
|
||||
Usage: "The host:port of a SOCKS proxy through " +
|
||||
"which all connections to the LN " +
|
||||
"daemon will be established over.",
|
||||
EnvVar: envVarSOCKSProxy,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "tlscertpath",
|
||||
Value: defaultTLSCertPath,
|
||||
Usage: "The path to lnd's TLS certificate.",
|
||||
TakesFile: true,
|
||||
EnvVar: envVarTLSCertPath,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "chain, c",
|
||||
Usage: "The chain lnd is running on, e.g. bitcoin.",
|
||||
Value: "bitcoin",
|
||||
EnvVar: envVarChain,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "network, n",
|
||||
Usage: "The network lnd is running on, e.g. mainnet, " +
|
||||
"testnet, etc.",
|
||||
Value: "mainnet",
|
||||
EnvVar: envVarNetwork,
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "no-macaroons",
|
||||
Usage: "Disable macaroon authentication.",
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "macaroonpath",
|
||||
Usage: "The path to macaroon file.",
|
||||
TakesFile: true,
|
||||
EnvVar: envVarMacaroonPath,
|
||||
},
|
||||
cli.Int64Flag{
|
||||
Name: "macaroontimeout",
|
||||
Value: 60,
|
||||
Usage: "Anti-replay macaroon validity time in " +
|
||||
"seconds.",
|
||||
EnvVar: envVarMacaroonTimeout,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "macaroonip",
|
||||
Usage: "If set, lock macaroon to specific IP address.",
|
||||
EnvVar: envVarMacaroonIP,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "profile, p",
|
||||
Usage: "Instead of reading settings from command " +
|
||||
"line parameters or using the default " +
|
||||
"profile, use a specific profile. If " +
|
||||
"a default profile is set, this flag can be " +
|
||||
"set to an empty string to disable reading " +
|
||||
"values from the profiles file.",
|
||||
EnvVar: envVarProfile,
|
||||
},
|
||||
cli.StringFlag{
|
||||
Name: "macfromjar",
|
||||
Usage: "Use this macaroon from the profile's " +
|
||||
"macaroon jar instead of the default one. " +
|
||||
"Can only be used if profiles are defined.",
|
||||
EnvVar: envVarMacFromJar,
|
||||
},
|
||||
cli.StringSliceFlag{
|
||||
Name: "metadata",
|
||||
Usage: "This flag can be used to specify a key-value " +
|
||||
"pair that should be appended to the " +
|
||||
"outgoing context before the request is sent " +
|
||||
"to lnd. This flag may be specified multiple " +
|
||||
"times. The format is: \"key:value\".",
|
||||
},
|
||||
cli.BoolFlag{
|
||||
Name: "insecure",
|
||||
Usage: "Connect to the rpc server without TLS " +
|
||||
"authentication",
|
||||
Hidden: true,
|
||||
},
|
||||
}
|
||||
app.Commands = []cli.Command{
|
||||
createCommand,
|
||||
createWatchOnlyCommand,
|
||||
unlockCommand,
|
||||
changePasswordCommand,
|
||||
newAddressCommand,
|
||||
estimateFeeCommand,
|
||||
sendManyCommand,
|
||||
sendCoinsCommand,
|
||||
listUnspentCommand,
|
||||
connectCommand,
|
||||
disconnectCommand,
|
||||
openChannelCommand,
|
||||
batchOpenChannelCommand,
|
||||
closeChannelCommand,
|
||||
closeAllChannelsCommand,
|
||||
abandonChannelCommand,
|
||||
listPeersCommand,
|
||||
walletBalanceCommand,
|
||||
channelBalanceCommand,
|
||||
getInfoCommand,
|
||||
getDebugInfoCommand,
|
||||
encryptDebugPackageCommand,
|
||||
decryptDebugPackageCommand,
|
||||
getRecoveryInfoCommand,
|
||||
pendingChannelsCommand,
|
||||
sendPaymentCommand,
|
||||
payInvoiceCommand,
|
||||
sendToRouteCommand,
|
||||
addInvoiceCommand,
|
||||
lookupInvoiceCommand,
|
||||
listInvoicesCommand,
|
||||
listChannelsCommand,
|
||||
closedChannelsCommand,
|
||||
listPaymentsCommand,
|
||||
describeGraphCommand,
|
||||
getNodeMetricsCommand,
|
||||
getChanInfoCommand,
|
||||
getNodeInfoCommand,
|
||||
queryRoutesCommand,
|
||||
getNetworkInfoCommand,
|
||||
debugLevelCommand,
|
||||
decodePayReqCommand,
|
||||
listChainTxnsCommand,
|
||||
stopCommand,
|
||||
signMessageCommand,
|
||||
verifyMessageCommand,
|
||||
feeReportCommand,
|
||||
updateChannelPolicyCommand,
|
||||
forwardingHistoryCommand,
|
||||
exportChanBackupCommand,
|
||||
verifyChanBackupCommand,
|
||||
restoreChanBackupCommand,
|
||||
bakeMacaroonCommand,
|
||||
listMacaroonIDsCommand,
|
||||
deleteMacaroonIDCommand,
|
||||
listPermissionsCommand,
|
||||
printMacaroonCommand,
|
||||
constrainMacaroonCommand,
|
||||
trackPaymentCommand,
|
||||
versionCommand,
|
||||
profileSubCommand,
|
||||
getStateCommand,
|
||||
deletePaymentsCommand,
|
||||
sendCustomCommand,
|
||||
subscribeCustomCommand,
|
||||
fishCompletionCommand,
|
||||
listAliasesCommand,
|
||||
estimateRouteFeeCommand,
|
||||
generateManPageCommand,
|
||||
}
|
||||
|
||||
// Add any extra commands determined by build flags.
|
||||
app.Commands = append(app.Commands, autopilotCommands()...)
|
||||
app.Commands = append(app.Commands, invoicesCommands()...)
|
||||
app.Commands = append(app.Commands, neutrinoCommands()...)
|
||||
app.Commands = append(app.Commands, routerCommands()...)
|
||||
app.Commands = append(app.Commands, walletCommands()...)
|
||||
app.Commands = append(app.Commands, watchtowerCommands()...)
|
||||
app.Commands = append(app.Commands, wtclientCommands()...)
|
||||
app.Commands = append(app.Commands, devCommands()...)
|
||||
app.Commands = append(app.Commands, peersCommands()...)
|
||||
app.Commands = append(app.Commands, chainCommands()...)
|
||||
|
||||
if err := app.Run(os.Args); err != nil {
|
||||
fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// readPassword reads a password from the terminal. This requires there to be an
|
||||
// actual TTY so passing in a password from stdin won't work.
|
||||
func readPassword(text string) ([]byte, error) {
|
||||
fmt.Print(text)
|
||||
|
||||
// The variable syscall.Stdin is of a different type in the Windows API
|
||||
// that's why we need the explicit cast. And of course the linter
|
||||
// doesn't like it either.
|
||||
pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
|
||||
fmt.Println()
|
||||
return pw, err
|
||||
}
|
||||
|
||||
// networkParams parses the global network flag into a chaincfg.Params.
|
||||
func networkParams(ctx *cli.Context) (*chaincfg.Params, error) {
|
||||
network := strings.ToLower(ctx.GlobalString("network"))
|
||||
switch network {
|
||||
case "mainnet":
|
||||
return &chaincfg.MainNetParams, nil
|
||||
|
||||
case "testnet":
|
||||
return &chaincfg.TestNet3Params, nil
|
||||
|
||||
case "regtest":
|
||||
return &chaincfg.RegressionNetParams, nil
|
||||
|
||||
case "simnet":
|
||||
return &chaincfg.SimNetParams, nil
|
||||
|
||||
case "signet":
|
||||
return &chaincfg.SigNetParams, nil
|
||||
|
||||
default:
|
||||
return nil, fmt.Errorf("unknown network: %v", network)
|
||||
}
|
||||
}
|
||||
|
||||
// parseCoinSelectionStrategy parses a coin selection strategy string
|
||||
// from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type.
|
||||
func parseCoinSelectionStrategy(ctx *cli.Context) (
|
||||
lnrpc.CoinSelectionStrategy, error) {
|
||||
|
||||
strategy := ctx.String(coinSelectionStrategyFlag.Name)
|
||||
if !ctx.IsSet(coinSelectionStrategyFlag.Name) {
|
||||
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
||||
nil
|
||||
}
|
||||
|
||||
switch strategy {
|
||||
case "global-config":
|
||||
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
||||
nil
|
||||
|
||||
case "largest":
|
||||
return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil
|
||||
|
||||
case "random":
|
||||
return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil
|
||||
|
||||
default:
|
||||
return 0, fmt.Errorf("unknown coin selection strategy "+
|
||||
"%v", strategy)
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,13 +1,11 @@
|
||||
//go:build neutrinorpc
|
||||
// +build neutrinorpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
|
||||
"github.com/urfave/cli"
|
||||
|
||||
"strconv"
|
||||
)
|
||||
|
||||
func getNeutrinoKitClient(ctx *cli.Context) (neutrinorpc.NeutrinoKitClient, func()) {
|
||||
@ -227,47 +225,6 @@ func getCFilter(ctx *cli.Context) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var getBlockHashCommand = cli.Command{
|
||||
Name: "getblockhash",
|
||||
Usage: "Get a block hash.",
|
||||
Category: "Neutrino",
|
||||
Description: "Returns the header hash of a block at a given height.",
|
||||
ArgsUsage: "height",
|
||||
Action: actionDecorator(getBlockHash),
|
||||
}
|
||||
|
||||
func getBlockHash(ctx *cli.Context) error {
|
||||
ctxc := getContext()
|
||||
args := ctx.Args()
|
||||
|
||||
// Display the command's help message if we do not have the expected
|
||||
// number of arguments/flags.
|
||||
if !args.Present() {
|
||||
return cli.ShowCommandHelp(ctx, "getblockhash")
|
||||
}
|
||||
|
||||
client, cleanUp := getNeutrinoKitClient(ctx)
|
||||
defer cleanUp()
|
||||
|
||||
height, err := strconv.ParseInt(args.First(), 10, 32)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
req := &neutrinorpc.GetBlockHashRequest{
|
||||
Height: int32(height),
|
||||
}
|
||||
|
||||
resp, err := client.GetBlockHash(ctxc, req)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
printRespJSON(resp)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// neutrinoCommands will return the set of commands to enable for neutrinorpc
|
||||
// builds.
|
||||
func neutrinoCommands() []cli.Command {
|
||||
@ -284,7 +241,6 @@ func neutrinoCommands() []cli.Command {
|
||||
isBannedCommand,
|
||||
getBlockHeaderNeutrinoCommand,
|
||||
getCFilterCommand,
|
||||
getBlockHashCommand,
|
||||
},
|
||||
},
|
||||
}
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build !neutrinorpc
|
||||
// +build !neutrinorpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build peersrpc
|
||||
// +build peersrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build !peersrpc
|
||||
// +build !peersrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build walletrpc
|
||||
// +build walletrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build !walletrpc
|
||||
// +build !walletrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build watchtowerrpc
|
||||
// +build watchtowerrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
|
||||
@ -1,7 +1,7 @@
|
||||
//go:build !watchtowerrpc
|
||||
// +build !watchtowerrpc
|
||||
|
||||
package commands
|
||||
package main
|
||||
|
||||
import "github.com/urfave/cli"
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
package commands
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/hex"
|
||||
@ -33,9 +33,6 @@ import (
|
||||
"github.com/lightningnetwork/lnd/chainreg"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/clock"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/funding"
|
||||
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||
"github.com/lightningnetwork/lnd/invoices"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
@ -43,14 +40,11 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnrpc"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/rpcwallet"
|
||||
"github.com/lightningnetwork/lnd/macaroons"
|
||||
"github.com/lightningnetwork/lnd/msgmux"
|
||||
"github.com/lightningnetwork/lnd/rpcperms"
|
||||
"github.com/lightningnetwork/lnd/signal"
|
||||
"github.com/lightningnetwork/lnd/sqldb"
|
||||
"github.com/lightningnetwork/lnd/sweep"
|
||||
"github.com/lightningnetwork/lnd/walletunlocker"
|
||||
"github.com/lightningnetwork/lnd/watchtower"
|
||||
"github.com/lightningnetwork/lnd/watchtower/wtclient"
|
||||
@ -109,7 +103,7 @@ type DatabaseBuilder interface {
|
||||
type WalletConfigBuilder interface {
|
||||
// BuildWalletConfig is responsible for creating or unlocking and then
|
||||
// fully initializing a wallet.
|
||||
BuildWalletConfig(context.Context, *DatabaseInstances, *AuxComponents,
|
||||
BuildWalletConfig(context.Context, *DatabaseInstances,
|
||||
*rpcperms.InterceptorChain,
|
||||
[]*ListenerWithSignal) (*chainreg.PartialChainControl,
|
||||
*btcwallet.Config, func(), error)
|
||||
@ -150,52 +144,6 @@ type ImplementationCfg struct {
|
||||
// ChainControlBuilder is a type that can provide a custom wallet
|
||||
// implementation.
|
||||
ChainControlBuilder
|
||||
|
||||
// AuxComponents is a set of auxiliary components that can be used by
|
||||
// lnd for certain custom channel types.
|
||||
AuxComponents
|
||||
}
|
||||
|
||||
// AuxComponents is a set of auxiliary components that can be used by lnd for
|
||||
// certain custom channel types.
|
||||
type AuxComponents struct {
|
||||
// AuxLeafStore is an optional data source that can be used by custom
|
||||
// channels to fetch+store various data.
|
||||
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||
|
||||
// TrafficShaper is an optional traffic shaper that can be used to
|
||||
// control the outgoing channel of a payment.
|
||||
TrafficShaper fn.Option[htlcswitch.AuxTrafficShaper]
|
||||
|
||||
// MsgRouter is an optional message router that if set will be used in
|
||||
// place of a new blank default message router.
|
||||
MsgRouter fn.Option[msgmux.Router]
|
||||
|
||||
// AuxFundingController is an optional controller that can be used to
|
||||
// modify the way we handle certain custom channel types. It's also
|
||||
// able to automatically handle new custom protocol messages related to
|
||||
// the funding process.
|
||||
AuxFundingController fn.Option[funding.AuxFundingController]
|
||||
|
||||
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||
|
||||
// AuxDataParser is an optional data parser that can be used to parse
|
||||
// auxiliary data for certain custom channel types.
|
||||
AuxDataParser fn.Option[AuxDataParser]
|
||||
|
||||
// AuxChanCloser is an optional channel closer that can be used to
|
||||
// modify the way a coop-close transaction is constructed.
|
||||
AuxChanCloser fn.Option[chancloser.AuxChanCloser]
|
||||
|
||||
// AuxSweeper is an optional interface that can be used to modify the
|
||||
// way sweep transaction are generated.
|
||||
AuxSweeper fn.Option[sweep.AuxSweeper]
|
||||
|
||||
// AuxContractResolver is an optional interface that can be used to
|
||||
// modify the way contracts are resolved.
|
||||
AuxContractResolver fn.Option[lnwallet.AuxContractResolver]
|
||||
}
|
||||
|
||||
// DefaultWalletImpl is the default implementation of our normal, btcwallet
|
||||
@ -280,8 +228,7 @@ func (d *DefaultWalletImpl) Permissions() map[string][]bakery.Op {
|
||||
//
|
||||
// NOTE: This is part of the WalletConfigBuilder interface.
|
||||
func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
|
||||
dbs *DatabaseInstances, aux *AuxComponents,
|
||||
interceptorChain *rpcperms.InterceptorChain,
|
||||
dbs *DatabaseInstances, interceptorChain *rpcperms.InterceptorChain,
|
||||
grpcListeners []*ListenerWithSignal) (*chainreg.PartialChainControl,
|
||||
*btcwallet.Config, func(), error) {
|
||||
|
||||
@ -601,8 +548,6 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
|
||||
HeightHintDB: dbs.HeightHintDB,
|
||||
ChanStateDB: dbs.ChanStateDB.ChannelStateDB(),
|
||||
NeutrinoCS: neutrinoCS,
|
||||
AuxLeafStore: aux.AuxLeafStore,
|
||||
AuxSigner: aux.AuxSigner,
|
||||
ActiveNetParams: d.cfg.ActiveNetParams,
|
||||
FeeURL: d.cfg.FeeURL,
|
||||
Fee: &lncfg.Fee{
|
||||
@ -666,9 +611,8 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
|
||||
|
||||
// proxyBlockEpoch proxies a block epoch subsections to the underlying neutrino
|
||||
// rebroadcaster client.
|
||||
func proxyBlockEpoch(
|
||||
notifier chainntnfs.ChainNotifier) func() (*blockntfns.Subscription,
|
||||
error) {
|
||||
func proxyBlockEpoch(notifier chainntnfs.ChainNotifier,
|
||||
) func() (*blockntfns.Subscription, error) {
|
||||
|
||||
return func() (*blockntfns.Subscription, error) {
|
||||
blockEpoch, err := notifier.RegisterBlockEpochNtfn(
|
||||
@ -759,8 +703,6 @@ func (d *DefaultWalletImpl) BuildChainControl(
|
||||
ChainIO: walletController,
|
||||
NetParams: *walletConfig.NetParams,
|
||||
CoinSelectionStrategy: walletConfig.CoinSelectionStrategy,
|
||||
AuxLeafStore: partialChainControl.Cfg.AuxLeafStore,
|
||||
AuxSigner: partialChainControl.Cfg.AuxSigner,
|
||||
}
|
||||
|
||||
// The broadcast is already always active for neutrino nodes, so we
|
||||
|
||||
@ -15,7 +15,6 @@ import (
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/labels"
|
||||
@ -23,8 +22,6 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnutils"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||
"github.com/lightningnetwork/lnd/sweep"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
const (
|
||||
@ -150,7 +147,7 @@ type BreachConfig struct {
|
||||
Estimator chainfee.Estimator
|
||||
|
||||
// GenSweepScript generates the receiving scripts for swept outputs.
|
||||
GenSweepScript func() fn.Result[lnwallet.AddrWithKey]
|
||||
GenSweepScript func() ([]byte, error)
|
||||
|
||||
// Notifier provides a publish/subscribe interface for event driven
|
||||
// notifications regarding the confirmation of txids.
|
||||
@ -175,10 +172,6 @@ type BreachConfig struct {
|
||||
// breached channels. This is used in conjunction with DB to recover
|
||||
// from crashes, restarts, or other failures.
|
||||
Store RetributionStorer
|
||||
|
||||
// AuxSweeper is an optional interface that can be used to modify the
|
||||
// way sweep transaction are generated.
|
||||
AuxSweeper fn.Option[sweep.AuxSweeper]
|
||||
}
|
||||
|
||||
// BreachArbitrator is a special subsystem which is responsible for watching and
|
||||
@ -742,28 +735,10 @@ justiceTxBroadcast:
|
||||
brarLog.Debugf("Broadcasting justice tx: %v", lnutils.SpewLogClosure(
|
||||
finalTx))
|
||||
|
||||
// As we're about to broadcast our breach transaction, we'll notify the
|
||||
// aux sweeper of our broadcast attempt first.
|
||||
err = fn.MapOptionZ(b.cfg.AuxSweeper, func(aux sweep.AuxSweeper) error {
|
||||
bumpReq := sweep.BumpRequest{
|
||||
Inputs: finalTx.inputs,
|
||||
DeliveryAddress: finalTx.sweepAddr,
|
||||
ExtraTxOut: finalTx.extraTxOut,
|
||||
}
|
||||
|
||||
return aux.NotifyBroadcast(
|
||||
&bumpReq, finalTx.justiceTx, finalTx.fee, nil,
|
||||
)
|
||||
})
|
||||
if err != nil {
|
||||
brarLog.Errorf("unable to notify broadcast: %w", err)
|
||||
return
|
||||
}
|
||||
|
||||
// We'll now attempt to broadcast the transaction which finalized the
|
||||
// channel's retribution against the cheating counter party.
|
||||
label := labels.MakeLabel(labels.LabelTypeJusticeTransaction, nil)
|
||||
err = b.cfg.PublishTransaction(finalTx.justiceTx, label)
|
||||
err = b.cfg.PublishTransaction(finalTx, label)
|
||||
if err != nil {
|
||||
brarLog.Errorf("Unable to broadcast justice tx: %v", err)
|
||||
}
|
||||
@ -883,9 +858,7 @@ Loop:
|
||||
"spending commitment outs: %v",
|
||||
lnutils.SpewLogClosure(tx))
|
||||
|
||||
err = b.cfg.PublishTransaction(
|
||||
tx.justiceTx, label,
|
||||
)
|
||||
err = b.cfg.PublishTransaction(tx, label)
|
||||
if err != nil {
|
||||
brarLog.Warnf("Unable to broadcast "+
|
||||
"commit out spending justice "+
|
||||
@ -900,9 +873,7 @@ Loop:
|
||||
"spending HTLC outs: %v",
|
||||
lnutils.SpewLogClosure(tx))
|
||||
|
||||
err = b.cfg.PublishTransaction(
|
||||
tx.justiceTx, label,
|
||||
)
|
||||
err = b.cfg.PublishTransaction(tx, label)
|
||||
if err != nil {
|
||||
brarLog.Warnf("Unable to broadcast "+
|
||||
"HTLC out spending justice "+
|
||||
@ -917,9 +888,7 @@ Loop:
|
||||
"spending second-level HTLC output: %v",
|
||||
lnutils.SpewLogClosure(tx))
|
||||
|
||||
err = b.cfg.PublishTransaction(
|
||||
tx.justiceTx, label,
|
||||
)
|
||||
err = b.cfg.PublishTransaction(tx, label)
|
||||
if err != nil {
|
||||
brarLog.Warnf("Unable to broadcast "+
|
||||
"second-level HTLC out "+
|
||||
@ -1098,18 +1067,15 @@ type breachedOutput struct {
|
||||
secondLevelTapTweak [32]byte
|
||||
|
||||
witnessFunc input.WitnessGenerator
|
||||
|
||||
resolutionBlob fn.Option[tlv.Blob]
|
||||
|
||||
// TODO(roasbeef): function opt and hook into brar
|
||||
}
|
||||
|
||||
// makeBreachedOutput assembles a new breachedOutput that can be used by the
|
||||
// breach arbiter to construct a justice or sweep transaction.
|
||||
func makeBreachedOutput(outpoint *wire.OutPoint,
|
||||
witnessType input.StandardWitnessType, secondLevelScript []byte,
|
||||
signDescriptor *input.SignDescriptor, confHeight uint32,
|
||||
resolutionBlob fn.Option[tlv.Blob]) breachedOutput {
|
||||
witnessType input.StandardWitnessType,
|
||||
secondLevelScript []byte,
|
||||
signDescriptor *input.SignDescriptor,
|
||||
confHeight uint32) breachedOutput {
|
||||
|
||||
amount := signDescriptor.Output.Value
|
||||
|
||||
@ -1120,7 +1086,6 @@ func makeBreachedOutput(outpoint *wire.OutPoint,
|
||||
witnessType: witnessType,
|
||||
signDesc: *signDescriptor,
|
||||
confHeight: confHeight,
|
||||
resolutionBlob: resolutionBlob,
|
||||
}
|
||||
}
|
||||
|
||||
@ -1160,11 +1125,6 @@ func (bo *breachedOutput) SignDesc() *input.SignDescriptor {
|
||||
return &bo.signDesc
|
||||
}
|
||||
|
||||
// Preimage returns the preimage that was used to create the breached output.
|
||||
func (bo *breachedOutput) Preimage() fn.Option[lntypes.Preimage] {
|
||||
return fn.None[lntypes.Preimage]()
|
||||
}
|
||||
|
||||
// CraftInputScript computes a valid witness that allows us to spend from the
|
||||
// breached output. It does so by first generating and memoizing the witness
|
||||
// generation function, which parameterized primarily by the witness type and
|
||||
@ -1214,12 +1174,6 @@ func (bo *breachedOutput) UnconfParent() *input.TxInfo {
|
||||
return nil
|
||||
}
|
||||
|
||||
// ResolutionBlob returns a special opaque blob to be used to sweep/resolve this
|
||||
// input.
|
||||
func (bo *breachedOutput) ResolutionBlob() fn.Option[tlv.Blob] {
|
||||
return bo.resolutionBlob
|
||||
}
|
||||
|
||||
// Add compile-time constraint ensuring breachedOutput implements the Input
|
||||
// interface.
|
||||
var _ input.Input = (*breachedOutput)(nil)
|
||||
@ -1304,7 +1258,6 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
||||
nil,
|
||||
breachInfo.LocalOutputSignDesc,
|
||||
breachInfo.BreachHeight,
|
||||
breachInfo.LocalResolutionBlob,
|
||||
)
|
||||
|
||||
breachedOutputs = append(breachedOutputs, localOutput)
|
||||
@ -1331,7 +1284,6 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
||||
nil,
|
||||
breachInfo.RemoteOutputSignDesc,
|
||||
breachInfo.BreachHeight,
|
||||
breachInfo.RemoteResolutionBlob,
|
||||
)
|
||||
|
||||
breachedOutputs = append(breachedOutputs, remoteOutput)
|
||||
@ -1366,7 +1318,6 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
||||
breachInfo.HtlcRetributions[i].SecondLevelWitnessScript,
|
||||
&breachInfo.HtlcRetributions[i].SignDesc,
|
||||
breachInfo.BreachHeight,
|
||||
breachInfo.HtlcRetributions[i].ResolutionBlob,
|
||||
)
|
||||
|
||||
// For taproot outputs, we also need to hold onto the second
|
||||
@ -1406,10 +1357,10 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
||||
// spend the to_local output and commitment level HTLC outputs separately,
|
||||
// before the CSV locks expire.
|
||||
type justiceTxVariants struct {
|
||||
spendAll *justiceTxCtx
|
||||
spendCommitOuts *justiceTxCtx
|
||||
spendHTLCs *justiceTxCtx
|
||||
spendSecondLevelHTLCs []*justiceTxCtx
|
||||
spendAll *wire.MsgTx
|
||||
spendCommitOuts *wire.MsgTx
|
||||
spendHTLCs *wire.MsgTx
|
||||
spendSecondLevelHTLCs []*wire.MsgTx
|
||||
}
|
||||
|
||||
// createJusticeTx creates transactions which exacts "justice" by sweeping ALL
|
||||
@ -1473,9 +1424,7 @@ func (b *BreachArbitrator) createJusticeTx(
|
||||
err)
|
||||
}
|
||||
|
||||
// TODO(roasbeef): only register one of them?
|
||||
|
||||
secondLevelSweeps := make([]*justiceTxCtx, 0, len(secondLevelInputs))
|
||||
secondLevelSweeps := make([]*wire.MsgTx, 0, len(secondLevelInputs))
|
||||
for _, input := range secondLevelInputs {
|
||||
sweepTx, err := b.createSweepTx(input)
|
||||
if err != nil {
|
||||
@ -1492,23 +1441,9 @@ func (b *BreachArbitrator) createJusticeTx(
|
||||
return txs, nil
|
||||
}
|
||||
|
||||
// justiceTxCtx contains the justice transaction along with other related meta
|
||||
// data.
|
||||
type justiceTxCtx struct {
|
||||
justiceTx *wire.MsgTx
|
||||
|
||||
sweepAddr lnwallet.AddrWithKey
|
||||
|
||||
extraTxOut fn.Option[sweep.SweepOutput]
|
||||
|
||||
fee btcutil.Amount
|
||||
|
||||
inputs []input.Input
|
||||
}
|
||||
|
||||
// createSweepTx creates a tx that sweeps the passed inputs back to our wallet.
|
||||
func (b *BreachArbitrator) createSweepTx(
|
||||
inputs ...input.Input) (*justiceTxCtx, error) {
|
||||
func (b *BreachArbitrator) createSweepTx(inputs ...input.Input) (*wire.MsgTx,
|
||||
error) {
|
||||
|
||||
if len(inputs) == 0 {
|
||||
return nil, nil
|
||||
@ -1531,18 +1466,6 @@ func (b *BreachArbitrator) createSweepTx(
|
||||
// nLockTime, and output are already included in the TxWeightEstimator.
|
||||
weightEstimate.AddP2TROutput()
|
||||
|
||||
// If any of our inputs has a resolution blob, then we'll add another
|
||||
// P2TR _output_, since we'll want to separate the custom channel
|
||||
// outputs from the regular, BTC only outputs. So we only need one such
|
||||
// output, which'll carry the custom channel "valuables" from both the
|
||||
// breached commitment and HTLC outputs.
|
||||
hasBlobs := fn.Any(func(i input.Input) bool {
|
||||
return i.ResolutionBlob().IsSome()
|
||||
}, inputs)
|
||||
if hasBlobs {
|
||||
weightEstimate.AddP2TROutput()
|
||||
}
|
||||
|
||||
// Next, we iterate over the breached outputs contained in the
|
||||
// retribution info. For each, we switch over the witness type such
|
||||
// that we contribute the appropriate weight for each input and
|
||||
@ -1576,13 +1499,13 @@ func (b *BreachArbitrator) createSweepTx(
|
||||
// sweepSpendableOutputsTxn creates a signed transaction from a sequence of
|
||||
// spendable outputs by sweeping the funds into a single p2wkh output.
|
||||
func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
|
||||
inputs ...input.Input) (*justiceTxCtx, error) {
|
||||
inputs ...input.Input) (*wire.MsgTx, error) {
|
||||
|
||||
// First, we obtain a new public key script from the wallet which we'll
|
||||
// sweep the funds to.
|
||||
// TODO(roasbeef): possibly create many outputs to minimize change in
|
||||
// the future?
|
||||
pkScript, err := b.cfg.GenSweepScript().Unpack()
|
||||
pkScript, err := b.cfg.GenSweepScript()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -1601,18 +1524,6 @@ func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
|
||||
}
|
||||
txFee := feePerKw.FeeForWeight(txWeight)
|
||||
|
||||
// At this point, we'll check to see if we have any extra outputs to
|
||||
// add from the aux sweeper.
|
||||
extraChangeOut := fn.MapOptionZ(
|
||||
b.cfg.AuxSweeper,
|
||||
func(aux sweep.AuxSweeper) fn.Result[sweep.SweepOutput] {
|
||||
return aux.DeriveSweepAddr(inputs, pkScript)
|
||||
},
|
||||
)
|
||||
if err := extraChangeOut.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO(roasbeef): already start to siphon their funds into fees
|
||||
sweepAmt := int64(totalAmt - txFee)
|
||||
|
||||
@ -1620,24 +1531,12 @@ func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
|
||||
// information gathered above and the provided retribution information.
|
||||
txn := wire.NewMsgTx(2)
|
||||
|
||||
// First, we'll add the extra sweep output if it exists, subtracting the
|
||||
// amount from the sweep amt.
|
||||
if b.cfg.AuxSweeper.IsSome() {
|
||||
extraChangeOut.WhenResult(func(o sweep.SweepOutput) {
|
||||
sweepAmt -= o.Value
|
||||
|
||||
txn.AddTxOut(&o.TxOut)
|
||||
})
|
||||
}
|
||||
|
||||
// Next, we'll add the output to which our funds will be deposited.
|
||||
// We begin by adding the output to which our funds will be deposited.
|
||||
txn.AddTxOut(&wire.TxOut{
|
||||
PkScript: pkScript.DeliveryAddress,
|
||||
PkScript: pkScript,
|
||||
Value: sweepAmt,
|
||||
})
|
||||
|
||||
// TODO(roasbeef): add other output change modify sweep amt
|
||||
|
||||
// Next, we add all of the spendable outputs as inputs to the
|
||||
// transaction.
|
||||
for _, inp := range inputs {
|
||||
@ -1693,13 +1592,7 @@ func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
|
||||
}
|
||||
}
|
||||
|
||||
return &justiceTxCtx{
|
||||
justiceTx: txn,
|
||||
sweepAddr: pkScript,
|
||||
extraTxOut: extraChangeOut.Option(),
|
||||
fee: txFee,
|
||||
inputs: inputs,
|
||||
}, nil
|
||||
return txn, nil
|
||||
}
|
||||
|
||||
// RetributionStore handles persistence of retribution states to disk and is
|
||||
@ -1729,29 +1622,13 @@ func taprootBriefcaseFromRetInfo(retInfo *retributionInfo) *taprootBriefcase {
|
||||
// commitment, we'll need to stash the control block.
|
||||
case input.TaprootRemoteCommitSpend:
|
||||
//nolint:lll
|
||||
tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock = bo.signDesc.ControlBlock
|
||||
|
||||
bo.resolutionBlob.WhenSome(func(blob tlv.Blob) {
|
||||
tapCase.SettledCommitBlob = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType2](
|
||||
blob,
|
||||
),
|
||||
)
|
||||
})
|
||||
tapCase.CtrlBlocks.CommitSweepCtrlBlock = bo.signDesc.ControlBlock
|
||||
|
||||
// To spend the revoked output again, we'll store the same
|
||||
// control block value as above, but in a different place.
|
||||
case input.TaprootCommitmentRevoke:
|
||||
//nolint:lll
|
||||
tapCase.CtrlBlocks.Val.RevokeSweepCtrlBlock = bo.signDesc.ControlBlock
|
||||
|
||||
bo.resolutionBlob.WhenSome(func(blob tlv.Blob) {
|
||||
tapCase.BreachedCommitBlob = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType3](
|
||||
blob,
|
||||
),
|
||||
)
|
||||
})
|
||||
tapCase.CtrlBlocks.RevokeSweepCtrlBlock = bo.signDesc.ControlBlock
|
||||
|
||||
// For spending the HTLC outputs, we'll store the first and
|
||||
// second level tweak values.
|
||||
@ -1765,10 +1642,10 @@ func taprootBriefcaseFromRetInfo(retInfo *retributionInfo) *taprootBriefcase {
|
||||
secondLevelTweak := bo.secondLevelTapTweak
|
||||
|
||||
//nolint:lll
|
||||
tapCase.TapTweaks.Val.BreachedHtlcTweaks[resID] = firstLevelTweak
|
||||
tapCase.TapTweaks.BreachedHtlcTweaks[resID] = firstLevelTweak
|
||||
|
||||
//nolint:lll
|
||||
tapCase.TapTweaks.Val.BreachedSecondLevelHltcTweaks[resID] = secondLevelTweak
|
||||
tapCase.TapTweaks.BreachedSecondLevelHltcTweaks[resID] = secondLevelTweak
|
||||
}
|
||||
}
|
||||
|
||||
@ -1788,25 +1665,13 @@ func applyTaprootRetInfo(tapCase *taprootBriefcase,
|
||||
// commitment, we'll apply the control block.
|
||||
case input.TaprootRemoteCommitSpend:
|
||||
//nolint:lll
|
||||
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock
|
||||
|
||||
tapCase.SettledCommitBlob.WhenSomeV(
|
||||
func(blob tlv.Blob) {
|
||||
bo.resolutionBlob = fn.Some(blob)
|
||||
},
|
||||
)
|
||||
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.CommitSweepCtrlBlock
|
||||
|
||||
// To spend the revoked output again, we'll apply the same
|
||||
// control block value as above, but to a different place.
|
||||
case input.TaprootCommitmentRevoke:
|
||||
//nolint:lll
|
||||
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.Val.RevokeSweepCtrlBlock
|
||||
|
||||
tapCase.BreachedCommitBlob.WhenSomeV(
|
||||
func(blob tlv.Blob) {
|
||||
bo.resolutionBlob = fn.Some(blob)
|
||||
},
|
||||
)
|
||||
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.RevokeSweepCtrlBlock
|
||||
|
||||
// For spending the HTLC outputs, we'll apply the first and
|
||||
// second level tweak values.
|
||||
@ -1815,8 +1680,7 @@ func applyTaprootRetInfo(tapCase *taprootBriefcase,
|
||||
case input.TaprootHtlcOfferedRevoke:
|
||||
resID := newResolverID(bo.OutPoint())
|
||||
|
||||
//nolint:lll
|
||||
tap1, ok := tapCase.TapTweaks.Val.BreachedHtlcTweaks[resID]
|
||||
tap1, ok := tapCase.TapTweaks.BreachedHtlcTweaks[resID]
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to find taproot "+
|
||||
"tweak for: %v", bo.OutPoint())
|
||||
@ -1824,7 +1688,7 @@ func applyTaprootRetInfo(tapCase *taprootBriefcase,
|
||||
bo.signDesc.TapTweak = tap1[:]
|
||||
|
||||
//nolint:lll
|
||||
tap2, ok := tapCase.TapTweaks.Val.BreachedSecondLevelHltcTweaks[resID]
|
||||
tap2, ok := tapCase.TapTweaks.BreachedSecondLevelHltcTweaks[resID]
|
||||
if !ok {
|
||||
return fmt.Errorf("unable to find taproot "+
|
||||
"tweak for: %v", bo.OutPoint())
|
||||
|
||||
@ -22,7 +22,6 @@ import (
|
||||
"github.com/go-errors/errors"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/lntest/channels"
|
||||
@ -1199,8 +1198,6 @@ func TestBreachCreateJusticeTx(t *testing.T) {
|
||||
input.HtlcSecondLevelRevoke,
|
||||
}
|
||||
|
||||
rBlob := fn.Some([]byte{0x01})
|
||||
|
||||
breachedOutputs := make([]breachedOutput, len(outputTypes))
|
||||
for i, wt := range outputTypes {
|
||||
// Create a fake breached output for each type, ensuring they
|
||||
@ -1219,7 +1216,6 @@ func TestBreachCreateJusticeTx(t *testing.T) {
|
||||
nil,
|
||||
signDesc,
|
||||
1,
|
||||
rBlob,
|
||||
)
|
||||
}
|
||||
|
||||
@ -1230,16 +1226,16 @@ func TestBreachCreateJusticeTx(t *testing.T) {
|
||||
|
||||
// The spendAll tx should be spending all the outputs. This is the
|
||||
// "regular" justice transaction type.
|
||||
require.Len(t, justiceTxs.spendAll.justiceTx.TxIn, len(breachedOutputs))
|
||||
require.Len(t, justiceTxs.spendAll.TxIn, len(breachedOutputs))
|
||||
|
||||
// The spendCommitOuts tx should be spending the 4 types of commit outs
|
||||
// (note that in practice there will be at most two commit outputs per
|
||||
// commit, but we test all 4 types here).
|
||||
require.Len(t, justiceTxs.spendCommitOuts.justiceTx.TxIn, 4)
|
||||
require.Len(t, justiceTxs.spendCommitOuts.TxIn, 4)
|
||||
|
||||
// Check that the spendHTLCs tx is spending the two revoked commitment
|
||||
// level HTLC output types.
|
||||
require.Len(t, justiceTxs.spendHTLCs.justiceTx.TxIn, 2)
|
||||
require.Len(t, justiceTxs.spendHTLCs.TxIn, 2)
|
||||
|
||||
// Finally, check that the spendSecondLevelHTLCs txs are spending the
|
||||
// second level type.
|
||||
@ -1594,10 +1590,6 @@ func testBreachSpends(t *testing.T, test breachTest) {
|
||||
// Notify the breach arbiter about the breach.
|
||||
retribution, err := lnwallet.NewBreachRetribution(
|
||||
alice.State(), height, 1, forceCloseTx,
|
||||
fn.Some[lnwallet.AuxLeafStore](&lnwallet.MockAuxLeafStore{}),
|
||||
fn.Some[lnwallet.AuxContractResolver](
|
||||
&lnwallet.MockAuxContractResolver{},
|
||||
),
|
||||
)
|
||||
require.NoError(t, err, "unable to create breach retribution")
|
||||
|
||||
@ -1807,10 +1799,6 @@ func TestBreachDelayedJusticeConfirmation(t *testing.T) {
|
||||
// Notify the breach arbiter about the breach.
|
||||
retribution, err := lnwallet.NewBreachRetribution(
|
||||
alice.State(), height, uint32(blockHeight), forceCloseTx,
|
||||
fn.Some[lnwallet.AuxLeafStore](&lnwallet.MockAuxLeafStore{}),
|
||||
fn.Some[lnwallet.AuxContractResolver](
|
||||
&lnwallet.MockAuxContractResolver{},
|
||||
),
|
||||
)
|
||||
require.NoError(t, err, "unable to create breach retribution")
|
||||
|
||||
@ -2138,19 +2126,15 @@ func createTestArbiter(t *testing.T, contractBreaches chan *ContractBreachEvent,
|
||||
// Assemble our test arbiter.
|
||||
notifier := mock.MakeMockSpendNotifier()
|
||||
ba := NewBreachArbitrator(&BreachConfig{
|
||||
CloseLink: func(_ *wire.OutPoint, _ ChannelCloseType) {},
|
||||
DB: db.ChannelStateDB(),
|
||||
Estimator: chainfee.NewStaticEstimator(12500, 0),
|
||||
GenSweepScript: func() fn.Result[lnwallet.AddrWithKey] {
|
||||
return fn.Ok(lnwallet.AddrWithKey{})
|
||||
},
|
||||
ContractBreaches: contractBreaches,
|
||||
Signer: signer,
|
||||
Notifier: notifier,
|
||||
PublishTransaction: func(_ *wire.MsgTx, _ string) error {
|
||||
return nil
|
||||
},
|
||||
Store: store,
|
||||
CloseLink: func(_ *wire.OutPoint, _ ChannelCloseType) {},
|
||||
DB: db.ChannelStateDB(),
|
||||
Estimator: chainfee.NewStaticEstimator(12500, 0),
|
||||
GenSweepScript: func() ([]byte, error) { return nil, nil },
|
||||
ContractBreaches: contractBreaches,
|
||||
Signer: signer,
|
||||
Notifier: notifier,
|
||||
PublishTransaction: func(_ *wire.MsgTx, _ string) error { return nil },
|
||||
Store: store,
|
||||
})
|
||||
|
||||
if err := ba.Start(); err != nil {
|
||||
@ -2373,12 +2357,9 @@ func createInitChannels(t *testing.T) (
|
||||
)
|
||||
bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil)
|
||||
|
||||
signerMock := lnwallet.NewDefaultAuxSignerMock(t)
|
||||
alicePool := lnwallet.NewSigPool(1, aliceSigner)
|
||||
channelAlice, err := lnwallet.NewLightningChannel(
|
||||
aliceSigner, aliceChannelState, alicePool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(signerMock),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
@ -2391,8 +2372,6 @@ func createInitChannels(t *testing.T) (
|
||||
bobPool := lnwallet.NewSigPool(1, bobSigner)
|
||||
channelBob, err := lnwallet.NewLightningChannel(
|
||||
bobSigner, bobChannelState, bobPool,
|
||||
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||
lnwallet.WithAuxSigner(signerMock),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
|
||||
@ -10,11 +10,9 @@ import (
|
||||
"github.com/btcsuite/btcd/txscript"
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
// ContractResolutions is a wrapper struct around the two forms of resolutions
|
||||
@ -1555,16 +1553,9 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
|
||||
commitResolution := c.CommitResolution
|
||||
commitSignDesc := commitResolution.SelfOutputSignDesc
|
||||
//nolint:lll
|
||||
tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock = commitSignDesc.ControlBlock
|
||||
|
||||
c.CommitResolution.ResolutionBlob.WhenSome(func(b []byte) {
|
||||
tapCase.SettledCommitBlob = tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType2](b),
|
||||
)
|
||||
})
|
||||
tapCase.CtrlBlocks.CommitSweepCtrlBlock = commitSignDesc.ControlBlock
|
||||
}
|
||||
|
||||
htlcBlobs := newAuxHtlcBlobs()
|
||||
for _, htlc := range c.HtlcResolutions.IncomingHTLCs {
|
||||
htlc := htlc
|
||||
|
||||
@ -1575,13 +1566,12 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
|
||||
continue
|
||||
}
|
||||
|
||||
var resID resolverID
|
||||
if htlc.SignedSuccessTx != nil {
|
||||
resID = newResolverID(
|
||||
resID := newResolverID(
|
||||
htlc.SignedSuccessTx.TxIn[0].PreviousOutPoint,
|
||||
)
|
||||
//nolint:lll
|
||||
tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID] = ctrlBlock
|
||||
tapCase.CtrlBlocks.SecondLevelCtrlBlocks[resID] = ctrlBlock
|
||||
|
||||
// For HTLCs we need to go to the second level for, we
|
||||
// also need to store the control block needed to
|
||||
@ -1590,17 +1580,13 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
|
||||
//nolint:lll
|
||||
bridgeCtrlBlock := htlc.SignDetails.SignDesc.ControlBlock
|
||||
//nolint:lll
|
||||
tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
|
||||
tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
|
||||
}
|
||||
} else {
|
||||
resID = newResolverID(htlc.ClaimOutpoint)
|
||||
resID := newResolverID(htlc.ClaimOutpoint)
|
||||
//nolint:lll
|
||||
tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID] = ctrlBlock
|
||||
tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID] = ctrlBlock
|
||||
}
|
||||
|
||||
htlc.ResolutionBlob.WhenSome(func(b []byte) {
|
||||
htlcBlobs[resID] = b
|
||||
})
|
||||
}
|
||||
for _, htlc := range c.HtlcResolutions.OutgoingHTLCs {
|
||||
htlc := htlc
|
||||
@ -1612,13 +1598,12 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
|
||||
continue
|
||||
}
|
||||
|
||||
var resID resolverID
|
||||
if htlc.SignedTimeoutTx != nil {
|
||||
resID = newResolverID(
|
||||
resID := newResolverID(
|
||||
htlc.SignedTimeoutTx.TxIn[0].PreviousOutPoint,
|
||||
)
|
||||
//nolint:lll
|
||||
tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID] = ctrlBlock
|
||||
tapCase.CtrlBlocks.SecondLevelCtrlBlocks[resID] = ctrlBlock
|
||||
|
||||
// For HTLCs we need to go to the second level for, we
|
||||
// also need to store the control block needed to
|
||||
@ -1629,28 +1614,18 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
|
||||
//nolint:lll
|
||||
bridgeCtrlBlock := htlc.SignDetails.SignDesc.ControlBlock
|
||||
//nolint:lll
|
||||
tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
|
||||
tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
|
||||
}
|
||||
} else {
|
||||
resID = newResolverID(htlc.ClaimOutpoint)
|
||||
resID := newResolverID(htlc.ClaimOutpoint)
|
||||
//nolint:lll
|
||||
tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID] = ctrlBlock
|
||||
tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID] = ctrlBlock
|
||||
}
|
||||
|
||||
htlc.ResolutionBlob.WhenSome(func(b []byte) {
|
||||
htlcBlobs[resID] = b
|
||||
})
|
||||
}
|
||||
|
||||
if c.AnchorResolution != nil {
|
||||
anchorSignDesc := c.AnchorResolution.AnchorSignDescriptor
|
||||
tapCase.TapTweaks.Val.AnchorTweak = anchorSignDesc.TapTweak
|
||||
}
|
||||
|
||||
if len(htlcBlobs) != 0 {
|
||||
tapCase.HtlcBlobs = tlv.SomeRecordT(
|
||||
tlv.NewRecordT[tlv.TlvType4](htlcBlobs),
|
||||
)
|
||||
tapCase.TapTweaks.AnchorTweak = anchorSignDesc.TapTweak
|
||||
}
|
||||
|
||||
return tapCase.Encode(w)
|
||||
@ -1664,15 +1639,9 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
|
||||
|
||||
if c.CommitResolution != nil {
|
||||
c.CommitResolution.SelfOutputSignDesc.ControlBlock =
|
||||
tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock
|
||||
|
||||
tapCase.SettledCommitBlob.WhenSomeV(func(b []byte) {
|
||||
c.CommitResolution.ResolutionBlob = fn.Some(b)
|
||||
})
|
||||
tapCase.CtrlBlocks.CommitSweepCtrlBlock
|
||||
}
|
||||
|
||||
htlcBlobs := tapCase.HtlcBlobs.ValOpt().UnwrapOr(newAuxHtlcBlobs())
|
||||
|
||||
for i := range c.HtlcResolutions.IncomingHTLCs {
|
||||
htlc := c.HtlcResolutions.IncomingHTLCs[i]
|
||||
|
||||
@ -1683,28 +1652,23 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
|
||||
)
|
||||
|
||||
//nolint:lll
|
||||
ctrlBlock := tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID]
|
||||
ctrlBlock := tapCase.CtrlBlocks.SecondLevelCtrlBlocks[resID]
|
||||
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
||||
|
||||
//nolint:lll
|
||||
if htlc.SignDetails != nil {
|
||||
bridgeCtrlBlock := tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID]
|
||||
bridgeCtrlBlock := tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID]
|
||||
htlc.SignDetails.SignDesc.ControlBlock = bridgeCtrlBlock
|
||||
}
|
||||
} else {
|
||||
resID = newResolverID(htlc.ClaimOutpoint)
|
||||
|
||||
//nolint:lll
|
||||
ctrlBlock := tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID]
|
||||
ctrlBlock := tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID]
|
||||
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
||||
}
|
||||
|
||||
if htlcBlob, ok := htlcBlobs[resID]; ok {
|
||||
htlc.ResolutionBlob = fn.Some(htlcBlob)
|
||||
}
|
||||
|
||||
c.HtlcResolutions.IncomingHTLCs[i] = htlc
|
||||
|
||||
}
|
||||
for i := range c.HtlcResolutions.OutgoingHTLCs {
|
||||
htlc := c.HtlcResolutions.OutgoingHTLCs[i]
|
||||
@ -1716,32 +1680,28 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
|
||||
)
|
||||
|
||||
//nolint:lll
|
||||
ctrlBlock := tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID]
|
||||
ctrlBlock := tapCase.CtrlBlocks.SecondLevelCtrlBlocks[resID]
|
||||
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
||||
|
||||
//nolint:lll
|
||||
if htlc.SignDetails != nil {
|
||||
bridgeCtrlBlock := tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID]
|
||||
bridgeCtrlBlock := tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID]
|
||||
htlc.SignDetails.SignDesc.ControlBlock = bridgeCtrlBlock
|
||||
}
|
||||
} else {
|
||||
resID = newResolverID(htlc.ClaimOutpoint)
|
||||
|
||||
//nolint:lll
|
||||
ctrlBlock := tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID]
|
||||
ctrlBlock := tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID]
|
||||
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
||||
}
|
||||
|
||||
if htlcBlob, ok := htlcBlobs[resID]; ok {
|
||||
htlc.ResolutionBlob = fn.Some(htlcBlob)
|
||||
}
|
||||
|
||||
c.HtlcResolutions.OutgoingHTLCs[i] = htlc
|
||||
}
|
||||
|
||||
if c.AnchorResolution != nil {
|
||||
c.AnchorResolution.AnchorSignDescriptor.TapTweak =
|
||||
tapCase.TapTweaks.Val.AnchorTweak
|
||||
tapCase.TapTweaks.AnchorTweak
|
||||
}
|
||||
|
||||
return nil
|
||||
|
||||
@ -217,18 +217,6 @@ type ChainArbitratorConfig struct {
|
||||
// meanwhile, turn `PaymentCircuit` into an interface or bring it to a
|
||||
// lower package.
|
||||
QueryIncomingCircuit func(circuit models.CircuitKey) *models.CircuitKey
|
||||
|
||||
// AuxLeafStore is an optional store that can be used to store auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||
|
||||
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||
// leaves for certain custom channel types.
|
||||
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||
|
||||
// AuxResolver is an optional interface that can be used to modify the
|
||||
// way contracts are resolved.
|
||||
AuxResolver fn.Option[lnwallet.AuxContractResolver]
|
||||
}
|
||||
|
||||
// ChainArbitrator is a sub-system that oversees the on-chain resolution of all
|
||||
@ -311,19 +299,8 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var chanOpts []lnwallet.ChannelOpt
|
||||
a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
|
||||
})
|
||||
a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
|
||||
})
|
||||
a.c.cfg.AuxResolver.WhenSome(func(s lnwallet.AuxContractResolver) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithAuxResolver(s))
|
||||
})
|
||||
|
||||
chanMachine, err := lnwallet.NewLightningChannel(
|
||||
a.c.cfg.Signer, channel, nil, chanOpts...,
|
||||
a.c.cfg.Signer, channel, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@ -335,10 +312,11 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
|
||||
// ForceCloseChan should force close the contract that this attendant is
|
||||
// watching over. We'll use this when we decide that we need to go to chain. It
|
||||
// should in addition tell the switch to remove the corresponding link, such
|
||||
// that we won't accept any new updates.
|
||||
// that we won't accept any new updates. The returned summary contains all items
|
||||
// needed to eventually resolve all outputs on chain.
|
||||
//
|
||||
// NOTE: Part of the ArbChannel interface.
|
||||
func (a *arbChannel) ForceCloseChan() (*wire.MsgTx, error) {
|
||||
func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) {
|
||||
// First, we mark the channel as borked, this ensure
|
||||
// that no new state transitions can happen, and also
|
||||
// that the link won't be loaded into the switch.
|
||||
@ -366,34 +344,15 @@ func (a *arbChannel) ForceCloseChan() (*wire.MsgTx, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var chanOpts []lnwallet.ChannelOpt
|
||||
a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
|
||||
})
|
||||
a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
|
||||
})
|
||||
a.c.cfg.AuxResolver.WhenSome(func(s lnwallet.AuxContractResolver) {
|
||||
chanOpts = append(chanOpts, lnwallet.WithAuxResolver(s))
|
||||
})
|
||||
|
||||
// Finally, we'll force close the channel completing
|
||||
// the force close workflow.
|
||||
chanMachine, err := lnwallet.NewLightningChannel(
|
||||
a.c.cfg.Signer, channel, nil, chanOpts...,
|
||||
a.c.cfg.Signer, channel, nil,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
closeSummary, err := chanMachine.ForceClose(
|
||||
lnwallet.WithSkipContractResolutions(),
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return closeSummary.CloseTx, nil
|
||||
return chanMachine.ForceClose()
|
||||
}
|
||||
|
||||
// newActiveChannelArbitrator creates a new instance of an active channel
|
||||
@ -598,8 +557,6 @@ func (c *ChainArbitrator) Start() error {
|
||||
isOurAddr: c.cfg.IsOurAddress,
|
||||
contractBreach: breachClosure,
|
||||
extractStateNumHint: lnwallet.GetStateNumHint,
|
||||
auxLeafStore: c.cfg.AuxLeafStore,
|
||||
auxResolver: c.cfg.AuxResolver,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
@ -1229,8 +1186,6 @@ func (c *ChainArbitrator) WatchNewChannel(newChan *channeldb.OpenChannel) error
|
||||
)
|
||||
},
|
||||
extractStateNumHint: lnwallet.GetStateNumHint,
|
||||
auxLeafStore: c.cfg.AuxLeafStore,
|
||||
auxResolver: c.cfg.AuxResolver,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
|
||||
@ -193,12 +193,6 @@ type chainWatcherConfig struct {
|
||||
// obfuscater. This is used by the chain watcher to identify which
|
||||
// state was broadcast and confirmed on-chain.
|
||||
extractStateNumHint func(*wire.MsgTx, [lnwallet.StateHintSize]byte) uint64
|
||||
|
||||
// auxLeafStore can be used to fetch information for custom channels.
|
||||
auxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||
|
||||
// auxResolver is used to supplement contract resolution.
|
||||
auxResolver fn.Option[lnwallet.AuxContractResolver]
|
||||
}
|
||||
|
||||
// chainWatcher is a system that's assigned to every active channel. The duty
|
||||
@ -314,7 +308,7 @@ func (c *chainWatcher) Start() error {
|
||||
)
|
||||
if chanState.ChanType.IsTaproot() {
|
||||
c.fundingPkScript, _, err = input.GenTaprootFundingScript(
|
||||
localKey, remoteKey, 0, chanState.TapscriptRoot,
|
||||
localKey, remoteKey, 0,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -429,37 +423,15 @@ func (c *chainWatcher) handleUnknownLocalState(
|
||||
&c.cfg.chanState.LocalChanCfg, &c.cfg.chanState.RemoteChanCfg,
|
||||
)
|
||||
|
||||
auxResult, err := fn.MapOptionZ(
|
||||
c.cfg.auxLeafStore,
|
||||
//nolint:lll
|
||||
func(s lnwallet.AuxLeafStore) fn.Result[lnwallet.CommitDiffAuxResult] {
|
||||
return s.FetchLeavesFromCommit(
|
||||
lnwallet.NewAuxChanState(c.cfg.chanState),
|
||||
c.cfg.chanState.LocalCommitment, *commitKeyRing,
|
||||
lntypes.Local,
|
||||
)
|
||||
},
|
||||
).Unpack()
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("unable to fetch aux leaves: %w", err)
|
||||
}
|
||||
|
||||
// With the keys derived, we'll construct the remote script that'll be
|
||||
// present if they have a non-dust balance on the commitment.
|
||||
var leaseExpiry uint32
|
||||
if c.cfg.chanState.ChanType.HasLeaseExpiration() {
|
||||
leaseExpiry = c.cfg.chanState.ThawHeight
|
||||
}
|
||||
|
||||
remoteAuxLeaf := fn.ChainOption(
|
||||
func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf {
|
||||
return l.RemoteAuxLeaf
|
||||
},
|
||||
)(auxResult.AuxLeaves)
|
||||
remoteScript, _, err := lnwallet.CommitScriptToRemote(
|
||||
c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
|
||||
commitKeyRing.ToRemoteKey, leaseExpiry,
|
||||
remoteAuxLeaf,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -468,16 +440,10 @@ func (c *chainWatcher) handleUnknownLocalState(
|
||||
// Next, we'll derive our script that includes the revocation base for
|
||||
// the remote party allowing them to claim this output before the CSV
|
||||
// delay if we breach.
|
||||
localAuxLeaf := fn.ChainOption(
|
||||
func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf {
|
||||
return l.LocalAuxLeaf
|
||||
},
|
||||
)(auxResult.AuxLeaves)
|
||||
localScript, err := lnwallet.CommitScriptToSelf(
|
||||
c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
|
||||
commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey,
|
||||
uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry,
|
||||
localAuxLeaf,
|
||||
)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@ -900,7 +866,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail,
|
||||
spendHeight := uint32(commitSpend.SpendingHeight)
|
||||
retribution, err := lnwallet.NewBreachRetribution(
|
||||
c.cfg.chanState, broadcastStateNum, spendHeight,
|
||||
commitSpend.SpendingTx, c.cfg.auxLeafStore, c.cfg.auxResolver,
|
||||
commitSpend.SpendingTx,
|
||||
)
|
||||
|
||||
switch {
|
||||
@ -1150,8 +1116,8 @@ func (c *chainWatcher) dispatchLocalForceClose(
|
||||
"detected", c.cfg.chanState.FundingOutpoint)
|
||||
|
||||
forceClose, err := lnwallet.NewLocalForceCloseSummary(
|
||||
c.cfg.chanState, c.cfg.signer, commitSpend.SpendingTx, stateNum,
|
||||
c.cfg.auxLeafStore, c.cfg.auxResolver,
|
||||
c.cfg.chanState, c.cfg.signer,
|
||||
commitSpend.SpendingTx, stateNum,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
@ -1175,29 +1141,16 @@ func (c *chainWatcher) dispatchLocalForceClose(
|
||||
LocalChanConfig: c.cfg.chanState.LocalChanCfg,
|
||||
}
|
||||
|
||||
resolutions, err := forceClose.ContractResolutions.UnwrapOrErr(
|
||||
fmt.Errorf("resolutions not found"),
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// If our commitment output isn't dust or we have active HTLC's on the
|
||||
// commitment transaction, then we'll populate the balances on the
|
||||
// close channel summary.
|
||||
if resolutions.CommitResolution != nil {
|
||||
localBalance := chanSnapshot.LocalBalance.ToSatoshis()
|
||||
closeSummary.SettledBalance = localBalance
|
||||
closeSummary.TimeLockedBalance = localBalance
|
||||
if forceClose.CommitResolution != nil {
|
||||
closeSummary.SettledBalance = chanSnapshot.LocalBalance.ToSatoshis()
|
||||
closeSummary.TimeLockedBalance = chanSnapshot.LocalBalance.ToSatoshis()
|
||||
}
|
||||
|
||||
if resolutions.HtlcResolutions != nil {
|
||||
for _, htlc := range resolutions.HtlcResolutions.OutgoingHTLCs {
|
||||
htlcValue := btcutil.Amount(
|
||||
htlc.SweepSignDesc.Output.Value,
|
||||
)
|
||||
closeSummary.TimeLockedBalance += htlcValue
|
||||
}
|
||||
for _, htlc := range forceClose.HtlcResolutions.OutgoingHTLCs {
|
||||
htlcValue := btcutil.Amount(htlc.SweepSignDesc.Output.Value)
|
||||
closeSummary.TimeLockedBalance += htlcValue
|
||||
}
|
||||
|
||||
// Attempt to add a channel sync message to the close summary.
|
||||
@ -1256,8 +1209,8 @@ func (c *chainWatcher) dispatchRemoteForceClose(
|
||||
// materials required to let each subscriber sweep the funds in the
|
||||
// channel on-chain.
|
||||
uniClose, err := lnwallet.NewUnilateralCloseSummary(
|
||||
c.cfg.chanState, c.cfg.signer, commitSpend, remoteCommit,
|
||||
commitPoint, c.cfg.auxLeafStore, c.cfg.auxResolver,
|
||||
c.cfg.chanState, c.cfg.signer, commitSpend,
|
||||
remoteCommit, commitPoint,
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
|
||||
@ -2,7 +2,6 @@ package contractcourt
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"crypto/sha256"
|
||||
"fmt"
|
||||
"testing"
|
||||
@ -146,15 +145,17 @@ func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) {
|
||||
|
||||
// With the HTLC added, we'll now manually initiate a state transition
|
||||
// from Alice to Bob.
|
||||
testQuit, testQuitFunc := context.WithCancel(context.Background())
|
||||
t.Cleanup(testQuitFunc)
|
||||
_, err = aliceChannel.SignNextCommitment(testQuit)
|
||||
require.NoError(t, err)
|
||||
_, err = aliceChannel.SignNextCommitment()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// At this point, we'll now Bob broadcasting this new pending unrevoked
|
||||
// commitment.
|
||||
bobPendingCommit, err := aliceChannel.State().RemoteCommitChainTip()
|
||||
require.NoError(t, err)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// We'll craft a fake spend notification with Bob's actual commitment.
|
||||
// The chain watcher should be able to detect that this is a pending
|
||||
@ -504,24 +505,14 @@ func TestChainWatcherLocalForceCloseDetect(t *testing.T) {
|
||||
// outputs.
|
||||
select {
|
||||
case summary := <-chanEvents.LocalUnilateralClosure:
|
||||
resOpt := summary.LocalForceCloseSummary.
|
||||
ContractResolutions
|
||||
|
||||
resolutions, err := resOpt.UnwrapOrErr(
|
||||
fmt.Errorf("resolutions not found"),
|
||||
)
|
||||
if err != nil {
|
||||
t.Fatalf("unable to get resolutions: %v", err)
|
||||
}
|
||||
|
||||
// Make sure we correctly extracted the commit
|
||||
// resolution if we had a local output.
|
||||
if remoteOutputOnly {
|
||||
if resolutions.CommitResolution != nil {
|
||||
if summary.CommitResolution != nil {
|
||||
t.Fatalf("expected no commit resolution")
|
||||
}
|
||||
} else {
|
||||
if resolutions.CommitResolution == nil {
|
||||
if summary.CommitResolution == nil {
|
||||
t.Fatalf("expected commit resolution")
|
||||
}
|
||||
}
|
||||
|
||||
@ -98,7 +98,7 @@ type ArbChannel interface {
|
||||
// corresponding link, such that we won't accept any new updates. The
|
||||
// returned summary contains all items needed to eventually resolve all
|
||||
// outputs on chain.
|
||||
ForceCloseChan() (*wire.MsgTx, error)
|
||||
ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error)
|
||||
|
||||
// NewAnchorResolutions returns the anchor resolutions for currently
|
||||
// valid commitment transactions.
|
||||
@ -482,20 +482,6 @@ func (c *ChannelArbitrator) Start(state *chanArbStartState) error {
|
||||
return err
|
||||
}
|
||||
|
||||
c.wg.Add(1)
|
||||
go c.channelAttendant(bestHeight, state.commitSet)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// progressStateMachineAfterRestart attempts to progress the state machine
|
||||
// after a restart. This makes sure that if the state transition failed, we
|
||||
// will try to progress the state machine again. Moreover it will relaunch
|
||||
// resolvers if the channel is still in the pending close state and has not
|
||||
// been fully resolved yet.
|
||||
func (c *ChannelArbitrator) progressStateMachineAfterRestart(bestHeight int32,
|
||||
commitSet *CommitSet) error {
|
||||
|
||||
// If the channel has been marked pending close in the database, and we
|
||||
// haven't transitioned the state machine to StateContractClosed (or a
|
||||
// succeeding state), then a state transition most likely failed. We'll
|
||||
@ -541,7 +527,7 @@ func (c *ChannelArbitrator) progressStateMachineAfterRestart(bestHeight int32,
|
||||
// on-chain state, and our set of active contracts.
|
||||
startingState := c.state
|
||||
nextState, _, err := c.advanceState(
|
||||
triggerHeight, trigger, commitSet,
|
||||
triggerHeight, trigger, state.commitSet,
|
||||
)
|
||||
if err != nil {
|
||||
switch err {
|
||||
@ -578,12 +564,14 @@ func (c *ChannelArbitrator) progressStateMachineAfterRestart(bestHeight int32,
|
||||
// receive a chain event from the chain watcher that the
|
||||
// commitment has been confirmed on chain, and before we
|
||||
// advance our state step, we call InsertConfirmedCommitSet.
|
||||
err := c.relaunchResolvers(commitSet, triggerHeight)
|
||||
err := c.relaunchResolvers(state.commitSet, triggerHeight)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
c.wg.Add(1)
|
||||
go c.channelAttendant(bestHeight)
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -1070,7 +1058,7 @@ func (c *ChannelArbitrator) stateStep(
|
||||
// We'll tell the switch that it should remove the link for
|
||||
// this channel, in addition to fetching the force close
|
||||
// summary needed to close this channel on chain.
|
||||
forceCloseTx, err := c.cfg.Channel.ForceCloseChan()
|
||||
closeSummary, err := c.cfg.Channel.ForceCloseChan()
|
||||
if err != nil {
|
||||
log.Errorf("ChannelArbitrator(%v): unable to "+
|
||||
"force close: %v", c.cfg.ChanPoint, err)
|
||||
@ -1090,7 +1078,7 @@ func (c *ChannelArbitrator) stateStep(
|
||||
|
||||
return StateError, closeTx, err
|
||||
}
|
||||
closeTx = forceCloseTx
|
||||
closeTx = closeSummary.CloseTx
|
||||
|
||||
// Before publishing the transaction, we store it to the
|
||||
// database, such that we can re-publish later in case it
|
||||
@ -1994,11 +1982,9 @@ func (c *ChannelArbitrator) isPreimageAvailable(hash lntypes.Hash) (bool,
|
||||
// have the incoming contest resolver decide that we don't want to
|
||||
// settle this invoice.
|
||||
invoice, err := c.cfg.Registry.LookupInvoice(context.Background(), hash)
|
||||
switch {
|
||||
case err == nil:
|
||||
case errors.Is(err, invoices.ErrInvoiceNotFound) ||
|
||||
errors.Is(err, invoices.ErrNoInvoicesCreated):
|
||||
|
||||
switch err {
|
||||
case nil:
|
||||
case invoices.ErrInvoiceNotFound, invoices.ErrNoInvoicesCreated:
|
||||
return false, nil
|
||||
default:
|
||||
return false, err
|
||||
@ -2787,28 +2773,13 @@ func (c *ChannelArbitrator) updateActiveHTLCs() {
|
||||
// Nursery for incubation, and ultimate sweeping.
|
||||
//
|
||||
// NOTE: This MUST be run as a goroutine.
|
||||
//
|
||||
//nolint:funlen
|
||||
func (c *ChannelArbitrator) channelAttendant(bestHeight int32,
|
||||
commitSet *CommitSet) {
|
||||
func (c *ChannelArbitrator) channelAttendant(bestHeight int32) {
|
||||
|
||||
// TODO(roasbeef): tell top chain arb we're done
|
||||
defer func() {
|
||||
c.wg.Done()
|
||||
}()
|
||||
|
||||
err := c.progressStateMachineAfterRestart(bestHeight, commitSet)
|
||||
if err != nil {
|
||||
// In case of an error, we return early but we do not shutdown
|
||||
// LND, because there might be other channels that still can be
|
||||
// resolved and we don't want to interfere with that.
|
||||
// We continue to run the channel attendant in case the channel
|
||||
// closes via other means for example the remote pary force
|
||||
// closes the channel. So we log the error and continue.
|
||||
log.Errorf("Unable to progress state machine after "+
|
||||
"restart: %v", err)
|
||||
}
|
||||
|
||||
for {
|
||||
select {
|
||||
|
||||
@ -2898,36 +2869,11 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32,
|
||||
}
|
||||
closeTx := closeInfo.CloseTx
|
||||
|
||||
resolutions, err := closeInfo.ContractResolutions.
|
||||
UnwrapOrErr(
|
||||
fmt.Errorf("resolutions not found"),
|
||||
)
|
||||
if err != nil {
|
||||
log.Errorf("ChannelArbitrator(%v): unable to "+
|
||||
"get resolutions: %v", c.cfg.ChanPoint,
|
||||
err)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
// We make sure that the htlc resolutions are present
|
||||
// otherwise we would panic dereferencing the pointer.
|
||||
//
|
||||
// TODO(ziggie): Refactor ContractResolutions to use
|
||||
// options.
|
||||
if resolutions.HtlcResolutions == nil {
|
||||
log.Errorf("ChannelArbitrator(%v): htlc "+
|
||||
"resolutions not found",
|
||||
c.cfg.ChanPoint)
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
contractRes := &ContractResolutions{
|
||||
CommitHash: closeTx.TxHash(),
|
||||
CommitResolution: resolutions.CommitResolution,
|
||||
HtlcResolutions: *resolutions.HtlcResolutions,
|
||||
AnchorResolution: resolutions.AnchorResolution,
|
||||
CommitResolution: closeInfo.CommitResolution,
|
||||
HtlcResolutions: *closeInfo.HtlcResolutions,
|
||||
AnchorResolution: closeInfo.AnchorResolution,
|
||||
}
|
||||
|
||||
// When processing a unilateral close event, we'll
|
||||
@ -2936,7 +2882,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32,
|
||||
// available to fetch in that state, we'll also write
|
||||
// the commit set so we can reconstruct our chain
|
||||
// actions on restart.
|
||||
err = c.log.LogContractResolutions(contractRes)
|
||||
err := c.log.LogContractResolutions(contractRes)
|
||||
if err != nil {
|
||||
log.Errorf("Unable to write resolutions: %v",
|
||||
err)
|
||||
|
||||
@ -21,7 +21,6 @@ import (
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
"github.com/lightningnetwork/lnd/lntest/mock"
|
||||
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/lnwire"
|
||||
@ -694,15 +693,11 @@ func TestChannelArbitratorLocalForceClose(t *testing.T) {
|
||||
chanArbCtx.AssertState(StateCommitmentBroadcasted)
|
||||
|
||||
// Now notify about the local force close getting confirmed.
|
||||
//
|
||||
//nolint:lll
|
||||
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
||||
SpendDetail: &chainntnfs.SpendDetail{},
|
||||
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
||||
CloseTx: &wire.MsgTx{},
|
||||
ContractResolutions: fn.Some(lnwallet.ContractResolutions{
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||
}),
|
||||
CloseTx: &wire.MsgTx{},
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||
},
|
||||
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
||||
}
|
||||
@ -974,18 +969,15 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:lll
|
||||
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
||||
SpendDetail: &chainntnfs.SpendDetail{},
|
||||
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
||||
CloseTx: closeTx,
|
||||
ContractResolutions: fn.Some(lnwallet.ContractResolutions{
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{
|
||||
OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{
|
||||
outgoingRes,
|
||||
},
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{
|
||||
OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{
|
||||
outgoingRes,
|
||||
},
|
||||
}),
|
||||
},
|
||||
},
|
||||
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
||||
CommitSet: CommitSet{
|
||||
@ -1044,19 +1036,10 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
||||
|
||||
// Post restart, it should be the case that our resolver was properly
|
||||
// supplemented, and we only have a single resolver in the final set.
|
||||
// The resolvers are added concurrently so we need to wait here.
|
||||
err = wait.NoError(func() error {
|
||||
chanArb.activeResolversLock.Lock()
|
||||
defer chanArb.activeResolversLock.Unlock()
|
||||
|
||||
if len(chanArb.activeResolvers) != 1 {
|
||||
return fmt.Errorf("expected single resolver, instead "+
|
||||
"got: %v", len(chanArb.activeResolvers))
|
||||
}
|
||||
|
||||
return nil
|
||||
}, defaultTimeout)
|
||||
require.NoError(t, err)
|
||||
if len(chanArb.activeResolvers) != 1 {
|
||||
t.Fatalf("expected single resolver, instead got: %v",
|
||||
len(chanArb.activeResolvers))
|
||||
}
|
||||
|
||||
// We'll now examine the in-memory state of the active resolvers to
|
||||
// ensure t hey were populated properly.
|
||||
@ -1628,15 +1611,12 @@ func TestChannelArbitratorCommitFailure(t *testing.T) {
|
||||
},
|
||||
{
|
||||
closeType: channeldb.LocalForceClose,
|
||||
//nolint:lll
|
||||
sendEvent: func(chanArb *ChannelArbitrator) {
|
||||
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
||||
SpendDetail: &chainntnfs.SpendDetail{},
|
||||
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
||||
CloseTx: &wire.MsgTx{},
|
||||
ContractResolutions: fn.Some(lnwallet.ContractResolutions{
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||
}),
|
||||
CloseTx: &wire.MsgTx{},
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||
},
|
||||
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
||||
}
|
||||
@ -1964,15 +1944,11 @@ func TestChannelArbitratorDanglingCommitForceClose(t *testing.T) {
|
||||
// being canalled back. Also note that there're no HTLC
|
||||
// resolutions sent since we have none on our
|
||||
// commitment transaction.
|
||||
//
|
||||
//nolint:lll
|
||||
uniCloseInfo := &LocalUnilateralCloseInfo{
|
||||
SpendDetail: &chainntnfs.SpendDetail{},
|
||||
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
||||
CloseTx: closeTx,
|
||||
ContractResolutions: fn.Some(lnwallet.ContractResolutions{
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||
}),
|
||||
CloseTx: closeTx,
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||
},
|
||||
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
||||
CommitSet: CommitSet{
|
||||
@ -2778,15 +2754,12 @@ func TestChannelArbitratorAnchors(t *testing.T) {
|
||||
},
|
||||
}
|
||||
|
||||
//nolint:lll
|
||||
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
||||
SpendDetail: &chainntnfs.SpendDetail{},
|
||||
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
||||
CloseTx: closeTx,
|
||||
ContractResolutions: fn.Some(lnwallet.ContractResolutions{
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||
AnchorResolution: anchorResolution,
|
||||
}),
|
||||
CloseTx: closeTx,
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||
AnchorResolution: anchorResolution,
|
||||
},
|
||||
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
||||
CommitSet: CommitSet{
|
||||
@ -2894,12 +2867,9 @@ func TestChannelArbitratorStartForceCloseFail(t *testing.T) {
|
||||
{
|
||||
name: "Commitment is rejected with an " +
|
||||
"unmatched error",
|
||||
broadcastErr: fmt.Errorf("Reject Commitment Tx"),
|
||||
expectedState: StateBroadcastCommit,
|
||||
// We should still be able to start up since we other
|
||||
// channels might be closing as well and we should
|
||||
// resolve the contracts.
|
||||
expectedStartup: true,
|
||||
broadcastErr: fmt.Errorf("Reject Commitment Tx"),
|
||||
expectedState: StateBroadcastCommit,
|
||||
expectedStartup: false,
|
||||
},
|
||||
|
||||
// We started after the DLP was triggered, and try to force
|
||||
@ -3023,10 +2993,14 @@ func (m *mockChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
|
||||
return &lnwallet.AnchorResolutions{}, nil
|
||||
}
|
||||
|
||||
func (m *mockChannel) ForceCloseChan() (*wire.MsgTx, error) {
|
||||
func (m *mockChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) {
|
||||
if m.forceCloseErr != nil {
|
||||
return nil, m.forceCloseErr
|
||||
}
|
||||
|
||||
return &wire.MsgTx{}, nil
|
||||
summary := &lnwallet.LocalForceCloseSummary{
|
||||
CloseTx: &wire.MsgTx{},
|
||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||
}
|
||||
return summary, nil
|
||||
}
|
||||
|
||||
@ -345,18 +345,12 @@ func (c *commitSweepResolver) Resolve(_ bool) (ContractResolver, error) {
|
||||
&c.commitResolution.SelfOutputSignDesc,
|
||||
c.broadcastHeight, c.commitResolution.MaturityDelay,
|
||||
c.leaseExpiry,
|
||||
input.WithResolutionBlob(
|
||||
c.commitResolution.ResolutionBlob,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
inp = input.NewCsvInput(
|
||||
&c.commitResolution.SelfOutPoint, witnessType,
|
||||
&c.commitResolution.SelfOutputSignDesc,
|
||||
c.broadcastHeight, c.commitResolution.MaturityDelay,
|
||||
input.WithResolutionBlob(
|
||||
c.commitResolution.ResolutionBlob,
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
|
||||
@ -308,8 +308,7 @@ func (h *htlcIncomingContestResolver) Resolve(
|
||||
|
||||
resolution, err := h.Registry.NotifyExitHopHtlc(
|
||||
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
|
||||
circuitKey, hodlQueue.ChanIn(), h.htlc.CustomRecords,
|
||||
payload,
|
||||
circuitKey, hodlQueue.ChanIn(), payload,
|
||||
)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -6,9 +6,7 @@ import (
|
||||
"github.com/btcsuite/btcd/wire"
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/input"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
// htlcLeaseResolver is a struct that houses the lease specific HTLC resolution
|
||||
@ -54,8 +52,8 @@ func (h *htlcLeaseResolver) deriveWaitHeight(csvDelay uint32,
|
||||
// send to the sweeper so the output can ultimately be swept.
|
||||
func (h *htlcLeaseResolver) makeSweepInput(op *wire.OutPoint,
|
||||
wType, cltvWtype input.StandardWitnessType,
|
||||
signDesc *input.SignDescriptor, csvDelay, broadcastHeight uint32,
|
||||
payHash [32]byte, resBlob fn.Option[tlv.Blob]) *input.BaseInput {
|
||||
signDesc *input.SignDescriptor,
|
||||
csvDelay, broadcastHeight uint32, payHash [32]byte) *input.BaseInput {
|
||||
|
||||
if h.hasCLTV() {
|
||||
log.Infof("%T(%x): CSV and CLTV locks expired, offering "+
|
||||
@ -65,17 +63,13 @@ func (h *htlcLeaseResolver) makeSweepInput(op *wire.OutPoint,
|
||||
op, cltvWtype, signDesc,
|
||||
broadcastHeight, csvDelay,
|
||||
h.leaseExpiry,
|
||||
input.WithResolutionBlob(resBlob),
|
||||
)
|
||||
}
|
||||
|
||||
log.Infof("%T(%x): CSV lock expired, offering second-layer output to "+
|
||||
"sweeper: %v", h, payHash, op)
|
||||
|
||||
return input.NewCsvInput(
|
||||
op, wType, signDesc, broadcastHeight, csvDelay,
|
||||
input.WithResolutionBlob(resBlob),
|
||||
)
|
||||
return input.NewCsvInput(op, wType, signDesc, broadcastHeight, csvDelay)
|
||||
}
|
||||
|
||||
// SupplementState allows the user of a ContractResolver to supplement it with
|
||||
|
||||
@ -247,9 +247,6 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) (
|
||||
h.htlcResolution.SignedSuccessTx,
|
||||
h.htlcResolution.SignDetails, h.htlcResolution.Preimage,
|
||||
h.broadcastHeight,
|
||||
input.WithResolutionBlob(
|
||||
h.htlcResolution.ResolutionBlob,
|
||||
),
|
||||
)
|
||||
} else {
|
||||
//nolint:lll
|
||||
@ -406,7 +403,7 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) (
|
||||
input.LeaseHtlcAcceptedSuccessSecondLevel,
|
||||
&h.htlcResolution.SweepSignDesc,
|
||||
h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight),
|
||||
h.htlc.RHash, h.htlcResolution.ResolutionBlob,
|
||||
h.htlc.RHash,
|
||||
)
|
||||
|
||||
// Calculate the budget for this sweep.
|
||||
@ -462,9 +459,6 @@ func (h *htlcSuccessResolver) resolveRemoteCommitOutput(immediate bool) (
|
||||
h.htlcResolution.Preimage[:],
|
||||
h.broadcastHeight,
|
||||
h.htlcResolution.CsvDelay,
|
||||
input.WithResolutionBlob(
|
||||
h.htlcResolution.ResolutionBlob,
|
||||
),
|
||||
))
|
||||
} else {
|
||||
inp = lnutils.Ptr(input.MakeHtlcSucceedInput(
|
||||
|
||||
@ -484,9 +484,6 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error {
|
||||
h.htlcResolution.SignedTimeoutTx,
|
||||
h.htlcResolution.SignDetails,
|
||||
h.broadcastHeight,
|
||||
input.WithResolutionBlob(
|
||||
h.htlcResolution.ResolutionBlob,
|
||||
),
|
||||
))
|
||||
} else {
|
||||
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutAnchorInput(
|
||||
@ -541,6 +538,7 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// TODO(yy): checkpoint here?
|
||||
return err
|
||||
}
|
||||
|
||||
@ -564,60 +562,6 @@ func (h *htlcTimeoutResolver) sendSecondLevelTxLegacy() error {
|
||||
return h.Checkpoint(h)
|
||||
}
|
||||
|
||||
// sweepDirectHtlcOutput sends the direct spend of the HTLC output to the
|
||||
// sweeper. This is used when the remote party goes on chain, and we're able to
|
||||
// sweep an HTLC we offered after a timeout. Only the CLTV encumbered outputs
|
||||
// are resolved via this path.
|
||||
func (h *htlcTimeoutResolver) sweepDirectHtlcOutput(immediate bool) error {
|
||||
var htlcWitnessType input.StandardWitnessType
|
||||
if h.isTaproot() {
|
||||
htlcWitnessType = input.TaprootHtlcOfferedRemoteTimeout
|
||||
} else {
|
||||
htlcWitnessType = input.HtlcOfferedRemoteTimeout
|
||||
}
|
||||
|
||||
sweepInput := input.NewCsvInputWithCltv(
|
||||
&h.htlcResolution.ClaimOutpoint, htlcWitnessType,
|
||||
&h.htlcResolution.SweepSignDesc, h.broadcastHeight,
|
||||
h.htlcResolution.CsvDelay, h.htlcResolution.Expiry,
|
||||
input.WithResolutionBlob(h.htlcResolution.ResolutionBlob),
|
||||
)
|
||||
|
||||
// Calculate the budget.
|
||||
//
|
||||
// TODO(yy): the budget is twice the output's value, which is needed as
|
||||
// we don't force sweep the output now. To prevent cascading force
|
||||
// closes, we use all its output value plus a wallet input as the
|
||||
// budget. This is a temporary solution until we can optionally cancel
|
||||
// the incoming HTLC, more details in,
|
||||
// - https://github.com/lightningnetwork/lnd/issues/7969
|
||||
budget := calculateBudget(
|
||||
btcutil.Amount(sweepInput.SignDesc().Output.Value), 2, 0,
|
||||
)
|
||||
|
||||
log.Infof("%T(%x): offering offered remote timeout HTLC output to "+
|
||||
"sweeper with deadline %v and budget=%v at height=%v",
|
||||
h, h.htlc.RHash[:], h.incomingHTLCExpiryHeight, budget,
|
||||
h.broadcastHeight)
|
||||
|
||||
_, err := h.Sweeper.SweepInput(
|
||||
sweepInput,
|
||||
sweep.Params{
|
||||
Budget: budget,
|
||||
|
||||
// This is an outgoing HTLC, so we want to make sure
|
||||
// that we sweep it before the incoming HTLC expires.
|
||||
DeadlineHeight: h.incomingHTLCExpiryHeight,
|
||||
Immediate: immediate,
|
||||
},
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// spendHtlcOutput handles the initial spend of an HTLC output via the timeout
|
||||
// clause. If this is our local commitment, the second-level timeout TX will be
|
||||
// used to spend the output into the next stage. If this is the remote
|
||||
@ -638,18 +582,8 @@ func (h *htlcTimeoutResolver) spendHtlcOutput(
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If this is a remote commitment there's no second level timeout txn,
|
||||
// and we can just send this directly to the sweeper.
|
||||
case h.htlcResolution.SignedTimeoutTx == nil && !h.outputIncubating:
|
||||
if err := h.sweepDirectHtlcOutput(immediate); err != nil {
|
||||
log.Errorf("Sending direct spend to sweeper: %v", err)
|
||||
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// If we have a SignedTimeoutTx but no SignDetails, this is a local
|
||||
// commitment for a non-anchor channel, so we'll send it to the utxo
|
||||
// nursery.
|
||||
// If we have no SignDetails, and we haven't already sent the output to
|
||||
// the utxo nursery, then we'll do so now.
|
||||
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
|
||||
if err := h.sendSecondLevelTxLegacy(); err != nil {
|
||||
log.Errorf("Sending timeout tx to nursery: %v", err)
|
||||
@ -756,13 +690,6 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
||||
)
|
||||
|
||||
switch {
|
||||
|
||||
// If we swept an HTLC directly off the remote party's commitment
|
||||
// transaction, then we can exit here as there's no second level sweep
|
||||
// to do.
|
||||
case h.htlcResolution.SignedTimeoutTx == nil:
|
||||
break
|
||||
|
||||
// If the sweeper is handling the second level transaction, wait for
|
||||
// the CSV and possible CLTV lock to expire, before sweeping the output
|
||||
// on the second-level.
|
||||
@ -835,9 +762,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
||||
&h.htlcResolution.SweepSignDesc,
|
||||
h.htlcResolution.CsvDelay,
|
||||
uint32(commitSpend.SpendingHeight), h.htlc.RHash,
|
||||
h.htlcResolution.ResolutionBlob,
|
||||
)
|
||||
|
||||
// Calculate the budget for this sweep.
|
||||
budget := calculateBudget(
|
||||
btcutil.Amount(inp.SignDesc().Output.Value),
|
||||
@ -875,7 +800,6 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
||||
case h.htlcResolution.SignedTimeoutTx != nil:
|
||||
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
|
||||
"delayed output", h, claimOutpoint)
|
||||
|
||||
sweepTx, err := waitForSpend(
|
||||
&claimOutpoint,
|
||||
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
||||
@ -942,11 +866,9 @@ func (h *htlcTimeoutResolver) IsResolved() bool {
|
||||
|
||||
// report returns a report on the resolution state of the contract.
|
||||
func (h *htlcTimeoutResolver) report() *ContractReport {
|
||||
// If we have a SignedTimeoutTx but no SignDetails, this is a local
|
||||
// commitment for a non-anchor channel, which was handled by the utxo
|
||||
// nursery.
|
||||
if h.htlcResolution.SignDetails == nil && h.
|
||||
htlcResolution.SignedTimeoutTx != nil {
|
||||
// If the sign details are nil, the report will be created by handled
|
||||
// by the nursery.
|
||||
if h.htlcResolution.SignDetails == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -966,20 +888,13 @@ func (h *htlcTimeoutResolver) initReport() {
|
||||
)
|
||||
}
|
||||
|
||||
// If there's no timeout transaction, then we're already effectively in
|
||||
// level two.
|
||||
stage := uint32(1)
|
||||
if h.htlcResolution.SignedTimeoutTx == nil {
|
||||
stage = 2
|
||||
}
|
||||
|
||||
h.currentReport = ContractReport{
|
||||
Outpoint: h.htlcResolution.ClaimOutpoint,
|
||||
Type: ReportOutputOutgoingHtlc,
|
||||
Amount: finalAmt,
|
||||
MaturityHeight: h.htlcResolution.Expiry,
|
||||
LimboBalance: finalAmt,
|
||||
Stage: stage,
|
||||
Stage: 1,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -69,31 +69,11 @@ func (m *mockWitnessBeacon) AddPreimages(preimages ...lntypes.Preimage) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
type htlcTimeoutTestCase struct {
|
||||
// name is a human readable description of the test case.
|
||||
name string
|
||||
// TestHtlcTimeoutResolver tests that the timeout resolver properly handles all
|
||||
// variations of possible local+remote spends.
|
||||
func TestHtlcTimeoutResolver(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
// remoteCommit denotes if the commitment broadcast was the remote
|
||||
// commitment or not.
|
||||
remoteCommit bool
|
||||
|
||||
// timeout denotes if the HTLC should be let timeout, or if the "remote"
|
||||
// party should sweep it on-chain. This also affects what type of
|
||||
// resolution message we expect.
|
||||
timeout bool
|
||||
|
||||
// txToBroadcast is a function closure that should generate the
|
||||
// transaction that should spend the HTLC output. Test authors can use
|
||||
// this to customize the witness used when spending to trigger various
|
||||
// redemption cases.
|
||||
txToBroadcast func() (*wire.MsgTx, error)
|
||||
|
||||
// outcome is the resolver outcome that we expect to be reported once
|
||||
// the contract is fully resolved.
|
||||
outcome channeldb.ResolverOutcome
|
||||
}
|
||||
|
||||
func genHtlcTimeoutTestCases() []htlcTimeoutTestCase {
|
||||
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
|
||||
|
||||
var (
|
||||
@ -125,7 +105,29 @@ func genHtlcTimeoutTestCases() []htlcTimeoutTestCase {
|
||||
},
|
||||
}
|
||||
|
||||
return []htlcTimeoutTestCase{
|
||||
testCases := []struct {
|
||||
// name is a human readable description of the test case.
|
||||
name string
|
||||
|
||||
// remoteCommit denotes if the commitment broadcast was the
|
||||
// remote commitment or not.
|
||||
remoteCommit bool
|
||||
|
||||
// timeout denotes if the HTLC should be let timeout, or if the
|
||||
// "remote" party should sweep it on-chain. This also affects
|
||||
// what type of resolution message we expect.
|
||||
timeout bool
|
||||
|
||||
// txToBroadcast is a function closure that should generate the
|
||||
// transaction that should spend the HTLC output. Test authors
|
||||
// can use this to customize the witness used when spending to
|
||||
// trigger various redemption cases.
|
||||
txToBroadcast func() (*wire.MsgTx, error)
|
||||
|
||||
// outcome is the resolver outcome that we expect to be reported
|
||||
// once the contract is fully resolved.
|
||||
outcome channeldb.ResolverOutcome
|
||||
}{
|
||||
// Remote commitment is broadcast, we time out the HTLC on
|
||||
// chain, and should expect a fail HTLC resolution.
|
||||
{
|
||||
@ -147,8 +149,7 @@ func genHtlcTimeoutTestCases() []htlcTimeoutTestCase {
|
||||
// immediately if the witness is already set
|
||||
// correctly.
|
||||
if reflect.DeepEqual(
|
||||
templateTx.TxIn[0].Witness,
|
||||
witness,
|
||||
templateTx.TxIn[0].Witness, witness,
|
||||
) {
|
||||
|
||||
return templateTx, nil
|
||||
@ -218,8 +219,7 @@ func genHtlcTimeoutTestCases() []htlcTimeoutTestCase {
|
||||
// immediately if the witness is already set
|
||||
// correctly.
|
||||
if reflect.DeepEqual(
|
||||
templateTx.TxIn[0].Witness,
|
||||
witness,
|
||||
templateTx.TxIn[0].Witness, witness,
|
||||
) {
|
||||
|
||||
return templateTx, nil
|
||||
@ -253,8 +253,7 @@ func genHtlcTimeoutTestCases() []htlcTimeoutTestCase {
|
||||
// immediately if the witness is already set
|
||||
// correctly.
|
||||
if reflect.DeepEqual(
|
||||
templateTx.TxIn[0].Witness,
|
||||
witness,
|
||||
templateTx.TxIn[0].Witness, witness,
|
||||
) {
|
||||
|
||||
return templateTx, nil
|
||||
@ -266,280 +265,243 @@ func genHtlcTimeoutTestCases() []htlcTimeoutTestCase {
|
||||
outcome: channeldb.ResolverOutcomeClaimed,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
|
||||
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
|
||||
var fakePreimage lntypes.Preimage
|
||||
|
||||
fakeSignDesc := &input.SignDescriptor{
|
||||
Output: &wire.TxOut{},
|
||||
}
|
||||
|
||||
copy(fakePreimage[:], fakePreimageBytes)
|
||||
|
||||
notifier := &mock.ChainNotifier{
|
||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||
SpendChan: make(chan *chainntnfs.SpendDetail),
|
||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
||||
}
|
||||
|
||||
witnessBeacon := newMockWitnessBeacon()
|
||||
checkPointChan := make(chan struct{}, 1)
|
||||
incubateChan := make(chan struct{}, 1)
|
||||
resolutionChan := make(chan ResolutionMsg, 1)
|
||||
reportChan := make(chan *channeldb.ResolverReport)
|
||||
|
||||
//nolint:lll
|
||||
chainCfg := ChannelArbitratorConfig{
|
||||
ChainArbitratorConfig: ChainArbitratorConfig{
|
||||
Notifier: notifier,
|
||||
Sweeper: newMockSweeper(),
|
||||
PreimageDB: witnessBeacon,
|
||||
IncubateOutputs: func(wire.OutPoint,
|
||||
fn.Option[lnwallet.OutgoingHtlcResolution],
|
||||
fn.Option[lnwallet.IncomingHtlcResolution],
|
||||
uint32, fn.Option[int32]) error {
|
||||
|
||||
incubateChan <- struct{}{}
|
||||
return nil
|
||||
},
|
||||
DeliverResolutionMsg: func(msgs ...ResolutionMsg) error {
|
||||
if len(msgs) != 1 {
|
||||
return fmt.Errorf("expected 1 "+
|
||||
"resolution msg, instead got %v",
|
||||
len(msgs))
|
||||
}
|
||||
|
||||
resolutionChan <- msgs[0]
|
||||
|
||||
return nil
|
||||
},
|
||||
Budget: *DefaultBudgetConfig(),
|
||||
QueryIncomingCircuit: func(circuit models.CircuitKey,
|
||||
) *models.CircuitKey {
|
||||
|
||||
return nil
|
||||
},
|
||||
},
|
||||
PutResolverReport: func(_ kvdb.RwTx,
|
||||
_ *channeldb.ResolverReport) error {
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cfg := ResolverConfig{
|
||||
ChannelArbitratorConfig: chainCfg,
|
||||
Checkpoint: func(_ ContractResolver,
|
||||
reports ...*channeldb.ResolverReport) error {
|
||||
|
||||
checkPointChan <- struct{}{}
|
||||
|
||||
// Send all of our reports into the channel.
|
||||
for _, report := range reports {
|
||||
reportChan <- report
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
resolver := &htlcTimeoutResolver{
|
||||
htlcResolution: lnwallet.OutgoingHtlcResolution{
|
||||
ClaimOutpoint: testChanPoint2,
|
||||
SweepSignDesc: *fakeSignDesc,
|
||||
},
|
||||
contractResolverKit: *newContractResolverKit(
|
||||
cfg,
|
||||
),
|
||||
htlc: channeldb.HTLC{
|
||||
Amt: testHtlcAmt,
|
||||
},
|
||||
}
|
||||
|
||||
var reports []*channeldb.ResolverReport
|
||||
|
||||
// If the test case needs the remote commitment to be
|
||||
// broadcast, then we'll set the timeout commit to a fake
|
||||
// transaction to force the code path.
|
||||
if !testCase.remoteCommit {
|
||||
timeoutTx, err := testCase.txToBroadcast()
|
||||
require.NoError(t, err)
|
||||
|
||||
resolver.htlcResolution.SignedTimeoutTx = timeoutTx
|
||||
|
||||
if testCase.timeout {
|
||||
timeoutTxID := timeoutTx.TxHash()
|
||||
report := &channeldb.ResolverReport{
|
||||
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, //nolint:lll
|
||||
Amount: testHtlcAmt.ToSatoshis(),
|
||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc, //nolint:lll
|
||||
ResolverOutcome: channeldb.ResolverOutcomeFirstStage, //nolint:lll
|
||||
SpendTxID: &timeoutTxID,
|
||||
}
|
||||
|
||||
reports = append(reports, report)
|
||||
}
|
||||
}
|
||||
|
||||
// With all the setup above complete, we can initiate the
|
||||
// resolution process, and the bulk of our test.
|
||||
var wg sync.WaitGroup
|
||||
resolveErr := make(chan error, 1)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
_, err := resolver.Resolve(false)
|
||||
if err != nil {
|
||||
resolveErr <- err
|
||||
}
|
||||
}()
|
||||
|
||||
// If this is a remote commit, then we expct the outputs should receive
|
||||
// an incubation request to go through the sweeper, otherwise the
|
||||
// nursery.
|
||||
var sweepChan chan input.Input
|
||||
if testCase.remoteCommit {
|
||||
mockSweeper, ok := resolver.Sweeper.(*mockSweeper)
|
||||
require.True(t, ok)
|
||||
sweepChan = mockSweeper.sweptInputs
|
||||
}
|
||||
|
||||
// The output should be offered to either the sweeper or
|
||||
// the nursery.
|
||||
select {
|
||||
case <-incubateChan:
|
||||
case <-sweepChan:
|
||||
case err := <-resolveErr:
|
||||
t.Fatalf("unable to resolve HTLC: %v", err)
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("failed to receive incubation request")
|
||||
}
|
||||
|
||||
// Next, the resolver should request a spend notification for
|
||||
// the direct HTLC output. We'll use the txToBroadcast closure
|
||||
// for the test case to generate the transaction that we'll
|
||||
// send to the resolver.
|
||||
spendingTx, err := testCase.txToBroadcast()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate tx: %v", err)
|
||||
}
|
||||
spendTxHash := spendingTx.TxHash()
|
||||
|
||||
select {
|
||||
case notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||
SpendingTx: spendingTx,
|
||||
SpenderTxHash: &spendTxHash,
|
||||
}:
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("failed to request spend ntfn")
|
||||
}
|
||||
|
||||
if !testCase.timeout {
|
||||
// If the resolver should settle now, then we'll
|
||||
// extract the pre-image to be extracted and the
|
||||
// resolution message sent.
|
||||
select {
|
||||
case newPreimage := <-witnessBeacon.newPreimages:
|
||||
if newPreimage[0] != fakePreimage {
|
||||
t.Fatalf("wrong pre-image: "+
|
||||
"expected %v, got %v",
|
||||
fakePreimage, newPreimage)
|
||||
}
|
||||
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("pre-image not added")
|
||||
}
|
||||
|
||||
// Finally, we should get a resolution message with the
|
||||
// pre-image set within the message.
|
||||
select {
|
||||
case resolutionMsg := <-resolutionChan:
|
||||
// Once again, the pre-images should match up.
|
||||
if *resolutionMsg.PreImage != fakePreimage {
|
||||
t.Fatalf("wrong pre-image: "+
|
||||
"expected %v, got %v",
|
||||
fakePreimage, resolutionMsg.PreImage)
|
||||
}
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("resolution not sent")
|
||||
}
|
||||
} else {
|
||||
// Otherwise, the HTLC should now timeout. First, we
|
||||
// should get a resolution message with a populated
|
||||
// failure message.
|
||||
select {
|
||||
case resolutionMsg := <-resolutionChan:
|
||||
if resolutionMsg.Failure == nil {
|
||||
t.Fatalf("expected failure resolution msg")
|
||||
}
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("resolution not sent")
|
||||
}
|
||||
|
||||
// We should also get another request for the spend
|
||||
// notification of the second-level transaction to
|
||||
// indicate that it's been swept by the nursery, but
|
||||
// only if this is a local commitment transaction.
|
||||
if !testCase.remoteCommit {
|
||||
select {
|
||||
case notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||
SpendingTx: spendingTx,
|
||||
SpenderTxHash: &spendTxHash,
|
||||
}:
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("failed to request spend ntfn")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In any case, before the resolver exits, it should checkpoint
|
||||
// its final state.
|
||||
select {
|
||||
case <-checkPointChan:
|
||||
case err := <-resolveErr:
|
||||
t.Fatalf("unable to resolve HTLC: %v", err)
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("check point not received")
|
||||
}
|
||||
|
||||
// Add a report to our set of expected reports with the outcome
|
||||
// that the test specifies (either success or timeout).
|
||||
spendTxID := spendingTx.TxHash()
|
||||
amt := btcutil.Amount(fakeSignDesc.Output.Value)
|
||||
|
||||
reports = append(reports, &channeldb.ResolverReport{
|
||||
OutPoint: testChanPoint2,
|
||||
Amount: amt,
|
||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||
ResolverOutcome: testCase.outcome,
|
||||
SpendTxID: &spendTxID,
|
||||
})
|
||||
|
||||
for _, report := range reports {
|
||||
assertResolverReport(t, reportChan, report)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Finally, the resolver should be marked as resolved.
|
||||
if !resolver.resolved {
|
||||
t.Fatalf("resolver should be marked as resolved")
|
||||
}
|
||||
}
|
||||
|
||||
// TestHtlcTimeoutResolver tests that the timeout resolver properly handles all
|
||||
// variations of possible local+remote spends.
|
||||
func TestHtlcTimeoutResolver(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
testCases := genHtlcTimeoutTestCases()
|
||||
|
||||
for _, testCase := range testCases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
testHtlcTimeoutResolver(t, testCase)
|
||||
t.Logf("Running test case: %v", testCase.name)
|
||||
|
||||
checkPointChan := make(chan struct{}, 1)
|
||||
incubateChan := make(chan struct{}, 1)
|
||||
resolutionChan := make(chan ResolutionMsg, 1)
|
||||
reportChan := make(chan *channeldb.ResolverReport)
|
||||
|
||||
//nolint:lll
|
||||
chainCfg := ChannelArbitratorConfig{
|
||||
ChainArbitratorConfig: ChainArbitratorConfig{
|
||||
Notifier: notifier,
|
||||
PreimageDB: witnessBeacon,
|
||||
IncubateOutputs: func(wire.OutPoint,
|
||||
fn.Option[lnwallet.OutgoingHtlcResolution],
|
||||
fn.Option[lnwallet.IncomingHtlcResolution],
|
||||
uint32, fn.Option[int32]) error {
|
||||
|
||||
incubateChan <- struct{}{}
|
||||
return nil
|
||||
},
|
||||
DeliverResolutionMsg: func(msgs ...ResolutionMsg) error {
|
||||
if len(msgs) != 1 {
|
||||
return fmt.Errorf("expected 1 "+
|
||||
"resolution msg, instead got %v",
|
||||
len(msgs))
|
||||
}
|
||||
|
||||
resolutionChan <- msgs[0]
|
||||
return nil
|
||||
},
|
||||
Budget: *DefaultBudgetConfig(),
|
||||
QueryIncomingCircuit: func(circuit models.CircuitKey) *models.CircuitKey {
|
||||
return nil
|
||||
},
|
||||
},
|
||||
PutResolverReport: func(_ kvdb.RwTx,
|
||||
_ *channeldb.ResolverReport) error {
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
|
||||
cfg := ResolverConfig{
|
||||
ChannelArbitratorConfig: chainCfg,
|
||||
Checkpoint: func(_ ContractResolver,
|
||||
reports ...*channeldb.ResolverReport) error {
|
||||
|
||||
checkPointChan <- struct{}{}
|
||||
|
||||
// Send all of our reports into the channel.
|
||||
for _, report := range reports {
|
||||
reportChan <- report
|
||||
}
|
||||
|
||||
return nil
|
||||
},
|
||||
}
|
||||
resolver := &htlcTimeoutResolver{
|
||||
htlcResolution: lnwallet.OutgoingHtlcResolution{
|
||||
ClaimOutpoint: testChanPoint2,
|
||||
SweepSignDesc: *fakeSignDesc,
|
||||
},
|
||||
contractResolverKit: *newContractResolverKit(
|
||||
cfg,
|
||||
),
|
||||
htlc: channeldb.HTLC{
|
||||
Amt: testHtlcAmt,
|
||||
},
|
||||
}
|
||||
|
||||
var reports []*channeldb.ResolverReport
|
||||
|
||||
// If the test case needs the remote commitment to be
|
||||
// broadcast, then we'll set the timeout commit to a fake
|
||||
// transaction to force the code path.
|
||||
if !testCase.remoteCommit {
|
||||
timeoutTx, err := testCase.txToBroadcast()
|
||||
require.NoError(t, err)
|
||||
|
||||
resolver.htlcResolution.SignedTimeoutTx = timeoutTx
|
||||
|
||||
if testCase.timeout {
|
||||
timeoutTxID := timeoutTx.TxHash()
|
||||
reports = append(reports, &channeldb.ResolverReport{
|
||||
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
|
||||
Amount: testHtlcAmt.ToSatoshis(),
|
||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
||||
SpendTxID: &timeoutTxID,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// With all the setup above complete, we can initiate the
|
||||
// resolution process, and the bulk of our test.
|
||||
var wg sync.WaitGroup
|
||||
resolveErr := make(chan error, 1)
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
|
||||
_, err := resolver.Resolve(false)
|
||||
if err != nil {
|
||||
resolveErr <- err
|
||||
}
|
||||
}()
|
||||
|
||||
// At the output isn't yet in the nursery, we expect that we
|
||||
// should receive an incubation request.
|
||||
select {
|
||||
case <-incubateChan:
|
||||
case err := <-resolveErr:
|
||||
t.Fatalf("unable to resolve HTLC: %v", err)
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("failed to receive incubation request")
|
||||
}
|
||||
|
||||
// Next, the resolver should request a spend notification for
|
||||
// the direct HTLC output. We'll use the txToBroadcast closure
|
||||
// for the test case to generate the transaction that we'll
|
||||
// send to the resolver.
|
||||
spendingTx, err := testCase.txToBroadcast()
|
||||
if err != nil {
|
||||
t.Fatalf("unable to generate tx: %v", err)
|
||||
}
|
||||
spendTxHash := spendingTx.TxHash()
|
||||
|
||||
select {
|
||||
case notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||
SpendingTx: spendingTx,
|
||||
SpenderTxHash: &spendTxHash,
|
||||
}:
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("failed to request spend ntfn")
|
||||
}
|
||||
|
||||
if !testCase.timeout {
|
||||
// If the resolver should settle now, then we'll
|
||||
// extract the pre-image to be extracted and the
|
||||
// resolution message sent.
|
||||
select {
|
||||
case newPreimage := <-witnessBeacon.newPreimages:
|
||||
if newPreimage[0] != fakePreimage {
|
||||
t.Fatalf("wrong pre-image: "+
|
||||
"expected %v, got %v",
|
||||
fakePreimage, newPreimage)
|
||||
}
|
||||
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("pre-image not added")
|
||||
}
|
||||
|
||||
// Finally, we should get a resolution message with the
|
||||
// pre-image set within the message.
|
||||
select {
|
||||
case resolutionMsg := <-resolutionChan:
|
||||
// Once again, the pre-images should match up.
|
||||
if *resolutionMsg.PreImage != fakePreimage {
|
||||
t.Fatalf("wrong pre-image: "+
|
||||
"expected %v, got %v",
|
||||
fakePreimage, resolutionMsg.PreImage)
|
||||
}
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("resolution not sent")
|
||||
}
|
||||
} else {
|
||||
|
||||
// Otherwise, the HTLC should now timeout. First, we
|
||||
// should get a resolution message with a populated
|
||||
// failure message.
|
||||
select {
|
||||
case resolutionMsg := <-resolutionChan:
|
||||
if resolutionMsg.Failure == nil {
|
||||
t.Fatalf("expected failure resolution msg")
|
||||
}
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("resolution not sent")
|
||||
}
|
||||
|
||||
// We should also get another request for the spend
|
||||
// notification of the second-level transaction to
|
||||
// indicate that it's been swept by the nursery, but
|
||||
// only if this is a local commitment transaction.
|
||||
if !testCase.remoteCommit {
|
||||
select {
|
||||
case notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||
SpendingTx: spendingTx,
|
||||
SpenderTxHash: &spendTxHash,
|
||||
}:
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("failed to request spend ntfn")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// In any case, before the resolver exits, it should checkpoint
|
||||
// its final state.
|
||||
select {
|
||||
case <-checkPointChan:
|
||||
case err := <-resolveErr:
|
||||
t.Fatalf("unable to resolve HTLC: %v", err)
|
||||
case <-time.After(time.Second * 5):
|
||||
t.Fatalf("check point not received")
|
||||
}
|
||||
|
||||
// Add a report to our set of expected reports with the outcome
|
||||
// that the test specifies (either success or timeout).
|
||||
spendTxID := spendingTx.TxHash()
|
||||
amt := btcutil.Amount(fakeSignDesc.Output.Value)
|
||||
|
||||
reports = append(reports, &channeldb.ResolverReport{
|
||||
OutPoint: testChanPoint2,
|
||||
Amount: amt,
|
||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||
ResolverOutcome: testCase.outcome,
|
||||
SpendTxID: &spendTxID,
|
||||
})
|
||||
|
||||
for _, report := range reports {
|
||||
assertResolverReport(t, reportChan, report)
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
// Finally, the resolver should be marked as resolved.
|
||||
if !resolver.resolved {
|
||||
t.Fatalf("resolver should be marked as resolved")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -574,12 +536,15 @@ func TestHtlcTimeoutSingleStage(t *testing.T) {
|
||||
}
|
||||
|
||||
checkpoints := []checkpoint{
|
||||
{
|
||||
// Output should be handed off to the nursery.
|
||||
incubating: true,
|
||||
},
|
||||
{
|
||||
// We send a confirmation the sweep tx from published
|
||||
// by the nursery.
|
||||
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||
_ bool) error {
|
||||
|
||||
// The nursery will create and publish a sweep
|
||||
// tx.
|
||||
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||
@ -605,7 +570,7 @@ func TestHtlcTimeoutSingleStage(t *testing.T) {
|
||||
// After the sweep has confirmed, we expect the
|
||||
// checkpoint to be resolved, and with the above
|
||||
// report.
|
||||
incubating: false,
|
||||
incubating: true,
|
||||
resolved: true,
|
||||
reports: []*channeldb.ResolverReport{
|
||||
claim,
|
||||
@ -688,7 +653,6 @@ func TestHtlcTimeoutSecondStage(t *testing.T) {
|
||||
// that our sweep succeeded.
|
||||
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||
_ bool) error {
|
||||
|
||||
// The nursery will publish the timeout tx.
|
||||
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||
SpendingTx: timeoutTx,
|
||||
@ -860,9 +824,9 @@ func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) {
|
||||
)
|
||||
}
|
||||
|
||||
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remote commitment
|
||||
// confirms, and the remote spends the output using the success tx, we properly
|
||||
// detect this and extract the preimage.
|
||||
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remite commitment
|
||||
// confirms, and the remote spends the output using the success tx, we
|
||||
// properly detect this and extract the preimage.
|
||||
func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
||||
commitOutpoint := wire.OutPoint{Index: 2}
|
||||
|
||||
@ -906,6 +870,10 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
||||
}
|
||||
|
||||
checkpoints := []checkpoint{
|
||||
{
|
||||
// Output should be handed off to the nursery.
|
||||
incubating: true,
|
||||
},
|
||||
{
|
||||
// We send a confirmation for the remote's second layer
|
||||
// success transcation.
|
||||
@ -951,7 +919,7 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
||||
// After the sweep has confirmed, we expect the
|
||||
// checkpoint to be resolved, and with the above
|
||||
// report.
|
||||
incubating: false,
|
||||
incubating: true,
|
||||
resolved: true,
|
||||
reports: []*channeldb.ResolverReport{
|
||||
claim,
|
||||
@ -1330,8 +1298,6 @@ func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) {
|
||||
func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution,
|
||||
checkpoints []checkpoint) {
|
||||
|
||||
t.Helper()
|
||||
|
||||
defer timeout()()
|
||||
|
||||
// We first run the resolver from start to finish, ensuring it gets
|
||||
|
||||
@ -30,7 +30,6 @@ type Registry interface {
|
||||
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
|
||||
expiry uint32, currentHeight int32,
|
||||
circuitKey models.CircuitKey, hodlChan chan<- interface{},
|
||||
wireCustomRecords lnwire.CustomRecords,
|
||||
payload invoices.Payload) (invoices.HtlcResolution, error)
|
||||
|
||||
// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
|
||||
|
||||
@ -10,6 +10,5 @@ type mockHTLCNotifier struct {
|
||||
}
|
||||
|
||||
func (m *mockHTLCNotifier) NotifyFinalHtlcEvent(key models.CircuitKey,
|
||||
info channeldb.FinalHtlcInfo) {
|
||||
|
||||
info channeldb.FinalHtlcInfo) { //nolint:whitespace
|
||||
}
|
||||
|
||||
@ -26,7 +26,6 @@ type mockRegistry struct {
|
||||
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
|
||||
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||
circuitKey models.CircuitKey, hodlChan chan<- interface{},
|
||||
wireCustomRecords lnwire.CustomRecords,
|
||||
payload invoices.Payload) (invoices.HtlcResolution, error) {
|
||||
|
||||
r.notifyChan <- notifyExitHopData{
|
||||
|
||||
@ -8,6 +8,9 @@ import (
|
||||
)
|
||||
|
||||
const (
|
||||
taprootCtrlBlockType tlv.Type = 0
|
||||
taprootTapTweakType tlv.Type = 1
|
||||
|
||||
commitCtrlBlockType tlv.Type = 0
|
||||
revokeCtrlBlockType tlv.Type = 1
|
||||
outgoingHtlcCtrlBlockType tlv.Type = 2
|
||||
@ -23,67 +26,36 @@ const (
|
||||
// information we need to sweep taproot outputs.
|
||||
type taprootBriefcase struct {
|
||||
// CtrlBlock is the set of control block for the taproot outputs.
|
||||
CtrlBlocks tlv.RecordT[tlv.TlvType0, ctrlBlocks]
|
||||
CtrlBlocks *ctrlBlocks
|
||||
|
||||
// TapTweaks is the set of taproot tweaks for the taproot outputs that
|
||||
// are to be spent via a keyspend path. This includes anchors, and any
|
||||
// revocation paths.
|
||||
TapTweaks tlv.RecordT[tlv.TlvType1, tapTweaks]
|
||||
|
||||
// SettledCommitBlob is an optional record that contains an opaque blob
|
||||
// that may be used to properly sweep commitment outputs on a force
|
||||
// close transaction.
|
||||
SettledCommitBlob tlv.OptionalRecordT[tlv.TlvType2, tlv.Blob]
|
||||
|
||||
// BreachCommitBlob is an optional record that contains an opaque blob
|
||||
// used to sweep a remote party's breached output.
|
||||
BreachedCommitBlob tlv.OptionalRecordT[tlv.TlvType3, tlv.Blob]
|
||||
|
||||
// HtlcBlobs is an optikonal record that contains the opaque blobs for
|
||||
// the set of active HTLCs on the commitment transaction.
|
||||
HtlcBlobs tlv.OptionalRecordT[tlv.TlvType4, htlcAuxBlobs]
|
||||
TapTweaks *tapTweaks
|
||||
}
|
||||
|
||||
// TODO(roasbeef): morph into new tlv record
|
||||
|
||||
// newTaprootBriefcase returns a new instance of the taproot specific briefcase
|
||||
// variant.
|
||||
func newTaprootBriefcase() *taprootBriefcase {
|
||||
return &taprootBriefcase{
|
||||
CtrlBlocks: tlv.NewRecordT[tlv.TlvType0](newCtrlBlocks()),
|
||||
TapTweaks: tlv.NewRecordT[tlv.TlvType1](newTapTweaks()),
|
||||
CtrlBlocks: newCtrlBlocks(),
|
||||
TapTweaks: newTapTweaks(),
|
||||
}
|
||||
}
|
||||
|
||||
// EncodeRecords returns a slice of TLV records that should be encoded.
|
||||
func (t *taprootBriefcase) EncodeRecords() []tlv.Record {
|
||||
records := []tlv.Record{
|
||||
t.CtrlBlocks.Record(),
|
||||
t.TapTweaks.Record(),
|
||||
return []tlv.Record{
|
||||
newCtrlBlocksRecord(&t.CtrlBlocks),
|
||||
newTapTweaksRecord(&t.TapTweaks),
|
||||
}
|
||||
|
||||
t.SettledCommitBlob.WhenSome(
|
||||
func(r tlv.RecordT[tlv.TlvType2, tlv.Blob]) {
|
||||
records = append(records, r.Record())
|
||||
},
|
||||
)
|
||||
t.BreachedCommitBlob.WhenSome(
|
||||
func(r tlv.RecordT[tlv.TlvType3, tlv.Blob]) {
|
||||
records = append(records, r.Record())
|
||||
},
|
||||
)
|
||||
t.HtlcBlobs.WhenSome(func(r tlv.RecordT[tlv.TlvType4, htlcAuxBlobs]) {
|
||||
records = append(records, r.Record())
|
||||
})
|
||||
|
||||
return records
|
||||
}
|
||||
|
||||
// DecodeRecords returns a slice of TLV records that should be decoded.
|
||||
func (t *taprootBriefcase) DecodeRecords() []tlv.Record {
|
||||
return []tlv.Record{
|
||||
t.CtrlBlocks.Record(),
|
||||
t.TapTweaks.Record(),
|
||||
newCtrlBlocksRecord(&t.CtrlBlocks),
|
||||
newTapTweaksRecord(&t.TapTweaks),
|
||||
}
|
||||
}
|
||||
|
||||
@ -99,35 +71,12 @@ func (t *taprootBriefcase) Encode(w io.Writer) error {
|
||||
|
||||
// Decode decodes the given reader into the target struct.
|
||||
func (t *taprootBriefcase) Decode(r io.Reader) error {
|
||||
settledCommitBlob := t.SettledCommitBlob.Zero()
|
||||
breachedCommitBlob := t.BreachedCommitBlob.Zero()
|
||||
htlcBlobs := t.HtlcBlobs.Zero()
|
||||
|
||||
records := append(
|
||||
t.DecodeRecords(), settledCommitBlob.Record(),
|
||||
breachedCommitBlob.Record(), htlcBlobs.Record(),
|
||||
)
|
||||
stream, err := tlv.NewStream(records...)
|
||||
stream, err := tlv.NewStream(t.DecodeRecords()...)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
typeMap, err := stream.DecodeWithParsedTypes(r)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if val, ok := typeMap[t.SettledCommitBlob.TlvType()]; ok && val == nil {
|
||||
t.SettledCommitBlob = tlv.SomeRecordT(settledCommitBlob)
|
||||
}
|
||||
if v, ok := typeMap[t.BreachedCommitBlob.TlvType()]; ok && v == nil {
|
||||
t.BreachedCommitBlob = tlv.SomeRecordT(breachedCommitBlob)
|
||||
}
|
||||
if v, ok := typeMap[t.HtlcBlobs.TlvType()]; ok && v == nil {
|
||||
t.HtlcBlobs = tlv.SomeRecordT(htlcBlobs)
|
||||
}
|
||||
|
||||
return nil
|
||||
return stream.Decode(r)
|
||||
}
|
||||
|
||||
// resolverCtrlBlocks is a map of resolver IDs to their corresponding control
|
||||
@ -267,8 +216,8 @@ type ctrlBlocks struct {
|
||||
}
|
||||
|
||||
// newCtrlBlocks returns a new instance of the ctrlBlocks struct.
|
||||
func newCtrlBlocks() ctrlBlocks {
|
||||
return ctrlBlocks{
|
||||
func newCtrlBlocks() *ctrlBlocks {
|
||||
return &ctrlBlocks{
|
||||
OutgoingHtlcCtrlBlocks: newResolverCtrlBlocks(),
|
||||
IncomingHtlcCtrlBlocks: newResolverCtrlBlocks(),
|
||||
SecondLevelCtrlBlocks: newResolverCtrlBlocks(),
|
||||
@ -311,7 +260,7 @@ func varBytesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
|
||||
|
||||
// ctrlBlockEncoder is a custom TLV encoder for the ctrlBlocks struct.
|
||||
func ctrlBlockEncoder(w io.Writer, val any, _ *[8]byte) error {
|
||||
if t, ok := val.(*ctrlBlocks); ok {
|
||||
if t, ok := val.(**ctrlBlocks); ok {
|
||||
return (*t).Encode(w)
|
||||
}
|
||||
|
||||
@ -320,7 +269,7 @@ func ctrlBlockEncoder(w io.Writer, val any, _ *[8]byte) error {
|
||||
|
||||
// ctrlBlockDecoder is a custom TLV decoder for the ctrlBlocks struct.
|
||||
func ctrlBlockDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
||||
if typ, ok := val.(*ctrlBlocks); ok {
|
||||
if typ, ok := val.(**ctrlBlocks); ok {
|
||||
ctrlReader := io.LimitReader(r, int64(l))
|
||||
|
||||
var ctrlBlocks ctrlBlocks
|
||||
@ -329,7 +278,7 @@ func ctrlBlockDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
*typ = ctrlBlocks
|
||||
*typ = &ctrlBlocks
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -337,6 +286,28 @@ func ctrlBlockDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
||||
return tlv.NewTypeForDecodingErr(val, "ctrlBlocks", l, l)
|
||||
}
|
||||
|
||||
// newCtrlBlocksRecord returns a new TLV record that can be used to
|
||||
// encode/decode the set of cotrol blocks for the taproot outputs for a
|
||||
// channel.
|
||||
func newCtrlBlocksRecord(blks **ctrlBlocks) tlv.Record {
|
||||
recordSize := func() uint64 {
|
||||
var (
|
||||
b bytes.Buffer
|
||||
buf [8]byte
|
||||
)
|
||||
if err := ctrlBlockEncoder(&b, blks, &buf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return uint64(len(b.Bytes()))
|
||||
}
|
||||
|
||||
return tlv.MakeDynamicRecord(
|
||||
taprootCtrlBlockType, blks, recordSize, ctrlBlockEncoder,
|
||||
ctrlBlockDecoder,
|
||||
)
|
||||
}
|
||||
|
||||
// EncodeRecords returns the set of TLV records that encode the control block
|
||||
// for the commitment transaction.
|
||||
func (c *ctrlBlocks) EncodeRecords() []tlv.Record {
|
||||
@ -411,21 +382,7 @@ func (c *ctrlBlocks) DecodeRecords() []tlv.Record {
|
||||
// Record returns a TLV record that can be used to encode/decode the control
|
||||
// blocks. type from a given TLV stream.
|
||||
func (c *ctrlBlocks) Record() tlv.Record {
|
||||
recordSize := func() uint64 {
|
||||
var (
|
||||
b bytes.Buffer
|
||||
buf [8]byte
|
||||
)
|
||||
if err := ctrlBlockEncoder(&b, c, &buf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return uint64(len(b.Bytes()))
|
||||
}
|
||||
|
||||
return tlv.MakeDynamicRecord(
|
||||
0, c, recordSize, ctrlBlockEncoder, ctrlBlockDecoder,
|
||||
)
|
||||
return tlv.MakePrimitiveRecord(commitCtrlBlockType, c)
|
||||
}
|
||||
|
||||
// Encode encodes the set of control blocks.
|
||||
@ -573,8 +530,8 @@ type tapTweaks struct {
|
||||
}
|
||||
|
||||
// newTapTweaks returns a new tapTweaks struct.
|
||||
func newTapTweaks() tapTweaks {
|
||||
return tapTweaks{
|
||||
func newTapTweaks() *tapTweaks {
|
||||
return &tapTweaks{
|
||||
BreachedHtlcTweaks: make(htlcTapTweaks),
|
||||
BreachedSecondLevelHltcTweaks: make(htlcTapTweaks),
|
||||
}
|
||||
@ -582,7 +539,7 @@ func newTapTweaks() tapTweaks {
|
||||
|
||||
// tapTweaksEncoder is a custom TLV encoder for the tapTweaks struct.
|
||||
func tapTweaksEncoder(w io.Writer, val any, _ *[8]byte) error {
|
||||
if t, ok := val.(*tapTweaks); ok {
|
||||
if t, ok := val.(**tapTweaks); ok {
|
||||
return (*t).Encode(w)
|
||||
}
|
||||
|
||||
@ -591,7 +548,7 @@ func tapTweaksEncoder(w io.Writer, val any, _ *[8]byte) error {
|
||||
|
||||
// tapTweaksDecoder is a custom TLV decoder for the tapTweaks struct.
|
||||
func tapTweaksDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
||||
if typ, ok := val.(*tapTweaks); ok {
|
||||
if typ, ok := val.(**tapTweaks); ok {
|
||||
tweakReader := io.LimitReader(r, int64(l))
|
||||
|
||||
var tapTweaks tapTweaks
|
||||
@ -600,7 +557,7 @@ func tapTweaksDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
||||
return err
|
||||
}
|
||||
|
||||
*typ = tapTweaks
|
||||
*typ = &tapTweaks
|
||||
|
||||
return nil
|
||||
}
|
||||
@ -608,6 +565,27 @@ func tapTweaksDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
||||
return tlv.NewTypeForDecodingErr(val, "tapTweaks", l, l)
|
||||
}
|
||||
|
||||
// newTapTweaksRecord returns a new TLV record that can be used to
|
||||
// encode/decode the tap tweak structs.
|
||||
func newTapTweaksRecord(tweaks **tapTweaks) tlv.Record {
|
||||
recordSize := func() uint64 {
|
||||
var (
|
||||
b bytes.Buffer
|
||||
buf [8]byte
|
||||
)
|
||||
if err := tapTweaksEncoder(&b, tweaks, &buf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return uint64(len(b.Bytes()))
|
||||
}
|
||||
|
||||
return tlv.MakeDynamicRecord(
|
||||
taprootTapTweakType, tweaks, recordSize, tapTweaksEncoder,
|
||||
tapTweaksDecoder,
|
||||
)
|
||||
}
|
||||
|
||||
// EncodeRecords returns the set of TLV records that encode the tweaks.
|
||||
func (t *tapTweaks) EncodeRecords() []tlv.Record {
|
||||
var records []tlv.Record
|
||||
@ -659,21 +637,7 @@ func (t *tapTweaks) DecodeRecords() []tlv.Record {
|
||||
// Record returns a TLV record that can be used to encode/decode the tap
|
||||
// tweaks.
|
||||
func (t *tapTweaks) Record() tlv.Record {
|
||||
recordSize := func() uint64 {
|
||||
var (
|
||||
b bytes.Buffer
|
||||
buf [8]byte
|
||||
)
|
||||
if err := tapTweaksEncoder(&b, t, &buf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return uint64(len(b.Bytes()))
|
||||
}
|
||||
|
||||
return tlv.MakeDynamicRecord(
|
||||
0, t, recordSize, tapTweaksEncoder, tapTweaksDecoder,
|
||||
)
|
||||
return tlv.MakePrimitiveRecord(taprootTapTweakType, t)
|
||||
}
|
||||
|
||||
// Encode encodes the set of tap tweaks.
|
||||
@ -695,110 +659,3 @@ func (t *tapTweaks) Decode(r io.Reader) error {
|
||||
|
||||
return stream.Decode(r)
|
||||
}
|
||||
|
||||
// htlcAuxBlobs is a map of resolver IDs to their corresponding HTLC blobs.
|
||||
// This is used to store the resolution blobs for HTLCs that are not yet
|
||||
// resolved.
|
||||
type htlcAuxBlobs map[resolverID]tlv.Blob
|
||||
|
||||
// newAuxHtlcBlobs returns a new instance of the htlcAuxBlobs struct.
|
||||
func newAuxHtlcBlobs() htlcAuxBlobs {
|
||||
return make(htlcAuxBlobs)
|
||||
}
|
||||
|
||||
// Encode encodes the set of HTLC blobs into the target writer.
|
||||
func (h *htlcAuxBlobs) Encode(w io.Writer) error {
|
||||
var buf [8]byte
|
||||
|
||||
numBlobs := uint64(len(*h))
|
||||
if err := tlv.WriteVarInt(w, numBlobs, &buf); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for id, blob := range *h {
|
||||
if _, err := w.Write(id[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := varBytesEncoder(w, &blob, &buf); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// Decode decodes the set of HTLC blobs from the target reader.
|
||||
func (h *htlcAuxBlobs) Decode(r io.Reader) error {
|
||||
var buf [8]byte
|
||||
|
||||
numBlobs, err := tlv.ReadVarInt(r, &buf)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := uint64(0); i < numBlobs; i++ {
|
||||
var id resolverID
|
||||
if _, err := io.ReadFull(r, id[:]); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var blob tlv.Blob
|
||||
if err := varBytesDecoder(r, &blob, &buf, 0); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
(*h)[id] = blob
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// eHtlcAuxBlobsEncoder is a custom TLV encoder for the htlcAuxBlobs struct.
|
||||
func htlcAuxBlobsEncoder(w io.Writer, val any, _ *[8]byte) error {
|
||||
if t, ok := val.(*htlcAuxBlobs); ok {
|
||||
return (*t).Encode(w)
|
||||
}
|
||||
|
||||
return tlv.NewTypeForEncodingErr(val, "htlcAuxBlobs")
|
||||
}
|
||||
|
||||
// dHtlcAuxBlobsDecoder is a custom TLV decoder for the htlcAuxBlobs struct.
|
||||
func htlcAuxBlobsDecoder(r io.Reader, val any, _ *[8]byte,
|
||||
l uint64) error {
|
||||
|
||||
if typ, ok := val.(*htlcAuxBlobs); ok {
|
||||
blobReader := io.LimitReader(r, int64(l))
|
||||
|
||||
htlcBlobs := newAuxHtlcBlobs()
|
||||
err := htlcBlobs.Decode(blobReader)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*typ = htlcBlobs
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
return tlv.NewTypeForDecodingErr(val, "htlcAuxBlobs", l, l)
|
||||
}
|
||||
|
||||
// Record returns a tlv.Record for the htlcAuxBlobs struct.
|
||||
func (h *htlcAuxBlobs) Record() tlv.Record {
|
||||
recordSize := func() uint64 {
|
||||
var (
|
||||
b bytes.Buffer
|
||||
buf [8]byte
|
||||
)
|
||||
if err := htlcAuxBlobsEncoder(&b, h, &buf); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return uint64(len(b.Bytes()))
|
||||
}
|
||||
|
||||
return tlv.MakeDynamicRecord(
|
||||
0, h, recordSize, htlcAuxBlobsEncoder, htlcAuxBlobsDecoder,
|
||||
)
|
||||
}
|
||||
|
||||
@ -5,9 +5,7 @@ import (
|
||||
"math/rand"
|
||||
"testing"
|
||||
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
"github.com/stretchr/testify/require"
|
||||
"pgregory.net/rapid"
|
||||
)
|
||||
|
||||
func randResolverCtrlBlocks(t *testing.T) resolverCtrlBlocks {
|
||||
@ -54,25 +52,6 @@ func randHtlcTweaks(t *testing.T) htlcTapTweaks {
|
||||
return tweaks
|
||||
}
|
||||
|
||||
func randHtlcAuxBlobs(t *testing.T) htlcAuxBlobs {
|
||||
numBlobs := rand.Int() % 256
|
||||
blobs := make(htlcAuxBlobs, numBlobs)
|
||||
|
||||
for i := 0; i < numBlobs; i++ {
|
||||
var id resolverID
|
||||
_, err := rand.Read(id[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
var blob [100]byte
|
||||
_, err = rand.Read(blob[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
blobs[id] = blob[:]
|
||||
}
|
||||
|
||||
return blobs
|
||||
}
|
||||
|
||||
// TestTaprootBriefcase tests the encode/decode methods of the taproot
|
||||
// briefcase extension.
|
||||
func TestTaprootBriefcase(t *testing.T) {
|
||||
@ -90,32 +69,19 @@ func TestTaprootBriefcase(t *testing.T) {
|
||||
_, err = rand.Read(anchorTweak[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
var commitBlob [100]byte
|
||||
_, err = rand.Read(commitBlob[:])
|
||||
require.NoError(t, err)
|
||||
|
||||
testCase := &taprootBriefcase{
|
||||
CtrlBlocks: tlv.NewRecordT[tlv.TlvType0](ctrlBlocks{
|
||||
CtrlBlocks: &ctrlBlocks{
|
||||
CommitSweepCtrlBlock: sweepCtrlBlock[:],
|
||||
RevokeSweepCtrlBlock: revokeCtrlBlock[:],
|
||||
OutgoingHtlcCtrlBlocks: randResolverCtrlBlocks(t),
|
||||
IncomingHtlcCtrlBlocks: randResolverCtrlBlocks(t),
|
||||
SecondLevelCtrlBlocks: randResolverCtrlBlocks(t),
|
||||
}),
|
||||
TapTweaks: tlv.NewRecordT[tlv.TlvType1](tapTweaks{
|
||||
},
|
||||
TapTweaks: &tapTweaks{
|
||||
AnchorTweak: anchorTweak[:],
|
||||
BreachedHtlcTweaks: randHtlcTweaks(t),
|
||||
BreachedSecondLevelHltcTweaks: randHtlcTweaks(t),
|
||||
}),
|
||||
SettledCommitBlob: tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType2](commitBlob[:]),
|
||||
),
|
||||
BreachedCommitBlob: tlv.SomeRecordT(
|
||||
tlv.NewPrimitiveRecord[tlv.TlvType3](commitBlob[:]),
|
||||
),
|
||||
HtlcBlobs: tlv.SomeRecordT(
|
||||
tlv.NewRecordT[tlv.TlvType4](randHtlcAuxBlobs(t)),
|
||||
),
|
||||
},
|
||||
}
|
||||
|
||||
var b bytes.Buffer
|
||||
@ -126,21 +92,3 @@ func TestTaprootBriefcase(t *testing.T) {
|
||||
|
||||
require.Equal(t, testCase, &decodedCase)
|
||||
}
|
||||
|
||||
// TestHtlcAuxBlobEncodeDecode tests the encode/decode methods of the HTLC aux
|
||||
// blobs.
|
||||
func TestHtlcAuxBlobEncodeDecode(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
rapid.Check(t, func(t *rapid.T) {
|
||||
htlcBlobs := rapid.Make[htlcAuxBlobs]().Draw(t, "htlcAuxBlobs")
|
||||
|
||||
var b bytes.Buffer
|
||||
require.NoError(t, htlcBlobs.Encode(&b))
|
||||
|
||||
decodedBlobs := newAuxHtlcBlobs()
|
||||
require.NoError(t, decodedBlobs.Decode(&b))
|
||||
|
||||
require.Equal(t, htlcBlobs, decodedBlobs)
|
||||
})
|
||||
}
|
||||
|
||||
@ -1,78 +0,0 @@
|
||||
# 2024/09/02 14:02:53.354676 [TestHtlcAuxBlobEncodeDecode] [rapid] draw htlcAuxBlobs: contractcourt.htlcAuxBlobs{contractcourt.resolverID{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}:[]uint8{}}
|
||||
#
|
||||
v0.4.8#15807814492030881602
|
||||
0x5555555555555
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
0x0
|
||||
@ -21,7 +21,6 @@ import (
|
||||
"github.com/lightningnetwork/lnd/lnutils"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/sweep"
|
||||
"github.com/lightningnetwork/lnd/tlv"
|
||||
)
|
||||
|
||||
// SUMMARY OF OUTPUT STATES
|
||||
@ -1424,7 +1423,6 @@ func makeKidOutput(outpoint, originChanPoint *wire.OutPoint,
|
||||
return kidOutput{
|
||||
breachedOutput: makeBreachedOutput(
|
||||
outpoint, witnessType, nil, signDescriptor, heightHint,
|
||||
fn.None[tlv.Blob](),
|
||||
),
|
||||
isHtlc: isHtlc,
|
||||
originChanPoint: *originChanPoint,
|
||||
|
||||
@ -3,7 +3,7 @@
|
||||
# /make/builder.Dockerfile
|
||||
# /.github/workflows/main.yml
|
||||
# /.github/workflows/release.yml
|
||||
FROM golang:1.22.6-alpine as builder
|
||||
FROM golang:1.22.5-alpine as builder
|
||||
|
||||
LABEL maintainer="Olaoluwa Osuntokun <laolu@lightning.engineering>"
|
||||
|
||||
|
||||
@ -20,7 +20,6 @@ import (
|
||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||
"github.com/lightningnetwork/lnd/channeldb"
|
||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/graph"
|
||||
"github.com/lightningnetwork/lnd/keychain"
|
||||
"github.com/lightningnetwork/lnd/kvdb"
|
||||
@ -83,10 +82,9 @@ var (
|
||||
// can provide that serve useful when processing a specific network
|
||||
// announcement.
|
||||
type optionalMsgFields struct {
|
||||
capacity *btcutil.Amount
|
||||
channelPoint *wire.OutPoint
|
||||
remoteAlias *lnwire.ShortChannelID
|
||||
tapscriptRoot fn.Option[chainhash.Hash]
|
||||
capacity *btcutil.Amount
|
||||
channelPoint *wire.OutPoint
|
||||
remoteAlias *lnwire.ShortChannelID
|
||||
}
|
||||
|
||||
// apply applies the optional fields within the functional options.
|
||||
@ -117,14 +115,6 @@ func ChannelPoint(op wire.OutPoint) OptionalMsgField {
|
||||
}
|
||||
}
|
||||
|
||||
// TapscriptRoot is an optional field that lets the gossiper know of the root of
|
||||
// the tapscript tree for a custom channel.
|
||||
func TapscriptRoot(root fn.Option[chainhash.Hash]) OptionalMsgField {
|
||||
return func(f *optionalMsgFields) {
|
||||
f.tapscriptRoot = root
|
||||
}
|
||||
}
|
||||
|
||||
// RemoteAlias is an optional field that lets the gossiper know that a locally
|
||||
// sent channel update is actually an update for the peer that should replace
|
||||
// the ShortChannelID field with the remote's alias. This is only used for
|
||||
@ -2588,9 +2578,6 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
||||
cp := *nMsg.optionalMsgFields.channelPoint
|
||||
edge.ChannelPoint = cp
|
||||
}
|
||||
|
||||
// Optional tapscript root for custom channels.
|
||||
edge.TapscriptRoot = nMsg.optionalMsgFields.tapscriptRoot
|
||||
}
|
||||
|
||||
log.Debugf("Adding edge for short_chan_id: %v", scid.ToUint64())
|
||||
|
||||
@ -835,6 +835,12 @@ func (g *GossipSyncer) processChanRangeReply(msg *lnwire.ReplyChannelRange) erro
|
||||
}
|
||||
|
||||
g.prevReplyChannelRange = msg
|
||||
if len(msg.Timestamps) != 0 &&
|
||||
len(msg.Timestamps) != len(msg.ShortChanIDs) {
|
||||
|
||||
return fmt.Errorf("number of timestamps not equal to " +
|
||||
"number of SCIDs")
|
||||
}
|
||||
|
||||
for i, scid := range msg.ShortChanIDs {
|
||||
info := channeldb.NewChannelUpdateInfo(
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
FROM golang:1.22.6-alpine as builder
|
||||
FROM golang:1.22.5-alpine as builder
|
||||
|
||||
LABEL maintainer="Olaoluwa Osuntokun <laolu@lightning.engineering>"
|
||||
|
||||
|
||||
@ -100,12 +100,12 @@ the following commands for your OS:
|
||||
<summary>Linux (x86-64)</summary>
|
||||
|
||||
```
|
||||
wget https://dl.google.com/go/go1.22.6.linux-amd64.tar.gz
|
||||
wget https://dl.google.com/go/go1.22.5.linux-amd64.tar.gz
|
||||
sha256sum go1.22.5.linux-amd64.tar.gz | awk -F " " '{ print $1 }'
|
||||
```
|
||||
|
||||
The final output of the command above should be
|
||||
`999805bed7d9039ec3da1a53bfbcafc13e367da52aa823cb60b68ba22d44c616`. If it
|
||||
`904b924d435eaea086515bc63235b192ea441bd8c9b198c507e85009e6e4c7f0`. If it
|
||||
isn't, then the target REPO HAS BEEN MODIFIED, and you shouldn't install
|
||||
this version of Go. If it matches, then proceed to install Go:
|
||||
```
|
||||
@ -123,7 +123,7 @@ the following commands for your OS:
|
||||
```
|
||||
|
||||
The final output of the command above should be
|
||||
`b566484fe89a54c525dd1a4cbfec903c1f6e8f0b7b3dbaf94c79bc9145391083`. If it
|
||||
`8c4587cf3e63c9aefbcafa92818c4d9d51683af93ea687bf6c7508d6fa36f85e`. If it
|
||||
isn't, then the target REPO HAS BEEN MODIFIED, and you shouldn't install
|
||||
this version of Go. If it matches, then proceed to install Go:
|
||||
```
|
||||
|
||||
@ -1,7 +0,0 @@
|
||||
# Bug Fixes
|
||||
|
||||
* Fixed a memory leak found in mempool management handled by
|
||||
[`btcwallet`](https://github.com/lightningnetwork/lnd/pull/7767).
|
||||
|
||||
# Contributors (Alphabetical Order)
|
||||
* Yong Yu
|
||||
@ -1,51 +0,0 @@
|
||||
# Release Notes
|
||||
- [Bug Fixes](#bug-fixes)
|
||||
- [New Features](#new-features)
|
||||
- [Functional Enhancements](#functional-enhancements)
|
||||
- [RPC Additions](#rpc-additions)
|
||||
- [lncli Additions](#lncli-additions)
|
||||
- [Improvements](#improvements)
|
||||
- [Functional Updates](#functional-updates)
|
||||
- [RPC Updates](#rpc-updates)
|
||||
- [lncli Updates](#lncli-updates)
|
||||
- [Breaking Changes](#breaking-changes)
|
||||
- [Performance Improvements](#performance-improvements)
|
||||
- [Technical and Architectural Updates](#technical-and-architectural-updates)
|
||||
- [BOLT Spec Updates](#bolt-spec-updates)
|
||||
- [Testing](#testing)
|
||||
- [Database](#database)
|
||||
- [Code Health](#code-health)
|
||||
- [Tooling and Documentation](#tooling-and-documentation)
|
||||
|
||||
# Bug Fixes
|
||||
|
||||
* A bug that would cause the peer goroutine to panic if SCB related messages
|
||||
needed to be retransmitted [has been
|
||||
fixed](https://github.com/lightningnetwork/lnd/pull/8186).
|
||||
|
||||
# New Features
|
||||
## Functional Enhancements
|
||||
|
||||
## RPC Additions
|
||||
|
||||
|
||||
## lncli Additions
|
||||
|
||||
# Improvements
|
||||
## Functional Updates
|
||||
## RPC Updates
|
||||
## lncli Updates
|
||||
## Code Health
|
||||
## Breaking Changes
|
||||
## Performance Improvements
|
||||
|
||||
# Technical and Architectural Updates
|
||||
## BOLT Spec Updates
|
||||
## Testing
|
||||
## Database
|
||||
## Code Health
|
||||
## Tooling and Documentation
|
||||
|
||||
# Contributors (Alphabetical Order)
|
||||
* Eugene Siegel
|
||||
* Matt Morehouse
|
||||
@ -1,132 +0,0 @@
|
||||
# Release Notes
|
||||
- [Bug Fixes](#bug-fixes)
|
||||
- [New Features](#new-features)
|
||||
- [Functional Enhancements](#functional-enhancements)
|
||||
- [RPC Additions](#rpc-additions)
|
||||
- [lncli Additions](#lncli-additions)
|
||||
- [Improvements](#improvements)
|
||||
- [Functional Updates](#functional-updates)
|
||||
- [RPC Updates](#rpc-updates)
|
||||
- [lncli Updates](#lncli-updates)
|
||||
- [Breaking Changes](#breaking-changes)
|
||||
- [Performance Improvements](#performance-improvements)
|
||||
- [Technical and Architectural Updates](#technical-and-architectural-updates)
|
||||
- [BOLT Spec Updates](#bolt-spec-updates)
|
||||
- [Testing](#testing)
|
||||
- [Database](#database)
|
||||
- [Code Health](#code-health)
|
||||
- [Tooling and Documentation](#tooling-and-documentation)
|
||||
|
||||
# Bug Fixes
|
||||
|
||||
* [Fix a bug](https://github.com/lightningnetwork/lnd/pull/9134) that would
|
||||
cause a nil pointer dereference during the probing of a payment request that
|
||||
does not contain a payment address.
|
||||
|
||||
* [Make the contract resolutions for the channel arbitrator optional](
|
||||
https://github.com/lightningnetwork/lnd/pull/9253).
|
||||
|
||||
* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9324) to prevent
|
||||
potential deadlocks when LND depends on external components (e.g. aux
|
||||
components, hooks).
|
||||
|
||||
* [Make sure blinded payment failures are handled correctly in the mission
|
||||
controller](https://github.com/lightningnetwork/lnd/pull/9316).
|
||||
|
||||
# New Features
|
||||
|
||||
The main channel state machine and database now allow for processing and storing
|
||||
custom Taproot script leaves, allowing the implementation of custom channel
|
||||
types in a series of changes:
|
||||
* https://github.com/lightningnetwork/lnd/pull/9025
|
||||
* https://github.com/lightningnetwork/lnd/pull/9030
|
||||
* https://github.com/lightningnetwork/lnd/pull/9049
|
||||
* https://github.com/lightningnetwork/lnd/pull/9072
|
||||
* https://github.com/lightningnetwork/lnd/pull/9095
|
||||
* https://github.com/lightningnetwork/lnd/pull/8960
|
||||
* https://github.com/lightningnetwork/lnd/pull/9194
|
||||
* https://github.com/lightningnetwork/lnd/pull/9288
|
||||
|
||||
## Functional Enhancements
|
||||
|
||||
* A new `protocol.simple-taproot-overlay-chans` configuration item/CLI flag was
|
||||
added [to turn on custom channel
|
||||
functionality](https://github.com/lightningnetwork/lnd/pull/8960).
|
||||
|
||||
* Compatibility with [`bitcoind
|
||||
v28.0`](https://github.com/lightningnetwork/lnd/pull/9059) was ensured by
|
||||
updating the version the CI pipeline is running against.
|
||||
|
||||
## RPC Additions
|
||||
|
||||
* Some new experimental [RPCs for managing SCID
|
||||
aliases](https://github.com/lightningnetwork/lnd/pull/8960) were added under
|
||||
the `routerrpc` package. These methods allow manually adding and deleting SCID
|
||||
aliases locally to your node.
|
||||
> NOTE: these new RPC methods are marked as experimental
|
||||
(`XAddLocalChanAliases` & `XDeleteLocalChanAliases`) and upon calling
|
||||
them the aliases will not be communicated with the channel peer.
|
||||
|
||||
* The responses for the `ListChannels`, `PendingChannels` and `ChannelBalance`
|
||||
RPCs now include [a new `custom_channel_data` field that is only set for
|
||||
custom channels](https://github.com/lightningnetwork/lnd/pull/8960).
|
||||
|
||||
* The `routerrpc.SendPaymentV2` RPC has a new field [`first_hop_custom_records`
|
||||
that allows the user to send custom p2p wire message TLV types to the first
|
||||
hop of a payment](https://github.com/lightningnetwork/lnd/pull/8960).
|
||||
That new field is also exposed in the `routerrpc.HtlcInterceptor`, so it can
|
||||
be read and interpreted by external software.
|
||||
|
||||
* The `routerrpc.HtlcInterceptor` now [allows some values of the HTLC to be
|
||||
modified before they're validated by the state
|
||||
machine](https://github.com/lightningnetwork/lnd/pull/8960). The fields that
|
||||
can be modified are `outgoing_amount_msat` (if transported overlaid value of
|
||||
HTLC doesn't match the actual BTC amount being transferred) and
|
||||
`outgoing_htlc_wire_custom_records` (allow adding custom TLV values to the
|
||||
p2p wire message of the forwarded HTLC).
|
||||
|
||||
* A new [`invoicesrpc.HtlcModifier` RPC now allows incoming HTLCs that attempt
|
||||
to satisfy an invoice to be modified before they're
|
||||
validated](https://github.com/lightningnetwork/lnd/pull/8960). This allows
|
||||
custom channels to determine what the actual (overlaid) value of an HTLC is,
|
||||
even if that value doesn't match the actual BTC amount being transferred by
|
||||
the HTLC.
|
||||
|
||||
## lncli Additions
|
||||
|
||||
# Improvements
|
||||
## Functional Updates
|
||||
|
||||
## RPC Updates
|
||||
|
||||
## lncli Updates
|
||||
|
||||
|
||||
## Code Health
|
||||
|
||||
## Breaking Changes
|
||||
## Performance Improvements
|
||||
|
||||
* [A new method](https://github.com/lightningnetwork/lnd/pull/9195)
|
||||
`AssertTxnsNotInMempool` has been added to `lntest` package to allow batch
|
||||
exclusion check in itest.
|
||||
|
||||
# Technical and Architectural Updates
|
||||
## BOLT Spec Updates
|
||||
|
||||
## Testing
|
||||
## Database
|
||||
|
||||
## Code Health
|
||||
|
||||
## Tooling and Documentation
|
||||
|
||||
# Contributors (Alphabetical Order)
|
||||
|
||||
* Elle Mouton
|
||||
* ffranr
|
||||
* George Tsagkarelis
|
||||
* Olaoluwa Osuntokun
|
||||
* Oliver Gugger
|
||||
* Ziggie
|
||||
|
||||
@ -92,8 +92,7 @@ var defaultSetDesc = setDesc{
|
||||
SetInit: {}, // I
|
||||
SetNodeAnn: {}, // N
|
||||
},
|
||||
lnwire.SimpleTaprootOverlayChansOptional: {
|
||||
SetInit: {}, // I
|
||||
SetNodeAnn: {}, // N
|
||||
lnwire.Bolt11BlindedPathsOptional: {
|
||||
SetInvoice: {}, // I
|
||||
},
|
||||
}
|
||||
|
||||
@ -79,11 +79,6 @@ var deps = depDesc{
|
||||
lnwire.AnchorsZeroFeeHtlcTxOptional: {},
|
||||
lnwire.ExplicitChannelTypeOptional: {},
|
||||
},
|
||||
lnwire.SimpleTaprootOverlayChansOptional: {
|
||||
lnwire.SimpleTaprootChannelsOptionalStaging: {},
|
||||
lnwire.TLVOnionPayloadOptional: {},
|
||||
lnwire.ScidAliasOptional: {},
|
||||
},
|
||||
lnwire.RouteBlindingOptional: {
|
||||
lnwire.TLVOnionPayloadOptional: {},
|
||||
},
|
||||
|
||||
@ -63,9 +63,6 @@ type Config struct {
|
||||
// NoRouteBlinding unsets route blinding feature bits.
|
||||
NoRouteBlinding bool
|
||||
|
||||
// NoTaprootOverlay unsets the taproot overlay channel feature bits.
|
||||
NoTaprootOverlay bool
|
||||
|
||||
// CustomFeatures is a set of custom features to advertise in each
|
||||
// set.
|
||||
CustomFeatures map[Set][]lnwire.FeatureBit
|
||||
@ -195,10 +192,6 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) {
|
||||
raw.Unset(lnwire.Bolt11BlindedPathsOptional)
|
||||
raw.Unset(lnwire.Bolt11BlindedPathsRequired)
|
||||
}
|
||||
if cfg.NoTaprootOverlay {
|
||||
raw.Unset(lnwire.SimpleTaprootOverlayChansOptional)
|
||||
raw.Unset(lnwire.SimpleTaprootOverlayChansRequired)
|
||||
}
|
||||
for _, custom := range cfg.CustomFeatures[set] {
|
||||
if custom > set.Maximum() {
|
||||
return nil, fmt.Errorf("feature bit: %v "+
|
||||
|
||||
@ -1,51 +0,0 @@
|
||||
package funding
|
||||
|
||||
import (
|
||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||
"github.com/lightningnetwork/lnd/fn"
|
||||
"github.com/lightningnetwork/lnd/lntypes"
|
||||
"github.com/lightningnetwork/lnd/lnwallet"
|
||||
"github.com/lightningnetwork/lnd/msgmux"
|
||||
)
|
||||
|
||||
// AuxFundingDescResult is a type alias for a function that returns an optional
|
||||
// aux funding desc.
|
||||
type AuxFundingDescResult = fn.Result[fn.Option[lnwallet.AuxFundingDesc]]
|
||||
|
||||
// AuxTapscriptResult is a type alias for a function that returns an optional
|
||||
// tapscript root.
|
||||
type AuxTapscriptResult = fn.Result[fn.Option[chainhash.Hash]]
|
||||
|
||||
// AuxFundingController permits the implementation of the funding of custom
|
||||
// channels types. The controller serves as a MsgEndpoint which allows it to
|
||||
// intercept custom messages, or even the regular funding messages. The
|
||||
// controller might also pass along an aux funding desc based on an existing
|
||||
// pending channel ID.
|
||||
type AuxFundingController interface {
|
||||
// Endpoint is the embedded interface that signals that the funding
|
||||
// controller is also a message endpoint. This'll allow it to handle
|
||||
// custom messages specific to the funding type.
|
||||
msgmux.Endpoint
|
||||
|
||||
// DescFromPendingChanID takes a pending channel ID, that may already be
|
||||
// known due to prior custom channel messages, and maybe returns an aux
|
||||
// funding desc which can be used to modify how a channel is funded.
|
||||
DescFromPendingChanID(pid PendingChanID, openChan lnwallet.AuxChanState,
|
||||
keyRing lntypes.Dual[lnwallet.CommitmentKeyRing],
|
||||
initiator bool) AuxFundingDescResult
|
||||
|
||||
// DeriveTapscriptRoot takes a pending channel ID and maybe returns a
|
||||
// tapscript root that should be used when creating any MuSig2 sessions
|
||||
// for a channel.
|
||||
DeriveTapscriptRoot(PendingChanID) AuxTapscriptResult
|
||||
|
||||
// ChannelReady is called when a channel has been fully opened (multiple
|
||||
// confirmations) and is ready to be used. This can be used to perform
|
||||
// any final setup or cleanup.
|
||||
ChannelReady(openChan lnwallet.AuxChanState) error
|
||||
|
||||
// ChannelFinalized is called when a channel has been fully finalized.
|
||||
// In this state, we've received the commitment sig from the remote
|
||||
// party, so we are safe to broadcast the funding transaction.
|
||||
ChannelFinalized(PendingChanID) error
|
||||
}
|
||||
@ -307,74 +307,6 @@ func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, local,
|
||||
|
||||
return lnwallet.CommitmentTypeSimpleTaproot, nil
|
||||
|
||||
// Simple taproot channels overlay only.
|
||||
case channelFeatures.OnlyContains(
|
||||
lnwire.SimpleTaprootOverlayChansRequired,
|
||||
):
|
||||
|
||||
if !hasFeatures(
|
||||
local, remote,
|
||||
lnwire.SimpleTaprootOverlayChansOptional,
|
||||
) {
|
||||
|
||||
return 0, errUnsupportedChannelType
|
||||
}
|
||||
|
||||
return lnwallet.CommitmentTypeSimpleTaprootOverlay, nil
|
||||
|
||||
// Simple taproot overlay channels with scid only.
|
||||
case channelFeatures.OnlyContains(
|
||||
lnwire.SimpleTaprootOverlayChansRequired,
|
||||
lnwire.ScidAliasRequired,
|
||||
):
|
||||
|
||||
if !hasFeatures(
|
||||
local, remote,
|
||||
lnwire.SimpleTaprootOverlayChansOptional,
|
||||
lnwire.ScidAliasOptional,
|
||||
) {
|
||||
|
||||
return 0, errUnsupportedChannelType
|
||||
}
|
||||
|
||||
return lnwallet.CommitmentTypeSimpleTaprootOverlay, nil
|
||||
|
||||
// Simple taproot overlay channels with zero conf only.
|
||||
case channelFeatures.OnlyContains(
|
||||
lnwire.SimpleTaprootOverlayChansRequired,
|
||||
lnwire.ZeroConfRequired,
|
||||
):
|
||||
|
||||
if !hasFeatures(
|
||||
local, remote,
|
||||
lnwire.SimpleTaprootOverlayChansOptional,
|
||||
lnwire.ZeroConfOptional,
|
||||
) {
|
||||
|
||||
return 0, errUnsupportedChannelType
|
||||
}
|
||||
|
||||
return lnwallet.CommitmentTypeSimpleTaprootOverlay, nil
|
||||
|
||||
// Simple taproot overlay channels with scid and zero conf.
|
||||
case channelFeatures.OnlyContains(
|
||||
lnwire.SimpleTaprootOverlayChansRequired,
|
||||
lnwire.ZeroConfRequired,
|
||||
lnwire.ScidAliasRequired,
|
||||
):
|
||||
|
||||
if !hasFeatures(
|
||||
local, remote,
|
||||
lnwire.SimpleTaprootOverlayChansOptional,
|
||||
lnwire.ZeroConfOptional,
|
||||
lnwire.ScidAliasOptional,
|
||||
) {
|
||||
|
||||
return 0, errUnsupportedChannelType
|
||||
}
|
||||
|
||||
return lnwallet.CommitmentTypeSimpleTaprootOverlay, nil
|
||||
|
||||
// No features, use legacy commitment type.
|
||||
case channelFeatures.IsEmpty():
|
||||
return lnwallet.CommitmentTypeLegacy, nil
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user