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

lightningnetwork / lnd / 14000719599

21 Mar 2025 08:54PM UTC coverage: 58.717% (-10.3%) from 68.989%
14000719599

Pull #8754

github

web-flow
Merge 29f363f18 into 5235f3b24
Pull Request #8754: Add `Outbound` Remote Signer implementation

1562 of 2088 new or added lines in 41 files covered. (74.81%)

28126 existing lines in 464 files now uncovered.

97953 of 166822 relevant lines covered (58.72%)

1.82 hits per line

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

82.61
/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,
123
        cfg lncfg.ConnectionCfg) (*OutboundConnection, error) {
3✔
124

3✔
125
        remoteSigner := &OutboundConnection{
3✔
126
                cfg: cfg,
3✔
127
        }
3✔
128

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

135
        return remoteSigner, nil
3✔
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.
142
func (r *OutboundConnection) Ready() chan error {
3✔
143
        // The inbound remote signer is ready as soon we have connected to the
3✔
144
        // remote signer node in the constructor. Therefore, we always send
3✔
145
        // nil here to signal that we are ready.
3✔
146
        readyChan := make(chan error, 1)
3✔
147
        readyChan <- nil
3✔
148

3✔
149
        return readyChan
3✔
150
}
3✔
151

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

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

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

2✔
173
        _, err := r.SignMessage(ctx, signMsgReq)
2✔
174

2✔
175
        return err
2✔
176
}
177

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

185
// Stop closes the connection to the remote signer.
186
//
187
// NOTE: This is part of the RemoteSignerConnection interface.
188
func (r *OutboundConnection) Stop() {
3✔
189
        if r.conn != nil {
6✔
190
                r.conn.Close()
3✔
191
        }
3✔
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,
197
        cfg lncfg.ConnectionCfg) error {
3✔
198

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

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

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

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

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

3✔
233
        ctxt, cancel := context.WithTimeout(ctx, cfg.Timeout)
3✔
234

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

3✔
241
        conn, err := grpc.DialContext(ctxt, cfg.RPCHost, opts...)
3✔
242
        if err != nil {
3✔
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.
248
        r.conn = conn
3✔
249
        r.SignerClient = signrpc.NewSignerClient(conn)
3✔
250
        r.WalletKitClient = walletrpc.NewWalletKitClient(conn)
3✔
251

3✔
252
        return nil
3✔
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,
282
        connectionTimeout time.Duration) *InboundConnection {
3✔
283

3✔
284
        remoteSigner := &InboundConnection{
3✔
285
                connectionTimeout: connectionTimeout,
3✔
286
        }
3✔
287

3✔
288
        remoteSigner.SignCoordinator = NewSignCoordinator(
3✔
289
                requestTimeout, connectionTimeout,
3✔
290
        )
3✔
291

3✔
292
        return remoteSigner
3✔
293
}
3✔
294

295
// Timeout returns the set connection timeout for the remote signer.
296
//
297
// NOTE: This is part of the RemoteSignerConnection interface.
298
func (r *InboundConnection) Timeout() time.Duration {
3✔
299
        return r.connectionTimeout
3✔
300
}
3✔
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.
306
func (r *InboundConnection) Ready() chan error {
3✔
307
        readyChan := make(chan error, 1)
3✔
308

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

3✔
314
                readyChan <- r.SignCoordinator.WaitUntilConnected()
3✔
315
                close(readyChan)
3✔
316
        }()
3✔
317

318
        return readyChan
3✔
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,
325
        timeout time.Duration) error {
2✔
326

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

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

337
        return nil
2✔
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.
346
func (r *InboundConnection) AddConnection(stream StreamServer) error {
3✔
347
        return r.SignCoordinator.Run(stream)
3✔
348
}
3✔
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