input+signrpc+lnwallet+multi: implement combined nonce support

Add CombinedNonce() and RegisterCombinedNonce() methods with full implementation
stack.

Interface and core implementation:
- input/musig2.go: Added methods to MuSig2Session and MuSig2Signer interfaces
- input/musig2_session_manager.go: MusigSessionManager implementation using
  HaveAllNonces flag for state tracking (simplified, no extra fields)
- internal/musig2v040: Stub implementations returning ErrUnsupportedMethod
- Mock implementations (MockInputSigner, MockSigner, DummySigner)

RPC layer:
- lnrpc/signrpc/signer.proto: RPC method definitions and messages
- lnrpc/signrpc/signer.yaml: REST API endpoint mappings
- Generated protobuf code (all .pb.go files)
- lnwallet/rpcwallet/rpcwallet.go: RPCKeyRing client implementation

The proto types and RPCKeyRing are added together since RPCKeyRing implements
the Signer interface and requires proto types to fulfill the contract.

For v0.4.0, these methods return ErrUnsupportedMethod. Use MuSig2Version100RC2
to access these features.
This commit is contained in:
Elle Mouton committed 2025-12-10 07:15:06 +02:00
1 parent cbcb6a4dcf
commit c945f6749e
15 files changed
+1204 -175

No files matched your search

+23
View File
@@ -258,6 +258,29 @@ func (m *MockInputSigner) MuSig2RegisterNonces(versio MuSig2SessionID,
return args.Bool(0), args.Error(1)
}
// MuSig2RegisterCombinedNonce registers a pre-aggregated combined nonce for a
// session identified by its ID.
func (m *MockInputSigner) MuSig2RegisterCombinedNonce(sessionID MuSig2SessionID,
combinedNonce [musig2.PubNonceSize]byte) error {
args := m.Called(sessionID, combinedNonce)
return args.Error(0)
}
// MuSig2GetCombinedNonce retrieves the combined nonce for a session identified
// by its ID.
func (m *MockInputSigner) MuSig2GetCombinedNonce(sessionID MuSig2SessionID) (
[musig2.PubNonceSize]byte, error) {
args := m.Called(sessionID)
if args.Get(0) == nil {
return [musig2.PubNonceSize]byte{}, args.Error(1)
}
return args.Get(0).([musig2.PubNonceSize]byte), args.Error(1)
}
// MuSig2Sign creates a partial signature using the local signing key that was
// specified when the session was created.
func (m *MockInputSigner) MuSig2Sign(sessionID MuSig2SessionID,
+39
View File
@@ -64,6 +64,25 @@ type MuSig2Signer interface {
MuSig2RegisterNonces(MuSig2SessionID,
[][musig2.PubNonceSize]byte) (bool, error)
// MuSig2RegisterCombinedNonce registers a pre-aggregated combined nonce
// for a session identified by its ID. This is an alternative to
// MuSig2RegisterNonces and is used when a coordinator has already
// aggregated all individual nonces and wants to distribute the combined
// nonce to participants.
//
// NOTE: This method is mutually exclusive with MuSig2RegisterNonces for
// the same session. Once this method is called, MuSig2RegisterNonces
// will return an error if called later for the same session.
MuSig2RegisterCombinedNonce(MuSig2SessionID,
[musig2.PubNonceSize]byte) error
// MuSig2GetCombinedNonce retrieves the combined nonce for a session
// identified by its ID. This will be available after either all
// individual nonces have been registered via MuSig2RegisterNonces, or a
// combined nonce has been registered via MuSig2RegisterCombinedNonce.
MuSig2GetCombinedNonce(MuSig2SessionID) ([musig2.PubNonceSize]byte,
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
@@ -130,6 +149,26 @@ type MuSig2Session interface {
// of signers. This method returns true once all the public nonces have
// been accounted for.
RegisterPubNonce(nonce [musig2.PubNonceSize]byte) (bool, error)
// CombinedNonce returns the combined/aggregated public nonce for the
// session. This will be available after either all individual nonces
// have been registered via RegisterPubNonce, or a combined nonce has
// been registered via RegisterCombinedNonce.
//
// If the combined nonce is not yet available, this method returns an
// error.
CombinedNonce() ([musig2.PubNonceSize]byte, error)
// RegisterCombinedNonce allows a caller to directly register a
// pre-aggregated nonce that was generated externally. This is useful
// in coordinator-based protocols where the coordinator aggregates all
// nonces and distributes the combined nonce to participants.
//
// NOTE: This method is mutually exclusive with RegisterPubNonce. Once
// this method is called, RegisterPubNonce will return an error if
// called later. Similarly, if RegisterPubNonce has already been called,
// this method will return an error.
RegisterCombinedNonce(combinedNonce [musig2.PubNonceSize]byte) error
}
// MuSig2SessionInfo is a struct for keeping track of a signing session
+67
View File
@@ -302,3 +302,70 @@ func (m *MusigSessionManager) MuSig2RegisterNonces(sessionID MuSig2SessionID,
return session.HaveAllNonces, nil
}
// MuSig2RegisterCombinedNonce registers a pre-aggregated combined nonce for a
// session identified by its ID. This is an alternative to MuSig2RegisterNonces
// and is used when a coordinator has already aggregated all individual nonces
// and wants to distribute the combined nonce to participants.
//
// NOTE: This method is mutually exclusive with MuSig2RegisterNonces for the
// same session. Once this method is called, MuSig2RegisterNonces will return
// an error if called later for the same session.
func (m *MusigSessionManager) MuSig2RegisterCombinedNonce(
sessionID MuSig2SessionID,
combinedNonce [musig2.PubNonceSize]byte) error {
// Hold the lock during the whole operation.
m.sessionMtx.Lock(sessionID)
defer m.sessionMtx.Unlock(sessionID)
// Load the session.
session, ok := m.musig2Sessions.Load(sessionID)
if !ok {
return fmt.Errorf("session with ID %x not found", sessionID[:])
}
// Check if we already have all nonces.
if session.HaveAllNonces {
return fmt.Errorf("already have all nonces")
}
// Delegate to the version-specific implementation.
err := session.session.RegisterCombinedNonce(combinedNonce)
if err != nil {
return fmt.Errorf("error registering combined nonce: %w", err)
}
// Mark that we have all nonces now.
session.HaveAllNonces = true
return nil
}
// MuSig2GetCombinedNonce retrieves the combined nonce for a session identified
// by its ID. This will be available after either all individual nonces have
// been registered via MuSig2RegisterNonces, or a combined nonce has been
// registered via MuSig2RegisterCombinedNonce.
func (m *MusigSessionManager) MuSig2GetCombinedNonce(
sessionID MuSig2SessionID) ([musig2.PubNonceSize]byte, error) {
// Hold the lock during the operation.
m.sessionMtx.Lock(sessionID)
defer m.sessionMtx.Unlock(sessionID)
// Load the session.
session, ok := m.musig2Sessions.Load(sessionID)
if !ok {
return [musig2.PubNonceSize]byte{}, fmt.Errorf("session with "+
"ID %x not found", sessionID[:])
}
// Get the combined nonce from the session.
combinedNonce, err := session.session.CombinedNonce()
if err != nil {
return [musig2.PubNonceSize]byte{}, fmt.Errorf("error getting "+
"combined nonce: %w", err)
}
return combinedNonce, nil
}