Skip to content

Commit c254a91

Browse files
committed
authmailbox: add new server and client implementation
1 parent e053e01 commit c254a91

File tree

9 files changed

+2401
-0
lines changed

9 files changed

+2401
-0
lines changed

authmailbox/client.go

Lines changed: 259 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,259 @@
1+
package authmailbox
2+
3+
import (
4+
"context"
5+
"crypto/tls"
6+
"errors"
7+
"fmt"
8+
"net"
9+
"sync/atomic"
10+
"time"
11+
12+
"github.com/btcsuite/btcd/btcec/v2"
13+
"github.com/btcsuite/btclog/v2"
14+
"github.com/lightninglabs/lndclient"
15+
"github.com/lightninglabs/taproot-assets/proof"
16+
mboxrpc "github.com/lightninglabs/taproot-assets/taprpc/authmailboxrpc"
17+
"github.com/lightningnetwork/lnd/keychain"
18+
"github.com/lightningnetwork/lnd/lnutils"
19+
"github.com/lightningnetwork/lnd/tor"
20+
"google.golang.org/grpc"
21+
"google.golang.org/grpc/credentials"
22+
)
23+
24+
var (
25+
// ErrServerShutdown is the error returned if the mailbox server signals
26+
// it's going to shut down.
27+
ErrServerShutdown = errors.New("server shutting down")
28+
29+
// ErrServerErrored is the error returned if the mailbox server
30+
// sends back an error instead of a proper message.
31+
ErrServerErrored = errors.New("server sent unexpected error")
32+
33+
// ErrClientShutdown is the error returned if the mailbox client itself
34+
// is shutting down.
35+
ErrClientShutdown = errors.New("client shutting down")
36+
37+
// ErrAuthCanceled is returned if the authentication process of a single
38+
// mailbox subscription is aborted.
39+
ErrAuthCanceled = errors.New("authentication was canceled")
40+
)
41+
42+
// ClientConfig holds the configuration options for the mailbox client.
43+
type ClientConfig struct {
44+
// ServerAddress is the domain:port of the mailbox server.
45+
ServerAddress string
46+
47+
// ProxyAddress is the SOCKS proxy that should be used to establish the
48+
// connection.
49+
ProxyAddress string
50+
51+
// Insecure signals that no TLS should be used if set to true.
52+
Insecure bool
53+
54+
// TLSPathServer is the path to a local file that holds the mailbox
55+
// server's TLS certificate. This is only needed if the server is using
56+
// a self-signed cert.
57+
TLSPathServer string
58+
59+
// DialOpts is a list of additional options that should be used when
60+
// dialing the gRPC connection.
61+
DialOpts []grpc.DialOption
62+
63+
// Signer is the signing interface used to sign messages during the
64+
// authentication handshake with the mailbox server.
65+
Signer lndclient.SignerClient
66+
67+
// MinBackoff is the minimum time waited before the next re-connect
68+
// attempt is made. After each try the backoff is doubled until
69+
// MaxBackoff is reached.
70+
MinBackoff time.Duration
71+
72+
// MaxBackoff is the maximum time waited between connection attempts.
73+
MaxBackoff time.Duration
74+
}
75+
76+
// Client performs the client side part of mailbox message exchange.
77+
type Client struct {
78+
cfg *ClientConfig
79+
80+
started atomic.Bool
81+
stopped atomic.Bool
82+
83+
serverConn *grpc.ClientConn
84+
client mboxrpc.MailboxClient
85+
}
86+
87+
// NewClient returns a new instance to initiate mailbox connections with.
88+
func NewClient(cfg *ClientConfig) *Client {
89+
return &Client{
90+
cfg: cfg,
91+
}
92+
}
93+
94+
// Start starts the client, establishing the connection to the server.
95+
func (c *Client) Start() error {
96+
if !c.started.CompareAndSwap(false, true) {
97+
return nil
98+
}
99+
100+
dialOpts, err := getServerDialOpts(
101+
c.cfg.Insecure, c.cfg.ProxyAddress, c.cfg.TLSPathServer,
102+
c.cfg.DialOpts...,
103+
)
104+
if err != nil {
105+
return err
106+
}
107+
108+
serverConn, err := grpc.Dial(c.cfg.ServerAddress, dialOpts...)
109+
if err != nil {
110+
return fmt.Errorf("unable to connect to RPC server: %w", err)
111+
}
112+
113+
c.serverConn = serverConn
114+
c.client = mboxrpc.NewMailboxClient(serverConn)
115+
116+
return nil
117+
}
118+
119+
// Stop shuts down the client connection to the mailbox server.
120+
func (c *Client) Stop() error {
121+
if !c.stopped.CompareAndSwap(false, true) {
122+
return nil
123+
}
124+
125+
log.Infof("Shutting down mailbox client")
126+
127+
return c.serverConn.Close()
128+
}
129+
130+
// SendMessage sends a message to the mailbox server. The receiverKey is the
131+
// public key of the receiver, senderEphemeralKey is the ephemeral key used
132+
// to encrypt the message, encryptedPayload is the encrypted message payload
133+
// and txProof is the proof of the transaction that contains the message.
134+
func (c *Client) SendMessage(ctx context.Context, receiverKey btcec.PublicKey,
135+
senderEphemeralKey btcec.PublicKey, encryptedPayload []byte,
136+
txProof proof.TxProof, expiryBlockHeight uint32) (uint64, error) {
137+
138+
if c.stopped.Load() {
139+
return 0, ErrClientShutdown
140+
}
141+
142+
rpcProof, err := proof.MarshalTxProof(txProof)
143+
if err != nil {
144+
return 0, fmt.Errorf("unable to marshal tx proof: %w", err)
145+
}
146+
147+
resp, err := c.client.SendMessage(ctx, &mboxrpc.SendMessageRequest{
148+
ReceiverId: receiverKey.SerializeCompressed(),
149+
EncryptedPayload: encryptedPayload,
150+
SenderEphemeralPubkey: senderEphemeralKey.SerializeCompressed(),
151+
Proof: &mboxrpc.SendMessageRequest_TxProof{
152+
TxProof: rpcProof,
153+
},
154+
ExpiryBlockHeight: expiryBlockHeight,
155+
})
156+
if err != nil {
157+
return 0, fmt.Errorf("unable to send message: %w", err)
158+
}
159+
160+
return resp.MessageId, nil
161+
}
162+
163+
// StartAccountSubscription opens a stream to the server and subscribes to all
164+
// updates that concern the given account, including all orders that spend from
165+
// that account. Only a single stream is ever open to the server, so a second
166+
// call to this method will send a second subscription over the same stream,
167+
// multiplexing all messages into the same connection. A stream can be
168+
// long-lived, so this can be called for every account as soon as it's confirmed
169+
// open. This method will return as soon as the authentication was successful.
170+
// Messages sent from the server can then be received on the FromServerChan
171+
// channel.
172+
func (c *Client) StartAccountSubscription(ctx context.Context,
173+
receiverKey keychain.KeyDescriptor,
174+
filter MessageFilter) (ReceiveSubscription, error) {
175+
176+
if c.stopped.Load() {
177+
return nil, ErrClientShutdown
178+
}
179+
180+
ctxl := btclog.WithCtx(
181+
ctx, lnutils.LogPubKey("receiver_key", receiverKey.PubKey),
182+
"server", false,
183+
)
184+
185+
return c.connectAndAuthenticate(ctxl, receiverKey, filter)
186+
}
187+
188+
// connectAndAuthenticate opens a stream to the server and authenticates the
189+
// account to receive updates. It returns the subscription and a bool that
190+
// indicates if recovery can be continued. That value can safely be ignored if
191+
// recovery is not requested. Checking the returned error must take precedence
192+
// to the boolean flag.
193+
func (c *Client) connectAndAuthenticate(ctx context.Context,
194+
acctKey keychain.KeyDescriptor,
195+
filter MessageFilter) (*receiveSubscription, error) {
196+
197+
var receiverKey [33]byte
198+
copy(receiverKey[:], acctKey.PubKey.SerializeCompressed())
199+
200+
// Before we can expect to receive any updates, we need to perform the
201+
// 3-way authentication handshake.
202+
sub := newReceiveSubscription(c.cfg, acctKey, filter, c.client)
203+
err := sub.connectAndAuthenticate(ctx, 0)
204+
if err != nil {
205+
log.ErrorS(ctx, "Authentication failed", err)
206+
207+
return nil, err
208+
}
209+
210+
return sub, nil
211+
}
212+
213+
// getServerDialOpts returns the dial options to connect to the mailbox server.
214+
func getServerDialOpts(insecure bool, proxyAddress, tlsPath string,
215+
dialOpts ...grpc.DialOption) ([]grpc.DialOption, error) {
216+
217+
// Create a copy of the dial options array.
218+
opts := dialOpts
219+
220+
// There are three options to connect to a mailbox server, either
221+
// insecure, using a self-signed certificate or with a certificate
222+
// signed by a public CA.
223+
switch {
224+
case insecure:
225+
opts = append(opts, grpc.WithInsecure())
226+
227+
case tlsPath != "":
228+
// Load the specified TLS certificate and build
229+
// transport credentials
230+
creds, err := credentials.NewClientTLSFromFile(tlsPath, "")
231+
if err != nil {
232+
return nil, err
233+
}
234+
opts = append(opts, grpc.WithTransportCredentials(creds))
235+
236+
default:
237+
creds := credentials.NewTLS(&tls.Config{})
238+
opts = append(opts, grpc.WithTransportCredentials(creds))
239+
}
240+
241+
// If a SOCKS proxy address was specified,
242+
// then we should dial through it.
243+
if proxyAddress != "" {
244+
log.Infof("Proxying connection to mailbox server over Tor "+
245+
"SOCKS proxy %v", proxyAddress)
246+
247+
torDialer := func(_ context.Context, addr string) (net.Conn,
248+
error) {
249+
250+
return tor.Dial(
251+
addr, proxyAddress, false, false,
252+
tor.DefaultConnTimeout,
253+
)
254+
}
255+
opts = append(opts, grpc.WithContextDialer(torDialer))
256+
}
257+
258+
return opts, nil
259+
}

0 commit comments

Comments
 (0)