diff --git a/ledger/alonzo/genesis.go b/ledger/alonzo/genesis.go index 7acf569b..c597c353 100644 --- a/ledger/alonzo/genesis.go +++ b/ledger/alonzo/genesis.go @@ -16,9 +16,12 @@ package alonzo import ( "encoding/json" + "fmt" "io" + "math" "math/big" "os" + "strconv" ) type AlonzoGenesis struct { @@ -29,7 +32,100 @@ 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]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)[strconv.Itoa(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: + 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() + 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) + } } func NewAlonzoGenesisFromReader(r io.Reader) (AlonzoGenesis, error) { @@ -39,6 +135,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 } diff --git a/ledger/alonzo/genesis_test.go b/ledger/alonzo/genesis_test.go index ac278f30..bfa7c16e 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]alonzo.CostModel{ + "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]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 c5164404..69a0038f 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -15,7 +15,9 @@ package alonzo import ( + "fmt" "math" + "strconv" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/common" @@ -23,6 +25,20 @@ import ( cardano "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) +// Constants for Plutus version mapping +const ( + PlutusV1Key uint = 0 + PlutusV2Key uint = 1 + PlutusV3Key uint = 2 +) + +// Expected parameter counts for validation +var plutusParamCounts = map[uint]int{ + PlutusV1Key: 166, + PlutusV2Key: 175, + PlutusV3Key: 187, +} + type AlonzoProtocolParameters struct { cbor.StructAsArray MinFeeA uint @@ -134,10 +150,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 @@ -150,6 +168,7 @@ 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 { p.ExecutionCosts = common.ExUnitPrice{ @@ -157,9 +176,56 @@ 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 + + if genesis.CostModels != nil { + p.CostModels = make(map[uint][]int64) + + for versionStr, model := range genesis.CostModels { + key, ok := plutusVersionToKey(versionStr) + if !ok { + continue + } + + expectedCount, ok := plutusParamCounts[key] + if !ok { + continue + } + + values := make([]int64, expectedCount) + + for paramName, val := range model { + if index, err := strconv.Atoi(paramName); err == nil { + if index >= 0 && index < expectedCount { + values[index] = int64(val) + } + } + } + + // 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 + } + } + return nil +} + +// 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 0, false + } } type AlonzoProtocolParameterUpdate struct { diff --git a/ledger/alonzo/pparams_test.go b/ledger/alonzo/pparams_test.go index 699a1887..dfe69ccf 100644 --- a/ledger/alonzo/pparams_test.go +++ b/ledger/alonzo/pparams_test.go @@ -16,17 +16,68 @@ package alonzo_test import ( "encoding/hex" + "encoding/json" + "fmt" "math/big" "reflect" + "strconv" "strings" "testing" "github.com/blinklabs-io/gouroboros/cbor" "github.com/blinklabs-io/gouroboros/ledger/alonzo" "github.com/blinklabs-io/gouroboros/ledger/common" - "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" + cardano "github.com/utxorpc/go-codegen/utxorpc/v1alpha/cardano" ) +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, + 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{ + alonzo.PlutusV1Key: completeCostModel(166), + alonzo.PlutusV2Key: completeCostModel(175), + }, + } +} + +func completeCostModel(size int) []int64 { + model := make([]int64, size) + for i := range model { + model[i] = int64(i + 1) + } + return model +} + func TestAlonzoProtocolParamsUpdate(t *testing.T) { testDefs := []struct { startParams alonzo.AlonzoProtocolParameters @@ -53,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) @@ -92,43 +126,190 @@ func TestAlonzoProtocolParamsUpdate(t *testing.T) { } } -func TestAlonzoProtocolParamsUpdateFromGenesis(t *testing.T) { - testDefs := []struct { - startParams alonzo.AlonzoProtocolParameters - genesisJson string - expectedParams alonzo.AlonzoProtocolParameters +func TestAlonzoProtocolParametersUpdateFromGenesis(t *testing.T) { + // 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]int) + for i := 0; i < 175; i++ { + plutusV2CostModel[strconv.Itoa(i)] = i + 1 // "0":1, "1":2, etc. + } + + 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)) + + 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) + } + + 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 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": { + "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 { + t.Fatalf("failed to unmarshal genesis JSON: %v", err) + } + + 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])) + } +} + +func TestScientificNotationInCostModels(t *testing.T) { + 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(`{ + "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(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) + } + + 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, params.CostModels[alonzo.PlutusV1Key][i], expected[i]) + } + } +} + +func TestInvalidCostModelFormats(t *testing.T) { + tests := []struct { + name string + costModels string + expectError string }{ { - startParams: alonzo.AlonzoProtocolParameters{ - Decentralization: &cbor.Rat{ - Rat: new(big.Rat).SetInt64(1), - }, - }, - genesisJson: `{"lovelacePerUTxOWord": 34482}`, - expectedParams: alonzo.AlonzoProtocolParameters{ - Decentralization: &cbor.Rat{ - Rat: new(big.Rat).SetInt64(1), - }, - AdaPerUtxoByte: 34482 / 8, - }, + name: "InvalidType", + costModels: `"costModels": { + "PlutusV1": "invalid" + }`, + expectError: "cannot unmarshal string into Go struct field AlonzoGenesis.costModels", + }, + { + name: "MissingParameters", + costModels: `"costModels": { + "PlutusV1": {"0":1, "1":2, "2":3} + }`, + expectError: "missing parameter at index 3 for PlutusV1", }, } - 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, - ) - } + + 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 + err := json.Unmarshal([]byte(fullJSON), &genesis) + if err == nil { + 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) + } + }) } } @@ -244,3 +425,11 @@ func TestAlonzoUtxorpc(t *testing.T) { ) } } + +func toJSON(v interface{}) string { + b, err := json.Marshal(v) + if err != nil { + panic(fmt.Sprintf("failed to marshal JSON: %v", err)) + } + return string(b) +}