Repository: lightningnetwork/lnd
Pull request #8953 (2024-07-29 23:21:49)

[MICRO]: multi: break ChannelConstraints into two sub-structures
This commit breaks the ChannelConstraints structure into two sub-structures that reflect the fundamental differences in how these parameters are used. On its face it may not seem necessary, however the distinction introduced here is relevant for how we will be implementing the Dynamic Commitments proposal.

## Change Description
Description of change / link to associated issue.

## Steps to Test
Steps for reviewers to follow to test the change.

## Pull Request Checklist
### Testing
- [ ] Your PR passes all CI checks.
- [ ] Tests covering the positive and negative (error paths) are included.
- [ ] Bug fixes contain tests triggering the bug to prevent regressions.

### Code Style and Documentation
- [ ] The change obeys the [Code Documentation and Commenting](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#CodeDocumentation) guidelines, and lines wrap at 80.
- [ ] Commits follow the [Ideal Git Commit Structure](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#IdealGitCommitStructure).
- [ ] Any new logging statements use an appropriate subsystem and logging level.
- [ ] Any new lncli commands have appropriate tags in the comments for the rpc in the proto file.
- [ ] [There is a change description in the release notes](https://github.com/lightningnetwork/lnd/tree/master/docs/release-notes), or `[skip ci]` in the commit message for small changes.

📝 Please see our [Contribution Guidelines](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md) for further guidance.


coderabbitai[bot]: <!-- This is an auto-generated comment: summarize by coderabbit.ai -->
<!-- This is an auto-generated comment: skip review by coderabbit.ai -->

> [!IMPORTANT]
> ## Review skipped
> 
> Auto reviews are limited to specific labels.
> 
> <details>
> <summary>Labels to auto review (1)</summary>
> 
> * llm-review
> 
> </details>
> 
> Please check the settings in the CodeRabbit UI or the `.coderabbit.yaml` file in this repository. To trigger a single review, invoke the `@coderabbitai review` command.
> 
> You can disable this status message by setting the `reviews.review_status` to `false` in the CodeRabbit configuration file.

<!-- end of auto-generated comment: skip review by coderabbit.ai --><!-- tips_start -->

---



Thank you for using CodeRabbit. We offer it for free to the OSS community and would appreciate your support in helping us grow. If you find it useful, would you consider giving us a shout-out on your favorite social media?

<details>
<summary>Share</summary>

- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)
- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)
- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)
- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)

</details>

<details>
<summary>Tips</summary>

### Chat

There are 3 ways to chat with [CodeRabbit](https://coderabbit.ai):

- Review comments: Directly reply to a review comment made by CodeRabbit. Example:
	- `I pushed a fix in commit <commit_id>.`
	- `Generate unit testing code for this file.`
	- `Open a follow-up GitHub issue for this discussion.`
- Files and specific lines of code (under the "Files changed" tab): Tag `@coderabbitai` in a new review comment at the desired location with your query. Examples:
	- `@coderabbitai generate unit testing code for this file.`
	-	`@coderabbitai modularize this function.`
- PR comments: Tag `@coderabbitai` in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
	- `@coderabbitai generate interesting stats about this repository and render them as a table.`
	- `@coderabbitai show all the console.log statements in this repository.`
	- `@coderabbitai read src/utils.ts and generate unit testing code.`
	- `@coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.`
	- `@coderabbitai help me debug CodeRabbit configuration file.`

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

### CodeRabbit Commands (invoked as PR comments)

- `@coderabbitai pause` to pause the reviews on a PR.
- `@coderabbitai resume` to resume the paused reviews.
- `@coderabbitai review` to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
- `@coderabbitai full review` to do a full review from scratch and review all the files again.
- `@coderabbitai summary` to regenerate the summary of the PR.
- `@coderabbitai resolve` resolve all the CodeRabbit review comments.
- `@coderabbitai configuration` to show the current CodeRabbit configuration for the repository.
- `@coderabbitai help` to get help.


Additionally, you can add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.

### CodeRabbit Configuration File (`.coderabbit.yaml`)

- You can programmatically configure CodeRabbit by adding a `.coderabbit.yaml` file to the root of your repository.
- Please see the [configuration documentation](https://docs.coderabbit.ai/guides/configure-coderabbit) for more information.
- If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: `# yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json`

### Documentation and Community

- Visit our [Documentation](https://coderabbit.ai/docs) for detailed information on how to use CodeRabbit.
- Join our [Discord Community](https://discord.com/invite/GsXnASn26c) to get help, request features, and share feedback.
- Follow us on [X/Twitter](https://twitter.com/coderabbitai) for updates and announcements.

</details>

<!-- tips_end -->
yyforyongyu: linter failed

Patch:
diff --git a/channeldb/channel.go b/channeldb/channel.go
index ad0208467..7569fc30e 100644
--- a/channeldb/channel.go
+++ b/channeldb/channel.go
@@ -395,19 +395,11 @@ func (c ChannelType) IsTaproot() bool {
 	return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit
 }
 
-// ChannelConstraints represents a set of constraints meant to allow a node to
-// limit their exposure, enact flow control and ensure that all HTLCs are
-// economically relevant. This struct will be mirrored for both sides of the
-// channel, as each side will enforce various constraints that MUST be adhered
-// to for the life time of the channel. The parameters for each of these
-// constraints are static for the duration of the channel, meaning the channel
-// must be torn down for them to change.
-type ChannelConstraints struct {
-	// DustLimit is the threshold (in satoshis) below which any outputs
-	// should be trimmed. When an output is trimmed, it isn't materialized
-	// as an actual output, but is instead burned to miner's fees.
-	DustLimit btcutil.Amount
-
+// ChannelStateBounds are the parameters from OpenChannel and AcceptChannel
+// that are responsible for providing bounds on the state space of the abstract
+// channel state. These values must be remembered for normal channel operation
+// but they do not impact how we compute the commitment transactions themselves.
+type ChannelStateBounds struct {
 	// ChanReserve is an absolute reservation on the channel for the
 	// owner of this set of constraints. This means that the current
 	// settled balance for this node CANNOT dip below the reservation
@@ -433,6 +425,19 @@ type ChannelConstraints struct {
 	// acted upon in the case of a unilateral channel closure or a contract
 	// breach.
 	MaxAcceptedHtlcs uint16
+}
+
+// CommitmentParams are the parameters from OpenChannel and
+// AcceptChannel that are required to render an abstract channel state to a
+// concrete commitment transaction. These values are necessary to (re)compute
+// the commitment transaction. We treat these differently than the state space
+// bounds because their history needs to be stored in order to properly handle
+// chain resolution.
+type CommitmentParams struct {
+	// DustLimit is the threshold (in satoshis) below which any outputs
+	// should be trimmed. When an output is trimmed, it isn't materialized
+	// as an actual output, but is instead burned to miner's fees.
+	DustLimit btcutil.Amount
 
 	// CsvDelay is the relative time lock delay expressed in blocks. Any
 	// settled outputs that pay to the owner of this channel configuration
@@ -448,12 +453,17 @@ type ChannelConstraints struct {
 // nature of HTLC's allotted, the keys to be used for delivery, and relative
 // time lock parameters.
 type ChannelConfig struct {
-	// ChannelConstraints is the set of constraints that must be upheld for
-	// the duration of the channel for the owner of this channel
+	// ChannelStateBounds is the set of constraints that must be
+	// upheld for the duration of the channel for the owner of this channel
 	// configuration. Constraints govern a number of flow control related
 	// parameters, also including the smallest HTLC that will be accepted
 	// by a participant.
-	ChannelConstraints
+	ChannelStateBounds
+
+	// CommitmentParams is an embedding of the parameters
+	// required to render an abstract channel state into a concrete
+	// commitment transaction.
+	CommitmentParams
 
 	// MultiSigKey is the key to be used within the 2-of-2 output script
 	// for the owner of this channel config.
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/funding/manager.go b/funding/manager.go
index 7fd0e9b11..8ad16005c 100644
--- a/funding/manager.go
+++ b/funding/manager.go
@@ -1671,16 +1671,18 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
 
 	// We'll also validate and apply all the constraints the initiating
 	// party is attempting to dictate for our commitment transaction.
-	channelConstraints := &channeldb.ChannelConstraints{
-		DustLimit:        msg.DustLimit,
+	stateBounds := &channeldb.ChannelStateBounds{
 		ChanReserve:      msg.ChannelReserve,
 		MaxPendingAmount: msg.MaxValueInFlight,
 		MinHTLC:          msg.HtlcMinimum,
 		MaxAcceptedHtlcs: msg.MaxAcceptedHTLCs,
-		CsvDelay:         msg.CsvDelay,
+	}
+	commitParams := &channeldb.CommitmentParams{
+		DustLimit: msg.DustLimit,
+		CsvDelay:  msg.CsvDelay,
 	}
 	err = reservation.CommitConstraints(
-		channelConstraints, f.cfg.MaxLocalCSVDelay, true,
+		stateBounds, commitParams, f.cfg.MaxLocalCSVDelay, true,
 	)
 	if err != nil {
 		log.Errorf("Unacceptable channel constraints: %v", err)
@@ -1780,7 +1782,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
 	// interactively.
 	ourContribution := reservation.OurContribution()
 	forwardingPolicy := f.defaultForwardingPolicy(
-		ourContribution.ChannelConstraints,
+		ourContribution.ChannelStateBounds,
 	)
 
 	// Once the reservation has been created successfully, we add it to
@@ -1810,37 +1812,41 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
 	// Update the timestamp once the fundingOpenMsg has been handled.
 	defer resCtx.updateTimestamp()
 
+	cfg := channeldb.ChannelConfig{
+		ChannelStateBounds: channeldb.ChannelStateBounds{
+			MaxPendingAmount: remoteMaxValue,
+			ChanReserve:      chanReserve,
+			MinHTLC:          minHtlc,
+			MaxAcceptedHtlcs: maxHtlcs,
+		},
+		CommitmentParams: channeldb.CommitmentParams{
+			DustLimit: msg.DustLimit,
+			CsvDelay:  remoteCsvDelay,
+		},
+		MultiSigKey: keychain.KeyDescriptor{
+			PubKey: copyPubKey(msg.FundingKey),
+		},
+		RevocationBasePoint: keychain.KeyDescriptor{
+			PubKey: copyPubKey(msg.RevocationPoint),
+		},
+		PaymentBasePoint: keychain.KeyDescriptor{
+			PubKey: copyPubKey(msg.PaymentPoint),
+		},
+		DelayBasePoint: keychain.KeyDescriptor{
+			PubKey: copyPubKey(msg.DelayedPaymentPoint),
+		},
+		HtlcBasePoint: keychain.KeyDescriptor{
+			PubKey: copyPubKey(msg.HtlcPoint),
+		},
+	}
+
 	// With our parameters set, we'll now process their contribution so we
 	// can move the funding workflow ahead.
 	remoteContribution := &lnwallet.ChannelContribution{
 		FundingAmount:        amt,
 		FirstCommitmentPoint: msg.FirstCommitmentPoint,
-		ChannelConfig: &channeldb.ChannelConfig{
-			ChannelConstraints: channeldb.ChannelConstraints{
-				DustLimit:        msg.DustLimit,
-				MaxPendingAmount: remoteMaxValue,
-				ChanReserve:      chanReserve,
-				MinHTLC:          minHtlc,
-				MaxAcceptedHtlcs: maxHtlcs,
-				CsvDelay:         remoteCsvDelay,
-			},
-			MultiSigKey: keychain.KeyDescriptor{
-				PubKey: copyPubKey(msg.FundingKey),
-			},
-			RevocationBasePoint: keychain.KeyDescriptor{
-				PubKey: copyPubKey(msg.RevocationPoint),
-			},
-			PaymentBasePoint: keychain.KeyDescriptor{
-				PubKey: copyPubKey(msg.PaymentPoint),
-			},
-			DelayBasePoint: keychain.KeyDescriptor{
-				PubKey: copyPubKey(msg.DelayedPaymentPoint),
-			},
-			HtlcBasePoint: keychain.KeyDescriptor{
-				PubKey: copyPubKey(msg.HtlcPoint),
-			},
-		},
-		UpfrontShutdown: msg.UpfrontShutdownScript,
+		ChannelConfig:        &cfg,
+		UpfrontShutdown:      msg.UpfrontShutdownScript,
 	}
 
 	if resCtx.reservation.IsTaproot() {
@@ -1867,8 +1873,12 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
 
 	log.Infof("Sending fundingResp for pending_id(%x)",
 		msg.PendingChannelID)
-	log.Debugf("Remote party accepted commitment constraints: %v",
-		spew.Sdump(remoteContribution.ChannelConfig.ChannelConstraints))
+	bounds := remoteContribution.ChannelConfig.ChannelStateBounds
+	log.Debugf("Remote party accepted channel state space bounds: %v",
+		lnutils.SpewLogClosure(bounds))
+	params := remoteContribution.ChannelConfig.CommitmentParams
+	log.Debugf("Remote party accepted commitment rendering params: %v",
+		lnutils.SpewLogClosure(params))
 
 	// With the initiator's contribution recorded, respond with our
 	// contribution in the next message of the workflow.
@@ -2036,16 +2046,18 @@ func (f *Manager) funderProcessAcceptChannel(peer lnpeer.Peer,
 	// required confirmations, and also the set of channel constraints
 	// they've specified for commitment states we can create.
 	resCtx.reservation.SetNumConfsRequired(uint16(minDepth))
-	channelConstraints := &channeldb.ChannelConstraints{
-		DustLimit:        msg.DustLimit,
+	bounds := channeldb.ChannelStateBounds{
 		ChanReserve:      msg.ChannelReserve,
 		MaxPendingAmount: msg.MaxValueInFlight,
 		MinHTLC:          msg.HtlcMinimum,
 		MaxAcceptedHtlcs: msg.MaxAcceptedHTLCs,
-		CsvDelay:         msg.CsvDelay,
+	}
+	commitParams := channeldb.CommitmentParams{
+		DustLimit: msg.DustLimit,
+		CsvDelay:  msg.CsvDelay,
 	}
 	err = resCtx.reservation.CommitConstraints(
-		channelConstraints, resCtx.maxLocalCsv, false,
+		&bounds, &commitParams, resCtx.maxLocalCsv, false,
 	)
 	if err != nil {
 		log.Warnf("Unacceptable channel constraints: %v", err)
@@ -2053,38 +2065,42 @@ func (f *Manager) funderProcessAcceptChannel(peer lnpeer.Peer,
 		return
 	}
 
+	cfg := channeldb.ChannelConfig{
+		ChannelStateBounds: channeldb.ChannelStateBounds{
+			MaxPendingAmount: resCtx.remoteMaxValue,
+			ChanReserve:      resCtx.remoteChanReserve,
+			MinHTLC:          resCtx.remoteMinHtlc,
+			MaxAcceptedHtlcs: resCtx.remoteMaxHtlcs,
+		},
+		CommitmentParams: channeldb.CommitmentParams{
+			DustLimit: msg.DustLimit,
+			CsvDelay:  resCtx.remoteCsvDelay,
+		},
+		MultiSigKey: keychain.KeyDescriptor{
+			PubKey: copyPubKey(msg.FundingKey),
+		},
+		RevocationBasePoint: keychain.KeyDescriptor{
+			PubKey: copyPubKey(msg.RevocationPoint),
+		},
+		PaymentBasePoint: keychain.KeyDescriptor{
+			PubKey: copyPubKey(msg.PaymentPoint),
+		},
+		DelayBasePoint: keychain.KeyDescriptor{
+			PubKey: copyPubKey(msg.DelayedPaymentPoint),
+		},
+		HtlcBasePoint: keychain.KeyDescriptor{
+			PubKey: copyPubKey(msg.HtlcPoint),
+		},
+	}
+
 	// The remote node has responded with their portion of the channel
 	// contribution. At this point, we can process their contribution which
 	// allows us to construct and sign both the commitment transaction, and
 	// the funding transaction.
 	remoteContribution := &lnwallet.ChannelContribution{
 		FirstCommitmentPoint: msg.FirstCommitmentPoint,
-		ChannelConfig: &channeldb.ChannelConfig{
-			ChannelConstraints: channeldb.ChannelConstraints{
-				DustLimit:        msg.DustLimit,
-				MaxPendingAmount: resCtx.remoteMaxValue,
-				ChanReserve:      resCtx.remoteChanReserve,
-				MinHTLC:          resCtx.remoteMinHtlc,
-				MaxAcceptedHtlcs: resCtx.remoteMaxHtlcs,
-				CsvDelay:         resCtx.remoteCsvDelay,
-			},
-			MultiSigKey: keychain.KeyDescriptor{
-				PubKey: copyPubKey(msg.FundingKey),
-			},
-			RevocationBasePoint: keychain.KeyDescriptor{
-				PubKey: copyPubKey(msg.RevocationPoint),
-			},
-			PaymentBasePoint: keychain.KeyDescriptor{
-				PubKey: copyPubKey(msg.PaymentPoint),
-			},
-			DelayBasePoint: keychain.KeyDescriptor{
-				PubKey: copyPubKey(msg.DelayedPaymentPoint),
-			},
-			HtlcBasePoint: keychain.KeyDescriptor{
-				PubKey: copyPubKey(msg.HtlcPoint),
-			},
-		},
-		UpfrontShutdown: msg.UpfrontShutdownScript,
+		ChannelConfig:        &cfg,
+		UpfrontShutdown:      msg.UpfrontShutdownScript,
 	}
 
 	if resCtx.reservation.IsTaproot() {
@@ -2149,8 +2165,12 @@ func (f *Manager) funderProcessAcceptChannel(peer lnpeer.Peer,
 	log.Infof("pendingChan(%x): remote party proposes num_confs=%v, "+
 		"csv_delay=%v", pendingChanID[:], msg.MinAcceptDepth,
 		msg.CsvDelay)
-	log.Debugf("Remote party accepted commitment constraints: %v",
-		spew.Sdump(remoteContribution.ChannelConfig.ChannelConstraints))
+	bounds = remoteContribution.ChannelConfig.ChannelStateBounds
+	log.Debugf("Remote party accepted channel state space bounds: %v",
+		lnutils.SpewLogClosure(bounds))
+	commitParams = remoteContribution.ChannelConfig.CommitmentParams
+	log.Debugf("Remote party accepted commitment rendering params: %v",
+		lnutils.SpewLogClosure(commitParams))
 
 	// If the user requested funding through a PSBT, we cannot directly
 	// continue now and need to wait for the fully funded and signed PSBT
@@ -4070,7 +4090,7 @@ func (f *Manager) ensureInitialForwardingPolicy(chanID lnwire.ChannelID,
 			"falling back to default values: %v", err)
 
 		forwardingPolicy = f.defaultForwardingPolicy(
-			channel.LocalChanCfg.ChannelConstraints,
+			channel.LocalChanCfg.ChannelStateBounds,
 		)
 		needDBUpdate = true
 	}
@@ -4695,7 +4715,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
 	// useBaseFee or useFeeRate are false the client did not provide fee
 	// values hence we assume default fee settings from the config.
 	forwardingPolicy := f.defaultForwardingPolicy(
-		ourContribution.ChannelConstraints,
+		ourContribution.ChannelStateBounds,
 	)
 	if baseFee != nil {
 		forwardingPolicy.BaseFee = lnwire.MilliSatoshi(*baseFee)
@@ -4750,16 +4770,18 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
 	defer resCtx.updateTimestamp()
 
 	// Check the sanity of the selected channel constraints.
-	channelConstraints := &channeldb.ChannelConstraints{
-		DustLimit:        ourDustLimit,
+	bounds := &channeldb.ChannelStateBounds{
 		ChanReserve:      chanReserve,
 		MaxPendingAmount: maxValue,
 		MinHTLC:          minHtlcIn,
 		MaxAcceptedHtlcs: maxHtlcs,
-		CsvDelay:         remoteCsvDelay,
+	}
+	commitParams := &channeldb.CommitmentParams{
+		DustLimit: ourDustLimit,
+		CsvDelay:  remoteCsvDelay,
 	}
 	err = lnwallet.VerifyConstraints(
-		channelConstraints, resCtx.maxLocalCsv, capacity,
+		bounds, commitParams, resCtx.maxLocalCsv, capacity,
 	)
 	if err != nil {
 		_, reserveErr := f.cancelReservationCtx(peerKey, chanID, false)
@@ -5032,11 +5054,11 @@ func copyPubKey(pub *btcec.PublicKey) *btcec.PublicKey {
 // defaultForwardingPolicy returns the default forwarding policy based on the
 // default routing policy and our local channel constraints.
 func (f *Manager) defaultForwardingPolicy(
-	constraints channeldb.ChannelConstraints) *models.ForwardingPolicy {
+	bounds channeldb.ChannelStateBounds) *models.ForwardingPolicy {
 
 	return &models.ForwardingPolicy{
-		MinHTLCOut:    constraints.MinHTLC,
-		MaxHTLC:       constraints.MaxPendingAmount,
+		MinHTLCOut:    bounds.MinHTLC,
+		MaxHTLC:       bounds.MaxPendingAmount,
 		BaseFee:       f.cfg.DefaultRoutingPolicy.BaseFee,
 		FeeRate:       f.cfg.DefaultRoutingPolicy.FeeRate,
 		TimeLockDelta: f.cfg.DefaultRoutingPolicy.TimeLockDelta,
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/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/lnwallet/reservation.go b/lnwallet/reservation.go
index 1c6dbbabe..529db1141 100644
--- a/lnwallet/reservation.go
+++ b/lnwallet/reservation.go
@@ -501,29 +501,34 @@ func (r *ChannelReservation) IsTaproot() bool {
 // of satoshis that can be transferred in a single commitment. This function
 // will also attempt to verify the constraints for sanity, returning an error
 // if the parameters are seemed unsound.
-func (r *ChannelReservation) CommitConstraints(c *channeldb.ChannelConstraints,
-	maxLocalCSVDelay uint16, responder bool) error {
+func (r *ChannelReservation) CommitConstraints(
+	bounds *channeldb.ChannelStateBounds,
+	commitParams *channeldb.CommitmentParams,
+	maxLocalCSVDelay uint16,
+	responder bool) error {
 
 	r.Lock()
 	defer r.Unlock()
 
 	// First, verify the sanity of the channel constraints.
-	err := VerifyConstraints(c, maxLocalCSVDelay, r.partialState.Capacity)
+	err := VerifyConstraints(
+		bounds, commitParams, maxLocalCSVDelay, r.partialState.Capacity,
+	)
 	if err != nil {
 		return err
 	}
 
 	// Our dust limit should always be less than or equal to our proposed
 	// channel reserve.
-	if responder && r.ourContribution.DustLimit > c.ChanReserve {
-		r.ourContribution.DustLimit = c.ChanReserve
+	if responder && r.ourContribution.DustLimit > bounds.ChanReserve {
+		r.ourContribution.DustLimit = bounds.ChanReserve
 	}
 
-	r.ourContribution.ChanReserve = c.ChanReserve
-	r.ourContribution.MaxPendingAmount = c.MaxPendingAmount
-	r.ourContribution.MinHTLC = c.MinHTLC
-	r.ourContribution.MaxAcceptedHtlcs = c.MaxAcceptedHtlcs
-	r.ourContribution.CsvDelay = c.CsvDelay
+	r.ourContribution.ChanReserve = bounds.ChanReserve
+	r.ourContribution.MaxPendingAmount = bounds.MaxPendingAmount
+	r.ourContribution.MinHTLC = bounds.MinHTLC
+	r.ourContribution.MaxAcceptedHtlcs = bounds.MaxAcceptedHtlcs
+	r.ourContribution.CsvDelay = commitParams.CsvDelay
 
 	return nil
 }
@@ -805,62 +810,75 @@ func (r *ChannelReservation) Cancel() error {
 
 // VerifyConstraints is a helper function that can be used to check the sanity
 // of various channel constraints.
-func VerifyConstraints(c *channeldb.ChannelConstraints,
-	maxLocalCSVDelay uint16, channelCapacity btcutil.Amount) error {
+func VerifyConstraints(bounds *channeldb.ChannelStateBounds,
+	commitParams *channeldb.CommitmentParams, maxLocalCSVDelay uint16,
+	channelCapacity btcutil.Amount) error {
 
 	// Fail if the csv delay for our funds exceeds our maximum.
-	if c.CsvDelay > maxLocalCSVDelay {
-		return ErrCsvDelayTooLarge(c.CsvDelay, maxLocalCSVDelay)
+	if commitParams.CsvDelay > maxLocalCSVDelay {
+		return ErrCsvDelayTooLarge(
+			commitParams.CsvDelay, maxLocalCSVDelay,
+		)
 	}
 
 	// The channel reserve should always be greater or equal to the dust
 	// limit. The reservation request should be denied if otherwise.
-	if c.DustLimit > c.ChanReserve {
-		return ErrChanReserveTooSmall(c.ChanReserve, c.DustLimit)
+	if commitParams.DustLimit > bounds.ChanReserve {
+		return ErrChanReserveTooSmall(
+			bounds.ChanReserve, commitParams.DustLimit,
+		)
 	}
 
 	// Validate against the maximum-sized witness script dust limit, and
 	// also ensure that the DustLimit is not too large.
 	maxWitnessLimit := DustLimitForSize(input.UnknownWitnessSize)
-	if c.DustLimit < maxWitnessLimit || c.DustLimit > 3*maxWitnessLimit {
-		return ErrInvalidDustLimit(c.DustLimit)
+	if commitParams.DustLimit < maxWitnessLimit ||
+		commitParams.DustLimit > 3*maxWitnessLimit {
+
+		return ErrInvalidDustLimit(commitParams.DustLimit)
 	}
 
 	// Fail if we consider the channel reserve to be too large.  We
 	// currently fail if it is greater than 20% of the channel capacity.
 	maxChanReserve := channelCapacity / 5
-	if c.ChanReserve > maxChanReserve {
-		return ErrChanReserveTooLarge(c.ChanReserve, maxChanReserve)
+	if bounds.ChanReserve > maxChanReserve {
+		return ErrChanReserveTooLarge(
+			bounds.ChanReserve, maxChanReserve,
+		)
 	}
 
 	// Fail if the minimum HTLC value is too large. If this is too large,
 	// the channel won't be useful for sending small payments. This limit
 	// is currently set to maxValueInFlight, effectively letting the remote
 	// setting this as large as it wants.
-	if c.MinHTLC > c.MaxPendingAmount {
-		return ErrMinHtlcTooLarge(c.MinHTLC, c.MaxPendingAmount)
+	if bounds.MinHTLC > bounds.MaxPendingAmount {
+		return ErrMinHtlcTooLarge(
+			bounds.MinHTLC, bounds.MaxPendingAmount,
+		)
 	}
 
 	// Fail if maxHtlcs is above the maximum allowed number of 483.  This
 	// number is specified in BOLT-02.
-	if c.MaxAcceptedHtlcs > uint16(input.MaxHTLCNumber/2) {
+	if bounds.MaxAcceptedHtlcs > uint16(input.MaxHTLCNumber/2) {
 		return ErrMaxHtlcNumTooLarge(
-			c.MaxAcceptedHtlcs, uint16(input.MaxHTLCNumber/2),
+			bounds.MaxAcceptedHtlcs, uint16(input.MaxHTLCNumber/2),
 		)
 	}
 
 	// Fail if we consider maxHtlcs too small. If this is too small we
 	// cannot offer many HTLCs to the remote.
 	const minNumHtlc = 5
-	if c.MaxAcceptedHtlcs < minNumHtlc {
-		return ErrMaxHtlcNumTooSmall(c.MaxAcceptedHtlcs, minNumHtlc)
+	if bounds.MaxAcceptedHtlcs < minNumHtlc {
+		return ErrMaxHtlcNumTooSmall(
+			bounds.MaxAcceptedHtlcs, minNumHtlc,
+		)
 	}
 
 	// Fail if we consider maxValueInFlight too small. We currently require
 	// the remote to at least allow minNumHtlc * minHtlc in flight.
-	if c.MaxPendingAmount < minNumHtlc*c.MinHTLC {
+	if bounds.MaxPendingAmount < minNumHtlc*bounds.MinHTLC {
 		return ErrMaxValueInFlightTooSmall(
-			c.MaxPendingAmount, minNumHtlc*c.MinHTLC,
+			bounds.MaxPendingAmount, minNumHtlc*bounds.MinHTLC,
 		)
 	}
 
diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go
index a56bf1c21..90a0bea2d 100644
--- a/lnwallet/wallet.go
+++ b/lnwallet/wallet.go
@@ -1379,7 +1379,7 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
 	)
 
 	reservation.partialState.RevocationProducer = producer
-	reservation.ourContribution.ChannelConstraints.DustLimit =
+	reservation.ourContribution.CommitmentParams.DustLimit =
 		DustLimitUnknownWitness()
 
 	// If taproot channels are active, then we'll generate our verification
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/localchans/manager.go b/routing/localchans/manager.go
index 930b4c96b..f0f9b88de 100644
--- a/routing/localchans/manager.go
+++ b/routing/localchans/manager.go
@@ -279,7 +279,7 @@ func (r *Manager) getHtlcAmtLimits(tx kvdb.RTx, chanPoint wire.OutPoint) (
 	// capacity AND less than or equal to the max in-flight HTLC value.
 	// Since the latter is always less than or equal to the former, just
 	// return the max in-flight value.
-	maxAmt := ch.LocalChanCfg.ChannelConstraints.MaxPendingAmount
+	maxAmt := ch.LocalChanCfg.ChannelStateBounds.MaxPendingAmount
 
 	return ch.LocalChanCfg.MinHTLC, maxAmt, 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/chanbackup/single_test.go b/chanbackup/single_test.go
index ab418e190..c1e940740 100644
--- a/chanbackup/single_test.go
+++ b/chanbackup/single_test.go
@@ -126,6 +126,64 @@ func genRandomOpenChannelShell() (*channeldb.OpenChannel, error) {
 
 	chanType := channeldb.ChannelType(rand.Intn(8))
 
+	localCfg := channeldb.ChannelConfig{
+		ChannelStateBounds: channeldb.ChannelStateBounds{},
+		CommitmentParams: channeldb.CommitmentParams{
+			CsvDelay: uint16(rand.Int63()),
+		},
+		MultiSigKey: keychain.KeyDescriptor{
+			KeyLocator: keychain.KeyLocator{
+				Family: keychain.KeyFamily(rand.Int63()),
+				Index:  uint32(rand.Int63()),
+			},
+		},
+		RevocationBasePoint: keychain.KeyDescriptor{
+			KeyLocator: keychain.KeyLocator{
+				Family: keychain.KeyFamily(rand.Int63()),
+				Index:  uint32(rand.Int63()),
+			},
+		},
+		PaymentBasePoint: keychain.KeyDescriptor{
+			KeyLocator: keychain.KeyLocator{
+				Family: keychain.KeyFamily(rand.Int63()),
+				Index:  uint32(rand.Int63()),
+			},
+		},
+		DelayBasePoint: keychain.KeyDescriptor{
+			KeyLocator: keychain.KeyLocator{
+				Family: keychain.KeyFamily(rand.Int63()),
+				Index:  uint32(rand.Int63()),
+			},
+		},
+		HtlcBasePoint: keychain.KeyDescriptor{
+			KeyLocator: keychain.KeyLocator{
+				Family: keychain.KeyFamily(rand.Int63()),
+				Index:  uint32(rand.Int63()),
+			},
+		},
+	}
+
+	remoteCfg := channeldb.ChannelConfig{
+		CommitmentParams: channeldb.CommitmentParams{
+			CsvDelay: uint16(rand.Int63()),
+		},
+		MultiSigKey: keychain.KeyDescriptor{
+			PubKey: pub,
+		},
+		RevocationBasePoint: keychain.KeyDescriptor{
+			PubKey: pub,
+		},
+		PaymentBasePoint: keychain.KeyDescriptor{
+			PubKey: pub,
+		},
+		DelayBasePoint: keychain.KeyDescriptor{
+			PubKey: pub,
+		},
+		HtlcBasePoint: keychain.KeyDescriptor{
+			PubKey: pub,
+		},
+	}
+
 	return &channeldb.OpenChannel{
 		ChainHash:       chainHash,
 		ChanType:        chanType,
@@ -134,63 +192,10 @@ func genRandomOpenChannelShell() (*channeldb.OpenChannel, error) {
 		ShortChannelID: lnwire.NewShortChanIDFromInt(
 			uint64(rand.Int63()),
 		),
-		ThawHeight:  rand.Uint32(),
-		IdentityPub: pub,
-		LocalChanCfg: channeldb.ChannelConfig{
-			ChannelConstraints: channeldb.ChannelConstraints{
-				CsvDelay: uint16(rand.Int63()),
-			},
-			MultiSigKey: keychain.KeyDescriptor{
-				KeyLocator: keychain.KeyLocator{
-					Family: keychain.KeyFamily(rand.Int63()),
-					Index:  uint32(rand.Int63()),
-				},
-			},
-			RevocationBasePoint: keychain.KeyDescriptor{
-				KeyLocator: keychain.KeyLocator{
-					Family: keychain.KeyFamily(rand.Int63()),
-					Index:  uint32(rand.Int63()),
-				},
-			},
-			PaymentBasePoint: keychain.KeyDescriptor{
-				KeyLocator: keychain.KeyLocator{
-					Family: keychain.KeyFamily(rand.Int63()),
-					Index:  uint32(rand.Int63()),
-				},
-			},
-			DelayBasePoint: keychain.KeyDescriptor{
-				KeyLocator: keychain.KeyLocator{
-					Family: keychain.KeyFamily(rand.Int63()),
-					Index:  uint32(rand.Int63()),
-				},
-			},
-			HtlcBasePoint: keychain.KeyDescriptor{
-				KeyLocator: keychain.KeyLocator{
-					Family: keychain.KeyFamily(rand.Int63()),
-					Index:  uint32(rand.Int63()),
-				},
-			},
-		},
-		RemoteChanCfg: channeldb.ChannelConfig{
-			ChannelConstraints: channeldb.ChannelConstraints{
-				CsvDelay: uint16(rand.Int63()),
-			},
-			MultiSigKey: keychain.KeyDescriptor{
-				PubKey: pub,
-			},
-			RevocationBasePoint: keychain.KeyDescriptor{
-				PubKey: pub,
-			},
-			PaymentBasePoint: keychain.KeyDescriptor{
-				PubKey: pub,
-			},
-			DelayBasePoint: keychain.KeyDescriptor{
-				PubKey: pub,
-			},
-			HtlcBasePoint: keychain.KeyDescriptor{
-				PubKey: pub,
-			},
-		},
+		ThawHeight:         rand.Uint32(),
+		IdentityPub:        pub,
+		LocalChanCfg:       localCfg,
+		RemoteChanCfg:      remoteCfg,
 		RevocationProducer: shaChainProducer,
 	}, nil
 }
diff --git a/channeldb/channel_test.go b/channeldb/channel_test.go
index e630b1c48..a7f3c1ebe 100644
--- a/channeldb/channel_test.go
+++ b/channeldb/channel_test.go
@@ -235,15 +235,21 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
 		}
 	}
 
+	localStateBounds := ChannelStateBounds{
+		MaxPendingAmount: lnwire.MilliSatoshi(rand.Int63()),
+		ChanReserve:      btcutil.Amount(rand.Int63()),
+		MinHTLC:          lnwire.MilliSatoshi(rand.Int63()),
+		MaxAcceptedHtlcs: uint16(rand.Int31()),
+	}
+
+	localRenderingParams := CommitmentParams{
+		DustLimit: btcutil.Amount(rand.Int63()),
+		CsvDelay:  uint16(rand.Int31()),
+	}
+
 	localCfg := ChannelConfig{
-		ChannelConstraints: ChannelConstraints{
-			DustLimit:        btcutil.Amount(rand.Int63()),
-			MaxPendingAmount: lnwire.MilliSatoshi(rand.Int63()),
-			ChanReserve:      btcutil.Amount(rand.Int63()),
-			MinHTLC:          lnwire.MilliSatoshi(rand.Int63()),
-			MaxAcceptedHtlcs: uint16(rand.Int31()),
-			CsvDelay:         uint16(rand.Int31()),
-		},
+		ChannelStateBounds: localStateBounds,
+		CommitmentParams:   localRenderingParams,
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: privKey.PubKey(),
 		},
@@ -260,15 +266,22 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
 			PubKey: privKey.PubKey(),
 		},
 	}
+
+	remoteStateBounds := ChannelStateBounds{
+		MaxPendingAmount: lnwire.MilliSatoshi(rand.Int63()),
+		ChanReserve:      btcutil.Amount(rand.Int63()),
+		MinHTLC:          lnwire.MilliSatoshi(rand.Int63()),
+		MaxAcceptedHtlcs: uint16(rand.Int31()),
+	}
+
+	remoteRenderingParams := CommitmentParams{
+		DustLimit: btcutil.Amount(rand.Int63()),
+		CsvDelay:  uint16(rand.Int31()),
+	}
+
 	remoteCfg := ChannelConfig{
-		ChannelConstraints: ChannelConstraints{
-			DustLimit:        btcutil.Amount(rand.Int63()),
-			MaxPendingAmount: lnwire.MilliSatoshi(rand.Int63()),
-			ChanReserve:      btcutil.Amount(rand.Int63()),
-			MinHTLC:          lnwire.MilliSatoshi(rand.Int63()),
-			MaxAcceptedHtlcs: uint16(rand.Int31()),
-			CsvDelay:         uint16(rand.Int31()),
-		},
+		ChannelStateBounds: remoteStateBounds,
+		CommitmentParams:   remoteRenderingParams,
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: privKey.PubKey(),
 			KeyLocator: keychain.KeyLocator{
diff --git a/channeldb/db_test.go b/channeldb/db_test.go
index 025bf1261..d8113db83 100644
--- a/channeldb/db_test.go
+++ b/channeldb/db_test.go
@@ -292,6 +292,10 @@ func genRandomChannelShell() (*ChannelShell, error) {
 	}
 	shaChainProducer := shachain.NewRevocationProducer(*revRoot)
 
+	commitParams := CommitmentParams{
+		CsvDelay: uint16(rand.Int63()),
+	}
+
 	return &ChannelShell{
 		NodeAddrs: []net.Addr{&net.TCPAddr{
 			IP:   net.ParseIP("127.0.0.1"),
@@ -306,9 +310,7 @@ func genRandomChannelShell() (*ChannelShell, error) {
 			),
 			IdentityPub: pub,
 			LocalChanCfg: ChannelConfig{
-				ChannelConstraints: ChannelConstraints{
-					CsvDelay: uint16(rand.Int63()),
-				},
+				CommitmentParams: commitParams,
 				PaymentBasePoint: keychain.KeyDescriptor{
 					KeyLocator: keychain.KeyLocator{
 						Family: keychain.KeyFamily(rand.Int63()),
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/contractcourt/breach_arbitrator_test.go b/contractcourt/breach_arbitrator_test.go
index f2883db4e..6a1865444 100644
--- a/contractcourt/breach_arbitrator_test.go
+++ b/contractcourt/breach_arbitrator_test.go
@@ -2178,13 +2178,15 @@ func createInitChannels(t *testing.T) (
 	fundingTxIn := wire.NewTxIn(prevOut, nil, nil)
 
 	aliceCfg := channeldb.ChannelConfig{
-		ChannelConstraints: channeldb.ChannelConstraints{
-			DustLimit:        aliceDustLimit,
+		ChannelStateBounds: channeldb.ChannelStateBounds{
 			MaxPendingAmount: lnwire.MilliSatoshi(rand.Int63()),
 			ChanReserve:      0,
 			MinHTLC:          0,
 			MaxAcceptedHtlcs: uint16(rand.Int31()),
-			CsvDelay:         uint16(csvTimeoutAlice),
+		},
+		CommitmentParams: channeldb.CommitmentParams{
+			DustLimit: aliceDustLimit,
+			CsvDelay:  uint16(csvTimeoutAlice),
 		},
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: aliceKeyPub,
@@ -2203,13 +2205,15 @@ func createInitChannels(t *testing.T) (
 		},
 	}
 	bobCfg := channeldb.ChannelConfig{
-		ChannelConstraints: channeldb.ChannelConstraints{
-			DustLimit:        bobDustLimit,
+		ChannelStateBounds: channeldb.ChannelStateBounds{
 			MaxPendingAmount: lnwire.MilliSatoshi(rand.Int63()),
 			ChanReserve:      0,
 			MinHTLC:          0,
 			MaxAcceptedHtlcs: uint16(rand.Int31()),
-			CsvDelay:         uint16(csvTimeoutBob),
+		},
+		CommitmentParams: channeldb.CommitmentParams{
+			DustLimit: bobDustLimit,
+			CsvDelay:  uint16(csvTimeoutBob),
 		},
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: bobKeyPub,
diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go
index 9a72197ec..5243eebe0 100644
--- a/htlcswitch/test_utils.go
+++ b/htlcswitch/test_utils.go
@@ -138,24 +138,28 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
 	csvTimeoutBob := uint32(4)
 	isAliceInitiator := true
 
-	aliceConstraints := &channeldb.ChannelConstraints{
-		DustLimit: btcutil.Amount(200),
+	aliceBounds := channeldb.ChannelStateBounds{
 		MaxPendingAmount: lnwire.NewMSatFromSatoshis(
 			channelCapacity),
 		ChanReserve:      aliceReserve,
 		MinHTLC:          0,
 		MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
-		CsvDelay:         uint16(csvTimeoutAlice),
+	}
+	aliceCommitParams := channeldb.CommitmentParams{
+		DustLimit: btcutil.Amount(200),
+		CsvDelay:  uint16(csvTimeoutAlice),
 	}
 
-	bobConstraints := &channeldb.ChannelConstraints{
-		DustLimit: btcutil.Amount(800),
+	bobBounds := channeldb.ChannelStateBounds{
 		MaxPendingAmount: lnwire.NewMSatFromSatoshis(
 			channelCapacity),
 		ChanReserve:      bobReserve,
 		MinHTLC:          0,
 		MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
-		CsvDelay:         uint16(csvTimeoutBob),
+	}
+	bobCommitParams := channeldb.CommitmentParams{
+		DustLimit: btcutil.Amount(800),
+		CsvDelay:  uint16(csvTimeoutBob),
 	}
 
 	var hash [sha256.Size]byte
@@ -172,7 +176,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
 	fundingTxIn := wire.NewTxIn(prevOut, nil, nil)
 
 	aliceCfg := channeldb.ChannelConfig{
-		ChannelConstraints: *aliceConstraints,
+		ChannelStateBounds: aliceBounds,
+		CommitmentParams:   aliceCommitParams,
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: aliceKeyPub,
 		},
@@ -190,7 +195,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
 		},
 	}
 	bobCfg := channeldb.ChannelConfig{
-		ChannelConstraints: *bobConstraints,
+		ChannelStateBounds: bobBounds,
+		CommitmentParams:   bobCommitParams,
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: bobKeyPub,
 		},
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/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go
index c3d26f6cf..dfabdc741 100644
--- a/lnwallet/test/test_interface.go
+++ b/lnwallet/test/test_interface.go
@@ -422,16 +422,18 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness,
 	aliceChanReservation, err := alice.InitChannelReservation(aliceReq)
 	require.NoError(t, err, "unable to initialize funding reservation")
 	aliceChanReservation.SetNumConfsRequired(numReqConfs)
-	channelConstraints := &channeldb.ChannelConstraints{
-		DustLimit:        lnwallet.DustLimitUnknownWitness(),
+	bounds := &channeldb.ChannelStateBounds{
 		ChanReserve:      fundingAmount / 100,
 		MaxPendingAmount: lnwire.NewMSatFromSatoshis(fundingAmount),
 		MinHTLC:          1,
 		MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
-		CsvDelay:         csvDelay,
+	}
+	commitParams := &channeldb.CommitmentParams{
+		DustLimit: lnwallet.DustLimitUnknownWitness(),
+		CsvDelay:  csvDelay,
 	}
 	err = aliceChanReservation.CommitConstraints(
-		channelConstraints, defaultMaxLocalCsvDelay, false,
+		bounds, commitParams, defaultMaxLocalCsvDelay, false,
 	)
 	require.NoError(t, err, "unable to verify constraints")
 
@@ -463,7 +465,7 @@ func testDualFundingReservationWorkflow(miner *rpctest.Harness,
 	bobChanReservation, err := bob.InitChannelReservation(bobReq)
 	require.NoError(t, err, "bob unable to init channel reservation")
 	err = bobChanReservation.CommitConstraints(
-		channelConstraints, defaultMaxLocalCsvDelay, true,
+		bounds, commitParams, defaultMaxLocalCsvDelay, true,
 	)
 	require.NoError(t, err, "unable to verify constraints")
 	bobChanReservation.SetNumConfsRequired(numReqConfs)
@@ -827,16 +829,18 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
 	aliceChanReservation, err := alice.InitChannelReservation(aliceReq)
 	require.NoError(t, err, "unable to init channel reservation")
 	aliceChanReservation.SetNumConfsRequired(numReqConfs)
-	channelConstraints := &channeldb.ChannelConstraints{
-		DustLimit:        lnwallet.DustLimitUnknownWitness(),
+	bounds := &channeldb.ChannelStateBounds{
 		ChanReserve:      fundingAmt / 100,
 		MaxPendingAmount: lnwire.NewMSatFromSatoshis(fundingAmt),
 		MinHTLC:          1,
 		MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
-		CsvDelay:         csvDelay,
+	}
+	commitParams := &channeldb.CommitmentParams{
+		DustLimit: lnwallet.DustLimitUnknownWitness(),
+		CsvDelay:  csvDelay,
 	}
 	err = aliceChanReservation.CommitConstraints(
-		channelConstraints, defaultMaxLocalCsvDelay, false,
+		bounds, commitParams, defaultMaxLocalCsvDelay, false,
 	)
 	require.NoError(t, err, "unable to verify constraints")
 
@@ -875,7 +879,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
 	bobChanReservation, err := bob.InitChannelReservation(bobReq)
 	require.NoError(t, err, "unable to create bob reservation")
 	err = bobChanReservation.CommitConstraints(
-		channelConstraints, defaultMaxLocalCsvDelay, true,
+		bounds, commitParams, defaultMaxLocalCsvDelay, true,
 	)
 	require.NoError(t, err, "unable to verify constraints")
 	bobChanReservation.SetNumConfsRequired(numReqConfs)
diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go
index 29213bb7f..0e5d52723 100644
--- a/lnwallet/test_utils.go
+++ b/lnwallet/test_utils.go
@@ -150,13 +150,15 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
 	}
 
 	aliceCfg := channeldb.ChannelConfig{
-		ChannelConstraints: channeldb.ChannelConstraints{
-			DustLimit:        aliceDustLimit,
+		ChannelStateBounds: channeldb.ChannelStateBounds{
 			MaxPendingAmount: lnwire.NewMSatFromSatoshis(channelCapacity),
 			ChanReserve:      channelCapacity / 100,
 			MinHTLC:          0,
 			MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
-			CsvDelay:         uint16(csvTimeoutAlice),
+		},
+		CommitmentParams: channeldb.CommitmentParams{
+			DustLimit: aliceDustLimit,
+			CsvDelay:  uint16(csvTimeoutAlice),
 		},
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: aliceKeys[0].PubKey(),
@@ -175,13 +177,15 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
 		},
 	}
 	bobCfg := channeldb.ChannelConfig{
-		ChannelConstraints: channeldb.ChannelConstraints{
-			DustLimit:        bobDustLimit,
+		ChannelStateBounds: channeldb.ChannelStateBounds{
 			MaxPendingAmount: lnwire.NewMSatFromSatoshis(channelCapacity),
 			ChanReserve:      channelCapacity / 100,
 			MinHTLC:          0,
 			MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
-			CsvDelay:         uint16(csvTimeoutBob),
+		},
+		CommitmentParams: channeldb.CommitmentParams{
+			DustLimit: bobDustLimit,
+			CsvDelay:  uint16(csvTimeoutBob),
 		},
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: bobKeys[0].PubKey(),
diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go
index d2a2a448b..4c7fbed53 100644
--- a/lnwallet/transactions_test.go
+++ b/lnwallet/transactions_test.go
@@ -603,14 +603,14 @@ func testSpendValidation(t *testing.T, tweakless bool) {
 	dustLimit := DustLimitForSize(input.UnknownWitnessSize)
 
 	aliceChanCfg := &channeldb.ChannelConfig{
-		ChannelConstraints: channeldb.ChannelConstraints{
+		CommitmentParams: channeldb.CommitmentParams{
 			DustLimit: dustLimit,
 			CsvDelay:  csvTimeout,
 		},
 	}
 
 	bobChanCfg := &channeldb.ChannelConfig{
-		ChannelConstraints: channeldb.ChannelConstraints{
+		CommitmentParams: channeldb.CommitmentParams{
 			DustLimit: dustLimit,
 			CsvDelay:  csvTimeout,
 		},
@@ -834,15 +834,17 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp
 
 	// Define channel configurations.
 	remoteCfg := channeldb.ChannelConfig{
-		ChannelConstraints: channeldb.ChannelConstraints{
-			DustLimit: tc.dustLimit,
+		ChannelStateBounds: channeldb.ChannelStateBounds{
 			MaxPendingAmount: lnwire.NewMSatFromSatoshis(
 				tc.fundingAmount,
 			),
 			ChanReserve:      0,
 			MinHTLC:          0,
 			MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
-			CsvDelay:         tc.localCsvDelay,
+		},
+		CommitmentParams: channeldb.CommitmentParams{
+			DustLimit: tc.dustLimit,
+			CsvDelay:  tc.localCsvDelay,
 		},
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: tc.remoteFundingPrivkey.PubKey(),
@@ -861,15 +863,17 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp
 		},
 	}
 	localCfg := channeldb.ChannelConfig{
-		ChannelConstraints: channeldb.ChannelConstraints{
-			DustLimit: tc.dustLimit,
+		ChannelStateBounds: channeldb.ChannelStateBounds{
 			MaxPendingAmount: lnwire.NewMSatFromSatoshis(
 				tc.fundingAmount,
 			),
 			ChanReserve:      0,
 			MinHTLC:          0,
 			MaxAcceptedHtlcs: input.MaxHTLCNumber / 2,
-			CsvDelay:         tc.localCsvDelay,
+		},
+		CommitmentParams: channeldb.CommitmentParams{
+			DustLimit: tc.dustLimit,
+			CsvDelay:  tc.localCsvDelay,
 		},
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: tc.localFundingPrivkey.PubKey(),
diff --git a/peer/test_utils.go b/peer/test_utils.go
index 8667b04cd..8d1355b18 100644
--- a/peer/test_utils.go
+++ b/peer/test_utils.go
@@ -115,13 +115,15 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a,
 	)
 
 	aliceCfg := channeldb.ChannelConfig{
-		ChannelConstraints: channeldb.ChannelConstraints{
-			DustLimit:        aliceDustLimit,
+		ChannelStateBounds: channeldb.ChannelStateBounds{
 			MaxPendingAmount: lnwire.MilliSatoshi(rand.Int63()),
 			ChanReserve:      btcutil.Amount(rand.Int63()),
 			MinHTLC:          lnwire.MilliSatoshi(rand.Int63()),
 			MaxAcceptedHtlcs: uint16(rand.Int31()),
-			CsvDelay:         uint16(csvTimeoutAlice),
+		},
+		CommitmentParams: channeldb.CommitmentParams{
+			DustLimit: aliceDustLimit,
+			CsvDelay:  uint16(csvTimeoutAlice),
 		},
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: aliceKeyPub,
@@ -140,13 +142,15 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a,
 		},
 	}
 	bobCfg := channeldb.ChannelConfig{
-		ChannelConstraints: channeldb.ChannelConstraints{
-			DustLimit:        bobDustLimit,
+		ChannelStateBounds: channeldb.ChannelStateBounds{
 			MaxPendingAmount: lnwire.MilliSatoshi(rand.Int63()),
 			ChanReserve:      btcutil.Amount(rand.Int63()),
 			MinHTLC:          lnwire.MilliSatoshi(rand.Int63()),
 			MaxAcceptedHtlcs: uint16(rand.Int31()),
-			CsvDelay:         uint16(csvTimeoutBob),
+		},
+		CommitmentParams: channeldb.CommitmentParams{
+			DustLimit: bobDustLimit,
+			CsvDelay:  uint16(csvTimeoutBob),
 		},
 		MultiSigKey: keychain.KeyDescriptor{
 			PubKey: bobKeyPub,
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/localchans/manager_test.go b/routing/localchans/manager_test.go
index b103ef31f..7594eef04 100644
--- a/routing/localchans/manager_test.go
+++ b/routing/localchans/manager_test.go
@@ -125,14 +125,14 @@ func TestManager(t *testing.T) {
 			return &channeldb.OpenChannel{}, channeldb.ErrChannelNotFound
 		}
 
-		constraints := channeldb.ChannelConstraints{
+		bounds := channeldb.ChannelStateBounds{
 			MaxPendingAmount: maxPendingAmount,
 			MinHTLC:          minHTLC,
 		}
 
 		return &channeldb.OpenChannel{
 			LocalChanCfg: channeldb.ChannelConfig{
-				ChannelConstraints: constraints,
+				ChannelStateBounds: bounds,
 			},
 		}, nil
 	}
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,
 			)