Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 4 additions & 4 deletions apollo.go
Original file line number Diff line number Diff line change
Expand Up @@ -1701,18 +1701,18 @@ func referenceScriptFeeForSize(refScriptSize int, pp backend.ProtocolParameters)
// reference-script fee. If the backend did not supply the base price, charging
// zero would silently underprice the transaction and produce a FeeTooSmallUTxO
// rejection at submission; fail loudly instead.
refFeePerByte := pp.RefScriptFeePerByte()
if refFeePerByte <= 0 {
refFeePerByte := pp.RefScriptFeePerByteRational()
if refFeePerByte == nil || refFeePerByte.Sign() <= 0 {
return 0, fmt.Errorf(
"transaction uses %d bytes of reference scripts but the protocol parameters provide no reference-script price (min_fee_ref_script_cost_per_byte); cannot compute a correct minimum fee",
refScriptSize,
)
}
return backend.TierRefScriptFee(
return backend.TierRefScriptFeeRational(
refScriptSize,
refFeePerByte,
pp.RefScriptSizeIncrement(),
pp.RefScriptMultiplier(),
pp.RefScriptMultiplierRational(),
), nil
}

Expand Down
113 changes: 88 additions & 25 deletions backend/base.go
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,20 @@ type ProtocolParameters struct {
MinFeeReferenceScriptsRange int `json:"min_fee_reference_scripts_range"`
MinFeeReferenceScriptsBase int `json:"min_fee_reference_scripts_base"`
MinFeeReferenceScriptsMultiplier int `json:"min_fee_reference_scripts_multiplier"`
// MinFeeRefScriptCostPerByteRational preserves the exact provider-supplied
// first-tier reference-script price. It takes precedence over the legacy
// float64 field below when computing transaction fees.
MinFeeRefScriptCostPerByteRational *big.Rat `json:"-"`
// MinFeeReferenceScriptsMultiplierRational preserves the exact
// provider-supplied per-tier multiplier. It takes precedence over the
// legacy integer field above when computing transaction fees.
MinFeeReferenceScriptsMultiplierRational *big.Rat `json:"-"`
// MinFeeRefScriptCostPerByte is the BlockFrost/ledger flat name for the
// reference-script base price (lovelace per byte for the first tier). Some
// providers (e.g. BlockFrost) expose only this field and not the structured
// MinFeeReferenceScripts{Base,Range,Multiplier} triple. RefScriptFeePerByte()
// reconciles the two representations.
// MinFeeReferenceScripts{Base,Range,Multiplier} triple. It is retained for
// compatibility; use MinFeeRefScriptCostPerByteRational for fee computation
// when a provider supplies a fractional value.
MinFeeRefScriptCostPerByte float64 `json:"min_fee_ref_script_cost_per_byte"`
}

Expand All @@ -103,16 +112,34 @@ type ProtocolParameters struct {
const (
DefaultRefScriptSizeIncrement = 25600
DefaultRefScriptMultiplier = 1.2
defaultRefScriptMultiplierNum = 6
defaultRefScriptMultiplierDen = 5
)

// RefScriptFeePerByte returns the base reference-script price (lovelace per
// byte for the first tier), preferring the structured MinFeeReferenceScriptsBase
// when present and falling back to the flat MinFeeRefScriptCostPerByte.
// It is retained for compatibility; fee computation should use
// RefScriptFeePerByteRational to avoid precision loss.
func (p ProtocolParameters) RefScriptFeePerByte() float64 {
r := p.RefScriptFeePerByteRational()
if r == nil {
return 0
}
value, _ := r.Float64()
return value
}

// RefScriptFeePerByteRational returns the exact first-tier reference-script
// price. The returned rational is a copy and may be safely modified by callers.
func (p ProtocolParameters) RefScriptFeePerByteRational() *big.Rat {
if p.MinFeeReferenceScriptsBase > 0 {
return float64(p.MinFeeReferenceScriptsBase)
return big.NewRat(int64(p.MinFeeReferenceScriptsBase), 1)
}
if p.MinFeeRefScriptCostPerByteRational != nil {
return new(big.Rat).Set(p.MinFeeRefScriptCostPerByteRational)
}
return p.MinFeeRefScriptCostPerByte
return rationalFromFloat64(p.MinFeeRefScriptCostPerByte)
}

// RefScriptSizeIncrement returns the per-tier size increment, defaulting to the
Expand All @@ -126,11 +153,24 @@ func (p ProtocolParameters) RefScriptSizeIncrement() int {

// RefScriptMultiplier returns the per-tier price multiplier, defaulting to the
// Conway ledger constant when a provider does not supply it.
// It is retained for compatibility; fee computation should use
// RefScriptMultiplierRational to avoid precision loss.
func (p ProtocolParameters) RefScriptMultiplier() float64 {
value, _ := p.RefScriptMultiplierRational().Float64()
return value
}

// RefScriptMultiplierRational returns the exact per-tier reference-script
// multiplier. The returned rational is a copy and may be safely modified by
// callers.
func (p ProtocolParameters) RefScriptMultiplierRational() *big.Rat {
if p.MinFeeReferenceScriptsMultiplierRational != nil && p.MinFeeReferenceScriptsMultiplierRational.Sign() > 0 {
return new(big.Rat).Set(p.MinFeeReferenceScriptsMultiplierRational)
}
if p.MinFeeReferenceScriptsMultiplier > 0 {
return float64(p.MinFeeReferenceScriptsMultiplier)
return big.NewRat(int64(p.MinFeeReferenceScriptsMultiplier), 1)
}
return DefaultRefScriptMultiplier
return big.NewRat(defaultRefScriptMultiplierNum, defaultRefScriptMultiplierDen)
}

// TierRefScriptFee computes the Conway tiered reference-script fee for a total
Expand All @@ -144,37 +184,36 @@ func (p ProtocolParameters) RefScriptMultiplier() float64 {
// with the first-tier price = baseFeePerByte. A zero base price yields a zero
// fee (pre-Conway / provider that does not charge for reference scripts).
//
// The accumulation uses exact rational arithmetic and floors once at the end,
// matching the ledger (which accumulates a Rational and applies floor). Using
// float64 here would let the repeated multiplier product (1.2 is not exactly
// representable in binary floating point) drift across an integer boundary on
// multi-tier reference scripts, producing a fee 1 lovelace below the ledger
// minimum and a FeeTooSmall rejection.
// The float64 arguments are retained for compatibility. New code should use
// TierRefScriptFeeRational so provider-supplied fractions never pass through a
// float64. The decimal representation of the legacy floats is converted to an
// exact rational without quantizing the multiplier.
func TierRefScriptFee(totalRefScriptSize int, baseFeePerByte float64, sizeIncrement int, multiplier float64) int64 {
if totalRefScriptSize <= 0 || baseFeePerByte <= 0 {
base := rationalFromFloat64(baseFeePerByte)
m := rationalFromFloat64(multiplier)
return TierRefScriptFeeRational(totalRefScriptSize, base, sizeIncrement, m)
}

// TierRefScriptFeeRational computes the Conway tiered reference-script fee
// using exact provider-supplied rational values. The result is the ledger's
// floor of the final accumulated rational fee.
func TierRefScriptFeeRational(totalRefScriptSize int, baseFeePerByte *big.Rat, sizeIncrement int, multiplier *big.Rat) int64 {
if totalRefScriptSize <= 0 || baseFeePerByte == nil || baseFeePerByte.Sign() <= 0 {
return 0
}
if sizeIncrement <= 0 {
sizeIncrement = DefaultRefScriptSizeIncrement
}
if multiplier <= 0 {
multiplier = DefaultRefScriptMultiplier
if multiplier == nil || multiplier.Sign() <= 0 {
multiplier = big.NewRat(defaultRefScriptMultiplierNum, defaultRefScriptMultiplierDen)
}
// Exact rationals. baseFeePerByte is an integer lovelace/byte in practice
// (15 at current params); the multiplier is the ledger constant 6/5. Recover
// it exactly from the float (1.2 -> 1200/1000 -> 6/5) so there is no drift.
base := new(big.Rat).SetFloat64(baseFeePerByte)
if base == nil { // NaN/Inf guard
return 0
}
m := big.NewRat(int64(math.Round(multiplier*1000)), 1000)
acc := new(big.Rat)
price := new(big.Rat).Set(base)
price := new(big.Rat).Set(baseFeePerByte)
incr := new(big.Rat).SetInt64(int64(sizeIncrement))
n := totalRefScriptSize
for n >= sizeIncrement {
acc.Add(acc, new(big.Rat).Mul(incr, price))
price.Mul(price, m)
price.Mul(price, multiplier)
n -= sizeIncrement
}
if n > 0 {
Expand All @@ -185,6 +224,30 @@ func TierRefScriptFee(totalRefScriptSize int, baseFeePerByte float64, sizeIncrem
return new(big.Int).Quo(acc.Num(), acc.Denom()).Int64()
}

// ParseRational parses a provider-supplied rational number without passing it
// through floating point. Decimal and exponent forms accepted by JSON numbers
// are supported by math/big.
func ParseRational(s string) (*big.Rat, error) {
value, ok := new(big.Rat).SetString(s)
if !ok {
return nil, fmt.Errorf("invalid rational number %q", s)
}
return value, nil
}

func rationalFromFloat64(value float64) *big.Rat {
if math.IsNaN(value) || math.IsInf(value, 0) {
return nil
}
// FormatFloat's shortest decimal is the value callers passed to the legacy
// API, rather than an arbitrary fixed-precision approximation.
rational, ok := new(big.Rat).SetString(strconv.FormatFloat(value, 'g', -1, 64))
if !ok {
return nil
}
return rational
}

// CoinsPerUtxoByteValue returns the coins per UTxO byte value parsed from the string field.
// Negative or absurdly large values (which would corrupt min-UTxO math downstream)
// fall back to the protocol default.
Expand Down
47 changes: 27 additions & 20 deletions backend/blockfrost/blockfrost.go
Original file line number Diff line number Diff line change
Expand Up @@ -843,7 +843,7 @@ type bfProtocolParams struct {
// BlockFrost exposes the Conway reference-script base price under this flat
// key (lovelace per byte for the first tier); it does not return the
// structured min_fee_reference_scripts_{base,range,multiplier} triple.
MinFeeRefScriptCostPerByte float64 `json:"min_fee_ref_script_cost_per_byte"`
MinFeeRefScriptCostPerByte json.Number `json:"min_fee_ref_script_cost_per_byte"`
}

func (p *bfProtocolParams) toProtocolParams() (backend.ProtocolParameters, error) {
Expand All @@ -868,25 +868,32 @@ func (p *bfProtocolParams) toProtocolParams() (backend.ProtocolParameters, error
return backend.ProtocolParameters{}, err
}
pp := backend.ProtocolParameters{
MinFeeConstant: p.MinFeeB,
MinFeeCoefficient: p.MinFeeA,
MaxBlockSize: maxBlockSize,
MaxTxSize: maxTxSize,
MaxBlockHeaderSize: maxBlockHeaderSize,
KeyDeposits: p.KeyDeposit,
PoolDeposits: p.PoolDeposit,
MinPoolCost: p.MinPoolCost,
PriceMem: p.PriceMem,
PriceStep: p.PriceStep,
MaxTxExMem: p.MaxTxExMem,
MaxTxExSteps: p.MaxTxExSteps,
MaxBlockExMem: p.MaxBlockExMem,
MaxBlockExSteps: p.MaxBlockExSteps,
MaxValSize: p.MaxValSize,
CollateralPercent: collateralPercent,
MaxCollateralInputs: maxCollateralInputs,
CoinsPerUtxoByte: p.CoinsPerUtxoSize,
MinFeeRefScriptCostPerByte: p.MinFeeRefScriptCostPerByte,
MinFeeConstant: p.MinFeeB,
MinFeeCoefficient: p.MinFeeA,
MaxBlockSize: maxBlockSize,
MaxTxSize: maxTxSize,
MaxBlockHeaderSize: maxBlockHeaderSize,
KeyDeposits: p.KeyDeposit,
PoolDeposits: p.PoolDeposit,
MinPoolCost: p.MinPoolCost,
PriceMem: p.PriceMem,
PriceStep: p.PriceStep,
MaxTxExMem: p.MaxTxExMem,
MaxTxExSteps: p.MaxTxExSteps,
MaxBlockExMem: p.MaxBlockExMem,
MaxBlockExSteps: p.MaxBlockExSteps,
MaxValSize: p.MaxValSize,
CollateralPercent: collateralPercent,
MaxCollateralInputs: maxCollateralInputs,
CoinsPerUtxoByte: p.CoinsPerUtxoSize,
}
if p.MinFeeRefScriptCostPerByte != "" {
price, err := backend.ParseRational(p.MinFeeRefScriptCostPerByte.String())
if err != nil {
return backend.ProtocolParameters{}, fmt.Errorf("invalid min_fee_ref_script_cost_per_byte: %w", err)
}
pp.MinFeeRefScriptCostPerByteRational = price
pp.MinFeeRefScriptCostPerByte, _ = price.Float64()
}

// Parse cost models from BlockFrost JSON.
Expand Down
27 changes: 27 additions & 0 deletions backend/blockfrost/blockfrost_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -385,6 +385,33 @@ func TestProtocolParamsParsesRefScriptCostPerByte(t *testing.T) {
if got := pp.RefScriptFeePerByte(); got != 15 {
t.Fatalf("RefScriptFeePerByte() = %v, want 15", got)
}
if got, want := pp.RefScriptFeePerByteRational(), big.NewRat(15, 1); got.Cmp(want) != 0 {
t.Fatalf("RefScriptFeePerByteRational() = %s, want %s", got, want)
}
}

func TestProtocolParamsPreservesFractionalRefScriptCostPerByte(t *testing.T) {
const body = `{
"min_fee_a": 44,
"min_fee_b": 155381,
"max_tx_size": 16384,
"coins_per_utxo_size": "4310",
"collateral_percent": 150,
"max_collateral_inputs": 3,
"min_fee_ref_script_cost_per_byte": 15.125
}`

var raw bfProtocolParams
if err := json.Unmarshal([]byte(body), &raw); err != nil {
t.Fatal(err)
}
pp, err := raw.toProtocolParams()
if err != nil {
t.Fatal(err)
}
if got, want := pp.RefScriptFeePerByteRational(), big.NewRat(121, 8); got.Cmp(want) != 0 {
t.Fatalf("RefScriptFeePerByteRational() = %s, want %s", got, want)
}
}

func TestProtocolParamsPrefersCostModelsRaw(t *testing.T) {
Expand Down
7 changes: 7 additions & 0 deletions backend/fixed/fixed.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package fixed
import (
"encoding/hex"
"errors"
"math/big"
"strconv"
"sync"

Expand Down Expand Up @@ -93,6 +94,12 @@ func (f *FixedChainContext) ProtocolParams() (backend.ProtocolParameters, error)
}
pp.CostModels = cm
}
if pp.MinFeeRefScriptCostPerByteRational != nil {
pp.MinFeeRefScriptCostPerByteRational = new(big.Rat).Set(pp.MinFeeRefScriptCostPerByteRational)
}
if pp.MinFeeReferenceScriptsMultiplierRational != nil {
pp.MinFeeReferenceScriptsMultiplierRational = new(big.Rat).Set(pp.MinFeeReferenceScriptsMultiplierRational)
}
return pp, nil
}

Expand Down
27 changes: 16 additions & 11 deletions backend/ogmios/ogmios.go
Original file line number Diff line number Diff line change
Expand Up @@ -381,9 +381,9 @@ type ogmiosProtocolParams struct {
}

type ogmiosRefScripts struct {
Base float64 `json:"base"`
Range int `json:"range"`
Multiplier float64 `json:"multiplier"`
Base json.Number `json:"base"`
Range int `json:"range"`
Multiplier json.Number `json:"multiplier"`
}

type ogmiosLovelace struct {
Expand Down Expand Up @@ -436,14 +436,19 @@ func (p *ogmiosProtocolParams) toProtocolParams() (backend.ProtocolParameters, e
}

if p.MinFeeReferenceScripts != nil {
// Only the base price (lovelace per byte) is provider-specific; the
// per-tier size increment and multiplier are the Conway ledger constants
// (25600 and 6/5). The multiplier is fractional (1.2), so it cannot be
// stored in the integer MinFeeReferenceScriptsMultiplier field without
// truncating to 1 and undercharging every tier after the first; we
// therefore leave it (and the increment) to RefScriptMultiplier() /
// RefScriptSizeIncrement() defaults.
pp.MinFeeRefScriptCostPerByte = p.MinFeeReferenceScripts.Base
base, err := backend.ParseRational(p.MinFeeReferenceScripts.Base.String())
if err != nil {
return backend.ProtocolParameters{}, fmt.Errorf("invalid reference-script base price: %w", err)
}
multiplier, err := backend.ParseRational(p.MinFeeReferenceScripts.Multiplier.String())
if err != nil {
return backend.ProtocolParameters{}, fmt.Errorf("invalid reference-script multiplier: %w", err)
}
pp.MinFeeReferenceScriptsRange = p.MinFeeReferenceScripts.Range
pp.MinFeeRefScriptCostPerByteRational = base
pp.MinFeeReferenceScriptsMultiplierRational = multiplier
// Preserve the legacy float fields for callers that read them directly.
pp.MinFeeRefScriptCostPerByte, _ = base.Float64()
}

// Parse cost models from Ogmios JSON.
Expand Down
Loading