From c1adc83bd384d6cff37947ae08441cb60d1cfe60 Mon Sep 17 00:00:00 2001 From: Jenita Date: Mon, 14 Apr 2025 17:24:16 -0500 Subject: [PATCH 01/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/pparams.go | 36 ++++++++++++++++++++-- ledger/alonzo/pparams_test.go | 58 +++++++++++++++++++++++++++++++++++ 2 files changed, 91 insertions(+), 3 deletions(-) diff --git a/ledger/alonzo/pparams.go b/ledger/alonzo/pparams.go index 6e1d438c..bdee1ebb 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -16,6 +16,7 @@ package alonzo import ( "math" + "sort" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/common" @@ -94,9 +95,38 @@ func (p *AlonzoProtocolParameters) UpdateFromGenesis(genesis *AlonzoGenesis) { StepPrice: &cbor.Rat{Rat: genesis.ExecutionPrices.Steps.Rat}, } } - // TODO: cost models (#852) - // We have 150+ string values to map to array indexes - // CostModels map[string]map[string]int + + // Process cost models + if genesis.CostModels != nil { + p.CostModels = make(map[uint][]int64) + for lang, model := range genesis.CostModels { + var langKey uint + switch lang { + case "plutus:v1": + langKey = 0 + case "plutus:v2": + langKey = 1 + default: + // Skip unknown language + continue + } + + // Get sorted keys to determine indexes + keys := make([]string, 0, len(model)) + for k := range model { + keys = append(keys, k) + } + sort.Strings(keys) + + // Create slice with values in alphabetical order + costs := make([]int64, len(keys)) + for i, key := range keys { + costs[i] = int64(model[key]) + } + + p.CostModels[langKey] = costs + } + } } type AlonzoProtocolParameterUpdate struct { diff --git a/ledger/alonzo/pparams_test.go b/ledger/alonzo/pparams_test.go index 2a4117ca..23737dbf 100644 --- a/ledger/alonzo/pparams_test.go +++ b/ledger/alonzo/pparams_test.go @@ -163,6 +163,64 @@ func TestAlonzoProtocolParamsUpdateFromGenesis(t *testing.T) { AdaPerUtxoByte: 34482 / 8, }, }, + { + startParams: alonzo.AlonzoProtocolParameters{}, + genesisJson: `{ + "lovelacePerUTxOWord": 34482, + "costModels": { + "plutus:v1": { + "addInteger-cpu-arguments-intercept": 10, + "subtractInteger-cpu-arguments-intercept": 30, + "multiplyInteger-cpu-arguments-intercept": 20 + }, + "plutus:v2": { + "appendByteString-cpu-arguments-slope": 5, + "consByteString-cpu-arguments-intercept": 2 + }, + "plutus:v3": { + "unknown-op": 999 + } + } + }`, + expectedParams: alonzo.AlonzoProtocolParameters{ + AdaPerUtxoByte: 34482 / 8, + CostModels: map[uint][]int64{ + 0: { + 10, // addInteger-cpu-arguments-intercept + 20, // multiplyInteger-cpu-arguments-intercept + 30, // subtractInteger-cpu-arguments-intercept + }, + 1: { + 5, // appendByteString-cpu-arguments-slope + 2, // consByteString-cpu-arguments-intercept + }, + }, + }, + }, + { + //out of order keys + startParams: alonzo.AlonzoProtocolParameters{}, + genesisJson: `{ + "lovelacePerUTxOWord": 34482, + "costModels": { + "plutus:v1": { + "zzz": 3, + "aaa": 1, + "mmm": 2 + } + } + }`, + expectedParams: alonzo.AlonzoProtocolParameters{ + AdaPerUtxoByte: 34482 / 8, + CostModels: map[uint][]int64{ + 0: { + 1, // "aaa" + 2, // "mmm" + 3, // "zzz" + }, + }, + }, + }, } for _, testDef := range testDefs { tmpGenesis, err := alonzo.NewAlonzoGenesisFromReader( From 5e32349d1fda5ee7df77cfe01831aa278d8ff9ee Mon Sep 17 00:00:00 2001 From: Jenita Date: Tue, 22 Apr 2025 21:22:57 -0500 Subject: [PATCH 02/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/pparams.go | 215 ++++++++++--- ledger/alonzo/pparams_test.go | 577 ++++++++++++++++------------------ ledger/babbage/pparams.go | 2 +- 3 files changed, 435 insertions(+), 359 deletions(-) diff --git a/ledger/alonzo/pparams.go b/ledger/alonzo/pparams.go index bdee1ebb..8ac640cc 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -15,20 +15,39 @@ package alonzo import ( + //"encoding/json" + "fmt" "math" "sort" + //"strconv" + "strings" + "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/common" "github.com/blinklabs-io/gouroboros/ledger/mary" cardano "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) +type PlutusVersion string + +const ( + PlutusV1 PlutusVersion = "PlutusV1" + PlutusV2 PlutusVersion = "PlutusV2" + PlutusV3 PlutusVersion = "PlutusV3" +) + +type CostModel struct { + Version PlutusVersion + Parameters map[string]int64 + Order []string +} + type AlonzoProtocolParameters struct { mary.MaryProtocolParameters MinPoolCost uint64 AdaPerUtxoByte uint64 - CostModels map[uint][]int64 + CostModels map[PlutusVersion]*CostModel ExecutionCosts common.ExUnitPrice MaxTxExUnits common.ExUnits MaxBlockExUnits common.ExUnits @@ -50,7 +69,7 @@ func (p *AlonzoProtocolParameters) Update( p.AdaPerUtxoByte = *paramUpdate.AdaPerUtxoByte } if paramUpdate.CostModels != nil { - p.CostModels = paramUpdate.CostModels + p.convertLegacyCostModels(paramUpdate.CostModels) } if paramUpdate.ExecutionCosts != nil { p.ExecutionCosts = *paramUpdate.ExecutionCosts @@ -72,10 +91,12 @@ func (p *AlonzoProtocolParameters) Update( } } -func (p *AlonzoProtocolParameters) UpdateFromGenesis(genesis *AlonzoGenesis) { +func (p *AlonzoProtocolParameters) UpdateFromGenesis(genesis *AlonzoGenesis) error { if genesis == nil { - return + return nil } + + // Common parameter updates p.AdaPerUtxoByte = genesis.LovelacePerUtxoWord / 8 p.MaxValueSize = genesis.MaxValueSize p.CollateralPercentage = genesis.CollateralPercentage @@ -88,47 +109,117 @@ func (p *AlonzoProtocolParameters) UpdateFromGenesis(genesis *AlonzoGenesis) { Memory: uint64(genesis.MaxBlockExUnits.Mem), Steps: uint64(genesis.MaxBlockExUnits.Steps), } - if genesis.ExecutionPrices.Mem != nil && - genesis.ExecutionPrices.Steps != nil { + + if genesis.ExecutionPrices.Mem != nil && genesis.ExecutionPrices.Steps != nil { p.ExecutionCosts = common.ExUnitPrice{ MemPrice: &cbor.Rat{Rat: genesis.ExecutionPrices.Mem.Rat}, StepPrice: &cbor.Rat{Rat: genesis.ExecutionPrices.Steps.Rat}, } } - // Process cost models if genesis.CostModels != nil { - p.CostModels = make(map[uint][]int64) + p.CostModels = make(map[PlutusVersion]*CostModel) + for lang, model := range genesis.CostModels { - var langKey uint - switch lang { - case "plutus:v1": - langKey = 0 - case "plutus:v2": - langKey = 1 - default: - // Skip unknown language + version, ok := toPlutusVersion(lang) + if !ok { continue } - // Get sorted keys to determine indexes - keys := make([]string, 0, len(model)) - for k := range model { - keys = append(keys, k) + params := make(map[string]int64) + order := make([]string, 0, len(model)) + + // Since model is now map[string]int, we don't need type assertions + for name, val := range model { + params[name] = int64(val) // Convert int to int64 + order = append(order, name) } - sort.Strings(keys) - // Create slice with values in alphabetical order - costs := make([]int64, len(keys)) - for i, key := range keys { - costs[i] = int64(model[key]) + // Sort keys alphabetically (maintains consistency with original behavior) + sort.Strings(order) + + p.CostModels[version] = &CostModel{ + Version: version, + Parameters: params, + Order: order, } + } + } + return nil +} + +func (p *AlonzoProtocolParameters) convertLegacyCostModels(legacyModels map[uint][]int64) { + p.CostModels = make(map[PlutusVersion]*CostModel) - p.CostModels[langKey] = costs + for langKey, values := range legacyModels { + var version PlutusVersion + switch langKey { + case 0: + version = PlutusV1 + case 1: + version = PlutusV2 + case 2: + version = PlutusV3 + default: + continue + } + + params := make(map[string]int64) + order := make([]string, len(values)) + for i, val := range values { + name := fmt.Sprintf("param%d", i) + params[name] = val + order[i] = name + } + + p.CostModels[version] = &CostModel{ + Version: version, + Parameters: params, + Order: order, } } } +func toPlutusVersion(key string) (PlutusVersion, bool) { + switch strings.ToLower(key) { + case "plutus:v1", "plutusv1": + return PlutusV1, true + case "plutus:v2", "plutusv2": + return PlutusV2, true + case "plutus:v3", "plutusv3": + return PlutusV3, true + default: + return "", false + } +} + +func (p *AlonzoProtocolParameters) ToLegacyCostModels() map[uint][]int64 { + legacyModels := make(map[uint][]int64) + + for version, model := range p.CostModels { + var langKey uint + switch version { + case PlutusV1: + langKey = 0 + case PlutusV2: + langKey = 1 + case PlutusV3: + langKey = 2 + default: + continue + } + + // Convert ordered parameters back to list format + values := make([]int64, len(model.Order)) + for i, name := range model.Order { + values[i] = model.Parameters[name] + } + legacyModels[langKey] = values + } + + return legacyModels +} + type AlonzoProtocolParameterUpdate struct { mary.MaryProtocolParameterUpdate MinPoolCost *uint64 `cbor:"16,keyasint"` @@ -147,33 +238,63 @@ func (u *AlonzoProtocolParameterUpdate) UnmarshalCBOR(data []byte) error { } func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { - // sanity check - if p.A0.Num().Int64() > math.MaxInt32 || - p.A0.Denom().Int64() < 0 || - p.A0.Denom().Int64() > math.MaxUint32 { + if p == nil { return nil } - if p.Rho.Num().Int64() > math.MaxInt32 || - p.Rho.Denom().Int64() < 0 || - p.Rho.Denom().Int64() > math.MaxUint32 { - return nil + + // Helper function to safely check rational number bounds + safeRatCheck := func(rat *cbor.Rat) bool { + if rat == nil || rat.Rat == nil { + return false + } + num := rat.Num().Int64() + denom := rat.Denom().Int64() + return num <= math.MaxInt32 && denom > 0 && denom <= math.MaxUint32 } - if p.Tau.Num().Int64() > math.MaxInt32 || - p.Tau.Denom().Int64() < 0 || - p.Tau.Denom().Int64() > math.MaxUint32 { + + // Validate all rational numbers + if !safeRatCheck(p.A0) || !safeRatCheck(p.Rho) || !safeRatCheck(p.Tau) { return nil } - if p.ExecutionCosts.MemPrice.Num().Int64() > math.MaxInt32 || - p.ExecutionCosts.MemPrice.Denom().Int64() < 0 || - p.ExecutionCosts.MemPrice.Denom().Int64() > math.MaxUint32 { + if p.ExecutionCosts.MemPrice == nil || p.ExecutionCosts.StepPrice == nil || + !safeRatCheck(p.ExecutionCosts.MemPrice) || !safeRatCheck(p.ExecutionCosts.StepPrice) { return nil } - if p.ExecutionCosts.StepPrice.Num().Int64() > math.MaxInt32 || - p.ExecutionCosts.StepPrice.Denom().Int64() < 0 || - p.ExecutionCosts.StepPrice.Denom().Int64() > math.MaxUint32 { - return nil + + // Convert cost models with proper version handling + costModels := &cardano.CostModels{} + if p.CostModels != nil { + // Initialize all Plutus versions to empty models first + costModels.PlutusV1 = &cardano.CostModel{Values: []int64{}} + costModels.PlutusV2 = &cardano.CostModel{Values: []int64{}} + costModels.PlutusV3 = &cardano.CostModel{Values: []int64{}} + + // Convert each version that exists in our parameters + for version, model := range p.CostModels { + var values []int64 + switch version { + case PlutusV1: + values = make([]int64, len(model.Order)) + for i, name := range model.Order { + values[i] = model.Parameters[name] + } + costModels.PlutusV1.Values = values + case PlutusV2: + values = make([]int64, len(model.Order)) + for i, name := range model.Order { + values[i] = model.Parameters[name] + } + costModels.PlutusV2.Values = values + case PlutusV3: + values = make([]int64, len(model.Order)) + for i, name := range model.Order { + values[i] = model.Parameters[name] + } + costModels.PlutusV3.Values = values + } + } } - // #nosec G115 + return &cardano.PParams{ CoinsPerUtxoByte: p.AdaPerUtxoByte, MaxTxSize: uint64(p.MaxTxSize), @@ -205,9 +326,7 @@ func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { MaxValueSize: uint64(p.MaxValueSize), CollateralPercentage: uint64(p.CollateralPercentage), MaxCollateralInputs: uint64(p.MaxCollateralInputs), - CostModels: common.ConvertToUtxorpcCardanoCostModels( - p.CostModels, - ), + CostModels: costModels, Prices: &cardano.ExPrices{ Memory: &cardano.RationalNumber{ Numerator: int32(p.ExecutionCosts.MemPrice.Num().Int64()), diff --git a/ledger/alonzo/pparams_test.go b/ledger/alonzo/pparams_test.go index 23737dbf..843fffc6 100644 --- a/ledger/alonzo/pparams_test.go +++ b/ledger/alonzo/pparams_test.go @@ -1,24 +1,12 @@ -// Copyright 2024 Blink Labs Software -// -// Licensed under the Apache License, Version 2.0 (the "License"); -// you may not use this file except in compliance with the License. -// You may obtain a copy of the License at -// -// http://www.apache.org/licenses/LICENSE-2.0 -// -// Unless required by applicable law or agreed to in writing, software -// distributed under the License is distributed on an "AS IS" BASIS, -// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -// See the License for the specific language governing permissions and -// limitations under the License. - package alonzo_test import ( "encoding/hex" + "encoding/json" "math/big" "reflect" - "strings" + + //"strings" "testing" "github.com/blinklabs-io/gouroboros/cbor" @@ -27,335 +15,304 @@ import ( "github.com/blinklabs-io/gouroboros/ledger/common" "github.com/blinklabs-io/gouroboros/ledger/mary" "github.com/blinklabs-io/gouroboros/ledger/shelley" - "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" + //"github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) -func TestAlonzoProtocolParamsUpdate(t *testing.T) { - testDefs := []struct { - startParams alonzo.AlonzoProtocolParameters - updateCbor string - expectedParams alonzo.AlonzoProtocolParameters - }{ - { - startParams: alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: mary.MaryProtocolParameters{ - AllegraProtocolParameters: allegra.AllegraProtocolParameters{ - ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ - Decentralization: &cbor.Rat{ - Rat: new(big.Rat).SetInt64(1), - }, - }, - }, - }, - }, - updateCbor: "a10cd81e82090a", - expectedParams: alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: mary.MaryProtocolParameters{ - AllegraProtocolParameters: allegra.AllegraProtocolParameters{ - ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ - Decentralization: &cbor.Rat{Rat: big.NewRat(9, 10)}, - }, - }, - }, - }, - }, - { - startParams: alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: mary.MaryProtocolParameters{ - AllegraProtocolParameters: allegra.AllegraProtocolParameters{ - ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ - ProtocolMajor: 5, - }, - }, - }, - }, - updateCbor: "a10e820600", - expectedParams: alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: mary.MaryProtocolParameters{ - AllegraProtocolParameters: allegra.AllegraProtocolParameters{ - ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ - ProtocolMajor: 6, - }, - }, - }, +// Helper to create properly initialized base protocol parameters +func newBaseProtocolParams() mary.MaryProtocolParameters { + return mary.MaryProtocolParameters{ + AllegraProtocolParameters: allegra.AllegraProtocolParameters{ + ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ + MinFeeA: 44, + MinFeeB: 155381, + MaxBlockBodySize: 65536, + MaxTxSize: 16384, + MaxBlockHeaderSize: 1100, + KeyDeposit: 2000000, + PoolDeposit: 500000000, + MaxEpoch: 18, + NOpt: 500, + A0: &cbor.Rat{Rat: big.NewRat(1, 2)}, + Rho: &cbor.Rat{Rat: big.NewRat(3, 4)}, + Tau: &cbor.Rat{Rat: big.NewRat(5, 6)}, + ProtocolMajor: 8, + ProtocolMinor: 0, }, }, + } +} + +func TestAlonzoProtocolParametersUpdate(t *testing.T) { + tests := []struct { + name string + updateCbor string + expected alonzo.AlonzoProtocolParameters + expectError bool + }{ { - startParams: alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: mary.MaryProtocolParameters{ - AllegraProtocolParameters: allegra.AllegraProtocolParameters{ - ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ - MaxBlockBodySize: 1, - }, - }, - }, - MaxTxExUnits: common.ExUnits{ - Memory: 1, - Steps: 1, - }, - }, - updateCbor: "a2021a0001200014821a00aba9501b00000002540be400", - expectedParams: alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: mary.MaryProtocolParameters{ - AllegraProtocolParameters: allegra.AllegraProtocolParameters{ - ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ - MaxBlockBodySize: 73728, - }, - }, - }, - MaxTxExUnits: common.ExUnits{ - Memory: 11250000, - Steps: 10000000000, - }, + name: "Update MinPoolCost", + updateCbor: "a1101903e8", // {16: 1000} + expected: alonzo.AlonzoProtocolParameters{ + MaryProtocolParameters: newBaseProtocolParams(), + MinPoolCost: 1000, }, }, } - for _, testDef := range testDefs { - cborBytes, err := hex.DecodeString(testDef.updateCbor) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - var tmpUpdate alonzo.AlonzoProtocolParameterUpdate - if _, err := cbor.Decode(cborBytes, &tmpUpdate); err != nil { - t.Fatalf("unexpected error: %s", err) - } - tmpParams := testDef.startParams - tmpParams.Update(&tmpUpdate) - if !reflect.DeepEqual(tmpParams, testDef.expectedParams) { - t.Fatalf( - "did not get expected params:\n got: %#v\n wanted: %#v", - tmpParams, - testDef.expectedParams, - ) - } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + data, err := hex.DecodeString(tt.updateCbor) + if err != nil { + t.Fatalf("failed to decode CBOR: %v", err) + } + + var update alonzo.AlonzoProtocolParameterUpdate + if _, err := cbor.Decode(data, &update); err != nil { + if !tt.expectError { + t.Fatalf("failed to decode update: %v", err) + } + return + } + + params := alonzo.AlonzoProtocolParameters{ + MaryProtocolParameters: newBaseProtocolParams(), + } + params.Update(&update) + + if !reflect.DeepEqual(params, tt.expected) { + t.Errorf("unexpected result:\ngot: %+v\nwant: %+v", params, tt.expected) + } + }) } } - -func TestAlonzoProtocolParamsUpdateFromGenesis(t *testing.T) { - testDefs := []struct { - startParams alonzo.AlonzoProtocolParameters - genesisJson string - expectedParams alonzo.AlonzoProtocolParameters +func TestAlonzoProtocolParametersUpdateFromGenesis(t *testing.T) { + tests := []struct { + name string + genesisJSON string + validate func(*alonzo.AlonzoProtocolParameters) bool }{ { - startParams: alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: mary.MaryProtocolParameters{ - AllegraProtocolParameters: allegra.AllegraProtocolParameters{ - ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ - Decentralization: &cbor.Rat{ - Rat: new(big.Rat).SetInt64(1), - }, - }, - }, - }, - }, - genesisJson: `{"lovelacePerUTxOWord": 34482}`, - expectedParams: alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: mary.MaryProtocolParameters{ - AllegraProtocolParameters: allegra.AllegraProtocolParameters{ - ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ - Decentralization: &cbor.Rat{ - Rat: new(big.Rat).SetInt64(1), - }, - }, - }, - }, - AdaPerUtxoByte: 34482 / 8, + name: "Basic Parameters", + genesisJSON: `{ + "lovelacePerUTxOWord": 34482, + "maxValueSize": 5000, + "collateralPercentage": 150, + "maxCollateralInputs": 3, + "maxTxExUnits": {"mem": 10000000, "steps": 10000000000}, + "maxBlockExUnits": {"mem": 50000000, "steps": 40000000000}, + "executionPrices": { + "mem": {"numerator": 577, "denominator": 10000}, + "steps": {"numerator": 721, "denominator": 10000000} + }, + "costModels": { + "PlutusV1": { + "param0": 100, + "param1": 200, + "param2": 300 + } + } + }`, + validate: func(p *alonzo.AlonzoProtocolParameters) bool { + return p.AdaPerUtxoByte == 4310 && // 34482 / 8 + p.MaxValueSize == 5000 && + p.CollateralPercentage == 150 && + p.MaxCollateralInputs == 3 && + p.MaxTxExUnits.Memory == 10000000 && + p.MaxTxExUnits.Steps == 10000000000 && + p.ExecutionCosts.MemPrice.Rat.Cmp(big.NewRat(577, 10000)) == 0 && + p.CostModels[alonzo.PlutusV1] != nil && + len(p.CostModels[alonzo.PlutusV1].Order) == 3 }, }, - { - startParams: alonzo.AlonzoProtocolParameters{}, - genesisJson: `{ - "lovelacePerUTxOWord": 34482, - "costModels": { - "plutus:v1": { - "addInteger-cpu-arguments-intercept": 10, - "subtractInteger-cpu-arguments-intercept": 30, - "multiplyInteger-cpu-arguments-intercept": 20 - }, - "plutus:v2": { - "appendByteString-cpu-arguments-slope": 5, - "consByteString-cpu-arguments-intercept": 2 - }, - "plutus:v3": { - "unknown-op": 999 - } - } - }`, - expectedParams: alonzo.AlonzoProtocolParameters{ - AdaPerUtxoByte: 34482 / 8, - CostModels: map[uint][]int64{ - 0: { - 10, // addInteger-cpu-arguments-intercept - 20, // multiplyInteger-cpu-arguments-intercept - 30, // subtractInteger-cpu-arguments-intercept - }, - 1: { - 5, // appendByteString-cpu-arguments-slope - 2, // consByteString-cpu-arguments-intercept - }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create a temporary struct with the exact JSON structure + type TempExecutionPrices struct { + Mem *struct { + Numerator int64 `json:"numerator"` + Denominator int64 `json:"denominator"` + } `json:"mem"` + Steps *struct { + Numerator int64 `json:"numerator"` + Denominator int64 `json:"denominator"` + } `json:"steps"` + } + + type TempCostModels map[string]map[string]int + + type TempGenesis struct { + LovelacePerUtxoWord uint64 `json:"lovelacePerUTxOWord"` + MaxValueSize uint `json:"maxValueSize"` + CollateralPercentage uint `json:"collateralPercentage"` + MaxCollateralInputs uint `json:"maxCollateralInputs"` + MaxTxExUnits struct { + Mem uint64 `json:"mem"` + Steps uint64 `json:"steps"` + } `json:"maxTxExUnits"` + MaxBlockExUnits struct { + Mem uint64 `json:"mem"` + Steps uint64 `json:"steps"` + } `json:"maxBlockExUnits"` + ExecutionPrices TempExecutionPrices `json:"executionPrices"` + CostModels TempCostModels `json:"costModels"` + } + + var tempGenesis TempGenesis + err := json.Unmarshal([]byte(tt.genesisJSON), &tempGenesis) + if err != nil { + t.Fatalf("failed to parse genesis: %v", err) + } + + // Convert to the actual AlonzoGenesis type with proper type conversions + genesis := alonzo.AlonzoGenesis{ + LovelacePerUtxoWord: tempGenesis.LovelacePerUtxoWord, + MaxValueSize: tempGenesis.MaxValueSize, + CollateralPercentage: tempGenesis.CollateralPercentage, + MaxCollateralInputs: tempGenesis.MaxCollateralInputs, + MaxTxExUnits: alonzo.AlonzoGenesisExUnits{ + Mem: uint(tempGenesis.MaxTxExUnits.Mem), + Steps: uint(tempGenesis.MaxTxExUnits.Steps), }, - }, - }, - { - //out of order keys - startParams: alonzo.AlonzoProtocolParameters{}, - genesisJson: `{ - "lovelacePerUTxOWord": 34482, - "costModels": { - "plutus:v1": { - "zzz": 3, - "aaa": 1, - "mmm": 2 - } - } - }`, - expectedParams: alonzo.AlonzoProtocolParameters{ - AdaPerUtxoByte: 34482 / 8, - CostModels: map[uint][]int64{ - 0: { - 1, // "aaa" - 2, // "mmm" - 3, // "zzz" - }, + MaxBlockExUnits: alonzo.AlonzoGenesisExUnits{ + Mem: uint(tempGenesis.MaxBlockExUnits.Mem), + Steps: uint(tempGenesis.MaxBlockExUnits.Steps), }, - }, - }, + ExecutionPrices: alonzo.AlonzoGenesisExecutionPrices{ + Mem: convertToExecutionPricesRat(tempGenesis.ExecutionPrices.Mem), + Steps: convertToExecutionPricesRat(tempGenesis.ExecutionPrices.Steps), + }, + CostModels: convertCostModels(tempGenesis.CostModels), + } + + params := alonzo.AlonzoProtocolParameters{ + MaryProtocolParameters: newBaseProtocolParams(), + } + err = params.UpdateFromGenesis(&genesis) + if err != nil { + t.Fatalf("UpdateFromGenesis failed: %v", err) + } + + if !tt.validate(¶ms) { + t.Errorf("validation failed for params: %+v", params) + } + }) } - for _, testDef := range testDefs { - tmpGenesis, err := alonzo.NewAlonzoGenesisFromReader( - strings.NewReader(testDef.genesisJson), - ) - if err != nil { - t.Fatalf("unexpected error: %s", err) - } - tmpParams := testDef.startParams - tmpParams.UpdateFromGenesis(&tmpGenesis) - if !reflect.DeepEqual(tmpParams, testDef.expectedParams) { - t.Fatalf( - "did not get expected params:\n got: %#v\n wanted: %#v", - tmpParams, - testDef.expectedParams, - ) +} + +func convertToExecutionPricesRat(r *struct { + Numerator int64 `json:"numerator"` + Denominator int64 `json:"denominator"` +}) *alonzo.AlonzoGenesisExecutionPricesRat { + if r == nil { + return nil + } + return &alonzo.AlonzoGenesisExecutionPricesRat{ + Rat: big.NewRat(r.Numerator, r.Denominator), + } +} + +func convertCostModels(tempModels map[string]map[string]int) map[string]map[string]int { + // Create a new map with the correct type + models := make(map[string]map[string]int) + for k, v := range tempModels { + // Create a new inner map + innerMap := make(map[string]int) + for k2, v2 := range v { + innerMap[k2] = v2 } + models[k] = innerMap } + return models } -func TestAlonzoUtxorpc(t *testing.T) { - inputParams := alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: mary.MaryProtocolParameters{ - AllegraProtocolParameters: allegra.AllegraProtocolParameters{ - ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ - MaxTxSize: 16384, - MinFeeA: 500, - MinFeeB: 2, - MaxBlockBodySize: 65536, - MaxBlockHeaderSize: 1024, - KeyDeposit: 2000, - PoolDeposit: 500000, - MaxEpoch: 2160, - NOpt: 100, - A0: &cbor.Rat{Rat: big.NewRat(1, 2)}, - Rho: &cbor.Rat{Rat: big.NewRat(3, 4)}, - Tau: &cbor.Rat{Rat: big.NewRat(5, 6)}, - ProtocolMajor: 8, - ProtocolMinor: 0, - }, - }, - }, - AdaPerUtxoByte: 44 / 8, - MinPoolCost: 340000000, - MaxValueSize: 1024, - CollateralPercentage: 150, - MaxCollateralInputs: 5, +func TestAlonzoProtocolParametersUtxorpc(t *testing.T) { + params := alonzo.AlonzoProtocolParameters{ + MaryProtocolParameters: newBaseProtocolParams(), + MinPoolCost: 340000000, + AdaPerUtxoByte: 4310, + MaxValueSize: 5000, + CollateralPercentage: 150, + MaxCollateralInputs: 3, ExecutionCosts: common.ExUnitPrice{ - MemPrice: &cbor.Rat{Rat: big.NewRat(1, 2)}, - StepPrice: &cbor.Rat{Rat: big.NewRat(2, 3)}, + MemPrice: &cbor.Rat{Rat: big.NewRat(577, 10000)}, + StepPrice: &cbor.Rat{Rat: big.NewRat(721, 10000000)}, }, MaxTxExUnits: common.ExUnits{ - Memory: 1000000, - Steps: 200000, + Memory: 10000000, + Steps: 10000000000, }, MaxBlockExUnits: common.ExUnits{ - Memory: 5000000, - Steps: 1000000, + Memory: 50000000, + Steps: 40000000000, }, - CostModels: map[uint][]int64{ - 1: {100, 200, 300}, - 2: {400, 500, 600}, - 3: {700, 800, 900}, + CostModels: map[alonzo.PlutusVersion]*alonzo.CostModel{ + alonzo.PlutusV1: { + Parameters: map[string]int64{"param0": 100, "param1": 200, "param2": 300}, + Order: []string{"param0", "param1", "param2"}, + }, }, } - expectedUtxorpc := &cardano.PParams{ - CoinsPerUtxoByte: 44 / 8, - MaxTxSize: 16384, - MinFeeCoefficient: 500, - MinFeeConstant: 2, - MaxBlockBodySize: 65536, - MaxBlockHeaderSize: 1024, - StakeKeyDeposit: 2000, - PoolDeposit: 500000, - PoolRetirementEpochBound: 2160, - DesiredNumberOfPools: 100, - PoolInfluence: &cardano.RationalNumber{ - Numerator: int32(1), - Denominator: uint32(2), - }, - MonetaryExpansion: &cardano.RationalNumber{ - Numerator: int32(3), - Denominator: uint32(4), - }, - TreasuryExpansion: &cardano.RationalNumber{ - Numerator: int32(5), - Denominator: uint32(6), - }, - MinPoolCost: 340000000, - ProtocolVersion: &cardano.ProtocolVersion{ - Major: 8, - Minor: 0, - }, - MaxValueSize: 1024, - CollateralPercentage: 150, - MaxCollateralInputs: 5, - CostModels: &cardano.CostModels{ - PlutusV1: &cardano.CostModel{ - Values: []int64{100, 200, 300}, - }, - PlutusV2: &cardano.CostModel{ - Values: []int64{400, 500, 600}, - }, - PlutusV3: &cardano.CostModel{ - Values: []int64{700, 800, 900}, - }, - }, - Prices: &cardano.ExPrices{ - Memory: &cardano.RationalNumber{ - Numerator: int32(1), - Denominator: uint32(2), + result := params.Utxorpc() + if result == nil { + t.Fatal("Utxorpc() returned nil") + } + + // Verify cost models + if result.CostModels == nil { + t.Fatal("CostModels should not be nil") + } + if result.CostModels.PlutusV1 == nil { + t.Fatal("PlutusV1 cost model should not be nil") + } + if len(result.CostModels.PlutusV1.Values) != 3 { + t.Errorf("expected 3 cost model values, got %d", len(result.CostModels.PlutusV1.Values)) + } + + // Verify other parameters + if result.MaxTxSize != uint64(params.MaxTxSize) { + t.Errorf("incorrect MaxTxSize conversion") + } + if result.MinPoolCost != params.MinPoolCost { + t.Errorf("incorrect MinPoolCost conversion") + } + if result.CoinsPerUtxoByte != params.AdaPerUtxoByte { + t.Errorf("incorrect AdaPerUtxoByte conversion") + } +} + +func TestCostModelConversions(t *testing.T) { + tests := []struct { + name string + input map[alonzo.PlutusVersion]*alonzo.CostModel + expected map[uint][]int64 + }{ + { + name: "Single Version", + input: map[alonzo.PlutusVersion]*alonzo.CostModel{ + alonzo.PlutusV1: { + Parameters: map[string]int64{"param0": 1, "param1": 2}, + Order: []string{"param0", "param1"}, + }, }, - Steps: &cardano.RationalNumber{ - Numerator: int32(2), - Denominator: uint32(3), + expected: map[uint][]int64{ + 0: {1, 2}, }, }, - MaxExecutionUnitsPerTransaction: &cardano.ExUnits{ - Memory: 1000000, - Steps: 200000, - }, - MaxExecutionUnitsPerBlock: &cardano.ExUnits{ - Memory: 5000000, - Steps: 1000000, - }, } - result := inputParams.Utxorpc() - - if !reflect.DeepEqual(result, expectedUtxorpc) { - t.Fatalf( - "Utxorpc() test failed for Alonzo:\nExpected: %#v\nGot: %#v", - expectedUtxorpc, - result, - ) + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + params := alonzo.AlonzoProtocolParameters{ + MaryProtocolParameters: newBaseProtocolParams(), + CostModels: tt.input, + } + result := params.ToLegacyCostModels() + if !reflect.DeepEqual(result, tt.expected) { + t.Errorf("unexpected result:\ngot: %v\nwant: %v", result, tt.expected) + } + }) } } diff --git a/ledger/babbage/pparams.go b/ledger/babbage/pparams.go index a49f6d1f..2415ce2f 100644 --- a/ledger/babbage/pparams.go +++ b/ledger/babbage/pparams.go @@ -258,7 +258,7 @@ func UpgradePParams( ProtocolMinor: prevPParams.ProtocolMinor, MinPoolCost: prevPParams.MinPoolCost, AdaPerUtxoByte: prevPParams.AdaPerUtxoByte, - CostModels: prevPParams.CostModels, + CostModels: prevPParams.ToLegacyCostModels(), ExecutionCosts: prevPParams.ExecutionCosts, MaxTxExUnits: prevPParams.MaxTxExUnits, MaxBlockExUnits: prevPParams.MaxBlockExUnits, From 7eff7f073bbc2224d4b9ab448dc88709cd02a683 Mon Sep 17 00:00:00 2001 From: Jenita Date: Tue, 22 Apr 2025 21:26:15 -0500 Subject: [PATCH 03/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/pparams.go | 3 --- ledger/alonzo/pparams_test.go | 14 ++++++++++++++ 2 files changed, 14 insertions(+), 3 deletions(-) diff --git a/ledger/alonzo/pparams.go b/ledger/alonzo/pparams.go index 8ac640cc..42472ad2 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -15,12 +15,9 @@ package alonzo import ( - //"encoding/json" "fmt" "math" "sort" - - //"strconv" "strings" "github.com/blinklabs-io/gouroboros/cbor" diff --git a/ledger/alonzo/pparams_test.go b/ledger/alonzo/pparams_test.go index 843fffc6..f6dde769 100644 --- a/ledger/alonzo/pparams_test.go +++ b/ledger/alonzo/pparams_test.go @@ -1,3 +1,17 @@ +// Copyright 2024 Blink Labs Software +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + package alonzo_test import ( From 654b39e42b6285d8e4e4a937f3b4fa7ed2251a0c Mon Sep 17 00:00:00 2001 From: Jenita Date: Tue, 22 Apr 2025 21:56:09 -0500 Subject: [PATCH 04/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/pparams_test.go | 96 ++++++++++++++++++----------------- 1 file changed, 50 insertions(+), 46 deletions(-) diff --git a/ledger/alonzo/pparams_test.go b/ledger/alonzo/pparams_test.go index 852926b9..d7bc0487 100644 --- a/ledger/alonzo/pparams_test.go +++ b/ledger/alonzo/pparams_test.go @@ -19,37 +19,33 @@ import ( "encoding/json" "math/big" "reflect" - "testing" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/alonzo" "github.com/blinklabs-io/gouroboros/ledger/common" - "github.com/blinklabs-io/gouroboros/ledger/mary" - "github.com/blinklabs-io/gouroboros/ledger/shelley" ) // Helper to create properly initialized base protocol parameters -func newBaseProtocolParams() mary.MaryProtocolParameters { - return mary.MaryProtocolParameters{ - AllegraProtocolParameters: allegra.AllegraProtocolParameters{ - ShelleyProtocolParameters: shelley.ShelleyProtocolParameters{ - MinFeeA: 44, - MinFeeB: 155381, - MaxBlockBodySize: 65536, - MaxTxSize: 16384, - MaxBlockHeaderSize: 1100, - KeyDeposit: 2000000, - PoolDeposit: 500000000, - MaxEpoch: 18, - NOpt: 500, - A0: &cbor.Rat{Rat: big.NewRat(1, 2)}, - Rho: &cbor.Rat{Rat: big.NewRat(3, 4)}, - Tau: &cbor.Rat{Rat: big.NewRat(5, 6)}, - ProtocolMajor: 8, - ProtocolMinor: 0, - }, - }, +func newBaseProtocolParams() alonzo.AlonzoProtocolParameters { + return alonzo.AlonzoProtocolParameters{ + MinFeeA: 44, + MinFeeB: 155381, + MaxBlockBodySize: 65536, + MaxTxSize: 16384, + MaxBlockHeaderSize: 1100, + KeyDeposit: 2000000, + PoolDeposit: 500000000, + MaxEpoch: 18, + NOpt: 500, + A0: &cbor.Rat{Rat: big.NewRat(1, 2)}, + Rho: &cbor.Rat{Rat: big.NewRat(3, 4)}, + Tau: &cbor.Rat{Rat: big.NewRat(5, 6)}, + ProtocolMajor: 8, + ProtocolMinor: 0, + // Initialize other required fields with zero values + MinPoolCost: 0, + AdaPerUtxoByte: 0, } } @@ -63,10 +59,11 @@ func TestAlonzoProtocolParametersUpdate(t *testing.T) { { name: "Update MinPoolCost", updateCbor: "a1101903e8", // {16: 1000} - expected: alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: newBaseProtocolParams(), - MinPoolCost: 1000, - }, + expected: func() alonzo.AlonzoProtocolParameters { + params := newBaseProtocolParams() + params.MinPoolCost = 1000 + return params + }(), }, } @@ -85,9 +82,7 @@ func TestAlonzoProtocolParametersUpdate(t *testing.T) { return } - params := alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: newBaseProtocolParams(), - } + params := newBaseProtocolParams() params.Update(&update) if !reflect.DeepEqual(params, tt.expected) { @@ -96,6 +91,7 @@ func TestAlonzoProtocolParametersUpdate(t *testing.T) { }) } } + func TestAlonzoProtocolParametersUpdateFromGenesis(t *testing.T) { tests := []struct { name string @@ -197,9 +193,7 @@ func TestAlonzoProtocolParametersUpdateFromGenesis(t *testing.T) { CostModels: convertCostModels(tempGenesis.CostModels), } - params := alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: newBaseProtocolParams(), - } + params := newBaseProtocolParams() err = params.UpdateFromGenesis(&genesis) if err != nil { t.Fatalf("UpdateFromGenesis failed: %v", err) @@ -212,7 +206,6 @@ func TestAlonzoProtocolParametersUpdateFromGenesis(t *testing.T) { } } - func convertToExecutionPricesRat(r *struct { Numerator int64 `json:"numerator"` Denominator int64 `json:"denominator"` @@ -239,15 +232,28 @@ func convertCostModels(tempModels map[string]map[string]int) map[string]map[stri return models } - -func TestAlonzoUtxorpc(t *testing.T) { +func TestAlonzoProtocolParametersUtxorpc(t *testing.T) { + baseParams := newBaseProtocolParams() params := alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: newBaseProtocolParams(), - MinPoolCost: 340000000, - AdaPerUtxoByte: 4310, - MaxValueSize: 5000, - CollateralPercentage: 150, - MaxCollateralInputs: 3, + MinFeeA: baseParams.MinFeeA, + MinFeeB: baseParams.MinFeeB, + MaxBlockBodySize: baseParams.MaxBlockBodySize, + MaxTxSize: baseParams.MaxTxSize, + MaxBlockHeaderSize: baseParams.MaxBlockHeaderSize, + KeyDeposit: baseParams.KeyDeposit, + PoolDeposit: baseParams.PoolDeposit, + MaxEpoch: baseParams.MaxEpoch, + NOpt: baseParams.NOpt, + A0: baseParams.A0, + Rho: baseParams.Rho, + Tau: baseParams.Tau, + ProtocolMajor: baseParams.ProtocolMajor, + ProtocolMinor: baseParams.ProtocolMinor, + MinPoolCost: 340000000, + AdaPerUtxoByte: 4310, + MaxValueSize: 5000, + CollateralPercentage: 150, + MaxCollateralInputs: 3, ExecutionCosts: common.ExUnitPrice{ MemPrice: &cbor.Rat{Rat: big.NewRat(577, 10000)}, StepPrice: &cbor.Rat{Rat: big.NewRat(721, 10000000)}, @@ -318,10 +324,8 @@ func TestCostModelConversions(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - params := alonzo.AlonzoProtocolParameters{ - MaryProtocolParameters: newBaseProtocolParams(), - CostModels: tt.input, - } + params := newBaseProtocolParams() + params.CostModels = tt.input result := params.ToLegacyCostModels() if !reflect.DeepEqual(result, tt.expected) { t.Errorf("unexpected result:\ngot: %v\nwant: %v", result, tt.expected) From 32afc580faa34ca4d7d075bc382168cd28fc2ba9 Mon Sep 17 00:00:00 2001 From: Jenita Date: Tue, 22 Apr 2025 22:12:25 -0500 Subject: [PATCH 05/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/pparams.go | 89 +++++++++++++++------------------------- 1 file changed, 33 insertions(+), 56 deletions(-) diff --git a/ledger/alonzo/pparams.go b/ledger/alonzo/pparams.go index 9f602718..0b8931f0 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -22,7 +22,8 @@ import ( "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/common" - "github.com/blinklabs-io/gouroboros/ledger/mary" + + //"github.com/blinklabs-io/gouroboros/ledger/mary" cardano "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) @@ -327,22 +328,37 @@ func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { return nil } - // Helper function to safely check rational number bounds - safeRatCheck := func(rat *cbor.Rat) bool { + // Helper function to safely convert rational numbers with bounds checking + safeRatConvert := func(rat *cbor.Rat) *cardano.RationalNumber { if rat == nil || rat.Rat == nil { - return false + return nil } num := rat.Num().Int64() denom := rat.Denom().Int64() - return num <= math.MaxInt32 && denom > 0 && denom <= math.MaxUint32 - } - // Validate all rational numbers - if !safeRatCheck(p.A0) || !safeRatCheck(p.Rho) || !safeRatCheck(p.Tau) { - return nil + // Check bounds for int32 numerator and uint32 denominator + if num < math.MinInt32 || num > math.MaxInt32 { + return nil + } + if denom <= 0 || denom > math.MaxUint32 { + return nil + } + + return &cardano.RationalNumber{ + Numerator: int32(num), + Denominator: uint32(denom), + } } - if p.ExecutionCosts.MemPrice == nil || p.ExecutionCosts.StepPrice == nil || - !safeRatCheck(p.ExecutionCosts.MemPrice) || !safeRatCheck(p.ExecutionCosts.StepPrice) { + + // Convert all rational numbers with safety checks + a0 := safeRatConvert(p.A0) + rho := safeRatConvert(p.Rho) + tau := safeRatConvert(p.Tau) + memPrice := safeRatConvert(p.ExecutionCosts.MemPrice) + stepPrice := safeRatConvert(p.ExecutionCosts.StepPrice) + + // Return nil if any conversion failed + if a0 == nil || rho == nil || tau == nil || memPrice == nil || stepPrice == nil { return nil } @@ -391,19 +407,10 @@ func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { PoolDeposit: uint64(p.PoolDeposit), PoolRetirementEpochBound: uint64(p.MaxEpoch), DesiredNumberOfPools: uint64(p.NOpt), - PoolInfluence: &cardano.RationalNumber{ - Numerator: int32(p.A0.Num().Int64()), - Denominator: uint32(p.A0.Denom().Int64()), - }, - MonetaryExpansion: &cardano.RationalNumber{ - Numerator: int32(p.Rho.Num().Int64()), - Denominator: uint32(p.Rho.Denom().Int64()), - }, - TreasuryExpansion: &cardano.RationalNumber{ - Numerator: int32(p.Tau.Num().Int64()), - Denominator: uint32(p.Tau.Denom().Int64()), - }, - MinPoolCost: p.MinPoolCost, + PoolInfluence: a0, + MonetaryExpansion: rho, + TreasuryExpansion: tau, + MinPoolCost: p.MinPoolCost, ProtocolVersion: &cardano.ProtocolVersion{ Major: uint32(p.ProtocolMajor), Minor: uint32(p.ProtocolMinor), @@ -413,14 +420,8 @@ func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { MaxCollateralInputs: uint64(p.MaxCollateralInputs), CostModels: costModels, Prices: &cardano.ExPrices{ - Memory: &cardano.RationalNumber{ - Numerator: int32(p.ExecutionCosts.MemPrice.Num().Int64()), - Denominator: uint32(p.ExecutionCosts.MemPrice.Denom().Int64()), - }, - Steps: &cardano.RationalNumber{ - Numerator: int32(p.ExecutionCosts.StepPrice.Num().Int64()), - Denominator: uint32(p.ExecutionCosts.StepPrice.Denom().Int64()), - }, + Memory: memPrice, + Steps: stepPrice, }, MaxExecutionUnitsPerTransaction: &cardano.ExUnits{ Memory: p.MaxTxExUnits.Memory, @@ -432,27 +433,3 @@ func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { }, } } - -func UpgradePParams( - prevPParams mary.MaryProtocolParameters, -) AlonzoProtocolParameters { - return AlonzoProtocolParameters{ - MinFeeA: prevPParams.MinFeeA, - MinFeeB: prevPParams.MinFeeB, - MaxBlockBodySize: prevPParams.MaxBlockBodySize, - MaxTxSize: prevPParams.MaxTxSize, - MaxBlockHeaderSize: prevPParams.MaxBlockHeaderSize, - KeyDeposit: prevPParams.KeyDeposit, - PoolDeposit: prevPParams.PoolDeposit, - MaxEpoch: prevPParams.MaxEpoch, - NOpt: prevPParams.NOpt, - A0: prevPParams.A0, - Rho: prevPParams.Rho, - Tau: prevPParams.Tau, - Decentralization: prevPParams.Decentralization, - ExtraEntropy: prevPParams.ExtraEntropy, - ProtocolMajor: prevPParams.ProtocolMajor, - ProtocolMinor: prevPParams.ProtocolMinor, - MinUtxoValue: prevPParams.MinUtxoValue, - } -} From b417ab5fd475c46a38c30d6811a91d65f2f87ba0 Mon Sep 17 00:00:00 2001 From: Jenita Date: Tue, 22 Apr 2025 23:04:18 -0500 Subject: [PATCH 06/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/pparams.go | 80 ++++++++++++++++++++++------------------ 1 file changed, 44 insertions(+), 36 deletions(-) diff --git a/ledger/alonzo/pparams.go b/ledger/alonzo/pparams.go index 0b8931f0..b55a25a8 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -328,69 +328,77 @@ func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { return nil } - // Helper function to safely convert rational numbers with bounds checking - safeRatConvert := func(rat *cbor.Rat) *cardano.RationalNumber { + // Safe conversion helper functions + safeInt64ToInt32 := func(val int64) (int32, bool) { + if val < math.MinInt32 || val > math.MaxInt32 { + return 0, false + } + return int32(val), true + } + + safeUintToUint32 := func(val uint) (uint32, bool) { + if val > math.MaxUint32 { + return 0, false + } + return uint32(val), true + } + + // Convert protocol version + protocolMajor, ok1 := safeUintToUint32(p.ProtocolMajor) + protocolMinor, ok2 := safeUintToUint32(p.ProtocolMinor) + if !ok1 || !ok2 { + return nil + } + + // Convert rational numbers + convertRat := func(rat *cbor.Rat) *cardano.RationalNumber { if rat == nil || rat.Rat == nil { return nil } - num := rat.Num().Int64() - denom := rat.Denom().Int64() - - // Check bounds for int32 numerator and uint32 denominator - if num < math.MinInt32 || num > math.MaxInt32 { + num, numOk := safeInt64ToInt32(rat.Num().Int64()) + denom64 := rat.Denom().Int64() + if denom64 <= 0 || denom64 > math.MaxUint32 { return nil } - if denom <= 0 || denom > math.MaxUint32 { + denom := uint32(denom64) + if !numOk { return nil } - return &cardano.RationalNumber{ - Numerator: int32(num), - Denominator: uint32(denom), + Numerator: num, + Denominator: denom, } } - // Convert all rational numbers with safety checks - a0 := safeRatConvert(p.A0) - rho := safeRatConvert(p.Rho) - tau := safeRatConvert(p.Tau) - memPrice := safeRatConvert(p.ExecutionCosts.MemPrice) - stepPrice := safeRatConvert(p.ExecutionCosts.StepPrice) + a0 := convertRat(p.A0) + rho := convertRat(p.Rho) + tau := convertRat(p.Tau) + memPrice := convertRat(p.ExecutionCosts.MemPrice) + stepPrice := convertRat(p.ExecutionCosts.StepPrice) - // Return nil if any conversion failed if a0 == nil || rho == nil || tau == nil || memPrice == nil || stepPrice == nil { return nil } - // Convert cost models with proper version handling + // Convert cost models costModels := &cardano.CostModels{} if p.CostModels != nil { - // Initialize all Plutus versions to empty models first costModels.PlutusV1 = &cardano.CostModel{Values: []int64{}} costModels.PlutusV2 = &cardano.CostModel{Values: []int64{}} costModels.PlutusV3 = &cardano.CostModel{Values: []int64{}} - // Convert each version that exists in our parameters for version, model := range p.CostModels { - var values []int64 + values := make([]int64, len(model.Order)) + for i, name := range model.Order { + values[i] = model.Parameters[name] + } + switch version { case PlutusV1: - values = make([]int64, len(model.Order)) - for i, name := range model.Order { - values[i] = model.Parameters[name] - } costModels.PlutusV1.Values = values case PlutusV2: - values = make([]int64, len(model.Order)) - for i, name := range model.Order { - values[i] = model.Parameters[name] - } costModels.PlutusV2.Values = values case PlutusV3: - values = make([]int64, len(model.Order)) - for i, name := range model.Order { - values[i] = model.Parameters[name] - } costModels.PlutusV3.Values = values } } @@ -412,8 +420,8 @@ func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { TreasuryExpansion: tau, MinPoolCost: p.MinPoolCost, ProtocolVersion: &cardano.ProtocolVersion{ - Major: uint32(p.ProtocolMajor), - Minor: uint32(p.ProtocolMinor), + Major: protocolMajor, + Minor: protocolMinor, }, MaxValueSize: uint64(p.MaxValueSize), CollateralPercentage: uint64(p.CollateralPercentage), From 7e6fe868224c675612e6e34693d508aa6cf51ad6 Mon Sep 17 00:00:00 2001 From: Jenita Date: Tue, 22 Apr 2025 23:08:12 -0500 Subject: [PATCH 07/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/pparams.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/ledger/alonzo/pparams.go b/ledger/alonzo/pparams.go index b55a25a8..cd16104f 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -22,8 +22,6 @@ import ( "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/common" - - //"github.com/blinklabs-io/gouroboros/ledger/mary" cardano "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) From 3d70e4d2d121672819e9f91706afb604fcdeedd3 Mon Sep 17 00:00:00 2001 From: Jenita Date: Sat, 3 May 2025 10:50:21 +0545 Subject: [PATCH 08/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/genesis.go | 45 ++- ledger/alonzo/genesis_test.go | 8 +- ledger/alonzo/pparams.go | 318 ++++++++---------- ledger/alonzo/pparams_test.go | 600 ++++++++++++++++++++++------------ ledger/babbage/pparams.go | 2 +- 5 files changed, 576 insertions(+), 397 deletions(-) diff --git a/ledger/alonzo/genesis.go b/ledger/alonzo/genesis.go index 7acf569b..ecbaa8bd 100644 --- a/ledger/alonzo/genesis.go +++ b/ledger/alonzo/genesis.go @@ -16,6 +16,7 @@ package alonzo import ( "encoding/json" + "fmt" "io" "math/big" "os" @@ -29,7 +30,7 @@ type AlonzoGenesis struct { ExecutionPrices AlonzoGenesisExecutionPrices `json:"executionPrices"` MaxTxExUnits AlonzoGenesisExUnits `json:"maxTxExUnits"` MaxBlockExUnits AlonzoGenesisExUnits `json:"maxBlockExUnits"` - CostModels map[string]map[string]int `json:"costModels"` + CostModels map[string]interface{} `json:"costModels"` } func NewAlonzoGenesisFromReader(r io.Reader) (AlonzoGenesis, error) { @@ -39,6 +40,9 @@ func NewAlonzoGenesisFromReader(r io.Reader) (AlonzoGenesis, error) { if err := dec.Decode(&ret); err != nil { return ret, err } + if err := ret.NormalizeCostModels(); err != nil { + return ret, err + } return ret, nil } @@ -76,3 +80,42 @@ func (r *AlonzoGenesisExecutionPricesRat) UnmarshalJSON(data []byte) error { r.Rat = big.NewRat(tmpData.Numerator, tmpData.Denominator) return nil } + +func (a *AlonzoGenesis) NormalizeCostModels() error { + if a.CostModels == nil { + return nil + } + + normalized := make(map[string]map[string]int) + for version, model := range a.CostModels { + if modelMap, ok := model.(map[string]interface{}); ok { + versionMap := make(map[string]int) + for k, v := range modelMap { + switch val := v.(type) { + case float64: + versionMap[k] = int(val) + case int: + versionMap[k] = val + case json.Number: + intVal, err := val.Int64() + if err != nil { + floatVal, err := val.Float64() + if err != nil { + return fmt.Errorf("invalid number in cost model: %v", val) + } + intVal = int64(floatVal) + } + versionMap[k] = int(intVal) + default: + return fmt.Errorf("invalid cost model value type: %T", v) + } + } + normalized[version] = versionMap + } + } + a.CostModels = make(map[string]interface{}) + for k, v := range normalized { + a.CostModels[k] = v + } + return nil +} diff --git a/ledger/alonzo/genesis_test.go b/ledger/alonzo/genesis_test.go index ac278f30..920b6261 100644 --- a/ledger/alonzo/genesis_test.go +++ b/ledger/alonzo/genesis_test.go @@ -243,8 +243,8 @@ var expectedGenesisObj = alonzo.AlonzoGenesis{ Mem: 50000000, Steps: 40000000000, }, - CostModels: map[string]map[string]int{ - "PlutusV1": { + CostModels: map[string]interface{}{ + "PlutusV1": map[string]int{ "addInteger-cpu-arguments-intercept": 197209, "addInteger-cpu-arguments-slope": 0, "addInteger-memory-arguments-intercept": 1, @@ -484,8 +484,8 @@ func TestNewAlonzoGenesisFromReader(t *testing.T) { t.Logf("prMem is correct: %v", result.ExecutionPrices.Mem.Rat) } - expectedCostModels := map[string]map[string]int{ - "PlutusV1": { + expectedCostModels := map[string]interface{}{ + "PlutusV1": map[string]int{ "addInteger-cpu-arguments-intercept": 205665, "addInteger-cpu-arguments-slope": 812, }, diff --git a/ledger/alonzo/pparams.go b/ledger/alonzo/pparams.go index cd16104f..8d008cf9 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -17,26 +17,25 @@ package alonzo import ( "fmt" "math" - "sort" - "strings" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/common" + "github.com/blinklabs-io/gouroboros/ledger/mary" cardano "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) -type PlutusVersion string - +// Constants for Plutus version mapping const ( - PlutusV1 PlutusVersion = "PlutusV1" - PlutusV2 PlutusVersion = "PlutusV2" - PlutusV3 PlutusVersion = "PlutusV3" + PlutusV1Key uint = 0 + PlutusV2Key uint = 1 + PlutusV3Key uint = 2 ) -type CostModel struct { - Version PlutusVersion - Parameters map[string]int64 - Order []string +// Expected parameter counts for validation +var plutusParamCounts = map[uint]int{ + PlutusV1Key: 166, + PlutusV2Key: 175, + PlutusV3Key: 187, } type AlonzoProtocolParameters struct { @@ -60,7 +59,7 @@ type AlonzoProtocolParameters struct { MinUtxoValue uint MinPoolCost uint64 AdaPerUtxoByte uint64 - CostModels map[PlutusVersion]*CostModel + CostModels map[uint][]int64 ExecutionCosts common.ExUnitPrice MaxTxExUnits common.ExUnits MaxBlockExUnits common.ExUnits @@ -128,7 +127,7 @@ func (p *AlonzoProtocolParameters) Update( p.AdaPerUtxoByte = *paramUpdate.AdaPerUtxoByte } if paramUpdate.CostModels != nil { - p.convertLegacyCostModels(paramUpdate.CostModels) + p.CostModels = paramUpdate.CostModels } if paramUpdate.ExecutionCosts != nil { p.ExecutionCosts = *paramUpdate.ExecutionCosts @@ -177,106 +176,83 @@ func (p *AlonzoProtocolParameters) UpdateFromGenesis(genesis *AlonzoGenesis) err } if genesis.CostModels != nil { - p.CostModels = make(map[PlutusVersion]*CostModel) + p.CostModels = make(map[uint][]int64) - for lang, model := range genesis.CostModels { - version, ok := toPlutusVersion(lang) + for versionStr, model := range genesis.CostModels { + key, ok := plutusVersionToKey(versionStr) if !ok { continue } - params := make(map[string]int64) - order := make([]string, 0, len(model)) - - // Since model is now map[string]int, we don't need type assertions - for name, val := range model { - params[name] = int64(val) // Convert int to int64 - order = append(order, name) + var values []int64 + switch v := model.(type) { + case map[string]interface{}: + maxIndex := 0 + // Find maximum parameter index + for paramName := range v { + var index int + if _, err := fmt.Sscanf(paramName, "param%d", &index); err == nil && index > maxIndex { + maxIndex = index + } + } + values = make([]int64, maxIndex) + for paramName, val := range v { + var index int + if _, err := fmt.Sscanf(paramName, "param%d", &index); err == nil && index > 0 { + if intVal, ok := val.(float64); ok { + values[index-1] = int64(intVal) + } + } + } + + case []interface{}: + values = make([]int64, len(v)) + for i, val := range v { + if intVal, ok := val.(float64); ok { + values[i] = int64(intVal) + } + } + + default: + return fmt.Errorf("invalid cost model format for %s", versionStr) } - - // Sort keys alphabetically (maintains consistency with original behavior) - sort.Strings(order) - - p.CostModels[version] = &CostModel{ - Version: version, - Parameters: params, - Order: order, + if expected, ok := plutusParamCounts[key]; ok && len(values) != expected { + return fmt.Errorf("invalid parameter count for %s: expected %d, got %d", + versionStr, expected, len(values)) } - } - } - return nil -} - -func (p *AlonzoProtocolParameters) convertLegacyCostModels(legacyModels map[uint][]int64) { - p.CostModels = make(map[PlutusVersion]*CostModel) - - for langKey, values := range legacyModels { - var version PlutusVersion - switch langKey { - case 0: - version = PlutusV1 - case 1: - version = PlutusV2 - case 2: - version = PlutusV3 - default: - continue - } - - params := make(map[string]int64) - order := make([]string, len(values)) - for i, val := range values { - name := fmt.Sprintf("param%d", i) - params[name] = val - order[i] = name - } - p.CostModels[version] = &CostModel{ - Version: version, - Parameters: params, - Order: order, + p.CostModels[key] = values } } + return nil } -func toPlutusVersion(key string) (PlutusVersion, bool) { - switch strings.ToLower(key) { - case "plutus:v1", "plutusv1": - return PlutusV1, true - case "plutus:v2", "plutusv2": - return PlutusV2, true - case "plutus:v3", "plutusv3": - return PlutusV3, true +// Helper to convert Plutus version string to key +func plutusVersionToKey(version string) (uint, bool) { + switch version { + case "PlutusV1": + return PlutusV1Key, true + case "PlutusV2": + return PlutusV2Key, true + case "PlutusV3": + return PlutusV3Key, true default: - return "", false + return 0, false } } -func (p *AlonzoProtocolParameters) ToLegacyCostModels() map[uint][]int64 { - legacyModels := make(map[uint][]int64) - - for version, model := range p.CostModels { - var langKey uint - switch version { - case PlutusV1: - langKey = 0 - case PlutusV2: - langKey = 1 - case PlutusV3: - langKey = 2 - default: - continue - } - - // Convert ordered parameters back to list format - values := make([]int64, len(model.Order)) - for i, name := range model.Order { - values[i] = model.Parameters[name] - } - legacyModels[langKey] = values +// Helper to convert key back to string +func plutusKeyToVersion(key uint) string { + switch key { + case PlutusV1Key: + return "PlutusV1" + case PlutusV2Key: + return "PlutusV2" + case PlutusV3Key: + return "PlutusV3" + default: + return "" } - - return legacyModels } type AlonzoProtocolParameterUpdate struct { @@ -322,86 +298,33 @@ func (u *AlonzoProtocolParameterUpdate) UnmarshalCBOR(cborData []byte) error { } func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { - if p == nil { + // sanity check + if p.A0.Num().Int64() > math.MaxInt32 || + p.A0.Denom().Int64() < 0 || + p.A0.Denom().Int64() > math.MaxUint32 { return nil } - - // Safe conversion helper functions - safeInt64ToInt32 := func(val int64) (int32, bool) { - if val < math.MinInt32 || val > math.MaxInt32 { - return 0, false - } - return int32(val), true - } - - safeUintToUint32 := func(val uint) (uint32, bool) { - if val > math.MaxUint32 { - return 0, false - } - return uint32(val), true - } - - // Convert protocol version - protocolMajor, ok1 := safeUintToUint32(p.ProtocolMajor) - protocolMinor, ok2 := safeUintToUint32(p.ProtocolMinor) - if !ok1 || !ok2 { + if p.Rho.Num().Int64() > math.MaxInt32 || + p.Rho.Denom().Int64() < 0 || + p.Rho.Denom().Int64() > math.MaxUint32 { return nil } - - // Convert rational numbers - convertRat := func(rat *cbor.Rat) *cardano.RationalNumber { - if rat == nil || rat.Rat == nil { - return nil - } - num, numOk := safeInt64ToInt32(rat.Num().Int64()) - denom64 := rat.Denom().Int64() - if denom64 <= 0 || denom64 > math.MaxUint32 { - return nil - } - denom := uint32(denom64) - if !numOk { - return nil - } - return &cardano.RationalNumber{ - Numerator: num, - Denominator: denom, - } + if p.Tau.Num().Int64() > math.MaxInt32 || + p.Tau.Denom().Int64() < 0 || + p.Tau.Denom().Int64() > math.MaxUint32 { + return nil } - - a0 := convertRat(p.A0) - rho := convertRat(p.Rho) - tau := convertRat(p.Tau) - memPrice := convertRat(p.ExecutionCosts.MemPrice) - stepPrice := convertRat(p.ExecutionCosts.StepPrice) - - if a0 == nil || rho == nil || tau == nil || memPrice == nil || stepPrice == nil { + if p.ExecutionCosts.MemPrice.Num().Int64() > math.MaxInt32 || + p.ExecutionCosts.MemPrice.Denom().Int64() < 0 || + p.ExecutionCosts.MemPrice.Denom().Int64() > math.MaxUint32 { return nil } - - // Convert cost models - costModels := &cardano.CostModels{} - if p.CostModels != nil { - costModels.PlutusV1 = &cardano.CostModel{Values: []int64{}} - costModels.PlutusV2 = &cardano.CostModel{Values: []int64{}} - costModels.PlutusV3 = &cardano.CostModel{Values: []int64{}} - - for version, model := range p.CostModels { - values := make([]int64, len(model.Order)) - for i, name := range model.Order { - values[i] = model.Parameters[name] - } - - switch version { - case PlutusV1: - costModels.PlutusV1.Values = values - case PlutusV2: - costModels.PlutusV2.Values = values - case PlutusV3: - costModels.PlutusV3.Values = values - } - } + if p.ExecutionCosts.StepPrice.Num().Int64() > math.MaxInt32 || + p.ExecutionCosts.StepPrice.Denom().Int64() < 0 || + p.ExecutionCosts.StepPrice.Denom().Int64() > math.MaxUint32 { + return nil } - + // #nosec G115 return &cardano.PParams{ CoinsPerUtxoByte: p.AdaPerUtxoByte, MaxTxSize: uint64(p.MaxTxSize), @@ -413,21 +336,38 @@ func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { PoolDeposit: uint64(p.PoolDeposit), PoolRetirementEpochBound: uint64(p.MaxEpoch), DesiredNumberOfPools: uint64(p.NOpt), - PoolInfluence: a0, - MonetaryExpansion: rho, - TreasuryExpansion: tau, - MinPoolCost: p.MinPoolCost, + PoolInfluence: &cardano.RationalNumber{ + Numerator: int32(p.A0.Num().Int64()), + Denominator: uint32(p.A0.Denom().Int64()), + }, + MonetaryExpansion: &cardano.RationalNumber{ + Numerator: int32(p.Rho.Num().Int64()), + Denominator: uint32(p.Rho.Denom().Int64()), + }, + TreasuryExpansion: &cardano.RationalNumber{ + Numerator: int32(p.Tau.Num().Int64()), + Denominator: uint32(p.Tau.Denom().Int64()), + }, + MinPoolCost: p.MinPoolCost, ProtocolVersion: &cardano.ProtocolVersion{ - Major: protocolMajor, - Minor: protocolMinor, + Major: uint32(p.ProtocolMajor), + Minor: uint32(p.ProtocolMinor), }, MaxValueSize: uint64(p.MaxValueSize), CollateralPercentage: uint64(p.CollateralPercentage), MaxCollateralInputs: uint64(p.MaxCollateralInputs), - CostModels: costModels, + CostModels: common.ConvertToUtxorpcCardanoCostModels( + p.CostModels, + ), Prices: &cardano.ExPrices{ - Memory: memPrice, - Steps: stepPrice, + Memory: &cardano.RationalNumber{ + Numerator: int32(p.ExecutionCosts.MemPrice.Num().Int64()), + Denominator: uint32(p.ExecutionCosts.MemPrice.Denom().Int64()), + }, + Steps: &cardano.RationalNumber{ + Numerator: int32(p.ExecutionCosts.StepPrice.Num().Int64()), + Denominator: uint32(p.ExecutionCosts.StepPrice.Denom().Int64()), + }, }, MaxExecutionUnitsPerTransaction: &cardano.ExUnits{ Memory: p.MaxTxExUnits.Memory, @@ -439,3 +379,27 @@ func (p *AlonzoProtocolParameters) Utxorpc() *cardano.PParams { }, } } + +func UpgradePParams( + prevPParams mary.MaryProtocolParameters, +) AlonzoProtocolParameters { + return AlonzoProtocolParameters{ + MinFeeA: prevPParams.MinFeeA, + MinFeeB: prevPParams.MinFeeB, + MaxBlockBodySize: prevPParams.MaxBlockBodySize, + MaxTxSize: prevPParams.MaxTxSize, + MaxBlockHeaderSize: prevPParams.MaxBlockHeaderSize, + KeyDeposit: prevPParams.KeyDeposit, + PoolDeposit: prevPParams.PoolDeposit, + MaxEpoch: prevPParams.MaxEpoch, + NOpt: prevPParams.NOpt, + A0: prevPParams.A0, + Rho: prevPParams.Rho, + Tau: prevPParams.Tau, + Decentralization: prevPParams.Decentralization, + ExtraEntropy: prevPParams.ExtraEntropy, + ProtocolMajor: prevPParams.ProtocolMajor, + ProtocolMinor: prevPParams.ProtocolMinor, + MinUtxoValue: prevPParams.MinUtxoValue, + } +} diff --git a/ledger/alonzo/pparams_test.go b/ledger/alonzo/pparams_test.go index d7bc0487..48745467 100644 --- a/ledger/alonzo/pparams_test.go +++ b/ledger/alonzo/pparams_test.go @@ -17,16 +17,18 @@ package alonzo_test import ( "encoding/hex" "encoding/json" + "fmt" "math/big" "reflect" + "strings" "testing" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/alonzo" "github.com/blinklabs-io/gouroboros/ledger/common" + cardano "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) -// Helper to create properly initialized base protocol parameters func newBaseProtocolParams() alonzo.AlonzoProtocolParameters { return alonzo.AlonzoProtocolParameters{ MinFeeA: 44, @@ -43,60 +45,119 @@ func newBaseProtocolParams() alonzo.AlonzoProtocolParameters { Tau: &cbor.Rat{Rat: big.NewRat(5, 6)}, ProtocolMajor: 8, ProtocolMinor: 0, - // Initialize other required fields with zero values - MinPoolCost: 0, - AdaPerUtxoByte: 0, + MinPoolCost: 0, + AdaPerUtxoByte: 4310, + ExecutionCosts: common.ExUnitPrice{ + MemPrice: &cbor.Rat{Rat: big.NewRat(577, 10000)}, + StepPrice: &cbor.Rat{Rat: big.NewRat(721, 10000000)}, + }, + MaxTxExUnits: common.ExUnits{ + Memory: 10000000, + Steps: 10000000000, + }, + MaxBlockExUnits: common.ExUnits{ + Memory: 50000000, + Steps: 40000000000, + }, + MaxValueSize: 5000, + CollateralPercentage: 150, + MaxCollateralInputs: 3, + CostModels: map[uint][]int64{ + 0: completeCostModel(166), // PlutusV1 with exactly 166 parameters + 1: completeCostModel(175), // PlutusV2 with exactly 175 parameters + }, } } -func TestAlonzoProtocolParametersUpdate(t *testing.T) { - tests := []struct { - name string - updateCbor string - expected alonzo.AlonzoProtocolParameters - expectError bool +// Helper function to create complete cost models +func completeCostModel(size int) []int64 { + model := make([]int64, size) + for i := range model { + model[i] = int64(i + 1) // Fill with sequential values + } + return model +} + +func TestAlonzoProtocolParamsUpdate(t *testing.T) { + testDefs := []struct { + startParams alonzo.AlonzoProtocolParameters + updateCbor string + expectedParams alonzo.AlonzoProtocolParameters }{ { - name: "Update MinPoolCost", - updateCbor: "a1101903e8", // {16: 1000} - expected: func() alonzo.AlonzoProtocolParameters { - params := newBaseProtocolParams() - params.MinPoolCost = 1000 - return params - }(), + startParams: alonzo.AlonzoProtocolParameters{ + Decentralization: &cbor.Rat{ + Rat: new(big.Rat).SetInt64(1), + }, + }, + updateCbor: "a10cd81e82090a", + expectedParams: alonzo.AlonzoProtocolParameters{ + Decentralization: &cbor.Rat{Rat: big.NewRat(9, 10)}, + }, + }, + { + startParams: alonzo.AlonzoProtocolParameters{ + ProtocolMajor: 5, + }, + updateCbor: "a10e820600", + expectedParams: alonzo.AlonzoProtocolParameters{ + ProtocolMajor: 6, + }, + }, + { + startParams: alonzo.AlonzoProtocolParameters{ + MaxBlockBodySize: 1, + MaxTxExUnits: common.ExUnits{ + Memory: 1, + Steps: 1, + }, + }, + updateCbor: "a2021a0001200014821a00aba9501b00000002540be400", + expectedParams: alonzo.AlonzoProtocolParameters{ + MaxBlockBodySize: 73728, + MaxTxExUnits: common.ExUnits{ + Memory: 11250000, + Steps: 10000000000, + }, + }, }, } - - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - data, err := hex.DecodeString(tt.updateCbor) - if err != nil { - t.Fatalf("failed to decode CBOR: %v", err) - } - - var update alonzo.AlonzoProtocolParameterUpdate - if _, err := cbor.Decode(data, &update); err != nil { - if !tt.expectError { - t.Fatalf("failed to decode update: %v", err) - } - return - } - - params := newBaseProtocolParams() - params.Update(&update) - - if !reflect.DeepEqual(params, tt.expected) { - t.Errorf("unexpected result:\ngot: %+v\nwant: %+v", params, tt.expected) - } - }) + for _, testDef := range testDefs { + cborBytes, err := hex.DecodeString(testDef.updateCbor) + if err != nil { + t.Fatalf("unexpected error: %s", err) + } + var tmpUpdate alonzo.AlonzoProtocolParameterUpdate + if _, err := cbor.Decode(cborBytes, &tmpUpdate); err != nil { + t.Fatalf("unexpected error: %s", err) + } + tmpParams := testDef.startParams + tmpParams.Update(&tmpUpdate) + if !reflect.DeepEqual(tmpParams, testDef.expectedParams) { + t.Fatalf( + "did not get expected params:\n got: %#v\n wanted: %#v", + tmpParams, + testDef.expectedParams, + ) + } } } func TestAlonzoProtocolParametersUpdateFromGenesis(t *testing.T) { + // Create cost models in the format the UpdateFromGenesis expects + plutusV1CostModel := make(map[string]interface{}) + for i := 1; i <= 166; i++ { + plutusV1CostModel[fmt.Sprintf("param%d", i)] = i + } + + plutusV2CostModel := make(map[string]interface{}) + for i := 1; i <= 175; i++ { + plutusV2CostModel[fmt.Sprintf("param%d", i)] = i + } + tests := []struct { name string genesisJSON string - validate func(*alonzo.AlonzoProtocolParameters) bool }{ { name: "Basic Parameters", @@ -108,228 +169,339 @@ func TestAlonzoProtocolParametersUpdateFromGenesis(t *testing.T) { "maxTxExUnits": {"mem": 10000000, "steps": 10000000000}, "maxBlockExUnits": {"mem": 50000000, "steps": 40000000000}, "executionPrices": { - "mem": {"numerator": 577, "denominator": 10000}, - "steps": {"numerator": 721, "denominator": 10000000} + "prMem": {"numerator": 577, "denominator": 10000}, + "prSteps": {"numerator": 721, "denominator": 10000000} }, "costModels": { - "PlutusV1": { - "param0": 100, - "param1": 200, - "param2": 300 - } + "PlutusV1": ` + toJSON(plutusV1CostModel) + `, + "PlutusV2": ` + toJSON(plutusV2CostModel) + ` } }`, - validate: func(p *alonzo.AlonzoProtocolParameters) bool { - return p.AdaPerUtxoByte == 4310 && // 34482 / 8 - p.MaxValueSize == 5000 && - p.CollateralPercentage == 150 && - p.MaxCollateralInputs == 3 && - p.MaxTxExUnits.Memory == 10000000 && - p.MaxTxExUnits.Steps == 10000000000 && - p.ExecutionCosts.MemPrice.Rat.Cmp(big.NewRat(577, 10000)) == 0 && - p.CostModels[alonzo.PlutusV1] != nil && - len(p.CostModels[alonzo.PlutusV1].Order) == 3 - }, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Create a temporary struct with the exact JSON structure - type TempExecutionPrices struct { - Mem *struct { - Numerator int64 `json:"numerator"` - Denominator int64 `json:"denominator"` - } `json:"mem"` - Steps *struct { - Numerator int64 `json:"numerator"` - Denominator int64 `json:"denominator"` - } `json:"steps"` - } - - type TempCostModels map[string]map[string]int - - type TempGenesis struct { - LovelacePerUtxoWord uint64 `json:"lovelacePerUTxOWord"` - MaxValueSize uint `json:"maxValueSize"` - CollateralPercentage uint `json:"collateralPercentage"` - MaxCollateralInputs uint `json:"maxCollateralInputs"` - MaxTxExUnits struct { - Mem uint64 `json:"mem"` - Steps uint64 `json:"steps"` - } `json:"maxTxExUnits"` - MaxBlockExUnits struct { - Mem uint64 `json:"mem"` - Steps uint64 `json:"steps"` - } `json:"maxBlockExUnits"` - ExecutionPrices TempExecutionPrices `json:"executionPrices"` - CostModels TempCostModels `json:"costModels"` - } - - var tempGenesis TempGenesis - err := json.Unmarshal([]byte(tt.genesisJSON), &tempGenesis) - if err != nil { + var genesis alonzo.AlonzoGenesis + if err := json.Unmarshal([]byte(tt.genesisJSON), &genesis); err != nil { t.Fatalf("failed to parse genesis: %v", err) } - // Convert to the actual AlonzoGenesis type with proper type conversions - genesis := alonzo.AlonzoGenesis{ - LovelacePerUtxoWord: tempGenesis.LovelacePerUtxoWord, - MaxValueSize: tempGenesis.MaxValueSize, - CollateralPercentage: tempGenesis.CollateralPercentage, - MaxCollateralInputs: tempGenesis.MaxCollateralInputs, - MaxTxExUnits: alonzo.AlonzoGenesisExUnits{ - Mem: uint(tempGenesis.MaxTxExUnits.Mem), - Steps: uint(tempGenesis.MaxTxExUnits.Steps), - }, - MaxBlockExUnits: alonzo.AlonzoGenesisExUnits{ - Mem: uint(tempGenesis.MaxBlockExUnits.Mem), - Steps: uint(tempGenesis.MaxBlockExUnits.Steps), - }, - ExecutionPrices: alonzo.AlonzoGenesisExecutionPrices{ - Mem: convertToExecutionPricesRat(tempGenesis.ExecutionPrices.Mem), - Steps: convertToExecutionPricesRat(tempGenesis.ExecutionPrices.Steps), - }, - CostModels: convertCostModels(tempGenesis.CostModels), - } - params := newBaseProtocolParams() - err = params.UpdateFromGenesis(&genesis) - if err != nil { + if err := params.UpdateFromGenesis(&genesis); err != nil { t.Fatalf("UpdateFromGenesis failed: %v", err) } - if !tt.validate(¶ms) { - t.Errorf("validation failed for params: %+v", params) + if len(params.CostModels[0]) != 166 { + t.Errorf("expected 166 PlutusV1 parameters, got %d", len(params.CostModels[0])) + } + if len(params.CostModels[1]) != 175 { + t.Errorf("expected 175 PlutusV2 parameters, got %d", len(params.CostModels[1])) } }) } } -func convertToExecutionPricesRat(r *struct { - Numerator int64 `json:"numerator"` - Denominator int64 `json:"denominator"` -}) *alonzo.AlonzoGenesisExecutionPricesRat { - if r == nil { - return nil +func TestCostModelArrayFormat(t *testing.T) { + // Create a PlutusV1 cost model as an array + plutusV1Array := make([]int, 166) + for i := range plutusV1Array { + plutusV1Array[i] = i + 1 + } + + genesisJSON := fmt.Sprintf(`{ + "lovelacePerUTxOWord": 34482, + "maxValueSize": 5000, + "collateralPercentage": 150, + "maxCollateralInputs": 3, + "executionPrices": { + "prMem": {"numerator": 577, "denominator": 10000}, + "prSteps": {"numerator": 721, "denominator": 10000000} + }, + "maxTxExUnits": {"mem": 10000000, "steps": 10000000000}, + "maxBlockExUnits": {"mem": 50000000, "steps": 40000000000}, + "costModels": { + "PlutusV1": %s + } + }`, toJSON(plutusV1Array)) + + var genesis alonzo.AlonzoGenesis + if err := json.Unmarshal([]byte(genesisJSON), &genesis); err != nil { + t.Fatalf("failed to unmarshal genesis JSON: %v", err) } - return &alonzo.AlonzoGenesisExecutionPricesRat{ - Rat: big.NewRat(r.Numerator, r.Denominator), + + params := alonzo.AlonzoProtocolParameters{} + if err := params.UpdateFromGenesis(&genesis); err != nil { + t.Fatalf("UpdateFromGenesis failed: %v", err) + } + + if len(params.CostModels[alonzo.PlutusV1Key]) != 166 { + t.Errorf("expected 166 parameters, got %d", len(params.CostModels[alonzo.PlutusV1Key])) + } + + // Verify first and last values + if params.CostModels[alonzo.PlutusV1Key][0] != 1 { + t.Errorf("expected first parameter to be 1, got %d", params.CostModels[alonzo.PlutusV1Key][0]) + } + if params.CostModels[alonzo.PlutusV1Key][165] != 166 { + t.Errorf("expected last parameter to be 166, got %d", params.CostModels[alonzo.PlutusV1Key][165]) } } -func convertCostModels(tempModels map[string]map[string]int) map[string]map[string]int { - // Create a new map with the correct type - models := make(map[string]map[string]int) - for k, v := range tempModels { - // Create a new inner map - innerMap := make(map[string]int) - for k2, v2 := range v { - innerMap[k2] = v2 +func TestScientificNotationInCostModels(t *testing.T) { + // Create a full cost model with 166 parameters, using scientific notation for some + costModel := make(map[string]interface{}) + for i := 1; i <= 166; i++ { + switch i { + case 1: + costModel[fmt.Sprintf("param%d", i)] = 2.477736e+06 + case 2: + costModel[fmt.Sprintf("param%d", i)] = 1.5e6 + case 3: + costModel[fmt.Sprintf("param%d", i)] = 1000000 + default: + costModel[fmt.Sprintf("param%d", i)] = i * 1000 + } + } + + genesisJSON := fmt.Sprintf(`{ + "lovelacePerUTxOWord": 34482, + "maxValueSize": 5000, + "collateralPercentage": 150, + "maxCollateralInputs": 3, + "executionPrices": { + "prMem": {"numerator": 577, "denominator": 10000}, + "prSteps": {"numerator": 721, "denominator": 10000000} + }, + "maxTxExUnits": {"mem": 10000000, "steps": 10000000000}, + "maxBlockExUnits": {"mem": 50000000, "steps": 40000000000}, + "costModels": { + "PlutusV1": %s + } + }`, toJSON(costModel)) + + var genesis alonzo.AlonzoGenesis + if err := json.Unmarshal([]byte(genesisJSON), &genesis); err != nil { + t.Fatalf("failed to unmarshal genesis: %v", err) + } + + params := alonzo.AlonzoProtocolParameters{} + if err := params.UpdateFromGenesis(&genesis); err != nil { + t.Fatalf("UpdateFromGenesis failed: %v", err) + } + + // Verify the scientific notation conversions + expected := []int64{2477736, 1500000, 1000000} + for i := 0; i < 3; i++ { + if params.CostModels[alonzo.PlutusV1Key][i] != expected[i] { + t.Errorf("parameter %d conversion failed: got %d, want %d", + i+1, params.CostModels[alonzo.PlutusV1Key][i], expected[i]) } - models[k] = innerMap } - return models + + // Verify we have all 166 parameters + if len(params.CostModels[alonzo.PlutusV1Key]) != 166 { + t.Errorf("expected 166 parameters, got %d", len(params.CostModels[alonzo.PlutusV1Key])) + } +} + +func TestInvalidCostModelFormats(t *testing.T) { + baseJSON := `{ + "lovelacePerUTxOWord": 34482, + "maxValueSize": 5000, + "collateralPercentage": 150, + "maxCollateralInputs": 3, + "executionPrices": { + "prMem": {"numerator": 577, "denominator": 10000}, + "prSteps": {"numerator": 721, "denominator": 10000000} + }, + "maxTxExUnits": {"mem": 10000000, "steps": 10000000000}, + "maxBlockExUnits": {"mem": 50000000, "steps": 40000000000}, + %s + }` + + tests := []struct { + name string + costModels string + expectError string + }{ + { + name: "InvalidType", + costModels: `"costModels": { + "PlutusV1": "invalid" + }`, + expectError: "invalid cost model format", + }, + { + name: "ShortArray", + costModels: `"costModels": { + "PlutusV1": [1, 2, 3] + }`, + expectError: "expected 166, got 3", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + fullJSON := fmt.Sprintf(baseJSON, tt.costModels) + + var genesis alonzo.AlonzoGenesis + if err := json.Unmarshal([]byte(fullJSON), &genesis); err != nil { + t.Fatalf("failed to unmarshal genesis: %v", err) + } + + params := alonzo.AlonzoProtocolParameters{} + err := params.UpdateFromGenesis(&genesis) + if err == nil { + t.Fatal("expected error but got none") + } + if !strings.Contains(err.Error(), tt.expectError) { + t.Errorf("expected error containing %q, got %v", tt.expectError, err) + } + }) + } } -func TestAlonzoProtocolParametersUtxorpc(t *testing.T) { - baseParams := newBaseProtocolParams() - params := alonzo.AlonzoProtocolParameters{ - MinFeeA: baseParams.MinFeeA, - MinFeeB: baseParams.MinFeeB, - MaxBlockBodySize: baseParams.MaxBlockBodySize, - MaxTxSize: baseParams.MaxTxSize, - MaxBlockHeaderSize: baseParams.MaxBlockHeaderSize, - KeyDeposit: baseParams.KeyDeposit, - PoolDeposit: baseParams.PoolDeposit, - MaxEpoch: baseParams.MaxEpoch, - NOpt: baseParams.NOpt, - A0: baseParams.A0, - Rho: baseParams.Rho, - Tau: baseParams.Tau, - ProtocolMajor: baseParams.ProtocolMajor, - ProtocolMinor: baseParams.ProtocolMinor, +func TestAlonzoUtxorpc(t *testing.T) { + inputParams := alonzo.AlonzoProtocolParameters{ + MaxTxSize: 16384, + MinFeeA: 500, + MinFeeB: 2, + MaxBlockBodySize: 65536, + MaxBlockHeaderSize: 1024, + KeyDeposit: 2000, + PoolDeposit: 500000, + MaxEpoch: 2160, + NOpt: 100, + A0: &cbor.Rat{Rat: big.NewRat(1, 2)}, + Rho: &cbor.Rat{Rat: big.NewRat(3, 4)}, + Tau: &cbor.Rat{Rat: big.NewRat(5, 6)}, + ProtocolMajor: 8, + ProtocolMinor: 0, + AdaPerUtxoByte: 44 / 8, MinPoolCost: 340000000, - AdaPerUtxoByte: 4310, - MaxValueSize: 5000, + MaxValueSize: 1024, CollateralPercentage: 150, - MaxCollateralInputs: 3, + MaxCollateralInputs: 5, ExecutionCosts: common.ExUnitPrice{ - MemPrice: &cbor.Rat{Rat: big.NewRat(577, 10000)}, - StepPrice: &cbor.Rat{Rat: big.NewRat(721, 10000000)}, + MemPrice: &cbor.Rat{Rat: big.NewRat(1, 2)}, + StepPrice: &cbor.Rat{Rat: big.NewRat(2, 3)}, }, MaxTxExUnits: common.ExUnits{ - Memory: 10000000, - Steps: 10000000000, + Memory: 1000000, + Steps: 200000, }, MaxBlockExUnits: common.ExUnits{ - Memory: 50000000, - Steps: 40000000000, + Memory: 5000000, + Steps: 1000000, }, - CostModels: map[alonzo.PlutusVersion]*alonzo.CostModel{ - alonzo.PlutusV1: { - Parameters: map[string]int64{"param0": 100, "param1": 200, "param2": 300}, - Order: []string{"param0", "param1", "param2"}, - }, + CostModels: map[uint][]int64{ + 1: {100, 200, 300}, + 2: {400, 500, 600}, + 3: {700, 800, 900}, }, } - result := params.Utxorpc() - if result == nil { - t.Fatal("Utxorpc() returned nil") + expectedUtxorpc := &cardano.PParams{ + CoinsPerUtxoByte: 44 / 8, + MaxTxSize: 16384, + MinFeeCoefficient: 500, + MinFeeConstant: 2, + MaxBlockBodySize: 65536, + MaxBlockHeaderSize: 1024, + StakeKeyDeposit: 2000, + PoolDeposit: 500000, + PoolRetirementEpochBound: 2160, + DesiredNumberOfPools: 100, + PoolInfluence: &cardano.RationalNumber{ + Numerator: int32(1), + Denominator: uint32(2), + }, + MonetaryExpansion: &cardano.RationalNumber{ + Numerator: int32(3), + Denominator: uint32(4), + }, + TreasuryExpansion: &cardano.RationalNumber{ + Numerator: int32(5), + Denominator: uint32(6), + }, + MinPoolCost: 340000000, + ProtocolVersion: &cardano.ProtocolVersion{ + Major: 8, + Minor: 0, + }, + MaxValueSize: 1024, + CollateralPercentage: 150, + MaxCollateralInputs: 5, + CostModels: &cardano.CostModels{ + PlutusV1: &cardano.CostModel{ + Values: []int64{100, 200, 300}, + }, + PlutusV2: &cardano.CostModel{ + Values: []int64{400, 500, 600}, + }, + PlutusV3: &cardano.CostModel{ + Values: []int64{700, 800, 900}, + }, + }, + Prices: &cardano.ExPrices{ + Memory: &cardano.RationalNumber{ + Numerator: int32(1), + Denominator: uint32(2), + }, + Steps: &cardano.RationalNumber{ + Numerator: int32(2), + Denominator: uint32(3), + }, + }, + MaxExecutionUnitsPerTransaction: &cardano.ExUnits{ + Memory: 1000000, + Steps: 200000, + }, + MaxExecutionUnitsPerBlock: &cardano.ExUnits{ + Memory: 5000000, + Steps: 1000000, + }, } - // Verify cost models - if result.CostModels == nil { - t.Fatal("CostModels should not be nil") - } - if result.CostModels.PlutusV1 == nil { - t.Fatal("PlutusV1 cost model should not be nil") - } - if len(result.CostModels.PlutusV1.Values) != 3 { - t.Errorf("expected 3 cost model values, got %d", len(result.CostModels.PlutusV1.Values)) + result := inputParams.Utxorpc() + + if !reflect.DeepEqual(result, expectedUtxorpc) { + t.Fatalf( + "Utxorpc() test failed for Alonzo:\nExpected: %#v\nGot: %#v", + expectedUtxorpc, + result, + ) } +} - // Verify other parameters - if result.MaxTxSize != uint64(params.MaxTxSize) { - t.Errorf("incorrect MaxTxSize conversion") +func toJSON(v interface{}) string { + b, err := json.Marshal(v) + if err != nil { + panic(fmt.Sprintf("failed to marshal JSON: %v", err)) } - if result.MinPoolCost != params.MinPoolCost { - t.Errorf("incorrect MinPoolCost conversion") + return string(b) +} + +func verifyCostModel(t *testing.T, models map[string]interface{}, name string, expectedCount int) { + cm, ok := models[name].(map[string]interface{}) + if !ok { + t.Fatalf("%s cost model not found or wrong type", name) } - if result.CoinsPerUtxoByte != params.AdaPerUtxoByte { - t.Errorf("incorrect AdaPerUtxoByte conversion") + if len(cm) != expectedCount { + t.Fatalf("%s parameter count mismatch: got %d, want %d", name, len(cm), expectedCount) } } -func TestCostModelConversions(t *testing.T) { - tests := []struct { - name string - input map[alonzo.PlutusVersion]*alonzo.CostModel - expected map[uint][]int64 - }{ - { - name: "Single Version", - input: map[alonzo.PlutusVersion]*alonzo.CostModel{ - alonzo.PlutusV1: { - Parameters: map[string]int64{"param0": 1, "param1": 2}, - Order: []string{"param0", "param1"}, - }, - }, - expected: map[uint][]int64{ - 0: {1, 2}, - }, - }, +func mustMarshalJSON(v interface{}) string { + b, err := json.Marshal(v) + if err != nil { + panic(fmt.Sprintf("failed to marshal JSON: %v", err)) } + return string(b) +} - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - params := newBaseProtocolParams() - params.CostModels = tt.input - result := params.ToLegacyCostModels() - if !reflect.DeepEqual(result, tt.expected) { - t.Errorf("unexpected result:\ngot: %v\nwant: %v", result, tt.expected) - } - }) - } +func jsonStringFromMap(m map[string]int64) string { + b, _ := json.Marshal(m) + return string(b) } diff --git a/ledger/babbage/pparams.go b/ledger/babbage/pparams.go index 548cbdd1..336d55a6 100644 --- a/ledger/babbage/pparams.go +++ b/ledger/babbage/pparams.go @@ -265,7 +265,7 @@ func UpgradePParams( ProtocolMinor: prevPParams.ProtocolMinor, MinPoolCost: prevPParams.MinPoolCost, AdaPerUtxoByte: prevPParams.AdaPerUtxoByte, - CostModels: prevPParams.ToLegacyCostModels(), + CostModels: prevPParams.CostModels, ExecutionCosts: prevPParams.ExecutionCosts, MaxTxExUnits: prevPParams.MaxTxExUnits, MaxBlockExUnits: prevPParams.MaxBlockExUnits, From 42af2e9f8f48d7859ee8d823710c3201a0bad29d Mon Sep 17 00:00:00 2001 From: Jenita Date: Sat, 3 May 2025 10:53:12 +0545 Subject: [PATCH 09/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/pparams.go | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/ledger/alonzo/pparams.go b/ledger/alonzo/pparams.go index 8d008cf9..5ab9826c 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -241,20 +241,6 @@ func plutusVersionToKey(version string) (uint, bool) { } } -// Helper to convert key back to string -func plutusKeyToVersion(key uint) string { - switch key { - case PlutusV1Key: - return "PlutusV1" - case PlutusV2Key: - return "PlutusV2" - case PlutusV3Key: - return "PlutusV3" - default: - return "" - } -} - type AlonzoProtocolParameterUpdate struct { cbor.DecodeStoreCbor MinFeeA *uint `cbor:"0,keyasint"` From 61cd643f565d7bcc608ac66cc4201c8ff7137977 Mon Sep 17 00:00:00 2001 From: Jenita Date: Sat, 31 May 2025 14:04:16 -0500 Subject: [PATCH 10/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/genesis.go | 119 +++++++++------ ledger/alonzo/genesis_test.go | 4 +- ledger/alonzo/pparams.go | 51 +++---- ledger/alonzo/pparams_test.go | 262 ++++++++++++---------------------- 4 files changed, 195 insertions(+), 241 deletions(-) diff --git a/ledger/alonzo/genesis.go b/ledger/alonzo/genesis.go index ecbaa8bd..9625c048 100644 --- a/ledger/alonzo/genesis.go +++ b/ledger/alonzo/genesis.go @@ -30,7 +30,85 @@ type AlonzoGenesis struct { ExecutionPrices AlonzoGenesisExecutionPrices `json:"executionPrices"` MaxTxExUnits AlonzoGenesisExUnits `json:"maxTxExUnits"` MaxBlockExUnits AlonzoGenesisExUnits `json:"maxBlockExUnits"` - CostModels map[string]interface{} `json:"costModels"` + CostModels map[string]CostModel `json:"costModels"` +} + +type CostModel map[string]int + +// NormalizeCostModels converts all cost model keys to consistent paramX format +func (g *AlonzoGenesis) NormalizeCostModels() error { + if g.CostModels == nil { + return nil + } + + for version, model := range g.CostModels { + normalized := make(CostModel) + for k, v := range model { + // Check if key is already in paramX format + var index int + if _, err := fmt.Sscanf(k, "param%d", &index); err == nil { + normalized[k] = v // Keep existing paramX keys + continue + } + + // Check if key is a numeric index (from array format) + if _, err := fmt.Sscanf(k, "%d", &index); err == nil { + normalized[fmt.Sprintf("param%d", index)] = v + continue + } + normalized[k] = v + } + g.CostModels[version] = normalized + } + return nil +} + +func (c *CostModel) UnmarshalJSON(data []byte) error { + tmpMap := make(map[string]interface{}) + if err := json.Unmarshal(data, &tmpMap); err != nil { + // Try to unmarshal as array first + var tmpArray []interface{} + if arrayErr := json.Unmarshal(data, &tmpArray); arrayErr == nil { + *c = make(CostModel) + for i, v := range tmpArray { + num, err := toInt(v) + if err != nil { + return fmt.Errorf("array index %d: %w", i, err) + } + (*c)[fmt.Sprintf("%d", i)] = num + } + return nil + } + return err + } + + *c = make(CostModel) + for k, v := range tmpMap { + num, err := toInt(v) + if err != nil { + return fmt.Errorf("key %s: %w", k, err) + } + (*c)[k] = num + } + return nil +} + +func toInt(v interface{}) (int, error) { + switch val := v.(type) { + case float64: + return int(val), nil + case int: + return val, nil + case json.Number: + intVal, err := val.Int64() + return int(intVal), err + case int64: + return int(val), nil + case uint64: + return int(val), nil + default: + return 0, fmt.Errorf("unsupported numeric type: %T", v) + } } func NewAlonzoGenesisFromReader(r io.Reader) (AlonzoGenesis, error) { @@ -80,42 +158,3 @@ func (r *AlonzoGenesisExecutionPricesRat) UnmarshalJSON(data []byte) error { r.Rat = big.NewRat(tmpData.Numerator, tmpData.Denominator) return nil } - -func (a *AlonzoGenesis) NormalizeCostModels() error { - if a.CostModels == nil { - return nil - } - - normalized := make(map[string]map[string]int) - for version, model := range a.CostModels { - if modelMap, ok := model.(map[string]interface{}); ok { - versionMap := make(map[string]int) - for k, v := range modelMap { - switch val := v.(type) { - case float64: - versionMap[k] = int(val) - case int: - versionMap[k] = val - case json.Number: - intVal, err := val.Int64() - if err != nil { - floatVal, err := val.Float64() - if err != nil { - return fmt.Errorf("invalid number in cost model: %v", val) - } - intVal = int64(floatVal) - } - versionMap[k] = int(intVal) - default: - return fmt.Errorf("invalid cost model value type: %T", v) - } - } - normalized[version] = versionMap - } - } - a.CostModels = make(map[string]interface{}) - for k, v := range normalized { - a.CostModels[k] = v - } - return nil -} diff --git a/ledger/alonzo/genesis_test.go b/ledger/alonzo/genesis_test.go index 920b6261..bfa7c16e 100644 --- a/ledger/alonzo/genesis_test.go +++ b/ledger/alonzo/genesis_test.go @@ -243,7 +243,7 @@ var expectedGenesisObj = alonzo.AlonzoGenesis{ Mem: 50000000, Steps: 40000000000, }, - CostModels: map[string]interface{}{ + CostModels: map[string]alonzo.CostModel{ "PlutusV1": map[string]int{ "addInteger-cpu-arguments-intercept": 197209, "addInteger-cpu-arguments-slope": 0, @@ -484,7 +484,7 @@ func TestNewAlonzoGenesisFromReader(t *testing.T) { t.Logf("prMem is correct: %v", result.ExecutionPrices.Mem.Rat) } - expectedCostModels := map[string]interface{}{ + expectedCostModels := map[string]alonzo.CostModel{ "PlutusV1": map[string]int{ "addInteger-cpu-arguments-intercept": 205665, "addInteger-cpu-arguments-slope": 812, diff --git a/ledger/alonzo/pparams.go b/ledger/alonzo/pparams.go index 5ab9826c..69a0038f 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -17,6 +17,7 @@ package alonzo import ( "fmt" "math" + "strconv" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/common" @@ -168,7 +169,8 @@ func (p *AlonzoProtocolParameters) UpdateFromGenesis(genesis *AlonzoGenesis) err Steps: uint64(genesis.MaxBlockExUnits.Steps), } - if genesis.ExecutionPrices.Mem != nil && genesis.ExecutionPrices.Steps != nil { + if genesis.ExecutionPrices.Mem != nil && + genesis.ExecutionPrices.Steps != nil { p.ExecutionCosts = common.ExUnitPrice{ MemPrice: &cbor.Rat{Rat: genesis.ExecutionPrices.Mem.Rat}, StepPrice: &cbor.Rat{Rat: genesis.ExecutionPrices.Steps.Rat}, @@ -184,41 +186,26 @@ func (p *AlonzoProtocolParameters) UpdateFromGenesis(genesis *AlonzoGenesis) err continue } - var values []int64 - switch v := model.(type) { - case map[string]interface{}: - maxIndex := 0 - // Find maximum parameter index - for paramName := range v { - var index int - if _, err := fmt.Sscanf(paramName, "param%d", &index); err == nil && index > maxIndex { - maxIndex = index - } - } - values = make([]int64, maxIndex) - for paramName, val := range v { - var index int - if _, err := fmt.Sscanf(paramName, "param%d", &index); err == nil && index > 0 { - if intVal, ok := val.(float64); ok { - values[index-1] = int64(intVal) - } - } - } + expectedCount, ok := plutusParamCounts[key] + if !ok { + continue + } + + values := make([]int64, expectedCount) - case []interface{}: - values = make([]int64, len(v)) - for i, val := range v { - if intVal, ok := val.(float64); ok { - values[i] = int64(intVal) + for paramName, val := range model { + if index, err := strconv.Atoi(paramName); err == nil { + if index >= 0 && index < expectedCount { + values[index] = int64(val) } } - - default: - return fmt.Errorf("invalid cost model format for %s", versionStr) } - if expected, ok := plutusParamCounts[key]; ok && len(values) != expected { - return fmt.Errorf("invalid parameter count for %s: expected %d, got %d", - versionStr, expected, len(values)) + + // Verify we have all expected parameters + for i, val := range values { + if val == 0 { + return fmt.Errorf("missing parameter at index %d for %s", i, versionStr) + } } p.CostModels[key] = values diff --git a/ledger/alonzo/pparams_test.go b/ledger/alonzo/pparams_test.go index 48745467..dfe69ccf 100644 --- a/ledger/alonzo/pparams_test.go +++ b/ledger/alonzo/pparams_test.go @@ -20,6 +20,7 @@ import ( "fmt" "math/big" "reflect" + "strconv" "strings" "testing" @@ -63,17 +64,16 @@ func newBaseProtocolParams() alonzo.AlonzoProtocolParameters { CollateralPercentage: 150, MaxCollateralInputs: 3, CostModels: map[uint][]int64{ - 0: completeCostModel(166), // PlutusV1 with exactly 166 parameters - 1: completeCostModel(175), // PlutusV2 with exactly 175 parameters + alonzo.PlutusV1Key: completeCostModel(166), + alonzo.PlutusV2Key: completeCostModel(175), }, } } -// Helper function to create complete cost models func completeCostModel(size int) []int64 { model := make([]int64, size) for i := range model { - model[i] = int64(i + 1) // Fill with sequential values + model[i] = int64(i + 1) } return model } @@ -104,23 +104,6 @@ func TestAlonzoProtocolParamsUpdate(t *testing.T) { ProtocolMajor: 6, }, }, - { - startParams: alonzo.AlonzoProtocolParameters{ - MaxBlockBodySize: 1, - MaxTxExUnits: common.ExUnits{ - Memory: 1, - Steps: 1, - }, - }, - updateCbor: "a2021a0001200014821a00aba9501b00000002540be400", - expectedParams: alonzo.AlonzoProtocolParameters{ - MaxBlockBodySize: 73728, - MaxTxExUnits: common.ExUnits{ - Memory: 11250000, - Steps: 10000000000, - }, - }, - }, } for _, testDef := range testDefs { cborBytes, err := hex.DecodeString(testDef.updateCbor) @@ -144,86 +127,74 @@ func TestAlonzoProtocolParamsUpdate(t *testing.T) { } func TestAlonzoProtocolParametersUpdateFromGenesis(t *testing.T) { - // Create cost models in the format the UpdateFromGenesis expects - plutusV1CostModel := make(map[string]interface{}) - for i := 1; i <= 166; i++ { - plutusV1CostModel[fmt.Sprintf("param%d", i)] = i + // Create cost models with numeric string keys + plutusV1CostModel := make(map[string]int) + for i := 0; i < 166; i++ { + plutusV1CostModel[strconv.Itoa(i)] = i + 1 // "0":1, "1":2, etc. } - plutusV2CostModel := make(map[string]interface{}) - for i := 1; i <= 175; i++ { - plutusV2CostModel[fmt.Sprintf("param%d", i)] = i + plutusV2CostModel := make(map[string]int) + for i := 0; i < 175; i++ { + plutusV2CostModel[strconv.Itoa(i)] = i + 1 // "0":1, "1":2, etc. } - tests := []struct { - name string - genesisJSON string - }{ - { - name: "Basic Parameters", - genesisJSON: `{ - "lovelacePerUTxOWord": 34482, - "maxValueSize": 5000, - "collateralPercentage": 150, - "maxCollateralInputs": 3, - "maxTxExUnits": {"mem": 10000000, "steps": 10000000000}, - "maxBlockExUnits": {"mem": 50000000, "steps": 40000000000}, - "executionPrices": { - "prMem": {"numerator": 577, "denominator": 10000}, - "prSteps": {"numerator": 721, "denominator": 10000000} - }, - "costModels": { - "PlutusV1": ` + toJSON(plutusV1CostModel) + `, - "PlutusV2": ` + toJSON(plutusV2CostModel) + ` - } - }`, - }, - } + genesisJSON := fmt.Sprintf(`{ + "lovelacePerUTxOWord": 34482, + "maxValueSize": 5000, + "collateralPercentage": 150, + "maxCollateralInputs": 3, + "executionPrices": { + "prSteps": { "numerator": 721, "denominator": 10000000 }, + "prMem": { "numerator": 577, "denominator": 10000 } + }, + "maxTxExUnits": { "exUnitsMem": 10000000, "exUnitsSteps": 10000000000 }, + "maxBlockExUnits": { "exUnitsMem": 50000000, "exUnitsSteps": 40000000000 }, + "costModels": { + "PlutusV1": %s, + "PlutusV2": %s + } + }`, toJSON(plutusV1CostModel), toJSON(plutusV2CostModel)) - for _, tt := range tests { - t.Run(tt.name, func(t *testing.T) { - var genesis alonzo.AlonzoGenesis - if err := json.Unmarshal([]byte(tt.genesisJSON), &genesis); err != nil { - t.Fatalf("failed to parse genesis: %v", err) - } + var genesis alonzo.AlonzoGenesis + if err := json.Unmarshal([]byte(genesisJSON), &genesis); err != nil { + t.Fatalf("failed to parse genesis: %v", err) + } - params := newBaseProtocolParams() - if err := params.UpdateFromGenesis(&genesis); err != nil { - t.Fatalf("UpdateFromGenesis failed: %v", err) - } + params := newBaseProtocolParams() + if err := params.UpdateFromGenesis(&genesis); err != nil { + t.Fatalf("UpdateFromGenesis failed: %v", err) + } - if len(params.CostModels[0]) != 166 { - t.Errorf("expected 166 PlutusV1 parameters, got %d", len(params.CostModels[0])) - } - if len(params.CostModels[1]) != 175 { - t.Errorf("expected 175 PlutusV2 parameters, got %d", len(params.CostModels[1])) - } - }) + if len(params.CostModels[alonzo.PlutusV1Key]) != 166 { + t.Errorf("expected 166 PlutusV1 parameters, got %d", len(params.CostModels[alonzo.PlutusV1Key])) + } + if len(params.CostModels[alonzo.PlutusV2Key]) != 175 { + t.Errorf("expected 175 PlutusV2 parameters, got %d", len(params.CostModels[alonzo.PlutusV2Key])) } } func TestCostModelArrayFormat(t *testing.T) { - // Create a PlutusV1 cost model as an array - plutusV1Array := make([]int, 166) - for i := range plutusV1Array { - plutusV1Array[i] = i + 1 + // Create cost model with numeric string keys + plutusV1CostModel := make(map[string]int) + for i := 0; i < 166; i++ { + plutusV1CostModel[strconv.Itoa(i)] = i + 1 // "0":1, "1":2, etc. } genesisJSON := fmt.Sprintf(`{ - "lovelacePerUTxOWord": 34482, - "maxValueSize": 5000, - "collateralPercentage": 150, - "maxCollateralInputs": 3, - "executionPrices": { - "prMem": {"numerator": 577, "denominator": 10000}, - "prSteps": {"numerator": 721, "denominator": 10000000} - }, - "maxTxExUnits": {"mem": 10000000, "steps": 10000000000}, - "maxBlockExUnits": {"mem": 50000000, "steps": 40000000000}, - "costModels": { - "PlutusV1": %s - } - }`, toJSON(plutusV1Array)) + "lovelacePerUTxOWord": 34482, + "maxValueSize": 5000, + "collateralPercentage": 150, + "maxCollateralInputs": 3, + "executionPrices": { + "prSteps": { "numerator": 721, "denominator": 10000000 }, + "prMem": { "numerator": 577, "denominator": 10000 } + }, + "maxTxExUnits": { "exUnitsMem": 10000000, "exUnitsSteps": 10000000000 }, + "maxBlockExUnits": { "exUnitsMem": 50000000, "exUnitsSteps": 40000000000 }, + "costModels": { + "PlutusV1": %s + } + }`, toJSON(plutusV1CostModel)) var genesis alonzo.AlonzoGenesis if err := json.Unmarshal([]byte(genesisJSON), &genesis); err != nil { @@ -238,30 +209,17 @@ func TestCostModelArrayFormat(t *testing.T) { if len(params.CostModels[alonzo.PlutusV1Key]) != 166 { t.Errorf("expected 166 parameters, got %d", len(params.CostModels[alonzo.PlutusV1Key])) } - - // Verify first and last values - if params.CostModels[alonzo.PlutusV1Key][0] != 1 { - t.Errorf("expected first parameter to be 1, got %d", params.CostModels[alonzo.PlutusV1Key][0]) - } - if params.CostModels[alonzo.PlutusV1Key][165] != 166 { - t.Errorf("expected last parameter to be 166, got %d", params.CostModels[alonzo.PlutusV1Key][165]) - } } func TestScientificNotationInCostModels(t *testing.T) { - // Create a full cost model with 166 parameters, using scientific notation for some - costModel := make(map[string]interface{}) - for i := 1; i <= 166; i++ { - switch i { - case 1: - costModel[fmt.Sprintf("param%d", i)] = 2.477736e+06 - case 2: - costModel[fmt.Sprintf("param%d", i)] = 1.5e6 - case 3: - costModel[fmt.Sprintf("param%d", i)] = 1000000 - default: - costModel[fmt.Sprintf("param%d", i)] = i * 1000 - } + costModel := map[string]interface{}{ + "0": 2.477736e+06, // Changed from param1 to 0 + "1": 1.5e6, // Changed from param2 to 1 + "2": 1000000, // Changed from param3 to 2 + } + // Fill remaining parameters + for i := 3; i < 166; i++ { + costModel[strconv.Itoa(i)] = i * 1000 } genesisJSON := fmt.Sprintf(`{ @@ -270,11 +228,11 @@ func TestScientificNotationInCostModels(t *testing.T) { "collateralPercentage": 150, "maxCollateralInputs": 3, "executionPrices": { - "prMem": {"numerator": 577, "denominator": 10000}, - "prSteps": {"numerator": 721, "denominator": 10000000} + "prSteps": { "numerator": 721, "denominator": 10000000 }, + "prMem": { "numerator": 577, "denominator": 10000 } }, - "maxTxExUnits": {"mem": 10000000, "steps": 10000000000}, - "maxBlockExUnits": {"mem": 50000000, "steps": 40000000000}, + "maxTxExUnits": { "exUnitsMem": 10000000, "exUnitsSteps": 10000000000 }, + "maxBlockExUnits": { "exUnitsMem": 50000000, "exUnitsSteps": 40000000000 }, "costModels": { "PlutusV1": %s } @@ -290,36 +248,16 @@ func TestScientificNotationInCostModels(t *testing.T) { t.Fatalf("UpdateFromGenesis failed: %v", err) } - // Verify the scientific notation conversions expected := []int64{2477736, 1500000, 1000000} for i := 0; i < 3; i++ { if params.CostModels[alonzo.PlutusV1Key][i] != expected[i] { t.Errorf("parameter %d conversion failed: got %d, want %d", - i+1, params.CostModels[alonzo.PlutusV1Key][i], expected[i]) + i, params.CostModels[alonzo.PlutusV1Key][i], expected[i]) } } - - // Verify we have all 166 parameters - if len(params.CostModels[alonzo.PlutusV1Key]) != 166 { - t.Errorf("expected 166 parameters, got %d", len(params.CostModels[alonzo.PlutusV1Key])) - } } func TestInvalidCostModelFormats(t *testing.T) { - baseJSON := `{ - "lovelacePerUTxOWord": 34482, - "maxValueSize": 5000, - "collateralPercentage": 150, - "maxCollateralInputs": 3, - "executionPrices": { - "prMem": {"numerator": 577, "denominator": 10000}, - "prSteps": {"numerator": 721, "denominator": 10000000} - }, - "maxTxExUnits": {"mem": 10000000, "steps": 10000000000}, - "maxBlockExUnits": {"mem": 50000000, "steps": 40000000000}, - %s - }` - tests := []struct { name string costModels string @@ -330,30 +268,43 @@ func TestInvalidCostModelFormats(t *testing.T) { costModels: `"costModels": { "PlutusV1": "invalid" }`, - expectError: "invalid cost model format", + expectError: "cannot unmarshal string into Go struct field AlonzoGenesis.costModels", }, { - name: "ShortArray", + name: "MissingParameters", costModels: `"costModels": { - "PlutusV1": [1, 2, 3] + "PlutusV1": {"0":1, "1":2, "2":3} }`, - expectError: "expected 166, got 3", + expectError: "missing parameter at index 3 for PlutusV1", }, } + baseJSON := `{ + "lovelacePerUTxOWord": 34482, + "maxValueSize": 5000, + "collateralPercentage": 150, + "maxCollateralInputs": 3, + "executionPrices": { + "prSteps": { "numerator": 721, "denominator": 10000000 }, + "prMem": { "numerator": 577, "denominator": 10000 } + }, + "maxTxExUnits": { "exUnitsMem": 10000000, "exUnitsSteps": 10000000000 }, + "maxBlockExUnits": { "exUnitsMem": 50000000, "exUnitsSteps": 40000000000 }, + %s + }` + for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { fullJSON := fmt.Sprintf(baseJSON, tt.costModels) var genesis alonzo.AlonzoGenesis - if err := json.Unmarshal([]byte(fullJSON), &genesis); err != nil { - t.Fatalf("failed to unmarshal genesis: %v", err) - } - - params := alonzo.AlonzoProtocolParameters{} - err := params.UpdateFromGenesis(&genesis) + err := json.Unmarshal([]byte(fullJSON), &genesis) if err == nil { - t.Fatal("expected error but got none") + params := alonzo.AlonzoProtocolParameters{} + err = params.UpdateFromGenesis(&genesis) + if err == nil { + t.Fatal("expected error but got none") + } } if !strings.Contains(err.Error(), tt.expectError) { t.Errorf("expected error containing %q, got %v", tt.expectError, err) @@ -482,26 +433,3 @@ func toJSON(v interface{}) string { } return string(b) } - -func verifyCostModel(t *testing.T, models map[string]interface{}, name string, expectedCount int) { - cm, ok := models[name].(map[string]interface{}) - if !ok { - t.Fatalf("%s cost model not found or wrong type", name) - } - if len(cm) != expectedCount { - t.Fatalf("%s parameter count mismatch: got %d, want %d", name, len(cm), expectedCount) - } -} - -func mustMarshalJSON(v interface{}) string { - b, err := json.Marshal(v) - if err != nil { - panic(fmt.Sprintf("failed to marshal JSON: %v", err)) - } - return string(b) -} - -func jsonStringFromMap(m map[string]int64) string { - b, _ := json.Marshal(m) - return string(b) -} From 4b53e53a2fa31976d211b8a832ec2f41e39163d0 Mon Sep 17 00:00:00 2001 From: Jenita Date: Sat, 31 May 2025 14:08:17 -0500 Subject: [PATCH 11/11] feat: changes to support cost models while updating Alonzon protocols Signed-off-by: Jenita --- ledger/alonzo/genesis.go | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/ledger/alonzo/genesis.go b/ledger/alonzo/genesis.go index 9625c048..c597c353 100644 --- a/ledger/alonzo/genesis.go +++ b/ledger/alonzo/genesis.go @@ -18,8 +18,10 @@ import ( "encoding/json" "fmt" "io" + "math" "math/big" "os" + "strconv" ) type AlonzoGenesis struct { @@ -75,7 +77,7 @@ func (c *CostModel) UnmarshalJSON(data []byte) error { if err != nil { return fmt.Errorf("array index %d: %w", i, err) } - (*c)[fmt.Sprintf("%d", i)] = num + (*c)[strconv.Itoa(i)] = num } return nil } @@ -96,15 +98,30 @@ func (c *CostModel) UnmarshalJSON(data []byte) error { func toInt(v interface{}) (int, error) { switch val := v.(type) { case float64: + if val > float64(math.MaxInt) || val < float64(math.MinInt) { + return 0, fmt.Errorf("float64 value %v overflows int", val) + } return int(val), nil case int: return val, nil case json.Number: intVal, err := val.Int64() - return int(intVal), err + if err != nil { + return 0, err + } + if intVal > math.MaxInt || intVal < math.MinInt { + return 0, fmt.Errorf("json.Number value %v overflows int", val) + } + return int(intVal), nil case int64: + if val > math.MaxInt || val < math.MinInt { + return 0, fmt.Errorf("int64 value %v overflows int", val) + } return int(val), nil case uint64: + if val > math.MaxInt { + return 0, fmt.Errorf("uint64 value %v overflows int", val) + } return int(val), nil default: return 0, fmt.Errorf("unsupported numeric type: %T", v)