Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
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 @@ -1729,18 +1729,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 @@ -241,11 +241,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 @@ -258,16 +267,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 @@ -281,11 +308,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 @@ -299,37 +339,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 @@ -340,6 +379,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 @@ -862,7 +862,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 @@ -887,25 +887,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 @@ -588,6 +588,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 @@ -103,6 +104,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 @@ -396,9 +396,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 @@ -451,14 +451,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
34 changes: 34 additions & 0 deletions backend/ogmios/ogmios_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -567,3 +567,37 @@ func TestOgmiosScriptRefJSONLanguageDetection(t *testing.T) {
}
}
}

func TestProtocolParamsPreserveReferenceScriptFeeParameters(t *testing.T) {
const body = `{
"scriptExecutionPrices": {"memory": "1/1", "cpu": "1/1"},
"minFeeReferenceScripts": {
"base": 15.125,
"range": 7,
"multiplier": 1.2345
}
}`

var raw ogmiosProtocolParams
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)
}
if got, want := pp.RefScriptMultiplierRational(), big.NewRat(2469, 2000); got.Cmp(want) != 0 {
t.Fatalf("RefScriptMultiplierRational() = %s, want %s", got, want)
}
if got := pp.RefScriptSizeIncrement(); got != 7 {
t.Fatalf("RefScriptSizeIncrement() = %d, want 7", got)
}

want := backend.TierRefScriptFeeRational(53, big.NewRat(121, 8), 7, big.NewRat(2469, 2000))
if got := backend.TierRefScriptFeeRational(53, pp.RefScriptFeePerByteRational(), pp.RefScriptSizeIncrement(), pp.RefScriptMultiplierRational()); got != want {
t.Fatalf("reference-script fee = %d, want %d", got, want)
}
}
Loading