Merge pull request #9183 from lightningnetwork/0-18-4-branch-rc1

release: create branch for v0.18.4-beta.rc1
This commit is contained in:
Oliver Gugger 2024-11-21 20:19:40 +01:00 committed by GitHub
commit c1129bb086
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
309 changed files with 24299 additions and 9708 deletions

View file

@ -21,7 +21,7 @@ defaults:
shell: bash
env:
BITCOIN_VERSION: "27"
BITCOIN_VERSION: "28"
TRANCHES: 8
@ -31,7 +31,7 @@ env:
# /dev.Dockerfile
# /make/builder.Dockerfile
# /.github/workflows/release.yml
GO_VERSION: 1.22.5
GO_VERSION: 1.22.6
jobs:
########################

View file

@ -11,12 +11,11 @@ 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.5
GO_VERSION: 1.22.6
jobs:
main:

1
.gitignore vendored
View file

@ -66,6 +66,7 @@ profile.tmp
.DS_Store
.vscode
*.code-workspace
# Coverage test
coverage.txt

View file

@ -1,18 +1,8 @@
run:
# timeout for analysis
deadline: 10m
go: "1.22.6"
# 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
# Abort after 10 minutes.
timeout: 10m
build-tags:
- autopilotrpc
@ -57,7 +47,6 @@ linters-settings:
- G306 # Poor file permissions used when writing to a new file.
staticcheck:
go: "1.22.5"
checks: ["-SA1019"]
lll:
@ -133,25 +122,15 @@ 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
@ -191,7 +170,7 @@ linters:
- wrapcheck
# Allow dynamic errors.
- goerr113
- err113
# We use ErrXXX instead.
- errname
@ -207,15 +186,41 @@ 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.
- 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: 8c66353e4c02329abdacb5a8df29998035ec2e24
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
exclude-rules:
# Exclude gosec from running for tests so that tests with weak randomness
@ -256,8 +261,8 @@ issues:
- forbidigo
- godot
# Allow fmt.Printf() in lncli.
- path: cmd/lncli/*
# Allow fmt.Printf() in commands.
- path: cmd/commands/*
linters:
- forbidigo

View file

@ -3,7 +3,7 @@
# /make/builder.Dockerfile
# /.github/workflows/main.yml
# /.github/workflows/release.yml
FROM golang:1.22.5-alpine as builder
FROM golang:1.22.6-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.

View file

@ -35,7 +35,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.5
GO_VERSION = 1.22.6
GOBUILD := $(LOOPVARFIX) go build -v
GOINSTALL := $(LOOPVARFIX) go install -v

View file

@ -5,11 +5,22 @@ 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
@ -47,17 +58,18 @@ var (
// operations.
byteOrder = binary.BigEndian
// startBlockHeight is the starting block height of the alias range.
startingBlockHeight = 16_000_000
// AliasStartBlockHeight is the starting block height of the alias
// range.
AliasStartBlockHeight uint32 = 16_000_000
// endBlockHeight is the ending block height of the alias range.
endBlockHeight = 16_250_000
// AliasEndBlockHeight is the ending block height of the alias range.
AliasEndBlockHeight uint32 = 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: uint32(startingBlockHeight),
BlockHeight: AliasStartBlockHeight,
TxIndex: 0,
TxPosition: 0,
}
@ -68,6 +80,10 @@ 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
@ -77,10 +93,14 @@ 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 map[lnwire.ShortChannelID][]lnwire.ShortChannelID
baseToSet ScidAliasMap
// aliasToBase is a mapping that maps all aliases for a given channel
// to its base SCID. This is only used for channels that have
@ -98,9 +118,15 @@ type Manager struct {
}
// NewManager initializes an alias Manager from the passed database backend.
func NewManager(db kvdb.Backend) (*Manager, error) {
m := &Manager{backend: db}
m.baseToSet = make(map[lnwire.ShortChannelID][]lnwire.ShortChannelID)
func NewManager(db kvdb.Backend, linkAliasUpdater UpdateLinkAliases) (*Manager,
error) {
m := &Manager{
backend: db,
baseToSet: make(ScidAliasMap),
linkAliasUpdater: linkAliasUpdater,
}
m.aliasToBase = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID)
m.peerAlias = make(map[lnwire.ChannelID]lnwire.ShortChannelID)
@ -215,12 +241,22 @@ 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 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.
func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID,
gossip bool) error {
gossip, linkUpdate 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()
defer m.Unlock()
unlockOnce := sync.Once{}
unlock := func() {
unlockOnce.Do(m.Unlock)
}
defer unlock()
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
// If the caller does not want to allow the alias to be used
@ -270,6 +306,18 @@ 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
}
@ -340,6 +388,74 @@ 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,
@ -392,6 +508,19 @@ 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 {
@ -404,6 +533,29 @@ 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[:])
@ -418,6 +570,26 @@ 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[:])
@ -433,11 +605,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() map[lnwire.ShortChannelID][]lnwire.ShortChannelID {
func (m *Manager) ListAliases() ScidAliasMap {
m.RLock()
defer m.RUnlock()
baseCopy := make(map[lnwire.ShortChannelID][]lnwire.ShortChannelID)
baseCopy := make(ScidAliasMap)
for k, v := range m.baseToSet {
setCopy := make([]lnwire.ShortChannelID, len(v))
@ -496,10 +668,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
// startingBlockHeight and less than endBlockHeight, then it is an alias
// AliasStartBlockHeight and less than AliasEndBlockHeight, 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 >= uint32(startingBlockHeight) &&
scid.BlockHeight < uint32(endBlockHeight)
return scid.BlockHeight >= AliasStartBlockHeight &&
scid.BlockHeight < AliasEndBlockHeight
}

View file

@ -23,7 +23,11 @@ func TestAliasStorePeerAlias(t *testing.T) {
require.NoError(t, err)
defer db.Close()
aliasStore, err := NewManager(db)
linkUpdater := func(shortID lnwire.ShortChannelID) error {
return nil
}
aliasStore, err := NewManager(db, linkUpdater)
require.NoError(t, err)
var chanID1 [32]byte
@ -52,7 +56,11 @@ func TestAliasStoreRequest(t *testing.T) {
require.NoError(t, err)
defer db.Close()
aliasStore, err := NewManager(db)
linkUpdater := func(shortID lnwire.ShortChannelID) error {
return nil
}
aliasStore, err := NewManager(db, linkUpdater)
require.NoError(t, err)
// We'll assert that the very first alias we receive is StartingAlias.
@ -68,6 +76,118 @@ 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) {
@ -80,7 +200,7 @@ func TestGetNextScid(t *testing.T) {
name: "starting alias",
current: StartingAlias,
expected: lnwire.ShortChannelID{
BlockHeight: uint32(startingBlockHeight),
BlockHeight: AliasStartBlockHeight,
TxIndex: 0,
TxPosition: 1,
},

View file

@ -43,11 +43,11 @@ const (
AppMinor uint = 18
// AppPatch defines the application patch for this binary.
AppPatch uint = 3
AppPatch uint = 4
// AppPreRelease MUST only contain characters from semanticAlphabet per
// the semantic versioning spec.
AppPreRelease = "beta"
AppPreRelease = "beta.rc1"
)
func init() {

View file

@ -24,6 +24,7 @@ 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"
@ -63,6 +64,14 @@ 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

View file

@ -356,6 +356,30 @@ 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,
):

View file

@ -226,28 +226,109 @@ 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
@ -325,6 +406,11 @@ 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
@ -395,6 +481,12 @@ 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
@ -496,6 +588,53 @@ 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
@ -562,6 +701,11 @@ 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
@ -571,9 +715,26 @@ type ChannelCommitment struct {
// Htlcs is the set of HTLC's that are pending at this particular
// commitment height.
Htlcs []HTLC
}
// TODO(roasbeef): pending commit pointer?
// * lets just walk through
// 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
}
// ChannelStatus is a bit vector used to indicate whether an OpenChannel is in
@ -867,6 +1028,16 @@ 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
@ -1025,6 +1196,64 @@ 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 {
@ -2351,6 +2580,12 @@ 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
@ -2369,6 +2604,11 @@ 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...)
}
@ -2390,8 +2630,19 @@ 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
}
@ -2529,6 +2780,8 @@ 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
}
@ -2690,6 +2943,14 @@ 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
}
@ -2750,6 +3011,17 @@ 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
}
@ -3728,6 +4000,13 @@ 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))
@ -4030,32 +4309,9 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error {
return 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
auxData := channel.extractTlvData()
if err := auxData.encode(&w); err != nil {
return fmt.Errorf("unable to encode aux data: %w", err)
}
if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil {
@ -4142,6 +4398,12 @@ 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())
}
@ -4244,45 +4506,14 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error {
}
}
// 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
var auxData openChannelTlvData
if err := auxData.decode(r); err != nil {
return fmt.Errorf("unable to decode aux data: %w", err)
}
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
}
// Assign all the relevant fields from the aux data into the actual
// open channel.
channel.amendTlvData(auxData)
channel.Packager = NewChannelPackager(channel.ShortChannelID)
@ -4318,7 +4549,9 @@ 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))
@ -4332,7 +4565,23 @@ func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment
}
r := bytes.NewReader(commitBytes)
return deserializeChanCommit(r)
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
}
func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error {
@ -4440,6 +4689,25 @@ 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 {
@ -4468,22 +4736,6 @@ 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 {

View file

@ -17,6 +17,7 @@ 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"
@ -173,7 +174,7 @@ func fundingPointOption(chanPoint wire.OutPoint) testChannelOption {
}
// channelIDOption is an option which sets the short channel ID of the channel.
var channelIDOption = func(chanID lnwire.ShortChannelID) testChannelOption {
func channelIDOption(chanID lnwire.ShortChannelID) testChannelOption {
return func(params *testChannelParams) {
params.channel.ShortChannelID = chanID
}
@ -326,6 +327,9 @@ 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,
@ -347,6 +351,7 @@ 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,
@ -356,6 +361,7 @@ 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(),
@ -368,6 +374,9 @@ 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}),
}
}
@ -575,24 +584,32 @@ 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, c.CommitTx.TxHash(), "CommitTx mismatch",
t, r.CommitTxHash.Val, 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, 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")
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",
)
}
}
@ -657,6 +674,7 @@ 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
@ -694,9 +712,14 @@ 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)
@ -799,10 +822,10 @@ func TestChannelStateTransition(t *testing.T) {
// Check the output indexes are saved as expected.
require.EqualValues(
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex,
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex.Val,
)
require.EqualValues(
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex,
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex.Val,
)
// The two deltas (the original vs the on-disk version) should
@ -844,10 +867,10 @@ func TestChannelStateTransition(t *testing.T) {
// Check the output indexes are saved as expected.
require.EqualValues(
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex,
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex.Val,
)
require.EqualValues(
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex,
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex.Val,
)
assertRevocationLogEntryEqual(t, &oldRemoteCommit, prevCommit)
@ -1642,6 +1665,24 @@ 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
@ -1663,6 +1704,7 @@ func TestHTLCsExtraData(t *testing.T) {
mockHtlc,
blindingPointHTLC,
mockHtlc,
customDataHTLC,
},
},
}

View file

@ -286,6 +286,27 @@ 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 {

View file

@ -2382,7 +2382,7 @@ func TestStressTestChannelGraphAPI(t *testing.T) {
methodsMu.Unlock()
err := fn()
require.NoErrorf(t, err, fmt.Sprintf(name))
require.NoErrorf(t, err, name)
}
})
}

View file

@ -2,7 +2,6 @@ package migration_01_to_11
import (
"bytes"
"fmt"
"testing"
"time"
@ -154,12 +153,7 @@ func signDigestCompact(hash []byte) ([]byte, error) {
privKey, _ := btcec.PrivKeyFromBytes(testPrivKeyBytes)
// ecdsa.SignCompact returns a pubkey-recoverable signature
sig, err := ecdsa.SignCompact(privKey, hash, isCompressedKey)
if err != nil {
return nil, fmt.Errorf("can't sign the hash: %w", err)
}
return sig, nil
return ecdsa.SignCompact(privKey, hash, isCompressedKey), nil
}
// getPayReq creates a payment request for the given net.

View file

@ -8,6 +8,7 @@ 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
@ -62,6 +63,11 @@ 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

View file

@ -195,6 +195,11 @@ 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
@ -1010,10 +1015,21 @@ 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{}
@ -1046,6 +1062,15 @@ func deserializePaymentCreationInfo(r io.Reader) (*PaymentCreationInfo, error) {
}
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
}
@ -1071,6 +1096,25 @@ 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
}
@ -1108,6 +1152,22 @@ 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
}
@ -1373,6 +1433,8 @@ func SerializeRoute(w io.Writer, r route.Route) error {
}
}
// Any new/extra TLV data is encoded in serializeHTLCAttemptInfo!
return nil
}
@ -1406,5 +1468,7 @@ func DeserializeRoute(r io.Reader) (route.Route, error) {
}
rt.Hops = hops
// Any new/extra TLV data is decoded in deserializeHTLCAttemptInfo!
return rt, nil
}

View file

@ -13,8 +13,10 @@ 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"
)
@ -108,7 +110,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(""),
PaymentRequest: []byte("test"),
}
a := NewHtlcAttempt(
@ -124,51 +126,64 @@ func TestSentPaymentSerialization(t *testing.T) {
c, s := makeFakeInfo()
var b bytes.Buffer
if err := serializePaymentCreationInfo(&b, c); err != nil {
t.Fatalf("unable to serialize creation info: %v", err)
}
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)
newCreationInfo, err := deserializePaymentCreationInfo(&b)
require.NoError(t, err, "unable to deserialize creation info")
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, err, "deserialize")
require.Equal(t, c, newCreationInfo)
b.Reset()
if err := serializeHTLCAttemptInfo(&b, s); err != nil {
t.Fatalf("unable to serialize info: %v", err)
// Now we add some custom records to the creation info and serialize it
// again.
c.FirstHopCustomRecords = lnwire.CustomRecords{
lnwire.MinCustomRecordsTlvType: []byte{1, 2, 3},
}
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")
newWireInfo, err := deserializeHTLCAttemptInfo(&b)
require.NoError(t, err, "unable to deserialize info")
newWireInfo.AttemptID = s.AttemptID
require.NoError(t, err, "deserialize")
// 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)
// 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},
}
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())
if !reflect.DeepEqual(s, newWireInfo) {
t.Fatalf("Payments do not match after "+
"serialization/deserialization %v vs %v",
spew.Sdump(s), spew.Sdump(newWireInfo),
)
}
require.Equal(t, s, newWireInfo)
}
// assertRouteEquals compares to routes for equality and returns an error if

View file

@ -7,6 +7,7 @@ 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"
@ -16,16 +17,15 @@ import (
const (
// OutputIndexEmpty is used when the output index doesn't exist.
OutputIndexEmpty = math.MaxUint16
)
// 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
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]
)
var (
@ -54,6 +54,74 @@ 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.
@ -72,118 +140,92 @@ var (
// made into tlv records without further conversion.
type HTLCEntry struct {
// RHash is the payment hash of the HTLC.
RHash [32]byte
RHash tlv.RecordT[tlv.TlvType0, SparsePayHash]
// RefundTimeout is the absolute timeout on the HTLC that the sender
// must wait before reclaiming the funds in limbo.
RefundTimeout uint32
RefundTimeout tlv.RecordT[tlv.TlvType1, 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 uint16
OutputIndex tlv.RecordT[tlv.TlvType2, uint16]
// Incoming denotes whether we're the receiver or the sender of this
// HTLC.
//
// NOTE: this field is the memory representation of the field
// incomingUint.
Incoming bool
Incoming tlv.RecordT[tlv.TlvType3, bool]
// Amt is the amount of satoshis this HTLC escrows.
//
// NOTE: this field is the memory representation of the field amtUint.
Amt btcutil.Amount
Amt tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]]
// 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
// 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]
// 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)
// HtlcIndex is the index of the HTLC in the channel.
HtlcIndex tlv.OptionalRecordT[tlv.TlvType6, uint16]
}
// toTlvStream converts an HTLCEntry record into a tlv representation.
func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) {
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
)
records := []tlv.Record{
h.RHash.Record(),
h.RefundTimeout.Record(),
h.OutputIndex.Record(),
h.Incoming.Record(),
h.Amt.Record(),
}
return tlv.NewStream(
tlv.MakeDynamicRecord(
rHashType, &h.RHash, h.RHashLen,
RHashEncoder, RHashDecoder,
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),
),
tlv.MakePrimitiveRecord(
refundTimeoutType, &h.RefundTimeout,
RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1](
htlc.RefundTimeout,
),
tlv.MakePrimitiveRecord(
outputIndexType, &h.OutputIndex,
OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2](
uint16(htlc.OutputIndex),
),
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),
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
}
// RevocationLog stores the info needed to construct a breach retribution. Its
// fields can be viewed as a subset of a ChannelCommitment's. In the database,
// all historical versions of the RevocationLog are saved using the
@ -191,15 +233,15 @@ func (h *HTLCEntry) toTlvStream() (*tlv.Stream, 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 uint16
OurOutputIndex tlv.RecordT[tlv.TlvType0, uint16]
// TheirOutputIndex specifies their output index in this commitment. In
// a remote commitment transaction, this is the to local output index.
TheirOutputIndex uint16
TheirOutputIndex tlv.RecordT[tlv.TlvType1, uint16]
// CommitTxHash is the hash of the latest version of the commitment
// state, broadcast able by us.
CommitTxHash [32]byte
CommitTxHash tlv.RecordT[tlv.TlvType2, [32]byte]
// HTLCEntries is the set of HTLCEntry's that are pending at this
// particular commitment height.
@ -209,21 +251,65 @@ 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 a pointer so that it is clear if the value is zero or
// NOTE: this is an option 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 *lnwire.MilliSatoshi
OurBalance tlv.OptionalRecordT[tlv.TlvType3, BigSizeMilliSatoshi]
// 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 a pointer so that it is clear if the value is zero or
// NOTE: this is an option 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 *lnwire.MilliSatoshi
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
}
// putRevocationLog uses the fields `CommitTx` and `Htlcs` from a
@ -242,15 +328,32 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment,
}
rl := &RevocationLog{
OurOutputIndex: uint16(ourOutputIndex),
TheirOutputIndex: uint16(theirOutputIndex),
CommitTxHash: commit.CommitTx.TxHash(),
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)),
}
commit.CustomBlob.WhenSome(func(blob tlv.Blob) {
rl.CustomBlob = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob),
)
})
if !noAmtData {
rl.OurBalance = &commit.LocalBalance
rl.TheirBalance = &commit.RemoteBalance
rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3](
tlv.NewBigSizeT(commit.LocalBalance),
))
rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4](
tlv.NewBigSizeT(commit.RemoteBalance),
))
}
for _, htlc := range commit.Htlcs {
@ -265,12 +368,9 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment,
return ErrOutputIndexTooBig
}
entry := &HTLCEntry{
RHash: htlc.RHash,
RefundTimeout: htlc.RefundTimeout,
Incoming: htlc.Incoming,
OutputIndex: uint16(htlc.OutputIndex),
Amt: htlc.Amt.ToSatoshis(),
entry, err := NewHTLCEntryFromHTLC(htlc)
if err != nil {
return err
}
rl.HTLCEntries = append(rl.HTLCEntries, entry)
}
@ -306,31 +406,27 @@ 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{
tlv.MakePrimitiveRecord(
revLogOurOutputIndexType, &rl.OurOutputIndex,
),
tlv.MakePrimitiveRecord(
revLogTheirOutputIndexType, &rl.TheirOutputIndex,
),
tlv.MakePrimitiveRecord(
revLogCommitTxHashType, &rl.CommitTxHash,
),
rl.OurOutputIndex.Record(),
rl.TheirOutputIndex.Record(),
rl.CommitTxHash.Record(),
}
// Now we add any optional fields that are non-nil.
if rl.OurBalance != nil {
lb := uint64(*rl.OurBalance)
records = append(records, tlv.MakeBigSizeRecord(
revLogOurBalanceType, &lb,
))
}
rl.OurBalance.WhenSome(
func(r tlv.RecordT[tlv.TlvType3, BigSizeMilliSatoshi]) {
records = append(records, r.Record())
},
)
if rl.TheirBalance != nil {
rb := uint64(*rl.TheirBalance)
records = append(records, tlv.MakeBigSizeRecord(
revLogTheirBalanceType, &rb,
))
}
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())
})
// Create the tlv stream.
tlvStream, err := tlv.NewStream(records...)
@ -351,14 +447,6 @@ 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 {
@ -376,27 +464,20 @@ 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 uint64
theirBalance uint64
)
var rl RevocationLog
ourBalance := rl.OurBalance.Zero()
theirBalance := rl.TheirBalance.Zero()
customBlob := rl.CustomBlob.Zero()
// Create the tlv stream.
tlvStream, err := tlv.NewStream(
tlv.MakePrimitiveRecord(
revLogOurOutputIndexType, &rl.OurOutputIndex,
),
tlv.MakePrimitiveRecord(
revLogTheirOutputIndexType, &rl.TheirOutputIndex,
),
tlv.MakePrimitiveRecord(
revLogCommitTxHashType, &rl.CommitTxHash,
),
tlv.MakeBigSizeRecord(revLogOurBalanceType, &ourBalance),
tlv.MakeBigSizeRecord(
revLogTheirBalanceType, &theirBalance,
),
rl.OurOutputIndex.Record(),
rl.TheirOutputIndex.Record(),
rl.CommitTxHash.Record(),
ourBalance.Record(),
theirBalance.Record(),
customBlob.Record(),
)
if err != nil {
return rl, err
@ -408,14 +489,16 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) {
return rl, err
}
if t, ok := parsedTypes[revLogOurBalanceType]; ok && t == nil {
lb := lnwire.MilliSatoshi(ourBalance)
rl.OurBalance = &lb
if t, ok := parsedTypes[ourBalance.TlvType()]; ok && t == nil {
rl.OurBalance = tlv.SomeRecordT(ourBalance)
}
if t, ok := parsedTypes[revLogTheirBalanceType]; ok && t == nil {
rb := lnwire.MilliSatoshi(theirBalance)
rl.TheirBalance = &rb
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)
}
// Read the HTLC entries.
@ -432,14 +515,28 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) {
for {
var htlc HTLCEntry
customBlob := htlc.CustomBlob.Zero()
htlcIndex := htlc.HtlcIndex.Zero()
// Create the tlv stream.
tlvStream, err := htlc.toTlvStream()
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...)
if err != nil {
return nil, err
}
// Read the HTLC entry.
if _, err := readTlvStream(r, tlvStream); err != nil {
parsedTypes, err := readTlvStream(r, tlvStream)
if err != nil {
// We've reached the end when hitting an EOF.
if err == io.ErrUnexpectedEOF {
break
@ -447,13 +544,13 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) {
return nil, err
}
// Patch the Incoming field.
if htlc.incomingTlv == 1 {
htlc.Incoming = true
if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil {
htlc.CustomBlob = tlv.SomeRecordT(customBlob)
}
// Patch the Amt field.
htlc.Amt = btcutil.Amount(htlc.amtTlv)
if t, ok := parsedTypes[htlcIndex.TlvType()]; ok && t == nil {
htlc.HtlcIndex = tlv.SomeRecordT(htlcIndex)
}
// Append the entry.
htlcs = append(htlcs, &htlc)
@ -469,6 +566,7 @@ 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 {

View file

@ -8,6 +8,7 @@ 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"
@ -33,17 +34,38 @@ 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: 740_000,
OutputIndex: 10,
Incoming: true,
Amt: 1000_000,
amtTlv: 1000_000,
incomingTlv: 1,
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),
),
}
testHTLCEntryBytes = []byte{
// Body length 23.
0x16,
// Body length 45.
0x2d,
// Rhash tlv.
0x0, 0x0,
// RefundTimeout tlv.
@ -54,6 +76,45 @@ 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)
@ -68,24 +129,29 @@ var (
CommitTx: channels.TestFundingTx,
CommitSig: bytes.Repeat([]byte{1}, 71),
Htlcs: []HTLC{{
RefundTimeout: testHTLCEntry.RefundTimeout,
OutputIndex: int32(testHTLCEntry.OutputIndex),
Incoming: testHTLCEntry.Incoming,
Amt: lnwire.NewMSatFromSatoshis(
testHTLCEntry.Amt,
RefundTimeout: testHTLCEntry.RefundTimeout.Val,
OutputIndex: int32(testHTLCEntry.OutputIndex.Val),
HtlcIndex: uint64(
testHTLCEntry.HtlcIndex.ValOpt().
UnsafeFromSome(),
),
Incoming: testHTLCEntry.Incoming.Val,
Amt: lnwire.NewMSatFromSatoshis(
testHTLCEntry.Amt.Val.Int(),
),
CustomRecords: customRecords,
}},
CustomBlob: fn.Some(blobBytes),
}
testRevocationLogNoAmts = RevocationLog{
OurOutputIndex: 0,
TheirOutputIndex: 1,
CommitTxHash: testChannelCommit.CommitTx.TxHash(),
HTLCEntries: []*HTLCEntry{&testHTLCEntry},
}
testRevocationLogNoAmts = NewRevocationLog(
0, 1, testChannelCommit.CommitTx.TxHash(),
fn.None[lnwire.MilliSatoshi](), fn.None[lnwire.MilliSatoshi](),
[]*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes),
)
testRevocationLogNoAmtsBytes = []byte{
// Body length 42.
0x2a,
// Body length 61.
0x3d,
// OurOutputIndex tlv.
0x0, 0x2, 0x0, 0x0,
// TheirOutputIndex tlv.
@ -96,19 +162,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 = RevocationLog{
OurOutputIndex: 0,
TheirOutputIndex: 1,
CommitTxHash: testChannelCommit.CommitTx.TxHash(),
HTLCEntries: []*HTLCEntry{&testHTLCEntry},
OurBalance: &localBalance,
TheirBalance: &remoteBalance,
}
testRevocationLogWithAmts = NewRevocationLog(
0, 1, testChannelCommit.CommitTx.TxHash(),
fn.Some(localBalance), fn.Some(remoteBalance),
[]*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes),
)
testRevocationLogWithAmtsBytes = []byte{
// Body length 52.
0x34,
// Body length 71.
0x47,
// OurOutputIndex tlv.
0x0, 0x2, 0x0, 0x0,
// TheirOutputIndex tlv.
@ -123,6 +189,9 @@ 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,
}
)
@ -193,11 +262,6 @@ 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})
@ -207,6 +271,21 @@ 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()
@ -215,7 +294,7 @@ func TestSerializeHTLCEntries(t *testing.T) {
// Create a fake rHash.
rHashBytes := bytes.Repeat([]byte{10}, 32)
copy(entry.RHash[:], rHashBytes)
copy(entry.RHash.Val[:], rHashBytes)
// Construct the serialized bytes.
//
@ -224,7 +303,7 @@ func TestSerializeHTLCEntries(t *testing.T) {
partialBytes := testHTLCEntryBytes[3:]
// Write the total length and RHash tlv.
expectedBytes := []byte{0x36, 0x0, 0x20}
expectedBytes := []byte{0x4d, 0x0, 0x20}
expectedBytes = append(expectedBytes, rHashBytes...)
// Append the rest.
@ -269,7 +348,7 @@ func TestSerializeAndDeserializeRevLog(t *testing.T) {
t, &test.revLog, test.revLogBytes,
)
testDerializeRevocationLog(
testDeserializeRevocationLog(
t, &test.revLog, test.revLogBytes,
)
})
@ -293,7 +372,7 @@ func testSerializeRevocationLog(t *testing.T, rl *RevocationLog,
require.Equal(t, revLogBytes, buf.Bytes()[:bodyIndex])
}
func testDerializeRevocationLog(t *testing.T, revLog *RevocationLog,
func testDeserializeRevocationLog(t *testing.T, revLog *RevocationLog,
revLogBytes []byte) {
// Construct the full bytes.
@ -309,7 +388,7 @@ func testDerializeRevocationLog(t *testing.T, revLog *RevocationLog,
require.Equal(t, *revLog, rl)
}
func TestDerializeHTLCEntriesEmptyRHash(t *testing.T) {
func TestDeserializeHTLCEntriesEmptyRHash(t *testing.T) {
t.Parallel()
// Read the tlv stream.
@ -322,7 +401,7 @@ func TestDerializeHTLCEntriesEmptyRHash(t *testing.T) {
require.Equal(t, &testHTLCEntry, htlcs[0])
}
func TestDerializeHTLCEntries(t *testing.T) {
func TestDeserializeHTLCEntries(t *testing.T) {
t.Parallel()
// Copy the testHTLCEntry.
@ -330,7 +409,7 @@ func TestDerializeHTLCEntries(t *testing.T) {
// Create a fake rHash.
rHashBytes := bytes.Repeat([]byte{10}, 32)
copy(entry.RHash[:], rHashBytes)
copy(entry.RHash.Val[:], rHashBytes)
// Construct the serialized bytes.
//
@ -339,7 +418,7 @@ func TestDerializeHTLCEntries(t *testing.T) {
partialBytes := testHTLCEntryBytes[3:]
// Write the total length and RHash tlv.
testBytes := append([]byte{0x36, 0x0, 0x20}, rHashBytes...)
testBytes := append([]byte{0x4d, 0x0, 0x20}, rHashBytes...)
// Append the rest.
testBytes = append(testBytes, partialBytes...)
@ -398,11 +477,11 @@ func TestDeleteLogBucket(t *testing.T) {
err = kvdb.Update(backend, func(tx kvdb.RwTx) error {
// Create the buckets.
chanBucket, _, err := createTestRevocatoinLogBuckets(tx)
chanBucket, _, err := createTestRevocationLogBuckets(tx)
require.NoError(t, err)
// Create the buckets again should give us an error.
_, _, err = createTestRevocatoinLogBuckets(tx)
_, _, err = createTestRevocationLogBuckets(tx)
require.ErrorIs(t, err, kvdb.ErrBucketExists)
// Delete both buckets.
@ -410,7 +489,7 @@ func TestDeleteLogBucket(t *testing.T) {
require.NoError(t, err)
// Create the buckets again should give us NO error.
_, _, err = createTestRevocatoinLogBuckets(tx)
_, _, err = createTestRevocationLogBuckets(tx)
return err
}, func() {})
require.NoError(t, err)
@ -516,7 +595,7 @@ func TestPutRevocationLog(t *testing.T) {
// Construct the testing db transaction.
dbTx := func(tx kvdb.RwTx) (RevocationLog, error) {
// Create the buckets.
_, bucket, err := createTestRevocatoinLogBuckets(tx)
_, bucket, err := createTestRevocationLogBuckets(tx)
require.NoError(t, err)
// Save the log.
@ -686,7 +765,7 @@ func TestFetchRevocationLogCompatible(t *testing.T) {
}
}
func createTestRevocatoinLogBuckets(tx kvdb.RwTx) (kvdb.RwBucket,
func createTestRevocationLogBuckets(tx kvdb.RwTx) (kvdb.RwBucket,
kvdb.RwBucket, error) {
chanBucket, err := tx.CreateTopLevelBucket(openChannelBucket)

View file

@ -1,4 +1,4 @@
package main
package commands
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))
}

View file

@ -1,4 +1,4 @@
package main
package commands
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)
}
}

View file

@ -1,7 +1,7 @@
//go:build autopilotrpc
// +build autopilotrpc
package main
package commands
import (
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"

View file

@ -1,7 +1,7 @@
//go:build !autopilotrpc
// +build !autopilotrpc
package main
package commands
import "github.com/urfave/cli"

View file

@ -1,7 +1,7 @@
//go:build chainrpc
// +build chainrpc
package main
package commands
import (
"bytes"

View file

@ -1,7 +1,7 @@
//go:build !chainrpc
// +build !chainrpc
package main
package commands
import "github.com/urfave/cli"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"context"

View file

@ -1,4 +1,4 @@
package main
package commands
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

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"fmt"
@ -265,6 +265,7 @@ func setCfg(ctx *cli.Context) error {
Config: mcCfg.Config,
},
)
return err
}
@ -366,5 +367,6 @@ func resetMissionControl(ctx *cli.Context) error {
req := &routerrpc.ResetMissionControlRequest{}
_, err := client.ResetMissionControl(ctxc, req)
return err
}

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"
@ -25,6 +25,7 @@ import (
"github.com/lightningnetwork/lnd/record"
"github.com/lightningnetwork/lnd/routing/route"
"github.com/urfave/cli"
"google.golang.org/grpc"
)
const (
@ -152,8 +153,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",
@ -202,7 +203,7 @@ func paymentFlags() []cli.Flag {
}
}
var sendPaymentCommand = cli.Command{
var SendPaymentCommand = cli.Command{
Name: "sendpayment",
Category: "Payments",
Usage: "Send a payment over lightning.",
@ -226,7 +227,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 " +
@ -253,7 +254,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
@ -324,20 +325,23 @@ 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),
@ -357,7 +361,9 @@ func sendPayment(ctx *cli.Context) error {
req.PaymentAddr = payAddr
return sendPaymentRequest(ctx, req)
return SendPaymentRequest(
ctx, req, conn, conn, routerRPCSendPayment,
)
}
var (
@ -466,19 +472,29 @@ func sendPayment(ctx *cli.Context) error {
req.PaymentAddr = payAddr
return sendPaymentRequest(ctx, req)
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
}
func sendPaymentRequest(ctx *cli.Context,
req *routerrpc.SendPaymentRequest) error {
// 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 {
ctxc := getContext()
conn := getClientConn(ctx, false)
defer conn.Close()
client := lnrpc.NewLightningClient(conn)
routerClient := routerrpc.NewRouterClient(conn)
lnClient := lnrpc.NewLightningClient(lnConn)
outChan := ctx.Int64Slice("outgoing_chan_id")
if len(outChan) != 0 {
@ -558,7 +574,7 @@ func sendPaymentRequest(ctx *cli.Context,
if req.PaymentRequest != "" {
// Decode payment request to find out the amount.
decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
decodeResp, err := client.DecodePayReq(ctxc, decodeReq)
decodeResp, err := lnClient.DecodePayReq(ctxc, decodeReq)
if err != nil {
return err
}
@ -602,14 +618,12 @@ func sendPaymentRequest(ctx *cli.Context,
printJSON := ctx.Bool(jsonFlag.Name)
req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
stream, err := routerClient.SendPaymentV2(ctxc, req)
stream, err := callSendPayment(ctxc, paymentConn, req)
if err != nil {
return err
}
finalState, err := printLivePayment(
ctxc, stream, client, printJSON,
)
finalState, err := PrintLivePayment(ctxc, stream, lnClient, printJSON)
if err != nil {
return err
}
@ -667,24 +681,29 @@ 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
}
// printLivePayment receives payment updates from the given stream and either
// 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
// 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 routerrpc.Router_TrackPaymentV2Client,
client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
func PrintLivePayment(ctxc context.Context, stream PaymentResultStream,
lnClient 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(client)
aliases := newAliasCache(lnClient)
first := true
var lastLineCount int
@ -706,17 +725,17 @@ func printLivePayment(ctxc context.Context,
// Write raw json to stdout.
printRespJSON(payment)
} else {
table := formatPayment(ctxc, payment, aliases)
resultTable := formatPayment(ctxc, payment, aliases)
// Clear all previously written lines and print the
// updated table.
clearLines(lastLineCount)
fmt.Print(table)
fmt.Print(resultTable)
// Store the number of lines written for the next update
// pass.
lastLineCount = 0
for _, b := range table {
for _, b := range resultTable {
if b == '\n' {
lastLineCount++
}
@ -874,7 +893,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 " +
@ -885,6 +904,9 @@ var payInvoiceCommand = cli.Command{
}
func payInvoice(ctx *cli.Context) error {
conn := getClientConn(ctx, false)
defer conn.Close()
args := ctx.Args()
var payReq string
@ -898,14 +920,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)
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
}
var sendToRouteCommand = cli.Command{
@ -1900,7 +1922,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())
}

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"fmt"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"context"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"errors"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"fmt"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"bufio"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"bufio"
@ -11,6 +11,7 @@ import (
"io"
"math"
"os"
"regexp"
"strconv"
"strings"
"sync"
@ -41,8 +42,49 @@ const (
defaultUtxoMinConf = 1
)
var errBadChanPoint = errors.New("expecting chan_point to be in format of: " +
"txid:index")
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()
}
func getContext() context.Context {
shutdownInterceptor, err := signal.Intercept()
@ -66,9 +108,9 @@ func printJSON(resp interface{}) {
}
var out bytes.Buffer
json.Indent(&out, b, "", "\t")
out.WriteString("\n")
out.WriteTo(os.Stdout)
_ = json.Indent(&out, b, "", " ")
_, _ = out.WriteString("\n")
_, _ = out.WriteTo(os.Stdout)
}
func printRespJSON(resp proto.Message) {
@ -78,7 +120,9 @@ func printRespJSON(resp proto.Message) {
return
}
fmt.Printf("%s\n", jsonBytes)
jsonBytesReplaced := replaceCustomData(jsonBytes)
fmt.Printf("%s\n", jsonBytesReplaced)
}
// actionDecorator is used to add additional information and error handling
@ -1442,15 +1486,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()
@ -1575,7 +1619,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.",
@ -1608,7 +1652,7 @@ var listChannelsCommand = cli.Command{
"order to improve performance",
},
},
Action: actionDecorator(listChannels),
Action: actionDecorator(ListChannels),
}
var listAliasesCommand = cli.Command{
@ -1616,10 +1660,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()
@ -1636,7 +1680,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()

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"
@ -120,3 +120,74 @@ 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))
})
}
}

View file

@ -1,7 +1,7 @@
//go:build dev
// +build dev
package main
package commands
import (
"fmt"

View file

@ -1,7 +1,7 @@
//go:build !dev
// +build !dev
package main
package commands
import "github.com/urfave/cli"

View file

@ -1,7 +1,7 @@
//go:build invoicesrpc
// +build invoicesrpc
package main
package commands
import (
"encoding/hex"

View file

@ -1,7 +1,7 @@
//go:build !invoicesrpc
// +build !invoicesrpc
package main
package commands
import "github.com/urfave/cli"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/base64"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

601
cmd/commands/main.go Normal file
View file

@ -0,0 +1,601 @@
// 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)
}
}

View file

@ -1,7 +1,7 @@
//go:build neutrinorpc
// +build neutrinorpc
package main
package commands
import (
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"

View file

@ -1,7 +1,7 @@
//go:build !neutrinorpc
// +build !neutrinorpc
package main
package commands
import "github.com/urfave/cli"

View file

@ -1,7 +1,7 @@
//go:build peersrpc
// +build peersrpc
package main
package commands
import (
"fmt"

View file

@ -1,7 +1,7 @@
//go:build !peersrpc
// +build !peersrpc
package main
package commands
import "github.com/urfave/cli"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"bytes"

View file

@ -1,4 +1,4 @@
package main
package commands
import "github.com/urfave/cli"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

View file

@ -1,7 +1,7 @@
//go:build walletrpc
// +build walletrpc
package main
package commands
import (
"bytes"

View file

@ -1,7 +1,7 @@
//go:build !walletrpc
// +build !walletrpc
package main
package commands
import "github.com/urfave/cli"

View file

@ -1,4 +1,4 @@
package main
package commands
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"

View file

@ -1,7 +1,7 @@
//go:build watchtowerrpc
// +build watchtowerrpc
package main
package commands
import (
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"

View file

@ -1,7 +1,7 @@
//go:build !watchtowerrpc
// +build !watchtowerrpc
package main
package commands
import "github.com/urfave/cli"

View file

@ -1,4 +1,4 @@
package main
package commands
import (
"encoding/hex"

View file

@ -1,594 +1,11 @@
// Copyright (c) 2013-2017 The btcsuite developers
// Copyright (c) 2015-2016 The Decred developers
// Copyright (C) 2015-2022 The Lightning Network Developers
// Copyright (C) 2015-2024 The Lightning Network Developers
package main
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
}
import "github.com/lightningnetwork/lnd/cmd/commands"
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)
}
commands.Main()
}

View file

@ -33,6 +33,8 @@ 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/invoices"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/kvdb"
@ -40,11 +42,15 @@ 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/routing"
"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"
@ -103,7 +109,7 @@ type DatabaseBuilder interface {
type WalletConfigBuilder interface {
// BuildWalletConfig is responsible for creating or unlocking and then
// fully initializing a wallet.
BuildWalletConfig(context.Context, *DatabaseInstances,
BuildWalletConfig(context.Context, *DatabaseInstances, *AuxComponents,
*rpcperms.InterceptorChain,
[]*ListenerWithSignal) (*chainreg.PartialChainControl,
*btcwallet.Config, func(), error)
@ -144,6 +150,52 @@ 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[routing.TlvTrafficShaper]
// 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
@ -228,7 +280,8 @@ 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, interceptorChain *rpcperms.InterceptorChain,
dbs *DatabaseInstances, aux *AuxComponents,
interceptorChain *rpcperms.InterceptorChain,
grpcListeners []*ListenerWithSignal) (*chainreg.PartialChainControl,
*btcwallet.Config, func(), error) {
@ -548,6 +601,8 @@ 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{
@ -611,8 +666,9 @@ 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(
@ -703,6 +759,8 @@ 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

View file

@ -15,6 +15,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/kvdb"
"github.com/lightningnetwork/lnd/labels"
@ -22,6 +23,8 @@ 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 (
@ -147,7 +150,7 @@ type BreachConfig struct {
Estimator chainfee.Estimator
// GenSweepScript generates the receiving scripts for swept outputs.
GenSweepScript func() ([]byte, error)
GenSweepScript func() fn.Result[lnwallet.AddrWithKey]
// Notifier provides a publish/subscribe interface for event driven
// notifications regarding the confirmation of txids.
@ -172,6 +175,10 @@ 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
@ -735,10 +742,28 @@ 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, label)
err = b.cfg.PublishTransaction(finalTx.justiceTx, label)
if err != nil {
brarLog.Errorf("Unable to broadcast justice tx: %v", err)
}
@ -858,7 +883,9 @@ Loop:
"spending commitment outs: %v",
lnutils.SpewLogClosure(tx))
err = b.cfg.PublishTransaction(tx, label)
err = b.cfg.PublishTransaction(
tx.justiceTx, label,
)
if err != nil {
brarLog.Warnf("Unable to broadcast "+
"commit out spending justice "+
@ -873,7 +900,9 @@ Loop:
"spending HTLC outs: %v",
lnutils.SpewLogClosure(tx))
err = b.cfg.PublishTransaction(tx, label)
err = b.cfg.PublishTransaction(
tx.justiceTx, label,
)
if err != nil {
brarLog.Warnf("Unable to broadcast "+
"HTLC out spending justice "+
@ -888,7 +917,9 @@ Loop:
"spending second-level HTLC output: %v",
lnutils.SpewLogClosure(tx))
err = b.cfg.PublishTransaction(tx, label)
err = b.cfg.PublishTransaction(
tx.justiceTx, label,
)
if err != nil {
brarLog.Warnf("Unable to broadcast "+
"second-level HTLC out "+
@ -1067,15 +1098,18 @@ 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) breachedOutput {
witnessType input.StandardWitnessType, secondLevelScript []byte,
signDescriptor *input.SignDescriptor, confHeight uint32,
resolutionBlob fn.Option[tlv.Blob]) breachedOutput {
amount := signDescriptor.Output.Value
@ -1086,6 +1120,7 @@ func makeBreachedOutput(outpoint *wire.OutPoint,
witnessType: witnessType,
signDesc: *signDescriptor,
confHeight: confHeight,
resolutionBlob: resolutionBlob,
}
}
@ -1125,6 +1160,11 @@ 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
@ -1174,6 +1214,12 @@ 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)
@ -1258,6 +1304,7 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
nil,
breachInfo.LocalOutputSignDesc,
breachInfo.BreachHeight,
breachInfo.LocalResolutionBlob,
)
breachedOutputs = append(breachedOutputs, localOutput)
@ -1284,6 +1331,7 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
nil,
breachInfo.RemoteOutputSignDesc,
breachInfo.BreachHeight,
breachInfo.RemoteResolutionBlob,
)
breachedOutputs = append(breachedOutputs, remoteOutput)
@ -1318,6 +1366,7 @@ 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
@ -1357,10 +1406,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 *wire.MsgTx
spendCommitOuts *wire.MsgTx
spendHTLCs *wire.MsgTx
spendSecondLevelHTLCs []*wire.MsgTx
spendAll *justiceTxCtx
spendCommitOuts *justiceTxCtx
spendHTLCs *justiceTxCtx
spendSecondLevelHTLCs []*justiceTxCtx
}
// createJusticeTx creates transactions which exacts "justice" by sweeping ALL
@ -1424,7 +1473,9 @@ func (b *BreachArbitrator) createJusticeTx(
err)
}
secondLevelSweeps := make([]*wire.MsgTx, 0, len(secondLevelInputs))
// TODO(roasbeef): only register one of them?
secondLevelSweeps := make([]*justiceTxCtx, 0, len(secondLevelInputs))
for _, input := range secondLevelInputs {
sweepTx, err := b.createSweepTx(input)
if err != nil {
@ -1441,9 +1492,23 @@ 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) (*wire.MsgTx,
error) {
func (b *BreachArbitrator) createSweepTx(
inputs ...input.Input) (*justiceTxCtx, error) {
if len(inputs) == 0 {
return nil, nil
@ -1466,6 +1531,18 @@ func (b *BreachArbitrator) createSweepTx(inputs ...input.Input) (*wire.MsgTx,
// 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
@ -1499,13 +1576,13 @@ func (b *BreachArbitrator) createSweepTx(inputs ...input.Input) (*wire.MsgTx,
// 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) (*wire.MsgTx, error) {
inputs ...input.Input) (*justiceTxCtx, 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()
pkScript, err := b.cfg.GenSweepScript().Unpack()
if err != nil {
return nil, err
}
@ -1524,6 +1601,18 @@ 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)
@ -1531,12 +1620,24 @@ func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
// information gathered above and the provided retribution information.
txn := wire.NewMsgTx(2)
// We begin by adding the output to which our funds will be deposited.
// 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.
txn.AddTxOut(&wire.TxOut{
PkScript: pkScript,
PkScript: pkScript.DeliveryAddress,
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 {
@ -1592,7 +1693,13 @@ func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
}
}
return txn, nil
return &justiceTxCtx{
justiceTx: txn,
sweepAddr: pkScript,
extraTxOut: extraChangeOut.Option(),
fee: txFee,
inputs: inputs,
}, nil
}
// RetributionStore handles persistence of retribution states to disk and is
@ -1622,13 +1729,29 @@ func taprootBriefcaseFromRetInfo(retInfo *retributionInfo) *taprootBriefcase {
// commitment, we'll need to stash the control block.
case input.TaprootRemoteCommitSpend:
//nolint:lll
tapCase.CtrlBlocks.CommitSweepCtrlBlock = bo.signDesc.ControlBlock
tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock = bo.signDesc.ControlBlock
bo.resolutionBlob.WhenSome(func(blob tlv.Blob) {
tapCase.SettledCommitBlob = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType2](
blob,
),
)
})
// 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.RevokeSweepCtrlBlock = bo.signDesc.ControlBlock
tapCase.CtrlBlocks.Val.RevokeSweepCtrlBlock = bo.signDesc.ControlBlock
bo.resolutionBlob.WhenSome(func(blob tlv.Blob) {
tapCase.BreachedCommitBlob = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType3](
blob,
),
)
})
// For spending the HTLC outputs, we'll store the first and
// second level tweak values.
@ -1642,10 +1765,10 @@ func taprootBriefcaseFromRetInfo(retInfo *retributionInfo) *taprootBriefcase {
secondLevelTweak := bo.secondLevelTapTweak
//nolint:lll
tapCase.TapTweaks.BreachedHtlcTweaks[resID] = firstLevelTweak
tapCase.TapTweaks.Val.BreachedHtlcTweaks[resID] = firstLevelTweak
//nolint:lll
tapCase.TapTweaks.BreachedSecondLevelHltcTweaks[resID] = secondLevelTweak
tapCase.TapTweaks.Val.BreachedSecondLevelHltcTweaks[resID] = secondLevelTweak
}
}
@ -1665,13 +1788,25 @@ func applyTaprootRetInfo(tapCase *taprootBriefcase,
// commitment, we'll apply the control block.
case input.TaprootRemoteCommitSpend:
//nolint:lll
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.CommitSweepCtrlBlock
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock
tapCase.SettledCommitBlob.WhenSomeV(
func(blob tlv.Blob) {
bo.resolutionBlob = fn.Some(blob)
},
)
// 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.RevokeSweepCtrlBlock
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.Val.RevokeSweepCtrlBlock
tapCase.BreachedCommitBlob.WhenSomeV(
func(blob tlv.Blob) {
bo.resolutionBlob = fn.Some(blob)
},
)
// For spending the HTLC outputs, we'll apply the first and
// second level tweak values.
@ -1680,7 +1815,8 @@ func applyTaprootRetInfo(tapCase *taprootBriefcase,
case input.TaprootHtlcOfferedRevoke:
resID := newResolverID(bo.OutPoint())
tap1, ok := tapCase.TapTweaks.BreachedHtlcTweaks[resID]
//nolint:lll
tap1, ok := tapCase.TapTweaks.Val.BreachedHtlcTweaks[resID]
if !ok {
return fmt.Errorf("unable to find taproot "+
"tweak for: %v", bo.OutPoint())
@ -1688,7 +1824,7 @@ func applyTaprootRetInfo(tapCase *taprootBriefcase,
bo.signDesc.TapTweak = tap1[:]
//nolint:lll
tap2, ok := tapCase.TapTweaks.BreachedSecondLevelHltcTweaks[resID]
tap2, ok := tapCase.TapTweaks.Val.BreachedSecondLevelHltcTweaks[resID]
if !ok {
return fmt.Errorf("unable to find taproot "+
"tweak for: %v", bo.OutPoint())

View file

@ -22,6 +22,7 @@ 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"
@ -1198,6 +1199,8 @@ 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
@ -1216,6 +1219,7 @@ func TestBreachCreateJusticeTx(t *testing.T) {
nil,
signDesc,
1,
rBlob,
)
}
@ -1226,16 +1230,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.TxIn, len(breachedOutputs))
require.Len(t, justiceTxs.spendAll.justiceTx.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.TxIn, 4)
require.Len(t, justiceTxs.spendCommitOuts.justiceTx.TxIn, 4)
// Check that the spendHTLCs tx is spending the two revoked commitment
// level HTLC output types.
require.Len(t, justiceTxs.spendHTLCs.TxIn, 2)
require.Len(t, justiceTxs.spendHTLCs.justiceTx.TxIn, 2)
// Finally, check that the spendSecondLevelHTLCs txs are spending the
// second level type.
@ -1590,6 +1594,10 @@ 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")
@ -1799,6 +1807,10 @@ 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")
@ -2129,11 +2141,15 @@ func createTestArbiter(t *testing.T, contractBreaches chan *ContractBreachEvent,
CloseLink: func(_ *wire.OutPoint, _ ChannelCloseType) {},
DB: db.ChannelStateDB(),
Estimator: chainfee.NewStaticEstimator(12500, 0),
GenSweepScript: func() ([]byte, error) { return nil, nil },
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 },
PublishTransaction: func(_ *wire.MsgTx, _ string) error {
return nil
},
Store: store,
})
@ -2357,9 +2373,12 @@ 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
@ -2372,6 +2391,8 @@ 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

View file

@ -10,9 +10,11 @@ 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
@ -1553,9 +1555,16 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
commitResolution := c.CommitResolution
commitSignDesc := commitResolution.SelfOutputSignDesc
//nolint:lll
tapCase.CtrlBlocks.CommitSweepCtrlBlock = commitSignDesc.ControlBlock
tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock = commitSignDesc.ControlBlock
c.CommitResolution.ResolutionBlob.WhenSome(func(b []byte) {
tapCase.SettledCommitBlob = tlv.SomeRecordT(
tlv.NewPrimitiveRecord[tlv.TlvType2](b),
)
})
}
htlcBlobs := newAuxHtlcBlobs()
for _, htlc := range c.HtlcResolutions.IncomingHTLCs {
htlc := htlc
@ -1566,12 +1575,13 @@ 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.SecondLevelCtrlBlocks[resID] = ctrlBlock
tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID] = ctrlBlock
// For HTLCs we need to go to the second level for, we
// also need to store the control block needed to
@ -1580,13 +1590,17 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
//nolint:lll
bridgeCtrlBlock := htlc.SignDetails.SignDesc.ControlBlock
//nolint:lll
tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
}
} else {
resID := newResolverID(htlc.ClaimOutpoint)
resID = newResolverID(htlc.ClaimOutpoint)
//nolint:lll
tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID] = ctrlBlock
tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID] = ctrlBlock
}
htlc.ResolutionBlob.WhenSome(func(b []byte) {
htlcBlobs[resID] = b
})
}
for _, htlc := range c.HtlcResolutions.OutgoingHTLCs {
htlc := htlc
@ -1598,12 +1612,13 @@ 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.SecondLevelCtrlBlocks[resID] = ctrlBlock
tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID] = ctrlBlock
// For HTLCs we need to go to the second level for, we
// also need to store the control block needed to
@ -1614,18 +1629,28 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
//nolint:lll
bridgeCtrlBlock := htlc.SignDetails.SignDesc.ControlBlock
//nolint:lll
tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
}
} else {
resID := newResolverID(htlc.ClaimOutpoint)
resID = newResolverID(htlc.ClaimOutpoint)
//nolint:lll
tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID] = ctrlBlock
tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID] = ctrlBlock
}
htlc.ResolutionBlob.WhenSome(func(b []byte) {
htlcBlobs[resID] = b
})
}
if c.AnchorResolution != nil {
anchorSignDesc := c.AnchorResolution.AnchorSignDescriptor
tapCase.TapTweaks.AnchorTweak = anchorSignDesc.TapTweak
tapCase.TapTweaks.Val.AnchorTweak = anchorSignDesc.TapTweak
}
if len(htlcBlobs) != 0 {
tapCase.HtlcBlobs = tlv.SomeRecordT(
tlv.NewRecordT[tlv.TlvType4](htlcBlobs),
)
}
return tapCase.Encode(w)
@ -1639,9 +1664,15 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
if c.CommitResolution != nil {
c.CommitResolution.SelfOutputSignDesc.ControlBlock =
tapCase.CtrlBlocks.CommitSweepCtrlBlock
tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock
tapCase.SettledCommitBlob.WhenSomeV(func(b []byte) {
c.CommitResolution.ResolutionBlob = fn.Some(b)
})
}
htlcBlobs := tapCase.HtlcBlobs.ValOpt().UnwrapOr(newAuxHtlcBlobs())
for i := range c.HtlcResolutions.IncomingHTLCs {
htlc := c.HtlcResolutions.IncomingHTLCs[i]
@ -1652,23 +1683,28 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
)
//nolint:lll
ctrlBlock := tapCase.CtrlBlocks.SecondLevelCtrlBlocks[resID]
ctrlBlock := tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID]
htlc.SweepSignDesc.ControlBlock = ctrlBlock
//nolint:lll
if htlc.SignDetails != nil {
bridgeCtrlBlock := tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID]
bridgeCtrlBlock := tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID]
htlc.SignDetails.SignDesc.ControlBlock = bridgeCtrlBlock
}
} else {
resID = newResolverID(htlc.ClaimOutpoint)
//nolint:lll
ctrlBlock := tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID]
ctrlBlock := tapCase.CtrlBlocks.Val.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]
@ -1680,28 +1716,32 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
)
//nolint:lll
ctrlBlock := tapCase.CtrlBlocks.SecondLevelCtrlBlocks[resID]
ctrlBlock := tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID]
htlc.SweepSignDesc.ControlBlock = ctrlBlock
//nolint:lll
if htlc.SignDetails != nil {
bridgeCtrlBlock := tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID]
bridgeCtrlBlock := tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID]
htlc.SignDetails.SignDesc.ControlBlock = bridgeCtrlBlock
}
} else {
resID = newResolverID(htlc.ClaimOutpoint)
//nolint:lll
ctrlBlock := tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID]
ctrlBlock := tapCase.CtrlBlocks.Val.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.AnchorTweak
tapCase.TapTweaks.Val.AnchorTweak
}
return nil

View file

@ -217,6 +217,18 @@ 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
@ -299,8 +311,19 @@ 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,
a.c.cfg.Signer, channel, nil, chanOpts...,
)
if err != nil {
return nil, err
@ -312,11 +335,10 @@ 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. The returned summary contains all items
// needed to eventually resolve all outputs on chain.
// that we won't accept any new updates.
//
// NOTE: Part of the ArbChannel interface.
func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) {
func (a *arbChannel) ForceCloseChan() (*wire.MsgTx, 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.
@ -344,15 +366,34 @@ func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, 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,
a.c.cfg.Signer, channel, nil, chanOpts...,
)
if err != nil {
return nil, err
}
return chanMachine.ForceClose()
closeSummary, err := chanMachine.ForceClose(
lnwallet.WithSkipContractResolutions(),
)
if err != nil {
return nil, err
}
return closeSummary.CloseTx, nil
}
// newActiveChannelArbitrator creates a new instance of an active channel
@ -557,6 +598,8 @@ 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 {
@ -1186,6 +1229,8 @@ func (c *ChainArbitrator) WatchNewChannel(newChan *channeldb.OpenChannel) error
)
},
extractStateNumHint: lnwallet.GetStateNumHint,
auxLeafStore: c.cfg.AuxLeafStore,
auxResolver: c.cfg.AuxResolver,
},
)
if err != nil {

View file

@ -193,6 +193,12 @@ 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
@ -308,7 +314,7 @@ func (c *chainWatcher) Start() error {
)
if chanState.ChanType.IsTaproot() {
c.fundingPkScript, _, err = input.GenTaprootFundingScript(
localKey, remoteKey, 0,
localKey, remoteKey, 0, chanState.TapscriptRoot,
)
if err != nil {
return err
@ -423,15 +429,37 @@ 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
@ -440,10 +468,16 @@ 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
@ -866,7 +900,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail,
spendHeight := uint32(commitSpend.SpendingHeight)
retribution, err := lnwallet.NewBreachRetribution(
c.cfg.chanState, broadcastStateNum, spendHeight,
commitSpend.SpendingTx,
commitSpend.SpendingTx, c.cfg.auxLeafStore, c.cfg.auxResolver,
)
switch {
@ -1116,8 +1150,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.chanState, c.cfg.signer, commitSpend.SpendingTx, stateNum,
c.cfg.auxLeafStore, c.cfg.auxResolver,
)
if err != nil {
return err
@ -1141,17 +1175,30 @@ 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 forceClose.CommitResolution != nil {
closeSummary.SettledBalance = chanSnapshot.LocalBalance.ToSatoshis()
closeSummary.TimeLockedBalance = chanSnapshot.LocalBalance.ToSatoshis()
if resolutions.CommitResolution != nil {
localBalance := chanSnapshot.LocalBalance.ToSatoshis()
closeSummary.SettledBalance = localBalance
closeSummary.TimeLockedBalance = localBalance
}
for _, htlc := range forceClose.HtlcResolutions.OutgoingHTLCs {
htlcValue := btcutil.Amount(htlc.SweepSignDesc.Output.Value)
if resolutions.HtlcResolutions != nil {
for _, htlc := range resolutions.HtlcResolutions.OutgoingHTLCs {
htlcValue := btcutil.Amount(
htlc.SweepSignDesc.Output.Value,
)
closeSummary.TimeLockedBalance += htlcValue
}
}
// Attempt to add a channel sync message to the close summary.
chanSync, err := c.cfg.chanState.ChanSyncMsg()
@ -1209,8 +1256,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.chanState, c.cfg.signer, commitSpend, remoteCommit,
commitPoint, c.cfg.auxLeafStore, c.cfg.auxResolver,
)
if err != nil {
return err

View file

@ -2,6 +2,7 @@ package contractcourt
import (
"bytes"
"context"
"crypto/sha256"
"fmt"
"testing"
@ -145,17 +146,15 @@ func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) {
// With the HTLC added, we'll now manually initiate a state transition
// from Alice to Bob.
_, err = aliceChannel.SignNextCommitment()
if err != nil {
t.Fatal(err)
}
testQuit, testQuitFunc := context.WithCancel(context.Background())
t.Cleanup(testQuitFunc)
_, err = aliceChannel.SignNextCommitment(testQuit)
require.NoError(t, err)
// At this point, we'll now Bob broadcasting this new pending unrevoked
// commitment.
bobPendingCommit, err := aliceChannel.State().RemoteCommitChainTip()
if err != nil {
t.Fatal(err)
}
require.NoError(t, 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
@ -505,14 +504,24 @@ 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 summary.CommitResolution != nil {
if resolutions.CommitResolution != nil {
t.Fatalf("expected no commit resolution")
}
} else {
if summary.CommitResolution == nil {
if resolutions.CommitResolution == nil {
t.Fatalf("expected commit resolution")
}
}

View file

@ -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() (*lnwallet.LocalForceCloseSummary, error)
ForceCloseChan() (*wire.MsgTx, error)
// NewAnchorResolutions returns the anchor resolutions for currently
// valid commitment transactions.
@ -1058,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.
closeSummary, err := c.cfg.Channel.ForceCloseChan()
forceCloseTx, err := c.cfg.Channel.ForceCloseChan()
if err != nil {
log.Errorf("ChannelArbitrator(%v): unable to "+
"force close: %v", c.cfg.ChanPoint, err)
@ -1078,7 +1078,7 @@ func (c *ChannelArbitrator) stateStep(
return StateError, closeTx, err
}
closeTx = closeSummary.CloseTx
closeTx = forceCloseTx
// Before publishing the transaction, we store it to the
// database, such that we can re-publish later in case it
@ -1982,9 +1982,11 @@ 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 err {
case nil:
case invoices.ErrInvoiceNotFound, invoices.ErrNoInvoicesCreated:
switch {
case err == nil:
case errors.Is(err, invoices.ErrInvoiceNotFound) ||
errors.Is(err, invoices.ErrNoInvoicesCreated):
return false, nil
default:
return false, err
@ -2869,11 +2871,36 @@ 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: closeInfo.CommitResolution,
HtlcResolutions: *closeInfo.HtlcResolutions,
AnchorResolution: closeInfo.AnchorResolution,
CommitResolution: resolutions.CommitResolution,
HtlcResolutions: *resolutions.HtlcResolutions,
AnchorResolution: resolutions.AnchorResolution,
}
// When processing a unilateral close event, we'll
@ -2882,7 +2909,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)

View file

@ -693,11 +693,15 @@ 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{},
}),
},
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
}
@ -969,15 +973,18 @@ 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,
},
},
}),
},
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
CommitSet: CommitSet{
@ -1611,12 +1618,15 @@ 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{},
}),
},
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
}
@ -1944,11 +1954,15 @@ 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{},
}),
},
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
CommitSet: CommitSet{
@ -2754,12 +2768,15 @@ 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,
}),
},
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
CommitSet: CommitSet{
@ -2993,14 +3010,10 @@ func (m *mockChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
return &lnwallet.AnchorResolutions{}, nil
}
func (m *mockChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) {
func (m *mockChannel) ForceCloseChan() (*wire.MsgTx, error) {
if m.forceCloseErr != nil {
return nil, m.forceCloseErr
}
summary := &lnwallet.LocalForceCloseSummary{
CloseTx: &wire.MsgTx{},
HtlcResolutions: &lnwallet.HtlcResolutions{},
}
return summary, nil
return &wire.MsgTx{}, nil
}

View file

@ -345,12 +345,18 @@ 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,
),
)
}

View file

@ -308,7 +308,7 @@ func (h *htlcIncomingContestResolver) Resolve(
resolution, err := h.Registry.NotifyExitHopHtlc(
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
circuitKey, hodlQueue.ChanIn(), payload,
circuitKey, hodlQueue.ChanIn(), nil, payload,
)
if err != nil {
return nil, err

View file

@ -6,7 +6,9 @@ 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
@ -52,8 +54,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) *input.BaseInput {
signDesc *input.SignDescriptor, csvDelay, broadcastHeight uint32,
payHash [32]byte, resBlob fn.Option[tlv.Blob]) *input.BaseInput {
if h.hasCLTV() {
log.Infof("%T(%x): CSV and CLTV locks expired, offering "+
@ -63,13 +65,17 @@ 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)
return input.NewCsvInput(
op, wType, signDesc, broadcastHeight, csvDelay,
input.WithResolutionBlob(resBlob),
)
}
// SupplementState allows the user of a ContractResolver to supplement it with

View file

@ -247,6 +247,9 @@ 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
@ -403,7 +406,7 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) (
input.LeaseHtlcAcceptedSuccessSecondLevel,
&h.htlcResolution.SweepSignDesc,
h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight),
h.htlc.RHash,
h.htlc.RHash, h.htlcResolution.ResolutionBlob,
)
// Calculate the budget for this sweep.
@ -459,6 +462,9 @@ 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(

View file

@ -484,6 +484,9 @@ 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(
@ -538,7 +541,6 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error {
return err
}
// TODO(yy): checkpoint here?
return err
}
@ -562,6 +564,60 @@ 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
@ -582,8 +638,18 @@ func (h *htlcTimeoutResolver) spendHtlcOutput(
return nil, err
}
// If we have no SignDetails, and we haven't already sent the output to
// the utxo nursery, then we'll do so now.
// 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.
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
if err := h.sendSecondLevelTxLegacy(); err != nil {
log.Errorf("Sending timeout tx to nursery: %v", err)
@ -690,6 +756,13 @@ 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.
@ -762,7 +835,9 @@ 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),
@ -800,6 +875,7 @@ 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,
@ -866,9 +942,11 @@ func (h *htlcTimeoutResolver) IsResolved() bool {
// report returns a report on the resolution state of the contract.
func (h *htlcTimeoutResolver) report() *ContractReport {
// If the sign details are nil, the report will be created by handled
// by the nursery.
if h.htlcResolution.SignDetails == nil {
// 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 {
return nil
}
@ -888,13 +966,20 @@ 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: 1,
Stage: stage,
}
}

View file

@ -69,11 +69,31 @@ func (m *mockWitnessBeacon) AddPreimages(preimages ...lntypes.Preimage) error {
return nil
}
// TestHtlcTimeoutResolver tests that the timeout resolver properly handles all
// variations of possible local+remote spends.
func TestHtlcTimeoutResolver(t *testing.T) {
t.Parallel()
type htlcTimeoutTestCase 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
}
func genHtlcTimeoutTestCases() []htlcTimeoutTestCase {
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
var (
@ -105,29 +125,7 @@ func TestHtlcTimeoutResolver(t *testing.T) {
},
}
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
}{
return []htlcTimeoutTestCase{
// Remote commitment is broadcast, we time out the HTLC on
// chain, and should expect a fail HTLC resolution.
{
@ -149,7 +147,8 @@ func TestHtlcTimeoutResolver(t *testing.T) {
// immediately if the witness is already set
// correctly.
if reflect.DeepEqual(
templateTx.TxIn[0].Witness, witness,
templateTx.TxIn[0].Witness,
witness,
) {
return templateTx, nil
@ -219,7 +218,8 @@ func TestHtlcTimeoutResolver(t *testing.T) {
// 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,7 +253,8 @@ func TestHtlcTimeoutResolver(t *testing.T) {
// immediately if the witness is already set
// correctly.
if reflect.DeepEqual(
templateTx.TxIn[0].Witness, witness,
templateTx.TxIn[0].Witness,
witness,
) {
return templateTx, nil
@ -265,17 +266,25 @@ func TestHtlcTimeoutResolver(t *testing.T) {
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()
for _, testCase := range testCases {
t.Logf("Running test case: %v", testCase.name)
checkPointChan := make(chan struct{}, 1)
incubateChan := make(chan struct{}, 1)
resolutionChan := make(chan ResolutionMsg, 1)
@ -285,6 +294,7 @@ func TestHtlcTimeoutResolver(t *testing.T) {
chainCfg := ChannelArbitratorConfig{
ChainArbitratorConfig: ChainArbitratorConfig{
Notifier: notifier,
Sweeper: newMockSweeper(),
PreimageDB: witnessBeacon,
IncubateOutputs: func(wire.OutPoint,
fn.Option[lnwallet.OutgoingHtlcResolution],
@ -302,10 +312,13 @@ func TestHtlcTimeoutResolver(t *testing.T) {
}
resolutionChan <- msgs[0]
return nil
},
Budget: *DefaultBudgetConfig(),
QueryIncomingCircuit: func(circuit models.CircuitKey) *models.CircuitKey {
QueryIncomingCircuit: func(circuit models.CircuitKey,
) *models.CircuitKey {
return nil
},
},
@ -357,13 +370,15 @@ func TestHtlcTimeoutResolver(t *testing.T) {
if testCase.timeout {
timeoutTxID := timeoutTx.TxHash()
reports = append(reports, &channeldb.ResolverReport{
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
report := &channeldb.ResolverReport{
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, //nolint:lll
Amount: testHtlcAmt.ToSatoshis(),
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
ResolverType: channeldb.ResolverTypeOutgoingHtlc, //nolint:lll
ResolverOutcome: channeldb.ResolverOutcomeFirstStage, //nolint:lll
SpendTxID: &timeoutTxID,
})
}
reports = append(reports, report)
}
}
@ -381,10 +396,21 @@ func TestHtlcTimeoutResolver(t *testing.T) {
}
}()
// At the output isn't yet in the nursery, we expect that we
// should receive an incubation request.
// 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):
@ -440,7 +466,6 @@ func TestHtlcTimeoutResolver(t *testing.T) {
t.Fatalf("resolution not sent")
}
} else {
// Otherwise, the HTLC should now timeout. First, we
// should get a resolution message with a populated
// failure message.
@ -503,6 +528,19 @@ func TestHtlcTimeoutResolver(t *testing.T) {
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)
})
}
}
// NOTE: the following tests essentially checks many of the same scenarios as
@ -536,15 +574,12 @@ 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{
@ -570,7 +605,7 @@ func TestHtlcTimeoutSingleStage(t *testing.T) {
// After the sweep has confirmed, we expect the
// checkpoint to be resolved, and with the above
// report.
incubating: true,
incubating: false,
resolved: true,
reports: []*channeldb.ResolverReport{
claim,
@ -653,6 +688,7 @@ 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,
@ -824,9 +860,9 @@ func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) {
)
}
// 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.
// 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.
func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
commitOutpoint := wire.OutPoint{Index: 2}
@ -870,10 +906,6 @@ 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.
@ -919,7 +951,7 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
// After the sweep has confirmed, we expect the
// checkpoint to be resolved, and with the above
// report.
incubating: true,
incubating: false,
resolved: true,
reports: []*channeldb.ResolverReport{
claim,
@ -1298,6 +1330,8 @@ 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

View file

@ -30,6 +30,7 @@ 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.

View file

@ -10,5 +10,6 @@ type mockHTLCNotifier struct {
}
func (m *mockHTLCNotifier) NotifyFinalHtlcEvent(key models.CircuitKey,
info channeldb.FinalHtlcInfo) { //nolint:whitespace
info channeldb.FinalHtlcInfo) {
}

View file

@ -26,6 +26,7 @@ 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{

View file

@ -8,9 +8,6 @@ import (
)
const (
taprootCtrlBlockType tlv.Type = 0
taprootTapTweakType tlv.Type = 1
commitCtrlBlockType tlv.Type = 0
revokeCtrlBlockType tlv.Type = 1
outgoingHtlcCtrlBlockType tlv.Type = 2
@ -26,36 +23,67 @@ const (
// information we need to sweep taproot outputs.
type taprootBriefcase struct {
// CtrlBlock is the set of control block for the taproot outputs.
CtrlBlocks *ctrlBlocks
CtrlBlocks tlv.RecordT[tlv.TlvType0, 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 *tapTweaks
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]
}
// TODO(roasbeef): morph into new tlv record
// newTaprootBriefcase returns a new instance of the taproot specific briefcase
// variant.
func newTaprootBriefcase() *taprootBriefcase {
return &taprootBriefcase{
CtrlBlocks: newCtrlBlocks(),
TapTweaks: newTapTweaks(),
CtrlBlocks: tlv.NewRecordT[tlv.TlvType0](newCtrlBlocks()),
TapTweaks: tlv.NewRecordT[tlv.TlvType1](newTapTweaks()),
}
}
// EncodeRecords returns a slice of TLV records that should be encoded.
func (t *taprootBriefcase) EncodeRecords() []tlv.Record {
return []tlv.Record{
newCtrlBlocksRecord(&t.CtrlBlocks),
newTapTweaksRecord(&t.TapTweaks),
records := []tlv.Record{
t.CtrlBlocks.Record(),
t.TapTweaks.Record(),
}
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{
newCtrlBlocksRecord(&t.CtrlBlocks),
newTapTweaksRecord(&t.TapTweaks),
t.CtrlBlocks.Record(),
t.TapTweaks.Record(),
}
}
@ -71,12 +99,35 @@ 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 {
stream, err := tlv.NewStream(t.DecodeRecords()...)
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...)
if err != nil {
return err
}
return stream.Decode(r)
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
}
// resolverCtrlBlocks is a map of resolver IDs to their corresponding control
@ -216,8 +267,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(),
@ -260,7 +311,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)
}
@ -269,7 +320,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
@ -278,7 +329,7 @@ func ctrlBlockDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
return err
}
*typ = &ctrlBlocks
*typ = ctrlBlocks
return nil
}
@ -286,28 +337,6 @@ 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 {
@ -382,7 +411,21 @@ 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 {
return tlv.MakePrimitiveRecord(commitCtrlBlockType, c)
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,
)
}
// Encode encodes the set of control blocks.
@ -530,8 +573,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),
}
@ -539,7 +582,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)
}
@ -548,7 +591,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
@ -557,7 +600,7 @@ func tapTweaksDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
return err
}
*typ = &tapTweaks
*typ = tapTweaks
return nil
}
@ -565,27 +608,6 @@ 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
@ -637,7 +659,21 @@ 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 {
return tlv.MakePrimitiveRecord(taprootTapTweakType, t)
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,
)
}
// Encode encodes the set of tap tweaks.
@ -659,3 +695,110 @@ 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,
)
}

View file

@ -5,7 +5,9 @@ import (
"math/rand"
"testing"
"github.com/lightningnetwork/lnd/tlv"
"github.com/stretchr/testify/require"
"pgregory.net/rapid"
)
func randResolverCtrlBlocks(t *testing.T) resolverCtrlBlocks {
@ -52,6 +54,25 @@ 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) {
@ -69,19 +90,32 @@ 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: &ctrlBlocks{
CtrlBlocks: tlv.NewRecordT[tlv.TlvType0](ctrlBlocks{
CommitSweepCtrlBlock: sweepCtrlBlock[:],
RevokeSweepCtrlBlock: revokeCtrlBlock[:],
OutgoingHtlcCtrlBlocks: randResolverCtrlBlocks(t),
IncomingHtlcCtrlBlocks: randResolverCtrlBlocks(t),
SecondLevelCtrlBlocks: randResolverCtrlBlocks(t),
},
TapTweaks: &tapTweaks{
}),
TapTweaks: tlv.NewRecordT[tlv.TlvType1](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
@ -92,3 +126,21 @@ 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)
})
}

View file

@ -0,0 +1,78 @@
# 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

View file

@ -21,6 +21,7 @@ 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
@ -1423,6 +1424,7 @@ func makeKidOutput(outpoint, originChanPoint *wire.OutPoint,
return kidOutput{
breachedOutput: makeBreachedOutput(
outpoint, witnessType, nil, signDescriptor, heightHint,
fn.None[tlv.Blob](),
),
isHtlc: isHtlc,
originChanPoint: *originChanPoint,

View file

@ -3,7 +3,7 @@
# /make/builder.Dockerfile
# /.github/workflows/main.yml
# /.github/workflows/release.yml
FROM golang:1.22.5-alpine as builder
FROM golang:1.22.6-alpine as builder
LABEL maintainer="Olaoluwa Osuntokun <laolu@lightning.engineering>"

View file

@ -20,6 +20,7 @@ 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"
@ -85,6 +86,7 @@ type optionalMsgFields struct {
capacity *btcutil.Amount
channelPoint *wire.OutPoint
remoteAlias *lnwire.ShortChannelID
tapscriptRoot fn.Option[chainhash.Hash]
}
// apply applies the optional fields within the functional options.
@ -115,6 +117,14 @@ 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
@ -2578,6 +2588,9 @@ 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())

View file

@ -835,12 +835,6 @@ 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(

View file

@ -1,4 +1,4 @@
FROM golang:1.22.5-alpine as builder
FROM golang:1.22.6-alpine as builder
LABEL maintainer="Olaoluwa Osuntokun <laolu@lightning.engineering>"

View file

@ -100,12 +100,12 @@ the following commands for your OS:
<summary>Linux (x86-64)</summary>
```
wget https://dl.google.com/go/go1.22.5.linux-amd64.tar.gz
wget https://dl.google.com/go/go1.22.6.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
`904b924d435eaea086515bc63235b192ea441bd8c9b198c507e85009e6e4c7f0`. If it
`999805bed7d9039ec3da1a53bfbcafc13e367da52aa823cb60b68ba22d44c616`. 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
`8c4587cf3e63c9aefbcafa92818c4d9d51683af93ea687bf6c7508d6fa36f85e`. If it
`b566484fe89a54c525dd1a4cbfec903c1f6e8f0b7b3dbaf94c79bc9145391083`. 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:
```

View file

@ -0,0 +1,124 @@
# 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).
# 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

View file

@ -92,7 +92,8 @@ var defaultSetDesc = setDesc{
SetInit: {}, // I
SetNodeAnn: {}, // N
},
lnwire.Bolt11BlindedPathsOptional: {
SetInvoice: {}, // I
lnwire.SimpleTaprootOverlayChansOptional: {
SetInit: {}, // I
SetNodeAnn: {}, // N
},
}

View file

@ -79,6 +79,11 @@ var deps = depDesc{
lnwire.AnchorsZeroFeeHtlcTxOptional: {},
lnwire.ExplicitChannelTypeOptional: {},
},
lnwire.SimpleTaprootOverlayChansOptional: {
lnwire.SimpleTaprootChannelsOptionalStaging: {},
lnwire.TLVOnionPayloadOptional: {},
lnwire.ScidAliasOptional: {},
},
lnwire.RouteBlindingOptional: {
lnwire.TLVOnionPayloadOptional: {},
},

View file

@ -63,6 +63,9 @@ 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
@ -192,6 +195,10 @@ 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 "+

51
funding/aux_funding.go Normal file
View file

@ -0,0 +1,51 @@
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
}

View file

@ -307,6 +307,74 @@ 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

View file

@ -36,7 +36,8 @@ type aliasHandler interface {
GetPeerAlias(lnwire.ChannelID) (lnwire.ShortChannelID, error)
// AddLocalAlias persists an alias to an underlying alias store.
AddLocalAlias(lnwire.ShortChannelID, lnwire.ShortChannelID, bool) error
AddLocalAlias(lnwire.ShortChannelID, lnwire.ShortChannelID, bool,
bool) error
// GetAliases returns the set of aliases given the main SCID of a
// channel. This SCID will be an alias for zero-conf channels and will

View file

@ -6,6 +6,7 @@ import (
"fmt"
"io"
"sync"
"sync/atomic"
"time"
"github.com/btcsuite/btcd/blockchain"
@ -23,6 +24,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/discovery"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/graph"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
@ -97,7 +99,6 @@ const (
// you and limitless channel size (apart from 21 million cap).
MaxBtcFundingAmountWumbo = btcutil.Amount(1000000000)
// TODO(roasbeef): tune.
msgBufferSize = 50
// MaxWaitNumBlocksFundingConf is the maximum number of blocks to wait
@ -287,7 +288,7 @@ type InitFundingMsg struct {
// PendingChanID is not all zeroes (the default value), then this will
// be the pending channel ID used for the funding flow within the wire
// protocol.
PendingChanID [32]byte
PendingChanID PendingChanID
// ChannelType allows the caller to use an explicit channel type for the
// funding negotiation. This type will only be observed if BOTH sides
@ -317,7 +318,7 @@ type fundingMsg struct {
// pendingChannels is a map instantiated per-peer which tracks all active
// pending single funded channels indexed by their pending channel identifier,
// which is a set of 32-bytes generated via a CSPRNG.
type pendingChannels map[[32]byte]*reservationWithCtx
type pendingChannels map[PendingChanID]*reservationWithCtx
// serializedPubKey is used within the FundingManager's activeReservations list
// to identify the nodes with which the FundingManager is actively working to
@ -543,6 +544,24 @@ type Config struct {
// backed funding flow to not use utxos still being swept by the sweeper
// subsystem.
IsSweeperOutpoint func(wire.OutPoint) bool
// AuxLeafStore is an optional store that can be used to store auxiliary
// leaves for certain custom channel types.
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
// 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[AuxFundingController]
// 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]
}
// Manager acts as an orchestrator/bridge between the wallet's
@ -568,8 +587,10 @@ type Manager struct {
// chanIDNonce is a nonce that's incremented for each new funding
// reservation created.
chanIDNonce atomic.Uint64
// nonceMtx is a mutex that guards the pendingMusigNonces.
nonceMtx sync.RWMutex
chanIDNonce uint64
// pendingMusigNonces is used to store the musig2 nonce we generate to
// send funding locked until we receive a funding locked message from
@ -591,7 +612,7 @@ type Manager struct {
// required as mid funding flow, we switch to referencing the channel
// by its full channel ID once the commitment transactions have been
// signed by both parties.
signedReservations map[lnwire.ChannelID][32]byte
signedReservations map[lnwire.ChannelID]PendingChanID
// resMtx guards both of the maps above to ensure that all access is
// goroutine safe.
@ -798,24 +819,28 @@ func (f *Manager) rebroadcastFundingTx(c *channeldb.OpenChannel) {
}
}
// PendingChanID is a type that represents a pending channel ID. This might be
// selected by the caller, but if not, will be automatically selected.
type PendingChanID = [32]byte
// nextPendingChanID returns the next free pending channel ID to be used to
// identify a particular future channel funding workflow.
func (f *Manager) nextPendingChanID() [32]byte {
// Obtain a fresh nonce. We do this by encoding the current nonce
// counter, then incrementing it by one.
f.nonceMtx.Lock()
var nonce [8]byte
binary.LittleEndian.PutUint64(nonce[:], f.chanIDNonce)
f.chanIDNonce++
f.nonceMtx.Unlock()
func (f *Manager) nextPendingChanID() PendingChanID {
// Obtain a fresh nonce. We do this by encoding the incremented nonce.
nextNonce := f.chanIDNonce.Add(1)
var nonceBytes [8]byte
binary.LittleEndian.PutUint64(nonceBytes[:], nextNonce)
// We'll generate the next pending channelID by "encrypting" 32-bytes
// of zeroes which'll extract 32 random bytes from our stream cipher.
var (
nextChanID [32]byte
nextChanID PendingChanID
zeroes [32]byte
)
salsa20.XORKeyStream(nextChanID[:], zeroes[:], nonce[:], &f.chanIDKey)
salsa20.XORKeyStream(
nextChanID[:], zeroes[:], nonceBytes[:], &f.chanIDKey,
)
return nextChanID
}
@ -1045,7 +1070,8 @@ func (f *Manager) reservationCoordinator() {
//
// NOTE: This MUST be run as a goroutine.
func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
pendingChanID [32]byte, updateChan chan<- *lnrpc.OpenStatusUpdate) {
pendingChanID PendingChanID,
updateChan chan<- *lnrpc.OpenStatusUpdate) {
defer f.wg.Done()
@ -1061,9 +1087,20 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
}
}
var chanOpts []lnwallet.ChannelOpt
f.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
})
f.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
})
f.cfg.AuxResolver.WhenSome(func(s lnwallet.AuxContractResolver) {
chanOpts = append(chanOpts, lnwallet.WithAuxResolver(s))
})
// We create the state-machine object which wraps the database state.
lnChannel, err := lnwallet.NewLightningChannel(
nil, channel, nil,
nil, channel, nil, chanOpts...,
)
if err != nil {
log.Errorf("Unable to create LightningChannel(%v): %v",
@ -1115,7 +1152,7 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
// updateChan can be set non-nil to get OpenStatusUpdates.
func (f *Manager) stateStep(channel *channeldb.OpenChannel,
lnChannel *lnwallet.LightningChannel,
shortChanID *lnwire.ShortChannelID, pendingChanID [32]byte,
shortChanID *lnwire.ShortChannelID, pendingChanID PendingChanID,
channelState channelOpeningState,
updateChan chan<- *lnrpc.OpenStatusUpdate) error {
@ -1238,14 +1275,14 @@ func (f *Manager) stateStep(channel *channeldb.OpenChannel,
// advancePendingChannelState waits for a pending channel's funding tx to
// confirm, and marks it open in the database when that happens.
func (f *Manager) advancePendingChannelState(
channel *channeldb.OpenChannel, pendingChanID [32]byte) error {
func (f *Manager) advancePendingChannelState(channel *channeldb.OpenChannel,
pendingChanID PendingChanID) error {
if channel.IsZeroConf() {
// Persist the alias to the alias database.
baseScid := channel.ShortChannelID
err := f.cfg.AliasManager.AddLocalAlias(
baseScid, baseScid, true,
baseScid, baseScid, true, false,
)
if err != nil {
return fmt.Errorf("error adding local alias to "+
@ -1608,6 +1645,23 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
return
}
// At this point, if we have an AuxFundingController active, we'll
// check to see if we have a special tapscript root to use in our
// MuSig funding output.
tapscriptRoot, err := fn.MapOptionZ(
f.cfg.AuxFundingController,
func(c AuxFundingController) AuxTapscriptResult {
return c.DeriveTapscriptRoot(msg.PendingChannelID)
},
).Unpack()
if err != nil {
err = fmt.Errorf("error deriving tapscript root: %w", err)
log.Error(err)
f.failFundingFlow(peer, cid, err)
return
}
req := &lnwallet.InitFundingReserveMsg{
ChainHash: &msg.ChainHash,
PendingChanID: msg.PendingChannelID,
@ -1624,6 +1678,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
ZeroConf: zeroConf,
OptionScidAlias: scid,
ScidAliasFeature: scidFeatureVal,
TapscriptRoot: tapscriptRoot,
}
reservation, err := f.cfg.Wallet.InitChannelReservation(req)
@ -1880,6 +1935,8 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
log.Debugf("Remote party accepted commitment rendering params: %v",
lnutils.SpewLogClosure(params))
reservation.SetState(lnwallet.SentAcceptChannel)
// With the initiator's contribution recorded, respond with our
// contribution in the next message of the workflow.
fundingAccept := lnwire.AcceptChannel{
@ -1940,6 +1997,10 @@ func (f *Manager) funderProcessAcceptChannel(peer lnpeer.Peer,
// Update the timestamp once the fundingAcceptMsg has been handled.
defer resCtx.updateTimestamp()
if resCtx.reservation.State() != lnwallet.SentOpenChannel {
return
}
log.Infof("Recv'd fundingResponse for pending_id(%x)",
pendingChanID[:])
@ -2243,10 +2304,34 @@ func (f *Manager) waitForPsbt(intent *chanfunding.PsbtIntent,
return
}
// At this point, we'll see if there's an AuxFundingDesc we
// need to deliver so the funding process can continue
// properly.
auxFundingDesc, err := fn.MapOptionZ(
f.cfg.AuxFundingController,
func(c AuxFundingController) AuxFundingDescResult {
return c.DescFromPendingChanID(
cid.tempChanID,
lnwallet.NewAuxChanState(
resCtx.reservation.ChanState(),
),
resCtx.reservation.CommitmentKeyRings(),
true,
)
},
).Unpack()
if err != nil {
failFlow("error continuing PSBT flow", err)
return
}
// A non-nil error means we can continue the funding flow.
// Notify the wallet so it can prepare everything we need to
// continue.
err = resCtx.reservation.ProcessPsbt()
//
// We'll also pass along the aux funding controller as well,
// which may be used to help process the finalized PSBT.
err = resCtx.reservation.ProcessPsbt(auxFundingDesc)
if err != nil {
failFlow("error continuing PSBT flow", err)
return
@ -2341,6 +2426,8 @@ func (f *Manager) continueFundingAccept(resCtx *reservationWithCtx,
}
}
resCtx.reservation.SetState(lnwallet.SentFundingCreated)
if err := resCtx.peer.SendMessage(true, fundingCreated); err != nil {
log.Errorf("Unable to send funding complete message: %v", err)
f.failFundingFlow(resCtx.peer, cid, err)
@ -2372,11 +2459,14 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer,
// final funding transaction, as well as a signature for our version of
// the commitment transaction. So at this point, we can validate the
// initiator's commitment transaction, then send our own if it's valid.
// TODO(roasbeef): make case (p vs P) consistent throughout
fundingOut := msg.FundingPoint
log.Infof("completing pending_id(%x) with ChannelPoint(%v)",
pendingChanID[:], fundingOut)
if resCtx.reservation.State() != lnwallet.SentAcceptChannel {
return
}
// Create the channel identifier without setting the active channel ID.
cid := newChanIdentifier(pendingChanID)
@ -2404,16 +2494,38 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer,
}
}
// At this point, we'll see if there's an AuxFundingDesc we need to
// deliver so the funding process can continue properly.
auxFundingDesc, err := fn.MapOptionZ(
f.cfg.AuxFundingController,
func(c AuxFundingController) AuxFundingDescResult {
return c.DescFromPendingChanID(
cid.tempChanID, lnwallet.NewAuxChanState(
resCtx.reservation.ChanState(),
), resCtx.reservation.CommitmentKeyRings(),
true,
)
},
).Unpack()
if err != nil {
log.Errorf("error continuing PSBT flow: %v", err)
f.failFundingFlow(peer, cid, err)
return
}
// With all the necessary data available, attempt to advance the
// funding workflow to the next stage. If this succeeds then the
// funding transaction will broadcast after our next message.
// CompleteReservationSingle will also mark the channel as 'IsPending'
// in the database.
//
// We'll also directly pass in the AuxFunding controller as well,
// which may be used by the reservation system to finalize funding our
// side.
completeChan, err := resCtx.reservation.CompleteReservationSingle(
&fundingOut, commitSig,
&fundingOut, commitSig, auxFundingDesc,
)
if err != nil {
// TODO(roasbeef): better error logging: peerID, channelID, etc.
log.Errorf("unable to complete single reservation: %v", err)
f.failFundingFlow(peer, cid, err)
return
@ -2614,6 +2726,14 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer,
return
}
if resCtx.reservation.State() != lnwallet.SentFundingCreated {
err := fmt.Errorf("unable to find reservation for chan_id=%x",
msg.ChanID)
f.failFundingFlow(peer, cid, err)
return
}
// Create an entry in the local discovery map so we can ensure that we
// process the channel confirmation fully before we receive a
// channel_ready message.
@ -2709,6 +2829,21 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer,
}
}
// Before we proceed, if we have a funding hook that wants a
// notification that it's safe to broadcast the funding transaction,
// then we'll send that now.
err = fn.MapOptionZ(
f.cfg.AuxFundingController,
func(controller AuxFundingController) error {
return controller.ChannelFinalized(cid.tempChanID)
},
)
if err != nil {
log.Errorf("Failed to inform aux funding controller about "+
"ChannelPoint(%v) being finalized: %v", fundingPoint,
err)
}
// Now that we have a finalized reservation for this funding flow,
// we'll send the to be active channel to the ChainArbitrator so it can
// watch for any on-chain actions before the channel has fully
@ -2724,9 +2859,6 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer,
// Send an update to the upstream client that the negotiation process
// is over.
//
// TODO(roasbeef): add abstraction over updates to accommodate
// long-polling, or SSE, etc.
upd := &lnrpc.OpenStatusUpdate{
Update: &lnrpc.OpenStatusUpdate_ChanPending{
ChanPending: &lnrpc.PendingUpdate{
@ -2770,7 +2902,7 @@ type confirmedChannel struct {
// channel as closed. The error is only returned for the responder of the
// channel flow.
func (f *Manager) fundingTimeout(c *channeldb.OpenChannel,
pendingID [32]byte) error {
pendingID PendingChanID) error {
// We'll get a timeout if the number of blocks mined since the channel
// was initiated reaches MaxWaitNumBlocksFundingConf and we are not the
@ -2891,6 +3023,7 @@ func makeFundingScript(channel *channeldb.OpenChannel) ([]byte, error) {
if channel.ChanType.IsTaproot() {
pkScript, _, err := input.GenTaprootFundingScript(
localKey, remoteKey, int64(channel.Capacity),
channel.TapscriptRoot,
)
if err != nil {
return nil, err
@ -3130,7 +3263,7 @@ func (f *Manager) handleFundingConfirmation(
}
err = f.cfg.AliasManager.AddLocalAlias(
aliasScid, confChannel.shortChanID, true,
aliasScid, confChannel.shortChanID, true, false,
)
if err != nil {
return fmt.Errorf("unable to request alias: %w", err)
@ -3296,7 +3429,7 @@ func (f *Manager) sendChannelReady(completeChan *channeldb.OpenChannel,
err = f.cfg.AliasManager.AddLocalAlias(
alias, completeChan.ShortChannelID,
false,
false, false,
)
if err != nil {
return err
@ -3431,6 +3564,7 @@ func (f *Manager) addToGraph(completeChan *channeldb.OpenChannel,
errChan := f.cfg.SendAnnouncement(
ann.chanAnn, discovery.ChannelCapacity(completeChan.Capacity),
discovery.ChannelPoint(completeChan.FundingOutpoint),
discovery.TapscriptRoot(completeChan.TapscriptRoot),
)
select {
case err := <-errChan:
@ -3627,7 +3761,7 @@ func (f *Manager) annAfterSixConfs(completeChan *channeldb.OpenChannel,
// waitForZeroConfChannel is called when the state is addedToGraph with
// a zero-conf channel. This will wait for the real confirmation, add the
// confirmed SCID to the graph, and then announce after six confs.
// confirmed SCID to the router graph, and then announce after six confs.
func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel) error {
// First we'll check whether the channel is confirmed on-chain. If it
// is already confirmed, the chainntnfs subsystem will return with the
@ -3873,7 +4007,7 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen
}
err = f.cfg.AliasManager.AddLocalAlias(
alias, channel.ShortChannelID, false,
alias, channel.ShortChannelID, false, false,
)
if err != nil {
log.Errorf("unable to add local alias: %v",
@ -3958,6 +4092,26 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen
PubNonce: remoteNonce,
}),
)
// Inform the aux funding controller that the liquidity in the
// custom channel is now ready to be advertised. We potentially
// haven't sent our own channel ready message yet, but other
// than that the channel is ready to count toward available
// liquidity.
err = fn.MapOptionZ(
f.cfg.AuxFundingController,
func(controller AuxFundingController) error {
return controller.ChannelReady(
lnwallet.NewAuxChanState(channel),
)
},
)
if err != nil {
cid := newChanIdentifier(msg.ChanID)
f.sendWarning(peer, cid, err)
return
}
}
// The channel_ready message contains the next commitment point we'll
@ -3995,7 +4149,7 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen
// channel is now active, thus we change its state to `addedToGraph` to
// let the channel start handling routing.
func (f *Manager) handleChannelReadyReceived(channel *channeldb.OpenChannel,
scid *lnwire.ShortChannelID, pendingChanID [32]byte,
scid *lnwire.ShortChannelID, pendingChanID PendingChanID,
updateChan chan<- *lnrpc.OpenStatusUpdate) error {
chanID := lnwire.NewChanIDFromOutPoint(channel.FundingOutpoint)
@ -4044,6 +4198,19 @@ func (f *Manager) handleChannelReadyReceived(channel *channeldb.OpenChannel,
log.Debugf("Channel(%v) with ShortChanID %v: successfully "+
"added to graph", chanID, scid)
err = fn.MapOptionZ(
f.cfg.AuxFundingController,
func(controller AuxFundingController) error {
return controller.ChannelReady(
lnwallet.NewAuxChanState(channel),
)
},
)
if err != nil {
return fmt.Errorf("failed notifying aux funding controller "+
"about channel ready: %w", err)
}
// Give the caller a final update notifying them that the channel is
fundingPoint := channel.FundingOutpoint
cp := &lnrpc.ChannelPoint{
@ -4357,9 +4524,9 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey,
//
// We can pass in zeroes for the min and max htlc policy, because we
// only use the channel announcement message from the returned struct.
ann, err := f.newChanAnnouncement(localIDKey, remoteIDKey,
localFundingKey, remoteFundingKey, shortChanID, chanID,
0, 0, nil, chanType,
ann, err := f.newChanAnnouncement(
localIDKey, remoteIDKey, localFundingKey, remoteFundingKey,
shortChanID, chanID, 0, 0, nil, chanType,
)
if err != nil {
log.Errorf("can't generate channel announcement: %v", err)
@ -4425,7 +4592,6 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey,
// InitFundingWorkflow sends a message to the funding manager instructing it
// to initiate a single funder workflow with the source peer.
// TODO(roasbeef): re-visit blocking nature..
func (f *Manager) InitFundingWorkflow(msg *InitFundingMsg) {
f.fundingRequests <- msg
}
@ -4519,7 +4685,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
// If the caller specified their own channel ID, then we'll use that.
// Otherwise we'll generate a fresh one as normal. This will be used
// to track this reservation throughout its lifetime.
var chanID [32]byte
var chanID PendingChanID
if msg.PendingChanID == zeroID {
chanID = f.nextPendingChanID()
} else {
@ -4615,6 +4781,23 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
scidFeatureVal = true
}
// At this point, if we have an AuxFundingController active, we'll check
// to see if we have a special tapscript root to use in our MuSig2
// funding output.
tapscriptRoot, err := fn.MapOptionZ(
f.cfg.AuxFundingController,
func(c AuxFundingController) AuxTapscriptResult {
return c.DeriveTapscriptRoot(chanID)
},
).Unpack()
if err != nil {
err = fmt.Errorf("error deriving tapscript root: %w", err)
log.Error(err)
msg.Err <- err
return
}
req := &lnwallet.InitFundingReserveMsg{
ChainHash: &msg.ChainHash,
PendingChanID: chanID,
@ -4654,6 +4837,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
OptionScidAlias: scid,
ScidAliasFeature: scidFeatureVal,
Memo: msg.Memo,
TapscriptRoot: tapscriptRoot,
}
reservation, err := f.cfg.Wallet.InitChannelReservation(req)
@ -4805,6 +4989,8 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
log.Infof("Starting funding workflow with %v for pending_id(%x), "+
"committype=%v", msg.Peer.Address(), chanID, commitType)
reservation.SetState(lnwallet.SentOpenChannel)
fundingOpen := lnwire.OpenChannel{
ChainHash: *f.cfg.Wallet.Cfg.NetParams.GenesisHash,
PendingChannelID: chanID,
@ -4942,7 +5128,8 @@ func (f *Manager) pruneZombieReservations() {
// cancelReservationCtx does all needed work in order to securely cancel the
// reservation.
func (f *Manager) cancelReservationCtx(peerKey *btcec.PublicKey,
pendingChanID [32]byte, byRemote bool) (*reservationWithCtx, error) {
pendingChanID PendingChanID,
byRemote bool) (*reservationWithCtx, error) {
log.Infof("Cancelling funding reservation for node_key=%x, "+
"chan_id=%x", peerKey.SerializeCompressed(), pendingChanID[:])
@ -4990,7 +5177,7 @@ func (f *Manager) cancelReservationCtx(peerKey *btcec.PublicKey,
// deleteReservationCtx deletes the reservation uniquely identified by the
// target public key of the peer, and the specified pending channel ID.
func (f *Manager) deleteReservationCtx(peerKey *btcec.PublicKey,
pendingChanID [32]byte) {
pendingChanID PendingChanID) {
peerIDKey := newSerializedKey(peerKey)
f.resMtx.Lock()
@ -5013,7 +5200,7 @@ func (f *Manager) deleteReservationCtx(peerKey *btcec.PublicKey,
// getReservationCtx returns the reservation context for a particular pending
// channel ID for a target peer.
func (f *Manager) getReservationCtx(peerKey *btcec.PublicKey,
pendingChanID [32]byte) (*reservationWithCtx, error) {
pendingChanID PendingChanID) (*reservationWithCtx, error) {
peerIDKey := newSerializedKey(peerKey)
f.resMtx.RLock()
@ -5033,7 +5220,7 @@ func (f *Manager) getReservationCtx(peerKey *btcec.PublicKey,
// of being funded. After the funding transaction has been confirmed, the
// channel will receive a new, permanent channel ID, and will no longer be
// considered pending.
func (f *Manager) IsPendingChannel(pendingChanID [32]byte,
func (f *Manager) IsPendingChannel(pendingChanID PendingChanID,
peer lnpeer.Peer) bool {
peerIDKey := newSerializedKey(peer.IdentityKey())

View file

@ -28,6 +28,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb/models"
"github.com/lightningnetwork/lnd/channelnotifier"
"github.com/lightningnetwork/lnd/discovery"
"github.com/lightningnetwork/lnd/fn"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lncfg"
@ -161,7 +162,7 @@ func (m *mockAliasMgr) GetPeerAlias(lnwire.ChannelID) (lnwire.ShortChannelID,
}
func (m *mockAliasMgr) AddLocalAlias(lnwire.ShortChannelID,
lnwire.ShortChannelID, bool) error {
lnwire.ShortChannelID, bool, bool) error {
return nil
}
@ -563,6 +564,12 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
IsSweeperOutpoint: func(wire.OutPoint) bool {
return false
},
AuxLeafStore: fn.Some[lnwallet.AuxLeafStore](
&lnwallet.MockAuxLeafStore{},
),
AuxSigner: fn.Some[lnwallet.AuxSigner](
lnwallet.NewAuxSignerMock(lnwallet.EmptyMockJobHandler),
),
}
for _, op := range options {
@ -672,6 +679,8 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) {
OpenChannelPredicate: chainedAcceptor,
DeleteAliasEdge: oldCfg.DeleteAliasEdge,
AliasManager: oldCfg.AliasManager,
AuxLeafStore: oldCfg.AuxLeafStore,
AuxSigner: oldCfg.AuxSigner,
})
require.NoError(t, err, "failed recreating aliceFundingManager")
@ -4644,8 +4653,8 @@ func testZeroConf(t *testing.T, chanType *lnwire.ChannelType) {
// opening behavior with a specified fundmax flag. To give a hypothetical
// example, if ANCHOR types had been introduced after the fundmax flag had been
// activated, the developer would have had to code for the anchor reserve in the
// funding manager in the context of public and private channels. Otherwise
// inconsistent bahvior would have resulted when specifying fundmax for
// funding manager in the context of public and private channels. Otherwise,
// inconsistent behavior would have resulted when specifying fundmax for
// different types of channel openings.
// To ensure consistency this test compares a map of locally defined channel
// commitment types to the list of channel types that are defined in the proto
@ -4661,6 +4670,7 @@ func TestCommitmentTypeFundmaxSanityCheck(t *testing.T) {
"ANCHORS": 3,
"SCRIPT_ENFORCED_LEASE": 4,
"SIMPLE_TAPROOT": 5,
"SIMPLE_TAPROOT_OVERLAY": 6,
}
for commitmentType := range lnrpc.CommitmentType_value {

Some files were not shown because too many files have changed in this diff Show more