Repository: lightningnetwork/lnd
Pull request #6361 (2022-03-23 21:05:02)

Musig2: add new MuSig2 RPC methods to `signrpc`
Depends on https://github.com/btcsuite/btcd/pull/1820.

Adds a new MuSig2 API for cooperatively producing Schnorr signatures that can be used either for Taproot internal keys or for `OP_CHECKSIG` signatures within a tap script tree.


lightninglabs-deploy: @guggero, remember to re-request review from reviewers when ready
Roasbeef: What needs to be done to take this out of the draft stage? Merging of the upstream btcd PR? 
guggero: > What needs to be done to take this out of the draft stage? Merging of the upstream btcd PR?

Mostly adding the MuSig2 methods to the signer interface so they can also be forwarded to the remote signer. And cleaning up some user input validation TODOs. So not a big diff, working on it today.
guggero: Took the PR out of draft state. Everything works now, including remote signing!

While working on remote signing support for MuSig2 I noticed that the standalone Taproot functionality does not work in remote mode. Turns out we need https://github.com/lightningnetwork/lnd/issues/6268 to enable that.
I'm going to work on that in a separate PR, commented out the failing remote mode itests for now.

lightninglabs-deploy: @roasbeef: review reminder
guggero: I added a convenience RPC called `MuSig2CombineKeys` to make it possible to just get the combined key and Taproot public key without creating a session.

I also went ahead and added a short doc explaining the current MuSig2 implementation and examples.
guggero: I updated to the latest version of https://github.com/btcsuite/btcd/pull/1820 and added an itest for using a MuSig2 combined key in a tap leaf with a simple `OP_CHECKSIG` script.

I also found a small bug in the helper function `TapscriptPartialReveal` that is used for the fee calculation. A proof for a tree with just a single leaf can be empty, so the inclusion proof can be variable size.
Roasbeef: The btcd PR has been merged, so this can be rebased! 
Roasbeef: > A proof for a tree with just a single leaf can be empty, so the inclusion proof can be variable size.

Yes in this case, the verifier just takes the leaf, computes one level of tapLeaf hashing and uses that directly. You can see this interaction here if you pretend the loop below doesn't actually exist: https://github.com/btcsuite/btcd/blob/master/txscript/taproot.go#L155-L177
Roasbeef: The new `btcec` tag: https://github.com/btcsuite/btcd/releases/tag/btcec%2Fv2.2.0

Patch:
diff --git a/.golangci.yml b/.golangci.yml
index 6c1fc1908..1b5226b16 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -43,7 +43,7 @@ linters-settings:
       - G402 # Look for bad TLS connection settings.
       - G306 # Poor file permissions used when writing to a new file.
   staticcheck: 
-    go: "1.16"
+    go: "1.18"
     checks: ["-SA1019"]
 
 linters:
diff --git a/docs/musig2.md b/docs/musig2.md
new file mode 100644
index 000000000..6a445bbef
--- /dev/null
+++ b/docs/musig2.md
@@ -0,0 +1,117 @@
+# MuSig2
+
+With `lnd v0.15.0-beta` a new, experimental MuSig2 API was added to the
+`signrpc` subserver RPC. With MuSig2 it is possible to combine the public keys
+of multiple signing participants into a single, combined key. The signers can
+then later come together and cooperatively produce a single signature that is
+valid for that combined public key. MuSig2 therefore is an interactive `n-of-n`
+signature scheme that produces a final/complete Schnorr signature out of `n`
+partial signatures.
+
+**NOTE**: At the time the MuSig2 code in `btcd`/`lnd` was written, there was no
+"official" MuSig2 support merged to either `bitcoind` or `secp256k1`. Therefore,
+some smaller details in the signing protocol could change in the future that
+might not be backward compatible. So this API must be seen as highly
+experimental and backward compatibility can't be guaranteed at this point.
+
+## References
+ * [MuSig2 paper](https://eprint.iacr.org/2020/1261.pdf)
+ * [Draft MuSig
+   BIP](https://github.com/jonasnick/bips/blob/musig2/bip-musig2.mediawiki)
+ * [MuSig2 implementation discussion in
+   `bitcoind`](https://github.com/bitcoin/bitcoin/issues/23326)
+
+## A note on security
+
+The MuSig2 signing protocol is secure from leaking private key information of
+the signers **as long as the same secret nonce is never used multiple times**.
+If the same nonce is used for signing multiple times then the private key can
+leak. To avoid this security risk, the `signrpc.MuSig2Sign` RPC can only be
+called a single time for the same session ID. This has the implication that if a
+signing session fails or is aborted (for example because a participant isn't
+responsive or the message changes after some participants have already started
+signing), a completely new signing session needs to be initialized, which
+internally creates fresh nonces.
+
+## Examples
+
+An API is sometimes easiest explained by showing concrete usage examples. Here
+we take a look at the MuSig2 integration tests in `lnd`, since they both serve
+to test the RPCs and to showcase the different use cases.
+
+### 3-of-3 Taproot key spend path (BIP-0086)
+
+See `testTaprootMuSig2KeySpendBip86` in
+[`lntest/itest/lnd_taproot_test.go`](../lntest/itest/lnd_taproot_test.go) to see
+the full code.
+
+This example uses combines the public keys of 3 participants into a shared
+MuSig2 public key that is then tweaked with the
+[BIP-0086](https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki#address-derivation)
+TapTweak to be turned into a Taproot public key that can be used directly as the
+`pkScript` of a p2tr output on chain.
+
+The most notable parameter for this to work is the `TaprootTweak` parameter in
+the `MuSig2CreateSession` RPC call:
+
+```go
+	taprootTweak := &signrpc.TaprootTweakDesc{
+        KeySpendOnly: true,
+	}
+	
+	sessResp1, err := node.SignerClient.MuSig2CreateSession(
+		ctx, &signrpc.MuSig2SessionRequest{
+			KeyLoc:           keyDesc1.KeyLoc,
+			AllSignerPubkeys: allPubKeys,
+			TaprootTweak:     taprootTweak,
+		},
+	)
+```
+
+### 3-of-3 Taproot key spend path with root hash commitment
+
+See `testTaprootMuSig2KeySpendRootHash` in
+[`lntest/itest/lnd_taproot_test.go`](../lntest/itest/lnd_taproot_test.go) to see
+the full code.
+
+This is very similar to the above example but with the main difference that the
+p2tr output on chain not only commits to a key spend path but also a script
+path. The MuSig2 combined key becomes the Taproot internal key and the TapTweak
+commits to both the internal key and the Taproot script merkle root hash.
+
+The most notable parameter for this to work is the `TaprootTweak` parameter in
+the `MuSig2CreateSession` RPC call:
+
+```go
+	taprootTweak := &signrpc.TaprootTweakDesc{
+	    ScriptRoot: rootHash[:],
+	}
+	
+	sessResp1, err := node.SignerClient.MuSig2CreateSession(
+		ctx, &signrpc.MuSig2SessionRequest{
+			KeyLoc:           keyDesc1.KeyLoc,
+			AllSignerPubkeys: allPubKeys,
+			TaprootTweak:     taprootTweak,
+		},
+	)
+```
+
+### 3-of-3 `OP_CHECKSIG` in Taproot script spend path
+
+See `testTaprootMuSig2CombinedLeafKeySpend` in
+[`lntest/itest/lnd_taproot_test.go`](../lntest/itest/lnd_taproot_test.go) to see
+the full code.
+
+This example is definitely the most involved one. To be able to use a MuSig2
+combined key and then spend it through a Taproot script spend with an
+`OP_CHECKSIG` script, the following steps need to be performed:
+
+1. Derive signing keys on signers, combine them through `MuSig2CombineKeys`.
+2. Create a Taproot script tree with a script leaf `<combinedKey> OP_CHECKSIG`.
+3. Create the Taproot key by committing to the script tree root hash.
+4. When spending the output, the message being signed needs to be the sighash of
+   a Taproot script spend that also commits to the leaf hash.
+5. The final witness stack needs to contain the combined signature, the leaf
+   script and the control block (which contains the internal public key and the
+   inclusion proof if there were any other script leaves).
+
diff --git a/docs/release-notes/release-notes-0.15.0.md b/docs/release-notes/release-notes-0.15.0.md
index f64945a84..ca56fb58a 100644
--- a/docs/release-notes/release-notes-0.15.0.md
+++ b/docs/release-notes/release-notes-0.15.0.md
@@ -25,6 +25,16 @@ addresses](https://github.com/lightningnetwork/lnd/pull/6263). Using
 then watch it on chain. Taproot script spends are also supported through the
 `signrpc.SignOutputRaw` RPC (`/v2/signer/signraw` in REST).
 
+## MuSig2
+
+The [`signrpc.Signer` RPC service now supports EXPERIMENTAL MuSig2
+signing](https://github.com/lightningnetwork/lnd/pull/6361).
+
+More information can be found in the [MuSig2 documentation](../musig2.md).
+Note that the MuSig2 BIP is not final yet and therefore the MuSig2 API must be
+considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+releases. Backward compatibility is not guaranteed!
+
 ## `lncli`
 
 * Add [auto-generated command-line completions](https://github.com/lightningnetwork/lnd/pull/4177) 
diff --git a/go.mod b/go.mod
index b5a8ea45f..2ed2d56d3 100644
--- a/go.mod
+++ b/go.mod
@@ -1,11 +1,10 @@
 module github.com/lightningnetwork/lnd
 
 require (
-	github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e // indirect
 	github.com/NebulousLabs/go-upnp v0.0.0-20180202185039-29b680b06c82
 	github.com/Yawning/aez v0.0.0-20211027044916-e49e68abd344
 	github.com/btcsuite/btcd v0.22.0-beta.0.20220330201728-074266215c26
-	github.com/btcsuite/btcd/btcec/v2 v2.1.3
+	github.com/btcsuite/btcd/btcec/v2 v2.2.0
 	github.com/btcsuite/btcd/btcutil v1.1.1
 	github.com/btcsuite/btcd/btcutil/psbt v1.1.3
 	github.com/btcsuite/btcd/chaincfg/chainhash v1.0.1
@@ -31,7 +30,6 @@ require (
 	github.com/jedib0t/go-pretty/v6 v6.2.7
 	github.com/jessevdk/go-flags v1.4.0
 	github.com/jrick/logrotate v1.0.0
-	github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 // indirect
 	github.com/kkdai/bstream v1.0.0
 	github.com/lightninglabs/neutrino v0.13.2
 	github.com/lightninglabs/protobuf-hex-display v1.4.3-hex-display
@@ -50,7 +48,6 @@ require (
 	github.com/stretchr/testify v1.7.0
 	github.com/tv42/zbase32 v0.0.0-20160707012821-501572607d02
 	github.com/urfave/cli v1.22.4
-	gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
 	go.etcd.io/etcd/client/pkg/v3 v3.5.0
 	go.etcd.io/etcd/client/v3 v3.5.0
 	golang.org/x/crypto v0.0.0-20210921155107-089bfa567519
@@ -58,14 +55,108 @@ require (
 	golang.org/x/sync v0.0.0-20210220032951-036812b2e83c
 	golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1
 	golang.org/x/time v0.0.0-20210220033141-f8bda1e9f3ba
-	golang.org/x/tools v0.1.8 // indirect
 	google.golang.org/grpc v1.38.0
 	google.golang.org/protobuf v1.26.0
-	gopkg.in/errgo.v1 v1.0.1 // indirect
 	gopkg.in/macaroon-bakery.v2 v2.0.1
 	gopkg.in/macaroon.v2 v2.0.0
 )
 
+require (
+	github.com/NebulousLabs/fastrand v0.0.0-20181203155948-6fb6489aac4e // indirect
+	github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
+	github.com/aead/siphash v1.0.1 // indirect
+	github.com/andybalholm/brotli v1.0.3 // indirect
+	github.com/beorn7/perks v1.0.1 // indirect
+	github.com/btcsuite/btcwallet/wallet/txsizes v1.1.0 // indirect
+	github.com/btcsuite/go-socks v0.0.0-20170105172521-4720035b7bfd // indirect
+	github.com/btcsuite/websocket v0.0.0-20150119174127-31079b680792 // indirect
+	github.com/btcsuite/winsvc v1.0.0 // indirect
+	github.com/cespare/xxhash/v2 v2.1.1 // indirect
+	github.com/coreos/go-semver v0.3.0 // indirect
+	github.com/coreos/go-systemd/v22 v22.3.2 // indirect
+	github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect
+	github.com/decred/dcrd/crypto/blake256 v1.0.0 // indirect
+	github.com/decred/dcrd/lru v1.0.0 // indirect
+	github.com/dsnet/compress v0.0.1 // indirect
+	github.com/dustin/go-humanize v1.0.0 // indirect
+	github.com/fergusstrange/embedded-postgres v1.10.0 // indirect
+	github.com/form3tech-oss/jwt-go v3.2.3+incompatible // indirect
+	github.com/gogo/protobuf v1.3.2 // indirect
+	github.com/golang/snappy v0.0.4 // indirect
+	github.com/google/btree v1.0.1 // indirect
+	github.com/grpc-ecosystem/grpc-gateway v1.16.0 // indirect
+	github.com/jackc/chunkreader/v2 v2.0.1 // indirect
+	github.com/jackc/pgconn v1.10.0 // indirect
+	github.com/jackc/pgio v1.0.0 // indirect
+	github.com/jackc/pgpassfile v1.0.0 // indirect
+	github.com/jackc/pgproto3/v2 v2.1.1 // indirect
+	github.com/jackc/pgservicefile v0.0.0-20200714003250-2b9c44734f2b // indirect
+	github.com/jackc/pgtype v1.8.1 // indirect
+	github.com/jackc/puddle v1.1.3 // indirect
+	github.com/jonboulle/clockwork v0.2.2 // indirect
+	github.com/json-iterator/go v1.1.11 // indirect
+	github.com/juju/loggo v0.0.0-20210728185423-eebad3a902c4 // indirect
+	github.com/juju/testing v0.0.0-20220203020004-a0ff61f03494 // indirect
+	github.com/klauspost/compress v1.13.6 // indirect
+	github.com/klauspost/pgzip v1.2.5 // indirect
+	github.com/lib/pq v1.10.3 // indirect
+	github.com/lightninglabs/gozmq v0.0.0-20191113021534-d20a764486bf // indirect
+	github.com/mattn/go-runewidth v0.0.13 // indirect
+	github.com/matttproud/golang_protobuf_extensions v1.0.1 // indirect
+	github.com/mholt/archiver/v3 v3.5.0 // indirect
+	github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
+	github.com/modern-go/reflect2 v1.0.1 // indirect
+	github.com/nwaples/rardecode v1.1.2 // indirect
+	github.com/pierrec/lz4/v4 v4.1.8 // indirect
+	github.com/pmezard/go-difflib v1.0.0 // indirect
+	github.com/prometheus/client_model v0.2.0 // indirect
+	github.com/prometheus/common v0.26.0 // indirect
+	github.com/prometheus/procfs v0.6.0 // indirect
+	github.com/rivo/uniseg v0.2.0 // indirect
+	github.com/rogpeppe/fastuuid v1.2.0 // indirect
+	github.com/russross/blackfriday/v2 v2.0.1 // indirect
+	github.com/shurcooL/sanitized_anchor_name v1.0.0 // indirect
+	github.com/sirupsen/logrus v1.7.0 // indirect
+	github.com/soheilhy/cmux v0.1.5 // indirect
+	github.com/spf13/pflag v1.0.5 // indirect
+	github.com/stretchr/objx v0.2.0 // indirect
+	github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 // indirect
+	github.com/tmc/grpc-websocket-proxy v0.0.0-20201229170055-e5319fda7802 // indirect
+	github.com/ulikunitz/xz v0.5.10 // indirect
+	github.com/xi2/xz v0.0.0-20171230120015-48954b6210f8 // indirect
+	github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2 // indirect
+	gitlab.com/yawning/bsaes.git v0.0.0-20190805113838-0a714cd429ec // indirect
+	go.etcd.io/bbolt v1.3.6 // indirect
+	go.etcd.io/etcd/api/v3 v3.5.0 // indirect
+	go.etcd.io/etcd/client/v2 v2.305.0 // indirect
+	go.etcd.io/etcd/pkg/v3 v3.5.0 // indirect
+	go.etcd.io/etcd/raft/v3 v3.5.0 // indirect
+	go.etcd.io/etcd/server/v3 v3.5.0 // indirect
+	go.opentelemetry.io/contrib v0.20.0 // indirect
+	go.opentelemetry.io/contrib/instrumentation/google.golang.org/grpc/otelgrpc v0.20.0 // indirect
+	go.opentelemetry.io/otel v0.20.0 // indirect
+	go.opentelemetry.io/otel/exporters/otlp v0.20.0 // indirect
+	go.opentelemetry.io/otel/metric v0.20.0 // indirect
+	go.opentelemetry.io/otel/sdk v0.20.0 // indirect
+	go.opentelemetry.io/otel/sdk/export/metric v0.20.0 // indirect
+	go.opentelemetry.io/otel/sdk/metric v0.20.0 // indirect
+	go.opentelemetry.io/otel/trace v0.20.0 // indirect
+	go.opentelemetry.io/proto/otlp v0.7.0 // indirect
+	go.uber.org/atomic v1.7.0 // indirect
+	go.uber.org/multierr v1.6.0 // indirect
+	go.uber.org/zap v1.17.0 // indirect
+	golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e // indirect
+	golang.org/x/text v0.3.7 // indirect
+	golang.org/x/tools v0.1.8 // indirect
+	google.golang.org/genproto v0.0.0-20210617175327-b9e0b3197ced // indirect
+	gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
+	gopkg.in/errgo.v1 v1.0.1 // indirect
+	gopkg.in/natefinch/lumberjack.v2 v2.0.0 // indirect
+	gopkg.in/yaml.v2 v2.4.0 // indirect
+	gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b // indirect
+	sigs.k8s.io/yaml v1.2.0 // indirect
+)
+
 // This replace is for https://github.com/advisories/GHSA-w73w-5m7g-f7qc
 replace github.com/dgrijalva/jwt-go => github.com/golang-jwt/jwt v3.2.1+incompatible
 
@@ -83,6 +174,6 @@ replace github.com/lightninglabs/neutrino => github.com/lightninglabs/neutrino v
 
 // If you change this please also update .github/pull_request_template.md and
 // docs/INSTALL.md.
-go 1.16
+go 1.17
 
 retract v0.0.2
diff --git a/go.sum b/go.sum
index c61a46f1a..705cb4f00 100644
--- a/go.sum
+++ b/go.sum
@@ -79,8 +79,9 @@ github.com/btcsuite/btcd v0.22.0-beta.0.20220330201728-074266215c26 h1:dgH5afJco
 github.com/btcsuite/btcd v0.22.0-beta.0.20220330201728-074266215c26/go.mod h1:taIcYprAW2g6Z9S0gGUxyR+zDwimyDMK5ePOX+iJ2ds=
 github.com/btcsuite/btcd/btcec/v2 v2.1.0/go.mod h1:2VzYrv4Gm4apmbVVsSq5bqf1Ec8v56E48Vt0Y/umPgA=
 github.com/btcsuite/btcd/btcec/v2 v2.1.1/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
-github.com/btcsuite/btcd/btcec/v2 v2.1.3 h1:xM/n3yIhHAhHy04z4i43C8p4ehixJZMsnrVJkgl+MTE=
 github.com/btcsuite/btcd/btcec/v2 v2.1.3/go.mod h1:ctjw4H1kknNJmRN4iP1R7bTQ+v3GJkZBd6mui8ZsAZE=
+github.com/btcsuite/btcd/btcec/v2 v2.2.0 h1:fzn1qaOt32TuLjFlkzYSsBC35Q3KUjT1SwPxiMSCF5k=
+github.com/btcsuite/btcd/btcec/v2 v2.2.0/go.mod h1:U7MHm051Al6XmscBQ0BoNydpOTsFAn707034b5nY8zU=
 github.com/btcsuite/btcd/btcutil v1.0.0/go.mod h1:Uoxwv0pqYWhD//tfTiipkxNfdhG9UrLwaeswfjfdF0A=
 github.com/btcsuite/btcd/btcutil v1.1.0/go.mod h1:5OapHB7A2hBBWLm48mmw4MOHNJCcUBTwmWH/0Jn8VHE=
 github.com/btcsuite/btcd/btcutil v1.1.1 h1:hDcDaXiP0uEzR8Biqo2weECKqEw0uHDZ9ixIWevVQqY=
@@ -757,6 +758,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0
 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4=
 golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM=
 golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU=
+golang.org/x/exp v0.0.0-20220426173459-3bcf042a4bf5/go.mod h1:lgLbSvA5ygNOMpwM/9anMpWVlVJ7Z+cHWq/eFuinpGE=
 golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
 golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
 golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
@@ -782,6 +784,7 @@ golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
 golang.org/x/mod v0.5.1/go.mod h1:5OXOZSfqPIIbmVBIIKWRFfZjPR0E5r58TLhUjH0a2Ro=
+golang.org/x/mod v0.6.0-dev.0.20211013180041-c96bc1413d57/go.mod h1:3p9vT2HGsQu2K1YbXdKPJLVgG5VJdoTa1poYQBtP1AY=
 golang.org/x/net v0.0.0-20180406214816-61147c48b25b/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180719180050-a680a1efc54d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
 golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@@ -980,6 +983,7 @@ golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc
 golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
 golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
 golang.org/x/tools v0.1.3/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
+golang.org/x/tools v0.1.8-0.20211029000441-d6a9af8af023/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/tools v0.1.8 h1:P1HhGGuLW4aAclzjtmJdf0mJOjVUZUzOTqkAkWL+l6w=
 golang.org/x/tools v0.1.8/go.mod h1:nABZi5QlRsZVlzPpHl034qft6wpY4eDcsTt5AaioBiU=
 golang.org/x/xerrors v0.0.0-20190410155217-1f06c39b4373/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
diff --git a/input/musig2.go b/input/musig2.go
new file mode 100644
index 000000000..a7a0d78e4
--- /dev/null
+++ b/input/musig2.go
@@ -0,0 +1,228 @@
+package input
+
+import (
+	"bytes"
+	"crypto/sha256"
+	"fmt"
+
+	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
+	"github.com/lightningnetwork/lnd/keychain"
+)
+
+const (
+	// MuSig2PartialSigSize is the size of a MuSig2 partial signature.
+	// Because a partial signature is just the s value, this corresponds to
+	// the length of a scalar.
+	MuSig2PartialSigSize = 32
+)
+
+// MuSig2SessionID is a type for a session ID that is just a hash of the MuSig2
+// combined key and the local public nonces.
+type MuSig2SessionID [sha256.Size]byte
+
+// MuSig2Signer is an interface that declares all methods that a MuSig2
+// compatible signer needs to implement.
+type MuSig2Signer interface {
+	// MuSig2CreateSession creates a new MuSig2 signing session using the
+	// local key identified by the key locator. The complete list of all
+	// public keys of all signing parties must be provided, including the
+	// public key of the local signing key. If nonces of other parties are
+	// already known, they can be submitted as well to reduce the number of
+	// method calls necessary later on.
+	MuSig2CreateSession(keychain.KeyLocator, []*btcec.PublicKey,
+		*MuSig2Tweaks, [][musig2.PubNonceSize]byte) (*MuSig2SessionInfo,
+		error)
+
+	// MuSig2RegisterNonces registers one or more public nonces of other
+	// signing participants for a session identified by its ID. This method
+	// returns true once we have all nonces for all other signing
+	// participants.
+	MuSig2RegisterNonces(MuSig2SessionID,
+		[][musig2.PubNonceSize]byte) (bool, error)
+
+	// MuSig2Sign creates a partial signature using the local signing key
+	// that was specified when the session was created. This can only be
+	// called when all public nonces of all participants are known and have
+	// been registered with the session. If this node isn't responsible for
+	// combining all the partial signatures, then the cleanup parameter
+	// should be set, indicating that the session can be removed from memory
+	// once the signature was produced.
+	MuSig2Sign(MuSig2SessionID, [sha256.Size]byte,
+		bool) (*musig2.PartialSignature, error)
+
+	// MuSig2CombineSig combines the given partial signature(s) with the
+	// local one, if it already exists. Once a partial signature of all
+	// participants is registered, the final signature will be combined and
+	// returned.
+	MuSig2CombineSig(MuSig2SessionID,
+		[]*musig2.PartialSignature) (*schnorr.Signature, bool, error)
+}
+
+// MuSig2SessionInfo is a struct for keeping track of a signing session
+// information in memory.
+type MuSig2SessionInfo struct {
+	// SessionID is the wallet's internal unique ID of this session. The ID
+	// is the hash over the combined public key and the local public nonces.
+	SessionID [32]byte
+
+	// PublicNonce contains the public nonce of the local signer session.
+	PublicNonce [musig2.PubNonceSize]byte
+
+	// CombinedKey is the combined public key with all tweaks applied to it.
+	CombinedKey *btcec.PublicKey
+
+	// TaprootTweak indicates whether a taproot tweak (BIP-0086 or script
+	// path) was used. The TaprootInternalKey will only be set if this is
+	// set to true.
+	TaprootTweak bool
+
+	// TaprootInternalKey is the raw combined public key without any tweaks
+	// applied to it. This is only set if TaprootTweak is true.
+	TaprootInternalKey *btcec.PublicKey
+
+	// HaveAllNonces indicates whether this session already has all nonces
+	// of all other signing participants registered.
+	HaveAllNonces bool
+
+	// HaveAllSigs indicates whether this session already has all partial
+	// signatures of all other signing participants registered.
+	HaveAllSigs bool
+}
+
+// MuSig2Tweaks is a struct that contains all tweaks that can be applied to a
+// MuSig2 combined public key.
+type MuSig2Tweaks struct {
+	// GenericTweaks is a list of normal tweaks to apply to the combined
+	// public key (and to the private key when signing).
+	GenericTweaks []musig2.KeyTweakDesc
+
+	// TaprootBIP0086Tweak indicates that the final key should use the
+	// taproot tweak as defined in BIP 341, with the BIP 86 modification:
+	//     outputKey = internalKey + h_tapTweak(internalKey)*G.
+	// In this case, the aggregated key before the tweak will be used as the
+	// internal key. If this is set to true then TaprootTweak will be
+	// ignored.
+	TaprootBIP0086Tweak bool
+
+	// TaprootTweak specifies that the final key should use the taproot
+	// tweak as defined in BIP 341:
+	//     outputKey = internalKey + h_tapTweak(internalKey || scriptRoot).
+	// In this case, the aggregated key before the tweak will be used as the
+	// internal key. Will be ignored if TaprootBIP0086Tweak is set to true.
+	TaprootTweak []byte
+}
+
+// HasTaprootTweak returns true if either a taproot BIP0086 tweak or a taproot
+// script root tweak is set.
+func (t *MuSig2Tweaks) HasTaprootTweak() bool {
+	return t.TaprootBIP0086Tweak || len(t.TaprootTweak) > 0
+}
+
+// ToContextOptions converts the tweak descriptor to context options.
+func (t *MuSig2Tweaks) ToContextOptions() []musig2.ContextOption {
+	var tweakOpts []musig2.ContextOption
+	if len(t.GenericTweaks) > 0 {
+		tweakOpts = append(tweakOpts, musig2.WithTweakedContext(
+			t.GenericTweaks...,
+		))
+	}
+
+	// The BIP0086 tweak and the taproot script tweak are mutually
+	// exclusive.
+	if t.TaprootBIP0086Tweak {
+		tweakOpts = append(tweakOpts, musig2.WithBip86TweakCtx())
+	} else if len(t.TaprootTweak) > 0 {
+		tweakOpts = append(tweakOpts, musig2.WithTaprootTweakCtx(
+			t.TaprootTweak,
+		))
+	}
+
+	return tweakOpts
+}
+
+// MuSig2CombineKeys combines the given set of public keys into a single
+// combined MuSig2 combined public key, applying the given tweaks.
+func MuSig2CombineKeys(allSignerPubKeys []*btcec.PublicKey,
+	tweaks *MuSig2Tweaks) (*musig2.AggregateKey, error) {
+
+	// Convert the tweak options into the appropriate MuSig2 API functional
+	// options.
+	var keyAggOpts []musig2.KeyAggOption
+	switch {
+	case tweaks.TaprootBIP0086Tweak:
+		keyAggOpts = append(keyAggOpts, musig2.WithBIP86KeyTweak())
+	case len(tweaks.TaprootTweak) > 0:
+		keyAggOpts = append(keyAggOpts, musig2.WithTaprootKeyTweak(
+			tweaks.TaprootTweak,
+		))
+	case len(tweaks.GenericTweaks) > 0:
+		keyAggOpts = append(keyAggOpts, musig2.WithKeyTweaks(
+			tweaks.GenericTweaks...,
+		))
+	}
+
+	// Then we'll use this information to compute the aggregated public key.
+	combinedKey, _, _, err := musig2.AggregateKeys(
+		allSignerPubKeys, true, keyAggOpts...,
+	)
+	return combinedKey, err
+}
+
+// NewMuSig2SessionID returns the unique ID of a MuSig2 session by using the
+// combined key and the local public nonces and hashing that data.
+func NewMuSig2SessionID(combinedKey *btcec.PublicKey,
+	publicNonces [musig2.PubNonceSize]byte) MuSig2SessionID {
+
+	// We hash the data to save some bytes in memory.
+	hash := sha256.New()
+	_, _ = hash.Write(combinedKey.SerializeCompressed())
+	_, _ = hash.Write(publicNonces[:])
+
+	id := MuSig2SessionID{}
+	copy(id[:], hash.Sum(nil))
+	return id
+}
+
+// SerializePartialSignature encodes the partial signature to a fixed size byte
+// array.
+func SerializePartialSignature(
+	sig *musig2.PartialSignature) ([MuSig2PartialSigSize]byte, error) {
+
+	var (
+		buf    bytes.Buffer
+		result [MuSig2PartialSigSize]byte
+	)
+	if err := sig.Encode(&buf); err != nil {
+		return result, fmt.Errorf("error encoding partial signature: "+
+			"%v", err)
+	}
+
+	if buf.Len() != MuSig2PartialSigSize {
+		return result, fmt.Errorf("invalid partial signature length, "+
+			"got %d wanted %d", buf.Len(), MuSig2PartialSigSize)
+	}
+
+	copy(result[:], buf.Bytes())
+
+	return result, nil
+}
+
+// DeserializePartialSignature decodes a partial signature from a byte slice.
+func DeserializePartialSignature(scalarBytes []byte) (*musig2.PartialSignature,
+	error) {
+
+	if len(scalarBytes) != MuSig2PartialSigSize {
+		return nil, fmt.Errorf("invalid partial signature length, got "+
+			"%d wanted %d", len(scalarBytes), MuSig2PartialSigSize)
+	}
+
+	sig := &musig2.PartialSignature{}
+	if err := sig.Decode(bytes.NewReader(scalarBytes)); err != nil {
+		return nil, fmt.Errorf("error decoding partial signature: %v",
+			err)
+	}
+
+	return sig, nil
+}
diff --git a/input/signer.go b/input/signer.go
index 5d6f4257b..fb3609115 100644
--- a/input/signer.go
+++ b/input/signer.go
@@ -10,6 +10,11 @@ import (
 // Signer implementations such as hardware wallets, hardware tokens, HSM's, or
 // simply a regular wallet.
 type Signer interface {
+	// MuSig2Signer is an embedded interface to make sure all our signers
+	// also support MuSig2 signing, so we can forward calls to a remote
+	// signer as well.
+	MuSig2Signer
+
 	// SignOutputRaw generates a signature for the passed transaction
 	// according to the data within the passed SignDescriptor.
 	//
diff --git a/input/taproot.go b/input/taproot.go
index 56a2c1a83..255fba427 100644
--- a/input/taproot.go
+++ b/input/taproot.go
@@ -75,12 +75,12 @@ func TapscriptFullTree(internalKey *btcec.PublicKey,
 // key and revealed script.
 func TapscriptPartialReveal(internalKey *btcec.PublicKey,
 	revealedLeaf txscript.TapLeaf,
-	inclusionProof [32]byte) *waddrmgr.Tapscript {
+	inclusionProof []byte) *waddrmgr.Tapscript {
 
 	controlBlock := &txscript.ControlBlock{
 		InternalKey:    internalKey,
 		LeafVersion:    txscript.BaseLeafVersion,
-		InclusionProof: inclusionProof[:],
+		InclusionProof: inclusionProof,
 	}
 	rootHash := controlBlock.RootHash(revealedLeaf.Script)
 	tapKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash)
diff --git a/lnrpc/signrpc/signer.pb.go b/lnrpc/signrpc/signer.pb.go
index 2d12035f4..fec74f3f7 100644
--- a/lnrpc/signrpc/signer.pb.go
+++ b/lnrpc/signrpc/signer.pb.go
@@ -931,6 +931,818 @@ func (x *SharedKeyResponse) GetSharedKey() []byte {
 	return nil
 }
 
+type TweakDesc struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//Tweak is the 32-byte value that will modify the public key.
+	Tweak []byte `protobuf:"bytes,1,opt,name=tweak,proto3" json:"tweak,omitempty"`
+	//
+	//Specifies if the target key should be converted to an x-only public key
+	//before tweaking. If true, then the public key will be mapped to an x-only
+	//key before the tweaking operation is applied.
+	IsXOnly bool `protobuf:"varint,2,opt,name=is_x_only,json=isXOnly,proto3" json:"is_x_only,omitempty"`
+}
+
+func (x *TweakDesc) Reset() {
+	*x = TweakDesc{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[14]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *TweakDesc) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TweakDesc) ProtoMessage() {}
+
+func (x *TweakDesc) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[14]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use TweakDesc.ProtoReflect.Descriptor instead.
+func (*TweakDesc) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{14}
+}
+
+func (x *TweakDesc) GetTweak() []byte {
+	if x != nil {
+		return x.Tweak
+	}
+	return nil
+}
+
+func (x *TweakDesc) GetIsXOnly() bool {
+	if x != nil {
+		return x.IsXOnly
+	}
+	return false
+}
+
+type TaprootTweakDesc struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//The root hash of the tapscript tree if a script path is committed to. If
+	//the MuSig2 key put on chain doesn't also commit to a script path (BIP-0086
+	//key spend only), then this needs to be empty and the key_spend_only field
+	//below must be set to true. This is required because gRPC cannot
+	//differentiate between a zero-size byte slice and a nil byte slice (both
+	//would be serialized the same way). So the extra boolean is required.
+	ScriptRoot []byte `protobuf:"bytes,1,opt,name=script_root,json=scriptRoot,proto3" json:"script_root,omitempty"`
+	//
+	//Indicates that the above script_root is expected to be empty because this
+	//is a BIP-0086 key spend only commitment where only the internal key is
+	//committed to instead of also including a script root hash.
+	KeySpendOnly bool `protobuf:"varint,2,opt,name=key_spend_only,json=keySpendOnly,proto3" json:"key_spend_only,omitempty"`
+}
+
+func (x *TaprootTweakDesc) Reset() {
+	*x = TaprootTweakDesc{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[15]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *TaprootTweakDesc) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*TaprootTweakDesc) ProtoMessage() {}
+
+func (x *TaprootTweakDesc) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[15]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use TaprootTweakDesc.ProtoReflect.Descriptor instead.
+func (*TaprootTweakDesc) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{15}
+}
+
+func (x *TaprootTweakDesc) GetScriptRoot() []byte {
+	if x != nil {
+		return x.ScriptRoot
+	}
+	return nil
+}
+
+func (x *TaprootTweakDesc) GetKeySpendOnly() bool {
+	if x != nil {
+		return x.KeySpendOnly
+	}
+	return false
+}
+
+type MuSig2CombineKeysRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//A list of all public keys (serialized in 32-byte x-only format!)
+	//participating in the signing session. The list will always be sorted
+	//lexicographically internally. This must include the local key which is
+	//described by the above key_loc.
+	AllSignerPubkeys [][]byte `protobuf:"bytes,1,rep,name=all_signer_pubkeys,json=allSignerPubkeys,proto3" json:"all_signer_pubkeys,omitempty"`
+	//
+	//A series of optional generic tweaks to be applied to the the aggregated
+	//public key.
+	Tweaks []*TweakDesc `protobuf:"bytes,2,rep,name=tweaks,proto3" json:"tweaks,omitempty"`
+	//
+	//An optional taproot specific tweak that must be specified if the MuSig2
+	//combined key will be used as the main taproot key of a taproot output
+	//on-chain.
+	TaprootTweak *TaprootTweakDesc `protobuf:"bytes,3,opt,name=taproot_tweak,json=taprootTweak,proto3" json:"taproot_tweak,omitempty"`
+}
+
+func (x *MuSig2CombineKeysRequest) Reset() {
+	*x = MuSig2CombineKeysRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[16]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MuSig2CombineKeysRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MuSig2CombineKeysRequest) ProtoMessage() {}
+
+func (x *MuSig2CombineKeysRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[16]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MuSig2CombineKeysRequest.ProtoReflect.Descriptor instead.
+func (*MuSig2CombineKeysRequest) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{16}
+}
+
+func (x *MuSig2CombineKeysRequest) GetAllSignerPubkeys() [][]byte {
+	if x != nil {
+		return x.AllSignerPubkeys
+	}
+	return nil
+}
+
+func (x *MuSig2CombineKeysRequest) GetTweaks() []*TweakDesc {
+	if x != nil {
+		return x.Tweaks
+	}
+	return nil
+}
+
+func (x *MuSig2CombineKeysRequest) GetTaprootTweak() *TaprootTweakDesc {
+	if x != nil {
+		return x.TaprootTweak
+	}
+	return nil
+}
+
+type MuSig2CombineKeysResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//The combined public key (in the 32-byte x-only format) with all tweaks
+	//applied to it. If a taproot tweak is specified, this corresponds to the
+	//taproot key that can be put into the on-chain output.
+	CombinedKey []byte `protobuf:"bytes,1,opt,name=combined_key,json=combinedKey,proto3" json:"combined_key,omitempty"`
+	//
+	//The raw combined public key (in the 32-byte x-only format) before any tweaks
+	//are applied to it. If a taproot tweak is specified, this corresponds to the
+	//internal key that needs to be put into the witness if the script spend path
+	//is used.
+	TaprootInternalKey []byte `protobuf:"bytes,2,opt,name=taproot_internal_key,json=taprootInternalKey,proto3" json:"taproot_internal_key,omitempty"`
+}
+
+func (x *MuSig2CombineKeysResponse) Reset() {
+	*x = MuSig2CombineKeysResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[17]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MuSig2CombineKeysResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MuSig2CombineKeysResponse) ProtoMessage() {}
+
+func (x *MuSig2CombineKeysResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[17]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MuSig2CombineKeysResponse.ProtoReflect.Descriptor instead.
+func (*MuSig2CombineKeysResponse) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{17}
+}
+
+func (x *MuSig2CombineKeysResponse) GetCombinedKey() []byte {
+	if x != nil {
+		return x.CombinedKey
+	}
+	return nil
+}
+
+func (x *MuSig2CombineKeysResponse) GetTaprootInternalKey() []byte {
+	if x != nil {
+		return x.TaprootInternalKey
+	}
+	return nil
+}
+
+type MuSig2SessionRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//The key locator that identifies which key to use for signing.
+	KeyLoc *KeyLocator `protobuf:"bytes,1,opt,name=key_loc,json=keyLoc,proto3" json:"key_loc,omitempty"`
+	//
+	//A list of all public keys (serialized in 32-byte x-only format!)
+	//participating in the signing session. The list will always be sorted
+	//lexicographically internally. This must include the local key which is
+	//described by the above key_loc.
+	AllSignerPubkeys [][]byte `protobuf:"bytes,2,rep,name=all_signer_pubkeys,json=allSignerPubkeys,proto3" json:"all_signer_pubkeys,omitempty"`
+	//
+	//An optional list of all public nonces of other signing participants that
+	//might already be known.
+	OtherSignerPublicNonces [][]byte `protobuf:"bytes,3,rep,name=other_signer_public_nonces,json=otherSignerPublicNonces,proto3" json:"other_signer_public_nonces,omitempty"`
+	//
+	//A series of optional generic tweaks to be applied to the the aggregated
+	//public key.
+	Tweaks []*TweakDesc `protobuf:"bytes,4,rep,name=tweaks,proto3" json:"tweaks,omitempty"`
+	//
+	//An optional taproot specific tweak that must be specified if the MuSig2
+	//combined key will be used as the main taproot key of a taproot output
+	//on-chain.
+	TaprootTweak *TaprootTweakDesc `protobuf:"bytes,5,opt,name=taproot_tweak,json=taprootTweak,proto3" json:"taproot_tweak,omitempty"`
+}
+
+func (x *MuSig2SessionRequest) Reset() {
+	*x = MuSig2SessionRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[18]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MuSig2SessionRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MuSig2SessionRequest) ProtoMessage() {}
+
+func (x *MuSig2SessionRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[18]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MuSig2SessionRequest.ProtoReflect.Descriptor instead.
+func (*MuSig2SessionRequest) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{18}
+}
+
+func (x *MuSig2SessionRequest) GetKeyLoc() *KeyLocator {
+	if x != nil {
+		return x.KeyLoc
+	}
+	return nil
+}
+
+func (x *MuSig2SessionRequest) GetAllSignerPubkeys() [][]byte {
+	if x != nil {
+		return x.AllSignerPubkeys
+	}
+	return nil
+}
+
+func (x *MuSig2SessionRequest) GetOtherSignerPublicNonces() [][]byte {
+	if x != nil {
+		return x.OtherSignerPublicNonces
+	}
+	return nil
+}
+
+func (x *MuSig2SessionRequest) GetTweaks() []*TweakDesc {
+	if x != nil {
+		return x.Tweaks
+	}
+	return nil
+}
+
+func (x *MuSig2SessionRequest) GetTaprootTweak() *TaprootTweakDesc {
+	if x != nil {
+		return x.TaprootTweak
+	}
+	return nil
+}
+
+type MuSig2SessionResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//The unique ID that represents this signing session. A session can be used
+	//for producing a signature a single time. If the signing fails for any
+	//reason, a new session with the same participants needs to be created.
+	SessionId []byte `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
+	//
+	//The combined public key (in the 32-byte x-only format) with all tweaks
+	//applied to it. If a taproot tweak is specified, this corresponds to the
+	//taproot key that can be put into the on-chain output.
+	CombinedKey []byte `protobuf:"bytes,2,opt,name=combined_key,json=combinedKey,proto3" json:"combined_key,omitempty"`
+	//
+	//The raw combined public key (in the 32-byte x-only format) before any tweaks
+	//are applied to it. If a taproot tweak is specified, this corresponds to the
+	//internal key that needs to be put into the witness if the script spend path
+	//is used.
+	TaprootInternalKey []byte `protobuf:"bytes,3,opt,name=taproot_internal_key,json=taprootInternalKey,proto3" json:"taproot_internal_key,omitempty"`
+	//
+	//The two public nonces the local signer uses, combined into a single value
+	//of 66 bytes. Can be split into the two 33-byte points to get the individual
+	//nonces.
+	LocalPublicNonces []byte `protobuf:"bytes,4,opt,name=local_public_nonces,json=localPublicNonces,proto3" json:"local_public_nonces,omitempty"`
+	//
+	//Indicates whether all nonces required to start the signing process are known
+	//now.
+	HaveAllNonces bool `protobuf:"varint,5,opt,name=have_all_nonces,json=haveAllNonces,proto3" json:"have_all_nonces,omitempty"`
+}
+
+func (x *MuSig2SessionResponse) Reset() {
+	*x = MuSig2SessionResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[19]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MuSig2SessionResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MuSig2SessionResponse) ProtoMessage() {}
+
+func (x *MuSig2SessionResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[19]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MuSig2SessionResponse.ProtoReflect.Descriptor instead.
+func (*MuSig2SessionResponse) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{19}
+}
+
+func (x *MuSig2SessionResponse) GetSessionId() []byte {
+	if x != nil {
+		return x.SessionId
+	}
+	return nil
+}
+
+func (x *MuSig2SessionResponse) GetCombinedKey() []byte {
+	if x != nil {
+		return x.CombinedKey
+	}
+	return nil
+}
+
+func (x *MuSig2SessionResponse) GetTaprootInternalKey() []byte {
+	if x != nil {
+		return x.TaprootInternalKey
+	}
+	return nil
+}
+
+func (x *MuSig2SessionResponse) GetLocalPublicNonces() []byte {
+	if x != nil {
+		return x.LocalPublicNonces
+	}
+	return nil
+}
+
+func (x *MuSig2SessionResponse) GetHaveAllNonces() bool {
+	if x != nil {
+		return x.HaveAllNonces
+	}
+	return false
+}
+
+type MuSig2RegisterNoncesRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//The unique ID of the signing session those nonces should be registered with.
+	SessionId []byte `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
+	//
+	//A list of all public nonces of other signing participants that should be
+	//registered.
+	OtherSignerPublicNonces [][]byte `protobuf:"bytes,3,rep,name=other_signer_public_nonces,json=otherSignerPublicNonces,proto3" json:"other_signer_public_nonces,omitempty"`
+}
+
+func (x *MuSig2RegisterNoncesRequest) Reset() {
+	*x = MuSig2RegisterNoncesRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[20]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MuSig2RegisterNoncesRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MuSig2RegisterNoncesRequest) ProtoMessage() {}
+
+func (x *MuSig2RegisterNoncesRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[20]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MuSig2RegisterNoncesRequest.ProtoReflect.Descriptor instead.
+func (*MuSig2RegisterNoncesRequest) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{20}
+}
+
+func (x *MuSig2RegisterNoncesRequest) GetSessionId() []byte {
+	if x != nil {
+		return x.SessionId
+	}
+	return nil
+}
+
+func (x *MuSig2RegisterNoncesRequest) GetOtherSignerPublicNonces() [][]byte {
+	if x != nil {
+		return x.OtherSignerPublicNonces
+	}
+	return nil
+}
+
+type MuSig2RegisterNoncesResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//Indicates whether all nonces required to start the signing process are known
+	//now.
+	HaveAllNonces bool `protobuf:"varint,1,opt,name=have_all_nonces,json=haveAllNonces,proto3" json:"have_all_nonces,omitempty"`
+}
+
+func (x *MuSig2RegisterNoncesResponse) Reset() {
+	*x = MuSig2RegisterNoncesResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[21]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MuSig2RegisterNoncesResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MuSig2RegisterNoncesResponse) ProtoMessage() {}
+
+func (x *MuSig2RegisterNoncesResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[21]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MuSig2RegisterNoncesResponse.ProtoReflect.Descriptor instead.
+func (*MuSig2RegisterNoncesResponse) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{21}
+}
+
+func (x *MuSig2RegisterNoncesResponse) GetHaveAllNonces() bool {
+	if x != nil {
+		return x.HaveAllNonces
+	}
+	return false
+}
+
+type MuSig2SignRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//The unique ID of the signing session to use for signing.
+	SessionId []byte `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
+	//
+	//The 32-byte SHA256 digest of the message to sign.
+	MessageDigest []byte `protobuf:"bytes,2,opt,name=message_digest,json=messageDigest,proto3" json:"message_digest,omitempty"`
+	//
+	//Cleanup indicates that after signing, the session state can be cleaned up,
+	//since another participant is going to be responsible for combining the
+	//partial signatures.
+	Cleanup bool `protobuf:"varint,3,opt,name=cleanup,proto3" json:"cleanup,omitempty"`
+}
+
+func (x *MuSig2SignRequest) Reset() {
+	*x = MuSig2SignRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[22]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MuSig2SignRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MuSig2SignRequest) ProtoMessage() {}
+
+func (x *MuSig2SignRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[22]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MuSig2SignRequest.ProtoReflect.Descriptor instead.
+func (*MuSig2SignRequest) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{22}
+}
+
+func (x *MuSig2SignRequest) GetSessionId() []byte {
+	if x != nil {
+		return x.SessionId
+	}
+	return nil
+}
+
+func (x *MuSig2SignRequest) GetMessageDigest() []byte {
+	if x != nil {
+		return x.MessageDigest
+	}
+	return nil
+}
+
+func (x *MuSig2SignRequest) GetCleanup() bool {
+	if x != nil {
+		return x.Cleanup
+	}
+	return false
+}
+
+type MuSig2SignResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//The partial signature created by the local signer.
+	LocalPartialSignature []byte `protobuf:"bytes,1,opt,name=local_partial_signature,json=localPartialSignature,proto3" json:"local_partial_signature,omitempty"`
+}
+
+func (x *MuSig2SignResponse) Reset() {
+	*x = MuSig2SignResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[23]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MuSig2SignResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MuSig2SignResponse) ProtoMessage() {}
+
+func (x *MuSig2SignResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[23]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MuSig2SignResponse.ProtoReflect.Descriptor instead.
+func (*MuSig2SignResponse) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{23}
+}
+
+func (x *MuSig2SignResponse) GetLocalPartialSignature() []byte {
+	if x != nil {
+		return x.LocalPartialSignature
+	}
+	return nil
+}
+
+type MuSig2CombineSigRequest struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//The unique ID of the signing session to combine the signatures for.
+	SessionId []byte `protobuf:"bytes,1,opt,name=session_id,json=sessionId,proto3" json:"session_id,omitempty"`
+	//
+	//The list of all other participants' partial signatures to add to the current
+	//session.
+	OtherPartialSignatures [][]byte `protobuf:"bytes,2,rep,name=other_partial_signatures,json=otherPartialSignatures,proto3" json:"other_partial_signatures,omitempty"`
+}
+
+func (x *MuSig2CombineSigRequest) Reset() {
+	*x = MuSig2CombineSigRequest{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[24]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MuSig2CombineSigRequest) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MuSig2CombineSigRequest) ProtoMessage() {}
+
+func (x *MuSig2CombineSigRequest) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[24]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MuSig2CombineSigRequest.ProtoReflect.Descriptor instead.
+func (*MuSig2CombineSigRequest) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{24}
+}
+
+func (x *MuSig2CombineSigRequest) GetSessionId() []byte {
+	if x != nil {
+		return x.SessionId
+	}
+	return nil
+}
+
+func (x *MuSig2CombineSigRequest) GetOtherPartialSignatures() [][]byte {
+	if x != nil {
+		return x.OtherPartialSignatures
+	}
+	return nil
+}
+
+type MuSig2CombineSigResponse struct {
+	state         protoimpl.MessageState
+	sizeCache     protoimpl.SizeCache
+	unknownFields protoimpl.UnknownFields
+
+	//
+	//Indicates whether all partial signatures required to create a final, full
+	//signature are known yet. If this is true, then the final_signature field is
+	//set, otherwise it is empty.
+	HaveAllSignatures bool `protobuf:"varint,1,opt,name=have_all_signatures,json=haveAllSignatures,proto3" json:"have_all_signatures,omitempty"`
+	//
+	//The final, full signature that is valid for the combined public key.
+	FinalSignature []byte `protobuf:"bytes,2,opt,name=final_signature,json=finalSignature,proto3" json:"final_signature,omitempty"`
+}
+
+func (x *MuSig2CombineSigResponse) Reset() {
+	*x = MuSig2CombineSigResponse{}
+	if protoimpl.UnsafeEnabled {
+		mi := &file_signrpc_signer_proto_msgTypes[25]
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		ms.StoreMessageInfo(mi)
+	}
+}
+
+func (x *MuSig2CombineSigResponse) String() string {
+	return protoimpl.X.MessageStringOf(x)
+}
+
+func (*MuSig2CombineSigResponse) ProtoMessage() {}
+
+func (x *MuSig2CombineSigResponse) ProtoReflect() protoreflect.Message {
+	mi := &file_signrpc_signer_proto_msgTypes[25]
+	if protoimpl.UnsafeEnabled && x != nil {
+		ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
+		if ms.LoadMessageInfo() == nil {
+			ms.StoreMessageInfo(mi)
+		}
+		return ms
+	}
+	return mi.MessageOf(x)
+}
+
+// Deprecated: Use MuSig2CombineSigResponse.ProtoReflect.Descriptor instead.
+func (*MuSig2CombineSigResponse) Descriptor() ([]byte, []int) {
+	return file_signrpc_signer_proto_rawDescGZIP(), []int{25}
+}
+
+func (x *MuSig2CombineSigResponse) GetHaveAllSignatures() bool {
+	if x != nil {
+		return x.HaveAllSignatures
+	}
+	return false
+}
+
+func (x *MuSig2CombineSigResponse) GetFinalSignature() []byte {
+	if x != nil {
+		return x.FinalSignature
+	}
+	return nil
+}
+
 var File_signrpc_signer_proto protoreflect.FileDescriptor
 
 var file_signrpc_signer_proto_rawDesc = []byte{
@@ -1026,32 +1838,159 @@ var file_signrpc_signer_proto_rawDesc = []byte{
 	0x73, 0x63, 0x22, 0x32, 0x0a, 0x11, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52,
 	0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x68, 0x61, 0x72, 0x65,
 	0x64, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x68, 0x61,
-	0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x32, 0xd4, 0x02, 0x0a, 0x06, 0x53, 0x69, 0x67, 0x6e, 0x65,
-	0x72, 0x12, 0x34, 0x0a, 0x0d, 0x53, 0x69, 0x67, 0x6e, 0x4f, 0x75, 0x74, 0x70, 0x75, 0x74, 0x52,
-	0x61, 0x77, 0x12, 0x10, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67,
-	0x6e, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53,
-	0x69, 0x67, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x12, 0x40, 0x0a, 0x12, 0x43, 0x6f, 0x6d, 0x70, 0x75,
-	0x74, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x12, 0x10, 0x2e,
-	0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x52, 0x65, 0x71, 0x1a,
-	0x18, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x53,
-	0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12, 0x40, 0x0a, 0x0b, 0x53, 0x69, 0x67,
-	0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x17, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72,
-	0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65,
-	0x71, 0x1a, 0x18, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e,
-	0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x46, 0x0a, 0x0d, 0x56,
-	0x65, 0x72, 0x69, 0x66, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x19, 0x2e, 0x73,
-	0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x4d, 0x65, 0x73,
-	0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70,
-	0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52,
-	0x65, 0x73, 0x70, 0x12, 0x48, 0x0a, 0x0f, 0x44, 0x65, 0x72, 0x69, 0x76, 0x65, 0x53, 0x68, 0x61,
-	0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63,
-	0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73,
-	0x74, 0x1a, 0x1a, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x68, 0x61, 0x72,
-	0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2f, 0x5a,
-	0x2d, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68,
-	0x74, 0x6e, 0x69, 0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64,
-	0x2f, 0x6c, 0x6e, 0x72, 0x70, 0x63, 0x2f, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x62, 0x06,
-	0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33,
+	0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x22, 0x3d, 0x0a, 0x09, 0x54, 0x77, 0x65, 0x61, 0x6b, 0x44,
+	0x65, 0x73, 0x63, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x77, 0x65, 0x61, 0x6b, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0c, 0x52, 0x05, 0x74, 0x77, 0x65, 0x61, 0x6b, 0x12, 0x1a, 0x0a, 0x09, 0x69, 0x73, 0x5f,
+	0x78, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x69, 0x73,
+	0x58, 0x4f, 0x6e, 0x6c, 0x79, 0x22, 0x59, 0x0a, 0x10, 0x54, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74,
+	0x54, 0x77, 0x65, 0x61, 0x6b, 0x44, 0x65, 0x73, 0x63, 0x12, 0x1f, 0x0a, 0x0b, 0x73, 0x63, 0x72,
+	0x69, 0x70, 0x74, 0x5f, 0x72, 0x6f, 0x6f, 0x74, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0a,
+	0x73, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x6f, 0x6f, 0x74, 0x12, 0x24, 0x0a, 0x0e, 0x6b, 0x65,
+	0x79, 0x5f, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x5f, 0x6f, 0x6e, 0x6c, 0x79, 0x18, 0x02, 0x20, 0x01,
+	0x28, 0x08, 0x52, 0x0c, 0x6b, 0x65, 0x79, 0x53, 0x70, 0x65, 0x6e, 0x64, 0x4f, 0x6e, 0x6c, 0x79,
+	0x22, 0xb4, 0x01, 0x0a, 0x18, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x43, 0x6f, 0x6d, 0x62, 0x69,
+	0x6e, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x2c, 0x0a,
+	0x12, 0x61, 0x6c, 0x6c, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6b,
+	0x65, 0x79, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x10, 0x61, 0x6c, 0x6c, 0x53, 0x69,
+	0x67, 0x6e, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x74,
+	0x77, 0x65, 0x61, 0x6b, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x73, 0x69,
+	0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x77, 0x65, 0x61, 0x6b, 0x44, 0x65, 0x73, 0x63, 0x52,
+	0x06, 0x74, 0x77, 0x65, 0x61, 0x6b, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x74, 0x61, 0x70, 0x72, 0x6f,
+	0x6f, 0x74, 0x5f, 0x74, 0x77, 0x65, 0x61, 0x6b, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19,
+	0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74,
+	0x54, 0x77, 0x65, 0x61, 0x6b, 0x44, 0x65, 0x73, 0x63, 0x52, 0x0c, 0x74, 0x61, 0x70, 0x72, 0x6f,
+	0x6f, 0x74, 0x54, 0x77, 0x65, 0x61, 0x6b, 0x22, 0x70, 0x0a, 0x19, 0x4d, 0x75, 0x53, 0x69, 0x67,
+	0x32, 0x43, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70,
+	0x6f, 0x6e, 0x73, 0x65, 0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x65, 0x64,
+	0x5f, 0x6b, 0x65, 0x79, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x62,
+	0x69, 0x6e, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x74, 0x61, 0x70, 0x72, 0x6f,
+	0x6f, 0x74, 0x5f, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x6b, 0x65, 0x79, 0x18,
+	0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x12, 0x74, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x49, 0x6e,
+	0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x4b, 0x65, 0x79, 0x22, 0x9b, 0x02, 0x0a, 0x14, 0x4d, 0x75,
+	0x53, 0x69, 0x67, 0x32, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65,
+	0x73, 0x74, 0x12, 0x2c, 0x0a, 0x07, 0x6b, 0x65, 0x79, 0x5f, 0x6c, 0x6f, 0x63, 0x18, 0x01, 0x20,
+	0x01, 0x28, 0x0b, 0x32, 0x13, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4b, 0x65,
+	0x79, 0x4c, 0x6f, 0x63, 0x61, 0x74, 0x6f, 0x72, 0x52, 0x06, 0x6b, 0x65, 0x79, 0x4c, 0x6f, 0x63,
+	0x12, 0x2c, 0x0a, 0x12, 0x61, 0x6c, 0x6c, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x5f, 0x70,
+	0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52, 0x10, 0x61, 0x6c,
+	0x6c, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6b, 0x65, 0x79, 0x73, 0x12, 0x3b,
+	0x0a, 0x1a, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x5f, 0x70,
+	0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03,
+	0x28, 0x0c, 0x52, 0x17, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x50,
+	0x75, 0x62, 0x6c, 0x69, 0x63, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x2a, 0x0a, 0x06, 0x74,
+	0x77, 0x65, 0x61, 0x6b, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x12, 0x2e, 0x73, 0x69,
+	0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x77, 0x65, 0x61, 0x6b, 0x44, 0x65, 0x73, 0x63, 0x52,
+	0x06, 0x74, 0x77, 0x65, 0x61, 0x6b, 0x73, 0x12, 0x3e, 0x0a, 0x0d, 0x74, 0x61, 0x70, 0x72, 0x6f,
+	0x6f, 0x74, 0x5f, 0x74, 0x77, 0x65, 0x61, 0x6b, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19,
+	0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x54, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74,
+	0x54, 0x77, 0x65, 0x61, 0x6b, 0x44, 0x65, 0x73, 0x63, 0x52, 0x0c, 0x74, 0x61, 0x70, 0x72, 0x6f,
+	0x6f, 0x74, 0x54, 0x77, 0x65, 0x61, 0x6b, 0x22, 0xe3, 0x01, 0x0a, 0x15, 0x4d, 0x75, 0x53, 0x69,
+	0x67, 0x32, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18,
+	0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64,
+	0x12, 0x21, 0x0a, 0x0c, 0x63, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x65, 0x64, 0x5f, 0x6b, 0x65, 0x79,
+	0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0b, 0x63, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x65, 0x64,
+	0x4b, 0x65, 0x79, 0x12, 0x30, 0x0a, 0x14, 0x74, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x5f, 0x69,
+	0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28,
+	0x0c, 0x52, 0x12, 0x74, 0x61, 0x70, 0x72, 0x6f, 0x6f, 0x74, 0x49, 0x6e, 0x74, 0x65, 0x72, 0x6e,
+	0x61, 0x6c, 0x4b, 0x65, 0x79, 0x12, 0x2e, 0x0a, 0x13, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x70,
+	0x75, 0x62, 0x6c, 0x69, 0x63, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x04, 0x20, 0x01,
+	0x28, 0x0c, 0x52, 0x11, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4e,
+	0x6f, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x26, 0x0a, 0x0f, 0x68, 0x61, 0x76, 0x65, 0x5f, 0x61, 0x6c,
+	0x6c, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x05, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0d,
+	0x68, 0x61, 0x76, 0x65, 0x41, 0x6c, 0x6c, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x79, 0x0a,
+	0x1b, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e,
+	0x6f, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a,
+	0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c,
+	0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x3b, 0x0a, 0x1a, 0x6f,
+	0x74, 0x68, 0x65, 0x72, 0x5f, 0x73, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x5f, 0x70, 0x75, 0x62, 0x6c,
+	0x69, 0x63, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x03, 0x20, 0x03, 0x28, 0x0c, 0x52,
+	0x17, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x50, 0x75, 0x62, 0x6c,
+	0x69, 0x63, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x22, 0x46, 0x0a, 0x1c, 0x4d, 0x75, 0x53, 0x69,
+	0x67, 0x32, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73,
+	0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x26, 0x0a, 0x0f, 0x68, 0x61, 0x76, 0x65,
+	0x5f, 0x61, 0x6c, 0x6c, 0x5f, 0x6e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28,
+	0x08, 0x52, 0x0d, 0x68, 0x61, 0x76, 0x65, 0x41, 0x6c, 0x6c, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73,
+	0x22, 0x73, 0x0a, 0x11, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d, 0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e,
+	0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69,
+	0x6f, 0x6e, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x5f,
+	0x64, 0x69, 0x67, 0x65, 0x73, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0d, 0x6d, 0x65,
+	0x73, 0x73, 0x61, 0x67, 0x65, 0x44, 0x69, 0x67, 0x65, 0x73, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x63,
+	0x6c, 0x65, 0x61, 0x6e, 0x75, 0x70, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x07, 0x63, 0x6c,
+	0x65, 0x61, 0x6e, 0x75, 0x70, 0x22, 0x4c, 0x0a, 0x12, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53,
+	0x69, 0x67, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x17, 0x6c,
+	0x6f, 0x63, 0x61, 0x6c, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x67,
+	0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x15, 0x6c, 0x6f,
+	0x63, 0x61, 0x6c, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74,
+	0x75, 0x72, 0x65, 0x22, 0x72, 0x0a, 0x17, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x43, 0x6f, 0x6d,
+	0x62, 0x69, 0x6e, 0x65, 0x53, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1d,
+	0x0a, 0x0a, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01,
+	0x28, 0x0c, 0x52, 0x09, 0x73, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x38, 0x0a,
+	0x18, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x5f, 0x70, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x5f, 0x73,
+	0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x02, 0x20, 0x03, 0x28, 0x0c, 0x52,
+	0x16, 0x6f, 0x74, 0x68, 0x65, 0x72, 0x50, 0x61, 0x72, 0x74, 0x69, 0x61, 0x6c, 0x53, 0x69, 0x67,
+	0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x22, 0x73, 0x0a, 0x18, 0x4d, 0x75, 0x53, 0x69, 0x67,
+	0x32, 0x43, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x65, 0x53, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x12, 0x2e, 0x0a, 0x13, 0x68, 0x61, 0x76, 0x65, 0x5f, 0x61, 0x6c, 0x6c, 0x5f,
+	0x73, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x08,
+	0x52, 0x11, 0x68, 0x61, 0x76, 0x65, 0x41, 0x6c, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75,
+	0x72, 0x65, 0x73, 0x12, 0x27, 0x0a, 0x0f, 0x66, 0x69, 0x6e, 0x61, 0x6c, 0x5f, 0x73, 0x69, 0x67,
+	0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0e, 0x66, 0x69,
+	0x6e, 0x61, 0x6c, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, 0x32, 0x8b, 0x06, 0x0a,
+	0x06, 0x53, 0x69, 0x67, 0x6e, 0x65, 0x72, 0x12, 0x34, 0x0a, 0x0d, 0x53, 0x69, 0x67, 0x6e, 0x4f,
+	0x75, 0x74, 0x70, 0x75, 0x74, 0x52, 0x61, 0x77, 0x12, 0x10, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72,
+	0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x11, 0x2e, 0x73, 0x69, 0x67,
+	0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x12, 0x40, 0x0a,
+	0x12, 0x43, 0x6f, 0x6d, 0x70, 0x75, 0x74, 0x65, 0x49, 0x6e, 0x70, 0x75, 0x74, 0x53, 0x63, 0x72,
+	0x69, 0x70, 0x74, 0x12, 0x10, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69,
+	0x67, 0x6e, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e,
+	0x49, 0x6e, 0x70, 0x75, 0x74, 0x53, 0x63, 0x72, 0x69, 0x70, 0x74, 0x52, 0x65, 0x73, 0x70, 0x12,
+	0x40, 0x0a, 0x0b, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x17,
+	0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73,
+	0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x18, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70,
+	0x63, 0x2e, 0x53, 0x69, 0x67, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73,
+	0x70, 0x12, 0x46, 0x0a, 0x0d, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61,
+	0x67, 0x65, 0x12, 0x19, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72,
+	0x69, 0x66, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x71, 0x1a, 0x1a, 0x2e,
+	0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x56, 0x65, 0x72, 0x69, 0x66, 0x79, 0x4d, 0x65,
+	0x73, 0x73, 0x61, 0x67, 0x65, 0x52, 0x65, 0x73, 0x70, 0x12, 0x48, 0x0a, 0x0f, 0x44, 0x65, 0x72,
+	0x69, 0x76, 0x65, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x12, 0x19, 0x2e, 0x73,
+	0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1a, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70,
+	0x63, 0x2e, 0x53, 0x68, 0x61, 0x72, 0x65, 0x64, 0x4b, 0x65, 0x79, 0x52, 0x65, 0x73, 0x70, 0x6f,
+	0x6e, 0x73, 0x65, 0x12, 0x5a, 0x0a, 0x11, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x43, 0x6f, 0x6d,
+	0x62, 0x69, 0x6e, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x12, 0x21, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72,
+	0x70, 0x63, 0x2e, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x43, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x65,
+	0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x73, 0x69,
+	0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x43, 0x6f, 0x6d, 0x62,
+	0x69, 0x6e, 0x65, 0x4b, 0x65, 0x79, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12,
+	0x54, 0x0a, 0x13, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x53,
+	0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x1d, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63,
+	0x2e, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65,
+	0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x1e, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e,
+	0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x65, 0x73, 0x73, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73,
+	0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x63, 0x0a, 0x14, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x52,
+	0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x24, 0x2e,
+	0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x52, 0x65,
+	0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75,
+	0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4d, 0x75,
+	0x53, 0x69, 0x67, 0x32, 0x52, 0x65, 0x67, 0x69, 0x73, 0x74, 0x65, 0x72, 0x4e, 0x6f, 0x6e, 0x63,
+	0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x45, 0x0a, 0x0a, 0x4d, 0x75,
+	0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x12, 0x1a, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72,
+	0x70, 0x63, 0x2e, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x52, 0x65, 0x71,
+	0x75, 0x65, 0x73, 0x74, 0x1a, 0x1b, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e, 0x4d,
+	0x75, 0x53, 0x69, 0x67, 0x32, 0x53, 0x69, 0x67, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73,
+	0x65, 0x12, 0x57, 0x0a, 0x10, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x43, 0x6f, 0x6d, 0x62, 0x69,
+	0x6e, 0x65, 0x53, 0x69, 0x67, 0x12, 0x20, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x2e,
+	0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x43, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x65, 0x53, 0x69, 0x67,
+	0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x21, 0x2e, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70,
+	0x63, 0x2e, 0x4d, 0x75, 0x53, 0x69, 0x67, 0x32, 0x43, 0x6f, 0x6d, 0x62, 0x69, 0x6e, 0x65, 0x53,
+	0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x2f, 0x5a, 0x2d, 0x67, 0x69,
+	0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x6c, 0x69, 0x67, 0x68, 0x74, 0x6e, 0x69,
+	0x6e, 0x67, 0x6e, 0x65, 0x74, 0x77, 0x6f, 0x72, 0x6b, 0x2f, 0x6c, 0x6e, 0x64, 0x2f, 0x6c, 0x6e,
+	0x72, 0x70, 0x63, 0x2f, 0x73, 0x69, 0x67, 0x6e, 0x72, 0x70, 0x63, 0x62, 0x06, 0x70, 0x72, 0x6f,
+	0x74, 0x6f, 0x33,
 }
 
 var (
@@ -1066,22 +2005,34 @@ func file_signrpc_signer_proto_rawDescGZIP() []byte {
 	return file_signrpc_signer_proto_rawDescData
 }
 
-var file_signrpc_signer_proto_msgTypes = make([]protoimpl.MessageInfo, 14)
+var file_signrpc_signer_proto_msgTypes = make([]protoimpl.MessageInfo, 26)
 var file_signrpc_signer_proto_goTypes = []interface{}{
-	(*KeyLocator)(nil),        // 0: signrpc.KeyLocator
-	(*KeyDescriptor)(nil),     // 1: signrpc.KeyDescriptor
-	(*TxOut)(nil),             // 2: signrpc.TxOut
-	(*SignDescriptor)(nil),    // 3: signrpc.SignDescriptor
-	(*SignReq)(nil),           // 4: signrpc.SignReq
-	(*SignResp)(nil),          // 5: signrpc.SignResp
-	(*InputScript)(nil),       // 6: signrpc.InputScript
-	(*InputScriptResp)(nil),   // 7: signrpc.InputScriptResp
-	(*SignMessageReq)(nil),    // 8: signrpc.SignMessageReq
-	(*SignMessageResp)(nil),   // 9: signrpc.SignMessageResp
-	(*VerifyMessageReq)(nil),  // 10: signrpc.VerifyMessageReq
-	(*VerifyMessageResp)(nil), // 11: signrpc.VerifyMessageResp
-	(*SharedKeyRequest)(nil),  // 12: signrpc.SharedKeyRequest
-	(*SharedKeyResponse)(nil), // 13: signrpc.SharedKeyResponse
+	(*KeyLocator)(nil),                   // 0: signrpc.KeyLocator
+	(*KeyDescriptor)(nil),                // 1: signrpc.KeyDescriptor
+	(*TxOut)(nil),                        // 2: signrpc.TxOut
+	(*SignDescriptor)(nil),               // 3: signrpc.SignDescriptor
+	(*SignReq)(nil),                      // 4: signrpc.SignReq
+	(*SignResp)(nil),                     // 5: signrpc.SignResp
+	(*InputScript)(nil),                  // 6: signrpc.InputScript
+	(*InputScriptResp)(nil),              // 7: signrpc.InputScriptResp
+	(*SignMessageReq)(nil),               // 8: signrpc.SignMessageReq
+	(*SignMessageResp)(nil),              // 9: signrpc.SignMessageResp
+	(*VerifyMessageReq)(nil),             // 10: signrpc.VerifyMessageReq
+	(*VerifyMessageResp)(nil),            // 11: signrpc.VerifyMessageResp
+	(*SharedKeyRequest)(nil),             // 12: signrpc.SharedKeyRequest
+	(*SharedKeyResponse)(nil),            // 13: signrpc.SharedKeyResponse
+	(*TweakDesc)(nil),                    // 14: signrpc.TweakDesc
+	(*TaprootTweakDesc)(nil),             // 15: signrpc.TaprootTweakDesc
+	(*MuSig2CombineKeysRequest)(nil),     // 16: signrpc.MuSig2CombineKeysRequest
+	(*MuSig2CombineKeysResponse)(nil),    // 17: signrpc.MuSig2CombineKeysResponse
+	(*MuSig2SessionRequest)(nil),         // 18: signrpc.MuSig2SessionRequest
+	(*MuSig2SessionResponse)(nil),        // 19: signrpc.MuSig2SessionResponse
+	(*MuSig2RegisterNoncesRequest)(nil),  // 20: signrpc.MuSig2RegisterNoncesRequest
+	(*MuSig2RegisterNoncesResponse)(nil), // 21: signrpc.MuSig2RegisterNoncesResponse
+	(*MuSig2SignRequest)(nil),            // 22: signrpc.MuSig2SignRequest
+	(*MuSig2SignResponse)(nil),           // 23: signrpc.MuSig2SignResponse
+	(*MuSig2CombineSigRequest)(nil),      // 24: signrpc.MuSig2CombineSigRequest
+	(*MuSig2CombineSigResponse)(nil),     // 25: signrpc.MuSig2CombineSigResponse
 }
 var file_signrpc_signer_proto_depIdxs = []int32{
 	0,  // 0: signrpc.KeyDescriptor.key_loc:type_name -> signrpc.KeyLocator
@@ -1093,21 +2044,36 @@ var file_signrpc_signer_proto_depIdxs = []int32{
 	0,  // 6: signrpc.SignMessageReq.key_loc:type_name -> signrpc.KeyLocator
 	0,  // 7: signrpc.SharedKeyRequest.key_loc:type_name -> signrpc.KeyLocator
 	1,  // 8: signrpc.SharedKeyRequest.key_desc:type_name -> signrpc.KeyDescriptor
-	4,  // 9: signrpc.Signer.SignOutputRaw:input_type -> signrpc.SignReq
-	4,  // 10: signrpc.Signer.ComputeInputScript:input_type -> signrpc.SignReq
-	8,  // 11: signrpc.Signer.SignMessage:input_type -> signrpc.SignMessageReq
-	10, // 12: signrpc.Signer.VerifyMessage:input_type -> signrpc.VerifyMessageReq
-	12, // 13: signrpc.Signer.DeriveSharedKey:input_type -> signrpc.SharedKeyRequest
-	5,  // 14: signrpc.Signer.SignOutputRaw:output_type -> signrpc.SignResp
-	7,  // 15: signrpc.Signer.ComputeInputScript:output_type -> signrpc.InputScriptResp
-	9,  // 16: signrpc.Signer.SignMessage:output_type -> signrpc.SignMessageResp
-	11, // 17: signrpc.Signer.VerifyMessage:output_type -> signrpc.VerifyMessageResp
-	13, // 18: signrpc.Signer.DeriveSharedKey:output_type -> signrpc.SharedKeyResponse
-	14, // [14:19] is the sub-list for method output_type
-	9,  // [9:14] is the sub-list for method input_type
-	9,  // [9:9] is the sub-list for extension type_name
-	9,  // [9:9] is the sub-list for extension extendee
-	0,  // [0:9] is the sub-list for field type_name
+	14, // 9: signrpc.MuSig2CombineKeysRequest.tweaks:type_name -> signrpc.TweakDesc
+	15, // 10: signrpc.MuSig2CombineKeysRequest.taproot_tweak:type_name -> signrpc.TaprootTweakDesc
+	0,  // 11: signrpc.MuSig2SessionRequest.key_loc:type_name -> signrpc.KeyLocator
+	14, // 12: signrpc.MuSig2SessionRequest.tweaks:type_name -> signrpc.TweakDesc
+	15, // 13: signrpc.MuSig2SessionRequest.taproot_tweak:type_name -> signrpc.TaprootTweakDesc
+	4,  // 14: signrpc.Signer.SignOutputRaw:input_type -> signrpc.SignReq
+	4,  // 15: signrpc.Signer.ComputeInputScript:input_type -> signrpc.SignReq
+	8,  // 16: signrpc.Signer.SignMessage:input_type -> signrpc.SignMessageReq
+	10, // 17: signrpc.Signer.VerifyMessage:input_type -> signrpc.VerifyMessageReq
+	12, // 18: signrpc.Signer.DeriveSharedKey:input_type -> signrpc.SharedKeyRequest
+	16, // 19: signrpc.Signer.MuSig2CombineKeys:input_type -> signrpc.MuSig2CombineKeysRequest
+	18, // 20: signrpc.Signer.MuSig2CreateSession:input_type -> signrpc.MuSig2SessionRequest
+	20, // 21: signrpc.Signer.MuSig2RegisterNonces:input_type -> signrpc.MuSig2RegisterNoncesRequest
+	22, // 22: signrpc.Signer.MuSig2Sign:input_type -> signrpc.MuSig2SignRequest
+	24, // 23: signrpc.Signer.MuSig2CombineSig:input_type -> signrpc.MuSig2CombineSigRequest
+	5,  // 24: signrpc.Signer.SignOutputRaw:output_type -> signrpc.SignResp
+	7,  // 25: signrpc.Signer.ComputeInputScript:output_type -> signrpc.InputScriptResp
+	9,  // 26: signrpc.Signer.SignMessage:output_type -> signrpc.SignMessageResp
+	11, // 27: signrpc.Signer.VerifyMessage:output_type -> signrpc.VerifyMessageResp
+	13, // 28: signrpc.Signer.DeriveSharedKey:output_type -> signrpc.SharedKeyResponse
+	17, // 29: signrpc.Signer.MuSig2CombineKeys:output_type -> signrpc.MuSig2CombineKeysResponse
+	19, // 30: signrpc.Signer.MuSig2CreateSession:output_type -> signrpc.MuSig2SessionResponse
+	21, // 31: signrpc.Signer.MuSig2RegisterNonces:output_type -> signrpc.MuSig2RegisterNoncesResponse
+	23, // 32: signrpc.Signer.MuSig2Sign:output_type -> signrpc.MuSig2SignResponse
+	25, // 33: signrpc.Signer.MuSig2CombineSig:output_type -> signrpc.MuSig2CombineSigResponse
+	24, // [24:34] is the sub-list for method output_type
+	14, // [14:24] is the sub-list for method input_type
+	14, // [14:14] is the sub-list for extension type_name
+	14, // [14:14] is the sub-list for extension extendee
+	0,  // [0:14] is the sub-list for field type_name
 }
 
 func init() { file_signrpc_signer_proto_init() }
@@ -1284,6 +2250,150 @@ func file_signrpc_signer_proto_init() {
 				return nil
 			}
 		}
+		file_signrpc_signer_proto_msgTypes[14].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*TweakDesc); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[15].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*TaprootTweakDesc); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[16].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MuSig2CombineKeysRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[17].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MuSig2CombineKeysResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[18].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MuSig2SessionRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[19].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MuSig2SessionResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[20].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MuSig2RegisterNoncesRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[21].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MuSig2RegisterNoncesResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[22].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MuSig2SignRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[23].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MuSig2SignResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[24].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MuSig2CombineSigRequest); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
+		file_signrpc_signer_proto_msgTypes[25].Exporter = func(v interface{}, i int) interface{} {
+			switch v := v.(*MuSig2CombineSigResponse); i {
+			case 0:
+				return &v.state
+			case 1:
+				return &v.sizeCache
+			case 2:
+				return &v.unknownFields
+			default:
+				return nil
+			}
+		}
 	}
 	type x struct{}
 	out := protoimpl.TypeBuilder{
@@ -1291,7 +2401,7 @@ func file_signrpc_signer_proto_init() {
 			GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
 			RawDescriptor: file_signrpc_signer_proto_rawDesc,
 			NumEnums:      0,
-			NumMessages:   14,
+			NumMessages:   26,
 			NumExtensions: 0,
 			NumServices:   1,
 		},
diff --git a/lnrpc/signrpc/signer.pb.gw.go b/lnrpc/signrpc/signer.pb.gw.go
index 9e9715917..4f4efc2e2 100644
--- a/lnrpc/signrpc/signer.pb.gw.go
+++ b/lnrpc/signrpc/signer.pb.gw.go
@@ -201,6 +201,176 @@ func local_request_Signer_DeriveSharedKey_0(ctx context.Context, marshaler runti
 
 }
 
+func request_Signer_MuSig2CombineKeys_0(ctx context.Context, marshaler runtime.Marshaler, client SignerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq MuSig2CombineKeysRequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := client.MuSig2CombineKeys(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_Signer_MuSig2CombineKeys_0(ctx context.Context, marshaler runtime.Marshaler, server SignerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq MuSig2CombineKeysRequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := server.MuSig2CombineKeys(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
+func request_Signer_MuSig2CreateSession_0(ctx context.Context, marshaler runtime.Marshaler, client SignerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq MuSig2SessionRequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := client.MuSig2CreateSession(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_Signer_MuSig2CreateSession_0(ctx context.Context, marshaler runtime.Marshaler, server SignerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq MuSig2SessionRequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := server.MuSig2CreateSession(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
+func request_Signer_MuSig2RegisterNonces_0(ctx context.Context, marshaler runtime.Marshaler, client SignerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq MuSig2RegisterNoncesRequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := client.MuSig2RegisterNonces(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_Signer_MuSig2RegisterNonces_0(ctx context.Context, marshaler runtime.Marshaler, server SignerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq MuSig2RegisterNoncesRequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := server.MuSig2RegisterNonces(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
+func request_Signer_MuSig2Sign_0(ctx context.Context, marshaler runtime.Marshaler, client SignerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq MuSig2SignRequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := client.MuSig2Sign(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_Signer_MuSig2Sign_0(ctx context.Context, marshaler runtime.Marshaler, server SignerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq MuSig2SignRequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := server.MuSig2Sign(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
+func request_Signer_MuSig2CombineSig_0(ctx context.Context, marshaler runtime.Marshaler, client SignerClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq MuSig2CombineSigRequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := client.MuSig2CombineSig(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD))
+	return msg, metadata, err
+
+}
+
+func local_request_Signer_MuSig2CombineSig_0(ctx context.Context, marshaler runtime.Marshaler, server SignerServer, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) {
+	var protoReq MuSig2CombineSigRequest
+	var metadata runtime.ServerMetadata
+
+	newReader, berr := utilities.IOReaderFactory(req.Body)
+	if berr != nil {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", berr)
+	}
+	if err := marshaler.NewDecoder(newReader()).Decode(&protoReq); err != nil && err != io.EOF {
+		return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err)
+	}
+
+	msg, err := server.MuSig2CombineSig(ctx, &protoReq)
+	return msg, metadata, err
+
+}
+
 // RegisterSignerHandlerServer registers the http handlers for service Signer to "mux".
 // UnaryRPC     :call SignerServer directly.
 // StreamingRPC :currently unsupported pending https://github.com/grpc/grpc-go/issues/906.
@@ -322,6 +492,121 @@ func RegisterSignerHandlerServer(ctx context.Context, mux *runtime.ServeMux, ser
 
 	})
 
+	mux.Handle("POST", pattern_Signer_MuSig2CombineKeys_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/signrpc.Signer/MuSig2CombineKeys", runtime.WithHTTPPathPattern("/v2/signer/musig2/combinekeys"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_Signer_MuSig2CombineKeys_0(rctx, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Signer_MuSig2CombineKeys_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("POST", pattern_Signer_MuSig2CreateSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/signrpc.Signer/MuSig2CreateSession", runtime.WithHTTPPathPattern("/v2/signer/musig2/createsession"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_Signer_MuSig2CreateSession_0(rctx, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Signer_MuSig2CreateSession_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("POST", pattern_Signer_MuSig2RegisterNonces_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/signrpc.Signer/MuSig2RegisterNonces", runtime.WithHTTPPathPattern("/v2/signer/musig2/registernonces"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_Signer_MuSig2RegisterNonces_0(rctx, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Signer_MuSig2RegisterNonces_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("POST", pattern_Signer_MuSig2Sign_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/signrpc.Signer/MuSig2Sign", runtime.WithHTTPPathPattern("/v2/signer/musig2/sign"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_Signer_MuSig2Sign_0(rctx, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Signer_MuSig2Sign_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("POST", pattern_Signer_MuSig2CombineSig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		var stream runtime.ServerTransportStream
+		ctx = grpc.NewContextWithServerTransportStream(ctx, &stream)
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateIncomingContext(ctx, mux, req, "/signrpc.Signer/MuSig2CombineSig", runtime.WithHTTPPathPattern("/v2/signer/musig2/combinesig"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := local_request_Signer_MuSig2CombineSig_0(rctx, inboundMarshaler, server, req, pathParams)
+		md.HeaderMD, md.TrailerMD = metadata.Join(md.HeaderMD, stream.Header()), metadata.Join(md.TrailerMD, stream.Trailer())
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Signer_MuSig2CombineSig_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	return nil
 }
 
@@ -463,6 +748,106 @@ func RegisterSignerHandlerClient(ctx context.Context, mux *runtime.ServeMux, cli
 
 	})
 
+	mux.Handle("POST", pattern_Signer_MuSig2CombineKeys_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/signrpc.Signer/MuSig2CombineKeys", runtime.WithHTTPPathPattern("/v2/signer/musig2/combinekeys"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_Signer_MuSig2CombineKeys_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Signer_MuSig2CombineKeys_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("POST", pattern_Signer_MuSig2CreateSession_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/signrpc.Signer/MuSig2CreateSession", runtime.WithHTTPPathPattern("/v2/signer/musig2/createsession"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_Signer_MuSig2CreateSession_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Signer_MuSig2CreateSession_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("POST", pattern_Signer_MuSig2RegisterNonces_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/signrpc.Signer/MuSig2RegisterNonces", runtime.WithHTTPPathPattern("/v2/signer/musig2/registernonces"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_Signer_MuSig2RegisterNonces_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Signer_MuSig2RegisterNonces_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("POST", pattern_Signer_MuSig2Sign_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/signrpc.Signer/MuSig2Sign", runtime.WithHTTPPathPattern("/v2/signer/musig2/sign"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_Signer_MuSig2Sign_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Signer_MuSig2Sign_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
+	mux.Handle("POST", pattern_Signer_MuSig2CombineSig_0, func(w http.ResponseWriter, req *http.Request, pathParams map[string]string) {
+		ctx, cancel := context.WithCancel(req.Context())
+		defer cancel()
+		inboundMarshaler, outboundMarshaler := runtime.MarshalerForRequest(mux, req)
+		rctx, err := runtime.AnnotateContext(ctx, mux, req, "/signrpc.Signer/MuSig2CombineSig", runtime.WithHTTPPathPattern("/v2/signer/musig2/combinesig"))
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+		resp, md, err := request_Signer_MuSig2CombineSig_0(rctx, inboundMarshaler, client, req, pathParams)
+		ctx = runtime.NewServerMetadataContext(ctx, md)
+		if err != nil {
+			runtime.HTTPError(ctx, mux, outboundMarshaler, w, req, err)
+			return
+		}
+
+		forward_Signer_MuSig2CombineSig_0(ctx, mux, outboundMarshaler, w, req, resp, mux.GetForwardResponseOptions()...)
+
+	})
+
 	return nil
 }
 
@@ -476,6 +861,16 @@ var (
 	pattern_Signer_VerifyMessage_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "signer", "verifymessage"}, ""))
 
 	pattern_Signer_DeriveSharedKey_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2}, []string{"v2", "signer", "sharedkey"}, ""))
+
+	pattern_Signer_MuSig2CombineKeys_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "signer", "musig2", "combinekeys"}, ""))
+
+	pattern_Signer_MuSig2CreateSession_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "signer", "musig2", "createsession"}, ""))
+
+	pattern_Signer_MuSig2RegisterNonces_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "signer", "musig2", "registernonces"}, ""))
+
+	pattern_Signer_MuSig2Sign_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "signer", "musig2", "sign"}, ""))
+
+	pattern_Signer_MuSig2CombineSig_0 = runtime.MustPattern(runtime.NewPattern(1, []int{2, 0, 2, 1, 2, 2, 2, 3}, []string{"v2", "signer", "musig2", "combinesig"}, ""))
 )
 
 var (
@@ -488,4 +883,14 @@ var (
 	forward_Signer_VerifyMessage_0 = runtime.ForwardResponseMessage
 
 	forward_Signer_DeriveSharedKey_0 = runtime.ForwardResponseMessage
+
+	forward_Signer_MuSig2CombineKeys_0 = runtime.ForwardResponseMessage
+
+	forward_Signer_MuSig2CreateSession_0 = runtime.ForwardResponseMessage
+
+	forward_Signer_MuSig2RegisterNonces_0 = runtime.ForwardResponseMessage
+
+	forward_Signer_MuSig2Sign_0 = runtime.ForwardResponseMessage
+
+	forward_Signer_MuSig2CombineSig_0 = runtime.ForwardResponseMessage
 )
diff --git a/lnrpc/signrpc/signer.pb.json.go b/lnrpc/signrpc/signer.pb.json.go
index 8f2e6ae2e..2ba8d913a 100644
--- a/lnrpc/signrpc/signer.pb.json.go
+++ b/lnrpc/signrpc/signer.pb.json.go
@@ -147,4 +147,129 @@ func RegisterSignerJSONCallbacks(registry map[string]func(ctx context.Context,
 		}
 		callback(string(respBytes), nil)
 	}
+
+	registry["signrpc.Signer.MuSig2CombineKeys"] = func(ctx context.Context,
+		conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
+
+		req := &MuSig2CombineKeysRequest{}
+		err := marshaler.Unmarshal([]byte(reqJSON), req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		client := NewSignerClient(conn)
+		resp, err := client.MuSig2CombineKeys(ctx, req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		respBytes, err := marshaler.Marshal(resp)
+		if err != nil {
+			callback("", err)
+			return
+		}
+		callback(string(respBytes), nil)
+	}
+
+	registry["signrpc.Signer.MuSig2CreateSession"] = func(ctx context.Context,
+		conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
+
+		req := &MuSig2SessionRequest{}
+		err := marshaler.Unmarshal([]byte(reqJSON), req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		client := NewSignerClient(conn)
+		resp, err := client.MuSig2CreateSession(ctx, req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		respBytes, err := marshaler.Marshal(resp)
+		if err != nil {
+			callback("", err)
+			return
+		}
+		callback(string(respBytes), nil)
+	}
+
+	registry["signrpc.Signer.MuSig2RegisterNonces"] = func(ctx context.Context,
+		conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
+
+		req := &MuSig2RegisterNoncesRequest{}
+		err := marshaler.Unmarshal([]byte(reqJSON), req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		client := NewSignerClient(conn)
+		resp, err := client.MuSig2RegisterNonces(ctx, req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		respBytes, err := marshaler.Marshal(resp)
+		if err != nil {
+			callback("", err)
+			return
+		}
+		callback(string(respBytes), nil)
+	}
+
+	registry["signrpc.Signer.MuSig2Sign"] = func(ctx context.Context,
+		conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
+
+		req := &MuSig2SignRequest{}
+		err := marshaler.Unmarshal([]byte(reqJSON), req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		client := NewSignerClient(conn)
+		resp, err := client.MuSig2Sign(ctx, req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		respBytes, err := marshaler.Marshal(resp)
+		if err != nil {
+			callback("", err)
+			return
+		}
+		callback(string(respBytes), nil)
+	}
+
+	registry["signrpc.Signer.MuSig2CombineSig"] = func(ctx context.Context,
+		conn *grpc.ClientConn, reqJSON string, callback func(string, error)) {
+
+		req := &MuSig2CombineSigRequest{}
+		err := marshaler.Unmarshal([]byte(reqJSON), req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		client := NewSignerClient(conn)
+		resp, err := client.MuSig2CombineSig(ctx, req)
+		if err != nil {
+			callback("", err)
+			return
+		}
+
+		respBytes, err := marshaler.Marshal(resp)
+		if err != nil {
+			callback("", err)
+			return
+		}
+		callback(string(respBytes), nil)
+	}
 }
diff --git a/lnrpc/signrpc/signer.proto b/lnrpc/signrpc/signer.proto
index c75b95a34..79e22caa0 100644
--- a/lnrpc/signrpc/signer.proto
+++ b/lnrpc/signrpc/signer.proto
@@ -62,6 +62,76 @@ service Signer {
     hashed with sha256, resulting in the final key length of 256bit.
     */
     rpc DeriveSharedKey (SharedKeyRequest) returns (SharedKeyResponse);
+
+    /*
+    MuSig2CombineKeys (experimental!) is a stateless helper RPC that can be used
+    to calculate the combined MuSig2 public key from a list of all participating
+    signers' public keys. This RPC is completely stateless and deterministic and
+    does not create any signing session. It can be used to determine the Taproot
+    public key that should be put in an on-chain output once all public keys are
+    known. A signing session is only needed later when that output should be
+    _spent_ again.
+
+    NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+    considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+    releases. Backward compatibility is not guaranteed!
+    */
+    rpc MuSig2CombineKeys (MuSig2CombineKeysRequest)
+        returns (MuSig2CombineKeysResponse);
+
+    /*
+    MuSig2CreateSession (experimental!) creates a new MuSig2 signing session
+    using the local key identified by the key locator. The complete list of all
+    public keys of all signing parties must be provided, including the public
+    key of the local signing key. If nonces of other parties are already known,
+    they can be submitted as well to reduce the number of RPC calls necessary
+    later on.
+
+    NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+    considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+    releases. Backward compatibility is not guaranteed!
+    */
+    rpc MuSig2CreateSession (MuSig2SessionRequest)
+        returns (MuSig2SessionResponse);
+
+    /*
+    MuSig2RegisterNonces (experimental!) registers one or more public nonces of
+    other signing participants for a session identified by its ID. This RPC can
+    be called multiple times until all nonces are registered.
+
+    NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+    considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+    releases. Backward compatibility is not guaranteed!
+    */
+    rpc MuSig2RegisterNonces (MuSig2RegisterNoncesRequest)
+        returns (MuSig2RegisterNoncesResponse);
+
+    /*
+    MuSig2Sign (experimental!) creates a partial signature using the local
+    signing key that was specified when the session was created. This can only
+    be called when all public nonces of all participants are known and have been
+    registered with the session. If this node isn't responsible for combining
+    all the partial signatures, then the cleanup flag should be set, indicating
+    that the session can be removed from memory once the signature was produced.
+
+    NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+    considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+    releases. Backward compatibility is not guaranteed!
+    */
+    rpc MuSig2Sign (MuSig2SignRequest) returns (MuSig2SignResponse);
+
+    /*
+    MuSig2CombineSig (experimental!) combines the given partial signature(s)
+    with the local one, if it already exists. Once a partial signature of all
+    participants is registered, the final signature will be combined and
+    returned.
+
+    NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+    considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+    releases. Backward compatibility is not guaranteed!
+    */
+    rpc MuSig2CombineSig (MuSig2CombineSigRequest)
+        returns (MuSig2CombineSigResponse);
 }
 
 message KeyLocator {
@@ -265,3 +335,221 @@ message SharedKeyResponse {
     // The shared public key, hashed with sha256.
     bytes shared_key = 1;
 }
+
+message TweakDesc {
+    /*
+    Tweak is the 32-byte value that will modify the public key.
+    */
+    bytes tweak = 1;
+
+    /*
+    Specifies if the target key should be converted to an x-only public key
+    before tweaking. If true, then the public key will be mapped to an x-only
+    key before the tweaking operation is applied.
+    */
+    bool is_x_only = 2;
+}
+
+message TaprootTweakDesc {
+    /*
+    The root hash of the tapscript tree if a script path is committed to. If
+    the MuSig2 key put on chain doesn't also commit to a script path (BIP-0086
+    key spend only), then this needs to be empty and the key_spend_only field
+    below must be set to true. This is required because gRPC cannot
+    differentiate between a zero-size byte slice and a nil byte slice (both
+    would be serialized the same way). So the extra boolean is required.
+    */
+    bytes script_root = 1;
+
+    /*
+    Indicates that the above script_root is expected to be empty because this
+    is a BIP-0086 key spend only commitment where only the internal key is
+    committed to instead of also including a script root hash.
+    */
+    bool key_spend_only = 2;
+}
+
+message MuSig2CombineKeysRequest {
+    /*
+    A list of all public keys (serialized in 32-byte x-only format!)
+    participating in the signing session. The list will always be sorted
+    lexicographically internally. This must include the local key which is
+    described by the above key_loc.
+    */
+    repeated bytes all_signer_pubkeys = 1;
+
+    /*
+    A series of optional generic tweaks to be applied to the the aggregated
+    public key.
+    */
+    repeated TweakDesc tweaks = 2;
+
+    /*
+    An optional taproot specific tweak that must be specified if the MuSig2
+    combined key will be used as the main taproot key of a taproot output
+    on-chain.
+    */
+    TaprootTweakDesc taproot_tweak = 3;
+}
+
+message MuSig2CombineKeysResponse {
+    /*
+    The combined public key (in the 32-byte x-only format) with all tweaks
+    applied to it. If a taproot tweak is specified, this corresponds to the
+    taproot key that can be put into the on-chain output.
+    */
+    bytes combined_key = 1;
+
+    /*
+    The raw combined public key (in the 32-byte x-only format) before any tweaks
+    are applied to it. If a taproot tweak is specified, this corresponds to the
+    internal key that needs to be put into the witness if the script spend path
+    is used.
+    */
+    bytes taproot_internal_key = 2;
+}
+
+message MuSig2SessionRequest {
+    /*
+    The key locator that identifies which key to use for signing.
+    */
+    KeyLocator key_loc = 1;
+
+    /*
+    A list of all public keys (serialized in 32-byte x-only format!)
+    participating in the signing session. The list will always be sorted
+    lexicographically internally. This must include the local key which is
+    described by the above key_loc.
+    */
+    repeated bytes all_signer_pubkeys = 2;
+
+    /*
+    An optional list of all public nonces of other signing participants that
+    might already be known.
+    */
+    repeated bytes other_signer_public_nonces = 3;
+
+    /*
+    A series of optional generic tweaks to be applied to the the aggregated
+    public key.
+    */
+    repeated TweakDesc tweaks = 4;
+
+    /*
+    An optional taproot specific tweak that must be specified if the MuSig2
+    combined key will be used as the main taproot key of a taproot output
+    on-chain.
+    */
+    TaprootTweakDesc taproot_tweak = 5;
+}
+
+message MuSig2SessionResponse {
+    /*
+    The unique ID that represents this signing session. A session can be used
+    for producing a signature a single time. If the signing fails for any
+    reason, a new session with the same participants needs to be created.
+    */
+    bytes session_id = 1;
+
+    /*
+    The combined public key (in the 32-byte x-only format) with all tweaks
+    applied to it. If a taproot tweak is specified, this corresponds to the
+    taproot key that can be put into the on-chain output.
+    */
+    bytes combined_key = 2;
+
+    /*
+    The raw combined public key (in the 32-byte x-only format) before any tweaks
+    are applied to it. If a taproot tweak is specified, this corresponds to the
+    internal key that needs to be put into the witness if the script spend path
+    is used.
+    */
+    bytes taproot_internal_key = 3;
+
+    /*
+    The two public nonces the local signer uses, combined into a single value
+    of 66 bytes. Can be split into the two 33-byte points to get the individual
+    nonces.
+    */
+    bytes local_public_nonces = 4;
+
+    /*
+    Indicates whether all nonces required to start the signing process are known
+    now.
+    */
+    bool have_all_nonces = 5;
+}
+
+message MuSig2RegisterNoncesRequest {
+    /*
+    The unique ID of the signing session those nonces should be registered with.
+    */
+    bytes session_id = 1;
+
+    /*
+    A list of all public nonces of other signing participants that should be
+    registered.
+    */
+    repeated bytes other_signer_public_nonces = 3;
+}
+
+message MuSig2RegisterNoncesResponse {
+    /*
+    Indicates whether all nonces required to start the signing process are known
+    now.
+    */
+    bool have_all_nonces = 1;
+}
+
+message MuSig2SignRequest {
+    /*
+    The unique ID of the signing session to use for signing.
+    */
+    bytes session_id = 1;
+
+    /*
+    The 32-byte SHA256 digest of the message to sign.
+    */
+    bytes message_digest = 2;
+
+    /*
+    Cleanup indicates that after signing, the session state can be cleaned up,
+    since another participant is going to be responsible for combining the
+    partial signatures.
+    */
+    bool cleanup = 3;
+}
+
+message MuSig2SignResponse {
+    /*
+    The partial signature created by the local signer.
+    */
+    bytes local_partial_signature = 1;
+}
+
+message MuSig2CombineSigRequest {
+    /*
+    The unique ID of the signing session to combine the signatures for.
+    */
+    bytes session_id = 1;
+
+    /*
+    The list of all other participants' partial signatures to add to the current
+    session.
+    */
+    repeated bytes other_partial_signatures = 2;
+}
+
+message MuSig2CombineSigResponse {
+    /*
+    Indicates whether all partial signatures required to create a final, full
+    signature are known yet. If this is true, then the final_signature field is
+    set, otherwise it is empty.
+    */
+    bool have_all_signatures = 1;
+
+    /*
+    The final, full signature that is valid for the combined public key.
+    */
+    bytes final_signature = 2;
+}
\ No newline at end of file
diff --git a/lnrpc/signrpc/signer.swagger.json b/lnrpc/signrpc/signer.swagger.json
index a39922207..6825f30d7 100644
--- a/lnrpc/signrpc/signer.swagger.json
+++ b/lnrpc/signrpc/signer.swagger.json
@@ -50,6 +50,176 @@
         ]
       }
     },
+    "/v2/signer/musig2/combinekeys": {
+      "post": {
+        "summary": "MuSig2CombineKeys (experimental!) is a stateless helper RPC that can be used\nto calculate the combined MuSig2 public key from a list of all participating\nsigners' public keys. This RPC is completely stateless and deterministic and\ndoes not create any signing session. It can be used to determine the Taproot\npublic key that should be put in an on-chain output once all public keys are\nknown. A signing session is only needed later when that output should be\n_spent_ again.",
+        "description": "NOTE: The MuSig2 BIP is not final yet and therefore this API must be\nconsidered to be HIGHLY EXPERIMENTAL and subject to change in upcoming\nreleases. Backward compatibility is not guaranteed!",
+        "operationId": "Signer_MuSig2CombineKeys",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/signrpcMuSig2CombineKeysResponse"
+            }
+          },
+          "default": {
+            "description": "An unexpected error response.",
+            "schema": {
+              "$ref": "#/definitions/rpcStatus"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/signrpcMuSig2CombineKeysRequest"
+            }
+          }
+        ],
+        "tags": [
+          "Signer"
+        ]
+      }
+    },
+    "/v2/signer/musig2/combinesig": {
+      "post": {
+        "summary": "MuSig2CombineSig (experimental!) combines the given partial signature(s)\nwith the local one, if it already exists. Once a partial signature of all\nparticipants is registered, the final signature will be combined and\nreturned.",
+        "description": "NOTE: The MuSig2 BIP is not final yet and therefore this API must be\nconsidered to be HIGHLY EXPERIMENTAL and subject to change in upcoming\nreleases. Backward compatibility is not guaranteed!",
+        "operationId": "Signer_MuSig2CombineSig",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/signrpcMuSig2CombineSigResponse"
+            }
+          },
+          "default": {
+            "description": "An unexpected error response.",
+            "schema": {
+              "$ref": "#/definitions/rpcStatus"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/signrpcMuSig2CombineSigRequest"
+            }
+          }
+        ],
+        "tags": [
+          "Signer"
+        ]
+      }
+    },
+    "/v2/signer/musig2/createsession": {
+      "post": {
+        "summary": "MuSig2CreateSession (experimental!) creates a new MuSig2 signing session\nusing the local key identified by the key locator. The complete list of all\npublic keys of all signing parties must be provided, including the public\nkey of the local signing key. If nonces of other parties are already known,\nthey can be submitted as well to reduce the number of RPC calls necessary\nlater on.",
+        "description": "NOTE: The MuSig2 BIP is not final yet and therefore this API must be\nconsidered to be HIGHLY EXPERIMENTAL and subject to change in upcoming\nreleases. Backward compatibility is not guaranteed!",
+        "operationId": "Signer_MuSig2CreateSession",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/signrpcMuSig2SessionResponse"
+            }
+          },
+          "default": {
+            "description": "An unexpected error response.",
+            "schema": {
+              "$ref": "#/definitions/rpcStatus"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/signrpcMuSig2SessionRequest"
+            }
+          }
+        ],
+        "tags": [
+          "Signer"
+        ]
+      }
+    },
+    "/v2/signer/musig2/registernonces": {
+      "post": {
+        "summary": "MuSig2RegisterNonces (experimental!) registers one or more public nonces of\nother signing participants for a session identified by its ID. This RPC can\nbe called multiple times until all nonces are registered.",
+        "description": "NOTE: The MuSig2 BIP is not final yet and therefore this API must be\nconsidered to be HIGHLY EXPERIMENTAL and subject to change in upcoming\nreleases. Backward compatibility is not guaranteed!",
+        "operationId": "Signer_MuSig2RegisterNonces",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/signrpcMuSig2RegisterNoncesResponse"
+            }
+          },
+          "default": {
+            "description": "An unexpected error response.",
+            "schema": {
+              "$ref": "#/definitions/rpcStatus"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/signrpcMuSig2RegisterNoncesRequest"
+            }
+          }
+        ],
+        "tags": [
+          "Signer"
+        ]
+      }
+    },
+    "/v2/signer/musig2/sign": {
+      "post": {
+        "summary": "MuSig2Sign (experimental!) creates a partial signature using the local\nsigning key that was specified when the session was created. This can only\nbe called when all public nonces of all participants are known and have been\nregistered with the session. If this node isn't responsible for combining\nall the partial signatures, then the cleanup flag should be set, indicating\nthat the session can be removed from memory once the signature was produced.",
+        "description": "NOTE: The MuSig2 BIP is not final yet and therefore this API must be\nconsidered to be HIGHLY EXPERIMENTAL and subject to change in upcoming\nreleases. Backward compatibility is not guaranteed!",
+        "operationId": "Signer_MuSig2Sign",
+        "responses": {
+          "200": {
+            "description": "A successful response.",
+            "schema": {
+              "$ref": "#/definitions/signrpcMuSig2SignResponse"
+            }
+          },
+          "default": {
+            "description": "An unexpected error response.",
+            "schema": {
+              "$ref": "#/definitions/rpcStatus"
+            }
+          }
+        },
+        "parameters": [
+          {
+            "name": "body",
+            "in": "body",
+            "required": true,
+            "schema": {
+              "$ref": "#/definitions/signrpcMuSig2SignRequest"
+            }
+          }
+        ],
+        "tags": [
+          "Signer"
+        ]
+      }
+    },
     "/v2/signer/sharedkey": {
       "post": {
         "summary": "DeriveSharedKey returns a shared secret key by performing Diffie-Hellman key\nderivation between the ephemeral public key in the request and the node's\nkey specified in the key_desc parameter. Either a key locator or a raw\npublic key is expected in the key_desc, if neither is supplied, defaults to\nthe node's identity private key:\nP_shared = privKeyNode * ephemeralPubkey\nThe resulting shared public key is serialized in the compressed format and\nhashed with sha256, resulting in the final key length of 256bit.",
@@ -276,6 +446,198 @@
         }
       }
     },
+    "signrpcMuSig2CombineKeysRequest": {
+      "type": "object",
+      "properties": {
+        "all_signer_pubkeys": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "byte"
+          },
+          "description": "A list of all public keys (serialized in 32-byte x-only format!)\nparticipating in the signing session. The list will always be sorted\nlexicographically internally. This must include the local key which is\ndescribed by the above key_loc."
+        },
+        "tweaks": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/signrpcTweakDesc"
+          },
+          "description": "A series of optional generic tweaks to be applied to the the aggregated\npublic key."
+        },
+        "taproot_tweak": {
+          "$ref": "#/definitions/signrpcTaprootTweakDesc",
+          "description": "An optional taproot specific tweak that must be specified if the MuSig2\ncombined key will be used as the main taproot key of a taproot output\non-chain."
+        }
+      }
+    },
+    "signrpcMuSig2CombineKeysResponse": {
+      "type": "object",
+      "properties": {
+        "combined_key": {
+          "type": "string",
+          "format": "byte",
+          "description": "The combined public key (in the 32-byte x-only format) with all tweaks\napplied to it. If a taproot tweak is specified, this corresponds to the\ntaproot key that can be put into the on-chain output."
+        },
+        "taproot_internal_key": {
+          "type": "string",
+          "format": "byte",
+          "description": "The raw combined public key (in the 32-byte x-only format) before any tweaks\nare applied to it. If a taproot tweak is specified, this corresponds to the\ninternal key that needs to be put into the witness if the script spend path\nis used."
+        }
+      }
+    },
+    "signrpcMuSig2CombineSigRequest": {
+      "type": "object",
+      "properties": {
+        "session_id": {
+          "type": "string",
+          "format": "byte",
+          "description": "The unique ID of the signing session to combine the signatures for."
+        },
+        "other_partial_signatures": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "byte"
+          },
+          "description": "The list of all other participants' partial signatures to add to the current\nsession."
+        }
+      }
+    },
+    "signrpcMuSig2CombineSigResponse": {
+      "type": "object",
+      "properties": {
+        "have_all_signatures": {
+          "type": "boolean",
+          "description": "Indicates whether all partial signatures required to create a final, full\nsignature are known yet. If this is true, then the final_signature field is\nset, otherwise it is empty."
+        },
+        "final_signature": {
+          "type": "string",
+          "format": "byte",
+          "description": "The final, full signature that is valid for the combined public key."
+        }
+      }
+    },
+    "signrpcMuSig2RegisterNoncesRequest": {
+      "type": "object",
+      "properties": {
+        "session_id": {
+          "type": "string",
+          "format": "byte",
+          "description": "The unique ID of the signing session those nonces should be registered with."
+        },
+        "other_signer_public_nonces": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "byte"
+          },
+          "description": "A list of all public nonces of other signing participants that should be\nregistered."
+        }
+      }
+    },
+    "signrpcMuSig2RegisterNoncesResponse": {
+      "type": "object",
+      "properties": {
+        "have_all_nonces": {
+          "type": "boolean",
+          "description": "Indicates whether all nonces required to start the signing process are known\nnow."
+        }
+      }
+    },
+    "signrpcMuSig2SessionRequest": {
+      "type": "object",
+      "properties": {
+        "key_loc": {
+          "$ref": "#/definitions/signrpcKeyLocator",
+          "description": "The key locator that identifies which key to use for signing."
+        },
+        "all_signer_pubkeys": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "byte"
+          },
+          "description": "A list of all public keys (serialized in 32-byte x-only format!)\nparticipating in the signing session. The list will always be sorted\nlexicographically internally. This must include the local key which is\ndescribed by the above key_loc."
+        },
+        "other_signer_public_nonces": {
+          "type": "array",
+          "items": {
+            "type": "string",
+            "format": "byte"
+          },
+          "description": "An optional list of all public nonces of other signing participants that\nmight already be known."
+        },
+        "tweaks": {
+          "type": "array",
+          "items": {
+            "$ref": "#/definitions/signrpcTweakDesc"
+          },
+          "description": "A series of optional generic tweaks to be applied to the the aggregated\npublic key."
+        },
+        "taproot_tweak": {
+          "$ref": "#/definitions/signrpcTaprootTweakDesc",
+          "description": "An optional taproot specific tweak that must be specified if the MuSig2\ncombined key will be used as the main taproot key of a taproot output\non-chain."
+        }
+      }
+    },
+    "signrpcMuSig2SessionResponse": {
+      "type": "object",
+      "properties": {
+        "session_id": {
+          "type": "string",
+          "format": "byte",
+          "description": "The unique ID that represents this signing session. A session can be used\nfor producing a signature a single time. If the signing fails for any\nreason, a new session with the same participants needs to be created."
+        },
+        "combined_key": {
+          "type": "string",
+          "format": "byte",
+          "description": "The combined public key (in the 32-byte x-only format) with all tweaks\napplied to it. If a taproot tweak is specified, this corresponds to the\ntaproot key that can be put into the on-chain output."
+        },
+        "taproot_internal_key": {
+          "type": "string",
+          "format": "byte",
+          "description": "The raw combined public key (in the 32-byte x-only format) before any tweaks\nare applied to it. If a taproot tweak is specified, this corresponds to the\ninternal key that needs to be put into the witness if the script spend path\nis used."
+        },
+        "local_public_nonces": {
+          "type": "string",
+          "format": "byte",
+          "description": "The two public nonces the local signer uses, combined into a single value\nof 66 bytes. Can be split into the two 33-byte points to get the individual\nnonces."
+        },
+        "have_all_nonces": {
+          "type": "boolean",
+          "description": "Indicates whether all nonces required to start the signing process are known\nnow."
+        }
+      }
+    },
+    "signrpcMuSig2SignRequest": {
+      "type": "object",
+      "properties": {
+        "session_id": {
+          "type": "string",
+          "format": "byte",
+          "description": "The unique ID of the signing session to use for signing."
+        },
+        "message_digest": {
+          "type": "string",
+          "format": "byte",
+          "description": "The 32-byte SHA256 digest of the message to sign."
+        },
+        "cleanup": {
+          "type": "boolean",
+          "description": "Cleanup indicates that after signing, the session state can be cleaned up,\nsince another participant is going to be responsible for combining the\npartial signatures."
+        }
+      }
+    },
+    "signrpcMuSig2SignResponse": {
+      "type": "object",
+      "properties": {
+        "local_partial_signature": {
+          "type": "string",
+          "format": "byte",
+          "description": "The partial signature created by the local signer."
+        }
+      }
+    },
     "signrpcSharedKeyRequest": {
       "type": "object",
       "properties": {
@@ -416,6 +778,34 @@
         }
       }
     },
+    "signrpcTaprootTweakDesc": {
+      "type": "object",
+      "properties": {
+        "script_root": {
+          "type": "string",
+          "format": "byte",
+          "description": "The root hash of the tapscript tree if a script path is committed to. If\nthe MuSig2 key put on chain doesn't also commit to a script path (BIP-0086\nkey spend only), then this needs to be empty and the key_spend_only field\nbelow must be set to true. This is required because gRPC cannot\ndifferentiate between a zero-size byte slice and a nil byte slice (both\nwould be serialized the same way). So the extra boolean is required."
+        },
+        "key_spend_only": {
+          "type": "boolean",
+          "description": "Indicates that the above script_root is expected to be empty because this\nis a BIP-0086 key spend only commitment where only the internal key is\ncommitted to instead of also including a script root hash."
+        }
+      }
+    },
+    "signrpcTweakDesc": {
+      "type": "object",
+      "properties": {
+        "tweak": {
+          "type": "string",
+          "format": "byte",
+          "description": "Tweak is the 32-byte value that will modify the public key."
+        },
+        "is_x_only": {
+          "type": "boolean",
+          "description": "Specifies if the target key should be converted to an x-only public key\nbefore tweaking. If true, then the public key will be mapped to an x-only\nkey before the tweaking operation is applied."
+        }
+      }
+    },
     "signrpcTxOut": {
       "type": "object",
       "properties": {
diff --git a/lnrpc/signrpc/signer.yaml b/lnrpc/signrpc/signer.yaml
index 295f8b160..94be763d0 100644
--- a/lnrpc/signrpc/signer.yaml
+++ b/lnrpc/signrpc/signer.yaml
@@ -18,3 +18,18 @@ http:
     - selector: signrpc.Signer.DeriveSharedKey
       post: "/v2/signer/sharedkey"
       body: "*"
+    - selector: signrpc.Signer.MuSig2CombineKeys
+      post: "/v2/signer/musig2/combinekeys"
+      body: "*"
+    - selector: signrpc.Signer.MuSig2CreateSession
+      post: "/v2/signer/musig2/createsession"
+      body: "*"
+    - selector: signrpc.Signer.MuSig2RegisterNonces
+      post: "/v2/signer/musig2/registernonces"
+      body: "*"
+    - selector: signrpc.Signer.MuSig2Sign
+      post: "/v2/signer/musig2/sign"
+      body: "*"
+    - selector: signrpc.Signer.MuSig2CombineSig
+      post: "/v2/signer/musig2/combinesig"
+      body: "*"
diff --git a/lnrpc/signrpc/signer_grpc.pb.go b/lnrpc/signrpc/signer_grpc.pb.go
index 4a6724fff..92a6a7a27 100644
--- a/lnrpc/signrpc/signer_grpc.pb.go
+++ b/lnrpc/signrpc/signer_grpc.pb.go
@@ -64,6 +64,62 @@ type SignerClient interface {
 	//The resulting shared public key is serialized in the compressed format and
 	//hashed with sha256, resulting in the final key length of 256bit.
 	DeriveSharedKey(ctx context.Context, in *SharedKeyRequest, opts ...grpc.CallOption) (*SharedKeyResponse, error)
+	//
+	//MuSig2CombineKeys (experimental!) is a stateless helper RPC that can be used
+	//to calculate the combined MuSig2 public key from a list of all participating
+	//signers' public keys. This RPC is completely stateless and deterministic and
+	//does not create any signing session. It can be used to determine the Taproot
+	//public key that should be put in an on-chain output once all public keys are
+	//known. A signing session is only needed later when that output should be
+	//_spent_ again.
+	//
+	//NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+	//considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+	//releases. Backward compatibility is not guaranteed!
+	MuSig2CombineKeys(ctx context.Context, in *MuSig2CombineKeysRequest, opts ...grpc.CallOption) (*MuSig2CombineKeysResponse, error)
+	//
+	//MuSig2CreateSession (experimental!) creates a new MuSig2 signing session
+	//using the local key identified by the key locator. The complete list of all
+	//public keys of all signing parties must be provided, including the public
+	//key of the local signing key. If nonces of other parties are already known,
+	//they can be submitted as well to reduce the number of RPC calls necessary
+	//later on.
+	//
+	//NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+	//considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+	//releases. Backward compatibility is not guaranteed!
+	MuSig2CreateSession(ctx context.Context, in *MuSig2SessionRequest, opts ...grpc.CallOption) (*MuSig2SessionResponse, error)
+	//
+	//MuSig2RegisterNonces (experimental!) registers one or more public nonces of
+	//other signing participants for a session identified by its ID. This RPC can
+	//be called multiple times until all nonces are registered.
+	//
+	//NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+	//considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+	//releases. Backward compatibility is not guaranteed!
+	MuSig2RegisterNonces(ctx context.Context, in *MuSig2RegisterNoncesRequest, opts ...grpc.CallOption) (*MuSig2RegisterNoncesResponse, error)
+	//
+	//MuSig2Sign (experimental!) creates a partial signature using the local
+	//signing key that was specified when the session was created. This can only
+	//be called when all public nonces of all participants are known and have been
+	//registered with the session. If this node isn't responsible for combining
+	//all the partial signatures, then the cleanup flag should be set, indicating
+	//that the session can be removed from memory once the signature was produced.
+	//
+	//NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+	//considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+	//releases. Backward compatibility is not guaranteed!
+	MuSig2Sign(ctx context.Context, in *MuSig2SignRequest, opts ...grpc.CallOption) (*MuSig2SignResponse, error)
+	//
+	//MuSig2CombineSig (experimental!) combines the given partial signature(s)
+	//with the local one, if it already exists. Once a partial signature of all
+	//participants is registered, the final signature will be combined and
+	//returned.
+	//
+	//NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+	//considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+	//releases. Backward compatibility is not guaranteed!
+	MuSig2CombineSig(ctx context.Context, in *MuSig2CombineSigRequest, opts ...grpc.CallOption) (*MuSig2CombineSigResponse, error)
 }
 
 type signerClient struct {
@@ -119,6 +175,51 @@ func (c *signerClient) DeriveSharedKey(ctx context.Context, in *SharedKeyRequest
 	return out, nil
 }
 
+func (c *signerClient) MuSig2CombineKeys(ctx context.Context, in *MuSig2CombineKeysRequest, opts ...grpc.CallOption) (*MuSig2CombineKeysResponse, error) {
+	out := new(MuSig2CombineKeysResponse)
+	err := c.cc.Invoke(ctx, "/signrpc.Signer/MuSig2CombineKeys", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *signerClient) MuSig2CreateSession(ctx context.Context, in *MuSig2SessionRequest, opts ...grpc.CallOption) (*MuSig2SessionResponse, error) {
+	out := new(MuSig2SessionResponse)
+	err := c.cc.Invoke(ctx, "/signrpc.Signer/MuSig2CreateSession", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *signerClient) MuSig2RegisterNonces(ctx context.Context, in *MuSig2RegisterNoncesRequest, opts ...grpc.CallOption) (*MuSig2RegisterNoncesResponse, error) {
+	out := new(MuSig2RegisterNoncesResponse)
+	err := c.cc.Invoke(ctx, "/signrpc.Signer/MuSig2RegisterNonces", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *signerClient) MuSig2Sign(ctx context.Context, in *MuSig2SignRequest, opts ...grpc.CallOption) (*MuSig2SignResponse, error) {
+	out := new(MuSig2SignResponse)
+	err := c.cc.Invoke(ctx, "/signrpc.Signer/MuSig2Sign", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
+func (c *signerClient) MuSig2CombineSig(ctx context.Context, in *MuSig2CombineSigRequest, opts ...grpc.CallOption) (*MuSig2CombineSigResponse, error) {
+	out := new(MuSig2CombineSigResponse)
+	err := c.cc.Invoke(ctx, "/signrpc.Signer/MuSig2CombineSig", in, out, opts...)
+	if err != nil {
+		return nil, err
+	}
+	return out, nil
+}
+
 // SignerServer is the server API for Signer service.
 // All implementations must embed UnimplementedSignerServer
 // for forward compatibility
@@ -169,6 +270,62 @@ type SignerServer interface {
 	//The resulting shared public key is serialized in the compressed format and
 	//hashed with sha256, resulting in the final key length of 256bit.
 	DeriveSharedKey(context.Context, *SharedKeyRequest) (*SharedKeyResponse, error)
+	//
+	//MuSig2CombineKeys (experimental!) is a stateless helper RPC that can be used
+	//to calculate the combined MuSig2 public key from a list of all participating
+	//signers' public keys. This RPC is completely stateless and deterministic and
+	//does not create any signing session. It can be used to determine the Taproot
+	//public key that should be put in an on-chain output once all public keys are
+	//known. A signing session is only needed later when that output should be
+	//_spent_ again.
+	//
+	//NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+	//considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+	//releases. Backward compatibility is not guaranteed!
+	MuSig2CombineKeys(context.Context, *MuSig2CombineKeysRequest) (*MuSig2CombineKeysResponse, error)
+	//
+	//MuSig2CreateSession (experimental!) creates a new MuSig2 signing session
+	//using the local key identified by the key locator. The complete list of all
+	//public keys of all signing parties must be provided, including the public
+	//key of the local signing key. If nonces of other parties are already known,
+	//they can be submitted as well to reduce the number of RPC calls necessary
+	//later on.
+	//
+	//NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+	//considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+	//releases. Backward compatibility is not guaranteed!
+	MuSig2CreateSession(context.Context, *MuSig2SessionRequest) (*MuSig2SessionResponse, error)
+	//
+	//MuSig2RegisterNonces (experimental!) registers one or more public nonces of
+	//other signing participants for a session identified by its ID. This RPC can
+	//be called multiple times until all nonces are registered.
+	//
+	//NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+	//considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+	//releases. Backward compatibility is not guaranteed!
+	MuSig2RegisterNonces(context.Context, *MuSig2RegisterNoncesRequest) (*MuSig2RegisterNoncesResponse, error)
+	//
+	//MuSig2Sign (experimental!) creates a partial signature using the local
+	//signing key that was specified when the session was created. This can only
+	//be called when all public nonces of all participants are known and have been
+	//registered with the session. If this node isn't responsible for combining
+	//all the partial signatures, then the cleanup flag should be set, indicating
+	//that the session can be removed from memory once the signature was produced.
+	//
+	//NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+	//considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+	//releases. Backward compatibility is not guaranteed!
+	MuSig2Sign(context.Context, *MuSig2SignRequest) (*MuSig2SignResponse, error)
+	//
+	//MuSig2CombineSig (experimental!) combines the given partial signature(s)
+	//with the local one, if it already exists. Once a partial signature of all
+	//participants is registered, the final signature will be combined and
+	//returned.
+	//
+	//NOTE: The MuSig2 BIP is not final yet and therefore this API must be
+	//considered to be HIGHLY EXPERIMENTAL and subject to change in upcoming
+	//releases. Backward compatibility is not guaranteed!
+	MuSig2CombineSig(context.Context, *MuSig2CombineSigRequest) (*MuSig2CombineSigResponse, error)
 	mustEmbedUnimplementedSignerServer()
 }
 
@@ -191,6 +348,21 @@ func (UnimplementedSignerServer) VerifyMessage(context.Context, *VerifyMessageRe
 func (UnimplementedSignerServer) DeriveSharedKey(context.Context, *SharedKeyRequest) (*SharedKeyResponse, error) {
 	return nil, status.Errorf(codes.Unimplemented, "method DeriveSharedKey not implemented")
 }
+func (UnimplementedSignerServer) MuSig2CombineKeys(context.Context, *MuSig2CombineKeysRequest) (*MuSig2CombineKeysResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method MuSig2CombineKeys not implemented")
+}
+func (UnimplementedSignerServer) MuSig2CreateSession(context.Context, *MuSig2SessionRequest) (*MuSig2SessionResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method MuSig2CreateSession not implemented")
+}
+func (UnimplementedSignerServer) MuSig2RegisterNonces(context.Context, *MuSig2RegisterNoncesRequest) (*MuSig2RegisterNoncesResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method MuSig2RegisterNonces not implemented")
+}
+func (UnimplementedSignerServer) MuSig2Sign(context.Context, *MuSig2SignRequest) (*MuSig2SignResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method MuSig2Sign not implemented")
+}
+func (UnimplementedSignerServer) MuSig2CombineSig(context.Context, *MuSig2CombineSigRequest) (*MuSig2CombineSigResponse, error) {
+	return nil, status.Errorf(codes.Unimplemented, "method MuSig2CombineSig not implemented")
+}
 func (UnimplementedSignerServer) mustEmbedUnimplementedSignerServer() {}
 
 // UnsafeSignerServer may be embedded to opt out of forward compatibility for this service.
@@ -294,6 +466,96 @@ func _Signer_DeriveSharedKey_Handler(srv interface{}, ctx context.Context, dec f
 	return interceptor(ctx, in, info, handler)
 }
 
+func _Signer_MuSig2CombineKeys_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(MuSig2CombineKeysRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(SignerServer).MuSig2CombineKeys(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/signrpc.Signer/MuSig2CombineKeys",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(SignerServer).MuSig2CombineKeys(ctx, req.(*MuSig2CombineKeysRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Signer_MuSig2CreateSession_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(MuSig2SessionRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(SignerServer).MuSig2CreateSession(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/signrpc.Signer/MuSig2CreateSession",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(SignerServer).MuSig2CreateSession(ctx, req.(*MuSig2SessionRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Signer_MuSig2RegisterNonces_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(MuSig2RegisterNoncesRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(SignerServer).MuSig2RegisterNonces(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/signrpc.Signer/MuSig2RegisterNonces",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(SignerServer).MuSig2RegisterNonces(ctx, req.(*MuSig2RegisterNoncesRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Signer_MuSig2Sign_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(MuSig2SignRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(SignerServer).MuSig2Sign(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/signrpc.Signer/MuSig2Sign",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(SignerServer).MuSig2Sign(ctx, req.(*MuSig2SignRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
+func _Signer_MuSig2CombineSig_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
+	in := new(MuSig2CombineSigRequest)
+	if err := dec(in); err != nil {
+		return nil, err
+	}
+	if interceptor == nil {
+		return srv.(SignerServer).MuSig2CombineSig(ctx, in)
+	}
+	info := &grpc.UnaryServerInfo{
+		Server:     srv,
+		FullMethod: "/signrpc.Signer/MuSig2CombineSig",
+	}
+	handler := func(ctx context.Context, req interface{}) (interface{}, error) {
+		return srv.(SignerServer).MuSig2CombineSig(ctx, req.(*MuSig2CombineSigRequest))
+	}
+	return interceptor(ctx, in, info, handler)
+}
+
 // Signer_ServiceDesc is the grpc.ServiceDesc for Signer service.
 // It's only intended for direct use with grpc.RegisterService,
 // and not to be introspected or modified (even as a copy)
@@ -321,6 +583,26 @@ var Signer_ServiceDesc = grpc.ServiceDesc{
 			MethodName: "DeriveSharedKey",
 			Handler:    _Signer_DeriveSharedKey_Handler,
 		},
+		{
+			MethodName: "MuSig2CombineKeys",
+			Handler:    _Signer_MuSig2CombineKeys_Handler,
+		},
+		{
+			MethodName: "MuSig2CreateSession",
+			Handler:    _Signer_MuSig2CreateSession_Handler,
+		},
+		{
+			MethodName: "MuSig2RegisterNonces",
+			Handler:    _Signer_MuSig2RegisterNonces_Handler,
+		},
+		{
+			MethodName: "MuSig2Sign",
+			Handler:    _Signer_MuSig2Sign_Handler,
+		},
+		{
+			MethodName: "MuSig2CombineSig",
+			Handler:    _Signer_MuSig2CombineSig_Handler,
+		},
 	},
 	Streams:  []grpc.StreamDesc{},
 	Metadata: "signrpc/signer.proto",
diff --git a/lnrpc/signrpc/signer_server.go b/lnrpc/signrpc/signer_server.go
index b191176b3..9523e12aa 100644
--- a/lnrpc/signrpc/signer_server.go
+++ b/lnrpc/signrpc/signer_server.go
@@ -6,12 +6,15 @@ package signrpc
 import (
 	"bytes"
 	"context"
+	"crypto/sha256"
 	"fmt"
 	"io/ioutil"
 	"os"
 	"path/filepath"
 
 	"github.com/btcsuite/btcd/btcec/v2"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/txscript"
 	"github.com/btcsuite/btcd/wire"
@@ -69,6 +72,26 @@ var (
 			Entity: "signer",
 			Action: "generate",
 		}},
+		"/signrpc.Signer/MuSig2CombineKeys": {{
+			Entity: "signer",
+			Action: "read",
+		}},
+		"/signrpc.Signer/MuSig2CreateSession": {{
+			Entity: "signer",
+			Action: "generate",
+		}},
+		"/signrpc.Signer/MuSig2RegisterNonces": {{
+			Entity: "signer",
+			Action: "generate",
+		}},
+		"/signrpc.Signer/MuSig2Sign": {{
+			Entity: "signer",
+			Action: "generate",
+		}},
+		"/signrpc.Signer/MuSig2CombineSig": {{
+			Entity: "signer",
+			Action: "generate",
+		}},
 	}
 
 	// DefaultSignerMacFilename is the default name of the signer macaroon
@@ -673,6 +696,250 @@ func (s *Server) DeriveSharedKey(_ context.Context, in *SharedKeyRequest) (
 	return &SharedKeyResponse{SharedKey: sharedKeyHash[:]}, nil
 }
 
+// MuSig2CombineKeys combines the given set of public keys into a single
+// combined MuSig2 combined public key, applying the given tweaks.
+func (s *Server) MuSig2CombineKeys(_ context.Context,
+	in *MuSig2CombineKeysRequest) (*MuSig2CombineKeysResponse, error) {
+
+	// Parse the public keys of all signing participants. This must also
+	// include our own, local key.
+	allSignerPubKeys := make([]*btcec.PublicKey, len(in.AllSignerPubkeys))
+	if len(in.AllSignerPubkeys) < 2 {
+		return nil, fmt.Errorf("need at least two signing public keys")
+	}
+
+	for idx, pubKeyBytes := range in.AllSignerPubkeys {
+		pubKey, err := schnorr.ParsePubKey(pubKeyBytes)
+		if err != nil {
+			return nil, fmt.Errorf("error parsing signer public "+
+				"key %d: %v", idx, err)
+		}
+		allSignerPubKeys[idx] = pubKey
+	}
+
+	// Are there any tweaks to apply to the combined public key?
+	tweaks, err := UnmarshalTweaks(in.Tweaks, in.TaprootTweak)
+	if err != nil {
+		return nil, fmt.Errorf("error unmarshaling tweak options: %v",
+			err)
+	}
+
+	// Combine the keys now without creating a session in memory.
+	combinedKey, err := input.MuSig2CombineKeys(allSignerPubKeys, tweaks)
+	if err != nil {
+		return nil, fmt.Errorf("error combining keys: %v", err)
+	}
+
+	var internalKeyBytes []byte
+	if combinedKey.PreTweakedKey != nil {
+		internalKeyBytes = schnorr.SerializePubKey(
+			combinedKey.PreTweakedKey,
+		)
+	}
+
+	return &MuSig2CombineKeysResponse{
+		CombinedKey: schnorr.SerializePubKey(
+			combinedKey.FinalKey,
+		),
+		TaprootInternalKey: internalKeyBytes,
+	}, nil
+}
+
+// MuSig2CreateSession creates a new MuSig2 signing session using the local
+// key identified by the key locator. The complete list of all public keys of
+// all signing parties must be provided, including the public key of the local
+// signing key. If nonces of other parties are already known, they can be
+// submitted as well to reduce the number of RPC calls necessary later on.
+func (s *Server) MuSig2CreateSession(_ context.Context,
+	in *MuSig2SessionRequest) (*MuSig2SessionResponse, error) {
+
+	// A key locator is always mandatory.
+	if in.KeyLoc == nil {
+		return nil, fmt.Errorf("missing key_loc")
+	}
+	keyLoc := keychain.KeyLocator{
+		Family: keychain.KeyFamily(in.KeyLoc.KeyFamily),
+		Index:  uint32(in.KeyLoc.KeyIndex),
+	}
+
+	// Parse the public keys of all signing participants. This must also
+	// include our own, local key.
+	allSignerPubKeys := make([]*btcec.PublicKey, len(in.AllSignerPubkeys))
+	if len(in.AllSignerPubkeys) < 2 {
+		return nil, fmt.Errorf("need at least two signing public keys")
+	}
+
+	for idx, pubKeyBytes := range in.AllSignerPubkeys {
+		pubKey, err := schnorr.ParsePubKey(pubKeyBytes)
+		if err != nil {
+			return nil, fmt.Errorf("error parsing signer public "+
+				"key %d: %v", idx, err)
+		}
+		allSignerPubKeys[idx] = pubKey
+	}
+
+	// We participate a nonce ourselves, so we can't have more nonces than
+	// the total number of participants minus ourselves.
+	maxNonces := len(in.AllSignerPubkeys) - 1
+	if len(in.OtherSignerPublicNonces) > maxNonces {
+		return nil, fmt.Errorf("too many other signer public nonces, "+
+			"got %d but expected a maximum of %d",
+			len(in.OtherSignerPublicNonces), maxNonces)
+	}
+
+	// Parse all other nonces we might already know.
+	otherSignerNonces, err := parseMuSig2PublicNonces(
+		in.OtherSignerPublicNonces, true,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing other nonces: %v", err)
+	}
+
+	// Are there any tweaks to apply to the combined public key?
+	tweaks, err := UnmarshalTweaks(in.Tweaks, in.TaprootTweak)
+	if err != nil {
+		return nil, fmt.Errorf("error unmarshaling tweak options: %v",
+			err)
+	}
+
+	// Register the session with the internal wallet/signer now.
+	session, err := s.cfg.Signer.MuSig2CreateSession(
+		keyLoc, allSignerPubKeys, tweaks, otherSignerNonces,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error registering session: %v", err)
+	}
+
+	var internalKeyBytes []byte
+	if session.TaprootTweak {
+		internalKeyBytes = schnorr.SerializePubKey(
+			session.TaprootInternalKey,
+		)
+	}
+
+	return &MuSig2SessionResponse{
+		SessionId: session.SessionID[:],
+		CombinedKey: schnorr.SerializePubKey(
+			session.CombinedKey,
+		),
+		TaprootInternalKey: internalKeyBytes,
+		LocalPublicNonces:  session.PublicNonce[:],
+		HaveAllNonces:      session.HaveAllNonces,
+	}, nil
+}
+
+// MuSig2RegisterNonces registers one or more public nonces of other signing
+// participants for a session identified by its ID.
+func (s *Server) MuSig2RegisterNonces(_ context.Context,
+	in *MuSig2RegisterNoncesRequest) (*MuSig2RegisterNoncesResponse, error) {
+
+	// Check session ID length.
+	sessionID, err := parseMuSig2SessionID(in.SessionId)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing session ID: %v", err)
+	}
+
+	// Parse the other signing participants' nonces. We can't validate the
+	// number of nonces here because we don't have access to the session in
+	// this context. But the signer will be able to make sure we don't
+	// register more nonces than there are signers (which would mean
+	// something is wrong in the signing setup). But we want at least a
+	// single nonce for each call.
+	otherSignerNonces, err := parseMuSig2PublicNonces(
+		in.OtherSignerPublicNonces, false,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing other nonces: %v", err)
+	}
+
+	// Register the nonces now.
+	haveAllNonces, err := s.cfg.Signer.MuSig2RegisterNonces(
+		sessionID, otherSignerNonces,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error registering nonces: %v", err)
+	}
+
+	return &MuSig2RegisterNoncesResponse{HaveAllNonces: haveAllNonces}, nil
+}
+
+// MuSig2Sign creates a partial signature using the local signing key that was
+// specified when the session was created. This can only be called when all
+// public nonces of all participants are known and have been registered with
+// the session. If this node isn't responsible for combining all the partial
+// signatures, then the cleanup flag should be set, indicating that the session
+// can be removed from memory once the signature was produced.
+func (s *Server) MuSig2Sign(_ context.Context,
+	in *MuSig2SignRequest) (*MuSig2SignResponse, error) {
+
+	// Check session ID length.
+	sessionID, err := parseMuSig2SessionID(in.SessionId)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing session ID: %v", err)
+	}
+
+	// Schnorr signatures only work reliably if the message is 32 bytes.
+	msg := [sha256.Size]byte{}
+	if len(in.MessageDigest) != sha256.Size {
+		return nil, fmt.Errorf("invalid message digest size, got %d "+
+			"but expected %d", len(in.MessageDigest), sha256.Size)
+	}
+	copy(msg[:], in.MessageDigest)
+
+	// Create our own partial signature with the local signing key.
+	partialSig, err := s.cfg.Signer.MuSig2Sign(sessionID, msg, in.Cleanup)
+	if err != nil {
+		return nil, fmt.Errorf("error signing: %v", err)
+	}
+
+	serializedPartialSig, err := input.SerializePartialSignature(partialSig)
+	if err != nil {
+		return nil, fmt.Errorf("error serializing sig: %v", err)
+	}
+
+	return &MuSig2SignResponse{
+		LocalPartialSignature: serializedPartialSig[:],
+	}, nil
+}
+
+// MuSig2CombineSig combines the given partial signature(s) with the local one,
+// if it already exists. Once a partial signature of all participants is
+// registered, the final signature will be combined and returned.
+func (s *Server) MuSig2CombineSig(_ context.Context,
+	in *MuSig2CombineSigRequest) (*MuSig2CombineSigResponse, error) {
+
+	// Check session ID length.
+	sessionID, err := parseMuSig2SessionID(in.SessionId)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing session ID: %v", err)
+	}
+
+	// Parse all other signatures. This can be called multiple times, so we
+	// can't really sanity check how many we already have vs. how many the
+	// user supplied in this call.
+	partialSigs, err := parseMuSig2PartialSignatures(
+		in.OtherPartialSignatures,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing partial signatures: %v",
+			err)
+	}
+
+	// Combine the signatures now, potentially getting the final, full
+	// signature if we've already got all partial ones.
+	finalSig, haveAllSigs, err := s.cfg.Signer.MuSig2CombineSig(
+		sessionID, partialSigs,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error combining signatures: %v", err)
+	}
+
+	return &MuSig2CombineSigResponse{
+		HaveAllSignatures: haveAllSigs,
+		FinalSignature:    finalSig.Serialize(),
+	}, nil
+}
+
 // parseRawKeyBytes checks that the provided raw public key is valid and returns
 // the public key. A nil public key is returned if the length of the rawKeyBytes
 // is zero.
@@ -695,3 +962,111 @@ func parseRawKeyBytes(rawKeyBytes []byte) (*btcec.PublicKey, error) {
 			"specified")
 	}
 }
+
+// parseMuSig2SessionID parses a MuSig2 session ID from a raw byte slice.
+func parseMuSig2SessionID(rawID []byte) (input.MuSig2SessionID, error) {
+	sessionID := input.MuSig2SessionID{}
+
+	// The session ID must be exact in its length.
+	if len(rawID) != sha256.Size {
+		return sessionID, fmt.Errorf("invalid session ID size, got "+
+			"%d but expected %d", len(rawID), sha256.Size)
+	}
+	copy(sessionID[:], rawID)
+
+	return sessionID, nil
+}
+
+// parseMuSig2PublicNonces sanity checks and parses the other signers' public
+// nonces.
+func parseMuSig2PublicNonces(pubNonces [][]byte,
+	emptyAllowed bool) ([][musig2.PubNonceSize]byte, error) {
+
+	// For some calls the nonces are optional while for others it doesn't
+	// make any sense to not specify them (for example for the explicit
+	// nonce registration call there should be at least one nonce).
+	if !emptyAllowed && len(pubNonces) == 0 {
+		return nil, fmt.Errorf("at least one other signer public " +
+			"nonce is required")
+	}
+
+	// Parse all other nonces. This can be called multiple times, so we
+	// can't really sanity check how many we already have vs. how many the
+	// user supplied in this call.
+	otherSignerNonces := make([][musig2.PubNonceSize]byte, len(pubNonces))
+	for idx, otherNonceBytes := range pubNonces {
+		if len(otherNonceBytes) != musig2.PubNonceSize {
+			return nil, fmt.Errorf("invalid public nonce at "+
+				"index %d: invalid length, got %d but "+
+				"expected %d", idx, len(otherNonceBytes),
+				musig2.PubNonceSize)
+		}
+		copy(otherSignerNonces[idx][:], otherNonceBytes)
+	}
+
+	return otherSignerNonces, nil
+}
+
+// parseMuSig2PartialSignatures sanity checks and parses the other signers'
+// partial signatures.
+func parseMuSig2PartialSignatures(
+	partialSignatures [][]byte) ([]*musig2.PartialSignature, error) {
+
+	// We always want at least one partial signature.
+	if len(partialSignatures) == 0 {
+		return nil, fmt.Errorf("at least one partial signature is " +
+			"required")
+	}
+
+	parsedPartialSigs := make(
+		[]*musig2.PartialSignature, len(partialSignatures),
+	)
+	for idx, otherPartialSigBytes := range partialSignatures {
+		sig, err := input.DeserializePartialSignature(
+			otherPartialSigBytes,
+		)
+		if err != nil {
+			return nil, fmt.Errorf("invalid partial signature at "+
+				"index %d: %v", idx, err)
+		}
+
+		parsedPartialSigs[idx] = sig
+	}
+
+	return parsedPartialSigs, nil
+}
+
+// UnmarshalTweaks parses the RPC tweak descriptions into their native
+// counterpart.
+func UnmarshalTweaks(rpcTweaks []*TweakDesc,
+	taprootTweak *TaprootTweakDesc) (*input.MuSig2Tweaks, error) {
+
+	// Parse the generic tweaks first.
+	tweaks := &input.MuSig2Tweaks{
+		GenericTweaks: make([]musig2.KeyTweakDesc, len(rpcTweaks)),
+	}
+	for idx, rpcTweak := range rpcTweaks {
+		if len(rpcTweak.Tweak) == 0 {
+			return nil, fmt.Errorf("tweak cannot be empty")
+		}
+
+		copy(tweaks.GenericTweaks[idx].Tweak[:], rpcTweak.Tweak)
+		tweaks.GenericTweaks[idx].IsXOnly = rpcTweak.IsXOnly
+	}
+
+	// Now parse the taproot specific tweak.
+	if taprootTweak != nil {
+		if taprootTweak.KeySpendOnly {
+			tweaks.TaprootBIP0086Tweak = true
+		} else {
+			if len(taprootTweak.ScriptRoot) == 0 {
+				return nil, fmt.Errorf("script root cannot " +
+					"be empty for non-keyspend")
+			}
+
+			tweaks.TaprootTweak = taprootTweak.ScriptRoot
+		}
+	}
+
+	return tweaks, nil
+}
diff --git a/lnwallet/btcwallet/btcwallet.go b/lnwallet/btcwallet/btcwallet.go
index 41bec9410..c6b8b1a46 100644
--- a/lnwallet/btcwallet/btcwallet.go
+++ b/lnwallet/btcwallet/btcwallet.go
@@ -24,6 +24,7 @@ import (
 	"github.com/btcsuite/btcwallet/walletdb"
 	"github.com/btcsuite/btcwallet/wtxmgr"
 	"github.com/lightningnetwork/lnd/blockcache"
+	"github.com/lightningnetwork/lnd/input"
 	"github.com/lightningnetwork/lnd/keychain"
 	"github.com/lightningnetwork/lnd/kvdb"
 	"github.com/lightningnetwork/lnd/lnwallet"
@@ -98,6 +99,9 @@ type BtcWallet struct {
 	chainKeyScope waddrmgr.KeyScope
 
 	blockCache *blockcache.BlockCache
+
+	musig2Sessions    map[input.MuSig2SessionID]*muSig2State
+	musig2SessionsMtx sync.Mutex
 }
 
 // A compile time check to ensure that BtcWallet implements the
@@ -160,13 +164,14 @@ func New(cfg Config, blockCache *blockcache.BlockCache) (*BtcWallet, error) {
 	}
 
 	return &BtcWallet{
-		cfg:           &cfg,
-		wallet:        wallet,
-		db:            wallet.Database(),
-		chain:         cfg.ChainSource,
-		netParams:     cfg.NetParams,
-		chainKeyScope: chainKeyScope,
-		blockCache:    blockCache,
+		cfg:            &cfg,
+		wallet:         wallet,
+		db:             wallet.Database(),
+		chain:          cfg.ChainSource,
+		netParams:      cfg.NetParams,
+		chainKeyScope:  chainKeyScope,
+		blockCache:     blockCache,
+		musig2Sessions: make(map[input.MuSig2SessionID]*muSig2State),
 	}, nil
 }
 
diff --git a/lnwallet/btcwallet/signer.go b/lnwallet/btcwallet/signer.go
index a205109b5..a52805b7d 100644
--- a/lnwallet/btcwallet/signer.go
+++ b/lnwallet/btcwallet/signer.go
@@ -1,11 +1,13 @@
 package btcwallet
 
 import (
+	"crypto/sha256"
 	"fmt"
 
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
 	"github.com/btcsuite/btcd/btcec/v2/schnorr"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/btcutil/hdkeychain"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
@@ -451,6 +453,257 @@ func (b *BtcWallet) ComputeInputScript(tx *wire.MsgTx,
 	}, nil
 }
 
+// muSig2State is a struct that holds on to the internal signing session state
+// of a MuSig2 session.
+type muSig2State struct {
+	// MuSig2SessionInfo is the associated meta information of the signing
+	// session.
+	input.MuSig2SessionInfo
+
+	// context is the signing context responsible for keeping track of the
+	// public keys involved in the signing process.
+	context *musig2.Context
+
+	// session is the signing session responsible for keeping track of the
+	// nonces and partial signatures involved in the signing process.
+	session *musig2.Session
+}
+
+// MuSig2CreateSession creates a new MuSig2 signing session using the local
+// key identified by the key locator. The complete list of all public keys of
+// all signing parties must be provided, including the public key of the local
+// signing key. If nonces of other parties are already known, they can be
+// submitted as well to reduce the number of method calls necessary later on.
+func (b *BtcWallet) MuSig2CreateSession(keyLoc keychain.KeyLocator,
+	allSignerPubKeys []*btcec.PublicKey, tweaks *input.MuSig2Tweaks,
+	otherSignerNonces [][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo,
+	error) {
+
+	// We need to derive the private key for signing. In the remote signing
+	// setup, this whole RPC call will be forwarded to the signing
+	// instance, which requires it to be stateful.
+	privKey, err := b.fetchPrivKey(&keychain.KeyDescriptor{
+		KeyLocator: keyLoc,
+	})
+	if err != nil {
+		return nil, fmt.Errorf("error deriving private key: %v", err)
+	}
+
+	// The context keeps track of all signing keys and our local key.
+	allOpts := append(
+		[]musig2.ContextOption{
+			musig2.WithKnownSigners(allSignerPubKeys),
+		},
+		tweaks.ToContextOptions()...,
+	)
+	musigContext, err := musig2.NewContext(privKey, true, allOpts...)
+	if err != nil {
+		return nil, fmt.Errorf("error creating MuSig2 signing "+
+			"context: %v", err)
+	}
+
+	// The session keeps track of the own and other nonces.
+	musigSession, err := musigContext.NewSession()
+	if err != nil {
+		return nil, fmt.Errorf("error creating MuSig2 signing "+
+			"session: %v", err)
+	}
+
+	// Add all nonces we might've learned so far.
+	haveAllNonces := false
+	for _, otherSignerNonce := range otherSignerNonces {
+		haveAllNonces, err = musigSession.RegisterPubNonce(
+			otherSignerNonce,
+		)
+		if err != nil {
+			return nil, fmt.Errorf("error registering other "+
+				"signer public nonce: %v", err)
+		}
+	}
+
+	// Register the new session.
+	combinedKey, err := musigContext.CombinedKey()
+	if err != nil {
+		return nil, fmt.Errorf("error getting combined key: %v", err)
+	}
+	session := &muSig2State{
+		MuSig2SessionInfo: input.MuSig2SessionInfo{
+			SessionID: input.NewMuSig2SessionID(
+				combinedKey, musigSession.PublicNonce(),
+			),
+			PublicNonce:   musigSession.PublicNonce(),
+			CombinedKey:   combinedKey,
+			TaprootTweak:  tweaks.HasTaprootTweak(),
+			HaveAllNonces: haveAllNonces,
+		},
+		context: musigContext,
+		session: musigSession,
+	}
+
+	// The internal key is only calculated if we are using a taproot tweak
+	// and need to know it for a potential script spend.
+	if tweaks.HasTaprootTweak() {
+		internalKey, err := musigContext.TaprootInternalKey()
+		if err != nil {
+			return nil, fmt.Errorf("error getting internal key: %v",
+				err)
+		}
+		session.TaprootInternalKey = internalKey
+	}
+
+	// Since we generate new nonces for every session, there is no way that
+	// a session with the same ID already exists. So even if we call the API
+	// twice with the same signers, we still get a new ID.
+	b.musig2SessionsMtx.Lock()
+	b.musig2Sessions[session.SessionID] = session
+	b.musig2SessionsMtx.Unlock()
+
+	return &session.MuSig2SessionInfo, nil
+}
+
+// MuSig2RegisterNonces registers one or more public nonces of other signing
+// participants for a session identified by its ID. This method returns true
+// once we have all nonces for all other signing participants.
+func (b *BtcWallet) MuSig2RegisterNonces(sessionID input.MuSig2SessionID,
+	otherSignerNonces [][musig2.PubNonceSize]byte) (bool, error) {
+
+	// We hold the lock during the whole operation, we don't want any
+	// interference with calls that might come through in parallel for the
+	// same session.
+	b.musig2SessionsMtx.Lock()
+	defer b.musig2SessionsMtx.Unlock()
+
+	session, ok := b.musig2Sessions[sessionID]
+	if !ok {
+		return false, fmt.Errorf("session with ID %x not found",
+			sessionID[:])
+	}
+
+	// Make sure we don't exceed the number of expected nonces as that would
+	// indicate something is wrong with the signing setup.
+	if session.HaveAllNonces {
+		return true, fmt.Errorf("already have all nonces")
+	}
+
+	numSigners := len(session.context.SigningKeys())
+	remainingNonces := numSigners - session.session.NumRegisteredNonces()
+	if len(otherSignerNonces) > remainingNonces {
+		return false, fmt.Errorf("only %d other nonces remaining but "+
+			"trying to register %d more", remainingNonces,
+			len(otherSignerNonces))
+	}
+
+	// Add all nonces we've learned so far.
+	var err error
+	for _, otherSignerNonce := range otherSignerNonces {
+		session.HaveAllNonces, err = session.session.RegisterPubNonce(
+			otherSignerNonce,
+		)
+		if err != nil {
+			return false, fmt.Errorf("error registering other "+
+				"signer public nonce: %v", err)
+		}
+	}
+
+	return session.HaveAllNonces, nil
+}
+
+// MuSig2Sign creates a partial signature using the local signing key
+// that was specified when the session was created. This can only be
+// called when all public nonces of all participants are known and have
+// been registered with the session. If this node isn't responsible for
+// combining all the partial signatures, then the cleanup parameter
+// should be set, indicating that the session can be removed from memory
+// once the signature was produced.
+func (b *BtcWallet) MuSig2Sign(sessionID input.MuSig2SessionID,
+	msg [sha256.Size]byte, cleanUp bool) (*musig2.PartialSignature, error) {
+
+	// We hold the lock during the whole operation, we don't want any
+	// interference with calls that might come through in parallel for the
+	// same session.
+	b.musig2SessionsMtx.Lock()
+	defer b.musig2SessionsMtx.Unlock()
+
+	session, ok := b.musig2Sessions[sessionID]
+	if !ok {
+		return nil, fmt.Errorf("session with ID %x not found",
+			sessionID[:])
+	}
+
+	// We can only sign once we have all other signer's nonces.
+	if !session.HaveAllNonces {
+		return nil, fmt.Errorf("only have %d of %d required nonces",
+			session.session.NumRegisteredNonces(),
+			len(session.context.SigningKeys()))
+	}
+
+	// Create our own partial signature with the local signing key.
+	partialSig, err := session.session.Sign(msg, musig2.WithSortedKeys())
+	if err != nil {
+		return nil, fmt.Errorf("error signing with local key: %v", err)
+	}
+
+	// Clean up our local state if requested.
+	if cleanUp {
+		delete(b.musig2Sessions, sessionID)
+	}
+
+	return partialSig, nil
+}
+
+// MuSig2CombineSig combines the given partial signature(s) with the
+// local one, if it already exists. Once a partial signature of all
+// participants is registered, the final signature will be combined and
+// returned.
+func (b *BtcWallet) MuSig2CombineSig(sessionID input.MuSig2SessionID,
+	partialSigs []*musig2.PartialSignature) (*schnorr.Signature, bool,
+	error) {
+
+	// We hold the lock during the whole operation, we don't want any
+	// interference with calls that might come through in parallel for the
+	// same session.
+	b.musig2SessionsMtx.Lock()
+	defer b.musig2SessionsMtx.Unlock()
+
+	session, ok := b.musig2Sessions[sessionID]
+	if !ok {
+		return nil, false, fmt.Errorf("session with ID %x not found",
+			sessionID[:])
+	}
+
+	// Make sure we don't exceed the number of expected partial signatures
+	// as that would indicate something is wrong with the signing setup.
+	if session.HaveAllSigs {
+		return nil, true, fmt.Errorf("already have all partial" +
+			"signatures")
+	}
+
+	// Add all sigs we got so far.
+	var (
+		finalSig *schnorr.Signature
+		err      error
+	)
+	for _, otherPartialSig := range partialSigs {
+		session.HaveAllSigs, err = session.session.CombineSig(
+			otherPartialSig,
+		)
+		if err != nil {
+			return nil, false, fmt.Errorf("error combining "+
+				"partial signature: %v", err)
+		}
+	}
+
+	// If we have all partial signatures, we should be able to get the
+	// complete signature now. We also remove this session from memory since
+	// there is nothing more left to do.
+	if session.HaveAllSigs {
+		finalSig = session.session.FinalSig()
+		delete(b.musig2Sessions, sessionID)
+	}
+
+	return finalSig, session.HaveAllSigs, nil
+}
+
 // A compile time check to ensure that BtcWallet implements the Signer
 // interface.
 var _ input.Signer = (*BtcWallet)(nil)
diff --git a/lnwallet/rpcwallet/rpcwallet.go b/lnwallet/rpcwallet/rpcwallet.go
index 4871744e0..0463c4d7c 100644
--- a/lnwallet/rpcwallet/rpcwallet.go
+++ b/lnwallet/rpcwallet/rpcwallet.go
@@ -3,6 +3,7 @@ package rpcwallet
 import (
 	"bytes"
 	"context"
+	"crypto/sha256"
 	"crypto/x509"
 	"errors"
 	"fmt"
@@ -11,6 +12,8 @@ import (
 
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/btcutil"
 	"github.com/btcsuite/btcd/btcutil/hdkeychain"
 	"github.com/btcsuite/btcd/btcutil/psbt"
@@ -537,7 +540,7 @@ func (r *RPCKeyRing) SignOutputRaw(tx *wire.MsgTx,
 //
 // NOTE: This method will ignore any tweak parameters set within the
 // passed SignDescriptor as it assumes a set of typical script
-// templates (p2wkh, np2wkh, etc).
+// templates (p2wkh, np2wkh, BIP0086 p2tr, etc).
 //
 // NOTE: This method is part of the input.Signer interface.
 func (r *RPCKeyRing) ComputeInputScript(tx *wire.MsgTx,
@@ -571,6 +574,214 @@ func (r *RPCKeyRing) ComputeInputScript(tx *wire.MsgTx,
 	}, nil
 }
 
+// MuSig2CreateSession creates a new MuSig2 signing session using the local
+// key identified by the key locator. The complete list of all public keys of
+// all signing parties must be provided, including the public key of the local
+// signing key. If nonces of other parties are already known, they can be
+// submitted as well to reduce the number of method calls necessary later on.
+func (r *RPCKeyRing) MuSig2CreateSession(keyLoc keychain.KeyLocator,
+	pubKeys []*btcec.PublicKey, tweaks *input.MuSig2Tweaks,
+	otherNonces [][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo,
+	error) {
+
+	// We need to serialize all data for the RPC call. We can do that by
+	// putting everything directly into the request struct.
+	req := &signrpc.MuSig2SessionRequest{
+		KeyLoc: &signrpc.KeyLocator{
+			KeyFamily: int32(keyLoc.Family),
+			KeyIndex:  int32(keyLoc.Index),
+		},
+		AllSignerPubkeys: make([][]byte, len(pubKeys)),
+		Tweaks: make(
+			[]*signrpc.TweakDesc, len(tweaks.GenericTweaks),
+		),
+		OtherSignerPublicNonces: make([][]byte, len(otherNonces)),
+	}
+	for idx, pubKey := range pubKeys {
+		req.AllSignerPubkeys[idx] = schnorr.SerializePubKey(pubKey)
+	}
+	for idx, genericTweak := range tweaks.GenericTweaks {
+		req.Tweaks[idx] = &signrpc.TweakDesc{
+			Tweak:   genericTweak.Tweak[:],
+			IsXOnly: genericTweak.IsXOnly,
+		}
+	}
+	for idx, nonce := range otherNonces {
+		req.OtherSignerPublicNonces[idx] = make([]byte, len(nonce))
+		copy(req.OtherSignerPublicNonces[idx], nonce[:])
+	}
+	if tweaks.HasTaprootTweak() {
+		req.TaprootTweak = &signrpc.TaprootTweakDesc{
+			KeySpendOnly: tweaks.TaprootBIP0086Tweak,
+			ScriptRoot:   tweaks.TaprootTweak,
+		}
+	}
+
+	ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout)
+	defer cancel()
+
+	resp, err := r.signerClient.MuSig2CreateSession(ctxt, req)
+	if err != nil {
+		err = fmt.Errorf("error creating MuSig2 session in remote "+
+			"signer instance: %v", err)
+
+		// Log as critical as we should shut down if there is no signer.
+		log.Criticalf("RPC signer error: %v", err)
+		return nil, err
+	}
+
+	// De-Serialize all the info back into our native struct.
+	info := &input.MuSig2SessionInfo{
+		TaprootTweak:  tweaks.HasTaprootTweak(),
+		HaveAllNonces: resp.HaveAllNonces,
+	}
+	copy(info.SessionID[:], resp.SessionId)
+	copy(info.PublicNonce[:], resp.LocalPublicNonces)
+
+	info.CombinedKey, err = schnorr.ParsePubKey(resp.CombinedKey)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing combined key: %v", err)
+	}
+
+	if tweaks.HasTaprootTweak() {
+		info.TaprootInternalKey, err = schnorr.ParsePubKey(
+			resp.TaprootInternalKey,
+		)
+		if err != nil {
+			return nil, fmt.Errorf("error parsing internal key: %v",
+				err)
+		}
+	}
+
+	return info, nil
+}
+
+// MuSig2RegisterNonces registers one or more public nonces of other signing
+// participants for a session identified by its ID. This method returns true
+// once we have all nonces for all other signing participants.
+func (r *RPCKeyRing) MuSig2RegisterNonces(sessionID input.MuSig2SessionID,
+	pubNonces [][musig2.PubNonceSize]byte) (bool, error) {
+
+	// We need to serialize all data for the RPC call. We can do that by
+	// putting everything directly into the request struct.
+	req := &signrpc.MuSig2RegisterNoncesRequest{
+		SessionId:               sessionID[:],
+		OtherSignerPublicNonces: make([][]byte, len(pubNonces)),
+	}
+	for idx, nonce := range pubNonces {
+		req.OtherSignerPublicNonces[idx] = make([]byte, len(nonce))
+		copy(req.OtherSignerPublicNonces[idx], nonce[:])
+	}
+
+	ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout)
+	defer cancel()
+
+	resp, err := r.signerClient.MuSig2RegisterNonces(ctxt, req)
+	if err != nil {
+		err = fmt.Errorf("error registering MuSig2 nonces in remote "+
+			"signer instance: %v", err)
+
+		// Log as critical as we should shut down if there is no signer.
+		log.Criticalf("RPC signer error: %v", err)
+		return false, err
+	}
+
+	return resp.HaveAllNonces, nil
+}
+
+// MuSig2Sign creates a partial signature using the local signing key
+// that was specified when the session was created. This can only be
+// called when all public nonces of all participants are known and have
+// been registered with the session. If this node isn't responsible for
+// combining all the partial signatures, then the cleanup parameter
+// should be set, indicating that the session can be removed from memory
+// once the signature was produced.
+func (r *RPCKeyRing) MuSig2Sign(sessionID input.MuSig2SessionID,
+	msg [sha256.Size]byte, cleanUp bool) (*musig2.PartialSignature, error) {
+
+	// We need to serialize all data for the RPC call. We can do that by
+	// putting everything directly into the request struct.
+	req := &signrpc.MuSig2SignRequest{
+		SessionId:     sessionID[:],
+		MessageDigest: msg[:],
+		Cleanup:       cleanUp,
+	}
+
+	ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout)
+	defer cancel()
+
+	resp, err := r.signerClient.MuSig2Sign(ctxt, req)
+	if err != nil {
+		err = fmt.Errorf("error signing MuSig2 session in remote "+
+			"signer instance: %v", err)
+
+		// Log as critical as we should shut down if there is no signer.
+		log.Criticalf("RPC signer error: %v", err)
+		return nil, err
+	}
+
+	partialSig, err := input.DeserializePartialSignature(
+		resp.LocalPartialSignature,
+	)
+	if err != nil {
+		return nil, fmt.Errorf("error parsing partial signature from "+
+			"remote signer: %v", err)
+	}
+
+	return partialSig, nil
+}
+
+// MuSig2CombineSig combines the given partial signature(s) with the
+// local one, if it already exists. Once a partial signature of all
+// participants is registered, the final signature will be combined and
+// returned.
+func (r *RPCKeyRing) MuSig2CombineSig(sessionID input.MuSig2SessionID,
+	partialSigs []*musig2.PartialSignature) (*schnorr.Signature, bool,
+	error) {
+
+	// We need to serialize all data for the RPC call. We can do that by
+	// putting everything directly into the request struct.
+	req := &signrpc.MuSig2CombineSigRequest{
+		SessionId:              sessionID[:],
+		OtherPartialSignatures: make([][]byte, len(partialSigs)),
+	}
+	for idx, partialSig := range partialSigs {
+		rawSig, err := input.SerializePartialSignature(partialSig)
+		if err != nil {
+			return nil, false, fmt.Errorf("error serializing "+
+				"partial signature: %v", err)
+		}
+		req.OtherPartialSignatures[idx] = rawSig[:]
+	}
+
+	ctxt, cancel := context.WithTimeout(context.Background(), r.rpcTimeout)
+	defer cancel()
+
+	resp, err := r.signerClient.MuSig2CombineSig(ctxt, req)
+	if err != nil {
+		err = fmt.Errorf("error combining MuSig2 signatures in remote "+
+			"signer instance: %v", err)
+
+		// Log as critical as we should shut down if there is no signer.
+		log.Criticalf("RPC signer error: %v", err)
+		return nil, false, err
+	}
+
+	// The final signature is only available when we have all the other
+	// partial signatures from all participants.
+	if !resp.HaveAllSignatures {
+		return nil, resp.HaveAllSignatures, nil
+	}
+
+	finalSig, err := schnorr.ParseSignature(resp.FinalSignature)
+	if err != nil {
+		return nil, false, fmt.Errorf("error parsing final signature: "+
+			"%v", err)
+	}
+
+	return finalSig, resp.HaveAllSignatures, nil
+}
+
 // remoteSign signs the input specified in signDesc of the given transaction tx
 // using the remote signing instance.
 func (r *RPCKeyRing) remoteSign(tx *wire.MsgTx, signDesc *input.SignDescriptor,
diff --git a/tools/Dockerfile b/tools/Dockerfile
index 9df610994..452c92738 100644
--- a/tools/Dockerfile
+++ b/tools/Dockerfile
@@ -1,4 +1,4 @@
-FROM golang:1.17.3-buster
+FROM golang:1.18.0-buster
 
 RUN apt-get update && apt-get install -y git
 ENV GOCACHE=/tmp/build/.cache
diff --git a/watchtower/wtmock/signer.go b/watchtower/wtmock/signer.go
index 1bc072636..94ba8171b 100644
--- a/watchtower/wtmock/signer.go
+++ b/watchtower/wtmock/signer.go
@@ -1,10 +1,13 @@
 package wtmock
 
 import (
+	"crypto/sha256"
 	"sync"
 
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/txscript"
 	"github.com/btcsuite/btcd/wire"
 	"github.com/lightningnetwork/lnd/input"
@@ -61,6 +64,50 @@ func (s *MockSigner) ComputeInputScript(tx *wire.MsgTx,
 	panic("not implemented")
 }
 
+// MuSig2CreateSession creates a new MuSig2 signing session using the local
+// key identified by the key locator. The complete list of all public keys of
+// all signing parties must be provided, including the public key of the local
+// signing key. If nonces of other parties are already known, they can be
+// submitted as well to reduce the number of method calls necessary later on.
+func (s *MockSigner) MuSig2CreateSession(keychain.KeyLocator,
+	[]*btcec.PublicKey, *input.MuSig2Tweaks,
+	[][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) {
+
+	return nil, nil
+}
+
+// MuSig2RegisterNonces registers one or more public nonces of other signing
+// participants for a session identified by its ID. This method returns true
+// once we have all nonces for all other signing participants.
+func (s *MockSigner) MuSig2RegisterNonces(input.MuSig2SessionID,
+	[][musig2.PubNonceSize]byte) (bool, error) {
+
+	return false, nil
+}
+
+// MuSig2Sign creates a partial signature using the local signing key
+// that was specified when the session was created. This can only be
+// called when all public nonces of all participants are known and have
+// been registered with the session. If this node isn't responsible for
+// combining all the partial signatures, then the cleanup parameter
+// should be set, indicating that the session can be removed from memory
+// once the signature was produced.
+func (s *MockSigner) MuSig2Sign(input.MuSig2SessionID,
+	[sha256.Size]byte, bool) (*musig2.PartialSignature, error) {
+
+	return nil, nil
+}
+
+// MuSig2CombineSig combines the given partial signature(s) with the
+// local one, if it already exists. Once a partial signature of all
+// participants is registered, the final signature will be combined and
+// returned.
+func (s *MockSigner) MuSig2CombineSig(input.MuSig2SessionID,
+	[]*musig2.PartialSignature) (*schnorr.Signature, bool, error) {
+
+	return nil, false, nil
+}
+
 // AddPrivKey records the passed privKey in the MockSigner's registry of keys it
 // can sign with in the future. A unique key locator is returned, allowing the
 // caller to sign with this key when presented via an input.SignDescriptor.


Test patch:
diff --git a/input/test_utils.go b/input/test_utils.go
index d59f07d50..531a0d672 100644
--- a/input/test_utils.go
+++ b/input/test_utils.go
@@ -2,16 +2,20 @@ package input
 
 import (
 	"bytes"
+	"crypto/sha256"
 	"encoding/hex"
 	"fmt"
 
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/btcutil"
 	"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/keychain"
 )
 
 var (
@@ -130,6 +134,50 @@ func (m *MockSigner) ComputeInputScript(tx *wire.MsgTx, signDesc *SignDescriptor
 	}
 }
 
+// MuSig2CreateSession creates a new MuSig2 signing session using the local
+// key identified by the key locator. The complete list of all public keys of
+// all signing parties must be provided, including the public key of the local
+// signing key. If nonces of other parties are already known, they can be
+// submitted as well to reduce the number of method calls necessary later on.
+func (m *MockSigner) MuSig2CreateSession(keychain.KeyLocator,
+	[]*btcec.PublicKey, *MuSig2Tweaks,
+	[][musig2.PubNonceSize]byte) (*MuSig2SessionInfo, error) {
+
+	return nil, nil
+}
+
+// MuSig2RegisterNonces registers one or more public nonces of other signing
+// participants for a session identified by its ID. This method returns true
+// once we have all nonces for all other signing participants.
+func (m *MockSigner) MuSig2RegisterNonces(MuSig2SessionID,
+	[][musig2.PubNonceSize]byte) (bool, error) {
+
+	return false, nil
+}
+
+// MuSig2Sign creates a partial signature using the local signing key
+// that was specified when the session was created. This can only be
+// called when all public nonces of all participants are known and have
+// been registered with the session. If this node isn't responsible for
+// combining all the partial signatures, then the cleanup parameter
+// should be set, indicating that the session can be removed from memory
+// once the signature was produced.
+func (m *MockSigner) MuSig2Sign(MuSig2SessionID,
+	[sha256.Size]byte, bool) (*musig2.PartialSignature, error) {
+
+	return nil, nil
+}
+
+// MuSig2CombineSig combines the given partial signature(s) with the
+// local one, if it already exists. Once a partial signature of all
+// participants is registered, the final signature will be combined and
+// returned.
+func (m *MockSigner) MuSig2CombineSig(MuSig2SessionID,
+	[]*musig2.PartialSignature) (*schnorr.Signature, bool, error) {
+
+	return nil, false, nil
+}
+
 // findKey searches through all stored private keys and returns one
 // corresponding to the hashed pubkey if it can be found. The public key may
 // either correspond directly to the private key or to the private key with a
diff --git a/lntest/itest/lnd_remote_signer_test.go b/lntest/itest/lnd_remote_signer_test.go
index ecd564519..e332a59ea 100644
--- a/lntest/itest/lnd_remote_signer_test.go
+++ b/lntest/itest/lnd_remote_signer_test.go
@@ -114,6 +114,32 @@ func testRemoteSigner(net *lntest.NetworkHarness, t *harnessTest) {
 		fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) {
 			runSignOutputRaw(tt, net, wo)
 		},
+	}, {
+		name:      "taproot",
+		sendCoins: true,
+		fn: func(tt *harnessTest, wo, carol *lntest.HarnessNode) {
+			ctxt, cancel := context.WithTimeout(
+				ctxb, 3*defaultTimeout,
+			)
+			defer cancel()
+
+			// TODO(guggero): Fix remote taproot signing by adding
+			// the required fields to PSBT.
+			// testTaprootComputeInputScriptKeySpendBip86(
+			//	ctxt, tt, wo, net,
+			// )
+			// testTaprootSignOutputRawScriptSpend(ctxt, tt, wo, net)
+			// testTaprootSignOutputRawKeySpendBip86(
+			// 	ctxt, tt, wo, net,
+			// )
+			// testTaprootSignOutputRawKeySpendRootHash(
+			//	ctxt, tt, wo, net,
+			// )
+			testTaprootMuSig2KeySpendRootHash(ctxt, tt, wo, net)
+			testTaprootMuSig2ScriptSpend(ctxt, tt, wo, net)
+			testTaprootMuSig2KeySpendBip86(ctxt, tt, wo, net)
+			testTaprootMuSig2CombinedLeafKeySpend(ctxt, tt, wo, net)
+		},
 	}}
 
 	for _, st := range subTests {
diff --git a/lntest/itest/lnd_taproot_test.go b/lntest/itest/lnd_taproot_test.go
index 20d5d37dc..e63be8ac1 100644
--- a/lntest/itest/lnd_taproot_test.go
+++ b/lntest/itest/lnd_taproot_test.go
@@ -23,29 +23,48 @@ import (
 	"github.com/stretchr/testify/require"
 )
 
+const (
+	testTaprootKeyFamily = 77
+	testAmount           = 800_000
+)
+
+var (
+	dummyInternalKeyBytes, _ = hex.DecodeString(
+		"03464805f5468e294d88cf15a3f06aef6c89d63ef1bd7b42db2e0c74c1ac" +
+			"eb90fe",
+	)
+	dummyInternalKey, _ = btcec.ParsePubKey(dummyInternalKeyBytes)
+)
+
 // testTaproot ensures that the daemon can send to and spend from taproot (p2tr)
 // outputs.
 func testTaproot(net *lntest.NetworkHarness, t *harnessTest) {
 	ctxb := context.Background()
-	ctxt, cancel := context.WithTimeout(ctxb, defaultTimeout)
+	ctxt, cancel := context.WithTimeout(ctxb, 2*defaultTimeout)
 	defer cancel()
 
-	testTaprootKeySpend(ctxt, t, net)
-	testTaprootScriptSpend(ctxt, t, net)
-	testTaprootKeySpendRPC(ctxt, t, net)
+	testTaprootComputeInputScriptKeySpendBip86(ctxt, t, net.Alice, net)
+	testTaprootSignOutputRawScriptSpend(ctxt, t, net.Alice, net)
+	testTaprootSignOutputRawKeySpendBip86(ctxt, t, net.Alice, net)
+	testTaprootSignOutputRawKeySpendRootHash(ctxt, t, net.Alice, net)
+	testTaprootMuSig2KeySpendBip86(ctxt, t, net.Alice, net)
+	testTaprootMuSig2KeySpendRootHash(ctxt, t, net.Alice, net)
+	testTaprootMuSig2ScriptSpend(ctxt, t, net.Alice, net)
+	testTaprootMuSig2CombinedLeafKeySpend(ctxt, t, net.Alice, net)
 }
 
-// testTaprootKeySpend tests sending to and spending from p2tr key spend only
-// (BIP-0086) addresses.
-func testTaprootKeySpend(ctxt context.Context, t *harnessTest,
-	net *lntest.NetworkHarness) {
+// testTaprootComputeInputScriptKeySpendBip86 tests sending to and spending from
+// p2tr key spend only (BIP-0086) addresses through the SendCoins RPC which
+// internally uses the ComputeInputScript method for signing.
+func testTaprootComputeInputScriptKeySpendBip86(ctxt context.Context,
+	t *harnessTest, alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
 
 	// We'll start the test by sending Alice some coins, which she'll use to
 	// send to herself on a p2tr output.
-	net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, net.Alice)
+	net.SendCoins(t.t, btcutil.SatoshiPerBitcoin, alice)
 
 	// Let's create a p2tr address now.
-	p2trResp, err := net.Alice.NewAddress(ctxt, &lnrpc.NewAddressRequest{
+	p2trResp, err := alice.NewAddress(ctxt, &lnrpc.NewAddressRequest{
 		Type: lnrpc.AddressType_TAPROOT_PUBKEY,
 	})
 	require.NoError(t.t, err)
@@ -57,7 +76,7 @@ func testTaprootKeySpend(ctxt context.Context, t *harnessTest,
 
 	// Send the coins from Alice's wallet to her own, but to the new p2tr
 	// address.
-	_, err = net.Alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
+	_, err = alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
 		Addr:   p2trResp.Address,
 		Amount: 0.5 * btcutil.SatoshiPerBitcoin,
 	})
@@ -72,80 +91,38 @@ func testTaprootKeySpend(ctxt context.Context, t *harnessTest,
 		TxidBytes:   txid[:],
 		OutputIndex: uint32(p2trOutputIndex),
 	}
-	assertWalletUnspent(t, net.Alice, op)
+	assertWalletUnspent(t, alice, op)
 
 	// Mine a block to clean up the mempool.
 	mineBlocks(t, net, 1, 1)
 
 	// Let's sweep the whole wallet to a new p2tr address, making sure we
 	// can sign transactions with v0 and v1 inputs.
-	p2trResp, err = net.Alice.NewAddress(ctxt, &lnrpc.NewAddressRequest{
+	p2trResp, err = alice.NewAddress(ctxt, &lnrpc.NewAddressRequest{
 		Type: lnrpc.AddressType_TAPROOT_PUBKEY,
 	})
 	require.NoError(t.t, err)
 
-	_, err = net.Alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
+	_, err = alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
 		Addr:    p2trResp.Address,
 		SendAll: true,
 	})
 	require.NoError(t.t, err)
 
-	// Wait until the wallet cleaning sweep tx is found.
-	txid, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
-	require.NoError(t.t, err)
-
-	// Wait until bob has seen the tx and considers it as owned.
-	p2trOutputIndex = getOutputIndex(t, net.Miner, txid, p2trResp.Address)
-	op = &lnrpc.OutPoint{
-		TxidBytes:   txid[:],
-		OutputIndex: uint32(p2trOutputIndex),
-	}
-	assertWalletUnspent(t, net.Alice, op)
-
-	// Before we confirm the transaction, let's register a confirmation
-	// listener for it, which we expect to fire after mining a block.
-	p2trAddr, err := btcutil.DecodeAddress(
-		p2trResp.Address, harnessNetParams,
-	)
-	require.NoError(t.t, err)
-	p2trPkScript, err := txscript.PayToAddrScript(p2trAddr)
-	require.NoError(t.t, err)
-
-	_, currentHeight, err := net.Miner.Client.GetBestBlock()
-	require.NoError(t.t, err)
-	confClient, err := net.Alice.ChainClient.RegisterConfirmationsNtfn(
-		ctxt, &chainrpc.ConfRequest{
-			Script:     p2trPkScript,
-			Txid:       txid[:],
-			HeightHint: uint32(currentHeight),
-			NumConfs:   1,
-		},
-	)
-	require.NoError(t.t, err)
-
-	// Mine another block to clean up the mempool.
-	mineBlocks(t, net, 1, 1)
-
-	// We now expect our confirmation to go through.
-	confMsg, err := confClient.Recv()
-	require.NoError(t.t, err)
-	conf := confMsg.GetConf()
-	require.NotNil(t.t, conf)
-	require.Equal(t.t, conf.BlockHeight, uint32(currentHeight+1))
+	// Make sure the coins sent to the address are confirmed correctly,
+	// including the confirmation notification.
+	confirmAddress(ctxt, t, net, alice, p2trResp.Address)
 }
 
-// testTaprootScriptSpend tests sending to and spending from p2tr script
-// addresses.
-func testTaprootScriptSpend(ctxt context.Context, t *harnessTest,
-	net *lntest.NetworkHarness) {
+// testTaprootSignOutputRawScriptSpend tests sending to and spending from p2tr
+// script addresses using the script path with the SignOutputRaw RPC.
+func testTaprootSignOutputRawScriptSpend(ctxt context.Context, t *harnessTest,
+	alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
 
 	// For the next step, we need a public key. Let's use a special family
 	// for this.
-	const taprootKeyFamily = 77
-	keyDesc, err := net.Alice.WalletKitClient.DeriveNextKey(
-		ctxt, &walletrpc.KeyReq{
-			KeyFamily: taprootKeyFamily,
-		},
+	keyDesc, err := alice.WalletKitClient.DeriveNextKey(
+		ctxt, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
 	)
 	require.NoError(t.t, err)
 
@@ -159,50 +136,21 @@ func testTaprootScriptSpend(ctxt context.Context, t *harnessTest,
 	// Let's add a second script output as well to test the partial reveal.
 	leaf2 := testScriptSchnorrSig(t.t, leafSigningKey)
 
-	dummyInternalKeyBytes, _ := hex.DecodeString(
-		"03464805f5468e294d88cf15a3f06aef6c89d63ef1bd7b42db2e0c74c1ac" +
-			"eb90fe",
-	)
-	dummyInternalKey, _ := btcec.ParsePubKey(dummyInternalKeyBytes)
-
+	inclusionProof := leaf1.TapHash()
 	tapscript := input.TapscriptPartialReveal(
-		dummyInternalKey, leaf2, leaf1.TapHash(),
+		dummyInternalKey, leaf2, inclusionProof[:],
 	)
 	taprootKey, err := tapscript.TaprootKey()
 	require.NoError(t.t, err)
 
-	tapScriptAddr, err := btcutil.NewAddressTaproot(
-		schnorr.SerializePubKey(taprootKey), harnessNetParams,
-	)
-	require.NoError(t.t, err)
-	p2trPkScript, err := txscript.PayToAddrScript(tapScriptAddr)
-	require.NoError(t.t, err)
-
 	// Send some coins to the generated tapscript address.
-	_, err = net.Alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
-		Addr:   tapScriptAddr.String(),
-		Amount: 800_000,
-	})
-	require.NoError(t.t, err)
-
-	// Wait until the TX is found in the mempool.
-	txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
-	require.NoError(t.t, err)
-
-	p2trOutputIndex := getOutputIndex(
-		t, net.Miner, txid, tapScriptAddr.String(),
+	p2trOutpoint, p2trPkScript := sendToTaprootOutput(
+		ctxt, t, net, alice, taprootKey, testAmount,
 	)
-	p2trOutpoint := wire.OutPoint{
-		Hash:  *txid,
-		Index: uint32(p2trOutputIndex),
-	}
-
-	// Clear the mempool.
-	mineBlocks(t, net, 1, 1)
 
 	// Spend the output again, this time back to a p2wkh address.
 	p2wkhAddr, p2wkhPkScript := newAddrWithScript(
-		ctxt, t.t, net.Alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
+		ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
 	)
 
 	// Create fee estimation for a p2tr input and p2wkh output.
@@ -219,7 +167,7 @@ func testTaprootScriptSpend(ctxt context.Context, t *harnessTest,
 	tx.TxIn = []*wire.TxIn{{
 		PreviousOutPoint: p2trOutpoint,
 	}}
-	value := int64(800_000 - requiredFee)
+	value := int64(testAmount - requiredFee)
 	tx.TxOut = []*wire.TxOut{{
 		PkScript: p2wkhPkScript,
 		Value:    value,
@@ -230,13 +178,13 @@ func testTaprootScriptSpend(ctxt context.Context, t *harnessTest,
 
 	utxoInfo := []*signrpc.TxOut{{
 		PkScript: p2trPkScript,
-		Value:    800_000,
+		Value:    testAmount,
 	}}
 
 	// Before we actually sign, we want to make sure that we get an error
 	// when we try to sign for a Taproot output without specifying all UTXO
 	// information.
-	_, err = net.Alice.SignerClient.SignOutputRaw(
+	_, err = alice.SignerClient.SignOutputRaw(
 		ctxt, &signrpc.SignReq{
 			RawTxBytes: buf.Bytes(),
 			SignDescs: []*signrpc.SignDescriptor{{
@@ -255,7 +203,7 @@ func testTaprootScriptSpend(ctxt context.Context, t *harnessTest,
 	)
 
 	// Do the actual signing now.
-	signResp, err := net.Alice.SignerClient.SignOutputRaw(
+	signResp, err := alice.SignerClient.SignOutputRaw(
 		ctxt, &signrpc.SignReq{
 			RawTxBytes: buf.Bytes(),
 			SignDescs: []*signrpc.SignDescriptor{{
@@ -280,93 +228,126 @@ func testTaprootScriptSpend(ctxt context.Context, t *harnessTest,
 		controlBlockBytes,
 	}
 
-	buf.Reset()
-	require.NoError(t.t, tx.Serialize(&buf))
+	// Serialize, weigh and publish the TX now, then make sure the
+	// coins are sent and confirmed to the final sweep destination address.
+	publishTxAndConfirmSweep(
+		ctxt, t, net, alice, tx, estimatedWeight,
+		&chainrpc.SpendRequest{
+			Outpoint: &chainrpc.Outpoint{
+				Hash:  p2trOutpoint.Hash[:],
+				Index: p2trOutpoint.Index,
+			},
+			Script: p2trPkScript,
+		},
+		p2wkhAddr.String(),
+	)
+}
 
-	// Since Schnorr signatures are fixed size, we must be able to estimate
-	// the size of this transaction exactly.
-	txWeight := blockchain.GetTransactionWeight(btcutil.NewTx(tx))
-	require.Equal(t.t, txWeight, estimatedWeight)
+// testTaprootSignOutputRawKeySpendBip86 tests that a tapscript address can
+// also be spent using the key spend path through the SignOutputRaw RPC using a
+// BIP0086 key spend only commitment.
+func testTaprootSignOutputRawKeySpendBip86(ctxt context.Context,
+	t *harnessTest, alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
 
-	// Before we publish the tx that spends the p2tr transaction, we want to
-	// register a spend listener that we expect to fire after mining the
-	// block.
-	_, currentHeight, err := net.Miner.Client.GetBestBlock()
+	// For the next step, we need a public key. Let's use a special family
+	// for this.
+	keyDesc, err := alice.WalletKitClient.DeriveNextKey(
+		ctxt, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
+	)
 	require.NoError(t.t, err)
 
-	// For a Taproot output we cannot leave the outpoint empty. Let's make
-	// sure the API returns the correct error here.
-	spendClient, err := net.Alice.ChainClient.RegisterSpendNtfn(
-		ctxt, &chainrpc.SpendRequest{
-			Script:     p2trPkScript,
-			HeightHint: uint32(currentHeight),
-		},
-	)
+	internalKey, err := btcec.ParsePubKey(keyDesc.RawKeyBytes)
 	require.NoError(t.t, err)
 
-	// The error is only thrown when trying to read a message.
-	_, err = spendClient.Recv()
-	require.Contains(
-		t.t, err.Error(),
-		"cannot register witness v1 spend request without outpoint",
+	// We want to make sure we can still use a tweaked key, even if it ends
+	// up being essentially double tweaked because of the taproot root hash.
+	dummyKeyTweak := sha256.Sum256([]byte("this is a key tweak"))
+	internalKey = input.TweakPubKeyWithTweak(internalKey, dummyKeyTweak[:])
+
+	// Our taproot key is a BIP0086 key spend only construction that just
+	// commits to the internal key and no root hash.
+	taprootKey := txscript.ComputeTaprootKeyNoScript(internalKey)
+
+	// Send some coins to the generated tapscript address.
+	p2trOutpoint, p2trPkScript := sendToTaprootOutput(
+		ctxt, t, net, alice, taprootKey, testAmount,
 	)
 
-	// Now try again, this time with the outpoint set.
-	spendClient, err = net.Alice.ChainClient.RegisterSpendNtfn(
-		ctxt, &chainrpc.SpendRequest{
-			Outpoint: &chainrpc.Outpoint{
-				Hash:  p2trOutpoint.Hash[:],
-				Index: p2trOutpoint.Index,
-			},
-			Script:     p2trPkScript,
-			HeightHint: uint32(currentHeight),
-		},
+	// Spend the output again, this time back to a p2wkh address.
+	p2wkhAddr, p2wkhPkScript := newAddrWithScript(
+		ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
 	)
-	require.NoError(t.t, err)
 
-	_, err = net.Alice.WalletKitClient.PublishTransaction(
-		ctxt, &walletrpc.Transaction{
-			TxHex: buf.Bytes(),
+	// Create fee estimation for a p2tr input and p2wkh output.
+	feeRate := chainfee.SatPerKWeight(12500)
+	estimator := input.TxWeightEstimator{}
+	estimator.AddTaprootKeySpendInput(txscript.SigHashDefault)
+	estimator.AddP2WKHOutput()
+	estimatedWeight := int64(estimator.Weight())
+	requiredFee := feeRate.FeeForWeight(estimatedWeight)
+
+	tx := wire.NewMsgTx(2)
+	tx.TxIn = []*wire.TxIn{{
+		PreviousOutPoint: p2trOutpoint,
+	}}
+	value := int64(testAmount - requiredFee)
+	tx.TxOut = []*wire.TxOut{{
+		PkScript: p2wkhPkScript,
+		Value:    value,
+	}}
+
+	var buf bytes.Buffer
+	require.NoError(t.t, tx.Serialize(&buf))
+
+	utxoInfo := []*signrpc.TxOut{{
+		PkScript: p2trPkScript,
+		Value:    testAmount,
+	}}
+	signResp, err := alice.SignerClient.SignOutputRaw(
+		ctxt, &signrpc.SignReq{
+			RawTxBytes: buf.Bytes(),
+			SignDescs: []*signrpc.SignDescriptor{{
+				Output:          utxoInfo[0],
+				InputIndex:      0,
+				KeyDesc:         keyDesc,
+				SingleTweak:     dummyKeyTweak[:],
+				Sighash:         uint32(txscript.SigHashDefault),
+				TaprootKeySpend: true,
+			}},
+			PrevOutputs: utxoInfo,
 		},
 	)
 	require.NoError(t.t, err)
 
-	// Wait until the spending tx is found.
-	txid, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
-	require.NoError(t.t, err)
-	p2wpkhOutputIndex := getOutputIndex(
-		t, net.Miner, txid, p2wkhAddr.String(),
-	)
-	op := &lnrpc.OutPoint{
-		TxidBytes:   txid[:],
-		OutputIndex: uint32(p2wpkhOutputIndex),
+	tx.TxIn[0].Witness = wire.TxWitness{
+		signResp.RawSigs[0],
 	}
-	assertWalletUnspent(t, net.Alice, op)
-
-	// Mine another block to clean up the mempool and to make sure the spend
-	// tx is actually included in a block.
-	mineBlocks(t, net, 1, 1)
 
-	// We now expect our spend event to go through.
-	spendMsg, err := spendClient.Recv()
-	require.NoError(t.t, err)
-	spend := spendMsg.GetSpend()
-	require.NotNil(t.t, spend)
-	require.Equal(t.t, spend.SpendingHeight, uint32(currentHeight+1))
+	// Serialize, weigh and publish the TX now, then make sure the
+	// coins are sent and confirmed to the final sweep destination address.
+	publishTxAndConfirmSweep(
+		ctxt, t, net, alice, tx, estimatedWeight,
+		&chainrpc.SpendRequest{
+			Outpoint: &chainrpc.Outpoint{
+				Hash:  p2trOutpoint.Hash[:],
+				Index: p2trOutpoint.Index,
+			},
+			Script: p2trPkScript,
+		},
+		p2wkhAddr.String(),
+	)
 }
 
-// testTaprootKeySpendRPC tests that a tapscript address can also be spent using
-// the key spend path through the RPC.
-func testTaprootKeySpendRPC(ctxt context.Context, t *harnessTest,
-	net *lntest.NetworkHarness) {
+// testTaprootSignOutputRawKeySpendRootHash tests that a tapscript address can
+// also be spent using the key spend path through the SignOutputRaw RPC using a
+// tapscript root hash.
+func testTaprootSignOutputRawKeySpendRootHash(ctxt context.Context,
+	t *harnessTest, alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
 
 	// For the next step, we need a public key. Let's use a special family
 	// for this.
-	const taprootKeyFamily = 77
-	keyDesc, err := net.Alice.WalletKitClient.DeriveNextKey(
-		ctxt, &walletrpc.KeyReq{
-			KeyFamily: taprootKeyFamily,
-		},
+	keyDesc, err := alice.WalletKitClient.DeriveNextKey(
+		ctxt, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
 	)
 	require.NoError(t.t, err)
 
@@ -385,34 +366,14 @@ func testTaprootKeySpendRPC(ctxt context.Context, t *harnessTest,
 	rootHash := leaf1.TapHash()
 	taprootKey := txscript.ComputeTaprootOutputKey(internalKey, rootHash[:])
 
-	tapScriptAddr, err := btcutil.NewAddressTaproot(
-		schnorr.SerializePubKey(taprootKey), harnessNetParams,
-	)
-	require.NoError(t.t, err)
-	p2trPkScript, err := txscript.PayToAddrScript(tapScriptAddr)
-	require.NoError(t.t, err)
-
 	// Send some coins to the generated tapscript address.
-	_, err = net.Alice.SendCoins(ctxt, &lnrpc.SendCoinsRequest{
-		Addr:   tapScriptAddr.String(),
-		Amount: 800_000,
-	})
-	require.NoError(t.t, err)
-
-	// Wait until the TX is found in the mempool.
-	txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
-	require.NoError(t.t, err)
-
-	p2trOutputIndex := getOutputIndex(
-		t, net.Miner, txid, tapScriptAddr.String(),
+	p2trOutpoint, p2trPkScript := sendToTaprootOutput(
+		ctxt, t, net, alice, taprootKey, testAmount,
 	)
 
-	// Clear the mempool.
-	mineBlocks(t, net, 1, 1)
-
 	// Spend the output again, this time back to a p2wkh address.
 	p2wkhAddr, p2wkhPkScript := newAddrWithScript(
-		ctxt, t.t, net.Alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
+		ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
 	)
 
 	// Create fee estimation for a p2tr input and p2wkh output.
@@ -425,12 +386,9 @@ func testTaprootKeySpendRPC(ctxt context.Context, t *harnessTest,
 
 	tx := wire.NewMsgTx(2)
 	tx.TxIn = []*wire.TxIn{{
-		PreviousOutPoint: wire.OutPoint{
-			Hash:  *txid,
-			Index: uint32(p2trOutputIndex),
-		},
+		PreviousOutPoint: p2trOutpoint,
 	}}
-	value := int64(800_000 - requiredFee)
+	value := int64(testAmount - requiredFee)
 	tx.TxOut = []*wire.TxOut{{
 		PkScript: p2wkhPkScript,
 		Value:    value,
@@ -441,9 +399,9 @@ func testTaprootKeySpendRPC(ctxt context.Context, t *harnessTest,
 
 	utxoInfo := []*signrpc.TxOut{{
 		PkScript: p2trPkScript,
-		Value:    800_000,
+		Value:    testAmount,
 	}}
-	signResp, err := net.Alice.SignerClient.SignOutputRaw(
+	signResp, err := alice.SignerClient.SignOutputRaw(
 		ctxt, &signrpc.SignReq{
 			RawTxBytes: buf.Bytes(),
 			SignDescs: []*signrpc.SignDescriptor{{
@@ -464,37 +422,538 @@ func testTaprootKeySpendRPC(ctxt context.Context, t *harnessTest,
 		signResp.RawSigs[0],
 	}
 
-	buf.Reset()
+	// Serialize, weigh and publish the TX now, then make sure the
+	// coins are sent and confirmed to the final sweep destination address.
+	publishTxAndConfirmSweep(
+		ctxt, t, net, alice, tx, estimatedWeight,
+		&chainrpc.SpendRequest{
+			Outpoint: &chainrpc.Outpoint{
+				Hash:  p2trOutpoint.Hash[:],
+				Index: p2trOutpoint.Index,
+			},
+			Script: p2trPkScript,
+		},
+		p2wkhAddr.String(),
+	)
+}
+
+// testTaprootMuSig2KeySpendBip86 tests that a combined MuSig2 key can also be
+// used as a BIP-0086 key spend only key.
+func testTaprootMuSig2KeySpendBip86(ctxt context.Context, t *harnessTest,
+	alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
+
+	// We're not going to commit to a script. So our taproot tweak will be
+	// empty and just specify the necessary flag.
+	taprootTweak := &signrpc.TaprootTweakDesc{
+		KeySpendOnly: true,
+	}
+
+	keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
+		ctxt, t, alice,
+	)
+	_, taprootKey, sessResp1, sessResp2, sessResp3 := createMuSigSessions(
+		ctxt, t, alice, taprootTweak, keyDesc1, keyDesc2, keyDesc3,
+		allPubKeys,
+	)
+
+	// Send some coins to the generated tapscript address.
+	p2trOutpoint, p2trPkScript := sendToTaprootOutput(
+		ctxt, t, net, alice, taprootKey, testAmount,
+	)
+
+	// Spend the output again, this time back to a p2wkh address.
+	p2wkhAddr, p2wkhPkScript := newAddrWithScript(
+		ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
+	)
+
+	// Create fee estimation for a p2tr input and p2wkh output.
+	feeRate := chainfee.SatPerKWeight(12500)
+	estimator := input.TxWeightEstimator{}
+	estimator.AddTaprootKeySpendInput(txscript.SigHashDefault)
+	estimator.AddP2WKHOutput()
+	estimatedWeight := int64(estimator.Weight())
+	requiredFee := feeRate.FeeForWeight(estimatedWeight)
+
+	tx := wire.NewMsgTx(2)
+	tx.TxIn = []*wire.TxIn{{
+		PreviousOutPoint: p2trOutpoint,
+	}}
+	value := int64(testAmount - requiredFee)
+	tx.TxOut = []*wire.TxOut{{
+		PkScript: p2wkhPkScript,
+		Value:    value,
+	}}
+
+	var buf bytes.Buffer
 	require.NoError(t.t, tx.Serialize(&buf))
 
-	// Since Schnorr signatures are fixed size, we must be able to estimate
-	// the size of this transaction exactly.
-	txWeight := blockchain.GetTransactionWeight(btcutil.NewTx(tx))
-	require.Equal(t.t, txWeight, estimatedWeight)
+	utxoInfo := []*signrpc.TxOut{{
+		PkScript: p2trPkScript,
+		Value:    testAmount,
+	}}
 
-	_, err = net.Alice.WalletKitClient.PublishTransaction(
-		ctxt, &walletrpc.Transaction{
-			TxHex: buf.Bytes(),
+	// We now need to create the raw sighash of the transaction, as that
+	// will be the message we're signing collaboratively.
+	prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
+		utxoInfo[0].PkScript, utxoInfo[0].Value,
+	)
+	sighashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
+
+	sigHash, err := txscript.CalcTaprootSignatureHash(
+		sighashes, txscript.SigHashDefault, tx, 0, prevOutputFetcher,
+	)
+	require.NoError(t.t, err)
+
+	// Now that we have the transaction prepared, we need to start with the
+	// signing. We simulate all three parties here, so we need to do
+	// everything three times. But because we're going to use session 1 to
+	// combine everything, we don't need its response, as it will store its
+	// own signature.
+	_, err = alice.SignerClient.MuSig2Sign(
+		ctxt, &signrpc.MuSig2SignRequest{
+			SessionId:     sessResp1.SessionId,
+			MessageDigest: sigHash,
 		},
 	)
 	require.NoError(t.t, err)
 
-	// Wait until the spending tx is found.
-	txid, err = waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
+	signResp2, err := alice.SignerClient.MuSig2Sign(
+		ctxt, &signrpc.MuSig2SignRequest{
+			SessionId:     sessResp2.SessionId,
+			MessageDigest: sigHash,
+			Cleanup:       true,
+		},
+	)
 	require.NoError(t.t, err)
-	p2wpkhOutputIndex := getOutputIndex(
-		t, net.Miner, txid, p2wkhAddr.String(),
+
+	signResp3, err := alice.SignerClient.MuSig2Sign(
+		ctxt, &signrpc.MuSig2SignRequest{
+			SessionId:     sessResp3.SessionId,
+			MessageDigest: sigHash,
+			Cleanup:       true,
+		},
 	)
-	op := &lnrpc.OutPoint{
-		TxidBytes:   txid[:],
-		OutputIndex: uint32(p2wpkhOutputIndex),
-	}
-	assertWalletUnspent(t, net.Alice, op)
+	require.NoError(t.t, err)
 
-	// Mine another block to clean up the mempool and to make sure the spend
-	// tx is actually included in a block.
-	mineBlocks(t, net, 1, 1)
-}
+	// Luckily only one of the signers needs to combine the signature, so
+	// let's do that now.
+	combineReq1, err := alice.SignerClient.MuSig2CombineSig(
+		ctxt, &signrpc.MuSig2CombineSigRequest{
+			SessionId: sessResp1.SessionId,
+			OtherPartialSignatures: [][]byte{
+				signResp2.LocalPartialSignature,
+				signResp3.LocalPartialSignature,
+			},
+		},
+	)
+	require.NoError(t.t, err)
+	require.Equal(t.t, true, combineReq1.HaveAllSignatures)
+	require.NotEmpty(t.t, combineReq1.FinalSignature)
+
+	sig, err := schnorr.ParseSignature(combineReq1.FinalSignature)
+	require.NoError(t.t, err)
+	require.True(t.t, sig.Verify(sigHash, taprootKey))
+
+	tx.TxIn[0].Witness = wire.TxWitness{
+		combineReq1.FinalSignature,
+	}
+
+	// Serialize, weigh and publish the TX now, then make sure the
+	// coins are sent and confirmed to the final sweep destination address.
+	publishTxAndConfirmSweep(
+		ctxt, t, net, alice, tx, estimatedWeight,
+		&chainrpc.SpendRequest{
+			Outpoint: &chainrpc.Outpoint{
+				Hash:  p2trOutpoint.Hash[:],
+				Index: p2trOutpoint.Index,
+			},
+			Script: p2trPkScript,
+		},
+		p2wkhAddr.String(),
+	)
+}
+
+// testTaprootMuSig2KeySpendRootHash tests that a tapscript address can also be
+// spent using a MuSig2 combined key.
+func testTaprootMuSig2KeySpendRootHash(ctxt context.Context, t *harnessTest,
+	alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
+
+	// We're going to commit to a script as well. This is a hash lock with a
+	// simple preimage of "foobar". We need to know this upfront so, we can
+	// specify the taproot tweak with the root hash when creating the Musig2
+	// signing session.
+	leaf1 := testScriptHashLock(t.t, []byte("foobar"))
+	rootHash := leaf1.TapHash()
+	taprootTweak := &signrpc.TaprootTweakDesc{
+		ScriptRoot: rootHash[:],
+	}
+
+	keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
+		ctxt, t, alice,
+	)
+	_, taprootKey, sessResp1, sessResp2, sessResp3 := createMuSigSessions(
+		ctxt, t, alice, taprootTweak, keyDesc1, keyDesc2, keyDesc3,
+		allPubKeys,
+	)
+
+	// Send some coins to the generated tapscript address.
+	p2trOutpoint, p2trPkScript := sendToTaprootOutput(
+		ctxt, t, net, alice, taprootKey, testAmount,
+	)
+
+	// Spend the output again, this time back to a p2wkh address.
+	p2wkhAddr, p2wkhPkScript := newAddrWithScript(
+		ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
+	)
+
+	// Create fee estimation for a p2tr input and p2wkh output.
+	feeRate := chainfee.SatPerKWeight(12500)
+	estimator := input.TxWeightEstimator{}
+	estimator.AddTaprootKeySpendInput(txscript.SigHashDefault)
+	estimator.AddP2WKHOutput()
+	estimatedWeight := int64(estimator.Weight())
+	requiredFee := feeRate.FeeForWeight(estimatedWeight)
+
+	tx := wire.NewMsgTx(2)
+	tx.TxIn = []*wire.TxIn{{
+		PreviousOutPoint: p2trOutpoint,
+	}}
+	value := int64(testAmount - requiredFee)
+	tx.TxOut = []*wire.TxOut{{
+		PkScript: p2wkhPkScript,
+		Value:    value,
+	}}
+
+	var buf bytes.Buffer
+	require.NoError(t.t, tx.Serialize(&buf))
+
+	utxoInfo := []*signrpc.TxOut{{
+		PkScript: p2trPkScript,
+		Value:    testAmount,
+	}}
+
+	// We now need to create the raw sighash of the transaction, as that
+	// will be the message we're signing collaboratively.
+	prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
+		utxoInfo[0].PkScript, utxoInfo[0].Value,
+	)
+	sighashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
+
+	sigHash, err := txscript.CalcTaprootSignatureHash(
+		sighashes, txscript.SigHashDefault, tx, 0, prevOutputFetcher,
+	)
+	require.NoError(t.t, err)
+
+	// Now that we have the transaction prepared, we need to start with the
+	// signing. We simulate all three parties here, so we need to do
+	// everything three times. But because we're going to use session 1 to
+	// combine everything, we don't need its response, as it will store its
+	// own signature.
+	_, err = alice.SignerClient.MuSig2Sign(
+		ctxt, &signrpc.MuSig2SignRequest{
+			SessionId:     sessResp1.SessionId,
+			MessageDigest: sigHash,
+		},
+	)
+	require.NoError(t.t, err)
+
+	signResp2, err := alice.SignerClient.MuSig2Sign(
+		ctxt, &signrpc.MuSig2SignRequest{
+			SessionId:     sessResp2.SessionId,
+			MessageDigest: sigHash,
+			Cleanup:       true,
+		},
+	)
+	require.NoError(t.t, err)
+
+	signResp3, err := alice.SignerClient.MuSig2Sign(
+		ctxt, &signrpc.MuSig2SignRequest{
+			SessionId:     sessResp3.SessionId,
+			MessageDigest: sigHash,
+			Cleanup:       true,
+		},
+	)
+	require.NoError(t.t, err)
+
+	// Luckily only one of the signers needs to combine the signature, so
+	// let's do that now.
+	combineReq1, err := alice.SignerClient.MuSig2CombineSig(
+		ctxt, &signrpc.MuSig2CombineSigRequest{
+			SessionId: sessResp1.SessionId,
+			OtherPartialSignatures: [][]byte{
+				signResp2.LocalPartialSignature,
+				signResp3.LocalPartialSignature,
+			},
+		},
+	)
+	require.NoError(t.t, err)
+	require.Equal(t.t, true, combineReq1.HaveAllSignatures)
+	require.NotEmpty(t.t, combineReq1.FinalSignature)
+
+	sig, err := schnorr.ParseSignature(combineReq1.FinalSignature)
+	require.NoError(t.t, err)
+	require.True(t.t, sig.Verify(sigHash, taprootKey))
+
+	tx.TxIn[0].Witness = wire.TxWitness{
+		combineReq1.FinalSignature,
+	}
+
+	// Serialize, weigh and publish the TX now, then make sure the
+	// coins are sent and confirmed to the final sweep destination address.
+	publishTxAndConfirmSweep(
+		ctxt, t, net, alice, tx, estimatedWeight,
+		&chainrpc.SpendRequest{
+			Outpoint: &chainrpc.Outpoint{
+				Hash:  p2trOutpoint.Hash[:],
+				Index: p2trOutpoint.Index,
+			},
+			Script: p2trPkScript,
+		},
+		p2wkhAddr.String(),
+	)
+}
+
+// testTaprootMuSig2ScriptSpend tests that a tapscript address with an internal
+// key that is a MuSig2 combined key can also be spent using the script path.
+func testTaprootMuSig2ScriptSpend(ctxt context.Context, t *harnessTest,
+	alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
+
+	// We're going to commit to a script and spend the output using the
+	// script. This is a hash lock with a simple preimage of "foobar". We
+	// need to know this upfront so, we can specify the taproot tweak with
+	// the root hash when creating the Musig2 signing session.
+	leaf1 := testScriptHashLock(t.t, []byte("foobar"))
+	rootHash := leaf1.TapHash()
+	taprootTweak := &signrpc.TaprootTweakDesc{
+		ScriptRoot: rootHash[:],
+	}
+
+	keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
+		ctxt, t, alice,
+	)
+	internalKey, taprootKey, _, _, _ := createMuSigSessions(
+		ctxt, t, alice, taprootTweak, keyDesc1, keyDesc2, keyDesc3,
+		allPubKeys,
+	)
+
+	// Because we know the internal key and the script we want to spend, we
+	// can now create the tapscript struct that's used for assembling the
+	// control block and fee estimation.
+	tapscript := input.TapscriptFullTree(internalKey, leaf1)
+
+	// Send some coins to the generated tapscript address.
+	p2trOutpoint, p2trPkScript := sendToTaprootOutput(
+		ctxt, t, net, alice, taprootKey, testAmount,
+	)
+
+	// Spend the output again, this time back to a p2wkh address.
+	p2wkhAddr, p2wkhPkScript := newAddrWithScript(
+		ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
+	)
+
+	// Create fee estimation for a p2tr input and p2wkh output.
+	feeRate := chainfee.SatPerKWeight(12500)
+	estimator := input.TxWeightEstimator{}
+	estimator.AddTapscriptInput(
+		len([]byte("foobar"))+len(leaf1.Script)+1, tapscript,
+	)
+	estimator.AddP2WKHOutput()
+	estimatedWeight := int64(estimator.Weight())
+	requiredFee := feeRate.FeeForWeight(estimatedWeight)
+
+	tx := wire.NewMsgTx(2)
+	tx.TxIn = []*wire.TxIn{{
+		PreviousOutPoint: p2trOutpoint,
+	}}
+	value := int64(testAmount - requiredFee)
+	tx.TxOut = []*wire.TxOut{{
+		PkScript: p2wkhPkScript,
+		Value:    value,
+	}}
+
+	// We can now assemble the witness stack.
+	controlBlockBytes, err := tapscript.ControlBlock.ToBytes()
+	require.NoError(t.t, err)
+
+	tx.TxIn[0].Witness = wire.TxWitness{
+		[]byte("foobar"),
+		leaf1.Script,
+		controlBlockBytes,
+	}
+
+	// Serialize, weigh and publish the TX now, then make sure the
+	// coins are sent and confirmed to the final sweep destination address.
+	publishTxAndConfirmSweep(
+		ctxt, t, net, alice, tx, estimatedWeight,
+		&chainrpc.SpendRequest{
+			Outpoint: &chainrpc.Outpoint{
+				Hash:  p2trOutpoint.Hash[:],
+				Index: p2trOutpoint.Index,
+			},
+			Script: p2trPkScript,
+		},
+		p2wkhAddr.String(),
+	)
+}
+
+// testTaprootMuSig2CombinedLeafKeySpend tests that a MuSig2 combined key can be
+// used for an OP_CHECKSIG inside a tap script leaf spend.
+func testTaprootMuSig2CombinedLeafKeySpend(ctxt context.Context, t *harnessTest,
+	alice *lntest.HarnessNode, net *lntest.NetworkHarness) {
+
+	// We're using the combined MuSig2 key in a script leaf. So we need to
+	// derive the combined key first, before we can build the script.
+	keyDesc1, keyDesc2, keyDesc3, allPubKeys := deriveSigningKeys(
+		ctxt, t, alice,
+	)
+	combineResp, err := alice.SignerClient.MuSig2CombineKeys(
+		ctxt, &signrpc.MuSig2CombineKeysRequest{
+			AllSignerPubkeys: allPubKeys,
+		},
+	)
+	require.NoError(t.t, err)
+	combinedPubKey, err := schnorr.ParsePubKey(combineResp.CombinedKey)
+	require.NoError(t.t, err)
+
+	// We're going to commit to a script and spend the output using the
+	// script. This is just an OP_CHECKSIG with the combined MuSig2 public
+	// key.
+	leaf := testScriptSchnorrSig(t.t, combinedPubKey)
+	tapscript := input.TapscriptPartialReveal(dummyInternalKey, leaf, nil)
+	taprootKey, err := tapscript.TaprootKey()
+	require.NoError(t.t, err)
+
+	// Send some coins to the generated tapscript address.
+	p2trOutpoint, p2trPkScript := sendToTaprootOutput(
+		ctxt, t, net, alice, taprootKey, testAmount,
+	)
+
+	// Spend the output again, this time back to a p2wkh address.
+	p2wkhAddr, p2wkhPkScript := newAddrWithScript(
+		ctxt, t.t, alice, lnrpc.AddressType_WITNESS_PUBKEY_HASH,
+	)
+
+	// Create fee estimation for a p2tr input and p2wkh output.
+	feeRate := chainfee.SatPerKWeight(12500)
+	estimator := input.TxWeightEstimator{}
+	estimator.AddTapscriptInput(
+		input.TaprootSignatureWitnessSize, tapscript,
+	)
+	estimator.AddP2WKHOutput()
+	estimatedWeight := int64(estimator.Weight())
+	requiredFee := feeRate.FeeForWeight(estimatedWeight)
+
+	tx := wire.NewMsgTx(2)
+	tx.TxIn = []*wire.TxIn{{
+		PreviousOutPoint: p2trOutpoint,
+	}}
+	value := int64(testAmount - requiredFee)
+	tx.TxOut = []*wire.TxOut{{
+		PkScript: p2wkhPkScript,
+		Value:    value,
+	}}
+
+	var buf bytes.Buffer
+	require.NoError(t.t, tx.Serialize(&buf))
+
+	utxoInfo := []*signrpc.TxOut{{
+		PkScript: p2trPkScript,
+		Value:    testAmount,
+	}}
+
+	// Do the actual signing now.
+	_, _, sessResp1, sessResp2, sessResp3 := createMuSigSessions(
+		ctxt, t, alice, nil, keyDesc1, keyDesc2, keyDesc3, allPubKeys,
+	)
+	require.NoError(t.t, err)
+
+	// We now need to create the raw sighash of the transaction, as that
+	// will be the message we're signing collaboratively.
+	prevOutputFetcher := txscript.NewCannedPrevOutputFetcher(
+		utxoInfo[0].PkScript, utxoInfo[0].Value,
+	)
+	sighashes := txscript.NewTxSigHashes(tx, prevOutputFetcher)
+
+	sigHash, err := txscript.CalcTapscriptSignaturehash(
+		sighashes, txscript.SigHashDefault, tx, 0, prevOutputFetcher,
+		leaf,
+	)
+	require.NoError(t.t, err)
+
+	// Now that we have the transaction prepared, we need to start with the
+	// signing. We simulate all three parties here, so we need to do
+	// everything three times. But because we're going to use session 1 to
+	// combine everything, we don't need its response, as it will store its
+	// own signature.
+	_, err = alice.SignerClient.MuSig2Sign(
+		ctxt, &signrpc.MuSig2SignRequest{
+			SessionId:     sessResp1.SessionId,
+			MessageDigest: sigHash,
+		},
+	)
+	require.NoError(t.t, err)
+
+	signResp2, err := alice.SignerClient.MuSig2Sign(
+		ctxt, &signrpc.MuSig2SignRequest{
+			SessionId:     sessResp2.SessionId,
+			MessageDigest: sigHash,
+			Cleanup:       true,
+		},
+	)
+	require.NoError(t.t, err)
+
+	signResp3, err := alice.SignerClient.MuSig2Sign(
+		ctxt, &signrpc.MuSig2SignRequest{
+			SessionId:     sessResp3.SessionId,
+			MessageDigest: sigHash,
+			Cleanup:       true,
+		},
+	)
+	require.NoError(t.t, err)
+
+	// Luckily only one of the signers needs to combine the signature, so
+	// let's do that now.
+	combineReq1, err := alice.SignerClient.MuSig2CombineSig(
+		ctxt, &signrpc.MuSig2CombineSigRequest{
+			SessionId: sessResp1.SessionId,
+			OtherPartialSignatures: [][]byte{
+				signResp2.LocalPartialSignature,
+				signResp3.LocalPartialSignature,
+			},
+		},
+	)
+	require.NoError(t.t, err)
+	require.Equal(t.t, true, combineReq1.HaveAllSignatures)
+	require.NotEmpty(t.t, combineReq1.FinalSignature)
+
+	sig, err := schnorr.ParseSignature(combineReq1.FinalSignature)
+	require.NoError(t.t, err)
+	require.True(t.t, sig.Verify(sigHash, combinedPubKey))
+
+	// We can now assemble the witness stack.
+	controlBlockBytes, err := tapscript.ControlBlock.ToBytes()
+	require.NoError(t.t, err)
+
+	tx.TxIn[0].Witness = wire.TxWitness{
+		combineReq1.FinalSignature,
+		leaf.Script,
+		controlBlockBytes,
+	}
+
+	// Serialize, weigh and publish the TX now, then make sure the
+	// coins are sent and confirmed to the final sweep destination address.
+	publishTxAndConfirmSweep(
+		ctxt, t, net, alice, tx, estimatedWeight,
+		&chainrpc.SpendRequest{
+			Outpoint: &chainrpc.Outpoint{
+				Hash:  p2trOutpoint.Hash[:],
+				Index: p2trOutpoint.Index,
+			},
+			Script: p2trPkScript,
+		},
+		p2wkhAddr.String(),
+	)
+}
 
 // testScriptHashLock returns a simple bitcoin script that locks the funds to
 // a hash lock of the given preimage.
@@ -542,3 +1001,330 @@ func newAddrWithScript(ctx context.Context, t *testing.T,
 
 	return p2wkhAddr, p2wkhPkScript
 }
+
+// sendToTaprootOutput sends coins to a p2tr output of the given taproot key and
+// mines a block to confirm the coins.
+func sendToTaprootOutput(ctx context.Context, t *harnessTest,
+	net *lntest.NetworkHarness, node *lntest.HarnessNode,
+	taprootKey *btcec.PublicKey, amt int64) (wire.OutPoint, []byte) {
+
+	tapScriptAddr, err := btcutil.NewAddressTaproot(
+		schnorr.SerializePubKey(taprootKey), harnessNetParams,
+	)
+	require.NoError(t.t, err)
+	p2trPkScript, err := txscript.PayToAddrScript(tapScriptAddr)
+	require.NoError(t.t, err)
+
+	// Send some coins to the generated tapscript address.
+	_, err = node.SendCoins(ctx, &lnrpc.SendCoinsRequest{
+		Addr:   tapScriptAddr.String(),
+		Amount: amt,
+	})
+	require.NoError(t.t, err)
+
+	// Wait until the TX is found in the mempool.
+	txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
+	require.NoError(t.t, err)
+
+	p2trOutputIndex := getOutputIndex(
+		t, net.Miner, txid, tapScriptAddr.String(),
+	)
+	p2trOutpoint := wire.OutPoint{
+		Hash:  *txid,
+		Index: uint32(p2trOutputIndex),
+	}
+
+	// Clear the mempool.
+	mineBlocks(t, net, 1, 1)
+
+	return p2trOutpoint, p2trPkScript
+}
+
+// publishTxAndConfirmSweep is a helper function that publishes a transaction
+// after checking its weight against an estimate. After asserting the given
+// spend request, the given sweep address' balance is verified to be seen as
+// funds belonging to the wallet.
+func publishTxAndConfirmSweep(ctx context.Context, t *harnessTest,
+	net *lntest.NetworkHarness, node *lntest.HarnessNode, tx *wire.MsgTx,
+	estimatedWeight int64, spendRequest *chainrpc.SpendRequest,
+	sweepAddr string) {
+
+	// Before we publish the tx that spends the p2tr transaction, we want to
+	// register a spend listener that we expect to fire after mining the
+	// block.
+	_, currentHeight, err := net.Miner.Client.GetBestBlock()
+	require.NoError(t.t, err)
+
+	// For a Taproot output we cannot leave the outpoint empty. Let's make
+	// sure the API returns the correct error here.
+	spendClient, err := node.ChainClient.RegisterSpendNtfn(
+		ctx, &chainrpc.SpendRequest{
+			Script:     spendRequest.Script,
+			HeightHint: uint32(currentHeight),
+		},
+	)
+	require.NoError(t.t, err)
+
+	// The error is only thrown when trying to read a message.
+	_, err = spendClient.Recv()
+	require.Contains(
+		t.t, err.Error(),
+		"cannot register witness v1 spend request without outpoint",
+	)
+
+	// Now try again, this time with the outpoint set.
+	spendClient, err = node.ChainClient.RegisterSpendNtfn(
+		ctx, &chainrpc.SpendRequest{
+			Outpoint:   spendRequest.Outpoint,
+			Script:     spendRequest.Script,
+			HeightHint: uint32(currentHeight),
+		},
+	)
+	require.NoError(t.t, err)
+
+	var buf bytes.Buffer
+	require.NoError(t.t, tx.Serialize(&buf))
+
+	// Since Schnorr signatures are fixed size, we must be able to estimate
+	// the size of this transaction exactly.
+	txWeight := blockchain.GetTransactionWeight(btcutil.NewTx(tx))
+	require.Equal(t.t, estimatedWeight, txWeight)
+
+	_, err = node.WalletKitClient.PublishTransaction(
+		ctx, &walletrpc.Transaction{
+			TxHex: buf.Bytes(),
+		},
+	)
+	require.NoError(t.t, err)
+
+	// Make sure the coins sent to the address are confirmed correctly,
+	// including the confirmation notification.
+	confirmAddress(ctx, t, net, node, sweepAddr)
+
+	// We now expect our spend event to go through.
+	spendMsg, err := spendClient.Recv()
+	require.NoError(t.t, err)
+	spend := spendMsg.GetSpend()
+	require.NotNil(t.t, spend)
+	require.Equal(t.t, spend.SpendingHeight, uint32(currentHeight+1))
+}
+
+// confirmAddress makes sure that a transaction in the mempool spends funds to
+// the given address. It also checks that a confirmation notification for the
+// address is triggered when the transaction is mined.
+func confirmAddress(ctx context.Context, t *harnessTest,
+	net *lntest.NetworkHarness, node *lntest.HarnessNode,
+	addrString string) {
+
+	// Wait until the tx that sends to the address is found.
+	txid, err := waitForTxInMempool(net.Miner.Client, minerMempoolTimeout)
+	require.NoError(t.t, err)
+
+	// Wait until bob has seen the tx and considers it as owned.
+	addrOutputIndex := getOutputIndex(t, net.Miner, txid, addrString)
+	op := &lnrpc.OutPoint{
+		TxidBytes:   txid[:],
+		OutputIndex: uint32(addrOutputIndex),
+	}
+	assertWalletUnspent(t, node, op)
+
+	// Before we confirm the transaction, let's register a confirmation
+	// listener for it, which we expect to fire after mining a block.
+	parsedAddr, err := btcutil.DecodeAddress(addrString, harnessNetParams)
+	require.NoError(t.t, err)
+	addrPkScript, err := txscript.PayToAddrScript(parsedAddr)
+	require.NoError(t.t, err)
+
+	_, currentHeight, err := net.Miner.Client.GetBestBlock()
+	require.NoError(t.t, err)
+	confClient, err := node.ChainClient.RegisterConfirmationsNtfn(
+		ctx, &chainrpc.ConfRequest{
+			Script:     addrPkScript,
+			Txid:       txid[:],
+			HeightHint: uint32(currentHeight),
+			NumConfs:   1,
+		},
+	)
+	require.NoError(t.t, err)
+
+	// Mine another block to clean up the mempool.
+	mineBlocks(t, net, 1, 1)
+
+	// We now expect our confirmation to go through.
+	confMsg, err := confClient.Recv()
+	require.NoError(t.t, err)
+	conf := confMsg.GetConf()
+	require.NotNil(t.t, conf)
+	require.Equal(t.t, conf.BlockHeight, uint32(currentHeight+1))
+}
+
+// deriveSigningKeys derives three signing keys and returns their descriptors,
+// as well as the public keys in the Schnorr serialized format.
+func deriveSigningKeys(ctx context.Context, t *harnessTest,
+	node *lntest.HarnessNode) (*signrpc.KeyDescriptor,
+	*signrpc.KeyDescriptor, *signrpc.KeyDescriptor, [][]byte) {
+
+	// For muSig2 we need multiple keys. We derive three of them from the
+	// same wallet, just so we know we can also sign for them again.
+	keyDesc1, err := node.WalletKitClient.DeriveNextKey(
+		ctx, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
+	)
+	require.NoError(t.t, err)
+	pubKey1, err := btcec.ParsePubKey(keyDesc1.RawKeyBytes)
+	require.NoError(t.t, err)
+
+	keyDesc2, err := node.WalletKitClient.DeriveNextKey(
+		ctx, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
+	)
+	require.NoError(t.t, err)
+	pubKey2, err := btcec.ParsePubKey(keyDesc2.RawKeyBytes)
+	require.NoError(t.t, err)
+
+	keyDesc3, err := node.WalletKitClient.DeriveNextKey(
+		ctx, &walletrpc.KeyReq{KeyFamily: testTaprootKeyFamily},
+	)
+	require.NoError(t.t, err)
+	pubKey3, err := btcec.ParsePubKey(keyDesc3.RawKeyBytes)
+	require.NoError(t.t, err)
+
+	// Now that we have all three keys we can create three sessions, one
+	// for each of the signers. This would of course normally not happen on
+	// the same node.
+	allPubKeys := [][]byte{
+		schnorr.SerializePubKey(pubKey1),
+		schnorr.SerializePubKey(pubKey2),
+		schnorr.SerializePubKey(pubKey3),
+	}
+
+	return keyDesc1, keyDesc2, keyDesc3, allPubKeys
+}
+
+// createMuSigSessions creates a MuSig2 session with three keys that are
+// combined into a single key. The same node is used for the three signing
+// participants but a separate key is generated for each session. So the result
+// should be the same as if it were three different nodes.
+func createMuSigSessions(ctx context.Context, t *harnessTest,
+	node *lntest.HarnessNode, taprootTweak *signrpc.TaprootTweakDesc,
+	keyDesc1, keyDesc2, keyDesc3 *signrpc.KeyDescriptor,
+	allPubKeys [][]byte) (*btcec.PublicKey, *btcec.PublicKey,
+	*signrpc.MuSig2SessionResponse, *signrpc.MuSig2SessionResponse,
+	*signrpc.MuSig2SessionResponse) {
+
+	sessResp1, err := node.SignerClient.MuSig2CreateSession(
+		ctx, &signrpc.MuSig2SessionRequest{
+			KeyLoc:           keyDesc1.KeyLoc,
+			AllSignerPubkeys: allPubKeys,
+			TaprootTweak:     taprootTweak,
+		},
+	)
+	require.NoError(t.t, err)
+
+	// Now that we have the three keys in a combined form, we want to make
+	// sure the tweaking for the taproot key worked correctly. We first need
+	// to parse the combined key without any tweaks applied to it. That will
+	// be our internal key. Once we know that, we can tweak it with the
+	// tapHash of the script root hash. We should arrive at the same result
+	// as the API.
+	combinedKey, err := schnorr.ParsePubKey(sessResp1.CombinedKey)
+	require.NoError(t.t, err)
+
+	// When combining the key without creating a session, we expect the same
+	// combined key to be created.
+	expectedCombinedKey := combinedKey
+
+	// Without a tweak, the internal key is equal to the combined key.
+	internalKey := combinedKey
+
+	// If there is a tweak, then there is the internal, pre-tweaked combined
+	// key and the taproot key which is fully tweaked.
+	if taprootTweak != nil {
+		internalKey, err = schnorr.ParsePubKey(
+			sessResp1.TaprootInternalKey,
+		)
+		require.NoError(t.t, err)
+
+		// We now know the taproot key. The session with the tweak
+		// applied should produce the same key!
+		expectedCombinedKey = txscript.ComputeTaprootOutputKey(
+			internalKey, taprootTweak.ScriptRoot,
+		)
+		require.Equal(
+			t.t, schnorr.SerializePubKey(expectedCombinedKey),
+			schnorr.SerializePubKey(combinedKey),
+		)
+	}
+
+	// We should also get the same keys when just calling the
+	// MuSig2CombineKeys RPC.
+	combineResp, err := node.SignerClient.MuSig2CombineKeys(
+		ctx, &signrpc.MuSig2CombineKeysRequest{
+			AllSignerPubkeys: allPubKeys,
+			TaprootTweak:     taprootTweak,
+		},
+	)
+	require.NoError(t.t, err)
+	require.Equal(
+		t.t, schnorr.SerializePubKey(expectedCombinedKey),
+		combineResp.CombinedKey,
+	)
+	require.Equal(
+		t.t, schnorr.SerializePubKey(internalKey),
+		combineResp.TaprootInternalKey,
+	)
+
+	// Everything is good so far, let's continue with creating the signing
+	// session for the other two participants.
+	sessResp2, err := node.SignerClient.MuSig2CreateSession(
+		ctx, &signrpc.MuSig2SessionRequest{
+			KeyLoc:           keyDesc2.KeyLoc,
+			AllSignerPubkeys: allPubKeys,
+			OtherSignerPublicNonces: [][]byte{
+				sessResp1.LocalPublicNonces,
+			},
+			TaprootTweak: taprootTweak,
+		},
+	)
+	require.NoError(t.t, err)
+	require.Equal(t.t, sessResp1.CombinedKey, sessResp2.CombinedKey)
+
+	sessResp3, err := node.SignerClient.MuSig2CreateSession(
+		ctx, &signrpc.MuSig2SessionRequest{
+			KeyLoc:           keyDesc3.KeyLoc,
+			AllSignerPubkeys: allPubKeys,
+			OtherSignerPublicNonces: [][]byte{
+				sessResp1.LocalPublicNonces,
+				sessResp2.LocalPublicNonces,
+			},
+			TaprootTweak: taprootTweak,
+		},
+	)
+	require.NoError(t.t, err)
+	require.Equal(t.t, sessResp2.CombinedKey, sessResp3.CombinedKey)
+	require.Equal(t.t, true, sessResp3.HaveAllNonces)
+
+	// We need to distribute the rest of the nonces.
+	nonceResp1, err := node.SignerClient.MuSig2RegisterNonces(
+		ctx, &signrpc.MuSig2RegisterNoncesRequest{
+			SessionId: sessResp1.SessionId,
+			OtherSignerPublicNonces: [][]byte{
+				sessResp2.LocalPublicNonces,
+				sessResp3.LocalPublicNonces,
+			},
+		},
+	)
+	require.NoError(t.t, err)
+	require.Equal(t.t, true, nonceResp1.HaveAllNonces)
+
+	nonceResp2, err := node.SignerClient.MuSig2RegisterNonces(
+		ctx, &signrpc.MuSig2RegisterNoncesRequest{
+			SessionId: sessResp2.SessionId,
+			OtherSignerPublicNonces: [][]byte{
+				sessResp3.LocalPublicNonces,
+			},
+		},
+	)
+	require.NoError(t.t, err)
+	require.Equal(t.t, true, nonceResp2.HaveAllNonces)
+
+	return internalKey, combinedKey, sessResp1, sessResp2, sessResp3
+}
diff --git a/lntest/mock/signer.go b/lntest/mock/signer.go
index 520c0aa16..7cfd0bd26 100644
--- a/lntest/mock/signer.go
+++ b/lntest/mock/signer.go
@@ -1,10 +1,13 @@
 package mock
 
 import (
+	"crypto/sha256"
 	"fmt"
 
 	"github.com/btcsuite/btcd/btcec/v2"
 	"github.com/btcsuite/btcd/btcec/v2/ecdsa"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr"
+	"github.com/btcsuite/btcd/btcec/v2/schnorr/musig2"
 	"github.com/btcsuite/btcd/chaincfg/chainhash"
 	"github.com/btcsuite/btcd/txscript"
 	"github.com/btcsuite/btcd/wire"
@@ -47,6 +50,50 @@ func (d *DummySigner) ComputeInputScript(tx *wire.MsgTx,
 	return &input.Script{}, nil
 }
 
+// MuSig2CreateSession creates a new MuSig2 signing session using the local
+// key identified by the key locator. The complete list of all public keys of
+// all signing parties must be provided, including the public key of the local
+// signing key. If nonces of other parties are already known, they can be
+// submitted as well to reduce the number of method calls necessary later on.
+func (d *DummySigner) MuSig2CreateSession(keychain.KeyLocator,
+	[]*btcec.PublicKey, *input.MuSig2Tweaks,
+	[][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) {
+
+	return nil, nil
+}
+
+// MuSig2RegisterNonces registers one or more public nonces of other signing
+// participants for a session identified by its ID. This method returns true
+// once we have all nonces for all other signing participants.
+func (d *DummySigner) MuSig2RegisterNonces(input.MuSig2SessionID,
+	[][musig2.PubNonceSize]byte) (bool, error) {
+
+	return false, nil
+}
+
+// MuSig2Sign creates a partial signature using the local signing key
+// that was specified when the session was created. This can only be
+// called when all public nonces of all participants are known and have
+// been registered with the session. If this node isn't responsible for
+// combining all the partial signatures, then the cleanup parameter
+// should be set, indicating that the session can be removed from memory
+// once the signature was produced.
+func (d *DummySigner) MuSig2Sign(input.MuSig2SessionID,
+	[sha256.Size]byte, bool) (*musig2.PartialSignature, error) {
+
+	return nil, nil
+}
+
+// MuSig2CombineSig combines the given partial signature(s) with the
+// local one, if it already exists. Once a partial signature of all
+// participants is registered, the final signature will be combined and
+// returned.
+func (d *DummySigner) MuSig2CombineSig(input.MuSig2SessionID,
+	[]*musig2.PartialSignature) (*schnorr.Signature, bool, error) {
+
+	return nil, false, nil
+}
+
 // SingleSigner is an implementation of the Signer interface that signs
 // everything with a single private key.
 type SingleSigner struct {
@@ -136,3 +183,47 @@ func (s *SingleSigner) SignMessage(keyLoc keychain.KeyLocator,
 	}
 	return ecdsa.Sign(s.Privkey, digest), nil
 }
+
+// MuSig2CreateSession creates a new MuSig2 signing session using the local
+// key identified by the key locator. The complete list of all public keys of
+// all signing parties must be provided, including the public key of the local
+// signing key. If nonces of other parties are already known, they can be
+// submitted as well to reduce the number of method calls necessary later on.
+func (s *SingleSigner) MuSig2CreateSession(keychain.KeyLocator,
+	[]*btcec.PublicKey, *input.MuSig2Tweaks,
+	[][musig2.PubNonceSize]byte) (*input.MuSig2SessionInfo, error) {
+
+	return nil, nil
+}
+
+// MuSig2RegisterNonces registers one or more public nonces of other signing
+// participants for a session identified by its ID. This method returns true
+// once we have all nonces for all other signing participants.
+func (s *SingleSigner) MuSig2RegisterNonces(input.MuSig2SessionID,
+	[][musig2.PubNonceSize]byte) (bool, error) {
+
+	return false, nil
+}
+
+// MuSig2Sign creates a partial signature using the local signing key
+// that was specified when the session was created. This can only be
+// called when all public nonces of all participants are known and have
+// been registered with the session. If this node isn't responsible for
+// combining all the partial signatures, then the cleanup parameter
+// should be set, indicating that the session can be removed from memory
+// once the signature was produced.
+func (s *SingleSigner) MuSig2Sign(input.MuSig2SessionID,
+	[sha256.Size]byte, bool) (*musig2.PartialSignature, error) {
+
+	return nil, nil
+}
+
+// MuSig2CombineSig combines the given partial signature(s) with the
+// local one, if it already exists. Once a partial signature of all
+// participants is registered, the final signature will be combined and
+// returned.
+func (s *SingleSigner) MuSig2CombineSig(input.MuSig2SessionID,
+	[]*musig2.PartialSignature) (*schnorr.Signature, bool, error) {
+
+	return nil, false, nil
+}
diff --git a/lnwallet/btcwallet/signer_test.go b/lnwallet/btcwallet/signer_test.go
index 949977c82..4b36accc7 100644
--- a/lnwallet/btcwallet/signer_test.go
+++ b/lnwallet/btcwallet/signer_test.go
@@ -273,8 +273,9 @@ func TestScriptImport(t *testing.T) {
 	// Now, as a last test, make sure that when we try adding an address
 	// with partial script reveal, we get an error that the address already
 	// exists.
+	inclusionProof := leaf2.TapHash()
 	tapscript2 := input.TapscriptPartialReveal(
-		testPubKey, leaf1, leaf2.TapHash(),
+		testPubKey, leaf1, inclusionProof[:],
 	)
 	_, err = w.ImportTaprootScript(scope, tapscript2)
 	require.Error(t, err)