-
Notifications
You must be signed in to change notification settings - Fork 134
assets: harden tapd client lifecycle and RFQ handling #1189
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -4,6 +4,8 @@ import ( | |
| "context" | ||
| "encoding/hex" | ||
| "fmt" | ||
| "math" | ||
| "math/big" | ||
| "os" | ||
| "path/filepath" | ||
| "sync" | ||
|
|
@@ -78,14 +80,19 @@ type TapdClient struct { | |
| rfqrpc.RfqClient | ||
| universerpc.UniverseClient | ||
|
|
||
| cfg *TapdConfig | ||
| assetNameCache map[string]string | ||
| assetNameMutex sync.Mutex | ||
| cc *grpc.ClientConn | ||
| rfqTimeoutSeconds uint32 | ||
| assetNameCache map[string]string | ||
| assetNameMutex sync.RWMutex | ||
| cc *grpc.ClientConn | ||
| } | ||
|
|
||
| // NewTapdClient returns a new taproot assets client. | ||
| func NewTapdClient(config *TapdConfig) (*TapdClient, error) { | ||
| rfqTimeoutSeconds, err := getRfqTimeoutSeconds(config.RFQtimeout) | ||
| if err != nil { | ||
| return nil, err | ||
| } | ||
|
|
||
| // Create the client connection to the server. | ||
| conn, err := getClientConn(config) | ||
| if err != nil { | ||
|
|
@@ -96,7 +103,7 @@ func NewTapdClient(config *TapdConfig) (*TapdClient, error) { | |
| client := &TapdClient{ | ||
| assetNameCache: make(map[string]string), | ||
| cc: conn, | ||
| cfg: config, | ||
| rfqTimeoutSeconds: rfqTimeoutSeconds, | ||
| TaprootAssetsClient: taprpc.NewTaprootAssetsClient(conn), | ||
| TaprootAssetChannelsClient: tapchannelrpc.NewTaprootAssetChannelsClient(conn), | ||
| PriceOracleClient: priceoraclerpc.NewPriceOracleClient(conn), | ||
|
|
@@ -139,7 +146,7 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context, | |
| PeerPubKey: peerPubkey, | ||
| PaymentMaxAmt: uint64(paymentMaxAmt), | ||
| Expiry: uint64(expiry), | ||
| TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()), | ||
| TimeoutSeconds: c.rfqTimeoutSeconds, | ||
| }) | ||
| if err != nil { | ||
| return nil, err | ||
|
|
@@ -163,12 +170,13 @@ func (c *TapdClient) GetRfqForAsset(ctx context.Context, | |
| func (c *TapdClient) GetAssetName(ctx context.Context, | ||
| assetId []byte) (string, error) { | ||
|
|
||
| c.assetNameMutex.Lock() | ||
| defer c.assetNameMutex.Unlock() | ||
| assetIdStr := hex.EncodeToString(assetId) | ||
| c.assetNameMutex.RLock() | ||
| if name, ok := c.assetNameCache[assetIdStr]; ok { | ||
| c.assetNameMutex.RUnlock() | ||
| return name, nil | ||
| } | ||
| c.assetNameMutex.RUnlock() | ||
|
|
||
| assetStats, err := c.UniverseClient.QueryAssetStats( | ||
| ctx, &universerpc.AssetStatsQuery{ | ||
|
|
@@ -192,7 +200,9 @@ func (c *TapdClient) GetAssetName(ctx context.Context, | |
| assetName = assetStats.AssetStats[0].Asset.AssetName | ||
| } | ||
|
|
||
| c.assetNameMutex.Lock() | ||
| c.assetNameCache[assetIdStr] = assetName | ||
| c.assetNameMutex.Unlock() | ||
|
|
||
| return assetName, nil | ||
| } | ||
|
|
@@ -220,7 +230,7 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string, | |
| }, | ||
| PaymentMaxAmt: uint64(msatAmt), | ||
| Expiry: uint64(rfqExpiry), | ||
| TimeoutSeconds: uint32(c.cfg.RFQtimeout.Seconds()), | ||
| TimeoutSeconds: c.rfqTimeoutSeconds, | ||
| PeerPubKey: peerPubkey, | ||
| }) | ||
| if err != nil { | ||
|
|
@@ -254,6 +264,19 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string, | |
| func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) ( | ||
| btcutil.Amount, error) { | ||
|
|
||
| if assetRate == nil { | ||
| return 0, fmt.Errorf("asset rate cannot be nil") | ||
| } | ||
|
|
||
| coefficient, ok := new(big.Int).SetString(assetRate.Coefficient, 10) | ||
| if !ok { | ||
| return 0, fmt.Errorf("invalid asset rate coefficient: %q", | ||
| assetRate.Coefficient) | ||
| } | ||
| if coefficient.Sign() <= 0 { | ||
| return 0, fmt.Errorf("asset rate coefficient must be positive") | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We need a similar hardening for GetRfqForAsset. Here is an example how to fix it: https://gist.github.com/starius/c23dd24330c3d36c9e852d2f4596313f |
||
|
|
||
| rateFP, err := rpcutils.UnmarshalRfqFixedPoint(assetRate) | ||
| if err != nil { | ||
| return 0, fmt.Errorf("cannot unmarshal asset rate: %w", err) | ||
|
|
@@ -288,6 +311,26 @@ func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( | |
| ) | ||
| } | ||
|
|
||
| // getRfqTimeoutSeconds converts the configured RFQ timeout to the whole | ||
| // seconds accepted by tapd. Sub-second precision is rounded up so a positive | ||
| // timeout can never become tapd's invalid zero value. | ||
|
Comment on lines
+298
to
+299
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Isn't there a stronger reason to do it so we don't get a timeout error? If we account on 1.5s timeout (and actively using it, e.g. use it at time 1.4s, i.e. 0.1s before the timeout), but tapd enforces the timeout of just 1s (rounded down, as before), we get a timeout error. So we round up to request enough time, right? |
||
| func getRfqTimeoutSeconds(timeout time.Duration) (uint32, error) { | ||
| if timeout <= 0 { | ||
| return 0, fmt.Errorf("RFQ timeout must be greater than zero") | ||
| } | ||
|
|
||
| seconds := timeout / time.Second | ||
| if timeout%time.Second != 0 { | ||
| seconds++ | ||
| } | ||
| if seconds > time.Duration(math.MaxUint32) { | ||
| return 0, fmt.Errorf("RFQ timeout exceeds maximum of %v seconds", | ||
| uint64(math.MaxUint32)) | ||
| } | ||
|
|
||
| return uint32(seconds), nil | ||
| } | ||
|
|
||
| func getClientConn(config *TapdConfig) (*grpc.ClientConn, error) { | ||
| // Load the specified TLS certificate and build transport credentials. | ||
| creds, err := credentials.NewClientTLSFromFile(config.TLSPath, "") | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -166,13 +166,23 @@ func (d *Daemon) Start() error { | |
| // and we can just return the error. | ||
| err = d.initialize(true) | ||
| if errors.Is(err, bbolterrors.ErrTimeout) { | ||
| if d.assetClient != nil { | ||
| d.assetClient.Close() | ||
| d.assetClient = nil | ||
| } | ||
|
|
||
| // We're trying to be started as a standalone Loop daemon, most | ||
| // likely LiT is already running and blocking the DB | ||
| return fmt.Errorf("%v: make sure no other loop daemon process "+ | ||
| "(standalone or embedded in lightning-terminal) is"+ | ||
| "running", err) | ||
| } | ||
| if err != nil { | ||
| if d.assetClient != nil { | ||
| d.assetClient.Close() | ||
| d.assetClient = nil | ||
| } | ||
|
Collaborator
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I propose to add |
||
|
|
||
| return err | ||
| } | ||
|
|
||
|
|
@@ -1145,6 +1155,10 @@ func (d *Daemon) stop() { | |
| if d.clientCleanup != nil { | ||
| d.clientCleanup() | ||
| } | ||
| if d.assetClient != nil { | ||
| d.assetClient.Close() | ||
| d.assetClient = nil | ||
| } | ||
|
|
||
| // Everything should be shutting down now, wait for completion. | ||
| d.wg.Wait() | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Let's preserve the
deferpattern somehow. Maybe make private accessor methods for this map? To encapsulate map+mutex work there.