From cfbade12394c4c6b6434b0d627da4ff90f2ce0cb Mon Sep 17 00:00:00 2001 From: Slyghtning Date: Fri, 31 Jul 2026 21:19:37 +0200 Subject: [PATCH] assets: reject malformed RFQ asset rates Validate the rate pointer and decimal coefficient before converting asset units. Return errors for nil, malformed, non-positive, and oversized-scale rates instead of allowing nil dereferences or division-by-zero panics. Add regression tests for each case. --- assets/client.go | 31 +++++++++++++++++++++++++++++-- assets/client_test.go | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/assets/client.go b/assets/client.go index ef320af8..39090afc 100644 --- a/assets/client.go +++ b/assets/client.go @@ -5,6 +5,7 @@ import ( "encoding/hex" "fmt" "math" + "math/big" "os" "path/filepath" "sync" @@ -12,7 +13,6 @@ import ( "github.com/btcsuite/btcd/btcutil" "github.com/lightninglabs/taproot-assets/rfqmath" - "github.com/lightninglabs/taproot-assets/rpcutils" "github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" @@ -275,7 +275,7 @@ func (c *TapdClient) GetAssetPrice(ctx context.Context, assetID string, func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) ( btcutil.Amount, error) { - rateFP, err := rpcutils.UnmarshalRfqFixedPoint(assetRate) + rateFP, err := unmarshalAssetRate(assetRate) if err != nil { return 0, fmt.Errorf("cannot unmarshal asset rate: %w", err) } @@ -287,6 +287,33 @@ func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) ( return msatAmt.ToSatoshis(), nil } +// unmarshalAssetRate validates and converts an RPC asset rate to the fixed +// point representation used for RFQ arithmetic. +func unmarshalAssetRate(assetRate *rfqrpc.FixedPoint) ( + *rfqmath.BigIntFixedPoint, error) { + + if assetRate == nil { + return nil, fmt.Errorf("asset rate cannot be nil") + } + if assetRate.Scale > math.MaxUint8 { + return nil, fmt.Errorf("scale value overflow: %v", assetRate.Scale) + } + + coefficient, ok := new(big.Int).SetString(assetRate.Coefficient, 10) + if !ok { + return nil, fmt.Errorf("invalid asset rate coefficient: %q", + assetRate.Coefficient) + } + if coefficient.Sign() <= 0 { + return nil, fmt.Errorf("asset rate coefficient must be positive") + } + + return &rfqmath.BigIntFixedPoint{ + Coefficient: rfqmath.NewBigInt(coefficient), + Scale: uint8(assetRate.Scale), + }, nil +} + // getPaymentMaxAmount returns the milisat amount we are willing to pay for the // payment. func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( diff --git a/assets/client_test.go b/assets/client_test.go index 5fd2b2e1..af3f671e 100644 --- a/assets/client_test.go +++ b/assets/client_test.go @@ -308,6 +308,39 @@ func TestGetSatsFromAssetAmt(t *testing.T) { expected: btcutil.Amount(0), expectError: false, }, + { + assetAmt: 1000, + assetRate: nil, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "not-a-number", Scale: 0, + }, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "0", Scale: 0, + }, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "-1", Scale: 0, + }, + expectError: true, + }, + { + assetAmt: 1000, + assetRate: &rfqrpc.FixedPoint{ + Coefficient: "1", Scale: 256, + }, + expectError: true, + }, } for _, test := range tests {