Skip to content

Commit e5db0d6

Browse files
committed
graph+discovery: move funding tx validation to gossiper
This commit is a pure refactor. We move the transaction validation (existence, spentness, correctness) from the `graph.Builder` to the gossiper since this is where all protocol level checks should happen. All tests involved are also updated/moved.
1 parent 39bb23e commit e5db0d6

File tree

7 files changed

+499
-418
lines changed

7 files changed

+499
-418
lines changed

discovery/gossiper.go

Lines changed: 240 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"bytes"
55
"errors"
66
"fmt"
7+
"strings"
78
"sync"
89
"sync/atomic"
910
"time"
@@ -28,6 +29,8 @@ import (
2829
"github.com/lightningnetwork/lnd/lnpeer"
2930
"github.com/lightningnetwork/lnd/lnutils"
3031
"github.com/lightningnetwork/lnd/lnwallet"
32+
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
33+
"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
3134
"github.com/lightningnetwork/lnd/lnwire"
3235
"github.com/lightningnetwork/lnd/multimutex"
3336
"github.com/lightningnetwork/lnd/netann"
@@ -80,6 +83,23 @@ var (
8083
// the remote peer.
8184
ErrGossipSyncerNotFound = errors.New("gossip syncer not found")
8285

86+
// ErrNoFundingTransaction is returned when we are unable to find the
87+
// funding transaction described by the short channel ID on chain.
88+
ErrNoFundingTransaction = errors.New(
89+
"unable to find the funding transaction",
90+
)
91+
92+
// ErrInvalidFundingOutput is returned if the channel funding output
93+
// fails validation.
94+
ErrInvalidFundingOutput = errors.New(
95+
"channel funding output validation failed",
96+
)
97+
98+
// ErrChannelSpent is returned when we go to validate a channel, but
99+
// the purported funding output has actually already been spent on
100+
// chain.
101+
ErrChannelSpent = errors.New("channel output has been spent")
102+
83103
// emptyPubkey is used to compare compressed pubkeys against an empty
84104
// byte array.
85105
emptyPubkey [33]byte
@@ -2078,7 +2098,7 @@ func (d *AuthenticatedGossiper) processNetworkAnnouncement(
20782098
// the existence of a channel and not yet the routing policies in
20792099
// either direction of the channel.
20802100
case *lnwire.ChannelAnnouncement1:
2081-
return d.handleChanAnnouncement(nMsg, msg, schedulerOp)
2101+
return d.handleChanAnnouncement(nMsg, msg, schedulerOp...)
20822102

20832103
// A new authenticated channel edge update has arrived. This indicates
20842104
// that the directional information for an already known channel has
@@ -2459,7 +2479,7 @@ func (d *AuthenticatedGossiper) handleNodeAnnouncement(nMsg *networkMsg,
24592479
// handleChanAnnouncement processes a new channel announcement.
24602480
func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
24612481
ann *lnwire.ChannelAnnouncement1,
2462-
ops []batch.SchedulerOption) ([]networkMsg, bool) {
2482+
ops ...batch.SchedulerOption) ([]networkMsg, bool) {
24632483

24642484
scid := ann.ShortChannelID
24652485

@@ -2642,23 +2662,116 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
26422662
// announcement at the same time.
26432663
d.channelMtx.Lock(scid.ToUint64())
26442664

2645-
// We only make use of the funding script later on during funding
2646-
// transaction validation if AssumeChannelValid is not true.
2647-
if !(d.cfg.AssumeChannelValid || d.cfg.IsAlias(scid)) {
2648-
fundingPkScript, err := makeFundingScript(
2649-
ann.BitcoinKey1[:], ann.BitcoinKey2[:], ann.Features,
2650-
tapscriptRoot,
2665+
// If AssumeChannelValid is present, then we are unable to perform any
2666+
// of the expensive checks below, so we'll short-circuit our path
2667+
// straight to adding the edge to our graph. If the passed
2668+
// ShortChannelID is an alias, then we'll skip validation as it will
2669+
// not map to a legitimate tx. This is not a DoS vector as only we can
2670+
// add an alias ChannelAnnouncement from the gossiper.
2671+
if !(d.cfg.AssumeChannelValid || d.cfg.IsAlias(scid)) { //nolint:nestif
2672+
op, capacity, script, err := d.validateFundingTransaction(
2673+
ann, tapscriptRoot,
26512674
)
26522675
if err != nil {
26532676
defer d.channelMtx.Unlock(scid.ToUint64())
26542677

2655-
log.Errorf("Unable to make funding script %v", err)
2678+
switch {
2679+
case errors.Is(err, ErrNoFundingTransaction),
2680+
errors.Is(err, ErrInvalidFundingOutput):
2681+
2682+
key := newRejectCacheKey(
2683+
scid.ToUint64(),
2684+
sourceToPub(nMsg.source),
2685+
)
2686+
_, _ = d.recentRejects.Put(
2687+
key, &cachedReject{},
2688+
)
2689+
2690+
// Increment the peer's ban score. We check
2691+
// isRemote so we don't actually ban the peer in
2692+
// case of a local bug.
2693+
if nMsg.isRemote {
2694+
d.banman.incrementBanScore(
2695+
nMsg.peer.PubKey(),
2696+
)
2697+
}
2698+
2699+
case errors.Is(err, ErrChannelSpent):
2700+
key := newRejectCacheKey(
2701+
scid.ToUint64(),
2702+
sourceToPub(nMsg.source),
2703+
)
2704+
_, _ = d.recentRejects.Put(key, &cachedReject{})
2705+
2706+
// Since this channel has already been closed,
2707+
// we'll add it to the graph's closed channel
2708+
// index such that we won't attempt to do
2709+
// expensive validation checks on it again.
2710+
// TODO: Populate the ScidCloser by using closed
2711+
// channel notifications.
2712+
dbErr := d.cfg.ScidCloser.PutClosedScid(scid)
2713+
if dbErr != nil {
2714+
log.Errorf("failed to mark scid(%v) "+
2715+
"as closed: %v", scid, dbErr)
2716+
2717+
nMsg.err <- dbErr
2718+
2719+
return nil, false
2720+
}
2721+
2722+
// Increment the peer's ban score. We check
2723+
// isRemote so we don't accidentally ban
2724+
// ourselves in case of a bug.
2725+
if nMsg.isRemote {
2726+
d.banman.incrementBanScore(
2727+
nMsg.peer.PubKey(),
2728+
)
2729+
}
2730+
2731+
default:
2732+
// Otherwise, this is just a regular rejected
2733+
// edge.
2734+
key := newRejectCacheKey(
2735+
scid.ToUint64(),
2736+
sourceToPub(nMsg.source),
2737+
)
2738+
_, _ = d.recentRejects.Put(key, &cachedReject{})
2739+
}
2740+
2741+
if !nMsg.isRemote {
2742+
log.Errorf("failed to add edge for local "+
2743+
"channel: %v", err)
2744+
nMsg.err <- err
2745+
2746+
return nil, false
2747+
}
2748+
2749+
shouldDc, dcErr := d.ShouldDisconnect(
2750+
nMsg.peer.IdentityKey(),
2751+
)
2752+
if dcErr != nil {
2753+
log.Errorf("failed to check if we should "+
2754+
"disconnect peer: %v", dcErr)
2755+
nMsg.err <- dcErr
2756+
2757+
return nil, false
2758+
}
2759+
2760+
if shouldDc {
2761+
nMsg.peer.Disconnect(ErrPeerBanned)
2762+
}
2763+
26562764
nMsg.err <- err
26572765

26582766
return nil, false
26592767
}
26602768

2661-
edge.FundingScript = fn.Some(fundingPkScript)
2769+
edge.FundingScript = fn.Some(script)
2770+
2771+
// TODO(roasbeef): this is a hack, needs to be removed after
2772+
// commitment fees are dynamic.
2773+
edge.Capacity = capacity
2774+
edge.ChannelPoint = op
26622775
}
26632776

26642777
log.Debugf("Adding edge for short_chan_id: %v", scid.ToUint64())
@@ -2676,8 +2789,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
26762789
// If the edge was rejected due to already being known, then it
26772790
// may be the case that this new message has a fresh channel
26782791
// proof, so we'll check.
2679-
switch {
2680-
case graph.IsError(err, graph.ErrIgnored):
2792+
if graph.IsError(err, graph.ErrIgnored) {
26812793
// Attempt to process the rejected message to see if we
26822794
// get any new announcements.
26832795
anns, rErr := d.processRejectedEdge(ann, proof)
@@ -2690,6 +2802,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
26902802
_, _ = d.recentRejects.Put(key, cr)
26912803

26922804
nMsg.err <- rErr
2805+
26932806
return nil, false
26942807
}
26952808

@@ -2705,62 +2818,15 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
27052818
nMsg.err <- nil
27062819

27072820
return anns, true
2708-
2709-
case errors.Is(err, graph.ErrNoFundingTransaction),
2710-
errors.Is(err, graph.ErrInvalidFundingOutput):
2711-
2712-
key := newRejectCacheKey(
2713-
scid.ToUint64(),
2714-
sourceToPub(nMsg.source),
2715-
)
2716-
_, _ = d.recentRejects.Put(key, &cachedReject{})
2717-
2718-
// Increment the peer's ban score. We check isRemote
2719-
// so we don't actually ban the peer in case of a local
2720-
// bug.
2721-
if nMsg.isRemote {
2722-
d.banman.incrementBanScore(nMsg.peer.PubKey())
2723-
}
2724-
2725-
case errors.Is(err, graph.ErrChannelSpent):
2726-
key := newRejectCacheKey(
2727-
scid.ToUint64(),
2728-
sourceToPub(nMsg.source),
2729-
)
2730-
_, _ = d.recentRejects.Put(key, &cachedReject{})
2731-
2732-
// Since this channel has already been closed, we'll
2733-
// add it to the graph's closed channel index such that
2734-
// we won't attempt to do expensive validation checks
2735-
// on it again.
2736-
// TODO: Populate the ScidCloser by using closed
2737-
// channel notifications.
2738-
dbErr := d.cfg.ScidCloser.PutClosedScid(scid)
2739-
if dbErr != nil {
2740-
log.Errorf("failed to mark scid(%v) as "+
2741-
"closed: %v", scid, dbErr)
2742-
2743-
nMsg.err <- dbErr
2744-
2745-
return nil, false
2746-
}
2747-
2748-
// Increment the peer's ban score. We check isRemote
2749-
// so we don't accidentally ban ourselves in case of a
2750-
// bug.
2751-
if nMsg.isRemote {
2752-
d.banman.incrementBanScore(nMsg.peer.PubKey())
2753-
}
2754-
2755-
default:
2756-
// Otherwise, this is just a regular rejected edge.
2757-
key := newRejectCacheKey(
2758-
scid.ToUint64(),
2759-
sourceToPub(nMsg.source),
2760-
)
2761-
_, _ = d.recentRejects.Put(key, &cachedReject{})
27622821
}
27632822

2823+
// Otherwise, this is just a regular rejected edge.
2824+
key := newRejectCacheKey(
2825+
scid.ToUint64(),
2826+
sourceToPub(nMsg.source),
2827+
)
2828+
_, _ = d.recentRejects.Put(key, &cachedReject{})
2829+
27642830
if !nMsg.isRemote {
27652831
log.Errorf("failed to add edge for local channel: %v",
27662832
err)
@@ -3622,6 +3688,114 @@ func (d *AuthenticatedGossiper) ShouldDisconnect(pubkey *btcec.PublicKey) (
36223688
return false, nil
36233689
}
36243690

3691+
// validateFundingTransaction fetches the channel announcements claimed funding
3692+
// transaction from chain to ensure that it exists, is not spent and matches
3693+
// the channel announcement proof. The transaction's outpoint and value are
3694+
// returned if we can glean them from the work done in this method.
3695+
func (d *AuthenticatedGossiper) validateFundingTransaction(
3696+
ann *lnwire.ChannelAnnouncement1,
3697+
tapscriptRoot fn.Option[chainhash.Hash]) (wire.OutPoint, btcutil.Amount,
3698+
[]byte, error) {
3699+
3700+
scid := ann.ShortChannelID
3701+
3702+
// Before we can add the channel to the channel graph, we need to obtain
3703+
// the full funding outpoint that's encoded within the channel ID.
3704+
fundingTx, err := lnwallet.FetchFundingTxWrapper(
3705+
d.cfg.ChainIO, &scid, d.quit,
3706+
)
3707+
if err != nil {
3708+
//nolint:ll
3709+
//
3710+
// In order to ensure we don't erroneously mark a channel as a
3711+
// zombie due to an RPC failure, we'll attempt to string match
3712+
// for the relevant errors.
3713+
//
3714+
// * btcd:
3715+
// * https://github.com/btcsuite/btcd/blob/master/rpcserver.go#L1316
3716+
// * https://github.com/btcsuite/btcd/blob/master/rpcserver.go#L1086
3717+
// * bitcoind:
3718+
// * https://github.com/bitcoin/bitcoin/blob/7fcf53f7b4524572d1d0c9a5fdc388e87eb02416/src/rpc/blockchain.cpp#L770
3719+
// * https://github.com/bitcoin/bitcoin/blob/7fcf53f7b4524572d1d0c9a5fdc388e87eb02416/src/rpc/blockchain.cpp#L954
3720+
switch {
3721+
case strings.Contains(err.Error(), "not found"):
3722+
fallthrough
3723+
3724+
case strings.Contains(err.Error(), "out of range"):
3725+
// If the funding transaction isn't found at all, then
3726+
// we'll mark the edge itself as a zombie so we don't
3727+
// continue to request it. We use the "zero key" for
3728+
// both node pubkeys so this edge can't be resurrected.
3729+
zErr := d.cfg.Graph.MarkZombieEdge(scid.ToUint64())
3730+
if zErr != nil {
3731+
return wire.OutPoint{}, 0, nil, zErr
3732+
}
3733+
3734+
default:
3735+
}
3736+
3737+
return wire.OutPoint{}, 0, nil, fmt.Errorf("%w: %w",
3738+
ErrNoFundingTransaction, err)
3739+
}
3740+
3741+
// Recreate witness output to be sure that declared in channel edge
3742+
// bitcoin keys and channel value corresponds to the reality.
3743+
fundingPkScript, err := makeFundingScript(
3744+
ann.BitcoinKey1[:], ann.BitcoinKey2[:], ann.Features,
3745+
tapscriptRoot,
3746+
)
3747+
if err != nil {
3748+
return wire.OutPoint{}, 0, nil, err
3749+
}
3750+
3751+
// Next we'll validate that this channel is actually well formed. If
3752+
// this check fails, then this channel either doesn't exist, or isn't
3753+
// the one that was meant to be created according to the passed channel
3754+
// proofs.
3755+
fundingPoint, err := chanvalidate.Validate(
3756+
&chanvalidate.Context{
3757+
Locator: &chanvalidate.ShortChanIDChanLocator{
3758+
ID: scid,
3759+
},
3760+
MultiSigPkScript: fundingPkScript,
3761+
FundingTx: fundingTx,
3762+
},
3763+
)
3764+
if err != nil {
3765+
// Mark the edge as a zombie so we won't try to re-validate it
3766+
// on start up.
3767+
zErr := d.cfg.Graph.MarkZombieEdge(scid.ToUint64())
3768+
if zErr != nil {
3769+
return wire.OutPoint{}, 0, nil, zErr
3770+
}
3771+
3772+
return wire.OutPoint{}, 0, nil, fmt.Errorf("%w: %w",
3773+
ErrInvalidFundingOutput, err)
3774+
}
3775+
3776+
// Now that we have the funding outpoint of the channel, ensure
3777+
// that it hasn't yet been spent. If so, then this channel has
3778+
// been closed so we'll ignore it.
3779+
chanUtxo, err := d.cfg.ChainIO.GetUtxo(
3780+
fundingPoint, fundingPkScript, scid.BlockHeight, d.quit,
3781+
)
3782+
if err != nil {
3783+
if errors.Is(err, btcwallet.ErrOutputSpent) {
3784+
zErr := d.cfg.Graph.MarkZombieEdge(scid.ToUint64())
3785+
if zErr != nil {
3786+
return wire.OutPoint{}, 0, nil, zErr
3787+
}
3788+
}
3789+
3790+
return wire.OutPoint{}, 0, nil, fmt.Errorf("%w: unable to "+
3791+
"fetch utxo for chan_id=%v, chan_point=%v: %w",
3792+
ErrChannelSpent, scid.ToUint64(), fundingPoint, err)
3793+
}
3794+
3795+
return *fundingPoint, btcutil.Amount(chanUtxo.Value), fundingPkScript,
3796+
nil
3797+
}
3798+
36253799
// makeFundingScript is used to make the funding script for both segwit v0 and
36263800
// segwit v1 (taproot) channels.
36273801
func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte,

0 commit comments

Comments
 (0)