Skip to content

feat: changes to support cost models while updating Alonzo protocols through genesis config #984

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 10 commits into
base: main
Choose a base branch
from
45 changes: 44 additions & 1 deletion ledger/alonzo/genesis.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ package alonzo

import (
"encoding/json"
"fmt"
"io"
"math/big"
"os"
Expand All @@ -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"`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This field type is going to force us to do something like foo.CostModels.(map[string]int)["foo"] every time that we access something from it, which isn't ideal. What we probably want is a separate CostModel type with a custom UnmarshalJSON() function that works similarly to NormalizeCostModels() below.

}

func NewAlonzoGenesisFromReader(r io.Reader) (AlonzoGenesis, error) {
Expand All @@ -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
}

Expand Down Expand Up @@ -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)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we encounter all 3 of these types when parsing example Alonzo genesis files, or is this just covering our bases?

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
}
8 changes: 4 additions & 4 deletions ledger/alonzo/genesis_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
},
Expand Down
93 changes: 86 additions & 7 deletions ledger/alonzo/pparams.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
package alonzo

import (
"fmt"
"math"

"github.com/blinklabs-io/gouroboros/cbor"
Expand All @@ -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
Expand Down Expand Up @@ -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
Expand All @@ -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 {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is going to get split back to 2 lines the next time that we run golines on this repo

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
}
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems to be expecting a structure like this, but I'm not seeing where that would come from outside of the tests. It seems that it would be easier to use []int64 or similar.

{ "param0": 123, "param1": 234, "param2": 345 }

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)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this only ever contain float64 values? It looks like anything else would be ignored

}

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 {
Expand Down
Loading
Loading