lnd/watchtower/wtclient/interface.go
Erick Cestari 2ae1db83b3
multi: drop tor v2 onion production, keep wire codec faithful
Tor stopped serving v2 onion services in October 2021; lnd should not
produce v2 addresses anymore, but it must still verify signatures on
and re-broadcast peer NodeAnnouncement messages that carry v2 entries.

Stop accepting v2 as configuration input (lncfg), strip the legacy
`--tor.v2` flag from the sample config, and remove the
`tor.OnionHostToFakeIP` helper. Operator entry points (`--externalip`,
`--listen`, `lncli connect`, `lncli wtclient towers add`) fail fast on
a v2 `.onion` string, so upgrading nodes must remove any v2 entry from
`lnd.conf` before lnd will start.

Filter persisted v2 state before use without rewriting on-disk records:
the self-announcement builder strips any v2 entry inherited from the
stored self-node; the watchtower client drops v2 entries from each
persisted tower's address list (skipping the tower entirely if no
non-v2 address remains); the autopilot connector, graph bootstrapper,
and static-channel backup restore paths skip v2 entries before
attempting outbound dials. Restrict the Tor controller's ADD_ONION
path to v3 keys, including the encrypted on-disk legacy-key fallback.

For inbound announcements, keep the wire codec wire-faithful:
`lnwire.WriteOnionAddr`, `graph/db.encodeOnionAddr`, and the matching
decoders round-trip v2 bytes so `DataToSign` reproduces the bytes the
remote peer signed, signature validation succeeds, and the announcement
is persisted to the graph DB and re-broadcast across restarts byte-for-
byte. RPC surfaces continue to expose the full address set so external
tools can independently reproduce and verify the signed bytes.

Add a netann regression test that signs a [v3, v2, ipv4] announcement,
round-trips it through Encode/Decode, verifies the signature, and
confirms the resulting models.Node preserves the v2 entry. Add a
graph bootstrapper test asserting v2 entries are skipped while v3 and
plain TCP entries on the same node still surface as bootstrap
candidates.
2026-05-22 09:42:37 -03:00

294 lines
11 KiB
Go

package wtclient
import (
"errors"
"net"
"github.com/btcsuite/btcd/btcec/v2"
"github.com/lightningnetwork/lnd/keychain"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tor"
"github.com/lightningnetwork/lnd/watchtower/blob"
"github.com/lightningnetwork/lnd/watchtower/wtdb"
"github.com/lightningnetwork/lnd/watchtower/wtserver"
)
// ErrTowerOnlyV2Onion is returned when a persisted tower has no usable
// addresses left after Tor v2 .onion entries are filtered out. The tower
// record is preserved on disk so an operator can attach a fresh v3 address
// for the same identity key.
var ErrTowerOnlyV2Onion = errors.New("tower has no non-v2-onion addresses")
// DB abstracts the required database operations required by the watchtower
// client.
type DB interface {
// CreateTower initialize an address record used to communicate with a
// watchtower. Each Tower is assigned a unique ID, that is used to
// amortize storage costs of the public key when used by multiple
// sessions. If the tower already exists, the address is appended to the
// list of all addresses used to that tower previously and its
// corresponding sessions are marked as active.
CreateTower(*lnwire.NetAddress) (*wtdb.Tower, error)
// RemoveTower modifies a tower's record within the database. If an
// address is provided, then _only_ the address record should be removed
// from the tower's persisted state. Otherwise, we'll attempt to mark
// the tower as inactive. If any of its sessions have unacked updates,
// then ErrTowerUnackedUpdates is returned. If the tower doesn't have
// any sessions at all, it'll be completely removed from the database.
//
// NOTE: An error is not returned if the tower doesn't exist.
RemoveTower(*btcec.PublicKey, net.Addr) error
// TerminateSession sets the given session's status to CSessionTerminal
// meaning that it will not be usable again.
TerminateSession(id wtdb.SessionID) error
// LoadTower retrieves a tower by its public key.
LoadTower(*btcec.PublicKey) (*wtdb.Tower, error)
// LoadTowerByID retrieves a tower by its tower ID.
LoadTowerByID(wtdb.TowerID) (*wtdb.Tower, error)
// ListTowers retrieves the list of towers available within the
// database. The filter function may be set in order to filter out the
// towers to be returned.
ListTowers(filter wtdb.TowerFilterFn) ([]*wtdb.Tower, error)
// NextSessionKeyIndex reserves a new session key derivation index for a
// particular tower id and blob type. The index is reserved for that
// (tower, blob type) pair until CreateClientSession is invoked for that
// tower and index, at which point a new index for that tower can be
// reserved. Multiple calls to this method before CreateClientSession is
// invoked should return the same index unless forceNext is true.
NextSessionKeyIndex(wtdb.TowerID, blob.Type, bool) (uint32, error)
// CreateClientSession saves a newly negotiated client session to the
// client's database. This enables the session to be used across
// restarts.
CreateClientSession(*wtdb.ClientSession) error
// ListClientSessions returns the set of all client sessions known to
// the db. An optional tower ID can be used to filter out any client
// sessions in the response that do not correspond to this tower.
ListClientSessions(*wtdb.TowerID, ...wtdb.ClientSessionListOption) (
map[wtdb.SessionID]*wtdb.ClientSession, error)
// GetClientSession loads the ClientSession with the given ID from the
// DB.
GetClientSession(wtdb.SessionID,
...wtdb.ClientSessionListOption) (*wtdb.ClientSession, error)
// FetchSessionCommittedUpdates retrieves the current set of un-acked
// updates of the given session.
FetchSessionCommittedUpdates(id *wtdb.SessionID) (
[]wtdb.CommittedUpdate, error)
// IsAcked returns true if the given backup has been backed up using
// the given session.
IsAcked(id *wtdb.SessionID, backupID *wtdb.BackupID) (bool, error)
// NumAckedUpdates returns the number of backups that have been
// successfully backed up using the given session.
NumAckedUpdates(id *wtdb.SessionID) (uint64, error)
// FetchChanInfos loads a mapping from all registered channels to
// their wtdb.ChannelInfo. Only the channels that have not yet been
// marked as closed will be loaded.
FetchChanInfos() (wtdb.ChannelInfos, error)
// MarkChannelClosed will mark a registered channel as closed by setting
// its closed-height as the given block height. It returns a list of
// session IDs for sessions that are now considered closable due to the
// close of this channel. The details for this channel will be deleted
// from the DB if there are no more sessions in the DB that contain
// updates for this channel.
MarkChannelClosed(chanID lnwire.ChannelID, blockHeight uint32) (
[]wtdb.SessionID, error)
// ListClosableSessions fetches and returns the IDs for all sessions
// marked as closable.
ListClosableSessions() (map[wtdb.SessionID]uint32, error)
// DeleteSession can be called when a session should be deleted from the
// DB. All references to the session will also be deleted from the DB.
// A session will only be deleted if it was previously marked as
// closable.
DeleteSession(id wtdb.SessionID) error
// RegisterChannel registers a channel for use within the client
// database. For now, all that is stored in the channel summary is the
// sweep pkscript that we'd like any tower sweeps to pay into. In the
// future, this will be extended to contain more info to allow the
// client efficiently request historical states to be backed up under
// the client's active policy.
RegisterChannel(lnwire.ChannelID, []byte) error
// MarkBackupIneligible records that the state identified by the
// (channel id, commit height) tuple was ineligible for being backed up
// under the current policy. This state can be retried later under a
// different policy.
MarkBackupIneligible(chanID lnwire.ChannelID, commitHeight uint64) error
// CommitUpdate writes the next state update for a particular
// session, so that we can be sure to resend it after a restart if it
// hasn't been ACK'd by the tower. The sequence number of the update
// should be exactly one greater than the existing entry, and less that
// or equal to the session's MaxUpdates.
CommitUpdate(id *wtdb.SessionID,
update *wtdb.CommittedUpdate) (uint16, error)
// AckUpdate records an acknowledgment from the watchtower that the
// update identified by seqNum was received and saved. The returned
// lastApplied will be recorded.
AckUpdate(id *wtdb.SessionID, seqNum, lastApplied uint16) error
// GetDBQueue returns a BackupID Queue instance under the given name
// space.
GetDBQueue(namespace []byte) wtdb.Queue[*wtdb.BackupID]
// DeleteCommittedUpdates deletes all the committed updates belonging to
// the given session from the db.
DeleteCommittedUpdates(id *wtdb.SessionID) error
// DeactivateTower sets the given tower's status to inactive. This means
// that this tower's sessions won't be loaded and used for backups.
// CreateTower can be used to reactivate the tower again.
DeactivateTower(pubKey *btcec.PublicKey) error
}
// AuthDialer connects to a remote node using an authenticated transport, such
// as brontide. The dialer argument is used to specify a resolver, which allows
// this method to be used over Tor or clear net connections.
type AuthDialer func(localKey keychain.SingleKeyECDH,
netAddr *lnwire.NetAddress,
dialer tor.DialFunc) (wtserver.Peer, error)
// ECDHKeyRing abstracts the ability to derive shared ECDH keys given a
// description of the derivation path of a private key.
type ECDHKeyRing interface {
keychain.ECDHRing
// DeriveKey attempts to derive an arbitrary key specified by the
// passed KeyLocator. This may be used in several recovery scenarios,
// or when manually rotating something like our current default node
// key.
DeriveKey(keyLoc keychain.KeyLocator) (keychain.KeyDescriptor, error)
}
// Tower represents the info about a watchtower server that a watchtower client
// needs in order to connect to it.
type Tower struct {
// ID is the unique, db-assigned, identifier for this tower.
ID wtdb.TowerID
// IdentityKey is the public key of the remote node, used to
// authenticate the brontide transport.
IdentityKey *btcec.PublicKey
// Addresses is an AddressIterator that can be used to manage the
// addresses for this tower.
Addresses AddressIterator
}
// isV2OnionAddr reports whether addr is a Tor v2 .onion address. Tor stopped
// serving v2 onion services in October 2021, so callers skip these on dial
// paths. Storage and gossip re-broadcast still preserve v2 byte-for-byte to
// keep peer-signed NodeAnnouncement signatures verifiable.
//
// TODO: move this helper into the `tor` module (as `tor.IsV2Onion`) and remove
// this copy along with the duplicate in the root server.go once a new `tor`
// module version is cut and the dependency is bumped.
func isV2OnionAddr(addr net.Addr) bool {
onion, ok := addr.(*tor.OnionAddr)
if !ok {
return false
}
return len(onion.OnionService) == tor.V2Len
}
// withoutV2Onion returns addrs with any Tor v2 .onion entries removed. See
// isV2OnionAddr for the rationale.
//
// TODO: move this helper into the `tor` module and remove this copy along
// with the duplicate in the root server.go once a new `tor` module version
// is cut and the dependency is bumped.
func withoutV2Onion(addrs []net.Addr) []net.Addr {
filtered := make([]net.Addr, 0, len(addrs))
for _, addr := range addrs {
if isV2OnionAddr(addr) {
continue
}
filtered = append(filtered, addr)
}
return filtered
}
// NewTowerFromDBTower converts a wtdb.Tower, which uses a static address list,
// into a Tower which uses an address iterator. Persisted Tor v2 .onion
// addresses are filtered out so an upgraded node never attempts to dial them;
// if filtering leaves zero usable addresses, ErrTowerOnlyV2Onion is returned
// and the caller is expected to skip the tower without modifying the DB.
func NewTowerFromDBTower(t *wtdb.Tower) (*Tower, error) {
filtered := withoutV2Onion(t.Addresses)
if len(filtered) == 0 {
return nil, ErrTowerOnlyV2Onion
}
addrs, err := newAddressIterator(filtered...)
if err != nil {
return nil, err
}
return &Tower{
ID: t.ID,
IdentityKey: t.IdentityKey,
Addresses: addrs,
}, nil
}
// ClientSession represents the session that a tower client has with a server.
type ClientSession struct {
// ID is the client's public key used when authenticating with the
// tower.
ID wtdb.SessionID
wtdb.ClientSessionBody
// Tower represents the tower that the client session has been made
// with.
Tower *Tower
// SessionKeyECDH is the ECDH capable wrapper of the ephemeral secret
// key used to connect to the watchtower.
SessionKeyECDH keychain.SingleKeyECDH
}
// NewClientSessionFromDBSession converts a wtdb.ClientSession to a
// ClientSession.
func NewClientSessionFromDBSession(s *wtdb.ClientSession, tower *Tower,
keyRing ECDHKeyRing) (*ClientSession, error) {
towerKeyDesc, err := keyRing.DeriveKey(
keychain.KeyLocator{
Family: keychain.KeyFamilyTowerSession,
Index: s.KeyIndex,
},
)
if err != nil {
return nil, err
}
sessionKeyECDH := keychain.NewPubKeyECDH(
towerKeyDesc, keyRing,
)
return &ClientSession{
ID: s.ID,
ClientSessionBody: s.ClientSessionBody,
Tower: tower,
SessionKeyECDH: sessionKeyECDH,
}, nil
}