Repository: lightningnetwork/lnd
Pull request #8962 (2024-07-31 20:21:25)

[NANO]: Refactor/quiescence micro spinoffs
## Change Description
Spun off from #8270 [comment](https://github.com/lightningnetwork/lnd/pull/8270#issuecomment-2260439007)

Just read the diff.



Patch:
diff --git a/cluster/etcd_elector.go b/cluster/etcd_elector.go
index 7e6c74d53..25b11af25 100644
--- a/cluster/etcd_elector.go
+++ b/cluster/etcd_elector.go
@@ -99,27 +99,9 @@ func (e *etcdLeaderElector) Leader(ctx context.Context) (string, error) {
 		return "", err
 	}
 
-	if resp == nil || len(resp.Kvs) == 0 {
-		return "", nil
-	}
-
 	return string(resp.Kvs[0].Value), nil
 }
 
-// IsLeader returns true if the caller is the leader.
-func (e *etcdLeaderElector) IsLeader(ctx context.Context) (bool, error) {
-	resp, err := e.election.Leader(ctx)
-	if err != nil {
-		return false, err
-	}
-
-	if resp == nil || len(resp.Kvs) == 0 {
-		return false, nil
-	}
-
-	return string(resp.Kvs[0].Value) == e.id, nil
-}
-
 // Campaign will start a new leader election campaign. Campaign will block until
 // the elector context is canceled or the caller is elected as the leader.
 func (e *etcdLeaderElector) Campaign(ctx context.Context) error {
@@ -128,6 +110,6 @@ func (e *etcdLeaderElector) Campaign(ctx context.Context) error {
 
 // Resign resigns the leader role allowing other election members to take
 // the place.
-func (e *etcdLeaderElector) Resign(ctx context.Context) error {
-	return e.election.Resign(ctx)
+func (e *etcdLeaderElector) Resign() error {
+	return e.election.Resign(context.Background())
 }
diff --git a/cluster/interface.go b/cluster/interface.go
index 098c061a6..8a317095e 100644
--- a/cluster/interface.go
+++ b/cluster/interface.go
@@ -19,11 +19,8 @@ type LeaderElector interface {
 
 	// Resign resigns from the leader role, allowing other election members
 	// to take on leadership.
-	Resign(ctx context.Context) error
+	Resign() error
 
 	// Leader returns the leader value for the current election.
 	Leader(ctx context.Context) (string, error)
-
-	// IsLeader returns true if the caller is the leader.
-	IsLeader(ctx context.Context) (bool, error)
 }
diff --git a/config.go b/config.go
index 34d84720c..792c286ac 100644
--- a/config.go
+++ b/config.go
@@ -169,17 +169,6 @@ const (
 	defaultRSBackoff  = time.Second * 30
 	defaultRSAttempts = 1
 
-	// Set defaults for a health check which ensures that the leader
-	// election is functioning correctly. Although this check is off by
-	// default (as etcd leader election is only used in a clustered setup),
-	// we still set the default values so that the health check can be
-	// easily enabled with sane defaults. Note that by default we only run
-	// this check once, as it is critical for the node's operation.
-	defaultLeaderCheckInterval = time.Minute
-	defaultLeaderCheckTimeout  = time.Second * 5
-	defaultLeaderCheckBackoff  = time.Second * 5
-	defaultLeaderCheckAttempts = 1
-
 	// defaultRemoteMaxHtlcs specifies the default limit for maximum
 	// concurrent HTLCs the remote party may add to commitment transactions.
 	// This value can be overridden with --default-remote-max-htlcs.
@@ -683,12 +672,6 @@ func DefaultConfig() Config {
 				Attempts: defaultRSAttempts,
 				Backoff:  defaultRSBackoff,
 			},
-			LeaderCheck: &lncfg.CheckConfig{
-				Interval: defaultLeaderCheckInterval,
-				Timeout:  defaultLeaderCheckTimeout,
-				Attempts: defaultLeaderCheckAttempts,
-				Backoff:  defaultLeaderCheckBackoff,
-			},
 		},
 		Gossip: &lncfg.Gossip{
 			MaxChannelUpdateBurst: discovery.DefaultMaxChannelUpdateBurst,
diff --git a/docs/release-notes/release-notes-0.18.3.md b/docs/release-notes/release-notes-0.18.3.md
index fbe9c3924..f8f239702 100644
--- a/docs/release-notes/release-notes-0.18.3.md
+++ b/docs/release-notes/release-notes-0.18.3.md
@@ -136,9 +136,6 @@ commitment when the channel was force closed.
   the `lncli addinvoice` command to instruct LND to include blinded paths in the
   invoice. 
 
-* Add the ability to [send to use multiple blinded payment
-  paths](https://github.com/lightningnetwork/lnd/pull/8764) in an MP payment.
-
 ## Testing
 ## Database
 
@@ -150,10 +147,6 @@ commitment when the channel was force closed.
 * [Fixed](https://github.com/lightningnetwork/lnd/pull/8854) pagination issues
   in SQL invoicedb queries.
 
-* [Check](https://github.com/lightningnetwork/lnd/pull/8938) leader status with
-  our health checker to correctly shut down LND if network partitioning occurs
-  towards the etcd cluster.
-
 ## Code Health
 
 * [Move graph building and
diff --git a/go.mod b/go.mod
index f3d7d1655..69e59f357 100644
--- a/go.mod
+++ b/go.mod
@@ -207,12 +207,6 @@ replace google.golang.org/protobuf => github.com/lightninglabs/protobuf-go-hex-d
 // Temporary replace until the next version of sqldb is tagged.
 replace github.com/lightningnetwork/lnd/sqldb => ./sqldb
 
-// Temporary replace until the next version of healthcheck is tagged.
-replace github.com/lightningnetwork/lnd/healthcheck => ./healthcheck
-
-// Temporary replace until the next version of kvdb is tagged.
-replace github.com/lightningnetwork/lnd/kvdb => ./kvdb
-
 // If you change this please also update .github/pull_request_template.md and
 // docs/INSTALL.md.
 go 1.21.4
diff --git a/go.sum b/go.sum
index d8536ac54..d371e4837 100644
--- a/go.sum
+++ b/go.sum
@@ -452,6 +452,10 @@ github.com/lightningnetwork/lnd/clock v1.1.1 h1:OfR3/zcJd2RhH0RU+zX/77c0ZiOnIMsD
 github.com/lightningnetwork/lnd/clock v1.1.1/go.mod h1:mGnAhPyjYZQJmebS7aevElXKTFDuO+uNFFfMXK1W8xQ=
 github.com/lightningnetwork/lnd/fn v1.2.0 h1:YTb2m8NN5ZiJAskHeBZAmR1AiPY8SXziIYPAX1VI/ZM=
 github.com/lightningnetwork/lnd/fn v1.2.0/go.mod h1:SyFohpVrARPKH3XVAJZlXdVe+IwMYc4OMAvrDY32kw0=
+github.com/lightningnetwork/lnd/healthcheck v1.2.4 h1:lLPLac+p/TllByxGSlkCwkJlkddqMP5UCoawCj3mgFQ=
+github.com/lightningnetwork/lnd/healthcheck v1.2.4/go.mod h1:G7Tst2tVvWo7cx6mSBEToQC5L1XOGxzZTPB29g9Rv2I=
+github.com/lightningnetwork/lnd/kvdb v1.4.8 h1:xH0a5Vi1yrcZ5BEeF2ba3vlKBRxrL9uYXlWTjOjbNTY=
+github.com/lightningnetwork/lnd/kvdb v1.4.8/go.mod h1:J2diNABOoII9UrMnxXS5w7vZwP7CA1CStrl8MnIrb3A=
 github.com/lightningnetwork/lnd/queue v1.1.1 h1:99ovBlpM9B0FRCGYJo6RSFDlt8/vOkQQZznVb18iNMI=
 github.com/lightningnetwork/lnd/queue v1.1.1/go.mod h1:7A6nC1Qrm32FHuhx/mi1cieAiBZo5O6l8IBIoQxvkz4=
 github.com/lightningnetwork/lnd/ticker v1.1.1 h1:J/b6N2hibFtC7JLV77ULQp++QLtCwT6ijJlbdiZFbSM=
diff --git a/healthcheck/healthcheck.go b/healthcheck/healthcheck.go
index ed4866678..e15cbd4cb 100644
--- a/healthcheck/healthcheck.go
+++ b/healthcheck/healthcheck.go
@@ -234,13 +234,14 @@ func (o *Observation) monitor(shutdown shutdownFunc, quit chan struct{}) {
 			// the max attempts are reached. In that case we will
 			// stop the ticker and quit.
 			if o.retryCheck(quit, shutdown) {
-				o.Debugf("max attempts failed, monitor exiting")
+				log.Debugf("Health check: max attempts " +
+					"failed, monitor exiting")
 				return
 			}
 
 		// Exit if we receive the instruction to shutdown.
 		case <-quit:
-			o.Debugf("monitor quit")
+			log.Debug("Health check: monitor quit")
 			return
 		}
 	}
@@ -269,7 +270,7 @@ func (o *Observation) retryCheck(quit chan struct{},
 			// so we'll invoke our success callback if defined and
 			// then exit.
 			if err == nil {
-				o.Debugf("invoking success callback")
+				log.Debug("invoking success callback")
 
 				// Invoke the success callback.
 				o.OnSuccess()
@@ -282,7 +283,7 @@ func (o *Observation) retryCheck(quit chan struct{},
 				"%v", o, o.Timeout)
 
 		case <-quit:
-			o.Debugf("monitor quit")
+			log.Debug("Health check: monitor quit")
 			return false
 		}
 
@@ -290,18 +291,17 @@ func (o *Observation) retryCheck(quit chan struct{},
 		// check has failed so we'll fire the on failure callback
 		// and request shutdown.
 		if count == o.Attempts {
-			o.Debugf("invoking failure callback")
+			log.Debug("invoking failure callback")
 
 			o.OnFailure()
 
-			shutdown("Health check: %v failed after %v calls", o,
-				o.Attempts)
-
+			shutdown("Health check: %v failed after %v "+
+				"calls", o, o.Attempts)
 			return true
 		}
 
-		o.Infof("failed with: %v, attempts: %v backing off for: %v",
-			err, count, o.Backoff)
+		log.Infof("Health check: %v, call: %v failed with: %v, "+
+			"backing off for: %v", o, count, err, o.Backoff)
 
 		// If we are still within the number of calls allowed for this
 		// check, we wait for our back off period to elapse, or exit if
@@ -310,22 +310,10 @@ func (o *Observation) retryCheck(quit chan struct{},
 		case <-time.After(o.Backoff):
 
 		case <-quit:
-			o.Debugf("monitor quit")
+			log.Debug("Health check: monitor quit")
 			return false
 		}
 	}
 
 	return false
 }
-
-// Infof logs an info message for an observation prefixed with the health check
-// name.
-func (o *Observation) Infof(format string, params ...interface{}) {
-	log.Debugf(fmt.Sprintf("Health check: %v ", o)+format, params...)
-}
-
-// Debugf logs a debug message for an observation prefixed with the health check
-// name.
-func (o *Observation) Debugf(format string, params ...interface{}) {
-	log.Debugf(fmt.Sprintf("Health check: %v ", o)+format, params...)
-}
diff --git a/htlcswitch/link.go b/htlcswitch/link.go
index eaeaf2e87..083677ec7 100644
--- a/htlcswitch/link.go
+++ b/htlcswitch/link.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	crand "crypto/rand"
 	"crypto/sha256"
+	"errors"
 	"fmt"
 	prand "math/rand"
 	"sync"
@@ -13,7 +14,6 @@ import (
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/btcsuite/btclog"
-	"github.com/go-errors/errors"
 	"github.com/lightningnetwork/lnd/build"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/channeldb/models"
@@ -480,7 +480,7 @@ var _ ChannelLink = (*channelLink)(nil)
 // NOTE: Part of the ChannelLink interface.
 func (l *channelLink) Start() error {
 	if !atomic.CompareAndSwapInt32(&l.started, 0, 1) {
-		err := errors.Errorf("channel link(%v): already started", l)
+		err := fmt.Errorf("channel link(%v): already started", l)
 		l.log.Warn("already started")
 		return err
 	}
@@ -4080,7 +4080,7 @@ func (l *channelLink) sendMalformedHTLCError(htlcIndex uint64,
 // remote peer.
 func (l *channelLink) fail(linkErr LinkFailureError,
 	format string, a ...interface{}) {
-	reason := errors.Errorf(format, a...)
+	reason := fmt.Errorf(format, a...)
 
 	// Return if we have already notified about a failure.
 	if l.failed {
diff --git a/kvdb/etcd/stm.go b/kvdb/etcd/stm.go
index 3c933d5c4..5b3a9a977 100644
--- a/kvdb/etcd/stm.go
+++ b/kvdb/etcd/stm.go
@@ -8,25 +8,12 @@ import (
 	"fmt"
 	"math"
 	"strings"
-	"time"
 
 	"github.com/google/btree"
 	pb "go.etcd.io/etcd/api/v3/etcdserverpb"
 	v3 "go.etcd.io/etcd/client/v3"
 )
 
-const (
-	// rpcTimeout is the timeout for all RPC calls to etcd. It is set to 30
-	// seconds to avoid blocking the server for too long but give reasonable
-	// time for etcd to respond. If any operations would take longer than 30
-	// seconds that generally means there's a problem with the etcd server
-	// or the network resulting in degraded performance in which case we
-	// want LND to fail fast. Due to the underlying gRPC implementation in
-	// etcd calls without a timeout can hang indefinitely even in the case
-	// of network partitions or other critical failures.
-	rpcTimeout = time.Second * 30
-)
-
 type CommitStats struct {
 	Rset    int
 	Wset    int
@@ -622,13 +609,8 @@ func (s *stm) FetchRangePaginatedRaw(prefix string, limit int64,
 
 	key := prefix
 	for {
-		timeoutCtx, cancel := context.WithTimeout(
-			s.options.ctx, rpcTimeout,
-		)
-		defer cancel()
-
 		resp, err := s.client.Get(
-			timeoutCtx, key, append(opts, s.getOpts...)...,
+			s.options.ctx, key, append(opts, s.getOpts...)...,
 		)
 		if err != nil {
 			return DatabaseError{
@@ -663,12 +645,8 @@ func (s *stm) FetchRangePaginatedRaw(prefix string, limit int64,
 // We'll also cache the returned key/value in the read set.
 func (s *stm) fetch(key string, opts ...v3.OpOption) ([]KV, error) {
 	s.callCount++
-
-	timeoutCtx, cancel := context.WithTimeout(s.options.ctx, rpcTimeout)
-	defer cancel()
-
 	resp, err := s.client.Get(
-		timeoutCtx, key, append(opts, s.getOpts...)...,
+		s.options.ctx, key, append(opts, s.getOpts...)...,
 	)
 	if err != nil {
 		return nil, DatabaseError{
@@ -1071,10 +1049,7 @@ func (s *stm) Prefetch(keys []string, prefixes []string) {
 		[]v3.OpOption{v3.WithPrefix()}, s.getOpts...,
 	)
 
-	timeoutCtx, cancel := context.WithTimeout(s.options.ctx, rpcTimeout)
-	defer cancel()
-
-	txn := s.client.Txn(timeoutCtx)
+	txn := s.client.Txn(s.options.ctx)
 	ops := make([]v3.Op, 0, len(fetchKeys)+len(fetchPrefixes))
 
 	for _, key := range fetchKeys {
@@ -1128,11 +1103,8 @@ func (s *stm) commit() (CommitStats, error) {
 
 	// Create the compare set.
 	cmps := append(rset, wset...)
-
 	// Create a transaction with the optional abort context.
-	timeoutCtx, cancel := context.WithTimeout(s.options.ctx, rpcTimeout)
-	defer cancel()
-	txn := s.client.Txn(timeoutCtx)
+	txn := s.client.Txn(s.options.ctx)
 
 	// If the compare set holds, try executing the puts.
 	txn = txn.If(cmps...)
diff --git a/lncfg/cluster.go b/lncfg/cluster.go
index e9e871a36..8b3b14f5f 100644
--- a/lncfg/cluster.go
+++ b/lncfg/cluster.go
@@ -33,7 +33,7 @@ func DefaultCluster() *Cluster {
 	return &Cluster{
 		LeaderElector:      cluster.EtcdLeaderElector,
 		EtcdElectionPrefix: DefaultEtcdElectionPrefix,
-		LeaderSessionTTL:   90,
+		LeaderSessionTTL:   60,
 		ID:                 hostname,
 	}
 }
diff --git a/lncfg/healthcheck.go b/lncfg/healthcheck.go
index 9a1403687..92c415474 100644
--- a/lncfg/healthcheck.go
+++ b/lncfg/healthcheck.go
@@ -34,8 +34,6 @@ type HealthCheckConfig struct {
 	TorConnection *CheckConfig `group:"torconnection" namespace:"torconnection"`
 
 	RemoteSigner *CheckConfig `group:"remotesigner" namespace:"remotesigner"`
-
-	LeaderCheck *CheckConfig `group:"leader" namespace:"leader"`
 }
 
 // Validate checks the values configured for our health checks.
diff --git a/lnd.go b/lnd.go
index bc74c9b96..0a85e1bf3 100644
--- a/lnd.go
+++ b/lnd.go
@@ -24,7 +24,6 @@ import (
 	"github.com/lightningnetwork/lnd/build"
 	"github.com/lightningnetwork/lnd/chanacceptor"
 	"github.com/lightningnetwork/lnd/channeldb"
-	"github.com/lightningnetwork/lnd/cluster"
 	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/lncfg"
 	"github.com/lightningnetwork/lnd/lnrpc"
@@ -57,14 +56,6 @@ const (
 	// admin macaroon unless the administrator explicitly allowed it. Thus
 	// there's no harm allowing group read.
 	adminMacaroonFilePermissions = 0640
-
-	// leaderResignTimeout is the timeout used when resigning from the
-	// leader role. This is kept short so LND can shut down quickly in case
-	// of a system failure or network partition making the cluster
-	// unresponsive. The cluster itself should ensure that the leader is not
-	// elected again until the previous leader has resigned or the leader
-	// election timeout has passed.
-	leaderResignTimeout = 5 * time.Second
 )
 
 // AdminAuthOptions returns a list of DialOptions that can be used to
@@ -390,7 +381,6 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
 	// blocked until this instance is elected as the current leader or
 	// shutting down.
 	elected := false
-	var leaderElector cluster.LeaderElector
 	if cfg.Cluster.EnableLeaderElection {
 		electionCtx, cancelElection := context.WithCancel(ctx)
 
@@ -402,7 +392,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
 		ltndLog.Infof("Using %v leader elector",
 			cfg.Cluster.LeaderElector)
 
-		leaderElector, err = cfg.Cluster.MakeLeaderElector(
+		leaderElector, err := cfg.Cluster.MakeLeaderElector(
 			electionCtx, cfg.DB,
 		)
 		if err != nil {
@@ -417,17 +407,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
 			ltndLog.Infof("Attempting to resign from leader role "+
 				"(%v)", cfg.Cluster.ID)
 
-			// Ensure that we don't block the shutdown process if
-			// the leader resigning process takes too long. The
-			// cluster will ensure that the leader is not elected
-			// again until the previous leader has resigned or the
-			// leader election timeout has passed.
-			timeoutCtx, cancel := context.WithTimeout(
-				ctx, leaderResignTimeout,
-			)
-			defer cancel()
-
-			if err := leaderElector.Resign(timeoutCtx); err != nil {
+			if err := leaderElector.Resign(); err != nil {
 				ltndLog.Errorf("Leader elector failed to "+
 					"resign: %v", err)
 			}
@@ -599,7 +579,7 @@ func Main(cfg *Config, lisCfg ListenerCfg, implCfg *ImplementationCfg,
 	server, err := newServer(
 		cfg, cfg.Listeners, dbs, activeChainControl, &idKeyDesc,
 		activeChainControl.Cfg.WalletUnlockParams.ChansToRestore,
-		multiAcceptor, torController, tlsManager, leaderElector,
+		multiAcceptor, torController, tlsManager,
 	)
 	if err != nil {
 		return mkErr("unable to create server: %v", err)
diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go
index faf53c4b6..970fb04cf 100644
--- a/lnrpc/routerrpc/router_backend.go
+++ b/lnrpc/routerrpc/router_backend.go
@@ -280,7 +280,7 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
 	var (
 		targetPubKey   *route.Vertex
 		routeHintEdges map[route.Vertex][]routing.AdditionalEdge
-		blindedPathSet *routing.BlindedPaymentPathSet
+		blindedPmt     *routing.BlindedPayment
 
 		// finalCLTVDelta varies depending on whether we're sending to
 		// a blinded route or an unblinded node. For blinded paths,
@@ -297,14 +297,13 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
 	// Validate that the fields provided in the request are sane depending
 	// on whether it is using a blinded path or not.
 	if len(in.BlindedPaymentPaths) > 0 {
-		blindedPathSet, err = parseBlindedPaymentPaths(in)
+		blindedPmt, err = parseBlindedPayment(in)
 		if err != nil {
 			return nil, err
 		}
 
-		pathFeatures := blindedPathSet.Features()
-		if pathFeatures != nil {
-			destinationFeatures = pathFeatures.Clone()
+		if blindedPmt.Features != nil {
+			destinationFeatures = blindedPmt.Features.Clone()
 		}
 	} else {
 		// If we do not have a blinded path, a target pubkey must be
@@ -388,10 +387,10 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
 				fromNode, toNode, amt, capacity,
 			)
 		},
-		DestCustomRecords:     record.CustomSet(in.DestCustomRecords),
-		CltvLimit:             cltvLimit,
-		DestFeatures:          destinationFeatures,
-		BlindedPaymentPathSet: blindedPathSet,
+		DestCustomRecords: record.CustomSet(in.DestCustomRecords),
+		CltvLimit:         cltvLimit,
+		DestFeatures:      destinationFeatures,
+		BlindedPayment:    blindedPmt,
 	}
 
 	// Pass along an outgoing channel restriction if specified.
@@ -420,24 +419,39 @@ func (r *RouterBackend) parseQueryRoutesRequest(in *lnrpc.QueryRoutesRequest) (
 
 	return routing.NewRouteRequest(
 		sourcePubKey, targetPubKey, amt, in.TimePref, restrictions,
-		customRecords, routeHintEdges, blindedPathSet,
-		finalCLTVDelta,
+		customRecords, routeHintEdges, blindedPmt, finalCLTVDelta,
 	)
 }
 
-func parseBlindedPaymentPaths(in *lnrpc.QueryRoutesRequest) (
-	*routing.BlindedPaymentPathSet, error) {
+func parseBlindedPayment(in *lnrpc.QueryRoutesRequest) (
+	*routing.BlindedPayment, error) {
 
 	if len(in.PubKey) != 0 {
 		return nil, fmt.Errorf("target pubkey: %x should not be set "+
 			"when blinded path is provided", in.PubKey)
 	}
 
+	if len(in.BlindedPaymentPaths) != 1 {
+		return nil, errors.New("query routes only supports a single " +
+			"blinded path")
+	}
+
+	blindedPath := in.BlindedPaymentPaths[0]
+
 	if len(in.RouteHints) > 0 {
 		return nil, errors.New("route hints and blinded path can't " +
 			"both be set")
 	}
 
+	blindedPmt, err := unmarshalBlindedPayment(blindedPath)
+	if err != nil {
+		return nil, fmt.Errorf("parse blinded payment: %w", err)
+	}
+
+	if err := blindedPmt.Validate(); err != nil {
+		return nil, fmt.Errorf("invalid blinded path: %w", err)
+	}
+
 	if in.FinalCltvDelta != 0 {
 		return nil, errors.New("final cltv delta should be " +
 			"zero for blinded paths")
@@ -452,21 +466,7 @@ func parseBlindedPaymentPaths(in *lnrpc.QueryRoutesRequest) (
 			"be populated in blinded path")
 	}
 
-	paths := make([]*routing.BlindedPayment, len(in.BlindedPaymentPaths))
-	for i, paymentPath := range in.BlindedPaymentPaths {
-		blindedPmt, err := unmarshalBlindedPayment(paymentPath)
-		if err != nil {
-			return nil, fmt.Errorf("parse blinded payment: %w", err)
-		}
-
-		if err := blindedPmt.Validate(); err != nil {
-			return nil, fmt.Errorf("invalid blinded path: %w", err)
-		}
-
-		paths[i] = blindedPmt
-	}
-
-	return routing.NewBlindedPaymentPathSet(paths)
+	return blindedPmt, nil
 }
 
 func unmarshalBlindedPayment(rpcPayment *lnrpc.BlindedPaymentPath) (
@@ -1001,24 +1001,28 @@ func (r *RouterBackend) extractIntentFromSendRequest(
 		payIntent.Metadata = payReq.Metadata
 
 		if len(payReq.BlindedPaymentPaths) > 0 {
-			pathSet, err := BuildBlindedPathSet(
-				payReq.BlindedPaymentPaths,
-			)
-			if err != nil {
-				return nil, err
+			// NOTE: Currently we only choose a single payment path.
+			// This will be updated in a future PR to handle
+			// multiple blinded payment paths.
+			path := payReq.BlindedPaymentPaths[0]
+			if len(path.Hops) == 0 {
+				return nil, fmt.Errorf("a blinded payment " +
+					"must have at least 1 hop")
 			}
-			payIntent.BlindedPathSet = pathSet
 
-			// Replace the target node with the target public key
-			// of the blinded path set.
+			finalHop := path.Hops[len(path.Hops)-1]
+
+			payIntent.BlindedPayment = MarshalBlindedPayment(path)
+
+			// Replace the target node with the blinded public key
+			// of the blinded path's final node.
 			copy(
 				payIntent.Target[:],
-				pathSet.TargetPubKey().SerializeCompressed(),
+				finalHop.BlindedNodePub.SerializeCompressed(),
 			)
 
-			pathFeatures := pathSet.Features()
-			if !pathFeatures.IsEmpty() {
-				payIntent.DestFeatures = pathFeatures.Clone()
+			if !path.Features.IsEmpty() {
+				payIntent.DestFeatures = path.Features.Clone()
 			}
 		}
 	} else {
@@ -1159,29 +1163,9 @@ func (r *RouterBackend) extractIntentFromSendRequest(
 	return payIntent, nil
 }
 
-// BuildBlindedPathSet marshals a set of zpay32.BlindedPaymentPath and uses
-// the result to build a new routing.BlindedPaymentPathSet.
-func BuildBlindedPathSet(paths []*zpay32.BlindedPaymentPath) (
-	*routing.BlindedPaymentPathSet, error) {
-
-	marshalledPaths := make([]*routing.BlindedPayment, len(paths))
-	for i, path := range paths {
-		paymentPath := marshalBlindedPayment(path)
-
-		err := paymentPath.Validate()
-		if err != nil {
-			return nil, err
-		}
-
-		marshalledPaths[i] = paymentPath
-	}
-
-	return routing.NewBlindedPaymentPathSet(marshalledPaths)
-}
-
-// marshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a
+// MarshalBlindedPayment marshals a zpay32.BLindedPaymentPath into a
 // routing.BlindedPayment.
-func marshalBlindedPayment(
+func MarshalBlindedPayment(
 	path *zpay32.BlindedPaymentPath) *routing.BlindedPayment {
 
 	return &routing.BlindedPayment{
diff --git a/routing/blinding.go b/routing/blinding.go
index 270f998d9..788fb7b77 100644
--- a/routing/blinding.go
+++ b/routing/blinding.go
@@ -4,10 +4,8 @@ import (
 	"errors"
 	"fmt"
 
-	"github.com/btcsuite/btcd/btcec/v2"
 	sphinx "github.com/lightningnetwork/lightning-onion"
 	"github.com/lightningnetwork/lnd/channeldb/models"
-	"github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/routing/route"
 )
@@ -27,218 +25,6 @@ var (
 	ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum")
 )
 
-// BlindedPaymentPathSet groups the data we need to handle sending to a set of
-// blinded paths provided by the recipient of a payment.
-//
-// NOTE: for now this only holds a single BlindedPayment. By the end of the PR
-// series, it will handle multiple paths.
-type BlindedPaymentPathSet struct {
-	// paths is the set of blinded payment paths for a single payment.
-	// NOTE: For now this will always only have a single entry. By the end
-	// of this PR, it can hold multiple.
-	paths []*BlindedPayment
-
-	// targetPubKey is the ephemeral node pub key that we will inject into
-	// each path as the last hop. This is only for the sake of path finding.
-	// Once the path has been found, the original destination pub key is
-	// used again. In the edge case where there is only a single hop in the
-	// path (the introduction node is the destination node), then this will
-	// just be the introduction node's real public key.
-	targetPubKey *btcec.PublicKey
-
-	// features is the set of relay features available for the payment.
-	// This is extracted from the set of blinded payment paths. At the
-	// moment we require that all paths for the same payment have the
-	// same feature set.
-	features *lnwire.FeatureVector
-
-	// finalCLTV is the final hop's expiry delta of _any_ path in the set.
-	// For any multi-hop path, the final CLTV delta should be seen as zero
-	// since the final hop's final CLTV delta is accounted for in the
-	// accumulated path policy values. The only edge case is for when the
-	// final hop in the path is also the introduction node in which case
-	// that path's FinalCLTV must be the non-zero min CLTV of the final hop
-	// so that it is accounted for in path finding. For this reason, if
-	// we have any single path in the set with only one hop, then we throw
-	// away all the other paths. This should be fine to do since if there is
-	// a path where the intro node is also the destination node, then there
-	// isn't any need to try any other longer blinded path. In other words,
-	// if this value is non-zero, then there is only one path in this
-	// blinded path set and that path only has a single hop: the
-	// introduction node.
-	finalCLTV uint16
-}
-
-// NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
-// BlindedPayments.
-func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
-	error) {
-
-	if len(paths) == 0 {
-		return nil, ErrNoBlindedPath
-	}
-
-	// For now, we assert that all the paths have the same set of features.
-	features := paths[0].Features
-	noFeatures := features == nil || features.IsEmpty()
-	for i := 1; i < len(paths); i++ {
-		noFeats := paths[i].Features == nil ||
-			paths[i].Features.IsEmpty()
-
-		if noFeatures && !noFeats {
-			return nil, fmt.Errorf("all blinded paths must have " +
-				"the same set of features")
-		}
-
-		if noFeatures {
-			continue
-		}
-
-		if !features.RawFeatureVector.Equals(
-			paths[i].Features.RawFeatureVector,
-		) {
-
-			return nil, fmt.Errorf("all blinded paths must have " +
-				"the same set of features")
-		}
-	}
-
-	// Derive an ephemeral target priv key that will be injected into each
-	// blinded path final hop.
-	targetPriv, err := btcec.NewPrivateKey()
-	if err != nil {
-		return nil, err
-	}
-	targetPub := targetPriv.PubKey()
-
-	var (
-		pathSet        = paths
-		finalCLTVDelta uint16
-	)
-	// If any provided blinded path only has a single hop (ie, the
-	// destination node is also the introduction node), then we discard all
-	// other paths since we know the real pub key of the destination node.
-	// We also then set the final CLTV delta to the path's delta since
-	// there are no other edge hints that will account for it. For a single
-	// hop path, there is also no need for the pseudo target pub key
-	// replacement, so our target pub key in this case just remains the
-	// real introduction node ID.
-	for _, path := range paths {
-		if len(path.BlindedPath.BlindedHops) != 1 {
-			continue
-		}
-
-		pathSet = []*BlindedPayment{path}
-		finalCLTVDelta = path.CltvExpiryDelta
-		targetPub = path.BlindedPath.IntroductionPoint
-
-		break
-	}
-
-	return &BlindedPaymentPathSet{
-		paths:        pathSet,
-		targetPubKey: targetPub,
-		features:     features,
-		finalCLTV:    finalCLTVDelta,
-	}, nil
-}
-
-// TargetPubKey returns the public key to be used as the destination node's
-// public key during pathfinding.
-func (s *BlindedPaymentPathSet) TargetPubKey() *btcec.PublicKey {
-	return s.targetPubKey
-}
-
-// Features returns the set of relay features available for the payment.
-func (s *BlindedPaymentPathSet) Features() *lnwire.FeatureVector {
-	return s.features
-}
-
-// IntroNodeOnlyPath can be called if it is expected that the path set only
-// contains a single payment path which itself only has one hop. It errors if
-// this is not the case.
-func (s *BlindedPaymentPathSet) IntroNodeOnlyPath() (*BlindedPayment, error) {
-	if len(s.paths) != 1 {
-		return nil, fmt.Errorf("expected only a single path in the "+
-			"blinded payment set, got %d", len(s.paths))
-	}
-
-	if len(s.paths[0].BlindedPath.BlindedHops) > 1 {
-		return nil, fmt.Errorf("an intro node only path cannot have " +
-			"more than one hop")
-	}
-
-	return s.paths[0], nil
-}
-
-// IsIntroNode returns true if the given vertex is an introduction node for one
-// of the paths in the blinded payment path set.
-func (s *BlindedPaymentPathSet) IsIntroNode(source route.Vertex) bool {
-	for _, path := range s.paths {
-		introVertex := route.NewVertex(
-			path.BlindedPath.IntroductionPoint,
-		)
-		if source == introVertex {
-			return true
-		}
-	}
-
-	return false
-}
-
-// FinalCLTVDelta is the minimum CLTV delta to use for the final hop on the
-// route. In most cases this will return zero since the value is accounted for
-// in the path's accumulated CLTVExpiryDelta. Only in the edge case of the path
-// set only including a single path which only includes an introduction node
-// will this return a non-zero value.
-func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
-	return s.finalCLTV
-}
-
-// LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
-// largest last-hop payload. This is to be used for onion size estimation in
-// path finding.
-func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() *BlindedPayment {
-	var (
-		largestPath *BlindedPayment
-		currentMax  int
-	)
-	for _, path := range s.paths {
-		numHops := len(path.BlindedPath.BlindedHops)
-		lastHop := path.BlindedPath.BlindedHops[numHops-1]
-
-		if len(lastHop.CipherText) > currentMax {
-			largestPath = path
-		}
-	}
-
-	return largestPath
-}
-
-// ToRouteHints converts the blinded path payment set into a RouteHints map so
-// that the blinded payment paths can be treated like route hints throughout the
-// code base.
-func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
-	hints := make(RouteHints)
-
-	for _, path := range s.paths {
-		pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey))
-		if err != nil {
-			return nil, err
-		}
-
-		for from, edges := range pathHints {
-			hints[from] = append(hints[from], edges...)
-		}
-	}
-
-	if len(hints) == 0 {
-		return nil, nil
-	}
-
-	return hints, nil
-}
-
 // BlindedPayment provides the path and payment parameters required to send a
 // payment along a blinded path.
 type BlindedPayment struct {
@@ -301,11 +87,8 @@ func (b *BlindedPayment) Validate() error {
 // effectively the final_cltv_delta for the receiving introduction node). In
 // the case of multiple blinded hops, CLTV delta is fully accounted for in the
 // hints (both for intermediate hops and the final_cltv_delta for the receiving
-// node). The pseudoTarget, if provided,  will be used to override the pub key
-// of the destination node in the path.
-func (b *BlindedPayment) toRouteHints(
-	pseudoTarget fn.Option[*btcec.PublicKey]) (RouteHints, error) {
-
+// node).
+func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
 	// If we just have a single hop in our blinded route, it just contains
 	// an introduction node (this is a valid path according to the spec).
 	// Since we have the un-blinded node ID for the introduction node, we
@@ -353,12 +136,12 @@ func (b *BlindedPayment) toRouteHints(
 		ToNodeFeatures: features,
 	}
 
-	lastEdge, err := NewBlindedEdge(edgePolicy, b, 0)
+	edge, err := NewBlindedEdge(edgePolicy, b, 0)
 	if err != nil {
 		return nil, err
 	}
 
-	hints[fromNode] = []AdditionalEdge{lastEdge}
+	hints[fromNode] = []AdditionalEdge{edge}
 
 	// Start at an offset of 1 because the first node in our blinded hops
 	// is the introduction node and terminate at the second-last node
@@ -385,24 +168,13 @@ func (b *BlindedPayment) toRouteHints(
 			ToNodeFeatures: features,
 		}
 
-		lastEdge, err = NewBlindedEdge(edgePolicy, b, i)
+		edge, err := NewBlindedEdge(edgePolicy, b, i)
 		if err != nil {
 			return nil, err
 		}
 
-		hints[fromNode] = []AdditionalEdge{lastEdge}
+		hints[fromNode] = []AdditionalEdge{edge}
 	}
 
-	pseudoTarget.WhenSome(func(key *btcec.PublicKey) {
-		// For the very last hop on the path, switch out the ToNodePub
-		// for the pseudo target pub key.
-		lastEdge.policy.ToNodePubKey = func() route.Vertex {
-			return route.NewVertex(key)
-		}
-
-		// Then override the final hint with this updated edge.
-		hints[fromNode] = []AdditionalEdge{lastEdge}
-	})
-
 	return hints, nil
 }
diff --git a/routing/pathfind.go b/routing/pathfind.go
index ba9d111c4..d76c5ea22 100644
--- a/routing/pathfind.go
+++ b/routing/pathfind.go
@@ -136,8 +136,9 @@ type finalHopParams struct {
 // NOTE: If a non-nil blinded path is provided it is assumed to have been
 // validated by the caller.
 func newRoute(sourceVertex route.Vertex,
-	pathEdges []*unifiedEdge, currentHeight uint32, finalHop finalHopParams,
-	blindedPathSet *BlindedPaymentPathSet) (*route.Route, error) {
+	pathEdges []*unifiedEdge, currentHeight uint32,
+	finalHop finalHopParams, blindedPath *sphinx.BlindedPath) (
+	*route.Route, error) {
 
 	var (
 		hops []*route.Hop
@@ -152,8 +153,6 @@ func newRoute(sourceVertex route.Vertex,
 		// backwards below, this next hop gets closer and closer to the
 		// sender of the payment.
 		nextIncomingAmount lnwire.MilliSatoshi
-
-		blindedPayment *BlindedPayment
 	)
 
 	pathLength := len(pathEdges)
@@ -162,15 +161,6 @@ func newRoute(sourceVertex route.Vertex,
 		// payload for the hop this edge is leading to.
 		edge := pathEdges[i].policy
 
-		// If this is an edge from a blinded path and the
-		// blindedPayment variable has not been set yet, then set it now
-		// by extracting the corresponding blinded payment from the
-		// edge.
-		isBlindedEdge := pathEdges[i].blindedPayment != nil
-		if isBlindedEdge && blindedPayment == nil {
-			blindedPayment = pathEdges[i].blindedPayment
-		}
-
 		// We'll calculate the amounts, timelocks, and fees for each hop
 		// in the route. The base case is the final hop which includes
 		// their amount and timelocks. These values will accumulate
@@ -210,12 +200,20 @@ func newRoute(sourceVertex route.Vertex,
 			// reporting through RPC. Set to zero for the final hop.
 			fee = 0
 
-			if blindedPathSet == nil {
+			// Only include the final hop CLTV delta in the total
+			// time lock value if this is not a route to a blinded
+			// path. For blinded paths, the total time-lock from the
+			// whole path will be deduced from the introduction
+			// node's CLTV delta. The exception is for the case
+			// where the final hop is the blinded path introduction
+			// node.
+			if blindedPath == nil ||
+				len(blindedPath.BlindedHops) == 1 {
+
+				// As this is the last hop, we'll use the
+				// specified final CLTV delta value instead of
+				// the value from the last link in the route.
 				totalTimeLock += uint32(finalHop.cltvDelta)
-			} else {
-				totalTimeLock += uint32(
-					blindedPathSet.FinalCLTVDelta(),
-				)
 			}
 			outgoingTimeLock = totalTimeLock
 
@@ -242,7 +240,7 @@ func newRoute(sourceVertex route.Vertex,
 
 			metadata = finalHop.metadata
 
-			if blindedPathSet != nil {
+			if blindedPath != nil {
 				totalAmtMsatBlinded = finalHop.totalAmt
 			}
 		} else {
@@ -302,29 +300,11 @@ func newRoute(sourceVertex route.Vertex,
 	// If we are creating a route to a blinded path, we need to add some
 	// additional data to the route that is required for blinded forwarding.
 	// We do another pass on our edges to append this data.
-	if blindedPathSet != nil {
-		// If the passed in BlindedPaymentPathSet is non-nil but no
-		// edge had a BlindedPayment attached, it means that the path
-		// chosen was an introduction-node-only path. So in this case,
-		// we can assume the relevant payment is the only one in the
-		// payment set.
-		if blindedPayment == nil {
-			var err error
-			blindedPayment, err = blindedPathSet.IntroNodeOnlyPath()
-			if err != nil {
-				return nil, err
-			}
-		}
-
+	if blindedPath != nil {
 		var (
 			inBlindedRoute bool
 			dataIndex      = 0
 
-			blindedPath = blindedPayment.BlindedPath
-			numHops     = len(blindedPath.BlindedHops)
-			realFinal   = blindedPath.BlindedHops[numHops-1].
-					BlindedNodePub
-
 			introVertex = route.NewVertex(
 				blindedPath.IntroductionPoint,
 			)
@@ -352,11 +332,6 @@ func newRoute(sourceVertex route.Vertex,
 			if i != len(hops)-1 {
 				hop.AmtToForward = 0
 				hop.OutgoingTimeLock = 0
-			} else {
-				// For the final hop, we swap out the pub key
-				// bytes to the original destination node pub
-				// key for that payment path.
-				hop.PubKeyBytes = route.NewVertex(realFinal)
 			}
 
 			dataIndex++
@@ -462,9 +437,9 @@ type RestrictParams struct {
 	// the payee.
 	Metadata []byte
 
-	// BlindedPaymentPathSet is necessary to determine the hop size of the
+	// BlindedPayment is necessary to determine the hop size of the
 	// last/exit hop.
-	BlindedPaymentPathSet *BlindedPaymentPathSet
+	BlindedPayment *BlindedPayment
 }
 
 // PathFindingConfig defines global parameters that control the trade-off in
@@ -1156,7 +1131,7 @@ type blindedPathRestrictions struct {
 // path.
 type blindedHop struct {
 	vertex       route.Vertex
-	channelID    uint64
+	edgePolicy   *models.CachedEdgePolicy
 	edgeCapacity btcutil.Amount
 }
 
@@ -1296,7 +1271,7 @@ func processNodeForBlindedPath(g Graph, node route.Vertex,
 
 			hop := blindedHop{
 				vertex:       channel.OtherNode,
-				channelID:    channel.ChannelID,
+				edgePolicy:   channel.InPolicy,
 				edgeCapacity: channel.Capacity,
 			}
 
@@ -1390,11 +1365,9 @@ func getProbabilityBasedDist(weight int64, probability float64,
 func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
 	amount lnwire.MilliSatoshi) uint64 {
 
-	if r.BlindedPaymentPathSet != nil {
-		paymentPath := r.BlindedPaymentPathSet.
-			LargestLastHopPayloadPath()
-		blindedPath := paymentPath.BlindedPath.BlindedHops
-		blindedPoint := paymentPath.BlindedPath.BlindingPoint
+	if r.BlindedPayment != nil {
+		blindedPath := r.BlindedPayment.BlindedPath.BlindedHops
+		blindedPoint := r.BlindedPayment.BlindedPath.BlindingPoint
 
 		encryptedData := blindedPath[len(blindedPath)-1].CipherText
 		finalHop := route.Hop{
diff --git a/routing/payment_session.go b/routing/payment_session.go
index 00b4ab70e..f320ce0dc 100644
--- a/routing/payment_session.go
+++ b/routing/payment_session.go
@@ -5,6 +5,7 @@ import (
 
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btclog"
+	sphinx "github.com/lightningnetwork/lightning-onion"
 	"github.com/lightningnetwork/lnd/build"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/channeldb/models"
@@ -205,13 +206,13 @@ func newPaymentSession(p *LightningPayment, selfNode route.Vertex,
 		return nil, err
 	}
 
-	if p.BlindedPathSet != nil {
+	if p.BlindedPayment != nil {
 		if len(edges) != 0 {
 			return nil, fmt.Errorf("cannot have both route hints " +
 				"and blinded path")
 		}
 
-		edges, err = p.BlindedPathSet.ToRouteHints()
+		edges, err = p.BlindedPayment.toRouteHints()
 		if err != nil {
 			return nil, err
 		}
@@ -341,7 +342,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
 			// can split. Split payments to blinded paths won't have
 			// MPP records.
 			if p.payment.PaymentAddr == nil &&
-				p.payment.BlindedPathSet == nil {
+				p.payment.BlindedPayment == nil {
 
 				p.log.Debugf("not splitting because payment " +
 					"address is unspecified")
@@ -406,6 +407,11 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
 			return nil, err
 		}
 
+		var blindedPath *sphinx.BlindedPath
+		if p.payment.BlindedPayment != nil {
+			blindedPath = p.payment.BlindedPayment.BlindedPath
+		}
+
 		// With the next candidate path found, we'll attempt to turn
 		// this into a route by applying the time-lock and fee
 		// requirements.
@@ -418,7 +424,7 @@ func (p *paymentSession) RequestRoute(maxAmt, feeLimit lnwire.MilliSatoshi,
 				records:     p.payment.DestCustomRecords,
 				paymentAddr: p.payment.PaymentAddr,
 				metadata:    p.payment.Metadata,
-			}, p.payment.BlindedPathSet,
+			}, blindedPath,
 		)
 		if err != nil {
 			return nil, err
diff --git a/routing/router.go b/routing/router.go
index 3c9be1030..5551c5345 100644
--- a/routing/router.go
+++ b/routing/router.go
@@ -477,10 +477,10 @@ type RouteRequest struct {
 	// in blinded payment.
 	FinalExpiry uint16
 
-	// BlindedPathSet contains a set of optional blinded paths and
-	// parameters used to reach a target node blinded paths. This field is
+	// BlindedPayment contains an optional blinded path and parameters
+	// used to reach a target node via a blinded path. This field is
 	// mutually exclusive with the Target field.
-	BlindedPathSet *BlindedPaymentPathSet
+	BlindedPayment *BlindedPayment
 }
 
 // RouteHints is an alias type for a set of route hints, with the source node
@@ -494,7 +494,7 @@ type RouteHints map[route.Vertex][]AdditionalEdge
 func NewRouteRequest(source route.Vertex, target *route.Vertex,
 	amount lnwire.MilliSatoshi, timePref float64,
 	restrictions *RestrictParams, customRecords record.CustomSet,
-	routeHints RouteHints, blindedPathSet *BlindedPaymentPathSet,
+	routeHints RouteHints, blindedPayment *BlindedPayment,
 	finalExpiry uint16) (*RouteRequest, error) {
 
 	var (
@@ -504,8 +504,16 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
 		err           error
 	)
 
-	if blindedPathSet != nil {
-		if blindedPathSet.IsIntroNode(source) {
+	if blindedPayment != nil {
+		if err := blindedPayment.Validate(); err != nil {
+			return nil, fmt.Errorf("invalid blinded payment: %w",
+				err)
+		}
+
+		introVertex := route.NewVertex(
+			blindedPayment.BlindedPath.IntroductionPoint,
+		)
+		if source == introVertex {
 			return nil, ErrSelfIntro
 		}
 
@@ -519,15 +527,25 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
 			return nil, ErrExpiryAndBlinded
 		}
 
-		requestExpiry = blindedPathSet.FinalCLTVDelta()
+		// If we have a blinded path with 1 hop, the cltv expiry
+		// will not be included in any hop hints (since we're just
+		// sending to the introduction node and need no blinded hints).
+		// In this case, we include it to make sure that the final
+		// cltv delta is accounted for (since it's part of the blinded
+		// delta). In the case of a multi-hop route, we set our final
+		// cltv to zero, since it's going to be accounted for in the
+		// delta for our hints.
+		if len(blindedPayment.BlindedPath.BlindedHops) == 1 {
+			requestExpiry = blindedPayment.CltvExpiryDelta
+		}
 
-		requestHints, err = blindedPathSet.ToRouteHints()
+		requestHints, err = blindedPayment.toRouteHints()
 		if err != nil {
 			return nil, err
 		}
 	}
 
-	requestTarget, err := getTargetNode(target, blindedPathSet)
+	requestTarget, err := getTargetNode(target, blindedPayment)
 	if err != nil {
 		return nil, err
 	}
@@ -541,15 +559,15 @@ func NewRouteRequest(source route.Vertex, target *route.Vertex,
 		CustomRecords:  customRecords,
 		RouteHints:     requestHints,
 		FinalExpiry:    requestExpiry,
-		BlindedPathSet: blindedPathSet,
+		BlindedPayment: blindedPayment,
 	}, nil
 }
 
-func getTargetNode(target *route.Vertex,
-	blindedPathSet *BlindedPaymentPathSet) (route.Vertex, error) {
+func getTargetNode(target *route.Vertex, blindedPayment *BlindedPayment) (
+	route.Vertex, error) {
 
 	var (
-		blinded   = blindedPathSet != nil
+		blinded   = blindedPayment != nil
 		targetSet = target != nil
 	)
 
@@ -558,7 +576,18 @@ func getTargetNode(target *route.Vertex,
 		return route.Vertex{}, ErrTargetAndBlinded
 
 	case blinded:
-		return route.NewVertex(blindedPathSet.TargetPubKey()), nil
+		// If we're dealing with an edge-case blinded path that just
+		// has an introduction node (first hop expected to be the intro
+		// hop), then we return the unblinded introduction node as our
+		// target.
+		hops := blindedPayment.BlindedPath.BlindedHops
+		if len(hops) == 1 {
+			return route.NewVertex(
+				blindedPayment.BlindedPath.IntroductionPoint,
+			), nil
+		}
+
+		return route.NewVertex(hops[len(hops)-1].BlindedNodePub), nil
 
 	case targetSet:
 		return *target, nil
@@ -568,6 +597,16 @@ func getTargetNode(target *route.Vertex,
 	}
 }
 
+// blindedPath returns the request's blinded path, which is set if the payment
+// is to a blinded route.
+func (r *RouteRequest) blindedPath() *sphinx.BlindedPath {
+	if r.BlindedPayment == nil {
+		return nil
+	}
+
+	return r.BlindedPayment.BlindedPath
+}
+
 // FindRoute attempts to query the ChannelRouter for the optimum path to a
 // particular target destination to which it is able to send `amt` after
 // factoring in channel capacities and cumulative fees along the route.
@@ -625,7 +664,7 @@ func (r *ChannelRouter) FindRoute(req *RouteRequest) (*route.Route, float64,
 			totalAmt:  req.Amount,
 			cltvDelta: req.FinalExpiry,
 			records:   req.CustomRecords,
-		}, req.BlindedPathSet,
+		}, req.blindedPath(),
 	)
 	if err != nil {
 		return nil, 0, err
@@ -722,7 +761,7 @@ func (r *ChannelRouter) FindBlindedPaths(destination route.Vertex,
 
 			hops = append(hops, &route.Hop{
 				PubKeyBytes: path[j].vertex,
-				ChannelID:   path[j-1].channelID,
+				ChannelID:   path[j-1].edgePolicy.ChannelID,
 			})
 
 			prevNode = path[j].vertex
@@ -887,10 +926,14 @@ type LightningPayment struct {
 	// BlindedPayment field.
 	RouteHints [][]zpay32.HopHint
 
-	// BlindedPathSet holds the information about a set of blinded paths to
-	// the payment recipient. This is mutually exclusive to the RouteHints
+	// BlindedPayment holds the information about a blinded path to the
+	// payment recipient. This is mutually exclusive to the RouteHints
 	// field.
-	BlindedPathSet *BlindedPaymentPathSet
+	//
+	// NOTE: a recipient may provide multiple blinded payment paths in the
+	// same invoice. Currently, LND will only attempt to use the first one.
+	// A future PR will handle multiple blinded payment paths.
+	BlindedPayment *BlindedPayment
 
 	// OutgoingChannelIDs is the list of channels that are allowed for the
 	// first hop. If nil, any channel may be used.
diff --git a/rpcserver.go b/rpcserver.go
index 86bc6415c..9ab014b3d 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -5105,7 +5105,7 @@ type rpcPaymentIntent struct {
 	paymentAddr        *[32]byte
 	payReq             []byte
 	metadata           []byte
-	blindedPathSet     *routing.BlindedPaymentPathSet
+	blindedPayment     *routing.BlindedPayment
 
 	destCustomRecords record.CustomSet
 
@@ -5242,24 +5242,28 @@ func (r *rpcServer) extractPaymentIntent(rpcPayReq *rpcPaymentRequest) (rpcPayme
 		payIntent.metadata = payReq.Metadata
 
 		if len(payReq.BlindedPaymentPaths) > 0 {
-			pathSet, err := routerrpc.BuildBlindedPathSet(
-				payReq.BlindedPaymentPaths,
-			)
-			if err != nil {
-				return payIntent, err
+			// NOTE: Currently we only choose a single payment path.
+			// This will be updated in a future PR to handle
+			// multiple blinded payment paths.
+			path := payReq.BlindedPaymentPaths[0]
+			if len(path.Hops) == 0 {
+				return payIntent, fmt.Errorf("a blinded " +
+					"payment must have at least 1 hop")
 			}
-			payIntent.blindedPathSet = pathSet
 
-			// Replace the destination node with the target public
-			// key of the blinded path set.
+			finalHop := path.Hops[len(path.Hops)-1]
+			payIntent.blindedPayment =
+				routerrpc.MarshalBlindedPayment(path)
+
+			// Replace the target node with the blinded public key
+			// of the blinded path's final node.
 			copy(
 				payIntent.dest[:],
-				pathSet.TargetPubKey().SerializeCompressed(),
+				finalHop.BlindedNodePub.SerializeCompressed(),
 			)
 
-			pathFeatures := pathSet.Features()
-			if !pathFeatures.IsEmpty() {
-				payIntent.destFeatures = pathFeatures.Clone()
+			if !payReq.BlindedPaymentPaths[0].Features.IsEmpty() {
+				payIntent.destFeatures = path.Features.Clone()
 			}
 		}
 
@@ -5417,7 +5421,7 @@ func (r *rpcServer) dispatchPaymentIntent(
 			DestFeatures:       payIntent.destFeatures,
 			PaymentAddr:        payIntent.paymentAddr,
 			Metadata:           payIntent.metadata,
-			BlindedPathSet:     payIntent.blindedPathSet,
+			BlindedPayment:     payIntent.blindedPayment,
 
 			// Don't enable multi-part payments on the main rpc.
 			// Users need to use routerrpc for that.
diff --git a/sample-lnd.conf b/sample-lnd.conf
index c9fb0e4f8..70251ea10 100644
--- a/sample-lnd.conf
+++ b/sample-lnd.conf
@@ -1130,24 +1130,6 @@
 ; checks. This value must be >= 1m.
 ; healthcheck.remotesigner.interval=1m
 
-; The number of times we should attempt to check the node's leader status
-; before gracefully shutting down. Set this value to 0 to disable this health 
-; check.
-; healthcheck.leader.attempts=1
-
-; The amount of time after the leader check times out due to unanswered RPC.
-; This value must be >= 1s.
-; healthcheck.leader.timeout=5s
-
-; The amount of time we should backoff between failed attempts of leader checks.
-; This value must be >= 1s.
-; healthcheck.leader.backoff=5s
-
-; The amount of time we should wait between leader checks. 
-; This value must be >= 1m.
-; healthcheck.leader.interval=1m
-
-
 
 [signrpc]
 
@@ -1555,7 +1537,7 @@
 
 ; The session TTL in seconds after which a new leader is elected if the old
 ; leader is shut down, crashed or becomes unreachable.
-; cluster.leader-session-ttl=90
+; cluster.leader-session-ttl=60
 
 
 [rpcmiddleware]
diff --git a/server.go b/server.go
index ae05637bc..5d52cd399 100644
--- a/server.go
+++ b/server.go
@@ -36,7 +36,6 @@ import (
 	"github.com/lightningnetwork/lnd/channeldb/models"
 	"github.com/lightningnetwork/lnd/channelnotifier"
 	"github.com/lightningnetwork/lnd/clock"
-	"github.com/lightningnetwork/lnd/cluster"
 	"github.com/lightningnetwork/lnd/contractcourt"
 	"github.com/lightningnetwork/lnd/discovery"
 	"github.com/lightningnetwork/lnd/feature"
@@ -485,8 +484,8 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
 	nodeKeyDesc *keychain.KeyDescriptor,
 	chansToRestore walletunlocker.ChannelsToRecover,
 	chanPredicate chanacceptor.ChannelAcceptor,
-	torController *tor.Controller, tlsManager *TLSManager,
-	leaderElector cluster.LeaderElector) (*server, error) {
+	torController *tor.Controller, tlsManager *TLSManager) (*server,
+	error) {
 
 	var (
 		err         error
@@ -1675,7 +1674,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
 	}
 
 	// Create liveness monitor.
-	s.createLivenessMonitor(cfg, cc, leaderElector)
+	s.createLivenessMonitor(cfg, cc)
 
 	// Create the connection manager which will be responsible for
 	// maintaining persistent outbound connections and also accepting new
@@ -1722,9 +1721,7 @@ func (s *server) signAliasUpdate(u *lnwire.ChannelUpdate) (*ecdsa.Signature,
 //
 // If a health check has been disabled by setting attempts to 0, our monitor
 // will not run it.
-func (s *server) createLivenessMonitor(cfg *Config, cc *chainreg.ChainControl,
-	leaderElector cluster.LeaderElector) {
-
+func (s *server) createLivenessMonitor(cfg *Config, cc *chainreg.ChainControl) {
 	chainBackendAttempts := cfg.HealthChecks.ChainCheck.Attempts
 	if cfg.Bitcoin.Node == "nochainbackend" {
 		srvrLog.Info("Disabling chain backend checks for " +
@@ -1840,49 +1837,6 @@ func (s *server) createLivenessMonitor(cfg *Config, cc *chainreg.ChainControl,
 		checks = append(checks, remoteSignerConnectionCheck)
 	}
 
-	// If we have a leader elector, we add a health check to ensure we are
-	// still the leader. During normal operation, we should always be the
-	// leader, but there are circumstances where this may change, such as
-	// when we lose network connectivity for long enough expiring out lease.
-	if leaderElector != nil {
-		leaderCheck := healthcheck.NewObservation(
-			"leader status",
-			func() error {
-				// Check if we are still the leader. Note that
-				// we don't need to use a timeout context here
-				// as the healthcheck observer will handle the
-				// timeout case for us.
-				timeoutCtx, cancel := context.WithTimeout(
-					context.Background(),
-					cfg.HealthChecks.LeaderCheck.Timeout,
-				)
-				defer cancel()
-
-				leader, err := leaderElector.IsLeader(
-					timeoutCtx,
-				)
-				if err != nil {
-					return fmt.Errorf("unable to check if "+
-						"still leader: %v", err)
-				}
-
-				if !leader {
-					srvrLog.Debug("Not the current leader")
-					return fmt.Errorf("not the current " +
-						"leader")
-				}
-
-				return nil
-			},
-			cfg.HealthChecks.LeaderCheck.Interval,
-			cfg.HealthChecks.LeaderCheck.Timeout,
-			cfg.HealthChecks.LeaderCheck.Backoff,
-			cfg.HealthChecks.LeaderCheck.Attempts,
-		)
-
-		checks = append(checks, leaderCheck)
-	}
-
 	// If we have not disabled all of our health checks, we create a
 	// liveness monitor with our configured checks.
 	s.livenessMonitor = healthcheck.NewMonitor(
diff --git a/zpay32/blinded_path.go b/zpay32/blinded_path.go
index 0c4da57ee..1f3adb8ce 100644
--- a/zpay32/blinded_path.go
+++ b/zpay32/blinded_path.go
@@ -4,6 +4,7 @@ import (
 	"encoding/binary"
 	"fmt"
 	"io"
+	"math"
 
 	"github.com/btcsuite/btcd/btcec/v2"
 	sphinx "github.com/lightningnetwork/lightning-onion"
@@ -20,12 +21,6 @@ const (
 	// proposal](https://github.com/lightning/blips/pull/39) for a detailed
 	// calculation.
 	maxNumHopsPerPath = 7
-
-	// maxCipherTextLength defines the largest cipher text size allowed.
-	// This is derived by using the `data_length` upper bound of 639 bytes
-	// and then assuming the case of a path with only a single hop (meaning
-	// the cipher text may be as large as possible).
-	maxCipherTextLength = 535
 )
 
 var (
@@ -220,12 +215,6 @@ func DecodeBlindedHop(r io.Reader) (*sphinx.BlindedHopInfo, error) {
 		return nil, err
 	}
 
-	if dataLen > maxCipherTextLength {
-		return nil, fmt.Errorf("a blinded hop cipher text blob may "+
-			"not exceed the maximum of %d bytes",
-			maxCipherTextLength)
-	}
-
 	encryptedData := make([]byte, dataLen)
 	_, err = r.Read(encryptedData)
 	if err != nil {
@@ -249,9 +238,9 @@ func EncodeBlindedHop(w io.Writer, hop *sphinx.BlindedHopInfo) error {
 		return err
 	}
 
-	if len(hop.CipherText) > maxCipherTextLength {
+	if len(hop.CipherText) > math.MaxUint16 {
 		return fmt.Errorf("encrypted recipient data can not exceed a "+
-			"length of %d bytes", maxCipherTextLength)
+			"length of %d bytes", math.MaxUint16)
 	}
 
 	err = tlv.WriteVarInt(w, uint64(len(hop.CipherText)), &[8]byte{})


Test patch:
diff --git a/cluster/etcd_elector_test.go b/cluster/etcd_elector_test.go
index f7fcebec3..999a3d791 100644
--- a/cluster/etcd_elector_test.go
+++ b/cluster/etcd_elector_test.go
@@ -87,12 +87,12 @@ func TestEtcdElector(t *testing.T) {
 	tmp := <-ch
 	first, err := tmp.Leader(ctxb)
 	require.NoError(t, err)
-	require.NoError(t, tmp.Resign(ctxb))
+	require.NoError(t, tmp.Resign())
 
 	tmp = <-ch
 	second, err := tmp.Leader(ctxb)
 	require.NoError(t, err)
-	require.NoError(t, tmp.Resign(ctxb))
+	require.NoError(t, tmp.Resign())
 
 	require.Contains(t, []string{id1, id2}, first)
 	require.Contains(t, []string{id1, id2}, second)
diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go
index f508ede06..36f273112 100644
--- a/htlcswitch/link_test.go
+++ b/htlcswitch/link_test.go
@@ -444,7 +444,7 @@ func TestChannelLinkSingleHopPayment(t *testing.T) {
 	t.Parallel()
 
 	// Setup a alice-bob network.
-	alice, bob, err := createTwoClusterChannels(
+	alice, bob, err := createMirroredChannel(
 		t, btcutil.SatoshiPerBitcoin*3, btcutil.SatoshiPerBitcoin*5,
 	)
 	require.NoError(t, err, "unable to create channel")
@@ -6318,7 +6318,7 @@ func TestChannelLinkCanceledInvoice(t *testing.T) {
 	t.Parallel()
 
 	// Setup a alice-bob network.
-	alice, bob, err := createTwoClusterChannels(
+	alice, bob, err := createMirroredChannel(
 		t, btcutil.SatoshiPerBitcoin*3, btcutil.SatoshiPerBitcoin*5,
 	)
 	require.NoError(t, err, "unable to create channel")
@@ -6374,7 +6374,7 @@ type hodlInvoiceTestCtx struct {
 
 func newHodlInvoiceTestCtx(t *testing.T) (*hodlInvoiceTestCtx, error) {
 	// Setup a alice-bob network.
-	alice, bob, err := createTwoClusterChannels(
+	alice, bob, err := createMirroredChannel(
 		t, btcutil.SatoshiPerBitcoin*3, btcutil.SatoshiPerBitcoin*5,
 	)
 	require.NoError(t, err, "unable to create channel")
diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go
index 9a72197ec..0b3b0fcdf 100644
--- a/htlcswitch/test_utils.go
+++ b/htlcswitch/test_utils.go
@@ -1053,17 +1053,17 @@ func serverOptionRejectHtlc(alice, bob, carol bool) serverOption {
 	}
 }
 
-// createTwoClusterChannels creates lightning channels which are needed for
-// a 2 hop network cluster to be initialized.
-func createTwoClusterChannels(t *testing.T, aliceToBob,
-	bobToCarol btcutil.Amount) (*testLightningChannel,
+// createMirroredChannel creates two LightningChannel objects which represent
+// the state machines on either side of a single channel between alice and bob.
+func createMirroredChannel(t *testing.T, aliceToBob,
+	bobToAlice btcutil.Amount) (*testLightningChannel,
 	*testLightningChannel, error) {
 
 	_, _, firstChanID, _ := genIDs()
 
-	// Create lightning channels between Alice<->Bob and Bob<->Carol
+	// Create lightning channels between Alice<->Bob for Alice and Bob
 	alice, bob, err := createTestChannel(t, alicePrivKey, bobPrivKey,
-		aliceToBob, aliceToBob, 0, 0, firstChanID,
+		aliceToBob, bobToAlice, 0, 0, firstChanID,
 	)
 	if err != nil {
 		return nil, nil, errors.Errorf("unable to create "+
diff --git a/itest/list_on_test.go b/itest/list_on_test.go
index f02036e7c..c5d63bd2d 100644
--- a/itest/list_on_test.go
+++ b/itest/list_on_test.go
@@ -233,10 +233,6 @@ var allTestCases = []*lntest.TestCase{
 		Name:     "etcd failover",
 		TestFunc: testEtcdFailover,
 	},
-	{
-		Name:     "leader health check",
-		TestFunc: testLeaderHealthCheck,
-	},
 	{
 		Name:     "hold invoice force close",
 		TestFunc: testHoldInvoiceForceClose,
@@ -598,10 +594,6 @@ var allTestCases = []*lntest.TestCase{
 		Name:     "mpp to single blinded path",
 		TestFunc: testMPPToSingleBlindedPath,
 	},
-	{
-		Name:     "mpp to multiple blinded paths",
-		TestFunc: testMPPToMultipleBlindedPaths,
-	},
 	{
 		Name:     "route blinding dummy hops",
 		TestFunc: testBlindedRouteDummyHops,
diff --git a/itest/lnd_etcd_failover_test.go b/itest/lnd_etcd_failover_test.go
index 53b1dd6b3..c2a345889 100644
--- a/itest/lnd_etcd_failover_test.go
+++ b/itest/lnd_etcd_failover_test.go
@@ -4,11 +4,6 @@
 package itest
 
 import (
-	"context"
-	"fmt"
-	"io"
-	"net"
-	"sync"
 	"testing"
 	"time"
 
@@ -142,200 +137,3 @@ func testEtcdFailoverCase(ht *lntest.HarnessTest, kill bool) {
 	// process.
 	ht.Shutdown(carol2)
 }
-
-// Proxy is a simple TCP proxy that forwards all traffic between a local and a
-// remote address. We use it to simulate a network partition in the leader
-// health check test.
-type Proxy struct {
-	listenAddr string
-	targetAddr string
-	cancel     context.CancelFunc
-	wg         sync.WaitGroup
-	stopped    chan struct{}
-}
-
-// NewProxy creates a new Proxy instance with a provided context.
-func NewProxy(listenAddr, targetAddr string) *Proxy {
-	return &Proxy{
-		listenAddr: listenAddr,
-		targetAddr: targetAddr,
-		stopped:    make(chan struct{}),
-	}
-}
-
-// Start starts the proxy. It listens on the listen address and forwards all
-// traffic to the target address.
-func (p *Proxy) Start(ctx context.Context, t *testing.T) {
-	listener, err := net.Listen("tcp", p.listenAddr)
-	require.NoError(t, err, "Failed to listen on %s", p.listenAddr)
-	t.Logf("Proxy is listening on %s", p.listenAddr)
-
-	proxyCtx, cancel := context.WithCancel(ctx)
-	p.cancel = cancel
-
-	p.wg.Add(1)
-	go func() {
-		defer func() {
-			close(p.stopped)
-			p.wg.Done()
-		}()
-
-		for {
-			select {
-			case <-proxyCtx.Done():
-				listener.Close()
-				return
-			default:
-			}
-
-			conn, err := listener.Accept()
-			if err != nil {
-				if proxyCtx.Err() != nil {
-					// Context is done, exit the loop
-					return
-				}
-				t.Logf("Proxy failed to accept connection: %v",
-					err)
-
-				continue
-			}
-
-			p.wg.Add(1)
-			go p.handleConnection(proxyCtx, t, conn)
-		}
-	}()
-}
-
-// handleConnection handles an accepted connection and forwards all traffic
-// between the listener and target.
-func (p *Proxy) handleConnection(ctx context.Context, t *testing.T,
-	conn net.Conn) {
-
-	targetConn, err := net.Dial("tcp", p.targetAddr)
-	require.NoError(t, err, "Failed to connect to target %s", p.targetAddr)
-
-	defer func() {
-		conn.Close()
-		targetConn.Close()
-		p.wg.Done()
-	}()
-
-	done := make(chan struct{})
-
-	p.wg.Add(2)
-	go func() {
-		defer p.wg.Done()
-		// Ignore the copy error due to the connection being closed.
-		_, _ = io.Copy(targetConn, conn)
-	}()
-
-	go func() {
-		defer p.wg.Done()
-		// Ignore the copy error due to the connection being closed.
-		_, _ = io.Copy(conn, targetConn)
-		close(done)
-	}()
-
-	select {
-	case <-ctx.Done():
-	case <-done:
-	}
-}
-
-// Stop stops the proxy and waits for all connections to be closed and all
-// goroutines to be stopped.
-func (p *Proxy) Stop(t *testing.T) {
-	require.NotNil(t, p.cancel, "Proxy is not started")
-
-	p.cancel()
-	p.wg.Wait()
-	<-p.stopped
-
-	t.Log("Proxy stopped", time.Now())
-}
-
-// testLeaderHealthCheck tests that a node is properly shut down when the leader
-// health check fails.
-func testLeaderHealthCheck(ht *lntest.HarnessTest) {
-	clientPort := port.NextAvailablePort()
-
-	// Let's start a test etcd instance that we'll redirect through a proxy.
-	etcdCfg, cleanup, err := kvdb.StartEtcdTestBackend(
-		ht.T.TempDir(), uint16(clientPort),
-		uint16(port.NextAvailablePort()), "",
-	)
-	require.NoError(ht, err, "Failed to start etcd instance")
-
-	// Make leader election session TTL 5 sec to make the test run fast.
-	const leaderSessionTTL = 5
-
-	// Create an election observer that we will use to monitor the leader
-	// election.
-	observer, err := cluster.MakeLeaderElector(
-		ht.Context(), cluster.EtcdLeaderElector, "observer",
-		lncfg.DefaultEtcdElectionPrefix, leaderSessionTTL, etcdCfg,
-	)
-	require.NoError(ht, err, "Cannot start election observer")
-
-	// Start a proxy that will forward all traffic to the etcd instance.
-	clientAddr := fmt.Sprintf("localhost:%d", clientPort)
-	proxyAddr := fmt.Sprintf("localhost:%d", port.NextAvailablePort())
-
-	ctx, cancel := context.WithCancel(ht.Context())
-	defer cancel()
-
-	proxy := NewProxy(proxyAddr, clientAddr)
-	proxy.Start(ctx, ht.T)
-
-	// Copy the etcd config so that we can modify the host to point to the
-	// proxy.
-	proxyEtcdCfg := *etcdCfg
-	// With the proxy in place, we can now configure the etcd client to
-	// connect to the proxy instead of the etcd instance.
-	proxyEtcdCfg.Host = "http://" + proxyAddr
-
-	defer cleanup()
-
-	// Start Carol-1 with cluster support and connect to etcd through the
-	// proxy.
-	password := []byte("the quick brown fox jumps the lazy dog")
-	stateless := false
-	cluster := true
-
-	carol, _, _ := ht.NewNodeWithSeedEtcd(
-		"Carol-1", &proxyEtcdCfg, password, stateless, cluster,
-		leaderSessionTTL,
-	)
-
-	// Make sure Carol-1 is indeed the leader.
-	assertLeader(ht, observer, "Carol-1")
-
-	// At this point Carol-1 is the elected leader, while Carol-2 will wait
-	// to become the leader when Carol-1 releases the lease. Note that for
-	// Carol-2 we don't use the proxy as we want to simulate a network
-	// partition only for Carol-1.
-	carol2 := ht.NewNodeEtcd(
-		"Carol-2", etcdCfg, password, cluster, leaderSessionTTL,
-	)
-
-	// Stop the proxy so that we simulate a network partition which
-	// consequently will make the leader health check fail and force Carol
-	// to shut down.
-	proxy.Stop(ht.T)
-
-	// Wait for Carol-1 to stop. If the health check wouldn't properly work
-	// this call would timeout and trigger a test failure.
-	require.NoError(ht.T, carol.WaitForProcessExit())
-
-	// Now that Carol-1 is shut down we should fail over to Carol-2.
-	failoverTimeout := time.Duration(2*leaderSessionTTL) * time.Second
-
-	// Make sure that Carol-2 becomes the leader (reported by Carol-2).
-	err = carol2.WaitUntilLeader(failoverTimeout)
-
-	require.NoError(ht, err, "Waiting for Carol-2 to become the leader "+
-		"failed")
-
-	// Make sure Carol-2 is indeed the leader (repoted by the observer).
-	assertLeader(ht, observer, "Carol-2")
-}
diff --git a/itest/lnd_no_etcd_dummy_failover_test.go b/itest/lnd_no_etcd_dummy_failover_test.go
index 468eca3f6..2d731febc 100644
--- a/itest/lnd_no_etcd_dummy_failover_test.go
+++ b/itest/lnd_no_etcd_dummy_failover_test.go
@@ -8,7 +8,3 @@ import "github.com/lightningnetwork/lnd/lntest"
 // testEtcdFailover is an empty itest when LND is not compiled with etcd
 // support.
 func testEtcdFailover(ht *lntest.HarnessTest) {}
-
-// testLeaderHealthCheck is an empty itest when LND is not compiled with etcd
-// support.
-func testLeaderHealthCheck(ht *lntest.HarnessTest) {}
diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go
index 48f31900f..3b10f3b59 100644
--- a/itest/lnd_route_blinding_test.go
+++ b/itest/lnd_route_blinding_test.go
@@ -1229,166 +1229,3 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
 		ht.AssertNumWaitingClose(hn, 0)
 	}
 }
-
-// testMPPToMultipleBlindedPaths tests that a two-shard MPP payment can be sent
-// over a multiple blinded paths. The following network is created where Dave
-// is the recipient and Alice the sender. Dave will create an invoice containing
-// two blinded paths: one with Bob at the intro node and one with Carol as the
-// intro node. Channel liquidity will be set up in such a way that Alice will be
-// forced to send one shared via the Bob->Dave route and one over the
-// Carol->Dave route.
-//
-//		   --- Bob ---
-//	          /            \
-//	      Alice           Dave
-//		  \	       /
-//		   --- Carol ---
-func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) {
-	alice, bob := ht.Alice, ht.Bob
-
-	// Create a four-node context consisting of Alice, Bob and three new
-	// nodes.
-	dave := ht.NewNode("dave", []string{
-		"--routing.blinding.min-num-real-hops=1",
-		"--routing.blinding.num-hops=1",
-	})
-	carol := ht.NewNode("carol", nil)
-
-	// Connect nodes to ensure propagation of channels.
-	ht.EnsureConnected(alice, carol)
-	ht.EnsureConnected(alice, bob)
-	ht.EnsureConnected(carol, dave)
-	ht.EnsureConnected(bob, dave)
-
-	// Fund the new nodes.
-	ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, carol)
-	ht.FundCoinsUnconfirmed(btcutil.SatoshiPerBitcoin, dave)
-	ht.MineBlocksAndAssertNumTxes(1, 2)
-
-	const paymentAmt = btcutil.Amount(300000)
-
-	nodes := []*node.HarnessNode{alice, bob, carol, dave}
-
-	reqs := []*lntest.OpenChannelRequest{
-		{
-			Local:  alice,
-			Remote: bob,
-			Param: lntest.OpenChannelParams{
-				Amt: paymentAmt * 2 / 3,
-			},
-		},
-		{
-			Local:  alice,
-			Remote: carol,
-			Param: lntest.OpenChannelParams{
-				Amt: paymentAmt * 2 / 3,
-			},
-		},
-		{
-			Local:  bob,
-			Remote: dave,
-			Param:  lntest.OpenChannelParams{Amt: paymentAmt * 2},
-		},
-		{
-			Local:  carol,
-			Remote: dave,
-			Param:  lntest.OpenChannelParams{Amt: paymentAmt * 2},
-		},
-	}
-
-	channelPoints := ht.OpenMultiChannelsAsync(reqs)
-
-	// Make sure every node has heard every channel.
-	for _, hn := range nodes {
-		for _, cp := range channelPoints {
-			ht.AssertTopologyChannelOpen(hn, cp)
-		}
-
-		// Each node should have exactly 5 edges.
-		ht.AssertNumEdges(hn, len(channelPoints), false)
-	}
-
-	// Ok now make a payment that must be split to succeed.
-
-	// Make Dave create an invoice for Alice to pay
-	invoice := &lnrpc.Invoice{
-		Memo:  "test",
-		Value: int64(paymentAmt),
-		Blind: true,
-	}
-	invoiceResp := dave.RPC.AddInvoice(invoice)
-
-	// Assert that two blinded paths are included in the invoice.
-	payReq := dave.RPC.DecodePayReq(invoiceResp.PaymentRequest)
-	require.Len(ht, payReq.BlindedPaths, 2)
-
-	sendReq := &routerrpc.SendPaymentRequest{
-		PaymentRequest: invoiceResp.PaymentRequest,
-		MaxParts:       10,
-		TimeoutSeconds: 60,
-		FeeLimitMsat:   noFeeLimitMsat,
-	}
-	payment := ht.SendPaymentAssertSettled(alice, sendReq)
-
-	preimageBytes, err := hex.DecodeString(payment.PaymentPreimage)
-	require.NoError(ht, err)
-
-	preimage, err := lntypes.MakePreimage(preimageBytes)
-	require.NoError(ht, err)
-
-	hash, err := lntypes.MakeHash(invoiceResp.RHash)
-	require.NoError(ht, err)
-
-	// Make sure we got the preimage.
-	require.True(ht, preimage.Matches(hash), "preimage doesn't match")
-
-	// Check that Alice split the payment in at least two shards. Because
-	// the hand-off of the htlc to the link is asynchronous (via a mailbox),
-	// there is some non-determinism in the process. Depending on whether
-	// the new pathfinding round is started before or after the htlc is
-	// locked into the channel, different sharding may occur. Therefore we
-	// can only check if the number of shards isn't below the theoretical
-	// minimum.
-	succeeded := 0
-	for _, htlc := range payment.Htlcs {
-		if htlc.Status == lnrpc.HTLCAttempt_SUCCEEDED {
-			succeeded++
-		}
-	}
-
-	const minExpectedShards = 2
-	require.GreaterOrEqual(ht, succeeded, minExpectedShards,
-		"expected shards not reached")
-
-	// Make sure Dave show the invoice as settled for the full amount.
-	inv := dave.RPC.LookupInvoice(invoiceResp.RHash)
-
-	require.EqualValues(ht, paymentAmt, inv.AmtPaidSat,
-		"incorrect payment amt")
-
-	require.Equal(ht, lnrpc.Invoice_SETTLED, inv.State,
-		"Invoice not settled")
-
-	settled := 0
-	for _, htlc := range inv.Htlcs {
-		if htlc.State == lnrpc.InvoiceHTLCState_SETTLED {
-			settled++
-		}
-	}
-	require.Equal(ht, succeeded, settled, "num of HTLCs wrong")
-
-	// Close all channels without mining the closing transactions.
-	ht.CloseChannelAssertPending(alice, channelPoints[0], false)
-	ht.CloseChannelAssertPending(alice, channelPoints[1], false)
-	ht.CloseChannelAssertPending(bob, channelPoints[2], false)
-	ht.CloseChannelAssertPending(carol, channelPoints[3], false)
-
-	// Now mine a block to include all the closing transactions. (first
-	// iteration: no blinded paths)
-	ht.MineBlocksAndAssertNumTxes(1, 4)
-
-	// Assert that the channels are closed.
-	for _, hn := range nodes {
-		ht.AssertNumWaitingClose(hn, 0)
-	}
-}
diff --git a/lntest/node/config.go b/lntest/node/config.go
index f0e3f7059..de1ea92ec 100644
--- a/lntest/node/config.go
+++ b/lntest/node/config.go
@@ -315,9 +315,6 @@ func ExtraArgsEtcd(etcdCfg *etcd.Config, name string, cluster bool,
 				leaderSessionTTL),
 		}
 		extraArgs = append(extraArgs, clusterArgs...)
-		extraArgs = append(
-			extraArgs, "--healthcheck.leader.interval=10s",
-		)
 	}
 
 	return extraArgs
diff --git a/routing/blinding_test.go b/routing/blinding_test.go
index 950cb0210..58ad56594 100644
--- a/routing/blinding_test.go
+++ b/routing/blinding_test.go
@@ -7,7 +7,6 @@ import (
 	"github.com/btcsuite/btcd/btcec/v2"
 	sphinx "github.com/lightningnetwork/lightning-onion"
 	"github.com/lightningnetwork/lnd/channeldb/models"
-	"github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/routing/route"
 	"github.com/stretchr/testify/require"
@@ -129,7 +128,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
 		HtlcMaximum:         htlcMax,
 		Features:            features,
 	}
-	hints, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
+	hints, err := blindedPayment.toRouteHints()
 	require.NoError(t, err)
 	require.Nil(t, hints)
 
@@ -184,7 +183,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
 		},
 	}
 
-	actual, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
+	actual, err := blindedPayment.toRouteHints()
 	require.NoError(t, err)
 
 	require.Equal(t, len(expected), len(actual))
diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go
index 8fc50bb4f..f67a8fa59 100644
--- a/routing/pathfind_test.go
+++ b/routing/pathfind_test.go
@@ -23,7 +23,6 @@ import (
 	sphinx "github.com/lightningnetwork/lightning-onion"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/channeldb/models"
-	"github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/htlcswitch"
 	switchhop "github.com/lightningnetwork/lnd/htlcswitch/hop"
 	"github.com/lightningnetwork/lnd/kvdb"
@@ -3278,39 +3277,21 @@ func TestBlindedRouteConstruction(t *testing.T) {
 
 	require.NoError(t, blindedPayment.Validate())
 
-	blindedPathSet, err := NewBlindedPaymentPathSet(
-		[]*BlindedPayment{blindedPayment},
-	)
-	require.NoError(t, err)
-
 	// Generate route hints from our blinded payment and a set of edges
 	// that make up the graph we'll give to route construction. The hints
 	// map is keyed by source node, so we can retrieve our blinded edges
 	// accordingly.
-	blindedEdges, err := blindedPayment.toRouteHints(
-		fn.None[*btcec.PublicKey](),
-	)
+	blindedEdges, err := blindedPayment.toRouteHints()
 	require.NoError(t, err)
 
 	carolDaveEdge := blindedEdges[carolVertex][0]
 	daveEveEdge := blindedEdges[daveBlindedVertex][0]
 
 	edges := []*unifiedEdge{
-		{
-			policy: aliceBobEdge,
-		},
-		{
-			policy:         bobCarolEdge,
-			blindedPayment: blindedPayment,
-		},
-		{
-			policy:         carolDaveEdge.EdgePolicy(),
-			blindedPayment: blindedPayment,
-		},
-		{
-			policy:         daveEveEdge.EdgePolicy(),
-			blindedPayment: blindedPayment,
-		},
+		{policy: aliceBobEdge},
+		{policy: bobCarolEdge},
+		{policy: carolDaveEdge.EdgePolicy()},
+		{policy: daveEveEdge.EdgePolicy()},
 	}
 
 	// Total timelock for the route should include:
@@ -3401,7 +3382,7 @@ func TestBlindedRouteConstruction(t *testing.T) {
 
 	route, err := newRoute(
 		sourceVertex, edges, currentHeight, finalHopParams,
-		blindedPathSet,
+		blindedPath,
 	)
 	require.NoError(t, err)
 	require.Equal(t, expectedRoute, route)
@@ -3428,38 +3409,31 @@ func TestLastHopPayloadSize(t *testing.T) {
 		amtToForward          = lnwire.MilliSatoshi(10000)
 		finalHopExpiry  int32 = 144
 
-		oneHopPath = &sphinx.BlindedPath{
-			BlindedHops: []*sphinx.BlindedHopInfo{
-				{
-					CipherText: encrypedData,
+		oneHopBlindedPayment = &BlindedPayment{
+			BlindedPath: &sphinx.BlindedPath{
+				BlindedHops: []*sphinx.BlindedHopInfo{
+					{
+						CipherText: encrypedData,
+					},
 				},
+				BlindingPoint: blindedPoint,
 			},
-			BlindingPoint: blindedPoint,
 		}
-
-		twoHopPath = &sphinx.BlindedPath{
-			BlindedHops: []*sphinx.BlindedHopInfo{
-				{
-					CipherText: encrypedData,
-				},
-				{
-					CipherText: encrypedData,
+		twoHopBlindedPayment = &BlindedPayment{
+			BlindedPath: &sphinx.BlindedPath{
+				BlindedHops: []*sphinx.BlindedHopInfo{
+					{
+						CipherText: encrypedData,
+					},
+					{
+						CipherText: encrypedData,
+					},
 				},
+				BlindingPoint: blindedPoint,
 			},
-			BlindingPoint: blindedPoint,
 		}
 	)
 
-	oneHopBlindedPayment, err := NewBlindedPaymentPathSet(
-		[]*BlindedPayment{{BlindedPath: oneHopPath}},
-	)
-	require.NoError(t, err)
-
-	twoHopBlindedPayment, err := NewBlindedPaymentPathSet(
-		[]*BlindedPayment{{BlindedPath: twoHopPath}},
-	)
-	require.NoError(t, err)
-
 	testCases := []struct {
 		name           string
 		restrictions   *RestrictParams
@@ -3480,7 +3454,7 @@ func TestLastHopPayloadSize(t *testing.T) {
 		{
 			name: "Blinded final hop introduction point",
 			restrictions: &RestrictParams{
-				BlindedPaymentPathSet: oneHopBlindedPayment,
+				BlindedPayment: oneHopBlindedPayment,
 			},
 			amount:         amtToForward,
 			finalHopExpiry: finalHopExpiry,
@@ -3488,7 +3462,7 @@ func TestLastHopPayloadSize(t *testing.T) {
 		{
 			name: "Blinded final hop of a two hop payment",
 			restrictions: &RestrictParams{
-				BlindedPaymentPathSet: twoHopBlindedPayment,
+				BlindedPayment: twoHopBlindedPayment,
 			},
 			amount:         amtToForward,
 			finalHopExpiry: finalHopExpiry,
@@ -3516,11 +3490,12 @@ func TestLastHopPayloadSize(t *testing.T) {
 			}
 
 			var finalHop route.Hop
-			if tc.restrictions.BlindedPaymentPathSet != nil {
-				path := tc.restrictions.BlindedPaymentPathSet.
-					LargestLastHopPayloadPath()
-				blindedPath := path.BlindedPath.BlindedHops
-				blindedPoint := path.BlindedPath.BlindingPoint
+			if tc.restrictions.BlindedPayment != nil {
+				blindedPath := tc.restrictions.BlindedPayment.
+					BlindedPath.BlindedHops
+
+				blindedPoint := tc.restrictions.BlindedPayment.
+					BlindedPath.BlindingPoint
 
 				//nolint:lll
 				finalHop = route.Hop{
diff --git a/routing/router_test.go b/routing/router_test.go
index 28cced4e6..f631669a9 100644
--- a/routing/router_test.go
+++ b/routing/router_test.go
@@ -2223,6 +2223,11 @@ func TestNewRouteRequest(t *testing.T) {
 			finalExpiry:    unblindedCltv,
 			err:            ErrExpiryAndBlinded,
 		},
+		{
+			name:           "invalid blinded payment",
+			blindedPayment: &BlindedPayment{},
+			err:            ErrNoBlindedPath,
+		},
 	}
 
 	for _, testCase := range testCases {
@@ -2231,26 +2236,9 @@ func TestNewRouteRequest(t *testing.T) {
 		t.Run(testCase.name, func(t *testing.T) {
 			t.Parallel()
 
-			var (
-				blindedPathInfo *BlindedPaymentPathSet
-				expectedTarget  = testCase.expectedTarget
-			)
-			if testCase.blindedPayment != nil {
-				blindedPathInfo, err = NewBlindedPaymentPathSet(
-					[]*BlindedPayment{
-						testCase.blindedPayment,
-					},
-				)
-				require.NoError(t, err)
-
-				expectedTarget = route.NewVertex(
-					blindedPathInfo.TargetPubKey(),
-				)
-			}
-
 			req, err := NewRouteRequest(
 				source, testCase.target, 1000, 0, nil, nil,
-				testCase.routeHints, blindedPathInfo,
+				testCase.routeHints, testCase.blindedPayment,
 				testCase.finalExpiry,
 			)
 			require.ErrorIs(t, err, testCase.err)
@@ -2260,7 +2248,7 @@ func TestNewRouteRequest(t *testing.T) {
 				return
 			}
 
-			require.Equal(t, req.Target, expectedTarget)
+			require.Equal(t, req.Target, testCase.expectedTarget)
 			require.Equal(
 				t, req.FinalExpiry, testCase.expectedCltv,
 			)