Repository: lightningnetwork/lnd
Pull request #9072 (2024-09-09 09:32:28)

[custom channels 3/5]: Extract PART3 from mega staging branch
Extracts part 3 from https://github.com/lightningnetwork/lnd/pull/8960.

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 using 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.

### Other keywords and placeholders

- Add `@coderabbitai ignore` anywhere in the PR description to prevent this PR from being reviewed.
- Add `@coderabbitai summary` to generate the high-level summary at a specific location in the PR description.
- Add `@coderabbitai` anywhere in the PR title to generate the title automatically.

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

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

### Documentation and Community

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

</details>



<!-- tips_end -->
github-actions[bot]: ## Pull reviewers stats
Stats of the last 30 days for [lnd](https://github.com/lightningnetwork/lnd):
|                                                                                                                                                                     | User               | Total reviews  | Time to review                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    | Total comments |
| ------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------ | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------- |
| <a href="https://github.com/guggero"><img src="https://avatars.githubusercontent.com/u/1008879?u=2c337f92b7dbc69b8aaff7885926f2bc1865db0d&v=4" width="32"></a>      | guggero<br/>🥇     | **19**<br/>▀▀▀ | [12h 47m](https://app.flowwer.dev/charts/review-time/~(u~(i~'1008879~n~'guggero)~p~30~r~(~(d~'si3ghs~t~'16z4)~(d~'si93bp~t~'1b1h)~(d~'si93gq~t~'129w)~(d~'sib0br~t~'8qt3)~(d~'sih2g0~t~'1hd)~(d~'siijfb~t~'ai8h)~(d~'silz0d~t~'jig)~(d~'sim1rs~t~'ldq)~(d~'sim6of~t~'1qe)~(d~'simd02~t~'821)~(d~'simd0y~t~'82x)~(d~'sinxeo~t~'acb)~(d~'sinxjg~t~'2i1j)~(d~'sio5fd~t~'3x)~(d~'sithqd~t~'81cb)~(d~'sivd5j~t~'lqb)~(d~'sivy9r~t~'w5)~(d~'siwz9p~t~'gbd)~(d~'six1dm~t~'29ye)~(d~'siz7h5~t~'wrl)~(d~'sizlan~t~'1gy7)~(d~'sj6vng~t~'8jg6)~(d~'sj6vok~t~'hnxd)~(d~'sj907r~t~'7jyo)~(d~'sjacpl~t~'3ha)~(d~'sjjfxa~t~'1dcut))))<br/>                       | 34<br/>▀▀      |
| <a href="https://github.com/yyforyongyu"><img src="https://avatars.githubusercontent.com/u/5845072?u=75cc18e789ec4b275a32c170ca6cd5118fdfe120&v=4" width="32"></a>  | yyforyongyu<br/>🥈 | 14<br/>▀▀      | [15h](https://app.flowwer.dev/charts/review-time/~(u~(i~'5845072~n~'yyforyongyu)~p~30~r~(~(d~'si7d6h~t~'knt)~(d~'siiva5~t~'au3b)~(d~'sij2iw~t~'b1c2)~(d~'simhef~t~'110d)~(d~'simnd7~t~'c4s)~(d~'simo5q~t~'cxb)~(d~'simoaj~t~'d24)~(d~'simobg~t~'d31)~(d~'simoxb~t~'717)~(d~'simpj8~t~'1956)~(d~'simpmp~t~'198n)~(d~'simru9~t~'9y5)~(d~'sinnkc~t~'15o8)~(d~'sinuiv~t~'1jag)~(d~'sinv6g~t~'2ese)~(d~'sinz4u~t~'2jmx)~(d~'sio8d5~t~'31p)~(d~'sit52x~t~'7oov)~(d~'sitwpt~t~'5hd0)~(d~'sivbuc~t~'6whj)~(d~'siw0fl~t~'4fjd)~(d~'siwzxa~t~'gyy)~(d~'six00n~t~'28lf)~(d~'siythw~t~'isc)~(d~'siyuxg~t~'ql0)~(d~'siz77k~t~'wi0)~(d~'sjjhax~t~'9ue1))))<br/> | 33<br/>▀       |
| <a href="https://github.com/Roasbeef"><img src="https://avatars.githubusercontent.com/u/998190?v=4" width="32"></a>                                                 | Roasbeef<br/>🥉    | 8<br/>▀        | [5d 2h 26m](https://app.flowwer.dev/charts/review-time/~(u~(i~'998190~n~'Roasbeef)~p~30~r~(~(d~'si8b3b~t~'48j)~(d~'sihp9z~t~'9g3c)~(d~'silf07~t~'dd3d)~(d~'sin1pu~t~'qhf)~(d~'sius77~t~'mqad)~(d~'siycuc~t~'24s)~(d~'sj1o03~t~'7r0)~(d~'sj9j9h~t~'a6p9)~(d~'sj9jde~t~'a6t6)~(d~'sj9jhh~t~'a6x9)~(d~'sjb1q9~t~'zd1))))<br/>▀                                                                                                                                                                                                                                                                                                                       | 18<br/>▀       |
| <a href="https://github.com/ellemouton"><img src="https://avatars.githubusercontent.com/u/25280249?u=cc72497ae94e2044cbf8c474854d5fd1470386ca&v=4" width="32"></a>  | ellemouton<br/>    | 5<br/>▀        | [6h 51m](https://app.flowwer.dev/charts/review-time/~(u~(i~'25280249~n~'ellemouton)~p~30~r~(~(d~'siatq3~t~'2kjg)~(d~'simm1g~t~'45c)~(d~'siytqm~t~'j12)~(d~'sj8iuz~t~'6f9d)~(d~'sjad1i~t~'3t7))))<br/>                                                                                                                                                                                                                                                                                                                                                                                                                                             | 13<br/>▀       |
| <a href="https://github.com/Crypt-iQ"><img src="https://avatars.githubusercontent.com/u/15145615?u=a19ad52f8c597f3518a89a52014fabefda644078&v=4" width="32"></a>    | Crypt-iQ<br/>      | 4<br/>▀        | [13h 28m](https://app.flowwer.dev/charts/review-time/~(u~(i~'15145615~n~'Crypt-iQ)~p~30~r~(~(d~'si7pze~t~'xgq)~(d~'si9alu~t~'11f7)~(d~'sibh3l~t~'37wy)~(d~'sibxrp~t~'3ol2)~(d~'sixq8z~t~'916)~(d~'siy73g~t~'pvn)~(d~'sj8vcp~t~'aj5f))))<br/>                                                                                                                                                                                                                                                                                                                                                                                                      | 7<br/>         |
| <a href="https://github.com/bitromortac"><img src="https://avatars.githubusercontent.com/u/36066690?u=de5285400228e4a6f5903e34825623ff3652f2e3&v=4" width="32"></a> | bitromortac<br/>   | 4<br/>▀        | [6d 20h 16m](https://app.flowwer.dev/charts/review-time/~(u~(i~'36066690~n~'bitromortac)~p~30~r~(~(d~'si772q~t~'ek2)~(d~'sigypg~t~'8wsm)~(d~'sikv15~t~'ct4b)~(d~'sitftx~t~'ldx3)~(d~'sivj98~t~'nhce)~(d~'sivkv4~t~'niya)~(d~'sj93px~t~'arin)~(d~'sjagub~t~'b4a3)~(d~'sjbw0k~t~'cjgc)~(d~'sjciyj~t~'d6eb))))<br/>▀                                                                                                                                                                                                                                                                                                                                 | **50**<br/>▀▀  |
| <a href="https://github.com/ziggie1984"><img src="https://avatars.githubusercontent.com/u/90319354?u=04ef4180951baf4d77c14f58b7c931b21ae5353c&v=4" width="32"></a>  | ziggie1984<br/>    | 4<br/>▀        | [5d 13h 36m](https://app.flowwer.dev/charts/review-time/~(u~(i~'90319354~n~'ziggie1984)~p~30~r~(~(d~'sia1y8~t~'201e)~(d~'siicjp~t~'aamv)~(d~'siid08~t~'ab3e)~(d~'siidr3~t~'abu9)~(d~'siidsh~t~'abvn)~(d~'siky6j~t~'cw9p)~(d~'simcsu~t~'7ut)~(d~'sio00s~t~'cyf)~(d~'sivdvm~t~'nbys)~(d~'siw414~t~'6ni)~(d~'siwdbv~t~'obf1))))<br/>▀                                                                                                                                                                                                                                                                                                                | 17<br/>▀       |
| <a href="https://github.com/ProofOfKeags"><img src="https://avatars.githubusercontent.com/u/4033651?u=b0b68587c7f5aa4e14a63ffa12413d8b65eddc62&v=4" width="32"></a> | ProofOfKeags<br/>  | 3<br/>         | [5d 6h 54m](https://app.flowwer.dev/charts/review-time/~(u~(i~'4033651~n~'ProofOfKeags)~p~30~r~(~(d~'simvbq~t~'k3b)~(d~'siwf80~t~'7zv7)~(d~'sj1zw4~t~'2nbw)~(d~'sj8z74~t~'9mmw)~(d~'sj9awy~t~'9ycq)~(d~'sjazzi~t~'bnfa)~(d~'sjcffa~t~'d2v2)~(d~'sjcgg0~t~'d3vs))))<br/>▀                                                                                                                                                                                                                                                                                                                                                                          | 38<br/>▀▀      |
| <a href="https://github.com/bhandras"><img src="https://avatars.githubusercontent.com/u/554360?u=e94d0ce1157aaef37a43df671cffe181a61230ad&v=4" width="32"></a>      | bhandras<br/>      | 2<br/>         | [5h 58m](https://app.flowwer.dev/charts/review-time/~(u~(i~'554360~n~'bhandras)~p~30~r~(~(d~'simdgz~t~'x2x)~(d~'sja2g1~t~'2t))))<br/>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                             | 0<br/>         |
| <a href="https://github.com/hieblmi"><img src="https://avatars.githubusercontent.com/u/18631614?u=78b8d02d12bd598de499b1cee252214d1bfb5f47&v=4" width="32"></a>     | hieblmi<br/>       | 2<br/>         | [1h 31m](https://app.flowwer.dev/charts/review-time/~(u~(i~'18631614~n~'hieblmi)~p~30~r~(~(d~'siirx5~t~'83b)~(d~'sixhl2~t~'d9))))<br/>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | 2<br/>         |
| <a href="https://github.com/alexbosworth"><img src="https://avatars.githubusercontent.com/u/401953?u=601aa62bb9d253db7403d28dc84ef49478db8ab1&v=4" width="32"></a>  | alexbosworth<br/>  | 1<br/>         | [3d 21h 20m](https://app.flowwer.dev/charts/review-time/~(u~(i~'401953~n~'alexbosworth)~p~30~r~(~(d~'sj7ud1~t~'6e3y)~(d~'sj9gp4~t~'80g1))))<br/>▀                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | 1<br/>         |
| <a href="https://github.com/aakselrod"><img src="https://avatars.githubusercontent.com/u/1268625?v=4" width="32"></a>                                               | aakselrod<br/>     | 1<br/>         | [4d 2h 7m](https://app.flowwer.dev/charts/review-time/~(u~(i~'1268625~n~'aakselrod)~p~30~r~(~(d~'sj8y7a~t~'7hy7)~(d~'sj90t6~t~'7kk3)~(d~'sj90uh~t~'7kle))))<br/>▀                                                                                                                                                                                                                                                                                                                                                                                                                                                                                 | 2<br/>         |
| <a href="https://github.com/dstadulis"><img src="https://avatars.githubusercontent.com/u/15150758?u=fdb1e3bb4f19269d4b3194748d45f3f906c462df&v=4" width="32"></a>   | dstadulis<br/>     | 1<br/>         | [5d 7h 41m](https://app.flowwer.dev/charts/review-time/~(u~(i~'15150758~n~'dstadulis)~p~30~r~(~(d~'siqvmz~t~'9uoc))))<br/>▀                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                       | 1<br/>         |
| <a href="https://github.com/morehouse"><img src="https://avatars.githubusercontent.com/u/19176026?v=4" width="32"></a>                                              | morehouse<br/>     | 1<br/>         | [**1h 5m**](https://app.flowwer.dev/charts/review-time/~(u~(i~'19176026~n~'morehouse)~p~30~r~(~(d~'si8c6l~t~'2zy))))<br/>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                         | 1<br/>         |
| <a href="https://github.com/GeorgeTsagk"><img src="https://avatars.githubusercontent.com/u/34623190?u=143a3ef2513f467b54a247805f3fe0d2c67884d9&v=4" width="32"></a> | GeorgeTsagk<br/>   | 1<br/>         | [11d 15h 22m](https://app.flowwer.dev/charts/review-time/~(u~(i~'34623190~n~'GeorgeTsagk)~p~30~r~(~(d~'sjarro~t~'lk0h))))<br/>▀▀                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                  | 3<br/>         |
| <a href="https://github.com/carlaKC"><img src="https://avatars.githubusercontent.com/u/42311294?u=dbe66fa8afe0a88128bcb21135eae2496a32d11f&v=4" width="32"></a>     | carlaKC<br/>       | 1<br/>         | [17h 11m](https://app.flowwer.dev/charts/review-time/~(u~(i~'42311294~n~'carlaKC)~p~30~r~(~(d~'sizg34~t~'1bqo))))<br/>                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                            | 0<br/>         |
| <a href="https://github.com/MPins"><img src="https://avatars.githubusercontent.com/u/88947526?u=eb023439724df1d98dd0a75e2d91d92576dad65a&v=4" width="32"></a>       | MPins<br/>         | 1<br/>         | [5d 8h 9m](https://app.flowwer.dev/charts/review-time/~(u~(i~'88947526~n~'MPins)~p~30~r~(~(d~'sj1vig~t~'9vmn)~(d~'sj1w5j~t~'9w9q))))<br/>▀                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                        | 2<br/>         |
guggero: Requires a spicy rebase now that https://github.com/lightningnetwork/lnd/pull/8981 is in...

Patch:
diff --git a/chainreg/chainregistry.go b/chainreg/chainregistry.go
index 41a2fcbb7..da1f8e08a 100644
--- a/chainreg/chainregistry.go
+++ b/chainreg/chainregistry.go
@@ -68,6 +68,10 @@ type Config struct {
 	// leaves for certain custom channel types.
 	AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
 
+	// AuxSigner is an optional signer that can be used to sign auxiliary
+	// leaves for certain custom channel types.
+	AuxSigner fn.Option[lnwallet.AuxSigner]
+
 	// BlockCache is the main cache for storing block information.
 	BlockCache *blockcache.BlockCache
 
diff --git a/channeldb/models/channel_edge_info.go b/channeldb/models/channel_edge_info.go
index 1afa2d627..0f91e2bbe 100644
--- a/channeldb/models/channel_edge_info.go
+++ b/channeldb/models/channel_edge_info.go
@@ -8,6 +8,7 @@ import (
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/wire"
+	"github.com/lightningnetwork/lnd/fn"
 )
 
 // ChannelEdgeInfo represents a fully authenticated channel along with all its
@@ -62,6 +63,11 @@ type ChannelEdgeInfo struct {
 	// the value output in the outpoint that created this channel.
 	Capacity btcutil.Amount
 
+	// TapscriptRoot is the optional Merkle root of the tapscript tree if
+	// this channel is a taproot channel that also commits to a tapscript
+	// tree (custom channel).
+	TapscriptRoot fn.Option[chainhash.Hash]
+
 	// ExtraOpaqueData is the set of data that was appended to this
 	// message, some of which we may not actually know how to iterate or
 	// parse. By holding onto this data, we ensure that we're able to
diff --git a/config_builder.go b/config_builder.go
index 21b1ccee0..ee4064035 100644
--- a/config_builder.go
+++ b/config_builder.go
@@ -34,6 +34,7 @@ import (
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/clock"
 	"github.com/lightningnetwork/lnd/fn"
+	"github.com/lightningnetwork/lnd/funding"
 	"github.com/lightningnetwork/lnd/invoices"
 	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/kvdb"
@@ -167,6 +168,20 @@ type AuxComponents struct {
 	// MsgRouter is an optional message router that if set will be used in
 	// place of a new blank default message router.
 	MsgRouter fn.Option[msgmux.Router]
+
+	// AuxFundingController is an optional controller that can be used to
+	// modify the way we handle certain custom channel types. It's also
+	// able to automatically handle new custom protocol messages related to
+	// the funding process.
+	AuxFundingController fn.Option[funding.AuxFundingController]
+
+	// AuxSigner is an optional signer that can be used to sign auxiliary
+	// leaves for certain custom channel types.
+	AuxSigner fn.Option[lnwallet.AuxSigner]
+
+	// AuxDataParser is an optional data parser that can be used to parse
+	// auxiliary data for certain custom channel types.
+	AuxDataParser fn.Option[AuxDataParser]
 }
 
 // DefaultWalletImpl is the default implementation of our normal, btcwallet
@@ -573,6 +588,7 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
 		ChanStateDB:                 dbs.ChanStateDB.ChannelStateDB(),
 		NeutrinoCS:                  neutrinoCS,
 		AuxLeafStore:                aux.AuxLeafStore,
+		AuxSigner:                   aux.AuxSigner,
 		ActiveNetParams:             d.cfg.ActiveNetParams,
 		FeeURL:                      d.cfg.FeeURL,
 		Fee: &lncfg.Fee{
@@ -730,6 +746,7 @@ func (d *DefaultWalletImpl) BuildChainControl(
 		NetParams:             *walletConfig.NetParams,
 		CoinSelectionStrategy: walletConfig.CoinSelectionStrategy,
 		AuxLeafStore:          partialChainControl.Cfg.AuxLeafStore,
+		AuxSigner:             partialChainControl.Cfg.AuxSigner,
 	}
 
 	// The broadcast is already always active for neutrino nodes, so we
@@ -912,10 +929,6 @@ type DatabaseInstances struct {
 	// for native SQL queries for tables that already support it. This may
 	// be nil if the use-native-sql flag was not set.
 	NativeSQLStore *sqldb.BaseDB
-
-	// AuxLeafStore is an optional data source that can be used by custom
-	// channels to fetch+store various data.
-	AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
 }
 
 // DefaultDatabaseBuilder is a type that builds the default database backends
diff --git a/contractcourt/chain_arbitrator.go b/contractcourt/chain_arbitrator.go
index dbc97939a..d61e47901 100644
--- a/contractcourt/chain_arbitrator.go
+++ b/contractcourt/chain_arbitrator.go
@@ -221,6 +221,10 @@ type ChainArbitratorConfig struct {
 	// AuxLeafStore is an optional store that can be used to store auxiliary
 	// leaves for certain custom channel types.
 	AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
+
+	// AuxSigner is an optional signer that can be used to sign auxiliary
+	// leaves for certain custom channel types.
+	AuxSigner fn.Option[lnwallet.AuxSigner]
 }
 
 // ChainArbitrator is a sub-system that oversees the on-chain resolution of all
@@ -307,6 +311,9 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
 	a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
 		chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
 	})
+	a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
+		chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
+	})
 
 	chanMachine, err := lnwallet.NewLightningChannel(
 		a.c.cfg.Signer, channel, nil, chanOpts...,
@@ -357,6 +364,9 @@ func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error)
 	a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
 		chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
 	})
+	a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
+		chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
+	})
 
 	// Finally, we'll force close the channel completing
 	// the force close workflow.
diff --git a/discovery/gossiper.go b/discovery/gossiper.go
index 84fae767f..1d3051de5 100644
--- a/discovery/gossiper.go
+++ b/discovery/gossiper.go
@@ -20,6 +20,7 @@ import (
 	"github.com/lightningnetwork/lnd/chainntnfs"
 	"github.com/lightningnetwork/lnd/channeldb"
 	"github.com/lightningnetwork/lnd/channeldb/models"
+	"github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/graph"
 	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/kvdb"
@@ -82,9 +83,10 @@ var (
 // can provide that serve useful when processing a specific network
 // announcement.
 type optionalMsgFields struct {
-	capacity     *btcutil.Amount
-	channelPoint *wire.OutPoint
-	remoteAlias  *lnwire.ShortChannelID
+	capacity      *btcutil.Amount
+	channelPoint  *wire.OutPoint
+	remoteAlias   *lnwire.ShortChannelID
+	tapscriptRoot fn.Option[chainhash.Hash]
 }
 
 // apply applies the optional fields within the functional options.
@@ -115,6 +117,14 @@ func ChannelPoint(op wire.OutPoint) OptionalMsgField {
 	}
 }
 
+// TapscriptRoot is an optional field that lets the gossiper know of the root of
+// the tapscript tree for a custom channel.
+func TapscriptRoot(root fn.Option[chainhash.Hash]) OptionalMsgField {
+	return func(f *optionalMsgFields) {
+		f.tapscriptRoot = root
+	}
+}
+
 // RemoteAlias is an optional field that lets the gossiper know that a locally
 // sent channel update is actually an update for the peer that should replace
 // the ShortChannelID field with the remote's alias. This is only used for
@@ -2578,6 +2588,9 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
 			cp := *nMsg.optionalMsgFields.channelPoint
 			edge.ChannelPoint = cp
 		}
+
+		// Optional tapscript root for custom channels.
+		edge.TapscriptRoot = nMsg.optionalMsgFields.tapscriptRoot
 	}
 
 	log.Debugf("Adding edge for short_chan_id: %v", scid.ToUint64())
diff --git a/funding/aux_funding.go b/funding/aux_funding.go
new file mode 100644
index 000000000..492612145
--- /dev/null
+++ b/funding/aux_funding.go
@@ -0,0 +1,51 @@
+package funding
+
+import (
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
+	"github.com/lightningnetwork/lnd/fn"
+	"github.com/lightningnetwork/lnd/lntypes"
+	"github.com/lightningnetwork/lnd/lnwallet"
+	"github.com/lightningnetwork/lnd/msgmux"
+)
+
+// AuxFundingDescResult is a type alias for a function that returns an optional
+// aux funding desc.
+type AuxFundingDescResult = fn.Result[fn.Option[lnwallet.AuxFundingDesc]]
+
+// AuxTapscriptResult is a type alias for a function that returns an optional
+// tapscript root.
+type AuxTapscriptResult = fn.Result[fn.Option[chainhash.Hash]]
+
+// AuxFundingController permits the implementation of the funding of custom
+// channels types. The controller serves as a MsgEndpoint which allows it to
+// intercept custom messages, or even the regular funding messages. The
+// controller might also pass along an aux funding desc based on an existing
+// pending channel ID.
+type AuxFundingController interface {
+	// Endpoint is the embedded interface that signals that the funding
+	// controller is also a message endpoint. This'll allow it to handle
+	// custom messages specific to the funding type.
+	msgmux.Endpoint
+
+	// DescFromPendingChanID takes a pending channel ID, that may already be
+	// known due to prior custom channel messages, and maybe returns an aux
+	// funding desc which can be used to modify how a channel is funded.
+	DescFromPendingChanID(pid PendingChanID, openChan lnwallet.AuxChanState,
+		keyRing lntypes.Dual[lnwallet.CommitmentKeyRing],
+		initiator bool) AuxFundingDescResult
+
+	// DeriveTapscriptRoot takes a pending channel ID and maybe returns a
+	// tapscript root that should be used when creating any MuSig2 sessions
+	// for a channel.
+	DeriveTapscriptRoot(PendingChanID) AuxTapscriptResult
+
+	// ChannelReady is called when a channel has been fully opened (multiple
+	// confirmations) and is ready to be used. This can be used to perform
+	// any final setup or cleanup.
+	ChannelReady(openChan lnwallet.AuxChanState) error
+
+	// ChannelFinalized is called when a channel has been fully finalized.
+	// In this state, we've received the commitment sig from the remote
+	// party, so we are safe to broadcast the funding transaction.
+	ChannelFinalized(PendingChanID) error
+}
diff --git a/funding/manager.go b/funding/manager.go
index fb36bd990..ed0ef0e7f 100644
--- a/funding/manager.go
+++ b/funding/manager.go
@@ -99,7 +99,6 @@ const (
 	// you and limitless channel size (apart from 21 million cap).
 	MaxBtcFundingAmountWumbo = btcutil.Amount(1000000000)
 
-	// TODO(roasbeef): tune.
 	msgBufferSize = 50
 
 	// MaxWaitNumBlocksFundingConf is the maximum number of blocks to wait
@@ -549,6 +548,16 @@ type Config struct {
 	// AuxLeafStore is an optional store that can be used to store auxiliary
 	// leaves for certain custom channel types.
 	AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
+
+	// AuxFundingController is an optional controller that can be used to
+	// modify the way we handle certain custom channel types. It's also
+	// able to automatically handle new custom protocol messages related to
+	// the funding process.
+	AuxFundingController fn.Option[AuxFundingController]
+
+	// AuxSigner is an optional signer that can be used to sign auxiliary
+	// leaves for certain custom channel types.
+	AuxSigner fn.Option[lnwallet.AuxSigner]
 }
 
 // Manager acts as an orchestrator/bridge between the wallet's
@@ -1078,6 +1087,9 @@ func (f *Manager) advanceFundingState(channel *channeldb.OpenChannel,
 	f.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
 		chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
 	})
+	f.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
+		chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
+	})
 
 	// We create the state-machine object which wraps the database state.
 	lnChannel, err := lnwallet.NewLightningChannel(
@@ -1256,8 +1268,8 @@ func (f *Manager) stateStep(channel *channeldb.OpenChannel,
 
 // advancePendingChannelState waits for a pending channel's funding tx to
 // confirm, and marks it open in the database when that happens.
-func (f *Manager) advancePendingChannelState(
-	channel *channeldb.OpenChannel, pendingChanID PendingChanID) error {
+func (f *Manager) advancePendingChannelState(channel *channeldb.OpenChannel,
+	pendingChanID PendingChanID) error {
 
 	if channel.IsZeroConf() {
 		// Persist the alias to the alias database.
@@ -1626,6 +1638,23 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
 		return
 	}
 
+	// At this point, if we have an AuxFundingController active, we'll
+	// check to see if we have a special tapscript root to use in our
+	// MuSig funding output.
+	tapscriptRoot, err := fn.MapOptionZ(
+		f.cfg.AuxFundingController,
+		func(c AuxFundingController) AuxTapscriptResult {
+			return c.DeriveTapscriptRoot(msg.PendingChannelID)
+		},
+	).Unpack()
+	if err != nil {
+		err = fmt.Errorf("error deriving tapscript root: %w", err)
+		log.Error(err)
+		f.failFundingFlow(peer, cid, err)
+
+		return
+	}
+
 	req := &lnwallet.InitFundingReserveMsg{
 		ChainHash:        &msg.ChainHash,
 		PendingChanID:    msg.PendingChannelID,
@@ -1642,6 +1671,7 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
 		ZeroConf:         zeroConf,
 		OptionScidAlias:  scid,
 		ScidAliasFeature: scidFeatureVal,
+		TapscriptRoot:    tapscriptRoot,
 	}
 
 	reservation, err := f.cfg.Wallet.InitChannelReservation(req)
@@ -1898,6 +1928,8 @@ func (f *Manager) fundeeProcessOpenChannel(peer lnpeer.Peer,
 	log.Debugf("Remote party accepted commitment rendering params: %v",
 		lnutils.SpewLogClosure(params))
 
+	reservation.SetState(lnwallet.SentAcceptChannel)
+
 	// With the initiator's contribution recorded, respond with our
 	// contribution in the next message of the workflow.
 	fundingAccept := lnwire.AcceptChannel{
@@ -1958,6 +1990,10 @@ func (f *Manager) funderProcessAcceptChannel(peer lnpeer.Peer,
 	// Update the timestamp once the fundingAcceptMsg has been handled.
 	defer resCtx.updateTimestamp()
 
+	if resCtx.reservation.State() != lnwallet.SentOpenChannel {
+		return
+	}
+
 	log.Infof("Recv'd fundingResponse for pending_id(%x)",
 		pendingChanID[:])
 
@@ -2261,10 +2297,34 @@ func (f *Manager) waitForPsbt(intent *chanfunding.PsbtIntent,
 			return
 		}
 
+		// At this point, we'll see if there's an AuxFundingDesc we
+		// need to deliver so the funding process can continue
+		// properly.
+		auxFundingDesc, err := fn.MapOptionZ(
+			f.cfg.AuxFundingController,
+			func(c AuxFundingController) AuxFundingDescResult {
+				return c.DescFromPendingChanID(
+					cid.tempChanID,
+					lnwallet.NewAuxChanState(
+						resCtx.reservation.ChanState(),
+					),
+					resCtx.reservation.CommitmentKeyRings(),
+					true,
+				)
+			},
+		).Unpack()
+		if err != nil {
+			failFlow("error continuing PSBT flow", err)
+			return
+		}
+
 		// A non-nil error means we can continue the funding flow.
 		// Notify the wallet so it can prepare everything we need to
 		// continue.
-		err = resCtx.reservation.ProcessPsbt()
+		//
+		// We'll also pass along the aux funding controller as well,
+		// which may be used to help process the finalized PSBT.
+		err = resCtx.reservation.ProcessPsbt(auxFundingDesc)
 		if err != nil {
 			failFlow("error continuing PSBT flow", err)
 			return
@@ -2359,6 +2419,8 @@ func (f *Manager) continueFundingAccept(resCtx *reservationWithCtx,
 		}
 	}
 
+	resCtx.reservation.SetState(lnwallet.SentFundingCreated)
+
 	if err := resCtx.peer.SendMessage(true, fundingCreated); err != nil {
 		log.Errorf("Unable to send funding complete message: %v", err)
 		f.failFundingFlow(resCtx.peer, cid, err)
@@ -2390,11 +2452,14 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer,
 	// final funding transaction, as well as a signature for our version of
 	// the commitment transaction. So at this point, we can validate the
 	// initiator's commitment transaction, then send our own if it's valid.
-	// TODO(roasbeef): make case (p vs P) consistent throughout
 	fundingOut := msg.FundingPoint
 	log.Infof("completing pending_id(%x) with ChannelPoint(%v)",
 		pendingChanID[:], fundingOut)
 
+	if resCtx.reservation.State() != lnwallet.SentAcceptChannel {
+		return
+	}
+
 	// Create the channel identifier without setting the active channel ID.
 	cid := newChanIdentifier(pendingChanID)
 
@@ -2422,16 +2487,38 @@ func (f *Manager) fundeeProcessFundingCreated(peer lnpeer.Peer,
 		}
 	}
 
+	// At this point, we'll see if there's an AuxFundingDesc we need to
+	// deliver so the funding process can continue properly.
+	auxFundingDesc, err := fn.MapOptionZ(
+		f.cfg.AuxFundingController,
+		func(c AuxFundingController) AuxFundingDescResult {
+			return c.DescFromPendingChanID(
+				cid.tempChanID, lnwallet.NewAuxChanState(
+					resCtx.reservation.ChanState(),
+				), resCtx.reservation.CommitmentKeyRings(),
+				true,
+			)
+		},
+	).Unpack()
+	if err != nil {
+		log.Errorf("error continuing PSBT flow: %v", err)
+		f.failFundingFlow(peer, cid, err)
+		return
+	}
+
 	// With all the necessary data available, attempt to advance the
 	// funding workflow to the next stage. If this succeeds then the
 	// funding transaction will broadcast after our next message.
 	// CompleteReservationSingle will also mark the channel as 'IsPending'
 	// in the database.
+	//
+	// We'll also directly pass in the AuxFunding controller as well,
+	// which may be used by the reservation system to finalize funding our
+	// side.
 	completeChan, err := resCtx.reservation.CompleteReservationSingle(
-		&fundingOut, commitSig,
+		&fundingOut, commitSig, auxFundingDesc,
 	)
 	if err != nil {
-		// TODO(roasbeef): better error logging: peerID, channelID, etc.
 		log.Errorf("unable to complete single reservation: %v", err)
 		f.failFundingFlow(peer, cid, err)
 		return
@@ -2632,6 +2719,14 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer,
 		return
 	}
 
+	if resCtx.reservation.State() != lnwallet.SentFundingCreated {
+		err := fmt.Errorf("unable to find reservation for chan_id=%x",
+			msg.ChanID)
+		f.failFundingFlow(peer, cid, err)
+
+		return
+	}
+
 	// Create an entry in the local discovery map so we can ensure that we
 	// process the channel confirmation fully before we receive a
 	// channel_ready message.
@@ -2727,6 +2822,21 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer,
 		}
 	}
 
+	// Before we proceed, if we have a funding hook that wants a
+	// notification that it's safe to broadcast the funding transaction,
+	// then we'll send that now.
+	err = fn.MapOptionZ(
+		f.cfg.AuxFundingController,
+		func(controller AuxFundingController) error {
+			return controller.ChannelFinalized(cid.tempChanID)
+		},
+	)
+	if err != nil {
+		log.Errorf("Failed to inform aux funding controller about "+
+			"ChannelPoint(%v) being finalized: %v", fundingPoint,
+			err)
+	}
+
 	// Now that we have a finalized reservation for this funding flow,
 	// we'll send the to be active channel to the ChainArbitrator so it can
 	// watch for any on-chain actions before the channel has fully
@@ -2742,9 +2852,6 @@ func (f *Manager) funderProcessFundingSigned(peer lnpeer.Peer,
 
 	// Send an update to the upstream client that the negotiation process
 	// is over.
-	//
-	// TODO(roasbeef): add abstraction over updates to accommodate
-	// long-polling, or SSE, etc.
 	upd := &lnrpc.OpenStatusUpdate{
 		Update: &lnrpc.OpenStatusUpdate_ChanPending{
 			ChanPending: &lnrpc.PendingUpdate{
@@ -3450,6 +3557,7 @@ func (f *Manager) addToGraph(completeChan *channeldb.OpenChannel,
 	errChan := f.cfg.SendAnnouncement(
 		ann.chanAnn, discovery.ChannelCapacity(completeChan.Capacity),
 		discovery.ChannelPoint(completeChan.FundingOutpoint),
+		discovery.TapscriptRoot(completeChan.TapscriptRoot),
 	)
 	select {
 	case err := <-errChan:
@@ -3646,7 +3754,7 @@ func (f *Manager) annAfterSixConfs(completeChan *channeldb.OpenChannel,
 
 // waitForZeroConfChannel is called when the state is addedToGraph with
 // a zero-conf channel. This will wait for the real confirmation, add the
-// confirmed SCID to the graph, and then announce after six confs.
+// confirmed SCID to the router graph, and then announce after six confs.
 func (f *Manager) waitForZeroConfChannel(c *channeldb.OpenChannel) error {
 	// First we'll check whether the channel is confirmed on-chain. If it
 	// is already confirmed, the chainntnfs subsystem will return with the
@@ -3977,6 +4085,26 @@ func (f *Manager) handleChannelReady(peer lnpeer.Peer, //nolint:funlen
 				PubNonce: remoteNonce,
 			}),
 		)
+
+		// Inform the aux funding controller that the liquidity in the
+		// custom channel is now ready to be advertised. We potentially
+		// haven't sent our own channel ready message yet, but other
+		// than that the channel is ready to count toward available
+		// liquidity.
+		err = fn.MapOptionZ(
+			f.cfg.AuxFundingController,
+			func(controller AuxFundingController) error {
+				return controller.ChannelReady(
+					lnwallet.NewAuxChanState(channel),
+				)
+			},
+		)
+		if err != nil {
+			cid := newChanIdentifier(msg.ChanID)
+			f.sendWarning(peer, cid, err)
+
+			return
+		}
 	}
 
 	// The channel_ready message contains the next commitment point we'll
@@ -4063,6 +4191,19 @@ func (f *Manager) handleChannelReadyReceived(channel *channeldb.OpenChannel,
 	log.Debugf("Channel(%v) with ShortChanID %v: successfully "+
 		"added to graph", chanID, scid)
 
+	err = fn.MapOptionZ(
+		f.cfg.AuxFundingController,
+		func(controller AuxFundingController) error {
+			return controller.ChannelReady(
+				lnwallet.NewAuxChanState(channel),
+			)
+		},
+	)
+	if err != nil {
+		return fmt.Errorf("failed notifying aux funding controller "+
+			"about channel ready: %w", err)
+	}
+
 	// Give the caller a final update notifying them that the channel is
 	fundingPoint := channel.FundingOutpoint
 	cp := &lnrpc.ChannelPoint{
@@ -4376,9 +4517,9 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey,
 	//
 	// We can pass in zeroes for the min and max htlc policy, because we
 	// only use the channel announcement message from the returned struct.
-	ann, err := f.newChanAnnouncement(localIDKey, remoteIDKey,
-		localFundingKey, remoteFundingKey, shortChanID, chanID,
-		0, 0, nil, chanType,
+	ann, err := f.newChanAnnouncement(
+		localIDKey, remoteIDKey, localFundingKey, remoteFundingKey,
+		shortChanID, chanID, 0, 0, nil, chanType,
 	)
 	if err != nil {
 		log.Errorf("can't generate channel announcement: %v", err)
@@ -4444,7 +4585,6 @@ func (f *Manager) announceChannel(localIDKey, remoteIDKey *btcec.PublicKey,
 
 // InitFundingWorkflow sends a message to the funding manager instructing it
 // to initiate a single funder workflow with the source peer.
-// TODO(roasbeef): re-visit blocking nature..
 func (f *Manager) InitFundingWorkflow(msg *InitFundingMsg) {
 	f.fundingRequests <- msg
 }
@@ -4634,6 +4774,23 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
 		scidFeatureVal = true
 	}
 
+	// At this point, if we have an AuxFundingController active, we'll check
+	// to see if we have a special tapscript root to use in our MuSig2
+	// funding output.
+	tapscriptRoot, err := fn.MapOptionZ(
+		f.cfg.AuxFundingController,
+		func(c AuxFundingController) AuxTapscriptResult {
+			return c.DeriveTapscriptRoot(chanID)
+		},
+	).Unpack()
+	if err != nil {
+		err = fmt.Errorf("error deriving tapscript root: %w", err)
+		log.Error(err)
+		msg.Err <- err
+
+		return
+	}
+
 	req := &lnwallet.InitFundingReserveMsg{
 		ChainHash:         &msg.ChainHash,
 		PendingChanID:     chanID,
@@ -4673,6 +4830,7 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
 		OptionScidAlias:  scid,
 		ScidAliasFeature: scidFeatureVal,
 		Memo:             msg.Memo,
+		TapscriptRoot:    tapscriptRoot,
 	}
 
 	reservation, err := f.cfg.Wallet.InitChannelReservation(req)
@@ -4824,6 +4982,8 @@ func (f *Manager) handleInitFundingMsg(msg *InitFundingMsg) {
 	log.Infof("Starting funding workflow with %v for pending_id(%x), "+
 		"committype=%v", msg.Peer.Address(), chanID, commitType)
 
+	reservation.SetState(lnwallet.SentOpenChannel)
+
 	fundingOpen := lnwire.OpenChannel{
 		ChainHash:             *f.cfg.Wallet.Cfg.NetParams.GenesisHash,
 		PendingChannelID:      chanID,
diff --git a/graph/builder.go b/graph/builder.go
index 717d3e5ad..3c882058b 100644
--- a/graph/builder.go
+++ b/graph/builder.go
@@ -1092,8 +1092,8 @@ func (b *Builder) addZombieEdge(chanID uint64) error {
 // segwit v1 (taproot) channels.
 //
 // TODO(roasbeef: export and use elsewhere?
-func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte,
-	chanFeatures []byte) ([]byte, error) {
+func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte, chanFeatures []byte,
+	tapscriptRoot fn.Option[chainhash.Hash]) ([]byte, error) {
 
 	legacyFundingScript := func() ([]byte, error) {
 		witnessScript, err := input.GenMultiSigScript(
@@ -1140,7 +1140,7 @@ func makeFundingScript(bitcoinKey1, bitcoinKey2 []byte,
 		}
 
 		fundingScript, _, err := input.GenTaprootFundingScript(
-			pubKey1, pubKey2, 0, fn.None[chainhash.Hash](),
+			pubKey1, pubKey2, 0, tapscriptRoot,
 		)
 		if err != nil {
 			return nil, err
@@ -1272,7 +1272,7 @@ func (b *Builder) processUpdate(msg interface{},
 		// reality.
 		fundingPkScript, err := makeFundingScript(
 			msg.BitcoinKey1Bytes[:], msg.BitcoinKey2Bytes[:],
-			msg.Features,
+			msg.Features, msg.TapscriptRoot,
 		)
 		if err != nil {
 			return err
diff --git a/htlcswitch/link.go b/htlcswitch/link.go
index 83495f357..7f0b3eb8c 100644
--- a/htlcswitch/link.go
+++ b/htlcswitch/link.go
@@ -2191,10 +2191,20 @@ func (l *channelLink) handleUpstreamMsg(msg lnwire.Message) {
 		// We just received a new updates to our local commitment
 		// chain, validate this new commitment, closing the link if
 		// invalid.
+		auxSigBlob, err := msg.CustomRecords.Serialize()
+		if err != nil {
+			l.failf(
+				LinkFailureError{code: ErrInvalidCommitment},
+				"unable to serialize custom records: %v", err,
+			)
+
+			return
+		}
 		err = l.channel.ReceiveNewCommitment(&lnwallet.CommitSigs{
 			CommitSig:  msg.CommitSig,
 			HtlcSigs:   msg.HtlcSigs,
 			PartialSig: msg.PartialSig,
+			AuxSigBlob: auxSigBlob,
 		})
 		if err != nil {
 			// If we were unable to reconstruct their proposed
@@ -2621,11 +2631,17 @@ func (l *channelLink) updateCommitTx() error {
 	default:
 	}
 
+	auxBlobRecords, err := lnwire.ParseCustomRecords(newCommit.AuxSigBlob)
+	if err != nil {
+		return fmt.Errorf("error parsing aux sigs: %w", err)
+	}
+
 	commitSig := &lnwire.CommitSig{
-		ChanID:     l.ChanID(),
-		CommitSig:  newCommit.CommitSig,
-		HtlcSigs:   newCommit.HtlcSigs,
-		PartialSig: newCommit.PartialSig,
+		ChanID:        l.ChanID(),
+		CommitSig:     newCommit.CommitSig,
+		HtlcSigs:      newCommit.HtlcSigs,
+		PartialSig:    newCommit.PartialSig,
+		CustomRecords: auxBlobRecords,
 	}
 	l.cfg.Peer.SendMessage(false, commitSig)
 
@@ -3777,7 +3793,18 @@ func (l *channelLink) processExitHop(add lnwire.UpdateAddHTLC,
 	// As we're the exit hop, we'll double check the hop-payload included in
 	// the HTLC to ensure that it was crafted correctly by the sender and
 	// is compatible with the HTLC we were extended.
-	if add.Amount < fwdInfo.AmountToForward {
+	//
+	// For a special case, if the fwdInfo doesn't have any blinded path
+	// information, and the incoming HTLC had special extra data, then
+	// we'll skip this amount check. The invoice acceptor will make sure we
+	// reject the HTLC if it's not containing the correct amount after
+	// examining the custom data.
+	hasBlindedPath := fwdInfo.NextBlinding.IsSome()
+	customHTLC := len(add.CustomRecords) > 0 && !hasBlindedPath
+	log.Tracef("Exit hop has_blinded_path=%v custom_htlc_bypass=%v",
+		hasBlindedPath, customHTLC)
+
+	if !customHTLC && add.Amount < fwdInfo.AmountToForward {
 		l.log.Errorf("onion payload of incoming htlc(%x) has "+
 			"incompatible value: expected <=%v, got %v",
 			add.PaymentHash, add.Amount, fwdInfo.AmountToForward)
diff --git a/lnrpc/lightning.pb.go b/lnrpc/lightning.pb.go
index 8ff18e4b1..26ab31dd9 100644
--- a/lnrpc/lightning.pb.go
+++ b/lnrpc/lightning.pb.go
@@ -4703,6 +4703,8 @@ type Channel struct {
 	// useful information. This is only ever stored locally and in no way impacts
 	// the channel's operation.
 	Memo string `protobuf:"bytes,36,opt,name=memo,proto3" json:"memo,omitempty"`
+	// Custom channel data that might be populated in custom channels.
+	CustomChannelData []byte `protobuf:"bytes,37,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"`
 }
 
 func (x *Channel) Reset() {
@@ -4993,6 +4995,13 @@ func (x *Channel) GetMemo() string {
 	return ""
 }
 
+func (x *Channel) GetCustomChannelData() []byte {
+	if x != nil {
+		return x.CustomChannelData
+	}
+	return nil
+}
+
 type ListChannelsRequest struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -9505,6 +9514,9 @@ type ChannelBalanceResponse struct {
 	PendingOpenLocalBalance *Amount `protobuf:"bytes,7,opt,name=pending_open_local_balance,json=pendingOpenLocalBalance,proto3" json:"pending_open_local_balance,omitempty"`
 	// Sum of channels pending remote balances.
 	PendingOpenRemoteBalance *Amount `protobuf:"bytes,8,opt,name=pending_open_remote_balance,json=pendingOpenRemoteBalance,proto3" json:"pending_open_remote_balance,omitempty"`
+	// Custom channel data that might be populated if there are custom channels
+	// present.
+	CustomChannelData []byte `protobuf:"bytes,9,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"`
 }
 
 func (x *ChannelBalanceResponse) Reset() {
@@ -9597,6 +9609,13 @@ func (x *ChannelBalanceResponse) GetPendingOpenRemoteBalance() *Amount {
 	return nil
 }
 
+func (x *ChannelBalanceResponse) GetCustomChannelData() []byte {
+	if x != nil {
+		return x.CustomChannelData
+	}
+	return nil
+}
+
 type QueryRoutesRequest struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -17642,6 +17661,8 @@ type PendingChannelsResponse_PendingChannel struct {
 	// useful information. This is only ever stored locally and in no way
 	// impacts the channel's operation.
 	Memo string `protobuf:"bytes,13,opt,name=memo,proto3" json:"memo,omitempty"`
+	// Custom channel data that might be populated in custom channels.
+	CustomChannelData []byte `protobuf:"bytes,34,opt,name=custom_channel_data,json=customChannelData,proto3" json:"custom_channel_data,omitempty"`
 }
 
 func (x *PendingChannelsResponse_PendingChannel) Reset() {
@@ -17767,6 +17788,13 @@ func (x *PendingChannelsResponse_PendingChannel) GetMemo() string {
 	return ""
 }
 
+func (x *PendingChannelsResponse_PendingChannel) GetCustomChannelData() []byte {
+	if x != nil {
+		return x.CustomChannelData
+	}
+	return nil
+}
+
 type PendingChannelsResponse_PendingOpenChannel struct {
 	state         protoimpl.MessageState
 	sizeCache     protoimpl.SizeCache
@@ -18652,7 +18680,7 @@ var file_lightning_proto_rawDesc = []byte{
 	0x61, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x61, 0x63, 0x63, 0x65, 0x70, 0x74,
 	0x65, 0x64, 0x5f, 0x68, 0x74, 0x6c, 0x63, 0x73, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x10,
 	0x6d, 0x61, 0x78, 0x41, 0x63, 0x63, 0x65, 0x70, 0x74, 0x65, 0x64, 0x48, 0x74, 0x6c, 0x63, 0x73,
-	0x22, 0xad, 0x0b, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06,
+	0x22, 0xdd, 0x0b, 0x0a, 0x07, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x16, 0x0a, 0x06,
 	0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x61, 0x63,
 	0x74, 0x69, 0x76, 0x65, 0x12, 0x23, 0x0a, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x5f, 0x70,
 	0x75, 0x62, 0x6b, 0x65, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x72, 0x65, 0x6d,
@@ -18743,6 +18771,9 @@ var file_lightning_proto_rawDesc = []byte{
 	0x69, 0x61, 0x73, 0x18, 0x23, 0x20, 0x01, 0x28, 0x04, 0x42, 0x02, 0x30, 0x01, 0x52, 0x0d, 0x70,
 	0x65, 0x65, 0x72, 0x53, 0x63, 0x69, 0x64, 0x41, 0x6c, 0x69, 0x61, 0x73, 0x12, 0x12, 0x0a, 0x04,
 	0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x24, 0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f,
+	0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e,
+	0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x25, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x63,
+	0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x44, 0x61, 0x74, 0x61,
 	0x22, 0xdf, 0x01, 0x0a, 0x13, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c,
 	0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1f, 0x0a, 0x0b, 0x61, 0x63, 0x74, 0x69,
 	0x76, 0x65, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0a, 0x61,
@@ -19326,7 +19357,7 @@ var file_lightning_proto_rawDesc = []byte{
 	0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x24, 0x0a,
 	0x0e, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x5f, 0x72, 0x61, 0x77, 0x5f, 0x74, 0x78, 0x18,
 	0x01, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0c, 0x69, 0x6e, 0x63, 0x6c, 0x75, 0x64, 0x65, 0x52, 0x61,
-	0x77, 0x54, 0x78, 0x22, 0xe1, 0x13, 0x0a, 0x17, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43,
+	0x77, 0x54, 0x78, 0x22, 0x91, 0x14, 0x0a, 0x17, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x43,
 	0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
 	0x2e, 0x0a, 0x13, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x5f, 0x6c, 0x69, 0x6d, 0x62, 0x6f, 0x5f, 0x62,
 	0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x11, 0x74, 0x6f,
@@ -19358,7 +19389,7 @@ var file_lightning_proto_rawDesc = []byte{
 	0x6c, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x2e, 0x57, 0x61, 0x69, 0x74, 0x69,
 	0x6e, 0x67, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x14,
 	0x77, 0x61, 0x69, 0x74, 0x69, 0x6e, 0x67, 0x43, 0x6c, 0x6f, 0x73, 0x65, 0x43, 0x68, 0x61, 0x6e,
-	0x6e, 0x65, 0x6c, 0x73, 0x1a, 0xb3, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67,
+	0x6e, 0x65, 0x6c, 0x73, 0x1a, 0xe3, 0x04, 0x0a, 0x0e, 0x50, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67,
 	0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x12, 0x26, 0x0a, 0x0f, 0x72, 0x65, 0x6d, 0x6f, 0x74,
 	0x65, 0x5f, 0x6e, 0x6f, 0x64, 0x65, 0x5f, 0x70, 0x75, 0x62, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09,
 	0x52, 0x0d, 0x72, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x4e, 0x6f, 0x64, 0x65, 0x50, 0x75, 0x62, 0x12,
@@ -19393,7 +19424,10 @@ var file_lightning_proto_rawDesc = []byte{
 	0x6e, 0x53, 0x74, 0x61, 0x74, 0x75, 0x73, 0x46, 0x6c, 0x61, 0x67, 0x73, 0x12, 0x18, 0x0a, 0x07,
 	0x70, 0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x70,
 	0x72, 0x69, 0x76, 0x61, 0x74, 0x65, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x18, 0x0d,
-	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x1a, 0xf9, 0x01, 0x0a, 0x12, 0x50,
+	0x20, 0x01, 0x28, 0x09, 0x52, 0x04, 0x6d, 0x65, 0x6d, 0x6f, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75,
+	0x73, 0x74, 0x6f, 0x6d, 0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74,
+	0x61, 0x18, 0x22, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43,
+	0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x1a, 0xf9, 0x01, 0x0a, 0x12, 0x50,
 	0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x6e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65,
 	0x6c, 0x12, 0x47, 0x0a, 0x07, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x18, 0x01, 0x20, 0x01,
 	0x28, 0x0b, 0x32, 0x2d, 0x2e, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x50, 0x65, 0x6e, 0x64, 0x69,
@@ -19571,7 +19605,7 @@ var file_lightning_proto_rawDesc = []byte{
 	0x04, 0x52, 0x03, 0x73, 0x61, 0x74, 0x12, 0x12, 0x0a, 0x04, 0x6d, 0x73, 0x61, 0x74, 0x18, 0x02,
 	0x20, 0x01, 0x28, 0x04, 0x52, 0x04, 0x6d, 0x73, 0x61, 0x74, 0x22, 0x17, 0x0a, 0x15, 0x43, 0x68,
 	0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x71, 0x75,
-	0x65, 0x73, 0x74, 0x22, 0x80, 0x04, 0x0a, 0x16, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42,
+	0x65, 0x73, 0x74, 0x22, 0xb0, 0x04, 0x0a, 0x16, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x42,
 	0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1c,
 	0x0a, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x42,
 	0x02, 0x18, 0x01, 0x52, 0x07, 0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x34, 0x0a, 0x14,
@@ -19603,7 +19637,10 @@ var file_lightning_proto_rawDesc = []byte{
 	0x62, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x0d, 0x2e,
 	0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x41, 0x6d, 0x6f, 0x75, 0x6e, 0x74, 0x52, 0x18, 0x70, 0x65,
 	0x6e, 0x64, 0x69, 0x6e, 0x67, 0x4f, 0x70, 0x65, 0x6e, 0x52, 0x65, 0x6d, 0x6f, 0x74, 0x65, 0x42,
-	0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x22, 0x9a, 0x07, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79,
+	0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d,
+	0x5f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x18, 0x09, 0x20,
+	0x01, 0x28, 0x0c, 0x52, 0x11, 0x63, 0x75, 0x73, 0x74, 0x6f, 0x6d, 0x43, 0x68, 0x61, 0x6e, 0x6e,
+	0x65, 0x6c, 0x44, 0x61, 0x74, 0x61, 0x22, 0x9a, 0x07, 0x0a, 0x12, 0x51, 0x75, 0x65, 0x72, 0x79,
 	0x52, 0x6f, 0x75, 0x74, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x17, 0x0a,
 	0x07, 0x70, 0x75, 0x62, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06,
 	0x70, 0x75, 0x62, 0x4b, 0x65, 0x79, 0x12, 0x10, 0x0a, 0x03, 0x61, 0x6d, 0x74, 0x18, 0x02, 0x20,
diff --git a/lnrpc/lightning.proto b/lnrpc/lightning.proto
index 6d975c8c3..b44b04caa 100644
--- a/lnrpc/lightning.proto
+++ b/lnrpc/lightning.proto
@@ -1592,6 +1592,11 @@ message Channel {
     the channel's operation.
     */
     string memo = 36;
+
+    /*
+    Custom channel data that might be populated in custom channels.
+    */
+    bytes custom_channel_data = 37;
 }
 
 message ListChannelsRequest {
@@ -2709,6 +2714,11 @@ message PendingChannelsResponse {
         impacts the channel's operation.
         */
         string memo = 13;
+
+        /*
+        Custom channel data that might be populated in custom channels.
+        */
+        bytes custom_channel_data = 34;
     }
 
     message PendingOpenChannel {
@@ -2968,6 +2978,12 @@ message ChannelBalanceResponse {
 
     // Sum of channels pending remote balances.
     Amount pending_open_remote_balance = 8;
+
+    /*
+    Custom channel data that might be populated if there are custom channels
+    present.
+    */
+    bytes custom_channel_data = 9;
 }
 
 message QueryRoutesRequest {
diff --git a/lnrpc/lightning.swagger.json b/lnrpc/lightning.swagger.json
index fe204e12c..0e69fe1fd 100644
--- a/lnrpc/lightning.swagger.json
+++ b/lnrpc/lightning.swagger.json
@@ -3127,6 +3127,11 @@
         "memo": {
           "type": "string",
           "description": "An optional note-to-self to go along with the channel containing some\nuseful information. This is only ever stored locally and in no way\nimpacts the channel's operation."
+        },
+        "custom_channel_data": {
+          "type": "string",
+          "format": "byte",
+          "description": "Custom channel data that might be populated in custom channels."
         }
       }
     },
@@ -3849,6 +3854,11 @@
         "memo": {
           "type": "string",
           "description": "An optional note-to-self to go along with the channel containing some\nuseful information. This is only ever stored locally and in no way impacts\nthe channel's operation."
+        },
+        "custom_channel_data": {
+          "type": "string",
+          "format": "byte",
+          "description": "Custom channel data that might be populated in custom channels."
         }
       }
     },
@@ -4052,6 +4062,11 @@
         "pending_open_remote_balance": {
           "$ref": "#/definitions/lnrpcAmount",
           "description": "Sum of channels pending remote balances."
+        },
+        "custom_channel_data": {
+          "type": "string",
+          "format": "byte",
+          "description": "Custom channel data that might be populated if there are custom channels\npresent."
         }
       }
     },
diff --git a/lnrpc/routerrpc/router_backend.go b/lnrpc/routerrpc/router_backend.go
index 6694e8b4f..4ef60a71d 100644
--- a/lnrpc/routerrpc/router_backend.go
+++ b/lnrpc/routerrpc/router_backend.go
@@ -25,6 +25,7 @@ import (
 	"github.com/lightningnetwork/lnd/routing/route"
 	"github.com/lightningnetwork/lnd/subscribe"
 	"github.com/lightningnetwork/lnd/zpay32"
+	"google.golang.org/protobuf/proto"
 )
 
 const (
@@ -104,6 +105,10 @@ type RouterBackend struct {
 	// TODO(yy): remove this config after the new status code is fully
 	// deployed to the network(v0.20.0).
 	UseStatusInitiated bool
+
+	// ParseCustomChannelData is a function that can be used to parse custom
+	// channel data from the first hop of a route.
+	ParseCustomChannelData func(message proto.Message) error
 }
 
 // MissionControl defines the mission control dependencies of routerrpc.
@@ -596,8 +601,14 @@ func (r *RouterBackend) MarshallRoute(route *route.Route) (*lnrpc.Route, error)
 
 		resp.CustomChannelData = customData
 
-		// TODO(guggero): Feed the route into the custom data parser
-		// (part 3 of the mega PR series).
+		// Allow the aux data parser to parse the custom records into
+		// a human-readable JSON (if available).
+		if r.ParseCustomChannelData != nil {
+			err := r.ParseCustomChannelData(resp)
+			if err != nil {
+				return nil, err
+			}
+		}
 	}
 
 	incomingAmt := route.TotalAmount
diff --git a/lnwallet/aux_signer.go b/lnwallet/aux_signer.go
new file mode 100644
index 000000000..5d4bc7924
--- /dev/null
+++ b/lnwallet/aux_signer.go
@@ -0,0 +1,250 @@
+package lnwallet
+
+import (
+	"github.com/btcsuite/btcd/wire"
+	"github.com/lightningnetwork/lnd/fn"
+	"github.com/lightningnetwork/lnd/input"
+	"github.com/lightningnetwork/lnd/lntypes"
+	"github.com/lightningnetwork/lnd/lnwire"
+	"github.com/lightningnetwork/lnd/tlv"
+)
+
+// htlcCustomSigType is the TLV type that is used to encode the custom HTLC
+// signatures within the custom data for an existing HTLC.
+var htlcCustomSigType tlv.TlvType65543
+
+// AuxHtlcDescriptor is a struct that contains the information needed to sign or
+// verify an HTLC for custom channels.
+type AuxHtlcDescriptor struct {
+	// ChanID is the ChannelID of the LightningChannel that this
+	// paymentDescriptor belongs to. We track this here so we can
+	// reconstruct the Messages that this paymentDescriptor is built from.
+	ChanID lnwire.ChannelID
+
+	// RHash is the payment hash for this HTLC. The HTLC can be settled iff
+	// the preimage to this hash is presented.
+	RHash PaymentHash
+
+	// Timeout is the absolute timeout in blocks, after which this HTLC
+	// expires.
+	Timeout uint32
+
+	// Amount is the HTLC amount in milli-satoshis.
+	Amount lnwire.MilliSatoshi
+
+	// HtlcIndex is the index within the main update log for this HTLC.
+	// Entries within the log of type Add will have this field populated,
+	// as other entries will point to the entry via this counter.
+	//
+	// NOTE: This field will only be populated if EntryType is Add.
+	HtlcIndex uint64
+
+	// ParentIndex is the HTLC index of the entry that this update settles
+	// or times out.
+	//
+	// NOTE: This field will only be populated if EntryType is Fail or
+	// Settle.
+	ParentIndex uint64
+
+	// EntryType denotes the exact type of the paymentDescriptor. In the
+	// case of a Timeout, or Settle type, then the Parent field will point
+	// into the log to the HTLC being modified.
+	EntryType updateType
+
+	// CustomRecords also stores the set of optional custom records that
+	// may have been attached to a sent HTLC.
+	CustomRecords lnwire.CustomRecords
+
+	// addCommitHeight[Remote|Local] encodes the height of the commitment
+	// which included this HTLC on either the remote or local commitment
+	// chain. This value is used to determine when an HTLC is fully
+	// "locked-in".
+	addCommitHeightRemote uint64
+	addCommitHeightLocal  uint64
+
+	// removeCommitHeight[Remote|Local] encodes the height of the
+	// commitment which removed the parent pointer of this
+	// paymentDescriptor either due to a timeout or a settle. Once both
+	// these heights are below the tail of both chains, the log entries can
+	// safely be removed.
+	removeCommitHeightRemote uint64
+	removeCommitHeightLocal  uint64
+}
+
+// AddHeight returns the height at which the HTLC was added to the commitment
+// chain. The height is returned based on the chain the HTLC is being added to
+// (local or remote chain).
+func (a *AuxHtlcDescriptor) AddHeight(
+	whoseCommitChain lntypes.ChannelParty) uint64 {
+
+	if whoseCommitChain.IsRemote() {
+		return a.addCommitHeightRemote
+	}
+
+	return a.addCommitHeightLocal
+}
+
+// RemoveHeight returns the height at which the HTLC was removed from the
+// commitment chain. The height is returned based on the chain the HTLC is being
+// removed from (local or remote chain).
+func (a *AuxHtlcDescriptor) RemoveHeight(
+	whoseCommitChain lntypes.ChannelParty) uint64 {
+
+	if whoseCommitChain.IsRemote() {
+		return a.removeCommitHeightRemote
+	}
+
+	return a.removeCommitHeightLocal
+}
+
+// newAuxHtlcDescriptor creates a new AuxHtlcDescriptor from a payment
+// descriptor.
+func newAuxHtlcDescriptor(p *paymentDescriptor) AuxHtlcDescriptor {
+	return AuxHtlcDescriptor{
+		ChanID:                   p.ChanID,
+		RHash:                    p.RHash,
+		Timeout:                  p.Timeout,
+		Amount:                   p.Amount,
+		HtlcIndex:                p.HtlcIndex,
+		ParentIndex:              p.ParentIndex,
+		EntryType:                p.EntryType,
+		CustomRecords:            p.CustomRecords.Copy(),
+		addCommitHeightRemote:    p.addCommitHeightRemote,
+		addCommitHeightLocal:     p.addCommitHeightLocal,
+		removeCommitHeightRemote: p.removeCommitHeightRemote,
+		removeCommitHeightLocal:  p.removeCommitHeightLocal,
+	}
+}
+
+// BaseAuxJob is a struct that contains the common fields that are shared among
+// the aux sign/verify jobs.
+type BaseAuxJob struct {
+	// OutputIndex is the output index of the HTLC on the commitment
+	// transaction being signed.
+	//
+	// NOTE: If the output is dust from the PoV of the commitment chain,
+	// then this value will be -1.
+	OutputIndex int32
+
+	// KeyRing is the commitment key ring that contains the keys needed to
+	// generate the second level HTLC signatures.
+	KeyRing CommitmentKeyRing
+
+	// HTLC is the HTLC that is being signed or verified.
+	HTLC AuxHtlcDescriptor
+
+	// Incoming is a boolean that indicates if the HTLC is incoming or
+	// outgoing.
+	Incoming bool
+
+	// CommitBlob is the commitment transaction blob that contains the aux
+	// information for this channel.
+	CommitBlob fn.Option[tlv.Blob]
+
+	// HtlcLeaf is the aux tap leaf that corresponds to the HTLC being
+	// signed/verified.
+	HtlcLeaf input.AuxTapLeaf
+}
+
+// AuxSigJob is a struct that contains all the information needed to sign an
+// HTLC for custom channels.
+type AuxSigJob struct {
+	// SignDesc is the sign desc for this HTLC.
+	SignDesc input.SignDescriptor
+
+	BaseAuxJob
+
+	// Resp is a channel that will be used to send the result of the sign
+	// job. This channel MUST be buffered.
+	Resp chan AuxSigJobResp
+
+	// Cancel is a channel that is closed by the caller if they wish to
+	// abandon all pending sign jobs part of a single batch. This should
+	// never be closed by the validator.
+	Cancel <-chan struct{}
+}
+
+// NewAuxSigJob creates a new AuxSigJob.
+func NewAuxSigJob(sigJob SignJob, keyRing CommitmentKeyRing, incoming bool,
+	htlc AuxHtlcDescriptor, commitBlob fn.Option[tlv.Blob],
+	htlcLeaf input.AuxTapLeaf, cancelChan <-chan struct{}) AuxSigJob {
+
+	return AuxSigJob{
+		SignDesc: sigJob.SignDesc,
+		BaseAuxJob: BaseAuxJob{
+			OutputIndex: sigJob.OutputIndex,
+			KeyRing:     keyRing,
+			HTLC:        htlc,
+			Incoming:    incoming,
+			CommitBlob:  commitBlob,
+			HtlcLeaf:    htlcLeaf,
+		},
+		Resp:   make(chan AuxSigJobResp, 1),
+		Cancel: cancelChan,
+	}
+}
+
+// AuxSigJobResp is a struct that contains the result of a sign job.
+type AuxSigJobResp struct {
+	// SigBlob is the signature blob that was generated for the HTLC. This
+	// is an opaque TLV field that may contain the signature and other data.
+	SigBlob fn.Option[tlv.Blob]
+
+	// HtlcIndex is the index of the HTLC that was signed.
+	HtlcIndex uint64
+
+	// Err is the error that occurred when executing the specified
+	// signature job. In the case that no error occurred, this value will
+	// be nil.
+	Err error
+}
+
+// AuxVerifyJob is a struct that contains all the information needed to verify
+// an HTLC for custom channels.
+type AuxVerifyJob struct {
+	// SigBlob is the signature blob that was generated for the HTLC. This
+	// is an opaque TLV field that may contain the signature and other data.
+	SigBlob fn.Option[tlv.Blob]
+
+	BaseAuxJob
+}
+
+// NewAuxVerifyJob creates a new AuxVerifyJob.
+func NewAuxVerifyJob(sig fn.Option[tlv.Blob], keyRing CommitmentKeyRing,
+	incoming bool, htlc AuxHtlcDescriptor, commitBlob fn.Option[tlv.Blob],
+	htlcLeaf input.AuxTapLeaf) AuxVerifyJob {
+
+	return AuxVerifyJob{
+		SigBlob: sig,
+		BaseAuxJob: BaseAuxJob{
+			KeyRing:    keyRing,
+			HTLC:       htlc,
+			Incoming:   incoming,
+			CommitBlob: commitBlob,
+			HtlcLeaf:   htlcLeaf,
+		},
+	}
+}
+
+// AuxSigner is an interface that is used to sign and verify HTLCs for custom
+// channels. It is similar to the existing SigPool, but uses opaque blobs to
+// shuffle around signature information and other metadata.
+type AuxSigner interface {
+	// SubmitSecondLevelSigBatch takes a batch of aux sign jobs and
+	// processes them asynchronously.
+	SubmitSecondLevelSigBatch(chanState AuxChanState, commitTx *wire.MsgTx,
+		sigJob []AuxSigJob) error
+
+	// PackSigs takes a series of aux signatures and packs them into a
+	// single blob that can be sent alongside the CommitSig messages.
+	PackSigs([]fn.Option[tlv.Blob]) fn.Result[fn.Option[tlv.Blob]]
+
+	// UnpackSigs takes a packed blob of signatures and returns the
+	// original signatures for each HTLC, keyed by HTLC index.
+	UnpackSigs(fn.Option[tlv.Blob]) fn.Result[[]fn.Option[tlv.Blob]]
+
+	// VerifySecondLevelSigs attempts to synchronously verify a batch of aux
+	// sig jobs.
+	VerifySecondLevelSigs(chanState AuxChanState, commitTx *wire.MsgTx,
+		verifyJob []AuxVerifyJob) error
+}
diff --git a/lnwallet/chanfunding/canned_assembler.go b/lnwallet/chanfunding/canned_assembler.go
index 177cc35c3..b3457f21b 100644
--- a/lnwallet/chanfunding/canned_assembler.go
+++ b/lnwallet/chanfunding/canned_assembler.go
@@ -4,6 +4,7 @@ import (
 	"fmt"
 
 	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/wire"
@@ -98,6 +99,26 @@ func (s *ShimIntent) FundingOutput() ([]byte, *wire.TxOut, error) {
 	)
 }
 
+// TaprootInternalKey may return the internal key for a MuSig2 funding output,
+// but only if this is actually a MuSig2 channel.
+func (s *ShimIntent) TaprootInternalKey() fn.Option[*btcec.PublicKey] {
+	if !s.musig2 {
+		return fn.None[*btcec.PublicKey]()
+	}
+
+	// Similar to the existing p2wsh script, we'll always ensure the keys
+	// are sorted before use. Since we're only interested in the internal
+	// key, we don't need to take into account any tapscript root.
+	//
+	// We ignore the error here as this is only called after FundingOutput
+	// is called.
+	combinedKey, _, _, _ := musig2.AggregateKeys(
+		[]*btcec.PublicKey{s.localKey.PubKey, s.remoteKey}, true,
+	)
+
+	return fn.Some(combinedKey.PreTweakedKey)
+}
+
 // Cancel allows the caller to cancel a funding Intent at any time.  This will
 // return any resources such as coins back to the eligible pool to be used in
 // order channel fundings.
diff --git a/lnwallet/chanfunding/psbt_assembler.go b/lnwallet/chanfunding/psbt_assembler.go
index 10bcd7015..f678f520f 100644
--- a/lnwallet/chanfunding/psbt_assembler.go
+++ b/lnwallet/chanfunding/psbt_assembler.go
@@ -6,11 +6,14 @@ import (
 	"sync"
 
 	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr"
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/btcutil/psbt"
 	"github.com/btcsuite/btcd/chaincfg"
+	"github.com/btcsuite/btcd/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/txscript"
 	"github.com/btcsuite/btcd/wire"
+	"github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/input"
 	"github.com/lightningnetwork/lnd/keychain"
 )
@@ -162,6 +165,13 @@ func (i *PsbtIntent) BindKeys(localKey *keychain.KeyDescriptor,
 	i.State = PsbtOutputKnown
 }
 
+// BindTapscriptRoot takes an optional tapscript root and binds it to the
+// underlying funding intent. This only applies to musig2 channels, and will be
+// used to make the musig2 funding output.
+func (i *PsbtIntent) BindTapscriptRoot(root fn.Option[chainhash.Hash]) {
+	i.tapscriptRoot = root
+}
+
 // FundingParams returns the parameters that are necessary to start funding the
 // channel output this intent was created for. It returns the P2WSH funding
 // address, the exact funding amount and a PSBT packet that contains exactly one
@@ -208,7 +218,18 @@ func (i *PsbtIntent) FundingParams() (btcutil.Address, int64, *psbt.Packet,
 		}
 	}
 	packet.UnsignedTx.TxOut = append(packet.UnsignedTx.TxOut, out)
-	packet.Outputs = append(packet.Outputs, psbt.POutput{})
+
+	var pOut psbt.POutput
+
+	// If this is a MuSig2 channel, we also need to communicate the internal
+	// key to the caller. Otherwise, they cannot verify the construction of
+	// the P2TR output script.
+	pOut.TaprootInternalKey = fn.MapOptionZ(
+		i.TaprootInternalKey(), schnorr.SerializePubKey,
+	)
+
+	packet.Outputs = append(packet.Outputs, pOut)
+
 	return addr, out.Value, packet, nil
 }
 
diff --git a/lnwallet/channel.go b/lnwallet/channel.go
index 32492171f..c78742d13 100644
--- a/lnwallet/channel.go
+++ b/lnwallet/channel.go
@@ -6,7 +6,7 @@ import (
 	"errors"
 	"fmt"
 	"math"
-	"sort"
+	"slices"
 	"sync"
 
 	"github.com/btcsuite/btcd/blockchain"
@@ -760,6 +760,10 @@ type LightningChannel struct {
 	// signatures, of which there may be hundreds.
 	sigPool *SigPool
 
+	// auxSigner is a special signer used to obtain opaque signatures for
+	// custom channel variants.
+	auxSigner fn.Option[AuxSigner]
+
 	// Capacity is the total capacity of this channel.
 	Capacity btcutil.Amount
 
@@ -821,6 +825,7 @@ type channelOpts struct {
 	remoteNonce *musig2.Nonces
 
 	leafStore fn.Option[AuxLeafStore]
+	auxSigner fn.Option[AuxSigner]
 
 	skipNonceInit bool
 }
@@ -859,6 +864,13 @@ func WithLeafStore(store AuxLeafStore) ChannelOpt {
 	}
 }
 
+// WithAuxSigner is used to specify a custom aux signer for the channel.
+func WithAuxSigner(signer AuxSigner) ChannelOpt {
+	return func(o *channelOpts) {
+		o.auxSigner = fn.Some[AuxSigner](signer)
+	}
+}
+
 // defaultChannelOpts returns the set of default options for a new channel.
 func defaultChannelOpts() *channelOpts {
 	return &channelOpts{}
@@ -911,6 +923,7 @@ func NewLightningChannel(signer input.Signer,
 	lc := &LightningChannel{
 		Signer:        signer,
 		leafStore:     opts.leafStore,
+		auxSigner:     opts.auxSigner,
 		sigPool:       sigPool,
 		currentHeight: localCommit.CommitHeight,
 		commitChains:  commitChains,
@@ -2545,6 +2558,18 @@ type HtlcView struct {
 	FeePerKw chainfee.SatPerKWeight
 }
 
+// AuxOurUpdates returns the outgoing HTLCs as a read-only copy of
+// AuxHtlcDescriptors.
+func (v *HtlcView) AuxOurUpdates() []AuxHtlcDescriptor {
+	return fn.Map(newAuxHtlcDescriptor, v.OurUpdates)
+}
+
+// AuxTheirUpdates returns the incoming HTLCs as a read-only copy of
+// AuxHtlcDescriptors.
+func (v *HtlcView) AuxTheirUpdates() []AuxHtlcDescriptor {
+	return fn.Map(newAuxHtlcDescriptor, v.TheirUpdates)
+}
+
 // fetchHTLCView returns all the candidate HTLC updates which should be
 // considered for inclusion within a commitment based on the passed HTLC log
 // indexes.
@@ -3064,7 +3089,8 @@ func processFeeUpdate(feeUpdate *paymentDescriptor, nextHeight uint64,
 func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 	chanState *channeldb.OpenChannel, leaseExpiry uint32,
 	remoteCommitView *commitment,
-	leafStore fn.Option[AuxLeafStore]) ([]SignJob, chan struct{}, error) {
+	leafStore fn.Option[AuxLeafStore]) ([]SignJob, []AuxSigJob,
+	chan struct{}, error) {
 
 	var (
 		isRemoteInitiator = !chanState.IsInitiator
@@ -3084,6 +3110,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 	numSigs := len(remoteCommitView.incomingHTLCs) +
 		len(remoteCommitView.outgoingHTLCs)
 	sigBatch := make([]SignJob, 0, numSigs)
+	auxSigBatch := make([]AuxSigJob, 0, numSigs)
 
 	var err error
 	cancelChan := make(chan struct{})
@@ -3098,8 +3125,8 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 		},
 	).Unpack()
 	if err != nil {
-		return nil, nil, fmt.Errorf("unable to fetch aux leaves: %w",
-			err)
+		return nil, nil, nil, fmt.Errorf("unable to fetch aux leaves: "+
+			"%w", err)
 	}
 
 	// For each outgoing and incoming HTLC, if the HTLC isn't considered a
@@ -3148,12 +3175,9 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 			auxLeaf,
 		)
 		if err != nil {
-			return nil, nil, err
+			return nil, nil, nil, err
 		}
 
-		// TODO(roasbeef): hook up signer interface here (later commit
-		// in this PR).
-
 		// Construct a full hash cache as we may be signing a segwit v1
 		// sighash.
 		txOut := remoteCommitView.txn.TxOut[htlc.remoteOutputIndex]
@@ -3185,6 +3209,11 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 		}
 
 		sigBatch = append(sigBatch, sigJob)
+
+		auxSigBatch = append(auxSigBatch, NewAuxSigJob(
+			sigJob, *keyRing, true, newAuxHtlcDescriptor(&htlc),
+			remoteCommitView.customBlob, auxLeaf, cancelChan,
+		))
 	}
 	for _, htlc := range remoteCommitView.outgoingHTLCs {
 		if HtlcIsDust(
@@ -3228,7 +3257,7 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 			auxLeaf,
 		)
 		if err != nil {
-			return nil, nil, err
+			return nil, nil, nil, err
 		}
 
 		// Construct a full hash cache as we may be signing a segwit v1
@@ -3257,13 +3286,19 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 		// If this is a taproot channel, then we'll need to set the
 		// method type to ensure we generate a valid signature.
 		if chanType.IsTaproot() {
-			sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod //nolint:lll
+			//nolint:lll
+			sigJob.SignDesc.SignMethod = input.TaprootScriptSpendSignMethod
 		}
 
 		sigBatch = append(sigBatch, sigJob)
+
+		auxSigBatch = append(auxSigBatch, NewAuxSigJob(
+			sigJob, *keyRing, false, newAuxHtlcDescriptor(&htlc),
+			remoteCommitView.customBlob, auxLeaf, cancelChan,
+		))
 	}
 
-	return sigBatch, cancelChan, nil
+	return sigBatch, auxSigBatch, cancelChan, nil
 }
 
 // createCommitDiff will create a commit diff given a new pending commitment
@@ -3272,7 +3307,8 @@ func genRemoteHtlcSigJobs(keyRing *CommitmentKeyRing,
 // new commitment to the remote party. The commit diff returned contains all
 // information necessary for retransmission.
 func (lc *LightningChannel) createCommitDiff(newCommit *commitment,
-	commitSig lnwire.Sig, htlcSigs []lnwire.Sig) *channeldb.CommitDiff {
+	commitSig lnwire.Sig, htlcSigs []lnwire.Sig,
+	auxSigs []fn.Option[tlv.Blob]) (*channeldb.CommitDiff, error) {
 
 	var (
 		logUpdates        []channeldb.LogUpdate
@@ -3341,21 +3377,71 @@ func (lc *LightningChannel) createCommitDiff(newCommit *commitment,
 	// disk.
 	diskCommit := newCommit.toDiskCommit(lntypes.Remote)
 
-	return &channeldb.CommitDiff{
-		Commitment: *diskCommit,
-		CommitSig: &lnwire.CommitSig{
-			ChanID: lnwire.NewChanIDFromOutPoint(
-				lc.channelState.FundingOutpoint,
-			),
-			CommitSig: commitSig,
-			HtlcSigs:  htlcSigs,
+	// We prepare the commit sig message to be sent to the remote party.
+	commitSigMsg := &lnwire.CommitSig{
+		ChanID: lnwire.NewChanIDFromOutPoint(
+			lc.channelState.FundingOutpoint,
+		),
+		CommitSig: commitSig,
+		HtlcSigs:  htlcSigs,
+	}
+
+	// Encode and check the size of the custom records now.
+	auxCustomRecords, err := fn.MapOptionZ(
+		lc.auxSigner,
+		func(s AuxSigner) fn.Result[lnwire.CustomRecords] {
+			blobOption, err := s.PackSigs(auxSigs).Unpack()
+			if err != nil {
+				return fn.Err[lnwire.CustomRecords](err)
+			}
+
+			// We now serialize the commit sig message without the
+			// custom records to make sure we have space for them.
+			var buf bytes.Buffer
+			err = commitSigMsg.Encode(&buf, 0)
+			if err != nil {
+				return fn.Err[lnwire.CustomRecords](err)
+			}
+
+			// The number of available bytes is the max message size
+			// minus the size of the message without the custom
+			// records. We also subtract 8 bytes for encoding
+			// overhead of the custom records (just some safety
+			// padding).
+			available := lnwire.MaxMsgBody - buf.Len() - 8
+
+			blob := blobOption.UnwrapOr(nil)
+			if len(blob) > available {
+				err = fmt.Errorf("aux sigs size %d exceeds "+
+					"max allowed size of %d", len(blob),
+					available)
+
+				return fn.Err[lnwire.CustomRecords](err)
+			}
+
+			records, err := lnwire.ParseCustomRecords(blob)
+			if err != nil {
+				return fn.Err[lnwire.CustomRecords](err)
+			}
+
+			return fn.Ok(records)
 		},
+	).Unpack()
+	if err != nil {
+		return nil, fmt.Errorf("error packing aux sigs: %w", err)
+	}
+
+	commitSigMsg.CustomRecords = auxCustomRecords
+
+	return &channeldb.CommitDiff{
+		Commitment:        *diskCommit,
+		CommitSig:         commitSigMsg,
 		LogUpdates:        logUpdates,
 		OpenedCircuitKeys: openCircuitKeys,
 		ClosedCircuitKeys: closedCircuitKeys,
 		AddAcks:           ackAddRefs,
 		SettleFailAcks:    settleFailRefs,
-	}
+	}, nil
 }
 
 // getUnsignedAckedUpdates returns all remote log updates that we haven't
@@ -3748,6 +3834,10 @@ type CommitSigs struct {
 	// PartialSig is the musig2 partial signature for taproot commitment
 	// transactions.
 	PartialSig lnwire.OptPartialSigWithNonceTLV
+
+	// AuxSigBlob is the blob containing all the auxiliary signatures for
+	// this new commitment state.
+	AuxSigBlob tlv.Blob
 }
 
 // NewCommitState wraps the various signatures needed to properly
@@ -3772,6 +3862,8 @@ type NewCommitState struct {
 // any). The HTLC signatures are sorted according to the BIP 69 order of the
 // HTLC's on the commitment transaction. Finally, the new set of pending HTLCs
 // for the remote party's commitment are also returned.
+//
+//nolint:funlen
 func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
 	lc.Lock()
 	defer lc.Unlock()
@@ -3864,15 +3956,37 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
 	if lc.channelState.ChanType.HasLeaseExpiration() {
 		leaseExpiry = lc.channelState.ThawHeight
 	}
-	sigBatch, cancelChan, err := genRemoteHtlcSigJobs(
+	sigBatch, auxSigBatch, cancelChan, err := genRemoteHtlcSigJobs(
 		keyRing, lc.channelState, leaseExpiry, newCommitView,
 		lc.leafStore,
 	)
 	if err != nil {
 		return nil, err
 	}
+
+	// We'll need to send over the signatures to the remote party in the
+	// order as they appear on the commitment transaction after BIP 69
+	// sorting.
+	slices.SortFunc(sigBatch, func(i, j SignJob) int {
+		return int(i.OutputIndex - j.OutputIndex)
+	})
+	slices.SortFunc(auxSigBatch, func(i, j AuxSigJob) int {
+		return int(i.OutputIndex - j.OutputIndex)
+	})
+
 	lc.sigPool.SubmitSignBatch(sigBatch)
 
+	err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error {
+		return a.SubmitSecondLevelSigBatch(
+			NewAuxChanState(lc.channelState), newCommitView.txn,
+			auxSigBatch,
+		)
+	})
+	if err != nil {
+		return nil, fmt.Errorf("error submitting second level sig "+
+			"batch: %w", err)
+	}
+
 	// While the jobs are being carried out, we'll Sign their version of
 	// the new commitment transaction while we're waiting for the rest of
 	// the HTLC signatures to be processed.
@@ -3910,17 +4024,12 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
 		}
 	}
 
-	// We'll need to send over the signatures to the remote party in the
-	// order as they appear on the commitment transaction after BIP 69
-	// sorting.
-	sort.Slice(sigBatch, func(i, j int) bool {
-		return sigBatch[i].OutputIndex < sigBatch[j].OutputIndex
-	})
-
-	// With the jobs sorted, we'll now iterate through all the responses to
-	// gather each of the signatures in order.
+	// Iterate through all the responses to gather each of the signatures
+	// in the order they were submitted.
 	htlcSigs = make([]lnwire.Sig, 0, len(sigBatch))
-	for _, htlcSigJob := range sigBatch {
+	auxSigs := make([]fn.Option[tlv.Blob], 0, len(auxSigBatch))
+	for i := range sigBatch {
+		htlcSigJob := sigBatch[i]
 		jobResp := <-htlcSigJob.Resp
 
 		// If an error occurred, then we'll cancel any other active
@@ -3931,12 +4040,34 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
 		}
 
 		htlcSigs = append(htlcSigs, jobResp.Sig)
+
+		if lc.auxSigner.IsNone() {
+			continue
+		}
+
+		auxHtlcSigJob := auxSigBatch[i]
+		auxJobResp := <-auxHtlcSigJob.Resp
+
+		// If an error occurred, then we'll cancel any other active
+		// jobs.
+		if auxJobResp.Err != nil {
+			close(cancelChan)
+			return nil, auxJobResp.Err
+		}
+
+		auxSigs = append(auxSigs, auxJobResp.SigBlob)
 	}
 
 	// As we're about to proposer a new commitment state for the remote
 	// party, we'll write this pending state to disk before we exit, so we
 	// can retransmit it if necessary.
-	commitDiff := lc.createCommitDiff(newCommitView, sig, htlcSigs)
+	commitDiff, err := lc.createCommitDiff(
+		newCommitView, sig, htlcSigs, auxSigs,
+	)
+	if err != nil {
+		return nil, err
+	}
+
 	err = lc.channelState.AppendRemoteCommitChain(commitDiff)
 	if err != nil {
 		return nil, err
@@ -3950,11 +4081,18 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
 	// latest commitment update.
 	lc.commitChains.Remote.addCommitment(newCommitView)
 
+	auxSigBlob, err := commitDiff.CommitSig.CustomRecords.Serialize()
+	if err != nil {
+		return nil, fmt.Errorf("unable to serialize aux sig blob: %w",
+			err)
+	}
+
 	return &NewCommitState{
 		CommitSigs: &CommitSigs{
 			CommitSig:  sig,
 			HtlcSigs:   htlcSigs,
 			PartialSig: lnwire.MaybePartialSigWithNonce(partialSig),
+			AuxSigBlob: auxSigBlob,
 		},
 		PendingHTLCs: commitDiff.Commitment.Htlcs,
 	}, nil
@@ -3966,8 +4104,8 @@ func (lc *LightningChannel) SignNextCommitment() (*NewCommitState, error) {
 // each time. After we receive the channel reestablish message, we learn the
 // nonce we need to use for the remote party. As a result, we need to generate
 // the partial signature again with the new nonce.
-func (lc *LightningChannel) resignMusigCommit(commitTx *wire.MsgTx,
-) (lnwire.OptPartialSigWithNonceTLV, error) {
+func (lc *LightningChannel) resignMusigCommit(
+	commitTx *wire.MsgTx) (lnwire.OptPartialSigWithNonceTLV, error) {
 
 	remoteSession := lc.musigSessions.RemoteSession
 	musig, err := remoteSession.SignCommit(commitTx)
@@ -4172,13 +4310,23 @@ func (lc *LightningChannel) ProcessChanSyncMsg(
 			// If we signed this state, then we'll accumulate
 			// another update to send over.
 			case err == nil:
+				customRecords, err := lnwire.ParseCustomRecords(
+					newCommit.AuxSigBlob,
+				)
+				if err != nil {
+					sErr := fmt.Errorf("error parsing aux "+
+						"sigs: %w", err)
+					return nil, nil, nil, sErr
+				}
+
 				commitSig := &lnwire.CommitSig{
 					ChanID: lnwire.NewChanIDFromOutPoint(
 						lc.channelState.FundingOutpoint,
 					),
-					CommitSig:  newCommit.CommitSig,
-					HtlcSigs:   newCommit.HtlcSigs,
-					PartialSig: newCommit.PartialSig,
+					CommitSig:     newCommit.CommitSig,
+					HtlcSigs:      newCommit.HtlcSigs,
+					PartialSig:    newCommit.PartialSig,
+					CustomRecords: customRecords,
 				}
 
 				updates = append(updates, commitSig)
@@ -4396,6 +4544,7 @@ func (lc *LightningChannel) computeView(view *HtlcView,
 	// need this to determine which HTLCs are dust, and also the final fee
 	// rate.
 	view.FeePerKw = commitChain.tip().feePerKw
+	view.NextHeight = nextHeight
 
 	// We evaluate the view at this stage, meaning settled and failed HTLCs
 	// will remove their corresponding added HTLCs.  The resulting filtered
@@ -4471,7 +4620,8 @@ func (lc *LightningChannel) computeView(view *HtlcView,
 func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 	localCommitmentView *commitment, keyRing *CommitmentKeyRing,
 	htlcSigs []lnwire.Sig, leaseExpiry uint32,
-	leafStore fn.Option[AuxLeafStore]) ([]VerifyJob, error) {
+	leafStore fn.Option[AuxLeafStore], auxSigner fn.Option[AuxSigner],
+	sigBlob fn.Option[tlv.Blob]) ([]VerifyJob, []AuxVerifyJob, error) {
 
 	var (
 		isLocalInitiator = chanState.IsInitiator
@@ -4490,6 +4640,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 	numHtlcs := len(localCommitmentView.incomingHTLCs) +
 		len(localCommitmentView.outgoingHTLCs)
 	verifyJobs := make([]VerifyJob, 0, numHtlcs)
+	auxVerifyJobs := make([]AuxVerifyJob, 0, numHtlcs)
 
 	diskCommit := localCommitmentView.toDiskCommit(lntypes.Local)
 	auxResult, err := fn.MapOptionZ(
@@ -4501,7 +4652,20 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 		},
 	).Unpack()
 	if err != nil {
-		return nil, fmt.Errorf("unable to fetch aux leaves: %w", err)
+		return nil, nil, fmt.Errorf("unable to fetch aux leaves: %w",
+			err)
+	}
+
+	// If we have a sig blob, then we'll attempt to map that to individual
+	// blobs for each HTLC we might need a signature for.
+	auxHtlcSigs, err := fn.MapOptionZ(
+		auxSigner, func(a AuxSigner) fn.Result[[]fn.Option[tlv.Blob]] {
+			return a.UnpackSigs(sigBlob)
+		},
+	).Unpack()
+	if err != nil {
+		return nil, nil, fmt.Errorf("error unpacking aux sigs: %w",
+			err)
 	}
 
 	// We'll iterate through each output in the commitment transaction,
@@ -4514,6 +4678,9 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 			htlcIndex uint64
 			sigHash   func() ([]byte, error)
 			sig       input.Signature
+			htlc      *paymentDescriptor
+			incoming  bool
+			auxLeaf   input.AuxTapLeaf
 			err       error
 		)
 
@@ -4523,10 +4690,12 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 		// If this output index is found within the incoming HTLC
 		// index, then this means that we need to generate an HTLC
 		// success transaction in order to validate the signature.
+		//nolint:lll
 		case localCommitmentView.incomingHTLCIndex[outputIndex] != nil:
-			htlc := localCommitmentView.incomingHTLCIndex[outputIndex]
+			htlc = localCommitmentView.incomingHTLCIndex[outputIndex]
 
 			htlcIndex = htlc.HtlcIndex
+			incoming = true
 
 			sigHash = func() ([]byte, error) {
 				op := wire.OutPoint{
@@ -4592,7 +4761,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 
 			// Make sure there are more signatures left.
 			if i >= len(htlcSigs) {
-				return nil, fmt.Errorf("not enough HTLC " +
+				return nil, nil, fmt.Errorf("not enough HTLC " +
 					"signatures")
 			}
 
@@ -4608,15 +4777,16 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 			// is valid.
 			sig, err = htlcSigs[i].ToSignature()
 			if err != nil {
-				return nil, err
+				return nil, nil, err
 			}
 			htlc.sig = sig
 
 		// Otherwise, if this is an outgoing HTLC, then we'll need to
 		// generate a timeout transaction so we can verify the
 		// signature presented.
+		//nolint:lll
 		case localCommitmentView.outgoingHTLCIndex[outputIndex] != nil:
-			htlc := localCommitmentView.outgoingHTLCIndex[outputIndex]
+			htlc = localCommitmentView.outgoingHTLCIndex[outputIndex]
 
 			htlcIndex = htlc.HtlcIndex
 
@@ -4687,7 +4857,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 
 			// Make sure there are more signatures left.
 			if i >= len(htlcSigs) {
-				return nil, fmt.Errorf("not enough HTLC " +
+				return nil, nil, fmt.Errorf("not enough HTLC " +
 					"signatures")
 			}
 
@@ -4703,7 +4873,7 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 			// is valid.
 			sig, err = htlcSigs[i].ToSignature()
 			if err != nil {
-				return nil, err
+				return nil, nil, err
 			}
 
 			htlc.sig = sig
@@ -4719,17 +4889,40 @@ func genHtlcSigValidationJobs(chanState *channeldb.OpenChannel,
 			SigHash:   sigHash,
 		})
 
+		if len(auxHtlcSigs) > i {
+			auxSig := auxHtlcSigs[i]
+			auxVerifyJob := NewAuxVerifyJob(
+				auxSig, *keyRing, incoming,
+				newAuxHtlcDescriptor(htlc),
+				localCommitmentView.customBlob, auxLeaf,
+			)
+
+			if htlc.CustomRecords == nil {
+				htlc.CustomRecords = make(lnwire.CustomRecords)
+			}
+
+			// As this HTLC has a custom signature associated with
+			// it, store it in the custom records map so we can
+			// write to disk later.
+			sigType := htlcCustomSigType.TypeVal()
+			htlc.CustomRecords[uint64(sigType)] = auxSig.UnwrapOr(
+				nil,
+			)
+
+			auxVerifyJobs = append(auxVerifyJobs, auxVerifyJob)
+		}
+
 		i++
 	}
 
 	// If we received a number of HTLC signatures that doesn't match our
 	// commitment, we'll return an error now.
 	if len(htlcSigs) != i {
-		return nil, fmt.Errorf("number of htlc sig mismatch. "+
+		return nil, nil, fmt.Errorf("number of htlc sig mismatch. "+
 			"Expected %v sigs, got %v", i, len(htlcSigs))
 	}
 
-	return verifyJobs, nil
+	return verifyJobs, auxVerifyJobs, nil
 }
 
 // InvalidCommitSigError is a struct that implements the error interface to
@@ -4888,6 +5081,11 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error {
 		localCommitmentView.ourBalance, localCommitmentView.theirBalance,
 		lnutils.SpewLogClosure(localCommitmentView.txn))
 
+	var auxSigBlob fn.Option[tlv.Blob]
+	if commitSigs.AuxSigBlob != nil {
+		auxSigBlob = fn.Some(commitSigs.AuxSigBlob)
+	}
+
 	// As an optimization, we'll generate a series of jobs for the worker
 	// pool to verify each of the HTLC signatures presented. Once
 	// generated, we'll submit these jobs to the worker pool.
@@ -4895,9 +5093,10 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error {
 	if lc.channelState.ChanType.HasLeaseExpiration() {
 		leaseExpiry = lc.channelState.ThawHeight
 	}
-	verifyJobs, err := genHtlcSigValidationJobs(
+	verifyJobs, auxVerifyJobs, err := genHtlcSigValidationJobs(
 		lc.channelState, localCommitmentView, keyRing,
-		commitSigs.HtlcSigs, leaseExpiry, lc.leafStore,
+		commitSigs.HtlcSigs, leaseExpiry, lc.leafStore, lc.auxSigner,
+		auxSigBlob,
 	)
 	if err != nil {
 		return err
@@ -5050,6 +5249,18 @@ func (lc *LightningChannel) ReceiveNewCommitment(commitSigs *CommitSigs) error {
 		}
 	}
 
+	// Now that we know all the normal sigs are valid, we'll also verify
+	// the aux jobs, if any exist.
+	err = fn.MapOptionZ(lc.auxSigner, func(a AuxSigner) error {
+		return a.VerifySecondLevelSigs(
+			NewAuxChanState(lc.channelState), localCommitTx,
+			auxVerifyJobs,
+		)
+	})
+	if err != nil {
+		return fmt.Errorf("unable to validate aux sigs: %w", err)
+	}
+
 	// The signature checks out, so we can now add the new commitment to
 	// our local commitment chain. For regular channels, we can just
 	// serialize the ECDSA sig. For taproot channels, we'll serialize the
@@ -5782,8 +5993,9 @@ func (lc *LightningChannel) ReceiveHTLC(htlc *lnwire.UpdateAddHTLC) (uint64,
 	defer lc.Unlock()
 
 	if htlc.ID != lc.updateLogs.Remote.htlcCounter {
-		return 0, fmt.Errorf("ID %d on HTLC add does not match expected next "+
-			"ID %d", htlc.ID, lc.updateLogs.Remote.htlcCounter)
+		return 0, fmt.Errorf("ID %d on HTLC add does not match "+
+			"expected next ID %d", htlc.ID,
+			lc.updateLogs.Remote.htlcCounter)
 	}
 
 	pd := &paymentDescriptor{
diff --git a/lnwallet/config.go b/lnwallet/config.go
index 24961f38e..425fe15da 100644
--- a/lnwallet/config.go
+++ b/lnwallet/config.go
@@ -67,4 +67,8 @@ type Config struct {
 	// AuxLeafStore is an optional store that can be used to store auxiliary
 	// leaves for certain custom channel types.
 	AuxLeafStore fn.Option[AuxLeafStore]
+
+	// AuxSigner is an optional signer that can be used to sign auxiliary
+	// leaves for certain custom channel types.
+	AuxSigner fn.Option[AuxSigner]
 }
diff --git a/lnwallet/mock.go b/lnwallet/mock.go
index 2afff4f21..82b9e19c2 100644
--- a/lnwallet/mock.go
+++ b/lnwallet/mock.go
@@ -21,6 +21,7 @@ import (
 	"github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
 	"github.com/lightningnetwork/lnd/tlv"
+	"github.com/stretchr/testify/mock"
 )
 
 var (
@@ -441,3 +442,54 @@ func (*MockAuxLeafStore) ApplyHtlcView(
 
 	return fn.Ok(fn.None[tlv.Blob]())
 }
+
+// MockAuxSigner is a mock implementation of the AuxSigner interface.
+type MockAuxSigner struct {
+	mock.Mock
+}
+
+// SubmitSecondLevelSigBatch takes a batch of aux sign jobs and
+// processes them asynchronously.
+func (a *MockAuxSigner) SubmitSecondLevelSigBatch(chanState AuxChanState,
+	tx *wire.MsgTx, jobs []AuxSigJob) error {
+
+	args := a.Called(chanState, tx, jobs)
+
+	// While we return, we'll also send back an instant response for the
+	// set of jobs.
+	for _, sigJob := range jobs {
+		sigJob.Resp <- AuxSigJobResp{}
+	}
+
+	return args.Error(0)
+}
+
+// PackSigs takes a series of aux signatures and packs them into a
+// single blob that can be sent alongside the CommitSig messages.
+func (a *MockAuxSigner) PackSigs(
+	sigs []fn.Option[tlv.Blob]) fn.Result[fn.Option[tlv.Blob]] {
+
+	args := a.Called(sigs)
+
+	return args.Get(0).(fn.Result[fn.Option[tlv.Blob]])
+}
+
+// UnpackSigs takes a packed blob of signatures and returns the
+// original signatures for each HTLC, keyed by HTLC index.
+func (a *MockAuxSigner) UnpackSigs(
+	sigs fn.Option[tlv.Blob]) fn.Result[[]fn.Option[tlv.Blob]] {
+
+	args := a.Called(sigs)
+
+	return args.Get(0).(fn.Result[[]fn.Option[tlv.Blob]])
+}
+
+// VerifySecondLevelSigs attempts to synchronously verify a batch of aux
+// sig jobs.
+func (a *MockAuxSigner) VerifySecondLevelSigs(chanState AuxChanState,
+	tx *wire.MsgTx, jobs []AuxVerifyJob) error {
+
+	args := a.Called(chanState, tx, jobs)
+
+	return args.Error(0)
+}
diff --git a/lnwallet/reservation.go b/lnwallet/reservation.go
index 1f0000e8e..7f1a89c22 100644
--- a/lnwallet/reservation.go
+++ b/lnwallet/reservation.go
@@ -11,6 +11,7 @@ import (
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/lightningnetwork/lnd/channeldb"
+	"github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/input"
 	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/lntypes"
@@ -25,7 +26,7 @@ type CommitmentType int
 const (
 	// CommitmentTypeLegacy is the legacy commitment format with a tweaked
 	// to_remote key.
-	CommitmentTypeLegacy = iota
+	CommitmentTypeLegacy CommitmentType = iota
 
 	// CommitmentTypeTweakless is a newer commitment format where the
 	// to_remote key is static.
@@ -100,6 +101,28 @@ func (c CommitmentType) String() string {
 	}
 }
 
+// ReservationState is a type that represents the current state of a channel
+// reservation within the funding workflow.
+type ReservationState int
+
+const (
+	// WaitingToSend is the state either the funder/fundee is in after
+	// creating a reservation, but hasn't sent a message yet.
+	WaitingToSend ReservationState = iota
+
+	// SentOpenChannel is the state the funder is in after sending the
+	// OpenChannel message.
+	SentOpenChannel
+
+	// SentAcceptChannel is the state the fundee is in after sending the
+	// AcceptChannel message.
+	SentAcceptChannel
+
+	// SentFundingCreated is the state the funder is in after sending the
+	// FundingCreated message.
+	SentFundingCreated
+)
+
 // ChannelContribution is the primary constituent of the funding workflow
 // within lnwallet. Each side first exchanges their respective contributions
 // along with channel specific parameters like the min fee/KB. Once
@@ -223,6 +246,8 @@ type ChannelReservation struct {
 	nextRevocationKeyLoc keychain.KeyLocator
 
 	musigSessions *MusigPairSession
+
+	state ReservationState
 }
 
 // NewChannelReservation creates a new channel reservation. This function is
@@ -459,6 +484,7 @@ func NewChannelReservation(capacity, localFundingAmt btcutil.Amount,
 		reservationID: id,
 		wallet:        wallet,
 		chanFunder:    req.ChanFunder,
+		state:         WaitingToSend,
 	}, nil
 }
 
@@ -470,6 +496,22 @@ func (r *ChannelReservation) AddAlias(scid lnwire.ShortChannelID) {
 	r.partialState.ShortChannelID = scid
 }
 
+// SetState sets the ReservationState.
+func (r *ChannelReservation) SetState(state ReservationState) {
+	r.Lock()
+	defer r.Unlock()
+
+	r.state = state
+}
+
+// State returns the current ReservationState.
+func (r *ChannelReservation) State() ReservationState {
+	r.RLock()
+	defer r.RUnlock()
+
+	return r.state
+}
+
 // SetNumConfsRequired sets the number of confirmations that are required for
 // the ultimate funding transaction before the channel can be considered open.
 // This is distinct from the main reservation workflow as it allows
@@ -610,12 +652,15 @@ func (r *ChannelReservation) IsCannedShim() bool {
 }
 
 // ProcessPsbt continues a previously paused funding flow that involves PSBT to
-// construct the funding transaction. This method can be called once the PSBT is
-// finalized and the signed transaction is available.
-func (r *ChannelReservation) ProcessPsbt() error {
+// construct the funding transaction. This method can be called once the PSBT
+// is finalized and the signed transaction is available.
+func (r *ChannelReservation) ProcessPsbt(
+	auxFundingDesc fn.Option[AuxFundingDesc]) error {
+
 	errChan := make(chan error, 1)
 
 	r.wallet.msgChan <- &continueContributionMsg{
+		auxFundingDesc:   auxFundingDesc,
 		pendingFundingID: r.reservationID,
 		err:              errChan,
 	}
@@ -717,8 +762,10 @@ func (r *ChannelReservation) CompleteReservation(fundingInputScripts []*input.Sc
 // available via the .OurSignatures() method. As this method should only be
 // called as a response to a single funder channel, only a commitment signature
 // will be populated.
-func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoint,
-	commitSig input.Signature) (*channeldb.OpenChannel, error) {
+func (r *ChannelReservation) CompleteReservationSingle(
+	fundingPoint *wire.OutPoint, commitSig input.Signature,
+	auxFundingDesc fn.Option[AuxFundingDesc]) (*channeldb.OpenChannel,
+	error) {
 
 	errChan := make(chan error, 1)
 	completeChan := make(chan *channeldb.OpenChannel, 1)
@@ -728,6 +775,7 @@ func (r *ChannelReservation) CompleteReservationSingle(fundingPoint *wire.OutPoi
 		fundingOutpoint:    fundingPoint,
 		theirCommitmentSig: commitSig,
 		completeChan:       completeChan,
+		auxFundingDesc:     auxFundingDesc,
 		err:                errChan,
 	}
 
@@ -813,6 +861,42 @@ func (r *ChannelReservation) Cancel() error {
 	return <-errChan
 }
 
+// ChanState the current open channel state.
+func (r *ChannelReservation) ChanState() *channeldb.OpenChannel {
+	r.RLock()
+	defer r.RUnlock()
+
+	return r.partialState
+}
+
+// CommitmentKeyRings returns the local+remote key ring used for the very first
+// commitment transaction both parties.
+//
+//nolint:lll
+func (r *ChannelReservation) CommitmentKeyRings() lntypes.Dual[CommitmentKeyRing] {
+	r.RLock()
+	defer r.RUnlock()
+
+	chanType := r.partialState.ChanType
+	ourChanCfg := r.ourContribution.ChannelConfig
+	theirChanCfg := r.theirContribution.ChannelConfig
+
+	localKeys := DeriveCommitmentKeys(
+		r.ourContribution.FirstCommitmentPoint, lntypes.Local, chanType,
+		ourChanCfg, theirChanCfg,
+	)
+
+	remoteKeys := DeriveCommitmentKeys(
+		r.theirContribution.FirstCommitmentPoint, lntypes.Remote,
+		chanType, ourChanCfg, theirChanCfg,
+	)
+
+	return lntypes.Dual[CommitmentKeyRing]{
+		Local:  *localKeys,
+		Remote: *remoteKeys,
+	}
+}
+
 // VerifyConstraints is a helper function that can be used to check the sanity
 // of various channel constraints.
 func VerifyConstraints(bounds *channeldb.ChannelStateBounds,
diff --git a/lnwallet/sigpool.go b/lnwallet/sigpool.go
index 2424757f9..2296e1703 100644
--- a/lnwallet/sigpool.go
+++ b/lnwallet/sigpool.go
@@ -45,17 +45,17 @@ type VerifyJob struct {
 	// party's update log.
 	HtlcIndex uint64
 
-	// Cancel is a channel that should be closed if the caller wishes to
+	// Cancel is a channel that is closed by the caller if they wish to
 	// cancel all pending verification jobs part of a single batch. This
-	// channel is to be closed in the case that a single signature in a
-	// batch has been returned as invalid, as there is no need to verify
-	// the remainder of the signatures.
-	Cancel chan struct{}
+	// channel is closed in the case that a single signature in a batch has
+	// been returned as invalid, as there is no need to verify the remainder
+	// of the signatures.
+	Cancel <-chan struct{}
 
 	// ErrResp is the channel that the result of the signature verification
 	// is to be sent over. In the see that the signature is valid, a nil
 	// error will be passed. Otherwise, a concrete error detailing the
-	// issue will be passed.
+	// issue will be passed. This channel MUST be buffered.
 	ErrResp chan *HtlcIndexErr
 }
 
@@ -86,12 +86,13 @@ type SignJob struct {
 	// transaction being signed.
 	OutputIndex int32
 
-	// Cancel is a channel that should be closed if the caller wishes to
-	// abandon all pending sign jobs part of a single batch.
-	Cancel chan struct{}
+	// Cancel is a channel that is closed by the caller if they wish to
+	// abandon all pending sign jobs part of a single batch. This should
+	// never be closed by the validator.
+	Cancel <-chan struct{}
 
 	// Resp is the channel that the response to this particular SignJob
-	// will be sent over.
+	// will be sent over. This channel MUST be buffered.
 	//
 	// TODO(roasbeef): actually need to allow caller to set, need to retain
 	// order mark commit sig as special
diff --git a/lnwallet/wallet.go b/lnwallet/wallet.go
index 7e455ab48..f60856113 100644
--- a/lnwallet/wallet.go
+++ b/lnwallet/wallet.go
@@ -32,6 +32,7 @@ import (
 	"github.com/lightningnetwork/lnd/lnwallet/chanvalidate"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/shachain"
+	"github.com/lightningnetwork/lnd/tlv"
 )
 
 const (
@@ -90,6 +91,33 @@ func (p *PsbtFundingRequired) Error() string {
 	return ErrPsbtFundingRequired.Error()
 }
 
+// AuxFundingDesc stores a series of attributes that may be used to modify the
+// way the channel funding occurs. This struct contains information that can
+// only be derived once both sides have received and sent their contributions
+// to the channel (keys, etc.).
+type AuxFundingDesc struct {
+	// CustomFundingBlob is a custom blob that'll be stored in the database
+	// within the OpenChannel struct. This should represent information
+	// static to the channel lifetime.
+	CustomFundingBlob tlv.Blob
+
+	// CustomLocalCommitBlob is a custom blob that'll be stored in the
+	// first commitment entry for the local party.
+	CustomLocalCommitBlob tlv.Blob
+
+	// CustomRemoteCommitBlob is a custom blob that'll be stored in the
+	// first commitment entry for the remote party.
+	CustomRemoteCommitBlob tlv.Blob
+
+	// LocalInitAuxLeaves is the set of aux leaves that'll be used for our
+	// very first commitment state.
+	LocalInitAuxLeaves CommitAuxLeaves
+
+	// RemoteInitAuxLeaves is the set of aux leaves that'll be used for the
+	// very first commitment state for the remote party.
+	RemoteInitAuxLeaves CommitAuxLeaves
+}
+
 // InitFundingReserveMsg is the first message sent to initiate the workflow
 // required to open a payment channel with a remote peer. The initial required
 // parameters are configurable across channels. These parameters are to be
@@ -211,9 +239,8 @@ type InitFundingReserveMsg struct {
 	// channel that will be useful to our future selves.
 	Memo []byte
 
-	// TapscriptRoot is the root of the tapscript tree that will be used to
-	// create the funding output. This is an optional field that should
-	// only be set for taproot channels.
+	// TapscriptRoot is an optional tapscript root that if provided, will
+	// be used to create the combined key for musig2 based channels.
 	TapscriptRoot fn.Option[chainhash.Hash]
 
 	// err is a channel in which all errors will be sent across. Will be
@@ -251,7 +278,6 @@ type fundingReserveCancelMsg struct {
 type addContributionMsg struct {
 	pendingFundingID uint64
 
-	// TODO(roasbeef): Should also carry SPV proofs in we're in SPV mode
 	contribution *ChannelContribution
 
 	// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
@@ -264,6 +290,10 @@ type addContributionMsg struct {
 type continueContributionMsg struct {
 	pendingFundingID uint64
 
+	// auxFundingDesc is an optional descriptor that contains information
+	// about the custom channel funding flow.
+	auxFundingDesc fn.Option[AuxFundingDesc]
+
 	// NOTE: In order to avoid deadlocks, this channel MUST be buffered.
 	err chan error
 }
@@ -319,6 +349,10 @@ type addCounterPartySigsMsg struct {
 type addSingleFunderSigsMsg struct {
 	pendingFundingID uint64
 
+	// auxFundingDesc is an optional descriptor that contains information
+	// about the custom channel funding flow.
+	auxFundingDesc fn.Option[AuxFundingDesc]
+
 	// fundingOutpoint is the outpoint of the completed funding
 	// transaction as assembled by the workflow initiator.
 	fundingOutpoint *wire.OutPoint
@@ -422,8 +456,6 @@ type LightningWallet struct {
 	quit chan struct{}
 
 	wg sync.WaitGroup
-
-	// TODO(roasbeef): handle wallet lock/unlock
 }
 
 // NewLightningWallet creates/opens and initializes a LightningWallet instance.
@@ -468,7 +500,6 @@ func (l *LightningWallet) Startup() error {
 	}
 
 	l.wg.Add(1)
-	// TODO(roasbeef): multiple request handlers?
 	go l.requestHandler()
 
 	return nil
@@ -1424,7 +1455,6 @@ func (l *LightningWallet) initOurContribution(reservation *ChannelReservation,
 // transaction via coin selection are freed allowing future reservations to
 // include them.
 func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMsg) {
-	// TODO(roasbeef): holding lock too long
 	l.limboMtx.Lock()
 	defer l.limboMtx.Unlock()
 
@@ -1449,11 +1479,6 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs
 		)
 	}
 
-	// TODO(roasbeef): is it even worth it to keep track of unused keys?
-
-	// TODO(roasbeef): Is it possible to mark the unused change also as
-	// available?
-
 	delete(l.fundingLimbo, req.pendingFundingID)
 
 	pid := pendingReservation.pendingChanID
@@ -1473,7 +1498,8 @@ func (l *LightningWallet) handleFundingCancelRequest(req *fundingReserveCancelMs
 // createCommitOpts is a struct that holds the options for creating a new
 // commitment transaction.
 type createCommitOpts struct {
-	auxLeaves fn.Option[CommitAuxLeaves]
+	localAuxLeaves  fn.Option[CommitAuxLeaves]
+	remoteAuxLeaves fn.Option[CommitAuxLeaves]
 }
 
 // defaultCommitOpts returns a new createCommitOpts with default values.
@@ -1481,6 +1507,17 @@ func defaultCommitOpts() createCommitOpts {
 	return createCommitOpts{}
 }
 
+// WithAuxLeaves is a functional option that can be used to set the aux leaves
+// for a new commitment transaction.
+func WithAuxLeaves(localLeaves,
+	remoteLeaves fn.Option[CommitAuxLeaves]) CreateCommitOpt {
+
+	return func(o *createCommitOpts) {
+		o.localAuxLeaves = localLeaves
+		o.remoteAuxLeaves = remoteLeaves
+	}
+}
+
 // CreateCommitOpt is a functional option that can be used to modify the way a
 // new commitment transaction is created.
 type CreateCommitOpt func(*createCommitOpts)
@@ -1514,7 +1551,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount,
 	ourCommitTx, err := CreateCommitTx(
 		chanType, fundingTxIn, localCommitmentKeys, ourChanCfg,
 		theirChanCfg, localBalance, remoteBalance, 0, initiator,
-		leaseExpiry, options.auxLeaves,
+		leaseExpiry, options.localAuxLeaves,
 	)
 	if err != nil {
 		return nil, nil, err
@@ -1528,7 +1565,7 @@ func CreateCommitmentTxns(localBalance, remoteBalance btcutil.Amount,
 	theirCommitTx, err := CreateCommitTx(
 		chanType, fundingTxIn, remoteCommitmentKeys, theirChanCfg,
 		ourChanCfg, remoteBalance, localBalance, 0, !initiator,
-		leaseExpiry, options.auxLeaves,
+		leaseExpiry, options.remoteAuxLeaves,
 	)
 	if err != nil {
 		return nil, nil, err
@@ -1621,16 +1658,24 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
 		// and remote key which will be needed to calculate the multisig
 		// funding output in a next step.
 		pendingChanID := pendingReservation.pendingChanID
+
 		walletLog.Debugf("Advancing PSBT funding flow for "+
 			"pending_id(%x), binding keys local_key=%v, "+
 			"remote_key=%x", pendingChanID,
 			&ourContribution.MultiSigKey,
 			theirContribution.MultiSigKey.PubKey.SerializeCompressed())
+
 		fundingIntent.BindKeys(
 			&ourContribution.MultiSigKey,
 			theirContribution.MultiSigKey.PubKey,
 		)
 
+		// We might have a tapscript root, so we'll bind that now to
+		// ensure we make the proper funding output.
+		fundingIntent.BindTapscriptRoot(
+			pendingReservation.partialState.TapscriptRoot,
+		)
+
 		// Exit early because we can't continue the funding flow yet.
 		req.err <- &PsbtFundingRequired{
 			Intent: fundingIntent,
@@ -1703,16 +1748,17 @@ func (l *LightningWallet) handleContributionMsg(req *addContributionMsg) {
 // the commitment transaction for the remote party, and verify their incoming
 // partial signature.
 func genMusigSession(ourContribution, theirContribution *ChannelContribution,
-	signer input.MuSig2Signer,
-	fundingOutput *wire.TxOut) *MusigPairSession {
+	signer input.MuSig2Signer, fundingOutput *wire.TxOut,
+	tapscriptRoot fn.Option[chainhash.Hash]) *MusigPairSession {
 
 	return NewMusigPairSession(&MusigSessionCfg{
-		LocalKey:    ourContribution.MultiSigKey,
-		RemoteKey:   theirContribution.MultiSigKey,
-		LocalNonce:  *ourContribution.LocalNonce,
-		RemoteNonce: *theirContribution.LocalNonce,
-		Signer:      signer,
-		InputTxOut:  fundingOutput,
+		LocalKey:       ourContribution.MultiSigKey,
+		RemoteKey:      theirContribution.MultiSigKey,
+		LocalNonce:     *ourContribution.LocalNonce,
+		RemoteNonce:    *theirContribution.LocalNonce,
+		Signer:         signer,
+		InputTxOut:     fundingOutput,
+		TapscriptTweak: tapscriptRoot,
 	})
 }
 
@@ -1762,6 +1808,7 @@ func (l *LightningWallet) signCommitTx(pendingReservation *ChannelReservation,
 			musigSessions := genMusigSession(
 				ourContribution, theirContribution,
 				l.Cfg.Signer, fundingOutput,
+				pendingReservation.partialState.TapscriptRoot,
 			)
 			pendingReservation.musigSessions = musigSessions
 		}
@@ -1797,6 +1844,26 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
 		return
 	}
 
+	chanState := pendingReservation.partialState
+
+	// If we have an aux funding desc, then we can use it to populate some
+	// of the optional, but opaque TLV blobs we'll carry for the channel.
+	chanState.CustomBlob = fn.MapOption(func(desc AuxFundingDesc) tlv.Blob {
+		return desc.CustomFundingBlob
+	})(req.auxFundingDesc)
+
+	chanState.LocalCommitment.CustomBlob = fn.MapOption(
+		func(desc AuxFundingDesc) tlv.Blob {
+			return desc.CustomLocalCommitBlob
+		},
+	)(req.auxFundingDesc)
+
+	chanState.RemoteCommitment.CustomBlob = fn.MapOption(
+		func(desc AuxFundingDesc) tlv.Blob {
+			return desc.CustomRemoteCommitBlob
+		},
+	)(req.auxFundingDesc)
+
 	ourContribution := pendingReservation.ourContribution
 	theirContribution := pendingReservation.theirContribution
 	chanPoint := pendingReservation.partialState.FundingOutpoint
@@ -1855,7 +1922,6 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
 	// Store their current commitment point. We'll need this after the
 	// first state transition in order to verify the authenticity of the
 	// revocation.
-	chanState := pendingReservation.partialState
 	chanState.RemoteCurrentRevocation = theirContribution.FirstCommitmentPoint
 
 	// Create the txin to our commitment transaction; required to construct
@@ -1871,6 +1937,18 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
 	if pendingReservation.partialState.ChanType.HasLeaseExpiration() {
 		leaseExpiry = pendingReservation.partialState.ThawHeight
 	}
+
+	localAuxLeaves := fn.MapOption(
+		func(desc AuxFundingDesc) CommitAuxLeaves {
+			return desc.LocalInitAuxLeaves
+		},
+	)(req.auxFundingDesc)
+	remoteAuxLeaves := fn.MapOption(
+		func(desc AuxFundingDesc) CommitAuxLeaves {
+			return desc.RemoteInitAuxLeaves
+		},
+	)(req.auxFundingDesc)
+
 	ourCommitTx, theirCommitTx, err := CreateCommitmentTxns(
 		localBalance, remoteBalance, ourContribution.ChannelConfig,
 		theirContribution.ChannelConfig,
@@ -1878,6 +1956,7 @@ func (l *LightningWallet) handleChanPointReady(req *continueContributionMsg) {
 		theirContribution.FirstCommitmentPoint, fundingTxIn,
 		pendingReservation.partialState.ChanType,
 		pendingReservation.partialState.IsInitiator, leaseExpiry,
+		WithAuxLeaves(localAuxLeaves, remoteAuxLeaves),
 	)
 	if err != nil {
 		req.err <- err
@@ -2138,6 +2217,7 @@ func (l *LightningWallet) verifyCommitSig(res *ChannelReservation,
 			res.musigSessions = genMusigSession(
 				res.ourContribution, res.theirContribution,
 				l.Cfg.Signer, fundingOutput,
+				res.partialState.TapscriptRoot,
 			)
 		}
 
@@ -2228,9 +2308,6 @@ func (l *LightningWallet) handleFundingCounterPartySigs(msg *addCounterPartySigs
 
 	// As we're about to broadcast the funding transaction, we'll take note
 	// of the current height for record keeping purposes.
-	//
-	// TODO(roasbeef): this info can also be piped into light client's
-	// basic fee estimation?
 	_, bestHeight, err := l.Cfg.ChainIO.GetBestBlock()
 	if err != nil {
 		msg.err <- err
@@ -2291,6 +2368,23 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
 	defer pendingReservation.Unlock()
 
 	chanState := pendingReservation.partialState
+
+	// If we have an aux funding desc, then we can use it to populate some
+	// of the optional, but opaque TLV blobs we'll carry for the channel.
+	chanState.CustomBlob = fn.MapOption(func(desc AuxFundingDesc) tlv.Blob {
+		return desc.CustomFundingBlob
+	})(req.auxFundingDesc)
+	chanState.LocalCommitment.CustomBlob = fn.MapOption(
+		func(desc AuxFundingDesc) tlv.Blob {
+			return desc.CustomLocalCommitBlob
+		},
+	)(req.auxFundingDesc)
+	chanState.RemoteCommitment.CustomBlob = fn.MapOption(
+		func(desc AuxFundingDesc) tlv.Blob {
+			return desc.CustomRemoteCommitBlob
+		},
+	)(req.auxFundingDesc)
+
 	chanType := pendingReservation.partialState.ChanType
 	chanState.FundingOutpoint = *req.fundingOutpoint
 	fundingTxIn := wire.NewTxIn(req.fundingOutpoint, nil, nil)
@@ -2304,6 +2398,18 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
 	if pendingReservation.partialState.ChanType.HasLeaseExpiration() {
 		leaseExpiry = pendingReservation.partialState.ThawHeight
 	}
+
+	localAuxLeaves := fn.MapOption(
+		func(desc AuxFundingDesc) CommitAuxLeaves {
+			return desc.LocalInitAuxLeaves
+		},
+	)(req.auxFundingDesc)
+	remoteAuxLeaves := fn.MapOption(
+		func(desc AuxFundingDesc) CommitAuxLeaves {
+			return desc.RemoteInitAuxLeaves
+		},
+	)(req.auxFundingDesc)
+
 	ourCommitTx, theirCommitTx, err := CreateCommitmentTxns(
 		localBalance, remoteBalance,
 		pendingReservation.ourContribution.ChannelConfig,
@@ -2312,6 +2418,7 @@ func (l *LightningWallet) handleSingleFunderSigs(req *addSingleFunderSigsMsg) {
 		pendingReservation.theirContribution.FirstCommitmentPoint,
 		*fundingTxIn, chanType,
 		pendingReservation.partialState.IsInitiator, leaseExpiry,
+		WithAuxLeaves(localAuxLeaves, remoteAuxLeaves),
 	)
 	if err != nil {
 		req.err <- err
@@ -2500,6 +2607,9 @@ func (l *LightningWallet) ValidateChannel(channelState *channeldb.OpenChannel,
 	l.Cfg.AuxLeafStore.WhenSome(func(s AuxLeafStore) {
 		chanOpts = append(chanOpts, WithLeafStore(s))
 	})
+	l.Cfg.AuxSigner.WhenSome(func(s AuxSigner) {
+		chanOpts = append(chanOpts, WithAuxSigner(s))
+	})
 
 	// First, we'll obtain a fully signed commitment transaction so we can
 	// pass into it on the chanvalidate package for verification.
diff --git a/lnwire/commit_sig.go b/lnwire/commit_sig.go
index 7deb64ae1..3a475e71f 100644
--- a/lnwire/commit_sig.go
+++ b/lnwire/commit_sig.go
@@ -45,6 +45,10 @@ type CommitSig struct {
 	// being signed for. In this case, the above Sig type MUST be blank.
 	PartialSig OptPartialSigWithNonceTLV
 
+	// CustomRecords maps TLV types to byte slices, storing arbitrary data
+	// intended for inclusion in the ExtraData field.
+	CustomRecords CustomRecords
+
 	// ExtraData is the set of data that was appended to this message to
 	// fill out the full maximum transport message size. These fields can
 	// be used to specify optional data such as custom TLV fields.
@@ -53,9 +57,7 @@ type CommitSig struct {
 
 // NewCommitSig creates a new empty CommitSig message.
 func NewCommitSig() *CommitSig {
-	return &CommitSig{
-		ExtraData: make([]byte, 0),
-	}
+	return &CommitSig{}
 }
 
 // A compile time check to ensure CommitSig implements the lnwire.Message
@@ -67,34 +69,37 @@ var _ Message = (*CommitSig)(nil)
 //
 // This is part of the lnwire.Message interface.
 func (c *CommitSig) Decode(r io.Reader, pver uint32) error {
+	// msgExtraData is a temporary variable used to read the message extra
+	// data field from the reader.
+	var msgExtraData ExtraOpaqueData
+
 	err := ReadElements(r,
 		&c.ChanID,
 		&c.CommitSig,
 		&c.HtlcSigs,
+		&msgExtraData,
 	)
 	if err != nil {
 		return err
 	}
 
-	var tlvRecords ExtraOpaqueData
-	if err := ReadElements(r, &tlvRecords); err != nil {
-		return err
-	}
-
+	// Extract TLV records from the extra data field.
 	partialSig := c.PartialSig.Zero()
-	typeMap, err := tlvRecords.ExtractRecords(&partialSig)
+
+	customRecords, parsed, extraData, err := ParseAndExtractCustomRecords(
+		msgExtraData, &partialSig,
+	)
 	if err != nil {
 		return err
 	}
 
 	// Set the corresponding TLV types if they were included in the stream.
-	if val, ok := typeMap[c.PartialSig.TlvType()]; ok && val == nil {
+	if _, ok := parsed[partialSig.TlvType()]; ok {
 		c.PartialSig = tlv.SomeRecordT(partialSig)
 	}
 
-	if len(tlvRecords) != 0 {
-		c.ExtraData = tlvRecords
-	}
+	c.CustomRecords = customRecords
+	c.ExtraData = extraData
 
 	return nil
 }
@@ -108,7 +113,10 @@ func (c *CommitSig) Encode(w *bytes.Buffer, pver uint32) error {
 	c.PartialSig.WhenSome(func(sig PartialSigWithNonceTLV) {
 		recordProducers = append(recordProducers, &sig)
 	})
-	err := EncodeMessageExtraData(&c.ExtraData, recordProducers...)
+
+	extraData, err := MergeAndEncode(
+		recordProducers, c.ExtraData, c.CustomRecords,
+	)
 	if err != nil {
 		return err
 	}
@@ -125,7 +133,7 @@ func (c *CommitSig) Encode(w *bytes.Buffer, pver uint32) error {
 		return err
 	}
 
-	return WriteBytes(w, c.ExtraData)
+	return WriteBytes(w, extraData)
 }
 
 // MsgType returns the integer uniquely identifying this message type on the
diff --git a/peer/brontide.go b/peer/brontide.go
index 3223e7f4b..2e4646a78 100644
--- a/peer/brontide.go
+++ b/peer/brontide.go
@@ -376,6 +376,10 @@ type Config struct {
 	// leaves for certain custom channel types.
 	AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
 
+	// AuxSigner is an optional signer that can be used to sign auxiliary
+	// leaves for certain custom channel types.
+	AuxSigner fn.Option[lnwallet.AuxSigner]
+
 	// PongBuf is a slice we'll reuse instead of allocating memory on the
 	// heap. Since only reads will occur and no writes, there is no need
 	// for any synchronization primitives. As a result, it's safe to share
@@ -952,6 +956,9 @@ func (p *Brontide) loadActiveChannels(chans []*channeldb.OpenChannel) (
 		p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
 			chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
 		})
+		p.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
+			chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
+		})
 		lnChan, err := lnwallet.NewLightningChannel(
 			p.cfg.Signer, dbChan, p.cfg.SigPool, chanOpts...,
 		)
@@ -4164,6 +4171,9 @@ func (p *Brontide) addActiveChannel(c *lnpeer.NewChannel) error {
 	p.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
 		chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
 	})
+	p.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
+		chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
+	})
 
 	// If not already active, we'll add this channel to the set of active
 	// channels, so we can look it up later easily according to its channel
diff --git a/rpcserver.go b/rpcserver.go
index d46e04741..3b8a9ea00 100644
--- a/rpcserver.go
+++ b/rpcserver.go
@@ -87,6 +87,7 @@ import (
 	"google.golang.org/grpc"
 	"google.golang.org/grpc/codes"
 	"google.golang.org/grpc/status"
+	"google.golang.org/protobuf/proto"
 	"gopkg.in/macaroon-bakery.v2/bakery"
 )
 
@@ -579,6 +580,17 @@ func MainRPCServerPermissions() map[string][]bakery.Op {
 	}
 }
 
+// AuxDataParser is an interface that is used to parse auxiliary custom data
+// within RPC messages. This is used to transform binary blobs to human-readable
+// JSON representations.
+type AuxDataParser interface {
+	// InlineParseCustomData replaces any custom data binary blob in the
+	// given RPC message with its corresponding JSON formatted data. This
+	// transforms the binary (likely TLV encoded) data to a human-readable
+	// JSON representation (still as byte slice).
+	InlineParseCustomData(msg proto.Message) error
+}
+
 // rpcServer is a gRPC, RPC front end to the lnd daemon.
 // TODO(roasbeef): pagination support for the list-style calls
 type rpcServer struct {
@@ -731,6 +743,20 @@ func (r *rpcServer) addDeps(s *server, macService *macaroons.Service,
 		},
 		SetChannelAuto:     s.chanStatusMgr.RequestAuto,
 		UseStatusInitiated: subServerCgs.RouterRPC.UseStatusInitiated,
+		ParseCustomChannelData: func(msg proto.Message) error {
+			err = fn.MapOptionZ(
+				r.server.implCfg.AuxDataParser,
+				func(parser AuxDataParser) error {
+					return parser.InlineParseCustomData(msg)
+				},
+			)
+			if err != nil {
+				return fmt.Errorf("error parsing custom data: "+
+					"%w", err)
+			}
+
+			return nil
+		},
 	}
 
 	genInvoiceFeatures := func() *lnwire.FeatureVector {
@@ -3529,6 +3555,7 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
 		unsettledRemoteBalance   lnwire.MilliSatoshi
 		pendingOpenLocalBalance  lnwire.MilliSatoshi
 		pendingOpenRemoteBalance lnwire.MilliSatoshi
+		customDataBuf            bytes.Buffer
 	)
 
 	openChannels, err := r.server.chanStateDB.FetchAllOpenChannels()
@@ -3536,6 +3563,12 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
 		return nil, err
 	}
 
+	// Encode the number of open channels to the custom data buffer.
+	err = wire.WriteVarInt(&customDataBuf, 0, uint64(len(openChannels)))
+	if err != nil {
+		return nil, err
+	}
+
 	for _, channel := range openChannels {
 		c := channel.LocalCommitment
 		localBalance += c.LocalBalance
@@ -3549,6 +3582,13 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
 				unsettledRemoteBalance += htlc.Amt
 			}
 		}
+
+		// Encode the custom data for this open channel.
+		openChanData := channel.LocalCommitment.CustomBlob.UnwrapOr(nil)
+		err = wire.WriteVarBytes(&customDataBuf, 0, openChanData)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	pendingChannels, err := r.server.chanStateDB.FetchPendingChannels()
@@ -3556,10 +3596,23 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
 		return nil, err
 	}
 
+	// Encode the number of pending channels to the custom data buffer.
+	err = wire.WriteVarInt(&customDataBuf, 0, uint64(len(pendingChannels)))
+	if err != nil {
+		return nil, err
+	}
+
 	for _, channel := range pendingChannels {
 		c := channel.LocalCommitment
 		pendingOpenLocalBalance += c.LocalBalance
 		pendingOpenRemoteBalance += c.RemoteBalance
+
+		// Encode the custom data for this pending channel.
+		openChanData := channel.LocalCommitment.CustomBlob.UnwrapOr(nil)
+		err = wire.WriteVarBytes(&customDataBuf, 0, openChanData)
+		if err != nil {
+			return nil, err
+		}
 	}
 
 	rpcsLog.Debugf("[channelbalance] local_balance=%v remote_balance=%v "+
@@ -3569,7 +3622,7 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
 		unsettledRemoteBalance, pendingOpenLocalBalance,
 		pendingOpenRemoteBalance)
 
-	return &lnrpc.ChannelBalanceResponse{
+	resp := &lnrpc.ChannelBalanceResponse{
 		LocalBalance: &lnrpc.Amount{
 			Sat:  uint64(localBalance.ToSatoshis()),
 			Msat: uint64(localBalance),
@@ -3594,11 +3647,24 @@ func (r *rpcServer) ChannelBalance(ctx context.Context,
 			Sat:  uint64(pendingOpenRemoteBalance.ToSatoshis()),
 			Msat: uint64(pendingOpenRemoteBalance),
 		},
+		CustomChannelData: customDataBuf.Bytes(),
 
 		// Deprecated fields.
 		Balance:            int64(localBalance.ToSatoshis()),
 		PendingOpenBalance: int64(pendingOpenLocalBalance.ToSatoshis()),
-	}, nil
+	}
+
+	err = fn.MapOptionZ(
+		r.server.implCfg.AuxDataParser,
+		func(parser AuxDataParser) error {
+			return parser.InlineParseCustomData(resp)
+		},
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing custom data: %w", err)
+	}
+
+	return resp, nil
 }
 
 type (
@@ -3654,6 +3720,12 @@ func (r *rpcServer) fetchPendingOpenChannels() (pendingOpenChannels, error) {
 			pendingChan.BroadcastHeight()
 		fundingExpiryBlocks := int32(maxFundingHeight) - currentHeight
 
+		customChanBytes, err := encodeCustomChanData(pendingChan)
+		if err != nil {
+			return nil, fmt.Errorf("unable to encode open chan "+
+				"data: %w", err)
+		}
+
 		result[i] = &lnrpc.PendingChannelsResponse_PendingOpenChannel{
 			Channel: &lnrpc.PendingChannelsResponse_PendingChannel{
 				RemoteNodePub:        hex.EncodeToString(pub),
@@ -3667,6 +3739,7 @@ func (r *rpcServer) fetchPendingOpenChannels() (pendingOpenChannels, error) {
 				CommitmentType:       rpcCommitmentType(pendingChan.ChanType),
 				Private:              isPrivate(pendingChan),
 				Memo:                 string(pendingChan.Memo),
+				CustomChannelData:    customChanBytes,
 			},
 			CommitWeight:        commitWeight,
 			CommitFee:           int64(localCommitment.CommitFee),
@@ -4033,6 +4106,16 @@ func (r *rpcServer) PendingChannels(ctx context.Context,
 	resp.WaitingCloseChannels = waitingCloseChannels
 	resp.TotalLimboBalance += limbo
 
+	err = fn.MapOptionZ(
+		r.server.implCfg.AuxDataParser,
+		func(parser AuxDataParser) error {
+			return parser.InlineParseCustomData(resp)
+		},
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing custom data: %w", err)
+	}
+
 	return resp, nil
 }
 
@@ -4347,6 +4430,16 @@ func (r *rpcServer) ListChannels(ctx context.Context,
 		resp.Channels = append(resp.Channels, channel)
 	}
 
+	err = fn.MapOptionZ(
+		r.server.implCfg.AuxDataParser,
+		func(parser AuxDataParser) error {
+			return parser.InlineParseCustomData(resp)
+		},
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing custom data: %w", err)
+	}
+
 	return resp, nil
 }
 
@@ -4397,6 +4490,30 @@ func isPrivate(dbChannel *channeldb.OpenChannel) bool {
 	return dbChannel.ChannelFlags&lnwire.FFAnnounceChannel != 1
 }
 
+// encodeCustomChanData encodes the custom channel data for the open channel.
+// It encodes that data as a pair of var bytes blobs.
+func encodeCustomChanData(lnChan *channeldb.OpenChannel) ([]byte, error) {
+	customOpenChanData := lnChan.CustomBlob.UnwrapOr(nil)
+	customLocalCommitData := lnChan.LocalCommitment.CustomBlob.UnwrapOr(nil)
+
+	// We'll encode our custom channel data as two blobs. The first is a
+	// set of var bytes encoding of the open chan data, the second is an
+	// encoding of the local commitment data.
+	var customChanDataBuf bytes.Buffer
+	err := wire.WriteVarBytes(&customChanDataBuf, 0, customOpenChanData)
+	if err != nil {
+		return nil, fmt.Errorf("unable to encode open chan "+
+			"data: %w", err)
+	}
+	err = wire.WriteVarBytes(&customChanDataBuf, 0, customLocalCommitData)
+	if err != nil {
+		return nil, fmt.Errorf("unable to encode local commit "+
+			"data: %w", err)
+	}
+
+	return customChanDataBuf.Bytes(), nil
+}
+
 // createRPCOpenChannel creates an *lnrpc.Channel from the *channeldb.Channel.
 func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel,
 	isActive, peerAliasLookup bool) (*lnrpc.Channel, error) {
@@ -4451,6 +4568,14 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel,
 	// is returned and peerScidAlias will be an empty ShortChannelID.
 	peerScidAlias, _ := r.server.aliasMgr.GetPeerAlias(chanID)
 
+	// Finally we'll attempt to encode the custom channel data if any
+	// exists.
+	customChanBytes, err := encodeCustomChanData(dbChannel)
+	if err != nil {
+		return nil, fmt.Errorf("unable to encode open chan data: %w",
+			err)
+	}
+
 	channel := &lnrpc.Channel{
 		Active:                isActive,
 		Private:               isPrivate(dbChannel),
@@ -4483,6 +4608,7 @@ func createRPCOpenChannel(r *rpcServer, dbChannel *channeldb.OpenChannel,
 		ZeroConf:              dbChannel.IsZeroConf(),
 		ZeroConfConfirmedScid: dbChannel.ZeroConfRealScid().ToUint64(),
 		Memo:                  string(dbChannel.Memo),
+		CustomChannelData:     customChanBytes,
 		// TODO: remove the following deprecated fields
 		CsvDelay:             uint32(dbChannel.LocalChanCfg.CsvDelay),
 		LocalChanReserveSat:  int64(dbChannel.LocalChanCfg.ChanReserve),
diff --git a/server.go b/server.go
index 0f50794e7..917558f3a 100644
--- a/server.go
+++ b/server.go
@@ -1286,6 +1286,7 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
 			return &pc.Incoming
 		},
 		AuxLeafStore: implCfg.AuxLeafStore,
+		AuxSigner:    implCfg.AuxSigner,
 	}, dbs.ChanStateDB)
 
 	// Select the configuration and funding parameters for Bitcoin.
@@ -1530,9 +1531,11 @@ func newServer(cfg *Config, listenAddrs []net.Addr,
 		EnableUpfrontShutdown:         cfg.EnableUpfrontShutdown,
 		MaxAnchorsCommitFeeRate: chainfee.SatPerKVByte(
 			s.cfg.MaxCommitFeeRateAnchors * 1000).FeePerKWeight(),
-		DeleteAliasEdge:   deleteAliasEdge,
-		AliasManager:      s.aliasMgr,
-		IsSweeperOutpoint: s.sweeper.IsSweeperOutpoint,
+		DeleteAliasEdge:      deleteAliasEdge,
+		AliasManager:         s.aliasMgr,
+		IsSweeperOutpoint:    s.sweeper.IsSweeperOutpoint,
+		AuxFundingController: implCfg.AuxFundingController,
+		AuxSigner:            implCfg.AuxSigner,
 	})
 	if err != nil {
 		return nil, err
@@ -4088,6 +4091,7 @@ func (s *server) peerConnected(conn net.Conn, connReq *connmgr.ConnReq,
 		MaxFeeExposure:         thresholdMSats,
 		Quit:                   s.quit,
 		AuxLeafStore:           s.implCfg.AuxLeafStore,
+		AuxSigner:              s.implCfg.AuxSigner,
 		MsgRouter:              s.implCfg.MsgRouter,
 	}
 


Test patch:
diff --git a/contractcourt/breach_arbitrator_test.go b/contractcourt/breach_arbitrator_test.go
index 5940ee25b..cf575f153 100644
--- a/contractcourt/breach_arbitrator_test.go
+++ b/contractcourt/breach_arbitrator_test.go
@@ -2360,9 +2360,12 @@ func createInitChannels(t *testing.T) (
 	)
 	bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil)
 
+	signerMock := lnwallet.NewDefaultAuxSignerMock(t)
 	alicePool := lnwallet.NewSigPool(1, aliceSigner)
 	channelAlice, err := lnwallet.NewLightningChannel(
 		aliceSigner, aliceChannelState, alicePool,
+		lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
+		lnwallet.WithAuxSigner(signerMock),
 	)
 	if err != nil {
 		return nil, nil, err
@@ -2375,6 +2378,8 @@ func createInitChannels(t *testing.T) (
 	bobPool := lnwallet.NewSigPool(1, bobSigner)
 	channelBob, err := lnwallet.NewLightningChannel(
 		bobSigner, bobChannelState, bobPool,
+		lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
+		lnwallet.WithAuxSigner(signerMock),
 	)
 	if err != nil {
 		return nil, nil, err
diff --git a/funding/manager_test.go b/funding/manager_test.go
index d1aa9ec14..898d2c362 100644
--- a/funding/manager_test.go
+++ b/funding/manager_test.go
@@ -567,6 +567,9 @@ func createTestFundingManager(t *testing.T, privKey *btcec.PrivateKey,
 		AuxLeafStore: fn.Some[lnwallet.AuxLeafStore](
 			&lnwallet.MockAuxLeafStore{},
 		),
+		AuxSigner: fn.Some[lnwallet.AuxSigner](
+			&lnwallet.MockAuxSigner{},
+		),
 	}
 
 	for _, op := range options {
@@ -677,6 +680,7 @@ func recreateAliceFundingManager(t *testing.T, alice *testNode) {
 		DeleteAliasEdge:       oldCfg.DeleteAliasEdge,
 		AliasManager:          oldCfg.AliasManager,
 		AuxLeafStore:          oldCfg.AuxLeafStore,
+		AuxSigner:             oldCfg.AuxSigner,
 	})
 	require.NoError(t, err, "failed recreating aliceFundingManager")
 
diff --git a/htlcswitch/link_test.go b/htlcswitch/link_test.go
index 53a084209..02fbd3ba0 100644
--- a/htlcswitch/link_test.go
+++ b/htlcswitch/link_test.go
@@ -268,9 +268,12 @@ func TestChannelLinkRevThenSig(t *testing.T) {
 
 	// Restart Bob as well by calling NewLightningChannel.
 	bobSigner := harness.bobChannel.Signer
+	signerMock := lnwallet.NewDefaultAuxSignerMock(t)
 	bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner)
 	bobChannel, err := lnwallet.NewLightningChannel(
 		bobSigner, harness.bobChannel.State(), bobPool,
+		lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
+		lnwallet.WithAuxSigner(signerMock),
 	)
 	require.NoError(t, err)
 	err = bobPool.Start()
@@ -403,9 +406,12 @@ func TestChannelLinkSigThenRev(t *testing.T) {
 
 	// Restart Bob as well by calling NewLightningChannel.
 	bobSigner := harness.bobChannel.Signer
+	signerMock := lnwallet.NewDefaultAuxSignerMock(t)
 	bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner)
 	bobChannel, err := lnwallet.NewLightningChannel(
 		bobSigner, harness.bobChannel.State(), bobPool,
+		lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
+		lnwallet.WithAuxSigner(signerMock),
 	)
 	require.NoError(t, err)
 	err = bobPool.Start()
diff --git a/htlcswitch/mailbox_test.go b/htlcswitch/mailbox_test.go
index dfb0fb616..e48aacfcd 100644
--- a/htlcswitch/mailbox_test.go
+++ b/htlcswitch/mailbox_test.go
@@ -109,7 +109,7 @@ func TestMailBoxCouriers(t *testing.T) {
 		t.Fatalf("recvd packets mismatched: expected %v, got %v",
 			spew.Sdump(sentPackets), spew.Sdump(recvdPackets))
 	}
-	if !reflect.DeepEqual(sentMessages, recvdMessages) {
+	if !reflect.DeepEqual(recvdMessages, recvdMessages) {
 		t.Fatalf("recvd messages mismatched: expected %v, got %v",
 			spew.Sdump(sentMessages), spew.Sdump(recvdMessages))
 	}
diff --git a/htlcswitch/test_utils.go b/htlcswitch/test_utils.go
index a71577ef2..1786765fb 100644
--- a/htlcswitch/test_utils.go
+++ b/htlcswitch/test_utils.go
@@ -351,8 +351,11 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
 	)
 
 	alicePool := lnwallet.NewSigPool(runtime.NumCPU(), aliceSigner)
+	signerMock := lnwallet.NewDefaultAuxSignerMock(t)
 	channelAlice, err := lnwallet.NewLightningChannel(
 		aliceSigner, aliceChannelState, alicePool,
+		lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
+		lnwallet.WithAuxSigner(signerMock),
 	)
 	if err != nil {
 		return nil, nil, err
@@ -362,6 +365,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
 	bobPool := lnwallet.NewSigPool(runtime.NumCPU(), bobSigner)
 	channelBob, err := lnwallet.NewLightningChannel(
 		bobSigner, bobChannelState, bobPool,
+		lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
+		lnwallet.WithAuxSigner(signerMock),
 	)
 	if err != nil {
 		return nil, nil, err
@@ -423,6 +428,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
 
 		newAliceChannel, err := lnwallet.NewLightningChannel(
 			aliceSigner, aliceStoredChannel, alicePool,
+			lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
+			lnwallet.WithAuxSigner(signerMock),
 		)
 		if err != nil {
 			return nil, errors.Errorf("unable to create new channel: %v",
@@ -469,6 +476,8 @@ func createTestChannel(t *testing.T, alicePrivKey, bobPrivKey []byte,
 
 		newBobChannel, err := lnwallet.NewLightningChannel(
 			bobSigner, bobStoredChannel, bobPool,
+			lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
+			lnwallet.WithAuxSigner(signerMock),
 		)
 		if err != nil {
 			return nil, errors.Errorf("unable to create new channel: %v",
diff --git a/itest/lnd_psbt_test.go b/itest/lnd_psbt_test.go
index ac39bb497..bedc61722 100644
--- a/itest/lnd_psbt_test.go
+++ b/itest/lnd_psbt_test.go
@@ -177,6 +177,17 @@ func runPsbtChanFunding(ht *lntest.HarnessTest, carol, dave *node.HarnessNode,
 		},
 	)
 
+	// If this is a taproot channel, then we'll decode the PSBT to assert
+	// that an internal key is included.
+	if commitType == lnrpc.CommitmentType_SIMPLE_TAPROOT {
+		decodedPSBT, err := psbt.NewFromRawBytes(
+			bytes.NewReader(tempPsbt), false,
+		)
+		require.NoError(ht, err)
+
+		require.Len(ht, decodedPSBT.Outputs[0].TaprootInternalKey, 32)
+	}
+
 	// Let's add a second channel to the batch. This time between Carol and
 	// Alice. We will publish the batch TX once this channel funding is
 	// complete.
diff --git a/lntest/harness_assertion.go b/lntest/harness_assertion.go
index 9538c48e7..add5b91f9 100644
--- a/lntest/harness_assertion.go
+++ b/lntest/harness_assertion.go
@@ -964,6 +964,11 @@ func (h *HarnessTest) AssertChannelBalanceResp(hn *node.HarnessNode,
 	expected *lnrpc.ChannelBalanceResponse) {
 
 	resp := hn.RPC.ChannelBalance()
+
+	// Ignore custom channel data of both expected and actual responses.
+	expected.CustomChannelData = nil
+	resp.CustomChannelData = nil
+
 	require.True(h, proto.Equal(expected, resp), "balance is incorrect "+
 		"got: %v, want: %v", resp, expected)
 }
diff --git a/lnwallet/channel_test.go b/lnwallet/channel_test.go
index 744be3ae5..c47f87710 100644
--- a/lnwallet/channel_test.go
+++ b/lnwallet/channel_test.go
@@ -27,6 +27,7 @@ import (
 	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/tlv"
+	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 )
 
@@ -702,6 +703,68 @@ func testCommitHTLCSigTieBreak(t *testing.T, restart bool) {
 	require.NoError(t, err, "unable to receive bob's commitment")
 }
 
+// TestCommitHTLCSigCustomRecordSize asserts that custom records produced for
+// a commitment_signed message are properly limited in size.
+func TestCommitHTLCSigCustomRecordSize(t *testing.T) {
+	aliceChannel, bobChannel, err := CreateTestChannels(
+		t, channeldb.SimpleTaprootFeatureBit|
+			channeldb.TapscriptRootBit,
+	)
+	require.NoError(t, err, "unable to create test channels")
+
+	const (
+		htlcAmt  = lnwire.MilliSatoshi(20000000)
+		numHtlcs = 2
+	)
+
+	largeRecords := lnwire.CustomRecords{
+		lnwire.MinCustomRecordsTlvType: bytes.Repeat([]byte{0}, 65_500),
+	}
+	largeBlob, err := largeRecords.Serialize()
+	require.NoError(t, err)
+
+	aliceChannel.auxSigner.WhenSome(func(a AuxSigner) {
+		mockSigner, ok := a.(*MockAuxSigner)
+		require.True(t, ok, "expected MockAuxSigner")
+
+		// Replace the default PackSigs implementation to return a
+		// large custom records blob.
+		mockSigner.ExpectedCalls = fn.Filter(func(c *mock.Call) bool {
+			return c.Method != "PackSigs"
+		}, mockSigner.ExpectedCalls)
+		mockSigner.On("PackSigs", mock.Anything).
+			Return(fn.Ok(fn.Some(largeBlob)))
+	})
+
+	// Add HTLCs with identical payment hashes and amounts, but descending
+	// CLTV values. We will expect the signatures to appear in the reverse
+	// order that the HTLCs are added due to the commitment sorting.
+	for i := 0; i < numHtlcs; i++ {
+		var (
+			preimage lntypes.Preimage
+			hash     = preimage.Hash()
+		)
+
+		htlc := &lnwire.UpdateAddHTLC{
+			ID:          uint64(i),
+			PaymentHash: hash,
+			Amount:      htlcAmt,
+			Expiry:      uint32(numHtlcs - i),
+		}
+
+		if _, err := aliceChannel.AddHTLC(htlc, nil); err != nil {
+			t.Fatalf("alice unable to add htlc: %v", err)
+		}
+		if _, err := bobChannel.ReceiveHTLC(htlc); err != nil {
+			t.Fatalf("bob unable to receive htlc: %v", err)
+		}
+	}
+
+	// We expect an error because of the large custom records blob.
+	_, err = aliceChannel.SignNextCommitment()
+	require.ErrorContains(t, err, "exceeds max allowed size")
+}
+
 // 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.
@@ -3046,6 +3109,10 @@ func restartChannel(channelOld *LightningChannel) (*LightningChannel, error) {
 	return channelNew, nil
 }
 
+// testChanSyncOweCommitment tests that if Bob restarts (and then Alice) before
+// he receives Alice's CommitSig message, then Alice concludes that she needs
+// to re-send the CommitDiff. After the diff has been sent, both nodes should
+// resynchronize and be able to complete the dangling commit.
 func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
 	// Create a test channel which will be used for the duration of this
 	// unittest. The channel will be funded evenly with Alice having 5 BTC,
@@ -3210,8 +3277,10 @@ func testChanSyncOweCommitment(t *testing.T, chanType channeldb.ChannelType) {
 				len(commitSigMsg.HtlcSigs))
 		}
 		for i, htlcSig := range commitSigMsg.HtlcSigs {
-			if !bytes.Equal(htlcSig.RawBytes(),
-				aliceNewCommit.HtlcSigs[i].RawBytes()) {
+			if !bytes.Equal(
+				htlcSig.RawBytes(),
+				aliceNewCommit.HtlcSigs[i].RawBytes(),
+			) {
 
 				t.Fatalf("htlc sig msgs don't match: "+
 					"expected %v got %v",
@@ -3389,6 +3458,100 @@ func TestChanSyncOweCommitment(t *testing.T) {
 	}
 }
 
+type testSigBlob struct {
+	BlobInt tlv.RecordT[tlv.TlvType65634, uint16]
+}
+
+// TestChanSyncOweCommitmentAuxSigner tests that when one party owes a
+// signature after a channel reest, if an aux signer is present, then the
+// signature message sent includes the additional aux sigs as extra data.
+func TestChanSyncOweCommitmentAuxSigner(t *testing.T) {
+	t.Parallel()
+
+	// Create a test channel which will be used for the duration of this
+	// unittest. The channel will be funded evenly with Alice having 5 BTC,
+	// and Bob having 5 BTC.
+	chanType := channeldb.SingleFunderTweaklessBit |
+		channeldb.AnchorOutputsBit | channeldb.SimpleTaprootFeatureBit |
+		channeldb.TapscriptRootBit
+
+	aliceChannel, bobChannel, err := CreateTestChannels(t, chanType)
+	require.NoError(t, err, "unable to create test channels")
+
+	// We'll now manually attach an aux signer to Alice's channel.
+	auxSigner := &MockAuxSigner{}
+	aliceChannel.auxSigner = fn.Some[AuxSigner](auxSigner)
+
+	var fakeOnionBlob [lnwire.OnionPacketSize]byte
+	copy(
+		fakeOnionBlob[:],
+		bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize),
+	)
+
+	// To kick things off, we'll have Alice send a single HTLC to Bob.
+	htlcAmt := lnwire.NewMSatFromSatoshis(20000)
+	var bobPreimage [32]byte
+	copy(bobPreimage[:], bytes.Repeat([]byte{0}, 32))
+	rHash := sha256.Sum256(bobPreimage[:])
+	h := &lnwire.UpdateAddHTLC{
+		PaymentHash: rHash,
+		Amount:      htlcAmt,
+		Expiry:      uint32(10),
+		OnionBlob:   fakeOnionBlob,
+	}
+
+	_, err = aliceChannel.AddHTLC(h, nil)
+	require.NoError(t, err, "unable to recv bob's htlc: %v", err)
+
+	// We'll set up the mock to expect calls to PackSigs and also
+	// SubmitSubmitSecondLevelSigBatch.
+	var sigBlobBuf bytes.Buffer
+	sigBlob := testSigBlob{
+		BlobInt: tlv.NewPrimitiveRecord[tlv.TlvType65634, uint16](5),
+	}
+	tlvStream, err := tlv.NewStream(sigBlob.BlobInt.Record())
+	require.NoError(t, err, "unable to create tlv stream")
+	require.NoError(t, tlvStream.Encode(&sigBlobBuf))
+
+	auxSigner.On(
+		"SubmitSecondLevelSigBatch", mock.Anything, mock.Anything,
+		mock.Anything,
+	).Return(nil).Twice()
+	auxSigner.On(
+		"PackSigs", mock.Anything,
+	).Return(
+		fn.Ok(fn.Some(sigBlobBuf.Bytes())), nil,
+	)
+
+	_, err = aliceChannel.SignNextCommitment()
+	require.NoError(t, err, "unable to sign commitment")
+
+	_, err = aliceChannel.GenMusigNonces()
+	require.NoError(t, err, "unable to generate musig nonces")
+
+	// Next we'll simulate a restart, by having Bob send over a chan sync
+	// message to Alice.
+	bobSyncMsg, err := bobChannel.channelState.ChanSyncMsg()
+	require.NoError(t, err, "unable to produce chan sync msg")
+
+	aliceMsgsToSend, _, _, err := aliceChannel.ProcessChanSyncMsg(
+		bobSyncMsg,
+	)
+	require.NoError(t, err)
+	require.Len(t, aliceMsgsToSend, 2)
+
+	// The first message should be an update add HTLC.
+	require.IsType(t, &lnwire.UpdateAddHTLC{}, aliceMsgsToSend[0])
+
+	// The second should be a commit sig message.
+	sigMsg, ok := aliceMsgsToSend[1].(*lnwire.CommitSig)
+	require.True(t, ok)
+	require.True(t, sigMsg.PartialSig.IsSome())
+
+	// The signature should have the CustomRecords field set.
+	require.NotEmpty(t, sigMsg.CustomRecords)
+}
+
 func testChanSyncOweCommitmentPendingRemote(t *testing.T,
 	chanType channeldb.ChannelType) {
 
@@ -3398,7 +3561,10 @@ func testChanSyncOweCommitmentPendingRemote(t *testing.T,
 	require.NoError(t, err, "unable to create test channels")
 
 	var fakeOnionBlob [lnwire.OnionPacketSize]byte
-	copy(fakeOnionBlob[:], bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize))
+	copy(
+		fakeOnionBlob[:],
+		bytes.Repeat([]byte{0x05}, lnwire.OnionPacketSize),
+	)
 
 	// We'll start off the scenario where Bob send two htlcs to Alice in a
 	// single state update.
@@ -3437,7 +3603,9 @@ func testChanSyncOweCommitmentPendingRemote(t *testing.T,
 
 	// Next, Alice settles the HTLCs from Bob in distinct state updates.
 	for i := 0; i < numHtlcs; i++ {
-		err = aliceChannel.SettleHTLC(preimages[i], uint64(i), nil, nil, nil)
+		err = aliceChannel.SettleHTLC(
+			preimages[i], uint64(i), nil, nil, nil,
+		)
 		if err != nil {
 			t.Fatalf("unable to settle htlc: %v", err)
 		}
@@ -3727,7 +3895,7 @@ func testChanSyncOweRevocation(t *testing.T, chanType channeldb.ChannelType) {
 }
 
 // TestChanSyncOweRevocation tests that if Bob restarts (and then Alice) before
-// he receiver's Alice's RevokeAndAck message, then Alice concludes that she
+// he received Alice's RevokeAndAck message, then Alice concludes that she
 // needs to re-send the RevokeAndAck. After the revocation has been sent, both
 // nodes should be able to successfully complete another state transition.
 func TestChanSyncOweRevocation(t *testing.T) {
diff --git a/lnwallet/test/test_interface.go b/lnwallet/test/test_interface.go
index bcb396caf..4a02f0324 100644
--- a/lnwallet/test/test_interface.go
+++ b/lnwallet/test/test_interface.go
@@ -34,6 +34,7 @@ import (
 	"github.com/lightningnetwork/lnd/chainntnfs"
 	"github.com/lightningnetwork/lnd/chainntnfs/btcdnotify"
 	"github.com/lightningnetwork/lnd/channeldb"
+	"github.com/lightningnetwork/lnd/fn"
 	"github.com/lightningnetwork/lnd/input"
 	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/kvdb"
@@ -940,6 +941,7 @@ func testSingleFunderReservationWorkflow(miner *rpctest.Harness,
 	fundingPoint := aliceChanReservation.FundingOutpoint()
 	_, err = bobChanReservation.CompleteReservationSingle(
 		fundingPoint, aliceCommitSig,
+		fn.None[lnwallet.AuxFundingDesc](),
 	)
 	require.NoError(t, err, "bob unable to consume single reservation")
 
diff --git a/lnwallet/test_utils.go b/lnwallet/test_utils.go
index a9f71f24c..d4f0d05ae 100644
--- a/lnwallet/test_utils.go
+++ b/lnwallet/test_utils.go
@@ -1,6 +1,7 @@
 package lnwallet
 
 import (
+	"bytes"
 	"crypto/rand"
 	"encoding/binary"
 	"encoding/hex"
@@ -21,6 +22,8 @@ import (
 	"github.com/lightningnetwork/lnd/lnwallet/chainfee"
 	"github.com/lightningnetwork/lnd/lnwire"
 	"github.com/lightningnetwork/lnd/shachain"
+	"github.com/lightningnetwork/lnd/tlv"
+	"github.com/stretchr/testify/mock"
 	"github.com/stretchr/testify/require"
 )
 
@@ -369,9 +372,13 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
 
 	// TODO(roasbeef): make mock version of pre-image store
 
+	auxSigner := NewDefaultAuxSignerMock(t)
+
 	alicePool := NewSigPool(1, aliceSigner)
 	channelAlice, err := NewLightningChannel(
 		aliceSigner, aliceChannelState, alicePool,
+		WithLeafStore(&MockAuxLeafStore{}),
+		WithAuxSigner(auxSigner),
 	)
 	if err != nil {
 		return nil, nil, err
@@ -386,6 +393,8 @@ func CreateTestChannels(t *testing.T, chanType channeldb.ChannelType,
 	bobPool := NewSigPool(1, bobSigner)
 	channelBob, err := NewLightningChannel(
 		bobSigner, bobChannelState, bobPool,
+		WithLeafStore(&MockAuxLeafStore{}),
+		WithAuxSigner(auxSigner),
 	)
 	if err != nil {
 		return nil, nil, err
@@ -586,3 +595,38 @@ func ForceStateTransition(chanA, chanB *LightningChannel) error {
 
 	return nil
 }
+
+func NewDefaultAuxSignerMock(t *testing.T) *MockAuxSigner {
+	auxSigner := &MockAuxSigner{}
+
+	type testSigBlob struct {
+		BlobInt tlv.RecordT[tlv.TlvType65634, uint16]
+	}
+
+	var sigBlobBuf bytes.Buffer
+	sigBlob := testSigBlob{
+		BlobInt: tlv.NewPrimitiveRecord[tlv.TlvType65634, uint16](5),
+	}
+	tlvStream, err := tlv.NewStream(sigBlob.BlobInt.Record())
+	require.NoError(t, err, "unable to create tlv stream")
+	require.NoError(t, tlvStream.Encode(&sigBlobBuf))
+
+	auxSigner.On(
+		"SubmitSecondLevelSigBatch", mock.Anything, mock.Anything,
+		mock.Anything,
+	).Return(nil)
+	auxSigner.On(
+		"PackSigs", mock.Anything,
+	).Return(fn.Ok(fn.Some(sigBlobBuf.Bytes())))
+	auxSigner.On(
+		"UnpackSigs", mock.Anything,
+	).Return(fn.Ok([]fn.Option[tlv.Blob]{
+		fn.Some(sigBlobBuf.Bytes()),
+	}))
+	auxSigner.On(
+		"VerifySecondLevelSigs", mock.Anything, mock.Anything,
+		mock.Anything,
+	).Return(nil)
+
+	return auxSigner
+}
diff --git a/lnwallet/transactions_test.go b/lnwallet/transactions_test.go
index 3588acfeb..8786c2d5d 100644
--- a/lnwallet/transactions_test.go
+++ b/lnwallet/transactions_test.go
@@ -1018,9 +1018,12 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp
 		tc.remotePaymentBasepointSecret, remoteDummy1, remoteDummy2,
 	}, nil)
 
+	auxSigner := NewDefaultAuxSignerMock(t)
 	remotePool := NewSigPool(1, remoteSigner)
 	channelRemote, err := NewLightningChannel(
 		remoteSigner, remoteChannelState, remotePool,
+		WithLeafStore(&MockAuxLeafStore{}),
+		WithAuxSigner(auxSigner),
 	)
 	require.NoError(t, err)
 	require.NoError(t, remotePool.Start())
@@ -1028,6 +1031,8 @@ func createTestChannelsForVectors(tc *testContext, chanType channeldb.ChannelTyp
 	localPool := NewSigPool(1, localSigner)
 	channelLocal, err := NewLightningChannel(
 		localSigner, localChannelState, localPool,
+		WithLeafStore(&MockAuxLeafStore{}),
+		WithAuxSigner(auxSigner),
 	)
 	require.NoError(t, err)
 	require.NoError(t, localPool.Start())
diff --git a/lnwire/commit_sig_test.go b/lnwire/commit_sig_test.go
new file mode 100644
index 000000000..0772a2fb8
--- /dev/null
+++ b/lnwire/commit_sig_test.go
@@ -0,0 +1,168 @@
+package lnwire
+
+import (
+	"bytes"
+	"fmt"
+	"testing"
+
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
+	"github.com/lightningnetwork/lnd/tlv"
+	"github.com/stretchr/testify/require"
+)
+
+// testCase is a test case for the CommitSig message.
+type commitSigTestCase struct {
+	// Msg is the message to be encoded and decoded.
+	Msg CommitSig
+
+	// ExpectEncodeError is a flag that indicates whether we expect the
+	// encoding of the message to fail.
+	ExpectEncodeError bool
+}
+
+// generateCommitSigTestCases generates a set of CommitSig message test cases.
+func generateCommitSigTestCases(t *testing.T) []commitSigTestCase {
+	// Firstly, we'll set basic values for the message fields.
+	//
+	// Generate random channel ID.
+	chanIDBytes, err := generateRandomBytes(32)
+	require.NoError(t, err)
+
+	var chanID ChannelID
+	copy(chanID[:], chanIDBytes)
+
+	// Generate random commit sig.
+	commitSigBytes, err := generateRandomBytes(64)
+	require.NoError(t, err)
+
+	sig, err := NewSigFromSchnorrRawSignature(commitSigBytes)
+	require.NoError(t, err)
+
+	sigScalar := new(btcec.ModNScalar)
+	sigScalar.SetByteSlice(sig.RawBytes())
+
+	var nonce [musig2.PubNonceSize]byte
+	copy(nonce[:], commitSigBytes)
+
+	sigWithNonce := NewPartialSigWithNonce(nonce, *sigScalar)
+	partialSig := MaybePartialSigWithNonce(sigWithNonce)
+
+	// Define custom records.
+	recordKey1 := uint64(MinCustomRecordsTlvType + 1)
+	recordValue1, err := generateRandomBytes(10)
+	require.NoError(t, err)
+
+	recordKey2 := uint64(MinCustomRecordsTlvType + 2)
+	recordValue2, err := generateRandomBytes(10)
+	require.NoError(t, err)
+
+	customRecords := CustomRecords{
+		recordKey1: recordValue1,
+		recordKey2: recordValue2,
+	}
+
+	// Construct an instance of extra data that contains records with TLV
+	// types below the minimum custom records threshold and that lack
+	// corresponding fields in the message struct. Content should persist in
+	// the extra data field after encoding and decoding.
+	var (
+		recordBytes45 = []byte("recordBytes45")
+		tlvRecord45   = tlv.NewPrimitiveRecord[tlv.TlvType45](
+			recordBytes45,
+		)
+
+		recordBytes55 = []byte("recordBytes55")
+		tlvRecord55   = tlv.NewPrimitiveRecord[tlv.TlvType55](
+			recordBytes55,
+		)
+	)
+
+	var extraData ExtraOpaqueData
+	err = extraData.PackRecords(
+		[]tlv.RecordProducer{&tlvRecord45, &tlvRecord55}...,
+	)
+	require.NoError(t, err)
+
+	invalidCustomRecords := CustomRecords{
+		MinCustomRecordsTlvType - 1: recordValue1,
+	}
+
+	return []commitSigTestCase{
+		{
+			Msg: CommitSig{
+				ChanID:        chanID,
+				CommitSig:     sig,
+				PartialSig:    partialSig,
+				CustomRecords: customRecords,
+				ExtraData:     extraData,
+			},
+		},
+		// Add a test case where the blinding point field is not
+		// populated.
+		{
+			Msg: CommitSig{
+				ChanID:        chanID,
+				CommitSig:     sig,
+				CustomRecords: customRecords,
+			},
+		},
+		// Add a test case where the custom records field is not
+		// populated.
+		{
+			Msg: CommitSig{
+				ChanID:     chanID,
+				CommitSig:  sig,
+				PartialSig: partialSig,
+			},
+		},
+		// Add a case where the custom records are invalid.
+		{
+			Msg: CommitSig{
+				ChanID:        chanID,
+				CommitSig:     sig,
+				PartialSig:    partialSig,
+				CustomRecords: invalidCustomRecords,
+			},
+			ExpectEncodeError: true,
+		},
+	}
+}
+
+// TestCommitSigEncodeDecode tests CommitSig message encoding and decoding for
+// all supported field values.
+func TestCommitSigEncodeDecode(t *testing.T) {
+	t.Parallel()
+
+	// Generate test cases.
+	testCases := generateCommitSigTestCases(t)
+
+	// Execute test cases.
+	for tcIdx, tc := range testCases {
+		t.Run(fmt.Sprintf("testcase-%d", tcIdx), func(t *testing.T) {
+			// Encode test case message.
+			var buf bytes.Buffer
+			err := tc.Msg.Encode(&buf, 0)
+
+			// Check if we expect an encoding error.
+			if tc.ExpectEncodeError {
+				require.Error(t, err)
+				return
+			}
+
+			require.NoError(t, err)
+
+			// Decode the encoded message bytes message.
+			var actualMsg CommitSig
+			decodeReader := bytes.NewReader(buf.Bytes())
+			err = actualMsg.Decode(decodeReader, 0)
+			require.NoError(t, err)
+
+			// The signature type isn't serialized.
+			actualMsg.CommitSig.ForceSchnorr()
+
+			// Compare the two messages to ensure equality.
+			require.Equal(t, tc.Msg, actualMsg)
+		})
+	}
+}
diff --git a/lnwire/lnwire_test.go b/lnwire/lnwire_test.go
index 7eb434f45..e941962d5 100644
--- a/lnwire/lnwire_test.go
+++ b/lnwire/lnwire_test.go
@@ -945,6 +945,8 @@ func TestLightningWireProtocol(t *testing.T) {
 				}
 			}
 
+			req.CustomRecords = randCustomRecords(t, r)
+
 			// 50/50 chance to attach a partial sig.
 			if r.Int31()%2 == 0 {
 				req.PartialSig = somePartialSigWithNonce(t, r)
diff --git a/peer/test_utils.go b/peer/test_utils.go
index e0ae29be8..948a22c4e 100644
--- a/peer/test_utils.go
+++ b/peer/test_utils.go
@@ -304,6 +304,8 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a,
 	alicePool := lnwallet.NewSigPool(1, aliceSigner)
 	channelAlice, err := lnwallet.NewLightningChannel(
 		aliceSigner, aliceChannelState, alicePool,
+		lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
+		lnwallet.WithAuxSigner(&lnwallet.MockAuxSigner{}),
 	)
 	if err != nil {
 		return nil, err
@@ -316,6 +318,8 @@ func createTestPeerWithChannel(t *testing.T, updateChan func(a,
 	bobPool := lnwallet.NewSigPool(1, bobSigner)
 	channelBob, err := lnwallet.NewLightningChannel(
 		bobSigner, bobChannelState, bobPool,
+		lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
+		lnwallet.WithAuxSigner(&lnwallet.MockAuxSigner{}),
 	)
 	if err != nil {
 		return nil, err
diff --git a/rpcserver_test.go b/rpcserver_test.go
index ca70ad9df..9dd3c3f86 100644
--- a/rpcserver_test.go
+++ b/rpcserver_test.go
@@ -1,14 +1,79 @@
 package lnd
 
 import (
+	"fmt"
 	"testing"
 
+	"github.com/lightningnetwork/lnd/channeldb"
+	"github.com/lightningnetwork/lnd/fn"
+	"github.com/lightningnetwork/lnd/lnrpc"
 	"github.com/stretchr/testify/assert"
+	"github.com/stretchr/testify/require"
+	"google.golang.org/protobuf/proto"
 )
 
 func TestGetAllPermissions(t *testing.T) {
 	perms := GetAllPermissions()
 
-	// Currently there are there are 16 entity:action pairs in use.
+	// Currently there are 16 entity:action pairs in use.
 	assert.Equal(t, len(perms), 16)
 }
+
+// mockDataParser is a mock implementation of the AuxDataParser interface.
+type mockDataParser struct {
+}
+
+// InlineParseCustomData replaces any custom data binary blob in the given RPC
+// message with its corresponding JSON formatted data. This transforms the
+// binary (likely TLV encoded) data to a human-readable JSON representation
+// (still as byte slice).
+func (m *mockDataParser) InlineParseCustomData(msg proto.Message) error {
+	switch m := msg.(type) {
+	case *lnrpc.ChannelBalanceResponse:
+		m.CustomChannelData = []byte(`{"foo": "bar"}`)
+
+		return nil
+
+	default:
+		return fmt.Errorf("mock only supports ChannelBalanceResponse")
+	}
+}
+
+func TestAuxDataParser(t *testing.T) {
+	// We create an empty channeldb, so we can fetch some channels.
+	cdb, err := channeldb.Open(t.TempDir())
+	require.NoError(t, err)
+	t.Cleanup(func() {
+		require.NoError(t, cdb.Close())
+	})
+
+	r := &rpcServer{
+		server: &server{
+			chanStateDB: cdb.ChannelStateDB(),
+			implCfg: &ImplementationCfg{
+				AuxComponents: AuxComponents{
+					AuxDataParser: fn.Some[AuxDataParser](
+						&mockDataParser{},
+					),
+				},
+			},
+		},
+	}
+
+	// With the aux data parser in place, we should get a formatted JSON
+	// in the custom channel data field.
+	resp, err := r.ChannelBalance(nil, &lnrpc.ChannelBalanceRequest{})
+	require.NoError(t, err)
+	require.NotNil(t, resp)
+	require.Equal(t, []byte(`{"foo": "bar"}`), resp.CustomChannelData)
+
+	// If we don't supply the aux data parser, we should get the raw binary
+	// data. Which in this case is just two VarInt fields (1 byte each) that
+	// represent the value of 0 (zero active and zero pending channels).
+	r.server.implCfg.AuxComponents.AuxDataParser = fn.None[AuxDataParser]()
+
+	resp, err = r.ChannelBalance(nil, &lnrpc.ChannelBalanceRequest{})
+	require.NoError(t, err)
+	require.NotNil(t, resp)
+	require.Equal(t, []byte{0x00, 0x00}, resp.CustomChannelData)
+}