• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

lightningnetwork / lnd / 13974489001

20 Mar 2025 04:32PM UTC coverage: 56.292% (-2.9%) from 59.168%
13974489001

Pull #8754

github

web-flow
Merge aed149e6b into ea050d06f
Pull Request #8754: Add `Outbound` Remote Signer implementation

594 of 1713 new or added lines in 26 files covered. (34.68%)

23052 existing lines in 272 files now uncovered.

105921 of 188165 relevant lines covered (56.29%)

23796.34 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

0.0
/lnwallet/rpcwallet/remote_signer_connection.go
1
package rpcwallet
2

3
import (
4
        "context"
5
        "crypto/x509"
6
        "errors"
7
        "fmt"
8
        "os"
9
        "time"
10

11
        "github.com/lightningnetwork/lnd/keychain"
12
        "github.com/lightningnetwork/lnd/lncfg"
13
        "github.com/lightningnetwork/lnd/lnrpc/signrpc"
14
        "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
15
        "github.com/lightningnetwork/lnd/macaroons"
16
        "google.golang.org/grpc"
17
        "google.golang.org/grpc/connectivity"
18
        "google.golang.org/grpc/credentials"
19
        "gopkg.in/macaroon.v2"
20
)
21

22
type (
23
        StreamClient = walletrpc.WalletKit_SignCoordinatorStreamsClient
24
        StreamServer = walletrpc.WalletKit_SignCoordinatorStreamsServer
25
)
26

27
// RemoteSignerConnection is an interface that abstracts the communication with
28
// a remote signer. It extends the RemoteSignerRequests interface, and adds som
29
// additional methods to manage the connection and verify the health of the
30
// remote signer.
31
type RemoteSignerConnection interface {
32
        // RemoteSignerRequests is an interface that defines the requests that
33
        // can be sent to a remote signer.
34
        RemoteSignerRequests
35

36
        // Timeout returns the set connection timeout for the remote signer.
37
        Timeout() time.Duration
38

39
        // Ready returns a channel that nil gets sent over once the remote
40
        // signer is ready to accept requests.
41
        Ready() chan error
42

43
        // Stop gracefully disconnects from the remote signer.
44
        Stop()
45

46
        // Ping verifies that the remote signer is still responsive.
47
        Ping(ctx context.Context, timeout time.Duration) error
48
}
49

50
// RemoteSignerRequests is an interface that defines the requests that can be
51
// sent to a remote signer. It's a subset of the signrpc.SignerClient and
52
// walletrpc.WalletKitClient interfaces.
53
type RemoteSignerRequests interface {
54
        // DeriveSharedKey sends a SharedKeyRequest to the remote signer and
55
        // waits for the corresponding response.
56
        DeriveSharedKey(ctx context.Context,
57
                in *signrpc.SharedKeyRequest,
58
                opts ...grpc.CallOption) (*signrpc.SharedKeyResponse, error)
59

60
        // MuSig2Cleanup sends a MuSig2CleanupRequest to the remote signer and
61
        // waits for the corresponding response.
62
        MuSig2Cleanup(ctx context.Context,
63
                in *signrpc.MuSig2CleanupRequest,
64
                opts ...grpc.CallOption) (*signrpc.MuSig2CleanupResponse, error)
65

66
        // MuSig2CombineSig sends a MuSig2CombineSigRequest to the remote signer
67
        // and waits for the corresponding response.
68
        MuSig2CombineSig(ctx context.Context,
69
                in *signrpc.MuSig2CombineSigRequest,
70
                opts ...grpc.CallOption) (*signrpc.MuSig2CombineSigResponse,
71
                error)
72

73
        // MuSig2CreateSession sends a MuSig2SessionRequest to the remote signer
74
        // and waits for the corresponding response.
75
        MuSig2CreateSession(ctx context.Context,
76
                in *signrpc.MuSig2SessionRequest,
77
                opts ...grpc.CallOption) (*signrpc.MuSig2SessionResponse, error)
78

79
        // MuSig2RegisterNonces sends a MuSig2RegisterNoncesRequest to the
80
        // remote signer and waits for the corresponding response.
81
        MuSig2RegisterNonces(ctx context.Context,
82
                in *signrpc.MuSig2RegisterNoncesRequest,
83
                opts ...grpc.CallOption) (*signrpc.MuSig2RegisterNoncesResponse,
84
                error)
85

86
        // MuSig2Sign sends a MuSig2SignRequest to the remote signer and waits
87
        // for the corresponding response.
88
        MuSig2Sign(ctx context.Context,
89
                in *signrpc.MuSig2SignRequest,
90
                opts ...grpc.CallOption) (*signrpc.MuSig2SignResponse, error)
91

92
        // SignMessage sends a SignMessageReq to the remote signer and waits for
93
        // the corresponding response.
94
        SignMessage(ctx context.Context,
95
                in *signrpc.SignMessageReq,
96
                opts ...grpc.CallOption) (*signrpc.SignMessageResp, error)
97

98
        // SignPsbt sends a SignPsbtRequest to the remote signer and waits for
99
        // the corresponding response.
100
        SignPsbt(ctx context.Context, in *walletrpc.SignPsbtRequest,
101
                opts ...grpc.CallOption) (*walletrpc.SignPsbtResponse, error)
102
}
103

104
// OutboundConnection is an abstraction of the outbound connection made to an
105
// inbound remote signer. An inbound remote signer is a remote signer that
106
// allows the watch-only node to connect to it via an inbound GRPC connection.
107
type OutboundConnection struct {
108
        // Embedded signrpc.SignerClient and walletrpc.WalletKitClient to
109
        // implement the RemoteSigner interface.
110
        signrpc.SignerClient
111
        walletrpc.WalletKitClient
112

113
        // The ConnectionCfg containing connection details of the remote signer.
114
        cfg lncfg.ConnectionCfg
115

116
        // conn represents the connection to the remote signer.
117
        conn *grpc.ClientConn
118
}
119

120
// NewOutboundConnection creates a new OutboundConnection instance.
121
// The function sets up a connection to the remote signer node.
122
func NewOutboundConnection(ctx context.Context,
NEW
123
        cfg lncfg.ConnectionCfg) (*OutboundConnection, error) {
×
NEW
124

×
NEW
125
        remoteSigner := &OutboundConnection{
×
NEW
126
                cfg: cfg,
×
NEW
127
        }
×
NEW
128

×
NEW
129
        err := remoteSigner.connect(ctx, cfg)
×
NEW
130
        if err != nil {
×
NEW
131
                return nil, fmt.Errorf("error connecting to the remote "+
×
NEW
132
                        "signing node through RPC: %v", err)
×
NEW
133
        }
×
134

NEW
135
        return remoteSigner, nil
×
136
}
137

138
// Ready returns a channel that nil gets sent over once the connection to the
139
// remote signer is set up and the remote signer is ready to accept requests.
140
//
141
// NOTE: This is part of the RemoteSignerConnection interface.
NEW
142
func (r *OutboundConnection) Ready() chan error {
×
NEW
143
        // The inbound remote signer is ready as soon we have connected to the
×
NEW
144
        // remote signer node in the constructor. Therefore, we always send
×
NEW
145
        // nil here to signal that we are ready.
×
NEW
146
        readyChan := make(chan error, 1)
×
NEW
147
        readyChan <- nil
×
NEW
148

×
NEW
149
        return readyChan
×
NEW
150
}
×
151

152
// Ping verifies that the remote signer is still responsive.
153
//
154
// NOTE: This is part of the RemoteSignerConnection interface.
NEW
155
func (r *OutboundConnection) Ping(ctx context.Context, _ time.Duration) error {
×
NEW
156
        if r.conn == nil || r.conn.GetState() != connectivity.Ready {
×
NEW
157
                return errors.New("remote signer is not connected")
×
NEW
158
        }
×
159

NEW
160
        pingMsg := []byte("ping test")
×
NEW
161
        keyLoc := &signrpc.KeyLocator{
×
NEW
162
                KeyFamily: int32(keychain.KeyFamilyNodeKey),
×
NEW
163
                KeyIndex:  1,
×
NEW
164
        }
×
NEW
165

×
NEW
166
        // Sign a message with the default ECDSA.
×
NEW
167
        signMsgReq := &signrpc.SignMessageReq{
×
NEW
168
                Msg:        pingMsg,
×
NEW
169
                KeyLoc:     keyLoc,
×
NEW
170
                SchnorrSig: false,
×
NEW
171
        }
×
NEW
172

×
NEW
173
        _, err := r.SignMessage(ctx, signMsgReq)
×
NEW
174

×
NEW
175
        return err
×
176
}
177

178
// Timeout returns the set connection timeout for the remote signer.
179
//
180
// NOTE: This is part of the RemoteSignerConnection interface.
NEW
181
func (r *OutboundConnection) Timeout() time.Duration {
×
NEW
182
        return r.cfg.Timeout
×
NEW
183
}
×
184

185
// Stop closes the connection to the remote signer.
186
//
187
// NOTE: This is part of the RemoteSignerConnection interface.
NEW
188
func (r *OutboundConnection) Stop() {
×
NEW
189
        if r.conn != nil {
×
NEW
190
                r.conn.Close()
×
NEW
191
        }
×
192
}
193

194
// connect tries to establish an RPC connection to the configured host:port with
195
// the supplied certificate and macaroon.
196
func (r *OutboundConnection) connect(ctx context.Context,
NEW
197
        cfg lncfg.ConnectionCfg) error {
×
NEW
198

×
NEW
199
        certBytes, err := os.ReadFile(cfg.TLSCertPath)
×
NEW
200
        if err != nil {
×
NEW
201
                return fmt.Errorf("error reading TLS cert file %v: %w",
×
NEW
202
                        cfg.TLSCertPath, err)
×
NEW
203
        }
×
204

NEW
205
        cp := x509.NewCertPool()
×
NEW
206
        if !cp.AppendCertsFromPEM(certBytes) {
×
NEW
207
                return fmt.Errorf("credentials: failed to append certificate")
×
NEW
208
        }
×
209

NEW
210
        macBytes, err := os.ReadFile(cfg.MacaroonPath)
×
NEW
211
        if err != nil {
×
NEW
212
                return fmt.Errorf("error reading macaroon file %v: %w",
×
NEW
213
                        cfg.MacaroonPath, err)
×
NEW
214
        }
×
NEW
215
        mac := &macaroon.Macaroon{}
×
NEW
216
        if err := mac.UnmarshalBinary(macBytes); err != nil {
×
NEW
217
                return fmt.Errorf("error decoding macaroon: %w", err)
×
NEW
218
        }
×
219

NEW
220
        macCred, err := macaroons.NewMacaroonCredential(mac)
×
NEW
221
        if err != nil {
×
NEW
222
                return fmt.Errorf("error creating creds: %w", err)
×
NEW
223
        }
×
224

NEW
225
        opts := []grpc.DialOption{
×
NEW
226
                grpc.WithTransportCredentials(credentials.NewClientTLSFromCert(
×
NEW
227
                        cp, "",
×
NEW
228
                )),
×
NEW
229
                grpc.WithPerRPCCredentials(macCred),
×
NEW
230
                grpc.WithBlock(),
×
NEW
231
        }
×
NEW
232

×
NEW
233
        ctxt, cancel := context.WithTimeout(ctx, cfg.Timeout)
×
NEW
234

×
NEW
235
        // In the blocking case, ctx can be used to cancel or expire the pending
×
NEW
236
        // connection. Once this function returns, the cancellation and
×
NEW
237
        // expiration of ctx will be noop. Users should call ClientConn.Close to
×
NEW
238
        // terminate all the pending operations after this function returns.
×
NEW
239
        defer cancel()
×
NEW
240

×
NEW
241
        conn, err := grpc.DialContext(ctxt, cfg.RPCHost, opts...)
×
NEW
242
        if err != nil {
×
NEW
243
                return fmt.Errorf("unable to connect to RPC server: %w", err)
×
NEW
244
        }
×
245

246
        // If we were able to connect to the remote signer, we store the
247
        // connection in the OutboundConnection struct.
NEW
248
        r.conn = conn
×
NEW
249
        r.SignerClient = signrpc.NewSignerClient(conn)
×
NEW
250
        r.WalletKitClient = walletrpc.NewWalletKitClient(conn)
×
NEW
251

×
NEW
252
        return nil
×
253
}
254

255
// A compile time assertion to ensure OutboundConnection meets the
256
// RemoteSignerConnection interface.
257
var _ RemoteSignerConnection = (*OutboundConnection)(nil)
258

259
// InboundRemoteSignerConnection is an interface that abstracts the
260
// communication with an outbound remote signer. It extends the
261
// RemoteSignerConnection insterface.
262
type InboundRemoteSignerConnection interface {
263
        RemoteSignerConnection
264

265
        // AddConnection feeds the inbound connection handler with the incoming
266
        // stream set up by an outbound remote signer and then blocks until the
267
        // stream is closed. Lnd can then send any requests to the remote signer
268
        // through the stream.
269
        AddConnection(stream StreamServer) error
270
}
271

272
// InboundConnection is an abstraction that manages the inbound connection that
273
// is set up by an outbound remote signer that connects to the watch-only node.
274
type InboundConnection struct {
275
        *SignCoordinator
276

277
        connectionTimeout time.Duration
278
}
279

280
// NewInboundConnection creates a new InboundConnection instance.
281
func NewInboundConnection(requestTimeout time.Duration,
NEW
282
        connectionTimeout time.Duration) *InboundConnection {
×
NEW
283

×
NEW
284
        remoteSigner := &InboundConnection{
×
NEW
285
                connectionTimeout: connectionTimeout,
×
NEW
286
        }
×
NEW
287

×
NEW
288
        remoteSigner.SignCoordinator = NewSignCoordinator(
×
NEW
289
                requestTimeout, connectionTimeout,
×
NEW
290
        )
×
NEW
291

×
NEW
292
        return remoteSigner
×
NEW
293
}
×
294

295
// Timeout returns the set connection timeout for the remote signer.
296
//
297
// NOTE: This is part of the RemoteSignerConnection interface.
NEW
298
func (r *InboundConnection) Timeout() time.Duration {
×
NEW
299
        return r.connectionTimeout
×
NEW
300
}
×
301

302
// Ready returns a channel that nil gets sent over once the remote signer
303
// connected and is ready to accept requests.
304
//
305
// NOTE: This is part of the RemoteSignerConnection interface.
NEW
306
func (r *InboundConnection) Ready() chan error {
×
NEW
307
        readyChan := make(chan error, 1)
×
NEW
308

×
NEW
309
        // We wait for the remote signer to connect in a go func and signal
×
NEW
310
        // over the channel once it's ready.
×
NEW
311
        go func() {
×
NEW
312
                log.Infof("Waiting for the remote signer to connect")
×
NEW
313

×
NEW
314
                readyChan <- r.SignCoordinator.WaitUntilConnected()
×
NEW
315
                close(readyChan)
×
NEW
316
        }()
×
317

NEW
318
        return readyChan
×
319
}
320

321
// Ping verifies that the remote signer is still responsive.
322
//
323
// NOTE: This is part of the RemoteSignerConnection interface.
324
func (r *InboundConnection) Ping(_ context.Context,
NEW
325
        timeout time.Duration) error {
×
NEW
326

×
NEW
327
        pong, err := r.SignCoordinator.Ping(timeout)
×
NEW
328
        if err != nil {
×
NEW
329
                return fmt.Errorf("ping request to remote signer "+
×
NEW
330
                        "errored: %w", err)
×
NEW
331
        }
×
332

NEW
333
        if !pong {
×
NEW
334
                return errors.New("incorrect Pong response from remote signer")
×
NEW
335
        }
×
336

NEW
337
        return nil
×
338
}
339

340
// AddConnection feeds the inbound connection handler with the incoming stream
341
// set up by an outbound remote signer and then blocks until the stream is
342
// closed. Lnd can then send any requests to the remote signer through the
343
// stream.
344
//
345
// NOTE: This is part of the InboundRemoteSignerConnection interface.
NEW
346
func (r *InboundConnection) AddConnection(stream StreamServer) error {
×
NEW
347
        return r.SignCoordinator.Run(stream)
×
NEW
348
}
×
349

350
// A compile time assertion to ensure InboundConnection meets the
351
// RemoteSigner interface.
352
var _ InboundRemoteSignerConnection = (*InboundConnection)(nil)
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc