Merge pull request #6361 from guggero/musig2

Musig2: add new MuSig2 RPC methods to `signrpc`
This commit is contained in:
Oliver Gugger 2022-04-29 12:55:52 +02:00 committed by GitHub
commit 9bbee09497
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
26 changed files with 5220 additions and 307 deletions

View file

@ -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:

117
docs/musig2.md Normal file
View file

@ -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).

View file

@ -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)

105
go.mod
View file

@ -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

6
go.sum
View file

@ -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=

228
input/musig2.go Normal file
View file

@ -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
}

View file

@ -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.
//

View file

@ -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)

View file

@ -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

File diff suppressed because it is too large Load diff

View file

@ -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
)

View file

@ -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)
}
}

View file

@ -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;
}

View file

@ -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": {

View file

@ -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: "*"

View file

@ -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",

View file

@ -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
}

View file

@ -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 {

File diff suppressed because it is too large Load diff

View file

@ -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
}

View file

@ -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
}

View file

@ -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)

View file

@ -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)

View file

@ -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,

View file

@ -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

View file

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