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.
This commit is contained in:
Slyghtning 2026-07-31 21:19:37 +02:00
parent dc39d63d8d
commit cfbade1239
No known key found for this signature in database
GPG key ID: F82D456EA023C9BF
2 changed files with 62 additions and 2 deletions

View file

@ -5,6 +5,7 @@ import (
"encoding/hex" "encoding/hex"
"fmt" "fmt"
"math" "math"
"math/big"
"os" "os"
"path/filepath" "path/filepath"
"sync" "sync"
@ -12,7 +13,6 @@ import (
"github.com/btcsuite/btcd/btcutil" "github.com/btcsuite/btcd/btcutil"
"github.com/lightninglabs/taproot-assets/rfqmath" "github.com/lightninglabs/taproot-assets/rfqmath"
"github.com/lightninglabs/taproot-assets/rpcutils"
"github.com/lightninglabs/taproot-assets/taprpc" "github.com/lightninglabs/taproot-assets/taprpc"
"github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc" "github.com/lightninglabs/taproot-assets/taprpc/priceoraclerpc"
"github.com/lightninglabs/taproot-assets/taprpc/rfqrpc" "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) ( func getSatsFromAssetAmt(assetAmt uint64, assetRate *rfqrpc.FixedPoint) (
btcutil.Amount, error) { btcutil.Amount, error) {
rateFP, err := rpcutils.UnmarshalRfqFixedPoint(assetRate) rateFP, err := unmarshalAssetRate(assetRate)
if err != nil { if err != nil {
return 0, fmt.Errorf("cannot unmarshal asset rate: %w", err) 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 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 // getPaymentMaxAmount returns the milisat amount we are willing to pay for the
// payment. // payment.
func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) ( func getPaymentMaxAmount(satAmount btcutil.Amount, feeLimitMultiplier float64) (

View file

@ -308,6 +308,39 @@ func TestGetSatsFromAssetAmt(t *testing.T) {
expected: btcutil.Amount(0), expected: btcutil.Amount(0),
expectError: false, 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 { for _, test := range tests {