Repository: lightningnetwork/lnd
Pull request #8914 (2024-07-16 06:57:32)

fn: add synchronous write file
Continue work of https://github.com/lightningnetwork/lnd/pull/8305.

A WriteFile function with sync and optional removal on error is added to fn module.

As described in [Use of Golang submodules](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md#use-of-golang-submodules), in a second PR the usage of ioutil.WriteFile and os.WriteFile will be replaced with this new function to fix issue https://github.com/lightningnetwork/lnd/issues/6720.


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 -->
z017: > Looks good, thanks a lot for picking this up. Left a few comments, nothing major.

Thanks for the review! I uploaded the changes with an ammend to the previous commit.
lightninglabs-deploy: @z017, remember to re-request review from reviewers when ready
lightninglabs-deploy: @crypt-iq: review reminder
@z017, remember to re-request review from reviewers when ready
z017: > @Crypt-iQ: review reminder @z017, remember to re-request review from reviewers when ready

Hi @guggero. Do I have to re-request the review or is it not required?
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder
lightninglabs-deploy: @crypt-iq: review reminder

Patch:
diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml
index e3e91f041..522b94dca 100644
--- a/.github/actions/setup-go/action.yml
+++ b/.github/actions/setup-go/action.yml
@@ -7,12 +7,6 @@ inputs:
   key-prefix:
     description: "A prefix to use for the cache key, to separate cache entries from other workflows"
     required: false
-  use-build-cache: 
-    description: "Whether to use the build cache"
-    required: false
-    # Boolean values aren't supported in the workflow syntax, so we use a
-    # string. To not confuse the value with true/false, we use 'yes' and 'no'.
-    default: 'yes'
 
 runs:
   using: "composite"
@@ -23,8 +17,7 @@ runs:
       with:
         go-version: '${{ inputs.go-version }}'
 
-    - name: go module and build cache
-      if: ${{ inputs.use-build-cache == 'yes' }}
+    - name: go cache
       uses: actions/cache@v3
       with:
         # In order:
@@ -42,18 +35,6 @@ runs:
           ${{ runner.os }}-go-${{ inputs.go-version }}-${{ inputs.key-prefix }}-${{ github.job }}-
           ${{ runner.os }}-go-${{ inputs.go-version }}-${{ inputs.key-prefix }}-
 
-    - name: go module cache
-      if: ${{ inputs.use-build-cache == 'no' }}
-      uses: actions/cache@v3
-      with:
-        # Just the module download cache.
-        path: |
-          ~/go/pkg/mod
-        key: ${{ runner.os }}-go-${{ inputs.go-version }}-${{ inputs.key-prefix }}-no-build-cache-${{ github.job }}-${{ hashFiles('**/go.sum') }}
-        restore-keys: |
-          ${{ runner.os }}-go-${{ inputs.go-version }}-${{ inputs.key-prefix }}-no-build-cache-${{ github.job }}-
-          ${{ runner.os }}-go-${{ inputs.go-version }}-${{ inputs.key-prefix }}-no-build-cache-
-
     - name: set GOPATH
       shell: bash
       run: |
diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml
index b12c5f962..dd8b5dbfe 100644
--- a/.github/workflows/main.yml
+++ b/.github/workflows/main.yml
@@ -139,17 +139,6 @@ jobs:
   cross-compile:
     name: cross compilation
     runs-on: ubuntu-latest
-    strategy:
-      fail-fast: true
-      matrix:
-        # Please keep this list in sync with make/release_flags.mk!
-        include:
-          - name: i386
-            sys: freebsd-386 linux-386 windows-386
-          - name: amd64
-            sys: darwin-amd64 freebsd-amd64 linux-amd64 netbsd-amd64 openbsd-amd64 windows-amd64
-          - name: arm
-            sys: darwin-arm64 freebsd-arm linux-armv6 linux-armv7 linux-arm64 windows-arm
     steps:
       - name: git checkout
         uses: actions/checkout@v3
@@ -159,10 +148,9 @@ jobs:
         with:
           go-version: '${{ env.GO_VERSION }}'
           key-prefix: cross-compile
-          use-build-cache: 'no'
 
       - name: build release for all architectures
-        run: make release sys="${{ matrix.sys }}"
+        run: make release
 
   ########################
   # sample configuration check
@@ -232,7 +220,7 @@ jobs:
         run: make ${{ matrix.unit_type }}
 
       - name: Send coverage
-        uses: ziggie1984/actions-goveralls@c440f43938a4032b627d2b03d61d4ae1a2ba2b5c
+        uses: shogo82148/actions-goveralls@v1
         if: matrix.unit_type == 'btcd unit-cover'
         with:
           path-to-profile: coverage.txt
@@ -296,7 +284,7 @@ jobs:
 
       - name: Send coverage
         if: ${{ contains(matrix.args, 'cover=1') }}
-        uses: ziggie1984/actions-goveralls@c440f43938a4032b627d2b03d61d4ae1a2ba2b5c
+        uses: shogo82148/actions-goveralls@v1
         with:
           path-to-profile: coverage.txt
           flag-name: 'itest-${{ matrix.name }}'
@@ -446,6 +434,6 @@ jobs:
     needs: [unit-test, ubuntu-integration-test]
     runs-on: ubuntu-latest
     steps:
-      - uses: ziggie1984/actions-goveralls@c440f43938a4032b627d2b03d61d4ae1a2ba2b5c
+      - uses: shogo82148/actions-goveralls@v1
         with:
           parallel-finished: true
diff --git a/docs/release-notes/release-notes-0.18.4.md b/docs/release-notes/release-notes-0.18.4.md
index e52dbc3d1..ab72c01c3 100644
--- a/docs/release-notes/release-notes-0.18.4.md
+++ b/docs/release-notes/release-notes-0.18.4.md
@@ -26,9 +26,6 @@
 * [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9324) to prevent
   potential deadlocks when LND depends on external components (e.g. aux
   components, hooks).
-  
-* [Make sure blinded payment failures are handled correctly in the mission
-controller](https://github.com/lightningnetwork/lnd/pull/9316).
 
 # New Features
 
@@ -124,5 +121,4 @@ types in a series of changes:
 * George Tsagkarelis
 * Olaoluwa Osuntokun
 * Oliver Gugger
-* Ziggie
 
diff --git a/docs/release-notes/release-notes-0.19.0.md b/docs/release-notes/release-notes-0.19.0.md
index 0b576fc7d..ee0b0c136 100644
--- a/docs/release-notes/release-notes-0.19.0.md
+++ b/docs/release-notes/release-notes-0.19.0.md
@@ -60,7 +60,7 @@
 * [Make the contract resolutions for the channel arbitrator optional](
   https://github.com/lightningnetwork/lnd/pull/9253)
 
-* [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9322) that caused
+  * [Fixed a bug](https://github.com/lightningnetwork/lnd/pull/9322) that caused
     estimateroutefee to ignore the default payment timeout.
 
 # New Features
@@ -80,7 +80,7 @@
 ## RPC Additions
 
 * [Add a new rpc endpoint](https://github.com/lightningnetwork/lnd/pull/8843)
-  `BumpForceCloseFee` which moves the functionality solely available in the
+  `BumpForceCloseFee` which moves the functionality soley available in the
   `lncli` to LND hence making it more universal.
 
 * [The `walletrpc.FundPsbt` RPC method now has an option to specify the fee as
@@ -221,10 +221,6 @@ The underlying functionality between those two options remain the same.
 * A code refactor that [moves all the graph related DB code out of the 
   `channeldb` package](https://github.com/lightningnetwork/lnd/pull/9236) and 
   into the `graph/db` package.
- 
-* [Improve the API](https://github.com/lightningnetwork/lnd/pull/9341) of the 
-  [GoroutineManager](https://github.com/lightningnetwork/lnd/pull/9141) so that 
-  its constructor does not take a context.
 
 ## Tooling and Documentation
 
diff --git a/fn/context_guard.go b/fn/context_guard.go
index 3868a3ff0..23914702e 100644
--- a/fn/context_guard.go
+++ b/fn/context_guard.go
@@ -3,7 +3,6 @@ package fn
 import (
 	"context"
 	"sync"
-	"sync/atomic"
 	"time"
 )
 
@@ -12,234 +11,103 @@ var (
 	DefaultTimeout = 30 * time.Second
 )
 
-// ContextGuard is a struct that provides a wait group and main quit channel
-// that can be used to create guarded contexts.
+// ContextGuard is an embeddable struct that provides a wait group and main quit
+// channel that can be used to create guarded contexts.
 type ContextGuard struct {
-	mu sync.Mutex
-	wg sync.WaitGroup
-
-	quit    chan struct{}
-	stopped sync.Once
-
-	// id is used to generate unique ids for each context that should be
-	// cancelled when the main quit signal is triggered.
-	id atomic.Uint32
-
-	// cancelFns is a map of cancel functions that can be used to cancel
-	// any context that should be cancelled when the main quit signal is
-	// triggered. The key is the id of the context. The mutex must be held
-	// when accessing this map.
-	cancelFns map[uint32]context.CancelFunc
+	DefaultTimeout time.Duration
+	Wg             sync.WaitGroup
+	Quit           chan struct{}
 }
 
-// NewContextGuard constructs and returns a new instance of ContextGuard.
 func NewContextGuard() *ContextGuard {
 	return &ContextGuard{
-		quit:      make(chan struct{}),
-		cancelFns: make(map[uint32]context.CancelFunc),
+		DefaultTimeout: DefaultTimeout,
+		Quit:           make(chan struct{}),
 	}
 }
 
-// Quit is used to signal the main quit channel, which will cancel all
-// non-blocking contexts derived from the ContextGuard.
-func (g *ContextGuard) Quit() {
-	g.stopped.Do(func() {
-		g.mu.Lock()
-		defer g.mu.Unlock()
-
-		for _, cancel := range g.cancelFns {
-			cancel()
-		}
-
-		close(g.quit)
-	})
-}
-
-// Done returns a channel that will be closed when the main quit signal is
-// triggered.
-func (g *ContextGuard) Done() <-chan struct{} {
-	return g.quit
+// WithCtxQuit is used to create a cancellable context that will be cancelled
+// if the main quit signal is triggered or after the default timeout occurred.
+func (g *ContextGuard) WithCtxQuit() (context.Context, func()) {
+	return g.WithCtxQuitCustomTimeout(g.DefaultTimeout)
 }
 
-// WgAdd is used to add delta to the internal wait group of the ContextGuard.
-func (g *ContextGuard) WgAdd(delta int) {
-	g.wg.Add(delta)
-}
+// WithCtxQuitCustomTimeout is used to create a cancellable context that will be
+// cancelled if the main quit signal is triggered or after the given timeout
+// occurred.
+func (g *ContextGuard) WithCtxQuitCustomTimeout(
+	timeout time.Duration) (context.Context, func()) {
 
-// WgDone is used to decrement the internal wait group of the ContextGuard.
-func (g *ContextGuard) WgDone() {
-	g.wg.Done()
-}
+	timeoutTimer := time.NewTimer(timeout)
+	ctx, cancel := context.WithCancel(context.Background())
 
-// WgWait is used to block until the internal wait group of the ContextGuard is
-// empty.
-func (g *ContextGuard) WgWait() {
-	g.wg.Wait()
-}
-
-// ctxGuardOptions is used to configure the behaviour of the context derived
-// via the WithCtx method of the ContextGuard.
-type ctxGuardOptions struct {
-	blocking    bool
-	withTimeout bool
-	timeout     time.Duration
-}
+	g.Wg.Add(1)
+	go func() {
+		defer timeoutTimer.Stop()
+		defer cancel()
+		defer g.Wg.Done()
 
-// ContextGuardOption defines the signature of a functional option that can be
-// used to configure the behaviour of the context derived via the WithCtx method
-// of the ContextGuard.
-type ContextGuardOption func(*ctxGuardOptions)
+		select {
+		case <-g.Quit:
 
-// WithBlockingCG is used to create a cancellable context that will NOT be
-// cancelled if the main quit signal is triggered, to block shutdown of
-// important tasks.
-func WithBlockingCG() ContextGuardOption {
-	return func(o *ctxGuardOptions) {
-		o.blocking = true
-	}
-}
+		case <-timeoutTimer.C:
 
-// WithCustomTimeoutCG is used to create a cancellable context with a custom
-// timeout. Such a context will be cancelled if either the parent context is
-// cancelled, the timeout is reached or, if the Blocking option is not provided,
-// the main quit signal is triggered.
-func WithCustomTimeoutCG(timeout time.Duration) ContextGuardOption {
-	return func(o *ctxGuardOptions) {
-		o.withTimeout = true
-		o.timeout = timeout
-	}
-}
+		case <-ctx.Done():
+		}
+	}()
 
-// WithTimeoutCG is used to create a cancellable context with a default timeout.
-// Such a context will be cancelled if either the parent context is cancelled,
-// the timeout is reached or, if the Blocking option is not provided, the main
-// quit signal is triggered.
-func WithTimeoutCG() ContextGuardOption {
-	return func(o *ctxGuardOptions) {
-		o.withTimeout = true
-		o.timeout = DefaultTimeout
-	}
+	return ctx, cancel
 }
 
-// Create is used to derive a cancellable context from the parent. Various
-// options can be provided to configure the behaviour of the derived context.
-func (g *ContextGuard) Create(ctx context.Context,
-	options ...ContextGuardOption) (context.Context, context.CancelFunc) {
-
-	// Exit early if the parent context has already been cancelled.
-	select {
-	case <-ctx.Done():
-		return ctx, func() {}
-	default:
-	}
-
-	var opts ctxGuardOptions
-	for _, o := range options {
-		o(&opts)
-	}
-
-	g.mu.Lock()
-	defer g.mu.Unlock()
-
-	var cancel context.CancelFunc
-	if opts.withTimeout {
-		ctx, cancel = context.WithTimeout(ctx, opts.timeout)
-	} else {
-		ctx, cancel = context.WithCancel(ctx)
-	}
-
-	if opts.blocking {
-		g.ctxBlocking(ctx, cancel)
-
-		return ctx, cancel
-	}
-
-	// If the call is non-blocking, then we can exit early if the main quit
-	// signal has been triggered.
-	select {
-	case <-g.quit:
-		cancel()
-
-		return ctx, cancel
-	default:
-	}
-
-	cancel = g.ctxQuitUnsafe(ctx, cancel)
-
-	return ctx, cancel
+// CtxBlocking is used to create a cancellable context that will NOT be
+// cancelled if the main quit signal is triggered, to block shutdown of
+// important tasks. The context will be cancelled if the timeout is reached.
+func (g *ContextGuard) CtxBlocking() (context.Context, func()) {
+	return g.CtxBlockingCustomTimeout(g.DefaultTimeout)
 }
 
-// ctxQuitUnsafe spins off a goroutine that will block until the passed context
-// is cancelled or until the quit channel has been signaled after which it will
-// call the passed cancel function and decrement the wait group.
-//
-// NOTE: the caller must hold the ContextGuard's mutex before calling this
-// function.
-func (g *ContextGuard) ctxQuitUnsafe(ctx context.Context,
-	cancel context.CancelFunc) context.CancelFunc {
+// CtxBlockingCustomTimeout is used to create a cancellable context with a
+// custom timeout that will NOT be cancelled if the main quit signal is
+// triggered, to block shutdown of important tasks. The context will be
+// cancelled if the timeout is reached.
+func (g *ContextGuard) CtxBlockingCustomTimeout(
+	timeout time.Duration) (context.Context, func()) {
 
-	cancel = g.addCancelFnUnsafe(cancel)
+	timeoutTimer := time.NewTimer(timeout)
+	ctx, cancel := context.WithCancel(context.Background())
 
-	g.wg.Add(1)
+	g.Wg.Add(1)
 	go func() {
+		defer timeoutTimer.Stop()
 		defer cancel()
-		defer g.wg.Done()
+		defer g.Wg.Done()
 
 		select {
-		case <-g.quit:
+		case <-timeoutTimer.C:
 
 		case <-ctx.Done():
 		}
 	}()
 
-	return cancel
+	return ctx, cancel
 }
 
-// ctxBlocking spins off a goroutine that will block until the passed context
-// is cancelled after which it will call the passed cancel function and
-// decrement the wait group.
-func (g *ContextGuard) ctxBlocking(ctx context.Context,
-	cancel context.CancelFunc) {
+// WithCtxQuitNoTimeout is used to create a cancellable context that will be
+// cancelled if the main quit signal is triggered.
+func (g *ContextGuard) WithCtxQuitNoTimeout() (context.Context, func()) {
+	ctx, cancel := context.WithCancel(context.Background())
 
-	g.wg.Add(1)
+	g.Wg.Add(1)
 	go func() {
 		defer cancel()
-		defer g.wg.Done()
+		defer g.Wg.Done()
 
 		select {
+		case <-g.Quit:
+
 		case <-ctx.Done():
 		}
 	}()
-}
 
-// addCancelFnUnsafe adds a context cancel function to the manager and returns a
-// call-back which can safely be used to cancel the context.
-//
-// NOTE: the caller must hold the ContextGuard's mutex before calling this
-// function.
-func (g *ContextGuard) addCancelFnUnsafe(
-	cancel context.CancelFunc) context.CancelFunc {
-
-	id := g.id.Add(1)
-	g.cancelFns[id] = cancel
-
-	return g.cancelCtxFn(id)
-}
-
-// cancelCtxFn returns a call-back that can be used to cancel the context
-// associated with the passed id.
-func (g *ContextGuard) cancelCtxFn(id uint32) context.CancelFunc {
-	return func() {
-		g.mu.Lock()
-
-		fn, ok := g.cancelFns[id]
-		if !ok {
-			g.mu.Unlock()
-			return
-		}
-		delete(g.cancelFns, id)
-		g.mu.Unlock()
-
-		fn()
-	}
+	return ctx, cancel
 }
diff --git a/fn/func.go b/fn/func.go
deleted file mode 100644
index d4e77aa5d..000000000
--- a/fn/func.go
+++ /dev/null
@@ -1,42 +0,0 @@
-package fn
-
-// Copyable is a generic interface for a type that's able to return a deep copy
-// of itself.
-type Copyable[T any] interface {
-	Copy() T
-}
-
-// CopyAll creates a new slice where each item of the slice is a deep copy of
-// the elements of the input slice.
-func CopyAll[T Copyable[T]](xs []T) []T {
-	newItems := make([]T, len(xs))
-	for i := range xs {
-		newItems[i] = xs[i].Copy()
-	}
-
-	return newItems
-}
-
-// CopyableErr is a generic interface for a type that's able to return a deep copy
-// of itself. This is identical to Copyable, but should be used in cases where
-// the copy method can return an error.
-type CopyableErr[T any] interface {
-	Copy() (T, error)
-}
-
-// CopyAllErr creates a new slice where each item of the slice is a deep copy of
-// the elements of the input slice. This is identical to CopyAll, but should be
-// used in cases where the copy method can return an error.
-func CopyAllErr[T CopyableErr[T]](xs []T) ([]T, error) {
-	var err error
-
-	newItems := make([]T, len(xs))
-	for i := range xs {
-		newItems[i], err = xs[i].Copy()
-		if err != nil {
-			return nil, err
-		}
-	}
-
-	return newItems, nil
-}
diff --git a/fn/goroutine_manager.go b/fn/goroutine_manager.go
index 3b0074f38..81c538ea0 100644
--- a/fn/goroutine_manager.go
+++ b/fn/goroutine_manager.go
@@ -3,123 +3,51 @@ package fn
 import (
 	"context"
 	"sync"
-	"sync/atomic"
 )
 
 // 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 {
-	// id is used to generate unique ids for each goroutine.
-	id atomic.Uint32
-
-	// cancelFns is a map of cancel functions that can be used to cancel the
-	// context of a goroutine. The mutex must be held when accessing this
-	// map. The key is the id of the goroutine.
-	cancelFns map[uint32]context.CancelFunc
-
-	mu sync.Mutex
-
-	stopped sync.Once
-	quit    chan struct{}
-	wg      sync.WaitGroup
+	wg     sync.WaitGroup
+	mu     sync.Mutex
+	ctx    context.Context
+	cancel func()
 }
 
 // NewGoroutineManager constructs and returns a new instance of
 // GoroutineManager.
-func NewGoroutineManager() *GoroutineManager {
-	return &GoroutineManager{
-		cancelFns: make(map[uint32]context.CancelFunc),
-		quit:      make(chan struct{}),
-	}
-}
-
-// addCancelFn adds a context cancel function to the manager and returns an id
-// that can can be used to cancel the context later on when the goroutine is
-// done.
-func (g *GoroutineManager) addCancelFn(cancel context.CancelFunc) uint32 {
-	g.mu.Lock()
-	defer g.mu.Unlock()
-
-	id := g.id.Add(1)
-	g.cancelFns[id] = cancel
-
-	return id
-}
-
-// cancel cancels the context associated with the passed id.
-func (g *GoroutineManager) cancel(id uint32) {
-	g.mu.Lock()
-	defer g.mu.Unlock()
-
-	g.cancelUnsafe(id)
-}
+func NewGoroutineManager(ctx context.Context) *GoroutineManager {
+	ctx, cancel := context.WithCancel(ctx)
 
-// cancelUnsafe cancels the context associated with the passed id without
-// acquiring the mutex.
-func (g *GoroutineManager) cancelUnsafe(id uint32) {
-	fn, ok := g.cancelFns[id]
-	if !ok {
-		return
+	return &GoroutineManager{
+		ctx:    ctx,
+		cancel: cancel,
 	}
-
-	fn()
-
-	delete(g.cancelFns, id)
 }
 
 // Go tries to start a new goroutine and returns a boolean indicating its
-// success. It returns true if the goroutine was successfully created and false
-// otherwise. A goroutine will fail to be created iff the goroutine manager is
-// stopping or the passed context has already expired. The passed call-back
-// function must exit if the passed context expires.
-func (g *GoroutineManager) Go(ctx context.Context,
-	f func(ctx context.Context)) bool {
-
-	// Derive a cancellable context from the passed context and store its
-	// cancel function in the manager. The context will be cancelled when
-	// either the parent context is cancelled or the quit channel is closed
-	// which will call the stored cancel function.
-	ctx, cancel := context.WithCancel(ctx)
-	id := g.addCancelFn(cancel)
-
-	// Calling wg.Add(1) and wg.Wait() when the wg's counter is 0 is a race
-	// condition, since it is not clear if Wait() should block or not. This
+// 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 {
+	// 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
-	// crash if running with `-race`. To prevent this, we protect the calls
-	// to wg.Add(1) and wg.Wait() with a mutex. If we block here because
-	// Stop is running first, then Stop will close the quit channel which
-	// will cause the context to be cancelled, and we will exit before
-	// calling wg.Add(1). If we grab the mutex here before Stop does, then
-	// Stop will block until after we call wg.Add(1).
+	// crash if running with `-race`. To prevent this, whole Go method is
+	// protected with a mutex. The call to wg.Wait() inside Stop() can still
+	// run in parallel with Go, but in that case g.ctx is in expired state,
+	// because cancel() was called in Stop, so Go returns before wg.Add(1)
+	// call.
 	g.mu.Lock()
 	defer g.mu.Unlock()
 
-	// Before continuing to start the goroutine, we need to check if the
-	// context has already expired. This could be the case if the parent
-	// context has already expired or if Stop has been called.
-	if ctx.Err() != nil {
-		g.cancelUnsafe(id)
-
-		return false
-	}
-
-	// Ensure that the goroutine is not started if the manager has stopped.
-	select {
-	case <-g.quit:
-		g.cancelUnsafe(id)
-
+	if g.ctx.Err() != nil {
 		return false
-	default:
 	}
 
 	g.wg.Add(1)
 	go func() {
-		defer func() {
-			g.cancel(id)
-			g.wg.Done()
-		}()
-
-		f(ctx)
+		defer g.wg.Done()
+		f(g.ctx)
 	}()
 
 	return true
@@ -128,30 +56,20 @@ func (g *GoroutineManager) Go(ctx context.Context,
 // Stop prevents new goroutines from being added and waits for all running
 // goroutines to finish.
 func (g *GoroutineManager) Stop() {
-	g.stopped.Do(func() {
-		// Closing the quit channel will prevent any new goroutines from
-		// starting.
-		g.mu.Lock()
-		close(g.quit)
-		for _, cancel := range g.cancelFns {
-			cancel()
-		}
-		g.mu.Unlock()
-
-		// Wait for all goroutines to finish. Note that this wg.Wait()
-		// call is 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).
-		g.wg.Wait()
-	})
+	g.mu.Lock()
+	g.cancel()
+	g.mu.Unlock()
+
+	// Wait for all goroutines to finish. Note that this wg.Wait() call is
+	// 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).
+	g.wg.Wait()
 }
 
-// Done returns a channel which is closed once Stop has been called and the
-// quit channel closed. Note that the channel closing indicates that shutdown
-// of the GoroutineManager has started but not necessarily that the Stop method
-// has finished.
+// Done returns a channel which is closed when either the context passed to
+// NewGoroutineManager expires or when Stop is called.
 func (g *GoroutineManager) Done() <-chan struct{} {
-	return g.quit
+	return g.ctx.Done()
 }
diff --git a/fn/io.go b/fn/io.go
new file mode 100644
index 000000000..b13b4ba75
--- /dev/null
+++ b/fn/io.go
@@ -0,0 +1,45 @@
+package fn
+
+import (
+	"os"
+)
+
+// WriteFile synchronously writes data to the named file.
+// If the file does not exist, WriteFile creates it with permissions perm
+// (before umask); otherwise WriteFile truncates it before writing, without
+// changing permissions.
+// By opening the file with O_SYNC, it ensures the data is written to disk.
+// If an error occurs, it does not remove the file.
+func WriteFile(name string, data []byte, perm os.FileMode) error {
+	f, err := os.OpenFile(
+		name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC|os.O_SYNC, perm,
+	)
+	if err != nil {
+		return err
+	}
+
+	_, err = f.Write(data)
+
+	// Prioritize the error on Write but make sure to call Close regardless
+	// to avoid leaking a file handle.
+	if err1 := f.Close(); err1 != nil && err == nil {
+		err = err1
+	}
+
+	return err
+}
+
+// WriteFileRemove synchronously writes data to the named file.
+// If the file does not exist, WriteFileRemove creates it with permissions perm
+// (before umask); otherwise WriteFileRemove truncates it before writing,
+// without changing permissions.
+// By opening the file with O_SYNC, it ensures the data is written to disk.
+// If an error occurs, it removes the file.
+func WriteFileRemove(name string, data []byte, perm os.FileMode) error {
+	err := WriteFile(name, data, perm)
+	if err != nil {
+		_ = os.Remove(name)
+	}
+
+	return err
+}
diff --git a/go.mod b/go.mod
index 36766d0d8..bbb421de4 100644
--- a/go.mod
+++ b/go.mod
@@ -4,7 +4,7 @@ require (
 	github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82
 	github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344
 	github.com/andybalholm/brotli v1.0.4
-	github.com/btcsuite/btcd v0.24.3-0.20241210095828-e646d437e95b
+	github.com/btcsuite/btcd v0.24.3-0.20240921052913-67b8efd3ba53
 	github.com/btcsuite/btcd/btcec/v2 v2.3.4
 	github.com/btcsuite/btcd/btcutil v1.1.5
 	github.com/btcsuite/btcd/btcutil/psbt v1.1.8
diff --git a/go.sum b/go.sum
index 69c57c20e..4c452df73 100644
--- a/go.sum
+++ b/go.sum
@@ -73,8 +73,8 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r
 github.com/btcsuite/btcd v0.20.1-beta/go.mod h1:wVuoA8VJLEcwgqHBwHmzLRazpKxTv13Px/pDuV7OomQ=
 github.com/btcsuite/btcd v0.22.0-beta.0.20220111032746-97732e52810c/go.mod h1:tjmYdS6MLJ5/s0Fj4DbLgSbDHbEqLJrtnHecBFkdz5M=
 github.com/btcsuite/btcd v0.23.5-0.20231215221805-96c9fd8078fd/go.mod h1:nm3Bko6zh6bWP60UxwoT5LzdGJsQJaPo6HjduXq9p6A=
-github.com/btcsuite/btcd v0.24.3-0.20241210095828-e646d437e95b h1:VQoobSrWdxICuqFU3tKVu/Lzk7BTk9SsCgRr5dUvC70=
-github.com/btcsuite/btcd v0.24.3-0.20241210095828-e646d437e95b/go.mod h1:zHK7t7sw8XbsCkD64WePHE3r3k9/XoGAcf6mXV14c64=
+github.com/btcsuite/btcd v0.24.3-0.20240921052913-67b8efd3ba53 h1:XOZ/wRGHkKv0AqxfDks5IkzaQ1Ge6fq322ZOOG5VIkU=
+github.com/btcsuite/btcd v0.24.3-0.20240921052913-67b8efd3ba53/go.mod h1:zHK7t7sw8XbsCkD64WePHE3r3k9/XoGAcf6mXV14c64=
 github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
 github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
 github.com/btcsuite/btcd/btcec/v2 v2.3.4 h1:3EJjcN70HCu/mwqlUsGK8GcNVyLVxFDlWurTXGPFfiQ=
diff --git a/input/script_utils.go b/input/script_utils.go
index 90e368dd1..000efe958 100644
--- a/input/script_utils.go
+++ b/input/script_utils.go
@@ -29,9 +29,9 @@ var (
 	SequenceLockTimeSeconds = uint32(1 << 22)
 )
 
-// MustParsePubKey parses a hex encoded public key string into a public key and
+// mustParsePubKey parses a hex encoded public key string into a public key and
 // panic if parsing fails.
-func MustParsePubKey(pubStr string) btcec.PublicKey {
+func mustParsePubKey(pubStr string) btcec.PublicKey {
 	pubBytes, err := hex.DecodeString(pubStr)
 	if err != nil {
 		panic(err)
@@ -55,7 +55,7 @@ var (
 	// https://github.com/lightninglabs/lightning-node-connect/tree/
 	// master/mailbox/numsgen, with the seed phrase "Lightning Simple
 	// Taproot".
-	TaprootNUMSKey = MustParsePubKey(TaprootNUMSHex)
+	TaprootNUMSKey = mustParsePubKey(TaprootNUMSHex)
 )
 
 // Signature is an interface for objects that can populate signatures during
diff --git a/lnwallet/chancloser/chancloser.go b/lnwallet/chancloser/chancloser.go
index 5081f0beb..398a8a9f3 100644
--- a/lnwallet/chancloser/chancloser.go
+++ b/lnwallet/chancloser/chancloser.go
@@ -539,8 +539,8 @@ func (c *ChanCloser) AuxOutputs() fn.Option[AuxCloseOutputs] {
 // upfront script is set, we check whether it matches the script provided by
 // our peer. If they do not match, we use the disconnect function provided to
 // disconnect from the peer.
-func validateShutdownScript(upfrontScript, peerScript lnwire.DeliveryAddress,
-	netParams *chaincfg.Params) error {
+func validateShutdownScript(disconnect func() error, upfrontScript,
+	peerScript lnwire.DeliveryAddress, netParams *chaincfg.Params) error {
 
 	// Either way, we'll make sure that the script passed meets our
 	// standards. The upfrontScript should have already been checked at an
@@ -568,6 +568,12 @@ func validateShutdownScript(upfrontScript, peerScript lnwire.DeliveryAddress,
 		chancloserLog.Warnf("peer's script: %x does not match upfront "+
 			"shutdown script: %x", peerScript, upfrontScript)
 
+		// Disconnect from the peer because they have violated option upfront
+		// shutdown.
+		if err := disconnect(); err != nil {
+			return err
+		}
+
 		return ErrUpfrontShutdownScriptMismatch
 	}
 
@@ -624,6 +630,7 @@ func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) (
 		// If the remote node opened the channel with option upfront
 		// shutdown script, check that the script they provided matches.
 		if err := validateShutdownScript(
+			c.cfg.Disconnect,
 			c.cfg.Channel.RemoteUpfrontShutdownScript(),
 			msg.Address, c.cfg.ChainParams,
 		); err != nil {
@@ -674,6 +681,7 @@ func (c *ChanCloser) ReceiveShutdown(msg lnwire.Shutdown) (
 		// If the remote node opened the channel with option upfront
 		// shutdown script, check that the script they provided matches.
 		if err := validateShutdownScript(
+			c.cfg.Disconnect,
 			c.cfg.Channel.RemoteUpfrontShutdownScript(),
 			msg.Address, c.cfg.ChainParams,
 		); err != nil {
diff --git a/lnwallet/chancloser/interface.go b/lnwallet/chancloser/interface.go
index 74f096973..f774c8103 100644
--- a/lnwallet/chancloser/interface.go
+++ b/lnwallet/chancloser/interface.go
@@ -3,6 +3,7 @@ package chancloser
 import (
 	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/btcutil"
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/fn/v2"
@@ -99,7 +100,7 @@ type Channel interface { //nolint:interfacebloat
 		localDeliveryScript []byte, remoteDeliveryScript []byte,
 		closeOpt ...lnwallet.ChanCloseOpt,
 	) (
-		input.Signature, *wire.MsgTx, btcutil.Amount, error)
+		input.Signature, *chainhash.Hash, btcutil.Amount, error)
 
 	// CompleteCooperativeClose persistently "completes" the cooperative
 	// close by producing a fully signed co-op close transaction.
diff --git a/lnwallet/chancloser/mock.go b/lnwallet/chancloser/mock.go
deleted file mode 100644
index a2cc4270c..000000000
--- a/lnwallet/chancloser/mock.go
+++ /dev/null
@@ -1,176 +0,0 @@
-package chancloser
-
-import (
-	"sync/atomic"
-
-	"github.com/btcsuite/btcd/btcec/v2"
-	"github.com/btcsuite/btcd/btcutil"
-	"github.com/btcsuite/btcd/chaincfg/chainhash"
-	"github.com/btcsuite/btcd/wire"
-	"github.com/lightningnetwork/lnd/chainntnfs"
-	"github.com/lightningnetwork/lnd/channeldb"
-	"github.com/lightningnetwork/lnd/fn/v2"
-	"github.com/lightningnetwork/lnd/input"
-	"github.com/lightningnetwork/lnd/lnwallet"
-	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
-	"github.com/lightningnetwork/lnd/lnwire"
-	"github.com/stretchr/testify/mock"
-)
-
-type dummyAdapters struct {
-	mock.Mock
-
-	msgSent atomic.Bool
-
-	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 {
-
-	defer d.msgSent.Store(true)
-
-	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) DisableChannel(op wire.OutPoint) error {
-	args := d.Called(op)
-
-	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
-}
-
-type mockFeeEstimator struct {
-	mock.Mock
-}
-
-func (m *mockFeeEstimator) EstimateFee(chanType channeldb.ChannelType,
-	localTxOut, remoteTxOut *wire.TxOut,
-	idealFeeRate chainfee.SatPerKWeight) btcutil.Amount {
-
-	args := m.Called(chanType, localTxOut, remoteTxOut, idealFeeRate)
-	return args.Get(0).(btcutil.Amount)
-}
-
-type mockChanObserver struct {
-	mock.Mock
-}
-
-func (m *mockChanObserver) NoDanglingUpdates() bool {
-	args := m.Called()
-	return args.Bool(0)
-}
-
-func (m *mockChanObserver) DisableIncomingAdds() error {
-	args := m.Called()
-	return args.Error(0)
-}
-
-func (m *mockChanObserver) DisableOutgoingAdds() error {
-	args := m.Called()
-	return args.Error(0)
-}
-
-func (m *mockChanObserver) MarkCoopBroadcasted(txn *wire.MsgTx,
-	local bool) error {
-
-	args := m.Called(txn, local)
-	return args.Error(0)
-}
-
-func (m *mockChanObserver) MarkShutdownSent(deliveryAddr []byte,
-	isInitiator bool) error {
-
-	args := m.Called(deliveryAddr, isInitiator)
-	return args.Error(0)
-}
-
-func (m *mockChanObserver) FinalBalances() fn.Option[ShutdownBalances] {
-	args := m.Called()
-	return args.Get(0).(fn.Option[ShutdownBalances])
-}
-
-func (m *mockChanObserver) DisableChannel() error {
-	args := m.Called()
-	return args.Error(0)
-}
-
-type mockErrorReporter struct {
-	mock.Mock
-}
-
-func (m *mockErrorReporter) ReportError(err error) {
-	m.Called(err)
-}
-
-type mockCloseSigner struct {
-	mock.Mock
-}
-
-func (m *mockCloseSigner) CreateCloseProposal(fee btcutil.Amount,
-	localScript []byte, remoteScript []byte,
-	closeOpt ...lnwallet.ChanCloseOpt) (
-	input.Signature, *wire.MsgTx, btcutil.Amount, error) {
-
-	args := m.Called(fee, localScript, remoteScript, closeOpt)
-
-	return args.Get(0).(input.Signature), args.Get(1).(*wire.MsgTx),
-		args.Get(2).(btcutil.Amount), args.Error(3)
-}
-
-func (m *mockCloseSigner) CompleteCooperativeClose(localSig,
-	remoteSig input.Signature,
-	localScript, remoteScript []byte,
-	fee btcutil.Amount, closeOpt ...lnwallet.ChanCloseOpt,
-) (*wire.MsgTx, btcutil.Amount, error) {
-
-	args := m.Called(
-		localSig, remoteSig, localScript, remoteScript, fee, closeOpt,
-	)
-
-	return args.Get(0).(*wire.MsgTx), args.Get(1).(btcutil.Amount),
-		args.Error(2)
-}
diff --git a/lnwallet/chancloser/rbf_coop_msg_mapper.go b/lnwallet/chancloser/rbf_coop_msg_mapper.go
deleted file mode 100644
index 96c855686..000000000
--- a/lnwallet/chancloser/rbf_coop_msg_mapper.go
+++ /dev/null
@@ -1,77 +0,0 @@
-package chancloser
-
-import (
-	"github.com/lightningnetwork/lnd/fn/v2"
-	"github.com/lightningnetwork/lnd/lnwire"
-)
-
-// RbfMsgMapper is a struct that implements the MsgMapper interface for the
-// rbf-coop close state machine. This enables the state machine to be used with
-// protofsm.
-type RbfMsgMapper struct {
-	// blockHeight is the height of the block when the co-op close request
-	// was initiated. This is used to validate conditions related to the
-	// thaw height.
-	blockHeight uint32
-
-	// chanID is the channel ID of the channel being closed.
-	chanID lnwire.ChannelID
-}
-
-// NewRbfMsgMapper creates a new RbfMsgMapper instance given the current block
-// height when the co-op close request was initiated.
-func NewRbfMsgMapper(blockHeight uint32,
-	chanID lnwire.ChannelID) *RbfMsgMapper {
-
-	return &RbfMsgMapper{
-		blockHeight: blockHeight,
-		chanID:      chanID,
-	}
-}
-
-// someEvent returns the target type as a protocol event option.
-func someEvent[T ProtocolEvent](m T) fn.Option[ProtocolEvent] {
-	return fn.Some(ProtocolEvent(m))
-}
-
-// isExpectedChanID returns true if the channel ID of the message matches the
-// bound instance.
-func (r *RbfMsgMapper) isExpectedChanID(chanID lnwire.ChannelID) bool {
-	return r.chanID == chanID
-}
-
-// MapMsg maps a wire message into a FSM event. If the message is not mappable,
-// then an error is returned.
-func (r *RbfMsgMapper) MapMsg(wireMsg lnwire.Message) fn.Option[ProtocolEvent] {
-	switch msg := wireMsg.(type) {
-	case *lnwire.Shutdown:
-		if !r.isExpectedChanID(msg.ChannelID) {
-			return fn.None[ProtocolEvent]()
-		}
-
-		return someEvent(&ShutdownReceived{
-			BlockHeight:    r.blockHeight,
-			ShutdownScript: msg.Address,
-		})
-
-	case *lnwire.ClosingComplete:
-		if !r.isExpectedChanID(msg.ChannelID) {
-			return fn.None[ProtocolEvent]()
-		}
-
-		return someEvent(&OfferReceivedEvent{
-			SigMsg: *msg,
-		})
-
-	case *lnwire.ClosingSig:
-		if !r.isExpectedChanID(msg.ChannelID) {
-			return fn.None[ProtocolEvent]()
-		}
-
-		return someEvent(&LocalSigReceived{
-			SigMsg: *msg,
-		})
-	}
-
-	return fn.None[ProtocolEvent]()
-}
diff --git a/lnwallet/chancloser/rbf_coop_states.go b/lnwallet/chancloser/rbf_coop_states.go
deleted file mode 100644
index d5c207066..000000000
--- a/lnwallet/chancloser/rbf_coop_states.go
+++ /dev/null
@@ -1,763 +0,0 @@
-package chancloser
-
-import (
-	"fmt"
-
-	"github.com/btcsuite/btcd/btcec/v2"
-	"github.com/btcsuite/btcd/btcutil"
-	"github.com/btcsuite/btcd/chaincfg"
-	"github.com/btcsuite/btcd/wire"
-	"github.com/lightningnetwork/lnd/chainntnfs"
-	"github.com/lightningnetwork/lnd/channeldb"
-	"github.com/lightningnetwork/lnd/fn/v2"
-	"github.com/lightningnetwork/lnd/input"
-	"github.com/lightningnetwork/lnd/lntypes"
-	"github.com/lightningnetwork/lnd/lnwallet"
-	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
-	"github.com/lightningnetwork/lnd/lnwire"
-	"github.com/lightningnetwork/lnd/protofsm"
-)
-
-var (
-	// ErrInvalidStateTransition is returned when we receive an unexpected
-	// event for a given state.
-	ErrInvalidStateTransition = fmt.Errorf("invalid state transition")
-
-	// ErrTooManySigs is returned when we receive too many sigs from the
-	// remote party in the ClosingSigs message.
-	ErrTooManySigs = fmt.Errorf("too many sigs received")
-
-	// ErrNoSig is returned when we receive no sig from the remote party.
-	ErrNoSig = fmt.Errorf("no sig received")
-
-	// ErrUnknownFinalBalance is returned if we're unable to determine the
-	// final channel balance after a flush.
-	ErrUnknownFinalBalance = fmt.Errorf("unknown final balance")
-
-	// ErrRemoteCannotPay is returned if the remote party cannot pay the
-	// pay for the fees when it sends a signature.
-	ErrRemoteCannotPay = fmt.Errorf("remote cannot pay fees")
-
-	// ErrNonFinalSequence is returned if we receive a non-final sequence
-	// from the remote party for their signature.
-	ErrNonFinalSequence = fmt.Errorf("received non-final sequence")
-
-	// ErrCloserNoClosee is returned if our balance is dust, but the remote
-	// party includes our output.
-	ErrCloserNoClosee = fmt.Errorf("expected CloserNoClosee sig")
-
-	// ErrCloserAndClosee is returned when we expect a sig covering both
-	// outputs, it isn't present.
-	ErrCloserAndClosee = fmt.Errorf("expected CloserAndClosee sig")
-)
-
-// ProtocolEvent is a special interface used to create the equivalent of a
-// sum-type, but using a "sealed" interface. Protocol events can be used as
-// input to trigger a state transition, and also as output to trigger a new set
-// of events into the very same state machine.
-type ProtocolEvent interface {
-	protocolSealed()
-}
-
-// ProtocolEvents is a special type constraint that enumerates all the possible
-// protocol events. This is used mainly as type-level documentation, and may
-// also be useful to constraint certain state transition functions.
-type ProtocolEvents interface {
-	SendShutdown | ShutdownReceived | ShutdownComplete | ChannelFlushed |
-		SendOfferEvent | OfferReceivedEvent | LocalSigReceived |
-		SpendEvent
-}
-
-// SpendEvent indicates that a transaction spending the funding outpoint has
-// been confirmed in the main chain.
-type SpendEvent struct {
-	// Tx is the spending transaction that has been confirmed.
-	Tx *wire.MsgTx
-
-	// BlockHeight is the height of the block that confirmed the
-	// transaction.
-	BlockHeight uint32
-}
-
-// protocolSealed indicates that this struct is a ProtocolEvent instance.
-func (s *SpendEvent) protocolSealed() {}
-
-// SendShutdown indicates that the user wishes to co-op close the channel, so we
-// should send a new shutdown message to the remote party.
-//
-// transition:
-//   - fromState: ChannelActive
-//   - toState: ChannelFlushing
-type SendShutdown struct {
-	// DeliveryAddr is the address we'd like to receive the funds to. If
-	// None, then a new addr will be generated.
-	DeliveryAddr fn.Option[lnwire.DeliveryAddress]
-
-	// IdealFeeRate is the ideal fee rate we'd like to use for the closing
-	// attempt.
-	IdealFeeRate chainfee.SatPerVByte
-}
-
-// protocolSealed indicates that this struct is a ProtocolEvent instance.
-func (s *SendShutdown) protocolSealed() {}
-
-// ShutdownReceived indicates that we received a shutdown event so we need to
-// enter the flushing state.
-//
-// transition:
-//   - fromState: ChannelActive
-//   - toState: ChannelFlushing
-type ShutdownReceived struct {
-	// ShutdownScript is the script the remote party wants to use to
-	// shutdown.
-	ShutdownScript lnwire.DeliveryAddress
-
-	// BlockHeight is the height at which the shutdown message was
-	// received. This is used for channel leases to determine if a co-op
-	// close can occur.
-	BlockHeight uint32
-}
-
-// protocolSealed indicates that this struct is a ProtocolEvent instance.
-func (s *ShutdownReceived) protocolSealed() {}
-
-// ShutdownComplete is an event that indicates the channel has been fully
-// shutdown. At this point, we'll go to the ChannelFlushing state so we can
-// wait for all pending updates to be gone from the channel.
-//
-// transition:
-//   - fromState: ShutdownPending
-//   - toState: ChannelFlushing
-type ShutdownComplete struct {
-}
-
-// protocolSealed indicates that this struct is a ProtocolEvent instance.
-func (s *ShutdownComplete) protocolSealed() {}
-
-// ShutdownBalances holds the local+remote balance once the channel has been
-// fully flushed.
-type ShutdownBalances struct {
-	// LocalBalance is the local balance of the channel.
-	LocalBalance lnwire.MilliSatoshi
-
-	// RemoteBalance is the remote balance of the channel.
-	RemoteBalance lnwire.MilliSatoshi
-}
-
-// unknownBalance is a special variable used to denote an unknown channel
-// balance (channel not fully flushed yet).
-var unknownBalance = ShutdownBalances{}
-
-// ChannelFlushed is an event that indicates the channel has been fully flushed
-// can we can now start closing negotiation.
-//
-// transition:
-//   - fromState: ChannelFlushing
-//   - toState: ClosingNegotiation
-type ChannelFlushed struct {
-	// FreshFlush indicates if this is the first time the channel has been
-	// flushed, or if this is a flush as part of an RBF iteration.
-	FreshFlush bool
-
-	// ShutdownBalances is the balances of the channel once it has been
-	// flushed. We tie this to the ChannelFlushed state as this may not be
-	// the same as the starting value.
-	ShutdownBalances
-}
-
-// protocolSealed indicates that this struct is a ProtocolEvent instance.
-func (c *ChannelFlushed) protocolSealed() {}
-
-// SendOfferEvent is a self-triggered event that transitions us from the
-// LocalCloseStart state to the LocalOfferSent state. This kicks off the new
-// signing process for the co-op close process.
-//
-// transition:
-//   - fromState: LocalCloseStart
-//   - toState: LocalOfferSent
-type SendOfferEvent struct {
-	// TargetFeeRate is the fee rate we'll use for the closing transaction.
-	TargetFeeRate chainfee.SatPerVByte
-}
-
-// protocolSealed indicates that this struct is a ProtocolEvent instance.
-func (s *SendOfferEvent) protocolSealed() {}
-
-// LocalSigReceived is an event that indicates we've received a signature from
-// the remote party, which signs our the co-op close transaction at our
-// specified fee rate.
-//
-// transition:
-//   - fromState: LocalOfferSent
-//   - toState: ClosePending
-type LocalSigReceived struct {
-	// SigMsg is the sig message we received from the remote party.
-	SigMsg lnwire.ClosingSig
-}
-
-// protocolSealed indicates that this struct is a ProtocolEvent instance.
-func (s *LocalSigReceived) protocolSealed() {}
-
-// OfferReceivedEvent is an event that indicates we've received an offer from
-// the remote party. This applies to the RemoteCloseStart state.
-//
-// transition:
-//   - fromState: RemoteCloseStart
-//   - toState: ClosePending
-type OfferReceivedEvent struct {
-	// SigMsg is the signature message we received from the remote party.
-	SigMsg lnwire.ClosingComplete
-}
-
-// protocolSealed indicates that this struct is a ProtocolEvent instance.
-func (s *OfferReceivedEvent) protocolSealed() {}
-
-// CloseSigner is an interface that abstracts away the details of the signing
-// new coop close transactions.
-type CloseSigner interface {
-	// CreateCloseProposal creates a new co-op close proposal in the form
-	// of a valid signature, the chainhash of the final txid, and our final
-	// balance in the created state.
-	CreateCloseProposal(proposedFee btcutil.Amount,
-		localDeliveryScript []byte, remoteDeliveryScript []byte,
-		closeOpt ...lnwallet.ChanCloseOpt,
-	) (
-		input.Signature, *wire.MsgTx, btcutil.Amount, error)
-
-	// CompleteCooperativeClose persistently "completes" the cooperative
-	// close by producing a fully signed co-op close transaction.
-	CompleteCooperativeClose(localSig, remoteSig input.Signature,
-		localDeliveryScript, remoteDeliveryScript []byte,
-		proposedFee btcutil.Amount, closeOpt ...lnwallet.ChanCloseOpt,
-	) (*wire.MsgTx, btcutil.Amount, error)
-}
-
-// ChanStateObserver is an interface used to observe state changes that occur
-// in a channel. This can be used to figure out if we're able to send a
-// shutdown message or not.
-type ChanStateObserver interface {
-	// NoDanglingUpdates returns true if there are no dangling updates in
-	// the channel. In other words, there are no active update messages
-	// that haven't already been covered by a commit sig.
-	NoDanglingUpdates() bool
-
-	// DisableIncomingAdds instructs the channel link to disable process new
-	// incoming add messages.
-	DisableIncomingAdds() error
-
-	// DisableOutgoingAdds instructs the channel link to disable process
-	// new outgoing add messages.
-	DisableOutgoingAdds() error
-
-	// DisableChannel attempts to disable a channel (marking it ineligible
-	// to forward), and also sends out a network update to disable the
-	// channel.
-	DisableChannel() error
-
-	// MarkCoopBroadcasted persistently marks that the channel close
-	// transaction has been broadcast.
-	MarkCoopBroadcasted(*wire.MsgTx, bool) error
-
-	// MarkShutdownSent persists the given ShutdownInfo. The existence of
-	// the ShutdownInfo represents the fact that the Shutdown message has
-	// been sent by us and so should be re-sent on re-establish.
-	MarkShutdownSent(deliveryAddr []byte, isInitiator bool) error
-
-	// FinalBalances is the balances of the channel once it has been
-	// flushed. If Some, then this indicates that the channel is now in a
-	// state where it's always flushed, so we can accelerate the state
-	// transitions.
-	FinalBalances() fn.Option[ShutdownBalances]
-}
-
-// Environment is a set of dependencies that a state machine may need to carry
-// out the logic for a given state transition. All fields are to be considered
-// immutable, and will be fixed for the lifetime of the state machine.
-type Environment struct {
-	// ChainParams is the chain parameters for the channel.
-	ChainParams chaincfg.Params
-
-	// ChanPeer is the peer we're attempting to close the channel with.
-	ChanPeer btcec.PublicKey
-
-	// ChanPoint is the channel point of the active channel.
-	ChanPoint wire.OutPoint
-
-	// ChanID is the channel ID of the channel we're attempting to close.
-	ChanID lnwire.ChannelID
-
-	// ShortChanID is the short channel ID of the channel we're attempting
-	// to close.
-	Scid lnwire.ShortChannelID
-
-	// ChanType is the type of channel we're attempting to close.
-	ChanType channeldb.ChannelType
-
-	// BlockHeight is the current block height.
-	BlockHeight uint32
-
-	// DefaultFeeRate is the fee we'll use for the closing transaction if
-	// the user didn't specify an ideal fee rate. This may happen if the
-	// remote party is the one that initiates the co-op close.
-	DefaultFeeRate chainfee.SatPerVByte
-
-	// ThawHeight is the height at which the channel will be thawed. If
-	// this is None, then co-op close can occur at any moment.
-	ThawHeight fn.Option[uint32]
-
-	// RemoteUprontShutdown is the upfront shutdown addr of the remote
-	// party. We'll use this to validate if the remote peer is authorized to
-	// close the channel with the sent addr or not.
-	RemoteUpfrontShutdown fn.Option[lnwire.DeliveryAddress]
-
-	// LocalUprontShutdown is our upfront shutdown address. If Some, then
-	// we'll default to using this.
-	LocalUpfrontShutdown fn.Option[lnwire.DeliveryAddress]
-
-	// NewDeliveryScript is a function that returns a new delivery script.
-	// This is used if we don't have an upfront shutdown addr, and no addr
-	// was specified at closing time.
-	NewDeliveryScript func() (lnwire.DeliveryAddress, error)
-
-	// FeeEstimator is the fee estimator we'll use to determine the fee in
-	// satoshis we'll pay given a local and/or remote output.
-	FeeEstimator CoopFeeEstimator
-
-	// ChanObserver is an interface used to observe state changes to the
-	// channel. We'll use this to figure out when/if we can send certain
-	// messages.
-	ChanObserver ChanStateObserver
-
-	// CloseSigner is the signer we'll use to sign the close transaction.
-	// This is a part of the ChannelFlushed state, as the channel state
-	// we'll be signing can only be determined once the channel has been
-	// flushed.
-	CloseSigner CloseSigner
-}
-
-// Name returns the name of the environment. This is used to uniquely identify
-// the environment of related state machines. For this state machine, the name
-// is based on the channel ID.
-func (e *Environment) Name() string {
-	return fmt.Sprintf("rbf_chan_closer(%v)", e.ChanPoint)
-}
-
-// CloseStateTransition is the StateTransition type specific to the coop close
-// state machine.
-//
-//nolint:ll
-type CloseStateTransition = protofsm.StateTransition[ProtocolEvent, *Environment]
-
-// ProtocolState is our sum-type ish interface that represents the current
-// protocol state.
-type ProtocolState interface {
-	// protocolStateSealed is a special method that is used to seal the
-	// interface (only types in this package can implement it).
-	protocolStateSealed()
-
-	// IsTerminal returns true if the target state is a terminal state.
-	IsTerminal() bool
-
-	// ProcessEvent takes a protocol event, and implements a state
-	// transition for the state.
-	ProcessEvent(ProtocolEvent, *Environment) (*CloseStateTransition, error)
-}
-
-// AsymmetricPeerState is an extension of the normal ProtocolState interface
-// that gives a caller a hit on if the target state should process an incoming
-// event or not.
-type AsymmetricPeerState interface {
-	ProtocolState
-
-	// ShouldRouteTo returns true if the target state should process the
-	// target event.
-	ShouldRouteTo(ProtocolEvent) bool
-}
-
-// ProtocolStates is a special type constraint that enumerates all the possible
-// protocol states.
-type ProtocolStates interface {
-	ChannelActive | ShutdownPending | ChannelFlushing | ClosingNegotiation |
-		LocalCloseStart | LocalOfferSent | RemoteCloseStart |
-		ClosePending | CloseFin
-}
-
-// ChannelActive is the base state for the channel closer state machine. In
-// this state, we haven't begun the shutdown process yet, so the channel is
-// still active. Receiving the ShutdownSent or ShutdownReceived events will
-// transition us to the ChannelFushing state.
-//
-// When we transition to this state, we emit a DaemonEvent to send the shutdown
-// message if we received one ourselves. Alternatively, we may send out a new
-// shutdown if we're initiating it for the very first time.
-//
-// transition:
-//   - fromState: None
-//   - toState: ChannelFlushing
-//
-// input events:
-//   - SendShutdown
-//   - ShutdownReceived
-type ChannelActive struct {
-}
-
-// IsTerminal returns true if the target state is a terminal state.
-func (c *ChannelActive) IsTerminal() bool {
-	return false
-}
-
-// protocolSealed indicates that this struct is a ProtocolEvent instance.
-func (c *ChannelActive) protocolStateSealed() {}
-
-// ShutdownScripts is a set of scripts that we'll use to co-op close the
-// channel.
-type ShutdownScripts struct {
-	// LocalDeliveryScript is the script that we'll send our settled
-	// channel funds to.
-	LocalDeliveryScript lnwire.DeliveryAddress
-
-	// RemoteDeliveryScript is the script that we'll send the remote
-	// party's settled channel funds to.
-	RemoteDeliveryScript lnwire.DeliveryAddress
-}
-
-// ShutdownPending is the state we enter into after we've sent or received the
-// shutdown message. If we sent the shutdown, then we'll wait for the remote
-// party to send a shutdown. Otherwise, if we received it, then we'll send our
-// shutdown then go to the next state.
-//
-// transition:
-//   - fromState: ChannelActive
-//   - toState: ChannelFlushing
-//
-// input events:
-//   - SendShutdown
-//   - ShutdownReceived
-type ShutdownPending struct {
-	// ShutdownScripts store the set of scripts we'll use to initiate a coop
-	// close.
-	ShutdownScripts
-
-	// IdealFeeRate is the ideal fee rate we'd like to use for the closing
-	// attempt.
-	IdealFeeRate fn.Option[chainfee.SatPerVByte]
-}
-
-// IsTerminal returns true if the target state is a terminal state.
-func (s *ShutdownPending) IsTerminal() bool {
-	return false
-}
-
-// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
-func (s *ShutdownPending) protocolStateSealed() {}
-
-// ChannelFlushing is the state we enter into after we've received or sent a
-// shutdown message. In this state, we wait the ChannelFlushed event, after
-// which we'll transition to the CloseReady state.
-//
-// transition:
-//   - fromState: ShutdownPending
-//   - toState: ClosingNegotiation
-//
-// input events:
-//   - ShutdownComplete
-//   - ShutdownReceived
-type ChannelFlushing struct {
-	// EarlyRemoteOffer is the offer we received from the remote party
-	// before we obtained the local channel flushed event. We'll stash this
-	// to process later.
-	EarlyRemoteOffer fn.Option[OfferReceivedEvent]
-
-	// ShutdownScripts store the set of scripts we'll use to initiate a coop
-	// close.
-	ShutdownScripts
-
-	// IdealFeeRate is the ideal fee rate we'd like to use for the closing
-	// transaction. Once the channel has been flushed, we'll use this as
-	// our target fee rate.
-	IdealFeeRate fn.Option[chainfee.SatPerVByte]
-}
-
-// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
-func (c *ChannelFlushing) protocolStateSealed() {}
-
-// IsTerminal returns true if the target state is a terminal state.
-func (c *ChannelFlushing) IsTerminal() bool {
-	return false
-}
-
-// ClosingNegotiation is the state we transition to once the channel has been
-// flushed. This is actually a composite state that contains one for each side
-// of the channel, as the closing process is asymmetric. Once either of the
-// peer states reaches the CloseFin state, then the channel is fully closed,
-// and we'll transition to that terminal state.
-//
-// transition:
-//   - fromState: ChannelFlushing
-//   - toState: CloseFin
-//
-// input events:
-//   - ChannelFlushed
-type ClosingNegotiation struct {
-	// PeerStates is a composite state that contains the state for both the
-	// local and remote parties. Our usage of Dual makes this a special
-	// state that allows us to treat two states as a single state. We'll use
-	// the ShouldRouteTo method to determine which state route incoming
-	// events to.
-	PeerState lntypes.Dual[AsymmetricPeerState]
-}
-
-// IsTerminal returns true if the target state is a terminal state.
-func (c *ClosingNegotiation) IsTerminal() bool {
-	return false
-}
-
-// protocolSealed indicates that this struct is a ProtocolEvent instance.
-func (c *ClosingNegotiation) protocolStateSealed() {}
-
-// CloseChannelTerms is a set of terms that we'll use to close the channel. This
-// includes the balances of the channel, and the scripts we'll use to send each
-// party's funds to.
-type CloseChannelTerms struct {
-	ShutdownScripts
-
-	ShutdownBalances
-}
-
-// DeriveCloseTxOuts takes the close terms, and returns the local and remote tx
-// out for the close transaction. If an output is dust, then it'll be nil.
-//
-// TODO(roasbeef): add func for w/e heuristic to not manifest own output?
-func (c *CloseChannelTerms) DeriveCloseTxOuts() (*wire.TxOut, *wire.TxOut) {
-	//nolint:ll
-	deriveTxOut := func(balance btcutil.Amount, pkScript []byte) *wire.TxOut {
-		dustLimit := lnwallet.DustLimitForSize(len(pkScript))
-		if balance >= dustLimit {
-			return &wire.TxOut{
-				PkScript: pkScript,
-				Value:    int64(balance),
-			}
-		}
-
-		return nil
-	}
-
-	localTxOut := deriveTxOut(
-		c.LocalBalance.ToSatoshis(),
-		c.LocalDeliveryScript,
-	)
-	remoteTxOut := deriveTxOut(
-		c.RemoteBalance.ToSatoshis(),
-		c.RemoteDeliveryScript,
-	)
-
-	return localTxOut, remoteTxOut
-}
-
-// RemoteAmtIsDust returns true if the remote output is dust.
-func (c *CloseChannelTerms) RemoteAmtIsDust() bool {
-	return c.RemoteBalance.ToSatoshis() < lnwallet.DustLimitForSize(
-		len(c.RemoteDeliveryScript),
-	)
-}
-
-// LocalAmtIsDust returns true if the local output is dust.
-func (c *CloseChannelTerms) LocalAmtIsDust() bool {
-	return c.LocalBalance.ToSatoshis() < lnwallet.DustLimitForSize(
-		len(c.LocalDeliveryScript),
-	)
-}
-
-// LocalCanPayFees returns true if the local party can pay the absolute fee
-// from their local settled balance.
-func (c *CloseChannelTerms) LocalCanPayFees(absoluteFee btcutil.Amount) bool {
-	return c.LocalBalance.ToSatoshis() >= absoluteFee
-}
-
-// RemoteCanPayFees returns true if the remote party can pay the absolute fee
-// from their remote settled balance.
-func (c *CloseChannelTerms) RemoteCanPayFees(absoluteFee btcutil.Amount) bool {
-	return c.RemoteBalance.ToSatoshis() >= absoluteFee
-}
-
-// LocalCloseStart is the state we enter into after we've received or sent
-// shutdown, and the channel has been flushed. In this state, we'll emit a new
-// event to send our offer to drive the rest of the process.
-//
-// transition:
-//   - fromState: ChannelFlushing
-//   - toState: LocalOfferSent
-//
-// input events:
-//   - SendOfferEvent
-type LocalCloseStart struct {
-	CloseChannelTerms
-}
-
-// ShouldRouteTo returns true if the target state should process the target
-// event.
-func (l *LocalCloseStart) ShouldRouteTo(event ProtocolEvent) bool {
-	switch event.(type) {
-	case *SendOfferEvent:
-		return true
-	default:
-		return false
-	}
-}
-
-// IsTerminal returns true if the target state is a terminal state.
-func (l *LocalCloseStart) IsTerminal() bool {
-	return false
-}
-
-// protocolStateaSealed indicates that this struct is a ProtocolEvent instance.
-func (l *LocalCloseStart) protocolStateSealed() {}
-
-// LocalOfferSent is the state we transition to after we reveiver the
-// SendOfferEvent in the LocalCloseStart state. With this state we send our
-// offer to the remote party, then await a sig from them which concludes the
-// local cooperative close process.
-//
-// transition:
-//   - fromState: LocalCloseStart
-//   - toState: ClosePending
-//
-// input events:
-//   - LocalSigReceived
-type LocalOfferSent struct {
-	CloseChannelTerms
-
-	// ProposedFee is the fee we proposed to the remote party.
-	ProposedFee btcutil.Amount
-
-	// LocalSig is the signature we sent to the remote party.
-	LocalSig lnwire.Sig
-}
-
-// ShouldRouteTo returns true if the target state should process the target
-// event.
-func (l *LocalOfferSent) ShouldRouteTo(event ProtocolEvent) bool {
-	switch event.(type) {
-	case *LocalSigReceived:
-		return true
-	default:
-		return false
-	}
-}
-
-// protocolStateaSealed indicates that this struct is a ProtocolEvent instance.
-func (l *LocalOfferSent) protocolStateSealed() {}
-
-// IsTerminal returns true if the target state is a terminal state.
-func (l *LocalOfferSent) IsTerminal() bool {
-	return false
-}
-
-// ClosePending is the state we enter after concluding the negotiation for the
-// remote or local state. At this point, given a confirmation notification we
-// can terminate the process. Otherwise, we can receive a fresh CoopCloseReq to
-// go back to the very start.
-//
-// transition:
-//   - fromState: LocalOfferSent || RemoteCloseStart
-//   - toState: CloseFin
-//
-// input events:
-//   - LocalSigReceived
-//   - OfferReceivedEvent
-type ClosePending struct {
-	// CloseTx is the pending close transaction.
-	CloseTx *wire.MsgTx
-}
-
-// ShouldRouteTo returns true if the target state should process the target
-// event.
-func (c *ClosePending) ShouldRouteTo(event ProtocolEvent) bool {
-	switch event.(type) {
-	case *SpendEvent:
-		return true
-	default:
-		return false
-	}
-}
-
-// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
-func (c *ClosePending) protocolStateSealed() {}
-
-// IsTerminal returns true if the target state is a terminal state.
-func (c *ClosePending) IsTerminal() bool {
-	return true
-}
-
-// CloseFin is the terminal state for the channel closer state machine. At this
-// point, the close tx has been confirmed on chain.
-type CloseFin struct {
-	// ConfirmedTx is the transaction that confirmed the channel close.
-	ConfirmedTx *wire.MsgTx
-}
-
-// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
-func (c *CloseFin) protocolStateSealed() {}
-
-// IsTerminal returns true if the target state is a terminal state.
-func (c *CloseFin) IsTerminal() bool {
-	return true
-}
-
-// RemoteCloseStart is similar to the LocalCloseStart, but is used to drive the
-// process of signing an offer for the remote party
-//
-// transition:
-//   - fromState: ChannelFlushing
-//   - toState: ClosePending
-type RemoteCloseStart struct {
-	CloseChannelTerms
-}
-
-// ShouldRouteTo returns true if the target state should process the target
-// event.
-func (l *RemoteCloseStart) ShouldRouteTo(event ProtocolEvent) bool {
-	switch event.(type) {
-	case *OfferReceivedEvent:
-		return true
-	default:
-		return false
-	}
-}
-
-// protocolStateSealed indicates that this struct is a ProtocolEvent instance.
-func (l *RemoteCloseStart) protocolStateSealed() {}
-
-// IsTerminal returns true if the target state is a terminal state.
-func (l *RemoteCloseStart) IsTerminal() bool {
-	return false
-}
-
-// RbfChanCloser is a state machine that handles the RBF-enabled cooperative
-// channel close protocol.
-type RbfChanCloser = protofsm.StateMachine[ProtocolEvent, *Environment]
-
-// RbfChanCloserCfg is a configuration struct that is used to initialize a new
-// RBF chan closer state machine.
-type RbfChanCloserCfg = protofsm.StateMachineCfg[ProtocolEvent, *Environment]
-
-// RbfSpendMapper is a type used to map the generic spend event to one specific
-// to this package.
-type RbfSpendMapper = protofsm.SpendMapper[ProtocolEvent]
-
-func SpendMapper(spendEvent *chainntnfs.SpendDetail) ProtocolEvent {
-	return &SpendEvent{
-		Tx:          spendEvent.SpendingTx,
-		BlockHeight: uint32(spendEvent.SpendingHeight),
-	}
-}
-
-// RbfMsgMapperT is a type used to map incoming wire messages to protocol
-// events.
-type RbfMsgMapperT = protofsm.MsgMapper[ProtocolEvent]
-
-// RbfState is a type alias for the state of the RBF channel closer.
-type RbfState = protofsm.State[ProtocolEvent, *Environment]
-
-// RbfEvent is a type alias for the event type of the RBF channel closer.
-type RbfEvent = protofsm.EmittedEvent[ProtocolEvent]
diff --git a/lnwallet/chancloser/rbf_coop_transitions.go b/lnwallet/chancloser/rbf_coop_transitions.go
deleted file mode 100644
index da45908b7..000000000
--- a/lnwallet/chancloser/rbf_coop_transitions.go
+++ /dev/null
@@ -1,1045 +0,0 @@
-package chancloser
-
-import (
-	"fmt"
-
-	"github.com/btcsuite/btcd/btcec/v2"
-	"github.com/btcsuite/btcd/chaincfg"
-	"github.com/btcsuite/btcd/mempool"
-	"github.com/btcsuite/btcd/wire"
-	"github.com/davecgh/go-spew/spew"
-	"github.com/lightningnetwork/lnd/fn/v2"
-	"github.com/lightningnetwork/lnd/input"
-	"github.com/lightningnetwork/lnd/labels"
-	"github.com/lightningnetwork/lnd/lntypes"
-	"github.com/lightningnetwork/lnd/lnutils"
-	"github.com/lightningnetwork/lnd/lnwallet"
-	"github.com/lightningnetwork/lnd/lnwire"
-	"github.com/lightningnetwork/lnd/protofsm"
-	"github.com/lightningnetwork/lnd/tlv"
-)
-
-// sendShutdownEvents is a helper function that returns a set of daemon events
-// we need to emit when we decide that we should send a shutdown message. We'll
-// also mark the channel as borked as well, as at this point, we no longer want
-// to continue with normal operation.
-func sendShutdownEvents(chanID lnwire.ChannelID, chanPoint wire.OutPoint,
-	deliveryAddr lnwire.DeliveryAddress, peerPub btcec.PublicKey,
-	postSendEvent fn.Option[ProtocolEvent],
-	chanState ChanStateObserver) (protofsm.DaemonEventSet, error) {
-
-	// We'll emit a daemon event that instructs the daemon to send out a
-	// new shutdown message to the remote peer.
-	msgsToSend := &protofsm.SendMsgEvent[ProtocolEvent]{
-		TargetPeer: peerPub,
-		Msgs: []lnwire.Message{&lnwire.Shutdown{
-			ChannelID: chanID,
-			Address:   deliveryAddr,
-		}},
-		SendWhen: fn.Some(func() bool {
-			ok := chanState.NoDanglingUpdates()
-			if ok {
-				chancloserLog.Infof("ChannelPoint(%v): no "+
-					"dangling updates sending shutdown "+
-					"message", chanPoint)
-			}
-
-			return ok
-		}),
-		PostSendEvent: postSendEvent,
-	}
-
-	// If a close is already in process (we're in the RBF loop), then we
-	// can skip everything below, and just send out the shutdown message.
-	if chanState.FinalBalances().IsSome() {
-		return protofsm.DaemonEventSet{msgsToSend}, nil
-	}
-
-	// Before closing, we'll attempt to send a disable update for the
-	// channel.  We do so before closing the channel as otherwise the
-	// current edge policy won't be retrievable from the graph.
-	if err := chanState.DisableChannel(); err != nil {
-		return nil, fmt.Errorf("unable to disable channel: %w", err)
-	}
-
-	// If we have a post-send event, then this means that we're the
-	// responder. We'll use this fact below to update state in the DB.
-	isInitiator := postSendEvent.IsNone()
-
-	chancloserLog.Infof("ChannelPoint(%v): disabling outgoing adds",
-		chanPoint)
-
-	// As we're about to send a shutdown, we'll disable adds in the
-	// outgoing direction.
-	if err := chanState.DisableOutgoingAdds(); err != nil {
-		return nil, fmt.Errorf("unable to disable outgoing "+
-			"adds: %w", err)
-	}
-
-	// To be able to survive a restart, we'll also write to disk
-	// information about the shutdown we're about to send out.
-	err := chanState.MarkShutdownSent(deliveryAddr, isInitiator)
-	if err != nil {
-		return nil, fmt.Errorf("unable to mark shutdown sent: %w", err)
-	}
-
-	chancloserLog.Debugf("ChannelPoint(%v): marking channel as borked",
-		chanPoint)
-
-	return protofsm.DaemonEventSet{msgsToSend}, nil
-}
-
-// validateShutdown is a helper function that validates that the shutdown has a
-// proper delivery script, and can be sent based on the current thaw height of
-// the channel.
-func validateShutdown(chanThawHeight fn.Option[uint32],
-	upfrontAddr fn.Option[lnwire.DeliveryAddress],
-	msg *ShutdownReceived, chanPoint wire.OutPoint,
-	chainParams chaincfg.Params) error {
-
-	// If we've received a shutdown message, and we have a thaw height,
-	// then we need to make sure that the channel can now be co-op closed.
-	err := fn.MapOptionZ(chanThawHeight, func(thawHeight uint32) error {
-		// If the current height is below the thaw height, then we'll
-		// reject the shutdown message as we can't yet co-op close the
-		// channel.
-		if msg.BlockHeight < thawHeight {
-			return fmt.Errorf("initiator attempting to "+
-				"co-op close frozen ChannelPoint(%v) "+
-				"(current_height=%v, thaw_height=%v)",
-				chanPoint, msg.BlockHeight,
-				thawHeight)
-		}
-
-		return nil
-	})
-	if err != nil {
-		return err
-	}
-
-	// Next, we'll verify that the remote party is sending the expected
-	// shutdown script.
-	return fn.MapOption(func(addr lnwire.DeliveryAddress) error {
-		return validateShutdownScript(
-			addr, msg.ShutdownScript, &chainParams,
-		)
-	})(upfrontAddr).UnwrapOr(nil)
-}
-
-// ProcessEvent takes a protocol event, and implements a state transition for
-// the state. From this state, we can receive two possible incoming events:
-// SendShutdown and ShutdownReceived. Both of these will transition us to the
-// ChannelFlushing state.
-func (c *ChannelActive) ProcessEvent(event ProtocolEvent, env *Environment,
-) (*CloseStateTransition, error) {
-
-	switch msg := event.(type) {
-	// If we get a confirmation, then a prior transaction we broadcasted
-	// has confirmed, so we can move to our terminal state early.
-	case *SpendEvent:
-		return &CloseStateTransition{
-			NextState: &CloseFin{
-				ConfirmedTx: msg.Tx,
-			},
-		}, nil
-
-	// If we receive the SendShutdown event, then we'll send our shutdown
-	// with a special SendPredicate, then go to the ShutdownPending where
-	// we'll wait for the remote to send their shutdown.
-	case *SendShutdown:
-		// If we have an upfront shutdown addr or a delivery addr then
-		// we'll use that. Otherwise, we'll generate a new delivery
-		// addr.
-		shutdownScript, err := env.LocalUpfrontShutdown.Alt(
-			msg.DeliveryAddr,
-		).UnwrapOrFuncErr(env.NewDeliveryScript)
-		if err != nil {
-			return nil, err
-		}
-
-		// We'll emit some daemon events to send the shutdown message
-		// and disable the channel on the network level. In this case,
-		// we don't need a post send event as receive their shutdown is
-		// what'll move us beyond the ShutdownPending state.
-		daemonEvents, err := sendShutdownEvents(
-			env.ChanID, env.ChanPoint, shutdownScript,
-			env.ChanPeer, fn.None[ProtocolEvent](),
-			env.ChanObserver,
-		)
-		if err != nil {
-			return nil, err
-		}
-
-		chancloserLog.Infof("ChannelPoint(%v): sending shutdown msg, "+
-			"delivery_script=%v", env.ChanPoint, shutdownScript)
-
-		// From here, we'll transition to the shutdown pending state. In
-		// this state we await their shutdown message (self loop), then
-		// also the flushing event.
-		return &CloseStateTransition{
-			NextState: &ShutdownPending{
-				IdealFeeRate: fn.Some(msg.IdealFeeRate),
-				ShutdownScripts: ShutdownScripts{
-					LocalDeliveryScript: shutdownScript,
-				},
-			},
-			NewEvents: fn.Some(RbfEvent{
-				ExternalEvents: daemonEvents,
-			}),
-		}, nil
-
-	// When we receive a shutdown from the remote party, we'll validate the
-	// shutdown message, then transition to the ShutdownPending state. We'll
-	// also emit similar events like the above to send out shutdown, and
-	// also disable the channel.
-	case *ShutdownReceived:
-		chancloserLog.Infof("ChannelPoint(%v): received shutdown msg")
-
-		// Validate that they can send the message now, and also that
-		// they haven't violated their commitment to a prior upfront
-		// shutdown addr.
-		err := validateShutdown(
-			env.ThawHeight, env.RemoteUpfrontShutdown, msg,
-			env.ChanPoint, env.ChainParams,
-		)
-		if err != nil {
-			chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
-				"shutdown attempt: %v", err)
-
-			return nil, err
-		}
-
-		// If we have an upfront shutdown addr we'll use that,
-		// otherwise, we'll generate a new delivery script.
-		shutdownAddr, err := env.LocalUpfrontShutdown.UnwrapOrFuncErr(
-			env.NewDeliveryScript,
-		)
-		if err != nil {
-			return nil, err
-		}
-
-		chancloserLog.Infof("ChannelPoint(%v): sending shutdown msg "+
-			"at next clean commit state", env.ChanPoint)
-
-		// Now that we know the shutdown message is valid, we'll obtain
-		// the set of daemon events we need to emit. We'll also specify
-		// that once the message has actually been sent, that we
-		// generate receive an input event of a ShutdownComplete.
-		daemonEvents, err := sendShutdownEvents(
-			env.ChanID, env.ChanPoint, shutdownAddr,
-			env.ChanPeer,
-			fn.Some[ProtocolEvent](&ShutdownComplete{}),
-			env.ChanObserver,
-		)
-		if err != nil {
-			return nil, err
-		}
-
-		chancloserLog.Infof("ChannelPoint(%v): disabling incoming adds",
-			env.ChanPoint)
-
-		// We just received a shutdown, so we'll disable the adds in
-		// the outgoing direction.
-		if err := env.ChanObserver.DisableIncomingAdds(); err != nil {
-			return nil, fmt.Errorf("unable to disable incoming "+
-				"adds: %w", err)
-		}
-
-		remoteAddr := msg.ShutdownScript
-
-		return &CloseStateTransition{
-			NextState: &ShutdownPending{
-				ShutdownScripts: ShutdownScripts{
-					LocalDeliveryScript:  shutdownAddr,
-					RemoteDeliveryScript: remoteAddr,
-				},
-			},
-			NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
-				ExternalEvents: daemonEvents,
-			}),
-		}, nil
-
-	// Any other messages in this state will result in an error, as this is
-	// an undefined state transition.
-	default:
-		return nil, fmt.Errorf("%w: received %T while in ChannelActive",
-			ErrInvalidStateTransition, msg)
-	}
-}
-
-// ProcessEvent takes a protocol event, and implements a state transition for
-// the state. Our path to this state will determine the set of valid events. If
-// we were the one that sent the shutdown, then we'll just wait on the
-// ShutdownReceived event. Otherwise, we received the shutdown, and can move
-// forward once we receive the ShutdownComplete event. Receiving
-// ShutdownComplete means that we've sent our shutdown, as this was specified
-// as a post send event.
-func (s *ShutdownPending) ProcessEvent(event ProtocolEvent, env *Environment,
-) (*CloseStateTransition, error) {
-
-	switch msg := event.(type) {
-	// If we get a confirmation, then a prior transaction we broadcasted
-	// has confirmed, so we can move to our terminal state early.
-	case *SpendEvent:
-		return &CloseStateTransition{
-			NextState: &CloseFin{
-				ConfirmedTx: msg.Tx,
-			},
-		}, nil
-
-	// When we receive a shutdown from the remote party, we'll validate the
-	// shutdown message, then transition to the ChannelFlushing state.
-	case *ShutdownReceived:
-		chancloserLog.Infof("ChannelPoint(%v): received shutdown msg",
-			env.ChanPoint)
-
-		// Validate that they can send the message now, and also that
-		// they haven't violated their commitment to a prior upfront
-		// shutdown addr.
-		err := validateShutdown(
-			env.ThawHeight, env.RemoteUpfrontShutdown, msg,
-			env.ChanPoint, env.ChainParams,
-		)
-		if err != nil {
-			chancloserLog.Errorf("ChannelPoint(%v): rejecting "+
-				"shutdown attempt: %v", err)
-
-			return nil, err
-		}
-
-		// If the channel is *already* flushed, and the close is
-		// go straight into negotiation, as this is the RBF loop.
-		// already in progress, then we can skip the flushing state and
-		var eventsToEmit fn.Option[protofsm.EmittedEvent[ProtocolEvent]]
-		finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
-			unknownBalance,
-		)
-		if finalBalances != unknownBalance {
-			channelFlushed := ProtocolEvent(&ChannelFlushed{
-				ShutdownBalances: finalBalances,
-			})
-			eventsToEmit = fn.Some(RbfEvent{
-				InternalEvent: []ProtocolEvent{
-					channelFlushed,
-				},
-			})
-		}
-
-		chancloserLog.Infof("ChannelPoint(%v): disabling incoming adds",
-			env.ChanPoint)
-
-		// We just received a shutdown, so we'll disable the adds in
-		// the outgoing direction.
-		if err := env.ChanObserver.DisableIncomingAdds(); err != nil {
-			return nil, fmt.Errorf("unable to disable incoming "+
-				"adds: %w", err)
-		}
-
-		chancloserLog.Infof("ChannelPoint(%v): waiting for channel to "+
-			"be flushed...", env.ChanPoint)
-
-		// We transition to the ChannelFlushing state, where we await
-		// the ChannelFlushed event.
-		return &CloseStateTransition{
-			NextState: &ChannelFlushing{
-				IdealFeeRate: s.IdealFeeRate,
-				ShutdownScripts: ShutdownScripts{
-					LocalDeliveryScript:  s.LocalDeliveryScript, //nolint:ll
-					RemoteDeliveryScript: msg.ShutdownScript,    //nolint:ll
-				},
-			},
-			NewEvents: eventsToEmit,
-		}, nil
-
-	// If we get this message, then this means that we were finally able to
-	// send out shutdown after receiving it from the remote party. We'll
-	// now transition directly to the ChannelFlushing state.
-	case *ShutdownComplete:
-		chancloserLog.Infof("ChannelPoint(%v): waiting for channel to "+
-			"be flushed...", env.ChanPoint)
-
-		// If the channel is *already* flushed, and the close is
-		// already in progress, then we can skip the flushing state and
-		// go straight into negotiation, as this is the RBF loop.
-		var eventsToEmit fn.Option[protofsm.EmittedEvent[ProtocolEvent]]
-		finalBalances := env.ChanObserver.FinalBalances().UnwrapOr(
-			unknownBalance,
-		)
-		if finalBalances != unknownBalance {
-			channelFlushed := ProtocolEvent(&ChannelFlushed{
-				ShutdownBalances: finalBalances,
-			})
-			eventsToEmit = fn.Some(RbfEvent{
-				InternalEvent: []ProtocolEvent{
-					channelFlushed,
-				},
-			})
-		}
-
-		// From here, we'll transition to the channel flushing state.
-		// We'll stay here until we receive the ChannelFlushed event.
-		return &CloseStateTransition{
-			NextState: &ChannelFlushing{
-				IdealFeeRate:    s.IdealFeeRate,
-				ShutdownScripts: s.ShutdownScripts,
-			},
-			NewEvents: eventsToEmit,
-		}, nil
-
-	// Any other messages in this state will result in an error, as this is
-	// an undefined state transition.
-	default:
-		return nil, fmt.Errorf("%w: received %T while in "+
-			"ShutdownPending", ErrInvalidStateTransition, msg)
-	}
-}
-
-// ProcessEvent takes a new protocol event, and figures out if we can
-// transition to the next state, or just loop back upon ourself. If we receive
-// a ShutdownReceived event, then we'll stay in the ChannelFlushing state, as
-// we haven't yet fully cleared the channel. Otherwise, we can move to the
-// CloseReady state which'll being the channel closing process.
-func (c *ChannelFlushing) ProcessEvent(event ProtocolEvent, env *Environment,
-) (*CloseStateTransition, error) {
-
-	switch msg := event.(type) {
-	// If we get a confirmation, then a prior transaction we broadcasted
-	// has confirmed, so we can move to our terminal state early.
-	case *SpendEvent:
-		return &CloseStateTransition{
-			NextState: &CloseFin{
-				ConfirmedTx: msg.Tx,
-			},
-		}, nil
-
-	// If we get an OfferReceived event, then the channel is flushed from
-	// the PoV of the remote party. However, due to propagation delay or
-	// concurrency, we may not have received the ChannelFlushed event yet.
-	// In this case, we'll stash the event and wait for the ChannelFlushed
-	// event.
-	case *OfferReceivedEvent:
-		chancloserLog.Infof("ChannelPoint(%v): received remote offer "+
-			"early, stashing...", env.ChanPoint)
-
-		c.EarlyRemoteOffer = fn.Some(*msg)
-
-		// TODO(roasbeef): unit test!
-		//  * actually do this ^
-
-		// We'll perform a noop update so we can wait for the actual
-		// channel flushed event.
-		return &CloseStateTransition{
-			NextState: c,
-		}, nil
-
-	// If we receive the ChannelFlushed event, then the coast is clear so
-	// we'll now morph into the dual peer state so we can handle any
-	// messages needed to drive forward the close process.
-	case *ChannelFlushed:
-		// Both the local and remote losing negotiation needs the terms
-		// we'll be using to close the channel, so we'll create them
-		// here.
-		closeTerms := CloseChannelTerms{
-			ShutdownScripts:  c.ShutdownScripts,
-			ShutdownBalances: msg.ShutdownBalances,
-		}
-
-		chancloserLog.Infof("ChannelPoint(%v): channel flushed! "+
-			"proceeding with co-op close", env.ChanPoint)
-
-		// Now that the channel has been flushed, we'll mark on disk
-		// that we're approaching the point of no return where we'll
-		// send a new signature to the remote party.
-		//
-		// TODO(roasbeef): doesn't actually matter if initiator here?
-		if msg.FreshFlush {
-			err := env.ChanObserver.MarkCoopBroadcasted(nil, true)
-			if err != nil {
-				return nil, err
-			}
-		}
-
-		// If an ideal fee rate was specified, then we'll use that,
-		// otherwise we'll fall back to the default value given in the
-		// env.
-		idealFeeRate := c.IdealFeeRate.UnwrapOr(env.DefaultFeeRate)
-
-		// We'll then use that fee rate to determine the absolute fee
-		// we'd propose.
-		//
-		// TODO(roasbeef): need to sign the 3 diff versions of this?
-		localTxOut, remoteTxOut := closeTerms.DeriveCloseTxOuts()
-		absoluteFee := env.FeeEstimator.EstimateFee(
-			env.ChanType, localTxOut, remoteTxOut,
-			idealFeeRate.FeePerKWeight(),
-		)
-
-		chancloserLog.Infof("ChannelPoint(%v): using ideal_fee=%v, "+
-			"absolute_fee=%v", env.ChanPoint, idealFeeRate,
-			absoluteFee)
-
-		var (
-			internalEvents []ProtocolEvent
-			newEvents      fn.Option[RbfEvent]
-		)
-
-		// If we received a remote offer early from the remote party,
-		// then we'll add that to the set of internal events to emit.
-		c.EarlyRemoteOffer.WhenSome(func(offer OfferReceivedEvent) {
-			internalEvents = append(internalEvents, &offer)
-		})
-
-		// Only if we have enough funds to pay for the fees do we need
-		// to emit a localOfferSign event.
-		//
-		// TODO(roasbeef): also only proceed if was higher than fee in
-		// last round?
-		if closeTerms.LocalCanPayFees(absoluteFee) {
-			// Each time we go into this negotiation flow, we'll
-			// kick off our local state with a new close attempt.
-			// So we'll emit a internal event to drive forward that
-			// part of the state.
-			localOfferSign := ProtocolEvent(&SendOfferEvent{
-				TargetFeeRate: idealFeeRate,
-			})
-			internalEvents = append(internalEvents, localOfferSign)
-		} else {
-			chancloserLog.Infof("ChannelPoint(%v): unable to pay "+
-				"fees with local balance, skipping "+
-				"closing_complete", env.ChanPoint)
-		}
-
-		if len(internalEvents) > 0 {
-			newEvents = fn.Some(RbfEvent{
-				InternalEvent: internalEvents,
-			})
-		}
-
-		return &CloseStateTransition{
-			NextState: &ClosingNegotiation{
-				PeerState: lntypes.Dual[AsymmetricPeerState]{
-					Local: &LocalCloseStart{
-						CloseChannelTerms: closeTerms,
-					},
-					Remote: &RemoteCloseStart{
-						CloseChannelTerms: closeTerms,
-					},
-				},
-			},
-			NewEvents: newEvents,
-		}, nil
-
-	default:
-		return nil, fmt.Errorf("%w: received %T while in "+
-			"ChannelFlushing", ErrInvalidStateTransition, msg)
-	}
-}
-
-// processNegotiateEvent is a helper function that processes a new event to
-// local channel state once we're in the ClosingNegotiation state.
-func processNegotiateEvent(c *ClosingNegotiation, event ProtocolEvent,
-	env *Environment, chanPeer lntypes.ChannelParty,
-) (*CloseStateTransition, error) {
-
-	targetPeerState := c.PeerState.GetForParty(chanPeer)
-
-	// Drive forward the remote state based on the next event.
-	transition, err := targetPeerState.ProcessEvent(
-		event, env,
-	)
-	if err != nil {
-		return nil, err
-	}
-
-	nextPeerState, ok := transition.NextState.(AsymmetricPeerState) //nolint:ll
-	if !ok {
-		return nil, fmt.Errorf("expected %T to be "+
-			"AsymmetricPeerState", transition.NextState)
-	}
-
-	// Make a copy of the input state, then update the peer state of the
-	// proper party.
-	newPeerState := *c
-	newPeerState.PeerState.SetForParty(chanPeer, nextPeerState)
-
-	return &CloseStateTransition{
-		NextState: &newPeerState,
-		NewEvents: transition.NewEvents,
-	}, nil
-}
-
-// ProcessEvent drives forward the composite states for the local and remote
-// party in response to new events. From this state, we'll continue to drive
-// forward the local and remote states until we arrive at the StateFin stage,
-// or we loop back up to the ShutdownPending state.
-func (c *ClosingNegotiation) ProcessEvent(event ProtocolEvent, env *Environment,
-) (*CloseStateTransition, error) {
-
-	// There're two classes of events that can break us out of this state:
-	// we receive a confirmation event, or we receive a signal to restart
-	// the co-op close process.
-	switch msg := event.(type) {
-	// If we get a confirmation, then the spend request we issued when we
-	// were leaving the ChannelFlushing state has been confirmed.  We'll
-	// now transition to the StateFin state.
-	case *SpendEvent:
-		return &CloseStateTransition{
-			NextState: &CloseFin{
-				ConfirmedTx: msg.Tx,
-			},
-		}, nil
-
-	// Otherwise, if we receive a shutdown, or receive an event to send a
-	// shutdown, then we'll go back up to the ChannelActive state, and have
-	// it handle this event by emitting an internal event.
-	//
-	// TODO(roasbeef): both will have fee rate specified, so ok?
-	case *ShutdownReceived, *SendShutdown:
-		chancloserLog.Infof("ChannelPoint(%v): RBF case triggered, "+
-			"restarting negotiation", env.ChanPoint)
-
-		return &CloseStateTransition{
-			NextState: &ChannelActive{},
-			NewEvents: fn.Some(RbfEvent{
-				InternalEvent: []ProtocolEvent{event},
-			}),
-		}, nil
-	}
-
-	// If we get to this point, then we have an event that'll drive forward
-	// the negotiation process.  Based on the event, we'll figure out which
-	// state we'll be modifying.
-	switch {
-	case c.PeerState.GetForParty(lntypes.Local).ShouldRouteTo(event):
-		chancloserLog.Infof("ChannelPoint(%v): routing %T to local "+
-			"chan state", env.ChanPoint, event)
-
-		// Drive forward the local state based on the next event.
-		return processNegotiateEvent(c, event, env, lntypes.Local)
-
-	case c.PeerState.GetForParty(lntypes.Remote).ShouldRouteTo(event):
-		chancloserLog.Infof("ChannelPoint(%v): routing %T to remote "+
-			"chan state", env.ChanPoint, event)
-
-		// Drive forward the remote state based on the next event.
-		return processNegotiateEvent(c, event, env, lntypes.Remote)
-	}
-
-	return nil, fmt.Errorf("%w: received %T while in ClosingNegotiation",
-		ErrInvalidStateTransition, event)
-}
-
-// newSigTlv is a helper function that returns a new optional TLV sig field for
-// the parametrized tlv.TlvType value.
-func newSigTlv[T tlv.TlvType](s lnwire.Sig) tlv.OptionalRecordT[T, lnwire.Sig] {
-	return tlv.SomeRecordT(tlv.NewRecordT[T](s))
-}
-
-// ProcessEvent implements the event processing to kick off the process of
-// obtaining a new (possibly RBF'd) signature for our commitment transaction.
-func (l *LocalCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
-) (*CloseStateTransition, error) {
-
-	switch msg := event.(type) { //nolint:gocritic
-	// If we receive a SendOfferEvent, then we'll use the specified fee
-	// rate to generate for the closing transaction with our ideal fee
-	// rate.
-	case *SendOfferEvent:
-		// First, we'll figure out the absolute fee rate we should pay
-		// given the state of the local/remote outputs.
-		localTxOut, remoteTxOut := l.DeriveCloseTxOuts()
-		absoluteFee := env.FeeEstimator.EstimateFee(
-			env.ChanType, localTxOut, remoteTxOut,
-			msg.TargetFeeRate.FeePerKWeight(),
-		)
-
-		// Now that we know what fee we want to pay, we'll create a new
-		// signature over our co-op close transaction. For our
-		// proposals, we'll just always use the known RBF sequence
-		// value.
-		localScript := l.LocalDeliveryScript
-		rawSig, closeTx, closeBalance, err := env.CloseSigner.CreateCloseProposal( //nolint:ll
-			absoluteFee, localScript, l.RemoteDeliveryScript,
-			lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
-			lnwallet.WithCustomPayer(lntypes.Local),
-		)
-		if err != nil {
-			return nil, err
-		}
-		wireSig, err := lnwire.NewSigFromSignature(rawSig)
-		if err != nil {
-			return nil, err
-		}
-
-		chancloserLog.Infof("closing w/ local_addr=%x, "+
-			"remote_addr=%x, fee=%v", localScript[:],
-			l.RemoteDeliveryScript[:], absoluteFee)
-
-		chancloserLog.Infof("proposing closing_tx=%v",
-			spew.Sdump(closeTx))
-
-		// Now that we have our signature, we'll set the proper
-		// closingSigs field based on if the remote party's output is
-		// dust or not.
-		var closingSigs lnwire.ClosingSigs
-		switch {
-		// If the remote party's output is dust, then we'll set the
-		// CloserNoClosee field.
-		case remoteTxOut == nil:
-			closingSigs.CloserNoClosee = newSigTlv[tlv.TlvType1](
-				wireSig,
-			)
-
-		// If after paying for fees, our balance is below dust, then
-		// we'll set the NoCloserClosee field.
-		case closeBalance < lnwallet.DustLimitForSize(len(localScript)):
-			closingSigs.NoCloserClosee = newSigTlv[tlv.TlvType2](
-				wireSig,
-			)
-
-		// Otherwise, we'll set the CloserAndClosee field.
-		//
-		// TODO(roasbeef): should actually set both??
-		default:
-			closingSigs.CloserAndClosee = newSigTlv[tlv.TlvType3](
-				wireSig,
-			)
-		}
-
-		// Now that we have our sig, we'll emit a daemon event to send
-		// it to the remote party, then transition to the
-		// LocalOfferSent state.
-		//
-		// TODO(roasbeef): type alias for protocol event
-		sendEvent := protofsm.DaemonEventSet{&protofsm.SendMsgEvent[ProtocolEvent]{ //nolint:ll
-			TargetPeer: env.ChanPeer,
-			// TODO(roasbeef): mew new func
-			Msgs: []lnwire.Message{&lnwire.ClosingComplete{
-				ChannelID:   env.ChanID,
-				FeeSatoshis: absoluteFee,
-				LockTime:    env.BlockHeight,
-				ClosingSigs: closingSigs,
-			}},
-		}}
-
-		chancloserLog.Infof("ChannelPoint(%v): sending closing sig "+
-			"to remote party, fee_sats=%v", env.ChanPoint,
-			absoluteFee)
-
-		return &CloseStateTransition{
-			NextState: &LocalOfferSent{
-				ProposedFee:       absoluteFee,
-				LocalSig:          wireSig,
-				CloseChannelTerms: l.CloseChannelTerms,
-			},
-			NewEvents: fn.Some(RbfEvent{
-				ExternalEvents: sendEvent,
-			}),
-		}, nil
-	}
-
-	return nil, fmt.Errorf("%w: received %T while in LocalCloseStart",
-		ErrInvalidStateTransition, event)
-}
-
-// extractSig extracts the expected signature from the closing sig message.
-// Only one of them should actually be populated as the closing sig message is
-// sent in response to a ClosingComplete message, it should only sign the same
-// version of the co-op close tx as the sender did.
-func extractSig(msg lnwire.ClosingSig) fn.Result[lnwire.Sig] {
-	// First, we'll validate that only one signature is included in their
-	// response to our initial offer. If not, then we'll exit here, and
-	// trigger a recycle of the connection.
-	sigInts := []bool{
-		msg.CloserNoClosee.IsSome(), msg.NoCloserClosee.IsSome(),
-		msg.CloserAndClosee.IsSome(),
-	}
-	numSigs := fn.Foldl(0, sigInts, func(acc int, sigInt bool) int {
-		if sigInt {
-			return acc + 1
-		}
-
-		return acc
-	})
-	if numSigs != 1 {
-		return fn.Errf[lnwire.Sig]("%w: only one sig should be set, "+
-			"got %v", ErrTooManySigs, numSigs)
-	}
-
-	// The final sig is the one that's actually set.
-	sig := msg.CloserAndClosee.ValOpt().Alt(
-		msg.NoCloserClosee.ValOpt(),
-	).Alt(
-		msg.CloserNoClosee.ValOpt(),
-	)
-
-	return fn.NewResult(sig.UnwrapOrErr(ErrNoSig))
-}
-
-// ProcessEvent implements the state transition function for the
-// LocalOfferSent state. In this state, we'll wait for the remote party to
-// send a close_signed message which gives us the ability to broadcast a new
-// co-op close transaction.
-func (l *LocalOfferSent) ProcessEvent(event ProtocolEvent, env *Environment,
-) (*CloseStateTransition, error) {
-
-	switch msg := event.(type) { //nolint:gocritic
-	// If we receive a LocalSigReceived event, then we'll attempt to
-	// validate the signature from the remote party. If valid, then we can
-	// broadcast the transaction, and transition to the ClosePending state.
-	case *LocalSigReceived:
-		// Extract and validate that only one sig field is set.
-		sig, err := extractSig(msg.SigMsg).Unpack()
-		if err != nil {
-			return nil, err
-		}
-
-		remoteSig, err := sig.ToSignature()
-		if err != nil {
-			return nil, err
-		}
-		localSig, err := l.LocalSig.ToSignature()
-		if err != nil {
-			return nil, err
-		}
-
-		// Now that we have their signature, we'll attempt to validate
-		// it, then extract a valid closing signature from it.
-		closeTx, _, err := env.CloseSigner.CompleteCooperativeClose(
-			localSig, remoteSig, l.LocalDeliveryScript,
-			l.RemoteDeliveryScript, l.ProposedFee,
-			lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
-			lnwallet.WithCustomPayer(lntypes.Local),
-		)
-		if err != nil {
-			return nil, err
-		}
-
-		// As we're about to broadcast a new version of the co-op close
-		// transaction, we'll mark again as broadcast, but with this
-		// variant of the co-op close tx.
-		err = env.ChanObserver.MarkCoopBroadcasted(closeTx, true)
-		if err != nil {
-			return nil, err
-		}
-
-		broadcastEvent := protofsm.DaemonEventSet{&protofsm.BroadcastTxn{ //nolint:ll
-			Tx: closeTx,
-			Label: labels.MakeLabel(
-				labels.LabelTypeChannelClose, &env.Scid,
-			),
-		}}
-
-		chancloserLog.Infof("ChannelPoint(%v): received sig from "+
-			"remote party, broadcasting: tx=%v", env.ChanPoint,
-			lnutils.SpewLogClosure(closeTx),
-		)
-
-		return &CloseStateTransition{
-			NextState: &ClosePending{
-				CloseTx: closeTx,
-			},
-			NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
-				ExternalEvents: broadcastEvent,
-			}),
-		}, nil
-	}
-
-	return nil, fmt.Errorf("%w: received %T while in LocalOfferSent",
-		ErrInvalidStateTransition, event)
-}
-
-// ProcessEvent implements the state transition function for the
-// RemoteCloseStart. In this state, we'll wait for the remote party to send a
-// closing_complete message. Assuming they can pay for the fees, we'll sign it
-// ourselves, then transition to the next state of ClosePending.
-func (l *RemoteCloseStart) ProcessEvent(event ProtocolEvent, env *Environment,
-) (*CloseStateTransition, error) {
-
-	switch msg := event.(type) { //nolint:gocritic
-	// If we receive a OfferReceived event, we'll make sure they can
-	// actually pay for the fee. If so, then we'll counter sign and
-	// transition to a terminal state.
-	case *OfferReceivedEvent:
-		// To start, we'll perform some basic validation of the sig
-		// message they've sent. We'll validate that the remote party
-		// actually has enough fees to pay the closing fees.
-		if !l.RemoteCanPayFees(msg.SigMsg.FeeSatoshis) {
-			return nil, fmt.Errorf("%w: %v vs %v",
-				ErrRemoteCannotPay,
-				msg.SigMsg.FeeSatoshis,
-				l.RemoteBalance.ToSatoshis())
-		}
-
-		// With the basic sanity checks out of the way, we'll now
-		// figure out which signature that we'll attempt to sign
-		// against.
-		var (
-			remoteSig input.Signature
-			noClosee  bool
-		)
-		switch {
-		// If our balance is dust, then we expect the CloserNoClosee
-		// sig to be set.
-		case l.LocalAmtIsDust():
-			if msg.SigMsg.CloserNoClosee.IsNone() {
-				return nil, ErrCloserNoClosee
-			}
-			msg.SigMsg.CloserNoClosee.WhenSomeV(func(s lnwire.Sig) {
-				remoteSig, _ = s.ToSignature()
-				noClosee = true
-			})
-
-		// Otherwise, we'll assume that CloseAndClosee is set.
-		//
-		// TODO(roasbeef): NoCloserClosee, but makes no sense?
-		default:
-			if msg.SigMsg.CloserAndClosee.IsNone() {
-				return nil, ErrCloserAndClosee
-			}
-			msg.SigMsg.CloserAndClosee.WhenSomeV(func(s lnwire.Sig) { //nolint:ll
-				remoteSig, _ = s.ToSignature()
-			})
-		}
-
-		chanOpts := []lnwallet.ChanCloseOpt{
-			lnwallet.WithCustomSequence(mempool.MaxRBFSequence),
-			lnwallet.WithCustomLockTime(msg.SigMsg.LockTime),
-			lnwallet.WithCustomPayer(lntypes.Remote),
-		}
-
-		chancloserLog.Infof("responding to close w/ local_addr=%x, "+
-			"remote_addr=%x, fee=%v",
-			l.LocalDeliveryScript[:], l.RemoteDeliveryScript[:],
-			msg.SigMsg.FeeSatoshis)
-
-		// Now that we have the remote sig, we'll sign the version they
-		// signed, then attempt to complete the cooperative close
-		// process.
-		//
-		// TODO(roasbeef): need to be able to omit an output when
-		// signing based on the above, as closing opt
-		rawSig, _, _, err := env.CloseSigner.CreateCloseProposal(
-			msg.SigMsg.FeeSatoshis, l.LocalDeliveryScript,
-			l.RemoteDeliveryScript, chanOpts...,
-		)
-		if err != nil {
-			return nil, err
-		}
-		wireSig, err := lnwire.NewSigFromSignature(rawSig)
-		if err != nil {
-			return nil, err
-		}
-
-		localSig, err := wireSig.ToSignature()
-		if err != nil {
-			return nil, err
-		}
-
-		// With our signature created, we'll now attempt to finalize the
-		// close process.
-		closeTx, _, err := env.CloseSigner.CompleteCooperativeClose(
-			localSig, remoteSig, l.LocalDeliveryScript,
-			l.RemoteDeliveryScript, msg.SigMsg.FeeSatoshis,
-			chanOpts...,
-		)
-		if err != nil {
-			return nil, err
-		}
-
-		chancloserLog.Infof("ChannelPoint(%v): received sig (fee=%v "+
-			"sats) from remote party, signing new tx=%v",
-			env.ChanPoint, msg.SigMsg.FeeSatoshis,
-			lnutils.SpewLogClosure(closeTx),
-		)
-
-		var closingSigs lnwire.ClosingSigs
-		if noClosee {
-			closingSigs.CloserNoClosee = newSigTlv[tlv.TlvType1](
-				wireSig,
-			)
-		} else {
-			closingSigs.CloserAndClosee = newSigTlv[tlv.TlvType3](
-				wireSig,
-			)
-		}
-
-		// As we're about to broadcast a new version of the co-op close
-		// transaction, we'll mark again as broadcast, but with this
-		// variant of the co-op close tx.
-		//
-		// TODO(roasbeef): db will only store one instance, store both?
-		err = env.ChanObserver.MarkCoopBroadcasted(closeTx, false)
-		if err != nil {
-			return nil, err
-		}
-
-		// As we transition, we'll omit two events: one to broadcast
-		// the transaction, and the other to send our ClosingSig
-		// message to the remote party.
-		sendEvent := &protofsm.SendMsgEvent[ProtocolEvent]{
-			TargetPeer: env.ChanPeer,
-			Msgs: []lnwire.Message{&lnwire.ClosingSig{
-				ChannelID:   env.ChanID,
-				ClosingSigs: closingSigs,
-			}},
-		}
-		broadcastEvent := &protofsm.BroadcastTxn{
-			Tx: closeTx,
-			Label: labels.MakeLabel(
-				labels.LabelTypeChannelClose, &env.Scid,
-			),
-		}
-		daemonEvents := protofsm.DaemonEventSet{
-			sendEvent, broadcastEvent,
-		}
-
-		// Now that we've extracted the signature, we'll transition to
-		// the next state where we'll sign+broadcast the sig.
-		return &CloseStateTransition{
-			NextState: &ClosePending{
-				CloseTx: closeTx,
-			},
-			NewEvents: fn.Some(protofsm.EmittedEvent[ProtocolEvent]{
-				ExternalEvents: daemonEvents,
-			}),
-		}, nil
-	}
-
-	return nil, fmt.Errorf("%w: received %T while in RemoteCloseStart",
-		ErrInvalidStateTransition, event)
-}
-
-// ProcessEvent is a semi-terminal state in the rbf-coop close state machine.
-// In this state, we're waiting for either a confirmation, or for either side
-// to attempt to create a new RBF'd co-op close transaction.
-func (c *ClosePending) ProcessEvent(event ProtocolEvent, env *Environment,
-) (*CloseStateTransition, error) {
-
-	switch msg := event.(type) {
-	// If we can a spend while waiting for the close, then we'll go to our
-	// terminal state.
-	case *SpendEvent:
-		return &CloseStateTransition{
-			NextState: &CloseFin{
-				ConfirmedTx: msg.Tx,
-			},
-		}, nil
-
-	default:
-
-		return &CloseStateTransition{
-			NextState: c,
-		}, nil
-	}
-}
-
-// ProcessEvent is the event processing for out terminal state. In this state,
-// we just keep looping back on ourselves.
-func (c *CloseFin) ProcessEvent(event ProtocolEvent, env *Environment,
-) (*CloseStateTransition, error) {
-
-	return &CloseStateTransition{
-		NextState: c,
-	}, nil
-}
diff --git a/lnwallet/channel.go b/lnwallet/channel.go
index dc45a12be..d190acdf5 100644
--- a/lnwallet/channel.go
+++ b/lnwallet/channel.go
@@ -8199,12 +8199,6 @@ type chanCloseOpt struct {
 	// transaction outputs. If this isn't set, then the default BIP-69
 	// sorting is used.
 	customSort CloseSortFunc
-
-	customSequence fn.Option[uint32]
-
-	customLockTime fn.Option[uint32]
-
-	customPayer fn.Option[lntypes.ChannelParty]
 }
 
 // ChanCloseOpt is a closure type that cen be used to modify the set of default
@@ -8241,31 +8235,6 @@ func WithCustomCoopSort(sorter CloseSortFunc) ChanCloseOpt {
 	}
 }
 
-// WithCustomSequence can be used to specify a custom sequence number for the
-// co-op close process. Otherwise, a default non-final sequence will be used.
-func WithCustomSequence(sequence uint32) ChanCloseOpt {
-	return func(opts *chanCloseOpt) {
-		opts.customSequence = fn.Some(sequence)
-	}
-}
-
-// WithCustomLockTime can be used to specify a custom lock time for the coop
-// close transaction.
-func WithCustomLockTime(lockTime uint32) ChanCloseOpt {
-	return func(opts *chanCloseOpt) {
-		opts.customLockTime = fn.Some(lockTime)
-	}
-}
-
-// WithCustomPayer can be used to specify a custom payer for the closing
-// transaction. This overrides the default payer, which is the initiator of the
-// channel.
-func WithCustomPayer(payer lntypes.ChannelParty) ChanCloseOpt {
-	return func(opts *chanCloseOpt) {
-		opts.customPayer = fn.Some(payer)
-	}
-}
-
 // CreateCloseProposal is used by both parties in a cooperative channel close
 // workflow to generate proposed close transactions and signatures. This method
 // should only be executed once all pending HTLCs (if any) on the channel have
@@ -8275,23 +8244,22 @@ func WithCustomPayer(payer lntypes.ChannelParty) ChanCloseOpt {
 // returned.
 func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
 	localDeliveryScript []byte, remoteDeliveryScript []byte,
-	closeOpts ...ChanCloseOpt) (input.Signature, *wire.MsgTx,
+	closeOpts ...ChanCloseOpt) (input.Signature, *chainhash.Hash,
 	btcutil.Amount, error) {
 
 	lc.Lock()
 	defer lc.Unlock()
 
+	// If we're already closing the channel, then ignore this request.
+	if lc.isClosed {
+		return nil, nil, 0, ErrChanClosing
+	}
+
 	opts := defaultCloseOpts()
 	for _, optFunc := range closeOpts {
 		optFunc(opts)
 	}
 
-	// Unless there's a custom payer (sign of the RBF flow), if we're
-	// already closing the channel, then ignore this request.
-	if lc.isClosed && opts.customPayer.IsNone() {
-		return nil, nil, 0, ErrChanClosing
-	}
-
 	// Get the final balances after subtracting the proposed fee, taking
 	// care not to persist the adjusted balance, as the feeRate may change
 	// during the channel closing process.
@@ -8301,7 +8269,6 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
 		lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
 		lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
 		lc.channelState.LocalCommitment.CommitFee,
-		opts.customPayer,
 	)
 	if err != nil {
 		return nil, nil, 0, err
@@ -8327,18 +8294,6 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
 		)
 	}
 
-	opts.customSequence.WhenSome(func(sequence uint32) {
-		closeTxOpts = append(closeTxOpts, WithCustomTxInSequence(
-			sequence,
-		))
-	})
-
-	opts.customLockTime.WhenSome(func(lockTime uint32) {
-		closeTxOpts = append(closeTxOpts, WithCustomTxLockTime(
-			lockTime,
-		))
-	})
-
 	closeTx, err := CreateCooperativeCloseTx(
 		fundingTxIn(lc.channelState), lc.channelState.LocalChanCfg.DustLimit,
 		lc.channelState.RemoteChanCfg.DustLimit, ourBalance, theirBalance,
@@ -8377,8 +8332,8 @@ func (lc *LightningChannel) CreateCloseProposal(proposedFee btcutil.Amount,
 		}
 	}
 
-	return sig, closeTx, ourBalance, nil
-
+	closeTXID := closeTx.TxHash()
+	return sig, &closeTXID, ourBalance, nil
 }
 
 // CompleteCooperativeClose completes the cooperative closure of the target
@@ -8397,17 +8352,17 @@ func (lc *LightningChannel) CompleteCooperativeClose(
 	lc.Lock()
 	defer lc.Unlock()
 
+	// If the channel is already closing, then ignore this request.
+	if lc.isClosed {
+		// TODO(roasbeef): check to ensure no pending payments
+		return nil, 0, ErrChanClosing
+	}
+
 	opts := defaultCloseOpts()
 	for _, optFunc := range closeOpts {
 		optFunc(opts)
 	}
 
-	// Unless there's a custom payer (sign of the RBF flow), if we're
-	// already closing the channel, then ignore this request.
-	if lc.isClosed && opts.customPayer.IsNone() {
-		return nil, 0, ErrChanClosing
-	}
-
 	// Get the final balances after subtracting the proposed fee.
 	ourBalance, theirBalance, err := CoopCloseBalance(
 		lc.channelState.ChanType, lc.channelState.IsInitiator,
@@ -8415,7 +8370,6 @@ func (lc *LightningChannel) CompleteCooperativeClose(
 		lc.channelState.LocalCommitment.LocalBalance.ToSatoshis(),
 		lc.channelState.LocalCommitment.RemoteBalance.ToSatoshis(),
 		lc.channelState.LocalCommitment.CommitFee,
-		opts.customPayer,
 	)
 	if err != nil {
 		return nil, 0, err
@@ -8441,18 +8395,6 @@ func (lc *LightningChannel) CompleteCooperativeClose(
 		)
 	}
 
-	opts.customSequence.WhenSome(func(sequence uint32) {
-		closeTxOpts = append(closeTxOpts, WithCustomTxInSequence(
-			sequence,
-		))
-	})
-
-	opts.customLockTime.WhenSome(func(lockTime uint32) {
-		closeTxOpts = append(closeTxOpts, WithCustomTxLockTime(
-			lockTime,
-		))
-	})
-
 	// Create the transaction used to return the current settled balance
 	// on this active channel back to both parties. In this current model,
 	// the initiator pays full fees for the cooperative close transaction.
@@ -9169,13 +9111,6 @@ type closeTxOpts struct {
 	// transaction outputs. If this isn't set, then the default BIP-69
 	// sorting is used.
 	customSort CloseSortFunc
-
-	// customSequence is an optional custom sequence to set on the co-op
-	// close transaction. This gives slightly more control compared to the
-	// enableRBF option.
-	customSequence fn.Option[uint32]
-
-	customLockTime fn.Option[uint32]
 }
 
 // defaultCloseTxOpts returns a closeTxOpts struct with default values.
@@ -9212,20 +9147,6 @@ func WithCustomTxSort(sorter CloseSortFunc) CloseTxOpt {
 	}
 }
 
-// WithCustomTxInSequence allows a caller to set a custom sequence on the sole
-// input of the co-op close tx.
-func WithCustomTxInSequence(sequence uint32) CloseTxOpt {
-	return func(o *closeTxOpts) {
-		o.customSequence = fn.Some(sequence)
-	}
-}
-
-func WithCustomTxLockTime(lockTime uint32) CloseTxOpt {
-	return func(o *closeTxOpts) {
-		o.customLockTime = fn.Some(lockTime)
-	}
-}
-
 // CreateCooperativeCloseTx creates a transaction which if signed by both
 // parties, then broadcast cooperatively closes an active channel. The creation
 // of the closure transaction is modified by a boolean indicating if the party
@@ -9248,11 +9169,6 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
 		fundingTxIn.Sequence = mempool.MaxRBFSequence
 	}
 
-	// Otherwise, a custom sequence might be specified.
-	opts.customSequence.WhenSome(func(sequence uint32) {
-		fundingTxIn.Sequence = sequence
-	})
-
 	// Construct the transaction to perform a cooperative closure of the
 	// channel. In the event that one side doesn't have any settled funds
 	// within the channel then a refund output for that particular side can
@@ -9260,12 +9176,6 @@ func CreateCooperativeCloseTx(fundingTxIn wire.TxIn,
 	closeTx := wire.NewMsgTx(2)
 	closeTx.AddTxIn(&fundingTxIn)
 
-	opts.customLockTime.WhenSome(func(lockTime uint32) {
-		closeTx.LockTime = lockTime
-	})
-
-	// TODO(roasbeef): needs support for dropping inputs
-
 	// Create both cooperative closure outputs, properly respecting the
 	// dust limits of both parties.
 	var localOutputIdx fn.Option[int]
diff --git a/lnwallet/commitment.go b/lnwallet/commitment.go
index ab20d9afa..787e8a71e 100644
--- a/lnwallet/commitment.go
+++ b/lnwallet/commitment.go
@@ -1033,9 +1033,8 @@ func CreateCommitTx(chanType channeldb.ChannelType,
 // CoopCloseBalance returns the final balances that should be used to create
 // the cooperative close tx, given the channel type and transaction fee.
 func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
-	coopCloseFee, ourBalance, theirBalance, commitFee btcutil.Amount,
-	feePayer fn.Option[lntypes.ChannelParty],
-) (btcutil.Amount, btcutil.Amount, error) {
+	coopCloseFee, ourBalance, theirBalance,
+	commitFee btcutil.Amount) (btcutil.Amount, btcutil.Amount, error) {
 
 	// We'll make sure we account for the complete balance by adding the
 	// current dangling commitment fee to the balance of the initiator.
@@ -1047,34 +1046,16 @@ func CoopCloseBalance(chanType channeldb.ChannelType, isInitiator bool,
 		initiatorDelta += 2 * AnchorSize
 	}
 
-	// To start with, we'll add the anchor and/or commitment fee to the
-	// balance of the initiator.
+	// The initiator will pay the full coop close fee, subtract that value
+	// from their balance.
+	initiatorDelta -= coopCloseFee
+
 	if isInitiator {
 		ourBalance += initiatorDelta
 	} else {
 		theirBalance += initiatorDelta
 	}
 
-	// With the initiator's balance credited, we'll now subtract the closing
-	// fee from the closing party. By default, the initiator pays the full
-	// amount, but this can be overridden by the feePayer option.
-	defaultPayer := func() lntypes.ChannelParty {
-		if isInitiator {
-			return lntypes.Local
-		}
-
-		return lntypes.Remote
-	}()
-	payer := feePayer.UnwrapOr(defaultPayer)
-
-	// Based on the payer computed above, we'll subtract the closing fee.
-	switch payer {
-	case lntypes.Local:
-		ourBalance -= coopCloseFee
-	case lntypes.Remote:
-		theirBalance -= coopCloseFee
-	}
-
 	// During fee negotiation it should always be verified that the
 	// initiator can pay the proposed fee, but we do a sanity check just to
 	// be sure here.
diff --git a/lnwire/closing_complete.go b/lnwire/closing_complete.go
index c16760a4e..d33abf672 100644
--- a/lnwire/closing_complete.go
+++ b/lnwire/closing_complete.go
@@ -34,9 +34,9 @@ type ClosingComplete struct {
 	// channel would like to propose for the close transaction.
 	FeeSatoshis btcutil.Amount
 
-	// LockTime is the locktime number to be used in the input spending the
+	// Sequence is the sequence number to be used in the input spending the
 	// funding transaction.
-	LockTime uint32
+	Sequence uint32
 
 	// ClosingSigs houses the 3 possible signatures that can be sent.
 	ClosingSigs
@@ -79,7 +79,7 @@ func decodeClosingSigs(c *ClosingSigs, tlvRecords ExtraOpaqueData) error {
 // passed io.Reader.
 func (c *ClosingComplete) Decode(r io.Reader, _ uint32) error {
 	// First, read out all the fields that are hard coded into the message.
-	err := ReadElements(r, &c.ChannelID, &c.FeeSatoshis, &c.LockTime)
+	err := ReadElements(r, &c.ChannelID, &c.FeeSatoshis, &c.Sequence)
 	if err != nil {
 		return err
 	}
@@ -129,7 +129,7 @@ func (c *ClosingComplete) Encode(w *bytes.Buffer, _ uint32) error {
 		return err
 	}
 
-	if err := WriteUint32(w, c.LockTime); err != nil {
+	if err := WriteUint32(w, c.Sequence); err != nil {
 		return err
 	}
 
diff --git a/make/release_flags.mk b/make/release_flags.mk
index 8cfe703bd..92e08c3ba 100644
--- a/make/release_flags.mk
+++ b/make/release_flags.mk
@@ -11,7 +11,6 @@ DOCKER_RELEASE_HELPER = docker run \
   -e SKIP_VERSION_CHECK \
   lnd-release-helper
 
-# Please keep this list in sync with .github/workflows/main.yml!
 BUILD_SYSTEM = darwin-amd64 \
 darwin-arm64 \
 freebsd-386 \
diff --git a/peer/brontide.go b/peer/brontide.go
index f0399b4e8..a41b5080c 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -2275,13 +2275,6 @@ func messageSummary(msg lnwire.Message) string {
 		return fmt.Sprintf("chan_id=%v, script=%x", msg.ChannelID,
 			msg.Address[:])
 
-	case *lnwire.ClosingComplete:
-		return fmt.Sprintf("chan_id=%v, fee_sat=%v, locktime=%v",
-			msg.ChannelID, msg.FeeSatoshis, msg.LockTime)
-
-	case *lnwire.ClosingSig:
-		return fmt.Sprintf("chan_id=%v", msg.ChannelID)
-
 	case *lnwire.ClosingSigned:
 		return fmt.Sprintf("chan_id=%v, fee_sat=%v", msg.ChannelID,
 			msg.FeeSatoshis)
diff --git a/protofsm/state_machine.go b/protofsm/state_machine.go
index 2cc121902..a81f5746b 100644
--- a/protofsm/state_machine.go
+++ b/protofsm/state_machine.go
@@ -34,11 +34,11 @@ 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 []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 DaemonEventSet
+	ExternalEvents fn.Option[DaemonEventSet]
 }
 
 // StateTransition is a state transition type. It denotes the next state to go
@@ -573,31 +573,46 @@ func (s *StateMachine[Event, Env]) applyEvents(currentState State[Event, Env],
 				// With the event processed, we'll process any
 				// new daemon events that were emitted as part
 				// of this new state transition.
-				for _, dEvent := range events.ExternalEvents {
-					err := s.executeDaemonEvent(
-						dEvent,
-					)
-					if err != nil {
-						return err
+				//
+				//nolint:ll
+				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.
+				// Next, we'll add any new emitted events to
+				// our event queue.
 				//
 				//nolint:ll
-				for _, inEvent := range events.InternalEvent {
-					log.Debugf("FSM(%v): adding "+
-						"new internal event "+
-						"to queue: %v",
-						s.cfg.Env.Name(),
-						lnutils.SpewLogClosure(
-							inEvent,
-						),
-					)
-
-					eventQueue.Enqueue(inEvent)
-				}
+				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
 			})
diff --git a/routing/blinding.go b/routing/blinding.go
index a343c15da..0c27e8743 100644
--- a/routing/blinding.go
+++ b/routing/blinding.go
@@ -1,27 +1,17 @@
 package routing
 
 import (
-	"bytes"
 	"errors"
 	"fmt"
 
 	"github.com/btcsuite/btcd/btcec/v2"
-	"github.com/decred/dcrd/dcrec/secp256k1/v4"
 	sphinx "github.com/lightningnetwork/lightning-onion"
+	"github.com/lightningnetwork/lnd/fn/v2"
 	"github.com/lightningnetwork/lnd/graph/db/models"
-	"github.com/lightningnetwork/lnd/input"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/routing/route"
 )
 
-// BlindedPathNUMSHex is the hex encoded version of the blinded path target
-// NUMs key (in compressed format) which has no known private key.
-// This was generated using the following script:
-// https://github.com/lightninglabs/lightning-node-connect/tree/master/
-// mailbox/numsgen, with the seed phrase "Lightning Blinded Path".
-const BlindedPathNUMSHex = "02667a98ef82ecb522f803b17a74f14508a48b25258f9831" +
-	"dd6e95f5e299dfd54e"
-
 var (
 	// ErrNoBlindedPath is returned when the blinded path in a blinded
 	// payment is missing.
@@ -35,14 +25,6 @@ var (
 	// ErrHTLCRestrictions is returned when a blinded path has invalid
 	// HTLC maximum and minimum values.
 	ErrHTLCRestrictions = errors.New("invalid htlc minimum and maximum")
-
-	// BlindedPathNUMSKey is a NUMS key (nothing up my sleeves number) that
-	// has no known private key.
-	BlindedPathNUMSKey = input.MustParsePubKey(BlindedPathNUMSHex)
-
-	// CompressedBlindedPathNUMSKey is the compressed version of the
-	// BlindedPathNUMSKey.
-	CompressedBlindedPathNUMSKey = BlindedPathNUMSKey.SerializeCompressed()
 )
 
 // BlindedPaymentPathSet groups the data we need to handle sending to a set of
@@ -88,9 +70,7 @@ type BlindedPaymentPathSet struct {
 }
 
 // NewBlindedPaymentPathSet constructs a new BlindedPaymentPathSet from a set of
-// BlindedPayments. For blinded paths which have more than one single hop a
-// dummy hop via a NUMS key is appeneded to allow for MPP path finding via
-// multiple blinded paths.
+// BlindedPayments.
 func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
 	error) {
 
@@ -123,53 +103,36 @@ func NewBlindedPaymentPathSet(paths []*BlindedPayment) (*BlindedPaymentPathSet,
 		}
 	}
 
-	// Deep copy the paths to avoid mutating the original paths.
-	pathSet := make([]*BlindedPayment, len(paths))
-	for i, path := range paths {
-		pathSet[i] = path.deepCopy()
+	// Derive an ephemeral target priv key that will be injected into each
+	// blinded path final hop.
+	targetPriv, err := btcec.NewPrivateKey()
+	if err != nil {
+		return nil, err
 	}
+	targetPub := targetPriv.PubKey()
 
-	// For blinded paths we use the NUMS key as a target if the blinded
-	// path has more hops than just the introduction node.
-	targetPub := &BlindedPathNUMSKey
-
-	var finalCLTVDelta uint16
-
-	// In case the paths do NOT include a single hop route we append a
-	// dummy hop via a NUMS key to allow for MPP path finding via multiple
-	// blinded paths. A unified target is needed to use all blinded paths
-	// during the payment lifecycle. A dummy hop is solely added for the
-	// path finding process and is removed after the path is found. This
-	// ensures that we still populate the mission control with the correct
-	// data and also respect these mc entries when looking for a path.
-	for _, path := range pathSet {
-		pathLength := len(path.BlindedPath.BlindedHops)
-
-		// If any provided blinded path only has a single hop (ie, the
-		// destination node is also the introduction node), then we
-		// discard all other paths since we know the real pub key of the
-		// destination node. We also then set the final CLTV delta to
-		// the path's delta since there are no other edge hints that
-		// will account for it.
-		if pathLength == 1 {
-			pathSet = []*BlindedPayment{path}
-			finalCLTVDelta = path.CltvExpiryDelta
-			targetPub = path.BlindedPath.IntroductionPoint
-
-			break
+	var (
+		pathSet        = paths
+		finalCLTVDelta uint16
+	)
+	// If any provided blinded path only has a single hop (ie, the
+	// destination node is also the introduction node), then we discard all
+	// other paths since we know the real pub key of the destination node.
+	// We also then set the final CLTV delta to the path's delta since
+	// there are no other edge hints that will account for it. For a single
+	// hop path, there is also no need for the pseudo target pub key
+	// replacement, so our target pub key in this case just remains the
+	// real introduction node ID.
+	for _, path := range paths {
+		if len(path.BlindedPath.BlindedHops) != 1 {
+			continue
 		}
 
-		lastHop := path.BlindedPath.BlindedHops[pathLength-1]
-		path.BlindedPath.BlindedHops = append(
-			path.BlindedPath.BlindedHops,
-			&sphinx.BlindedHopInfo{
-				BlindedNodePub: &BlindedPathNUMSKey,
-				// We add the last hop's cipher text so that
-				// the payload size of the final hop is equal
-				// to the real last hop.
-				CipherText: lastHop.CipherText,
-			},
-		)
+		pathSet = []*BlindedPayment{path}
+		finalCLTVDelta = path.CltvExpiryDelta
+		targetPub = path.BlindedPath.IntroductionPoint
+
+		break
 	}
 
 	return &BlindedPaymentPathSet{
@@ -235,33 +198,21 @@ func (s *BlindedPaymentPathSet) FinalCLTVDelta() uint16 {
 // LargestLastHopPayloadPath returns the BlindedPayment in the set that has the
 // largest last-hop payload. This is to be used for onion size estimation in
 // path finding.
-func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() (*BlindedPayment,
-	error) {
-
+func (s *BlindedPaymentPathSet) LargestLastHopPayloadPath() *BlindedPayment {
 	var (
 		largestPath *BlindedPayment
 		currentMax  int
 	)
-
-	if len(s.paths) == 0 {
-		return nil, fmt.Errorf("no blinded paths in the set")
-	}
-
-	// We set the largest path to make sure we always return a path even
-	// if the cipher text is empty.
-	largestPath = s.paths[0]
-
 	for _, path := range s.paths {
 		numHops := len(path.BlindedPath.BlindedHops)
 		lastHop := path.BlindedPath.BlindedHops[numHops-1]
 
 		if len(lastHop.CipherText) > currentMax {
 			largestPath = path
-			currentMax = len(lastHop.CipherText)
 		}
 	}
 
-	return largestPath, nil
+	return largestPath
 }
 
 // ToRouteHints converts the blinded path payment set into a RouteHints map so
@@ -271,7 +222,7 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
 	hints := make(RouteHints)
 
 	for _, path := range s.paths {
-		pathHints, err := path.toRouteHints()
+		pathHints, err := path.toRouteHints(fn.Some(s.targetPubKey))
 		if err != nil {
 			return nil, err
 		}
@@ -288,12 +239,6 @@ func (s *BlindedPaymentPathSet) ToRouteHints() (RouteHints, error) {
 	return hints, nil
 }
 
-// IsBlindedRouteNUMSTargetKey returns true if the given public key is the
-// NUMS key used as a target for blinded path final hops.
-func IsBlindedRouteNUMSTargetKey(pk []byte) bool {
-	return bytes.Equal(pk, CompressedBlindedPathNUMSKey)
-}
-
 // BlindedPayment provides the path and payment parameters required to send a
 // payment along a blinded path.
 type BlindedPayment struct {
@@ -346,22 +291,6 @@ func (b *BlindedPayment) Validate() error {
 			b.HtlcMaximum, b.HtlcMinimum)
 	}
 
-	for _, hop := range b.BlindedPath.BlindedHops {
-		// The first hop of the blinded path does not necessarily have
-		// blinded node pub key because it is the introduction point.
-		if hop.BlindedNodePub == nil {
-			continue
-		}
-
-		if IsBlindedRouteNUMSTargetKey(
-			hop.BlindedNodePub.SerializeCompressed(),
-		) {
-
-			return fmt.Errorf("blinded path cannot include NUMS "+
-				"key: %s", BlindedPathNUMSHex)
-		}
-	}
-
 	return nil
 }
 
@@ -372,8 +301,11 @@ func (b *BlindedPayment) Validate() error {
 // effectively the final_cltv_delta for the receiving introduction node). In
 // the case of multiple blinded hops, CLTV delta is fully accounted for in the
 // hints (both for intermediate hops and the final_cltv_delta for the receiving
-// node).
-func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
+// node). The pseudoTarget, if provided,  will be used to override the pub key
+// of the destination node in the path.
+func (b *BlindedPayment) toRouteHints(
+	pseudoTarget fn.Option[*btcec.PublicKey]) (RouteHints, error) {
+
 	// If we just have a single hop in our blinded route, it just contains
 	// an introduction node (this is a valid path according to the spec).
 	// Since we have the un-blinded node ID for the introduction node, we
@@ -461,77 +393,16 @@ func (b *BlindedPayment) toRouteHints() (RouteHints, error) {
 		hints[fromNode] = []AdditionalEdge{lastEdge}
 	}
 
-	return hints, nil
-}
-
-// deepCopy returns a deep copy of the BlindedPayment.
-func (b *BlindedPayment) deepCopy() *BlindedPayment {
-	if b == nil {
-		return nil
-	}
-
-	cpyPayment := &BlindedPayment{
-		BaseFee:             b.BaseFee,
-		ProportionalFeeRate: b.ProportionalFeeRate,
-		CltvExpiryDelta:     b.CltvExpiryDelta,
-		HtlcMinimum:         b.HtlcMinimum,
-		HtlcMaximum:         b.HtlcMaximum,
-	}
-
-	// Deep copy the BlindedPath if it exists
-	if b.BlindedPath != nil {
-		cpyPayment.BlindedPath = &sphinx.BlindedPath{
-			BlindedHops: make([]*sphinx.BlindedHopInfo,
-				len(b.BlindedPath.BlindedHops)),
-		}
-
-		if b.BlindedPath.IntroductionPoint != nil {
-			cpyPayment.BlindedPath.IntroductionPoint =
-				copyPublicKey(b.BlindedPath.IntroductionPoint)
+	pseudoTarget.WhenSome(func(key *btcec.PublicKey) {
+		// For the very last hop on the path, switch out the ToNodePub
+		// for the pseudo target pub key.
+		lastEdge.policy.ToNodePubKey = func() route.Vertex {
+			return route.NewVertex(key)
 		}
 
-		if b.BlindedPath.BlindingPoint != nil {
-			cpyPayment.BlindedPath.BlindingPoint =
-				copyPublicKey(b.BlindedPath.BlindingPoint)
-		}
-
-		// Copy each blinded hop info.
-		for i, hop := range b.BlindedPath.BlindedHops {
-			if hop == nil {
-				continue
-			}
-
-			cpyHop := &sphinx.BlindedHopInfo{
-				CipherText: hop.CipherText,
-			}
-
-			if hop.BlindedNodePub != nil {
-				cpyHop.BlindedNodePub =
-					copyPublicKey(hop.BlindedNodePub)
-			}
-
-			cpyHop.CipherText = make([]byte, len(hop.CipherText))
-			copy(cpyHop.CipherText, hop.CipherText)
-
-			cpyPayment.BlindedPath.BlindedHops[i] = cpyHop
-		}
-	}
-
-	// Deep copy the Features if they exist
-	if b.Features != nil {
-		cpyPayment.Features = b.Features.Clone()
-	}
-
-	return cpyPayment
-}
-
-// copyPublicKey makes a deep copy of a public key.
-//
-// TODO(ziggie): Remove this function if this is available in the btcec library.
-func copyPublicKey(pk *btcec.PublicKey) *btcec.PublicKey {
-	var result secp256k1.JacobianPoint
-	pk.AsJacobian(&result)
-	result.ToAffine()
+		// Then override the final hint with this updated edge.
+		hints[fromNode] = []AdditionalEdge{lastEdge}
+	})
 
-	return btcec.NewPublicKey(&result.X, &result.Y)
+	return hints, nil
 }
diff --git a/routing/pathfind.go b/routing/pathfind.go
index 3f1d0ba09..80f4b1e68 100644
--- a/routing/pathfind.go
+++ b/routing/pathfind.go
@@ -158,32 +158,6 @@ func newRoute(sourceVertex route.Vertex,
 	)
 
 	pathLength := len(pathEdges)
-
-	// When paying to a blinded route we might have appended a dummy hop at
-	// the end to make MPP payments possible via all paths of the blinded
-	// route set. We always append a dummy hop when the internal pathfiner
-	// looks for a route to a blinded path which is at least one hop long
-	// (excluding the introduction point). We add this dummy hop so that
-	// we search for a universal target but also respect potential mc
-	// entries which might already be present for a particular blinded path.
-	// However when constructing the Sphinx packet we need to remove this
-	// dummy hop again which we do here.
-	//
-	// NOTE: The path length is always at least 1 because there must be one
-	// edge from the source to the destination. However we check for > 0
-	// just for robustness here.
-	if blindedPathSet != nil && pathLength > 0 {
-		finalBlindedPubKey := pathEdges[pathLength-1].policy.
-			ToNodePubKey()
-
-		if IsBlindedRouteNUMSTargetKey(finalBlindedPubKey[:]) {
-			// If the last hop is the NUMS key for blinded paths, we
-			// remove the dummy hop from the route.
-			pathEdges = pathEdges[:pathLength-1]
-			pathLength--
-		}
-	}
-
 	for i := pathLength - 1; i >= 0; i-- {
 		// Now we'll start to calculate the items within the per-hop
 		// payload for the hop this edge is leading to.
@@ -345,6 +319,10 @@ func newRoute(sourceVertex route.Vertex,
 			dataIndex      = 0
 
 			blindedPath = blindedPayment.BlindedPath
+			numHops     = len(blindedPath.BlindedHops)
+			realFinal   = blindedPath.BlindedHops[numHops-1].
+					BlindedNodePub
+
 			introVertex = route.NewVertex(
 				blindedPath.IntroductionPoint,
 			)
@@ -372,6 +350,11 @@ func newRoute(sourceVertex route.Vertex,
 			if i != len(hops)-1 {
 				hop.AmtToForward = 0
 				hop.OutgoingTimeLock = 0
+			} else {
+				// For the final hop, we swap out the pub key
+				// bytes to the original destination node pub
+				// key for that payment path.
+				hop.PubKeyBytes = route.NewVertex(realFinal)
 			}
 
 			dataIndex++
@@ -700,10 +683,7 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 
 	// The payload size of the final hop differ from intermediate hops
 	// and depends on whether the destination is blinded or not.
-	lastHopPayloadSize, err := lastHopPayloadSize(r, finalHtlcExpiry, amt)
-	if err != nil {
-		return nil, 0, err
-	}
+	lastHopPayloadSize := lastHopPayloadSize(r, finalHtlcExpiry, amt)
 
 	// We can't always assume that the end destination is publicly
 	// advertised to the network so we'll manually include the target node.
@@ -921,13 +901,6 @@ func findPath(g *graphParams, r *RestrictParams, cfg *PathFindingConfig,
 		// included. If we are coming from the source hop, the payload
 		// size is zero, because the original htlc isn't in the onion
 		// blob.
-		//
-		// NOTE: For blinded paths with the NUMS key as the last hop,
-		// the payload size accounts for this dummy hop which is of
-		// the same size as the real last hop. So we account for a
-		// bigger size than the route is however we accept this
-		// little inaccuracy here because we are over estimating by
-		// 1 hop.
 		var payloadSize uint64
 		if fromVertex != source {
 			// In case the unifiedEdge does not have a payload size
@@ -1436,15 +1409,11 @@ func getProbabilityBasedDist(weight int64, probability float64,
 // It depends on the tlv types which are present and also whether the hop is
 // part of a blinded route or not.
 func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
-	amount lnwire.MilliSatoshi) (uint64, error) {
+	amount lnwire.MilliSatoshi) uint64 {
 
 	if r.BlindedPaymentPathSet != nil {
-		paymentPath, err := r.BlindedPaymentPathSet.
+		paymentPath := r.BlindedPaymentPathSet.
 			LargestLastHopPayloadPath()
-		if err != nil {
-			return 0, err
-		}
-
 		blindedPath := paymentPath.BlindedPath.BlindedHops
 		blindedPoint := paymentPath.BlindedPath.BlindingPoint
 
@@ -1459,7 +1428,7 @@ func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
 		}
 
 		// The final hop does not have a short chanID set.
-		return finalHop.PayloadSize(0), nil
+		return finalHop.PayloadSize(0)
 	}
 
 	var mpp *record.MPP
@@ -1485,7 +1454,7 @@ func lastHopPayloadSize(r *RestrictParams, finalHtlcExpiry int32,
 	}
 
 	// The final hop does not have a short chanID set.
-	return finalHop.PayloadSize(0), nil
+	return finalHop.PayloadSize(0)
 }
 
 // overflowSafeAdd adds two MilliSatoshi values and returns the result. If an
diff --git a/routing/result_interpretation.go b/routing/result_interpretation.go
index 60dd22c43..bc1749dfb 100644
--- a/routing/result_interpretation.go
+++ b/routing/result_interpretation.go
@@ -100,27 +100,8 @@ func interpretResult(rt *mcRoute,
 
 // processSuccess processes a successful payment attempt.
 func (i *interpretedResult) processSuccess(route *mcRoute) {
-	// For successes, all nodes must have acted in the right way.
-	// Therefore we mark all of them with a success result. However we need
-	// to handle the blinded route part separately because for intermediate
-	// blinded nodes the amount field is set to zero so we use the receiver
-	// amount.
-	introIdx, isBlinded := introductionPointIndex(route)
-	if isBlinded {
-		// Report success for all the pairs until the introduction
-		// point.
-		i.successPairRange(route, 0, introIdx-1)
-
-		// Handle the blinded route part.
-		//
-		// NOTE: The introIdx index here does describe the node after
-		// the introduction point.
-		i.markBlindedRouteSuccess(route, introIdx)
-
-		return
-	}
-
-	// Mark nodes as successful in the non-blinded case of the payment.
+	// For successes, all nodes must have acted in the right way. Therefore
+	// we mark all of them with a success result.
 	i.successPairRange(route, 0, len(route.hops.Val)-1)
 }
 
@@ -536,22 +517,11 @@ func (i *interpretedResult) processPaymentOutcomeIntermediate(route *mcRoute,
 		if introIdx == len(route.hops.Val)-1 {
 			i.finalFailureReason = &reasonError
 		} else {
-			// We penalize the final hop of the blinded route which
-			// is sufficient to not reuse this route again and is
-			// also more memory efficient because the other hops
-			// of the blinded path are ephemeral and will only be
-			// used in conjunction with the final hop. Moreover we
-			// don't want to punish the introduction node because
-			// the blinded failure does not necessarily mean that
-			// the introduction node was at fault.
-			//
-			// TODO(ziggie): Make sure we only keep mc data for
-			// blinded paths, in both the success and failure case,
-			// in memory during the time of the payment and remove
-			// it afterwards. Blinded paths and their blinded hop
-			// keys are always changing per blinded route so there
-			// is no point in persisting this data.
-			i.failBlindedRoute(route)
+			// If there are other hops between the recipient and
+			// introduction node, then we just penalize the last
+			// hop in the blinded route to minimize the storage of
+			// results for ephemeral keys.
+			i.failPairBalance(route, len(route.hops.Val)-1)
 		}
 
 	// In all other cases, we penalize the reporting node. These are all
@@ -858,41 +828,6 @@ func (i *interpretedResult) successPairRange(rt *mcRoute, fromIdx, toIdx int) {
 	}
 }
 
-// failBlindedRoute marks a blinded route as failed for the specific amount to
-// send by only punishing the last pair.
-func (i *interpretedResult) failBlindedRoute(rt *mcRoute) {
-	// We fail the last pair of the route, in order to fail the complete
-	// blinded route. This is because the combination of ephemeral pubkeys
-	// is unique to the route. We fail the last pair in order to not punish
-	// the introduction node, since we don't want to disincentivize them
-	// from providing that service.
-	pair, _ := getPair(rt, len(rt.hops.Val)-1)
-
-	// Since all the hops along a blinded path don't have any amount set, we
-	// extract the minimal amount to punish from the value that is tried to
-	// be sent to the receiver.
-	amt := rt.hops.Val[len(rt.hops.Val)-1].amtToFwd.Val
-
-	i.pairResults[pair] = failPairResult(amt)
-}
-
-// markBlindedRouteSuccess marks the hops of the blinded route AFTER the
-// introduction node as successful.
-//
-// NOTE: The introIdx must be the index of the first hop of the blinded route
-// AFTER the introduction node.
-func (i *interpretedResult) markBlindedRouteSuccess(rt *mcRoute, introIdx int) {
-	// For blinded hops we do not have the forwarding amount so we take the
-	// minimal amount which went through the route by looking at the last
-	// hop.
-	successAmt := rt.hops.Val[len(rt.hops.Val)-1].amtToFwd.Val
-	for idx := introIdx; idx < len(rt.hops.Val); idx++ {
-		pair, _ := getPair(rt, idx)
-
-		i.pairResults[pair] = successPairResult(successAmt)
-	}
-}
-
 // getPair returns a node pair from the route and the amount passed between that
 // pair.
 func getPair(rt *mcRoute, channelIdx int) (DirectedNodePair,
diff --git a/scripts/verify-install.sh b/scripts/verify-install.sh
index 653a375ee..26c2c3b0f 100755
--- a/scripts/verify-install.sh
+++ b/scripts/verify-install.sh
@@ -21,7 +21,7 @@ MIN_REQUIRED_SIGNATURES=5
 KEYS=()
 KEYS+=("F4FC70F07310028424EFC20A8E4256593F177720 guggero")
 KEYS+=("15E7ECF257098A4EF91655EB4CA7FE54A6213C91 carlaKC")
-KEYS+=("A5B61896952D9FDA83BC054CDC42612E89237182 roasbeef")
+KEYS+=("E4D85299674B2D31FAA1892E372CBD7633C61696 roasbeef")
 KEYS+=("729E9D9D92C75A5FBFEEE057B5DD717BEF7CA5B1 wpaulino")
 KEYS+=("7E81EF6B9989A9CC93884803118759E83439A9B1 Crypt-iQ")
 KEYS+=("9FC6B0BFD597A94DBF09708280E5375C094198D8 bhandras")


Test patch:
diff --git a/chainntnfs/bitcoindnotify/bitcoind_test.go b/chainntnfs/bitcoindnotify/bitcoind_test.go
index cf96a7cce..fa51efa5e 100644
--- a/chainntnfs/bitcoindnotify/bitcoind_test.go
+++ b/chainntnfs/bitcoindnotify/bitcoind_test.go
@@ -109,15 +109,11 @@ func syncNotifierWithMiner(t *testing.T, notifier *BitcoindNotifier,
 // TestHistoricalConfDetailsTxIndex ensures that we correctly retrieve
 // historical confirmation details using the backend node's txindex.
 func TestHistoricalConfDetailsTxIndex(t *testing.T) {
-	success := t.Run("rpc polling enabled", func(st *testing.T) {
+	t.Run("rpc polling enabled", func(st *testing.T) {
 		st.Parallel()
 		testHistoricalConfDetailsTxIndex(st, true)
 	})
 
-	if !success {
-		return
-	}
-
 	t.Run("rpc polling disabled", func(st *testing.T) {
 		st.Parallel()
 		testHistoricalConfDetailsTxIndex(st, false)
@@ -211,15 +207,11 @@ func testHistoricalConfDetailsTxIndex(t *testing.T, rpcPolling bool) {
 // historical confirmation details using the set of fallback methods when the
 // backend node's txindex is disabled.
 func TestHistoricalConfDetailsNoTxIndex(t *testing.T) {
-	success := t.Run("rpc polling enabled", func(st *testing.T) {
+	t.Run("rpc polling enabled", func(st *testing.T) {
 		st.Parallel()
 		testHistoricalConfDetailsNoTxIndex(st, true)
 	})
 
-	if !success {
-		return
-	}
-
 	t.Run("rpc polling disabled", func(st *testing.T) {
 		st.Parallel()
 		testHistoricalConfDetailsNoTxIndex(st, false)
diff --git a/chainntnfs/test/bitcoind/bitcoind_test.go b/chainntnfs/test/bitcoind/bitcoind_test.go
index b5a2e6626..cc379de81 100644
--- a/chainntnfs/test/bitcoind/bitcoind_test.go
+++ b/chainntnfs/test/bitcoind/bitcoind_test.go
@@ -12,15 +12,11 @@ import (
 // TestInterfaces executes the generic notifier test suite against a bitcoind
 // powered chain notifier.
 func TestInterfaces(t *testing.T) {
-	success := t.Run("bitcoind", func(st *testing.T) {
+	t.Run("bitcoind", func(st *testing.T) {
 		st.Parallel()
 		chainntnfstest.TestInterfaces(st, "bitcoind")
 	})
 
-	if !success {
-		return
-	}
-
 	t.Run("bitcoind rpc polling", func(st *testing.T) {
 		st.Parallel()
 		chainntnfstest.TestInterfaces(st, "bitcoind-rpc-polling")
diff --git a/chainntnfs/txnotifier_test.go b/chainntnfs/txnotifier_test.go
index c7bc8da50..3ef276b42 100644
--- a/chainntnfs/txnotifier_test.go
+++ b/chainntnfs/txnotifier_test.go
@@ -173,7 +173,7 @@ func TestTxNotifierRegistrationValidation(t *testing.T) {
 
 	for _, testCase := range testCases {
 		testCase := testCase
-		success := t.Run(testCase.name, func(t *testing.T) {
+		t.Run(testCase.name, func(t *testing.T) {
 			hintCache := newMockHintCache()
 			n := chainntnfs.NewTxNotifier(
 				10, chainntnfs.ReorgSafetyLimit, hintCache, hintCache,
@@ -201,10 +201,6 @@ func TestTxNotifierRegistrationValidation(t *testing.T) {
 					"\"%v\", got \"%v\"", testCase.err, err)
 			}
 		})
-
-		if !success {
-			return
-		}
 	}
 }
 
diff --git a/contractcourt/htlc_incoming_contest_resolver.go b/contractcourt/htlc_incoming_contest_resolver.go
index ee526dfff..e5be63cbf 100644
--- a/contractcourt/htlc_incoming_contest_resolver.go
+++ b/contractcourt/htlc_incoming_contest_resolver.go
@@ -308,8 +308,7 @@ func (h *htlcIncomingContestResolver) Resolve(
 
 		resolution, err := h.Registry.NotifyExitHopHtlc(
 			h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
-			circuitKey, hodlQueue.ChanIn(), h.htlc.CustomRecords,
-			payload,
+			circuitKey, hodlQueue.ChanIn(), nil, payload,
 		)
 		if err != nil {
 			return nil, err
diff --git a/fn/context_guard_test.go b/fn/context_guard_test.go
deleted file mode 100644
index e13cba5dc..000000000
--- a/fn/context_guard_test.go
+++ /dev/null
@@ -1,435 +0,0 @@
-package fn
-
-import (
-	"context"
-	"testing"
-	"time"
-)
-
-// TestContextGuard tests the behaviour of the ContextGuard.
-func TestContextGuard(t *testing.T) {
-	t.Parallel()
-
-	defer GuardTest(t)()
-
-	// Test that the derived context is cancelled when the passed context is
-	// cancelled.
-	t.Run("Parent context is cancelled", func(t *testing.T) {
-		t.Parallel()
-		var (
-			ctx, cancel = context.WithCancel(context.Background())
-			g           = NewContextGuard()
-		)
-
-		ctxc, _ := g.Create(ctx)
-
-		// Cancel the parent context.
-		cancel()
-		// Assert that the derived context is cancelled.
-		select {
-		case <-ctxc.Done():
-		default:
-			t.Errorf("The derived context should be cancelled at " +
-				"this point")
-		}
-	})
-
-	// Test that the derived context is cancelled when the returned cancel
-	// function is called.
-	t.Run("Derived context is cancelled", func(t *testing.T) {
-		t.Parallel()
-		var (
-			ctx = context.Background()
-			g   = NewContextGuard()
-		)
-
-		ctxc, cancel := g.Create(ctx)
-
-		// Cancel the context.
-		cancel()
-
-		// Assert that the derived context is cancelled.
-		select {
-		case <-ctxc.Done():
-		default:
-			t.Errorf("The derived context should be cancelled at " +
-				"this point")
-		}
-	})
-
-	// Test that the derived context is cancelled when the quit channel is
-	// closed.
-	t.Run("Quit channel is closed", func(t *testing.T) {
-		t.Parallel()
-
-		var (
-			ctx = context.Background()
-			g   = NewContextGuard()
-		)
-		ctxc, _ := g.Create(ctx)
-
-		// Close the quit channel.
-		g.Quit()
-
-		// Assert that the derived context is cancelled.
-		select {
-		case <-ctxc.Done():
-		default:
-			t.Errorf("The derived context should be cancelled at " +
-				"this point")
-		}
-	})
-
-	t.Run("Parent context is already closed", func(t *testing.T) {
-		t.Parallel()
-
-		var (
-			ctx, cancel = context.WithCancel(context.Background())
-			g           = NewContextGuard()
-		)
-		cancel()
-
-		ctxc, _ := g.Create(ctx)
-
-		// Assert that the derived context is already cancelled.
-		select {
-		case <-ctxc.Done():
-		default:
-			t.Errorf("The derived context should be cancelled at " +
-				"this point")
-		}
-	})
-
-	t.Run("Quit channel is already closed", func(t *testing.T) {
-		t.Parallel()
-
-		var (
-			ctx = context.Background()
-			g   = NewContextGuard()
-		)
-
-		g.Quit()
-
-		ctxc, _ := g.Create(ctx)
-
-		// Assert that the derived context is already cancelled.
-		select {
-		case <-ctxc.Done():
-		default:
-			t.Errorf("The derived context should be cancelled at " +
-				"this point")
-		}
-	})
-
-	t.Run("Child context should be cancelled synchronously with "+
-		"parent", func(t *testing.T) {
-
-		t.Parallel()
-
-		var (
-			ctx, cancel = context.WithCancel(context.Background())
-			g           = NewContextGuard()
-			task        = make(chan struct{})
-			done        = make(chan struct{})
-		)
-		// Derive a child context.
-		ctxc, _ := g.Create(ctx)
-
-		// Spin off a routine that exists cleanly if the child context
-		// is cancelled but fails if the task is performed.
-		go func() {
-			defer close(done)
-			select {
-			case <-ctxc.Done():
-			case <-task:
-				t.Fatalf("should not get here")
-			}
-		}()
-
-		// Give the goroutine above a chance to spin up so that it's
-		// waiting on the select.
-		time.Sleep(time.Millisecond * 200)
-
-		// First cancel the parent context. Then immediately execute the
-		// task.
-		cancel()
-		close(task)
-
-		// Wait for the goroutine to exit.
-		select {
-		case <-done:
-		case <-time.After(time.Second):
-			t.Fatalf("timeout")
-		}
-	})
-
-	t.Run("Child context should be cancelled synchronously with the "+
-		"close of the quit channel", func(t *testing.T) {
-
-		t.Parallel()
-
-		var (
-			ctx  = context.Background()
-			g    = NewContextGuard()
-			task = make(chan struct{})
-			done = make(chan struct{})
-		)
-
-		// Derive a child context.
-		ctxc, _ := g.Create(ctx)
-
-		// Spin off a routine that exists cleanly if the child context
-		// is cancelled but fails if the task is performed.
-		go func() {
-			defer close(done)
-			select {
-			case <-ctxc.Done():
-			case <-task:
-				t.Fatalf("should not get here")
-			}
-		}()
-
-		// Give the goroutine above a chance to spin up so that it's
-		// waiting on the select.
-		time.Sleep(time.Millisecond * 200)
-
-		// First cancel the parent context. Then immediately execute the
-		// task.
-		g.Quit()
-
-		// Execute the task.
-		close(task)
-
-		// Wait for the goroutine to exit.
-		select {
-		case <-done:
-		case <-time.After(time.Second):
-			t.Fatalf("timeout")
-		}
-	})
-
-	// Test that if we add the BlockingCGOpt option, then the context will
-	// not be cancelled when the quit channel is closed but will be when the
-	// cancel function is called.
-	t.Run("Blocking context no timeout", func(t *testing.T) {
-		t.Parallel()
-
-		var (
-			ctx  = context.Background()
-			g    = NewContextGuard()
-			task = make(chan struct{})
-			done = make(chan struct{})
-		)
-
-		// Derive a blocking child context.
-		ctxc, cancel := g.Create(ctx, WithBlockingCG())
-
-		// Spin of a routine that will exit cleanly if the context is
-		// cancelled but will fail if the task is performed.
-		go func() {
-			defer func() {
-				done <- struct{}{}
-			}()
-
-			select {
-			case <-ctxc.Done():
-			case <-task:
-				t.Fatalf("Expected context to be cancelled")
-			}
-		}()
-
-		// Give the goroutine above a chance to spin up so that it's
-		// waiting on the select.
-		time.Sleep(time.Millisecond * 200)
-
-		// Cancel the context.
-		cancel()
-
-		// Attempt to perform the task.
-		select {
-		case task <- struct{}{}:
-			t.Fatalf("Expected task to not be performed")
-		default:
-		}
-
-		// Assert that the task goroutine has now completed.
-		select {
-		case <-done:
-		case <-time.After(time.Second):
-			t.Fatalf("timeout")
-		}
-
-		// Derive a new blocking child context.
-		ctxc, cancel = g.Create(ctx, WithBlockingCG())
-
-		// Repeat the task but this time, we will call Quit first, but
-		// since this is a blocking context, the context should not be
-		// cancelled and the task _should_ be performed.
-		go func() {
-			defer func() {
-				done <- struct{}{}
-			}()
-
-			select {
-			case <-ctxc.Done():
-				t.Fatalf("Expected task to be performed")
-			case <-task:
-			}
-		}()
-
-		// Give the goroutine above a chance to spin up so that it's
-		// waiting on the select.
-		time.Sleep(time.Millisecond * 200)
-
-		// Close the quit channel. This should NOT cause the context
-		// to be cancelled.
-		g.Quit()
-
-		// Now, perform the task.
-		select {
-		case task <- struct{}{}:
-		case <-time.After(time.Second):
-			t.Fatalf("timeout")
-		}
-
-		// Assert that the task goroutine has now completed.
-		select {
-		case <-done:
-		case <-time.After(time.Second):
-			t.Fatalf("timeout")
-		}
-	})
-
-	// Test that if we add the CustomTimeoutCGOpt option, then the context
-	// will be not be cancelled when the quit channel is closed but will be
-	// if either the context is cancelled or the timeout is reached.
-	t.Run("Blocking context with timeout", func(t *testing.T) {
-		t.Parallel()
-
-		var (
-			ctx     = context.Background()
-			g       = NewContextGuard()
-			task    = make(chan struct{})
-			done    = make(chan struct{})
-			timeout = time.Millisecond * 500
-		)
-
-		// Derive a blocking child context.
-		ctxc, cancel := g.Create(
-			ctx, WithBlockingCG(), WithCustomTimeoutCG(timeout),
-		)
-
-		// Spin of a routine that will exit cleanly if the context is
-		// cancelled but will fail if the task is performed.
-		go func() {
-			defer func() {
-				done <- struct{}{}
-			}()
-
-			select {
-			case <-ctxc.Done():
-			case <-task:
-				t.Fatalf("Expected context to be cancelled")
-			}
-		}()
-
-		// Give the goroutine above a chance to spin up so that it's
-		// waiting on the select.
-		time.Sleep(time.Millisecond * 200)
-
-		// Cancel the context.
-		cancel()
-
-		// Attempt to perform the task.
-		select {
-		case task <- struct{}{}:
-			t.Fatalf("Expected task to not be performed")
-		default:
-		}
-
-		// Assert that the task goroutine has now completed.
-		select {
-		case <-done:
-		case <-time.After(time.Second):
-			t.Fatalf("timeout")
-		}
-
-		// Derive a new blocking child context with a timeout.
-		ctxc, cancel = g.Create(
-			ctx, WithBlockingCG(), WithCustomTimeoutCG(timeout),
-		)
-
-		// Repeat the task but this time, but this time, we will assert
-		// that the context is cancelled if the timeout is reached.
-		// We will again fail if the task is performed.
-		go func() {
-			defer func() {
-				done <- struct{}{}
-			}()
-
-			select {
-			case <-ctxc.Done():
-			case <-task:
-				t.Fatalf("Expected context to be cancelled")
-			}
-		}()
-
-		// Wait for the timeout to be reached.
-		time.Sleep(timeout + time.Millisecond*100)
-
-		// Attempt to perform the task.
-		select {
-		case task <- struct{}{}:
-			t.Fatalf("Expected task to not be performed")
-		default:
-		}
-
-		// Assert that the task goroutine has now completed.
-		select {
-		case <-done:
-		case <-time.After(time.Second):
-			t.Fatalf("timeout")
-		}
-
-		// Finally, repeat the task but this time show that calling
-		// Quit does not cancel the context and that the task still gets
-		// performed if it takes place before the context is timed out.
-		ctxc, cancel = g.Create(
-			ctx, WithBlockingCG(), WithCustomTimeoutCG(timeout),
-		)
-
-		go func() {
-			defer func() {
-				done <- struct{}{}
-			}()
-
-			select {
-			case <-ctxc.Done():
-				t.Fatalf("Expected the task to be performed")
-			case <-task:
-			}
-		}()
-
-		// Give the goroutine above a chance to spin up so that it's
-		// waiting on the select.
-		time.Sleep(time.Millisecond * 200)
-
-		// Close the quit channel. This should NOT cause the context
-		// to be cancelled.
-		g.Quit()
-
-		// Now, perform the task.
-		select {
-		case task <- struct{}{}:
-		case <-time.After(time.Second):
-			t.Fatalf("timeout")
-		}
-
-		// Assert that the task goroutine has now completed.
-		select {
-		case <-done:
-		case <-time.After(time.Second):
-			t.Fatalf("timeout")
-		}
-	})
-}
diff --git a/fn/goroutine_manager_test.go b/fn/goroutine_manager_test.go
index e39dce995..1fc945b97 100644
--- a/fn/goroutine_manager_test.go
+++ b/fn/goroutine_manager_test.go
@@ -2,145 +2,156 @@ package fn
 
 import (
 	"context"
+	"sync"
 	"testing"
 	"time"
 
 	"github.com/stretchr/testify/require"
 )
 
-// TestGoroutineManager tests the behaviour of the GoroutineManager.
+// TestGoroutineManager tests that the GoroutineManager starts goroutines until
+// ctx expires. It also makes sure it fails to start new goroutines after the
+// context expired and the GoroutineManager is in the process of waiting for
+// already started goroutines in the Stop method.
 func TestGoroutineManager(t *testing.T) {
 	t.Parallel()
 
-	// Here we test that the GoroutineManager starts goroutines until it has
-	// been stopped.
-	t.Run("GM is stopped", func(t *testing.T) {
-		t.Parallel()
-
-		var (
-			ctx      = context.Background()
-			m        = NewGoroutineManager()
-			taskChan = make(chan struct{})
-		)
-
-		// The gm has not stopped yet and the passed in context has not
-		// expired, so we expect the goroutine to start. The taskChan is
-		// blocking, so this goroutine will be live for a while.
-		require.True(t, m.Go(ctx, func(ctx context.Context) {
-			<-taskChan
-		}))
+	m := NewGoroutineManager(context.Background())
 
-		t1 := time.Now()
+	taskChan := make(chan struct{})
 
-		// Close taskChan in 1s, causing the goroutine to stop.
-		time.AfterFunc(time.Second, func() {
-			close(taskChan)
-		})
+	require.True(t, m.Go(func(ctx context.Context) {
+		<-taskChan
+	}))
 
-		m.Stop()
-		stopDelay := time.Since(t1)
+	t1 := time.Now()
 
-		// Make sure Stop was waiting for the goroutine to stop.
-		require.Greater(t, stopDelay, time.Second)
+	// Close taskChan in 1s, causing the goroutine to stop.
+	time.AfterFunc(time.Second, func() {
+		close(taskChan)
+	})
 
-		// Make sure new goroutines do not start after Stop.
-		require.False(t, m.Go(ctx, func(ctx context.Context) {}))
+	m.Stop()
+	stopDelay := time.Since(t1)
 
-		// When Stop() is called, gm quit channel has been closed and so
-		// Done() should return.
-		select {
-		case <-m.Done():
-		default:
-			t.Errorf("Done() channel must be closed at this point")
-		}
-	})
+	// Make sure Stop was waiting for the goroutine to stop.
+	require.Greater(t, stopDelay, time.Second)
 
-	// Test that the GoroutineManager fails to start a goroutine or exits a
-	// goroutine if the caller context has expired.
-	t.Run("Caller context expires", func(t *testing.T) {
-		t.Parallel()
-
-		var (
-			ctx      = context.Background()
-			m        = NewGoroutineManager()
-			taskChan = make(chan struct{})
-		)
-
-		// Derive a child context with a cancel function.
-		ctxc, cancel := context.WithCancel(ctx)
-
-		// The gm has not stopped yet and the passed in context has not
-		// expired, so we expect the goroutine to start.
-		require.True(t, m.Go(ctxc, func(ctx context.Context) {
-			select {
-			case <-ctx.Done():
-			case <-taskChan:
-				t.Fatalf("The task was performed when it " +
-					"should not have")
-			}
-		}))
-
-		// Give the GM a little bit of time to start the goroutine so
-		// that we can be sure that it is already listening on the
-		// ctx and taskChan before calling cancel.
-		time.Sleep(time.Millisecond * 500)
-
-		// Cancel the context so that the goroutine exits.
-		cancel()
-
-		// Attempt to send a signal on the task channel, nothing should
-		// happen since the goroutine has already exited.
-		select {
-		case taskChan <- struct{}{}:
-		case <-time.After(time.Millisecond * 200):
-		}
+	// Make sure new goroutines do not start after Stop.
+	require.False(t, m.Go(func(ctx context.Context) {}))
 
-		// Again attempt to add a goroutine with the same cancelled
-		// context. This should fail since the context has already
-		// expired.
-		require.False(t, m.Go(ctxc, func(ctx context.Context) {
-			t.Fatalf("The goroutine should not have started")
-		}))
+	// When Stop() is called, the internal context expires and m.Done() is
+	// closed. Test this.
+	select {
+	case <-m.Done():
+	default:
+		t.Errorf("Done() channel must be closed at this point")
+	}
+}
 
-		// Stop the goroutine manager.
+// TestGoroutineManagerContextExpires tests the effect of context expiry.
+func TestGoroutineManagerContextExpires(t *testing.T) {
+	t.Parallel()
+
+	ctx, cancel := context.WithCancel(context.Background())
+
+	m := NewGoroutineManager(ctx)
+
+	require.True(t, m.Go(func(ctx context.Context) {
+		<-ctx.Done()
+	}))
+
+	// The Done channel of the manager should not be closed, so the
+	// following call must block.
+	select {
+	case <-m.Done():
+		t.Errorf("Done() channel must not be closed at this point")
+	default:
+	}
+
+	cancel()
+
+	// The Done channel of the manager should be closed, so the following
+	// call must not block.
+	select {
+	case <-m.Done():
+	default:
+		t.Errorf("Done() channel must be closed at this point")
+	}
+
+	// Make sure new goroutines do not start after context expiry.
+	require.False(t, m.Go(func(ctx context.Context) {}))
+
+	// Stop will wait for all goroutines to stop.
+	m.Stop()
+}
+
+// TestGoroutineManagerStress starts many goroutines while calling Stop. It
+// is needed to make sure the GoroutineManager does not crash if this happen.
+// If the mutex was not used, it would crash because of a race condition between
+// wg.Add(1) and wg.Wait().
+func TestGoroutineManagerStress(t *testing.T) {
+	t.Parallel()
+
+	m := NewGoroutineManager(context.Background())
+
+	stopChan := make(chan struct{})
+
+	time.AfterFunc(1*time.Millisecond, func() {
 		m.Stop()
+		close(stopChan)
 	})
 
-	// Start many goroutines while calling Stop. We do this to make sure
-	// that the GoroutineManager does not crash when these calls are done in
-	// parallel because of the potential race between wg.Add() and
-	// wg.Done() when the wg counter is 0.
-	t.Run("Stress test", func(t *testing.T) {
-		t.Parallel()
+	// Starts 100 goroutines sequentially. Sequential order is needed to
+	// keep wg.counter low (0 or 1) to increase probability of race
+	// condition to be caught if it exists. If mutex is removed in the
+	// implementation, this test crashes under `-race`.
+	for i := 0; i < 100; i++ {
+		taskChan := make(chan struct{})
+		ok := m.Go(func(ctx context.Context) {
+			close(taskChan)
+		})
+		// If goroutine was started, wait for its completion.
+		if ok {
+			<-taskChan
+		}
+	}
+
+	// Wait for Stop to complete.
+	<-stopChan
+}
 
-		var (
-			ctx      = context.Background()
-			m        = NewGoroutineManager()
-			stopChan = make(chan struct{})
-		)
+// 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()
 
-		time.AfterFunc(1*time.Millisecond, func() {
-			m.Stop()
-			close(stopChan)
-		})
+	m := NewGoroutineManager(context.Background())
 
-		// Start 100 goroutines sequentially. Sequential order is
-		// needed to keep wg.counter low (0 or 1) to increase
-		// probability of the race condition to triggered if it exists.
-		// If mutex is removed in the implementation, this test crashes
-		// under `-race`.
-		for i := 0; i < 100; i++ {
-			taskChan := make(chan struct{})
-			ok := m.Go(ctx, func(ctx context.Context) {
-				close(taskChan)
-			})
-			// If goroutine was started, wait for its completion.
-			if ok {
-				<-taskChan
-			}
-		}
+	// jobChan is used to make the task to finish.
+	jobChan := make(chan struct{})
 
-		// Wait for Stop to complete.
-		<-stopChan
+	// 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/fn/io_test.go b/fn/io_test.go
new file mode 100644
index 000000000..522fe136e
--- /dev/null
+++ b/fn/io_test.go
@@ -0,0 +1,96 @@
+package fn
+
+import (
+	"os"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+var (
+	data1 = "The network is robust in its unstructured simplicity."
+	data2 = "Nodes work all at once with little coordination."
+)
+
+// TestWriteFile uses same scenario of ioutil asserting the content created and
+// stored on new file with the original one.
+func TestWriteFile(t *testing.T) {
+	t.Parallel()
+
+	f, deferred := ensureTempfile(t)
+	filename := f.Name()
+	defer deferred()
+
+	// Write file.
+	err := WriteFile(filename, []byte(data2), 0644)
+	require.NoError(t, err, "couldn't write file")
+	ensureFileContents(t, filename, data2)
+
+	// Overwrite file.
+	err = WriteFile(filename, []byte(data1), 0644)
+	require.NoError(t, err, "couldn't overwrite file")
+	ensureFileContents(t, filename, data1)
+
+	// Change file permission to read-only.
+	err = os.Chmod(filename, 0444)
+	require.NoError(t, err, "couldn't change file to read-only")
+
+	// Write must fail and keep the file.
+	err = WriteFile(filename, []byte(data2), 0644)
+	require.Error(t, err, "shouldn't write a read-only file")
+
+	_, err = os.Stat(filename)
+	require.NoError(t, err, "shouldn't remove file on error")
+	ensureFileContents(t, filename, data1)
+}
+
+func TestWriteFileRemove(t *testing.T) {
+	t.Parallel()
+
+	f, deferred := ensureTempfile(t)
+	filename := f.Name()
+	defer deferred()
+
+	// Write file.
+	err := WriteFileRemove(filename, []byte(data2), 0644)
+	require.NoError(t, err, "couldn't write file")
+	ensureFileContents(t, filename, data2)
+
+	// Overwrite file.
+	err = WriteFileRemove(filename, []byte(data1), 0644)
+	require.NoError(t, err, "couldn't overwrite file")
+	ensureFileContents(t, filename, data1)
+
+	// Change file permission to read-only.
+	err = os.Chmod(filename, 0444)
+	require.NoError(t, err, "couldn't change file to read-only")
+
+	// Write must fail and remove the file.
+	err = WriteFileRemove(filename, []byte(data2), 0644)
+	require.Error(t, err, "shouldn't write a read-only file")
+
+	_, err = os.Stat(filename)
+	require.ErrorContains(
+		t, err, "no such file or directory",
+		"should remove file on error",
+	)
+}
+
+func ensureTempfile(t *testing.T) (*os.File, func()) {
+	t.Helper()
+	f, err := os.CreateTemp("", "fn-io-test-TestWriteFile")
+	require.NoError(t, err, "couldn't create temporary file")
+
+	return f, func() {
+		f.Close()
+		os.Remove(f.Name())
+	}
+}
+
+func ensureFileContents(t *testing.T, filename string, data string) {
+	t.Helper()
+	contents, err := os.ReadFile(filename)
+	require.NoError(t, err, "couldn't read file")
+
+	require.Equal(t, data, string(contents))
+}
diff --git a/fn/tests.go b/fn/tests.go
deleted file mode 100644
index e492e5330..000000000
--- a/fn/tests.go
+++ /dev/null
@@ -1,51 +0,0 @@
-package fn
-
-import (
-	"os"
-	"runtime/pprof"
-	"testing"
-	"time"
-
-	"github.com/stretchr/testify/require"
-)
-
-// GuardConfig stores options for Guard function.
-type GuardConfig struct {
-	timeout time.Duration
-}
-
-// GuardOption is an option for Guard function.
-type GuardOption func(*GuardConfig)
-
-// WithGuardTimeout sets timeout for the guard. Default is 5s.
-func WithGuardTimeout(timeout time.Duration) GuardOption {
-	return func(c *GuardConfig) {
-		c.timeout = timeout
-	}
-}
-
-// GuardTest implements a test level timeout.
-func GuardTest(t *testing.T, opts ...GuardOption) func() {
-	cfg := GuardConfig{
-		timeout: 5 * time.Second,
-	}
-	for _, opt := range opts {
-		opt(&cfg)
-	}
-
-	done := make(chan struct{})
-	go func() {
-		select {
-		case <-time.After(cfg.timeout):
-			err := pprof.Lookup("goroutine").WriteTo(os.Stdout, 1)
-			require.NoError(t, err)
-			panic("test timeout")
-
-		case <-done:
-		}
-	}()
-
-	return func() {
-		close(done)
-	}
-}
diff --git a/lnwallet/chancloser/chancloser_test.go b/lnwallet/chancloser/chancloser_test.go
index f7bdc74b4..fe71fe5e3 100644
--- a/lnwallet/chancloser/chancloser_test.go
+++ b/lnwallet/chancloser/chancloser_test.go
@@ -129,8 +129,8 @@ func TestMaybeMatchScript(t *testing.T) {
 			t.Parallel()
 
 			err := validateShutdownScript(
-				test.upfrontScript, test.shutdownScript,
-				&chaincfg.SimNetParams,
+				func() error { return nil }, test.upfrontScript,
+				test.shutdownScript, &chaincfg.SimNetParams,
 			)
 
 			if err != test.expectedErr {
@@ -189,7 +189,7 @@ func (m *mockChannel) RemoteUpfrontShutdownScript() lnwire.DeliveryAddress {
 
 func (m *mockChannel) CreateCloseProposal(fee btcutil.Amount,
 	localScript, remoteScript []byte,
-	_ ...lnwallet.ChanCloseOpt) (input.Signature, *wire.MsgTx,
+	_ ...lnwallet.ChanCloseOpt) (input.Signature, *chainhash.Hash,
 	btcutil.Amount, error) {
 
 	if m.chanType.IsTaproot() {
diff --git a/lnwallet/chancloser/rbf_coop_test.go b/lnwallet/chancloser/rbf_coop_test.go
deleted file mode 100644
index 07bc32e3d..000000000
--- a/lnwallet/chancloser/rbf_coop_test.go
+++ /dev/null
@@ -1,1582 +0,0 @@
-package chancloser
-
-import (
-	"bytes"
-	"encoding/hex"
-	"errors"
-	"fmt"
-	"math/rand"
-	"testing"
-	"time"
-
-	"github.com/btcsuite/btcd/btcec/v2"
-	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
-	"github.com/btcsuite/btcd/btcutil"
-	"github.com/btcsuite/btcd/chaincfg"
-	"github.com/btcsuite/btcd/mempool"
-	"github.com/btcsuite/btcd/txscript"
-	"github.com/btcsuite/btcd/wire"
-	"github.com/lightningnetwork/lnd/fn/v2"
-	"github.com/lightningnetwork/lnd/input"
-	"github.com/lightningnetwork/lnd/lntest/wait"
-	"github.com/lightningnetwork/lnd/lntypes"
-	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
-	"github.com/lightningnetwork/lnd/lnwire"
-	"github.com/lightningnetwork/lnd/protofsm"
-	"github.com/lightningnetwork/lnd/tlv"
-	"github.com/stretchr/testify/mock"
-	"github.com/stretchr/testify/require"
-)
-
-var (
-	localAddr = lnwire.DeliveryAddress(append(
-		[]byte{txscript.OP_1, txscript.OP_DATA_32},
-		bytes.Repeat([]byte{0x01}, 32)...,
-	))
-
-	remoteAddr = lnwire.DeliveryAddress(append(
-		[]byte{txscript.OP_1, txscript.OP_DATA_32},
-		bytes.Repeat([]byte{0x02}, 32)...,
-	))
-
-	localSigBytes = fromHex("3045022100cd496f2ab4fe124f977ffe3caa09f757" +
-		"6d8a34156b4e55d326b4dffc0399a094022013500a0510b5094bff220c7" +
-		"4656879b8ca0369d3da78004004c970790862fc03")
-	localSig     = sigMustParse(localSigBytes)
-	localSigWire = mustWireSig(&localSig)
-
-	remoteSigBytes = fromHex("304502210082235e21a2300022738dabb8e1bbd9d1" +
-		"9cfb1e7ab8c30a23b0afbb8d178abcf3022024bf68e256c534ddfaf966b" +
-		"f908deb944305596f7bdcc38d69acad7f9c868724")
-	remoteSig     = sigMustParse(remoteSigBytes)
-	remoteWireSig = mustWireSig(&remoteSig)
-
-	localTx = wire.MsgTx{Version: 2}
-
-	closeTx = wire.NewMsgTx(2)
-)
-
-func sigMustParse(sigBytes []byte) ecdsa.Signature {
-	sig, err := ecdsa.ParseSignature(sigBytes)
-	if err != nil {
-		panic(err)
-	}
-
-	return *sig
-}
-
-func mustWireSig(e input.Signature) lnwire.Sig {
-	wireSig, err := lnwire.NewSigFromSignature(e)
-	if err != nil {
-		panic(err)
-	}
-
-	return wireSig
-}
-
-func fromHex(s string) []byte {
-	r, err := hex.DecodeString(s)
-	if err != nil {
-		panic("invalid hex in source file: " + s)
-	}
-
-	return r
-}
-
-func randOutPoint(t *testing.T) wire.OutPoint {
-	var op wire.OutPoint
-	if _, err := rand.Read(op.Hash[:]); err != nil {
-		t.Fatalf("unable to generate random outpoint: %v", err)
-	}
-	op.Index = rand.Uint32()
-
-	return op
-}
-
-func randPubKey(t *testing.T) *btcec.PublicKey {
-	priv, err := btcec.NewPrivateKey()
-	if err != nil {
-		t.Fatalf("unable to generate private key: %v", err)
-	}
-
-	return priv.PubKey()
-}
-
-func assertStateTransitions[Event any, Env protofsm.Environment](
-	t *testing.T, stateSub protofsm.StateSubscriber[Event, Env],
-	expectedStates []protofsm.State[Event, Env]) {
-
-	t.Helper()
-
-	for _, expectedState := range expectedStates {
-		newState := <-stateSub.NewItemCreated.ChanOut()
-
-		require.IsType(t, expectedState, newState)
-	}
-
-	// We should have no more states.
-	select {
-	case newState := <-stateSub.NewItemCreated.ChanOut():
-		t.Fatalf("unexpected state transition: %v", newState)
-	default:
-	}
-}
-
-// unknownEvent is a dummy event that is used to test that the state machine
-// transitions properly fail when an unknown event is received.
-type unknownEvent struct {
-}
-
-func (u *unknownEvent) protocolSealed() {
-}
-
-// assertUnknownEventFail asserts that the state machine fails as expected
-// given an unknown event.
-func assertUnknownEventFail(t *testing.T, startingState ProtocolState) {
-	t.Helper()
-
-	// Any other event should be ignored.
-	t.Run("unknown_event", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some(startingState),
-		})
-		defer closeHarness.stopAndAssert()
-
-		closeHarness.expectFailure(ErrInvalidStateTransition)
-
-		closeHarness.chanCloser.SendEvent(&unknownEvent{})
-
-		// There should be no further state transitions.
-		closeHarness.assertNoStateTransitions()
-	})
-}
-
-type harnessCfg struct {
-	initialState fn.Option[ProtocolState]
-
-	thawHeight fn.Option[uint32]
-
-	localUpfrontAddr  fn.Option[lnwire.DeliveryAddress]
-	remoteUpfrontAddr fn.Option[lnwire.DeliveryAddress]
-}
-
-// rbfCloserTestHarness is a test harness for the RBF closer.
-type rbfCloserTestHarness struct {
-	*testing.T
-
-	cfg *harnessCfg
-
-	chanCloser *RbfChanCloser
-	env        Environment
-
-	newAddrErr error
-
-	pkScript []byte
-	peerPub  btcec.PublicKey
-
-	startingState RbfState
-
-	feeEstimator   *mockFeeEstimator
-	chanObserver   *mockChanObserver
-	signer         *mockCloseSigner
-	daemonAdapters *dummyAdapters
-	errReporter    *mockErrorReporter
-
-	stateSub protofsm.StateSubscriber[ProtocolEvent, *Environment]
-}
-
-var errfailAddr = fmt.Errorf("fail")
-
-// failNewAddrFunc causes the newAddrFunc to fail.
-func (r *rbfCloserTestHarness) failNewAddrFunc() {
-	r.T.Helper()
-
-	r.newAddrErr = errfailAddr
-}
-
-func (r *rbfCloserTestHarness) newAddrFunc() (
-	lnwire.DeliveryAddress, error) {
-
-	r.T.Helper()
-
-	return lnwire.DeliveryAddress{}, r.newAddrErr
-}
-
-func (r *rbfCloserTestHarness) assertExpectations() {
-	r.T.Helper()
-
-	r.feeEstimator.AssertExpectations(r.T)
-	r.chanObserver.AssertExpectations(r.T)
-	r.daemonAdapters.AssertExpectations(r.T)
-	r.errReporter.AssertExpectations(r.T)
-	r.signer.AssertExpectations(r.T)
-}
-
-func (r *rbfCloserTestHarness) stopAndAssert() {
-	r.T.Helper()
-
-	defer r.chanCloser.RemoveStateSub(r.stateSub)
-	r.chanCloser.Stop()
-
-	r.assertExpectations()
-}
-
-func (r *rbfCloserTestHarness) assertStartupAssertions() {
-	r.T.Helper()
-
-	// When the state machine has started up, we recv a starting state
-	// transition for the initial state.
-	expectedStates := []RbfState{r.startingState}
-	assertStateTransitions(r.T, r.stateSub, expectedStates)
-
-	// Registering the spend transaction should have been called.
-	r.daemonAdapters.AssertCalled(
-		r.T, "RegisterSpendNtfn", &r.env.ChanPoint, r.pkScript,
-		r.env.Scid.BlockHeight,
-	)
-}
-
-func (r *rbfCloserTestHarness) assertNoStateTransitions() {
-	select {
-	case newState := <-r.stateSub.NewItemCreated.ChanOut():
-		r.T.Fatalf("unexpected state transition: %T", newState)
-	case <-time.After(10 * time.Millisecond):
-	}
-}
-
-func (r *rbfCloserTestHarness) assertStateTransitions(states ...RbfState) {
-	assertStateTransitions(r.T, r.stateSub, states)
-}
-
-func (r *rbfCloserTestHarness) currentState() RbfState {
-	state, err := r.chanCloser.CurrentState()
-	require.NoError(r.T, err)
-
-	return state
-}
-
-type shutdownExpect struct {
-	isInitiator   bool
-	allowSend     bool
-	recvShutdown  bool
-	finalBalances fn.Option[ShutdownBalances]
-}
-
-func (r *rbfCloserTestHarness) expectShutdownEvents(expect shutdownExpect) {
-	r.T.Helper()
-
-	r.chanObserver.On("FinalBalances").Return(expect.finalBalances)
-
-	if expect.isInitiator {
-		r.chanObserver.On("NoDanglingUpdates").Return(expect.allowSend)
-	} else {
-		r.chanObserver.On("NoDanglingUpdates").Return(
-			expect.allowSend,
-		).Maybe()
-	}
-
-	// When we're receiving a shutdown, we should also disable incoming
-	// adds.
-	if expect.recvShutdown {
-		r.chanObserver.On("DisableIncomingAdds").Return(nil)
-	}
-
-	// If a close is in progress, this is an RBF iteration, the link has
-	// already been cleaned up, so we don't expect any assertions, other
-	// than to check the closing state.
-	if expect.finalBalances.IsSome() {
-		return
-	}
-
-	r.chanObserver.On("DisableOutgoingAdds").Return(nil)
-
-	r.chanObserver.On(
-		"MarkShutdownSent", mock.Anything, expect.isInitiator,
-	).Return(nil)
-
-	r.chanObserver.On("DisableChannel").Return(nil)
-}
-
-func (r *rbfCloserTestHarness) expectFinalBalances(
-	b fn.Option[ShutdownBalances]) {
-
-	r.chanObserver.On("FinalBalances").Return(b)
-}
-
-func (r *rbfCloserTestHarness) expectIncomingAddsDisabled() {
-	r.T.Helper()
-
-	r.chanObserver.On("DisableIncomingAdds").Return(nil)
-}
-
-type msgMatcher func([]lnwire.Message) bool
-
-func singleMsgMatcher[M lnwire.Message](f func(M) bool) msgMatcher {
-	return func(msgs []lnwire.Message) bool {
-		if len(msgs) != 1 {
-			return false
-		}
-
-		wireMsg := msgs[0]
-
-		msg, ok := wireMsg.(M)
-		if !ok {
-			return false
-		}
-
-		if f == nil {
-			return true
-		}
-
-		return f(msg)
-	}
-}
-
-func (r *rbfCloserTestHarness) expectMsgSent(matcher msgMatcher) {
-	r.T.Helper()
-
-	if matcher == nil {
-		r.daemonAdapters.On(
-			"SendMessages", r.peerPub, mock.Anything,
-		).Return(nil)
-	} else {
-		r.daemonAdapters.On(
-			"SendMessages", r.peerPub, mock.MatchedBy(matcher),
-		).Return(nil)
-	}
-}
-
-func (r *rbfCloserTestHarness) expectFeeEstimate(absoluteFee btcutil.Amount,
-	numTimes int) {
-
-	r.T.Helper()
-
-	// TODO(roasbeef): mo assertions for dust case
-
-	r.feeEstimator.On(
-		"EstimateFee", mock.Anything, mock.Anything, mock.Anything,
-		mock.Anything,
-	).Return(absoluteFee, nil).Times(numTimes)
-}
-
-func (r *rbfCloserTestHarness) expectFailure(err error) {
-	r.T.Helper()
-
-	errorMatcher := func(e error) bool {
-		return errors.Is(e, err)
-	}
-
-	r.errReporter.On("ReportError", mock.MatchedBy(errorMatcher)).Return()
-}
-
-func (r *rbfCloserTestHarness) expectNewCloseSig(
-	localScript, remoteScript []byte, fee btcutil.Amount,
-	closeBalance btcutil.Amount) {
-
-	r.T.Helper()
-
-	r.signer.On(
-		"CreateCloseProposal", fee, localScript, remoteScript,
-		mock.Anything,
-	).Return(&localSig, &localTx, closeBalance, nil)
-}
-
-func (r *rbfCloserTestHarness) waitForMsgSent() {
-	r.T.Helper()
-
-	err := wait.Predicate(func() bool {
-		return r.daemonAdapters.msgSent.Load()
-	}, time.Second*3)
-	require.NoError(r.T, err)
-}
-
-func (r *rbfCloserTestHarness) expectRemoteCloseFinalized(
-	localCoopSig, remoteCoopSig input.Signature, localScript,
-	remoteScript []byte, fee btcutil.Amount,
-	balanceAfterClose btcutil.Amount, isLocal bool) {
-
-	r.expectNewCloseSig(
-		localScript, remoteScript, fee, balanceAfterClose,
-	)
-
-	r.expectCloseFinalized(
-		localCoopSig, remoteCoopSig, localScript, remoteScript,
-		fee, balanceAfterClose, isLocal,
-	)
-
-	r.expectMsgSent(singleMsgMatcher[*lnwire.ClosingSig](nil))
-}
-
-func (r *rbfCloserTestHarness) expectCloseFinalized(
-	localCoopSig, remoteCoopSig input.Signature, localScript,
-	remoteScript []byte, fee btcutil.Amount,
-	balanceAfterClose btcutil.Amount, isLocal bool) {
-
-	// The caller should obtain the final signature.
-	r.signer.On("CompleteCooperativeClose",
-		localCoopSig, remoteCoopSig, localScript,
-		remoteScript, fee, mock.Anything,
-	).Return(closeTx, balanceAfterClose, nil)
-
-	// The caller should also mark the transaction as broadcast on disk.
-	r.chanObserver.On("MarkCoopBroadcasted", closeTx, isLocal).Return(nil)
-
-	// Finally, we expect that the daemon executor should broadcast the
-	// above transaction.
-	r.daemonAdapters.On(
-		"BroadcastTransaction", closeTx, mock.Anything,
-	).Return(nil)
-}
-
-func (r *rbfCloserTestHarness) expectChanPendingClose() {
-	var nilTx *wire.MsgTx
-	r.chanObserver.On("MarkCoopBroadcasted", nilTx, true).Return(nil)
-}
-
-func (r *rbfCloserTestHarness) assertLocalClosePending() {
-	// We should then remain in the outer close negotiation state.
-	r.assertStateTransitions(&ClosingNegotiation{})
-
-	// If we examine the final resting state, we should see that the we're
-	// now in the negotiation state still for our local peer state.
-	currentState := assertStateT[*ClosingNegotiation](r)
-
-	// From this, we'll assert the resulting peer state, and that the co-op
-	// close txn is known.
-	closePendingState, ok := currentState.PeerState.GetForParty(
-		lntypes.Local,
-	).(*ClosePending)
-	require.True(r.T, ok)
-
-	require.Equal(r.T, closeTx, closePendingState.CloseTx)
-}
-
-type dustExpectation uint
-
-const (
-	noDustExpect dustExpectation = iota
-
-	localDustExpect
-
-	remoteDustExpect
-)
-
-func (d dustExpectation) String() string {
-	switch d {
-	case noDustExpect:
-		return "no dust"
-	case localDustExpect:
-		return "local dust"
-	case remoteDustExpect:
-		return "remote dust"
-	default:
-		return "unknown"
-	}
-}
-
-// expectHalfSignerIteration asserts that we carry out 1/2 of the locally
-// initiated signer iteration. This constitutes sending a ClosingComplete
-// message to the remote party, and all the other intermediate steps.
-func (r *rbfCloserTestHarness) expectHalfSignerIteration(
-	initEvent ProtocolEvent, balanceAfterClose, absoluteFee btcutil.Amount,
-	dustExpect dustExpectation) {
-
-	numFeeCalls := 2
-
-	// If we're using the SendOfferEvent as a trigger, we only need to call
-	// the few estimation once.
-	if _, ok := initEvent.(*SendOfferEvent); ok {
-		numFeeCalls = 1
-	}
-
-	// We'll now expect that fee estimation is called, and then
-	// send in the flushed event. We expect two calls: one to
-	// figure out if we can pay the fee, and then another when we
-	// actually pay the fee.
-	r.expectFeeEstimate(absoluteFee, numFeeCalls)
-
-	// Next, we'll assert that we receive calls to generate a new
-	// commitment signature, and then send out the commit
-	// ClosingComplete message to the remote peer.
-	r.expectNewCloseSig(
-		localAddr, remoteAddr, absoluteFee, balanceAfterClose,
-	)
-
-	// We expect that only the closer and closee sig is set as both
-	// parties have a balance.
-	msgExpect := singleMsgMatcher(func(m *lnwire.ClosingComplete) bool {
-		r.T.Helper()
-
-		switch {
-		case m.CloserNoClosee.IsSome():
-			r.T.Logf("closer no closee field set, expected: %v",
-				dustExpect)
-
-			return dustExpect == remoteDustExpect
-		case m.NoCloserClosee.IsSome():
-			r.T.Logf("no close closee field set, expected: %v",
-				dustExpect)
-
-			return dustExpect == localDustExpect
-		default:
-			r.T.Logf("no dust field set, expected: %v", dustExpect)
-
-			return (m.CloserAndClosee.IsSome() &&
-				dustExpect == noDustExpect)
-		}
-	})
-	r.expectMsgSent(msgExpect)
-
-	r.chanCloser.SendEvent(initEvent)
-
-	// Based on the init event, we'll either just go to the closing
-	// negotiation state, or go through the channel flushing state first.
-	var expectedStates []RbfState
-	switch initEvent.(type) {
-	case *ShutdownReceived:
-		expectedStates = []RbfState{
-			&ChannelFlushing{}, &ClosingNegotiation{},
-		}
-
-	case *SendOfferEvent:
-
-		expectedStates = []RbfState{&ClosingNegotiation{}}
-	case *ChannelFlushed:
-		// If we're sending a flush event here, then this means that we
-		// also have enough balance to cover the fee so we'll have
-		// another inner transition to the negotiation state.
-		expectedStates = []RbfState{
-			&ClosingNegotiation{}, &ClosingNegotiation{},
-		}
-
-	default:
-		r.T.Fatalf("unknown event type: %T", initEvent)
-	}
-
-	// We should transition from the negotiation state back to
-	// itself.
-	//
-	// TODO(roasbeef): take in expected set of transitions!!!
-	//  * or base off of event, if shutdown recv'd know we're doing a full
-	//  loop
-	r.assertStateTransitions(expectedStates...)
-
-	// If we examine the final resting state, we should see that
-	// the we're now in the LocalOffersSent for our local peer
-	// state.
-	currentState := assertStateT[*ClosingNegotiation](r)
-
-	// From this, we'll assert the resulting peer state.
-	offerSentState, ok := currentState.PeerState.GetForParty(
-		lntypes.Local,
-	).(*LocalOfferSent)
-	require.True(r.T, ok)
-
-	// The proposed fee, as well as our local signature should be
-	// properly stashed in the state.
-	require.Equal(r.T, absoluteFee, offerSentState.ProposedFee)
-	require.Equal(r.T, localSigWire, offerSentState.LocalSig)
-}
-
-func (r *rbfCloserTestHarness) assertSingleRbfIteration(
-	initEvent ProtocolEvent, balanceAfterClose, absoluteFee btcutil.Amount,
-	dustExpect dustExpectation) {
-
-	// We'll now send in the send offer event, which should trigger 1/2 of
-	// the RBF loop, ending us in the LocalOfferSent state.
-	r.expectHalfSignerIteration(
-		initEvent, balanceAfterClose, absoluteFee, noDustExpect,
-	)
-
-	// Now that we're in the local offer sent state, we'll send the
-	// response of the remote party, which completes one iteration
-	localSigEvent := &LocalSigReceived{
-		SigMsg: lnwire.ClosingSig{
-			ClosingSigs: lnwire.ClosingSigs{
-				CloserAndClosee: newSigTlv[tlv.TlvType3](
-					remoteWireSig,
-				),
-			},
-		},
-	}
-
-	// Before we send the event, we expect the close the final signature to
-	// be combined/obtained, and for the close to finalized on disk.
-	r.expectCloseFinalized(
-		&localSig, &remoteSig, localAddr, remoteAddr, absoluteFee,
-		balanceAfterClose, true,
-	)
-
-	r.chanCloser.SendEvent(localSigEvent)
-
-	// We should transition to the pending closing state now.
-	r.assertLocalClosePending()
-}
-
-func (r *rbfCloserTestHarness) assertSingleRemoteRbfIteration(
-	initEvent ProtocolEvent, balanceAfterClose, absoluteFee btcutil.Amount,
-	sequence uint32, iteration bool) {
-
-	// If this is an iteration, then we expect some intermediate states,
-	// before we enter the main RBF/sign loop.
-	if iteration {
-		r.expectFeeEstimate(absoluteFee, 1)
-
-		r.assertStateTransitions(
-			&ChannelActive{}, &ShutdownPending{},
-			&ChannelFlushing{}, &ClosingNegotiation{},
-		)
-	}
-
-	// When we receive the signature below, our local state machine should
-	// move to finalize the close.
-	r.expectRemoteCloseFinalized(
-		&localSig, &remoteSig, localAddr, remoteAddr,
-		absoluteFee, balanceAfterClose, false,
-	)
-
-	r.chanCloser.SendEvent(initEvent)
-
-	// Our outer state should transition to ClosingNegotiation state.
-	r.assertStateTransitions(&ClosingNegotiation{})
-
-	// If we examine the final resting state, we should see that the we're
-	// now in the ClosePending state for the remote peer.
-	currentState := assertStateT[*ClosingNegotiation](r)
-
-	// From this, we'll assert the resulting peer state.
-	pendingState, ok := currentState.PeerState.GetForParty(
-		lntypes.Remote,
-	).(*ClosePending)
-	require.True(r.T, ok)
-
-	// The proposed fee, as well as our local signature should be properly
-	// stashed in the state.
-	require.Equal(r.T, closeTx, pendingState.CloseTx)
-}
-
-func assertStateT[T ProtocolState](h *rbfCloserTestHarness) T {
-	h.T.Helper()
-
-	currentState, ok := h.currentState().(T)
-	require.True(h.T, ok)
-
-	return currentState
-}
-
-// newRbfCloserTestHarness creates a new test harness for the RBF closer.
-func newRbfCloserTestHarness(t *testing.T,
-	cfg *harnessCfg) *rbfCloserTestHarness {
-
-	startingHeight := 200
-
-	chanPoint := randOutPoint(t)
-	chanID := lnwire.NewChanIDFromOutPoint(chanPoint)
-
-	scid := lnwire.NewShortChanIDFromInt(rand.Uint64())
-
-	peerPub := randPubKey(t)
-
-	msgMapper := NewRbfMsgMapper(uint32(startingHeight), chanID)
-
-	initialState := cfg.initialState.UnwrapOr(&ChannelActive{})
-
-	defaultFeeRate := chainfee.FeePerKwFloor
-
-	feeEstimator := &mockFeeEstimator{}
-	mockObserver := &mockChanObserver{}
-	errReporter := &mockErrorReporter{}
-	mockSigner := &mockCloseSigner{}
-
-	harness := &rbfCloserTestHarness{
-		T:             t,
-		cfg:           cfg,
-		startingState: initialState,
-		feeEstimator:  feeEstimator,
-		signer:        mockSigner,
-		chanObserver:  mockObserver,
-		errReporter:   errReporter,
-		peerPub:       *peerPub,
-	}
-
-	env := Environment{
-		ChainParams:           chaincfg.RegressionNetParams,
-		ChanPeer:              *peerPub,
-		ChanPoint:             chanPoint,
-		ChanID:                chanID,
-		Scid:                  scid,
-		DefaultFeeRate:        defaultFeeRate.FeePerVByte(),
-		ThawHeight:            cfg.thawHeight,
-		RemoteUpfrontShutdown: cfg.remoteUpfrontAddr,
-		LocalUpfrontShutdown:  cfg.localUpfrontAddr,
-		NewDeliveryScript:     harness.newAddrFunc,
-		FeeEstimator:          feeEstimator,
-		ChanObserver:          mockObserver,
-		CloseSigner:           mockSigner,
-	}
-	harness.env = env
-
-	var pkScript []byte
-	harness.pkScript = pkScript
-
-	spendEvent := protofsm.RegisterSpend[ProtocolEvent]{
-		OutPoint:       chanPoint,
-		HeightHint:     scid.BlockHeight,
-		PkScript:       pkScript,
-		PostSpendEvent: fn.Some[RbfSpendMapper](SpendMapper),
-	}
-
-	daemonAdapters := newDaemonAdapters()
-	harness.daemonAdapters = daemonAdapters
-
-	protoCfg := RbfChanCloserCfg{
-		ErrorReporter: errReporter,
-		Daemon:        daemonAdapters,
-		InitialState:  initialState,
-		Env:           &env,
-		InitEvent:     fn.Some[protofsm.DaemonEvent](&spendEvent),
-		MsgMapper: fn.Some[protofsm.MsgMapper[ProtocolEvent]](
-			msgMapper,
-		),
-		CustomPollInterval: fn.Some(time.Nanosecond),
-	}
-
-	// Before we start we always expect an initial spend event.
-	daemonAdapters.On(
-		"RegisterSpendNtfn", &chanPoint, pkScript, scid.BlockHeight,
-	).Return(nil)
-
-	chanCloser := protofsm.NewStateMachine(protoCfg)
-	chanCloser.Start()
-
-	harness.stateSub = chanCloser.RegisterStateEvents()
-
-	harness.chanCloser = &chanCloser
-
-	return harness
-}
-
-func newCloser(t *testing.T, cfg *harnessCfg) *rbfCloserTestHarness {
-	chanCloser := newRbfCloserTestHarness(t, cfg)
-
-	// We should start in the active state, and have our spend req
-	// daemon event handled.
-	chanCloser.assertStartupAssertions()
-
-	return chanCloser
-}
-
-// TestRbfChannelActiveTransitions tests the transitions of from the
-// ChannelActive state.
-func TestRbfChannelActiveTransitions(t *testing.T) {
-	localAddr := lnwire.DeliveryAddress(bytes.Repeat([]byte{0x01}, 20))
-	remoteAddr := lnwire.DeliveryAddress(bytes.Repeat([]byte{0x02}, 20))
-
-	feeRate := chainfee.SatPerVByte(1000)
-
-	// Test that if a spend event is received, the FSM transitions to the
-	// CloseFin terminal state.
-	t.Run("spend_event", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{
-			localUpfrontAddr: fn.Some(localAddr),
-		})
-		defer closeHarness.stopAndAssert()
-
-		closeHarness.chanCloser.SendEvent(&SpendEvent{})
-
-		closeHarness.assertStateTransitions(&CloseFin{})
-	})
-
-	// If we send in a local shutdown event, but fail to get an addr, the
-	// state machine should terminate.
-	t.Run("local_initiated_close_addr_fail", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{})
-		defer closeHarness.stopAndAssert()
-
-		closeHarness.failNewAddrFunc()
-		closeHarness.expectFailure(errfailAddr)
-
-		// We don't specify an upfront shutdown addr, and don't specify
-		// on here in the vent, so we should call new addr, but then
-		// fail.
-		closeHarness.chanCloser.SendEvent(&SendShutdown{})
-
-		// We shouldn't have transitioned to a new state.
-		closeHarness.assertNoStateTransitions()
-	})
-
-	// Initiating the shutdown should have us transition to the shutdown
-	// pending state. We should also emit events to disable the channel,
-	// and also send a message to our target peer.
-	t.Run("local_initiated_close_ok", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{
-			localUpfrontAddr: fn.Some(localAddr),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// Once we send the event below, we should get calls to the
-		// chan observer, the msg sender, and the link control.
-		closeHarness.expectShutdownEvents(shutdownExpect{
-			isInitiator: true,
-			allowSend:   true,
-		})
-		closeHarness.expectMsgSent(nil)
-
-		// If we send the shutdown event, we should transition to the
-		// shutdown pending state.
-		closeHarness.chanCloser.SendEvent(&SendShutdown{
-			IdealFeeRate: feeRate,
-		})
-		closeHarness.assertStateTransitions(&ShutdownPending{})
-
-		// If we examine the internal state, it should be consistent
-		// with the fee+addr we sent in.
-		currentState := assertStateT[*ShutdownPending](closeHarness)
-
-		require.Equal(
-			t, feeRate, currentState.IdealFeeRate.UnsafeFromSome(),
-		)
-		require.Equal(
-			t, localAddr,
-			currentState.ShutdownScripts.LocalDeliveryScript,
-		)
-
-		// Wait till the msg has been sent to assert our expectations.
-		//
-		// TODO(roasbeef): can use call.WaitFor here?
-		closeHarness.waitForMsgSent()
-	})
-
-	// TODO(roasbeef): thaw height fail
-
-	// When we receive a shutdown, we should transition to the shutdown
-	// pending state, with the local+remote shutdown addrs known.
-	t.Run("remote_initiated_close_ok", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{
-			localUpfrontAddr: fn.Some(localAddr),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We assert our shutdown events, and also that we eventually
-		// send a shutdown to the remote party. We'll hold back the
-		// send in this case though, as we should only send once the no
-		// updates are dangling.
-		closeHarness.expectShutdownEvents(shutdownExpect{
-			isInitiator:  false,
-			allowSend:    false,
-			recvShutdown: true,
-		})
-
-		// Next, we'll emit the recv event, with the addr of the remote
-		// party.
-		closeHarness.chanCloser.SendEvent(&ShutdownReceived{
-			ShutdownScript: remoteAddr,
-		})
-
-		// We should transition to the shutdown pending state.
-		closeHarness.assertStateTransitions(&ShutdownPending{})
-
-		currentState := assertStateT[*ShutdownPending](closeHarness)
-
-		// Both the local and remote shutdown scripts should be set.
-		require.Equal(
-			t, localAddr,
-			currentState.ShutdownScripts.LocalDeliveryScript,
-		)
-		require.Equal(
-			t, remoteAddr,
-			currentState.ShutdownScripts.RemoteDeliveryScript,
-		)
-	})
-
-	// Any other event should be ignored.
-	assertUnknownEventFail(t, &ChannelActive{})
-}
-
-// TestRbfShutdownPendingTransitions tests the transitions of the RBF closer
-// once we get to the shutdown pending state. In this state, we wait for either
-// a shutdown to be received, or a notification that we're able to send a
-// shutdown ourselves.
-func TestRbfShutdownPendingTransitions(t *testing.T) {
-	t.Parallel()
-
-	startingState := &ShutdownPending{}
-
-	// Test that if a spend event is received, the FSM transitions to the
-	// CloseFin terminal state.
-	t.Run("spend_event", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](
-				startingState,
-			),
-			localUpfrontAddr: fn.Some(localAddr),
-		})
-		defer closeHarness.stopAndAssert()
-
-		closeHarness.chanCloser.SendEvent(&SpendEvent{})
-
-		closeHarness.assertStateTransitions(&CloseFin{})
-	})
-
-	// If the remote party sends us a diff shutdown addr than we expected,
-	// then we'll fail.
-	t.Run("initiator_shutdown_recv_validate_fail", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](
-				startingState,
-			),
-			remoteUpfrontAddr: fn.Some(remoteAddr),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We should fail as the shutdown script isn't what we
-		// expected.
-		closeHarness.expectFailure(ErrUpfrontShutdownScriptMismatch)
-
-		// We'll now send in a ShutdownReceived event, but with a
-		// different address provided in the shutdown message. This
-		// should result in an error.
-		closeHarness.chanCloser.SendEvent(&ShutdownReceived{
-			ShutdownScript: localAddr,
-		})
-
-		// We shouldn't have transitioned to a new state.
-		closeHarness.assertNoStateTransitions()
-	})
-
-	// Otherwise, if the shutdown is well composed, then we should
-	// transition to the ChannelFlushing state.
-	t.Run("initiator_shutdown_recv_ok", func(t *testing.T) {
-		firstState := *startingState
-		firstState.IdealFeeRate = fn.Some(
-			chainfee.FeePerKwFloor.FeePerVByte(),
-		)
-		firstState.ShutdownScripts = ShutdownScripts{
-			LocalDeliveryScript:  localAddr,
-			RemoteDeliveryScript: remoteAddr,
-		}
-
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](
-				&firstState,
-			),
-			localUpfrontAddr:  fn.Some(localAddr),
-			remoteUpfrontAddr: fn.Some(remoteAddr),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We should disable the outgoing adds for the channel at this
-		// point as well.
-		closeHarness.expectFinalBalances(fn.None[ShutdownBalances]())
-		closeHarness.expectIncomingAddsDisabled()
-
-		// We'll send in a shutdown received event, with the expected
-		// co-op close addr.
-		closeHarness.chanCloser.SendEvent(&ShutdownReceived{
-			ShutdownScript: remoteAddr,
-		})
-
-		// We should transition to the channel flushing state.
-		closeHarness.assertStateTransitions(&ChannelFlushing{})
-
-		// Now we'll ensure that the flushing state has the proper
-		// co-op close state.
-		currentState := assertStateT[*ChannelFlushing](closeHarness)
-
-		require.Equal(t, localAddr, currentState.LocalDeliveryScript)
-		require.Equal(t, remoteAddr, currentState.RemoteDeliveryScript)
-		require.Equal(
-			t, firstState.IdealFeeRate, currentState.IdealFeeRate,
-		)
-	})
-
-	// If we received the shutdown event, then we'll rely on the external
-	// signal that we were able to send the message once there were no
-	// updates dangling.
-	t.Run("responder_complete", func(t *testing.T) {
-		firstState := *startingState
-		firstState.IdealFeeRate = fn.Some(
-			chainfee.FeePerKwFloor.FeePerVByte(),
-		)
-		firstState.ShutdownScripts = ShutdownScripts{
-			LocalDeliveryScript:  localAddr,
-			RemoteDeliveryScript: remoteAddr,
-		}
-
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](
-				&firstState,
-			),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// In this case we're doing the shutdown dance for the first
-		// time, so we'll mark the channel as not being flushed.
-		closeHarness.expectFinalBalances(fn.None[ShutdownBalances]())
-
-		// We'll send in a shutdown received event.
-		closeHarness.chanCloser.SendEvent(&ShutdownComplete{})
-
-		// We should transition to the channel flushing state.
-		closeHarness.assertStateTransitions(&ChannelFlushing{})
-	})
-
-	// Any other event should be ignored.
-	assertUnknownEventFail(t, startingState)
-}
-
-// TestRbfChannelFlushingTransitions tests the transitions of the RBF closer
-// for the channel flushing state. Once the coast is clear, we should
-// transition to the negotiation state.
-func TestRbfChannelFlushingTransitions(t *testing.T) {
-	t.Parallel()
-
-	localBalance := lnwire.NewMSatFromSatoshis(10_000)
-	remoteBalance := lnwire.NewMSatFromSatoshis(50_000)
-
-	absoluteFee := btcutil.Amount(10_100)
-
-	startingState := &ChannelFlushing{
-		ShutdownScripts: ShutdownScripts{
-			LocalDeliveryScript:  localAddr,
-			RemoteDeliveryScript: remoteAddr,
-		},
-	}
-
-	flushTemplate := &ChannelFlushed{
-		ShutdownBalances: ShutdownBalances{
-			LocalBalance:  localBalance,
-			RemoteBalance: remoteBalance,
-		},
-	}
-
-	// If send in the channel flushed event, but the local party can't pay
-	// for fees, then we should just head to the negotiation state.
-	for _, isFreshFlush := range []bool{true, false} {
-		chanFlushedEvent := *flushTemplate
-		chanFlushedEvent.FreshFlush = isFreshFlush
-
-		testName := fmt.Sprintf("local_cannot_pay_for_fee/"+
-			"fresh_flush=%v", isFreshFlush)
-
-		t.Run(testName, func(t *testing.T) {
-			firstState := *startingState
-
-			closeHarness := newCloser(t, &harnessCfg{
-				initialState: fn.Some[ProtocolState](
-					&firstState,
-				),
-			})
-			defer closeHarness.stopAndAssert()
-
-			// As part of the set up for this state, we'll have the
-			// final absolute fee required be greater than the
-			// balance of the local party.
-			closeHarness.expectFeeEstimate(absoluteFee, 1)
-
-			// If this is a fresh flush, then we expect the state
-			// to be marked on disk.
-			if isFreshFlush {
-				closeHarness.expectChanPendingClose()
-			}
-
-			// We'll now send in the event which should trigger
-			// this code path.
-			closeHarness.chanCloser.SendEvent(&chanFlushedEvent)
-
-			// With the event sent, we should now transition
-			// straight to the ClosingNegotiation state, with no
-			// further state transitions.
-			closeHarness.assertStateTransitions(
-				&ClosingNegotiation{},
-			)
-		})
-	}
-
-	for _, isFreshFlush := range []bool{true, false} {
-		flushEvent := *flushTemplate
-		flushEvent.FreshFlush = isFreshFlush
-
-		// We'll modify the starting balance to be 3x the required fee
-		// to ensure that we can pay for the fee.
-		flushEvent.ShutdownBalances.LocalBalance = lnwire.NewMSatFromSatoshis( //nolint:ll
-			absoluteFee * 3,
-		)
-
-		testName := fmt.Sprintf("local_can_pay_for_fee/"+
-			"fresh_flush=%v", isFreshFlush)
-
-		// This scenario, we'll have the local party be able to pay for
-		// the fees, which will trigger additional state transitions.
-		t.Run(testName, func(t *testing.T) {
-			firstState := *startingState
-
-			closeHarness := newCloser(t, &harnessCfg{
-				initialState: fn.Some[ProtocolState](
-					&firstState,
-				),
-			})
-			defer closeHarness.stopAndAssert()
-
-			localBalance := flushEvent.ShutdownBalances.LocalBalance
-			balanceAfterClose := localBalance.ToSatoshis() - absoluteFee //nolint:ll
-
-			// If this is a fresh flush, then we expect the state
-			// to be marked on disk.
-			if isFreshFlush {
-				closeHarness.expectChanPendingClose()
-			}
-
-			// From where, we expect the state transition to go
-			// back to closing negotiated, for a ClosingComplete
-			// message to be sent and then for us to terminate at
-			// that state. This is 1/2 of the normal RBF signer
-			// flow.
-			closeHarness.expectHalfSignerIteration(
-				&flushEvent, balanceAfterClose, absoluteFee,
-				noDustExpect,
-			)
-		})
-	}
-
-	// Any other event should be ignored.
-	assertUnknownEventFail(t, startingState)
-}
-
-// TestRbfCloseClosingNegotiationLocal tests the local portion of the primary
-// RBF close loop. We should be able to transition to a close state, get a sig,
-// then restart all over again to re-request a signature of at new higher fee
-// rate.
-func TestRbfCloseClosingNegotiationLocal(t *testing.T) {
-	t.Parallel()
-
-	localBalance := lnwire.NewMSatFromSatoshis(40_000)
-	remoteBalance := lnwire.NewMSatFromSatoshis(50_000)
-
-	absoluteFee := btcutil.Amount(10_100)
-
-	closeTerms := &CloseChannelTerms{
-		ShutdownBalances: ShutdownBalances{
-			LocalBalance:  localBalance,
-			RemoteBalance: remoteBalance,
-		},
-		ShutdownScripts: ShutdownScripts{
-			LocalDeliveryScript:  localAddr,
-			RemoteDeliveryScript: remoteAddr,
-		},
-	}
-	startingState := &ClosingNegotiation{
-		PeerState: lntypes.Dual[AsymmetricPeerState]{
-			Local: &LocalCloseStart{
-				CloseChannelTerms: *closeTerms,
-			},
-		},
-	}
-
-	sendOfferEvent := &SendOfferEvent{
-		TargetFeeRate: chainfee.FeePerKwFloor.FeePerVByte(),
-	}
-
-	balanceAfterClose := localBalance.ToSatoshis() - absoluteFee
-
-	// In this state, we'll simulate deciding that we need to send a new
-	// offer to the remote party.
-	t.Run("send_offer_iteration_no_dust", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](startingState),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We'll now send in the initial sender offer event, which
-		// should then trigger a single RBF iteration, ending at the
-		// pending state.
-		closeHarness.assertSingleRbfIteration(
-			sendOfferEvent, balanceAfterClose, absoluteFee,
-			noDustExpect,
-		)
-	})
-
-	// We'll run a similar test as above, but verify that if more than one
-	// sig field is set, we error out.
-	t.Run("send_offer_too_many_sigs_received", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](startingState),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We'll kick off the test as normal, triggering a send offer
-		// event to advance the state machine.
-		closeHarness.expectHalfSignerIteration(
-			sendOfferEvent, balanceAfterClose, absoluteFee,
-			noDustExpect,
-		)
-
-		// We'll now craft the local sig received event, but this time
-		// we'll specify 2 signature fields.
-		localSigEvent := &LocalSigReceived{
-			SigMsg: lnwire.ClosingSig{
-				ClosingSigs: lnwire.ClosingSigs{
-					CloserNoClosee: newSigTlv[tlv.TlvType1](
-						remoteWireSig,
-					),
-					CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
-						remoteWireSig,
-					),
-				},
-			},
-		}
-
-		// We expect that the state machine fails as we received more
-		// than one error.
-		closeHarness.expectFailure(ErrTooManySigs)
-
-		// We should fail as the remote party sent us more than one
-		// signature.
-		closeHarness.chanCloser.SendEvent(localSigEvent)
-	})
-
-	// Next, we'll verify that if the balance of the remote party is dust,
-	// then the proper sig field is set.
-	t.Run("send_offer_iteration_remote_dust", func(t *testing.T) {
-		// We'll modify the starting state to reduce the balance of the
-		// remote party to something that'll be dust.
-		newCloseTerms := *closeTerms
-		newCloseTerms.ShutdownBalances.RemoteBalance = 100
-		firstState := &ClosingNegotiation{
-			PeerState: lntypes.Dual[AsymmetricPeerState]{
-				Local: &LocalCloseStart{
-					CloseChannelTerms: newCloseTerms,
-				},
-			},
-		}
-
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](firstState),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We'll kick off the half iteration as normal, but this time
-		// we expect that the remote party's output is dust, so the
-		// proper field is set.
-		closeHarness.expectHalfSignerIteration(
-			sendOfferEvent, balanceAfterClose, absoluteFee,
-			remoteDustExpect,
-		)
-	})
-
-	// Similarly, we'll verify that if our final closing balance is dust,
-	// then we send the sig that omits our output.
-	t.Run("send_offer_iteration_local_dust", func(t *testing.T) {
-		firstState := &ClosingNegotiation{
-			PeerState: lntypes.Dual[AsymmetricPeerState]{
-				Local: &LocalCloseStart{
-					CloseChannelTerms: *closeTerms,
-				},
-			},
-		}
-
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](firstState),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We'll kick off the half iteration as normal, but this time
-		// we'll have our balance after close be dust, so we expect
-		// that the local output is dust in the sig we send.
-		dustBalance := btcutil.Amount(100)
-		closeHarness.expectHalfSignerIteration(
-			sendOfferEvent, dustBalance, absoluteFee,
-			localDustExpect,
-		)
-	})
-
-	// In this test, we'll assert that we're able to restart the RBF loop
-	// to trigger additional signature iterations.
-	t.Run("send_offer_rbf_iteration_loop", func(t *testing.T) {
-		firstState := &ClosingNegotiation{
-			PeerState: lntypes.Dual[AsymmetricPeerState]{
-				Local: &LocalCloseStart{
-					CloseChannelTerms: *closeTerms,
-				},
-			},
-		}
-
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState:     fn.Some[ProtocolState](firstState),
-			localUpfrontAddr: fn.Some(localAddr),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We'll start out by first triggering a routine iteration,
-		// assuming we start in this negotiation state.
-		closeHarness.assertSingleRbfIteration(
-			sendOfferEvent, balanceAfterClose, absoluteFee,
-			noDustExpect,
-		)
-
-		// Next, we'll send in a new SendShutdown event which simulates
-		// the user requesting a RBF fee bump. We'll use 10x the fee we
-		// used in the last iteration.
-		rbfFeeBump := chainfee.FeePerKwFloor.FeePerVByte() * 10
-		sendShutdown := &SendShutdown{
-			IdealFeeRate: rbfFeeBump,
-		}
-
-		// We should send shutdown as normal, but skip some other
-		// checks as we know the close is in progress.
-		closeHarness.expectShutdownEvents(shutdownExpect{
-			allowSend:     true,
-			finalBalances: fn.Some(closeTerms.ShutdownBalances),
-			recvShutdown:  true,
-		})
-		closeHarness.expectMsgSent(
-			singleMsgMatcher[*lnwire.Shutdown](nil),
-		)
-
-		closeHarness.chanCloser.SendEvent(sendShutdown)
-
-		// We should first transition to the Channel Active state
-		// momentarily, before transitioning to the shutdown pending
-		// state.
-		closeHarness.assertStateTransitions(
-			&ChannelActive{}, &ShutdownPending{},
-		)
-
-		// Next, we'll send in the shutdown received event, which
-		// should transition us to the channel flushing state.
-		shutdownEvent := &ShutdownReceived{
-			ShutdownScript: remoteAddr,
-		}
-
-		// Now we expect that aanother full RBF iteration takes place
-		// (we initiatea a new local sig).
-		closeHarness.assertSingleRbfIteration(
-			shutdownEvent, balanceAfterClose, absoluteFee,
-			noDustExpect,
-		)
-
-		// We should terminate in the negotiation state.
-		closeHarness.assertStateTransitions(
-			&ClosingNegotiation{},
-		)
-	})
-}
-
-// TestRbfCloseClosingNegotiationRemote tests that state machine is able to
-// handle RBF iterations to sign for the closing transaction of the remote
-// party.
-func TestRbfCloseClosingNegotiationRemote(t *testing.T) {
-	t.Parallel()
-
-	localBalance := lnwire.NewMSatFromSatoshis(40_000)
-	remoteBalance := lnwire.NewMSatFromSatoshis(50_000)
-
-	absoluteFee := btcutil.Amount(10_100)
-
-	closeTerms := &CloseChannelTerms{
-		ShutdownBalances: ShutdownBalances{
-			LocalBalance:  localBalance,
-			RemoteBalance: remoteBalance,
-		},
-		ShutdownScripts: ShutdownScripts{
-			LocalDeliveryScript:  localAddr,
-			RemoteDeliveryScript: remoteAddr,
-		},
-	}
-	startingState := &ClosingNegotiation{
-		PeerState: lntypes.Dual[AsymmetricPeerState]{
-			Local: &LocalCloseStart{
-				CloseChannelTerms: *closeTerms,
-			},
-			Remote: &RemoteCloseStart{
-				CloseChannelTerms: *closeTerms,
-			},
-		},
-	}
-
-	balanceAfterClose := remoteBalance.ToSatoshis() - absoluteFee
-
-	sequence := uint32(mempool.MaxRBFSequence)
-
-	// This case tests that if we receive a signature from the remote
-	// party, where they can't pay for the fees, we exit.
-	t.Run("recv_offer_cannot_pay_for_fees", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](startingState),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We should fail as they sent a sig, but can't pay for fees.
-		closeHarness.expectFailure(ErrRemoteCannotPay)
-
-		// We'll send in a new fee proposal, but the proposed fee will
-		// be higher than the remote party's balance.
-		feeOffer := &OfferReceivedEvent{
-			SigMsg: lnwire.ClosingComplete{
-				FeeSatoshis: absoluteFee * 10,
-			},
-		}
-		closeHarness.chanCloser.SendEvent(feeOffer)
-
-		// We shouldn't have transitioned to a new state.
-		closeHarness.assertNoStateTransitions()
-	})
-
-	// If our balance, is dust, then the remote party should send a
-	// signature that doesn't include our output.
-	t.Run("recv_offer_err_closer_no_closee", func(t *testing.T) {
-		// We'll modify our local balance to be dust.
-		closingTerms := *closeTerms
-		closingTerms.ShutdownBalances.LocalBalance = 100
-
-		firstState := &ClosingNegotiation{
-			PeerState: lntypes.Dual[AsymmetricPeerState]{
-				Local: &LocalCloseStart{
-					CloseChannelTerms: closingTerms,
-				},
-				Remote: &RemoteCloseStart{
-					CloseChannelTerms: closingTerms,
-				},
-			},
-		}
-
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](firstState),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We should fail as they included the wrong sig.
-		closeHarness.expectFailure(ErrCloserNoClosee)
-
-		// Our balance is dust, so we should reject this signature that
-		// includes our output.
-		feeOffer := &OfferReceivedEvent{
-			SigMsg: lnwire.ClosingComplete{
-				FeeSatoshis: absoluteFee,
-				ClosingSigs: lnwire.ClosingSigs{
-					CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
-						remoteWireSig,
-					),
-				},
-			},
-		}
-		closeHarness.chanCloser.SendEvent(feeOffer)
-
-		// We shouldn't have transitioned to a new state.
-		closeHarness.assertNoStateTransitions()
-	})
-
-	// If no balances are dust, then they should send a sig covering both
-	// outputs.
-	t.Run("recv_offer_err_closer_and_closee", func(t *testing.T) {
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState: fn.Some[ProtocolState](startingState),
-		})
-		defer closeHarness.stopAndAssert()
-
-		// We should fail as they included the wrong sig.
-		closeHarness.expectFailure(ErrCloserAndClosee)
-
-		// Both balances are above dust, we should reject this
-		// signature as it excludes an output.
-		feeOffer := &OfferReceivedEvent{
-			SigMsg: lnwire.ClosingComplete{
-				FeeSatoshis: absoluteFee,
-				ClosingSigs: lnwire.ClosingSigs{
-					CloserNoClosee: newSigTlv[tlv.TlvType1]( //nolint:ll
-						remoteWireSig,
-					),
-				},
-			},
-		}
-		closeHarness.chanCloser.SendEvent(feeOffer)
-
-		// We shouldn't have transitioned to a new state.
-		closeHarness.assertNoStateTransitions()
-	})
-
-	// If everything lines up, then we should be able to do multiple RBF
-	// loops to enable the remote party to sign.new versions of the co-op
-	// close transaction.
-	t.Run("recv_offer_rbf_loop_iterations", func(t *testing.T) {
-		// We'll modify our s.t we're unable to pay for fees, but
-		// aren't yet dust.
-		closingTerms := *closeTerms
-		closingTerms.ShutdownBalances.LocalBalance = lnwire.NewMSatFromSatoshis( //nolint:ll
-			9000,
-		)
-
-		firstState := &ClosingNegotiation{
-			PeerState: lntypes.Dual[AsymmetricPeerState]{
-				Local: &LocalCloseStart{
-					CloseChannelTerms: closingTerms,
-				},
-				Remote: &RemoteCloseStart{
-					CloseChannelTerms: closingTerms,
-				},
-			},
-		}
-
-		closeHarness := newCloser(t, &harnessCfg{
-			initialState:     fn.Some[ProtocolState](firstState),
-			localUpfrontAddr: fn.Some(localAddr),
-		})
-		defer closeHarness.stopAndAssert()
-
-		feeOffer := &OfferReceivedEvent{
-			SigMsg: lnwire.ClosingComplete{
-				FeeSatoshis: absoluteFee,
-				LockTime:    1,
-				ClosingSigs: lnwire.ClosingSigs{
-					CloserAndClosee: newSigTlv[tlv.TlvType3]( //nolint:ll
-						remoteWireSig,
-					),
-				},
-			},
-		}
-
-		// As we're already in the negotiation phase, we'll now trigger
-		// a new iteration by having the remote party send a new offer
-		// sig.
-		closeHarness.assertSingleRemoteRbfIteration(
-			feeOffer, balanceAfterClose, absoluteFee, sequence,
-			false,
-		)
-
-		// At this point, we've completed a single RBF iteration, and
-		// want to test further iterations, so we'll use a shutdown
-		// even tot kick it all off.
-		//
-		// Before we send the shutdown messages below, we'll mark the
-		// balances as so we fast track to the negotiation state.
-		closeHarness.expectShutdownEvents(shutdownExpect{
-			allowSend:     true,
-			finalBalances: fn.Some(closingTerms.ShutdownBalances),
-			recvShutdown:  true,
-		})
-		closeHarness.expectMsgSent(
-			singleMsgMatcher[*lnwire.Shutdown](nil),
-		)
-
-		// We'll now simulate the start of the RBF loop, by receiving a
-		// new Shutdown message from the remote party. This signals
-		// that they want to obtain a new commit sig.
-		closeHarness.chanCloser.SendEvent(&ShutdownReceived{
-			ShutdownScript: remoteAddr,
-		})
-
-		// Next, we'll receive an offer from the remote party, and
-		// drive another RBF iteration. This time, we'll increase the
-		// absolute fee by 1k sats.
-		feeOffer.SigMsg.FeeSatoshis += 1000
-		absoluteFee = feeOffer.SigMsg.FeeSatoshis
-		closeHarness.assertSingleRemoteRbfIteration(
-			feeOffer, balanceAfterClose, absoluteFee, sequence,
-			true,
-		)
-
-		closeHarness.assertNoStateTransitions()
-	})
-
-	// TODO(roasbeef): cross sig case? tested isolation, so wolog?
-}
diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go
index 9da174a3d..d0caa9781 100644
--- a/lnwallet/channel_test.go
+++ b/lnwallet/channel_test.go
@@ -770,66 +770,29 @@ func TestCommitHTLCSigCustomRecordSize(t *testing.T) {
 }
 
 // TestCooperativeChannelClosure checks that the coop close process finishes
-// with an agreement from both parties, and that the final balances of the close
-// tx check out.
+// with an agreement from both parties, and that the final balances of the
+// close tx check out.
 func TestCooperativeChannelClosure(t *testing.T) {
-	testCases := []struct {
-		name      string
-		closeCase coopCloseTestCase
-	}{
-		{
-			name: "tweakless",
-			closeCase: coopCloseTestCase{
-				chanType: channeldb.SingleFunderTweaklessBit,
-			},
-		},
-		{
-			name: "anchors",
-			closeCase: coopCloseTestCase{
-				chanType: channeldb.SingleFunderTweaklessBit |
-					channeldb.AnchorOutputsBit,
-				anchorAmt: AnchorSize * 2,
-			},
-		},
-		{
-			name: "anchors local pay",
-			closeCase: coopCloseTestCase{
-				chanType: channeldb.SingleFunderTweaklessBit |
-					channeldb.AnchorOutputsBit,
-				anchorAmt:   AnchorSize * 2,
-				customPayer: fn.Some(lntypes.Local),
-			},
-		},
-		{
-			name: "anchors remote pay",
-			closeCase: coopCloseTestCase{
-				chanType: channeldb.SingleFunderTweaklessBit |
-					channeldb.AnchorOutputsBit,
-				anchorAmt:   AnchorSize * 2,
-				customPayer: fn.Some(lntypes.Remote),
-			},
-		},
-	}
-	for _, testCase := range testCases {
-		t.Run(testCase.name, func(t *testing.T) {
-			testCoopClose(t, testCase.closeCase)
+	t.Run("tweakless", func(t *testing.T) {
+		testCoopClose(t, &coopCloseTestCase{
+			chanType: channeldb.SingleFunderTweaklessBit,
 		})
-	}
+	})
+	t.Run("anchors", func(t *testing.T) {
+		testCoopClose(t, &coopCloseTestCase{
+			chanType: channeldb.SingleFunderTweaklessBit |
+				channeldb.AnchorOutputsBit,
+			anchorAmt: AnchorSize * 2,
+		})
+	})
 }
 
 type coopCloseTestCase struct {
 	chanType  channeldb.ChannelType
 	anchorAmt btcutil.Amount
-
-	customPayer fn.Option[lntypes.ChannelParty]
-}
-
-type closeOpts struct {
-	aliceOpts []ChanCloseOpt
-	bobOpts   []ChanCloseOpt
 }
 
-func testCoopClose(t *testing.T, testCase coopCloseTestCase) {
+func testCoopClose(t *testing.T, testCase *coopCloseTestCase) {
 	t.Parallel()
 
 	// Create a test channel which will be used for the duration of this
@@ -850,38 +813,17 @@ func testCoopClose(t *testing.T, testCase coopCloseTestCase) {
 		bobChannel.channelState.LocalCommitment.FeePerKw,
 	)
 
-	customPayer := testCase.customPayer
-
-	closeOpts := fn.MapOptionZ(
-		customPayer, func(payer lntypes.ChannelParty) closeOpts {
-			// If the local party is paying then from Alice's PoV,
-			// then local party is paying. From Bob's PoV, the
-			// remote party is paying. If the remote party is, then
-			// the opposite is true.
-			return closeOpts{
-				aliceOpts: []ChanCloseOpt{
-					WithCustomPayer(payer),
-				},
-				bobOpts: []ChanCloseOpt{
-					WithCustomPayer(payer.CounterParty()),
-				},
-			}
-		},
-	)
-
 	// We'll start with both Alice and Bob creating a new close proposal
 	// with the same fee.
 	aliceFee := aliceChannel.CalcFee(aliceFeeRate)
 	aliceSig, _, _, err := aliceChannel.CreateCloseProposal(
 		aliceFee, aliceDeliveryScript, bobDeliveryScript,
-		closeOpts.aliceOpts...,
 	)
 	require.NoError(t, err, "unable to create alice coop close proposal")
 
 	bobFee := bobChannel.CalcFee(bobFeeRate)
 	bobSig, _, _, err := bobChannel.CreateCloseProposal(
 		bobFee, bobDeliveryScript, aliceDeliveryScript,
-		closeOpts.bobOpts...,
 	)
 	require.NoError(t, err, "unable to create bob coop close proposal")
 
@@ -890,14 +832,14 @@ func testCoopClose(t *testing.T, testCase coopCloseTestCase) {
 	// transaction is well formed, and the signatures verify.
 	aliceCloseTx, bobTxBalance, err := bobChannel.CompleteCooperativeClose(
 		bobSig, aliceSig, bobDeliveryScript, aliceDeliveryScript,
-		bobFee, closeOpts.bobOpts...,
+		bobFee,
 	)
 	require.NoError(t, err, "unable to complete alice cooperative close")
 	bobCloseSha := aliceCloseTx.TxHash()
 
 	bobCloseTx, aliceTxBalance, err := aliceChannel.CompleteCooperativeClose(
 		aliceSig, bobSig, aliceDeliveryScript, bobDeliveryScript,
-		aliceFee, closeOpts.aliceOpts...,
+		aliceFee,
 	)
 	require.NoError(t, err, "unable to complete bob cooperative close")
 	aliceCloseSha := bobCloseTx.TxHash()
@@ -906,43 +848,18 @@ func testCoopClose(t *testing.T, testCase coopCloseTestCase) {
 		t.Fatalf("alice and bob close transactions don't match: %v", err)
 	}
 
-	type chanFees struct {
-		alice btcutil.Amount
-		bob   btcutil.Amount
-	}
-
-	// Compute the closing fees for each party. If not specified, Alice will
-	// always pay the fees. Otherwise, it depends on who the payer is.
-	closeFees := fn.MapOption(func(payer lntypes.ChannelParty) chanFees {
-		var alice, bob btcutil.Amount
-
-		switch payer {
-		case lntypes.Local:
-			alice = bobFee
-			bob = 0
-		case lntypes.Remote:
-			bob = bobFee
-			alice = 0
-		}
-
-		return chanFees{
-			alice: alice,
-			bob:   bob,
-		}
-	})(testCase.customPayer).UnwrapOr(chanFees{alice: bobFee})
-
-	// Finally, make sure the final balances are correct from both
-	// perspectives.
+	// Finally, make sure the final balances are correct from both's
+	// perspective.
 	aliceBalance := aliceChannel.channelState.LocalCommitment.
 		LocalBalance.ToSatoshis()
 
-	// The commit balance have had the initiator's (Alice) commit fee and
+	// The commit balance have had the initiator's (Alice) commitfee and
 	// any anchors subtracted, so add that back to the final expected
 	// balance. Alice also pays the coop close fee, so that must be
 	// subtracted.
 	commitFee := aliceChannel.channelState.LocalCommitment.CommitFee
 	expBalanceAlice := aliceBalance + commitFee +
-		testCase.anchorAmt - closeFees.alice
+		testCase.anchorAmt - bobFee
 	if aliceTxBalance != expBalanceAlice {
 		t.Fatalf("expected balance %v got %v", expBalanceAlice,
 			aliceTxBalance)
@@ -951,7 +868,7 @@ func testCoopClose(t *testing.T, testCase coopCloseTestCase) {
 	// Bob is not the initiator, so his final balance should simply be
 	// equal to the latest commitment balance.
 	expBalanceBob := bobChannel.channelState.LocalCommitment.
-		LocalBalance.ToSatoshis() - closeFees.bob
+		LocalBalance.ToSatoshis()
 	if bobTxBalance != expBalanceBob {
 		t.Fatalf("expected bob's balance to be %v got %v",
 			expBalanceBob, bobTxBalance)
diff --git a/lnwallet/close_test.go b/lnwallet/close_test.go
deleted file mode 100644
index 7ece3fc45..000000000
--- a/lnwallet/close_test.go
+++ /dev/null
@@ -1,241 +0,0 @@
-package lnwallet
-
-import (
-	"strconv"
-	"testing"
-
-	"github.com/btcsuite/btcd/btcutil"
-	"github.com/lightningnetwork/lnd/channeldb"
-	"github.com/lightningnetwork/lnd/fn/v2"
-	"github.com/lightningnetwork/lnd/lntypes"
-	"github.com/stretchr/testify/require"
-	"pgregory.net/rapid"
-)
-
-// genValidAmount generates valid bitcoin amounts (non-negative).
-func genValidAmount(t *rapid.T, label string) btcutil.Amount {
-	return btcutil.Amount(
-		rapid.Int64Range(
-			100_000, 21_000_000*100_000_000,
-		).Draw(t, label),
-	)
-}
-
-// genCoopCloseFee generates a reasonable non-zero cooperative close fee.
-func genCoopCloseFee(t *rapid.T) btcutil.Amount {
-	// Generate a fee between 250-10000 sats which is a reasonable range for
-	// closing transactions
-	return btcutil.Amount(
-		rapid.Int64Range(250, 10_000).Draw(t, "coop_close_fee"),
-	)
-}
-
-// genChannelType generates various channel types, ensuring good coverage of
-// different channel configurations including anchor outputs and other features.
-func genChannelType(t *rapid.T) channeldb.ChannelType {
-	var chanType channeldb.ChannelType
-
-	// For each bit, decide randomly if it should be set.
-	bits := []channeldb.ChannelType{
-		channeldb.DualFunderBit,
-		channeldb.SingleFunderTweaklessBit,
-		channeldb.NoFundingTxBit,
-		channeldb.AnchorOutputsBit,
-		channeldb.FrozenBit,
-		channeldb.ZeroHtlcTxFeeBit,
-		channeldb.LeaseExpirationBit,
-		channeldb.ZeroConfBit,
-		channeldb.ScidAliasChanBit,
-		channeldb.ScidAliasFeatureBit,
-		channeldb.SimpleTaprootFeatureBit,
-		channeldb.TapscriptRootBit,
-	}
-
-	// Helper to bias towards setting specific bits more frequently.
-	setBit := func(bit channeldb.ChannelType, probability int) {
-		bitRange := rapid.IntRange(0, 100)
-		label := "bit_" + strconv.FormatUint(uint64(bit), 2)
-		if bitRange.Draw(t, label) < probability {
-			chanType |= bit
-		}
-	}
-
-	// We want to ensure good coverage of anchor outputs since they affect
-	// the balance calculation directly. We'll set the anchor bit with a 50%
-	// chance.
-	setBit(channeldb.AnchorOutputsBit, 50)
-
-	// For other bits, use varying probabilities to ensure good
-	// distribution.
-	for _, bit := range bits {
-		// The anchor bit was already set above so we can skip it here.
-		if bit == channeldb.AnchorOutputsBit {
-			continue
-		}
-
-		// Some bits are related, so we'll make sure we capture that
-		// dep.
-		switch bit {
-		case channeldb.TapscriptRootBit:
-			// If we have TapscriptRootBit, we must have
-			// SimpleTaprootFeatureBit.
-			if chanType&channeldb.SimpleTaprootFeatureBit != 0 {
-				// 70% chance if taproot is enabled.
-				setBit(bit, 70)
-			}
-
-		case channeldb.DualFunderBit:
-			// 40% chance of dual funding.
-			setBit(bit, 40)
-
-		default:
-			// 30% chance for other bits.
-			setBit(bit, 30)
-		}
-	}
-
-	return chanType
-}
-
-// genFeePayer generates optional fee payer.
-func genFeePayer(t *rapid.T) fn.Option[lntypes.ChannelParty] {
-	if !rapid.Bool().Draw(t, "has_fee_payer") {
-		return fn.None[lntypes.ChannelParty]()
-	}
-
-	if rapid.Bool().Draw(t, "is_local") {
-		return fn.Some(lntypes.Local)
-	}
-
-	return fn.Some(lntypes.Remote)
-}
-
-// genCommitFee generates a reasonable non-zero commitment fee.
-func genCommitFee(t *rapid.T) btcutil.Amount {
-	// Generate a reasonable commit fee between 100-5000 sats
-	return btcutil.Amount(
-		rapid.Int64Range(100, 5_000).Draw(t, "commit_fee"),
-	)
-}
-
-// TestCoopCloseBalance tests fundamental properties of CoopCloseBalance. This
-// ensures that the closing fee is always subtracted from the correct balance,
-// amongst other properties.
-func TestCoopCloseBalance(tt *testing.T) {
-	tt.Parallel()
-
-	rapid.Check(tt, func(t *rapid.T) {
-		require := require.New(t)
-
-		// Generate test inputs
-		chanType := genChannelType(t)
-		isInitiator := rapid.Bool().Draw(t, "is_initiator")
-
-		// Generate amounts using specific generators
-		coopCloseFee := genCoopCloseFee(t)
-		ourBalance := genValidAmount(t, "local balance")
-		theirBalance := genValidAmount(t, "remote balance")
-		feePayer := genFeePayer(t)
-		commitFee := genCommitFee(t)
-
-		ourFinal, theirFinal, err := CoopCloseBalance(
-			chanType, isInitiator, coopCloseFee,
-			ourBalance, theirBalance, commitFee, feePayer,
-		)
-
-		// Property 1: If inputs are non-negative, we either get valid
-		// outputs or an error.
-		if err != nil {
-			// On error, final balances should be 0
-			require.Zero(
-				ourFinal,
-				"expected zero our_balance on error",
-			)
-			require.Zero(
-				theirFinal,
-				"expected zero their_balance on error",
-			)
-
-			return
-		}
-
-		// Property 2: Final balances should be non-negative.
-		require.GreaterOrEqual(
-			ourFinal, btcutil.Amount(0),
-			"our final balance should be non-negative",
-		)
-		require.GreaterOrEqual(
-			theirFinal, btcutil.Amount(0),
-			"their final balance should be non-negative",
-		)
-
-		// Property 3: Total balance should be conserved minus fees.
-		initialTotal := ourBalance + theirBalance
-		initialTotal += commitFee
-
-		if chanType.HasAnchors() {
-			initialTotal += 2 * AnchorSize
-		}
-
-		finalTotal := ourFinal + theirFinal + coopCloseFee
-		require.Equal(
-			initialTotal, finalTotal,
-			"total balance should be conserved",
-		)
-
-		// Property 4: When feePayer is specified, that party's balance
-		// should be reduced by exactly the coopCloseFee.
-		if feePayer.IsSome() {
-			payer := feePayer.UnwrapOrFail(tt)
-
-			if payer == lntypes.Local {
-				require.LessOrEqual(
-					ourBalance-(ourFinal+coopCloseFee),
-					btcutil.Amount(0),
-					"local balance reduced by more than fee", //nolint:ll
-				)
-			} else {
-				require.LessOrEqual(
-					theirBalance-(theirFinal+coopCloseFee),
-					btcutil.Amount(0),
-					"remote balance reduced by more than fee", //nolint:ll
-				)
-			}
-		}
-
-		// Property 5: For anchor channels, verify the correct final
-		// balance factors in the anchor amount.
-		if chanType.HasAnchors() {
-			// The initiator delta is the commit fee plus anchor
-			// amount.
-			initiatorDelta := commitFee + 2*AnchorSize
-
-			// Default to initiator paying unless explicitly
-			// specified.
-			isLocalPaying := isInitiator
-			if feePayer.IsSome() {
-				isLocalPaying = feePayer.UnwrapOrFail(tt) ==
-					lntypes.Local
-			}
-
-			if isInitiator {
-				expectedBalance := ourBalance + initiatorDelta
-				if isLocalPaying {
-					expectedBalance -= coopCloseFee
-				}
-
-				require.Equal(expectedBalance, ourFinal,
-					"initiator (local) balance incorrect")
-			} else {
-				// They are the initiator
-				expectedBalance := theirBalance + initiatorDelta
-				if !isLocalPaying {
-					expectedBalance -= coopCloseFee
-				}
-
-				require.Equal(expectedBalance, theirFinal,
-					"initiator (remote) balance incorrect")
-			}
-		}
-	})
-}
diff --git a/lnwire/lnwire_test.go b/lnwire/lnwire_test.go
index f42426ac5..6bfbb465e 100644
--- a/lnwire/lnwire_test.go
+++ b/lnwire/lnwire_test.go
@@ -1352,7 +1352,7 @@ func TestLightningWireProtocol(t *testing.T) {
 			req := ClosingComplete{
 				ChannelID:   ChannelID(c),
 				FeeSatoshis: btcutil.Amount(r.Int63()),
-				LockTime:    uint32(r.Int63()),
+				Sequence:    uint32(r.Int63()),
 				ClosingSigs: ClosingSigs{},
 			}
 
diff --git a/protofsm/state_machine_test.go b/protofsm/state_machine_test.go
index ea596dc25..fc7a4ccfd 100644
--- a/protofsm/state_machine_test.go
+++ b/protofsm/state_machine_test.go
@@ -80,7 +80,9 @@ func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
 		return &StateTransition[dummyEvents, *dummyEnv]{
 			NextState: &dummyStateStart{},
 			NewEvents: fn.Some(EmittedEvent[dummyEvents]{
-				InternalEvent: []dummyEvents{&goToFin{}},
+				InternalEvent: fn.Some(
+					[]dummyEvents{&goToFin{}},
+				),
 			}),
 		}, nil
 
@@ -112,13 +114,13 @@ func (d *dummyStateStart) ProcessEvent(event dummyEvents, env *dummyEnv,
 				canSend: d.canSend,
 			},
 			NewEvents: fn.Some(EmittedEvent[dummyEvents]{
-				ExternalEvents: DaemonEventSet{
+				ExternalEvents: fn.Some(DaemonEventSet{
 					sendEvent, sendEvent2,
 					&BroadcastTxn{
 						Tx:    &wire.MsgTx{},
 						Label: "test",
 					},
-				},
+				}),
 			}),
 		}, nil
 	}
diff --git a/routing/blinding_test.go b/routing/blinding_test.go
index 0a8846adb..8f83f7fd8 100644
--- a/routing/blinding_test.go
+++ b/routing/blinding_test.go
@@ -2,11 +2,11 @@ package routing
 
 import (
 	"bytes"
-	"reflect"
 	"testing"
 
 	"github.com/btcsuite/btcd/btcec/v2"
 	sphinx "github.com/lightningnetwork/lightning-onion"
+	"github.com/lightningnetwork/lnd/fn/v2"
 	"github.com/lightningnetwork/lnd/graph/db/models"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/routing/route"
@@ -129,7 +129,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
 		HtlcMaximum:         htlcMax,
 		Features:            features,
 	}
-	hints, err := blindedPayment.toRouteHints()
+	hints, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
 	require.NoError(t, err)
 	require.Nil(t, hints)
 
@@ -184,7 +184,7 @@ func TestBlindedPaymentToHints(t *testing.T) {
 		},
 	}
 
-	actual, err := blindedPayment.toRouteHints()
+	actual, err := blindedPayment.toRouteHints(fn.None[*btcec.PublicKey]())
 	require.NoError(t, err)
 
 	require.Equal(t, len(expected), len(actual))
@@ -218,63 +218,3 @@ func TestBlindedPaymentToHints(t *testing.T) {
 		require.Equal(t, expectedHint[0], actualHint[0])
 	}
 }
-
-// TestBlindedPaymentDeepCopy tests the deep copy method of the BLindedPayment
-// struct.
-//
-// TODO(ziggie): Make this a property test instead.
-func TestBlindedPaymentDeepCopy(t *testing.T) {
-	_, pkBlind1 := btcec.PrivKeyFromBytes([]byte{1})
-	_, blindingPoint := btcec.PrivKeyFromBytes([]byte{2})
-	_, pkBlind2 := btcec.PrivKeyFromBytes([]byte{3})
-
-	// Create a test BlindedPayment with non-nil fields
-	original := &BlindedPayment{
-		BaseFee:             1000,
-		ProportionalFeeRate: 2000,
-		CltvExpiryDelta:     144,
-		HtlcMinimum:         1000,
-		HtlcMaximum:         1000000,
-		Features:            lnwire.NewFeatureVector(nil, nil),
-		BlindedPath: &sphinx.BlindedPath{
-			IntroductionPoint: pkBlind1,
-			BlindingPoint:     blindingPoint,
-			BlindedHops: []*sphinx.BlindedHopInfo{
-				{
-					BlindedNodePub: pkBlind2,
-					CipherText:     []byte("test cipher"),
-				},
-			},
-		},
-	}
-
-	// Make a deep copy
-	cpyPayment := original.deepCopy()
-
-	// Test 1: Verify the copy is not the same pointer
-	if cpyPayment == original {
-		t.Fatal("deepCopy returned same pointer")
-	}
-
-	// Verify all fields are equal
-	if !reflect.DeepEqual(original, cpyPayment) {
-		t.Fatal("copy is not equal to original")
-	}
-
-	// Modify the copy and verify it doesn't affect the original
-	cpyPayment.BaseFee = 2000
-	cpyPayment.BlindedPath.BlindedHops[0].CipherText = []byte("modified")
-
-	require.NotEqual(t, original.BaseFee, cpyPayment.BaseFee)
-
-	require.NotEqual(
-		t,
-		original.BlindedPath.BlindedHops[0].CipherText,
-		cpyPayment.BlindedPath.BlindedHops[0].CipherText,
-	)
-
-	// Verify nil handling.
-	var nilPayment *BlindedPayment
-	nilCopy := nilPayment.deepCopy()
-	require.Nil(t, nilCopy)
-}
diff --git a/routing/pathfind_test.go b/routing/pathfind_test.go
index f91d95d6b..c463b8135 100644
--- a/routing/pathfind_test.go
+++ b/routing/pathfind_test.go
@@ -765,9 +765,6 @@ func TestPathFinding(t *testing.T) {
 	}, {
 		name: "path finding with additional edges",
 		fn:   runPathFindingWithAdditionalEdges,
-	}, {
-		name: "path finding with duplicate blinded hop",
-		fn:   runPathFindingWithBlindedPathDuplicateHop,
 	}, {
 		name: "path finding with redundant additional edges",
 		fn:   runPathFindingWithRedundantAdditionalEdges,
@@ -1268,107 +1265,6 @@ func runPathFindingWithAdditionalEdges(t *testing.T, useCache bool) {
 	assertExpectedPath(t, graph.aliasMap, path, "songoku", "doge")
 }
 
-// runPathFindingWithBlindedPathDuplicateHop tests that in case a blinded path
-// has duplicate hops that the path finding algorithm does not fail or behave
-// incorrectly. This can happen because the creator of the blinded path can
-// specify the same hop multiple times and this will only be detected at the
-// forwarding nodes, so it is important that we can handle this case.
-func runPathFindingWithBlindedPathDuplicateHop(t *testing.T, useCache bool) {
-	graph, err := parseTestGraph(t, useCache, basicGraphFilePath)
-	require.NoError(t, err, "unable to create graph")
-
-	sourceNode, err := graph.graph.SourceNode()
-	require.NoError(t, err, "unable to fetch source node")
-
-	paymentAmt := lnwire.NewMSatFromSatoshis(100)
-
-	songokuPubKeyBytes := graph.aliasMap["songoku"]
-	songokuPubKey, err := btcec.ParsePubKey(songokuPubKeyBytes[:])
-	require.NoError(t, err, "unable to parse public key from bytes")
-
-	_, pkb1 := btcec.PrivKeyFromBytes([]byte{2})
-	_, pkb2 := btcec.PrivKeyFromBytes([]byte{3})
-	_, blindedPoint := btcec.PrivKeyFromBytes([]byte{5})
-
-	sizeEncryptedData := 100
-	cipherText := bytes.Repeat(
-		[]byte{1}, sizeEncryptedData,
-	)
-
-	vb1 := route.NewVertex(pkb1)
-	vb2 := route.NewVertex(pkb2)
-
-	// Payments to blinded paths always pay to the NUMS target key.
-	dummyTarget := route.NewVertex(&BlindedPathNUMSKey)
-
-	graph.aliasMap["pkb1"] = vb1
-	graph.aliasMap["pkb2"] = vb2
-	graph.aliasMap["dummyTarget"] = dummyTarget
-
-	// Create a blinded payment with duplicate hops and make sure the
-	// path finding algorithm can cope with that. We add blinded hop 2
-	// 3 times. The path finding algorithm should create a path with a
-	// single hop to pkb2 (the first entry).
-	blindedPayment := &BlindedPayment{
-		BlindedPath: &sphinx.BlindedPath{
-			IntroductionPoint: songokuPubKey,
-			BlindingPoint:     blindedPoint,
-			BlindedHops: []*sphinx.BlindedHopInfo{
-				{
-					CipherText: cipherText,
-				},
-				{
-					BlindedNodePub: pkb2,
-					CipherText:     cipherText,
-				},
-				{
-					BlindedNodePub: pkb1,
-					CipherText:     cipherText,
-				},
-				{
-					BlindedNodePub: pkb2,
-					CipherText:     cipherText,
-				},
-				{
-					BlindedNodePub: &BlindedPathNUMSKey,
-					CipherText:     cipherText,
-				},
-				{
-					BlindedNodePub: pkb2,
-					CipherText:     cipherText,
-				},
-			},
-		},
-		HtlcMinimum:     1,
-		HtlcMaximum:     100_000_000,
-		CltvExpiryDelta: 140,
-	}
-
-	blindedPath, err := blindedPayment.toRouteHints()
-	require.NoError(t, err)
-
-	find := func(r *RestrictParams) (
-		[]*unifiedEdge, error) {
-
-		return dbFindPath(
-			graph.graph, blindedPath, &mockBandwidthHints{},
-			r, testPathFindingConfig,
-			sourceNode.PubKeyBytes, dummyTarget, paymentAmt,
-			0, 0,
-		)
-	}
-
-	// We should now be able to find a path however not the chained path
-	// of the blinded hops.
-	path, err := find(noRestrictions)
-	require.NoError(t, err, "unable to create route to blinded path")
-
-	// The path should represent the following hops:
-	//	source node -> songoku -> pkb2 -> dummyTarget
-	assertExpectedPath(t, graph.aliasMap, path, "songoku", "pkb2",
-		"dummyTarget")
-}
-
 // runPathFindingWithRedundantAdditionalEdges asserts that we are able to find
 // paths to nodes ignoring additional edges that are already known by self node.
 func runPathFindingWithRedundantAdditionalEdges(t *testing.T, useCache bool) {
@@ -3388,7 +3284,9 @@ func TestBlindedRouteConstruction(t *testing.T) {
 	// that make up the graph we'll give to route construction. The hints
 	// map is keyed by source node, so we can retrieve our blinded edges
 	// accordingly.
-	blindedEdges, err := blindedPayment.toRouteHints()
+	blindedEdges, err := blindedPayment.toRouteHints(
+		fn.None[*btcec.PublicKey](),
+	)
 	require.NoError(t, err)
 
 	carolDaveEdge := blindedEdges[carolVertex][0]
@@ -3517,48 +3415,32 @@ func TestLastHopPayloadSize(t *testing.T) {
 		customRecords = map[uint64][]byte{
 			record.CustomTypeStart: {1, 2, 3},
 		}
-
-		encrypedDataSmall = bytes.Repeat(
-			[]byte{1}, 5,
+		sizeEncryptedData = 100
+		encrypedData      = bytes.Repeat(
+			[]byte{1}, sizeEncryptedData,
 		)
-		encrypedDataLarge = bytes.Repeat(
-			[]byte{1}, 100,
-		)
-		_, blindedPoint          = btcec.PrivKeyFromBytes([]byte{5})
-		paymentAddr              = &[32]byte{1}
-		ampOptions               = &AMPOptions{}
-		amtToForward             = lnwire.MilliSatoshi(10000)
-		emptyEncryptedData       = []byte{}
-		finalHopExpiry     int32 = 144
+		_, blindedPoint       = btcec.PrivKeyFromBytes([]byte{5})
+		paymentAddr           = &[32]byte{1}
+		ampOptions            = &AMPOptions{}
+		amtToForward          = lnwire.MilliSatoshi(10000)
+		finalHopExpiry  int32 = 144
 
 		oneHopPath = &sphinx.BlindedPath{
 			BlindedHops: []*sphinx.BlindedHopInfo{
 				{
-					CipherText: emptyEncryptedData,
-				},
-			},
-			BlindingPoint: blindedPoint,
-		}
-
-		twoHopPathSmallHopSize = &sphinx.BlindedPath{
-			BlindedHops: []*sphinx.BlindedHopInfo{
-				{
-					CipherText: encrypedDataLarge,
-				},
-				{
-					CipherText: encrypedDataLarge,
+					CipherText: encrypedData,
 				},
 			},
 			BlindingPoint: blindedPoint,
 		}
 
-		twoHopPathLargeHopSize = &sphinx.BlindedPath{
+		twoHopPath = &sphinx.BlindedPath{
 			BlindedHops: []*sphinx.BlindedHopInfo{
 				{
-					CipherText: encrypedDataSmall,
+					CipherText: encrypedData,
 				},
 				{
-					CipherText: encrypedDataSmall,
+					CipherText: encrypedData,
 				},
 			},
 			BlindingPoint: blindedPoint,
@@ -3571,19 +3453,15 @@ func TestLastHopPayloadSize(t *testing.T) {
 	require.NoError(t, err)
 
 	twoHopBlindedPayment, err := NewBlindedPaymentPathSet(
-		[]*BlindedPayment{
-			{BlindedPath: twoHopPathLargeHopSize},
-			{BlindedPath: twoHopPathSmallHopSize},
-		},
+		[]*BlindedPayment{{BlindedPath: twoHopPath}},
 	)
 	require.NoError(t, err)
 
 	testCases := []struct {
-		name                  string
-		restrictions          *RestrictParams
-		finalHopExpiry        int32
-		amount                lnwire.MilliSatoshi
-		expectedEncryptedData []byte
+		name           string
+		restrictions   *RestrictParams
+		finalHopExpiry int32
+		amount         lnwire.MilliSatoshi
 	}{
 		{
 			name: "Non blinded final hop",
@@ -3601,18 +3479,16 @@ func TestLastHopPayloadSize(t *testing.T) {
 			restrictions: &RestrictParams{
 				BlindedPaymentPathSet: oneHopBlindedPayment,
 			},
-			amount:                amtToForward,
-			finalHopExpiry:        finalHopExpiry,
-			expectedEncryptedData: emptyEncryptedData,
+			amount:         amtToForward,
+			finalHopExpiry: finalHopExpiry,
 		},
 		{
 			name: "Blinded final hop of a two hop payment",
 			restrictions: &RestrictParams{
 				BlindedPaymentPathSet: twoHopBlindedPayment,
 			},
-			amount:                amtToForward,
-			finalHopExpiry:        finalHopExpiry,
-			expectedEncryptedData: encrypedDataLarge,
+			amount:         amtToForward,
+			finalHopExpiry: finalHopExpiry,
 		},
 	}
 
@@ -3636,23 +3512,16 @@ func TestLastHopPayloadSize(t *testing.T) {
 
 			var finalHop route.Hop
 			if tc.restrictions.BlindedPaymentPathSet != nil {
-				bPSet := tc.restrictions.BlindedPaymentPathSet
-				path, err := bPSet.LargestLastHopPayloadPath()
-				require.NotNil(t, path)
-
-				require.NoError(t, err)
-
+				path := tc.restrictions.BlindedPaymentPathSet.
+					LargestLastHopPayloadPath()
 				blindedPath := path.BlindedPath.BlindedHops
 				blindedPoint := path.BlindedPath.BlindingPoint
-				lastHop := blindedPath[len(blindedPath)-1]
-				require.Equal(t, lastHop.CipherText,
-					tc.expectedEncryptedData)
 
 				//nolint:ll
 				finalHop = route.Hop{
 					AmtToForward:     tc.amount,
 					OutgoingTimeLock: uint32(tc.finalHopExpiry),
-					EncryptedData:    lastHop.CipherText,
+					EncryptedData:    blindedPath[len(blindedPath)-1].CipherText,
 				}
 				if len(blindedPath) == 1 {
 					finalHop.BlindingPoint = blindedPoint
@@ -3672,11 +3541,11 @@ func TestLastHopPayloadSize(t *testing.T) {
 			payLoad, err := createHopPayload(finalHop, 0, true)
 			require.NoErrorf(t, err, "failed to create hop payload")
 
-			expectedPayloadSize, err := lastHopPayloadSize(
+			expectedPayloadSize := lastHopPayloadSize(
 				tc.restrictions, tc.finalHopExpiry,
 				tc.amount,
 			)
-			require.NoError(t, err)
+
 			require.Equal(
 				t, expectedPayloadSize,
 				uint64(payLoad.NumBytes()),
diff --git a/routing/result_interpretation_test.go b/routing/result_interpretation_test.go
index a0c3e9062..8c67bdeea 100644
--- a/routing/result_interpretation_test.go
+++ b/routing/result_interpretation_test.go
@@ -96,17 +96,13 @@ var (
 				AmtToForward: 99,
 			},
 			{
-				PubKeyBytes: hops[2],
-				// Intermediate blinded hops don't have an
-				// amount set.
-				AmtToForward:  0,
+				PubKeyBytes:   hops[2],
+				AmtToForward:  95,
 				BlindingPoint: genTestPubKey(),
 			},
 			{
-				PubKeyBytes: hops[3],
-				// Intermediate blinded hops don't have an
-				// amount set.
-				AmtToForward: 0,
+				PubKeyBytes:  hops[3],
+				AmtToForward: 88,
 			},
 			{
 				PubKeyBytes:  hops[4],
@@ -126,10 +122,8 @@ var (
 				AmtToForward: 99,
 			},
 			{
-				PubKeyBytes: hops[2],
-				// Intermediate blinded hops don't have an
-				// amount set.
-				AmtToForward:  0,
+				PubKeyBytes:   hops[2],
+				AmtToForward:  95,
 				BlindingPoint: genTestPubKey(),
 			},
 			{
@@ -146,17 +140,13 @@ var (
 		TotalAmount:  100,
 		Hops: []*route.Hop{
 			{
-				PubKeyBytes: hops[1],
-				// Intermediate blinded hops don't have an
-				// amount set.
-				AmtToForward:  0,
+				PubKeyBytes:   hops[1],
+				AmtToForward:  90,
 				BlindingPoint: genTestPubKey(),
 			},
 			{
-				PubKeyBytes: hops[2],
-				// Intermediate blinded hops don't have an
-				// amount set.
-				AmtToForward: 0,
+				PubKeyBytes:  hops[2],
+				AmtToForward: 75,
 			},
 			{
 				PubKeyBytes:  hops[3],
@@ -562,12 +552,7 @@ var resultTestCases = []resultTestCase{
 			pairResults: map[DirectedNodePair]pairResult{
 				getTestPair(0, 1): successPairResult(100),
 				getTestPair(1, 2): successPairResult(99),
-
-				// The amount for the last hop is always the
-				// receiver amount because the amount to forward
-				// is always set to 0 for intermediate blinded
-				// hops.
-				getTestPair(3, 4): failPairResult(77),
+				getTestPair(3, 4): failPairResult(88),
 			},
 		},
 	},
@@ -582,12 +567,7 @@ var resultTestCases = []resultTestCase{
 		expectedResult: &interpretedResult{
 			pairResults: map[DirectedNodePair]pairResult{
 				getTestPair(0, 1): successPairResult(100),
-
-				// The amount for the last hop is always the
-				// receiver amount because the amount to forward
-				// is always set to 0 for intermediate blinded
-				// hops.
-				getTestPair(2, 3): failPairResult(58),
+				getTestPair(2, 3): failPairResult(75),
 			},
 		},
 	},
@@ -702,25 +682,6 @@ var resultTestCases = []resultTestCase{
 			finalFailureReason: &reasonError,
 		},
 	},
-	// Test a multi-hop blinded route and that in a success case the amounts
-	// for the blinded route part are correctly set to the receiver amount.
-	{
-		name:    "blinded multi-hop success",
-		route:   blindedMultiToIntroduction,
-		success: true,
-		expectedResult: &interpretedResult{
-			pairResults: map[DirectedNodePair]pairResult{
-				getTestPair(0, 1): successPairResult(100),
-
-				// For the route blinded part of the route the
-				// success amount is determined by the receiver
-				// amount because the intermediate blinded hops
-				// set the forwarded amount to 0.
-				getTestPair(1, 2): successPairResult(58),
-				getTestPair(2, 3): successPairResult(58),
-			},
-		},
-	},
 }
 
 // TestResultInterpretation executes a list of test cases that test the result