Repository: lightningnetwork/lnd
Pull request #8805 (2024-06-03 10:17:51)

localchans: recreate missing edge if not found


## Change Description
Description of change / link to associated issue.If a node contains a channel, but doesn't have a corresponding edge in the graph database, updating the channel policy would fail. In this commit the edge is recreated if the channel exists. This ensures a node can recover from a missing edge in the graph database by calling `updatechanpolicy`.

Alternative for https://github.com/lightningnetwork/lnd/pull/8768, namely option 2 in https://github.com/lightningnetwork/lnd/pull/8768#issuecomment-2143799767
Partially fixes https://github.com/lightningnetwork/lnd/issues/7261 by allowing to recreate the edge by calling `updatechanpolicy`.

## Steps to Test
- Create a node that has a channel with a missing edge
- Calling getchaninfo on this channel will fail
- Call updatechanpolicy on this channel
- Calling getchaninfo on this channel should succeed

I'm not sure how to create an integration test where I can modify the graph database to delete an edge in order to test this. Please advise.

## Pull Request Checklist
### Testing
- [x] Your PR passes all CI checks.
- [x] Tests covering the positive and negative (error paths) are included.
- [x] Bug fixes contain tests triggering the bug to prevent regressions.

### Code Style and Documentation
- [x] 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.
- [x] Commits follow the [Ideal Git Commit Structure](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#IdealGitCommitStructure).
- [x] Any new logging statements use an appropriate subsystem and logging level.
- [x] Any new lncli commands have appropriate tags in the comments for the rpc in the proto file.
- [x]  [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 Configration 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 -->
guggero: Release notes need to be moved to the `v0.18.1` file.
lightninglabs-deploy: @jssdwt, remember to re-request review from reviewers when ready
yyforyongyu: Maybe it's related to [this](https://github.com/lightningnetwork/lnd/issues/8870#issuecomment-2192464477)?
ziggie1984: > Maybe it's related to https://github.com/lightningnetwork/lnd/issues/8870#issuecomment-2192464477?

Hmm I don't think so, not having the edge available means somehow that our own `ChanAnnouncment` didn't got through? Not receiving the node announcment would just leaf just with a `NodeAnnouncement Shell`, we add during adding the edge to the db.
ziggie1984: Concept Ack from my side as well, I think it's the best way to fix broken channels suffering from the `Edge not Found` problem. I think we still have not traced down the origin of the problem, this gives us at least the possibility to mitigate the problem.
JssDWt: Sorry for letting this sit for so long. Updated with your feedback now @yyforyongyu 
ziggie1984: what about passing the graph db as a config to the `localchan_manager`, so that we can when recreating the edge just call `AddChannelEdge` and short-circuit all the other stuff which is done in the gossiper ? Later during the ChanUpdate (`PropagateChanPolicyUpdate`) all the necessary stuff is done (adding the update to the Topology etc.). 
JssDWt: Handled most comments. Still have to create a test.
JssDWt: Added a test for createEdge
JssDWt: Now also added a test to the UpdatePolicy function
JssDWt: If this gets a thumbs up, we'll try it out in production to see whether it helps with our `edge not found` issues.
ziggie1984: Awesome job @JssDWt 🎉, thanks for fixing this in LND. Just a little style nit.

I will approve this PR once you give us feedback how your local testing went but it's gtg from my side.
lightninglabs-deploy: @ziggie1984: review reminder
lightninglabs-deploy: @ziggie1984: review reminder
ziggie1984: @JssDWt have you tested already, because we are planning to ship it in 18.3 ?
JssDWt: @ziggie1984 you can't ship it with 0.18.3. There was a crash. I'm afk for the next week, I'll look at it next week.
lightninglabs-deploy: @ziggie1984: review reminder
JssDWt: Added a fix for a cash, because SchedulerOptions were nil.
JssDWt: Rebased
ziggie1984: Just for general information this was the problem:

```
AddEdge: func(edge *models.ChannelEdgeInfo) error {
			return s.chanRouter.AddEdge(edge, nil)}
```

by accident we provided a nil for the `SchedulerOptions `

however no its correct, we don't test db interaction in the unit-test case.

However I think because this happened lets add a `nil` checke here:

```
for _, f := range op {
         if f != nil{
		f(r)
         }
	                        }
```

better safe than sorry.
JssDWt: Noting that during the rebase I had to replace `chanRouter` with `graphBuilder` [here](https://github.com/lightningnetwork/lnd/pull/8805/files#diff-366e46a40f6f60b4f7614eb0976bb51820364bf5ca6ccc4787eb49d7bdbef3e6R1071).
JssDWt: > However I think because this happened lets add a `nil` checke here:
> 
> ```
> for _, f := range op {
>          if f != nil{
> 		f(r)
>          }
> 	                        }
> ```
> 
> better safe than sorry.

Added this in this PR
lightninglabs-deploy: @ziggie1984: review reminder
lightninglabs-deploy: @ziggie1984: review reminder
lightninglabs-deploy: @ziggie1984: review reminder
lightninglabs-deploy: @ziggie1984: review reminder
lightninglabs-deploy: @ziggie1984: review reminder
lightninglabs-deploy: @ziggie1984: review reminder
lightninglabs-deploy: @ziggie1984: review reminder
lightninglabs-deploy: @ziggie1984: review reminder
lightninglabs-deploy: @ziggie1984: review reminder
lightninglabs-deploy: @ziggie1984: review reminder
JssDWt: Fixed a bug where the timestamp was not set, so the policy wouldn't be added.

Adn rebased.

JssDWt: Added a commit that ensures the policy is also recreated if the edge exists, but only not the policy.
ziggie1984: Looking good, waiting your final test and hope we can get this thing merged then.
ziggie1984: Will approve as soon as @JssDWt reports back that this actually fixes the problem of missing edges, so it is only on hold because we cannot easily reproduce missing `edgeinfo` in a db testing it in an itest.
guggero: > Will approve as soon as @JssDWt reports back that this actually fixes the problem of missing edges, so it is only on hold because we cannot easily reproduce missing `edgeinfo` in a db testing it in an itest.

Ah, sorry. Wasn't sure if the latest push contained those (probably should've checked :sweat_smile:).
lightninglabs-deploy: @jssdwt, remember to re-request review from reviewers when ready

Patch:
diff --git a/chainntnfs/bitcoindnotify/bitcoind.go b/chainntnfs/bitcoindnotify/bitcoind.go
index 91c38d8d6..f0547e4df 100644
--- a/chainntnfs/bitcoindnotify/bitcoind.go
+++ b/chainntnfs/bitcoindnotify/bitcoind.go
@@ -139,7 +139,6 @@ func (b *BitcoindNotifier) Stop() error {
 	// Shutdown the rpc client, this gracefully disconnects from bitcoind,
 	// and cleans up all related resources.
 	b.chainConn.Stop()
-	b.chainConn.WaitForShutdown()
 
 	close(b.quit)
 	b.wg.Wait()
@@ -665,14 +664,8 @@ func (b *BitcoindNotifier) handleBlockConnected(block chainntnfs.BlockEpoch) err
 	// satisfy any client requests based upon the new block.
 	b.bestBlock = block
 
-	err = b.txNotifier.NotifyHeight(uint32(block.Height))
-	if err != nil {
-		return fmt.Errorf("unable to notify height: %w", err)
-	}
-
 	b.notifyBlockEpochs(block.Height, block.Hash, block.BlockHeader)
-
-	return nil
+	return b.txNotifier.NotifyHeight(uint32(block.Height))
 }
 
 // notifyBlockEpochs notifies all registered block epoch clients of the newly
diff --git a/chainntnfs/btcdnotify/btcd.go b/chainntnfs/btcdnotify/btcd.go
index ff91b5aee..1bf63f1e6 100644
--- a/chainntnfs/btcdnotify/btcd.go
+++ b/chainntnfs/btcdnotify/btcd.go
@@ -180,7 +180,7 @@ func (b *BtcdNotifier) Stop() error {
 
 	// Shutdown the rpc client, this gracefully disconnects from btcd, and
 	// cleans up all related resources.
-	b.chainConn.Stop()
+	b.chainConn.Shutdown()
 
 	close(b.quit)
 	b.wg.Wait()
@@ -725,16 +725,11 @@ func (b *BtcdNotifier) handleBlockConnected(epoch chainntnfs.BlockEpoch) error {
 	// satisfy any client requests based upon the new block.
 	b.bestBlock = epoch
 
-	err = b.txNotifier.NotifyHeight(uint32(epoch.Height))
-	if err != nil {
-		return fmt.Errorf("unable to notify height: %w", err)
-	}
-
 	b.notifyBlockEpochs(
 		epoch.Height, epoch.Hash, epoch.BlockHeader,
 	)
 
-	return nil
+	return b.txNotifier.NotifyHeight(uint32(epoch.Height))
 }
 
 // notifyBlockEpochs notifies all registered block epoch clients of the newly
diff --git a/chainntnfs/interface.go b/chainntnfs/interface.go
index b2383636a..3337f1451 100644
--- a/chainntnfs/interface.go
+++ b/chainntnfs/interface.go
@@ -258,9 +258,6 @@ type ConfirmationEvent struct {
 // channels.
 func NewConfirmationEvent(numConfs uint32, cancel func()) *ConfirmationEvent {
 	return &ConfirmationEvent{
-		// We cannot rely on the subscriber to immediately read from
-		// the channel so we need to create a larger buffer to avoid
-		// blocking the notifier.
 		Confirmed:    make(chan *TxConfirmation, 1),
 		Updates:      make(chan uint32, numConfs),
 		NegativeConf: make(chan int32, 1),
diff --git a/chainntnfs/neutrinonotify/neutrino.go b/chainntnfs/neutrinonotify/neutrino.go
index 7b93bcd80..55d923577 100644
--- a/chainntnfs/neutrinonotify/neutrino.go
+++ b/chainntnfs/neutrinonotify/neutrino.go
@@ -689,16 +689,10 @@ func (n *NeutrinoNotifier) handleBlockConnected(newBlock *filteredBlock) error {
 	n.bestBlock.Height = int32(newBlock.height)
 	n.bestBlock.BlockHeader = newBlock.header
 
-	err = n.txNotifier.NotifyHeight(newBlock.height)
-	if err != nil {
-		return fmt.Errorf("unable to notify height: %w", err)
-	}
-
 	n.notifyBlockEpochs(
 		int32(newBlock.height), &newBlock.hash, newBlock.header,
 	)
-
-	return nil
+	return n.txNotifier.NotifyHeight(newBlock.height)
 }
 
 // getFilteredBlock is a utility to retrieve the full filtered block from a block epoch.
diff --git a/chainntnfs/txnotifier.go b/chainntnfs/txnotifier.go
index 71b9c929d..be471da3c 100644
--- a/chainntnfs/txnotifier.go
+++ b/chainntnfs/txnotifier.go
@@ -244,25 +244,20 @@ type ConfNtfn struct {
 	// notification is to be sent.
 	NumConfirmations uint32
 
-	// Event contains references to the channels that the notifications are
-	// to be sent over.
+	// Event contains references to the channels that the notifications are to
+	// be sent over.
 	Event *ConfirmationEvent
 
 	// HeightHint is the minimum height in the chain that we expect to find
 	// this txid.
 	HeightHint uint32
 
-	// dispatched is false if the confirmed notification has not been sent
-	// yet.
+	// dispatched is false if the confirmed notification has not been sent yet.
 	dispatched bool
 
 	// includeBlock is true if the dispatched notification should also have
 	// the block included with it.
 	includeBlock bool
-
-	// numConfsLeft is the number of confirmations left to be sent to the
-	// subscriber.
-	numConfsLeft uint32
 }
 
 // HistoricalConfDispatch parametrizes a manual rescan for a particular
@@ -594,7 +589,6 @@ func (n *TxNotifier) newConfNtfn(txid *chainhash.Hash,
 		}),
 		HeightHint:   heightHint,
 		includeBlock: opts.includeBlock,
-		numConfsLeft: numConfs,
 	}, nil
 }
 
@@ -670,8 +664,8 @@ func (n *TxNotifier) RegisterConf(txid *chainhash.Hash, pkScript []byte,
 		// already been found, we'll attempt to deliver them immediately
 		// to this client.
 		Log.Debugf("Attempting to dispatch confirmation for %v on "+
-			"registration since rescan has finished, conf_id=%v",
-			ntfn.ConfRequest, ntfn.ConfID)
+			"registration since rescan has finished",
+			ntfn.ConfRequest)
 
 		// The default notification we assigned above includes the
 		// block along with the rest of the details. However not all
@@ -685,13 +679,9 @@ func (n *TxNotifier) RegisterConf(txid *chainhash.Hash, pkScript []byte,
 			confDetails = &confDetailsCopy
 		}
 
-		// Deliver the details to the whole conf set where this ntfn
-		// lives in.
-		for _, subscriber := range confSet.ntfns {
-			err := n.dispatchConfDetails(subscriber, confDetails)
-			if err != nil {
-				return nil, err
-			}
+		err := n.dispatchConfDetails(ntfn, confDetails)
+		if err != nil {
+			return nil, err
 		}
 
 		return &ConfRegistration{
@@ -922,16 +912,10 @@ func (n *TxNotifier) dispatchConfDetails(
 	// If there are no conf details to dispatch or if the notification has
 	// already been dispatched, then we can skip dispatching to this
 	// client.
-	if details == nil {
-		Log.Debugf("Skipped dispatching nil conf details for request "+
-			"%v, conf_id=%v", ntfn.ConfRequest, ntfn.ConfID)
-
-		return nil
-	}
-
-	if ntfn.dispatched {
-		Log.Debugf("Skipped dispatched conf details for request %v "+
-			"conf_id=%v", ntfn.ConfRequest, ntfn.ConfID)
+	if details == nil || ntfn.dispatched {
+		Log.Debugf("Skipping dispatch of conf details(%v) for "+
+			"request %v, dispatched=%v", details, ntfn.ConfRequest,
+			ntfn.dispatched)
 
 		return nil
 	}
@@ -941,16 +925,16 @@ func (n *TxNotifier) dispatchConfDetails(
 	// we'll dispatch a confirmation notification to the caller.
 	confHeight := details.BlockHeight + ntfn.NumConfirmations - 1
 	if confHeight <= n.currentHeight {
-		Log.Debugf("Dispatching %v confirmation notification for "+
-			"conf_id=%v, %v", ntfn.NumConfirmations, ntfn.ConfID,
-			ntfn.ConfRequest)
+		Log.Debugf("Dispatching %v confirmation notification for %v",
+			ntfn.NumConfirmations, ntfn.ConfRequest)
 
 		// We'll send a 0 value to the Updates channel,
 		// indicating that the transaction/output script has already
 		// been confirmed.
-		err := n.notifyNumConfsLeft(ntfn, 0)
-		if err != nil {
-			return err
+		select {
+		case ntfn.Event.Updates <- 0:
+		case <-n.quit:
+			return ErrTxNotifierExiting
 		}
 
 		select {
@@ -960,8 +944,8 @@ func (n *TxNotifier) dispatchConfDetails(
 			return ErrTxNotifierExiting
 		}
 	} else {
-		Log.Debugf("Queueing %v confirmation notification for %v at "+
-			"tip", ntfn.NumConfirmations, ntfn.ConfRequest)
+		Log.Debugf("Queueing %v confirmation notification for %v at tip ",
+			ntfn.NumConfirmations, ntfn.ConfRequest)
 
 		// Otherwise, we'll keep track of the notification
 		// request by the height at which we should dispatch the
@@ -977,9 +961,10 @@ func (n *TxNotifier) dispatchConfDetails(
 		// confirmations are left for the transaction/output script to
 		// be confirmed.
 		numConfsLeft := confHeight - n.currentHeight
-		err := n.notifyNumConfsLeft(ntfn, numConfsLeft)
-		if err != nil {
-			return err
+		select {
+		case ntfn.Event.Updates <- numConfsLeft:
+		case <-n.quit:
+			return ErrTxNotifierExiting
 		}
 	}
 
@@ -1744,9 +1729,10 @@ func (n *TxNotifier) NotifyHeight(height uint32) error {
 					continue
 				}
 
-				err := n.notifyNumConfsLeft(ntfn, numConfsLeft)
-				if err != nil {
-					return err
+				select {
+				case ntfn.Event.Updates <- numConfsLeft:
+				case <-n.quit:
+					return ErrTxNotifierExiting
 				}
 			}
 		}
@@ -1757,9 +1743,8 @@ func (n *TxNotifier) NotifyHeight(height uint32) error {
 	for ntfn := range n.ntfnsByConfirmHeight[height] {
 		confSet := n.confNotifications[ntfn.ConfRequest]
 
-		Log.Debugf("Dispatching %v confirmation notification for "+
-			"conf_id=%v, %v", ntfn.NumConfirmations, ntfn.ConfID,
-			ntfn.ConfRequest)
+		Log.Debugf("Dispatching %v confirmation notification for %v",
+			ntfn.NumConfirmations, ntfn.ConfRequest)
 
 		// The default notification we assigned above includes the
 		// block along with the rest of the details. However not all
@@ -1848,9 +1833,6 @@ func (n *TxNotifier) DisconnectTip(blockHeight uint32) error {
 				default:
 				}
 
-				// We also reset the num of confs update.
-				ntfn.numConfsLeft = ntfn.NumConfirmations
-
 				// Then, we'll check if the current
 				// transaction/output script was included in the
 				// block currently being disconnected. If it
@@ -2087,30 +2069,3 @@ func (n *TxNotifier) TearDown() {
 		}
 	}
 }
-
-// notifyNumConfsLeft sends the number of confirmations left to the
-// notification subscriber through the Event.Updates channel.
-//
-// NOTE: must be used with the TxNotifier's lock held.
-func (n *TxNotifier) notifyNumConfsLeft(ntfn *ConfNtfn, num uint32) error {
-	// If the number left is no less than the recorded value, we can skip
-	// sending it as it means this same value has already been sent before.
-	if num >= ntfn.numConfsLeft {
-		Log.Debugf("Skipped dispatched update (numConfsLeft=%v) for "+
-			"request %v conf_id=%v", num, ntfn.ConfRequest,
-			ntfn.ConfID)
-
-		return nil
-	}
-
-	// Update the number of confirmations left to the notification.
-	ntfn.numConfsLeft = num
-
-	select {
-	case ntfn.Event.Updates <- num:
-	case <-n.quit:
-		return ErrTxNotifierExiting
-	}
-
-	return nil
-}
diff --git a/chanacceptor/rpcacceptor.go b/chanacceptor/rpcacceptor.go
index 6ec787895..779ba6f28 100644
--- a/chanacceptor/rpcacceptor.go
+++ b/chanacceptor/rpcacceptor.go
@@ -356,30 +356,6 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
 				):
 					commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT
 
-				case channelFeatures.OnlyContains(
-					lnwire.SimpleTaprootOverlayChansRequired,
-					lnwire.ZeroConfRequired,
-					lnwire.ScidAliasRequired,
-				):
-					commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
-
-				case channelFeatures.OnlyContains(
-					lnwire.SimpleTaprootOverlayChansRequired,
-					lnwire.ZeroConfRequired,
-				):
-					commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
-
-				case channelFeatures.OnlyContains(
-					lnwire.SimpleTaprootOverlayChansRequired,
-					lnwire.ScidAliasRequired,
-				):
-					commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
-
-				case channelFeatures.OnlyContains(
-					lnwire.SimpleTaprootOverlayChansRequired,
-				):
-					commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
-
 				case channelFeatures.OnlyContains(
 					lnwire.StaticRemoteKeyRequired,
 				):
diff --git a/channeldb/graph.go b/channeldb/graph.go
index 86cbe9aa9..d7a4480d0 100644
--- a/channeldb/graph.go
+++ b/channeldb/graph.go
@@ -1023,6 +1023,10 @@ func (c *ChannelGraph) AddChannelEdge(edge *models.ChannelEdgeInfo,
 	}
 
 	for _, f := range op {
+		if f == nil {
+			return fmt.Errorf("nil scheduler option was used")
+		}
+
 		f(r)
 	}
 
diff --git a/channeldb/peers.go b/channeldb/peers.go
index ed36c51df..62342fc79 100644
--- a/channeldb/peers.go
+++ b/channeldb/peers.go
@@ -50,12 +50,6 @@ type FlapCount struct {
 // bucket for the peer's pubkey if necessary. Note that this function overwrites
 // the current value.
 func (d *DB) WriteFlapCounts(flapCounts map[route.Vertex]*FlapCount) error {
-	// Exit early if there are no updates.
-	if len(flapCounts) == 0 {
-		log.Debugf("No flap counts to write, skipped db update")
-		return nil
-	}
-
 	return kvdb.Update(d, func(tx kvdb.RwTx) error {
 		// Run through our set of flap counts and record them for
 		// each peer, creating a bucket for the peer pubkey if required.
diff --git a/cmd/commands/commands.go b/cmd/commands/commands.go
index de13022f0..ff20701e6 100644
--- a/cmd/commands/commands.go
+++ b/cmd/commands/commands.go
@@ -2359,6 +2359,21 @@ var updateChannelPolicyCommand = cli.Command{
 				"channels will be updated. Takes the form of " +
 				"txid:output_index",
 		},
+		cli.BoolFlag{
+			Name: "create_missing_edge",
+			Usage: "Under unknown circumstances a channel can " +
+				"exist with a missing edge in the graph " +
+				"database. This can cause an 'edge not " +
+				"found' error when calling `getchaninfo` " +
+				"and/or cause the default channel policy to " +
+				"be used during forwards. Setting this flag " +
+				"will recreate the edge if not found, " +
+				"allowing updating this channel policy and " +
+				"fixing the missing edge problem for this " +
+				"channel permanently. For fields not set in " +
+				"this command, the default policy will be " +
+				"created.",
+		},
 	},
 	Action: actionDecorator(updateChannelPolicy),
 }
@@ -2518,11 +2533,14 @@ func updateChannelPolicy(ctx *cli.Context) error {
 		}
 	}
 
+	createMissingEdge := ctx.Bool("create_missing_edge")
+
 	req := &lnrpc.PolicyUpdateRequest{
-		BaseFeeMsat:   baseFee,
-		TimeLockDelta: uint32(timeLockDelta),
-		MaxHtlcMsat:   ctx.Uint64("max_htlc_msat"),
-		InboundFee:    inboundFee,
+		BaseFeeMsat:       baseFee,
+		TimeLockDelta:     uint32(timeLockDelta),
+		MaxHtlcMsat:       ctx.Uint64("max_htlc_msat"),
+		InboundFee:        inboundFee,
+		CreateMissingEdge: createMissingEdge,
 	}
 
 	if ctx.IsSet("min_htlc_msat") {
diff --git a/config.go b/config.go
index 04a491765..9b9e9573e 100644
--- a/config.go
+++ b/config.go
@@ -562,6 +562,8 @@ func DefaultConfig() Config {
 		LetsEncryptDir:    defaultLetsEncryptDir,
 		LetsEncryptListen: defaultLetsEncryptListen,
 		LogDir:            defaultLogDir,
+		MaxLogFiles:       build.DefaultMaxLogFiles,
+		MaxLogFileSize:    build.DefaultMaxLogFileSize,
 		AcceptorTimeout:   defaultAcceptorTimeout,
 		WSPingInterval:    lnrpc.DefaultPingInterval,
 		WSPongWait:        lnrpc.DefaultPongWait,
@@ -860,18 +862,6 @@ func LoadConfig(interceptor signal.Interceptor) (*Config, error) {
 func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser,
 	flagParser *flags.Parser) (*Config, error) {
 
-	// Special show command to list supported subsystems and exit.
-	if cfg.DebugLevel == "show" {
-		subLogMgr := build.NewSubLoggerManager()
-
-		// Initialize logging at the default logging level.
-		SetupLoggers(subLogMgr, interceptor)
-
-		fmt.Println("Supported subsystems",
-			subLogMgr.SupportedSubsystems())
-		os.Exit(0)
-	}
-
 	// If the provided lnd directory is not the default, we'll modify the
 	// path to all of the files and directories that will live within it.
 	lndDir := CleanAndExpandPath(cfg.LndDir)
@@ -1261,7 +1251,7 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser,
 	// The target network must be provided, otherwise, we won't
 	// know how to initialize the daemon.
 	if numNets == 0 {
-		str := "either --bitcoin.mainnet, or bitcoin.testnet, " +
+		str := "either --bitcoin.mainnet, or bitcoin.testnet," +
 			"bitcoin.simnet, bitcoin.regtest or bitcoin.signet " +
 			"must be specified"
 
@@ -1420,7 +1410,14 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser,
 	// Initialize logging at the default logging level.
 	SetupLoggers(cfg.SubLogMgr, interceptor)
 
-	if cfg.MaxLogFiles != 0 {
+	// Special show command to list supported subsystems and exit.
+	if cfg.DebugLevel == "show" {
+		fmt.Println("Supported subsystems",
+			cfg.SubLogMgr.SupportedSubsystems())
+		os.Exit(0)
+	}
+
+	if cfg.MaxLogFiles != build.DefaultMaxLogFiles {
 		if cfg.LogConfig.File.MaxLogFiles !=
 			build.DefaultMaxLogFiles {
 
@@ -1430,7 +1427,7 @@ func ValidateConfig(cfg Config, interceptor signal.Interceptor, fileParser,
 
 		cfg.LogConfig.File.MaxLogFiles = cfg.MaxLogFiles
 	}
-	if cfg.MaxLogFileSize != 0 {
+	if cfg.MaxLogFileSize != build.DefaultMaxLogFileSize {
 		if cfg.LogConfig.File.MaxLogFileSize !=
 			build.DefaultMaxLogFileSize {
 
diff --git a/contractcourt/breach_arbitrator.go b/contractcourt/breach_arbitrator.go
index a8154d0e6..dc690e85c 100644
--- a/contractcourt/breach_arbitrator.go
+++ b/contractcourt/breach_arbitrator.go
@@ -752,7 +752,7 @@ justiceTxBroadcast:
 		}
 
 		return aux.NotifyBroadcast(
-			&bumpReq, finalTx.justiceTx, finalTx.fee, nil,
+			&bumpReq, finalTx.justiceTx, finalTx.fee,
 		)
 	})
 	if err != nil {
@@ -1160,11 +1160,6 @@ func (bo *breachedOutput) SignDesc() *input.SignDescriptor {
 	return &bo.signDesc
 }
 
-// Preimage returns the preimage that was used to create the breached output.
-func (bo *breachedOutput) Preimage() fn.Option[lntypes.Preimage] {
-	return fn.None[lntypes.Preimage]()
-}
-
 // CraftInputScript computes a valid witness that allows us to spend from the
 // breached output. It does so by first generating and memoizing the witness
 // generation function, which parameterized primarily by the witness type and
diff --git a/contractcourt/briefcase.go b/contractcourt/briefcase.go
index cd4fd90cc..acc49be44 100644
--- a/contractcourt/briefcase.go
+++ b/contractcourt/briefcase.go
@@ -1571,7 +1571,6 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
 		})
 	}
 
-	htlcBlobs := newAuxHtlcBlobs()
 	for _, htlc := range c.HtlcResolutions.IncomingHTLCs {
 		htlc := htlc
 
@@ -1582,9 +1581,8 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
 			continue
 		}
 
-		var resID resolverID
 		if htlc.SignedSuccessTx != nil {
-			resID = newResolverID(
+			resID := newResolverID(
 				htlc.SignedSuccessTx.TxIn[0].PreviousOutPoint,
 			)
 			//nolint:lll
@@ -1600,14 +1598,10 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
 				tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
 			}
 		} else {
-			resID = newResolverID(htlc.ClaimOutpoint)
+			resID := newResolverID(htlc.ClaimOutpoint)
 			//nolint:lll
 			tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID] = ctrlBlock
 		}
-
-		htlc.ResolutionBlob.WhenSome(func(b []byte) {
-			htlcBlobs[resID] = b
-		})
 	}
 	for _, htlc := range c.HtlcResolutions.OutgoingHTLCs {
 		htlc := htlc
@@ -1619,9 +1613,8 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
 			continue
 		}
 
-		var resID resolverID
 		if htlc.SignedTimeoutTx != nil {
-			resID = newResolverID(
+			resID := newResolverID(
 				htlc.SignedTimeoutTx.TxIn[0].PreviousOutPoint,
 			)
 			//nolint:lll
@@ -1639,14 +1632,10 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
 				tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
 			}
 		} else {
-			resID = newResolverID(htlc.ClaimOutpoint)
+			resID := newResolverID(htlc.ClaimOutpoint)
 			//nolint:lll
 			tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID] = ctrlBlock
 		}
-
-		htlc.ResolutionBlob.WhenSome(func(b []byte) {
-			htlcBlobs[resID] = b
-		})
 	}
 
 	if c.AnchorResolution != nil {
@@ -1654,12 +1643,6 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
 		tapCase.TapTweaks.Val.AnchorTweak = anchorSignDesc.TapTweak
 	}
 
-	if len(htlcBlobs) != 0 {
-		tapCase.HtlcBlobs = tlv.SomeRecordT(
-			tlv.NewRecordT[tlv.TlvType4](htlcBlobs),
-		)
-	}
-
 	return tapCase.Encode(w)
 }
 
@@ -1678,8 +1661,6 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
 		})
 	}
 
-	htlcBlobs := tapCase.HtlcBlobs.ValOpt().UnwrapOr(newAuxHtlcBlobs())
-
 	for i := range c.HtlcResolutions.IncomingHTLCs {
 		htlc := c.HtlcResolutions.IncomingHTLCs[i]
 
@@ -1706,12 +1687,7 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
 			htlc.SweepSignDesc.ControlBlock = ctrlBlock
 		}
 
-		if htlcBlob, ok := htlcBlobs[resID]; ok {
-			htlc.ResolutionBlob = fn.Some(htlcBlob)
-		}
-
 		c.HtlcResolutions.IncomingHTLCs[i] = htlc
-
 	}
 	for i := range c.HtlcResolutions.OutgoingHTLCs {
 		htlc := c.HtlcResolutions.OutgoingHTLCs[i]
@@ -1739,10 +1715,6 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
 			htlc.SweepSignDesc.ControlBlock = ctrlBlock
 		}
 
-		if htlcBlob, ok := htlcBlobs[resID]; ok {
-			htlc.ResolutionBlob = fn.Some(htlcBlob)
-		}
-
 		c.HtlcResolutions.OutgoingHTLCs[i] = htlc
 	}
 
diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go
index d7d10ba25..c29178b43 100644
--- a/contractcourt/chain_arbitrator.go
+++ b/contractcourt/chain_arbitrator.go
@@ -335,10 +335,11 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
 // ForceCloseChan should force close the contract that this attendant is
 // watching over. We'll use this when we decide that we need to go to chain. It
 // should in addition tell the switch to remove the corresponding link, such
-// that we won't accept any new updates.
+// that we won't accept any new updates. The returned summary contains all items
+// needed to eventually resolve all outputs on chain.
 //
 // NOTE: Part of the ArbChannel interface.
-func (a *arbChannel) ForceCloseChan() (*wire.MsgTx, error) {
+func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) {
 	// First, we mark the channel as borked, this ensure
 	// that no new state transitions can happen, and also
 	// that the link won't be loaded into the switch.
@@ -385,15 +386,7 @@ func (a *arbChannel) ForceCloseChan() (*wire.MsgTx, error) {
 	if err != nil {
 		return nil, err
 	}
-
-	closeSummary, err := chanMachine.ForceClose(
-		lnwallet.WithSkipContractResolutions(),
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	return closeSummary.CloseTx, nil
+	return chanMachine.ForceClose()
 }
 
 // newActiveChannelArbitrator creates a new instance of an active channel
diff --git a/contractcourt/chain_watcher.go b/contractcourt/chain_watcher.go
index 808f41f2e..f25ca7e0a 100644
--- a/contractcourt/chain_watcher.go
+++ b/contractcourt/chain_watcher.go
@@ -436,7 +436,6 @@ func (c *chainWatcher) handleUnknownLocalState(
 			return s.FetchLeavesFromCommit(
 				lnwallet.NewAuxChanState(c.cfg.chanState),
 				c.cfg.chanState.LocalCommitment, *commitKeyRing,
-				lntypes.Local,
 			)
 		},
 	).Unpack()
@@ -1175,29 +1174,16 @@ func (c *chainWatcher) dispatchLocalForceClose(
 		LocalChanConfig:         c.cfg.chanState.LocalChanCfg,
 	}
 
-	resolutions, err := forceClose.ContractResolutions.UnwrapOrErr(
-		fmt.Errorf("resolutions not found"),
-	)
-	if err != nil {
-		return err
-	}
-
 	// If our commitment output isn't dust or we have active HTLC's on the
 	// commitment transaction, then we'll populate the balances on the
 	// close channel summary.
-	if resolutions.CommitResolution != nil {
-		localBalance := chanSnapshot.LocalBalance.ToSatoshis()
-		closeSummary.SettledBalance = localBalance
-		closeSummary.TimeLockedBalance = localBalance
+	if forceClose.CommitResolution != nil {
+		closeSummary.SettledBalance = chanSnapshot.LocalBalance.ToSatoshis()
+		closeSummary.TimeLockedBalance = chanSnapshot.LocalBalance.ToSatoshis()
 	}
-
-	if resolutions.HtlcResolutions != nil {
-		for _, htlc := range resolutions.HtlcResolutions.OutgoingHTLCs {
-			htlcValue := btcutil.Amount(
-				htlc.SweepSignDesc.Output.Value,
-			)
-			closeSummary.TimeLockedBalance += htlcValue
-		}
+	for _, htlc := range forceClose.HtlcResolutions.OutgoingHTLCs {
+		htlcValue := btcutil.Amount(htlc.SweepSignDesc.Output.Value)
+		closeSummary.TimeLockedBalance += htlcValue
 	}
 
 	// Attempt to add a channel sync message to the close summary.
diff --git a/contractcourt/channel_arbitrator.go b/contractcourt/channel_arbitrator.go
index ffa4a5d6e..cc1ee6958 100644
--- a/contractcourt/channel_arbitrator.go
+++ b/contractcourt/channel_arbitrator.go
@@ -98,7 +98,7 @@ type ArbChannel interface {
 	// corresponding link, such that we won't accept any new updates. The
 	// returned summary contains all items needed to eventually resolve all
 	// outputs on chain.
-	ForceCloseChan() (*wire.MsgTx, error)
+	ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error)
 
 	// NewAnchorResolutions returns the anchor resolutions for currently
 	// valid commitment transactions.
@@ -1098,7 +1098,7 @@ func (c *ChannelArbitrator) stateStep(
 		// We'll tell the switch that it should remove the link for
 		// this channel, in addition to fetching the force close
 		// summary needed to close this channel on chain.
-		forceCloseTx, err := c.cfg.Channel.ForceCloseChan()
+		closeSummary, err := c.cfg.Channel.ForceCloseChan()
 		if err != nil {
 			log.Errorf("ChannelArbitrator(%v): unable to "+
 				"force close: %v", c.cfg.ChanPoint, err)
@@ -1118,7 +1118,7 @@ func (c *ChannelArbitrator) stateStep(
 
 			return StateError, closeTx, err
 		}
-		closeTx = forceCloseTx
+		closeTx = closeSummary.CloseTx
 
 		// Before publishing the transaction, we store it to the
 		// database, such that we can re-publish later in case it
@@ -2812,36 +2812,11 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) {
 			}
 			closeTx := closeInfo.CloseTx
 
-			resolutions, err := closeInfo.ContractResolutions.
-				UnwrapOrErr(
-					fmt.Errorf("resolutions not found"),
-				)
-			if err != nil {
-				log.Errorf("ChannelArbitrator(%v): unable to "+
-					"get resolutions: %v", c.cfg.ChanPoint,
-					err)
-
-				return
-			}
-
-			// We make sure that the htlc resolutions are present
-			// otherwise we would panic dereferencing the pointer.
-			//
-			// TODO(ziggie): Refactor ContractResolutions to use
-			// options.
-			if resolutions.HtlcResolutions == nil {
-				log.Errorf("ChannelArbitrator(%v): htlc "+
-					"resolutions not found",
-					c.cfg.ChanPoint)
-
-				return
-			}
-
 			contractRes := &ContractResolutions{
 				CommitHash:       closeTx.TxHash(),
-				CommitResolution: resolutions.CommitResolution,
-				HtlcResolutions:  *resolutions.HtlcResolutions,
-				AnchorResolution: resolutions.AnchorResolution,
+				CommitResolution: closeInfo.CommitResolution,
+				HtlcResolutions:  *closeInfo.HtlcResolutions,
+				AnchorResolution: closeInfo.AnchorResolution,
 			}
 
 			// When processing a unilateral close event, we'll
@@ -2850,7 +2825,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) {
 			// available to fetch in that state, we'll also write
 			// the commit set so we can reconstruct our chain
 			// actions on restart.
-			err = c.log.LogContractResolutions(contractRes)
+			err := c.log.LogContractResolutions(contractRes)
 			if err != nil {
 				log.Errorf("Unable to write resolutions: %v",
 					err)
diff --git a/contractcourt/htlc_lease_resolver.go b/contractcourt/htlc_lease_resolver.go
index 53fa89355..87e55d5cc 100644
--- a/contractcourt/htlc_lease_resolver.go
+++ b/contractcourt/htlc_lease_resolver.go
@@ -6,9 +6,7 @@ import (
 	"github.com/btcsuite/btcd/wire"
 	"github.com/lightningnetwork/lnd/chainntnfs"
 	"github.com/lightningnetwork/lnd/channeldb"
-	"github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/input"
-	"github.com/lightningnetwork/lnd/tlv"
 )
 
 // htlcLeaseResolver is a struct that houses the lease specific HTLC resolution
@@ -54,8 +52,8 @@ func (h *htlcLeaseResolver) deriveWaitHeight(csvDelay uint32,
 // send to the sweeper so the output can ultimately be swept.
 func (h *htlcLeaseResolver) makeSweepInput(op *wire.OutPoint,
 	wType, cltvWtype input.StandardWitnessType,
-	signDesc *input.SignDescriptor, csvDelay, broadcastHeight uint32,
-	payHash [32]byte, resBlob fn.Option[tlv.Blob]) *input.BaseInput {
+	signDesc *input.SignDescriptor,
+	csvDelay, broadcastHeight uint32, payHash [32]byte) *input.BaseInput {
 
 	if h.hasCLTV() {
 		log.Infof("%T(%x): CSV and CLTV locks expired, offering "+
@@ -65,17 +63,13 @@ func (h *htlcLeaseResolver) makeSweepInput(op *wire.OutPoint,
 			op, cltvWtype, signDesc,
 			broadcastHeight, csvDelay,
 			h.leaseExpiry,
-			input.WithResolutionBlob(resBlob),
 		)
 	}
 
 	log.Infof("%T(%x): CSV lock expired, offering second-layer output to "+
 		"sweeper: %v", h, payHash, op)
 
-	return input.NewCsvInput(
-		op, wType, signDesc, broadcastHeight, csvDelay,
-		input.WithResolutionBlob(resBlob),
-	)
+	return input.NewCsvInput(op, wType, signDesc, broadcastHeight, csvDelay)
 }
 
 // SupplementState allows the user of a ContractResolver to supplement it with
diff --git a/contractcourt/htlc_success_resolver.go b/contractcourt/htlc_success_resolver.go
index 4c9d2b200..3b07828d4 100644
--- a/contractcourt/htlc_success_resolver.go
+++ b/contractcourt/htlc_success_resolver.go
@@ -123,6 +123,17 @@ func (h *htlcSuccessResolver) Resolve(
 		return nil, nil
 	}
 
+	// If the HTLC has custom records, then for now we'll pause resolution.
+	//
+	// TODO(roasbeef): Implement resolving HTLCs with custom records
+	// (follow-up PR).
+	if len(h.htlc.CustomRecords) != 0 {
+		select { //nolint:gosimple
+		case <-h.quit:
+			return nil, errResolverShuttingDown
+		}
+	}
+
 	// If we don't have a success transaction, then this means that this is
 	// an output on the remote party's commitment transaction.
 	if h.htlcResolution.SignedSuccessTx == nil {
@@ -247,9 +258,6 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) (
 				h.htlcResolution.SignedSuccessTx,
 				h.htlcResolution.SignDetails, h.htlcResolution.Preimage,
 				h.broadcastHeight,
-				input.WithResolutionBlob(
-					h.htlcResolution.ResolutionBlob,
-				),
 			)
 		} else {
 			//nolint:lll
@@ -406,7 +414,7 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) (
 		input.LeaseHtlcAcceptedSuccessSecondLevel,
 		&h.htlcResolution.SweepSignDesc,
 		h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight),
-		h.htlc.RHash, h.htlcResolution.ResolutionBlob,
+		h.htlc.RHash,
 	)
 
 	// Calculate the budget for this sweep.
@@ -462,9 +470,6 @@ func (h *htlcSuccessResolver) resolveRemoteCommitOutput(immediate bool) (
 			h.htlcResolution.Preimage[:],
 			h.broadcastHeight,
 			h.htlcResolution.CsvDelay,
-			input.WithResolutionBlob(
-				h.htlcResolution.ResolutionBlob,
-			),
 		))
 	} else {
 		inp = lnutils.Ptr(input.MakeHtlcSucceedInput(
diff --git a/contractcourt/htlc_timeout_resolver.go b/contractcourt/htlc_timeout_resolver.go
index e7ab42169..1c5620fc6 100644
--- a/contractcourt/htlc_timeout_resolver.go
+++ b/contractcourt/htlc_timeout_resolver.go
@@ -426,6 +426,17 @@ func (h *htlcTimeoutResolver) Resolve(
 		return nil, nil
 	}
 
+	// If the HTLC has custom records, then for now we'll pause resolution.
+	//
+	// TODO(roasbeef): Implement resolving HTLCs with custom records
+	// (follow-up PR).
+	if len(h.htlc.CustomRecords) != 0 {
+		select { //nolint:gosimple
+		case <-h.quit:
+			return nil, errResolverShuttingDown
+		}
+	}
+
 	// Start by spending the HTLC output, either by broadcasting the
 	// second-level timeout transaction, or directly if this is the remote
 	// commitment.
@@ -488,9 +499,6 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error {
 			h.htlcResolution.SignedTimeoutTx,
 			h.htlcResolution.SignDetails,
 			h.broadcastHeight,
-			input.WithResolutionBlob(
-				h.htlcResolution.ResolutionBlob,
-			),
 		))
 	} else {
 		inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutAnchorInput(
@@ -584,7 +592,6 @@ func (h *htlcTimeoutResolver) sweepDirectHtlcOutput(immediate bool) error {
 		&h.htlcResolution.ClaimOutpoint, htlcWitnessType,
 		&h.htlcResolution.SweepSignDesc, h.broadcastHeight,
 		h.htlcResolution.CsvDelay, h.htlcResolution.Expiry,
-		input.WithResolutionBlob(h.htlcResolution.ResolutionBlob),
 	)
 
 	// Calculate the budget.
@@ -839,7 +846,6 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
 			&h.htlcResolution.SweepSignDesc,
 			h.htlcResolution.CsvDelay,
 			uint32(commitSpend.SpendingHeight), h.htlc.RHash,
-			h.htlcResolution.ResolutionBlob,
 		)
 
 		// Calculate the budget for this sweep.
diff --git a/contractcourt/taproot_briefcase.go b/contractcourt/taproot_briefcase.go
index 4b703dd29..0a2beeff7 100644
--- a/contractcourt/taproot_briefcase.go
+++ b/contractcourt/taproot_briefcase.go
@@ -39,9 +39,7 @@ type taprootBriefcase struct {
 	// used to sweep a remote party's breached output.
 	BreachedCommitBlob tlv.OptionalRecordT[tlv.TlvType3, tlv.Blob]
 
-	// HtlcBlobs is an optikonal record that contains the opaque blobs for
-	// the set of active HTLCs on the commitment transaction.
-	HtlcBlobs tlv.OptionalRecordT[tlv.TlvType4, htlcAuxBlobs]
+	// TODO(roasbeef): htlc blobs
 }
 
 // TODO(roasbeef): morph into new tlv record
@@ -72,9 +70,6 @@ func (t *taprootBriefcase) EncodeRecords() []tlv.Record {
 			records = append(records, r.Record())
 		},
 	)
-	t.HtlcBlobs.WhenSome(func(r tlv.RecordT[tlv.TlvType4, htlcAuxBlobs]) {
-		records = append(records, r.Record())
-	})
 
 	return records
 }
@@ -101,11 +96,10 @@ func (t *taprootBriefcase) Encode(w io.Writer) error {
 func (t *taprootBriefcase) Decode(r io.Reader) error {
 	settledCommitBlob := t.SettledCommitBlob.Zero()
 	breachedCommitBlob := t.BreachedCommitBlob.Zero()
-	htlcBlobs := t.HtlcBlobs.Zero()
-
 	records := append(
-		t.DecodeRecords(), settledCommitBlob.Record(),
-		breachedCommitBlob.Record(), htlcBlobs.Record(),
+		t.DecodeRecords(),
+		settledCommitBlob.Record(),
+		breachedCommitBlob.Record(),
 	)
 	stream, err := tlv.NewStream(records...)
 	if err != nil {
@@ -123,9 +117,6 @@ func (t *taprootBriefcase) Decode(r io.Reader) error {
 	if v, ok := typeMap[t.BreachedCommitBlob.TlvType()]; ok && v == nil {
 		t.BreachedCommitBlob = tlv.SomeRecordT(breachedCommitBlob)
 	}
-	if v, ok := typeMap[t.HtlcBlobs.TlvType()]; ok && v == nil {
-		t.HtlcBlobs = tlv.SomeRecordT(htlcBlobs)
-	}
 
 	return nil
 }
@@ -695,110 +686,3 @@ func (t *tapTweaks) Decode(r io.Reader) error {
 
 	return stream.Decode(r)
 }
-
-// htlcAuxBlobs is a map of resolver IDs to their corresponding HTLC blobs.
-// This is used to store the resolution blobs for HTLCs that are not yet
-// resolved.
-type htlcAuxBlobs map[resolverID]tlv.Blob
-
-// newAuxHtlcBlobs returns a new instance of the htlcAuxBlobs struct.
-func newAuxHtlcBlobs() htlcAuxBlobs {
-	return make(htlcAuxBlobs)
-}
-
-// Encode encodes the set of HTLC blobs into the target writer.
-func (h *htlcAuxBlobs) Encode(w io.Writer) error {
-	var buf [8]byte
-
-	numBlobs := uint64(len(*h))
-	if err := tlv.WriteVarInt(w, numBlobs, &buf); err != nil {
-		return err
-	}
-
-	for id, blob := range *h {
-		if _, err := w.Write(id[:]); err != nil {
-			return err
-		}
-
-		if err := varBytesEncoder(w, &blob, &buf); err != nil {
-			return err
-		}
-	}
-
-	return nil
-}
-
-// Decode decodes the set of HTLC blobs from the target reader.
-func (h *htlcAuxBlobs) Decode(r io.Reader) error {
-	var buf [8]byte
-
-	numBlobs, err := tlv.ReadVarInt(r, &buf)
-	if err != nil {
-		return err
-	}
-
-	for i := uint64(0); i < numBlobs; i++ {
-		var id resolverID
-		if _, err := io.ReadFull(r, id[:]); err != nil {
-			return err
-		}
-
-		var blob tlv.Blob
-		if err := varBytesDecoder(r, &blob, &buf, 0); err != nil {
-			return err
-		}
-
-		(*h)[id] = blob
-	}
-
-	return nil
-}
-
-// eHtlcAuxBlobsEncoder is a custom TLV encoder for the htlcAuxBlobs struct.
-func htlcAuxBlobsEncoder(w io.Writer, val any, _ *[8]byte) error {
-	if t, ok := val.(*htlcAuxBlobs); ok {
-		return (*t).Encode(w)
-	}
-
-	return tlv.NewTypeForEncodingErr(val, "htlcAuxBlobs")
-}
-
-// dHtlcAuxBlobsDecoder is a custom TLV decoder for the htlcAuxBlobs struct.
-func htlcAuxBlobsDecoder(r io.Reader, val any, _ *[8]byte,
-	l uint64) error {
-
-	if typ, ok := val.(*htlcAuxBlobs); ok {
-		blobReader := io.LimitReader(r, int64(l))
-
-		htlcBlobs := newAuxHtlcBlobs()
-		err := htlcBlobs.Decode(blobReader)
-		if err != nil {
-			return err
-		}
-
-		*typ = htlcBlobs
-
-		return nil
-	}
-
-	return tlv.NewTypeForDecodingErr(val, "htlcAuxBlobs", l, l)
-}
-
-// Record returns a tlv.Record for the htlcAuxBlobs struct.
-func (h *htlcAuxBlobs) Record() tlv.Record {
-	recordSize := func() uint64 {
-		var (
-			b   bytes.Buffer
-			buf [8]byte
-		)
-		if err := htlcAuxBlobsEncoder(&b, h, &buf); err != nil {
-			panic(err)
-		}
-
-		return uint64(len(b.Bytes()))
-	}
-
-	return tlv.MakeDynamicRecord(
-		0, h, recordSize, htlcAuxBlobsEncoder, htlcAuxBlobsDecoder,
-	)
-}
diff --git a/docs/grpc/ruby.md b/docs/grpc/ruby.md
index 0fa200a93..f56e8bf91 100644
--- a/docs/grpc/ruby.md
+++ b/docs/grpc/ruby.md
@@ -63,7 +63,7 @@ require 'grpc'
 require 'lightning_services_pb'
 
 # Due to updated ECDSA generated tls.cert we need to let gprc know that
-# we need to use that cipher suite otherwise there will be a handshake
+# we need to use that cipher suite otherwise there will be a handhsake
 # error when we communicate with the lnd rpc server.
 ENV['GRPC_SSL_CIPHER_SUITES'] = "HIGH+ECDSA"
 
diff --git a/docs/release-notes/release-notes-0.18.4.md b/docs/release-notes/release-notes-0.18.4.md
index 62019bf85..a4b0ed859 100644
--- a/docs/release-notes/release-notes-0.18.4.md
+++ b/docs/release-notes/release-notes-0.18.4.md
@@ -18,24 +18,11 @@
     - [Tooling and Documentation](#tooling-and-documentation)
 
 # Bug Fixes
-
-* [Fix a bug](https://github.com/lightningnetwork/lnd/pull/9134) that would
-  cause a nil pointer dereference during the probing of a payment request that
-  does not contain a payment address.
-
 # New Features
 
 The main channel state machine and database now allow for processing and storing
-custom Taproot script leaves, allowing the implementation of custom channel
-types in a series of changes:
-  * https://github.com/lightningnetwork/lnd/pull/9025
-  * https://github.com/lightningnetwork/lnd/pull/9030
-  * https://github.com/lightningnetwork/lnd/pull/9049
-  * https://github.com/lightningnetwork/lnd/pull/9072
-  * https://github.com/lightningnetwork/lnd/pull/9095
-  * https://github.com/lightningnetwork/lnd/pull/8960
-  * https://github.com/lightningnetwork/lnd/pull/9194
-  * https://github.com/lightningnetwork/lnd/pull/9288
+custom Taproot script leaves, [allowing the implementation of custom channel
+types](https://github.com/lightningnetwork/lnd/pull/8960).
 
 ## Functional Enhancements
 
@@ -84,6 +71,10 @@ types in a series of changes:
 
 ## lncli Additions
 
+* [`updatechanpolicy`](https://github.com/lightningnetwork/lnd/pull/8805) will
+  now update the channel policy if the edge was not found in the graph
+  database if the `create_missing_edge` flag is set.
+
 # Improvements
 ## Functional Updates
 ## RPC Updates
@@ -95,10 +86,6 @@ types in a series of changes:
 ## Breaking Changes
 ## Performance Improvements
 
-* [A new method](https://github.com/lightningnetwork/lnd/pull/9195)
-  `AssertTxnsNotInMempool` has been added to `lntest` package to allow batch
-  exclusion check in itest.
-
 # Technical and Architectural Updates
 ## BOLT Spec Updates
 
diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md
index 12049b40c..ab9ec1253 100644
--- a/docs/release-notes/release-notes-0.19.0.md
+++ b/docs/release-notes/release-notes-0.19.0.md
@@ -22,16 +22,16 @@
 * [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/8857) to correctly 
   propagate mission control and debug level config values to the main LND config
   struct so that the GetDebugInfo response is accurate.
+
+* [Fix a bug](https://github.com/lightningnetwork/lnd/pull/9134) that would 
+  cause a nil pointer dereference during the probing of a payment request that 
+  does not contain a payment address.
   
 * [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9033) where we
-  would not signal an error when trying to bump a non-anchor channel but
+  would not signal an error when trying to bump an non-anchor channel but
   instead report a successful cpfp registration although no fee bumping is
   possible for non-anchor channels anyways.
 
-* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9269) where a
-  negative fee limit for `SendPaymentV2` would lead to omitting the fee limit
-  check.
-
 * [Use the required route blinding 
   feature-bit](https://github.com/lightningnetwork/lnd/pull/9143) for invoices 
   containing blinded paths.
@@ -48,18 +48,6 @@
 * [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9249) found in the
   mission control store that can block the shutdown process of LND.
 
-* Make sure the RPC clients used to access the chain backend are [properly
-  shutdown](https://github.com/lightningnetwork/lnd/pull/9261).
-
-* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9275) where the
-  peer may block the shutdown process of lnd.
-
-* [Fixed a case](https://github.com/lightningnetwork/lnd/pull/9258) where the
-  confirmation notification may be missed.
-  
-* [Make the contract resolutions for the channel arbitrator optional](
-  https://github.com/lightningnetwork/lnd/pull/9253)
-
 # New Features
 ## Functional Enhancements
 ## RPC Additions
@@ -166,7 +154,11 @@ The underlying functionality between those two options remain the same.
 ## Breaking Changes
 ## Performance Improvements
 
-* Log rotation can now use ZSTD
+* Log rotation can now use ZSTD 
+
+* [A new method](https://github.com/lightningnetwork/lnd/pull/9195)
+  `AssertTxnsNotInMempool` has been added to `lntest` package to allow batch
+  exclusion check in itest.
 
 # Technical and Architectural Updates
 ## BOLT Spec Updates
@@ -201,9 +193,7 @@ The underlying functionality between those two options remain the same.
 * CharlieZKSmith
 * Elle Mouton
 * George Tsagkarelis
-* hieblmi
 * Oliver Gugger
 * Pins
 * Viktor Tigerström
-* Yong Yu
-* Ziggie
+* Ziggie
\ No newline at end of file
diff --git a/fn/go.mod b/fn/go.mod
index 89dc280df..1780b9968 100644
--- a/fn/go.mod
+++ b/fn/go.mod
@@ -1,4 +1,4 @@
-module github.com/lightningnetwork/lnd/fn/v2
+module github.com/lightningnetwork/lnd/fn
 
 go 1.19
 
diff --git a/fn/goroutine_manager.go b/fn/goroutine_manager.go
index 81c538ea0..8c9ad8b2d 100644
--- a/fn/goroutine_manager.go
+++ b/fn/goroutine_manager.go
@@ -2,9 +2,13 @@ package fn
 
 import (
 	"context"
+	"errors"
 	"sync"
 )
 
+// ErrStopping is returned when trying to add a new goroutine while stopping.
+var ErrStopping = errors.New("can not add goroutine, stopping")
+
 // GoroutineManager is used to launch goroutines until context expires or the
 // manager is stopped. The Stop method blocks until all started goroutines stop.
 type GoroutineManager struct {
@@ -25,10 +29,8 @@ func NewGoroutineManager(ctx context.Context) *GoroutineManager {
 	}
 }
 
-// Go tries to start a new goroutine and returns a boolean indicating its
-// success. It fails iff the goroutine manager is stopping or its context passed
-// to NewGoroutineManager has expired.
-func (g *GoroutineManager) Go(f func(ctx context.Context)) bool {
+// Go starts a new goroutine if the manager is not stopping.
+func (g *GoroutineManager) Go(f func(ctx context.Context)) error {
 	// Calling wg.Add(1) and wg.Wait() when wg's counter is 0 is a race
 	// condition, since it is not clear should Wait() block or not. This
 	// kind of race condition is detected by Go runtime and results in a
@@ -41,7 +43,7 @@ func (g *GoroutineManager) Go(f func(ctx context.Context)) bool {
 	defer g.mu.Unlock()
 
 	if g.ctx.Err() != nil {
-		return false
+		return ErrStopping
 	}
 
 	g.wg.Add(1)
@@ -50,7 +52,7 @@ func (g *GoroutineManager) Go(f func(ctx context.Context)) bool {
 		f(g.ctx)
 	}()
 
-	return true
+	return nil
 }
 
 // Stop prevents new goroutines from being added and waits for all running
@@ -64,7 +66,7 @@ func (g *GoroutineManager) Stop() {
 	// safe, since it can't run in parallel with wg.Add(1) call in Go, since
 	// we just cancelled the context and even if Go call starts running here
 	// after acquiring the mutex, it would see that the context has expired
-	// and return false instead of calling wg.Add(1).
+	// and return ErrStopping instead of calling wg.Add(1).
 	g.wg.Wait()
 }
 
diff --git a/go.mod b/go.mod
index 6e2bd9f77..e0bda2c29 100644
--- a/go.mod
+++ b/go.mod
@@ -11,7 +11,7 @@ require (
 	github.com/btcsuite/btcd/chaincfg/chainhash v1.1.0
 	github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c
 	github.com/btcsuite/btclog/v2 v2.0.0-20241017175713-3428138b75c7
-	github.com/btcsuite/btcwallet v0.16.10-0.20241113134707-b4ff60753aaa
+	github.com/btcsuite/btcwallet v0.16.10-0.20240912233857-ffb143c77cc5
 	github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5
 	github.com/btcsuite/btcwallet/wallet/txrules v1.2.2
 	github.com/btcsuite/btcwallet/walletdb v1.4.4
@@ -64,7 +64,6 @@ require (
 	google.golang.org/protobuf v1.33.0
 	gopkg.in/macaroon-bakery.v2 v2.0.1
 	gopkg.in/macaroon.v2 v2.0.0
-	pgregory.net/rapid v1.1.0
 )
 
 require (
diff --git a/go.sum b/go.sum
index 86c1c8a21..0cf08569c 100644
--- a/go.sum
+++ b/go.sum
@@ -95,8 +95,8 @@ github.com/btcsuite/btclog v0.0.0-20241003133417-09c4e92e319c/go.mod h1:w7xnGOhw
 github.com/btcsuite/btclog/v2 v2.0.0-20241017175713-3428138b75c7 h1:3Ct3zN3VCEKVm5nceWBBEKczc+jvTfVyOEG71ob2Yuc=
 github.com/btcsuite/btclog/v2 v2.0.0-20241017175713-3428138b75c7/go.mod h1:XItGUfVOxotJL8kkuk2Hj3EVow5KCugXl3wWfQ6K0AE=
 github.com/btcsuite/btcutil v0.0.0-20190425235716-9e5f4b9a998d/go.mod h1:+5NJ2+qvTyV9exUAL/rxXi3DcLg2Ts+ymUAY5y4NvMg=
-github.com/btcsuite/btcwallet v0.16.10-0.20241113134707-b4ff60753aaa h1:x7vYpwkPL5zeJEWPPaRunybH9ERRMGWeNf7x/0aU/38=
-github.com/btcsuite/btcwallet v0.16.10-0.20241113134707-b4ff60753aaa/go.mod h1:1HJXYbjJzgumlnxOC2+ViR1U+gnHWoOn7WeK5OfY1eU=
+github.com/btcsuite/btcwallet v0.16.10-0.20240912233857-ffb143c77cc5 h1:zYy233eUBvkF3lq2MUkybEhxhDsrRDSgiToIKN57mtk=
+github.com/btcsuite/btcwallet v0.16.10-0.20240912233857-ffb143c77cc5/go.mod h1:1HJXYbjJzgumlnxOC2+ViR1U+gnHWoOn7WeK5OfY1eU=
 github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5 h1:Rr0njWI3r341nhSPesKQ2JF+ugDSzdPoeckS75SeDZk=
 github.com/btcsuite/btcwallet/wallet/txauthor v1.3.5/go.mod h1:+tXJ3Ym0nlQc/iHSwW1qzjmPs3ev+UVWMbGgfV1OZqU=
 github.com/btcsuite/btcwallet/wallet/txrules v1.2.2 h1:YEO+Lx1ZJJAtdRrjuhXjWrYsmAk26wLTlNzxt2q0lhk=
@@ -1076,8 +1076,6 @@ modernc.org/strutil v1.2.0 h1:agBi9dp1I+eOnxXeiZawM8F4LawKv4NzGWSaLfyeNZA=
 modernc.org/strutil v1.2.0/go.mod h1:/mdcBmfOibveCTBxUl5B5l6W+TTH1FXPLHZE6bTosX0=
 modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
 modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
-pgregory.net/rapid v1.1.0 h1:CMa0sjHSru3puNx+J0MIAuiiEV4N0qj8/cMWGBBCsjw=
-pgregory.net/rapid v1.1.0/go.mod h1:PY5XlDGj0+V1FCq0o192FdRhpKHGTRIWBgqjDBTrq04=
 rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8=
 rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0=
 rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA=
diff --git a/input/input.go b/input/input.go
index 6835f82c8..6693e9fa8 100644
--- a/input/input.go
+++ b/input/input.go
@@ -69,9 +69,6 @@ type Input interface {
 	// ResolutionBlob returns a special opaque blob to be used to
 	// sweep/resolve this input.
 	ResolutionBlob() fn.Option[tlv.Blob]
-
-	// Preimage returns the preimage for the input if it is an HTLC input.
-	Preimage() fn.Option[lntypes.Preimage]
 }
 
 // TxInfo describes properties of a parent tx that are relevant for CPFP.
@@ -288,11 +285,6 @@ func (bi *BaseInput) CraftInputScript(signer Signer, txn *wire.MsgTx,
 	return witnessFunc(txn, hashCache, txinIdx)
 }
 
-// Preimage returns the preimage for the input if it is an HTLC input.
-func (bi *BaseInput) Preimage() fn.Option[lntypes.Preimage] {
-	return fn.None[lntypes.Preimage]()
-}
-
 // HtlcSucceedInput constitutes a sweep input that needs a pre-image. The input
 // is expected to reside on the commitment tx of the remote party and should
 // not be a second level tx output.
@@ -365,6 +357,7 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx,
 		}
 
 		desc.SignMethod = TaprootScriptSpendSignMethod
+
 		witness, err = SenderHTLCScriptTaprootRedeem(
 			signer, &desc, txn, h.preimage, nil, nil,
 		)
@@ -382,15 +375,6 @@ func (h *HtlcSucceedInput) CraftInputScript(signer Signer, txn *wire.MsgTx,
 	}, nil
 }
 
-// Preimage returns the preimage for the input if it is an HTLC input.
-func (h *HtlcSucceedInput) Preimage() fn.Option[lntypes.Preimage] {
-	if len(h.preimage) == 0 {
-		return fn.None[lntypes.Preimage]()
-	}
-
-	return fn.Some(lntypes.Preimage(h.preimage))
-}
-
 // HtlcSecondLevelAnchorInput is an input type used to spend HTLC outputs
 // using a re-signed second level transaction, either via the timeout or success
 // paths.
@@ -407,8 +391,6 @@ type HtlcSecondLevelAnchorInput struct {
 		hashCache *txscript.TxSigHashes,
 		prevOutputFetcher txscript.PrevOutputFetcher,
 		txinIdx int) (wire.TxWitness, error)
-
-	preimage []byte
 }
 
 // RequiredTxOut returns the tx out needed to be present on the sweep tx for
@@ -445,15 +427,6 @@ func (i *HtlcSecondLevelAnchorInput) CraftInputScript(signer Signer,
 	}, nil
 }
 
-// Preimage returns the preimage for the input if it is an HTLC input.
-func (i *HtlcSecondLevelAnchorInput) Preimage() fn.Option[lntypes.Preimage] {
-	if len(i.preimage) == 0 {
-		return fn.None[lntypes.Preimage]()
-	}
-
-	return fn.Some(lntypes.Preimage(i.preimage))
-}
-
 // MakeHtlcSecondLevelTimeoutAnchorInput creates an input allowing the sweeper
 // to spend the HTLC output on our commit using the second level timeout
 // transaction.
@@ -572,7 +545,6 @@ func MakeHtlcSecondLevelSuccessAnchorInput(signedTx *wire.MsgTx,
 		SignedTx:      signedTx,
 		inputKit:      input.inputKit,
 		createWitness: createWitness,
-		preimage:      preimage[:],
 	}
 }
 
@@ -616,7 +588,6 @@ func MakeHtlcSecondLevelSuccessTaprootInput(signedTx *wire.MsgTx,
 		inputKit:      input.inputKit,
 		SignedTx:      signedTx,
 		createWitness: createWitness,
-		preimage:      preimage[:],
 	}
 }
 
diff --git a/input/mocks.go b/input/mocks.go
index c2af637ed..695525955 100644
--- a/input/mocks.go
+++ b/input/mocks.go
@@ -140,17 +140,6 @@ func (m *MockInput) ResolutionBlob() fn.Option[tlv.Blob] {
 	return info.(fn.Option[tlv.Blob])
 }
 
-func (m *MockInput) Preimage() fn.Option[lntypes.Preimage] {
-	args := m.Called()
-
-	info := args.Get(0)
-	if info == nil {
-		return fn.None[lntypes.Preimage]()
-	}
-
-	return info.(fn.Option[lntypes.Preimage])
-}
-
 // MockWitnessType implements the `WitnessType` interface and is used by other
 // packages for mock testing.
 type MockWitnessType struct {
diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go
index 2f8a9c42a..eabe952b2 100644
--- a/lnrpc/lightning.pb.go
+++ b/lnrpc/lightning.pb.go
@@ -15183,6 +15183,14 @@ type PolicyUpdateRequest struct {
 	// Optional inbound fee. If unset, the previously set value will be
 	// retained [EXPERIMENTAL].
 	InboundFee *InboundFee `protobuf:"bytes,10,opt,name=inbound_fee,json=inboundFee,proto3" json:"inbound_fee,omitempty"`
+	// Under unknown circumstances a channel can exist with a missing edge in
+	// the graph database. This can cause an 'edge not found' error when calling
+	// `getchaninfo` and/or cause the default channel policy to be used during
+	// forwards. Setting this flag will recreate the edge if not found, allowing
+	// updating this channel policy and fixing the missing edge problem for this
+	// channel permanently. For fields not set in this command, the default
+	// policy will be created.
+	CreateMissingEdge bool `protobuf:"varint,11,opt,name=create_missing_edge,json=createMissingEdge,proto3" json:"create_missing_edge,omitempty"`
 }
 
 func (x *PolicyUpdateRequest) Reset() {
@@ -15294,6 +15302,13 @@ func (x *PolicyUpdateRequest) GetInboundFee() *InboundFee {
 	return nil
 }
 
+func (x *PolicyUpdateRequest) GetCreateMissingEdge() bool {
+	if x != nil {
+		return x.CreateMissingEdge
+	}
+	return false
+}
+
 type isPolicyUpdateRequest_Scope interface {
 	isPolicyUpdateRequest_Scope()
 }
@@ -20763,7 +20778,7 @@ var file_lightning_proto_rawDesc = []byte{
 	0x65, 0x65, 0x5f, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x62,
 	0x61, 0x73, 0x65, 0x46, 0x65, 0x65, 0x4d, 0x73, 0x61, 0x74, 0x12, 0x20, 0x0a, 0x0c, 0x66, 0x65,
 	0x65, 0x5f, 0x72, 0x61, 0x74, 0x65, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x05,
-	0x52, 0x0a, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x22, 0xaa, 0x03, 0x0a,
+	0x52, 0x0a, 0x66, 0x65, 0x65, 0x52, 0x61, 0x74, 0x65, 0x50, 0x70, 0x6d, 0x22, 0xda, 0x03, 0x0a,
 	0x13, 0x50, 0x6f, 0x6c, 0x69, 0x63, 0x79, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x52, 0x65, 0x71,
 	0x75, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x06, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x18, 0x01,
 	0x20, 0x01, 0x28, 0x08, 0x48, 0x00, 0x52, 0x06, 0x67, 0x6c, 0x6f, 0x62, 0x61, 0x6c, 0x12, 0x34,
@@ -20790,6 +20805,9 @@ var file_lightning_proto_rawDesc = []byte{
 	0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x5f, 0x66, 0x65, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28,
 	0x0b, 0x32, 0x11, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x62, 0x6f, 0x75, 0x6e,
 	0x64, 0x46, 0x65, 0x65, 0x52, 0x0a, 0x69, 0x6e, 0x62, 0x6f, 0x75, 0x6e, 0x64, 0x46, 0x65, 0x65,
+	0x12, 0x2e, 0x0a, 0x13, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x5f, 0x6d, 0x69, 0x73, 0x73, 0x69,
+	0x6e, 0x67, 0x5f, 0x65, 0x64, 0x67, 0x65, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x08, 0x52, 0x11, 0x63,
+	0x72, 0x65, 0x61, 0x74, 0x65, 0x4d, 0x69, 0x73, 0x73, 0x69, 0x6e, 0x67, 0x45, 0x64, 0x67, 0x65,
 	0x42, 0x07, 0x0a, 0x05, 0x73, 0x63, 0x6f, 0x70, 0x65, 0x22, 0x8c, 0x01, 0x0a, 0x0c, 0x46, 0x61,
 	0x69, 0x6c, 0x65, 0x64, 0x55, 0x70, 0x64, 0x61, 0x74, 0x65, 0x12, 0x2b, 0x0a, 0x08, 0x6f, 0x75,
 	0x74, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0f, 0x2e, 0x6c,
diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto
index 3d554352b..4ea163f20 100644
--- a/lnrpc/lightning.proto
+++ b/lnrpc/lightning.proto
@@ -4558,6 +4558,15 @@ message PolicyUpdateRequest {
     // Optional inbound fee. If unset, the previously set value will be
     // retained [EXPERIMENTAL].
     InboundFee inbound_fee = 10;
+
+    // Under unknown circumstances a channel can exist with a missing edge in
+    // the graph database. This can cause an 'edge not found' error when calling
+    // `getchaninfo` and/or cause the default channel policy to be used during
+    // forwards. Setting this flag will recreate the edge if not found, allowing
+    // updating this channel policy and fixing the missing edge problem for this
+    // channel permanently. For fields not set in this command, the default
+    // policy will be created.
+    bool create_missing_edge = 11;
 }
 
 enum UpdateFailure {
diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json
index 64c36d17d..c4774334e 100644
--- a/lnrpc/lightning.swagger.json
+++ b/lnrpc/lightning.swagger.json
@@ -6751,6 +6751,10 @@
         "inbound_fee": {
           "$ref": "#/definitions/lnrpcInboundFee",
           "description": "Optional inbound fee. If unset, the previously set value will be\nretained [EXPERIMENTAL]."
+        },
+        "create_missing_edge": {
+          "type": "boolean",
+          "description": "Under unknown circumstances a channel can exist with a missing edge in\nthe graph database. This can cause an 'edge not found' error when calling\n`getchaninfo` and/or cause the default channel policy to be used during\nforwards. Setting this flag will recreate the edge if not found, allowing\nupdating this channel policy and fixing the missing edge problem for this\nchannel permanently. For fields not set in this command, the default\npolicy will be created."
         }
       }
     },
diff --git a/lnrpc/marshall_utils.go b/lnrpc/marshall_utils.go
index 230fea35b..0dcbe46fa 100644
--- a/lnrpc/marshall_utils.go
+++ b/lnrpc/marshall_utils.go
@@ -23,9 +23,6 @@ var (
 	ErrSatMsatMutualExclusive = errors.New(
 		"sat and msat arguments are mutually exclusive",
 	)
-
-	// ErrNegativeAmt is returned when a negative amount is specified.
-	ErrNegativeAmt = errors.New("amount cannot be negative")
 )
 
 // CalculateFeeLimit returns the fee limit in millisatoshis. If a percentage
@@ -59,10 +56,6 @@ func UnmarshallAmt(amtSat, amtMsat int64) (lnwire.MilliSatoshi, error) {
 		return 0, ErrSatMsatMutualExclusive
 	}
 
-	if amtSat < 0 || amtMsat < 0 {
-		return 0, ErrNegativeAmt
-	}
-
 	if amtSat != 0 {
 		return lnwire.NewMSatFromSatoshis(btcutil.Amount(amtSat)), nil
 	}
diff --git a/lnwallet/aux_leaf_store.go b/lnwallet/aux_leaf_store.go
index c457a9250..4558c2f81 100644
--- a/lnwallet/aux_leaf_store.go
+++ b/lnwallet/aux_leaf_store.go
@@ -178,8 +178,7 @@ type AuxLeafStore interface {
 	// commitment.
 	FetchLeavesFromCommit(chanState AuxChanState,
 		commit channeldb.ChannelCommitment,
-		keyRing CommitmentKeyRing, whoseCommit lntypes.ChannelParty,
-	) fn.Result[CommitDiffAuxResult]
+		keyRing CommitmentKeyRing) fn.Result[CommitDiffAuxResult]
 
 	// FetchLeavesFromRevocation attempts to fetch the auxiliary leaves
 	// from a channel revocation that stores balance + blob information.
diff --git a/lnwallet/aux_resolutions.go b/lnwallet/aux_resolutions.go
index 382232640..e3b04fc5a 100644
--- a/lnwallet/aux_resolutions.go
+++ b/lnwallet/aux_resolutions.go
@@ -3,7 +3,6 @@ package lnwallet
 import (
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/wire"
-	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/input"
 	"github.com/lightningnetwork/lnd/lnwire"
@@ -25,19 +24,6 @@ const (
 	Breach
 )
 
-// AuxSigDesc stores optional information related to 2nd level HTLCs for aux
-// channels.
-type AuxSigDesc struct {
-	// AuxSig is the second-level signature for the HTLC that we are trying
-	// to resolve. This is only present if this is a resolution request for
-	// an HTLC on our commitment transaction.
-	AuxSig []byte
-
-	// SignDetails is the sign details for the second-level HTLC. This may
-	// be used to generate the second signature needed for broadcast.
-	SignDetails input.SignDetails
-}
-
 // ResolutionReq is used to ask an outside sub-system for additional
 // information needed to resolve a contract.
 type ResolutionReq struct {
@@ -45,9 +31,6 @@ type ResolutionReq struct {
 	// resolve.
 	ChanPoint wire.OutPoint
 
-	// ChanType is the type of the channel that we are trying to resolve.
-	ChanType channeldb.ChannelType
-
 	// ShortChanID is the short channel ID of the channel that we are
 	// trying to resolve.
 	ShortChanID lnwire.ShortChannelID
@@ -61,13 +44,6 @@ type ResolutionReq struct {
 	// FundingBlob is an optional funding blob for the channel.
 	FundingBlob fn.Option[tlv.Blob]
 
-	// HtlcID is the ID of the HTLC that we are trying to resolve. This is
-	// only set if this is a resolution request for an HTLC.
-	HtlcID fn.Option[input.HtlcIndex]
-
-	// HtlcAmt is the amount of the HTLC that we are trying to resolve.
-	HtlcAmt btcutil.Amount
-
 	// Type is the type of the witness that we are trying to resolve.
 	Type input.WitnessType
 
@@ -93,26 +69,14 @@ type ResolutionReq struct {
 	// CsvDelay is the CSV delay for the local output for this commitment.
 	CsvDelay uint32
 
-	// CommitCsvDelay is the CSV delay for the remote output for this
-	// commitment.
-	CommitCsvDelay uint32
-
 	// BreachCsvDelay is the CSV delay for the remote output. This is only
 	// set when the CloseType is Breach. This indicates the CSV delay to
 	// use for the remote party's to_local delayed output, that is now
 	// rightfully ours in a breach situation.
 	BreachCsvDelay fn.Option[uint32]
 
-	// CltvDelay is the CLTV delay for the outpoint/transaction.
+	// CltvDelay is the CLTV delay for the outpoint.
 	CltvDelay fn.Option[uint32]
-
-	// PayHash is the payment hash for the HTLC that we are trying to
-	// resolve. This is optional as it only applies HTLC outputs.
-	PayHash fn.Option[[32]byte]
-
-	// AuxSigDesc is an optional field that contains additional information
-	// needed to sweep second level HTLCs.
-	AuxSigDesc fn.Option[AuxSigDesc]
 }
 
 // AuxContractResolver is an interface that is used to resolve contracts that
diff --git a/lnwallet/chainfee/estimator.go b/lnwallet/chainfee/estimator.go
index f83cce285..6f59c3f7f 100644
--- a/lnwallet/chainfee/estimator.go
+++ b/lnwallet/chainfee/estimator.go
@@ -238,7 +238,6 @@ func (b *BtcdEstimator) Stop() error {
 	b.filterManager.Stop()
 
 	b.btcdConn.Shutdown()
-	b.btcdConn.WaitForShutdown()
 
 	return nil
 }
diff --git a/lnwallet/channel.go b/lnwallet/channel.go
index 1e61abe3f..33360228d 100644
--- a/lnwallet/channel.go
+++ b/lnwallet/channel.go
@@ -666,7 +666,6 @@ func (lc *LightningChannel) diskCommitToMemCommit(
 			return s.FetchLeavesFromCommit(
 				NewAuxChanState(lc.channelState), *diskCommit,
 				*commitKeys.GetForParty(whoseCommit),
-				whoseCommit,
 			)
 		},
 	).Unpack()
@@ -1835,7 +1834,7 @@ func (lc *LightningChannel) restorePendingLocalUpdates(
 		func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] {
 			return s.FetchLeavesFromCommit(
 				NewAuxChanState(lc.channelState), pendingCommit,
-				*pendingRemoteKeys, lntypes.Remote,
+				*pendingRemoteKeys,
 			)
 		},
 	).Unpack()
@@ -2203,7 +2202,6 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
 		// resolution data for this output.
 		resolveReq := ResolutionReq{
 			ChanPoint:      chanState.FundingOutpoint,
-			ChanType:       chanState.ChanType,
 			ShortChanID:    chanState.ShortChanID(),
 			Initiator:      chanState.IsInitiator,
 			FundingBlob:    chanState.CustomBlob,
@@ -2283,7 +2281,6 @@ func NewBreachRetribution(chanState *channeldb.OpenChannel, stateNum uint64,
 		// resolution data for this output.
 		resolveReq := ResolutionReq{
 			ChanPoint:      chanState.FundingOutpoint,
-			ChanType:       chanState.ChanType,
 			ShortChanID:    chanState.ShortChanID(),
 			Initiator:      chanState.IsInitiator,
 			FundingBlob:    chanState.CustomBlob,
@@ -3155,7 +3152,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 		leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] {
 			return s.FetchLeavesFromCommit(
 				NewAuxChanState(chanState), *diskCommit,
-				*keyRing, lntypes.Remote,
+				*keyRing,
 			)
 		},
 	).Unpack()
@@ -4456,9 +4453,7 @@ func (lc *LightningChannel) ProcessChanSyncMsg(ctx context.Context,
 		// Next, we'll need to send over any updates we sent as part of
 		// this new proposed commitment state.
 		for _, logUpdate := range commitDiff.LogUpdates {
-			commitUpdates = append(
-				commitUpdates, logUpdate.UpdateMsg,
-			)
+			commitUpdates = append(commitUpdates, logUpdate.UpdateMsg)
 		}
 
 		// If this is a taproot channel, then we need to regenerate the
@@ -4741,7 +4736,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 		leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] {
 			return s.FetchLeavesFromCommit(
 				NewAuxChanState(chanState), *diskCommit,
-				*keyRing, lntypes.Local,
+				*keyRing,
 			)
 		},
 	).Unpack()
@@ -6709,7 +6704,7 @@ type UnilateralCloseSummary struct {
 // happen in case we have lost state) it should be set to an empty struct, in
 // which case we will attempt to sweep the non-HTLC output using the passed
 // commitPoint.
-func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, //nolint:funlen
+func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel,
 	signer input.Signer, commitSpend *chainntnfs.SpendDetail,
 	remoteCommit channeldb.ChannelCommitment, commitPoint *btcec.PublicKey,
 	leafStore fn.Option[AuxLeafStore],
@@ -6728,7 +6723,7 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, //nolint:funlen
 		leafStore, func(s AuxLeafStore) fn.Result[CommitDiffAuxResult] {
 			return s.FetchLeavesFromCommit(
 				NewAuxChanState(chanState), remoteCommit,
-				*keyRing, lntypes.Remote,
+				*keyRing,
 			)
 		},
 	).Unpack()
@@ -6753,8 +6748,8 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, //nolint:funlen
 		chainfee.SatPerKWeight(remoteCommit.FeePerKw), commitType,
 		signer, remoteCommit.Htlcs, keyRing, &chanState.LocalChanCfg,
 		&chanState.RemoteChanCfg, commitSpend.SpendingTx,
-		chanState.ChanType, isRemoteInitiator, leaseExpiry, chanState,
-		auxResult.AuxLeaves, auxResolver,
+		chanState.ChanType, isRemoteInitiator, leaseExpiry,
+		auxResult.AuxLeaves,
 	)
 	if err != nil {
 		return nil, fmt.Errorf("unable to create htlc resolutions: %w",
@@ -6844,7 +6839,6 @@ func NewUnilateralCloseSummary(chanState *channeldb.OpenChannel, //nolint:funlen
 		// resolution data for this output.
 		resolveReq := ResolutionReq{
 			ChanPoint:     chanState.FundingOutpoint,
-			ChanType:      chanState.ChanType,
 			ShortChanID:   chanState.ShortChanID(),
 			Initiator:     chanState.IsInitiator,
 			CommitBlob:    chanState.RemoteCommitment.CustomBlob,
@@ -6962,11 +6956,6 @@ type IncomingHtlcResolution struct {
 	// necessary items required to spend the sole output of the above
 	// transaction.
 	SweepSignDesc input.SignDescriptor
-
-	// ResolutionBlob is a blob used for aux channels that permits a
-	// spender of the output to properly resolve it in the case of a force
-	// close.
-	ResolutionBlob fn.Option[tlv.Blob]
 }
 
 // OutgoingHtlcResolution houses the information necessary to sweep any
@@ -7016,11 +7005,6 @@ type OutgoingHtlcResolution struct {
 	// necessary items required to spend the sole output of the above
 	// transaction.
 	SweepSignDesc input.SignDescriptor
-
-	// ResolutionBlob is a blob used for aux channels that permits a
-	// spender of the output to properly resolve it in the case of a force
-	// close.
-	ResolutionBlob fn.Option[tlv.Blob]
 }
 
 // HtlcResolutions contains the items necessary to sweep HTLC's on chain
@@ -7045,10 +7029,8 @@ func newOutgoingHtlcResolution(signer input.Signer,
 	htlc *channeldb.HTLC, keyRing *CommitmentKeyRing,
 	feePerKw chainfee.SatPerKWeight, csvDelay, leaseExpiry uint32,
 	whoseCommit lntypes.ChannelParty, isCommitFromInitiator bool,
-	chanType channeldb.ChannelType, chanState *channeldb.OpenChannel,
-	auxLeaves fn.Option[CommitAuxLeaves],
-	auxResolver fn.Option[AuxContractResolver],
-) (*OutgoingHtlcResolution, error) {
+	chanType channeldb.ChannelType,
+	auxLeaves fn.Option[CommitAuxLeaves]) (*OutgoingHtlcResolution, error) {
 
 	op := wire.OutPoint{
 		Hash:  commitTx.TxHash(),
@@ -7079,8 +7061,6 @@ func newOutgoingHtlcResolution(signer input.Signer,
 		return nil, err
 	}
 
-	htlcCsvDelay := HtlcSecondLevelInputSequence(chanType)
-
 	// If we're spending this HTLC output from the remote node's
 	// commitment, then we won't need to go to the second level as our
 	// outputs don't have a CSV delay.
@@ -7118,43 +7098,11 @@ func newOutgoingHtlcResolution(signer input.Signer,
 			}
 		}
 
-		resReq := ResolutionReq{
-			ChanPoint:     chanState.FundingOutpoint,
-			ChanType:      chanType,
-			ShortChanID:   chanState.ShortChanID(),
-			Initiator:     chanState.IsInitiator,
-			CommitBlob:    chanState.RemoteCommitment.CustomBlob,
-			FundingBlob:   chanState.CustomBlob,
-			Type:          input.TaprootHtlcOfferedRemoteTimeout,
-			CloseType:     RemoteForceClose,
-			CommitTx:      commitTx,
-			ContractPoint: op,
-			SignDesc:      signDesc,
-			KeyRing:       keyRing,
-			CsvDelay:      htlcCsvDelay,
-			CltvDelay:     fn.Some(htlc.RefundTimeout),
-			CommitFee:     chanState.RemoteCommitment.CommitFee,
-			HtlcID:        fn.Some(htlc.HtlcIndex),
-			PayHash:       fn.Some(htlc.RHash),
-		}
-		resolveRes := fn.MapOptionZ(
-			auxResolver,
-			func(a AuxContractResolver) fn.Result[tlv.Blob] {
-				return a.ResolveContract(resReq)
-			},
-		)
-		if err := resolveRes.Err(); err != nil {
-			return nil, fmt.Errorf("unable to aux resolve: %w", err)
-		}
-
-		resolutionBlob := resolveRes.Option()
-
 		return &OutgoingHtlcResolution{
-			Expiry:         htlc.RefundTimeout,
-			ClaimOutpoint:  op,
-			SweepSignDesc:  signDesc,
-			CsvDelay:       csvDelay,
-			ResolutionBlob: resolutionBlob,
+			Expiry:        htlc.RefundTimeout,
+			ClaimOutpoint: op,
+			SweepSignDesc: signDesc,
+			CsvDelay:      HtlcSecondLevelInputSequence(chanType),
 		}, nil
 	}
 
@@ -7305,78 +7253,31 @@ func newOutgoingHtlcResolution(signer input.Signer,
 		keyRing.CommitPoint, localChanCfg.DelayBasePoint.PubKey,
 	)
 
-	// In addition to the info in txSignDetails, we also need extra
-	// information to sweep the second level output after confirmation.
-	sweepSignDesc := input.SignDescriptor{
-		KeyDesc:       localChanCfg.DelayBasePoint,
-		SingleTweak:   localDelayTweak,
-		WitnessScript: htlcSweepWitnessScript,
-		Output: &wire.TxOut{
-			PkScript: htlcSweepScript.PkScript(),
-			Value:    int64(secondLevelOutputAmt),
-		},
-		HashType: sweepSigHash(chanType),
-		PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher(
-			htlcSweepScript.PkScript(),
-			int64(secondLevelOutputAmt),
-		),
-		SignMethod:   signMethod,
-		ControlBlock: ctrlBlock,
-	}
-
-	// This might be an aux channel, so we'll go ahead and attempt to
-	// generate the resolution blob for the channel so we can pass along to
-	// the sweeping sub-system.
-	resolveRes := fn.MapOptionZ(
-		auxResolver, func(a AuxContractResolver) fn.Result[tlv.Blob] {
-			resReq := ResolutionReq{
-				ChanPoint:      chanState.FundingOutpoint,
-				ChanType:       chanType,
-				ShortChanID:    chanState.ShortChanID(),
-				Initiator:      chanState.IsInitiator,
-				CommitBlob:     chanState.LocalCommitment.CustomBlob, //nolint:lll
-				FundingBlob:    chanState.CustomBlob,
-				Type:           input.TaprootHtlcLocalOfferedTimeout, //nolint:lll
-				CloseType:      LocalForceClose,
-				CommitTx:       commitTx,
-				ContractPoint:  op,
-				SignDesc:       sweepSignDesc,
-				KeyRing:        keyRing,
-				CsvDelay:       htlcCsvDelay,
-				HtlcAmt:        btcutil.Amount(txOut.Value),
-				CommitCsvDelay: csvDelay,
-				CltvDelay:      fn.Some(htlc.RefundTimeout),
-				CommitFee:      chanState.LocalCommitment.CommitFee, //nolint:lll
-				HtlcID:         fn.Some(htlc.HtlcIndex),
-				PayHash:        fn.Some(htlc.RHash),
-				AuxSigDesc: fn.Some(AuxSigDesc{
-					SignDetails: *txSignDetails,
-					AuxSig: func() []byte {
-						tlvType := htlcCustomSigType.TypeVal()     //nolint:lll
-						return htlc.CustomRecords[uint64(tlvType)] //nolint:lll
-					}(),
-				}),
-			}
-
-			return a.ResolveContract(resReq)
-		},
-	)
-	if err := resolveRes.Err(); err != nil {
-		return nil, fmt.Errorf("unable to aux resolve: %w", err)
-	}
-	resolutionBlob := resolveRes.Option()
-
 	return &OutgoingHtlcResolution{
 		Expiry:          htlc.RefundTimeout,
 		SignedTimeoutTx: timeoutTx,
 		SignDetails:     txSignDetails,
 		CsvDelay:        csvDelay,
-		ResolutionBlob:  resolutionBlob,
 		ClaimOutpoint: wire.OutPoint{
 			Hash:  timeoutTx.TxHash(),
 			Index: 0,
 		},
-		SweepSignDesc: sweepSignDesc,
+		SweepSignDesc: input.SignDescriptor{
+			KeyDesc:       localChanCfg.DelayBasePoint,
+			SingleTweak:   localDelayTweak,
+			WitnessScript: htlcSweepWitnessScript,
+			Output: &wire.TxOut{
+				PkScript: htlcSweepScript.PkScript(),
+				Value:    int64(secondLevelOutputAmt),
+			},
+			HashType: sweepSigHash(chanType),
+			PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher(
+				htlcSweepScript.PkScript(),
+				int64(secondLevelOutputAmt),
+			),
+			SignMethod:   signMethod,
+			ControlBlock: ctrlBlock,
+		},
 	}, nil
 }
 
@@ -7392,10 +7293,8 @@ func newIncomingHtlcResolution(signer input.Signer,
 	htlc *channeldb.HTLC, keyRing *CommitmentKeyRing,
 	feePerKw chainfee.SatPerKWeight, csvDelay, leaseExpiry uint32,
 	whoseCommit lntypes.ChannelParty, isCommitFromInitiator bool,
-	chanType channeldb.ChannelType, chanState *channeldb.OpenChannel,
-	auxLeaves fn.Option[CommitAuxLeaves],
-	auxResolver fn.Option[AuxContractResolver],
-) (*IncomingHtlcResolution, error) {
+	chanType channeldb.ChannelType,
+	auxLeaves fn.Option[CommitAuxLeaves]) (*IncomingHtlcResolution, error) {
 
 	op := wire.OutPoint{
 		Hash:  commitTx.TxHash(),
@@ -7427,8 +7326,6 @@ func newIncomingHtlcResolution(signer input.Signer,
 		return nil, err
 	}
 
-	htlcCsvDelay := HtlcSecondLevelInputSequence(chanType)
-
 	// If we're spending this output from the remote node's commitment,
 	// then we can skip the second layer and spend the output directly.
 	if whoseCommit.IsRemote() {
@@ -7464,44 +7361,10 @@ func newIncomingHtlcResolution(signer input.Signer,
 			}
 		}
 
-		resReq := ResolutionReq{
-			ChanPoint:      chanState.FundingOutpoint,
-			ChanType:       chanType,
-			ShortChanID:    chanState.ShortChanID(),
-			Initiator:      chanState.IsInitiator,
-			CommitBlob:     chanState.RemoteCommitment.CustomBlob,
-			Type:           input.TaprootHtlcAcceptedRemoteSuccess,
-			FundingBlob:    chanState.CustomBlob,
-			CloseType:      RemoteForceClose,
-			CommitTx:       commitTx,
-			ContractPoint:  op,
-			SignDesc:       signDesc,
-			KeyRing:        keyRing,
-			HtlcID:         fn.Some(htlc.HtlcIndex),
-			CsvDelay:       htlcCsvDelay,
-			CltvDelay:      fn.Some(htlc.RefundTimeout),
-			CommitFee:      chanState.RemoteCommitment.CommitFee,
-			PayHash:        fn.Some(htlc.RHash),
-			CommitCsvDelay: csvDelay,
-			HtlcAmt:        htlc.Amt.ToSatoshis(),
-		}
-		resolveRes := fn.MapOptionZ(
-			auxResolver,
-			func(a AuxContractResolver) fn.Result[tlv.Blob] {
-				return a.ResolveContract(resReq)
-			},
-		)
-		if err := resolveRes.Err(); err != nil {
-			return nil, fmt.Errorf("unable to aux resolve: %w", err)
-		}
-
-		resolutionBlob := resolveRes.Option()
-
 		return &IncomingHtlcResolution{
-			ClaimOutpoint:  op,
-			SweepSignDesc:  signDesc,
-			CsvDelay:       htlcCsvDelay,
-			ResolutionBlob: resolutionBlob,
+			ClaimOutpoint: op,
+			SweepSignDesc: signDesc,
+			CsvDelay:      HtlcSecondLevelInputSequence(chanType),
 		}, nil
 	}
 
@@ -7647,76 +7510,30 @@ func newIncomingHtlcResolution(signer input.Signer,
 	localDelayTweak := input.SingleTweakBytes(
 		keyRing.CommitPoint, localChanCfg.DelayBasePoint.PubKey,
 	)
-
-	// In addition to the info in txSignDetails, we also need extra
-	// information to sweep the second level output after confirmation.
-	sweepSignDesc := input.SignDescriptor{
-		KeyDesc:       localChanCfg.DelayBasePoint,
-		SingleTweak:   localDelayTweak,
-		WitnessScript: htlcSweepWitnessScript,
-		Output: &wire.TxOut{
-			PkScript: htlcSweepScript.PkScript(),
-			Value:    int64(secondLevelOutputAmt),
-		},
-		HashType: sweepSigHash(chanType),
-		PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher(
-			htlcSweepScript.PkScript(),
-			int64(secondLevelOutputAmt),
-		),
-		SignMethod:   signMethod,
-		ControlBlock: ctrlBlock,
-	}
-
-	resolveRes := fn.MapOptionZ(
-		auxResolver, func(a AuxContractResolver) fn.Result[tlv.Blob] {
-			resReq := ResolutionReq{
-				ChanPoint:     chanState.FundingOutpoint,
-				ChanType:      chanType,
-				ShortChanID:   chanState.ShortChanID(),
-				Initiator:     chanState.IsInitiator,
-				CommitBlob:    chanState.LocalCommitment.CustomBlob,  //nolint:lll
-				Type:          input.TaprootHtlcAcceptedLocalSuccess, //nolint:lll
-				FundingBlob:   chanState.CustomBlob,
-				CloseType:     LocalForceClose,
-				CommitTx:      commitTx,
-				ContractPoint: op,
-				SignDesc:      sweepSignDesc,
-				KeyRing:       keyRing,
-				HtlcID:        fn.Some(htlc.HtlcIndex),
-				CsvDelay:      htlcCsvDelay,
-				CommitFee:     chanState.LocalCommitment.CommitFee, //nolint:lll
-				PayHash:       fn.Some(htlc.RHash),
-				AuxSigDesc: fn.Some(AuxSigDesc{
-					SignDetails: *txSignDetails,
-					AuxSig: func() []byte {
-						tlvType := htlcCustomSigType.TypeVal()     //nolint:lll
-						return htlc.CustomRecords[uint64(tlvType)] //nolint:lll
-					}(),
-				}),
-				CommitCsvDelay: csvDelay,
-				HtlcAmt:        btcutil.Amount(txOut.Value),
-				CltvDelay:      fn.Some(htlc.RefundTimeout),
-			}
-
-			return a.ResolveContract(resReq)
-		},
-	)
-	if err := resolveRes.Err(); err != nil {
-		return nil, fmt.Errorf("unable to aux resolve: %w", err)
-	}
-
-	resolutionBlob := resolveRes.Option()
-
 	return &IncomingHtlcResolution{
 		SignedSuccessTx: successTx,
 		SignDetails:     txSignDetails,
 		CsvDelay:        csvDelay,
-		ResolutionBlob:  resolutionBlob,
 		ClaimOutpoint: wire.OutPoint{
 			Hash:  successTx.TxHash(),
 			Index: 0,
 		},
-		SweepSignDesc: sweepSignDesc,
+		SweepSignDesc: input.SignDescriptor{
+			KeyDesc:       localChanCfg.DelayBasePoint,
+			SingleTweak:   localDelayTweak,
+			WitnessScript: htlcSweepWitnessScript,
+			Output: &wire.TxOut{
+				PkScript: htlcSweepScript.PkScript(),
+				Value:    int64(secondLevelOutputAmt),
+			},
+			HashType: sweepSigHash(chanType),
+			PrevOutputFetcher: txscript.NewCannedPrevOutputFetcher(
+				htlcSweepScript.PkScript(),
+				int64(secondLevelOutputAmt),
+			),
+			SignMethod:   signMethod,
+			ControlBlock: ctrlBlock,
+		},
 	}, nil
 }
 
@@ -7753,8 +7570,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight,
 	localChanCfg, remoteChanCfg *channeldb.ChannelConfig,
 	commitTx *wire.MsgTx, chanType channeldb.ChannelType,
 	isCommitFromInitiator bool, leaseExpiry uint32,
-	chanState *channeldb.OpenChannel, auxLeaves fn.Option[CommitAuxLeaves],
-	auxResolver fn.Option[AuxContractResolver]) (*HtlcResolutions, error) {
+	auxLeaves fn.Option[CommitAuxLeaves]) (*HtlcResolutions, error) {
 
 	// TODO(roasbeef): don't need to swap csv delay?
 	dustLimit := remoteChanCfg.DustLimit
@@ -7789,7 +7605,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight,
 				signer, localChanCfg, commitTx, &htlc,
 				keyRing, feePerKw, uint32(csvDelay),
 				leaseExpiry, whoseCommit, isCommitFromInitiator,
-				chanType, chanState, auxLeaves, auxResolver,
+				chanType, auxLeaves,
 			)
 			if err != nil {
 				return nil, fmt.Errorf("incoming resolution "+
@@ -7803,8 +7619,7 @@ func extractHtlcResolutions(feePerKw chainfee.SatPerKWeight,
 		ohr, err := newOutgoingHtlcResolution(
 			signer, localChanCfg, commitTx, &htlc, keyRing,
 			feePerKw, uint32(csvDelay), leaseExpiry, whoseCommit,
-			isCommitFromInitiator, chanType, chanState, auxLeaves,
-			auxResolver,
+			isCommitFromInitiator, chanType, auxLeaves,
 		)
 		if err != nil {
 			return nil, fmt.Errorf("outgoing resolution "+
@@ -7852,18 +7667,6 @@ type LocalForceCloseSummary struct {
 	// commitment state.
 	CloseTx *wire.MsgTx
 
-	// ChanSnapshot is a snapshot of the final state of the channel at the
-	// time the summary was created.
-	ChanSnapshot channeldb.ChannelSnapshot
-
-	// ContractResolutions contains all the data required for resolving the
-	// different output types of a commitment transaction.
-	ContractResolutions fn.Option[ContractResolutions]
-}
-
-// ContractResolutions contains all the data required for resolving the
-// different output types of a commitment transaction.
-type ContractResolutions struct {
 	// CommitResolution contains all the data required to sweep the output
 	// to ourselves. Since this is our commitment transaction, we'll need
 	// to wait a time delay before we can sweep the output.
@@ -7872,38 +7675,19 @@ type ContractResolutions struct {
 	// then this will be nil.
 	CommitResolution *CommitOutputResolution
 
-	// AnchorResolution contains the data required to sweep the anchor
-	// output. If the channel type doesn't include anchors, the value of
-	// this field will be nil.
-	AnchorResolution *AnchorResolution
-
 	// HtlcResolutions contains all the data required to sweep any outgoing
 	// HTLC's and incoming HTLc's we know the preimage to. For each of these
 	// HTLC's, we'll need to go to the second level to sweep them fully.
 	HtlcResolutions *HtlcResolutions
-}
-
-// ForceCloseOpt is a functional option argument for the ForceClose method.
-type ForceCloseOpt func(*forceCloseConfig)
-
-// forceCloseConfig holds the configuration options for force closing a channel.
-type forceCloseConfig struct {
-	// skipResolution if true will skip creating the contract resolutions
-	// when generating the force close summary.
-	skipResolution bool
-}
 
-// defaultForceCloseConfig returns the default force close configuration.
-func defaultForceCloseConfig() *forceCloseConfig {
-	return &forceCloseConfig{}
-}
+	// ChanSnapshot is a snapshot of the final state of the channel at the
+	// time the summary was created.
+	ChanSnapshot channeldb.ChannelSnapshot
 
-// WithSkipContractResolutions creates an option to skip the contract
-// resolutions from the returned summary.
-func WithSkipContractResolutions() ForceCloseOpt {
-	return func(cfg *forceCloseConfig) {
-		cfg.skipResolution = true
-	}
+	// AnchorResolution contains the data required to sweep the anchor
+	// output. If the channel type doesn't include anchors, the value of
+	// this field will be nil.
+	AnchorResolution *AnchorResolution
 }
 
 // ForceClose executes a unilateral closure of the transaction at the current
@@ -7914,17 +7698,10 @@ func WithSkipContractResolutions() ForceCloseOpt {
 // outputs within the commitment transaction.
 //
 // TODO(roasbeef): all methods need to abort if in dispute state
-func (lc *LightningChannel) ForceClose(opts ...ForceCloseOpt) (
-	*LocalForceCloseSummary, error) {
-
+func (lc *LightningChannel) ForceClose() (*LocalForceCloseSummary, error) {
 	lc.Lock()
 	defer lc.Unlock()
 
-	cfg := defaultForceCloseConfig()
-	for _, opt := range opts {
-		opt(cfg)
-	}
-
 	// If we've detected local data loss for this channel, then we won't
 	// allow a force close, as it may be the case that we have a dated
 	// version of the commitment, or this is actually a channel shell.
@@ -7939,14 +7716,6 @@ func (lc *LightningChannel) ForceClose(opts ...ForceCloseOpt) (
 		return nil, err
 	}
 
-	if cfg.skipResolution {
-		return &LocalForceCloseSummary{
-			ChanPoint:    lc.channelState.FundingOutpoint,
-			ChanSnapshot: *lc.channelState.Snapshot(),
-			CloseTx:      commitTx,
-		}, nil
-	}
-
 	localCommitment := lc.channelState.LocalCommitment
 	summary, err := NewLocalForceCloseSummary(
 		lc.channelState, lc.Signer, commitTx,
@@ -7996,7 +7765,6 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
 			return s.FetchLeavesFromCommit(
 				NewAuxChanState(chanState),
 				chanState.LocalCommitment, *keyRing,
-				lntypes.Local,
 			)
 		},
 	).Unpack()
@@ -8103,8 +7871,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
 			func(a AuxContractResolver) fn.Result[tlv.Blob] {
 				//nolint:lll
 				return a.ResolveContract(ResolutionReq{
-					ChanPoint:     chanState.FundingOutpoint, //nolint:lll
-					ChanType:      chanState.ChanType,
+					ChanPoint:     chanState.FundingOutpoint,
 					ShortChanID:   chanState.ShortChanID(),
 					Initiator:     chanState.IsInitiator,
 					CommitBlob:    chanState.LocalCommitment.CustomBlob,
@@ -8137,8 +7904,7 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
 		chainfee.SatPerKWeight(localCommit.FeePerKw), lntypes.Local,
 		signer, localCommit.Htlcs, keyRing, &chanState.LocalChanCfg,
 		&chanState.RemoteChanCfg, commitTx, chanState.ChanType,
-		chanState.IsInitiator, leaseExpiry, chanState,
-		auxResult.AuxLeaves, auxResolver,
+		chanState.IsInitiator, leaseExpiry, auxResult.AuxLeaves,
 	)
 	if err != nil {
 		return nil, fmt.Errorf("unable to gen htlc resolution: %w", err)
@@ -8153,14 +7919,12 @@ func NewLocalForceCloseSummary(chanState *channeldb.OpenChannel,
 	}
 
 	return &LocalForceCloseSummary{
-		ChanPoint:    chanState.FundingOutpoint,
-		CloseTx:      commitTx,
-		ChanSnapshot: *chanState.Snapshot(),
-		ContractResolutions: fn.Some(ContractResolutions{
-			CommitResolution: commitResolution,
-			HtlcResolutions:  htlcResolutions,
-			AnchorResolution: anchorResolution,
-		}),
+		ChanPoint:        chanState.FundingOutpoint,
+		CloseTx:          commitTx,
+		CommitResolution: commitResolution,
+		HtlcResolutions:  htlcResolutions,
+		ChanSnapshot:     *chanState.Snapshot(),
+		AnchorResolution: anchorResolution,
 	}, nil
 }
 
diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go
index 36ff75ede..6d61729a4 100644
--- a/lnwallet/commitment.go
+++ b/lnwallet/commitment.go
@@ -1307,7 +1307,7 @@ func findOutputIndexesFromRemote(revocationPreimage *chainhash.Hash,
 		leafStore, func(a AuxLeafStore) fn.Result[CommitDiffAuxResult] {
 			return a.FetchLeavesFromCommit(
 				NewAuxChanState(chanState), chanCommit,
-				*keyRing, lntypes.Remote,
+				*keyRing,
 			)
 		},
 	).Unpack()
diff --git a/lnwallet/mock.go b/lnwallet/mock.go
index 6623d8014..d71b292a7 100644
--- a/lnwallet/mock.go
+++ b/lnwallet/mock.go
@@ -19,7 +19,6 @@ import (
 	"github.com/lightningnetwork/lnd/chainntnfs"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/fn"
-	"github.com/lightningnetwork/lnd/lntypes"
 	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
 	"github.com/lightningnetwork/lnd/tlv"
 	"github.com/stretchr/testify/mock"
@@ -421,8 +420,8 @@ func (*MockAuxLeafStore) FetchLeavesFromView(
 // correspond to the passed aux blob, and an existing channel
 // commitment.
 func (*MockAuxLeafStore) FetchLeavesFromCommit(_ AuxChanState,
-	_ channeldb.ChannelCommitment, _ CommitmentKeyRing,
-	_ lntypes.ChannelParty) fn.Result[CommitDiffAuxResult] {
+	_ channeldb.ChannelCommitment,
+	_ CommitmentKeyRing) fn.Result[CommitDiffAuxResult] {
 
 	return fn.Ok(CommitDiffAuxResult{})
 }
diff --git a/log.go b/log.go
index c88208ef3..343d86f38 100644
--- a/log.go
+++ b/log.go
@@ -46,6 +46,7 @@ import (
 	"github.com/lightningnetwork/lnd/peernotifier"
 	"github.com/lightningnetwork/lnd/routing"
 	"github.com/lightningnetwork/lnd/routing/blindedpath"
+	"github.com/lightningnetwork/lnd/routing/localchans"
 	"github.com/lightningnetwork/lnd/rpcperms"
 	"github.com/lightningnetwork/lnd/signal"
 	"github.com/lightningnetwork/lnd/sweep"
@@ -172,6 +173,7 @@ func SetupLoggers(root *build.SubLoggerManager, interceptor signal.Interceptor)
 	AddSubLogger(root, "CHFD", interceptor, chanfunding.UseLogger)
 	AddSubLogger(root, "PEER", interceptor, peer.UseLogger)
 	AddSubLogger(root, "CHCL", interceptor, chancloser.UseLogger)
+	AddSubLogger(root, "LCHN", interceptor, localchans.UseLogger)
 
 	AddSubLogger(root, routing.Subsystem, interceptor, routing.UseLogger)
 	AddSubLogger(root, routerrpc.Subsystem, interceptor, routerrpc.UseLogger)
diff --git a/peer/brontide.go b/peer/brontide.go
index 50d511101..ae4d373ca 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -1467,18 +1467,10 @@ func (p *Brontide) Disconnect(reason error) {
 
 	// Make sure initialization has completed before we try to tear things
 	// down.
-	//
-	// NOTE: We only read the `startReady` chan if the peer has been
-	// started, otherwise we will skip reading it as this chan won't be
-	// closed, hence blocks forever.
-	if atomic.LoadInt32(&p.started) == 1 {
-		p.log.Debugf("Started, waiting on startReady signal")
-
-		select {
-		case <-p.startReady:
-		case <-p.quit:
-			return
-		}
+	select {
+	case <-p.startReady:
+	case <-p.quit:
+		return
 	}
 
 	err := fmt.Errorf("disconnecting %s, reason: %v", p, reason)
diff --git a/protofsm/daemon_events.go b/protofsm/daemon_events.go
deleted file mode 100644
index e5de0b695..000000000
--- a/protofsm/daemon_events.go
+++ /dev/null
@@ -1,122 +0,0 @@
-package protofsm
-
-import (
-	"github.com/btcsuite/btcd/btcec/v2"
-	"github.com/btcsuite/btcd/chaincfg/chainhash"
-	"github.com/btcsuite/btcd/wire"
-	"github.com/lightningnetwork/lnd/chainntnfs"
-	"github.com/lightningnetwork/lnd/fn"
-	"github.com/lightningnetwork/lnd/lnwire"
-)
-
-// DaemonEvent is a special event that can be emitted by a state transition
-// function. A state machine can use this to perform side effects, such as
-// sending a message to a peer, or broadcasting a transaction.
-type DaemonEvent interface {
-	daemonSealed()
-}
-
-// DaemonEventSet is a set of daemon events that can be emitted by a state
-// transition.
-type DaemonEventSet []DaemonEvent
-
-// DaemonEvents is a special type constraint that enumerates all the possible
-// types of daemon events.
-type DaemonEvents interface {
-	SendMsgEvent[any] | BroadcastTxn | RegisterSpend[any] |
-		RegisterConf[any]
-}
-
-// SendPredicate is a function that returns true if the target message should
-// sent.
-type SendPredicate = func() bool
-
-// SendMsgEvent is a special event that can be emitted by a state transition
-// that instructs the daemon to send the contained message to the target peer.
-type SendMsgEvent[Event any] struct {
-	// TargetPeer is the peer to send the message to.
-	TargetPeer btcec.PublicKey
-
-	// Msgs is the set of messages to send to the target peer.
-	Msgs []lnwire.Message
-
-	// SendWhen implements a system for a conditional send once a special
-	// send predicate has been met.
-	//
-	// TODO(roasbeef): contrast with usage of OnCommitFlush, etc
-	SendWhen fn.Option[SendPredicate]
-
-	// PostSendEvent is an optional event that is to be emitted after the
-	// message has been sent. If a SendWhen is specified, then this will
-	// only be executed after that returns true to unblock the send.
-	PostSendEvent fn.Option[Event]
-}
-
-// daemonSealed indicates that this struct is a DaemonEvent instance.
-func (s *SendMsgEvent[E]) daemonSealed() {}
-
-// BroadcastTxn indicates the target transaction should be broadcast to the
-// network.
-type BroadcastTxn struct {
-	// Tx is the transaction to broadcast.
-	Tx *wire.MsgTx
-
-	// Label is an optional label to attach to the transaction.
-	Label string
-}
-
-// daemonSealed indicates that this struct is a DaemonEvent instance.
-func (b *BroadcastTxn) daemonSealed() {}
-
-// SpendMapper is a function that's used to map a spend notification to a
-// custom state machine event.
-type SpendMapper[Event any] func(*chainntnfs.SpendDetail) Event
-
-// RegisterSpend is used to request that a certain event is sent into the state
-// machine once the specified outpoint has been spent.
-type RegisterSpend[Event any] struct {
-	// OutPoint is the outpoint on chain to watch.
-	OutPoint wire.OutPoint
-
-	// PkScript is the script that we expect to be spent along with the
-	// outpoint.
-	PkScript []byte
-
-	// HeightHint is a value used to give the chain scanner a hint on how
-	// far back it needs to start its search.
-	HeightHint uint32
-
-	// PostSpendEvent is a special spend mapper, that if present, will be
-	// used to map the protofsm spend event to a custom event.
-	PostSpendEvent fn.Option[SpendMapper[Event]]
-}
-
-// daemonSealed indicates that this struct is a DaemonEvent instance.
-func (r *RegisterSpend[E]) daemonSealed() {}
-
-// RegisterConf is used to request that a certain event is sent into the state
-// machien once the specified outpoint has been spent.
-type RegisterConf[Event any] struct {
-	// Txid is the txid of the txn we want to watch the chain for.
-	Txid chainhash.Hash
-
-	// PkScript is the script that we expect to be created along with the
-	// outpoint.
-	PkScript []byte
-
-	// HeightHint is a value used to give the chain scanner a hint on how
-	// far back it needs to start its search.
-	HeightHint uint32
-
-	// NumConfs is the number of confirmations that the spending
-	// transaction needs to dispatch an event.
-	NumConfs fn.Option[uint32]
-
-	// PostConfEvent is an event that's sent back to the requester once the
-	// transaction specified above has confirmed in the chain with
-	// sufficient depth.
-	PostConfEvent fn.Option[Event]
-}
-
-// daemonSealed indicates that this struct is a DaemonEvent instance.
-func (r *RegisterConf[E]) daemonSealed() {}
diff --git a/protofsm/msg_mapper.go b/protofsm/msg_mapper.go
deleted file mode 100644
index b96d677e6..000000000
--- a/protofsm/msg_mapper.go
+++ /dev/null
@@ -1,15 +0,0 @@
-package protofsm
-
-import (
-	"github.com/lightningnetwork/lnd/fn"
-	"github.com/lightningnetwork/lnd/lnwire"
-)
-
-// MsgMapper is used to map incoming wire messages into a FSM event. This is
-// useful to decouple the translation of an outside or wire message into an
-// event type that can be understood by the FSM.
-type MsgMapper[Event any] interface {
-	// MapMsg maps a wire message into a FSM event. If the message is not
-	// mappable, then an None is returned.
-	MapMsg(msg lnwire.Message) fn.Option[Event]
-}
diff --git a/protofsm/state_machine.go b/protofsm/state_machine.go
deleted file mode 100644
index ecbd74834..000000000
--- a/protofsm/state_machine.go
+++ /dev/null
@@ -1,670 +0,0 @@
-package protofsm
-
-import (
-	"context"
-	"fmt"
-	"sync"
-	"time"
-
-	"github.com/btcsuite/btcd/btcec/v2"
-	"github.com/btcsuite/btcd/chaincfg/chainhash"
-	"github.com/btcsuite/btcd/wire"
-	"github.com/lightningnetwork/lnd/chainntnfs"
-	"github.com/lightningnetwork/lnd/fn"
-	"github.com/lightningnetwork/lnd/lnutils"
-	"github.com/lightningnetwork/lnd/lnwire"
-)
-
-const (
-	// pollInterval is the interval at which we'll poll the SendWhen
-	// predicate if specified.
-	pollInterval = time.Millisecond * 100
-)
-
-// EmittedEvent is a special type that can be emitted by a state transition.
-// This can container internal events which are to be routed back to the state,
-// or external events which are to be sent to the daemon.
-type EmittedEvent[Event any] struct {
-	// InternalEvent is an optional internal event that is to be routed
-	// back to the target state. This enables state to trigger one or many
-	// state transitions without a new external event.
-	InternalEvent fn.Option[[]Event]
-
-	// ExternalEvent is an optional external event that is to be sent to
-	// the daemon for dispatch. Usually, this is some form of I/O.
-	ExternalEvents fn.Option[DaemonEventSet]
-}
-
-// StateTransition is a state transition type. It denotes the next state to go
-// to, and also the set of events to emit.
-type StateTransition[Event any, Env Environment] struct {
-	// NextState is the next state to transition to.
-	NextState State[Event, Env]
-
-	// NewEvents is the set of events to emit.
-	NewEvents fn.Option[EmittedEvent[Event]]
-}
-
-// Environment is an abstract interface that represents the environment that
-// the state machine will execute using. From the PoV of the main state machine
-// executor, we just care about being able to clean up any resources that were
-// allocated by the environment.
-type Environment interface {
-	// Name returns the name of the environment. This is used to uniquely
-	// identify the environment of related state machines.
-	Name() string
-}
-
-// State defines an abstract state along, namely its state transition function
-// that takes as input an event and an environment, and returns a state
-// transition (next state, and set of events to emit). As state can also either
-// be terminal, or not, a terminal event causes state execution to halt.
-type State[Event any, Env Environment] interface {
-	// ProcessEvent takes an event and an environment, and returns a new
-	// state transition. This will be iteratively called until either a
-	// terminal state is reached, or no further internal events are
-	// emitted.
-	ProcessEvent(event Event, env Env) (*StateTransition[Event, Env], error)
-
-	// IsTerminal returns true if this state is terminal, and false
-	// otherwise.
-	IsTerminal() bool
-
-	// TODO(roasbeef): also add state serialization?
-}
-
-// DaemonAdapters is a set of methods that server as adapters to bridge the
-// pure world of the FSM to the real world of the daemon. These will be used to
-// do things like broadcast transactions, or send messages to peers.
-type DaemonAdapters interface {
-	// SendMessages sends the target set of messages to the target peer.
-	SendMessages(btcec.PublicKey, []lnwire.Message) error
-
-	// BroadcastTransaction broadcasts a transaction with the target label.
-	BroadcastTransaction(*wire.MsgTx, string) error
-
-	// RegisterConfirmationsNtfn registers an intent to be notified once
-	// txid reaches numConfs confirmations. We also pass in the pkScript as
-	// the default light client instead needs to match on scripts created
-	// in the block. If a nil txid is passed in, then not only should we
-	// match on the script, but we should also dispatch once the
-	// transaction containing the script reaches numConfs confirmations.
-	// This can be useful in instances where we only know the script in
-	// advance, but not the transaction containing it.
-	//
-	// TODO(roasbeef): could abstract further?
-	RegisterConfirmationsNtfn(txid *chainhash.Hash, pkScript []byte,
-		numConfs, heightHint uint32,
-		opts ...chainntnfs.NotifierOption,
-	) (*chainntnfs.ConfirmationEvent, error)
-
-	// RegisterSpendNtfn registers an intent to be notified once the target
-	// outpoint is successfully spent within a transaction. The script that
-	// the outpoint creates must also be specified. This allows this
-	// interface to be implemented by BIP 158-like filtering.
-	RegisterSpendNtfn(outpoint *wire.OutPoint, pkScript []byte,
-		heightHint uint32) (*chainntnfs.SpendEvent, error)
-}
-
-// stateQuery is used by outside callers to query the internal state of the
-// state machine.
-type stateQuery[Event any, Env Environment] struct {
-	// CurrentState is a channel that will be sent the current state of the
-	// state machine.
-	CurrentState chan State[Event, Env]
-}
-
-// StateMachine represents an abstract FSM that is able to process new incoming
-// events and drive a state machine to termination. This implementation uses
-// type params to abstract over the types of events and environment. Events
-// trigger new state transitions, that use the environment to perform some
-// action.
-//
-// TODO(roasbeef): terminal check, daemon event execution, init?
-type StateMachine[Event any, Env Environment] struct {
-	cfg StateMachineCfg[Event, Env]
-
-	// events is the channel that will be used to send new events to the
-	// FSM.
-	events chan Event
-
-	// newStateEvents is an EventDistributor that will be used to notify
-	// any relevant callers of new state transitions that occur.
-	newStateEvents *fn.EventDistributor[State[Event, Env]]
-
-	// stateQuery is a channel that will be used by outside callers to
-	// query the internal state machine state.
-	stateQuery chan stateQuery[Event, Env]
-
-	wg   fn.GoroutineManager
-	quit chan struct{}
-
-	startOnce sync.Once
-	stopOnce  sync.Once
-}
-
-// ErrorReporter is an interface that's used to report errors that occur during
-// state machine execution.
-type ErrorReporter interface {
-	// ReportError is a method that's used to report an error that occurred
-	// during state machine execution.
-	ReportError(err error)
-}
-
-// StateMachineCfg is a configuration struct that's used to create a new state
-// machine.
-type StateMachineCfg[Event any, Env Environment] struct {
-	// ErrorReporter is used to report errors that occur during state
-	// transitions.
-	ErrorReporter ErrorReporter
-
-	// Daemon is a set of adapters that will be used to bridge the FSM to
-	// the daemon.
-	Daemon DaemonAdapters
-
-	// InitialState is the initial state of the state machine.
-	InitialState State[Event, Env]
-
-	// Env is the environment that the state machine will use to execute.
-	Env Env
-
-	// InitEvent is an optional event that will be sent to the state
-	// machine as if it was emitted at the onset of the state machine. This
-	// can be used to set up tracking state such as a txid confirmation
-	// event.
-	InitEvent fn.Option[DaemonEvent]
-
-	// MsgMapper is an optional message mapper that can be used to map
-	// normal wire messages into FSM events.
-	MsgMapper fn.Option[MsgMapper[Event]]
-
-	// CustomPollInterval is an optional custom poll interval that can be
-	// used to set a quicker interval for tests.
-	CustomPollInterval fn.Option[time.Duration]
-}
-
-// NewStateMachine creates a new state machine given a set of daemon adapters,
-// an initial state, an environment, and an event to process as if emitted at
-// the onset of the state machine. Such an event can be used to set up tracking
-// state such as a txid confirmation event.
-func NewStateMachine[Event any, Env Environment](cfg StateMachineCfg[Event, Env], //nolint:lll
-) StateMachine[Event, Env] {
-
-	return StateMachine[Event, Env]{
-		cfg:            cfg,
-		events:         make(chan Event, 1),
-		stateQuery:     make(chan stateQuery[Event, Env]),
-		wg:             *fn.NewGoroutineManager(context.Background()),
-		newStateEvents: fn.NewEventDistributor[State[Event, Env]](),
-		quit:           make(chan struct{}),
-	}
-}
-
-// Start starts the state machine. This will spawn a goroutine that will drive
-// the state machine to completion.
-func (s *StateMachine[Event, Env]) Start() {
-	s.startOnce.Do(func() {
-		_ = s.wg.Go(func(ctx context.Context) {
-			s.driveMachine()
-		})
-	})
-}
-
-// Stop stops the state machine. This will block until the state machine has
-// reached a stopping point.
-func (s *StateMachine[Event, Env]) Stop() {
-	s.stopOnce.Do(func() {
-		close(s.quit)
-		s.wg.Stop()
-	})
-}
-
-// SendEvent sends a new event to the state machine.
-//
-// TODO(roasbeef): bool if processed?
-func (s *StateMachine[Event, Env]) SendEvent(event Event) {
-	log.Debugf("FSM(%v): sending event: %v", s.cfg.Env.Name(),
-		lnutils.SpewLogClosure(event),
-	)
-
-	select {
-	case s.events <- event:
-	case <-s.quit:
-		return
-	}
-}
-
-// CanHandle returns true if the target message can be routed to the state
-// machine.
-func (s *StateMachine[Event, Env]) CanHandle(msg lnwire.Message) bool {
-	cfgMapper := s.cfg.MsgMapper
-	return fn.MapOptionZ(cfgMapper, func(mapper MsgMapper[Event]) bool {
-		return mapper.MapMsg(msg).IsSome()
-	})
-}
-
-// Name returns the name of the state machine's environment.
-func (s *StateMachine[Event, Env]) Name() string {
-	return s.cfg.Env.Name()
-}
-
-// SendMessage attempts to send a wire message to the state machine. If the
-// message can be mapped using the default message mapper, then true is
-// returned indicating that the message was processed. Otherwise, false is
-// returned.
-func (s *StateMachine[Event, Env]) SendMessage(msg lnwire.Message) bool {
-	// If we have no message mapper, then return false as we can't process
-	// this message.
-	if !s.cfg.MsgMapper.IsSome() {
-		return false
-	}
-
-	log.Debugf("FSM(%v): sending msg: %v", s.cfg.Env.Name(),
-		lnutils.SpewLogClosure(msg),
-	)
-
-	// Otherwise, try to map the message using the default message mapper.
-	// If we can't extract an event, then we'll return false to indicate
-	// that the message wasn't processed.
-	var processed bool
-	s.cfg.MsgMapper.WhenSome(func(mapper MsgMapper[Event]) {
-		event := mapper.MapMsg(msg)
-
-		event.WhenSome(func(event Event) {
-			s.SendEvent(event)
-
-			processed = true
-		})
-	})
-
-	return processed
-}
-
-// CurrentState returns the current state of the state machine.
-func (s *StateMachine[Event, Env]) CurrentState() (State[Event, Env], error) {
-	query := stateQuery[Event, Env]{
-		CurrentState: make(chan State[Event, Env], 1),
-	}
-
-	if !fn.SendOrQuit(s.stateQuery, query, s.quit) {
-		return nil, fmt.Errorf("state machine is shutting down")
-	}
-
-	return fn.RecvOrTimeout(query.CurrentState, time.Second)
-}
-
-// StateSubscriber represents an active subscription to be notified of new
-// state transitions.
-type StateSubscriber[E any, F Environment] *fn.EventReceiver[State[E, F]]
-
-// RegisterStateEvents registers a new event listener that will be notified of
-// new state transitions.
-func (s *StateMachine[Event, Env]) RegisterStateEvents() StateSubscriber[
-	Event, Env] {
-
-	subscriber := fn.NewEventReceiver[State[Event, Env]](10)
-
-	// TODO(roasbeef): instead give the state and the input event?
-
-	s.newStateEvents.RegisterSubscriber(subscriber)
-
-	return subscriber
-}
-
-// RemoveStateSub removes the target state subscriber from the set of active
-// subscribers.
-func (s *StateMachine[Event, Env]) RemoveStateSub(sub StateSubscriber[
-	Event, Env]) {
-
-	_ = s.newStateEvents.RemoveSubscriber(sub)
-}
-
-// executeDaemonEvent executes a daemon event, which is a special type of event
-// that can be emitted as part of the state transition function of the state
-// machine. An error is returned if the type of event is unknown.
-func (s *StateMachine[Event, Env]) executeDaemonEvent(
-	event DaemonEvent) error {
-
-	switch daemonEvent := event.(type) {
-	// This is a send message event, so we'll send the event, and also mind
-	// any preconditions as well as post-send events.
-	case *SendMsgEvent[Event]:
-		sendAndCleanUp := func() error {
-			log.Debugf("FSM(%v): sending message to target(%x): "+
-				"%v", s.cfg.Env.Name(),
-				daemonEvent.TargetPeer.SerializeCompressed(),
-				lnutils.SpewLogClosure(daemonEvent.Msgs),
-			)
-
-			err := s.cfg.Daemon.SendMessages(
-				daemonEvent.TargetPeer, daemonEvent.Msgs,
-			)
-			if err != nil {
-				return fmt.Errorf("unable to send msgs: %w",
-					err)
-			}
-
-			// If a post-send event was specified, then we'll funnel
-			// that back into the main state machine now as well.
-			return fn.MapOptionZ(daemonEvent.PostSendEvent, func(event Event) error { //nolint:lll
-				return s.wg.Go(func(ctx context.Context) {
-					log.Debugf("FSM(%v): sending "+
-						"post-send event: %v",
-						s.cfg.Env.Name(),
-						lnutils.SpewLogClosure(event),
-					)
-
-					s.SendEvent(event)
-				})
-			})
-		}
-
-		// If this doesn't have a SendWhen predicate, then we can just
-		// send it off right away.
-		if !daemonEvent.SendWhen.IsSome() {
-			return sendAndCleanUp()
-		}
-
-		// Otherwise, this has a SendWhen predicate, so we'll need
-		// launch a goroutine to poll the SendWhen, then send only once
-		// the predicate is true.
-		return s.wg.Go(func(ctx context.Context) {
-			predicateTicker := time.NewTicker(
-				s.cfg.CustomPollInterval.UnwrapOr(pollInterval),
-			)
-			defer predicateTicker.Stop()
-
-			log.Infof("FSM(%v): waiting for send predicate to "+
-				"be true", s.cfg.Env.Name())
-
-			for {
-				select {
-				case <-predicateTicker.C:
-					canSend := fn.MapOptionZ(
-						daemonEvent.SendWhen,
-						func(pred SendPredicate) bool {
-							return pred()
-						},
-					)
-
-					if canSend {
-						log.Infof("FSM(%v): send "+
-							"active predicate",
-							s.cfg.Env.Name())
-
-						err := sendAndCleanUp()
-						if err != nil {
-							//nolint:lll
-							log.Errorf("FSM(%v): unable to send message: %v", err)
-						}
-
-						return
-					}
-
-				case <-ctx.Done():
-					return
-				}
-			}
-		})
-
-	// If this is a broadcast transaction event, then we'll broadcast with
-	// the label attached.
-	case *BroadcastTxn:
-		log.Debugf("FSM(%v): broadcasting txn, txid=%v",
-			s.cfg.Env.Name(), daemonEvent.Tx.TxHash())
-
-		err := s.cfg.Daemon.BroadcastTransaction(
-			daemonEvent.Tx, daemonEvent.Label,
-		)
-		if err != nil {
-			return fmt.Errorf("unable to broadcast txn: %w", err)
-		}
-
-		return nil
-
-	// The state machine has requested a new event to be sent once a
-	// transaction spending a specified outpoint has confirmed.
-	case *RegisterSpend[Event]:
-		log.Debugf("FSM(%v): registering spend: %v", s.cfg.Env.Name(),
-			daemonEvent.OutPoint)
-
-		spendEvent, err := s.cfg.Daemon.RegisterSpendNtfn(
-			&daemonEvent.OutPoint, daemonEvent.PkScript,
-			daemonEvent.HeightHint,
-		)
-		if err != nil {
-			return fmt.Errorf("unable to register spend: %w", err)
-		}
-
-		return s.wg.Go(func(ctx context.Context) {
-			for {
-				select {
-				case spend, ok := <-spendEvent.Spend:
-					if !ok {
-						return
-					}
-
-					// If there's a post-send event, then
-					// we'll send that into the current
-					// state now.
-					postSpend := daemonEvent.PostSpendEvent
-					postSpend.WhenSome(func(f SpendMapper[Event]) { //nolint:lll
-						customEvent := f(spend)
-						s.SendEvent(customEvent)
-					})
-
-					return
-
-				case <-ctx.Done():
-					return
-				}
-			}
-		})
-
-	// The state machine has requested a new event to be sent once a
-	// specified txid+pkScript pair has confirmed.
-	case *RegisterConf[Event]:
-		log.Debugf("FSM(%v): registering conf: %v", s.cfg.Env.Name(),
-			daemonEvent.Txid)
-
-		numConfs := daemonEvent.NumConfs.UnwrapOr(1)
-		confEvent, err := s.cfg.Daemon.RegisterConfirmationsNtfn(
-			&daemonEvent.Txid, daemonEvent.PkScript,
-			numConfs, daemonEvent.HeightHint,
-		)
-		if err != nil {
-			return fmt.Errorf("unable to register conf: %w", err)
-		}
-
-		return s.wg.Go(func(ctx context.Context) {
-			for {
-				select {
-				case <-confEvent.Confirmed:
-					// If there's a post-conf event, then
-					// we'll send that into the current
-					// state now.
-					//
-					// TODO(roasbeef): refactor to
-					// dispatchAfterRecv w/ above
-					postConf := daemonEvent.PostConfEvent
-					postConf.WhenSome(func(e Event) {
-						s.SendEvent(e)
-					})
-
-					return
-
-				case <-ctx.Done():
-					return
-				}
-			}
-		})
-	}
-
-	return fmt.Errorf("unknown daemon event: %T", event)
-}
-
-// applyEvents applies a new event to the state machine. This will continue
-// until no further events are emitted by the state machine. Along the way,
-// we'll also ensure to execute any daemon events that are emitted.
-func (s *StateMachine[Event, Env]) applyEvents(currentState State[Event, Env],
-	newEvent Event) (State[Event, Env], error) {
-
-	log.Debugf("FSM(%v): applying new event", s.cfg.Env.Name(),
-		lnutils.SpewLogClosure(newEvent),
-	)
-	eventQueue := fn.NewQueue(newEvent)
-
-	// Given the next event to handle, we'll process the event, then add
-	// any new emitted internal events to our event queue. This continues
-	// until we reach a terminal state, or we run out of internal events to
-	// process.
-	//
-	//nolint:lll
-	for nextEvent := eventQueue.Dequeue(); nextEvent.IsSome(); nextEvent = eventQueue.Dequeue() {
-		err := fn.MapOptionZ(nextEvent, func(event Event) error {
-			log.Debugf("FSM(%v): processing event: %v",
-				s.cfg.Env.Name(),
-				lnutils.SpewLogClosure(event),
-			)
-
-			// Apply the state transition function of the current
-			// state given this new event and our existing env.
-			transition, err := currentState.ProcessEvent(
-				event, s.cfg.Env,
-			)
-			if err != nil {
-				return err
-			}
-
-			newEvents := transition.NewEvents
-			err = fn.MapOptionZ(newEvents, func(events EmittedEvent[Event]) error { //nolint:lll
-				// With the event processed, we'll process any
-				// new daemon events that were emitted as part
-				// of this new state transition.
-				//
-				//nolint:lll
-				err := fn.MapOptionZ(events.ExternalEvents, func(dEvents DaemonEventSet) error {
-					log.Debugf("FSM(%v): processing "+
-						"daemon %v daemon events",
-						s.cfg.Env.Name(), len(dEvents))
-
-					for _, dEvent := range dEvents {
-						err := s.executeDaemonEvent(
-							dEvent,
-						)
-						if err != nil {
-							return err
-						}
-					}
-
-					return nil
-				})
-				if err != nil {
-					return err
-				}
-
-				// Next, we'll add any new emitted events to
-				// our event queue.
-				//
-				//nolint:lll
-				events.InternalEvent.WhenSome(func(es []Event) {
-					for _, inEvent := range es {
-						log.Debugf("FSM(%v): adding "+
-							"new internal event "+
-							"to queue: %v",
-							s.cfg.Env.Name(),
-							lnutils.SpewLogClosure(
-								inEvent,
-							),
-						)
-
-						eventQueue.Enqueue(inEvent)
-					}
-				})
-
-				return nil
-			})
-			if err != nil {
-				return err
-			}
-
-			log.Infof("FSM(%v): state transition: from_state=%T, "+
-				"to_state=%T",
-				s.cfg.Env.Name(), currentState,
-				transition.NextState)
-
-			// With our events processed, we'll now update our
-			// internal state.
-			currentState = transition.NextState
-
-			// Notify our subscribers of the new state transition.
-			//
-			// TODO(roasbeef): will only give us the outer state?
-			//  * let FSMs choose which state to emit?
-			s.newStateEvents.NotifySubscribers(currentState)
-
-			return nil
-		})
-		if err != nil {
-			return currentState, err
-		}
-	}
-
-	return currentState, nil
-}
-
-// driveMachine is the main event loop of the state machine. It accepts any new
-// incoming events, and then drives the state machine forward until it reaches
-// a terminal state.
-func (s *StateMachine[Event, Env]) driveMachine() {
-	log.Debugf("FSM(%v): starting state machine", s.cfg.Env.Name())
-
-	currentState := s.cfg.InitialState
-
-	// Before we start, if we have an init daemon event specified, then
-	// we'll handle that now.
-	err := fn.MapOptionZ(s.cfg.InitEvent, func(event DaemonEvent) error {
-		return s.executeDaemonEvent(event)
-	})
-	if err != nil {
-		log.Errorf("unable to execute init event: %w", err)
-		return
-	}
-
-	// We just started driving the state machine, so we'll notify our
-	// subscribers of this starting state.
-	s.newStateEvents.NotifySubscribers(currentState)
-
-	for {
-		select {
-		// We have a new external event, so we'll drive the state
-		// machine forward until we either run out of internal events,
-		// or we reach a terminal state.
-		case newEvent := <-s.events:
-			newState, err := s.applyEvents(currentState, newEvent)
-			if err != nil {
-				s.cfg.ErrorReporter.ReportError(err)
-
-				log.Errorf("unable to apply event: %v", err)
-
-				// An error occurred, so we'll tear down the
-				// entire state machine as we can't proceed.
-				go s.Stop()
-
-				return
-			}
-
-			currentState = newState
-
-		// An outside caller is querying our state, so we'll return the
-		// latest state.
-		case stateQuery := <-s.stateQuery:
-			if !fn.SendOrQuit(stateQuery.CurrentState, currentState, s.quit) { //nolint:lll
-				return
-			}
-
-		case <-s.wg.Done():
-			return
-		}
-	}
-}
diff --git a/routing/chainview/bitcoind.go b/routing/chainview/bitcoind.go
index cbddb37c1..56e30c24a 100644
--- a/routing/chainview/bitcoind.go
+++ b/routing/chainview/bitcoind.go
@@ -136,7 +136,6 @@ func (b *BitcoindFilteredChainView) Stop() error {
 	// Shutdown the rpc client, this gracefully disconnects from bitcoind's
 	// zmq socket, and cleans up all related resources.
 	b.chainClient.Stop()
-	b.chainClient.WaitForShutdown()
 
 	b.blockQueue.Stop()
 
diff --git a/routing/chainview/btcd.go b/routing/chainview/btcd.go
index cf08abafe..54c2ee4db 100644
--- a/routing/chainview/btcd.go
+++ b/routing/chainview/btcd.go
@@ -146,7 +146,6 @@ func (b *BtcdFilteredChainView) Stop() error {
 	// Shutdown the rpc client, this gracefully disconnects from btcd, and
 	// cleans up all related resources.
 	b.btcdConn.Shutdown()
-	b.btcdConn.WaitForShutdown()
 
 	b.blockQueue.Stop()
 
diff --git a/protofsm/log.go b/routing/localchans/log.go
similarity index 53%
rename from protofsm/log.go
rename to routing/localchans/log.go
index 8ff9c1b62..fdd233c04 100644
--- a/protofsm/log.go
+++ b/routing/localchans/log.go
@@ -1,29 +1,31 @@
-package protofsm
+package localchans
 
 import (
-	"github.com/btcsuite/btclog"
+	"github.com/btcsuite/btclog/v2"
 	"github.com/lightningnetwork/lnd/build"
 )
 
-// log is a logger that is initialized with no output filters.  This
-// means the package will not perform any logging by default until the caller
-// requests it.
+// log is a logger that is initialized with no output filters.  This means the
+// package will not perform any logging by default until the caller requests
+// it.
 var log btclog.Logger
 
+const Subsystem = "LCHN"
+
 // The default amount of logging is none.
 func init() {
-	UseLogger(build.NewSubLogger("PFSM", nil))
+	UseLogger(build.NewSubLogger(Subsystem, nil))
 }
 
-// DisableLog disables all library log output.  Logging output is disabled
-// by default until UseLogger is called.
+// DisableLog disables all library log output.  Logging output is disabled by
+// default until UseLogger is called.
 func DisableLog() {
 	UseLogger(btclog.Disabled)
 }
 
-// UseLogger uses a specified Logger to output package logging info.
-// This should be used in preference to SetLogWriter if the caller is also
-// using btclog.
+// UseLogger uses a specified Logger to output package logging info.  This
+// should be used in preference to SetLogWriter if the caller is also using
+// btclog.
 func UseLogger(logger btclog.Logger) {
 	log = logger
 }
diff --git a/routing/localchans/manager.go b/routing/localchans/manager.go
index f0f9b88de..2639928e0 100644
--- a/routing/localchans/manager.go
+++ b/routing/localchans/manager.go
@@ -1,10 +1,13 @@
 package localchans
 
 import (
+	"bytes"
 	"errors"
 	"fmt"
 	"sync"
+	"time"
 
+	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/channeldb/models"
@@ -19,6 +22,12 @@ import (
 // Manager manages the node's local channels. The only operation that is
 // currently implemented is updating forwarding policies.
 type Manager struct {
+	// SelfPub contains the public key of the local node.
+	SelfPub *btcec.PublicKey
+
+	// DefaultRoutingPolicy is the default routing policy.
+	DefaultRoutingPolicy models.ForwardingPolicy
+
 	// UpdateForwardingPolicies is used by the manager to update active
 	// links with a new policy.
 	UpdateForwardingPolicies func(
@@ -30,7 +39,7 @@ type Manager struct {
 		edgesToUpdate []discovery.EdgeWithInfo) error
 
 	// ForAllOutgoingChannels is required to iterate over all our local
-	// channels.
+	// channels. The ChannelEdgePolicy parameter may be nil.
 	ForAllOutgoingChannels func(cb func(kvdb.RTx,
 		*models.ChannelEdgeInfo,
 		*models.ChannelEdgePolicy) error) error
@@ -40,6 +49,9 @@ type Manager struct {
 	FetchChannel func(tx kvdb.RTx, chanPoint wire.OutPoint) (
 		*channeldb.OpenChannel, error)
 
+	// AddEdge is used to add edge/channel to the topology of the router.
+	AddEdge func(edge *models.ChannelEdgeInfo) error
+
 	// policyUpdateLock ensures that the database and the link do not fall
 	// out of sync if there are concurrent fee update calls. Without it,
 	// there is a chance that policy A updates the database, then policy B
@@ -51,7 +63,8 @@ type Manager struct {
 // UpdatePolicy updates the policy for the specified channels on disk and in
 // the active links.
 func (r *Manager) UpdatePolicy(newSchema routing.ChannelPolicy,
-	chanPoints ...wire.OutPoint) ([]*lnrpc.FailedUpdate, error) {
+	createMissingEdge bool, chanPoints ...wire.OutPoint) (
+	[]*lnrpc.FailedUpdate, error) {
 
 	r.policyUpdateLock.Lock()
 	defer r.policyUpdateLock.Unlock()
@@ -69,10 +82,8 @@ func (r *Manager) UpdatePolicy(newSchema routing.ChannelPolicy,
 	var edgesToUpdate []discovery.EdgeWithInfo
 	policiesToUpdate := make(map[wire.OutPoint]models.ForwardingPolicy)
 
-	// Next, we'll loop over all the outgoing channels the router knows of.
-	// If we have a filter then we'll only collected those channels,
-	// otherwise we'll collect them all.
-	err := r.ForAllOutgoingChannels(func(
+	// NOTE: edge may be nil when this function is called.
+	processChan := func(
 		tx kvdb.RTx,
 		info *models.ChannelEdgeInfo,
 		edge *models.ChannelEdgePolicy) error {
@@ -88,8 +99,24 @@ func (r *Manager) UpdatePolicy(newSchema routing.ChannelPolicy,
 		// will be used to report invalid channels later on.
 		delete(unprocessedChans, info.ChannelPoint)
 
+		if edge == nil {
+			log.Errorf("Got nil channel edge policy when updating "+
+				"a channel. Channel point: %v",
+				info.ChannelPoint.String())
+
+			failedUpdates = append(failedUpdates, makeFailureItem(
+				info.ChannelPoint,
+				lnrpc.UpdateFailure_UPDATE_FAILURE_NOT_FOUND,
+				"edge policy not found",
+			))
+
+			return nil
+		}
+
 		// Apply the new policy to the edge.
-		err := r.updateEdge(tx, info.ChannelPoint, edge, newSchema)
+		err := r.updateEdge(
+			tx, info.ChannelPoint, edge, newSchema,
+		)
 		if err != nil {
 			failedUpdates = append(failedUpdates,
 				makeFailureItem(info.ChannelPoint,
@@ -125,7 +152,12 @@ func (r *Manager) UpdatePolicy(newSchema routing.ChannelPolicy,
 		}
 
 		return nil
-	})
+	}
+
+	// Next, we'll loop over all the outgoing channels the router knows of.
+	// If we have a filter then we'll only collect those channels, otherwise
+	// we'll collect them all.
+	err := r.ForAllOutgoingChannels(processChan)
 	if err != nil {
 		return nil, err
 	}
@@ -155,7 +187,37 @@ func (r *Manager) UpdatePolicy(newSchema routing.ChannelPolicy,
 					"not yet confirmed",
 				))
 
+		// If the edge was not found, but the channel is found, that
+		// means the edge is missing in the graph database and should be
+		// recreated. The edge and policy are created in-memory. The
+		// edge is inserted in createEdge below and the policy will be
+		// added to the graph in the PropagateChanPolicyUpdate call
+		// below.
+		case createMissingEdge:
+			log.Warnf("Missing edge for active channel (%s) "+
+				"during policy update. Recreating edge with "+
+				"default policy.",
+				channel.FundingOutpoint.String())
+
+			info, edge, failedUpdate := r.createMissingEdge(
+				channel, newSchema,
+			)
+			if failedUpdate == nil {
+				err = processChan(nil, info, edge)
+				if err != nil {
+					return nil, err
+				}
+			} else {
+				failedUpdates = append(
+					failedUpdates, failedUpdate,
+				)
+			}
+
 		default:
+			log.Warnf("Missing edge for active channel (%s) "+
+				"during policy update. Could not update "+
+				"policy.", channel.FundingOutpoint.String())
+
 			failedUpdates = append(failedUpdates,
 				makeFailureItem(chanPoint,
 					lnrpc.UpdateFailure_UPDATE_FAILURE_UNKNOWN,
@@ -180,11 +242,124 @@ func (r *Manager) UpdatePolicy(newSchema routing.ChannelPolicy,
 	return failedUpdates, nil
 }
 
+func (r *Manager) createMissingEdge(channel *channeldb.OpenChannel,
+	newSchema routing.ChannelPolicy) (*models.ChannelEdgeInfo,
+	*models.ChannelEdgePolicy, *lnrpc.FailedUpdate) {
+
+	info, edge, err := r.createEdge(channel, time.Now())
+	if err != nil {
+		log.Errorf("Failed to recreate missing edge "+
+			"for channel (%s): %v",
+			channel.FundingOutpoint.String(), err)
+
+		return nil, nil, makeFailureItem(
+			channel.FundingOutpoint,
+			lnrpc.UpdateFailure_UPDATE_FAILURE_UNKNOWN,
+			"could not update policies",
+		)
+	}
+
+	// Validate the newly created edge policy with the user defined new
+	// schema before adding the edge to the database.
+	err = r.updateEdge(nil, channel.FundingOutpoint, edge, newSchema)
+	if err != nil {
+		return nil, nil, makeFailureItem(
+			info.ChannelPoint,
+			lnrpc.UpdateFailure_UPDATE_FAILURE_INVALID_PARAMETER,
+			err.Error(),
+		)
+	}
+
+	// Insert the edge into the database to avoid `edge not
+	// found` errors during policy update propagation.
+	err = r.AddEdge(info)
+	if err != nil {
+		log.Errorf("Attempt to add missing edge for "+
+			"channel (%s) errored with: %v",
+			channel.FundingOutpoint.String(), err)
+
+		return nil, nil, makeFailureItem(
+			channel.FundingOutpoint,
+			lnrpc.UpdateFailure_UPDATE_FAILURE_UNKNOWN,
+			"could not add edge",
+		)
+	}
+
+	return info, edge, nil
+}
+
+// createEdge recreates an edge and policy from an open channel in-memory.
+func (r *Manager) createEdge(channel *channeldb.OpenChannel,
+	timestamp time.Time) (*models.ChannelEdgeInfo,
+	*models.ChannelEdgePolicy, error) {
+
+	nodeKey1Bytes := r.SelfPub.SerializeCompressed()
+	nodeKey2Bytes := channel.IdentityPub.SerializeCompressed()
+	bitcoinKey1Bytes := channel.LocalChanCfg.MultiSigKey.PubKey.
+		SerializeCompressed()
+	bitcoinKey2Bytes := channel.RemoteChanCfg.MultiSigKey.PubKey.
+		SerializeCompressed()
+	channelFlags := lnwire.ChanUpdateChanFlags(0)
+
+	// Make it such that node_id_1 is the lexicographically-lesser of the
+	// two compressed keys sorted in ascending lexicographic order.
+	if bytes.Compare(nodeKey2Bytes, nodeKey1Bytes) < 0 {
+		nodeKey1Bytes, nodeKey2Bytes = nodeKey2Bytes, nodeKey1Bytes
+		bitcoinKey1Bytes, bitcoinKey2Bytes = bitcoinKey2Bytes,
+			bitcoinKey1Bytes
+		channelFlags = 1
+	}
+
+	var featureBuf bytes.Buffer
+	err := lnwire.NewRawFeatureVector().Encode(&featureBuf)
+	if err != nil {
+		return nil, nil, fmt.Errorf("unable to encode features: %w",
+			err)
+	}
+
+	info := &models.ChannelEdgeInfo{
+		ChannelID:    channel.ShortChanID().ToUint64(),
+		ChainHash:    channel.ChainHash,
+		Features:     featureBuf.Bytes(),
+		Capacity:     channel.Capacity,
+		ChannelPoint: channel.FundingOutpoint,
+	}
+
+	copy(info.NodeKey1Bytes[:], nodeKey1Bytes)
+	copy(info.NodeKey2Bytes[:], nodeKey2Bytes)
+	copy(info.BitcoinKey1Bytes[:], bitcoinKey1Bytes)
+	copy(info.BitcoinKey2Bytes[:], bitcoinKey2Bytes)
+
+	// Construct a dummy channel edge policy with default values that will
+	// be updated with the new values in the call to processChan below.
+	timeLockDelta := uint16(r.DefaultRoutingPolicy.TimeLockDelta)
+	edge := &models.ChannelEdgePolicy{
+		ChannelID:                 channel.ShortChanID().ToUint64(),
+		LastUpdate:                timestamp,
+		TimeLockDelta:             timeLockDelta,
+		ChannelFlags:              channelFlags,
+		MessageFlags:              lnwire.ChanUpdateRequiredMaxHtlc,
+		FeeBaseMSat:               r.DefaultRoutingPolicy.BaseFee,
+		FeeProportionalMillionths: r.DefaultRoutingPolicy.FeeRate,
+		MinHTLC:                   r.DefaultRoutingPolicy.MinHTLCOut,
+		MaxHTLC:                   r.DefaultRoutingPolicy.MaxHTLC,
+	}
+
+	copy(edge.ToNode[:], channel.IdentityPub.SerializeCompressed())
+
+	return info, edge, nil
+}
+
 // updateEdge updates the given edge with the new schema.
 func (r *Manager) updateEdge(tx kvdb.RTx, chanPoint wire.OutPoint,
 	edge *models.ChannelEdgePolicy,
 	newSchema routing.ChannelPolicy) error {
 
+	channel, err := r.FetchChannel(tx, chanPoint)
+	if err != nil {
+		return err
+	}
+
 	// Update forwarding fee scheme and required time lock delta.
 	edge.FeeBaseMSat = newSchema.BaseFee
 	edge.FeeProportionalMillionths = lnwire.MilliSatoshi(
@@ -192,7 +367,7 @@ func (r *Manager) updateEdge(tx kvdb.RTx, chanPoint wire.OutPoint,
 	)
 
 	// If inbound fees are set, we update the edge with them.
-	err := fn.MapOptionZ(newSchema.InboundFee,
+	err = fn.MapOptionZ(newSchema.InboundFee,
 		func(f models.InboundFee) error {
 			inboundWireFee := f.ToWire()
 			return edge.ExtraOpaqueData.PackRecords(
@@ -206,7 +381,7 @@ func (r *Manager) updateEdge(tx kvdb.RTx, chanPoint wire.OutPoint,
 	edge.TimeLockDelta = uint16(newSchema.TimeLockDelta)
 
 	// Retrieve negotiated channel htlc amt limits.
-	amtMin, amtMax, err := r.getHtlcAmtLimits(tx, chanPoint)
+	amtMin, amtMax, err := r.getHtlcAmtLimits(channel)
 	if err != nil {
 		return err
 	}
@@ -267,14 +442,9 @@ func (r *Manager) updateEdge(tx kvdb.RTx, chanPoint wire.OutPoint,
 
 // getHtlcAmtLimits retrieves the negotiated channel min and max htlc amount
 // constraints.
-func (r *Manager) getHtlcAmtLimits(tx kvdb.RTx, chanPoint wire.OutPoint) (
+func (r *Manager) getHtlcAmtLimits(ch *channeldb.OpenChannel) (
 	lnwire.MilliSatoshi, lnwire.MilliSatoshi, error) {
 
-	ch, err := r.FetchChannel(tx, chanPoint)
-	if err != nil {
-		return 0, 0, err
-	}
-
 	// The max htlc policy field must be less than or equal to the channel
 	// 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
diff --git a/rpcserver.go b/rpcserver.go
index efb6cf54f..cace7fb94 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -2739,14 +2739,6 @@ func (r *rpcServer) CloseChannel(in *lnrpc.CloseChannelRequest,
 			return err
 		}
 
-		// Safety check which should never happen.
-		//
-		// TODO(ziggie): remove pointer as return value from
-		// ForceCloseContract.
-		if closingTx == nil {
-			return fmt.Errorf("force close transaction is nil")
-		}
-
 		closingTxid := closingTx.TxHash()
 
 		// With the transaction broadcast, we send our first update to
@@ -7770,7 +7762,7 @@ func (r *rpcServer) UpdateChannelPolicy(ctx context.Context,
 	// With the scope resolved, we'll now send this to the local channel
 	// manager so it can propagate the new policy for our target channel(s).
 	failedUpdates, err := r.server.localChanMgr.UpdatePolicy(chanPolicy,
-		targetChans...)
+		req.CreateMissingEdge, targetChans...)
 	if err != nil {
 		return nil, err
 	}
diff --git a/server.go b/server.go
index c13175912..212fddebc 100644
--- a/server.go
+++ b/server.go
@@ -196,15 +196,7 @@ type server struct {
 	// to handle dynamic IP changes.
 	lastDetectedIP net.IP
 
-	mu sync.RWMutex
-
-	// peersByPub is a map of the active peers.
-	//
-	// NOTE: The key used here is the raw bytes of the peer's public key to
-	// string conversion, which means it cannot be printed using `%s` as it
-	// will just print the binary.
-	//
-	// TODO(yy): Use the hex string instead.
+	mu         sync.RWMutex
 	peersByPub map[string]*peer.Brontide
 
 	inboundPeers  map[string]*peer.Brontide
@@ -1118,12 +1110,31 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
 		ScidCloser:              scidCloserMan,
 	}, nodeKeyDesc)
 
+	selfVertex := route.Vertex(nodeKeyDesc.PubKey.SerializeCompressed())
 	//nolint:lll
 	s.localChanMgr = &localchans.Manager{
-		ForAllOutgoingChannels:    s.graphBuilder.ForAllOutgoingChannels,
+		SelfPub:              nodeKeyDesc.PubKey,
+		DefaultRoutingPolicy: cc.RoutingPolicy,
+		ForAllOutgoingChannels: func(cb func(kvdb.RTx,
+			*models.ChannelEdgeInfo, *models.ChannelEdgePolicy) error) error {
+
+			return s.graphDB.ForEachNodeChannel(selfVertex,
+				func(tx kvdb.RTx, c *models.ChannelEdgeInfo,
+					e *models.ChannelEdgePolicy,
+					_ *models.ChannelEdgePolicy) error {
+
+					// NOTE: The invoked callback here may
+					// receive a nil channel policy.
+					return cb(tx, c, e)
+				},
+			)
+		},
 		PropagateChanPolicyUpdate: s.authGossiper.PropagateChanPolicyUpdate,
 		UpdateForwardingPolicies:  s.htlcSwitch.UpdateForwardingPolicies,
 		FetchChannel:              s.chanStateDB.FetchChannel,
+		AddEdge: func(edge *models.ChannelEdgeInfo) error {
+			return s.graphBuilder.AddEdge(edge)
+		},
 	}
 
 	utxnStore, err := contractcourt.NewNurseryStore(
@@ -2481,9 +2492,6 @@ func (s *server) Stop() error {
 		if err := s.chanRouter.Stop(); err != nil {
 			srvrLog.Warnf("failed to stop chanRouter: %v", err)
 		}
-		if err := s.graphBuilder.Stop(); err != nil {
-			srvrLog.Warnf("failed to stop graphBuilder %v", err)
-		}
 		if err := s.chainArb.Stop(); err != nil {
 			srvrLog.Warnf("failed to stop chainArb: %v", err)
 		}
@@ -4226,14 +4234,9 @@ func (s *server) addPeer(p *peer.Brontide) {
 		return
 	}
 
-	pubBytes := p.IdentityKey().SerializeCompressed()
-
 	// Ignore new peers if we're shutting down.
 	if s.Stopped() {
-		srvrLog.Infof("Server stopped, skipped adding peer=%x",
-			pubBytes)
 		p.Disconnect(ErrServerShuttingDown)
-
 		return
 	}
 
@@ -4242,9 +4245,8 @@ func (s *server) addPeer(p *peer.Brontide) {
 	// TODO(roasbeef): pipe all requests through to the
 	// queryHandler/peerManager
 
-	// NOTE: This pubStr is a raw bytes to string conversion and will NOT
-	// be human-readable.
-	pubStr := string(pubBytes)
+	pubSer := p.IdentityKey().SerializeCompressed()
+	pubStr := string(pubSer)
 
 	s.peersByPub[pubStr] = p
 
@@ -4257,7 +4259,7 @@ func (s *server) addPeer(p *peer.Brontide) {
 	// Inform the peer notifier of a peer online event so that it can be reported
 	// to clients listening for peer events.
 	var pubKey [33]byte
-	copy(pubKey[:], pubBytes)
+	copy(pubKey[:], pubSer)
 
 	s.peerNotifier.NotifyPeerOnline(pubKey)
 }
@@ -4274,12 +4276,8 @@ func (s *server) addPeer(p *peer.Brontide) {
 func (s *server) peerInitializer(p *peer.Brontide) {
 	defer s.wg.Done()
 
-	pubBytes := p.IdentityKey().SerializeCompressed()
-
 	// Avoid initializing peers while the server is exiting.
 	if s.Stopped() {
-		srvrLog.Infof("Server stopped, skipped initializing peer=%x",
-			pubBytes)
 		return
 	}
 
@@ -4297,6 +4295,8 @@ func (s *server) peerInitializer(p *peer.Brontide) {
 	s.wg.Add(1)
 	go s.peerTerminationWatcher(p, ready)
 
+	pubBytes := p.IdentityKey().SerializeCompressed()
+
 	// Start the peer! If an error occurs, we Disconnect the peer, which
 	// will unblock the peerTerminationWatcher.
 	if err := p.Start(); err != nil {
diff --git a/sweep/fee_bumper.go b/sweep/fee_bumper.go
index adb4db65e..fd3dfba9e 100644
--- a/sweep/fee_bumper.go
+++ b/sweep/fee_bumper.go
@@ -455,7 +455,6 @@ func (t *TxPublisher) createRBFCompliantTx(req *BumpRequest,
 			// The tx is valid, return the request ID.
 			requestID := t.storeRecord(
 				sweepCtx.tx, req, f, sweepCtx.fee,
-				sweepCtx.outpointToTxIndex,
 			)
 
 			log.Infof("Created tx %v for %v inputs: feerate=%v, "+
@@ -511,8 +510,7 @@ func (t *TxPublisher) createRBFCompliantTx(req *BumpRequest,
 
 // storeRecord stores the given record in the records map.
 func (t *TxPublisher) storeRecord(tx *wire.MsgTx, req *BumpRequest,
-	f FeeFunction, fee btcutil.Amount,
-	outpointToTxIndex map[wire.OutPoint]int) uint64 {
+	f FeeFunction, fee btcutil.Amount) uint64 {
 
 	// Increase the request counter.
 	//
@@ -522,11 +520,10 @@ func (t *TxPublisher) storeRecord(tx *wire.MsgTx, req *BumpRequest,
 
 	// Register the record.
 	t.records.Store(requestID, &monitorRecord{
-		tx:                tx,
-		req:               req,
-		feeFunction:       f,
-		fee:               fee,
-		outpointToTxIndex: outpointToTxIndex,
+		tx:          tx,
+		req:         req,
+		feeFunction: f,
+		fee:         fee,
 	})
 
 	return requestID
@@ -606,9 +603,7 @@ func (t *TxPublisher) broadcast(requestID uint64) (*BumpResult, error) {
 	// Before we go to broadcast, we'll notify the aux sweeper, if it's
 	// present of this new broadcast attempt.
 	err := fn.MapOptionZ(t.cfg.AuxSweeper, func(aux AuxSweeper) error {
-		return aux.NotifyBroadcast(
-			record.req, tx, record.fee, record.outpointToTxIndex,
-		)
+		return aux.NotifyBroadcast(record.req, tx, record.fee)
 	})
 	if err != nil {
 		return nil, fmt.Errorf("unable to notify aux sweeper: %w", err)
@@ -730,9 +725,6 @@ type monitorRecord struct {
 
 	// fee is the fee paid by the tx.
 	fee btcutil.Amount
-
-	// outpointToTxIndex is a map of outpoint to tx index.
-	outpointToTxIndex map[wire.OutPoint]int
 }
 
 // Start starts the publisher by subscribing to block epoch updates and kicking
@@ -1050,11 +1042,10 @@ func (t *TxPublisher) createAndPublishTx(requestID uint64,
 	// The tx has been created without any errors, we now register a new
 	// record by overwriting the same requestID.
 	t.records.Store(requestID, &monitorRecord{
-		tx:                sweepCtx.tx,
-		req:               r.req,
-		feeFunction:       r.feeFunction,
-		fee:               sweepCtx.fee,
-		outpointToTxIndex: sweepCtx.outpointToTxIndex,
+		tx:          sweepCtx.tx,
+		req:         r.req,
+		feeFunction: r.feeFunction,
+		fee:         sweepCtx.fee,
 	})
 
 	// Attempt to broadcast this new tx.
@@ -1208,10 +1199,6 @@ type sweepTxCtx struct {
 	fee btcutil.Amount
 
 	extraTxOut fn.Option[SweepOutput]
-
-	// outpointToTxIndex maps the outpoint of the inputs to their index in
-	// the sweep transaction.
-	outpointToTxIndex map[wire.OutPoint]int
 }
 
 // createSweepTx creates a sweeping tx based on the given inputs, change
@@ -1242,7 +1229,6 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input,
 	// We start by adding all inputs that commit to an output. We do this
 	// since the input and output index must stay the same for the
 	// signatures to be valid.
-	outpointToTxIndex := make(map[wire.OutPoint]int)
 	for _, o := range inputs {
 		if o.RequiredTxOut() == nil {
 			continue
@@ -1254,8 +1240,6 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input,
 			Sequence:         o.BlocksToMaturity(),
 		})
 		sweepTx.AddTxOut(o.RequiredTxOut())
-
-		outpointToTxIndex[o.OutPoint()] = len(sweepTx.TxOut) - 1
 	}
 
 	// Sum up the value contained in the remaining inputs, and add them to
@@ -1347,10 +1331,9 @@ func (t *TxPublisher) createSweepTx(inputs []input.Input,
 	)(changeOutputsOpt)
 
 	return &sweepTxCtx{
-		tx:                sweepTx,
-		fee:               txFee,
-		extraTxOut:        fn.FlattenOption(extraTxOut),
-		outpointToTxIndex: outpointToTxIndex,
+		tx:         sweepTx,
+		fee:        txFee,
+		extraTxOut: fn.FlattenOption(extraTxOut),
 	}, nil
 }
 
diff --git a/sweep/interface.go b/sweep/interface.go
index f2fff84b0..acece3143 100644
--- a/sweep/interface.go
+++ b/sweep/interface.go
@@ -93,6 +93,5 @@ type AuxSweeper interface {
 	// NotifyBroadcast is used to notify external callers of the broadcast
 	// of a sweep transaction, generated by the passed BumpRequest.
 	NotifyBroadcast(req *BumpRequest, tx *wire.MsgTx,
-		totalFees btcutil.Amount,
-		outpointToTxIndex map[wire.OutPoint]int) error
+		totalFees btcutil.Amount) error
 }
diff --git a/sweep/tx_input_set.go b/sweep/tx_input_set.go
index ce144a8eb..3a95fff2f 100644
--- a/sweep/tx_input_set.go
+++ b/sweep/tx_input_set.go
@@ -373,9 +373,7 @@ func (b *BudgetInputSet) Budget() btcutil.Amount {
 		budget += input.params.Budget
 	}
 
-	// We'll also tack on the extra budget which will eventually be
-	// accounted for by the wallet txns when we're broadcasting.
-	return budget + b.extraBudget
+	return budget
 }
 
 // DeadlineHeight returns the deadline height of the set.
diff --git a/watchtower/wtdb/version.go b/watchtower/wtdb/version.go
index 7267c4d79..dd9c55472 100644
--- a/watchtower/wtdb/version.go
+++ b/watchtower/wtdb/version.go
@@ -78,12 +78,6 @@ func getLatestDBVersion(versions []version) uint32 {
 	return uint32(len(versions))
 }
 
-// LatestDBMigrationVersion returns the number of the latest existing database
-// migration version available.
-func LatestDBMigrationVersion() uint32 {
-	return getLatestDBVersion(clientDBVersions)
-}
-
 // getMigrations returns a slice of all updates with a greater number that
 // curVersion that need to be applied to sync up with the latest version.
 func getMigrations(versions []version, curVersion uint32) []version {
@@ -97,27 +91,6 @@ func getMigrations(versions []version, curVersion uint32) []version {
 	return updates
 }
 
-// CurrentDatabaseVersion reads the current database version from the database
-// and returns it.
-func CurrentDatabaseVersion(db kvdb.Backend) (uint32, error) {
-	var (
-		version uint32
-		err     error
-	)
-
-	err = kvdb.View(db, func(tx kvdb.RTx) error {
-		version, err = getDBVersion(tx)
-		return err
-	}, func() {
-		version = 0
-	})
-	if err != nil {
-		return 0, err
-	}
-
-	return version, nil
-}
-
 // getDBVersion retrieves the current database version from the metadata bucket
 // using the dbVersionKey.
 func getDBVersion(tx kvdb.RTx) (uint32, error) {


Test patch:
diff --git a/contractcourt/chain_watcher_test.go b/contractcourt/chain_watcher_test.go
index baea8d873..2781170f0 100644
--- a/contractcourt/chain_watcher_test.go
+++ b/contractcourt/chain_watcher_test.go
@@ -504,24 +504,14 @@ func TestChainWatcherLocalForceCloseDetect(t *testing.T) {
 		// outputs.
 		select {
 		case summary := <-chanEvents.LocalUnilateralClosure:
-			resOpt := summary.LocalForceCloseSummary.
-				ContractResolutions
-
-			resolutions, err := resOpt.UnwrapOrErr(
-				fmt.Errorf("resolutions not found"),
-			)
-			if err != nil {
-				t.Fatalf("unable to get resolutions: %v", err)
-			}
-
 			// Make sure we correctly extracted the commit
 			// resolution if we had a local output.
 			if remoteOutputOnly {
-				if resolutions.CommitResolution != nil {
+				if summary.CommitResolution != nil {
 					t.Fatalf("expected no commit resolution")
 				}
 			} else {
-				if resolutions.CommitResolution == nil {
+				if summary.CommitResolution == nil {
 					t.Fatalf("expected commit resolution")
 				}
 			}
diff --git a/contractcourt/channel_arbitrator_test.go b/contractcourt/channel_arbitrator_test.go
index ac5253787..1353770d8 100644
--- a/contractcourt/channel_arbitrator_test.go
+++ b/contractcourt/channel_arbitrator_test.go
@@ -693,15 +693,11 @@ func TestChannelArbitratorLocalForceClose(t *testing.T) {
 	chanArbCtx.AssertState(StateCommitmentBroadcasted)
 
 	// Now notify about the local force close getting confirmed.
-	//
-	//nolint:lll
 	chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
 		SpendDetail: &chainntnfs.SpendDetail{},
 		LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
-			CloseTx: &wire.MsgTx{},
-			ContractResolutions: fn.Some(lnwallet.ContractResolutions{
-				HtlcResolutions: &lnwallet.HtlcResolutions{},
-			}),
+			CloseTx:         &wire.MsgTx{},
+			HtlcResolutions: &lnwallet.HtlcResolutions{},
 		},
 		ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
 	}
@@ -991,18 +987,15 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
 		},
 	}
 
-	//nolint:lll
 	chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
 		SpendDetail: &chainntnfs.SpendDetail{},
 		LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
 			CloseTx: closeTx,
-			ContractResolutions: fn.Some(lnwallet.ContractResolutions{
-				HtlcResolutions: &lnwallet.HtlcResolutions{
-					OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{
-						outgoingRes,
-					},
+			HtlcResolutions: &lnwallet.HtlcResolutions{
+				OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{
+					outgoingRes,
 				},
-			}),
+			},
 		},
 		ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
 		CommitSet: CommitSet{
@@ -1620,15 +1613,12 @@ func TestChannelArbitratorCommitFailure(t *testing.T) {
 		},
 		{
 			closeType: channeldb.LocalForceClose,
-			//nolint:lll
 			sendEvent: func(chanArb *ChannelArbitrator) {
 				chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
 					SpendDetail: &chainntnfs.SpendDetail{},
 					LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
-						CloseTx: &wire.MsgTx{},
-						ContractResolutions: fn.Some(lnwallet.ContractResolutions{
-							HtlcResolutions: &lnwallet.HtlcResolutions{},
-						}),
+						CloseTx:         &wire.MsgTx{},
+						HtlcResolutions: &lnwallet.HtlcResolutions{},
 					},
 					ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
 				}
@@ -1956,15 +1946,11 @@ func TestChannelArbitratorDanglingCommitForceClose(t *testing.T) {
 			// being canalled back. Also note that there're no HTLC
 			// resolutions sent since we have none on our
 			// commitment transaction.
-			//
-			//nolint:lll
 			uniCloseInfo := &LocalUnilateralCloseInfo{
 				SpendDetail: &chainntnfs.SpendDetail{},
 				LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
-					CloseTx: closeTx,
-					ContractResolutions: fn.Some(lnwallet.ContractResolutions{
-						HtlcResolutions: &lnwallet.HtlcResolutions{},
-					}),
+					CloseTx:         closeTx,
+					HtlcResolutions: &lnwallet.HtlcResolutions{},
 				},
 				ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
 				CommitSet: CommitSet{
@@ -2884,15 +2870,12 @@ func TestChannelArbitratorAnchors(t *testing.T) {
 		},
 	}
 
-	//nolint:lll
 	chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
 		SpendDetail: &chainntnfs.SpendDetail{},
 		LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
-			CloseTx: closeTx,
-			ContractResolutions: fn.Some(lnwallet.ContractResolutions{
-				HtlcResolutions:  &lnwallet.HtlcResolutions{},
-				AnchorResolution: anchorResolution,
-			}),
+			CloseTx:          closeTx,
+			HtlcResolutions:  &lnwallet.HtlcResolutions{},
+			AnchorResolution: anchorResolution,
 		},
 		ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
 		CommitSet: CommitSet{
@@ -3126,10 +3109,14 @@ func (m *mockChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
 	return &lnwallet.AnchorResolutions{}, nil
 }
 
-func (m *mockChannel) ForceCloseChan() (*wire.MsgTx, error) {
+func (m *mockChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) {
 	if m.forceCloseErr != nil {
 		return nil, m.forceCloseErr
 	}
 
-	return &wire.MsgTx{}, nil
+	summary := &lnwallet.LocalForceCloseSummary{
+		CloseTx:         &wire.MsgTx{},
+		HtlcResolutions: &lnwallet.HtlcResolutions{},
+	}
+	return summary, nil
 }
diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go
index 6bda4e398..9ffa79943 100644
--- a/contractcourt/htlc_incoming_contest_resolver.go
+++ b/contractcourt/htlc_incoming_contest_resolver.go
@@ -99,6 +99,17 @@ func (h *htlcIncomingContestResolver) Resolve(
 		return nil, nil
 	}
 
+	// If the HTLC has custom records, then for now we'll pause resolution.
+	//
+	// TODO(roasbeef): Implement resolving HTLCs with custom records
+	// (follow-up PR).
+	if len(h.htlc.CustomRecords) != 0 {
+		select { //nolint:gosimple
+		case <-h.quit:
+			return nil, errResolverShuttingDown
+		}
+	}
+
 	// First try to parse the payload. If that fails, we can stop resolution
 	// now.
 	payload, nextHopOnionBlob, err := h.decodePayload()
diff --git a/contractcourt/htlc_outgoing_contest_resolver.go b/contractcourt/htlc_outgoing_contest_resolver.go
index 2466544c9..c75b89822 100644
--- a/contractcourt/htlc_outgoing_contest_resolver.go
+++ b/contractcourt/htlc_outgoing_contest_resolver.go
@@ -58,6 +58,17 @@ func (h *htlcOutgoingContestResolver) Resolve(
 		return nil, nil
 	}
 
+	// If the HTLC has custom records, then for now we'll pause resolution.
+	//
+	// TODO(roasbeef): Implement resolving HTLCs with custom records
+	// (follow-up PR).
+	if len(h.htlc.CustomRecords) != 0 {
+		select { //nolint:gosimple
+		case <-h.quit:
+			return nil, errResolverShuttingDown
+		}
+	}
+
 	// Otherwise, we'll watch for two external signals to decide if we'll
 	// morph into another resolver, or fully resolve the contract.
 	//
diff --git a/contractcourt/taproot_briefcase_test.go b/contractcourt/taproot_briefcase_test.go
index 441aebf1d..a7d52d963 100644
--- a/contractcourt/taproot_briefcase_test.go
+++ b/contractcourt/taproot_briefcase_test.go
@@ -7,7 +7,6 @@ import (
 
 	"github.com/lightningnetwork/lnd/tlv"
 	"github.com/stretchr/testify/require"
-	"pgregory.net/rapid"
 )
 
 func randResolverCtrlBlocks(t *testing.T) resolverCtrlBlocks {
@@ -54,25 +53,6 @@ func randHtlcTweaks(t *testing.T) htlcTapTweaks {
 	return tweaks
 }
 
-func randHtlcAuxBlobs(t *testing.T) htlcAuxBlobs {
-	numBlobs := rand.Int() % 256
-	blobs := make(htlcAuxBlobs, numBlobs)
-
-	for i := 0; i < numBlobs; i++ {
-		var id resolverID
-		_, err := rand.Read(id[:])
-		require.NoError(t, err)
-
-		var blob [100]byte
-		_, err = rand.Read(blob[:])
-		require.NoError(t, err)
-
-		blobs[id] = blob[:]
-	}
-
-	return blobs
-}
-
 // TestTaprootBriefcase tests the encode/decode methods of the taproot
 // briefcase extension.
 func TestTaprootBriefcase(t *testing.T) {
@@ -113,9 +93,6 @@ func TestTaprootBriefcase(t *testing.T) {
 		BreachedCommitBlob: tlv.SomeRecordT(
 			tlv.NewPrimitiveRecord[tlv.TlvType3](commitBlob[:]),
 		),
-		HtlcBlobs: tlv.SomeRecordT(
-			tlv.NewRecordT[tlv.TlvType4](randHtlcAuxBlobs(t)),
-		),
 	}
 
 	var b bytes.Buffer
@@ -126,21 +103,3 @@ func TestTaprootBriefcase(t *testing.T) {
 
 	require.Equal(t, testCase, &decodedCase)
 }
-
-// TestHtlcAuxBlobEncodeDecode tests the encode/decode methods of the HTLC aux
-// blobs.
-func TestHtlcAuxBlobEncodeDecode(t *testing.T) {
-	t.Parallel()
-
-	rapid.Check(t, func(t *rapid.T) {
-		htlcBlobs := rapid.Make[htlcAuxBlobs]().Draw(t, "htlcAuxBlobs")
-
-		var b bytes.Buffer
-		require.NoError(t, htlcBlobs.Encode(&b))
-
-		decodedBlobs := newAuxHtlcBlobs()
-		require.NoError(t, decodedBlobs.Decode(&b))
-
-		require.Equal(t, htlcBlobs, decodedBlobs)
-	})
-}
diff --git a/contractcourt/testdata/rapid/TestHtlcAuxBlobEncodeDecode/TestHtlcAuxBlobEncodeDecode-20240902140253-81338.fail b/contractcourt/testdata/rapid/TestHtlcAuxBlobEncodeDecode/TestHtlcAuxBlobEncodeDecode-20240902140253-81338.fail
deleted file mode 100644
index 86bb07ed7..000000000
--- a/contractcourt/testdata/rapid/TestHtlcAuxBlobEncodeDecode/TestHtlcAuxBlobEncodeDecode-20240902140253-81338.fail
+++ /dev/null
@@ -1,78 +0,0 @@
-# 2024/09/02 14:02:53.354676 [TestHtlcAuxBlobEncodeDecode] [rapid] draw htlcAuxBlobs: contractcourt.htlcAuxBlobs{contractcourt.resolverID{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}:[]uint8{}}
-# 
-v0.4.8#15807814492030881602
-0x5555555555555
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
-0x0
\ No newline at end of file
diff --git a/fn/goroutine_manager_test.go b/fn/goroutine_manager_test.go
index 1fc945b97..d06a62b4a 100644
--- a/fn/goroutine_manager_test.go
+++ b/fn/goroutine_manager_test.go
@@ -2,7 +2,6 @@ package fn
 
 import (
 	"context"
-	"sync"
 	"testing"
 	"time"
 
@@ -20,7 +19,7 @@ func TestGoroutineManager(t *testing.T) {
 
 	taskChan := make(chan struct{})
 
-	require.True(t, m.Go(func(ctx context.Context) {
+	require.NoError(t, m.Go(func(ctx context.Context) {
 		<-taskChan
 	}))
 
@@ -38,7 +37,7 @@ func TestGoroutineManager(t *testing.T) {
 	require.Greater(t, stopDelay, time.Second)
 
 	// Make sure new goroutines do not start after Stop.
-	require.False(t, m.Go(func(ctx context.Context) {}))
+	require.ErrorIs(t, m.Go(func(ctx context.Context) {}), ErrStopping)
 
 	// When Stop() is called, the internal context expires and m.Done() is
 	// closed. Test this.
@@ -57,7 +56,7 @@ func TestGoroutineManagerContextExpires(t *testing.T) {
 
 	m := NewGoroutineManager(ctx)
 
-	require.True(t, m.Go(func(ctx context.Context) {
+	require.NoError(t, m.Go(func(ctx context.Context) {
 		<-ctx.Done()
 	}))
 
@@ -80,7 +79,7 @@ func TestGoroutineManagerContextExpires(t *testing.T) {
 	}
 
 	// Make sure new goroutines do not start after context expiry.
-	require.False(t, m.Go(func(ctx context.Context) {}))
+	require.ErrorIs(t, m.Go(func(ctx context.Context) {}), ErrStopping)
 
 	// Stop will wait for all goroutines to stop.
 	m.Stop()
@@ -108,11 +107,11 @@ func TestGoroutineManagerStress(t *testing.T) {
 	// implementation, this test crashes under `-race`.
 	for i := 0; i < 100; i++ {
 		taskChan := make(chan struct{})
-		ok := m.Go(func(ctx context.Context) {
+		err := m.Go(func(ctx context.Context) {
 			close(taskChan)
 		})
 		// If goroutine was started, wait for its completion.
-		if ok {
+		if err == nil {
 			<-taskChan
 		}
 	}
@@ -120,38 +119,3 @@ func TestGoroutineManagerStress(t *testing.T) {
 	// Wait for Stop to complete.
 	<-stopChan
 }
-
-// TestGoroutineManagerStopsStress launches many Stop() calls in parallel with a
-// task exiting. It attempts to catch a race condition between wg.Done() and
-// wg.Wait() calls. According to documentation of wg.Wait() this is acceptable,
-// therefore this test passes even with -race.
-func TestGoroutineManagerStopsStress(t *testing.T) {
-	t.Parallel()
-
-	m := NewGoroutineManager(context.Background())
-
-	// jobChan is used to make the task to finish.
-	jobChan := make(chan struct{})
-
-	// Start a task and wait inside it until we start calling Stop() method.
-	ok := m.Go(func(ctx context.Context) {
-		<-jobChan
-	})
-	require.True(t, ok)
-
-	// Now launch many gorotines calling Stop() method in parallel.
-	var wg sync.WaitGroup
-	for i := 0; i < 100; i++ {
-		wg.Add(1)
-		go func() {
-			defer wg.Done()
-			m.Stop()
-		}()
-	}
-
-	// Exit the task in parallel with Stop() calls.
-	close(jobChan)
-
-	// Wait until all the Stop() calls complete.
-	wg.Wait()
-}
diff --git a/itest/config.go b/itest/config.go
deleted file mode 100644
index 98e633652..000000000
--- a/itest/config.go
+++ /dev/null
@@ -1,28 +0,0 @@
-//go:build integration
-
-package itest
-
-import (
-	"os/exec"
-
-	"github.com/lightningnetwork/lnd/lntest"
-	"github.com/stretchr/testify/require"
-)
-
-// testDebuglevelShow tests that "lnd --debuglevel=show" command works and
-// prints the list of supported subsystems.
-func testDebuglevelShow(ht *lntest.HarnessTest) {
-	// We can't use ht.NewNode, because it adds more arguments to the
-	// command line (e.g. flags configuring bitcoin backend), but we want to
-	// make sure that "lnd --debuglevel=show" works without any other flags.
-	lndBinary := getLndBinary(ht.T)
-	cmd := exec.Command(lndBinary, "--debuglevel=show")
-	stdoutStderrBytes, err := cmd.CombinedOutput()
-	require.NoError(ht, err, "failed to run 'lnd --debuglevel=show'")
-
-	// Make sure that the output contains the list of supported subsystems
-	// and that the list is not empty. We search PEER subsystem.
-	stdoutStderr := string(stdoutStderrBytes)
-	require.Contains(ht, stdoutStderr, "Supported subsystems")
-	require.Contains(ht, stdoutStderr, "PEER")
-}
diff --git a/itest/list_on_test.go b/itest/list_on_test.go
index 68c923c3b..c5fa7cebf 100644
--- a/itest/list_on_test.go
+++ b/itest/list_on_test.go
@@ -702,8 +702,4 @@ var allTestCases = []*lntest.TestCase{
 		Name:     "send to route failed htlc timeout",
 		TestFunc: testSendToRouteFailHTLCTimeout,
 	},
-	{
-		Name:     "debuglevel show",
-		TestFunc: testDebuglevelShow,
-	},
 }
diff --git a/itest/lnd_channel_backup_test.go b/itest/lnd_channel_backup_test.go
index 5ae0df958..94bcb5997 100644
--- a/itest/lnd_channel_backup_test.go
+++ b/itest/lnd_channel_backup_test.go
@@ -1435,8 +1435,8 @@ func createLegacyRevocationChannel(ht *lntest.HarnessTest,
 	}
 
 	ht.MineBlocksAndAssertNumTxes(6, 1)
-	ht.AssertChannelInGraph(from, chanPoint)
-	ht.AssertChannelInGraph(to, chanPoint)
+	ht.AssertTopologyChannelOpen(from, chanPoint)
+	ht.AssertTopologyChannelOpen(to, chanPoint)
 }
 
 // chanRestoreViaRPC is a helper test method that returns a nodeRestorer
diff --git a/itest/lnd_channel_policy_test.go b/itest/lnd_channel_policy_test.go
index bb0520975..108397453 100644
--- a/itest/lnd_channel_policy_test.go
+++ b/itest/lnd_channel_policy_test.go
@@ -127,8 +127,8 @@ func testUpdateChannelPolicy(ht *lntest.HarnessTest) {
 	}
 
 	// Make sure Alice and Carol have seen each other's channels.
-	ht.AssertChannelInGraph(alice, chanPoint2)
-	ht.AssertChannelInGraph(carol, chanPoint)
+	ht.AssertTopologyChannelOpen(alice, chanPoint2)
+	ht.AssertTopologyChannelOpen(carol, chanPoint)
 
 	// First we'll try to send a payment from Alice to Carol with an amount
 	// less than the min_htlc value required by Carol. This payment should
@@ -292,7 +292,7 @@ func testUpdateChannelPolicy(ht *lntest.HarnessTest) {
 	)
 
 	// Make sure Bob knows this channel.
-	ht.AssertChannelInGraph(bob, chanPoint3)
+	ht.AssertTopologyChannelOpen(bob, chanPoint3)
 
 	// Make a global update, and check that both channels' new policies get
 	// propagated.
@@ -515,9 +515,9 @@ func testSendUpdateDisableChannel(ht *lntest.HarnessTest) {
 	ht.RestartNodeWithExtraArgs(eve, nodeCfg)
 
 	// Dave should know all the channels.
-	ht.AssertChannelInGraph(dave, chanPointAliceBob)
-	ht.AssertChannelInGraph(dave, chanPointAliceCarol)
-	ht.AssertChannelInGraph(dave, chanPointEveCarol)
+	ht.AssertTopologyChannelOpen(dave, chanPointAliceBob)
+	ht.AssertTopologyChannelOpen(dave, chanPointAliceCarol)
+	ht.AssertTopologyChannelOpen(dave, chanPointEveCarol)
 
 	// We should expect to see a channel update with the default routing
 	// policy, except that it should indicate the channel is disabled.
@@ -701,7 +701,7 @@ func testUpdateChannelPolicyForPrivateChannel(ht *lntest.HarnessTest) {
 	)
 
 	// Carol should be aware of the channel between Alice and Bob.
-	ht.AssertChannelInGraph(carol, chanPointAliceBob)
+	ht.AssertTopologyChannelOpen(carol, chanPointAliceBob)
 
 	// We should have the following topology now,
 	// Alice <--public:100k--> Bob <--private:100k--> Carol
diff --git a/itest/lnd_coop_close_with_htlcs_test.go b/itest/lnd_coop_close_with_htlcs_test.go
index 2b7bcad72..55d4d6d20 100644
--- a/itest/lnd_coop_close_with_htlcs_test.go
+++ b/itest/lnd_coop_close_with_htlcs_test.go
@@ -47,7 +47,7 @@ func coopCloseWithHTLCs(ht *lntest.HarnessTest) {
 	})
 
 	// Wait for Bob to understand that the channel is ready to use.
-	ht.AssertChannelInGraph(bob, chanPoint)
+	ht.AssertTopologyChannelOpen(bob, chanPoint)
 
 	// Here we set things up so that Alice generates a HODL invoice so we
 	// can test whether the shutdown is deferred until the settlement of
@@ -141,7 +141,7 @@ func coopCloseWithHTLCsWithRestart(ht *lntest.HarnessTest) {
 	})
 
 	// Wait for Bob to understand that the channel is ready to use.
-	ht.AssertChannelInGraph(bob, chanPoint)
+	ht.AssertTopologyChannelOpen(bob, chanPoint)
 
 	// Set up a HODL invoice so that we can be sure that an HTLC is pending
 	// on the channel at the time that shutdown is requested.
diff --git a/itest/lnd_forward_interceptor_test.go b/itest/lnd_forward_interceptor_test.go
index 9bbecd31b..6148ba7f1 100644
--- a/itest/lnd_forward_interceptor_test.go
+++ b/itest/lnd_forward_interceptor_test.go
@@ -58,7 +58,7 @@ func testForwardInterceptorDedupHtlc(ht *lntest.HarnessTest) {
 	cpAB, cpBC := resp[0], resp[1]
 
 	// Make sure Alice is aware of channel Bob=>Carol.
-	ht.AssertChannelInGraph(alice, cpBC)
+	ht.AssertTopologyChannelOpen(alice, cpBC)
 
 	// Connect the interceptor.
 	interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor()
@@ -209,7 +209,7 @@ func testForwardInterceptorBasic(ht *lntest.HarnessTest) {
 	cpAB, cpBC := resp[0], resp[1]
 
 	// Make sure Alice is aware of channel Bob=>Carol.
-	ht.AssertChannelInGraph(alice, cpBC)
+	ht.AssertTopologyChannelOpen(alice, cpBC)
 
 	// Connect the interceptor.
 	interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor()
@@ -370,7 +370,7 @@ func testForwardInterceptorModifiedHtlc(ht *lntest.HarnessTest) {
 	cpAB, cpBC := resp[0], resp[1]
 
 	// Make sure Alice is aware of channel Bob=>Carol.
-	ht.AssertChannelInGraph(alice, cpBC)
+	ht.AssertTopologyChannelOpen(alice, cpBC)
 
 	// Connect an interceptor to Bob's node.
 	bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
@@ -478,7 +478,7 @@ func testForwardInterceptorWireRecords(ht *lntest.HarnessTest) {
 	cpAB, cpBC, cpCD := resp[0], resp[1], resp[2]
 
 	// Make sure Alice is aware of channel Bob=>Carol.
-	ht.AssertChannelInGraph(alice, cpBC)
+	ht.AssertTopologyChannelOpen(alice, cpBC)
 
 	// Connect an interceptor to Bob's node.
 	bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
@@ -609,8 +609,8 @@ func testForwardInterceptorRestart(ht *lntest.HarnessTest) {
 	cpAB, cpBC, cpCD := resp[0], resp[1], resp[2]
 
 	// Make sure Alice is aware of channels Bob=>Carol and Carol=>Dave.
-	ht.AssertChannelInGraph(alice, cpBC)
-	ht.AssertChannelInGraph(alice, cpCD)
+	ht.AssertTopologyChannelOpen(alice, cpBC)
+	ht.AssertTopologyChannelOpen(alice, cpCD)
 
 	// Connect an interceptor to Bob's node.
 	bobInterceptor, cancelBobInterceptor := bob.RPC.HtlcInterceptor()
diff --git a/itest/lnd_funding_test.go b/itest/lnd_funding_test.go
index 7daf95960..8d715067d 100644
--- a/itest/lnd_funding_test.go
+++ b/itest/lnd_funding_test.go
@@ -927,8 +927,8 @@ func testChannelFundingPersistence(ht *lntest.HarnessTest) {
 
 	// Make sure Alice and Carol have seen the channel in their network
 	// topology.
-	ht.AssertChannelInGraph(alice, chanPoint)
-	ht.AssertChannelInGraph(carol, chanPoint)
+	ht.AssertTopologyChannelOpen(alice, chanPoint)
+	ht.AssertTopologyChannelOpen(carol, chanPoint)
 
 	// Create an additional check for our channel assertion that will
 	// check that our label is as expected.
@@ -1055,7 +1055,7 @@ func testBatchChanFunding(ht *lntest.HarnessTest) {
 
 	// Ensure that Alice can send funds to Eve via the zero-conf channel
 	// before the batch transaction was mined.
-	ht.AssertChannelInGraph(alice, chanPoint4)
+	ht.AssertTopologyChannelOpen(alice, chanPoint4)
 	eveInvoiceParams := &lnrpc.Invoice{
 		Value:   int64(10_000),
 		Private: true,
@@ -1068,9 +1068,9 @@ func testBatchChanFunding(ht *lntest.HarnessTest) {
 	// Mine the batch transaction and check the network topology.
 	block := ht.MineBlocksAndAssertNumTxes(6, 1)[0]
 	ht.AssertTxInBlock(block, *txHash)
-	ht.AssertChannelInGraph(alice, chanPoint1)
-	ht.AssertChannelInGraph(alice, chanPoint2)
-	ht.AssertChannelInGraph(alice, chanPoint3)
+	ht.AssertTopologyChannelOpen(alice, chanPoint1)
+	ht.AssertTopologyChannelOpen(alice, chanPoint2)
+	ht.AssertTopologyChannelOpen(alice, chanPoint3)
 
 	// Check if the change type from the batch_open_channel funding is P2TR.
 	rawTx := ht.GetRawTransaction(*txHash)
diff --git a/itest/lnd_hold_persistence_test.go b/itest/lnd_hold_persistence_test.go
index f1c2a7d82..c7cd5eac2 100644
--- a/itest/lnd_hold_persistence_test.go
+++ b/itest/lnd_hold_persistence_test.go
@@ -51,7 +51,7 @@ func testHoldInvoicePersistence(ht *lntest.HarnessTest) {
 	)
 
 	// Wait for Carol to see the open channel Alice-Bob.
-	ht.AssertChannelInGraph(carol, chanPointBob)
+	ht.AssertTopologyChannelOpen(carol, chanPointBob)
 
 	// Create preimages for all payments we are going to initiate.
 	var preimages []lntypes.Preimage
diff --git a/itest/lnd_invoice_acceptor_test.go b/itest/lnd_invoice_acceptor_test.go
index 97d14650c..5a4a35ba0 100644
--- a/itest/lnd_invoice_acceptor_test.go
+++ b/itest/lnd_invoice_acceptor_test.go
@@ -35,7 +35,7 @@ func testInvoiceHtlcModifierBasic(ht *lntest.HarnessTest) {
 	cpAB, cpBC := resp[0], resp[1]
 
 	// Make sure Alice is aware of channel Bob=>Carol.
-	ht.AssertChannelInGraph(alice, cpBC)
+	ht.AssertTopologyChannelOpen(alice, cpBC)
 
 	// Initiate Carol's invoice HTLC modifier.
 	invoiceModifier, cancelModifier := carol.RPC.InvoiceHtlcModifier()
diff --git a/itest/lnd_misc_test.go b/itest/lnd_misc_test.go
index 02e3386e6..ccd879ab6 100644
--- a/itest/lnd_misc_test.go
+++ b/itest/lnd_misc_test.go
@@ -172,10 +172,10 @@ func testSphinxReplayPersistence(ht *lntest.HarnessTest) {
 	invoiceResp := dave.RPC.AddInvoice(invoice)
 
 	// Wait for all channels to be recognized and advertized.
-	ht.AssertChannelInGraph(carol, chanPoint)
-	ht.AssertChannelInGraph(dave, chanPoint)
-	ht.AssertChannelInGraph(carol, chanPointFC)
-	ht.AssertChannelInGraph(fred, chanPointFC)
+	ht.AssertTopologyChannelOpen(carol, chanPoint)
+	ht.AssertTopologyChannelOpen(dave, chanPoint)
+	ht.AssertTopologyChannelOpen(carol, chanPointFC)
+	ht.AssertTopologyChannelOpen(fred, chanPointFC)
 
 	// With the invoice for Dave added, send a payment from Fred paying
 	// to the above generated invoice.
@@ -417,7 +417,7 @@ func testMaxPendingChannels(ht *lntest.HarnessTest) {
 		// Ensure that the funding transaction enters a block, and is
 		// properly advertised by Alice.
 		ht.AssertTxInBlock(block, fundingTxID)
-		ht.AssertChannelInGraph(alice, fundingChanPoint)
+		ht.AssertTopologyChannelOpen(alice, fundingChanPoint)
 
 		// The channel should be listed in the peer information
 		// returned by both peers.
diff --git a/itest/lnd_mpp_test.go b/itest/lnd_mpp_test.go
index 800708569..37da783c1 100644
--- a/itest/lnd_mpp_test.go
+++ b/itest/lnd_mpp_test.go
@@ -295,7 +295,7 @@ func (m *mppTestScenario) openChannels(r *mppOpenChannelRequest) {
 	// Make sure every node has heard every channel.
 	for _, hn := range m.nodes {
 		for _, cp := range m.channelPoints {
-			m.ht.AssertChannelInGraph(hn, cp)
+			m.ht.AssertTopologyChannelOpen(hn, cp)
 		}
 
 		// Each node should have exactly 6 edges.
diff --git a/itest/lnd_multi-hop-error-propagation_test.go b/itest/lnd_multi-hop-error-propagation_test.go
index 6fe34fdcf..6bd3c1568 100644
--- a/itest/lnd_multi-hop-error-propagation_test.go
+++ b/itest/lnd_multi-hop-error-propagation_test.go
@@ -49,7 +49,7 @@ func testHtlcErrorPropagation(ht *lntest.HarnessTest) {
 	)
 
 	// Ensure that Alice has Carol in her routing table before proceeding.
-	ht.AssertChannelInGraph(alice, chanPointBob)
+	ht.AssertTopologyChannelOpen(alice, chanPointBob)
 
 	cType := ht.GetChannelCommitType(alice, chanPointAlice)
 	commitFee := lntest.CalcStaticFee(cType, 0)
diff --git a/itest/lnd_multi-hop_test.go b/itest/lnd_multi-hop_test.go
index f3a906c14..f08a748c3 100644
--- a/itest/lnd_multi-hop_test.go
+++ b/itest/lnd_multi-hop_test.go
@@ -2295,8 +2295,8 @@ func createThreeHopNetwork(ht *lntest.HarnessTest,
 	// We'll only do this though if it wasn't a private channel we opened
 	// earlier.
 	if !privateChan {
-		ht.AssertChannelInGraph(alice, bobChanPoint)
-		ht.AssertChannelInGraph(carol, aliceChanPoint)
+		ht.AssertTopologyChannelOpen(alice, bobChanPoint)
+		ht.AssertTopologyChannelOpen(carol, aliceChanPoint)
 	} else {
 		// Otherwise, we want to wait for all the channels to be shown
 		// as active before we proceed.
diff --git a/itest/lnd_open_channel_test.go b/itest/lnd_open_channel_test.go
index a4134abca..48e68fba0 100644
--- a/itest/lnd_open_channel_test.go
+++ b/itest/lnd_open_channel_test.go
@@ -80,8 +80,8 @@ func testOpenChannelAfterReorg(ht *lntest.HarnessTest) {
 
 	// Wait for Alice and Bob to recognize and advertise the new channel
 	// generated above.
-	ht.AssertChannelInGraph(alice, chanPoint)
-	ht.AssertChannelInGraph(bob, chanPoint)
+	ht.AssertTopologyChannelOpen(alice, chanPoint)
+	ht.AssertTopologyChannelOpen(bob, chanPoint)
 
 	// Alice should now have 1 edge in her graph.
 	ht.AssertNumActiveEdges(alice, 1, true)
diff --git a/itest/lnd_payment_test.go b/itest/lnd_payment_test.go
index 3515cbc94..cb6409373 100644
--- a/itest/lnd_payment_test.go
+++ b/itest/lnd_payment_test.go
@@ -1113,7 +1113,7 @@ func testPaymentFailureReasonCanceled(ht *lntest.HarnessTest) {
 	cpAB, cpBC := resp[0], resp[1]
 
 	// Make sure Alice is aware of channel Bob=>Carol.
-	ht.AssertChannelInGraph(alice, cpBC)
+	ht.AssertTopologyChannelOpen(alice, cpBC)
 
 	// Connect the interceptor.
 	interceptor, cancelInterceptor := bob.RPC.HtlcInterceptor()
diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go
index 438661138..d67503972 100644
--- a/itest/lnd_psbt_test.go
+++ b/itest/lnd_psbt_test.go
@@ -307,8 +307,8 @@ func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode,
 	txHash := finalTx.TxHash()
 	block := ht.MineBlocksAndAssertNumTxes(6, 1)[0]
 	ht.AssertTxInBlock(block, txHash)
-	ht.AssertChannelInGraph(carol, chanPoint)
-	ht.AssertChannelInGraph(carol, chanPoint2)
+	ht.AssertTopologyChannelOpen(carol, chanPoint)
+	ht.AssertTopologyChannelOpen(carol, chanPoint2)
 
 	// With the channel open, ensure that it is counted towards Carol's
 	// total channel balance.
@@ -482,8 +482,8 @@ func runPsbtChanFundingExternal(ht *lntest.HarnessTest, carol,
 	// for the new channel to be propagated through the network.
 	block := ht.MineBlocksAndAssertNumTxes(6, 1)[0]
 	ht.AssertTxInBlock(block, txHash)
-	ht.AssertChannelInGraph(carol, chanPoint)
-	ht.AssertChannelInGraph(carol, chanPoint2)
+	ht.AssertTopologyChannelOpen(carol, chanPoint)
+	ht.AssertTopologyChannelOpen(carol, chanPoint2)
 
 	// With the channel open, ensure that it is counted towards Carol's
 	// total channel balance.
@@ -639,7 +639,7 @@ func runPsbtChanFundingSingleStep(ht *lntest.HarnessTest, carol,
 	txHash := finalTx.TxHash()
 	block := ht.MineBlocksAndAssertNumTxes(6, 1)[0]
 	ht.AssertTxInBlock(block, txHash)
-	ht.AssertChannelInGraph(carol, chanPoint)
+	ht.AssertTopologyChannelOpen(carol, chanPoint)
 
 	// Next, to make sure the channel functions as normal, we'll make some
 	// payments within the channel.
diff --git a/itest/lnd_res_handoff_test.go b/itest/lnd_res_handoff_test.go
index dbf286293..941cc5283 100644
--- a/itest/lnd_res_handoff_test.go
+++ b/itest/lnd_res_handoff_test.go
@@ -37,7 +37,7 @@ func testResHandoff(ht *lntest.HarnessTest) {
 	chanPointCarol := ht.OpenChannel(bob, carol, params)
 
 	// Wait for Alice to see the channel edge in the graph.
-	ht.AssertChannelInGraph(alice, chanPointCarol)
+	ht.AssertTopologyChannelOpen(alice, chanPointCarol)
 
 	// We'll create an invoice for Carol that Alice will attempt to pay.
 	// Since Carol is in hodl.commit mode, she won't send back any commit
diff --git a/itest/lnd_revocation_test.go b/itest/lnd_revocation_test.go
index 8e2638fe9..a8cef5007 100644
--- a/itest/lnd_revocation_test.go
+++ b/itest/lnd_revocation_test.go
@@ -276,7 +276,7 @@ func revokedCloseRetributionZeroValueRemoteOutputCase(ht *lntest.HarnessTest,
 	ht.EnsureConnected(dave, carol)
 
 	// Once connected, give Dave some time to enable the channel again.
-	ht.AssertChannelInGraph(dave, chanPoint)
+	ht.AssertTopologyChannelOpen(dave, chanPoint)
 
 	// Finally, send payments from Dave to Carol, consuming Carol's
 	// remaining payment hashes.
@@ -502,7 +502,7 @@ func revokedCloseRetributionRemoteHodlCase(ht *lntest.HarnessTest,
 	ht.EnsureConnected(dave, carol)
 
 	// Once connected, give Dave some time to enable the channel again.
-	ht.AssertChannelInGraph(dave, chanPoint)
+	ht.AssertTopologyChannelOpen(dave, chanPoint)
 
 	// Finally, send payments from Dave to Carol, consuming Carol's
 	// remaining payment hashes.
diff --git a/itest/lnd_route_blinding_test.go b/itest/lnd_route_blinding_test.go
index fd378a25f..b921103ac 100644
--- a/itest/lnd_route_blinding_test.go
+++ b/itest/lnd_route_blinding_test.go
@@ -63,7 +63,7 @@ func testQueryBlindedRoutes(ht *lntest.HarnessTest) {
 
 	// Wait for Alice to see Bob/Carol's channel because she'll need it for
 	// pathfinding.
-	ht.AssertChannelInGraph(alice, chanPointBobCarol)
+	ht.AssertTopologyChannelOpen(alice, chanPointBobCarol)
 
 	// Lookup full channel info so that we have channel ids for our route.
 	aliceBobChan := ht.GetChannelByChanPoint(alice, chanPointAliceBob)
@@ -593,7 +593,7 @@ func setupFourHopNetwork(ht *lntest.HarnessTest,
 	nodes := []*node.HarnessNode{ht.Alice, ht.Bob, carol, dave}
 	for _, chanPoint := range networkChans {
 		for _, node := range nodes {
-			ht.AssertChannelInGraph(node, chanPoint)
+			ht.AssertTopologyChannelOpen(node, chanPoint)
 		}
 	}
 
@@ -981,7 +981,7 @@ func testMPPToSingleBlindedPath(ht *lntest.HarnessTest) {
 			}
 
 			numPublic++
-			ht.AssertChannelInGraph(hn, cp)
+			ht.AssertTopologyChannelOpen(hn, cp)
 		}
 
 		// Each node should have exactly numPublic edges.
@@ -1152,7 +1152,7 @@ func testBlindedRouteDummyHops(ht *lntest.HarnessTest) {
 	// Make sure every node has heard about every channel.
 	for _, hn := range nodes {
 		for _, cp := range channelPoints {
-			ht.AssertChannelInGraph(hn, cp)
+			ht.AssertTopologyChannelOpen(hn, cp)
 		}
 
 		// Each node should have exactly 5 edges.
@@ -1321,7 +1321,7 @@ func testMPPToMultipleBlindedPaths(ht *lntest.HarnessTest) {
 	// Make sure every node has heard every channel.
 	for _, hn := range nodes {
 		for _, cp := range channelPoints {
-			ht.AssertChannelInGraph(hn, cp)
+			ht.AssertTopologyChannelOpen(hn, cp)
 		}
 
 		// Each node should have exactly 5 edges.
diff --git a/itest/lnd_routing_test.go b/itest/lnd_routing_test.go
index 07adcf559..343cb2b99 100644
--- a/itest/lnd_routing_test.go
+++ b/itest/lnd_routing_test.go
@@ -344,7 +344,7 @@ func runMultiHopSendToRoute(ht *lntest.HarnessTest, useGraphCache bool) {
 	defer ht.CloseChannel(carol, chanPointBob)
 
 	// Make sure Alice knows the channel between Bob and Carol.
-	ht.AssertChannelInGraph(alice, chanPointBob)
+	ht.AssertTopologyChannelOpen(alice, chanPointBob)
 
 	// Create 5 invoices for Carol, which expect a payment from Alice for
 	// 1k satoshis with a different preimage each time.
@@ -965,7 +965,7 @@ func testMultiHopOverPrivateChannels(ht *lntest.HarnessTest) {
 	)
 
 	// Alice should know the new channel from Bob.
-	ht.AssertChannelInGraph(alice, chanPointBob)
+	ht.AssertTopologyChannelOpen(alice, chanPointBob)
 
 	// Next, we'll create Dave's node and open a private channel between
 	// him and Carol with Carol being the funder.
@@ -981,7 +981,7 @@ func testMultiHopOverPrivateChannels(ht *lntest.HarnessTest) {
 	)
 
 	// Dave should know the channel[Bob<->Carol] from Carol.
-	ht.AssertChannelInGraph(dave, chanPointBob)
+	ht.AssertTopologyChannelOpen(dave, chanPointBob)
 
 	// Now that all the channels are set up according to the topology from
 	// above, we can proceed to test payments. We'll create an invoice for
@@ -1077,8 +1077,8 @@ func testQueryRoutes(ht *lntest.HarnessTest) {
 
 	// Before we continue, give Alice some time to catch up with the newly
 	// opened channels.
-	ht.AssertChannelInGraph(alice, chanPointBob)
-	ht.AssertChannelInGraph(alice, chanPointCarol)
+	ht.AssertTopologyChannelOpen(alice, chanPointBob)
+	ht.AssertTopologyChannelOpen(alice, chanPointCarol)
 
 	// Query for routes to pay from Alice to Dave.
 	const paymentAmt = 1000
@@ -1389,7 +1389,7 @@ func testRouteFeeCutoff(ht *lntest.HarnessTest) {
 	}
 	for _, chanPoint := range networkChans {
 		for _, node := range nodes {
-			ht.AssertChannelInGraph(node, chanPoint)
+			ht.AssertTopologyChannelOpen(node, chanPoint)
 		}
 	}
 
diff --git a/itest/lnd_rpc_middleware_interceptor_test.go b/itest/lnd_rpc_middleware_interceptor_test.go
index 92042c980..7c99f6f20 100644
--- a/itest/lnd_rpc_middleware_interceptor_test.go
+++ b/itest/lnd_rpc_middleware_interceptor_test.go
@@ -153,13 +153,9 @@ func testRPCMiddlewareInterceptor(ht *lntest.HarnessTest) {
 	// And finally make sure mandatory middleware is always checked for any
 	// RPC request.
 	ht.Run("mandatory middleware", func(tt *testing.T) {
-		middlewareMandatoryTest(ht, alice)
+		st := ht.Subtest(tt)
+		middlewareMandatoryTest(st, alice)
 	})
-
-	// We now shut down the node manually to prevent the test from failing
-	// because we can't call the stop RPC if we unregister the middleware
-	// in the defer statement above.
-	ht.KillNode(alice)
 }
 
 // middlewareRegistrationRestrictionTests tests all restrictions that apply to
@@ -597,6 +593,11 @@ func middlewareMandatoryTest(ht *lntest.HarnessTest, node *node.HarnessNode) {
 	time.Sleep(500 * time.Millisecond)
 	node.RPC.ListChannels(&lnrpc.ListChannelsRequest{})
 	node.RPC.SubscribeInvoices(&lnrpc.InvoiceSubscription{})
+
+	// We now shut down the node manually to prevent the test from failing
+	// because we can't call the stop RPC if we unregister the middleware
+	// in the defer statement above.
+	ht.KillNode(node)
 }
 
 // assertInterceptedType makes sure that the intercept message sent by the RPC
diff --git a/itest/lnd_switch_test.go b/itest/lnd_switch_test.go
index f1ac628ca..be1992ed4 100644
--- a/itest/lnd_switch_test.go
+++ b/itest/lnd_switch_test.go
@@ -415,7 +415,7 @@ func setupScenarioFourNodes(ht *lntest.HarnessTest) *scenarioFourNodes {
 	nodes := []*node.HarnessNode{alice, bob, carol, dave}
 	for _, chanPoint := range resp {
 		for _, node := range nodes {
-			ht.AssertChannelInGraph(node, chanPoint)
+			ht.AssertTopologyChannelOpen(node, chanPoint)
 		}
 	}
 
diff --git a/itest/lnd_wallet_import_test.go b/itest/lnd_wallet_import_test.go
index f4acc6365..d8fa84dce 100644
--- a/itest/lnd_wallet_import_test.go
+++ b/itest/lnd_wallet_import_test.go
@@ -413,8 +413,8 @@ func fundChanAndCloseFromImportedAccount(ht *lntest.HarnessTest, srcNode,
 		},
 		OutputIndex: upd.ChanPending.OutputIndex,
 	}
-	ht.AssertChannelInGraph(srcNode, chanPoint)
-	ht.AssertChannelInGraph(destNode, chanPoint)
+	ht.AssertTopologyChannelOpen(srcNode, chanPoint)
+	ht.AssertTopologyChannelOpen(destNode, chanPoint)
 
 	// Send a test payment to ensure the channel is operating as normal.
 	const invoiceAmt = 100000
diff --git a/itest/lnd_watchtower_test.go b/itest/lnd_watchtower_test.go
index 0a36b077d..4fee5d6c4 100644
--- a/itest/lnd_watchtower_test.go
+++ b/itest/lnd_watchtower_test.go
@@ -450,7 +450,7 @@ func testRevokedCloseRetributionAltruistWatchtowerCase(ht *lntest.HarnessTest,
 	ht.EnsureConnected(dave, carol)
 
 	// Once connected, give Dave some time to enable the channel again.
-	ht.AssertChannelInGraph(dave, chanPoint)
+	ht.AssertTopologyChannelOpen(dave, chanPoint)
 
 	// Finally, send payments from Dave to Carol, consuming Carol's
 	// remaining payment hashes.
diff --git a/itest/lnd_wipe_fwdpkgs_test.go b/itest/lnd_wipe_fwdpkgs_test.go
index a302f596a..cee1d8e76 100644
--- a/itest/lnd_wipe_fwdpkgs_test.go
+++ b/itest/lnd_wipe_fwdpkgs_test.go
@@ -49,7 +49,7 @@ func testWipeForwardingPackages(ht *lntest.HarnessTest) {
 
 	// Before we continue, make sure Alice has seen the channel between Bob
 	// and Carol.
-	ht.AssertChannelInGraph(alice, chanPointBC)
+	ht.AssertTopologyChannelOpen(alice, chanPointBC)
 
 	// Alice sends several payments to Carol through Bob, which triggers
 	// Bob to create forwarding packages.
diff --git a/itest/lnd_zero_conf_test.go b/itest/lnd_zero_conf_test.go
index 6a658ab38..e0dd74b1b 100644
--- a/itest/lnd_zero_conf_test.go
+++ b/itest/lnd_zero_conf_test.go
@@ -75,8 +75,8 @@ func testZeroConfChannelOpen(ht *lntest.HarnessTest) {
 	// having to mine any blocks.
 	fundingPoint2 := ht.WaitForChannelOpenEvent(stream)
 
-	ht.AssertChannelInGraph(carol, fundingPoint2)
-	ht.AssertChannelInGraph(dave, fundingPoint2)
+	ht.AssertTopologyChannelOpen(carol, fundingPoint2)
+	ht.AssertTopologyChannelOpen(dave, fundingPoint2)
 
 	// Attempt to send a 10K satoshi payment from Carol to Dave.
 	daveInvoiceParams := &lnrpc.Invoice{
@@ -144,8 +144,8 @@ func testZeroConfChannelOpen(ht *lntest.HarnessTest) {
 	// Wait to receive the OpenStatusUpdate_ChanOpen update.
 	fundingPoint3 := ht.WaitForChannelOpenEvent(stream)
 
-	ht.AssertChannelInGraph(eve, fundingPoint3)
-	ht.AssertChannelInGraph(carol, fundingPoint3)
+	ht.AssertTopologyChannelOpen(eve, fundingPoint3)
+	ht.AssertTopologyChannelOpen(carol, fundingPoint3)
 
 	// Attempt to send a 20K satoshi payment from Eve to Dave.
 	daveInvoiceParams.Value = int64(20_000)
@@ -182,7 +182,7 @@ func testZeroConfChannelOpen(ht *lntest.HarnessTest) {
 	require.Len(ht, payReq.RouteHints, 0)
 
 	// Make sure Dave is aware of this channel and send the payment.
-	ht.AssertChannelInGraph(dave, fundingPoint3)
+	ht.AssertTopologyChannelOpen(dave, fundingPoint3)
 	ht.CompletePaymentRequests(
 		dave, []string{eveInvoiceResp.PaymentRequest},
 	)
@@ -269,7 +269,7 @@ func optionScidAliasScenario(ht *lntest.HarnessTest, chantype, private bool) {
 
 	// Make sure Bob knows this channel if it's public.
 	if !private {
-		ht.AssertChannelInGraph(bob, fundingPoint)
+		ht.AssertTopologyChannelOpen(bob, fundingPoint)
 	}
 
 	// Assert that a payment from Carol to Dave works as expected.
@@ -291,7 +291,7 @@ func optionScidAliasScenario(ht *lntest.HarnessTest, chantype, private bool) {
 	fundingPoint2 := ht.OpenChannel(bob, carol, p)
 
 	// Wait until Dave receives the Bob<->Carol channel.
-	ht.AssertChannelInGraph(dave, fundingPoint2)
+	ht.AssertTopologyChannelOpen(dave, fundingPoint2)
 
 	daveInvoiceResp2 := dave.RPC.AddInvoice(daveInvoiceParams)
 	decodedReq := dave.RPC.DecodePayReq(daveInvoiceResp2.PaymentRequest)
@@ -490,7 +490,7 @@ func testPrivateUpdateAlias(ht *lntest.HarnessTest,
 	fundingPoint := ht.OpenChannel(eve, carol, p)
 
 	// Make sure Dave has seen this public channel.
-	ht.AssertChannelInGraph(dave, fundingPoint)
+	ht.AssertTopologyChannelOpen(dave, fundingPoint)
 
 	// Setup a ChannelAcceptor for Dave.
 	acceptStream, cancel := dave.RPC.ChannelAcceptor()
@@ -792,7 +792,7 @@ func testOptionScidUpgrade(ht *lntest.HarnessTest) {
 	fundingPoint2 := ht.OpenChannel(bob, carol, p)
 
 	// Make sure Dave knows this channel.
-	ht.AssertChannelInGraph(dave, fundingPoint2)
+	ht.AssertTopologyChannelOpen(dave, fundingPoint2)
 
 	// Carol will now set the option-scid-alias feature bit and restart.
 	carolArgs = append(carolArgs, "--protocol.option-scid-alias")
diff --git a/lntest/harness.go b/lntest/harness.go
index f96a3aadd..274f22413 100644
--- a/lntest/harness.go
+++ b/lntest/harness.go
@@ -193,9 +193,6 @@ func (h *HarnessTest) Start(chain node.BackendConfig,
 
 	// Assemble the miner.
 	h.miner = miner
-
-	// Update block height.
-	h.updateCurrentHeight()
 }
 
 // ChainBackendName returns the chain backend name used in the test.
@@ -844,10 +841,9 @@ func (h *HarnessTest) NewNodeRemoteSigner(name string, extraArgs []string,
 	return hn
 }
 
-// KillNode kills the node and waits for the node process to stop.
+// KillNode kills the node (but won't wait for the node process to stop).
 func (h *HarnessTest) KillNode(hn *node.HarnessNode) {
-	h.Logf("Manually killing the node %s", hn.Name())
-	require.NoErrorf(h, hn.KillAndWait(), "%s: kill got error", hn.Name())
+	require.NoErrorf(h, hn.Kill(), "%s: kill got error", hn.Name())
 	delete(h.manager.activeNodes, hn.Cfg.NodeID)
 }
 
@@ -1223,8 +1219,8 @@ func (h *HarnessTest) openChannel(alice, bob *node.HarnessNode,
 
 	// Check that both alice and bob have seen the channel from their
 	// network topology.
-	h.AssertChannelInGraph(alice, fundingChanPoint)
-	h.AssertChannelInGraph(bob, fundingChanPoint)
+	h.AssertTopologyChannelOpen(alice, fundingChanPoint)
+	h.AssertTopologyChannelOpen(bob, fundingChanPoint)
 
 	// Check that the channel can be seen in their ListChannels.
 	h.AssertChannelExists(alice, fundingChanPoint)
@@ -1245,8 +1241,8 @@ func (h *HarnessTest) openChannelZeroConf(alice, bob *node.HarnessNode,
 
 	// Check that both alice and bob have seen the channel from their
 	// network topology.
-	h.AssertChannelInGraph(alice, fundingChanPoint)
-	h.AssertChannelInGraph(bob, fundingChanPoint)
+	h.AssertTopologyChannelOpen(alice, fundingChanPoint)
+	h.AssertTopologyChannelOpen(bob, fundingChanPoint)
 
 	// Finally, check that the channel can be seen in their ListChannels.
 	h.AssertChannelExists(alice, fundingChanPoint)
@@ -1830,8 +1826,8 @@ func (h *HarnessTest) OpenMultiChannelsAsync(
 		if !req.Param.Private {
 			// Check that both alice and bob have seen the channel
 			// from their channel watch request.
-			h.AssertChannelInGraph(req.Local, cp)
-			h.AssertChannelInGraph(req.Remote, cp)
+			h.AssertTopologyChannelOpen(req.Local, cp)
+			h.AssertTopologyChannelOpen(req.Remote, cp)
 		}
 
 		// Finally, check that the channel can be seen in their
@@ -2306,6 +2302,7 @@ func (h *HarnessTest) openChannelsForNodes(nodes []*node.HarnessNode,
 	p OpenChannelParams) []*lnrpc.ChannelPoint {
 
 	// Sanity check the params.
+	require.False(h, p.ZeroConf, "zero-conf channels must be disabled")
 	require.Greater(h, len(nodes), 1, "need at least 2 nodes")
 
 	// Open channels in batch to save blocks mined.
@@ -2327,7 +2324,7 @@ func (h *HarnessTest) openChannelsForNodes(nodes []*node.HarnessNode,
 	if !p.Private {
 		for _, node := range nodes {
 			for _, chanPoint := range resp {
-				h.AssertChannelInGraph(node, chanPoint)
+				h.AssertTopologyChannelOpen(node, chanPoint)
 			}
 		}
 	}
diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go
index 1b079fea1..3d1039846 100644
--- a/lntest/harness_assertion.go
+++ b/lntest/harness_assertion.go
@@ -255,10 +255,10 @@ func (h *HarnessTest) AssertNumActiveEdges(hn *node.HarnessNode,
 	// filterDisabled is a helper closure that filters out disabled
 	// channels.
 	filterDisabled := func(edge *lnrpc.ChannelEdge) bool {
-		if edge.Node1Policy != nil && edge.Node1Policy.Disabled {
+		if edge.Node1Policy.Disabled {
 			return false
 		}
-		if edge.Node2Policy != nil && edge.Node2Policy.Disabled {
+		if edge.Node2Policy.Disabled {
 			return false
 		}
 
@@ -403,6 +403,15 @@ func (h HarnessTest) WaitForChannelOpenEvent(
 	return resp.ChanOpen.ChannelPoint
 }
 
+// AssertTopologyChannelOpen asserts that a given channel outpoint is seen by
+// the passed node's network topology.
+func (h *HarnessTest) AssertTopologyChannelOpen(hn *node.HarnessNode,
+	chanPoint *lnrpc.ChannelPoint) {
+
+	err := hn.Watcher.WaitForChannelOpen(chanPoint)
+	require.NoErrorf(h, err, "%s didn't report channel", hn.Name())
+}
+
 // AssertChannelExists asserts that an active channel identified by the
 // specified channel point exists from the point-of-view of the node.
 func (h *HarnessTest) AssertChannelExists(hn *node.HarnessNode,
@@ -497,8 +506,7 @@ func (h *HarnessTest) findChannel(hn *node.HarnessNode,
 		}
 	}
 
-	return nil, fmt.Errorf("%s: channel not found using %s", hn.Name(),
-		fp.String())
+	return nil, fmt.Errorf("channel not found using %s", chanPoint)
 }
 
 // ReceiveCloseChannelUpdate waits until a message or an error is received on
@@ -1632,18 +1640,23 @@ func (h *HarnessTest) AssertNumHTLCsAndStage(hn *node.HarnessNode,
 // findPayment queries the payment from the node's ListPayments which matches
 // the specified preimage hash.
 func (h *HarnessTest) findPayment(hn *node.HarnessNode,
-	paymentHash string) (*lnrpc.Payment, error) {
+	paymentHash string) *lnrpc.Payment {
 
 	req := &lnrpc.ListPaymentsRequest{IncludeIncomplete: true}
 	paymentsResp := hn.RPC.ListPayments(req)
 
 	for _, p := range paymentsResp.Payments {
-		if p.PaymentHash == paymentHash {
-			return p, nil
+		if p.PaymentHash != paymentHash {
+			continue
 		}
+
+		return p
 	}
 
-	return nil, fmt.Errorf("payment %v cannot be found", paymentHash)
+	require.Failf(h, "payment not found", "payment %v cannot be found",
+		paymentHash)
+
+	return nil
 }
 
 // PaymentCheck is a function that checks a payment for a specific condition.
@@ -1661,11 +1674,7 @@ func (h *HarnessTest) AssertPaymentStatus(hn *node.HarnessNode,
 	payHash := preimage.Hash()
 
 	err := wait.NoError(func() error {
-		p, err := h.findPayment(hn, payHash.String())
-		if err != nil {
-			return err
-		}
-
+		p := h.findPayment(hn, payHash.String())
 		if status == p.Status {
 			target = p
 			return nil
@@ -1704,11 +1713,7 @@ func (h *HarnessTest) AssertPaymentFailureReason(hn *node.HarnessNode,
 
 	payHash := preimage.Hash()
 	err := wait.NoError(func() error {
-		p, err := h.findPayment(hn, payHash.String())
-		if err != nil {
-			return err
-		}
-
+		p := h.findPayment(hn, payHash.String())
 		if reason == p.FailureReason {
 			return nil
 		}
@@ -1937,37 +1942,6 @@ func (h *HarnessTest) AssertNotInGraph(hn *node.HarnessNode, chanID uint64) {
 		"found in graph")
 }
 
-// AssertChannelInGraph asserts that a given channel is found in the graph.
-func (h *HarnessTest) AssertChannelInGraph(hn *node.HarnessNode,
-	chanPoint *lnrpc.ChannelPoint) *lnrpc.ChannelEdge {
-
-	ctxt, cancel := context.WithCancel(h.runCtx)
-	defer cancel()
-
-	var edge *lnrpc.ChannelEdge
-
-	op := h.OutPointFromChannelPoint(chanPoint)
-	err := wait.NoError(func() error {
-		resp, err := hn.RPC.LN.GetChanInfo(
-			ctxt, &lnrpc.ChanInfoRequest{
-				ChanPoint: op.String(),
-			},
-		)
-		if err != nil {
-			return fmt.Errorf("channel %s not found in graph: %w",
-				op, err)
-		}
-
-		edge = resp
-
-		return nil
-	}, DefaultTimeout)
-	require.NoError(h, err, "%s: timeout finding channel in graph",
-		hn.Name())
-
-	return edge
-}
-
 // AssertTxAtHeight gets all of the transactions that a node's wallet has a
 // record of at the target height, and finds and returns the tx with the target
 // txid, failing if it is not found.
diff --git a/lntest/harness_node_manager.go b/lntest/harness_node_manager.go
index 6ad4b9031..90a74f75e 100644
--- a/lntest/harness_node_manager.go
+++ b/lntest/harness_node_manager.go
@@ -46,7 +46,7 @@ type nodeManager struct {
 
 	// nodeCounter is a monotonically increasing counter that's used as the
 	// node's unique ID.
-	nodeCounter atomic.Uint32
+	nodeCounter uint32
 
 	// feeServiceURL is the url of the fee service.
 	feeServiceURL string
@@ -67,8 +67,8 @@ func newNodeManager(lndBinary string, dbBackend node.DatabaseBackend,
 
 // nextNodeID generates a unique sequence to be used as the node's ID.
 func (nm *nodeManager) nextNodeID() uint32 {
-	nodeID := nm.nodeCounter.Add(1)
-	return nodeID
+	nodeID := atomic.AddUint32(&nm.nodeCounter, 1)
+	return nodeID - 1
 }
 
 // newNode initializes a new HarnessNode, supporting the ability to initialize
diff --git a/lntest/node/config.go b/lntest/node/config.go
index 1e8129e7d..f0e3f7059 100644
--- a/lntest/node/config.go
+++ b/lntest/node/config.go
@@ -215,6 +215,7 @@ func (cfg *BaseNodeConfig) GenArgs() []string {
 		fmt.Sprintf("--readonlymacaroonpath=%v", cfg.ReadMacPath),
 		fmt.Sprintf("--invoicemacaroonpath=%v", cfg.InvoiceMacPath),
 		fmt.Sprintf("--trickledelay=%v", trickleDelay),
+		fmt.Sprintf("--profile=%d", cfg.ProfilePort),
 
 		// Use a small batch delay so we can broadcast the
 		// announcements quickly in the tests.
diff --git a/lntest/node/harness_node.go b/lntest/node/harness_node.go
index 9ef1f212c..7415dfed2 100644
--- a/lntest/node/harness_node.go
+++ b/lntest/node/harness_node.go
@@ -399,9 +399,6 @@ func (hn *HarnessNode) StartLndCmd(ctxb context.Context) error {
 		return err
 	}
 
-	pid := hn.cmd.Process.Pid
-	hn.T.Logf("Starting node (name=%v) with PID=%v", hn.Cfg.Name, pid)
-
 	return nil
 }
 
@@ -799,18 +796,6 @@ func (hn *HarnessNode) Kill() error {
 	return hn.cmd.Process.Kill()
 }
 
-// KillAndWait kills the lnd process and waits for it to finish.
-func (hn *HarnessNode) KillAndWait() error {
-	err := hn.cmd.Process.Kill()
-	if err != nil {
-		return err
-	}
-
-	_, err = hn.cmd.Process.Wait()
-
-	return err
-}
-
 // printErrf prints an error to the console.
 func (hn *HarnessNode) printErrf(format string, a ...interface{}) {
 	fmt.Printf("itest error from [%s:%s]: %s\n", //nolint:forbidigo
diff --git a/lntest/node/state.go b/lntest/node/state.go
index a89ab7d2c..6b9446240 100644
--- a/lntest/node/state.go
+++ b/lntest/node/state.go
@@ -312,10 +312,10 @@ func (s *State) updateEdgeStats() {
 	// filterDisabled is a helper closure that filters out disabled
 	// channels.
 	filterDisabled := func(edge *lnrpc.ChannelEdge) bool {
-		if edge.Node1Policy != nil && edge.Node1Policy.Disabled {
+		if edge.Node1Policy.Disabled {
 			return false
 		}
-		if edge.Node2Policy != nil && edge.Node2Policy.Disabled {
+		if edge.Node2Policy.Disabled {
 			return false
 		}
 
diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go
index 2e037c90b..b900e30dc 100644
--- a/lnwallet/channel_test.go
+++ b/lnwallet/channel_test.go
@@ -959,26 +959,24 @@ func testForceClose(t *testing.T, testCase *forceCloseTestCase) {
 	closeSummary, err := aliceChannel.ForceClose()
 	require.NoError(t, err, "unable to force close channel")
 
-	resolutionsAlice := closeSummary.ContractResolutions.UnwrapOrFail(t)
-
 	// Alice should detect that she can sweep the outgoing HTLC after a
 	// timeout, but also that she's able to sweep in incoming HTLC Bob sent
 	// her.
-	if len(resolutionsAlice.HtlcResolutions.OutgoingHTLCs) != 1 {
+	if len(closeSummary.HtlcResolutions.OutgoingHTLCs) != 1 {
 		t.Fatalf("alice out htlc resolutions not populated: expected %v "+
 			"htlcs, got %v htlcs",
-			1, len(resolutionsAlice.HtlcResolutions.OutgoingHTLCs))
+			1, len(closeSummary.HtlcResolutions.OutgoingHTLCs))
 	}
-	if len(resolutionsAlice.HtlcResolutions.IncomingHTLCs) != 1 {
+	if len(closeSummary.HtlcResolutions.IncomingHTLCs) != 1 {
 		t.Fatalf("alice in htlc resolutions not populated: expected %v "+
 			"htlcs, got %v htlcs",
-			1, len(resolutionsAlice.HtlcResolutions.IncomingHTLCs))
+			1, len(closeSummary.HtlcResolutions.IncomingHTLCs))
 	}
 
 	// Verify the anchor resolutions for the anchor commitment format.
 	if testCase.chanType.HasAnchors() {
 		// Check the close summary resolution.
-		anchorRes := resolutionsAlice.AnchorResolution
+		anchorRes := closeSummary.AnchorResolution
 		if anchorRes == nil {
 			t.Fatal("expected anchor resolution")
 		}
@@ -1012,7 +1010,7 @@ func testForceClose(t *testing.T, testCase *forceCloseTestCase) {
 
 	// The SelfOutputSignDesc should be non-nil since the output to-self is
 	// non-dust.
-	aliceCommitResolution := resolutionsAlice.CommitResolution
+	aliceCommitResolution := closeSummary.CommitResolution
 	if aliceCommitResolution == nil {
 		t.Fatalf("alice fails to include to-self output in " +
 			"ForceCloseSummary")
@@ -1058,7 +1056,7 @@ func testForceClose(t *testing.T, testCase *forceCloseTestCase) {
 	// Next, we'll ensure that the second level HTLC transaction it itself
 	// spendable, and also that the delivery output (with delay) itself has
 	// a valid sign descriptor.
-	htlcResolution := resolutionsAlice.HtlcResolutions.OutgoingHTLCs[0]
+	htlcResolution := closeSummary.HtlcResolutions.OutgoingHTLCs[0]
 	outHtlcIndex := htlcResolution.SignedTimeoutTx.TxIn[0].PreviousOutPoint.Index
 	senderHtlcPkScript := closeSummary.CloseTx.TxOut[outHtlcIndex].PkScript
 
@@ -1142,7 +1140,7 @@ func testForceClose(t *testing.T, testCase *forceCloseTestCase) {
 	// We'll now perform similar set of checks to ensure that Alice is able
 	// to sweep the output that Bob sent to her on-chain with knowledge of
 	// the preimage.
-	inHtlcResolution := resolutionsAlice.HtlcResolutions.IncomingHTLCs[0]
+	inHtlcResolution := closeSummary.HtlcResolutions.IncomingHTLCs[0]
 	inHtlcIndex := inHtlcResolution.SignedSuccessTx.TxIn[0].PreviousOutPoint.Index
 	receiverHtlcScript := closeSummary.CloseTx.TxOut[inHtlcIndex].PkScript
 
@@ -1223,8 +1221,7 @@ func testForceClose(t *testing.T, testCase *forceCloseTestCase) {
 	// Check the same for Bob's ForceCloseSummary.
 	closeSummary, err = bobChannel.ForceClose()
 	require.NoError(t, err, "unable to force close channel")
-	resolutionsBob := closeSummary.ContractResolutions.UnwrapOrFail(t)
-	bobCommitResolution := resolutionsBob.CommitResolution
+	bobCommitResolution := closeSummary.CommitResolution
 	if bobCommitResolution == nil {
 		t.Fatalf("bob fails to include to-self output in ForceCloseSummary")
 	}
@@ -1258,19 +1255,19 @@ func testForceClose(t *testing.T, testCase *forceCloseTestCase) {
 	// As we didn't add the preimage of Alice's HTLC to bob's preimage
 	// cache, he should only detect that he can sweep only his outgoing
 	// HTLC upon force close.
-	if len(resolutionsBob.HtlcResolutions.OutgoingHTLCs) != 1 {
+	if len(closeSummary.HtlcResolutions.OutgoingHTLCs) != 1 {
 		t.Fatalf("alice out htlc resolutions not populated: expected %v "+
 			"htlcs, got %v htlcs",
-			1, len(resolutionsBob.HtlcResolutions.OutgoingHTLCs))
+			1, len(closeSummary.HtlcResolutions.OutgoingHTLCs))
 	}
 
 	// Bob should recognize that the incoming HTLC is there, but the
 	// preimage should be empty as he doesn't have the knowledge required
 	// to sweep it.
-	if len(resolutionsBob.HtlcResolutions.IncomingHTLCs) != 1 {
+	if len(closeSummary.HtlcResolutions.IncomingHTLCs) != 1 {
 		t.Fatalf("bob in htlc resolutions not populated: expected %v "+
 			"htlcs, got %v htlcs",
-			1, len(resolutionsBob.HtlcResolutions.IncomingHTLCs))
+			1, len(closeSummary.HtlcResolutions.IncomingHTLCs))
 	}
 }
 
@@ -1329,11 +1326,9 @@ func TestForceCloseDustOutput(t *testing.T) {
 	closeSummary, err := aliceChannel.ForceClose()
 	require.NoError(t, err, "unable to force close channel")
 
-	resolutionsAlice := closeSummary.ContractResolutions.UnwrapOrFail(t)
-
 	// Alice's to-self output should still be in the commitment
 	// transaction.
-	commitResolution := resolutionsAlice.CommitResolution
+	commitResolution := closeSummary.CommitResolution
 	if commitResolution == nil {
 		t.Fatalf("alice fails to include to-self output in " +
 			"ForceCloseSummary")
@@ -1367,11 +1362,10 @@ func TestForceCloseDustOutput(t *testing.T) {
 
 	closeSummary, err = bobChannel.ForceClose()
 	require.NoError(t, err, "unable to force close channel")
-	resolutionsBob := closeSummary.ContractResolutions.UnwrapOrFail(t)
 
 	// Bob's to-self output is below Bob's dust value and should be
 	// reflected in the ForceCloseSummary.
-	commitResolution = resolutionsBob.CommitResolution
+	commitResolution = closeSummary.CommitResolution
 	if commitResolution != nil {
 		t.Fatalf("bob incorrectly includes to-self output in " +
 			"ForceCloseSummary")
diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go
index 791277296..0f7988a32 100644
--- a/lnwallet/transactions_test.go
+++ b/lnwallet/transactions_test.go
@@ -406,8 +406,6 @@ func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) {
 		hex.EncodeToString(txBytes.Bytes()),
 	)
 
-	resolutions := forceCloseSum.ContractResolutions.UnwrapOrFail(t)
-
 	// Obtain the second level transactions that the local node's channel
 	// state machine has produced. Store them in a map indexed by commit tx
 	// output index. Also complete the second level transaction with the
@@ -421,8 +419,7 @@ func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) {
 		secondLevelTxes[index] = tx
 	}
 
-	htlcResolutions := resolutions.HtlcResolutions
-	for _, r := range htlcResolutions.IncomingHTLCs {
+	for _, r := range forceCloseSum.HtlcResolutions.IncomingHTLCs {
 		successTx := r.SignedSuccessTx
 		witnessScript := successTx.TxIn[0].Witness[4]
 		var hash160 [20]byte
@@ -431,7 +428,7 @@ func testVectors(t *testing.T, chanType channeldb.ChannelType, test testCase) {
 		successTx.TxIn[0].Witness[3] = preimage[:]
 		storeTx(r.HtlcPoint().Index, successTx)
 	}
-	for _, r := range htlcResolutions.OutgoingHTLCs {
+	for _, r := range forceCloseSum.HtlcResolutions.OutgoingHTLCs {
 		storeTx(r.HtlcPoint().Index, r.SignedTimeoutTx)
 	}
 
diff --git a/lnwire/fuzz_test.go b/lnwire/fuzz_test.go
index e608eb9b0..d6ec3b61b 100644
--- a/lnwire/fuzz_test.go
+++ b/lnwire/fuzz_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"compress/zlib"
 	"encoding/binary"
+	"reflect"
 	"testing"
 
 	"github.com/stretchr/testify/require"
@@ -19,18 +20,12 @@ func prefixWithMsgType(data []byte, prefix MessageType) []byte {
 	return data
 }
 
-// assertEqualFunc is a function used to assert that two deserialized messages
-// are equivalent.
-type assertEqualFunc func(t *testing.T, x, y any)
-
-// wireMsgHarnessCustom performs the actual fuzz testing of the appropriate wire
-// message. This function will check that the passed-in message passes wire
-// length checks, is a valid message once deserialized, and passes a sequence of
+// harness performs the actual fuzz testing of the appropriate wire message.
+// This function will check that the passed-in message passes wire length
+// checks, is a valid message once deserialized, and passes a sequence of
 // serialization and deserialization checks.
-func wireMsgHarnessCustom(t *testing.T, data []byte, msgType MessageType,
-	assertEqual assertEqualFunc) {
-
-	data = prefixWithMsgType(data, msgType)
+func harness(t *testing.T, data []byte) {
+	t.Helper()
 
 	// Create a reader with the byte array.
 	r := bytes.NewReader(data)
@@ -56,235 +51,356 @@ func wireMsgHarnessCustom(t *testing.T, data []byte, msgType MessageType,
 	// message.
 	newMsg, err := ReadMessage(&b, 0)
 	require.NoError(t, err)
-
-	assertEqual(t, msg, newMsg)
-}
-
-func wireMsgHarness(t *testing.T, data []byte, msgType MessageType) {
-	t.Helper()
-	assertEq := func(t *testing.T, x, y any) {
-		require.Equal(t, x, y)
-	}
-	wireMsgHarnessCustom(t, data, msgType, assertEq)
+	require.Equal(t, msg, newMsg)
 }
 
 func FuzzAcceptChannel(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		// We can't use require.Equal for UpfrontShutdownScript, since
-		// we consider the empty slice and nil to be equivalent.
-		assertEq := func(t *testing.T, x, y any) {
-			require.IsType(t, &AcceptChannel{}, x)
-			first, _ := x.(*AcceptChannel)
-			require.IsType(t, &AcceptChannel{}, y)
-			second, _ := y.(*AcceptChannel)
+		data = prefixWithMsgType(data, MsgAcceptChannel)
+		// Create a reader with the byte array.
+		r := bytes.NewReader(data)
 
-			require.True(
-				t, bytes.Equal(
-					first.UpfrontShutdownScript,
-					second.UpfrontShutdownScript,
-				),
-			)
-			first.UpfrontShutdownScript = nil
-			second.UpfrontShutdownScript = nil
+		// Make sure byte array length (excluding 2 bytes for message
+		// type) is less than max payload size for the wire message.
+		payloadLen := uint32(len(data)) - 2
+		if payloadLen > MaxMsgBody {
+			return
+		}
 
-			require.Equal(t, first, second)
+		msg, err := ReadMessage(r, 0)
+		if err != nil {
+			return
 		}
 
-		wireMsgHarnessCustom(t, data, MsgAcceptChannel, assertEq)
+		// We will serialize the message into a new bytes buffer.
+		var b bytes.Buffer
+		_, err = WriteMessage(&b, msg, 0)
+		require.NoError(t, err)
+
+		// Deserialize the message from the serialized bytes buffer, and
+		// then assert that the original message is equal to the newly
+		// deserialized message.
+		newMsg, err := ReadMessage(&b, 0)
+		require.NoError(t, err)
+
+		require.IsType(t, &AcceptChannel{}, msg)
+		first, _ := msg.(*AcceptChannel)
+		require.IsType(t, &AcceptChannel{}, newMsg)
+		second, _ := newMsg.(*AcceptChannel)
+
+		// We can't use require.Equal for UpfrontShutdownScript, since
+		// we consider the empty slice and nil to be equivalent.
+		require.True(
+			t, bytes.Equal(
+				first.UpfrontShutdownScript,
+				second.UpfrontShutdownScript,
+			),
+		)
+		first.UpfrontShutdownScript = nil
+		second.UpfrontShutdownScript = nil
+
+		require.Equal(t, first, second)
 	})
 }
 
 func FuzzAnnounceSignatures(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgAnnounceSignatures)
-	})
-}
+		// Prefix with MsgAnnounceSignatures.
+		data = prefixWithMsgType(data, MsgAnnounceSignatures)
 
-func FuzzAnnounceSignatures2(f *testing.F) {
-	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgAnnounceSignatures2)
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzChannelAnnouncement(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgChannelAnnouncement)
-	})
-}
-
-func FuzzChannelAnnouncement2(f *testing.F) {
-	f.Fuzz(func(t *testing.T, data []byte) {
-		// We can't use require.Equal for Features, since we consider
-		// the empty map and nil to be equivalent.
-		assertEq := func(t *testing.T, x, y any) {
-			require.IsType(t, &ChannelAnnouncement2{}, x)
-			first, _ := x.(*ChannelAnnouncement2)
-			require.IsType(t, &ChannelAnnouncement2{}, y)
-			second, _ := y.(*ChannelAnnouncement2)
-
-			require.True(
-				t,
-				first.Features.Val.Equals(&second.Features.Val),
-			)
-			first.Features.Val = *NewRawFeatureVector()
-			second.Features.Val = *NewRawFeatureVector()
-
-			require.Equal(t, first, second)
-		}
+		// Prefix with MsgChannelAnnouncement.
+		data = prefixWithMsgType(data, MsgChannelAnnouncement)
 
-		wireMsgHarnessCustom(t, data, MsgChannelAnnouncement2, assertEq)
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzChannelReestablish(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgChannelReestablish)
+		// Prefix with MsgChannelReestablish.
+		data = prefixWithMsgType(data, MsgChannelReestablish)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzChannelUpdate(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgChannelUpdate)
-	})
-}
+		// Prefix with MsgChannelUpdate.
+		data = prefixWithMsgType(data, MsgChannelUpdate)
 
-func FuzzChannelUpdate2(f *testing.F) {
-	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgChannelUpdate2)
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzClosingSigned(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgClosingSigned)
+		// Prefix with MsgClosingSigned.
+		data = prefixWithMsgType(data, MsgClosingSigned)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzCommitSig(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgCommitSig)
+		// Prefix with MsgCommitSig.
+		data = prefixWithMsgType(data, MsgCommitSig)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzError(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgError)
+		// Prefix with MsgError.
+		data = prefixWithMsgType(data, MsgError)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzWarning(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgWarning)
+		// Prefix with MsgWarning.
+		data = prefixWithMsgType(data, MsgWarning)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzStfu(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgStfu)
+		// Prefix with MsgStfu.
+		data = prefixWithMsgType(data, MsgStfu)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages.
+		harness(t, data)
 	})
 }
 
 func FuzzFundingCreated(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgFundingCreated)
+		// Prefix with MsgFundingCreated.
+		data = prefixWithMsgType(data, MsgFundingCreated)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzChannelReady(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgChannelReady)
+		// Prefix with MsgChannelReady.
+		data = prefixWithMsgType(data, MsgChannelReady)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzFundingSigned(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgFundingSigned)
+		// Prefix with MsgFundingSigned.
+		data = prefixWithMsgType(data, MsgFundingSigned)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzGossipTimestampRange(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgGossipTimestampRange)
+		// Prefix with MsgGossipTimestampRange.
+		data = prefixWithMsgType(data, MsgGossipTimestampRange)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzInit(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgInit)
+		// Prefix with MsgInit.
+		data = prefixWithMsgType(data, MsgInit)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzNodeAnnouncement(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
+		// Prefix with MsgNodeAnnouncement.
+		data = prefixWithMsgType(data, MsgNodeAnnouncement)
+
+		// We have to do this here instead of in harness so that
+		// reflect.DeepEqual isn't called. Address (de)serialization
+		// messes up the fuzzing assertions.
+
+		// Create a reader with the byte array.
+		r := bytes.NewReader(data)
+
+		// Make sure byte array length (excluding 2 bytes for message
+		// type) is less than max payload size for the wire message.
+		payloadLen := uint32(len(data)) - 2
+		if payloadLen > MaxMsgBody {
+			return
+		}
+
+		msg, err := ReadMessage(r, 0)
+		if err != nil {
+			return
+		}
+
+		// We will serialize the message into a new bytes buffer.
+		var b bytes.Buffer
+		_, err = WriteMessage(&b, msg, 0)
+		require.NoError(t, err)
+
+		// Deserialize the message from the serialized bytes buffer, and
+		// then assert that the original message is equal to the newly
+		// deserialized message.
+		newMsg, err := ReadMessage(&b, 0)
+		require.NoError(t, err)
+
+		require.IsType(t, &NodeAnnouncement{}, msg)
+		first, _ := msg.(*NodeAnnouncement)
+		require.IsType(t, &NodeAnnouncement{}, newMsg)
+		second, _ := newMsg.(*NodeAnnouncement)
+
 		// We can't use require.Equal for Addresses, since the same IP
 		// can be represented by different underlying bytes. Instead, we
 		// compare the normalized string representation of each address.
-		assertEq := func(t *testing.T, x, y any) {
-			require.IsType(t, &NodeAnnouncement{}, x)
-			first, _ := x.(*NodeAnnouncement)
-			require.IsType(t, &NodeAnnouncement{}, y)
-			second, _ := y.(*NodeAnnouncement)
-
+		require.Equal(t, len(first.Addresses), len(second.Addresses))
+		for i := range first.Addresses {
 			require.Equal(
-				t, len(first.Addresses), len(second.Addresses),
+				t, first.Addresses[i].String(),
+				second.Addresses[i].String(),
 			)
-			for i := range first.Addresses {
-				require.Equal(
-					t, first.Addresses[i].String(),
-					second.Addresses[i].String(),
-				)
-			}
-			first.Addresses = nil
-			second.Addresses = nil
-
-			require.Equal(t, first, second)
 		}
+		first.Addresses = nil
+		second.Addresses = nil
 
-		wireMsgHarnessCustom(t, data, MsgNodeAnnouncement, assertEq)
+		require.Equal(t, first, second)
 	})
 }
 
 func FuzzOpenChannel(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		// We can't use require.Equal for UpfrontShutdownScript, since
-		// we consider the empty slice and nil to be equivalent.
-		assertEq := func(t *testing.T, x, y any) {
-			require.IsType(t, &OpenChannel{}, x)
-			first, _ := x.(*OpenChannel)
-			require.IsType(t, &OpenChannel{}, y)
-			second, _ := y.(*OpenChannel)
+		// Prefix with MsgOpenChannel.
+		data = prefixWithMsgType(data, MsgOpenChannel)
 
-			require.True(
-				t, bytes.Equal(
-					first.UpfrontShutdownScript,
-					second.UpfrontShutdownScript,
-				),
-			)
-			first.UpfrontShutdownScript = nil
-			second.UpfrontShutdownScript = nil
+		// We have to do this here instead of in harness so that
+		// reflect.DeepEqual isn't called. Because of the
+		// UpfrontShutdownScript encoding, the first message and second
+		// message aren't deeply equal since the first has a nil slice
+		// and the other has an empty slice.
+
+		// Create a reader with the byte array.
+		r := bytes.NewReader(data)
 
-			require.Equal(t, first, second)
+		// Make sure byte array length (excluding 2 bytes for message
+		// type) is less than max payload size for the wire message.
+		payloadLen := uint32(len(data)) - 2
+		if payloadLen > MaxMsgBody {
+			return
+		}
+
+		msg, err := ReadMessage(r, 0)
+		if err != nil {
+			return
 		}
 
-		wireMsgHarnessCustom(t, data, MsgOpenChannel, assertEq)
+		// We will serialize the message into a new bytes buffer.
+		var b bytes.Buffer
+		_, err = WriteMessage(&b, msg, 0)
+		require.NoError(t, err)
+
+		// Deserialize the message from the serialized bytes buffer, and
+		// then assert that the original message is equal to the newly
+		// deserialized message.
+		newMsg, err := ReadMessage(&b, 0)
+		require.NoError(t, err)
+
+		require.IsType(t, &OpenChannel{}, msg)
+		first, _ := msg.(*OpenChannel)
+		require.IsType(t, &OpenChannel{}, newMsg)
+		second, _ := newMsg.(*OpenChannel)
+
+		// We can't use require.Equal for UpfrontShutdownScript, since
+		// we consider the empty slice and nil to be equivalent.
+		require.True(
+			t, bytes.Equal(
+				first.UpfrontShutdownScript,
+				second.UpfrontShutdownScript,
+			),
+		)
+		first.UpfrontShutdownScript = nil
+		second.UpfrontShutdownScript = nil
+
+		require.Equal(t, first, second)
 	})
 }
 
 func FuzzPing(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgPing)
+		// Prefix with MsgPing.
+		data = prefixWithMsgType(data, MsgPing)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzPong(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgPong)
+		// Prefix with MsgPong.
+		data = prefixWithMsgType(data, MsgPong)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzQueryChannelRange(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgQueryChannelRange)
+		// Prefix with MsgQueryChannelRange.
+		data = prefixWithMsgType(data, MsgQueryChannelRange)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
@@ -312,13 +428,23 @@ func FuzzZlibQueryShortChanIDs(f *testing.F) {
 		payload = append(payload, zlibByte...)
 		payload = append(payload, compressedPayload...)
 
-		wireMsgHarness(t, payload, MsgQueryShortChanIDs)
+		// Prefix with MsgQueryShortChanIDs.
+		payload = prefixWithMsgType(payload, MsgQueryShortChanIDs)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, payload)
 	})
 }
 
 func FuzzQueryShortChanIDs(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgQueryShortChanIDs)
+		// Prefix with MsgQueryShortChanIDs.
+		data = prefixWithMsgType(data, MsgQueryShortChanIDs)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
@@ -354,107 +480,193 @@ func FuzzZlibReplyChannelRange(f *testing.F) {
 		payload = append(payload, zlibByte...)
 		payload = append(payload, compressedPayload...)
 
-		wireMsgHarness(t, payload, MsgReplyChannelRange)
+		// Prefix with MsgReplyChannelRange.
+		payload = prefixWithMsgType(payload, MsgReplyChannelRange)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, payload)
 	})
 }
 
 func FuzzReplyChannelRange(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
+		// Prefix with MsgReplyChannelRange.
+		data = prefixWithMsgType(data, MsgReplyChannelRange)
+
+		// Because require.Equal considers nil slices and empty slices
+		// to be non-equal, we must manually compare the Timestamps
+		// field rather than using the harness.
+
+		if len(data) > MaxSliceLength {
+			return
+		}
+
+		r := bytes.NewReader(data)
+		msg, err := ReadMessage(r, 0)
+		if err != nil {
+			return
+		}
+
+		// We will serialize the message into a new bytes buffer.
+		var b bytes.Buffer
+		_, err = WriteMessage(&b, msg, 0)
+		require.NoError(t, err)
+
+		// Deserialize the message from the serialized bytes buffer, and
+		// then assert that the original message is equal to the newly
+		// deserialized message.
+		newMsg, err := ReadMessage(&b, 0)
+		require.NoError(t, err)
+
+		require.IsType(t, &ReplyChannelRange{}, msg)
+		first, _ := msg.(*ReplyChannelRange)
+		require.IsType(t, &ReplyChannelRange{}, newMsg)
+		second, _ := newMsg.(*ReplyChannelRange)
+
 		// We can't use require.Equal for Timestamps, since we consider
 		// the empty slice and nil to be equivalent.
-		assertEq := func(t *testing.T, x, y any) {
-			require.IsType(t, &ReplyChannelRange{}, x)
-			first, _ := x.(*ReplyChannelRange)
-			require.IsType(t, &ReplyChannelRange{}, y)
-			second, _ := y.(*ReplyChannelRange)
-
-			require.Equal(
-				t, len(first.Timestamps),
-				len(second.Timestamps),
-			)
-			for i, ts1 := range first.Timestamps {
-				ts2 := second.Timestamps[i]
-				require.Equal(t, ts1, ts2)
-			}
-			first.Timestamps = nil
-			second.Timestamps = nil
-
-			require.Equal(t, first, second)
+		require.Equal(t, len(first.Timestamps), len(second.Timestamps))
+		for i, ts1 := range first.Timestamps {
+			ts2 := second.Timestamps[i]
+			require.Equal(t, ts1, ts2)
 		}
+		first.Timestamps = nil
+		second.Timestamps = nil
 
-		wireMsgHarnessCustom(t, data, MsgReplyChannelRange, assertEq)
+		require.Equal(t, first, second)
 	})
 }
 
 func FuzzReplyShortChanIDsEnd(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgReplyShortChanIDsEnd)
+		// Prefix with MsgReplyShortChanIDsEnd.
+		data = prefixWithMsgType(data, MsgReplyShortChanIDsEnd)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzRevokeAndAck(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgRevokeAndAck)
+		// Prefix with MsgRevokeAndAck.
+		data = prefixWithMsgType(data, MsgRevokeAndAck)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzShutdown(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgShutdown)
+		// Prefix with MsgShutdown.
+		data = prefixWithMsgType(data, MsgShutdown)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzUpdateAddHTLC(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgUpdateAddHTLC)
+		// Prefix with MsgUpdateAddHTLC.
+		data = prefixWithMsgType(data, MsgUpdateAddHTLC)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzUpdateFailHTLC(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgUpdateFailHTLC)
+		// Prefix with MsgUpdateFailHTLC.
+		data = prefixWithMsgType(data, MsgUpdateFailHTLC)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzUpdateFailMalformedHTLC(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgUpdateFailMalformedHTLC)
+		// Prefix with MsgUpdateFailMalformedHTLC.
+		data = prefixWithMsgType(data, MsgUpdateFailMalformedHTLC)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzUpdateFee(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgUpdateFee)
+		// Prefix with MsgUpdateFee.
+		data = prefixWithMsgType(data, MsgUpdateFee)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzUpdateFulfillHTLC(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgUpdateFulfillHTLC)
+		// Prefix with MsgUpdateFulFillHTLC.
+		data = prefixWithMsgType(data, MsgUpdateFulfillHTLC)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzDynPropose(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgDynPropose)
+		// Prefix with DynPropose.
+		data = prefixWithMsgType(data, MsgDynPropose)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzDynReject(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgDynReject)
+		// Prefix with DynReject.
+		data = prefixWithMsgType(data, MsgDynReject)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzDynAck(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgDynAck)
+		// Prefix with DynReject.
+		data = prefixWithMsgType(data, MsgDynAck)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzKickoffSig(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgKickoffSig)
+		// Prefix with KickoffSig
+		data = prefixWithMsgType(data, MsgKickoffSig)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
@@ -464,7 +676,12 @@ func FuzzCustomMessage(f *testing.F) {
 			customMessageType += uint16(CustomTypeStart)
 		}
 
-		wireMsgHarness(t, data, MessageType(customMessageType))
+		// Prefix with CustomMessage.
+		data = prefixWithMsgType(data, MessageType(customMessageType))
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
@@ -513,37 +730,6 @@ func FuzzConvertFixedSignature(f *testing.F) {
 	})
 }
 
-// FuzzConvertFixedSchnorrSignature tests that conversion of fixed 64-byte
-// Schnorr signatures to and from the btcec format does not panic or mutate the
-// signatures.
-func FuzzConvertFixedSchnorrSignature(f *testing.F) {
-	f.Fuzz(func(t *testing.T, data []byte) {
-		var sig Sig
-		if len(data) > len(sig.bytes[:]) {
-			return
-		}
-		copy(sig.bytes[:], data)
-		sig.ForceSchnorr()
-
-		btcecSig, err := sig.ToSignature()
-		if err != nil {
-			return
-		}
-
-		sig2, err := NewSigFromSignature(btcecSig)
-		require.NoError(t, err, "failed to parse signature")
-
-		btcecSig2, err := sig2.ToSignature()
-		require.NoError(
-			t, err, "failed to reconvert signature to btcec format",
-		)
-
-		btcecBytes := btcecSig.Serialize()
-		btcecBytes2 := btcecSig2.Serialize()
-		require.Equal(t, btcecBytes, btcecBytes2, "signature mismatch")
-	})
-}
-
 // prefixWithFailCode adds a failure code prefix to data.
 func prefixWithFailCode(data []byte, code FailCode) []byte {
 	var codeBytes [2]byte
@@ -553,12 +739,16 @@ func prefixWithFailCode(data []byte, code FailCode) []byte {
 	return data
 }
 
+// equalFunc is a function used to determine whether two deserialized messages
+// are equivalent.
+type equalFunc func(x, y any) bool
+
 // onionFailureHarnessCustom performs the actual fuzz testing of the appropriate
 // onion failure message. This function will check that the passed-in message
 // passes wire length checks, is a valid message once deserialized, and passes a
 // sequence of serialization and deserialization checks.
 func onionFailureHarnessCustom(t *testing.T, data []byte, code FailCode,
-	assertEqual assertEqualFunc) {
+	eq equalFunc) {
 
 	data = prefixWithFailCode(data, code)
 
@@ -584,7 +774,12 @@ func onionFailureHarnessCustom(t *testing.T, data []byte, code FailCode,
 	newMsg, err := DecodeFailureMessage(&b, 0)
 	require.NoError(t, err, "failed to decode serialized failure message")
 
-	assertEqual(t, msg, newMsg)
+	require.True(
+		t, eq(msg, newMsg),
+		"original message and deserialized message are not equal: "+
+			"%v != %v",
+		msg, newMsg,
+	)
 
 	// Now verify that encoding/decoding full packets works as expected.
 
@@ -618,15 +813,17 @@ func onionFailureHarnessCustom(t *testing.T, data []byte, code FailCode,
 	pktMsg, err := DecodeFailure(&pktBuf, 0)
 	require.NoError(t, err, "failed to decode failure packet")
 
-	assertEqual(t, msg, pktMsg)
+	require.True(
+		t, eq(msg, pktMsg),
+		"original message and decoded packet message are not equal: "+
+			"%v != %v",
+		msg, pktMsg,
+	)
 }
 
 func onionFailureHarness(t *testing.T, data []byte, code FailCode) {
 	t.Helper()
-	assertEq := func(t *testing.T, x, y any) {
-		require.Equal(t, x, y)
-	}
-	onionFailureHarnessCustom(t, data, code, assertEq)
+	onionFailureHarnessCustom(t, data, code, reflect.DeepEqual)
 }
 
 func FuzzFailIncorrectDetails(f *testing.F) {
@@ -634,7 +831,7 @@ func FuzzFailIncorrectDetails(f *testing.F) {
 		// Since FailIncorrectDetails.Decode can leave extraOpaqueData
 		// as nil while FailIncorrectDetails.Encode writes an empty
 		// slice, we need to use a custom equality function.
-		assertEq := func(t *testing.T, x, y any) {
+		eq := func(x, y any) bool {
 			msg1, ok := x.(*FailIncorrectDetails)
 			require.True(
 				t, ok, "msg1 was not FailIncorrectDetails",
@@ -645,18 +842,16 @@ func FuzzFailIncorrectDetails(f *testing.F) {
 				t, ok, "msg2 was not FailIncorrectDetails",
 			)
 
-			require.Equal(t, msg1.amount, msg2.amount)
-			require.Equal(t, msg1.height, msg2.height)
-			require.True(
-				t, bytes.Equal(
+			return msg1.amount == msg2.amount &&
+				msg1.height == msg2.height &&
+				bytes.Equal(
 					msg1.extraOpaqueData,
 					msg2.extraOpaqueData,
-				),
-			)
+				)
 		}
 
 		onionFailureHarnessCustom(
-			t, data, CodeIncorrectOrUnknownPaymentDetails, assertEq,
+			t, data, CodeIncorrectOrUnknownPaymentDetails, eq,
 		)
 	})
 }
@@ -733,47 +928,24 @@ func FuzzInvalidOnionPayload(f *testing.F) {
 	})
 }
 
-func FuzzFailInvalidBlinding(f *testing.F) {
-	f.Fuzz(func(t *testing.T, data []byte) {
-		onionFailureHarness(t, data, CodeInvalidBlinding)
-	})
-}
-
 func FuzzClosingSig(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgClosingSig)
+		// Prefix with ClosingSig.
+		data = prefixWithMsgType(data, MsgClosingSig)
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
 
 func FuzzClosingComplete(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, MsgClosingComplete)
-	})
-}
+		// Prefix with ClosingComplete.
+		data = prefixWithMsgType(data, MsgClosingComplete)
 
-// FuzzFee tests that decoding and re-encoding a Fee TLV does not mutate it.
-func FuzzFee(f *testing.F) {
-	f.Fuzz(func(t *testing.T, data []byte) {
-		if len(data) > 8 {
-			return
-		}
-
-		var fee Fee
-		var buf [8]byte
-		r := bytes.NewReader(data)
-
-		if err := feeDecoder(r, &fee, &buf, 8); err != nil {
-			return
-		}
-
-		var b bytes.Buffer
-		require.NoError(t, feeEncoder(&b, &fee, &buf))
-
-		// Use bytes.Equal instead of require.Equal so that nil and
-		// empty slices are considered equal.
-		require.True(
-			t, bytes.Equal(data, b.Bytes()), "%v != %v", data,
-			b.Bytes(),
-		)
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data)
 	})
 }
diff --git a/macaroons/fuzz_test.go b/macaroons/fuzz_test.go
deleted file mode 100644
index defae4143..000000000
--- a/macaroons/fuzz_test.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package macaroons
-
-import (
-	"context"
-	"testing"
-
-	"gopkg.in/macaroon-bakery.v2/bakery"
-	"gopkg.in/macaroon.v2"
-)
-
-func FuzzUnmarshalMacaroon(f *testing.F) {
-	f.Fuzz(func(t *testing.T, data []byte) {
-		mac := &macaroon.Macaroon{}
-		_ = mac.UnmarshalBinary(data)
-	})
-}
-
-func FuzzAuthChecker(f *testing.F) {
-	rootKeyStore := bakery.NewMemRootKeyStore()
-	ctx := context.Background()
-
-	f.Fuzz(func(t *testing.T, location, entity, action, method string,
-		rootKey, id []byte) {
-
-		macService, err := NewService(
-			rootKeyStore, location, true, IPLockChecker,
-		)
-		if err != nil {
-			return
-		}
-
-		requiredPermissions := []bakery.Op{{
-			Entity: entity,
-			Action: action,
-		}}
-
-		mac, err := macaroon.New(rootKey, id, location, macaroon.V2)
-		if err != nil {
-			return
-		}
-
-		macBytes, err := mac.MarshalBinary()
-		if err != nil {
-			return
-		}
-
-		_ = macService.CheckMacAuth(
-			ctx, macBytes, requiredPermissions, method,
-		)
-	})
-}
diff --git a/protofsm/state_machine_test.go b/protofsm/state_machine_test.go
deleted file mode 100644
index 0432f386b..000000000
--- a/protofsm/state_machine_test.go
+++ /dev/null
@@ -1,456 +0,0 @@
-package protofsm
-
-import (
-	"encoding/hex"
-	"fmt"
-	"sync/atomic"
-	"testing"
-
-	"github.com/btcsuite/btcd/btcec/v2"
-	"github.com/btcsuite/btcd/chaincfg/chainhash"
-	"github.com/btcsuite/btcd/wire"
-	"github.com/lightningnetwork/lnd/chainntnfs"
-	"github.com/lightningnetwork/lnd/fn"
-	"github.com/lightningnetwork/lnd/lnwire"
-	"github.com/stretchr/testify/mock"
-	"github.com/stretchr/testify/require"
-)
-
-type dummyEvents interface {
-	dummy()
-}
-
-type goToFin struct {
-}
-
-func (g *goToFin) dummy() {
-}
-
-type emitInternal struct {
-}
-
-func (e *emitInternal) dummy() {
-}
-
-type daemonEvents struct {
-}
-
-func (s *daemonEvents) dummy() {
-}
-
-type dummyEnv struct {
-	mock.Mock
-}
-
-func (d *dummyEnv) Name() string {
-	return "test"
-}
-
-type dummyStateStart struct {
-	canSend *atomic.Bool
-}
-
-var (
-	hexDecode = func(keyStr string) []byte {
-		keyBytes, _ := hex.DecodeString(keyStr)
-		return keyBytes
-	}
-	pub1, _ = btcec.ParsePubKey(hexDecode(
-		"02ec95e4e8ad994861b95fc5986eedaac24739e5ea3d0634db4c8ccd44cd" +
-			"a126ea",
-	))
-	pub2, _ = btcec.ParsePubKey(hexDecode(
-		"0356167ba3e54ac542e86e906d4186aba9ca0b9df45001c62b753d33fe06" +
-			"f5b4e8",
-	))
-)
-
-func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
-) (*StateTransition[dummyEvents, *dummyEnv], error) {
-
-	switch event.(type) {
-	case *goToFin:
-		return &StateTransition[dummyEvents, *dummyEnv]{
-			NextState: &dummyStateFin{},
-		}, nil
-
-	// This state will loop back upon itself, but will also emit an event
-	// to head to the terminal state.
-	case *emitInternal:
-		return &StateTransition[dummyEvents, *dummyEnv]{
-			NextState: &dummyStateStart{},
-			NewEvents: fn.Some(EmittedEvent[dummyEvents]{
-				InternalEvent: fn.Some(
-					[]dummyEvents{&goToFin{}},
-				),
-			}),
-		}, nil
-
-	// This state will proceed to the terminal state, but will emit all the
-	// possible daemon events.
-	case *daemonEvents:
-		// This send event can only succeed once the bool turns to
-		// true. After that, then we'll expect another event to take us
-		// to the final state.
-		sendEvent := &SendMsgEvent[dummyEvents]{
-			TargetPeer: *pub1,
-			SendWhen: fn.Some(func() bool {
-				return d.canSend.Load()
-			}),
-			PostSendEvent: fn.Some(dummyEvents(&goToFin{})),
-		}
-
-		// We'll also send out a normal send event that doesn't have
-		// any preconditions.
-		sendEvent2 := &SendMsgEvent[dummyEvents]{
-			TargetPeer: *pub2,
-		}
-
-		return &StateTransition[dummyEvents, *dummyEnv]{
-			// We'll state in this state until the send succeeds
-			// based on our predicate. Then it'll transition to the
-			// final state.
-			NextState: &dummyStateStart{
-				canSend: d.canSend,
-			},
-			NewEvents: fn.Some(EmittedEvent[dummyEvents]{
-				ExternalEvents: fn.Some(DaemonEventSet{
-					sendEvent, sendEvent2,
-					&BroadcastTxn{
-						Tx:    &wire.MsgTx{},
-						Label: "test",
-					},
-				}),
-			}),
-		}, nil
-	}
-
-	return nil, fmt.Errorf("unknown event: %T", event)
-}
-
-func (d *dummyStateStart) IsTerminal() bool {
-	return false
-}
-
-type dummyStateFin struct {
-}
-
-func (d *dummyStateFin) ProcessEvent(event dummyEvents, env *dummyEnv,
-) (*StateTransition[dummyEvents, *dummyEnv], error) {
-
-	return &StateTransition[dummyEvents, *dummyEnv]{
-		NextState: &dummyStateFin{},
-	}, nil
-}
-
-func (d *dummyStateFin) IsTerminal() bool {
-	return true
-}
-
-func assertState[Event any, Env Environment](t *testing.T,
-	m *StateMachine[Event, Env], expectedState State[Event, Env]) {
-
-	state, err := m.CurrentState()
-	require.NoError(t, err)
-	require.IsType(t, expectedState, state)
-}
-
-func assertStateTransitions[Event any, Env Environment](
-	t *testing.T, stateSub StateSubscriber[Event, Env],
-	expectedStates []State[Event, Env]) {
-
-	for _, expectedState := range expectedStates {
-		newState := <-stateSub.NewItemCreated.ChanOut()
-
-		require.IsType(t, expectedState, newState)
-	}
-}
-
-type dummyAdapters struct {
-	mock.Mock
-
-	confChan  chan *chainntnfs.TxConfirmation
-	spendChan chan *chainntnfs.SpendDetail
-}
-
-func newDaemonAdapters() *dummyAdapters {
-	return &dummyAdapters{
-		confChan:  make(chan *chainntnfs.TxConfirmation, 1),
-		spendChan: make(chan *chainntnfs.SpendDetail, 1),
-	}
-}
-
-func (d *dummyAdapters) SendMessages(pub btcec.PublicKey,
-	msgs []lnwire.Message) error {
-
-	args := d.Called(pub, msgs)
-
-	return args.Error(0)
-}
-
-func (d *dummyAdapters) BroadcastTransaction(tx *wire.MsgTx,
-	label string) error {
-
-	args := d.Called(tx, label)
-
-	return args.Error(0)
-}
-
-func (d *dummyAdapters) RegisterConfirmationsNtfn(txid *chainhash.Hash,
-	pkScript []byte, numConfs, heightHint uint32,
-	opts ...chainntnfs.NotifierOption,
-) (*chainntnfs.ConfirmationEvent, error) {
-
-	args := d.Called(txid, pkScript, numConfs)
-
-	err := args.Error(0)
-
-	return &chainntnfs.ConfirmationEvent{
-		Confirmed: d.confChan,
-	}, err
-}
-
-func (d *dummyAdapters) RegisterSpendNtfn(outpoint *wire.OutPoint,
-	pkScript []byte, heightHint uint32) (*chainntnfs.SpendEvent, error) {
-
-	args := d.Called(outpoint, pkScript, heightHint)
-
-	err := args.Error(0)
-
-	return &chainntnfs.SpendEvent{
-		Spend: d.spendChan,
-	}, err
-}
-
-// TestStateMachineOnInitDaemonEvent tests that the state machine will properly
-// execute any init-level daemon events passed into it.
-func TestStateMachineOnInitDaemonEvent(t *testing.T) {
-	// First, we'll create our state machine given the env, and our
-	// starting state.
-	env := &dummyEnv{}
-	startingState := &dummyStateStart{}
-
-	adapters := newDaemonAdapters()
-
-	// We'll make an init event that'll send to a peer, then transition us
-	// to our terminal state.
-	initEvent := &SendMsgEvent[dummyEvents]{
-		TargetPeer:    *pub1,
-		PostSendEvent: fn.Some(dummyEvents(&goToFin{})),
-	}
-
-	cfg := StateMachineCfg[dummyEvents, *dummyEnv]{
-		Daemon:       adapters,
-		InitialState: startingState,
-		Env:          env,
-		InitEvent:    fn.Some[DaemonEvent](initEvent),
-	}
-	stateMachine := NewStateMachine(cfg)
-
-	// Before we start up the state machine, we'll assert that the send
-	// message adapter is called on start up.
-	adapters.On("SendMessages", *pub1, mock.Anything).Return(nil)
-
-	stateMachine.Start()
-	defer stateMachine.Stop()
-
-	// As we're triggering internal events, we'll also subscribe to the set
-	// of new states so we can assert as we go.
-	stateSub := stateMachine.RegisterStateEvents()
-	defer stateMachine.RemoveStateSub(stateSub)
-
-	// Assert that we go from the starting state to the final state.  The
-	// state machine should now also be on the final terminal state.
-	expectedStates := []State[dummyEvents, *dummyEnv]{
-		&dummyStateStart{}, &dummyStateFin{},
-	}
-	assertStateTransitions(t, stateSub, expectedStates)
-
-	// We'll now assert that after the daemon was started, the send message
-	// adapter was called above as specified in the init event.
-	adapters.AssertExpectations(t)
-	env.AssertExpectations(t)
-}
-
-// TestStateMachineInternalEvents tests that the state machine is able to add
-// new internal events to the event queue for further processing during a state
-// transition.
-func TestStateMachineInternalEvents(t *testing.T) {
-	t.Parallel()
-
-	// First, we'll create our state machine given the env, and our
-	// starting state.
-	env := &dummyEnv{}
-	startingState := &dummyStateStart{}
-
-	adapters := newDaemonAdapters()
-
-	cfg := StateMachineCfg[dummyEvents, *dummyEnv]{
-		Daemon:       adapters,
-		InitialState: startingState,
-		Env:          env,
-		InitEvent:    fn.None[DaemonEvent](),
-	}
-	stateMachine := NewStateMachine(cfg)
-	stateMachine.Start()
-	defer stateMachine.Stop()
-
-	// As we're triggering internal events, we'll also subscribe to the set
-	// of new states so we can assert as we go.
-	stateSub := stateMachine.RegisterStateEvents()
-	defer stateMachine.RemoveStateSub(stateSub)
-
-	// For this transition, we'll send in the emitInternal event, which'll
-	// send us back to the starting event, but emit an internal event.
-	stateMachine.SendEvent(&emitInternal{})
-
-	// We'll now also assert the path we took to get here to ensure the
-	// internal events were processed.
-	expectedStates := []State[dummyEvents, *dummyEnv]{
-		&dummyStateStart{}, &dummyStateStart{}, &dummyStateFin{},
-	}
-	assertStateTransitions(
-		t, stateSub, expectedStates,
-	)
-
-	// We should ultimately end up in the terminal state.
-	assertState[dummyEvents, *dummyEnv](t, &stateMachine, &dummyStateFin{})
-
-	// Make sure all the env expectations were met.
-	env.AssertExpectations(t)
-}
-
-// TestStateMachineDaemonEvents tests that the state machine is able to process
-// daemon emitted as part of the state transition process.
-func TestStateMachineDaemonEvents(t *testing.T) {
-	t.Parallel()
-
-	// First, we'll create our state machine given the env, and our
-	// starting state.
-	env := &dummyEnv{}
-
-	var boolTrigger atomic.Bool
-	startingState := &dummyStateStart{
-		canSend: &boolTrigger,
-	}
-
-	adapters := newDaemonAdapters()
-
-	cfg := StateMachineCfg[dummyEvents, *dummyEnv]{
-		Daemon:       adapters,
-		InitialState: startingState,
-		Env:          env,
-		InitEvent:    fn.None[DaemonEvent](),
-	}
-	stateMachine := NewStateMachine(cfg)
-	stateMachine.Start()
-	defer stateMachine.Stop()
-
-	// As we're triggering internal events, we'll also subscribe to the set
-	// of new states so we can assert as we go.
-	stateSub := stateMachine.RegisterStateEvents()
-	defer stateMachine.RemoveStateSub(stateSub)
-
-	// As soon as we send in the daemon event, we expect the
-	// disable+broadcast events to be processed, as they are unconditional.
-	adapters.On(
-		"BroadcastTransaction", mock.Anything, mock.Anything,
-	).Return(nil)
-	adapters.On("SendMessages", *pub2, mock.Anything).Return(nil)
-
-	// We'll start off by sending in the daemon event, which'll trigger the
-	// state machine to execute the series of daemon events.
-	stateMachine.SendEvent(&daemonEvents{})
-
-	// We should transition back to the starting state now, after we
-	// started from the very same state.
-	expectedStates := []State[dummyEvents, *dummyEnv]{
-		&dummyStateStart{}, &dummyStateStart{},
-	}
-	assertStateTransitions(t, stateSub, expectedStates)
-
-	// At this point, we expect that the two methods above were called.
-	adapters.AssertExpectations(t)
-
-	// However, we don't expect the SendMessages for the first peer target
-	// to be called yet, as the condition hasn't yet been met.
-	adapters.AssertNotCalled(t, "SendMessages", *pub1)
-
-	// We'll now flip the bool to true, which should cause the SendMessages
-	// method to be called, and for us to transition to the final state.
-	boolTrigger.Store(true)
-	adapters.On("SendMessages", *pub1, mock.Anything).Return(nil)
-
-	expectedStates = []State[dummyEvents, *dummyEnv]{&dummyStateFin{}}
-	assertStateTransitions(t, stateSub, expectedStates)
-
-	adapters.AssertExpectations(t)
-	env.AssertExpectations(t)
-}
-
-type dummyMsgMapper struct {
-	mock.Mock
-}
-
-func (d *dummyMsgMapper) MapMsg(wireMsg lnwire.Message) fn.Option[dummyEvents] {
-	args := d.Called(wireMsg)
-
-	//nolint:forcetypeassert
-	return args.Get(0).(fn.Option[dummyEvents])
-}
-
-// TestStateMachineMsgMapper tests that given a message mapper, we can properly
-// send in wire messages get mapped to FSM events.
-func TestStateMachineMsgMapper(t *testing.T) {
-	// First, we'll create our state machine given the env, and our
-	// starting state.
-	env := &dummyEnv{}
-	startingState := &dummyStateStart{}
-	adapters := newDaemonAdapters()
-
-	// We'll also provide a message mapper that only knows how to map a
-	// single wire message (error).
-	dummyMapper := &dummyMsgMapper{}
-
-	// The only thing we know how to map is the error message, which'll
-	// terminate the state machine.
-	wireError := &lnwire.Error{}
-	initMsg := &lnwire.Init{}
-	dummyMapper.On("MapMsg", wireError).Return(
-		fn.Some(dummyEvents(&goToFin{})),
-	)
-	dummyMapper.On("MapMsg", initMsg).Return(fn.None[dummyEvents]())
-
-	cfg := StateMachineCfg[dummyEvents, *dummyEnv]{
-		Daemon:       adapters,
-		InitialState: startingState,
-		Env:          env,
-		MsgMapper:    fn.Some[MsgMapper[dummyEvents]](dummyMapper),
-	}
-	stateMachine := NewStateMachine(cfg)
-	stateMachine.Start()
-	defer stateMachine.Stop()
-
-	// As we're triggering internal events, we'll also subscribe to the set
-	// of new states so we can assert as we go.
-	stateSub := stateMachine.RegisterStateEvents()
-	defer stateMachine.RemoveStateSub(stateSub)
-
-	// First, we'll verify that the CanHandle method works as expected.
-	require.True(t, stateMachine.CanHandle(wireError))
-	require.False(t, stateMachine.CanHandle(&lnwire.Init{}))
-
-	// Next, we'll attempt to send the wire message into the state machine.
-	// We should transition to the final state.
-	require.True(t, stateMachine.SendMessage(wireError))
-
-	// We should transition to the final state.
-	expectedStates := []State[dummyEvents, *dummyEnv]{
-		&dummyStateStart{}, &dummyStateFin{},
-	}
-	assertStateTransitions(t, stateSub, expectedStates)
-
-	dummyMapper.AssertExpectations(t)
-	adapters.AssertExpectations(t)
-	env.AssertExpectations(t)
-}
diff --git a/routing/localchans/manager_test.go b/routing/localchans/manager_test.go
index 7594eef04..f4512fab2 100644
--- a/routing/localchans/manager_test.go
+++ b/routing/localchans/manager_test.go
@@ -1,14 +1,19 @@
 package localchans
 
 import (
+	"encoding/hex"
 	"testing"
+	"time"
 
+	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcutil"
+	"github.com/btcsuite/btcd/chaincfg"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/channeldb/models"
 	"github.com/lightningnetwork/lnd/discovery"
+	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/kvdb"
 	"github.com/lightningnetwork/lnd/lnrpc"
 	"github.com/lightningnetwork/lnd/lnwire"
@@ -35,6 +40,18 @@ func TestManager(t *testing.T) {
 		channelSet         []channel
 	)
 
+	sp := [33]byte{}
+	_, _ = hex.Decode(sp[:], []byte("028d7500dd4c12685d1f568b4c2b5048e85"+
+		"34b873319f3a8daa612b469132ec7f7"))
+	rp := [33]byte{}
+	_, _ = hex.Decode(rp[:], []byte("034f355bdcb7cc0af728ef3cceb9615d906"+
+		"84bb5b2ca5f859ab0f0b704075871aa"))
+	selfpub, _ := btcec.ParsePubKey(sp[:])
+	remotepub, _ := btcec.ParsePubKey(rp[:])
+	localMultisigPrivKey, _ := btcec.NewPrivateKey()
+	localMultisigKey := localMultisigPrivKey.PubKey()
+	remoteMultisigPrivKey, _ := btcec.NewPrivateKey()
+	remoteMultisigKey := remoteMultisigPrivKey.PubKey()
 	newPolicy := routing.ChannelPolicy{
 		FeeSchema: routing.FeeSchema{
 			BaseFee: 100,
@@ -131,17 +148,42 @@ func TestManager(t *testing.T) {
 		}
 
 		return &channeldb.OpenChannel{
+			FundingOutpoint: chanPointValid,
+			IdentityPub:     remotepub,
 			LocalChanCfg: channeldb.ChannelConfig{
 				ChannelStateBounds: bounds,
+				MultiSigKey: keychain.KeyDescriptor{
+					PubKey: localMultisigKey,
+				},
+			},
+			RemoteChanCfg: channeldb.ChannelConfig{
+				ChannelStateBounds: bounds,
+				MultiSigKey: keychain.KeyDescriptor{
+					PubKey: remoteMultisigKey,
+				},
 			},
 		}, nil
 	}
 
+	addEdge := func(edge *models.ChannelEdgeInfo) error {
+		return nil
+	}
+
 	manager := Manager{
 		UpdateForwardingPolicies:  updateForwardingPolicies,
 		PropagateChanPolicyUpdate: propagateChanPolicyUpdate,
 		ForAllOutgoingChannels:    forAllOutgoingChannels,
 		FetchChannel:              fetchChannel,
+		SelfPub:                   selfpub,
+		DefaultRoutingPolicy: models.ForwardingPolicy{
+			MinHTLCOut:    minHTLC,
+			MaxHTLC:       maxPendingAmount,
+			BaseFee:       lnwire.MilliSatoshi(1000),
+			FeeRate:       lnwire.MilliSatoshi(0),
+			InboundFee:    models.InboundFee{},
+			TimeLockDelta: 80,
+		},
+		AddEdge: addEdge,
 	}
 
 	// Policy with no max htlc value.
@@ -156,6 +198,7 @@ func TestManager(t *testing.T) {
 		newPolicy              routing.ChannelPolicy
 		channelSet             []channel
 		specifiedChanPoints    []wire.OutPoint
+		createMissingEdge      bool
 		expectedNumUpdates     int
 		expectedUpdateFailures []lnrpc.UpdateFailure
 		expectErr              error
@@ -173,6 +216,7 @@ func TestManager(t *testing.T) {
 				},
 			},
 			specifiedChanPoints:    []wire.OutPoint{chanPointValid},
+			createMissingEdge:      false,
 			expectedNumUpdates:     1,
 			expectedUpdateFailures: []lnrpc.UpdateFailure{},
 			expectErr:              nil,
@@ -190,6 +234,7 @@ func TestManager(t *testing.T) {
 				},
 			},
 			specifiedChanPoints:    []wire.OutPoint{},
+			createMissingEdge:      false,
 			expectedNumUpdates:     1,
 			expectedUpdateFailures: []lnrpc.UpdateFailure{},
 			expectErr:              nil,
@@ -207,6 +252,7 @@ func TestManager(t *testing.T) {
 				},
 			},
 			specifiedChanPoints: []wire.OutPoint{chanPointMissing},
+			createMissingEdge:   false,
 			expectedNumUpdates:  0,
 			expectedUpdateFailures: []lnrpc.UpdateFailure{
 				lnrpc.UpdateFailure_UPDATE_FAILURE_NOT_FOUND,
@@ -228,10 +274,39 @@ func TestManager(t *testing.T) {
 				},
 			},
 			specifiedChanPoints:    []wire.OutPoint{chanPointValid},
+			createMissingEdge:      false,
+			expectedNumUpdates:     1,
+			expectedUpdateFailures: []lnrpc.UpdateFailure{},
+			expectErr:              nil,
+		},
+		{
+			// Here, the edge is missing, causing the edge to be
+			// recreated.
+			name:                   "missing edge recreated",
+			currentPolicy:          models.ChannelEdgePolicy{},
+			newPolicy:              newPolicy,
+			channelSet:             []channel{},
+			specifiedChanPoints:    []wire.OutPoint{chanPointValid},
+			createMissingEdge:      true,
 			expectedNumUpdates:     1,
 			expectedUpdateFailures: []lnrpc.UpdateFailure{},
 			expectErr:              nil,
 		},
+		{
+			// Here, the edge is missing, but the edge will not be
+			// recreated, because createMissingEdge is false.
+			name:                "missing edge ignored",
+			currentPolicy:       models.ChannelEdgePolicy{},
+			newPolicy:           newPolicy,
+			channelSet:          []channel{},
+			specifiedChanPoints: []wire.OutPoint{chanPointValid},
+			createMissingEdge:   false,
+			expectedNumUpdates:  0,
+			expectedUpdateFailures: []lnrpc.UpdateFailure{
+				lnrpc.UpdateFailure_UPDATE_FAILURE_UNKNOWN,
+			},
+			expectErr: nil,
+		},
 	}
 
 	for _, test := range tests {
@@ -242,6 +317,7 @@ func TestManager(t *testing.T) {
 			expectedNumUpdates = test.expectedNumUpdates
 
 			failedUpdates, err := manager.UpdatePolicy(test.newPolicy,
+				test.createMissingEdge,
 				test.specifiedChanPoints...)
 
 			if len(failedUpdates) != len(test.expectedUpdateFailures) {
@@ -258,3 +334,179 @@ func TestManager(t *testing.T) {
 		})
 	}
 }
+
+// Tests creating a new edge in the manager where the local pubkey is the
+// lexicographically lesser or the two.
+func TestCreateEdgeLower(t *testing.T) {
+	sp := [33]byte{}
+	_, _ = hex.Decode(sp[:], []byte("028d7500dd4c12685d1f568b4c2b5048e85"+
+		"34b873319f3a8daa612b469132ec7f7"))
+	rp := [33]byte{}
+	_, _ = hex.Decode(rp[:], []byte("034f355bdcb7cc0af728ef3cceb9615d906"+
+		"84bb5b2ca5f859ab0f0b704075871aa"))
+	selfpub, _ := btcec.ParsePubKey(sp[:])
+	remotepub, _ := btcec.ParsePubKey(rp[:])
+	localMultisigPrivKey, _ := btcec.NewPrivateKey()
+	localMultisigKey := localMultisigPrivKey.PubKey()
+	remoteMultisigPrivKey, _ := btcec.NewPrivateKey()
+	remoteMultisigKey := remoteMultisigPrivKey.PubKey()
+	timestamp := time.Now()
+	defaultPolicy := models.ForwardingPolicy{
+		MinHTLCOut: 1,
+		MaxHTLC:    2,
+		BaseFee:    3,
+		FeeRate:    4,
+		InboundFee: models.InboundFee{
+			Base: 5,
+			Rate: 6,
+		},
+		TimeLockDelta: 7,
+	}
+
+	channel := &channeldb.OpenChannel{
+		IdentityPub: remotepub,
+		LocalChanCfg: channeldb.ChannelConfig{
+			MultiSigKey: keychain.KeyDescriptor{
+				PubKey: localMultisigKey,
+			},
+		},
+		RemoteChanCfg: channeldb.ChannelConfig{
+			MultiSigKey: keychain.KeyDescriptor{
+				PubKey: remoteMultisigKey,
+			},
+		},
+		ShortChannelID: lnwire.NewShortChanIDFromInt(8),
+		ChainHash:      *chaincfg.RegressionNetParams.GenesisHash,
+		Capacity:       9,
+		FundingOutpoint: wire.OutPoint{
+			Hash:  chainhash.Hash([32]byte{}),
+			Index: 0,
+		},
+	}
+	expectedInfo := &models.ChannelEdgeInfo{
+		ChannelID:     8,
+		ChainHash:     channel.ChainHash,
+		Features:      []byte{0, 0},
+		Capacity:      9,
+		ChannelPoint:  channel.FundingOutpoint,
+		NodeKey1Bytes: sp,
+		NodeKey2Bytes: rp,
+		BitcoinKey1Bytes: [33]byte(
+			localMultisigKey.SerializeCompressed()),
+		BitcoinKey2Bytes: [33]byte(
+			remoteMultisigKey.SerializeCompressed()),
+		AuthProof:       nil,
+		ExtraOpaqueData: nil,
+	}
+	expectedEdge := &models.ChannelEdgePolicy{
+		ChannelID:                 8,
+		LastUpdate:                timestamp,
+		TimeLockDelta:             7,
+		ChannelFlags:              0,
+		MessageFlags:              lnwire.ChanUpdateRequiredMaxHtlc,
+		FeeBaseMSat:               3,
+		FeeProportionalMillionths: 4,
+		MinHTLC:                   1,
+		MaxHTLC:                   2,
+		SigBytes:                  nil,
+		ToNode:                    rp,
+		ExtraOpaqueData:           nil,
+	}
+	manager := Manager{
+		SelfPub:              selfpub,
+		DefaultRoutingPolicy: defaultPolicy,
+	}
+
+	info, edge, err := manager.createEdge(channel, timestamp)
+	require.NoError(t, err)
+	require.Equal(t, expectedInfo, info)
+	require.Equal(t, expectedEdge, edge)
+}
+
+// Tests creating a new edge in the manager where the local pubkey is the
+// lexicographically higher or the two.
+func TestCreateEdgeHigher(t *testing.T) {
+	sp := [33]byte{}
+	_, _ = hex.Decode(sp[:], []byte("034f355bdcb7cc0af728ef3cceb9615d906"+
+		"84bb5b2ca5f859ab0f0b704075871aa"))
+	rp := [33]byte{}
+	_, _ = hex.Decode(rp[:], []byte("028d7500dd4c12685d1f568b4c2b5048e85"+
+		"34b873319f3a8daa612b469132ec7f7"))
+	selfpub, _ := btcec.ParsePubKey(sp[:])
+	remotepub, _ := btcec.ParsePubKey(rp[:])
+	localMultisigPrivKey, _ := btcec.NewPrivateKey()
+	localMultisigKey := localMultisigPrivKey.PubKey()
+	remoteMultisigPrivKey, _ := btcec.NewPrivateKey()
+	remoteMultisigKey := remoteMultisigPrivKey.PubKey()
+	timestamp := time.Now()
+	defaultPolicy := models.ForwardingPolicy{
+		MinHTLCOut: 1,
+		MaxHTLC:    2,
+		BaseFee:    3,
+		FeeRate:    4,
+		InboundFee: models.InboundFee{
+			Base: 5,
+			Rate: 6,
+		},
+		TimeLockDelta: 7,
+	}
+
+	channel := &channeldb.OpenChannel{
+		IdentityPub: remotepub,
+		LocalChanCfg: channeldb.ChannelConfig{
+			MultiSigKey: keychain.KeyDescriptor{
+				PubKey: localMultisigKey,
+			},
+		},
+		RemoteChanCfg: channeldb.ChannelConfig{
+			MultiSigKey: keychain.KeyDescriptor{
+				PubKey: remoteMultisigKey,
+			},
+		},
+		ShortChannelID: lnwire.NewShortChanIDFromInt(8),
+		ChainHash:      *chaincfg.RegressionNetParams.GenesisHash,
+		Capacity:       9,
+		FundingOutpoint: wire.OutPoint{
+			Hash:  chainhash.Hash([32]byte{}),
+			Index: 0,
+		},
+	}
+	expectedInfo := &models.ChannelEdgeInfo{
+		ChannelID:     8,
+		ChainHash:     channel.ChainHash,
+		Features:      []byte{0, 0},
+		Capacity:      9,
+		ChannelPoint:  channel.FundingOutpoint,
+		NodeKey1Bytes: rp,
+		NodeKey2Bytes: sp,
+		BitcoinKey1Bytes: [33]byte(
+			remoteMultisigKey.SerializeCompressed()),
+		BitcoinKey2Bytes: [33]byte(
+			localMultisigKey.SerializeCompressed()),
+		AuthProof:       nil,
+		ExtraOpaqueData: nil,
+	}
+	expectedEdge := &models.ChannelEdgePolicy{
+		ChannelID:                 8,
+		LastUpdate:                timestamp,
+		TimeLockDelta:             7,
+		ChannelFlags:              1,
+		MessageFlags:              lnwire.ChanUpdateRequiredMaxHtlc,
+		FeeBaseMSat:               3,
+		FeeProportionalMillionths: 4,
+		MinHTLC:                   1,
+		MaxHTLC:                   2,
+		SigBytes:                  nil,
+		ToNode:                    rp,
+		ExtraOpaqueData:           nil,
+	}
+	manager := Manager{
+		SelfPub:              selfpub,
+		DefaultRoutingPolicy: defaultPolicy,
+	}
+
+	info, edge, err := manager.createEdge(channel, timestamp)
+	require.NoError(t, err)
+	require.Equal(t, expectedInfo, info)
+	require.Equal(t, expectedEdge, edge)
+}
diff --git a/sweep/fee_bumper_test.go b/sweep/fee_bumper_test.go
index 5030dee22..53b38607f 100644
--- a/sweep/fee_bumper_test.go
+++ b/sweep/fee_bumper_test.go
@@ -323,16 +323,8 @@ func TestStoreRecord(t *testing.T) {
 	// Get the current counter and check it's increased later.
 	initialCounter := tp.requestCounter.Load()
 
-	op := wire.OutPoint{
-		Hash:  chainhash.Hash{1},
-		Index: 0,
-	}
-	utxoIndex := map[wire.OutPoint]int{
-		op: 0,
-	}
-
 	// Call the method under test.
-	requestID := tp.storeRecord(tx, req, feeFunc, fee, utxoIndex)
+	requestID := tp.storeRecord(tx, req, feeFunc, fee)
 
 	// Check the request ID is as expected.
 	require.Equal(t, initialCounter+1, requestID)
@@ -344,7 +336,6 @@ func TestStoreRecord(t *testing.T) {
 	require.Equal(t, feeFunc, record.feeFunction)
 	require.Equal(t, fee, record.fee)
 	require.Equal(t, req, record.req)
-	require.Equal(t, utxoIndex, record.outpointToTxIndex)
 }
 
 // mockers wraps a list of mocked interfaces used inside tx publisher.
@@ -674,17 +665,9 @@ func TestTxPublisherBroadcast(t *testing.T) {
 	feerate := chainfee.SatPerKWeight(1000)
 	m.feeFunc.On("FeeRate").Return(feerate)
 
-	op := wire.OutPoint{
-		Hash:  chainhash.Hash{1},
-		Index: 0,
-	}
-	utxoIndex := map[wire.OutPoint]int{
-		op: 0,
-	}
-
 	// Create a testing record and put it in the map.
 	fee := btcutil.Amount(1000)
-	requestID := tp.storeRecord(tx, req, m.feeFunc, fee, utxoIndex)
+	requestID := tp.storeRecord(tx, req, m.feeFunc, fee)
 
 	// Quickly check when the requestID cannot be found, an error is
 	// returned.
@@ -771,14 +754,6 @@ func TestRemoveResult(t *testing.T) {
 	// Create a testing record and put it in the map.
 	fee := btcutil.Amount(1000)
 
-	op := wire.OutPoint{
-		Hash:  chainhash.Hash{1},
-		Index: 0,
-	}
-	utxoIndex := map[wire.OutPoint]int{
-		op: 0,
-	}
-
 	testCases := []struct {
 		name        string
 		setupRecord func() uint64
@@ -790,9 +765,7 @@ func TestRemoveResult(t *testing.T) {
 			// removed.
 			name: "remove on TxConfirmed",
 			setupRecord: func() uint64 {
-				id := tp.storeRecord(
-					tx, req, m.feeFunc, fee, utxoIndex,
-				)
+				id := tp.storeRecord(tx, req, m.feeFunc, fee)
 				tp.subscriberChans.Store(id, nil)
 
 				return id
@@ -807,9 +780,7 @@ func TestRemoveResult(t *testing.T) {
 			// When the tx is failed, the records will be removed.
 			name: "remove on TxFailed",
 			setupRecord: func() uint64 {
-				id := tp.storeRecord(
-					tx, req, m.feeFunc, fee, utxoIndex,
-				)
+				id := tp.storeRecord(tx, req, m.feeFunc, fee)
 				tp.subscriberChans.Store(id, nil)
 
 				return id
@@ -825,9 +796,7 @@ func TestRemoveResult(t *testing.T) {
 			// Noop when the tx is neither confirmed or failed.
 			name: "noop when tx is not confirmed or failed",
 			setupRecord: func() uint64 {
-				id := tp.storeRecord(
-					tx, req, m.feeFunc, fee, utxoIndex,
-				)
+				id := tp.storeRecord(tx, req, m.feeFunc, fee)
 				tp.subscriberChans.Store(id, nil)
 
 				return id
@@ -875,17 +844,9 @@ func TestNotifyResult(t *testing.T) {
 	// Create a test tx.
 	tx := &wire.MsgTx{LockTime: 1}
 
-	op := wire.OutPoint{
-		Hash:  chainhash.Hash{1},
-		Index: 0,
-	}
-	utxoIndex := map[wire.OutPoint]int{
-		op: 0,
-	}
-
 	// Create a testing record and put it in the map.
 	fee := btcutil.Amount(1000)
-	requestID := tp.storeRecord(tx, req, m.feeFunc, fee, utxoIndex)
+	requestID := tp.storeRecord(tx, req, m.feeFunc, fee)
 
 	// Create a subscription to the event.
 	subscriber := make(chan *BumpResult, 1)
@@ -1240,17 +1201,9 @@ func TestHandleTxConfirmed(t *testing.T) {
 	// Create a test tx.
 	tx := &wire.MsgTx{LockTime: 1}
 
-	op := wire.OutPoint{
-		Hash:  chainhash.Hash{1},
-		Index: 0,
-	}
-	utxoIndex := map[wire.OutPoint]int{
-		op: 0,
-	}
-
 	// Create a testing record and put it in the map.
 	fee := btcutil.Amount(1000)
-	requestID := tp.storeRecord(tx, req, m.feeFunc, fee, utxoIndex)
+	requestID := tp.storeRecord(tx, req, m.feeFunc, fee)
 	record, ok := tp.records.Load(requestID)
 	require.True(t, ok)
 
@@ -1320,17 +1273,9 @@ func TestHandleFeeBumpTx(t *testing.T) {
 		tx:          tx,
 	}
 
-	op := wire.OutPoint{
-		Hash:  chainhash.Hash{1},
-		Index: 0,
-	}
-	utxoIndex := map[wire.OutPoint]int{
-		op: 0,
-	}
-
 	// Create a testing record and put it in the map.
 	fee := btcutil.Amount(1000)
-	requestID := tp.storeRecord(tx, req, m.feeFunc, fee, utxoIndex)
+	requestID := tp.storeRecord(tx, req, m.feeFunc, fee)
 
 	// Create a subscription to the event.
 	subscriber := make(chan *BumpResult, 1)
diff --git a/sweep/mock_test.go b/sweep/mock_test.go
index 34202b145..c623ca3c0 100644
--- a/sweep/mock_test.go
+++ b/sweep/mock_test.go
@@ -352,7 +352,7 @@ func (m *MockAuxSweeper) ExtraBudgetForInputs(
 // NotifyBroadcast is used to notify external callers of the broadcast
 // of a sweep transaction, generated by the passed BumpRequest.
 func (*MockAuxSweeper) NotifyBroadcast(_ *BumpRequest, _ *wire.MsgTx,
-	_ btcutil.Amount, _ map[wire.OutPoint]int) error {
+	_ btcutil.Amount) error {
 
 	return nil
 }
diff --git a/watchtower/wtwire/fuzz_test.go b/watchtower/wtwire/fuzz_test.go
index 385e5a58b..5a03b84e3 100644
--- a/watchtower/wtwire/fuzz_test.go
+++ b/watchtower/wtwire/fuzz_test.go
@@ -18,28 +18,25 @@ func prefixWithMsgType(data []byte, prefix MessageType) []byte {
 	return data
 }
 
-// wireMsgHarness performs the actual fuzz testing of the appropriate wire
-// message. This function will check that the passed-in message passes wire
-// length checks, is a valid message once deserialized, and passes a sequence of
-// serialization and deserialization checks. emptyMsg must be an empty Message
-// of the type to be fuzzed, as it is used to determine the appropriate prefix
-// bytes and max payload length for decoding.
-func wireMsgHarness(t *testing.T, data []byte, emptyMsg Message) {
+// harness performs the actual fuzz testing of the appropriate wire message.
+// This function will check that the passed-in message passes wire length
+// checks, is a valid message once deserialized, and passes a sequence of
+// serialization and deserialization checks. Returns an int that determines
+// whether the input is unique or not.
+func harness(t *testing.T, data []byte, emptyMsg Message) {
 	t.Helper()
 
-	// Make sure byte array length is less than max payload size for the
-	// wire message.
-	payloadLen := uint32(len(data))
+	// Create a reader with the byte array.
+	r := bytes.NewReader(data)
+
+	// Make sure byte array length (excluding 2 bytes for message type) is
+	// less than max payload size for the wire message.
+	payloadLen := uint32(len(data)) - 2
 	if payloadLen > emptyMsg.MaxPayloadLength(0) {
 		// Ignore this input - max payload constraint violated.
 		return
 	}
 
-	data = prefixWithMsgType(data, emptyMsg.MsgType())
-
-	// Create a reader with the byte array.
-	r := bytes.NewReader(data)
-
 	msg, err := ReadMessage(r, 0)
 	if err != nil {
 		return
@@ -60,48 +57,120 @@ func wireMsgHarness(t *testing.T, data []byte, emptyMsg Message) {
 
 func FuzzCreateSessionReply(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, &CreateSessionReply{})
+		// Prefix with MsgCreateSessionReply.
+		data = prefixWithMsgType(data, MsgCreateSessionReply)
+
+		// Create an empty message so that the FuzzHarness func can
+		// check if the max payload constraint is violated.
+		emptyMsg := CreateSessionReply{}
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data, &emptyMsg)
 	})
 }
 
 func FuzzCreateSession(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, &CreateSession{})
+		// Prefix with MsgCreateSession.
+		data = prefixWithMsgType(data, MsgCreateSession)
+
+		// Create an empty message so that the FuzzHarness func can
+		// check if the max payload constraint is violated.
+		emptyMsg := CreateSession{}
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data, &emptyMsg)
 	})
 }
 
 func FuzzDeleteSessionReply(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, &DeleteSessionReply{})
+		// Prefix with MsgDeleteSessionReply.
+		data = prefixWithMsgType(data, MsgDeleteSessionReply)
+
+		// Create an empty message so that the FuzzHarness func can
+		// check if the max payload constraint is violated.
+		emptyMsg := DeleteSessionReply{}
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data, &emptyMsg)
 	})
 }
 
 func FuzzDeleteSession(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, &DeleteSession{})
+		// Prefix with MsgDeleteSession.
+		data = prefixWithMsgType(data, MsgDeleteSession)
+
+		// Create an empty message so that the FuzzHarness func can
+		// check if the max payload constraint is violated.
+		emptyMsg := DeleteSession{}
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data, &emptyMsg)
 	})
 }
 
 func FuzzError(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, &Error{})
+		// Prefix with MsgError.
+		data = prefixWithMsgType(data, MsgError)
+
+		// Create an empty message so that the FuzzHarness func can
+		// check if the max payload constraint is violated.
+		emptyMsg := Error{}
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data, &emptyMsg)
 	})
 }
 
 func FuzzInit(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, &Init{})
+		// Prefix with MsgInit.
+		data = prefixWithMsgType(data, MsgInit)
+
+		// Create an empty message so that the FuzzHarness func can
+		// check if the max payload constraint is violated.
+		emptyMsg := Init{}
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data, &emptyMsg)
 	})
 }
 
 func FuzzStateUpdateReply(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, &StateUpdateReply{})
+		// Prefix with MsgStateUpdateReply.
+		data = prefixWithMsgType(data, MsgStateUpdateReply)
+
+		// Create an empty message so that the FuzzHarness func can
+		// check if the max payload constraint is violated.
+		emptyMsg := StateUpdateReply{}
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data, &emptyMsg)
 	})
 }
 
 func FuzzStateUpdate(f *testing.F) {
 	f.Fuzz(func(t *testing.T, data []byte) {
-		wireMsgHarness(t, data, &StateUpdate{})
+		// Prefix with MsgStateUpdate.
+		data = prefixWithMsgType(data, MsgStateUpdate)
+
+		// Create an empty message so that the FuzzHarness func can
+		// check if the max payload constraint is violated.
+		emptyMsg := StateUpdate{}
+
+		// Pass the message into our general fuzz harness for wire
+		// messages!
+		harness(t, data, &emptyMsg)
 	})
 }