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 c5164404..5ab9826c 100644 --- a/ledger/alonzo/pparams.go +++ b/ledger/alonzo/pparams.go @@ -15,6 +15,7 @@ package alonzo import ( + "fmt" "math" "github.com/blinklabs-io/gouroboros/cbor" @@ -23,6 +24,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 +149,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,16 +167,78 @@ 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}, } } - // 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 + } + + 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) + } + 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)) + } + + 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..48745467 100644 --- a/ledger/alonzo/pparams_test.go +++ b/ledger/alonzo/pparams_test.go @@ -16,6 +16,8 @@ package alonzo_test import ( "encoding/hex" + "encoding/json" + "fmt" "math/big" "reflect" "strings" @@ -24,9 +26,58 @@ import ( "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{ + 0: completeCostModel(166), // PlutusV1 with exactly 166 parameters + 1: completeCostModel(175), // PlutusV2 with exactly 175 parameters + }, + } +} + +// 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 @@ -92,44 +143,223 @@ 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 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 }{ { - 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: "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) + ` + } + }`, }, } - for _, testDef := range testDefs { - tmpGenesis, err := alonzo.NewAlonzoGenesisFromReader( - strings.NewReader(testDef.genesisJson), - ) - if err != nil { - t.Fatalf("unexpected error: %s", err) + + 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) + } + + 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])) + } + }) + } +} + +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 } - 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, - ) + }`, toJSON(plutusV1Array)) + + 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])) + } + + // 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 + } + } + + 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]) } } + + // 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 TestAlonzoUtxorpc(t *testing.T) { @@ -244,3 +474,34 @@ 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) +} + +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) +}