Merge pull request #5955 from Crypt-iQ/zero-conf-alias-patch

multi: implement option_scid_alias, zero-conf w/ channel_type
This commit is contained in:
Olaoluwa Osuntokun 2022-07-07 17:02:56 -07:00 committed by GitHub
commit d9c79d874e
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
83 changed files with 9662 additions and 3169 deletions

456
aliasmgr/aliasmgr.go Normal file
View File

@ -0,0 +1,456 @@
package aliasmgr
import (
"encoding/binary"
"fmt"
"sync"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnwire"
)
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
// are alias SCIDs and the values are their respective base SCIDs. This
// is used instead of the other way around (base -> alias...) because
// updating an alias would require fetching all the existing aliases,
// adding another one, and then flushing the write to disk. This is
// inefficient compared to N 1:1 mappings at the cost of marginally
// more disk space.
aliasBucket = []byte("alias-bucket")
// confirmedBucket stores whether or not a given base SCID should no
// longer have entries in the ToBase maps. The key is the SCID that is
// confirmed with 6 confirmations and is public, and the value is
// empty.
confirmedBucket = []byte("base-bucket")
// aliasAllocBucket is a root-level bucket that stores the last alias
// that was allocated. It is used to allocate a new alias when
// requested.
aliasAllocBucket = []byte("alias-alloc-bucket")
// lastAliasKey is a key in the aliasAllocBucket whose value is the
// last allocated alias ShortChannelID. This will be updated upon calls
// to RequestAlias.
lastAliasKey = []byte("last-alias-key")
// invoiceAliasBucket is a root-level bucket that stores the alias
// SCIDs that our peers send us in the funding_locked TLV. The keys are
// the ChannelID generated from the FundingOutpoint and the values are
// the remote peer's alias SCID.
invoiceAliasBucket = []byte("invoice-alias-bucket")
// byteOrder denotes the byte order of database (de)-serialization
// operations.
byteOrder = binary.BigEndian
// startBlockHeight is the starting block height of the alias range.
startingBlockHeight = 16_000_000
// endBlockHeight is the ending block height of the alias range.
endBlockHeight = 16_250_000
// startingAlias is the first alias ShortChannelID that will get
// assigned by RequestAlias. The starting BlockHeight is chosen so that
// legitimate SCIDs in integration tests aren't mistaken for an alias.
startingAlias = lnwire.ShortChannelID{
BlockHeight: uint32(startingBlockHeight),
TxIndex: 0,
TxPosition: 0,
}
// errNoBase is returned when a base SCID isn't found.
errNoBase = fmt.Errorf("no base found")
// errNoPeerAlias is returned when the peer's alias for a given
// channel is not found.
errNoPeerAlias = fmt.Errorf("no peer alias found")
)
// Manager is a struct that handles aliases for LND. It has an underlying
// database that can allocate aliases for channels, stores the peer's last
// alias for use in our hop hints, and contains mappings that both the Switch
// and Gossiper use.
type Manager struct {
backend kvdb.Backend
// 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
// aliasToBase is a mapping that maps all aliases for a given channel
// to its base SCID. This is only used for channels that have
// negotiated option-scid-alias feature bit.
aliasToBase map[lnwire.ShortChannelID]lnwire.ShortChannelID
sync.RWMutex
}
// 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,
)
m.aliasToBase = make(
map[lnwire.ShortChannelID]lnwire.ShortChannelID,
)
err := m.populateMaps()
return m, err
}
// populateMaps reads the database state and populates the maps.
func (m *Manager) populateMaps() error {
// This map tracks the base SCIDs that are confirmed and don't need to
// have entries in the *ToBase mappings as they won't be used in the
// gossiper.
baseConfMap := make(map[lnwire.ShortChannelID]struct{})
// This map caches what is found in the database and is used to
// populate the Manager's actual maps.
aliasMap := make(map[lnwire.ShortChannelID]lnwire.ShortChannelID)
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
baseConfBucket, err := tx.CreateTopLevelBucket(confirmedBucket)
if err != nil {
return err
}
err = baseConfBucket.ForEach(func(k, v []byte) error {
// The key will the base SCID and the value will be
// empty. Existence in the bucket means the SCID is
// confirmed.
baseScid := lnwire.NewShortChanIDFromInt(
byteOrder.Uint64(k),
)
baseConfMap[baseScid] = struct{}{}
return nil
})
if err != nil {
return err
}
aliasToBaseBucket, err := tx.CreateTopLevelBucket(aliasBucket)
if err != nil {
return err
}
err = aliasToBaseBucket.ForEach(func(k, v []byte) error {
// The key will be the alias SCID and the value will be
// the base SCID.
aliasScid := lnwire.NewShortChanIDFromInt(
byteOrder.Uint64(k),
)
baseScid := lnwire.NewShortChanIDFromInt(
byteOrder.Uint64(v),
)
aliasMap[aliasScid] = baseScid
return nil
})
return err
}, func() {
baseConfMap = make(map[lnwire.ShortChannelID]struct{})
aliasMap = make(
map[lnwire.ShortChannelID]lnwire.ShortChannelID,
)
})
if err != nil {
return err
}
// Populate the baseToSet map regardless if the baseSCID is marked as
// public with 6 confirmations.
for aliasSCID, baseSCID := range aliasMap {
m.baseToSet[baseSCID] = append(m.baseToSet[baseSCID], aliasSCID)
// Skip if baseSCID is in the baseConfMap.
if _, ok := baseConfMap[baseSCID]; ok {
continue
}
m.aliasToBase[aliasSCID] = baseSCID
}
return nil
}
// 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.
func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID,
gossip bool) error {
m.Lock()
defer m.Unlock()
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
// If the caller does not want to allow the alias to be used
// for a channel update, we'll mark it in the baseConfBucket.
if !gossip {
var baseGossipBytes [8]byte
byteOrder.PutUint64(
baseGossipBytes[:], baseScid.ToUint64(),
)
confBucket, err := tx.CreateTopLevelBucket(
confirmedBucket,
)
if err != nil {
return err
}
err = confBucket.Put(baseGossipBytes[:], []byte{})
if err != nil {
return err
}
}
aliasToBaseBucket, err := tx.CreateTopLevelBucket(aliasBucket)
if err != nil {
return err
}
var (
aliasBytes [8]byte
baseBytes [8]byte
)
byteOrder.PutUint64(aliasBytes[:], alias.ToUint64())
byteOrder.PutUint64(baseBytes[:], baseScid.ToUint64())
return aliasToBaseBucket.Put(aliasBytes[:], baseBytes[:])
}, func() {})
if err != nil {
return err
}
// Update the aliasToBase and baseToSet maps.
m.baseToSet[baseScid] = append(m.baseToSet[baseScid], alias)
// Only store the gossiper map if gossip is true.
if gossip {
m.aliasToBase[alias] = baseScid
}
return nil
}
// GetAliases fetches the set of aliases stored under a given base SCID from
// write-through caches.
func (m *Manager) GetAliases(base lnwire.ShortChannelID) []lnwire.ShortChannelID {
m.RLock()
defer m.RUnlock()
aliasSet, ok := m.baseToSet[base]
if ok {
// Copy the found alias slice.
setCopy := make([]lnwire.ShortChannelID, len(aliasSet))
copy(setCopy, aliasSet)
return setCopy
}
return nil
}
// FindBaseSCID finds the base SCID for a given alias. This is used in the
// gossiper to find the correct SCID to lookup in the graph database.
func (m *Manager) FindBaseSCID(
alias lnwire.ShortChannelID) (lnwire.ShortChannelID, error) {
m.RLock()
defer m.RUnlock()
base, ok := m.aliasToBase[alias]
if ok {
return base, nil
}
return lnwire.ShortChannelID{}, errNoBase
}
// DeleteSixConfs removes a mapping for the gossiper once six confirmations
// have been reached and the channel is public. At this point, only the
// confirmed SCID should be used.
func (m *Manager) DeleteSixConfs(baseScid lnwire.ShortChannelID) error {
m.Lock()
defer m.Unlock()
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
baseConfBucket, err := tx.CreateTopLevelBucket(confirmedBucket)
if err != nil {
return err
}
var baseBytes [8]byte
byteOrder.PutUint64(baseBytes[:], baseScid.ToUint64())
return baseConfBucket.Put(baseBytes[:], []byte{})
}, func() {})
if err != nil {
return err
}
// Now that the database state has been updated, we'll delete all of
// the aliasToBase mappings for this SCID.
for alias, base := range m.aliasToBase {
if base.ToUint64() == baseScid.ToUint64() {
delete(m.aliasToBase, alias)
}
}
return nil
}
// PutPeerAlias stores the peer's alias SCID once we learn of it in the
// funding_locked message.
func (m *Manager) PutPeerAlias(chanID lnwire.ChannelID,
alias lnwire.ShortChannelID) error {
return kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
bucket, err := tx.CreateTopLevelBucket(invoiceAliasBucket)
if err != nil {
return err
}
var scratch [8]byte
byteOrder.PutUint64(scratch[:], alias.ToUint64())
return bucket.Put(chanID[:], scratch[:])
}, func() {})
}
// GetPeerAlias retrieves a peer's alias SCID by the channel's ChanID.
func (m *Manager) GetPeerAlias(chanID lnwire.ChannelID) (
lnwire.ShortChannelID, error) {
var alias lnwire.ShortChannelID
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
bucket, err := tx.CreateTopLevelBucket(invoiceAliasBucket)
if err != nil {
return err
}
aliasBytes := bucket.Get(chanID[:])
if aliasBytes == nil {
return nil
}
alias = lnwire.NewShortChanIDFromInt(
byteOrder.Uint64(aliasBytes),
)
return nil
}, func() {})
if alias == hop.Source {
return alias, errNoPeerAlias
}
return alias, err
}
// RequestAlias returns a new ALIAS ShortChannelID to the caller by allocating
// the next un-allocated ShortChannelID. The starting ShortChannelID is
// 16000000:0:0 and the ending ShortChannelID is 16250000:16777215:65535. This
// gives roughly 2^58 possible ALIAS ShortChannelIDs which ensures this space
// won't get exhausted.
func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
var nextAlias lnwire.ShortChannelID
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
bucket, err := tx.CreateTopLevelBucket(aliasAllocBucket)
if err != nil {
return err
}
lastBytes := bucket.Get(lastAliasKey)
if lastBytes == nil {
// If the key does not exist, then we can write the
// startingAlias to it.
nextAlias = startingAlias
var scratch [8]byte
byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())
return bucket.Put(lastAliasKey, scratch[:])
}
// Otherwise the key does exist so we can convert the retrieved
// lastAlias to a ShortChannelID and use it to assign the next
// ShortChannelID. This next ShortChannelID will then be
// persisted in the database.
lastScid := lnwire.NewShortChanIDFromInt(
byteOrder.Uint64(lastBytes),
)
nextAlias = getNextScid(lastScid)
var scratch [8]byte
byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())
return bucket.Put(lastAliasKey, scratch[:])
}, func() {
nextAlias = lnwire.ShortChannelID{}
})
if err != nil {
return nextAlias, err
}
return nextAlias, nil
}
// getNextScid is a utility function that returns the next SCID for a given
// alias SCID. The BlockHeight ranges from [16000000, 16250000], the TxIndex
// ranges from [1, 16777215], and the TxPosition ranges from [1, 65535].
func getNextScid(last lnwire.ShortChannelID) lnwire.ShortChannelID {
var (
next lnwire.ShortChannelID
incrementIdx bool
incrementHeight bool
)
// If the TxPosition is 65535, then it goes to 0 and we need to
// increment the TxIndex.
if last.TxPosition == 65535 {
incrementIdx = true
}
// If the TxIndex is 16777215 and we need to increment it, then it goes
// to 0 and we need to increment the BlockHeight.
if last.TxIndex == 16777215 && incrementIdx {
incrementIdx = false
incrementHeight = true
}
switch {
// If we increment the TxIndex, then TxPosition goes to 0.
case incrementIdx:
next.BlockHeight = last.BlockHeight
next.TxIndex = last.TxIndex + 1
next.TxPosition = 0
// If we increment the BlockHeight, then the Tx fields go to 0.
case incrementHeight:
next.BlockHeight = last.BlockHeight + 1
next.TxIndex = 0
next.TxPosition = 0
// Otherwise, we only need to increment the TxPosition.
default:
next.BlockHeight = last.BlockHeight
next.TxIndex = last.TxIndex
next.TxPosition = last.TxPosition + 1
}
return next
}
// 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
// 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)
}

146
aliasmgr/aliasmgr_test.go Normal file
View File

@ -0,0 +1,146 @@
package aliasmgr
import (
"io/ioutil"
"math/rand"
"os"
"path/filepath"
"testing"
"github.com/lightningnetwork/lnd/kvdb"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/stretchr/testify/require"
)
// TestAliasStorePeerAlias tests that putting and retrieving a peer's alias
// works properly.
func TestAliasStorePeerAlias(t *testing.T) {
t.Parallel()
// Create the backend database and use this to create the aliasStore.
dbDir, err := ioutil.TempDir("", "aliasStore")
require.NoError(t, err)
defer os.RemoveAll(dbDir)
dbPath := filepath.Join(dbDir, "testdb")
db, err := kvdb.Create(
kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
)
require.NoError(t, err)
defer db.Close()
aliasStore, err := NewManager(db)
require.NoError(t, err)
var chanID1 [32]byte
_, err = rand.Read(chanID1[:])
require.NoError(t, err)
// Test that we can put the (chanID, alias) mapping in the database.
// Also check that we retrieve exactly what we put in.
err = aliasStore.PutPeerAlias(chanID1, startingAlias)
require.NoError(t, err)
storedAlias, err := aliasStore.GetPeerAlias(chanID1)
require.NoError(t, err)
require.Equal(t, startingAlias, storedAlias)
}
// TestAliasStoreRequest tests that the aliasStore delivers the expected SCID.
func TestAliasStoreRequest(t *testing.T) {
t.Parallel()
// Create the backend database and use this to create the aliasStore.
dbDir, err := ioutil.TempDir("", "aliasStore")
require.NoError(t, err)
defer os.RemoveAll(dbDir)
dbPath := filepath.Join(dbDir, "testdb")
db, err := kvdb.Create(
kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
)
require.NoError(t, err)
defer db.Close()
aliasStore, err := NewManager(db)
require.NoError(t, err)
// We'll assert that the very first alias we receive is startingAlias.
alias1, err := aliasStore.RequestAlias()
require.NoError(t, err)
require.Equal(t, startingAlias, alias1)
// The next alias should be the result of passing in startingAlias to
// getNextScid.
nextAlias := getNextScid(alias1)
alias2, err := aliasStore.RequestAlias()
require.NoError(t, err)
require.Equal(t, nextAlias, alias2)
}
// TestGetNextScid tests that given a current lnwire.ShortChannelID,
// getNextScid returns the expected alias to use next.
func TestGetNextScid(t *testing.T) {
tests := []struct {
name string
current lnwire.ShortChannelID
expected lnwire.ShortChannelID
}{
{
name: "starting alias",
current: startingAlias,
expected: lnwire.ShortChannelID{
BlockHeight: uint32(startingBlockHeight),
TxIndex: 0,
TxPosition: 1,
},
},
{
name: "txposition rollover",
current: lnwire.ShortChannelID{
BlockHeight: 16_100_000,
TxIndex: 15,
TxPosition: 65535,
},
expected: lnwire.ShortChannelID{
BlockHeight: 16_100_000,
TxIndex: 16,
TxPosition: 0,
},
},
{
name: "txindex max no rollover",
current: lnwire.ShortChannelID{
BlockHeight: 16_100_000,
TxIndex: 16777215,
TxPosition: 15,
},
expected: lnwire.ShortChannelID{
BlockHeight: 16_100_000,
TxIndex: 16777215,
TxPosition: 16,
},
},
{
name: "txindex rollover",
current: lnwire.ShortChannelID{
BlockHeight: 16_100_000,
TxIndex: 16777215,
TxPosition: 65535,
},
expected: lnwire.ShortChannelID{
BlockHeight: 16_100_001,
TxIndex: 0,
TxPosition: 0,
},
},
}
for _, test := range tests {
test := test
t.Run(test.name, func(t *testing.T) {
nextScid := getNextScid(test.current)
require.Equal(t, test.expected, nextScid)
})
}
}

View File

@ -184,13 +184,15 @@ func TestMultipleAcceptClients(t *testing.T) {
queries = map[*lnwire.OpenChannel]*ChannelAcceptResponse{
chan1: NewChannelAcceptResponse(
true, nil, testUpfront, 1, 2, 3, 4, 5, 6,
false,
),
chan2: NewChannelAcceptResponse(
false, errChannelRejected, nil, 0, 0, 0,
0, 0, 0,
0, 0, 0, false,
),
chan3: NewChannelAcceptResponse(
false, customError, nil, 0, 0, 0, 0, 0, 0,
false,
),
}
@ -245,7 +247,7 @@ func TestInvalidResponse(t *testing.T) {
PendingChannelID: chan1,
}: NewChannelAcceptResponse(
false, errChannelRejected, nil, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0, false,
),
}
@ -288,7 +290,7 @@ func TestInvalidReserve(t *testing.T) {
DustLimit: dustLimit,
}: NewChannelAcceptResponse(
false, errChannelRejected, nil, 0, 0,
0, reserve, 0, 0,
0, reserve, 0, 0, false,
),
}

View File

@ -80,7 +80,7 @@ func (c *ChainedAcceptor) Accept(req *ChannelAcceptRequest) *ChannelAcceptRespon
return NewChannelAcceptResponse(
false, errChannelRejected, nil, 0, 0,
0, 0, 0, 0,
0, 0, 0, 0, false,
)
}
}

View File

@ -62,6 +62,10 @@ type ChannelAcceptResponse struct {
// MinAcceptDepth is the minimum depth that the initiator of the
// channel should wait before considering the channel open.
MinAcceptDepth uint16
// ZeroConf indicates that the fundee wishes to send min_depth = 0 and
// request a zero-conf channel with the counter-party.
ZeroConf bool
}
// NewChannelAcceptResponse is a constructor for a channel accept response,
@ -72,7 +76,7 @@ type ChannelAcceptResponse struct {
func NewChannelAcceptResponse(accept bool, acceptErr error,
upfrontShutdown lnwire.DeliveryAddress, csvDelay, htlcLimit,
minDepth uint16, reserve btcutil.Amount, inFlight,
minHtlcIn lnwire.MilliSatoshi) *ChannelAcceptResponse {
minHtlcIn lnwire.MilliSatoshi, zeroConf bool) *ChannelAcceptResponse {
resp := &ChannelAcceptResponse{
UpfrontShutdown: upfrontShutdown,
@ -82,6 +86,7 @@ func NewChannelAcceptResponse(accept bool, acceptErr error,
HtlcLimit: htlcLimit,
MinHtlcIn: minHtlcIn,
MinAcceptDepth: minDepth,
ZeroConf: zeroConf,
}
// If we want to accept the channel, we return a response with a nil

View File

@ -20,6 +20,10 @@ const (
fieldUpfrontShutdown = "upfront shutdown"
)
var (
errZeroConf = fmt.Errorf("zero-conf set with non-zero min-depth")
)
// fieldMismatchError returns a merge error for a named field when we get two
// channel acceptor responses which have different values set.
func fieldMismatchError(name string, current, newValue interface{}) error {
@ -27,6 +31,13 @@ func fieldMismatchError(name string, current, newValue interface{}) error {
name, current, newValue)
}
// mergeBool merges two boolean values.
func mergeBool(current, newValue bool) bool {
// If either is true, return true. It is not possible to have different
// "non-zero" values like the other cases.
return current || newValue
}
// mergeInt64 merges two int64 values, failing if they have different non-zero
// values.
func mergeInt64(name string, current, newValue int64) (int64, error) {
@ -117,6 +128,13 @@ func mergeResponse(current,
}
current.MinAcceptDepth = uint16(minDepth)
current.ZeroConf = mergeBool(current.ZeroConf, newValue.ZeroConf)
// Assert that if zero-conf is set, min-depth is zero.
if current.ZeroConf && current.MinAcceptDepth != 0 {
return current, errZeroConf
}
reserve, err := mergeInt64(
fieldReserve, int64(current.Reserve), int64(newValue.Reserve),
)

View File

@ -167,6 +167,18 @@ func TestMergeResponse(t *testing.T) {
},
err: nil,
},
{
// Test the case where one response has ZeroConf set
// and another has a non-zero min depth set.
name: "zero conf conflict",
current: ChannelAcceptResponse{
ZeroConf: true,
},
new: ChannelAcceptResponse{
MinAcceptDepth: 5,
},
err: errZeroConf,
},
}
for _, test := range tests {

View File

@ -107,7 +107,7 @@ func (r *RPCAcceptor) Accept(req *ChannelAcceptRequest) *ChannelAcceptResponse {
// Create a rejection response which we can use for the cases where we
// reject the channel.
rejectChannel := NewChannelAcceptResponse(
false, errChannelRejected, nil, 0, 0, 0, 0, 0, 0,
false, errChannelRejected, nil, 0, 0, 0, 0, 0, 0, false,
)
// Send the request to the newRequests channel.
@ -216,6 +216,7 @@ func (r *RPCAcceptor) receiveResponses(errChan chan error,
MaxHtlcCount: resp.MaxHtlcCount,
MinHtlcIn: resp.MinHtlcIn,
MinAcceptDepth: resp.MinAcceptDepth,
ZeroConf: resp.ZeroConf,
}
// We have received a decision for one of our channel
@ -348,6 +349,7 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
btcutil.Amount(resp.ReserveSat),
lnwire.MilliSatoshi(resp.InFlightMaxMsat),
lnwire.MilliSatoshi(resp.MinHtlcIn),
resp.ZeroConf,
)
// Delete the channel from the acceptRequests map.

View File

@ -177,7 +177,25 @@ func NewSingle(channel *channeldb.OpenChannel,
// to the channel ID so we can use that as height hint on restore.
chanID := channel.ShortChanID()
if chanID.BlockHeight == 0 {
chanID.BlockHeight = channel.FundingBroadcastHeight
chanID.BlockHeight = channel.BroadcastHeight()
}
// If this is a zero-conf channel, we'll need to have separate logic
// depending on whether it's confirmed or not. This is because the
// ShortChanID is an alias.
if channel.IsZeroConf() {
// If the channel is confirmed, we'll use the confirmed SCID.
if channel.ZeroConfConfirmed() {
chanID = channel.ZeroConfRealScid()
} else {
// Else if the zero-conf channel is unconfirmed, we'll
// need to use the broadcast height and zero out the
// TxIndex and TxPosition fields. This is so
// openChannelShell works properly.
chanID.BlockHeight = channel.BroadcastHeight()
chanID.TxIndex = 0
chanID.TxPosition = 0
}
}
single := Single{

View File

@ -16,6 +16,7 @@ import (
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/htlcswitch/hop"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/kvdb"
@ -53,6 +54,14 @@ var (
//
outpointBucket = []byte("outpoint-bucket")
// chanIDBucket stores all of the 32-byte channel ID's we know about.
// These could be derived from outpointBucket, but it is more
// convenient to have these in their own bucket.
//
// chanID -> tlv stream.
//
chanIDBucket = []byte("chan-id-bucket")
// historicalChannelBucket stores all channels that have seen their
// commitment tx confirm. All information from their previous open state
// is retained.
@ -190,6 +199,10 @@ const (
// 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
)
// indexStatus is an enum-like type that describes what state the
@ -211,7 +224,7 @@ const (
// fee negotiation, channel closing, the format of HTLCs, etc. Structure-wise,
// a ChannelType is a bit field, with each bit denoting a modification from the
// base channel type of single funder.
type ChannelType uint8
type ChannelType uint64
const (
// NOTE: iota isn't used here for this enum needs to be stable
@ -254,6 +267,17 @@ const (
// period of time, constraining every output that pays to the channel
// initiator with an additional CLTV of the lease maturity.
LeaseExpirationBit ChannelType = 1 << 6
// ZeroConfBit indicates that the channel is a zero-conf channel.
ZeroConfBit ChannelType = 1 << 7
// ScidAliasChanBit indicates that the channel has negotiated the
// scid-alias channel type.
ScidAliasChanBit ChannelType = 1 << 8
// ScidAliasFeatureBit indicates that the scid-alias feature bit was
// negotiated during the lifetime of this channel.
ScidAliasFeatureBit ChannelType = 1 << 9
)
// IsSingleFunder returns true if the channel type if one of the known single
@ -303,6 +327,22 @@ func (c ChannelType) HasLeaseExpiration() bool {
return c&LeaseExpirationBit == LeaseExpirationBit
}
// HasZeroConf returns true if the channel is a zero-conf channel.
func (c ChannelType) HasZeroConf() bool {
return c&ZeroConfBit == ZeroConfBit
}
// HasScidAliasChan returns true if the scid-alias channel type was negotiated.
func (c ChannelType) HasScidAliasChan() bool {
return c&ScidAliasChanBit == ScidAliasChanBit
}
// HasScidAliasFeature returns true if the scid-alias feature bit was
// negotiated during the lifetime of this channel.
func (c ChannelType) HasScidAliasFeature() bool {
return c&ScidAliasFeatureBit == ScidAliasFeatureBit
}
// ChannelConstraints represents a set of constraints meant to allow a node to
// limit their exposure, enact flow control and ensure that all HTLCs are
// economically relevant. This struct will be mirrored for both sides of the
@ -476,7 +516,7 @@ type ChannelCommitment struct {
// ChannelStatus is a bit vector used to indicate whether an OpenChannel is in
// the default usable state, or a state where it shouldn't be used.
type ChannelStatus uint8
type ChannelStatus uint64
var (
// ChanStatusDefault is the normal state of an open channel.
@ -604,6 +644,9 @@ type OpenChannel struct {
// ShortChannelID encodes the exact location in the chain in which the
// channel was initially confirmed. This includes: the block height,
// transaction index, and the output within the target transaction.
//
// If IsZeroConf(), then this will the "base" (very first) ALIAS scid
// and the confirmed SCID will be stored in ConfirmedScid.
ShortChannelID lnwire.ShortChannelID
// IsPending indicates whether a channel's funding transaction has been
@ -739,6 +782,11 @@ type OpenChannel struct {
// have private key isolation from lnd.
RevocationKeyLocator keychain.KeyLocator
// confirmedScid is the confirmed ShortChannelID for a zero-conf
// channel. If the channel is unconfirmed, then this will be the
// default ShortChannelID. This is only set for zero-conf channels.
confirmedScid lnwire.ShortChannelID
// TODO(roasbeef): eww
Db *ChannelStateDB
@ -755,6 +803,50 @@ func (c *OpenChannel) ShortChanID() lnwire.ShortChannelID {
return c.ShortChannelID
}
// ZeroConfRealScid returns the zero-conf channel's confirmed scid. This should
// only be called if IsZeroConf returns true.
func (c *OpenChannel) ZeroConfRealScid() lnwire.ShortChannelID {
c.RLock()
defer c.RUnlock()
return c.confirmedScid
}
// ZeroConfConfirmed returns whether the zero-conf channel has confirmed. This
// should only be called if IsZeroConf returns true.
func (c *OpenChannel) ZeroConfConfirmed() bool {
c.RLock()
defer c.RUnlock()
return c.confirmedScid != hop.Source
}
// IsZeroConf returns whether the option_zeroconf channel type was negotiated.
func (c *OpenChannel) IsZeroConf() bool {
c.RLock()
defer c.RUnlock()
return c.ChanType.HasZeroConf()
}
// IsOptionScidAlias returns whether the option_scid_alias channel type was
// negotiated.
func (c *OpenChannel) IsOptionScidAlias() bool {
c.RLock()
defer c.RUnlock()
return c.ChanType.HasScidAliasChan()
}
// NegotiatedAliasFeature returns whether the option-scid-alias feature bit was
// negotiated.
func (c *OpenChannel) NegotiatedAliasFeature() bool {
c.RLock()
defer c.RUnlock()
return c.ChanType.HasScidAliasFeature()
}
// ChanStatus returns the current ChannelStatus of this channel.
func (c *OpenChannel) ChanStatus() ChannelStatus {
c.RLock()
@ -801,13 +893,25 @@ func (c *OpenChannel) hasChanStatus(status ChannelStatus) bool {
return c.chanStatus&status == status
}
// RefreshShortChanID updates the in-memory channel state using the latest
// value observed on disk.
//
// TODO: the name of this function should be changed to reflect the fact that
// it is not only refreshing the short channel id but all the channel state.
// maybe Refresh/Reload?
func (c *OpenChannel) RefreshShortChanID() error {
// BroadcastHeight returns the height at which the funding tx was broadcast.
func (c *OpenChannel) BroadcastHeight() uint32 {
c.RLock()
defer c.RUnlock()
return c.FundingBroadcastHeight
}
// SetBroadcastHeight sets the FundingBroadcastHeight.
func (c *OpenChannel) SetBroadcastHeight(height uint32) {
c.Lock()
defer c.Unlock()
c.FundingBroadcastHeight = height
}
// Refresh updates the in-memory channel state using the latest state observed
// on disk.
func (c *OpenChannel) Refresh() error {
c.Lock()
defer c.Unlock()
@ -825,6 +929,19 @@ func (c *OpenChannel) RefreshShortChanID() error {
return fmt.Errorf("unable to fetch chan info: %v", err)
}
// Also populate the channel's commitment states for both sides
// of the channel.
if err := fetchChanCommitments(chanBucket, c); err != nil {
return fmt.Errorf("unable to fetch chan commitments: "+
"%v", err)
}
// Also retrieve the current revocation state.
if err := fetchChanRevocationState(chanBucket, c); err != nil {
return fmt.Errorf("unable to fetch chan revocations: "+
"%v", err)
}
return nil
}, func() {})
if err != nil {
@ -931,6 +1048,7 @@ func fetchChanBucketRw(tx kvdb.RwTx, nodeKey *btcec.PublicKey,
func (c *OpenChannel) fullSync(tx kvdb.RwTx) error {
// Fetch the outpoint bucket and check if the outpoint already exists.
opBucket := tx.ReadWriteBucket(outpointBucket)
cidBucket := tx.ReadWriteBucket(chanIDBucket)
var chanPointBuf bytes.Buffer
if err := writeOutpoint(&chanPointBuf, &c.FundingOutpoint); err != nil {
@ -942,6 +1060,11 @@ func (c *OpenChannel) fullSync(tx kvdb.RwTx) error {
return ErrChanAlreadyExists
}
cid := lnwire.NewChanIDFromOutPoint(&c.FundingOutpoint)
if cidBucket.Get(cid[:]) != nil {
return ErrChanAlreadyExists
}
status := uint8(outpointOpen)
// Write the status of this outpoint as the first entry in a tlv
@ -962,6 +1085,10 @@ func (c *OpenChannel) fullSync(tx kvdb.RwTx) error {
return err
}
if err := cidBucket.Put(cid[:], []byte{}); err != nil {
return err
}
// First fetch the top level bucket which stores all data related to
// current, active channels.
openChanBucket, err := tx.CreateTopLevelBucket(openChannelBucket)
@ -1035,6 +1162,71 @@ func (c *OpenChannel) MarkAsOpen(openLoc lnwire.ShortChannelID) error {
return nil
}
// MarkRealScid marks the zero-conf channel's confirmed ShortChannelID. This
// should only be done if IsZeroConf returns true.
func (c *OpenChannel) MarkRealScid(realScid lnwire.ShortChannelID) error {
c.Lock()
defer c.Unlock()
if err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error {
chanBucket, err := fetchChanBucketRw(
tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash,
)
if err != nil {
return err
}
channel, err := fetchOpenChannel(
chanBucket, &c.FundingOutpoint,
)
if err != nil {
return err
}
channel.confirmedScid = realScid
return putOpenChannel(chanBucket, channel)
}, func() {}); err != nil {
return err
}
c.confirmedScid = realScid
return nil
}
// MarkScidAliasNegotiated adds ScidAliasFeatureBit to ChanType in-memory and
// in the database.
func (c *OpenChannel) MarkScidAliasNegotiated() error {
c.Lock()
defer c.Unlock()
if err := kvdb.Update(c.Db.backend, func(tx kvdb.RwTx) error {
chanBucket, err := fetchChanBucketRw(
tx, c.IdentityPub, &c.FundingOutpoint, c.ChainHash,
)
if err != nil {
return err
}
channel, err := fetchOpenChannel(
chanBucket, &c.FundingOutpoint,
)
if err != nil {
return err
}
channel.ChanType |= ScidAliasFeatureBit
return putOpenChannel(chanBucket, channel)
}, func() {}); err != nil {
return err
}
c.ChanType |= ScidAliasFeatureBit
return nil
}
// MarkDataLoss marks sets the channel status to LocalDataLoss and stores the
// passed commitPoint for use to retrieve funds in case the remote force closes
// the channel.
@ -1101,6 +1293,22 @@ func (c *OpenChannel) MarkBorked() error {
return c.putChanStatus(ChanStatusBorked)
}
// SecondCommitmentPoint returns the second per-commitment-point for use in the
// funding_locked message.
func (c *OpenChannel) SecondCommitmentPoint() (*btcec.PublicKey, error) {
c.RLock()
defer c.RUnlock()
// Since we start at commitment height = 0, the second per commitment
// point is actually at the 1st index.
revocation, err := c.RevocationProducer.AtIndex(1)
if err != nil {
return nil, err
}
return input.ComputeCommitmentPoint(revocation[:]), nil
}
// ChanSyncMsg returns the ChannelReestablish message that should be sent upon
// reconnection with the remote peer that we're maintaining this channel with.
// The information contained within this message is necessary to re-sync our
@ -3095,7 +3303,24 @@ func (c *OpenChannel) AbsoluteThawHeight() (uint32, error) {
return 0, errors.New("cannot use relative thaw " +
"height for unconfirmed channel")
}
return c.ShortChannelID.BlockHeight + c.ThawHeight, nil
// For non-zero-conf channels, this is the base height to use.
blockHeightBase := c.ShortChannelID.BlockHeight
// If this is a zero-conf channel, the ShortChannelID will be
// an alias.
if c.IsZeroConf() {
if !c.ZeroConfConfirmed() {
return 0, errors.New("cannot use relative " +
"height for unconfirmed zero-conf " +
"channel")
}
// Use the confirmed SCID's BlockHeight.
blockHeightBase = c.confirmedScid.BlockHeight
}
return blockHeightBase + c.ThawHeight, nil
}
return c.ThawHeight, nil
@ -3325,6 +3550,7 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error {
tlv.MakePrimitiveRecord(
initialRemoteBalanceType, &remoteBalance,
),
MakeScidRecord(realScidType, &channel.confirmedScid),
)
if err != nil {
return err
@ -3541,6 +3767,7 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error {
tlv.MakePrimitiveRecord(
initialRemoteBalanceType, &remoteBalance,
),
MakeScidRecord(realScidType, &channel.confirmedScid),
)
if err != nil {
return err
@ -3744,3 +3971,12 @@ func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
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,
)
}

View File

@ -1148,10 +1148,10 @@ func TestFetchWaitingCloseChannels(t *testing.T) {
}
}
// TestRefreshShortChanID asserts that RefreshShortChanID updates the in-memory
// state of another OpenChannel to reflect a preceding call to MarkOpen on a
// different OpenChannel.
func TestRefreshShortChanID(t *testing.T) {
// TestRefresh asserts that Refresh updates the in-memory state of another
// OpenChannel to reflect a preceding call to MarkOpen on a different
// OpenChannel.
func TestRefresh(t *testing.T) {
t.Parallel()
fullDB, cleanUp, err := MakeTestDB()
@ -1209,8 +1209,8 @@ func TestRefreshShortChanID(t *testing.T) {
state.Packager.(*ChannelPackager).source)
}
// Now, refresh the short channel ID of the pending channel.
err = pendingChannel.RefreshShortChanID()
// Now, refresh the state of the pending channel.
err = pendingChannel.Refresh()
require.NoError(t, err, "unable to refresh short_chan_id")
// This should result in both OpenChannel's now having the same

View File

@ -14,6 +14,7 @@ import (
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/shachain"
"github.com/lightningnetwork/lnd/tlv"
)
// writeOutpoint writes an outpoint to the passed writer using the minimal
@ -85,7 +86,8 @@ func WriteElement(w io.Writer, element interface{}) error {
return binary.Write(w, byteOrder, false)
case ChannelType:
if err := binary.Write(w, byteOrder, e); err != nil {
var buf [8]byte
if err := tlv.WriteVarInt(w, uint64(e), &buf); err != nil {
return err
}
@ -194,7 +196,8 @@ func WriteElement(w io.Writer, element interface{}) error {
}
case ChannelStatus:
if err := binary.Write(w, byteOrder, e); err != nil {
var buf [8]byte
if err := tlv.WriteVarInt(w, uint64(e), &buf); err != nil {
return err
}
@ -270,10 +273,14 @@ func ReadElement(r io.Reader, element interface{}) error {
}
case *ChannelType:
if err := binary.Read(r, byteOrder, e); err != nil {
var buf [8]byte
ctype, err := tlv.ReadVarInt(r, &buf)
if err != nil {
return err
}
*e = ChannelType(ctype)
case *chainhash.Hash:
if _, err := io.ReadFull(r, e[:]); err != nil {
return err
@ -419,10 +426,14 @@ func ReadElement(r io.Reader, element interface{}) error {
*e = msg
case *ChannelStatus:
if err := binary.Read(r, byteOrder, e); err != nil {
var buf [8]byte
status, err := tlv.ReadVarInt(r, &buf)
if err != nil {
return err
}
*e = ChannelStatus(status)
case *ClosureType:
if err := binary.Read(r, byteOrder, e); err != nil {
return err

View File

@ -22,6 +22,7 @@ import (
"github.com/lightningnetwork/lnd/channeldb/migration25"
"github.com/lightningnetwork/lnd/channeldb/migration26"
"github.com/lightningnetwork/lnd/channeldb/migration27"
"github.com/lightningnetwork/lnd/channeldb/migration29"
"github.com/lightningnetwork/lnd/channeldb/migration_01_to_11"
"github.com/lightningnetwork/lnd/clock"
"github.com/lightningnetwork/lnd/kvdb"
@ -226,6 +227,14 @@ var (
number: 27,
migration: migration27.MigrateHistoricalBalances,
},
{
number: 28,
migration: mig.CreateTLB(chanIDBucket),
},
{
number: 29,
migration: migration29.MigrateChanID,
},
}
// Big endian is the preferred byte order, due to cursor scans over
@ -352,6 +361,7 @@ var dbTopLevelBuckets = [][]byte{
metaBucket,
closeSummaryBucket,
outpointBucket,
chanIDBucket,
historicalChannelBucket,
}

View File

@ -1692,8 +1692,9 @@ func (c *ChannelGraph) PruneTip() (*chainhash.Hash, uint32, error) {
// database, then ErrEdgeNotFound will be returned. If strictZombiePruning is
// true, then when we mark these edges as zombies, we'll set up the keys such
// that we require the node that failed to send the fresh update to be the one
// that resurrects the channel from its zombie state.
func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning bool,
// that resurrects the channel from its zombie state. The markZombie bool
// denotes whether or not to mark the channel as a zombie.
func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning, markZombie bool,
chanIDs ...uint64) error {
// TODO(roasbeef): possibly delete from node bucket if node has no more
@ -1730,7 +1731,7 @@ func (c *ChannelGraph) DeleteChannelEdges(strictZombiePruning bool,
byteOrder.PutUint64(rawChanID[:], chanID)
err := c.delChannelEdge(
edges, edgeIndex, chanIndex, zombieIndex, nodes,
rawChanID[:], true, strictZombiePruning,
rawChanID[:], markZombie, strictZombiePruning,
)
if err != nil {
return err
@ -2167,8 +2168,18 @@ func (c *ChannelGraph) FilterChannelRange(startHeight,
// We'll now iterate through the database, and find each
// channel ID that resides within the specified range.
for k, _ := cursor.Seek(chanIDStart[:]); k != nil &&
bytes.Compare(k, chanIDEnd[:]) <= 0; k, _ = cursor.Next() {
for k, v := cursor.Seek(chanIDStart[:]); k != nil &&
bytes.Compare(k, chanIDEnd[:]) <= 0; k, v = cursor.Next() {
// Don't send alias SCIDs during gossip sync.
edgeReader := bytes.NewReader(v)
edgeInfo, err := deserializeChanEdgeInfo(edgeReader)
if err != nil {
return err
}
if edgeInfo.AuthProof == nil {
continue
}
// This channel ID rests within the target range, so
// we'll add it to our returned set.

View File

@ -378,7 +378,7 @@ func TestEdgeInsertionDeletion(t *testing.T) {
// Next, attempt to delete the edge from the database, again this
// should proceed without any issues.
if err := graph.DeleteChannelEdges(false, chanID); err != nil {
if err := graph.DeleteChannelEdges(false, true, chanID); err != nil {
t.Fatalf("unable to delete edge: %v", err)
}
assertNoEdge(t, graph, chanID)
@ -398,7 +398,7 @@ func TestEdgeInsertionDeletion(t *testing.T) {
// Finally, attempt to delete a (now) non-existent edge within the
// database, this should result in an error.
err = graph.DeleteChannelEdges(false, chanID)
err = graph.DeleteChannelEdges(false, true, chanID)
if err != ErrEdgeNotFound {
t.Fatalf("deleting a non-existent edge should fail!")
}
@ -1993,7 +1993,7 @@ func TestFilterKnownChanIDs(t *testing.T) {
if err := graph.AddChannelEdge(&channel); err != nil {
t.Fatalf("unable to create channel edge: %v", err)
}
err := graph.DeleteChannelEdges(false, channel.ChannelID)
err := graph.DeleteChannelEdges(false, true, channel.ChannelID)
if err != nil {
t.Fatalf("unable to mark edge zombie: %v", err)
}
@ -2251,7 +2251,7 @@ func TestFetchChanInfos(t *testing.T) {
if err := graph.AddChannelEdge(&zombieChan); err != nil {
t.Fatalf("unable to create channel edge: %v", err)
}
err = graph.DeleteChannelEdges(false, zombieChan.ChannelID)
err = graph.DeleteChannelEdges(false, true, zombieChan.ChannelID)
require.NoError(t, err, "unable to delete and mark edge zombie")
edgeQuery = append(edgeQuery, zombieChanID.ToUint64())
@ -2789,7 +2789,9 @@ func TestNodeIsPublic(t *testing.T) {
// graph. This will make Alice be seen as a private node as it no longer
// has any advertised edges.
for _, graph := range graphs {
err := graph.DeleteChannelEdges(false, aliceBobEdge.ChannelID)
err := graph.DeleteChannelEdges(
false, true, aliceBobEdge.ChannelID,
)
if err != nil {
t.Fatalf("unable to remove edge: %v", err)
}
@ -2806,7 +2808,9 @@ func TestNodeIsPublic(t *testing.T) {
// completely remove the edge as it is not possible for her to know of
// it without it being advertised.
for i, graph := range graphs {
err := graph.DeleteChannelEdges(false, bobCarolEdge.ChannelID)
err := graph.DeleteChannelEdges(
false, true, bobCarolEdge.ChannelID,
)
if err != nil {
t.Fatalf("unable to remove edge: %v", err)
}
@ -2900,7 +2904,9 @@ func TestDisabledChannelIDs(t *testing.T) {
}
// Delete the channel edge and ensure it is removed from the disabled list.
if err = graph.DeleteChannelEdges(false, edgeInfo.ChannelID); err != nil {
if err = graph.DeleteChannelEdges(
false, true, edgeInfo.ChannelID,
); err != nil {
t.Fatalf("unable to delete channel edge: %v", err)
}
disabledChanIds, err = graph.DisabledChannelIDs()
@ -3111,7 +3117,7 @@ func TestGraphZombieIndex(t *testing.T) {
// If we delete the edge and mark it as a zombie, then we should expect
// to see it within the index.
err = graph.DeleteChannelEdges(false, edge.ChannelID)
err = graph.DeleteChannelEdges(false, true, edge.ChannelID)
require.NoError(t, err, "unable to mark edge as zombie")
isZombie, pubKey1, pubKey2 := graph.IsZombieEdge(edge.ChannelID)
if !isZombie {

View File

@ -0,0 +1,66 @@
package migration29
import (
"encoding/binary"
"encoding/hex"
"io"
"github.com/btcsuite/btcd/wire"
)
var (
byteOrder = binary.BigEndian
)
// ChannelID is a series of 32-bytes that uniquely identifies all channels
// within the network. The ChannelID is computed using the outpoint of the
// funding transaction (the txid, and output index). Given a funding output the
// ChannelID can be calculated by XOR'ing the big-endian serialization of the
// txid and the big-endian serialization of the output index, truncated to
// 2 bytes.
type ChannelID [32]byte
// String returns the string representation of the ChannelID. This is just the
// hex string encoding of the ChannelID itself.
func (c ChannelID) String() string {
return hex.EncodeToString(c[:])
}
// NewChanIDFromOutPoint converts a target OutPoint into a ChannelID that is
// usable within the network. In order to convert the OutPoint into a ChannelID,
// we XOR the lower 2-bytes of the txid within the OutPoint with the big-endian
// serialization of the Index of the OutPoint, truncated to 2-bytes.
func NewChanIDFromOutPoint(op *wire.OutPoint) ChannelID {
// First we'll copy the txid of the outpoint into our channel ID slice.
var cid ChannelID
copy(cid[:], op.Hash[:])
// With the txid copied over, we'll now XOR the lower 2-bytes of the
// partial channelID with big-endian serialization of output index.
xorTxid(&cid, uint16(op.Index))
return cid
}
// xorTxid performs the transformation needed to transform an OutPoint into a
// ChannelID. To do this, we expect the cid parameter to contain the txid
// unaltered and the outputIndex to be the output index
func xorTxid(cid *ChannelID, outputIndex uint16) {
var buf [2]byte
binary.BigEndian.PutUint16(buf[:], outputIndex)
cid[30] ^= buf[0]
cid[31] ^= buf[1]
}
// readOutpoint reads an outpoint from the passed reader.
func readOutpoint(r io.Reader, o *wire.OutPoint) error {
if _, err := io.ReadFull(r, o.Hash[:]); err != nil {
return err
}
if err := binary.Read(r, byteOrder, &o.Index); err != nil {
return err
}
return nil
}

View File

@ -0,0 +1,12 @@
package migration29
import "github.com/btcsuite/btclog"
// log is a logger that is initialized as disabled. This means the package will
// not perform any logging by default until a logger is set.
var log = btclog.Disabled
// UseLogger uses a specific Logger to output package logging info.
func UseLogger(logger btclog.Logger) {
log = logger
}

View File

@ -0,0 +1,72 @@
package migration29
import (
"bytes"
"github.com/btcsuite/btcd/wire"
"github.com/lightningnetwork/lnd/kvdb"
)
var (
// outpointBucket is the bucket that stores the set of outpoints we
// know about.
outpointBucket = []byte("outpoint-bucket")
// chanIDBucket is the bucket that stores the set of ChannelID's we
// know about.
chanIDBucket = []byte("chan-id-bucket")
)
// MigrateChanID populates the ChannelID index by using the set of outpoints
// retrieved from the outpoint bucket.
func MigrateChanID(tx kvdb.RwTx) error {
log.Info("Populating ChannelID index")
// First we'll retrieve the set of outpoints we know about.
ops, err := fetchOutPoints(tx)
if err != nil {
return err
}
return populateChanIDIndex(tx, ops)
}
// fetchOutPoints loops through the outpointBucket and returns each stored
// outpoint.
func fetchOutPoints(tx kvdb.RwTx) ([]*wire.OutPoint, error) {
var ops []*wire.OutPoint
bucket := tx.ReadBucket(outpointBucket)
err := bucket.ForEach(func(k, _ []byte) error {
var op wire.OutPoint
r := bytes.NewReader(k)
if err := readOutpoint(r, &op); err != nil {
return err
}
ops = append(ops, &op)
return nil
})
if err != nil {
return nil, err
}
return ops, nil
}
// populateChanIDIndex uses the set of retrieved outpoints and populates the
// ChannelID index.
func populateChanIDIndex(tx kvdb.RwTx, ops []*wire.OutPoint) error {
bucket := tx.ReadWriteBucket(chanIDBucket)
for _, op := range ops {
chanID := NewChanIDFromOutPoint(op)
if err := bucket.Put(chanID[:], []byte{}); err != nil {
return err
}
}
return nil
}

View File

@ -0,0 +1,67 @@
package migration29
import (
"testing"
"github.com/lightningnetwork/lnd/channeldb/migtest"
"github.com/lightningnetwork/lnd/kvdb"
)
var (
hexStr = migtest.Hex
outpoint1 = hexStr("81b637d8fcd2c6da6859e6963113a1170de793e4b725b84d1e0b4cf99ec58ce90fb463ad")
outpoint2 = hexStr("81b637d8fcd2c6da6859e6963113a1170de793e4b725b84d1e0b4cf99ec58ce952d6c6c7")
chanID1 = hexStr("81b637d8fcd2c6da6859e6963113a1170de793e4b725b84d1e0b4cf99ec5ef44")
chanID2 = hexStr("81b637d8fcd2c6da6859e6963113a1170de793e4b725b84d1e0b4cf99ec54a2e")
// These tlv streams are used to populate the outpoint bucket at the
// start of the test.
tlvOutpointOpen = hexStr("000100")
tlvOutpointClosed = hexStr("000101")
// outpointData is used to populate the outpoint bucket.
outpointData = map[string]interface{}{
outpoint1: tlvOutpointOpen,
outpoint2: tlvOutpointClosed,
}
// chanIDBefore is the ChannelID bucket before the migration.
chanIDBefore = map[string]interface{}{}
// post is the expected data in the ChannelID bucket after the
// migration.
post = map[string]interface{}{
chanID1: "",
chanID2: "",
}
)
// TestMigrateChannelIDIndex asserts that the ChannelID index is properly
// populated.
func TestMigrateChannelIDIndex(t *testing.T) {
// Prime the database with the populated outpoint bucket. We create the
// ChannelID bucket since the prior migration creates it anyways.
before := func(tx kvdb.RwTx) error {
err := migtest.RestoreDB(tx, outpointBucket, outpointData)
if err != nil {
return err
}
return migtest.RestoreDB(tx, chanIDBucket, chanIDBefore)
}
// After the migration, ensure that the ChannelID bucket is properly
// populated.
after := func(tx kvdb.RwTx) error {
err := migtest.VerifyDB(tx, outpointBucket, outpointData)
if err != nil {
return err
}
return migtest.VerifyDB(tx, chanIDBucket, post)
}
migtest.ApplyMigration(t, before, after, MigrateChanID, false)
}

View File

@ -243,7 +243,7 @@ func (c *chanDBRestorer) RestoreChansFromSingles(backups ...chanbackup.Single) e
// funding broadcast height to a reasonable value that we
// determined earlier.
case channel.ShortChanID().BlockHeight == 0:
channel.FundingBroadcastHeight = firstChanHeight
channel.SetBroadcastHeight(firstChanHeight)
// Fallback case 2: It is extremely unlikely at this point that
// a channel we are trying to restore has a coinbase funding TX.
@ -255,7 +255,7 @@ func (c *chanDBRestorer) RestoreChansFromSingles(backups ...chanbackup.Single) e
// unconfirmed one here.
case channel.ShortChannelID.TxIndex == 0:
broadcastHeight := channel.ShortChannelID.BlockHeight
channel.FundingBroadcastHeight = broadcastHeight
channel.SetBroadcastHeight(broadcastHeight)
channel.ShortChannelID.BlockHeight = 0
}
}

View File

@ -210,6 +210,16 @@ var openChannelCommand = cli.Command{
"propose to the remote peer (%q, %q)",
channelTypeTweakless, channelTypeAnchors),
},
cli.BoolFlag{
Name: "zero_conf",
Usage: "(optional) whether a zero-conf channel open " +
"should be attempted.",
},
cli.BoolFlag{
Name: "scid_alias",
Usage: "(optional) whether a scid-alias channel type" +
" should be negotiated.",
},
},
Action: actionDecorator(openChannel),
}
@ -249,6 +259,8 @@ func openChannel(ctx *cli.Context) error {
CloseAddress: ctx.String("close_address"),
RemoteMaxValueInFlightMsat: ctx.Uint64("remote_max_value_in_flight_msat"),
MaxLocalCsv: uint32(ctx.Uint64("max_local_csv")),
ZeroConf: ctx.Bool("zero_conf"),
ScidAlias: ctx.Bool("scid_alias"),
}
switch {

View File

@ -266,10 +266,25 @@ func (c *chainWatcher) Start() error {
// As a height hint, we'll try to use the opening height, but if the
// channel isn't yet open, then we'll use the height it was broadcast
// at.
// at. This may be an unconfirmed zero-conf channel.
heightHint := c.cfg.chanState.ShortChanID().BlockHeight
if heightHint == 0 {
heightHint = chanState.FundingBroadcastHeight
heightHint = chanState.BroadcastHeight()
}
// Since no zero-conf state is stored in a channel backup, the below
// logic will not be triggered for restored, zero-conf channels. Set
// the height hint for zero-conf channels.
if chanState.IsZeroConf() {
if chanState.ZeroConfConfirmed() {
// If the zero-conf channel is confirmed, we'll use the
// confirmed SCID's block height.
heightHint = chanState.ZeroConfRealScid().BlockHeight
} else {
// The zero-conf channel is unconfirmed. We'll need to
// use the FundingBroadcastHeight.
heightHint = chanState.BroadcastHeight()
}
}
localKey := chanState.LocalChanCfg.MultiSigKey.PubKey.SerializeCompressed()

View File

@ -8,6 +8,7 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
@ -72,6 +73,7 @@ var (
type optionalMsgFields struct {
capacity *btcutil.Amount
channelPoint *wire.OutPoint
remoteAlias *lnwire.ShortChannelID
}
// apply applies the optional fields within the functional options.
@ -102,6 +104,18 @@ func ChannelPoint(op wire.OutPoint) OptionalMsgField {
}
}
// 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
// channels with peers where the option-scid-alias feature bit was negotiated.
// The channel update will be added to the graph under the original SCID, but
// will be modified and re-signed with this alias.
func RemoteAlias(alias *lnwire.ShortChannelID) OptionalMsgField {
return func(f *optionalMsgFields) {
f.remoteAlias = alias
}
}
// networkMsg couples a routing related wire message with the peer that
// originally sent it.
type networkMsg struct {
@ -277,12 +291,40 @@ type Config struct {
// how often we should allow a new update for a specific channel and
// direction.
ChannelUpdateInterval time.Duration
// IsAlias returns true if a given ShortChannelID is an alias for
// option_scid_alias channels.
IsAlias func(scid lnwire.ShortChannelID) bool
// SignAliasUpdate is used to re-sign a channel update using the
// remote's alias if the option-scid-alias feature bit was negotiated.
SignAliasUpdate func(u *lnwire.ChannelUpdate) (*ecdsa.Signature,
error)
// FindBaseByAlias finds the SCID stored in the graph by an alias SCID.
// This is used for channels that have negotiated the option-scid-alias
// feature bit.
FindBaseByAlias func(alias lnwire.ShortChannelID) (
lnwire.ShortChannelID, error)
// GetAlias allows the gossiper to look up the peer's alias for a given
// ChannelID. This is used to sign updates for them if the channel has
// no AuthProof and the option-scid-alias feature bit was negotiated.
GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)
}
// processedNetworkMsg is a wrapper around networkMsg and a boolean. It is
// used to let the caller of the lru.Cache know if a message has already been
// processed or not.
type processedNetworkMsg struct {
processed bool
msg *networkMsg
}
// cachedNetworkMsg is a wrapper around a network message that can be used with
// *lru.Cache.
type cachedNetworkMsg struct {
msgs []*networkMsg
msgs []*processedNetworkMsg
}
// Size returns the "size" of an entry. We return the number of items as we
@ -596,11 +638,11 @@ func (d *AuthenticatedGossiper) resendFutureMessages(height uint32) {
log.Debugf("Resending %d network messages at height %d",
len(msgs), height)
for _, msg := range msgs {
for _, pMsg := range msgs {
select {
case d.networkMsgs <- msg:
case d.networkMsgs <- pMsg.msg:
case <-d.quit:
msg.err <- ErrGossiperShuttingDown
pMsg.msg.err <- ErrGossiperShuttingDown
}
}
}
@ -1523,6 +1565,37 @@ func (d *AuthenticatedGossiper) processChanPolicyUpdate(
// avoid directly giving away their existence. Instead, we'll
// send the update directly to the remote party.
if edgeInfo.Info.AuthProof == nil {
// If AuthProof is nil and an alias was found for this
// ChannelID (meaning the option-scid-alias feature was
// negotiated), we'll replace the ShortChannelID in the
// update with the peer's alias. We do this after
// updateChannel so that the alias isn't persisted to
// the database.
op := &edgeInfo.Info.ChannelPoint
chanID := lnwire.NewChanIDFromOutPoint(op)
var defaultAlias lnwire.ShortChannelID
foundAlias, _ := d.cfg.GetAlias(chanID)
if foundAlias != defaultAlias {
chanUpdate.ShortChannelID = foundAlias
sig, err := d.cfg.SignAliasUpdate(chanUpdate)
if err != nil {
log.Errorf("Unable to sign alias "+
"update: %v", err)
continue
}
lnSig, err := lnwire.NewSigFromSignature(sig)
if err != nil {
log.Errorf("Unable to create sig: %v",
err)
continue
}
chanUpdate.Signature = lnSig
}
remotePubKey := remotePubFromChanInfo(
edgeInfo.Info, chanUpdate.ChannelFlags,
)
@ -1703,7 +1776,7 @@ func (d *AuthenticatedGossiper) isPremature(chanID lnwire.ShortChannelID,
// Init an empty cached message and overwrite it if there are cached
// messages found.
cachedMsgs := &cachedNetworkMsg{
msgs: make([]*networkMsg, 0),
msgs: make([]*processedNetworkMsg, 0),
}
result, err := d.futureMsgs.Get(msgHeight)
@ -1723,8 +1796,11 @@ func (d *AuthenticatedGossiper) isPremature(chanID lnwire.ShortChannelID,
err: make(chan error, 1),
}
// The processed boolean is unused in the futureMsgs case.
pMsg := &processedNetworkMsg{msg: copied}
// Add the network message.
cachedMsgs.msgs = append(cachedMsgs.msgs, copied)
cachedMsgs.msgs = append(cachedMsgs.msgs, pMsg)
_, err = d.futureMsgs.Put(msgHeight, cachedMsgs)
if err != nil {
log.Errorf("Adding future message got error: %v", err)
@ -1826,7 +1902,8 @@ func (d *AuthenticatedGossiper) processZombieUpdate(
// With the signature valid, we'll proceed to mark the
// edge as live and wait for the channel announcement to
// come through again.
err = d.cfg.Router.MarkEdgeLive(msg.ShortChannelID)
baseScid := lnwire.NewShortChanIDFromInt(chanInfo.ChannelID)
err = d.cfg.Router.MarkEdgeLive(baseScid)
if err != nil {
return fmt.Errorf("unable to remove edge with "+
"chan_id=%v from zombie index: %v",
@ -2147,6 +2224,24 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
return nil, false
}
// If this is a remote ChannelAnnouncement with an alias SCID, we'll
// reject the announcement. Since the router accepts alias SCIDs,
// not erroring out would be a DoS vector.
if nMsg.isRemote && d.cfg.IsAlias(ann.ShortChannelID) {
err := fmt.Errorf("ignoring remote alias channel=%v",
ann.ShortChannelID)
log.Errorf(err.Error())
key := newRejectCacheKey(
ann.ShortChannelID.ToUint64(),
sourceToPub(nMsg.source),
)
_, _ = d.recentRejects.Put(key, &cachedReject{})
nMsg.err <- err
return nil, false
}
// If the advertised inclusionary block is beyond our knowledge of the
// chain tip, then we'll ignore it for now.
d.Lock()
@ -2293,7 +2388,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
// If we earlier received any ChannelUpdates for this channel, we can
// now process them, as the channel is added to the graph.
shortChanID := ann.ShortChannelID.ToUint64()
var channelUpdates []*networkMsg
var channelUpdates []*processedNetworkMsg
earlyChanUpdates, err := d.prematureChannelUpdates.Get(shortChanID)
if err == nil {
@ -2308,6 +2403,16 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
// ensure we don't block here, as we can handle only one announcement
// at a time.
for _, cu := range channelUpdates {
// Skip if already processed.
if cu.processed {
continue
}
// Mark the ChannelUpdate as processed. This ensures that a
// subsequent announcement in the option-scid-alias case does
// not re-use an old ChannelUpdate.
cu.processed = true
d.wg.Add(1)
go func(updMsg *networkMsg) {
defer d.wg.Done()
@ -2333,7 +2438,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
log.Errorf("Unsupported message type found "+
"among ChannelUpdates: %T", msg)
}
}(cu)
}(cu.msg)
}
// Channel announcement was successfully processed and now it might be
@ -2380,9 +2485,13 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
// If the advertised inclusionary block is beyond our knowledge of the
// chain tip, then we'll put the announcement in limbo to be fully
// verified once we advance forward in the chain.
// verified once we advance forward in the chain. If the update has an
// alias SCID, we'll skip the isPremature check. This is necessary
// since aliases start at block height 16_000_000.
d.Lock()
if nMsg.isRemote && d.isPremature(upd.ShortChannelID, 0, nMsg) {
if nMsg.isRemote && !d.cfg.IsAlias(upd.ShortChannelID) &&
d.isPremature(upd.ShortChannelID, 0, nMsg) {
log.Warnf("Update announcement for short_chan_id(%v), is "+
"premature: advertises height %v, only height %v is "+
"known", shortChanID, blockHeight, d.bestHeight)
@ -2396,8 +2505,21 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
// whether this update is stale or is for a zombie channel in order to
// quickly reject it.
timestamp := time.Unix(int64(upd.Timestamp), 0)
// Fetch the SCID we should be using to lock the channelMtx and make
// graph queries with.
graphScid, err := d.cfg.FindBaseByAlias(upd.ShortChannelID)
if err != nil {
// Fallback and set the graphScid to the peer-provided SCID.
// This will occur for non-option-scid-alias channels and for
// public option-scid-alias channels after 6 confirmations.
// Once public option-scid-alias channels have 6 confs, we'll
// ignore ChannelUpdates with one of their aliases.
graphScid = upd.ShortChannelID
}
if d.cfg.Router.IsStaleEdgePolicy(
upd.ShortChannelID, timestamp, upd.ChannelFlags,
graphScid, timestamp, upd.ChannelFlags,
) {
log.Debugf("Ignored stale edge policy: peer=%v, source=%x, "+
@ -2418,11 +2540,10 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
// access the database. This ensures the state we read from the
// database has not changed between this point and when we call
// UpdateEdge() later.
d.channelMtx.Lock(upd.ShortChannelID.ToUint64())
defer d.channelMtx.Unlock(upd.ShortChannelID.ToUint64())
chanInfo, e1, e2, err := d.cfg.Router.GetChannelByID(
upd.ShortChannelID,
)
d.channelMtx.Lock(graphScid.ToUint64())
defer d.channelMtx.Unlock(graphScid.ToUint64())
chanInfo, e1, e2, err := d.cfg.Router.GetChannelByID(graphScid)
switch err {
// No error, break.
case nil:
@ -2458,6 +2579,13 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
// ChannelAnnouncement for since we reject them. Because of
// this, we temporarily add it to a map, and reprocess it after
// our own ChannelAnnouncement has been processed.
//
// The shortChanID may be an alias, but it is fine to use here
// since we don't have an edge in the graph and if the peer is
// not buggy, we should be able to use it once the gossiper
// receives the local announcement.
pMsg := &processedNetworkMsg{msg: nMsg}
earlyMsgs, err := d.prematureChannelUpdates.Get(shortChanID)
switch {
// Nothing in the cache yet, we can just directly insert this
@ -2465,14 +2593,14 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
case err == cache.ErrElementNotFound:
_, _ = d.prematureChannelUpdates.Put(
shortChanID, &cachedNetworkMsg{
msgs: []*networkMsg{nMsg},
msgs: []*processedNetworkMsg{pMsg},
})
// There's already something in the cache, so we'll combine the
// set of messages into a single value.
default:
msgs := earlyMsgs.(*cachedNetworkMsg).msgs
msgs = append(msgs, nMsg)
msgs = append(msgs, pMsg)
_, _ = d.prematureChannelUpdates.Put(
shortChanID, &cachedNetworkMsg{
msgs: msgs,
@ -2555,8 +2683,16 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
// maximum burst of 10. If we haven't seen an update
// for this channel before, we'll need to initialize a
// rate limiter for each direction.
//
// Since the edge exists in the graph, we'll create a
// rate limiter for chanInfo.ChannelID rather then the
// SCID the peer sent. This is because there may be
// multiple aliases for a channel and we may otherwise
// rate-limit only a single alias of the channel,
// instead of the whole channel.
baseScid := chanInfo.ChannelID
d.Lock()
rls, ok := d.chanUpdateRateLimiter[shortChanID]
rls, ok := d.chanUpdateRateLimiter[baseScid]
if !ok {
r := rate.Every(d.cfg.ChannelUpdateInterval)
b := d.cfg.MaxChannelUpdateBurst
@ -2564,7 +2700,7 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
rate.NewLimiter(r, b),
rate.NewLimiter(r, b),
}
d.chanUpdateRateLimiter[shortChanID] = rls
d.chanUpdateRateLimiter[baseScid] = rls
}
d.Unlock()
@ -2578,9 +2714,16 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
}
}
// We'll use chanInfo.ChannelID rather than the peer-supplied
// ShortChannelID in the ChannelUpdate to avoid the router having to
// lookup the stored SCID. If we're sending the update, we'll always
// use the SCID stored in the database rather than a potentially
// different alias. This might mean that SigBytes is incorrect as it
// signs a different SCID than the database SCID, but since there will
// only be a difference if AuthProof == nil, this is fine.
update := &channeldb.ChannelEdgePolicy{
SigBytes: upd.Signature.ToSignatureBytes(),
ChannelID: shortChanID,
ChannelID: chanInfo.ChannelID,
LastUpdate: timestamp,
MessageFlags: upd.MessageFlags,
ChannelFlags: upd.ChannelFlags,
@ -2601,8 +2744,10 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
log.Debug(err)
} else {
// Since we know the stored SCID in the graph, we'll
// cache that SCID.
key := newRejectCacheKey(
upd.ShortChannelID.ToUint64(),
chanInfo.ChannelID,
sourceToPub(nMsg.source),
)
_, _ = d.recentRejects.Put(key, &cachedReject{})
@ -2620,6 +2765,35 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
// to be given the update, so we'll try sending the update directly to
// the remote peer.
if !nMsg.isRemote && chanInfo.AuthProof == nil {
if nMsg.optionalMsgFields != nil {
remoteAlias := nMsg.optionalMsgFields.remoteAlias
if remoteAlias != nil {
// The remoteAlias field was specified, meaning
// that we should replace the SCID in the
// update with the remote's alias. We'll also
// need to re-sign the channel update. This is
// required for option-scid-alias feature-bit
// negotiated channels.
upd.ShortChannelID = *remoteAlias
sig, err := d.cfg.SignAliasUpdate(upd)
if err != nil {
log.Error(err)
nMsg.err <- err
return nil, false
}
lnSig, err := lnwire.NewSigFromSignature(sig)
if err != nil {
log.Error(err)
nMsg.err <- err
return nil, false
}
upd.Signature = lnSig
}
}
// Get our peer's public key.
remotePubKey := remotePubFromChanInfo(
chanInfo, upd.ChannelFlags,
@ -2645,9 +2819,10 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
// Channel update announcement was successfully processed and now it
// can be broadcast to the rest of the network. However, we'll only
// broadcast the channel update announcement if it has an attached
// authentication proof.
// authentication proof. We also won't broadcast the update if it
// contains an alias because the network would reject this.
var announcements []networkMsg
if chanInfo.AuthProof != nil {
if chanInfo.AuthProof != nil && !d.cfg.IsAlias(upd.ShortChannelID) {
announcements = append(announcements, networkMsg{
peer: nMsg.peer,
source: nMsg.source,
@ -2704,7 +2879,6 @@ func (d *AuthenticatedGossiper) handleAnnSig(nMsg *networkMsg,
chanInfo, e1, e2, err := d.cfg.Router.GetChannelByID(
ann.ShortChannelID,
)
if err != nil {
proof := channeldb.NewWaitingProof(nMsg.isRemote, ann)
err := d.cfg.WaitingProofStore.Add(proof)

View File

@ -16,6 +16,7 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
@ -734,6 +735,27 @@ func createTestCtx(startHeight uint32) (*testCtx, func(), error) {
}
broadcastedMessage := make(chan msgWithSenders, 10)
isAlias := func(lnwire.ShortChannelID) bool {
return false
}
signAliasUpdate := func(*lnwire.ChannelUpdate) (*ecdsa.Signature,
error) {
return nil, nil
}
findBaseByAlias := func(lnwire.ShortChannelID) (lnwire.ShortChannelID,
error) {
return lnwire.ShortChannelID{}, fmt.Errorf("no base scid")
}
getAlias := func(lnwire.ChannelID) (lnwire.ShortChannelID, error) {
return lnwire.ShortChannelID{}, fmt.Errorf("no peer alias")
}
gossiper := New(Config{
Notifier: notifier,
Broadcast: func(senders map[route.Vertex]struct{},
@ -778,6 +800,10 @@ func createTestCtx(startHeight uint32) (*testCtx, func(), error) {
MinimumBatchSize: 10,
MaxChannelUpdateBurst: DefaultMaxChannelUpdateBurst,
ChannelUpdateInterval: DefaultChannelUpdateInterval,
IsAlias: isAlias,
SignAliasUpdate: signAliasUpdate,
FindBaseByAlias: findBaseByAlias,
GetAlias: getAlias,
}, selfKeyDesc)
if err := gossiper.Start(); err != nil {
@ -1387,6 +1413,27 @@ func TestSignatureAnnouncementRetryAtStartup(t *testing.T) {
// NotifyWhenOffline methods. This should trigger a new attempt to send
// the message to the peer.
ctx.gossiper.Stop()
isAlias := func(lnwire.ShortChannelID) bool {
return false
}
signAliasUpdate := func(*lnwire.ChannelUpdate) (*ecdsa.Signature,
error) {
return nil, nil
}
findBaseByAlias := func(lnwire.ShortChannelID) (lnwire.ShortChannelID,
error) {
return lnwire.ShortChannelID{}, fmt.Errorf("no base scid")
}
getAlias := func(lnwire.ChannelID) (lnwire.ShortChannelID, error) {
return lnwire.ShortChannelID{}, fmt.Errorf("no peer alias")
}
gossiper := New(Config{
Notifier: ctx.gossiper.cfg.Notifier,
Broadcast: ctx.gossiper.cfg.Broadcast,
@ -1405,6 +1452,10 @@ func TestSignatureAnnouncementRetryAtStartup(t *testing.T) {
NumActiveSyncers: 3,
MinimumBatchSize: 10,
SubBatchDelay: time.Second * 5,
IsAlias: isAlias,
SignAliasUpdate: signAliasUpdate,
FindBaseByAlias: findBaseByAlias,
GetAlias: getAlias,
}, &keychain.KeyDescriptor{
PubKey: ctx.gossiper.selfKey,
KeyLocator: ctx.gossiper.selfKeyLoc,

View File

@ -1,5 +1,10 @@
# Release Notes
## Protocol Extensions
### Zero-Conf Channel Opens
* [Introduces support for zero-conf channel opens and non-zero-conf option_scid_alias channels.](https://github.com/lightningnetwork/lnd/pull/5955)
## Build system
* [Add the release build directory to the `.gitignore` file to avoid the release

View File

@ -71,4 +71,12 @@ var defaultSetDesc = setDesc{
SetInit: {}, // I
SetNodeAnn: {}, // N
},
lnwire.ScidAliasOptional: {
SetInit: {}, // I
SetNodeAnn: {}, // N
},
lnwire.ZeroConfOptional: {
SetInit: {}, // I
SetNodeAnn: {}, // N
},
}

View File

@ -72,6 +72,12 @@ var deps = depDesc{
lnwire.KeysendOptional: {
lnwire.TLVOnionPayloadOptional: {},
},
lnwire.ScidAliasOptional: {
lnwire.ExplicitChannelTypeOptional: {},
},
lnwire.ZeroConfOptional: {
lnwire.ScidAliasOptional: {},
},
}
// ValidateDeps asserts that a feature vector sets all features and their

View File

@ -31,6 +31,15 @@ type Config struct {
// NoKeysend unsets any bits signaling support for accepting keysend
// payments.
NoKeysend bool
// NoOptionScidAlias unsets any bits signalling support for
// option_scid_alias. This also implicitly disables zero-conf channels.
NoOptionScidAlias bool
// NoZeroConf unsets any bits signalling support for zero-conf
// channels. This should be used instead of NoOptionScidAlias to still
// keep option-scid-alias support.
NoZeroConf bool
}
// Manager is responsible for generating feature vectors for different requested
@ -125,6 +134,14 @@ func newManager(cfg Config, desc setDesc) (*Manager, error) {
raw.Unset(lnwire.KeysendOptional)
raw.Unset(lnwire.KeysendRequired)
}
if cfg.NoOptionScidAlias {
raw.Unset(lnwire.ScidAliasOptional)
raw.Unset(lnwire.ScidAliasRequired)
}
if cfg.NoZeroConf {
raw.Unset(lnwire.ZeroConfOptional)
raw.Unset(lnwire.ZeroConfRequired)
}
// Ensure that all of our feature sets properly set any
// dependent features.

View File

@ -24,10 +24,12 @@ var (
// negotiateCommitmentType negotiates the commitment type of a newly opened
// channel. If a channelType is provided, explicit negotiation for said type
// will be attempted if the set of both local and remote features support it.
// Otherwise, implicit negotiation will be attempted.
func negotiateCommitmentType(channelType *lnwire.ChannelType,
local, remote *lnwire.FeatureVector, mustBeExplicit bool,
) (bool, *lnwire.ChannelType, lnwallet.CommitmentType, error) {
// Otherwise, implicit negotiation will be attempted. Two booleans are
// returned letting the caller know if the option-scid-alias or zero-conf
// channel types were negotiated.
func negotiateCommitmentType(channelType *lnwire.ChannelType, local,
remote *lnwire.FeatureVector, mustBeExplicit bool) (bool,
*lnwire.ChannelType, lnwallet.CommitmentType, error) {
if channelType != nil {
// If the peer does know explicit negotiation, let's attempt
@ -57,12 +59,127 @@ func negotiateCommitmentType(channelType *lnwire.ChannelType,
// specific channel type. Since the channel type is comprised of a set of even
// feature bits, we also make sure each feature is supported by both peers. An
// error is returned if either peer does not support said channel type.
func explicitNegotiateCommitmentType(channelType lnwire.ChannelType,
local, remote *lnwire.FeatureVector) (lnwallet.CommitmentType, error) {
func explicitNegotiateCommitmentType(channelType lnwire.ChannelType, local,
remote *lnwire.FeatureVector) (lnwallet.CommitmentType, error) {
channelFeatures := lnwire.RawFeatureVector(channelType)
switch {
// Lease script enforcement + anchors zero fee + static remote key +
// zero conf + scid alias features only.
case channelFeatures.OnlyContains(
lnwire.ZeroConfRequired,
lnwire.ScidAliasRequired,
lnwire.ScriptEnforcedLeaseRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
if !hasFeatures(
local, remote,
lnwire.ZeroConfOptional,
lnwire.ScriptEnforcedLeaseOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.StaticRemoteKeyOptional,
) {
return 0, errUnsupportedChannelType
}
return lnwallet.CommitmentTypeScriptEnforcedLease, nil
// Anchors zero fee + static remote key + zero conf + scid alias
// features only.
case channelFeatures.OnlyContains(
lnwire.ZeroConfRequired,
lnwire.ScidAliasRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
if !hasFeatures(
local, remote,
lnwire.ZeroConfOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.StaticRemoteKeyOptional,
) {
return 0, errUnsupportedChannelType
}
return lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx, nil
// Lease script enforcement + anchors zero fee + static remote key +
// zero conf features only.
case channelFeatures.OnlyContains(
lnwire.ZeroConfRequired,
lnwire.ScriptEnforcedLeaseRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
if !hasFeatures(
local, remote,
lnwire.ZeroConfOptional,
lnwire.ScriptEnforcedLeaseOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.StaticRemoteKeyOptional,
) {
return 0, errUnsupportedChannelType
}
return lnwallet.CommitmentTypeScriptEnforcedLease, nil
// Anchors zero fee + static remote key + zero conf features only.
case channelFeatures.OnlyContains(
lnwire.ZeroConfRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
if !hasFeatures(
local, remote,
lnwire.ZeroConfOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.StaticRemoteKeyOptional,
) {
return 0, errUnsupportedChannelType
}
return lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx, nil
// Lease script enforcement + anchors zero fee + static remote key +
// option-scid-alias features only.
case channelFeatures.OnlyContains(
lnwire.ScidAliasRequired,
lnwire.ScriptEnforcedLeaseRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
if !hasFeatures(
local, remote,
lnwire.ScidAliasOptional,
lnwire.ScriptEnforcedLeaseOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.StaticRemoteKeyOptional,
) {
return 0, errUnsupportedChannelType
}
return lnwallet.CommitmentTypeScriptEnforcedLease, nil
// Anchors zero fee + static remote key + option-scid-alias features
// only.
case channelFeatures.OnlyContains(
lnwire.ScidAliasRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.StaticRemoteKeyRequired,
):
if !hasFeatures(
local, remote,
lnwire.ScidAliasOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.StaticRemoteKeyOptional,
) {
return 0, errUnsupportedChannelType
}
return lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx, nil
// Lease script enforcement + anchors zero fee + static remote key
// features only.
case channelFeatures.OnlyContains(

View File

@ -21,6 +21,8 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
remoteFeatures *lnwire.RawFeatureVector
expectsCommitType lnwallet.CommitmentType
expectsChanType lnwire.ChannelType
zeroConf bool
scidAlias bool
expectsErr error
}{
{
@ -81,6 +83,134 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
),
expectsErr: errUnsupportedChannelType,
},
{
name: "explicit zero-conf script enforced",
channelFeatures: lnwire.NewRawFeatureVector(
lnwire.ZeroConfRequired,
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.ScriptEnforcedLeaseRequired,
),
localFeatures: lnwire.NewRawFeatureVector(
lnwire.ZeroConfOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.ScriptEnforcedLeaseOptional,
lnwire.ExplicitChannelTypeOptional,
),
remoteFeatures: lnwire.NewRawFeatureVector(
lnwire.ZeroConfOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.ScriptEnforcedLeaseOptional,
lnwire.ExplicitChannelTypeOptional,
),
expectsCommitType: lnwallet.CommitmentTypeScriptEnforcedLease,
expectsChanType: lnwire.ChannelType(
*lnwire.NewRawFeatureVector(
lnwire.ZeroConfRequired,
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.ScriptEnforcedLeaseRequired,
),
),
zeroConf: true,
expectsErr: nil,
},
{
name: "explicit zero-conf anchors",
channelFeatures: lnwire.NewRawFeatureVector(
lnwire.ZeroConfRequired,
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
),
localFeatures: lnwire.NewRawFeatureVector(
lnwire.ZeroConfOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.ExplicitChannelTypeOptional,
),
remoteFeatures: lnwire.NewRawFeatureVector(
lnwire.ZeroConfOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.ExplicitChannelTypeOptional,
),
expectsCommitType: lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx,
expectsChanType: lnwire.ChannelType(
*lnwire.NewRawFeatureVector(
lnwire.ZeroConfRequired,
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
),
),
zeroConf: true,
expectsErr: nil,
},
{
name: "explicit scid-alias script enforced",
channelFeatures: lnwire.NewRawFeatureVector(
lnwire.ScidAliasRequired,
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.ScriptEnforcedLeaseRequired,
),
localFeatures: lnwire.NewRawFeatureVector(
lnwire.ScidAliasOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.ScriptEnforcedLeaseOptional,
lnwire.ExplicitChannelTypeOptional,
),
remoteFeatures: lnwire.NewRawFeatureVector(
lnwire.ScidAliasOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.ScriptEnforcedLeaseOptional,
lnwire.ExplicitChannelTypeOptional,
),
expectsCommitType: lnwallet.CommitmentTypeScriptEnforcedLease,
expectsChanType: lnwire.ChannelType(
*lnwire.NewRawFeatureVector(
lnwire.ScidAliasRequired,
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.ScriptEnforcedLeaseRequired,
),
),
scidAlias: true,
expectsErr: nil,
},
{
name: "explicit scid-alias anchors",
channelFeatures: lnwire.NewRawFeatureVector(
lnwire.ScidAliasRequired,
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
),
localFeatures: lnwire.NewRawFeatureVector(
lnwire.ScidAliasOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.ExplicitChannelTypeOptional,
),
remoteFeatures: lnwire.NewRawFeatureVector(
lnwire.ScidAliasOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
lnwire.ExplicitChannelTypeOptional,
),
expectsCommitType: lnwallet.CommitmentTypeAnchorsZeroFeeHtlcTx,
expectsChanType: lnwire.ChannelType(
*lnwire.NewRawFeatureVector(
lnwire.ScidAliasRequired,
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
),
),
scidAlias: true,
expectsErr: nil,
},
{
name: "explicit anchors",
channelFeatures: lnwire.NewRawFeatureVector(
@ -212,16 +342,50 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
*testCase.channelFeatures,
)
}
_, localChanType, localCommitType, err := negotiateCommitmentType(
_, lChan, lCommit, err := negotiateCommitmentType(
channelType, localFeatures, remoteFeatures,
testCase.mustBeExplicit,
)
var (
localZc bool
localScid bool
remoteZc bool
remoteScid bool
)
if lChan != nil {
localFv := lnwire.RawFeatureVector(*lChan)
localZc = localFv.IsSet(
lnwire.ZeroConfRequired,
)
localScid = localFv.IsSet(
lnwire.ScidAliasRequired,
)
}
require.Equal(t, testCase.zeroConf, localZc)
require.Equal(t, testCase.scidAlias, localScid)
require.Equal(t, testCase.expectsErr, err)
_, remoteChanType, remoteCommitType, err := negotiateCommitmentType(
_, rChan, rCommit, err := negotiateCommitmentType(
channelType, remoteFeatures, localFeatures,
testCase.mustBeExplicit,
)
if rChan != nil {
remoteFv := lnwire.RawFeatureVector(*rChan)
remoteZc = remoteFv.IsSet(
lnwire.ZeroConfRequired,
)
remoteScid = remoteFv.IsSet(
lnwire.ScidAliasRequired,
)
}
require.Equal(t, testCase.zeroConf, remoteZc)
require.Equal(t, testCase.scidAlias, remoteScid)
require.Equal(t, testCase.expectsErr, err)
if testCase.expectsErr != nil {
@ -229,20 +393,20 @@ func TestCommitmentTypeNegotiation(t *testing.T) {
}
require.Equal(
t, testCase.expectsCommitType, localCommitType,
t, testCase.expectsCommitType, lCommit,
testCase.name,
)
require.Equal(
t, testCase.expectsCommitType, remoteCommitType,
t, testCase.expectsCommitType, rCommit,
testCase.name,
)
require.Equal(
t, testCase.expectsChanType, *localChanType,
t, testCase.expectsChanType, *lChan,
testCase.name,
)
require.Equal(
t, testCase.expectsChanType, *remoteChanType,
t, testCase.expectsChanType, *rChan,
testCase.name,
)
})

View File

@ -19,3 +19,31 @@ type Controller interface {
// represents a pending channel in the Controller implementation.
IsPendingChannel([32]byte, lnpeer.Peer) bool
}
// aliasHandler is an interface that abstracts the managing of aliases.
type aliasHandler interface {
// RequestAlias lets the funding manager request a unique SCID alias to
// use in the funding_locked message.
RequestAlias() (lnwire.ShortChannelID, error)
// PutPeerAlias lets the funding manager store the received alias SCID
// in the funding_locked message.
PutPeerAlias(lnwire.ChannelID, lnwire.ShortChannelID) error
// GetPeerAlias lets the funding manager lookup the received alias SCID
// from the funding_locked message. This is not the same as GetAliases
// which retrieves OUR aliases for a given channel.
GetPeerAlias(lnwire.ChannelID) (lnwire.ShortChannelID, error)
// AddLocalAlias persists an alias to an underlying alias store.
AddLocalAlias(lnwire.ShortChannelID, lnwire.ShortChannelID, 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
// be the confirmed SCID otherwise.
GetAliases(lnwire.ShortChannelID) []lnwire.ShortChannelID
// DeleteSixConfs removes the passed SCID from one of the underlying
// alias store's indices.
DeleteSixConfs(lnwire.ShortChannelID) error
}

File diff suppressed because it is too large Load Diff

View File

@ -115,8 +115,48 @@ var (
testKeyLoc = keychain.KeyLocator{Family: keychain.KeyFamilyNodeKey}
fundingNetParams = chainreg.BitcoinTestNetParams
alias = lnwire.ShortChannelID{
BlockHeight: 16_000_000,
TxIndex: 0,
TxPosition: 0,
}
)
type mockAliasMgr struct{}
func (m *mockAliasMgr) RequestAlias() (lnwire.ShortChannelID, error) {
return alias, nil
}
func (m *mockAliasMgr) PutPeerAlias(lnwire.ChannelID,
lnwire.ShortChannelID) error {
return nil
}
func (m *mockAliasMgr) GetPeerAlias(lnwire.ChannelID) (lnwire.ShortChannelID,
error) {
return alias, nil
}
func (m *mockAliasMgr) AddLocalAlias(lnwire.ShortChannelID,
lnwire.ShortChannelID, bool) error {
return nil
}
func (m *mockAliasMgr) GetAliases(
lnwire.ShortChannelID) []lnwire.ShortChannelID {
return []lnwire.ShortChannelID{alias}
}
func (m *mockAliasMgr) DeleteSixConfs(lnwire.ShortChannelID) error {
return nil
}
type mockNotifier struct {
oneConfChannel chan *chainntnfs.TxConfirmation
sixConfChannel chan *chainntnfs.TxConfirmation
@ -199,6 +239,8 @@ type testNode struct {
mockChanEvent *mockChanEvent
testDir string
shutdownChannel chan struct{}
reportScidChan chan struct{}
localFeatures []lnwire.FeatureBit
remoteFeatures []lnwire.FeatureBit
remotePeer *testNode
@ -234,7 +276,9 @@ func (n *testNode) QuitSignal() <-chan struct{} {
}
func (n *testNode) LocalFeatures() *lnwire.FeatureVector {
return lnwire.NewFeatureVector(nil, nil)
return lnwire.NewFeatureVector(
lnwire.NewRawFeatureVector(n.localFeatures...), nil,
)
}
func (n *testNode) RemoteFeatures() *lnwire.FeatureVector {
@ -307,10 +351,13 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
epochChan: make(chan *chainntnfs.BlockEpoch, 2),
}
aliasMgr := &mockAliasMgr{}
sentMessages := make(chan lnwire.Message)
sentAnnouncements := make(chan lnwire.Message)
publTxChan := make(chan *wire.MsgTx, 1)
shutdownChan := make(chan struct{})
reportScidChan := make(chan struct{})
wc := &mock.WalletController{
RootKey: alicePrivKey,
@ -382,14 +429,16 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
return lnwire.NodeAnnouncement{}, nil
},
TempChanIDSeed: chanIDSeed,
FindChannel: func(chanID lnwire.ChannelID) (
*channeldb.OpenChannel, error) {
dbChannels, err := cdb.FetchAllChannels()
FindChannel: func(node *btcec.PublicKey,
chanID lnwire.ChannelID) (*channeldb.OpenChannel,
error) {
nodeChans, err := cdb.FetchOpenChannels(node)
if err != nil {
return nil, err
}
for _, channel := range dbChannels {
for _, channel := range nodeChans {
if chanID.IsChanPoint(&channel.FundingOutpoint) {
return channel, nil
}
@ -432,6 +481,7 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
return nil
},
ReportShortChanID: func(wire.OutPoint) error {
reportScidChan <- struct{}{}
return nil
},
PublishTransaction: func(txn *wire.MsgTx, _ string) error {
@ -450,6 +500,12 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
OpenChannelPredicate: chainedAcceptor,
NotifyPendingOpenChannelEvent: evt.NotifyPendingOpenChannelEvent,
RegisteredChains: chainreg.NewChainRegistry(),
DeleteAliasEdge: func(scid lnwire.ShortChannelID) (
*channeldb.ChannelEdgePolicy, error) {
return nil, nil
},
AliasManager: aliasMgr,
}
for _, op := range options {
@ -473,6 +529,7 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
mockChanEvent: evt,
testDir: tempTestDir,
shutdownChannel: shutdownChan,
reportScidChan: reportScidChan,
addr: addr,
}
@ -542,6 +599,7 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) {
},
DefaultMinHtlcIn: 5,
RequiredRemoteMaxValue: oldCfg.RequiredRemoteMaxValue,
ReportShortChanID: oldCfg.ReportShortChanID,
PublishTransaction: func(txn *wire.MsgTx, _ string) error {
publishChan <- txn
return nil
@ -552,6 +610,8 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) {
ZombieSweeperInterval: oldCfg.ZombieSweeperInterval,
ReservationTimeout: oldCfg.ReservationTimeout,
OpenChannelPredicate: chainedAcceptor,
DeleteAliasEdge: oldCfg.DeleteAliasEdge,
AliasManager: oldCfg.AliasManager,
})
require.NoError(t, err, "failed recreating aliceFundingManager")
@ -639,7 +699,7 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt,
publ := fundChannel(
t, alice, bob, localFundingAmt, pushAmt, false, numConfs,
updateChan, announceChan,
updateChan, announceChan, nil,
)
fundingOutPoint := &wire.OutPoint{
Hash: publ.TxHash(),
@ -652,7 +712,8 @@ func openChannel(t *testing.T, alice, bob *testNode, localFundingAmt,
// transaction is confirmed on-chain. Returns the funding tx.
func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt,
pushAmt btcutil.Amount, subtractFees bool, numConfs uint32,
updateChan chan *lnrpc.OpenStatusUpdate, announceChan bool) *wire.MsgTx {
updateChan chan *lnrpc.OpenStatusUpdate, announceChan bool,
chanType *lnwire.ChannelType) *wire.MsgTx {
// Create a funding request and start the workflow.
errChan := make(chan error, 1)
@ -665,6 +726,7 @@ func fundChannel(t *testing.T, alice, bob *testNode, localFundingAmt,
PushAmt: lnwire.NewMSatFromSatoshis(pushAmt),
FundingFeePerKw: 1000,
Private: !announceChan,
ChannelType: chanType,
Updates: updateChan,
Err: errChan,
}
@ -3231,7 +3293,7 @@ func TestFundingManagerFundAll(t *testing.T) {
pushAmt := btcutil.Amount(0)
fundingTx := fundChannel(
t, alice, bob, test.spendAmt, pushAmt, true, 1,
updateChan, true,
updateChan, true, nil,
)
// Check whether the expected change output is present.
@ -3662,3 +3724,128 @@ func testUpfrontFailure(t *testing.T, pkscript []byte, expectErr bool) {
require.True(t, ok, "did not receive AcceptChannel")
}
}
// TestFundingManagerZeroConf tests that the fundingmanager properly handles
// the whole flow for zero-conf channels.
func TestFundingManagerZeroConf(t *testing.T) {
t.Parallel()
alice, bob := setupFundingManagers(t)
defer tearDownFundingManagers(t, alice, bob)
// Alice and Bob will have the same set of feature bits in our test.
featureBits := []lnwire.FeatureBit{
lnwire.ZeroConfOptional,
lnwire.ScidAliasOptional,
lnwire.ExplicitChannelTypeOptional,
lnwire.StaticRemoteKeyOptional,
lnwire.AnchorsZeroFeeHtlcTxOptional,
}
alice.localFeatures = featureBits
alice.remoteFeatures = featureBits
bob.localFeatures = featureBits
bob.remoteFeatures = featureBits
fundingAmt := btcutil.Amount(500000)
pushAmt := btcutil.Amount(0)
updateChan := make(chan *lnrpc.OpenStatusUpdate)
// Construct the zero-conf ChannelType for use in open_channel.
channelTypeBits := []lnwire.FeatureBit{
lnwire.ZeroConfRequired,
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
}
channelType := lnwire.ChannelType(
*lnwire.NewRawFeatureVector(channelTypeBits...),
)
// Call fundChannel with the zero-conf ChannelType.
fundingTx := fundChannel(
t, alice, bob, fundingAmt, pushAmt, false, 1, updateChan, true,
&channelType,
)
fundingOp := &wire.OutPoint{
Hash: fundingTx.TxHash(),
Index: 0,
}
// Assert that Bob's funding_locked message has an AliasScid.
bobFundingLocked := assertFundingMsgSent(
t, bob.msgChan, "FundingLocked",
).(*lnwire.FundingLocked)
require.NotNil(t, bobFundingLocked.AliasScid)
require.Equal(t, *bobFundingLocked.AliasScid, alias)
// Do the same for Alice as well.
aliceFundingLocked := assertFundingMsgSent(
t, alice.msgChan, "FundingLocked",
).(*lnwire.FundingLocked)
require.NotNil(t, aliceFundingLocked.AliasScid)
require.Equal(t, *aliceFundingLocked.AliasScid, alias)
// Exchange the funding_locked messages.
alice.fundingMgr.ProcessFundingMsg(bobFundingLocked, bob)
bob.fundingMgr.ProcessFundingMsg(aliceFundingLocked, alice)
// We'll assert that they both create new links.
assertHandleFundingLocked(t, alice, bob)
// We'll now assert that both sides send ChannelAnnouncement and
// ChannelUpdate messages.
assertChannelAnnouncements(t, alice, bob, fundingAmt, nil, nil)
// We'll now wait for the OpenStatusUpdate_ChanOpen update.
waitForOpenUpdate(t, updateChan)
// Assert that both Alice & Bob are in the addedToRouterGraph state.
assertAddedToRouterGraph(t, alice, bob, fundingOp)
// We'll now restart Alice's funding manager and assert that the tx
// is rebroadcast.
recreateAliceFundingManager(t, alice)
select {
case <-alice.publTxChan:
case <-time.After(time.Second * 5):
t.Fatalf("timed out waiting for alice to rebroadcast tx")
}
// We'll now confirm the funding transaction.
alice.mockNotifier.sixConfChannel <- &chainntnfs.TxConfirmation{
Tx: fundingTx,
}
bob.mockNotifier.sixConfChannel <- &chainntnfs.TxConfirmation{
Tx: fundingTx,
}
assertChannelAnnouncements(t, alice, bob, fundingAmt, nil, nil)
// Both Alice and Bob should send on reportScidChan.
select {
case <-alice.reportScidChan:
case <-time.After(time.Second * 5):
t.Fatalf("did not call ReportShortChanID in time")
}
select {
case <-bob.reportScidChan:
case <-time.After(time.Second * 5):
t.Fatalf("did not call ReportShortChanID in time")
}
// Send along the 6-confirmation channel so that announcement sigs can
// be exchanged.
alice.mockNotifier.sixConfChannel <- &chainntnfs.TxConfirmation{
Tx: fundingTx,
}
bob.mockNotifier.sixConfChannel <- &chainntnfs.TxConfirmation{
Tx: fundingTx,
}
assertAnnouncementSignatures(t, alice, bob)
// Assert that the channel state is deleted from the fundingmanager's
// datastore.
assertNoChannelState(t, alice, bob, fundingOp)
}

View File

@ -527,11 +527,18 @@ func (f *interceptedForward) FailWithCode(code lnwire.FailCode) error {
}
case lnwire.CodeTemporaryChannelFailure:
update, err := f.htlcSwitch.cfg.FetchLastChannelUpdate(
f.packet.incomingChanID,
update := f.htlcSwitch.failAliasUpdate(
f.packet.incomingChanID, true,
)
if err != nil {
return err
if update == nil {
// Fallback to the original, non-alias behavior.
var err error
update, err = f.htlcSwitch.cfg.FetchLastChannelUpdate(
f.packet.incomingChanID,
)
if err != nil {
return err
}
}
failureMsg = lnwire.NewTemporaryChannelFailure(update)

View File

@ -69,6 +69,36 @@ type dustHandler interface {
getDustClosure() dustClosure
}
// scidAliasHandler is an interface that the ChannelLink implements so it can
// properly handle option_scid_alias channels.
type scidAliasHandler interface {
// attachFailAliasUpdate allows the link to properly fail incoming
// HTLCs on option_scid_alias channels.
attachFailAliasUpdate(failClosure func(
sid lnwire.ShortChannelID,
incoming bool) *lnwire.ChannelUpdate)
// getAliases fetches the link's underlying aliases. This is used by
// the Switch to determine whether to forward an HTLC and where to
// forward an HTLC.
getAliases() []lnwire.ShortChannelID
// isZeroConf returns whether or not the underlying channel is a
// zero-conf channel.
isZeroConf() bool
// negotiatedAliasFeature returns whether the option-scid-alias feature
// bit was negotiated.
negotiatedAliasFeature() bool
// confirmedScid returns the confirmed SCID for a zero-conf channel.
confirmedScid() lnwire.ShortChannelID
// zeroConfConfirmed returns whether or not the zero-conf channel has
// confirmed.
zeroConfConfirmed() bool
}
// ChannelUpdateHandler is an interface that provides methods that allow
// sending lnwire.Message to the underlying link as well as querying state.
type ChannelUpdateHandler interface {
@ -138,6 +168,13 @@ type ChannelLink interface {
// Embed the dustHandler interface.
dustHandler
// Embed the scidAliasHandler interface.
scidAliasHandler
// IsUnadvertised returns true if the underlying channel is
// unadvertised.
IsUnadvertised() bool
// ChannelPoint returns the channel outpoint for the channel link.
ChannelPoint() *wire.OutPoint
@ -165,7 +202,7 @@ type ChannelLink interface {
CheckHtlcForward(payHash [32]byte, incomingAmt lnwire.MilliSatoshi,
amtToForward lnwire.MilliSatoshi,
incomingTimeout, outgoingTimeout uint32,
heightNow uint32) *LinkError
heightNow uint32, scid lnwire.ShortChannelID) *LinkError
// CheckHtlcTransit should return a nil error if the passed HTLC details
// satisfy the current channel policy. Otherwise, a LinkError with a

View File

@ -294,6 +294,15 @@ type ChannelLinkConfig struct {
// HtlcNotifier is an instance of a htlcNotifier which we will pipe htlc
// events through.
HtlcNotifier htlcNotifier
// FailAliasUpdate is a function used to fail an HTLC for an
// option_scid_alias channel.
FailAliasUpdate func(sid lnwire.ShortChannelID,
incoming bool) *lnwire.ChannelUpdate
// GetAliases is used by the link and switch to fetch the set of
// aliases for a given link.
GetAliases func(base lnwire.ShortChannelID) []lnwire.ShortChannelID
}
// shutdownReq contains an error channel that will be used by the channelLink
@ -581,6 +590,12 @@ func (l *channelLink) markReestablished() {
atomic.StoreInt32(&l.reestablished, 1)
}
// IsUnadvertised returns true if the underlying channel is unadvertised.
func (l *channelLink) IsUnadvertised() bool {
state := l.channel.State()
return state.ChannelFlags&lnwire.FFAnnounceChannel == 0
}
// sampleNetworkFee samples the current fee rate on the network to get into the
// chain in a timely manner. The returned value is expressed in fee-per-kw, as
// this is the native rate used when computing the fee for commitment
@ -629,14 +644,33 @@ func shouldAdjustCommitFee(netFee, chanFee,
}
}
// createFailureWithUpdate retrieves this link's last channel update message and
// passes it into the callback. It expects a fully populated failure message.
func (l *channelLink) createFailureWithUpdate(
cb func(update *lnwire.ChannelUpdate) lnwire.FailureMessage) lnwire.FailureMessage {
// failCb is used to cut down on the argument verbosity.
type failCb func(update *lnwire.ChannelUpdate) lnwire.FailureMessage
update, err := l.cfg.FetchLastChannelUpdate(l.ShortChanID())
if err != nil {
return &lnwire.FailTemporaryNodeFailure{}
// createFailureWithUpdate creates a ChannelUpdate when failing an incoming or
// outgoing HTLC. It may return a FailureMessage that references a channel's
// alias. If the channel does not have an alias, then the regular channel
// update from disk will be returned.
func (l *channelLink) createFailureWithUpdate(incoming bool,
outgoingScid lnwire.ShortChannelID, cb failCb) lnwire.FailureMessage {
// Determine which SCID to use in case we need to use aliases in the
// ChannelUpdate.
scid := outgoingScid
if incoming {
scid = l.ShortChanID()
}
// Try using the FailAliasUpdate function. If it returns nil, fallback
// to the non-alias behavior.
update := l.cfg.FailAliasUpdate(scid, incoming)
if update == nil {
// Fallback to the non-alias behavior.
var err error
update, err = l.cfg.FetchLastChannelUpdate(l.ShortChanID())
if err != nil {
return &lnwire.FailTemporaryNodeFailure{}
}
}
return cb(update)
@ -697,6 +731,28 @@ func (l *channelLink) syncChanStates() error {
fundingLockedMsg := lnwire.NewFundingLocked(
l.ChanID(), nextRevocation,
)
// For channels that negotiated the option-scid-alias
// feature bit, ensure that we send over the alias in
// the funding_locked message. We'll send the first
// alias we find for the channel since it does not
// matter which alias we send. We'll error out if no
// aliases are found.
if l.negotiatedAliasFeature() {
aliases := l.getAliases()
if len(aliases) == 0 {
// This shouldn't happen since we
// always add at least one alias before
// the channel reaches the link.
return fmt.Errorf("no aliases found")
}
// getAliases returns a copy of the alias slice
// so it is ok to use a pointer to the first
// entry.
fundingLockedMsg.AliasScid = &aliases[0]
}
err = l.cfg.Peer.SendMessage(false, fundingLockedMsg)
if err != nil {
return fmt.Errorf("unable to re-send "+
@ -2243,36 +2299,14 @@ func (l *channelLink) UpdateShortChanID() (lnwire.ShortChannelID, error) {
// Refresh the channel state's short channel ID by loading it from disk.
// This ensures that the channel state accurately reflects the updated
// short channel ID.
err := l.channel.State().RefreshShortChanID()
err := l.channel.State().Refresh()
if err != nil {
l.log.Errorf("unable to refresh short_chan_id for chan_id=%v: "+
"%v", chanID, err)
return hop.Source, err
}
sid := l.channel.ShortChanID()
l.log.Infof("updating to short_chan_id=%v for chan_id=%v", sid, chanID)
l.Lock()
l.shortChanID = sid
l.Unlock()
go func() {
err := l.cfg.UpdateContractSignals(&contractcourt.ContractSignals{
ShortChanID: sid,
})
if err != nil {
l.log.Errorf("unable to update signals")
}
}()
// Now that the short channel ID has been properly updated, we can begin
// garbage collecting any forwarding packages we create.
l.wg.Add(1)
go l.fwdPkgGarbager()
return sid, nil
return hop.Source, nil
}
// ChanID returns the channel ID for the channel link. The channel ID is a more
@ -2362,6 +2396,58 @@ func dustHelper(chantype channeldb.ChannelType, localDustLimit,
return isDust
}
// zeroConfConfirmed returns whether or not the zero-conf channel has
// confirmed on-chain.
//
// Part of the scidAliasHandler interface.
func (l *channelLink) zeroConfConfirmed() bool {
return l.channel.State().ZeroConfConfirmed()
}
// confirmedScid returns the confirmed SCID for a zero-conf channel. This
// should not be called for non-zero-conf channels.
//
// Part of the scidAliasHandler interface.
func (l *channelLink) confirmedScid() lnwire.ShortChannelID {
return l.channel.State().ZeroConfRealScid()
}
// isZeroConf returns whether or not the underlying channel is a zero-conf
// channel.
//
// Part of the scidAliasHandler interface.
func (l *channelLink) isZeroConf() bool {
return l.channel.State().IsZeroConf()
}
// negotiatedAliasFeature returns whether or not the underlying channel has
// negotiated the option-scid-alias feature bit. This will be true for both
// option-scid-alias and zero-conf channel-types. It will also be true for
// channels with the feature bit but without the above channel-types.
//
// Part of the scidAliasFeature interface.
func (l *channelLink) negotiatedAliasFeature() bool {
return l.channel.State().NegotiatedAliasFeature()
}
// getAliases returns the set of aliases for the underlying channel.
//
// Part of the scidAliasHandler interface.
func (l *channelLink) getAliases() []lnwire.ShortChannelID {
return l.cfg.GetAliases(l.ShortChanID())
}
// attachFailAliasUpdate sets the link's FailAliasUpdate function.
//
// Part of the scidAliasHandler interface.
func (l *channelLink) attachFailAliasUpdate(closure func(
sid lnwire.ShortChannelID, incoming bool) *lnwire.ChannelUpdate) {
l.Lock()
l.cfg.FailAliasUpdate = closure
l.Unlock()
}
// AttachMailBox updates the current mailbox used by this link, and hooks up
// the mailbox's message and packet outboxes to the link's upstream and
// downstream chans, respectively.
@ -2405,7 +2491,7 @@ func (l *channelLink) UpdateForwardingPolicy(newPolicy ForwardingPolicy) {
func (l *channelLink) CheckHtlcForward(payHash [32]byte,
incomingHtlcAmt, amtToForward lnwire.MilliSatoshi,
incomingTimeout, outgoingTimeout uint32,
heightNow uint32) *LinkError {
heightNow uint32, originalScid lnwire.ShortChannelID) *LinkError {
l.RLock()
policy := l.cfg.FwrdingPolicy
@ -2414,6 +2500,7 @@ func (l *channelLink) CheckHtlcForward(payHash [32]byte,
// First check whether the outgoing htlc satisfies the channel policy.
err := l.canSendHtlc(
policy, payHash, amtToForward, outgoingTimeout, heightNow,
originalScid,
)
if err != nil {
return err
@ -2437,13 +2524,10 @@ func (l *channelLink) CheckHtlcForward(payHash [32]byte,
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up to date data.
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewFeeInsufficient(
amtToForward, *upd,
)
},
)
cb := func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewFeeInsufficient(amtToForward, *upd)
}
failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewLinkError(failure)
}
@ -2459,13 +2543,12 @@ func (l *channelLink) CheckHtlcForward(payHash [32]byte,
// Grab the latest routing policy so the sending node is up to
// date with our current policy.
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewIncorrectCltvExpiry(
incomingTimeout, *upd,
)
},
)
cb := func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewIncorrectCltvExpiry(
incomingTimeout, *upd,
)
}
failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewLinkError(failure)
}
@ -2485,8 +2568,11 @@ func (l *channelLink) CheckHtlcTransit(payHash [32]byte,
policy := l.cfg.FwrdingPolicy
l.RUnlock()
// We pass in hop.Source here as this is only used in the Switch when
// trying to send over a local link. This causes the fallback mechanism
// to occur.
return l.canSendHtlc(
policy, payHash, amt, timeout, heightNow,
policy, payHash, amt, timeout, heightNow, hop.Source,
)
}
@ -2494,7 +2580,7 @@ func (l *channelLink) CheckHtlcTransit(payHash [32]byte,
// the channel's amount and time lock constraints.
func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
payHash [32]byte, amt lnwire.MilliSatoshi, timeout uint32,
heightNow uint32) *LinkError {
heightNow uint32, originalScid lnwire.ShortChannelID) *LinkError {
// As our first sanity check, we'll ensure that the passed HTLC isn't
// too small for the next hop. If so, then we'll cancel the HTLC
@ -2506,13 +2592,10 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up to date data.
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewAmountBelowMinimum(
amt, *upd,
)
},
)
cb := func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewAmountBelowMinimum(amt, *upd)
}
failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewLinkError(failure)
}
@ -2524,11 +2607,10 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
// As part of the returned error, we'll send our latest routing
// policy so the sending node obtains the most up-to-date data.
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure(upd)
},
)
cb := func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure(upd)
}
failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewDetailedLinkError(failure, OutgoingFailureHTLCExceedsMax)
}
@ -2539,11 +2621,11 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
l.log.Warnf("htlc(%x) has an expiry that's too soon: "+
"outgoing_expiry=%v, best_height=%v", payHash[:],
timeout, heightNow)
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewExpiryTooSoon(*upd)
},
)
cb := func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewExpiryTooSoon(*upd)
}
failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewLinkError(failure)
}
@ -2560,11 +2642,10 @@ func (l *channelLink) canSendHtlc(policy ForwardingPolicy,
if amt > l.Bandwidth() {
l.log.Warnf("insufficient bandwidth to route htlc: %v is "+
"larger than %v", amt, l.Bandwidth())
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure(upd)
},
)
cb := func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure(upd)
}
failure := l.createFailureWithUpdate(false, originalScid, cb)
return NewDetailedLinkError(
failure, OutgoingFailureInsufficientBalance,
)
@ -3009,12 +3090,12 @@ func (l *channelLink) processRemoteAdds(fwdPkg *channeldb.FwdPkg,
l.log.Errorf("unable to encode the "+
"remaining route %v", err)
cb := func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure(upd)
}
failure := l.createFailureWithUpdate(
func(upd *lnwire.ChannelUpdate) lnwire.FailureMessage {
return lnwire.NewTemporaryChannelFailure(
upd,
)
},
true, hop.Source, cb,
)
l.sendHTLCError(

View File

@ -1874,6 +1874,12 @@ func newSingleLinkTestHarness(chanAmt, chanReserve btcutil.Amount) (
return nil
}
getAliases := func(
base lnwire.ShortChannelID) []lnwire.ShortChannelID {
return nil
}
// Instantiate with a long interval, so that we can precisely control
// the firing via force feeding.
bticker := ticker.NewForce(time.Hour)
@ -1917,6 +1923,7 @@ func newSingleLinkTestHarness(chanAmt, chanReserve btcutil.Amount) (
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
HtlcNotifier: aliceSwitch.cfg.HtlcNotifier,
GetAliases: getAliases,
}
aliceLink := NewChannelLink(aliceCfg, aliceLc.channel)
@ -4325,6 +4332,12 @@ func (h *persistentLinkHarness) restartLink(
return nil
}
getAliases := func(
base lnwire.ShortChannelID) []lnwire.ShortChannelID {
return nil
}
// Instantiate with a long interval, so that we can precisely control
// the firing via force feeding.
bticker := ticker.NewForce(time.Hour)
@ -4371,6 +4384,7 @@ func (h *persistentLinkHarness) restartLink(
NotifyInactiveChannel: func(wire.OutPoint) {},
HtlcNotifier: aliceSwitch.cfg.HtlcNotifier,
SyncStates: syncStates,
GetAliases: getAliases,
}
aliceLink := NewChannelLink(aliceCfg, aliceChannel)
@ -5571,6 +5585,12 @@ func TestCheckHtlcForward(t *testing.T) {
return &lnwire.ChannelUpdate{}, nil
}
failAliasUpdate := func(sid lnwire.ShortChannelID,
incoming bool) *lnwire.ChannelUpdate {
return nil
}
testChannel, _, fCleanUp, err := createTestChannel(
alicePrivKey, bobPrivKey, 100000, 100000,
1000, 1000, lnwire.ShortChannelID{},
@ -5596,11 +5616,13 @@ func TestCheckHtlcForward(t *testing.T) {
channel: testChannel.channel,
}
link.attachFailAliasUpdate(failAliasUpdate)
var hash [32]byte
t.Run("satisfied", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 150, 0)
200, 150, 0, lnwire.ShortChannelID{})
if result != nil {
t.Fatalf("expected policy to be satisfied")
}
@ -5608,7 +5630,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("below minhtlc", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 100, 50,
200, 150, 0)
200, 150, 0, lnwire.ShortChannelID{})
if _, ok := result.WireMessage().(*lnwire.FailAmountBelowMinimum); !ok {
t.Fatalf("expected FailAmountBelowMinimum failure code")
}
@ -5616,7 +5638,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("above maxhtlc", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1200,
200, 150, 0)
200, 150, 0, lnwire.ShortChannelID{})
if _, ok := result.WireMessage().(*lnwire.FailTemporaryChannelFailure); !ok {
t.Fatalf("expected FailTemporaryChannelFailure failure code")
}
@ -5624,7 +5646,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("insufficient fee", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1005, 1000,
200, 150, 0)
200, 150, 0, lnwire.ShortChannelID{})
if _, ok := result.WireMessage().(*lnwire.FailFeeInsufficient); !ok {
t.Fatalf("expected FailFeeInsufficient failure code")
}
@ -5632,7 +5654,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("expiry too soon", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 150, 190)
200, 150, 190, lnwire.ShortChannelID{})
if _, ok := result.WireMessage().(*lnwire.FailExpiryTooSoon); !ok {
t.Fatalf("expected FailExpiryTooSoon failure code")
}
@ -5640,7 +5662,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("incorrect cltv expiry", func(t *testing.T) {
result := link.CheckHtlcForward(hash, 1500, 1000,
200, 190, 0)
200, 190, 0, lnwire.ShortChannelID{})
if _, ok := result.WireMessage().(*lnwire.FailIncorrectCltvExpiry); !ok {
t.Fatalf("expected FailIncorrectCltvExpiry failure code")
}
@ -5650,7 +5672,7 @@ func TestCheckHtlcForward(t *testing.T) {
t.Run("cltv expiry too far in the future", func(t *testing.T) {
// Check that expiry isn't too far in the future.
result := link.CheckHtlcForward(hash, 1500, 1000,
10200, 10100, 0)
10200, 10100, 0, lnwire.ShortChannelID{})
if _, ok := result.WireMessage().(*lnwire.FailExpiryTooFar); !ok {
t.Fatalf("expected FailExpiryTooFar failure code")
}

View File

@ -91,10 +91,6 @@ type mailBoxConfig struct {
// belongs to.
shortChanID lnwire.ShortChannelID
// fetchUpdate retrieves the most recent channel update for the channel
// this mailbox belongs to.
fetchUpdate func(lnwire.ShortChannelID) (*lnwire.ChannelUpdate, error)
// forwardPackets send a varidic number of htlcPackets to the switch to
// be routed. A quit channel should be provided so that the call can
// properly exit during shutdown.
@ -107,6 +103,11 @@ type mailBoxConfig struct {
// have not been yet been delivered. The computed deadline will expiry
// this long after the Adds are added via AddPacket.
expiry time.Duration
// failMailboxUpdate is used to fail an expired HTLC and use the
// correct SCID if the underlying channel uses aliases.
failMailboxUpdate func(outScid,
mailboxScid lnwire.ShortChannelID) lnwire.FailureMessage
}
// memoryMailBox is an implementation of the MailBox struct backed by purely
@ -710,13 +711,9 @@ func (m *memoryMailBox) FailAdd(pkt *htlcPacket) {
// Create a temporary channel failure which we will send back to our
// peer if this is a forward, or report to the user if the failed
// payment was locally initiated.
var failure lnwire.FailureMessage
update, err := m.cfg.fetchUpdate(m.cfg.shortChanID)
if err != nil {
failure = &lnwire.FailTemporaryNodeFailure{}
} else {
failure = lnwire.NewTemporaryChannelFailure(update)
}
failure := m.cfg.failMailboxUpdate(
pkt.originalOutgoingChanID, m.cfg.shortChanID,
)
// If the payment was locally initiated (which is indicated by a nil
// obfuscator), we do not need to encrypt it back to the sender.
@ -817,10 +814,6 @@ type mailOrchConfig struct {
// properly exit during shutdown.
forwardPackets func(chan struct{}, ...*htlcPacket) error
// fetchUpdate retrieves the most recent channel update for the channel
// this mailbox belongs to.
fetchUpdate func(lnwire.ShortChannelID) (*lnwire.ChannelUpdate, error)
// clock is a time source for the generated mailboxes.
clock clock.Clock
@ -828,6 +821,11 @@ type mailOrchConfig struct {
// have not been yet been delivered. The computed deadline will expiry
// this long after the Adds are added to a mailbox via AddPacket.
expiry time.Duration
// failMailboxUpdate is used to fail an expired HTLC and use the
// correct SCID if the underlying channel uses aliases.
failMailboxUpdate func(outScid,
mailboxScid lnwire.ShortChannelID) lnwire.FailureMessage
}
// newMailOrchestrator initializes a fresh mailOrchestrator.
@ -881,11 +879,11 @@ func (mo *mailOrchestrator) exclusiveGetOrCreateMailBox(
mailbox, ok := mo.mailboxes[chanID]
if !ok {
mailbox = newMemoryMailBox(&mailBoxConfig{
shortChanID: shortChanID,
fetchUpdate: mo.cfg.fetchUpdate,
forwardPackets: mo.cfg.forwardPackets,
clock: mo.cfg.clock,
expiry: mo.cfg.expiry,
shortChanID: shortChanID,
forwardPackets: mo.cfg.forwardPackets,
clock: mo.cfg.clock,
expiry: mo.cfg.expiry,
failMailboxUpdate: mo.cfg.failMailboxUpdate,
})
mailbox.Start()
mo.mailboxes[chanID] = mailbox

View File

@ -201,17 +201,18 @@ func newMailboxContext(t *testing.T, startTime time.Time,
clock: clock.NewTestClock(startTime),
forwards: make(chan *htlcPacket, 1),
}
ctx.mailbox = newMemoryMailBox(&mailBoxConfig{
fetchUpdate: func(sid lnwire.ShortChannelID) (
*lnwire.ChannelUpdate, error) {
return &lnwire.ChannelUpdate{
ShortChannelID: sid,
}, nil
},
forwardPackets: ctx.forward,
clock: ctx.clock,
expiry: expiry,
failMailboxUpdate := func(outScid,
mboxScid lnwire.ShortChannelID) lnwire.FailureMessage {
return &lnwire.FailTemporaryNodeFailure{}
}
ctx.mailbox = newMemoryMailBox(&mailBoxConfig{
failMailboxUpdate: failMailboxUpdate,
forwardPackets: ctx.forward,
clock: ctx.clock,
expiry: expiry,
})
ctx.mailbox.Start()
@ -660,15 +661,15 @@ func testMailBoxDust(t *testing.T, chantype channeldb.ChannelType) {
func TestMailOrchestrator(t *testing.T) {
t.Parallel()
failMailboxUpdate := func(outScid,
mboxScid lnwire.ShortChannelID) lnwire.FailureMessage {
return &lnwire.FailTemporaryNodeFailure{}
}
// First, we'll create a new instance of our orchestrator.
mo := newMailOrchestrator(&mailOrchConfig{
fetchUpdate: func(sid lnwire.ShortChannelID) (
*lnwire.ChannelUpdate, error) {
return &lnwire.ChannelUpdate{
ShortChannelID: sid,
}, nil
},
failMailboxUpdate: failMailboxUpdate,
forwardPackets: func(_ chan struct{},
pkts ...*htlcPacket) error {

View File

@ -15,6 +15,7 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/go-errors/errors"
@ -33,6 +34,10 @@ import (
"github.com/lightningnetwork/lnd/ticker"
)
func isAlias(scid lnwire.ShortChannelID) bool {
return scid.BlockHeight >= 16_000_000 && scid.BlockHeight < 16_250_000
}
type mockPreimageCache struct {
sync.Mutex
preimageMap map[lntypes.Hash]lntypes.Preimage
@ -180,6 +185,12 @@ func initSwitchWithDB(startingHeight uint32, db *channeldb.DB) (*Switch, error)
}
}
signAliasUpdate := func(u *lnwire.ChannelUpdate) (*ecdsa.Signature,
error) {
return testSig, nil
}
cfg := Config{
DB: db,
FetchAllOpenChannels: db.ChannelStateDB().FetchAllOpenChannels,
@ -188,21 +199,27 @@ func initSwitchWithDB(startingHeight uint32, db *channeldb.DB) (*Switch, error)
FwdingLog: &mockForwardingLog{
events: make(map[time.Time]channeldb.ForwardingEvent),
},
FetchLastChannelUpdate: func(lnwire.ShortChannelID) (*lnwire.ChannelUpdate, error) {
return &lnwire.ChannelUpdate{}, nil
FetchLastChannelUpdate: func(scid lnwire.ShortChannelID) (
*lnwire.ChannelUpdate, error) {
return &lnwire.ChannelUpdate{
ShortChannelID: scid,
}, nil
},
Notifier: &mock.ChainNotifier{
SpendChan: make(chan *chainntnfs.SpendDetail),
EpochChan: make(chan *chainntnfs.BlockEpoch),
ConfChan: make(chan *chainntnfs.TxConfirmation),
},
FwdEventTicker: ticker.NewForce(DefaultFwdEventInterval),
LogEventTicker: ticker.NewForce(DefaultLogInterval),
AckEventTicker: ticker.NewForce(DefaultAckInterval),
HtlcNotifier: &mockHTLCNotifier{},
Clock: clock.NewDefaultClock(),
HTLCExpiry: time.Hour,
DustThreshold: DefaultDustThreshold,
FwdEventTicker: ticker.NewForce(DefaultFwdEventInterval),
LogEventTicker: ticker.NewForce(DefaultLogInterval),
AckEventTicker: ticker.NewForce(DefaultAckInterval),
HtlcNotifier: &mockHTLCNotifier{},
Clock: clock.NewDefaultClock(),
HTLCExpiry: time.Hour,
DustThreshold: DefaultDustThreshold,
SignAliasUpdate: signAliasUpdate,
IsAlias: isAlias,
}
return New(cfg, startingHeight)
@ -658,6 +675,11 @@ type mockChannelLink struct {
shortChanID lnwire.ShortChannelID
// Only used for zero-conf channels.
realScid lnwire.ShortChannelID
aliases []lnwire.ShortChannelID
chanID lnwire.ChannelID
peer lnpeer.Peer
@ -668,11 +690,22 @@ type mockChannelLink struct {
eligible bool
unadvertised bool
zeroConf bool
optionFeature bool
htlcID uint64
checkHtlcTransitResult *LinkError
checkHtlcForwardResult *LinkError
failAliasUpdate func(sid lnwire.ShortChannelID,
incoming bool) *lnwire.ChannelUpdate
confirmedZC bool
}
// completeCircuit is a helper method for adding the finalized payment circuit
@ -712,16 +745,39 @@ func (f *mockChannelLink) deleteCircuit(pkt *htlcPacket) error {
}
func newMockChannelLink(htlcSwitch *Switch, chanID lnwire.ChannelID,
shortChanID lnwire.ShortChannelID, peer lnpeer.Peer, eligible bool,
shortChanID, realScid lnwire.ShortChannelID, peer lnpeer.Peer,
eligible, unadvertised, zeroConf, optionFeature bool,
) *mockChannelLink {
return &mockChannelLink{
htlcSwitch: htlcSwitch,
chanID: chanID,
shortChanID: shortChanID,
peer: peer,
eligible: eligible,
aliases := make([]lnwire.ShortChannelID, 0)
var realConfirmed bool
if zeroConf {
aliases = append(aliases, shortChanID)
}
if realScid != hop.Source {
realConfirmed = true
}
return &mockChannelLink{
htlcSwitch: htlcSwitch,
chanID: chanID,
shortChanID: shortChanID,
realScid: realScid,
peer: peer,
eligible: eligible,
unadvertised: unadvertised,
zeroConf: zeroConf,
optionFeature: optionFeature,
aliases: aliases,
confirmedZC: realConfirmed,
}
}
// addAlias is not part of any interface method.
func (f *mockChannelLink) addAlias(alias lnwire.ShortChannelID) {
f.aliases = append(f.aliases, alias)
}
func (f *mockChannelLink) handleSwitchPacket(pkt *htlcPacket) error {
@ -750,7 +806,8 @@ func (f *mockChannelLink) HandleChannelUpdate(lnwire.Message) {
func (f *mockChannelLink) UpdateForwardingPolicy(_ ForwardingPolicy) {
}
func (f *mockChannelLink) CheckHtlcForward([32]byte, lnwire.MilliSatoshi,
lnwire.MilliSatoshi, uint32, uint32, uint32) *LinkError {
lnwire.MilliSatoshi, uint32, uint32, uint32,
lnwire.ShortChannelID) *LinkError {
return f.checkHtlcForwardResult
}
@ -772,6 +829,32 @@ func (f *mockChannelLink) AttachMailBox(mailBox MailBox) {
mailBox.SetDustClosure(f.getDustClosure())
}
func (f *mockChannelLink) attachFailAliasUpdate(closure func(
sid lnwire.ShortChannelID, incoming bool) *lnwire.ChannelUpdate) {
f.failAliasUpdate = closure
}
func (f *mockChannelLink) getAliases() []lnwire.ShortChannelID {
return f.aliases
}
func (f *mockChannelLink) isZeroConf() bool {
return f.zeroConf
}
func (f *mockChannelLink) negotiatedAliasFeature() bool {
return f.optionFeature
}
func (f *mockChannelLink) confirmedScid() lnwire.ShortChannelID {
return f.realScid
}
func (f *mockChannelLink) zeroConfConfirmed() bool {
return f.confirmedZC
}
func (f *mockChannelLink) Start() error {
f.mailBox.ResetMessages()
f.mailBox.ResetPackets()
@ -788,6 +871,7 @@ func (f *mockChannelLink) EligibleToForward() bool { return
func (f *mockChannelLink) MayAddOutgoingHtlc(lnwire.MilliSatoshi) error { return nil }
func (f *mockChannelLink) ShutdownIfChannelClean() error { return nil }
func (f *mockChannelLink) setLiveShortChanID(sid lnwire.ShortChannelID) { f.shortChanID = sid }
func (f *mockChannelLink) IsUnadvertised() bool { return f.unadvertised }
func (f *mockChannelLink) UpdateShortChanID() (lnwire.ShortChannelID, error) {
f.eligible = true
return f.shortChanID, nil

View File

@ -96,6 +96,13 @@ type htlcPacket struct {
// customRecords are user-defined records in the custom type range that
// were included in the payload.
customRecords record.CustomSet
// originalOutgoingChanID is used when sending back failure messages.
// It is only used for forwarded Adds on option_scid_alias channels.
// This is to avoid possible confusion if a payer uses the public SCID
// but receives a channel_update with the alias SCID. Instead, the
// payer should receive a channel_update with the public SCID.
originalOutgoingChanID lnwire.ShortChannelID
}
// inKey returns the circuit key used to identify the incoming htlc.

View File

@ -9,6 +9,7 @@ import (
"sync/atomic"
"time"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
@ -200,6 +201,16 @@ type Config struct {
// DustThreshold is the threshold in milli-satoshis after which we'll
// fail incoming or outgoing dust payments for a particular channel.
DustThreshold lnwire.MilliSatoshi
// SignAliasUpdate is used when sending FailureMessages backwards for
// option_scid_alias channels. This avoids a potential privacy leak by
// replacing the public, confirmed SCID with the alias in the
// ChannelUpdate.
SignAliasUpdate func(u *lnwire.ChannelUpdate) (*ecdsa.Signature,
error)
// IsAlias returns whether or not a given SCID is an alias.
IsAlias func(scid lnwire.ShortChannelID) bool
}
// Switch is the central messaging bus for all incoming/outgoing HTLCs.
@ -247,8 +258,7 @@ type Switch struct {
indexMtx sync.RWMutex
// pendingLinkIndex holds links that have not had their final, live
// short_chan_id assigned. These links can be transitioned into the
// primary linkIndex by using UpdateShortChanID to load their live id.
// short_chan_id assigned.
pendingLinkIndex map[lnwire.ChannelID]ChannelLink
// links is a map of channel id and channel link which manages
@ -311,6 +321,21 @@ type Switch struct {
// contractcourt. This is used so the Switch can properly forward them,
// even on restarts.
resMsgStore *resolutionStore
// aliasToReal is a map used for option-scid-alias feature-bit links.
// The alias SCID is the key and the real, confirmed SCID is the value.
// If the channel is unconfirmed, there will not be a mapping for it.
// Since channels can have multiple aliases, this map is essentially a
// N->1 mapping for a channel. This MUST be accessed with the indexMtx.
aliasToReal map[lnwire.ShortChannelID]lnwire.ShortChannelID
// baseIndex is a map used for option-scid-alias feature-bit links.
// The value is the SCID of the link's ShortChannelID. This value may
// be an alias for zero-conf channels or a confirmed SCID for
// non-zero-conf channels with the option-scid-alias feature-bit. The
// key includes the value itself and also any other aliases. This MUST
// be accessed with the indexMtx.
baseIndex map[lnwire.ShortChannelID]lnwire.ShortChannelID
}
// New creates the new instance of htlc switch.
@ -345,11 +370,14 @@ func New(cfg Config, currentHeight uint32) (*Switch, error) {
quit: make(chan struct{}),
}
s.aliasToReal = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID)
s.baseIndex = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID)
s.mailOrchestrator = newMailOrchestrator(&mailOrchConfig{
fetchUpdate: s.cfg.FetchLastChannelUpdate,
forwardPackets: s.ForwardPackets,
clock: s.cfg.Clock,
expiry: s.cfg.HTLCExpiry,
forwardPackets: s.ForwardPackets,
clock: s.cfg.Clock,
expiry: s.cfg.HTLCExpiry,
failMailboxUpdate: s.failMailboxUpdate,
})
return s, nil
@ -725,14 +753,28 @@ func (s *Switch) ForwardPackets(linkQuit chan struct{},
// failures.
if len(failedPackets) > 0 {
var failure lnwire.FailureMessage
update, err := s.cfg.FetchLastChannelUpdate(
failedPackets[0].incomingChanID,
)
if err != nil {
failure = &lnwire.FailTemporaryNodeFailure{}
incomingID := failedPackets[0].incomingChanID
// If the incoming channel is an option_scid_alias channel,
// then we'll need to replace the SCID in the ChannelUpdate.
update := s.failAliasUpdate(incomingID, true)
if update == nil {
// Fallback to the original non-option behavior.
update, err := s.cfg.FetchLastChannelUpdate(
incomingID,
)
if err != nil {
failure = &lnwire.FailTemporaryNodeFailure{}
} else {
failure = lnwire.NewTemporaryChannelFailure(
update,
)
}
} else {
// This is an option_scid_alias channel.
failure = lnwire.NewTemporaryChannelFailure(update)
}
linkError := NewDetailedLinkError(
failure, OutgoingFailureIncompleteForward,
)
@ -804,10 +846,29 @@ func (s *Switch) getLocalLink(pkt *htlcPacket, htlc *lnwire.UpdateAddHTLC) (
// Try to find links by node destination.
s.indexMtx.RLock()
link, err := s.getLinkByShortID(pkt.outgoingChanID)
s.indexMtx.RUnlock()
defer s.indexMtx.RUnlock()
if err != nil {
log.Errorf("Link %v not found", pkt.outgoingChanID)
return nil, NewLinkError(&lnwire.FailUnknownNextPeer{})
// If the link was not found for the outgoingChanID, an outside
// subsystem may be using the confirmed SCID of a zero-conf
// channel. In this case, we'll consult the Switch maps to see
// if an alias exists and use the alias to lookup the link.
// This extra step is a consequence of not updating the Switch
// forwardingIndex when a zero-conf channel is confirmed. We
// don't need to change the outgoingChanID since the link will
// do that upon receiving the packet.
baseScid, ok := s.baseIndex[pkt.outgoingChanID]
if !ok {
log.Errorf("Link %v not found", pkt.outgoingChanID)
return nil, NewLinkError(&lnwire.FailUnknownNextPeer{})
}
// The base SCID was found, so we'll use that to fetch the
// link.
link, err = s.getLinkByShortID(baseScid)
if err != nil {
log.Errorf("Link %v not found", baseScid)
return nil, NewLinkError(&lnwire.FailUnknownNextPeer{})
}
}
if !link.EligibleToForward() {
@ -1043,8 +1104,11 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
// same incoming and outgoing channel. If our node does not
// allow forwards of this nature, we fail the htlc early. This
// check is in place to disallow inefficiently routed htlcs from
// locking up our balance.
linkErr := checkCircularForward(
// locking up our balance. With channels where the
// option-scid-alias feature was negotiated, we also have to be
// sure that the IDs aren't the same since one or both could be
// an alias.
linkErr := s.checkCircularForward(
packet.incomingChanID, packet.outgoingChanID,
s.cfg.AllowCircularRoute, htlc.PaymentHash,
)
@ -1053,7 +1117,7 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
}
s.indexMtx.RLock()
targetLink, err := s.getLinkByShortID(packet.outgoingChanID)
targetLink, err := s.getLinkByMapping(packet)
if err != nil {
s.indexMtx.RUnlock()
@ -1101,6 +1165,7 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
htlc.PaymentHash, packet.incomingAmount,
packet.amount, packet.incomingTimeout,
packet.outgoingTimeout, currentHeight,
packet.originalOutgoingChanID,
)
}
@ -1306,12 +1371,51 @@ func (s *Switch) handlePacketForward(packet *htlcPacket) error {
// checkCircularForward checks whether a forward is circular (arrives and
// departs on the same link) and returns a link error if the switch is
// configured to disallow this behaviour.
func checkCircularForward(incoming, outgoing lnwire.ShortChannelID,
func (s *Switch) checkCircularForward(incoming, outgoing lnwire.ShortChannelID,
allowCircular bool, paymentHash lntypes.Hash) *LinkError {
// If the route is not circular we do not need to perform any further
// checks.
if incoming != outgoing {
// If they are equal, we can skip the alias mapping checks.
if incoming == outgoing {
// The switch may be configured to allow circular routes, so
// just log and return nil.
if allowCircular {
log.Debugf("allowing circular route over link: %v "+
"(payment hash: %x)", incoming, paymentHash)
return nil
}
// Otherwise, we'll return a temporary channel failure.
return NewDetailedLinkError(
lnwire.NewTemporaryChannelFailure(nil),
OutgoingFailureCircularRoute,
)
}
// We'll fetch the "base" SCID from the baseIndex for the incoming and
// outgoing SCIDs. If either one does not have a base SCID, then the
// two channels are not equal since one will be a channel that does not
// need a mapping and SCID equality was checked above. If the "base"
// SCIDs are equal, then this is a circular route. Otherwise, it isn't.
s.indexMtx.RLock()
incomingBaseScid, ok := s.baseIndex[incoming]
if !ok {
// This channel does not use baseIndex, bail out.
s.indexMtx.RUnlock()
return nil
}
outgoingBaseScid, ok := s.baseIndex[outgoing]
if !ok {
// This channel does not use baseIndex, bail out.
s.indexMtx.RUnlock()
return nil
}
s.indexMtx.RUnlock()
// Check base SCID equality.
if incomingBaseScid != outgoingBaseScid {
// The base SCIDs are not equal so these are not the same
// channel.
return nil
}
@ -2170,6 +2274,9 @@ func (s *Switch) AddLink(link ChannelLink) error {
mailbox := s.mailOrchestrator.GetOrCreateMailBox(chanID, shortChanID)
link.AttachMailBox(mailbox)
// Attach the Switch's failAliasUpdate function to the link.
link.attachFailAliasUpdate(s.failAliasUpdate)
if err := link.Start(); err != nil {
s.removeLink(chanID)
return err
@ -2196,12 +2303,14 @@ func (s *Switch) AddLink(link ChannelLink) error {
// addLiveLink adds a link to all associated forwarding index, this makes it a
// candidate for forwarding HTLCs.
func (s *Switch) addLiveLink(link ChannelLink) {
linkScid := link.ShortChanID()
// We'll add the link to the linkIndex which lets us quickly
// look up a channel when we need to close or register it, and
// the forwarding index which'll be used when forwarding HTLC's
// in the multi-hop setting.
s.linkIndex[link.ChanID()] = link
s.forwardingIndex[link.ShortChanID()] = link
s.forwardingIndex[linkScid] = link
// Next we'll add the link to the interface index so we can
// quickly look up all the channels for a particular node.
@ -2210,6 +2319,42 @@ func (s *Switch) addLiveLink(link ChannelLink) {
s.interfaceIndex[peerPub] = make(map[lnwire.ChannelID]ChannelLink)
}
s.interfaceIndex[peerPub][link.ChanID()] = link
aliases := link.getAliases()
if link.isZeroConf() {
if link.zeroConfConfirmed() {
// Since the zero-conf channel has confirmed, we can
// populate the aliasToReal mapping.
confirmedScid := link.confirmedScid()
for _, alias := range aliases {
s.aliasToReal[alias] = confirmedScid
}
// Add the confirmed SCID as a key in the baseIndex.
s.baseIndex[confirmedScid] = linkScid
}
// Now we populate the baseIndex which will be used to fetch
// the link given any of the channel's alias SCIDs or the real
// SCID. The link's SCID is an alias, so we don't need to
// special-case it like the option-scid-alias feature-bit case
// further down.
for _, alias := range aliases {
s.baseIndex[alias] = linkScid
}
} else if link.negotiatedAliasFeature() {
// The link's SCID is the confirmed SCID for non-zero-conf
// option-scid-alias feature bit channels.
for _, alias := range aliases {
s.aliasToReal[alias] = linkScid
s.baseIndex[alias] = linkScid
}
// Since the link's SCID is confirmed, it was not included in
// the baseIndex above as a key. Add it now.
s.baseIndex[linkScid] = linkScid
}
}
// GetLink is used to initiate the handling of the get link command. The
@ -2245,7 +2390,21 @@ func (s *Switch) GetLinkByShortID(chanID lnwire.ShortChannelID) (ChannelLink,
s.indexMtx.RLock()
defer s.indexMtx.RUnlock()
return s.getLinkByShortID(chanID)
link, err := s.getLinkByShortID(chanID)
if err != nil {
// If we failed to find the link under the passed-in SCID, we
// consult the Switch's baseIndex map to see if the confirmed
// SCID was used for a zero-conf channel.
aliasID, ok := s.baseIndex[chanID]
if !ok {
return nil, err
}
// An alias was found, use it to lookup if a link exists.
return s.getLinkByShortID(aliasID)
}
return link, nil
}
// getLinkByShortID attempts to return the link which possesses the target
@ -2261,6 +2420,93 @@ func (s *Switch) getLinkByShortID(chanID lnwire.ShortChannelID) (ChannelLink, er
return link, nil
}
// getLinkByMapping attempts to fetch the link via the htlcPacket's
// outgoingChanID, possibly using a mapping. If it finds the link via mapping,
// the outgoingChanID will be changed so that an error can be properly
// attributed when looping over linkErrs in handlePacketForward.
//
// * If the outgoingChanID is an alias, we'll fetch the link regardless if it's
// public or not.
//
// * If the outgoingChanID is a confirmed SCID, we'll need to do more checks.
// - If there is no entry found in baseIndex, fetch the link. This channel
// did not have the option-scid-alias feature negotiated (which includes
// zero-conf and option-scid-alias channel-types).
// - If there is an entry found, fetch the link from forwardingIndex and
// fail if this is a private link.
//
// NOTE: This MUST be called with the indexMtx read lock held.
func (s *Switch) getLinkByMapping(pkt *htlcPacket) (ChannelLink, error) {
// Determine if this ShortChannelID is an alias or a confirmed SCID.
chanID := pkt.outgoingChanID
aliasID := s.cfg.IsAlias(chanID)
// Set the originalOutgoingChanID so the proper channel_update can be
// sent back if the option-scid-alias feature bit was negotiated.
pkt.originalOutgoingChanID = chanID
if aliasID {
// Since outgoingChanID is an alias, we'll fetch the link via
// baseIndex.
baseScid, ok := s.baseIndex[chanID]
if !ok {
// No mapping exists, bail.
return nil, ErrChannelLinkNotFound
}
// A mapping exists, so use baseScid to find the link in the
// forwardingIndex.
link, ok := s.forwardingIndex[baseScid]
if !ok {
// Link not found, bail.
return nil, ErrChannelLinkNotFound
}
// Change the packet's outgoingChanID field so that errors are
// properly attributed.
pkt.outgoingChanID = baseScid
// Return the link without checking if it's private or not.
return link, nil
}
// The outgoingChanID is a confirmed SCID. Attempt to fetch the base
// SCID from baseIndex.
baseScid, ok := s.baseIndex[chanID]
if !ok {
// outgoingChanID is not a key in base index meaning this
// channel did not have the option-scid-alias feature bit
// negotiated. We'll fetch the link and return it.
link, ok := s.forwardingIndex[chanID]
if !ok {
// The link wasn't found, bail out.
return nil, ErrChannelLinkNotFound
}
return link, nil
}
// Fetch the link whose internal SCID is baseScid.
link, ok := s.forwardingIndex[baseScid]
if !ok {
// Link wasn't found, bail out.
return nil, ErrChannelLinkNotFound
}
// If the link is unadvertised, we fail since the real SCID was used to
// forward over it and this is a channel where the option-scid-alias
// feature bit was negotiated.
if link.IsUnadvertised() {
return nil, ErrChannelLinkNotFound
}
// The link is public so the confirmed SCID can be used to forward over
// it. We'll also replace pkt's outgoingChanID field so errors can
// properly be attributed in the calling function.
pkt.outgoingChanID = baseScid
return link, nil
}
// HasActiveLink returns true if the given channel ID has a link in the link
// index AND the link is eligible to forward.
func (s *Switch) HasActiveLink(chanID lnwire.ChannelID) bool {
@ -2357,50 +2603,38 @@ func (s *Switch) removeLink(chanID lnwire.ChannelID) ChannelLink {
return link
}
// UpdateShortChanID updates the short chan ID for an existing channel. This is
// required in the case of a re-org and re-confirmation or a channel, or in the
// case that a link was added to the switch before its short chan ID was known.
// UpdateShortChanID locates the link with the passed-in chanID and updates the
// underlying channel state. This is only used in zero-conf channels to allow
// the confirmed SCID to be updated.
func (s *Switch) UpdateShortChanID(chanID lnwire.ChannelID) error {
s.indexMtx.Lock()
defer s.indexMtx.Unlock()
// Locate the target link in the pending link index. If no such link
// exists, then we will ignore the request.
link, ok := s.pendingLinkIndex[chanID]
// Locate the target link in the link index. If no such link exists,
// then we will ignore the request.
link, ok := s.linkIndex[chanID]
if !ok {
return fmt.Errorf("link %v not found", chanID)
}
oldShortChanID := link.ShortChanID()
// Try to update the link's short channel ID, returning early if this
// update failed.
shortChanID, err := link.UpdateShortChanID()
// Try to update the link's underlying channel state, returning early
// if this update failed.
_, err := link.UpdateShortChanID()
if err != nil {
return err
}
// Reject any blank short channel ids.
if shortChanID == hop.Source {
return fmt.Errorf("refusing trivial short_chan_id for chan_id=%v"+
"live link", chanID)
// Since the zero-conf channel is confirmed, we should populate the
// aliasToReal map and update the baseIndex.
aliases := link.getAliases()
confirmedScid := link.confirmedScid()
for _, alias := range aliases {
s.aliasToReal[alias] = confirmedScid
}
log.Infof("Updated short_chan_id for ChannelLink(%v): old=%v, new=%v",
chanID, oldShortChanID, shortChanID)
// Since the link was in the pending state before, we will remove it
// from the pending link index and add it to the live link index so that
// it can be available in forwarding.
delete(s.pendingLinkIndex, chanID)
s.addLiveLink(link)
// Finally, alert the mail orchestrator to the change of short channel
// ID, and deliver any unclaimed packets to the link.
mailbox := s.mailOrchestrator.GetOrCreateMailBox(chanID, shortChanID)
s.mailOrchestrator.BindLiveShortChanID(
mailbox, chanID, shortChanID,
)
s.baseIndex[confirmedScid] = link.ShortChanID()
return nil
}
@ -2569,3 +2803,205 @@ func (s *Switch) evaluateDustThreshold(link ChannelLink,
// If we reached this point, this HTLC is fine to forward.
return false
}
// failMailboxUpdate is passed to the mailbox orchestrator which in turn passes
// it to individual mailboxes. It allows the mailboxes to construct a
// FailureMessage when failing back HTLC's due to expiry and may include an
// alias in the ShortChannelID field. The outgoingScid is the SCID originally
// used in the onion. The mailboxScid is the SCID that the mailbox and link
// use. The mailboxScid is only used in the non-alias case, so it is always
// the confirmed SCID.
func (s *Switch) failMailboxUpdate(outgoingScid,
mailboxScid lnwire.ShortChannelID) lnwire.FailureMessage {
// Try to use the failAliasUpdate function in case this is a channel
// that uses aliases. If it returns nil, we'll fallback to the original
// pre-alias behavior.
update := s.failAliasUpdate(outgoingScid, false)
if update == nil {
// Execute the fallback behavior.
var err error
update, err = s.cfg.FetchLastChannelUpdate(mailboxScid)
if err != nil {
return &lnwire.FailTemporaryNodeFailure{}
}
}
return lnwire.NewTemporaryChannelFailure(update)
}
// failAliasUpdate prepares a ChannelUpdate for a failed incoming or outgoing
// HTLC on a channel where the option-scid-alias feature bit was negotiated. If
// the associated channel is not one of these, this function will return nil
// and the caller is expected to handle this properly. In this case, a return
// to the original non-alias behavior is expected.
func (s *Switch) failAliasUpdate(scid lnwire.ShortChannelID,
incoming bool) *lnwire.ChannelUpdate {
// This function does not defer the unlocking because of the database
// lookups for ChannelUpdate.
s.indexMtx.RLock()
if s.cfg.IsAlias(scid) {
// The alias SCID was used. In the incoming case this means
// the channel is zero-conf as the link sets the scid. In the
// outgoing case, the sender set the scid to use and may be
// either the alias or the confirmed one, if it exists.
realScid, ok := s.aliasToReal[scid]
if !ok {
// The real, confirmed SCID does not exist yet. Find
// the "base" SCID that the link uses via the
// baseIndex. If we can't find it, return nil. This
// means the channel is zero-conf.
baseScid, ok := s.baseIndex[scid]
s.indexMtx.RUnlock()
if !ok {
return nil
}
update, err := s.cfg.FetchLastChannelUpdate(baseScid)
if err != nil {
return nil
}
// Replace the baseScid with the passed-in alias.
update.ShortChannelID = scid
sig, err := s.cfg.SignAliasUpdate(update)
if err != nil {
return nil
}
update.Signature, err = lnwire.NewSigFromSignature(sig)
if err != nil {
return nil
}
return update
}
s.indexMtx.RUnlock()
// Fetch the SCID via the confirmed SCID and replace it with
// the alias.
update, err := s.cfg.FetchLastChannelUpdate(realScid)
if err != nil {
return nil
}
// In the incoming case, we want to ensure that we don't leak
// the UTXO in case the channel is private. In the outgoing
// case, since the alias was used, we do the same thing.
update.ShortChannelID = scid
sig, err := s.cfg.SignAliasUpdate(update)
if err != nil {
return nil
}
update.Signature, err = lnwire.NewSigFromSignature(sig)
if err != nil {
return nil
}
return update
}
// If the confirmed SCID is not in baseIndex, this is not an
// option-scid-alias or zero-conf channel.
baseScid, ok := s.baseIndex[scid]
if !ok {
s.indexMtx.RUnlock()
return nil
}
// Fetch the link so we can get an alias to use in the ShortChannelID
// of the ChannelUpdate.
link, ok := s.forwardingIndex[baseScid]
s.indexMtx.RUnlock()
if !ok {
// This should never happen, but if it does for some reason,
// fallback to the old behavior.
return nil
}
aliases := link.getAliases()
if len(aliases) == 0 {
// This should never happen, but if it does, fallback.
return nil
}
// Fetch the ChannelUpdate via the real, confirmed SCID.
update, err := s.cfg.FetchLastChannelUpdate(scid)
if err != nil {
return nil
}
// The incoming case will replace the ShortChannelID in the retrieved
// ChannelUpdate with the alias to ensure no privacy leak occurs. This
// would happen if a private non-zero-conf option-scid-alias
// feature-bit channel leaked its UTXO here rather than supplying an
// alias. In the outgoing case, the confirmed SCID was actually used
// for forwarding in the onion, so no replacement is necessary as the
// sender knows the scid.
if incoming {
// We will replace and sign the update with the first alias.
// Since this happens on the incoming side, it's not actually
// possible to know what the sender used in the onion.
update.ShortChannelID = aliases[0]
sig, err := s.cfg.SignAliasUpdate(update)
if err != nil {
return nil
}
update.Signature, err = lnwire.NewSigFromSignature(sig)
if err != nil {
return nil
}
}
return update
}
// AddAliasForLink instructs the Switch to update its in-memory maps to reflect
// that a link has a new alias.
func (s *Switch) AddAliasForLink(chanID lnwire.ChannelID,
alias lnwire.ShortChannelID) error {
// Fetch the link so that we can update the underlying channel's set of
// aliases.
s.indexMtx.RLock()
link, err := s.getLink(chanID)
s.indexMtx.RUnlock()
if err != nil {
return err
}
// If the link is a channel where the option-scid-alias feature bit was
// not negotiated, we'll return an error.
if !link.negotiatedAliasFeature() {
return fmt.Errorf("attempted to update non-alias channel")
}
linkScid := link.ShortChanID()
// We'll update the maps so the Switch includes this alias in its
// forwarding decisions.
if link.isZeroConf() {
if link.zeroConfConfirmed() {
// If the channel has confirmed on-chain, we'll
// add this alias to the aliasToReal map.
confirmedScid := link.confirmedScid()
s.aliasToReal[alias] = confirmedScid
}
// Add this alias to the baseIndex mapping.
s.baseIndex[alias] = linkScid
} else if link.negotiatedAliasFeature() {
// The channel is confirmed, so we'll populate the aliasToReal
// and baseIndex maps.
s.aliasToReal[alias] = linkScid
s.baseIndex[alias] = linkScid
}
return nil
}

File diff suppressed because it is too large Load Diff

View File

@ -1128,6 +1128,12 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer,
return nil
}
getAliases := func(
base lnwire.ShortChannelID) []lnwire.ShortChannelID {
return nil
}
link := NewChannelLink(
ChannelLinkConfig{
Switch: server.htlcSwitch,
@ -1168,6 +1174,7 @@ func (h *hopNetwork) createChannelLink(server, peer *mockServer,
NotifyActiveChannel: func(wire.OutPoint) {},
NotifyInactiveChannel: func(wire.OutPoint) {},
HtlcNotifier: server.htlcSwitch.cfg.HtlcNotifier,
GetAliases: getAliases,
},
channel,
)

View File

@ -29,6 +29,15 @@ type ProtocolOptions struct {
// opening or accepting channels having the script enforced commitment
// type for leased channel.
NoScriptEnforcedLease bool `long:"no-script-enforced-lease" description:"disable support for script enforced lease commitments"`
// OptionScidAlias should be set if we want to signal the
// option-scid-alias feature bit. This allows scid aliases and the
// option-scid-alias channel-type.
OptionScidAlias bool `long:"option-scid-alias" description:"enable support for option_scid_alias channels"`
// OptionZeroConf should be set if we want to signal the zero-conf
// feature bit.
OptionZeroConf bool `long:"zero-conf" description:"enable support for zero-conf channels, must have option-scid-alias set also"`
}
// Wumbo returns true if lnd should permit the creation and acceptance of wumbo
@ -48,3 +57,13 @@ func (l *ProtocolOptions) NoAnchorCommitments() bool {
func (l *ProtocolOptions) NoScriptEnforcementLease() bool {
return l.NoScriptEnforcedLease
}
// ScidAlias returns true if we have enabled the option-scid-alias feature bit.
func (l *ProtocolOptions) ScidAlias() bool {
return l.OptionScidAlias
}
// ZeroConf returns true if we have enabled the zero-conf feature bit.
func (l *ProtocolOptions) ZeroConf() bool {
return l.OptionZeroConf
}

View File

@ -30,6 +30,15 @@ type ProtocolOptions struct {
//
// TODO: Move to experimental?
ScriptEnforcedLease bool `long:"script-enforced-lease" description:"enable support for script enforced lease commitments"`
// OptionScidAlias should be set if we want to signal the
// option-scid-alias feature bit. This allows scid aliases and the
// option-scid-alias channel-type.
OptionScidAlias bool `long:"option-scid-alias" description:"enable support for option_scid_alias channels"`
// OptionZeroConf should be set if we want to signal the zero-conf
// feature bit.
OptionZeroConf bool `long:"zero-conf" description:"enable support for zero-conf channels, must have option-scid-alias set also"`
}
// Wumbo returns true if lnd should permit the creation and acceptance of wumbo
@ -49,3 +58,13 @@ func (l *ProtocolOptions) NoAnchorCommitments() bool {
func (l *ProtocolOptions) NoScriptEnforcementLease() bool {
return !l.ScriptEnforcedLease
}
// ScidAlias returns true if we have enabled the option-scid-alias feature bit.
func (l *ProtocolOptions) ScidAlias() bool {
return l.OptionScidAlias
}
// ZeroConf returns true if we have enabled the zero-conf feature bit.
func (l *ProtocolOptions) ZeroConf() bool {
return l.OptionZeroConf
}

View File

@ -72,6 +72,10 @@ type AddInvoiceConfig struct {
// GenAmpInvoiceFeatures returns a feature containing feature bits that
// should be advertised on freshly generated AMP invoices.
GenAmpInvoiceFeatures func() *lnwire.FeatureVector
// GetAlias allows the peer's alias SCID to be retrieved for private
// option_scid_alias channels.
GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)
}
// AddInvoiceData contains the required data to create a new invoice.
@ -387,9 +391,27 @@ func AddInvoice(ctx context.Context, cfg *AddInvoiceConfig,
continue
}
// If this is a zero-conf channel, check if the
// confirmed SCID was used in forcedHints.
realScid := c.ZeroConfRealScid().ToUint64()
if c.IsZeroConf() {
if _, ok := forcedHints[realScid]; ok {
continue
}
}
chanID := lnwire.NewChanIDFromOutPoint(
&c.FundingOutpoint,
)
// Check whether the the peer's alias was
// provided in forcedHints.
peerAlias, _ := cfg.GetAlias(chanID)
peerScid := peerAlias.ToUint64()
if _, ok := forcedHints[peerScid]; ok {
continue
}
isActive := cfg.IsChannelActive(chanID)
hopHintInfo := newHopHintInfo(c, isActive)
@ -529,10 +551,17 @@ func chanCanBeHopHint(channel *HopHintInfo, cfg *SelectHopHintsCfg) (
// Fetch the policies for each end of the channel.
info, p1, p2, err := cfg.FetchChannelEdgesByID(channel.ShortChannelID)
if err != nil {
log.Errorf("Unable to fetch the routing "+
"policies for the edges of the channel "+
"%v: %v", channel.ShortChannelID, err)
return nil, false
// In the case of zero-conf channels, it may be the case that
// the alias SCID was deleted from the graph, and replaced by
// the confirmed SCID. Check the Graph for the confirmed SCID.
confirmedScid := channel.ConfirmedScidZC
info, p1, p2, err = cfg.FetchChannelEdgesByID(confirmedScid)
if err != nil {
log.Errorf("Unable to fetch the routing policies for "+
"the edges of the channel %v: %v",
channel.ShortChannelID, err)
return nil, false
}
}
// Now, we'll need to determine which is the correct policy for HTLCs
@ -550,7 +579,8 @@ func chanCanBeHopHint(channel *HopHintInfo, cfg *SelectHopHintsCfg) (
// addHopHint creates a hop hint out of the passed channel and channel policy.
// The new hop hint is appended to the passed slice.
func addHopHint(hopHints *[][]zpay32.HopHint,
channel *HopHintInfo, chanPolicy *channeldb.ChannelEdgePolicy) {
channel *HopHintInfo, chanPolicy *channeldb.ChannelEdgePolicy,
aliasScid lnwire.ShortChannelID) {
hopHint := zpay32.HopHint{
NodeID: channel.RemotePubkey,
@ -562,6 +592,11 @@ func addHopHint(hopHints *[][]zpay32.HopHint,
CLTVExpiryDelta: chanPolicy.TimeLockDelta,
}
var defaultScid lnwire.ShortChannelID
if aliasScid != defaultScid {
hopHint.ChannelID = aliasScid.ToUint64()
}
*hopHints = append(*hopHints, []zpay32.HopHint{hopHint})
}
@ -587,18 +622,28 @@ type HopHintInfo struct {
// ShortChannelID is the short channel ID of the channel.
ShortChannelID uint64
// ConfirmedScidZC is the confirmed SCID of a zero-conf channel. This
// may be used for looking up a channel in the graph.
ConfirmedScidZC uint64
// ScidAliasFeature denotes whether the channel has negotiated the
// option-scid-alias feature bit.
ScidAliasFeature bool
}
func newHopHintInfo(c *channeldb.OpenChannel, isActive bool) *HopHintInfo {
isPublic := c.ChannelFlags&lnwire.FFAnnounceChannel != 0
return &HopHintInfo{
IsPublic: isPublic,
IsActive: isActive,
FundingOutpoint: c.FundingOutpoint,
RemotePubkey: c.IdentityPub,
RemoteBalance: c.LocalCommitment.RemoteBalance,
ShortChannelID: c.ShortChannelID.ToUint64(),
IsPublic: isPublic,
IsActive: isActive,
FundingOutpoint: c.FundingOutpoint,
RemotePubkey: c.IdentityPub,
RemoteBalance: c.LocalCommitment.RemoteBalance,
ShortChannelID: c.ShortChannelID.ToUint64(),
ConfirmedScidZC: c.ZeroConfRealScid().ToUint64(),
ScidAliasFeature: c.ChanType.HasScidAliasFeature(),
}
}
@ -615,12 +660,17 @@ type SelectHopHintsCfg struct {
FetchChannelEdgesByID func(chanID uint64) (*channeldb.ChannelEdgeInfo,
*channeldb.ChannelEdgePolicy, *channeldb.ChannelEdgePolicy,
error)
// GetAlias allows the peer's alias SCID to be retrieved for private
// option_scid_alias channels.
GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)
}
func newSelectHopHintsCfg(invoicesCfg *AddInvoiceConfig) *SelectHopHintsCfg {
return &SelectHopHintsCfg{
IsPublicNode: invoicesCfg.Graph.IsPublicNode,
FetchChannelEdgesByID: invoicesCfg.Graph.FetchChannelEdgesByID,
GetAlias: invoicesCfg.GetAlias,
}
}
@ -693,9 +743,26 @@ func SelectHopHints(amtMSat lnwire.MilliSatoshi, cfg *SelectHopHintsCfg,
continue
}
// Lookup and see if there is an alias SCID that exists.
chanID := lnwire.NewChanIDFromOutPoint(
&channel.FundingOutpoint,
)
alias, _ := cfg.GetAlias(chanID)
// If this is a channel where the option-scid-alias feature bit
// was negotiated and the alias is not yet assigned, we cannot
// issue an invoice. Doing so might expose the confirmed SCID
// of a private channel.
if channel.ScidAliasFeature {
var defaultScid lnwire.ShortChannelID
if alias == defaultScid {
continue
}
}
// Now that we now this channel use usable, add it as a hop
// hint and the indexes we'll use later.
addHopHint(&hopHints, channel, edgePolicy)
addHopHint(&hopHints, channel, edgePolicy, alias)
hopHintChans[channel.FundingOutpoint] = struct{}{}
totalHintBandwidth += channel.RemoteBalance
@ -733,9 +800,26 @@ func SelectHopHints(amtMSat lnwire.MilliSatoshi, cfg *SelectHopHintsCfg,
continue
}
// Lookup and see if there's an alias SCID that exists.
chanID := lnwire.NewChanIDFromOutPoint(
&channel.FundingOutpoint,
)
alias, _ := cfg.GetAlias(chanID)
// If this is a channel where the option-scid-alias feature bit
// was negotiated and the alias is not yet assigned, we cannot
// issue an invoice. Doing so might expose the confirmed SCID
// of a private channel.
if channel.ScidAliasFeature {
var defaultScid lnwire.ShortChannelID
if alias == defaultScid {
continue
}
}
// Include the route hint in our set of options that will be
// used when creating the invoice.
addHopHint(&hopHints, channel, remotePolicy)
addHopHint(&hopHints, channel, remotePolicy, alias)
// As we've just added a new hop hint, we'll accumulate it's
// available balance now to update our tally.

View File

@ -274,11 +274,11 @@ func TestSelectHopHints(t *testing.T) {
// hop hint.
h.Mock.On(
"FetchChannelEdgesByID",
private1ShortID,
mock.Anything,
).Return(
nil, nil, nil,
errors.New("no edge"),
)
).Times(4)
},
amount: 100,
channels: []*HopHintInfo{
@ -536,6 +536,10 @@ func TestSelectHopHints(t *testing.T) {
},
}
getAlias := func(lnwire.ChannelID) (lnwire.ShortChannelID, error) {
return lnwire.ShortChannelID{}, nil
}
for _, test := range tests {
test := test
@ -548,6 +552,7 @@ func TestSelectHopHints(t *testing.T) {
cfg := &SelectHopHintsCfg{
IsPublicNode: mock.IsPublicNode,
FetchChannelEdgesByID: mock.FetchChannelEdgesByID,
GetAlias: getAlias,
}
hints := SelectHopHints(

View File

@ -60,4 +60,8 @@ type Config struct {
// GenAmpInvoiceFeatures returns a feature containing feature bits that
// should be advertised on freshly generated AMP invoices.
GenAmpInvoiceFeatures func() *lnwire.FeatureVector
// GetAlias returns the peer's alias SCID if it exists given the
// 32-byte ChannelID.
GetAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)
}

View File

@ -331,6 +331,7 @@ func (s *Server) AddHoldInvoice(ctx context.Context,
Graph: s.cfg.GraphDB,
GenInvoiceFeatures: s.cfg.GenInvoiceFeatures,
GenAmpInvoiceFeatures: s.cfg.GenAmpInvoiceFeatures,
GetAlias: s.cfg.GetAlias,
}
hash, err := lntypes.MakeHash(invoice.Hash)

File diff suppressed because it is too large Load Diff

View File

@ -979,6 +979,13 @@ message ChannelAcceptResponse {
The number of confirmations we require before we consider the channel open.
*/
uint32 min_accept_depth = 10;
/*
Whether the responder wants this to be a zero-conf channel. This will fail
if either side does not have the scid-alias feature bit set. The minimum
depth field must be zero if this is true.
*/
bool zero_conf = 11;
}
message ChannelPoint {
@ -2114,6 +2121,17 @@ message OpenChannelRequest {
the remote peer supports explicit channel negotiation.
*/
CommitmentType commitment_type = 18;
/*
If this is true, then a zero-conf channel open will be attempted.
*/
bool zero_conf = 19;
/*
If this is true, then an option-scid-alias channel-type open will be
attempted.
*/
bool scid_alias = 20;
}
message OpenStatusUpdate {
oneof update {

View File

@ -3409,6 +3409,10 @@
"type": "integer",
"format": "int64",
"description": "The number of confirmations we require before we consider the channel open."
},
"zero_conf": {
"type": "boolean",
"description": "Whether the responder wants this to be a zero-conf channel. This will fail\nif either side does not have the scid-alias feature bit set. The minimum\ndepth field must be zero if this is true."
}
}
},
@ -5414,6 +5418,14 @@
"commitment_type": {
"$ref": "#/definitions/lnrpcCommitmentType",
"description": "The explicit commitment type to use. Note this field will only be used if\nthe remote peer supports explicit channel negotiation."
},
"zero_conf": {
"type": "boolean",
"description": "If this is true, then a zero-conf channel open will be attempted."
},
"scid_alias": {
"type": "boolean",
"description": "If this is true, then an option-scid-alias channel-type open will be\nattempted."
}
}
},

View File

@ -989,6 +989,15 @@ type OpenChannelParams struct {
// CommitmentType is the commitment type that should be used for the
// channel to be opened.
CommitmentType lnrpc.CommitmentType
// ZeroConf is used to determine if the channel will be a zero-conf
// channel. This only works if the explicit negotiation is used with
// anchors or script enforced leases.
ZeroConf bool
// ScidAlias denotes whether the channel will be an option-scid-alias
// channel type negotiation.
ScidAlias bool
}
// OpenChannel attempts to open a channel between srcNode and destNode with the
@ -1027,6 +1036,8 @@ func (n *NetworkHarness) OpenChannel(srcNode, destNode *HarnessNode,
FundingShim: p.FundingShim,
SatPerByte: int64(p.SatPerVByte),
CommitmentType: p.CommitmentType,
ZeroConf: p.ZeroConf,
ScidAlias: p.ScidAlias,
}
// We need to use n.runCtx here to keep the response stream alive after

View File

@ -447,6 +447,97 @@ func testChannelBackupRestore(net *lntest.NetworkHarness, t *harnessTest) {
)
},
},
// Restore the backup from the on-disk file, using the RPC
// interface, for zero-conf anchor channels.
{
name: "restore from backup file for zero-conf " +
"anchors channel",
initiator: true,
private: false,
commitmentType: lnrpc.CommitmentType_ANCHORS,
zeroConf: true,
restoreMethod: func(oldNode *lntest.HarnessNode,
backupFilePath string,
mnemonic []string) (nodeRestorer, error) {
// Read the entire Multi backup stored within
// this node's channels.backup file.
multi, err := ioutil.ReadFile(backupFilePath)
if err != nil {
return nil, err
}
// Now that we have Dave's backup file, we'll
// create a new nodeRestorer that we'll restore
// using the on-disk channels.backup.
return chanRestoreViaRPC(
net, password, mnemonic, multi,
oldNode,
)
},
},
// Restore the backup from the on-disk file, using the RPC
// interface for a zero-conf script-enforced leased channel.
{
name: "restore from backup file zero-conf " +
"script-enforced leased channel",
initiator: true,
private: false,
commitmentType: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE,
zeroConf: true,
restoreMethod: func(oldNode *lntest.HarnessNode,
backupFilePath string,
mnemonic []string) (nodeRestorer, error) {
// Read the entire Multi backup stored within
// this node's channel.backup file.
multi, err := ioutil.ReadFile(backupFilePath)
if err != nil {
return nil, err
}
// Now that we have Dave's backup file, we'll
// create a new nodeRestorer that we'll restore
// using the on-disk channel backup.
return chanRestoreViaRPC(
net, password, mnemonic, multi,
oldNode,
)
},
},
// Restore a zero-conf anchors channel that was force closed by
// dave just before going offline.
{
name: "restore force closed from backup file " +
"anchors w/ zero-conf",
initiator: true,
private: false,
commitmentType: lnrpc.CommitmentType_ANCHORS,
localForceClose: true,
zeroConf: true,
restoreMethod: func(oldNode *lntest.HarnessNode,
backupFilePath string,
mnemonic []string) (nodeRestorer, error) {
// Read the entire Multi backup stored within
// this node's channel.backup file.
multi, err := ioutil.ReadFile(backupFilePath)
if err != nil {
return nil, err
}
// Now that we have Dave's backup file, we'll
// create a new nodeRestorer that we'll restore
// using the on-disk channel backup.
return chanRestoreViaRPC(
net, password, mnemonic, multi,
oldNode,
)
},
},
}
// TODO(roasbeef): online vs offline close?
@ -861,6 +952,10 @@ type chanRestoreTestCase struct {
restoreMethod func(oldNode *lntest.HarnessNode,
backupFilePath string,
mnemonic []string) (nodeRestorer, error)
// zeroConf denotes whether the opened channel is a zero-conf channel
// or not.
zeroConf bool
}
// testChanRestoreScenario executes a chanRestoreTestCase from end to end,
@ -886,6 +981,13 @@ func testChanRestoreScenario(t *harnessTest, net *lntest.NetworkHarness,
nodeArgs = append(nodeArgs, args...)
}
if testCase.zeroConf {
nodeArgs = append(
nodeArgs, "--protocol.option-scid-alias",
"--protocol.zero-conf",
)
}
// First, we'll create a brand new node we'll use within the test. If
// we have a custom backup file specified, then we'll also create that
// for use.
@ -971,14 +1073,16 @@ func testChanRestoreScenario(t *harnessTest, net *lntest.NetworkHarness,
net, t, from, to, chanAmt, thawHeight, true,
)
}
params := lntest.OpenChannelParams{
Amt: chanAmt,
PushAmt: pushAmt,
Private: testCase.private,
FundingShim: fundingShim,
CommitmentType: testCase.commitmentType,
ZeroConf: testCase.zeroConf,
}
chanPoint = openChannelAndAssert(
t, net, from, to, lntest.OpenChannelParams{
Amt: chanAmt,
PushAmt: pushAmt,
Private: testCase.private,
FundingShim: fundingShim,
CommitmentType: testCase.commitmentType,
},
t, net, from, to, params,
)
// Wait for both sides to see the opened channel.

View File

@ -23,14 +23,15 @@ import (
// case of anchor channels, the second-level spends can also be aggregated and
// properly feebumped, so we'll check that as well.
func testMultiHopHtlcAggregation(net *lntest.NetworkHarness, t *harnessTest,
alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) {
alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType,
zeroConf bool) {
const finalCltvDelta = 40
ctxb := context.Background()
// First, we'll create a three hop network: Alice -> Bob -> Carol.
aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork(
t, net, alice, bob, false, c,
t, net, alice, bob, false, c, zeroConf,
)
defer shutdownAndAssert(net, t, carol)

View File

@ -20,7 +20,8 @@ import (
// preimage via the witness beacon, we properly settle the HTLC on-chain using
// the HTLC success transaction in order to ensure we don't lose any funds.
func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest,
alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) {
alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType,
zeroConf bool) {
ctxb := context.Background()
@ -28,7 +29,7 @@ func testMultiHopHtlcLocalChainClaim(net *lntest.NetworkHarness, t *harnessTest,
// Carol refusing to actually settle or directly cancel any HTLC's
// self.
aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork(
t, net, alice, bob, false, c,
t, net, alice, bob, false, c, zeroConf,
)
// Clean up carol's node when the test finishes.

View File

@ -20,7 +20,8 @@ import (
// canceled backwards. Once the timeout has been reached, then we should sweep
// it on-chain, and cancel the HTLC backwards.
func testMultiHopHtlcLocalTimeout(net *lntest.NetworkHarness, t *harnessTest,
alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) {
alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType,
zeroConf bool) {
ctxb := context.Background()
@ -28,7 +29,7 @@ func testMultiHopHtlcLocalTimeout(net *lntest.NetworkHarness, t *harnessTest,
// Carol refusing to actually settle or directly cancel any HTLC's
// self.
aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork(
t, net, alice, bob, true, c,
t, net, alice, bob, true, c, zeroConf,
)
// Clean up carol's node when the test finishes.

View File

@ -22,7 +22,8 @@ import (
// extract the preimage from the sweep transaction, and finish settling the
// HTLC backwards into the route.
func testMultiHopReceiverChainClaim(net *lntest.NetworkHarness, t *harnessTest,
alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) {
alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType,
zeroConf bool) {
ctxb := context.Background()
@ -30,7 +31,7 @@ func testMultiHopReceiverChainClaim(net *lntest.NetworkHarness, t *harnessTest,
// Carol refusing to actually settle or directly cancel any HTLC's
// self.
aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork(
t, net, alice, bob, false, c,
t, net, alice, bob, false, c, zeroConf,
)
// Clean up carol's node when the test finishes.

View File

@ -20,7 +20,8 @@ import (
// HTLC directly on-chain using the preimage in order to ensure that we don't
// lose any funds.
func testMultiHopHtlcRemoteChainClaim(net *lntest.NetworkHarness, t *harnessTest,
alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) {
alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType,
zeroConf bool) {
ctxb := context.Background()
@ -28,7 +29,7 @@ func testMultiHopHtlcRemoteChainClaim(net *lntest.NetworkHarness, t *harnessTest
// Carol refusing to actually settle or directly cancel any HTLC's
// self.
aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork(
t, net, alice, bob, false, c,
t, net, alice, bob, false, c, zeroConf,
)
// Clean up carol's node when the test finishes.

View File

@ -19,7 +19,8 @@ import (
// that's timed out. At this point, the node should timeout the HTLC using the
// HTLC timeout transaction, then cancel it backwards as normal.
func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness,
t *harnessTest, alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) {
t *harnessTest, alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType,
zeroConf bool) {
ctxb := context.Background()
@ -27,7 +28,7 @@ func testMultiHopLocalForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness,
// Carol refusing to actually settle or directly cancel any HTLC's
// self.
aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork(
t, net, alice, bob, true, c,
t, net, alice, bob, true, c, zeroConf,
)
// Clean up carol's node when the test finishes.

View File

@ -21,7 +21,8 @@ import (
// transaction once the timeout has expired. Once we sweep the transaction, we
// should also cancel back the initial HTLC.
func testMultiHopRemoteForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness,
t *harnessTest, alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType) {
t *harnessTest, alice, bob *lntest.HarnessNode, c lnrpc.CommitmentType,
zeroConf bool) {
ctxb := context.Background()
@ -29,7 +30,7 @@ func testMultiHopRemoteForceCloseOnChainHtlcTimeout(net *lntest.NetworkHarness,
// Carol refusing to actually settle or directly cancel any HTLC's
// self.
aliceChanPoint, bobChanPoint, carol := createThreeHopNetwork(
t, net, alice, bob, true, c,
t, net, alice, bob, true, c, zeroConf,
)
// Clean up carol's node when the test finishes.

View File

@ -19,7 +19,8 @@ func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) {
type testCase struct {
name string
test func(net *lntest.NetworkHarness, t *harnessTest, alice,
bob *lntest.HarnessNode, c lnrpc.CommitmentType)
bob *lntest.HarnessNode, c lnrpc.CommitmentType,
zeroConf bool)
}
subTests := []testCase{
@ -68,20 +69,51 @@ func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) {
},
}
commitTypes := []lnrpc.CommitmentType{
lnrpc.CommitmentType_LEGACY,
lnrpc.CommitmentType_ANCHORS,
lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE,
commitWithZeroConf := []struct {
commitType lnrpc.CommitmentType
zeroConf bool
}{
{
commitType: lnrpc.CommitmentType_LEGACY,
zeroConf: false,
},
{
commitType: lnrpc.CommitmentType_ANCHORS,
zeroConf: false,
},
{
commitType: lnrpc.CommitmentType_ANCHORS,
zeroConf: true,
},
{
commitType: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE,
zeroConf: false,
},
{
commitType: lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE,
zeroConf: true,
},
}
for _, commitType := range commitTypes {
commitType := commitType
testName := fmt.Sprintf("committype=%v", commitType.String())
for _, typeAndConf := range commitWithZeroConf {
typeAndConf := typeAndConf
testName := fmt.Sprintf(
"committype=%v zeroconf=%v",
typeAndConf.commitType.String(), typeAndConf.zeroConf,
)
success := t.t.Run(testName, func(t *testing.T) {
ht := newHarnessTest(t, net)
args := nodeArgsForCommitType(commitType)
args := nodeArgsForCommitType(typeAndConf.commitType)
if typeAndConf.zeroConf {
args = append(
args, "--protocol.option-scid-alias",
"--protocol.zero-conf",
)
}
alice := net.NewNode(t, "Alice", args)
defer shutdownAndAssert(net, ht, alice)
@ -107,7 +139,11 @@ func testMultiHopHtlcClaims(net *lntest.NetworkHarness, t *harnessTest) {
// static fee estimate.
net.SetFeeEstimate(12500)
subTest.test(net, ht, alice, bob, commitType)
subTest.test(
net, ht, alice, bob,
typeAndConf.commitType,
typeAndConf.zeroConf,
)
})
if !success {
return
@ -204,7 +240,8 @@ func checkPaymentStatus(node *lntest.HarnessNode, preimage lntypes.Preimage,
}
func createThreeHopNetwork(t *harnessTest, net *lntest.NetworkHarness,
alice, bob *lntest.HarnessNode, carolHodl bool, c lnrpc.CommitmentType) (
alice, bob *lntest.HarnessNode, carolHodl bool, c lnrpc.CommitmentType,
zeroConf bool) (
*lnrpc.ChannelPoint, *lnrpc.ChannelPoint, *lntest.HarnessNode) {
net.EnsureConnected(t.t, alice, bob)
@ -234,6 +271,7 @@ func createThreeHopNetwork(t *harnessTest, net *lntest.NetworkHarness,
Amt: chanAmt,
CommitmentType: c,
FundingShim: aliceFundingShim,
ZeroConf: zeroConf,
},
)
@ -254,6 +292,14 @@ func createThreeHopNetwork(t *harnessTest, net *lntest.NetworkHarness,
if carolHodl {
carolFlags = append(carolFlags, "--hodl.exit-settle")
}
if zeroConf {
carolFlags = append(
carolFlags, "--protocol.option-scid-alias",
"--protocol.zero-conf",
)
}
carol := net.NewNode(t.t, "Carol", carolFlags)
net.ConnectNodes(t.t, bob, carol)
@ -280,6 +326,7 @@ func createThreeHopNetwork(t *harnessTest, net *lntest.NetworkHarness,
Amt: chanAmt,
CommitmentType: c,
FundingShim: bobFundingShim,
ZeroConf: zeroConf,
},
)
err = bob.WaitForNetworkChannelOpen(bobChanPoint)

View File

@ -407,4 +407,20 @@ var allTestCases = []*testCase{
name: "resolution handoff",
test: testResHandoff,
},
{
name: "zero conf channel open",
test: testZeroConfChannelOpen,
},
{
name: "option scid alias",
test: testOptionScidAlias,
},
{
name: "scid alias channel update",
test: testUpdateChannelPolicyScidAlias,
},
{
name: "scid alias upgrade",
test: testOptionScidUpgrade,
},
}

File diff suppressed because it is too large Load Diff

View File

@ -12,7 +12,6 @@ import (
"github.com/lightningnetwork/lnd/channeldb"
"github.com/lightningnetwork/lnd/input"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
"github.com/lightningnetwork/lnd/lnwallet/chanfunding"
"github.com/lightningnetwork/lnd/lnwire"
)
@ -208,11 +207,9 @@ type ChannelReservation struct {
// creation of all channel reservations should be carried out via the
// lnwallet.InitChannelReservation interface.
func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
commitFeePerKw chainfee.SatPerKWeight, wallet *LightningWallet,
id uint64, pushMSat lnwire.MilliSatoshi, chainHash *chainhash.Hash,
flags lnwire.FundingFlag, commitType CommitmentType,
fundingAssembler chanfunding.Assembler,
pendingChanID [32]byte, thawHeight uint32) (*ChannelReservation, error) {
wallet *LightningWallet, id uint64, chainHash *chainhash.Hash,
thawHeight uint32, req *InitFundingReserveMsg) (*ChannelReservation,
error) {
var (
ourBalance lnwire.MilliSatoshi
@ -223,10 +220,10 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
// Based on the channel type, we determine the initial commit weight
// and fee.
commitWeight := int64(input.CommitWeight)
if commitType.HasAnchors() {
if req.CommitType.HasAnchors() {
commitWeight = int64(input.AnchorCommitWeight)
}
commitFee := commitFeePerKw.FeeForWeight(commitWeight)
commitFee := req.CommitFeePerKw.FeeForWeight(commitWeight)
localFundingMSat := lnwire.NewMSatFromSatoshis(localFundingAmt)
// TODO(halseth): make method take remote funding amount directly
@ -236,7 +233,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
// The total fee paid by the initiator will be the commitment fee in
// addition to the two anchor outputs.
feeMSat := lnwire.NewMSatFromSatoshis(commitFee)
if commitType.HasAnchors() {
if req.CommitType.HasAnchors() {
feeMSat += 2 * lnwire.NewMSatFromSatoshis(anchorSize)
}
@ -247,8 +244,8 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
// no initial balance in the channel unless the remote party is pushing
// some funds to us within the first commitment state.
if localFundingAmt == 0 {
ourBalance = pushMSat
theirBalance = capacityMSat - feeMSat - pushMSat
ourBalance = req.PushMSat
theirBalance = capacityMSat - feeMSat - req.PushMSat
initiator = false
// If the responder doesn't have enough funds to actually pay
@ -268,14 +265,14 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
// we pay all the initial fees within the commitment
// transaction. We also deduct our balance by the
// amount pushed as part of the initial state.
ourBalance = capacityMSat - feeMSat - pushMSat
theirBalance = pushMSat
ourBalance = capacityMSat - feeMSat - req.PushMSat
theirBalance = req.PushMSat
} else {
// Otherwise, this is a dual funder workflow where both
// slides split the amount funded and the commitment
// fee.
ourBalance = localFundingMSat - (feeMSat / 2)
theirBalance = capacityMSat - localFundingMSat - (feeMSat / 2) + pushMSat
theirBalance = capacityMSat - localFundingMSat - (feeMSat / 2) + req.PushMSat
}
initiator = true
@ -320,16 +317,16 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
// If either of the balances are zero at this point, or we have a
// non-zero push amt (there's no pushing for dual funder), then this is
// a single-funder channel.
if ourBalance == 0 || theirBalance == 0 || pushMSat != 0 {
if ourBalance == 0 || theirBalance == 0 || req.PushMSat != 0 {
// Both the tweakless type and the anchor type is tweakless,
// hence set the bit.
if commitType.HasStaticRemoteKey() {
if req.CommitType.HasStaticRemoteKey() {
chanType |= channeldb.SingleFunderTweaklessBit
} else {
chanType |= channeldb.SingleFunderBit
}
switch a := fundingAssembler.(type) {
switch a := req.ChanFunder.(type) {
// The first channels of a batch shouldn't publish the batch TX
// to avoid problems if some of the funding flows can't be
// completed. Only the last channel of a batch should publish.
@ -358,14 +355,14 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
// We are adding anchor outputs to our commitment. We only support this
// in combination with zero-fee second-levels HTLCs.
if commitType.HasAnchors() {
if req.CommitType.HasAnchors() {
chanType |= channeldb.AnchorOutputsBit
chanType |= channeldb.ZeroHtlcTxFeeBit
}
// Set the appropriate LeaseExpiration/Frozen bit based on the
// reservation parameters.
if commitType == CommitmentTypeScriptEnforcedLease {
if req.CommitType == CommitmentTypeScriptEnforcedLease {
if thawHeight == 0 {
return nil, errors.New("missing absolute expiration " +
"for script enforced lease commitment type")
@ -375,6 +372,18 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
chanType |= channeldb.FrozenBit
}
if req.ZeroConf {
chanType |= channeldb.ZeroConfBit
}
if req.OptionScidAlias {
chanType |= channeldb.ScidAliasChanBit
}
if req.ScidAliasFeature {
chanType |= channeldb.ScidAliasFeatureBit
}
return &ChannelReservation{
ourContribution: &ChannelContribution{
FundingAmount: ourBalance.ToSatoshis(),
@ -389,18 +398,18 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
ChainHash: *chainHash,
IsPending: true,
IsInitiator: initiator,
ChannelFlags: flags,
ChannelFlags: req.Flags,
Capacity: capacity,
LocalCommitment: channeldb.ChannelCommitment{
LocalBalance: ourBalance,
RemoteBalance: theirBalance,
FeePerKw: btcutil.Amount(commitFeePerKw),
FeePerKw: btcutil.Amount(req.CommitFeePerKw),
CommitFee: commitFee,
},
RemoteCommitment: channeldb.ChannelCommitment{
LocalBalance: ourBalance,
RemoteBalance: theirBalance,
FeePerKw: btcutil.Amount(commitFeePerKw),
FeePerKw: btcutil.Amount(req.CommitFeePerKw),
CommitFee: commitFee,
},
ThawHeight: thawHeight,
@ -408,14 +417,22 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
InitialLocalBalance: ourBalance,
InitialRemoteBalance: theirBalance,
},
pushMSat: pushMSat,
pendingChanID: pendingChanID,
pushMSat: req.PushMSat,
pendingChanID: req.PendingChanID,
reservationID: id,
wallet: wallet,
chanFunder: fundingAssembler,
chanFunder: req.ChanFunder,
}, nil
}
// AddAlias stores the first alias for zero-conf channels.
func (r *ChannelReservation) AddAlias(scid lnwire.ShortChannelID) {
r.Lock()
defer r.Unlock()
r.partialState.ShortChannelID = scid
}
// SetNumConfsRequired sets the number of confirmations that are required for
// the ultimate funding transaction before the channel can be considered open.
// This is distinct from the main reservation workflow as it allows
@ -428,6 +445,15 @@ func (r *ChannelReservation) SetNumConfsRequired(numConfs uint16) {
r.partialState.NumConfsRequired = numConfs
}
// IsZeroConf returns if the reservation's underlying partial channel state is
// a zero-conf channel.
func (r *ChannelReservation) IsZeroConf() bool {
r.RLock()
defer r.RUnlock()
return r.partialState.IsZeroConf()
}
// CommitConstraints takes the constraints that the remote party specifies for
// the type of commitments that we can generate for them. These constraints
// include several parameters that serve as flow control restricting the amount

View File

@ -728,11 +728,17 @@ func testCancelNonExistentReservation(miner *rpctest.Harness,
feePerKw, err := alice.Cfg.FeeEstimator.EstimateFeePerKW(1)
require.NoError(t, err, "unable to query fee estimator")
req := &lnwallet.InitFundingReserveMsg{
CommitFeePerKw: feePerKw,
PushMSat: 10,
Flags: lnwire.FFAnnounceChannel,
CommitType: lnwallet.CommitmentTypeTweakless,
PendingChanID: [32]byte{},
}
// Create our own reservation, give it some ID.
res, err := lnwallet.NewChannelReservation(
10000, 10000, feePerKw, alice, 22, 10, &testHdSeed,
lnwire.FFAnnounceChannel, lnwallet.CommitmentTypeTweakless,
nil, [32]byte{}, 0,
10000, 10000, alice, 22, &testHdSeed, 0, req,
)
require.NoError(t, err, "unable to create res")

View File

@ -153,6 +153,18 @@ type InitFundingReserveMsg struct {
// used.
ChanFunder chanfunding.Assembler
// ZeroConf is a boolean that is true if a zero-conf channel was
// negotiated.
ZeroConf bool
// OptionScidAlias is a boolean that is true if an option-scid-alias
// channel type was explicitly negotiated.
OptionScidAlias bool
// ScidAliasFeature is true if the option-scid-alias feature bit was
// negotiated.
ScidAliasFeature bool
// err is a channel in which all errors will be sent across. Will be
// nil if this initial set is successful.
//
@ -845,10 +857,8 @@ func (l *LightningWallet) handleFundingReserveRequest(req *InitFundingReserveMsg
id := atomic.AddUint64(&l.nextFundingID, 1)
reservation, err := NewChannelReservation(
capacity, localFundingAmt, req.CommitFeePerKw, l, id,
req.PushMSat, l.Cfg.NetParams.GenesisHash, req.Flags,
req.CommitType, req.ChanFunder, req.PendingChanID,
thawHeight,
capacity, localFundingAmt, l, id, l.Cfg.NetParams.GenesisHash,
thawHeight, req,
)
if err != nil {
fundingIntent.Cancel()

View File

@ -155,6 +155,16 @@ const (
// TODO: Decide on actual feature bit value.
ExplicitChannelTypeOptional = 45
// ScidAliasRequired is a required feature bit that signals that the
// node requires understanding of ShortChannelID aliases in the TLV
// segment of the funding_locked message.
ScidAliasRequired FeatureBit = 46
// ScidAliasOptional is an optional feature bit that signals that the
// node understands ShortChannelID aliases in the TLV segment of the
// funding_locked message.
ScidAliasOptional FeatureBit = 47
// PaymentMetadataRequired is a required bit that denotes that if an
// invoice contains metadata, it must be passed along with the payment
// htlc(s).
@ -165,6 +175,14 @@ const (
// htlc(s).
PaymentMetadataOptional = 49
// ZeroConfRequired is a required feature bit that signals that the
// node requires understanding of the zero-conf channel_type.
ZeroConfRequired FeatureBit = 50
// ZeroConfOptional is an optional feature bit that signals that the
// node understands the zero-conf channel type.
ZeroConfOptional FeatureBit = 51
// KeysendRequired is a required bit that indicates that the node is
// able and willing to accept keysend payments.
KeysendRequired = 54
@ -173,7 +191,7 @@ const (
// able and willing to accept keysend payments.
KeysendOptional = 55
// ScriptEnforcedLeaseRequired is a required feature bit that signals
// ScriptEnforcedLeaseOptional is an optional feature bit that signals
// that the node requires channels having zero-fee second-level HTLC
// transactions, which also imply anchor commitments, along with an
// additional CLTV constraint of a channel lease's expiration height
@ -244,6 +262,10 @@ var Features = map[FeatureBit]string{
KeysendRequired: "keysend",
ScriptEnforcedLeaseRequired: "script-enforced-lease",
ScriptEnforcedLeaseOptional: "script-enforced-lease",
ScidAliasRequired: "scid-alias",
ScidAliasOptional: "scid-alias",
ZeroConfRequired: "zero-conf",
ZeroConfOptional: "zero-conf",
}
// RawFeatureVector represents a set of feature bits as defined in BOLT-09. A

View File

@ -5,6 +5,7 @@ import (
"io"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/tlv"
)
// FundingLocked is the message that both parties to a new channel creation
@ -21,6 +22,11 @@ type FundingLocked struct {
// next commitment transaction for the channel.
NextPerCommitmentPoint *btcec.PublicKey
// AliasScid is an alias ShortChannelID used to refer to the underlying
// channel. It can be used instead of the confirmed on-chain
// ShortChannelID for forwarding.
AliasScid *ShortChannelID
// ExtraData is the set of data that was appended to this message to
// fill out the full maximum transport message size. These fields can
// be used to specify optional data such as custom TLV fields.
@ -47,11 +53,31 @@ var _ Message = (*FundingLocked)(nil)
//
// This is part of the lnwire.Message interface.
func (c *FundingLocked) Decode(r io.Reader, pver uint32) error {
return ReadElements(r,
// Read all the mandatory fields in the message.
err := ReadElements(r,
&c.ChanID,
&c.NextPerCommitmentPoint,
&c.ExtraData,
)
if err != nil {
return err
}
// Next we'll parse out the set of known records. For now, this is just
// the AliasScidRecordType.
var aliasScid ShortChannelID
typeMap, err := c.ExtraData.ExtractRecords(&aliasScid)
if err != nil {
return err
}
// We'll only set AliasScid if the corresponding TLV type was included
// in the stream.
if val, ok := typeMap[AliasScidRecordType]; ok && val == nil {
c.AliasScid = &aliasScid
}
return nil
}
// Encode serializes the target FundingLocked message into the passed io.Writer
@ -68,6 +94,15 @@ func (c *FundingLocked) Encode(w *bytes.Buffer, pver uint32) error {
return err
}
// We'll only encode the AliasScid in a TLV segment if it exists.
if c.AliasScid != nil {
recordProducers := []tlv.RecordProducer{c.AliasScid}
err := EncodeMessageExtraData(&c.ExtraData, recordProducers...)
if err != nil {
return err
}
}
return WriteBytes(w, c.ExtraData)
}

View File

@ -2,6 +2,15 @@ package lnwire
import (
"fmt"
"io"
"github.com/lightningnetwork/lnd/tlv"
)
const (
// AliasScidRecordType is the type of the experimental record to denote
// the alias being used in an option_scid_alias channel.
AliasScidRecordType tlv.Type = 1
)
// ShortChannelID represents the set of data which is needed to retrieve all
@ -46,3 +55,38 @@ func (c ShortChannelID) ToUint64() uint64 {
func (c ShortChannelID) String() string {
return fmt.Sprintf("%d:%d:%d", c.BlockHeight, c.TxIndex, c.TxPosition)
}
// Record returns a TLV record that can be used to encode/decode a
// ShortChannelID to/from a TLV stream.
func (c *ShortChannelID) Record() tlv.Record {
return tlv.MakeStaticRecord(
AliasScidRecordType, c, 8, EShortChannelID, DShortChannelID,
)
}
// EShortChannelID is an encoder for ShortChannelID. It is exported so other
// packages can use the encoding scheme.
func EShortChannelID(w io.Writer, val interface{}, buf *[8]byte) error {
if v, ok := val.(*ShortChannelID); ok {
return tlv.EUint64T(w, v.ToUint64(), buf)
}
return tlv.NewTypeForEncodingErr(val, "lnwire.ShortChannelID")
}
// DShortChannelID is a decoder for ShortChannelID. It is exported so other
// packages can use the decoding scheme.
func DShortChannelID(r io.Reader, val interface{}, buf *[8]byte,
l uint64) error {
if v, ok := val.(*ShortChannelID); ok {
var scid uint64
err := tlv.DUint64(r, &scid, buf, 8)
if err != nil {
return err
}
*v = NewShortChanIDFromInt(scid)
return nil
}
return tlv.NewTypeForDecodingErr(val, "lnwire.ShortChannelID", l, 8)
}

View File

@ -5,6 +5,7 @@ import (
"testing"
"github.com/davecgh/go-spew/spew"
"github.com/stretchr/testify/require"
)
func TestShortChannelIDEncoding(t *testing.T) {
@ -39,3 +40,25 @@ func TestShortChannelIDEncoding(t *testing.T) {
}
}
}
// TestScidTypeEncodeDecode tests that we're able to properly encode and decode
// ShortChannelID within TLV streams.
func TestScidTypeEncodeDecode(t *testing.T) {
t.Parallel()
aliasScid := ShortChannelID{
BlockHeight: (1 << 24) - 1,
TxIndex: (1 << 24) - 1,
TxPosition: (1 << 16) - 1,
}
var extraData ExtraOpaqueData
require.NoError(t, extraData.PackRecords(&aliasScid))
var aliasScid2 ShortChannelID
tlvs, err := extraData.ExtractRecords(&aliasScid2)
require.NoError(t, err)
require.Contains(t, tlvs, AliasScidRecordType)
require.Equal(t, aliasScid, aliasScid2)
}

View File

@ -60,7 +60,8 @@ type ChanStatusConfig struct {
// ApplyChannelUpdate processes new ChannelUpdates signed by our node by
// updating our local routing table and broadcasting the update to our
// peers.
ApplyChannelUpdate func(*lnwire.ChannelUpdate) error
ApplyChannelUpdate func(*lnwire.ChannelUpdate, *wire.OutPoint,
bool) error
// DB stores the set of channels that are to be monitored.
DB DB
@ -621,7 +622,7 @@ func (m *ChanStatusManager) signAndSendNextUpdate(outpoint wire.OutPoint,
// Retrieve the latest update for this channel. We'll use this
// as our starting point to send the new update.
chanUpdate, err := m.fetchLastChanUpdateByOutPoint(outpoint)
chanUpdate, private, err := m.fetchLastChanUpdateByOutPoint(outpoint)
if err != nil {
return err
}
@ -634,22 +635,26 @@ func (m *ChanStatusManager) signAndSendNextUpdate(outpoint wire.OutPoint,
return err
}
return m.cfg.ApplyChannelUpdate(chanUpdate)
return m.cfg.ApplyChannelUpdate(chanUpdate, &outpoint, private)
}
// fetchLastChanUpdateByOutPoint fetches the latest policy for our direction of
// a channel, and crafts a new ChannelUpdate with this policy. Returns an error
// in case our ChannelEdgePolicy is not found in the database.
// in case our ChannelEdgePolicy is not found in the database. Also returns if
// the channel is private by checking AuthProof for nil.
func (m *ChanStatusManager) fetchLastChanUpdateByOutPoint(op wire.OutPoint) (
*lnwire.ChannelUpdate, error) {
*lnwire.ChannelUpdate, bool, error) {
// Get the edge info and policies for this channel from the graph.
info, edge1, edge2, err := m.cfg.Graph.FetchChannelEdgesByOutpoint(&op)
if err != nil {
return nil, err
return nil, false, err
}
return ExtractChannelUpdate(m.ourPubKeyBytes, info, edge1, edge2)
update, err := ExtractChannelUpdate(
m.ourPubKeyBytes, info, edge1, edge2,
)
return update, info.AuthProof == nil, err
}
// loadInitialChanState determines the initial ChannelState for a particular
@ -660,7 +665,7 @@ func (m *ChanStatusManager) fetchLastChanUpdateByOutPoint(op wire.OutPoint) (
func (m *ChanStatusManager) loadInitialChanState(
outpoint *wire.OutPoint) (ChannelState, error) {
lastUpdate, err := m.fetchLastChanUpdateByOutPoint(*outpoint)
lastUpdate, _, err := m.fetchLastChanUpdateByOutPoint(*outpoint)
if err != nil {
return ChannelState{}, err
}

View File

@ -176,7 +176,9 @@ func (g *mockGraph) FetchChannelEdgesByOutpoint(
return info, pol1, pol2, nil
}
func (g *mockGraph) ApplyChannelUpdate(update *lnwire.ChannelUpdate) error {
func (g *mockGraph) ApplyChannelUpdate(update *lnwire.ChannelUpdate,
op *wire.OutPoint, private bool) error {
g.mu.Lock()
defer g.mu.Unlock()

View File

@ -330,6 +330,18 @@ type Config struct {
// from the peer.
HandleCustomMessage func(peer [33]byte, msg *lnwire.Custom) error
// GetAliases is passed to created links so the Switch and link can be
// aware of the channel's aliases.
GetAliases func(base lnwire.ShortChannelID) []lnwire.ShortChannelID
// RequestAlias allows the Brontide struct to request an alias to send
// to the peer.
RequestAlias func() (lnwire.ShortChannelID, error)
// AddLocalAlias persists an alias to an underlying alias store.
AddLocalAlias func(alias, base lnwire.ShortChannelID,
gossip bool) error
// PongBuf is a slice we'll reuse instead of allocating memory on the
// heap. Since only reads will occur and no writes, there is no need
// for any synchronization primitives. As a result, it's safe to share
@ -666,7 +678,65 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
// cannot be loaded normally.
var msgs []lnwire.Message
scidAliasNegotiated := p.hasNegotiatedScidAlias()
for _, dbChan := range chans {
hasScidFeature := dbChan.ChanType.HasScidAliasFeature()
if scidAliasNegotiated && !hasScidFeature {
// We'll request and store an alias, making sure that a
// gossiper mapping is not created for the alias to the
// real SCID. This is done because the peer and funding
// manager are not aware of each other's states and if
// we did not do this, we would accept alias channel
// updates after 6 confirmations, which would be buggy.
// We'll queue a funding_locked message with the new
// alias. This should technically be done *after* the
// reestablish, but this behavior is pre-existing since
// the funding manager may already queue a
// funding_locked before the channel_reestablish.
if !dbChan.IsPending {
aliasScid, err := p.cfg.RequestAlias()
if err != nil {
return nil, err
}
err = p.cfg.AddLocalAlias(
aliasScid, dbChan.ShortChanID(), false,
)
if err != nil {
return nil, err
}
chanID := lnwire.NewChanIDFromOutPoint(
&dbChan.FundingOutpoint,
)
// Fetch the second commitment point to send in
// the funding_locked message.
second, err := dbChan.SecondCommitmentPoint()
if err != nil {
return nil, err
}
fundingLockedMsg := lnwire.NewFundingLocked(
chanID, second,
)
fundingLockedMsg.AliasScid = &aliasScid
msgs = append(msgs, fundingLockedMsg)
}
// If we've negotiated the option-scid-alias feature
// and this channel does not have ScidAliasFeature set
// to true due to an upgrade where the feature bit was
// turned on, we'll update the channel's database
// state.
err := dbChan.MarkScidAliasNegotiated()
if err != nil {
return nil, err
}
}
lnChan, err := lnwallet.NewLightningChannel(
p.cfg.Signer, dbChan, p.cfg.SigPool,
)
@ -900,6 +970,7 @@ func (p *Brontide) addLink(chanPoint *wire.OutPoint,
NotifyActiveChannel: p.cfg.ChannelNotifier.NotifyActiveChannelEvent,
NotifyInactiveChannel: p.cfg.ChannelNotifier.NotifyInactiveChannelEvent,
HtlcNotifier: p.cfg.HtlcNotifier,
GetAliases: p.cfg.GetAliases,
}
// Before adding our new link, purge the switch of any pending or live
@ -3054,6 +3125,14 @@ func (p *Brontide) RemoteFeatures() *lnwire.FeatureVector {
return p.remoteFeatures
}
// hasNegotiatedScidAlias returns true if we've negotiated the
// option-scid-alias feature bit with the peer.
func (p *Brontide) hasNegotiatedScidAlias() bool {
peerHas := p.remoteFeatures.HasFeature(lnwire.ScidAliasOptional)
localHas := p.cfg.Features.HasFeature(lnwire.ScidAliasOptional)
return peerHas && localHas
}
// sendInitMsg sends the Init message to the remote peer. This message contains
// our currently supported local and global features.
func (p *Brontide) sendInitMsg(legacyChan bool) error {

View File

@ -340,7 +340,11 @@ func createTestPeer(notifier chainntnfs.ChainNotifier,
OurPubKey: aliceKeyPub,
OurKeyLoc: testKeyLoc,
IsChannelActive: func(lnwire.ChannelID) bool { return true },
ApplyChannelUpdate: func(*lnwire.ChannelUpdate) error { return nil },
ApplyChannelUpdate: func(*lnwire.ChannelUpdate,
*wire.OutPoint, bool) error {
return nil
},
})
if err != nil {
return nil, nil, nil, err

View File

@ -473,6 +473,9 @@ func testPaymentLifecycle(t *testing.T, test paymentLifecycleTestCase,
return next, nil
},
Clock: clock.NewTestClock(time.Unix(1, 0)),
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
if err != nil {
t.Fatalf("unable to create router %v", err)

View File

@ -358,6 +358,10 @@ type Config struct {
// Otherwise, we'll only prune the channel when both edges have a very
// dated last update.
StrictZombiePruning bool
// IsAlias returns whether a passed ShortChannelID is an alias. This is
// only used for our local channels.
IsAlias func(scid lnwire.ShortChannelID) bool
}
// EdgeLocator is a struct used to identify a specific edge.
@ -971,7 +975,9 @@ func (r *ChannelRouter) pruneZombieChans() error {
toPrune = append(toPrune, chanID)
log.Tracef("Pruning zombie channel with ChannelID(%v)", chanID)
}
err = r.cfg.Graph.DeleteChannelEdges(r.cfg.StrictZombiePruning, toPrune...)
err = r.cfg.Graph.DeleteChannelEdges(
r.cfg.StrictZombiePruning, true, toPrune...,
)
if err != nil {
return fmt.Errorf("unable to delete zombie channels: %v", err)
}
@ -1455,8 +1461,12 @@ func (r *ChannelRouter) processUpdate(msg interface{},
// If AssumeChannelValid is present, then we are unable to
// perform any of the expensive checks below, so we'll
// short-circuit our path straight to adding the edge to our
// graph.
if r.cfg.AssumeChannelValid {
// graph. If the passed ShortChannelID is an alias, then we'll
// skip validation as it will not map to a legitimate tx. This
// is not a DoS vector as only we can add an alias
// ChannelAnnouncement from the gossiper.
scid := lnwire.NewShortChanIDFromInt(msg.ChannelID)
if r.cfg.AssumeChannelValid || r.cfg.IsAlias(scid) {
if err := r.cfg.Graph.AddChannelEdge(msg, op...); err != nil {
return fmt.Errorf("unable to add edge: %v", err)
}

View File

@ -82,6 +82,9 @@ func (c *testCtx) RestartRouter(t *testing.T) {
Control: makeMockControlTower(),
ChannelPruneExpiry: time.Hour * 24,
GraphPruneInterval: time.Hour * 2,
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "unable to create router")
require.NoError(t, router.Start(), "unable to start router")
@ -165,6 +168,9 @@ func createTestCtxFromGraphInstanceAssumeValid(t *testing.T,
Clock: clock.NewTestClock(time.Unix(1, 0)),
AssumeChannelValid: assumeValid,
StrictZombiePruning: strictPruning,
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "unable to create router")
require.NoError(t, router.Start(), "unable to start router")
@ -1738,6 +1744,10 @@ func TestWakeUpOnStaleBranch(t *testing.T) {
// We'll set the delay to zero to prune immediately.
FirstTimePruneDelay: 0,
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
if err != nil {
t.Fatalf("unable to create router %v", err)
@ -3485,6 +3495,10 @@ func TestSendMPPaymentSucceed(t *testing.T) {
next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil
},
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "failed to create router")
@ -3648,6 +3662,10 @@ func TestSendMPPaymentSucceedOnExtraShards(t *testing.T) {
next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil
},
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "failed to create router")
@ -3856,6 +3874,10 @@ func TestSendMPPaymentFailed(t *testing.T) {
next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil
},
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "failed to create router")
@ -4056,6 +4078,10 @@ func TestSendMPPaymentFailedWithShardsInFlight(t *testing.T) {
next := atomic.AddUint64(&uniquePaymentID, 1)
return next, nil
},
IsAlias: func(scid lnwire.ShortChannelID) bool {
return false
},
})
require.NoError(t, err, "failed to create router")

View File

@ -756,6 +756,7 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
r.cfg.net.ResolveTCPAddr, genInvoiceFeatures,
genAmpInvoiceFeatures, getNodeAnnouncement,
s.updateAndBrodcastSelfNode, parseAddr, rpcsLog,
s.aliasMgr.GetPeerAlias,
)
if err != nil {
return err
@ -2012,7 +2013,9 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
var channelType *lnwire.ChannelType
switch in.CommitmentType {
case lnrpc.CommitmentType_UNKNOWN_COMMITMENT_TYPE:
break
if in.ZeroConf {
return nil, fmt.Errorf("use anchors for zero-conf")
}
case lnrpc.CommitmentType_LEGACY:
channelType = new(lnwire.ChannelType)
@ -2026,18 +2029,38 @@ func (r *rpcServer) parseOpenChannelReq(in *lnrpc.OpenChannelRequest,
case lnrpc.CommitmentType_ANCHORS:
channelType = new(lnwire.ChannelType)
*channelType = lnwire.ChannelType(*lnwire.NewRawFeatureVector(
fv := lnwire.NewRawFeatureVector(
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
))
)
if in.ZeroConf {
fv.Set(lnwire.ZeroConfRequired)
}
if in.ScidAlias {
fv.Set(lnwire.ScidAliasRequired)
}
*channelType = lnwire.ChannelType(*fv)
case lnrpc.CommitmentType_SCRIPT_ENFORCED_LEASE:
channelType = new(lnwire.ChannelType)
*channelType = lnwire.ChannelType(*lnwire.NewRawFeatureVector(
fv := lnwire.NewRawFeatureVector(
lnwire.StaticRemoteKeyRequired,
lnwire.AnchorsZeroFeeHtlcTxRequired,
lnwire.ScriptEnforcedLeaseRequired,
))
)
if in.ZeroConf {
fv.Set(lnwire.ZeroConfRequired)
}
if in.ScidAlias {
fv.Set(lnwire.ScidAliasRequired)
}
*channelType = lnwire.ChannelType(*fv)
default:
return nil, fmt.Errorf("unhandled request channel type %v",
@ -2576,7 +2599,7 @@ func abandonChanFromGraph(chanGraph *channeldb.ChannelGraph,
// If the channel ID is still in the graph, then that means the channel
// is still open, so we'll now move to purge it from the graph.
return chanGraph.DeleteChannelEdges(false, chanID)
return chanGraph.DeleteChannelEdges(false, true, chanID)
}
// abandonChan removes a channel from the database, graph and contract court.
@ -3039,7 +3062,7 @@ func (r *rpcServer) SubscribePeerEvents(req *lnrpc.PeerEventSubscription,
// confirmed unspent outputs and all unconfirmed unspent outputs under control
// by the wallet. This method can be modified by having the request specify
// only witness outputs should be factored into the final output sum.
// TODO(roasbeef): add async hooks into wallet balance changes
// TODO(roasbeef): add async hooks into wallet balance changes.
func (r *rpcServer) WalletBalance(ctx context.Context,
in *lnrpc.WalletBalanceRequest) (*lnrpc.WalletBalanceResponse, error) {
@ -5260,6 +5283,7 @@ func (r *rpcServer) AddInvoice(ctx context.Context,
GenAmpInvoiceFeatures: func() *lnwire.FeatureVector {
return r.server.featureMgr.Get(feature.SetInvoiceAmp)
},
GetAlias: r.server.aliasMgr.GetPeerAlias,
}
value, err := lnrpc.UnmarshallAmt(invoice.Value, invoice.ValueMsat)

View File

@ -1182,6 +1182,14 @@ litecoin.node=ltcd
; channel type if it is enabled.
; protocol.no-script-enforced-lease=true
; Set to enable support for option_scid_alias channels, which can be referred
; to by an alias instead of the confirmed ShortChannelID. Additionally, is
; needed to open zero-conf channels.
; protocol.option-scid-alias=true
; Set to enable support for zero-conf channels. This requires the
; option-scid-alias flag to also be set.
; protocol.zero-conf=true
[db]

132
server.go
View File

@ -16,6 +16,7 @@ import (
"time"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/connmgr"
@ -23,6 +24,7 @@ import (
"github.com/btcsuite/btcd/wire"
"github.com/go-errors/errors"
sphinx "github.com/lightningnetwork/lightning-onion"
"github.com/lightningnetwork/lnd/aliasmgr"
"github.com/lightningnetwork/lnd/autopilot"
"github.com/lightningnetwork/lnd/brontide"
"github.com/lightningnetwork/lnd/cert"
@ -243,6 +245,8 @@ type server struct {
// channel DB that haven't been separated out yet.
miscDB *channeldb.DB
aliasMgr *aliasmgr.Manager
htlcSwitch *htlcswitch.Switch
interceptableSwitch *htlcswitch.InterceptableSwitch
@ -530,6 +534,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
NoWumbo: !cfg.ProtocolOptions.Wumbo(),
NoScriptEnforcementLease: cfg.ProtocolOptions.NoScriptEnforcementLease(),
NoKeysend: !cfg.AcceptKeySend,
NoOptionScidAlias: !cfg.ProtocolOptions.ScidAlias(),
NoZeroConf: !cfg.ProtocolOptions.ZeroConf(),
})
if err != nil {
return nil, err
@ -614,6 +620,11 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
thresholdSats := btcutil.Amount(cfg.DustThreshold)
thresholdMSats := lnwire.NewMSatFromSatoshis(thresholdSats)
s.aliasMgr, err = aliasmgr.NewManager(dbs.ChanStateDB)
if err != nil {
return nil, err
}
s.htlcSwitch, err = htlcswitch.New(htlcswitch.Config{
DB: dbs.ChanStateDB,
FetchAllOpenChannels: s.chanStateDB.FetchAllOpenChannels,
@ -646,6 +657,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
Clock: clock.NewDefaultClock(),
HTLCExpiry: htlcswitch.DefaultHTLCExpiry,
DustThreshold: thresholdMSats,
SignAliasUpdate: s.signAliasUpdate,
IsAlias: aliasmgr.IsAlias,
}, uint32(currentHeight))
if err != nil {
return nil, err
@ -909,6 +922,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
PathFindingConfig: pathFindingConfig,
Clock: clock.NewDefaultClock(),
StrictZombiePruning: strictPruning,
IsAlias: aliasmgr.IsAlias,
})
if err != nil {
return nil, fmt.Errorf("can't create router: %v", err)
@ -951,6 +965,10 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
PinnedSyncers: cfg.Gossip.PinnedSyncers,
MaxChannelUpdateBurst: cfg.Gossip.MaxChannelUpdateBurst,
ChannelUpdateInterval: cfg.Gossip.ChannelUpdateInterval,
IsAlias: aliasmgr.IsAlias,
SignAliasUpdate: s.signAliasUpdate,
FindBaseByAlias: s.aliasMgr.FindBaseSCID,
GetAlias: s.aliasMgr.GetPeerAlias,
}, nodeKeyDesc)
s.localChanMgr = &localchans.Manager{
@ -1155,6 +1173,46 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
return nil, err
}
// Wrap the DeleteChannelEdges method so that the funding manager can
// use it without depending on several layers of indirection.
deleteAliasEdge := func(scid lnwire.ShortChannelID) (
*channeldb.ChannelEdgePolicy, error) {
info, e1, e2, err := s.graphDB.FetchChannelEdgesByID(
scid.ToUint64(),
)
if err == channeldb.ErrEdgeNotFound {
// This is unlikely but there is a slim chance of this
// being hit if lnd was killed via SIGKILL and the
// funding manager was stepping through the delete
// alias edge logic.
return nil, nil
} else if err != nil {
return nil, err
}
// Grab our key to find our policy.
var ourKey [33]byte
copy(ourKey[:], nodeKeyDesc.PubKey.SerializeCompressed())
var ourPolicy *channeldb.ChannelEdgePolicy
if info != nil && info.NodeKey1Bytes == ourKey {
ourPolicy = e1
} else {
ourPolicy = e2
}
if ourPolicy == nil {
// Something is wrong, so return an error.
return nil, fmt.Errorf("we don't have an edge")
}
err = s.graphDB.DeleteChannelEdges(
false, false, scid.ToUint64(),
)
return ourPolicy, err
}
s.fundingMgr, err = funding.NewFundingManager(funding.Config{
NoWumboChans: !cfg.ProtocolOptions.Wumbo(),
IDKey: nodeKeyDesc.PubKey,
@ -1173,15 +1231,16 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
SendAnnouncement: s.authGossiper.ProcessLocalAnnouncement,
NotifyWhenOnline: s.NotifyWhenOnline,
TempChanIDSeed: chanIDSeed,
FindChannel: func(chanID lnwire.ChannelID) (
*channeldb.OpenChannel, error) {
FindChannel: func(node *btcec.PublicKey,
chanID lnwire.ChannelID) (*channeldb.OpenChannel,
error) {
dbChannels, err := s.chanStateDB.FetchAllChannels()
nodeChans, err := s.chanStateDB.FetchOpenChannels(node)
if err != nil {
return nil, err
}
for _, channel := range dbChannels {
for _, channel := range nodeChans {
if chanID.IsChanPoint(&channel.FundingOutpoint) {
return channel, nil
}
@ -1339,6 +1398,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
RegisteredChains: cfg.registeredChains,
MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte(
s.cfg.MaxCommitFeeRateAnchors * 1000).FeePerKWeight(),
DeleteAliasEdge: deleteAliasEdge,
AliasManager: s.aliasMgr,
})
if err != nil {
return nil, err
@ -1495,6 +1556,20 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
return s, nil
}
// signAliasUpdate takes a ChannelUpdate and returns the signature. This is
// used for option_scid_alias channels where the ChannelUpdate to be sent back
// may differ from what is on disk.
func (s *server) signAliasUpdate(u *lnwire.ChannelUpdate) (*ecdsa.Signature,
error) {
data, err := u.DataToSign()
if err != nil {
return nil, err
}
return s.cc.MsgSigner.SignMessage(s.identityKeyLoc, data, true)
}
// createLivenessMonitor creates a set of health checks using our configured
// values and uses these checks to create a liveliness monitor. Available
// health checks,
@ -2063,7 +2138,9 @@ func (s *server) Stop() error {
s.connMgr.Stop()
// Shutdown the wallet, funding manager, and the rpc server.
s.chanStatusMgr.Stop()
if err := s.chanStatusMgr.Stop(); err != nil {
srvrLog.Warnf("failed to stop chanStatusMgr: %v", err)
}
if err := s.htlcSwitch.Stop(); err != nil {
srvrLog.Warnf("failed to stop htlcSwitch: %v", err)
}
@ -3100,7 +3177,6 @@ func (s *server) NotifyWhenOnline(peerKey [33]byte,
peerChan chan<- lnpeer.Peer) {
s.mu.Lock()
defer s.mu.Unlock()
// Compute the target peer's identifier.
pubStr := string(peerKey[:])
@ -3108,6 +3184,19 @@ func (s *server) NotifyWhenOnline(peerKey [33]byte,
// Check if peer is connected.
peer, ok := s.peersByPub[pubStr]
if ok {
// Unlock here so that the mutex isn't held while we are
// waiting for the peer to become active.
s.mu.Unlock()
// Wait until the peer signals that it is actually active
// rather than only in the server's maps.
select {
case <-peer.ActiveSignal():
case <-peer.QuitSignal():
// The peer quit so we'll just return.
return
}
// Connected, can return early.
srvrLog.Debugf("Notifying that peer %x is online", peerKey)
@ -3124,6 +3213,7 @@ func (s *server) NotifyWhenOnline(peerKey [33]byte,
s.peerConnectedListeners[pubStr] = append(
s.peerConnectedListeners[pubStr], peerChan,
)
s.mu.Unlock()
}
// NotifyWhenOffline delivers a notification to the caller of when the peer with
@ -3621,6 +3711,9 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
PendingCommitInterval: s.cfg.PendingCommitInterval,
ChannelCommitBatchSize: s.cfg.ChannelCommitBatchSize,
HandleCustomMessage: s.handleCustomMessage,
GetAliases: s.aliasMgr.GetAliases,
RequestAlias: s.aliasMgr.RequestAlias,
AddLocalAlias: s.aliasMgr.AddLocalAlias,
Quit: s.quit,
}
@ -4369,9 +4462,30 @@ func (s *server) fetchLastChanUpdate() func(lnwire.ShortChannelID) (
}
// applyChannelUpdate applies the channel update to the different sub-systems of
// the server.
func (s *server) applyChannelUpdate(update *lnwire.ChannelUpdate) error {
errChan := s.authGossiper.ProcessLocalAnnouncement(update)
// the server. The useAlias boolean denotes whether or not to send an alias in
// place of the real SCID.
func (s *server) applyChannelUpdate(update *lnwire.ChannelUpdate,
op *wire.OutPoint, useAlias bool) error {
var (
peerAlias *lnwire.ShortChannelID
defaultAlias lnwire.ShortChannelID
)
chanID := lnwire.NewChanIDFromOutPoint(op)
// Fetch the peer's alias from the lnwire.ChannelID so it can be used
// in the ChannelUpdate if it hasn't been announced yet.
if useAlias {
foundAlias, _ := s.aliasMgr.GetPeerAlias(chanID)
if foundAlias != defaultAlias {
peerAlias = &foundAlias
}
}
errChan := s.authGossiper.ProcessLocalAnnouncement(
update, discovery.RemoteAlias(peerAlias),
)
select {
case err := <-errChan:
return err

View File

@ -121,7 +121,8 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
getNodeAnnouncement func() (lnwire.NodeAnnouncement, error),
updateNodeAnnouncement func(modifiers ...netann.NodeAnnModifier) error,
parseAddr func(addr string) (net.Addr, error),
rpcLogger btclog.Logger) error {
rpcLogger btclog.Logger,
getAlias func(lnwire.ChannelID) (lnwire.ShortChannelID, error)) error {
// First, we'll use reflect to obtain a version of the config struct
// that allows us to programmatically inspect its fields.
@ -257,6 +258,9 @@ func (s *subRPCServerConfigs) PopulateDependencies(cfg *Config,
subCfgValue.FieldByName("GenAmpInvoiceFeatures").Set(
reflect.ValueOf(genAmpInvoiceFeatures),
)
subCfgValue.FieldByName("GetAlias").Set(
reflect.ValueOf(getAlias),
)
case *neutrinorpc.Config:
subCfgValue := extractReflectValue(subCfg)