Skip to content

Commit

Permalink
netann: update ChanAnn2 validation to work for P2WSH channels
Browse files Browse the repository at this point in the history
This commit expands the ChannelAnnouncement2 validation for the case
where it is announcing a P2WSH channel.
  • Loading branch information
ellemouton committed Dec 2, 2024
1 parent 2e1ed2b commit 5e509ab
Show file tree
Hide file tree
Showing 5 changed files with 310 additions and 56 deletions.
54 changes: 39 additions & 15 deletions discovery/gossiper.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
"github.com/lightninglabs/neutrino/cache"
Expand Down Expand Up @@ -166,14 +168,9 @@ type PinnedSyncers map[route.Vertex]struct{}
// Config defines the configuration for the service. ALL elements within the
// configuration MUST be non-nil for the service to carry out its duties.
type Config struct {
// ChainHash is a hash that indicates which resident chain of the
// AuthenticatedGossiper. Any announcements that don't match this
// chain hash will be ignored.
//
// TODO(roasbeef): eventually make into map so can de-multiplex
// incoming announcements
// * also need to do same for Notifier
ChainHash chainhash.Hash
// ChainParams holds the chain parameters for the active network this
// node is participating on.
ChainParams *chaincfg.Params

// Graph is the subsystem which is responsible for managing the
// topology of lightning network. After incoming channel, node, channel
Expand Down Expand Up @@ -359,6 +356,12 @@ type Config struct {
// updates for a channel and returns true if the channel should be
// considered a zombie based on these timestamps.
IsStillZombieChannel func(time.Time, time.Time) bool

// chainHash is a hash that indicates which resident chain of the
// AuthenticatedGossiper. Any announcements that don't match this
// chain hash will be ignored. This is an internal config value obtained
// from ChainParams.
chainHash *chainhash.Hash
}

// processedNetworkMsg is a wrapper around networkMsg and a boolean. It is
Expand Down Expand Up @@ -518,6 +521,8 @@ type AuthenticatedGossiper struct {
// New creates a new AuthenticatedGossiper instance, initialized with the
// passed configuration parameters.
func New(cfg Config, selfKeyDesc *keychain.KeyDescriptor) *AuthenticatedGossiper {
cfg.chainHash = cfg.ChainParams.GenesisHash

gossiper := &AuthenticatedGossiper{
selfKey: selfKeyDesc.PubKey,
selfKeyLoc: selfKeyDesc.KeyLocator,
Expand All @@ -538,7 +543,7 @@ func New(cfg Config, selfKeyDesc *keychain.KeyDescriptor) *AuthenticatedGossiper
}

gossiper.syncMgr = newSyncManager(&SyncManagerCfg{
ChainHash: cfg.ChainHash,
ChainHash: *cfg.chainHash,
ChanSeries: cfg.ChanSeries,
RotateTicker: cfg.RotateTicker,
HistoricalSyncTicker: cfg.HistoricalSyncTicker,
Expand Down Expand Up @@ -1945,9 +1950,28 @@ func (d *AuthenticatedGossiper) processRejectedEdge(

// fetchPKScript fetches the output script for the given SCID.
func (d *AuthenticatedGossiper) fetchPKScript(chanID *lnwire.ShortChannelID) (
[]byte, error) {
txscript.ScriptClass, btcutil.Address, error) {

pkScript, err := lnwallet.FetchPKScriptWithQuit(
d.cfg.ChainIO, chanID, d.quit,
)
if err != nil {
return txscript.WitnessUnknownTy, nil, err
}

scriptClass, addrs, _, err := txscript.ExtractPkScriptAddrs(
pkScript, d.cfg.ChainParams,
)
if err != nil {
return txscript.WitnessUnknownTy, nil, err
}

if len(addrs) != 1 {
return txscript.WitnessUnknownTy, nil, fmt.Errorf("expected "+
"1 address, got: %d", len(addrs))
}

return lnwallet.FetchPKScriptWithQuit(d.cfg.ChainIO, chanID, d.quit)
return scriptClass, addrs[0], nil
}

// addNode processes the given node announcement, and adds it to our channel
Expand Down Expand Up @@ -2447,10 +2471,10 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,

// We'll ignore any channel announcements that target any chain other
// than the set of chains we know of.
if !bytes.Equal(ann.ChainHash[:], d.cfg.ChainHash[:]) {
if !bytes.Equal(ann.ChainHash[:], d.cfg.chainHash[:]) {
err := fmt.Errorf("ignoring ChannelAnnouncement1 from chain=%v"+
", gossiper on chain=%v", ann.ChainHash,
d.cfg.ChainHash)
d.cfg.chainHash)
log.Errorf(err.Error())

key := newRejectCacheKey(
Expand Down Expand Up @@ -2836,9 +2860,9 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,

// We'll ignore any channel updates that target any chain other than
// the set of chains we know of.
if !bytes.Equal(upd.ChainHash[:], d.cfg.ChainHash[:]) {
if !bytes.Equal(upd.ChainHash[:], d.cfg.chainHash[:]) {
err := fmt.Errorf("ignoring ChannelUpdate from chain=%v, "+
"gossiper on chain=%v", upd.ChainHash, d.cfg.ChainHash)
"gossiper on chain=%v", upd.ChainHash, d.cfg.chainHash)
log.Errorf(err.Error())

key := newRejectCacheKey(
Expand Down
5 changes: 4 additions & 1 deletion discovery/gossiper_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/ecdsa"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/wire"
"github.com/davecgh/go-spew/spew"
Expand Down Expand Up @@ -749,7 +750,8 @@ func createTestCtx(t *testing.T, startHeight uint32, isChanPeer bool) (
}

gossiper := New(Config{
Notifier: notifier,
ChainParams: &chaincfg.MainNetParams,
Notifier: notifier,
Broadcast: func(senders map[route.Vertex]struct{},
msgs ...lnwire.Message) error {

Expand Down Expand Up @@ -1463,6 +1465,7 @@ func TestSignatureAnnouncementRetryAtStartup(t *testing.T) {

//nolint:lll
gossiper := New(Config{
ChainParams: &chaincfg.MainNetParams,
Notifier: ctx.gossiper.cfg.Notifier,
Broadcast: ctx.gossiper.cfg.Broadcast,
NotifyWhenOnline: ctx.gossiper.reliableSender.cfg.NotifyWhenOnline,
Expand Down
140 changes: 115 additions & 25 deletions netann/channel_announcement.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@ import (
"github.com/btcsuite/btcd/btcec/v2"
"github.com/btcsuite/btcd/btcec/v2/schnorr"
"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
"github.com/btcsuite/btcd/btcutil"
"github.com/btcsuite/btcd/chaincfg/chainhash"
"github.com/btcsuite/btcd/txscript"
"github.com/lightningnetwork/lnd/graph/db/models"
"github.com/lightningnetwork/lnd/lnwire"
"github.com/lightningnetwork/lnd/tlv"
Expand Down Expand Up @@ -108,7 +110,8 @@ func CreateChanAnnouncement(chanProof *models.ChannelAuthProof,

// FetchPkScript defines a function that can be used to fetch the output script
// for the transaction with the given SCID.
type FetchPkScript func(*lnwire.ShortChannelID) ([]byte, error)
type FetchPkScript func(*lnwire.ShortChannelID) (txscript.ScriptClass,
btcutil.Address, error)

// ValidateChannelAnn validates the channel announcement.
func ValidateChannelAnn(a lnwire.ChannelAnnouncement,
Expand Down Expand Up @@ -202,24 +205,124 @@ func validateChannelAnn1(a *lnwire.ChannelAnnouncement1) error {
func validateChannelAnn2(a *lnwire.ChannelAnnouncement2,
fetchPkScript FetchPkScript) error {

// Next, we fetch the funding transaction's PK script. We need this so
// that we know what type of channel we will be validating: P2WSH or
// P2TR.
scriptClass, scriptAddr, err := fetchPkScript(&a.ShortChannelID.Val)
if err != nil {
return err
}

var keys []*btcec.PublicKey

switch scriptClass {
case txscript.WitnessV0ScriptHashTy:
keys, err = chanAnn2P2WSHMuSig2Keys(a)
if err != nil {
return err
}
case txscript.WitnessV1TaprootTy:
keys, err = chanAnn2P2TRMuSig2Keys(a, scriptAddr)
if err != nil {
return err
}
default:
return fmt.Errorf("invalid on-chain pk script type for "+
"channel_announcement_2: %s", scriptClass)
}

// Do a MuSig2 aggregation of the keys to obtain the aggregate key that
// the signature will be validated against.
aggKey, _, _, err := musig2.AggregateKeys(keys, true)
if err != nil {
return err
}

// Get the message that the signature should have signed.
dataHash, err := ChanAnn2DigestToSign(a)
if err != nil {
return err
}

// Obtain the signature.
sig, err := a.Signature.Val.ToSignature()
if err != nil {
return err
}

// Check that the signature is valid for the aggregate key given the
// message digest.
if !sig.Verify(dataHash.CloneBytes(), aggKey.FinalKey) {
return fmt.Errorf("invalid sig")
}

return nil
}

// chanAnn2P2WSHMuSig2Keys returns the set of keys that should be used to
// construct the aggregate key that the signature in an
// lnwire.ChannelAnnouncement2 message should be verified against in the case
// where the channel being announced is a P2WSH channel.
func chanAnn2P2WSHMuSig2Keys(a *lnwire.ChannelAnnouncement2) (
[]*btcec.PublicKey, error) {

nodeKey1, err := btcec.ParsePubKey(a.NodeID1.Val[:])
if err != nil {
return err
return nil, err
}

nodeKey2, err := btcec.ParsePubKey(a.NodeID2.Val[:])
if err != nil {
return err
return nil, err
}

btcKeyMissingErrString := "bitcoin key %d missing for announcement " +
"of a P2WSH channel"

btcKey1Bytes, err := a.BitcoinKey1.UnwrapOrErr(
fmt.Errorf(btcKeyMissingErrString, 1),
)
if err != nil {
return nil, err
}

btcKey1, err := btcec.ParsePubKey(btcKey1Bytes.Val[:])
if err != nil {
return nil, err
}

btcKey2Bytes, err := a.BitcoinKey2.UnwrapOrErr(
fmt.Errorf(btcKeyMissingErrString, 2),
)
if err != nil {
return nil, err
}

btcKey2, err := btcec.ParsePubKey(btcKey2Bytes.Val[:])
if err != nil {
return nil, err
}

return []*btcec.PublicKey{
nodeKey1, nodeKey2, btcKey1, btcKey2,
}, nil
}

// chanAnn2P2TRMuSig2Keys returns the set of keys that should be used to
// construct the aggregate key that the signature in an
// lnwire.ChannelAnnouncement2 message should be verified against in the case
// where the channel being announced is a P2TR channel.
func chanAnn2P2TRMuSig2Keys(a *lnwire.ChannelAnnouncement2,
scriptAddr btcutil.Address) ([]*btcec.PublicKey, error) {

nodeKey1, err := btcec.ParsePubKey(a.NodeID1.Val[:])
if err != nil {
return nil, err
}

nodeKey2, err := btcec.ParsePubKey(a.NodeID2.Val[:])
if err != nil {
return nil, err
}

keys := []*btcec.PublicKey{
Expand All @@ -240,42 +343,29 @@ func validateChannelAnn2(a *lnwire.ChannelAnnouncement2,

bitcoinKey1, err := btcec.ParsePubKey(btcKey1.Val[:])
if err != nil {
return err
return nil, err
}

bitcoinKey2, err := btcec.ParsePubKey(btcKey2.Val[:])
if err != nil {
return err
return nil, err
}

keys = append(keys, bitcoinKey1, bitcoinKey2)
} else {
// If bitcoin keys are not provided, then we need to get the
// on-chain output key since this will be the 3rd key in the
// 3-of-3 MuSig2 signature.
pkScript, err := fetchPkScript(&a.ShortChannelID.Val)
if err != nil {
return err
}

outputKey, err := schnorr.ParsePubKey(pkScript[2:])
// If bitcoin keys are not provided, then the on-chain output
// key is considered the 3rd key in the 3-of-3 MuSig2 signature.
outputKey, err := schnorr.ParsePubKey(
scriptAddr.ScriptAddress(),
)
if err != nil {
return err
return nil, err
}

keys = append(keys, outputKey)
}

aggKey, _, _, err := musig2.AggregateKeys(keys, true)
if err != nil {
return err
}

if !sig.Verify(dataHash.CloneBytes(), aggKey.FinalKey) {
return fmt.Errorf("invalid sig")
}

return nil
return keys, nil
}

// ChanAnn2DigestToSign computes the digest of the message to be signed.
Expand Down
Loading

0 comments on commit 5e509ab

Please sign in to comment.