From 8cc54f2160c37e7fc2f05b4a5f3e28f1dcedf26f Mon Sep 17 00:00:00 2001 From: Felix Lange Date: Tue, 15 Jul 2025 10:12:40 +0200 Subject: [PATCH] params: WIP config2 --- params/chainparam.go | 58 ++++++ params/config2.go | 373 ++++++++++++++++++++++++++++++++++++++ params/config2_presets.go | 115 ++++++++++++ params/forks/forks.go | 130 ++++++++++--- params/registry.go | 85 +++++++++ 5 files changed, 732 insertions(+), 29 deletions(-) create mode 100644 params/chainparam.go create mode 100644 params/config2.go create mode 100644 params/config2_presets.go create mode 100644 params/registry.go diff --git a/params/chainparam.go b/params/chainparam.go new file mode 100644 index 0000000000..225558f3f7 --- /dev/null +++ b/params/chainparam.go @@ -0,0 +1,58 @@ +package params + +import ( + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params/forks" +) + +// ChainID +type ChainID big.Int + +// TerminalTotalDifficulty (TTD) is the total difficulty value where +type TerminalTotalDifficulty big.Int + +func (v *TerminalTotalDifficulty) MarshalText() ([]byte, error) { + return (*big.Int)(v).MarshalText() +} + +func (v *TerminalTotalDifficulty) UnmarshalText(input []byte) error { + return (*big.Int)(v).UnmarshalText(input) +} + +// DepositContractAddress configures the location of the deposit contract. +type DepositContractAddress common.Address + +// This configures the EIP-4844 parameters across forks. +// There must be an entry for each fork +type BlobSchedule map[forks.Fork]BlobConfig + +// DAOForkSupport is the chain parameter that configures the DAO fork. +// true=supports or false=opposes the fork. +// The default value is true. +type DAOForkSupport bool + +func init() { + Define(Parameter[*ChainID]{ + Name: "chainId", + Optional: false, + }) + Define(Parameter[*TerminalTotalDifficulty]{ + Name: "terminalTotalDifficulty", + Optional: false, + }) + Define(Parameter[DAOForkSupport]{ + Name: "daoForkSupport", + Optional: true, + Default: true, + }) + Define(Parameter[BlobSchedule]{ + Name: "blobSchedule", + Optional: true, + }) + Define(Parameter[DepositContractAddress]{ + Name: "depositContractAddress", + Optional: true, + }) +} diff --git a/params/config2.go b/params/config2.go new file mode 100644 index 0000000000..3e4aeb3844 --- /dev/null +++ b/params/config2.go @@ -0,0 +1,373 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package params + +import ( + "bytes" + "encoding/json" + "fmt" + "maps" + "math/big" + "reflect" + "slices" + "strings" + + "github.com/ethereum/go-ethereum/params/forks" +) + +// Activations contains the block numbers/timestamps where hard forks activate. +type Activations map[forks.Fork]uint64 + +// Config2 represents the chain configuration. +type Config2 struct { + activation Activations + param map[reflect.Type]ParameterType +} + +func NewConfig2(activations Activations, param ...ParameterType) *Config2 { + cfg := &Config2{ + activation: maps.Clone(activations), + param: make(map[reflect.Type]ParameterType, len(param)), + } + for _, pv := range param { + info, ok := findParam(pv) + if !ok { + panic(fmt.Sprintf("undefined chain parameter type %T", pv)) + } + cfg.param[info.rtype] = pv + } + return cfg +} + +// Active reports whether the given fork is active for a block number/time. +func (cfg *Config2) Active(f forks.Fork, block, timestamp uint64) bool { + activation, ok := cfg.activation[f] + if f.BlockBased() { + return ok && block >= activation + } + return ok && timestamp >= activation +} + +func (cfg *Config2) Activation(f forks.Fork) (uint64, bool) { + a, ok := cfg.activation[f] + return a, ok +} + +// Scheduled says whether the fork is scheduled at all. +func (cfg *Config2) Scheduled(f forks.Fork) bool { + _, ok := cfg.activation[f] + return ok +} + +// SetActivations returns a new configuration with the given forks activated. +func (cfg *Config2) SetActivations(forks Activations) *Config2 { + newA := maps.Clone(cfg.activation) + maps.Copy(newA, forks) + return &Config2{activation: newA, param: cfg.param} +} + +// SetParam returns a new configuration with parameters modified. +func (cfg *Config2) SetParam(p ParameterType) *Config2 { + return &Config2{activation: cfg.activation} +} + +// LatestFork returns the latest time-based fork that would be active for the given time. +func (cfg *Config2) LatestFork(time uint64) forks.Fork { + londonBlock := cfg.activation[forks.London] + for _, f := range slices.Backward(forks.CanonOrder) { + if f.BlockBased() { + break + } + if cfg.Active(f, londonBlock, time) { + return f + } + } + return forks.Paris +} + +// MarshalJSON encodes the config as JSON. +func (cfg *Config2) MarshalJSON() ([]byte, error) { + m := make(map[string]any) + // params + for _, p := range cfg.param { + info, ok := findParam(p) + if !ok { + panic(fmt.Sprintf("unknown chain parameter %T", p)) + } + m[info.name] = p + } + // forks + for f, act := range cfg.activation { + var name string + if f.BlockBased() { + name = fmt.Sprintf("%sBlock", strings.ToLower(name)) + } else { + name = fmt.Sprintf("%sTime", strings.ToLower(name)) + } + m[name] = act + } + return json.Marshal(m) +} + +// MarshalJSON encodes the config as JSON. +func (cfg *Config2) UnmarshalJSON(input []byte) error { + dec := json.NewDecoder(bytes.NewReader(input)) + tok, err := dec.Token() + if err != nil { + return err + } + if tok != json.Delim('{') { + return fmt.Errorf("expected JSON object for chain configuration") + } + // Now we're in the object. + newcfg := Config2{ + activation: make(Activations), + } + for { + tok, err = dec.Token() + if tok == json.Delim('}') { + break + } else if key, ok := tok.(string); ok { + if strings.HasSuffix(key, "Block") || strings.HasSuffix(key, "Time") { + err = newcfg.decodeActivation(key, dec) + } else { + err = newcfg.decodeParameter(key, dec) + } + } + if err != nil { + return err + } + } + + *cfg = newcfg + return nil +} + +func (cfg *Config2) decodeActivation(key string, dec *json.Decoder) error { + var num uint64 + if err := dec.Decode(&num); err != nil { + return err + } + + var f forks.Fork + name, ok := strings.CutSuffix(key, "Block") + if ok { + f, ok = forks.ByName(name) + if !ok || !f.BlockBased() { + return fmt.Errorf("unknown block-based fork %q", name) + } + } else if name, ok = strings.CutSuffix(key, "Time"); ok { + f, ok = forks.ByName(name) + if !ok || f.BlockBased() { + return fmt.Errorf("unknown time-based fork %q", name) + } + } + cfg.activation[f] = num + return nil +} + +func (cfg *Config2) decodeParameter(key string, dec *json.Decoder) error { + info, ok := paramRegistryByName[key] + if !ok { + return fmt.Errorf("unknown chain parameter %q", key) + } + v := reflect.New(info.rtype).Interface() + if err := dec.Decode(v); err != nil { + return err + } + cfg.param[info.rtype] = v + return nil +} + +// Validate checks the configuration to ensure forks are scheduled in order, +// and required settings are present. +func (cfg *Config2) Validate() error { + sanityCheckCanonOrder() + + lastFork := forks.CanonOrder[0] + for _, f := range forks.CanonOrder[1:] { + act := "timestamp" + if f.BlockBased() { + act = "block" + } + + switch { + // Non-optional forks must all be present in the chain config up to the last defined fork. + case !cfg.Scheduled(lastFork) && cfg.Scheduled(f): + return fmt.Errorf("unsupported fork ordering: %v not enabled, but %v enabled at %s %v", lastFork, f, act, cfg.activation[f]) + + // Fork (whether defined by block or timestamp) must follow the fork definition sequence. + case cfg.Scheduled(lastFork) && cfg.Scheduled(f): + // Timestamp based forks can follow block based ones, but not the other way around. + if !lastFork.BlockBased() && f.BlockBased() { + return fmt.Errorf("unsupported fork ordering: %v used timestamp ordering, but %v reverted to block ordering", lastFork, f) + } + if lastFork.BlockBased() == f.BlockBased() && cfg.activation[lastFork] > cfg.activation[f] { + return fmt.Errorf("unsupported fork ordering: %v enabled at %s %v, but %v enabled at %s %v", lastFork, act, cfg.activation[lastFork], f, act, cfg.activation[f]) + } + } + + // If it was optional and not set, then ignore it. + if !f.Optional() || cfg.Scheduled(f) { + lastFork = f + } + + // Check that all forks with blobs explicitly define the blob schedule configuration. + if f.HasBlobs() { + schedule := Get[BlobSchedule](cfg) + bcfg, defined := schedule[f] + if cfg.Scheduled(f) && !defined { + return fmt.Errorf("invalid chain configuration: missing entry for fork %q in blobSchedule", f) + } + if defined { + if err := bcfg.validate(); err != nil { + return fmt.Errorf("invalid chain configuration in blobSchedule for fork %q: %v", f, err) + } + } + } + } + return nil +} + +// CheckCompatible validates chain configuration changes. +// This called before applying changes to the 'stored configuration', the config +// which is held in the database. The given block number and time represent the current head +// of the chain in that database. +// +// An error is returned when the new configuration attempts to schedule a fork below the +// current chain head. The error contains enough information to rewind the chain to a +// point where the new config can be applied safely. +func (c *Config2) CheckCompatible(newcfg *Config2, blocknum uint64, time uint64) *ConfigCompatError { + sanityCheckCanonOrder() + + // Iterate checkCompatible to find the lowest conflict. + var lasterr *ConfigCompatError + bhead, btime := blocknum, time + for { + err := c.checkCompatible(newcfg, bhead, btime) + if err == nil || (lasterr != nil && err.RewindToBlock == lasterr.RewindToBlock && err.RewindToTime == lasterr.RewindToTime) { + break + } + lasterr = err + + if err.RewindToTime > 0 { + btime = err.RewindToTime + } else { + bhead = err.RewindToBlock + } + } + return lasterr +} + +// checkCompatible checks config compatibility at a specific block height. +func (cfg *Config2) checkCompatible(newcfg *Config2, num uint64, time uint64) *ConfigCompatError { + incompatible := func(f forks.Fork) bool { + return (cfg.Active(f, num, time) || newcfg.Active(f, num, time)) && !activationEqual(f, cfg, newcfg) + } + + for _, f := range forks.CanonOrder[1:] { + if incompatible(f) { + if f.BlockBased() { + return newBlockCompatError2(fmt.Sprintf("%v fork block", f), f, cfg, newcfg) + } + return newTimestampCompatError2("%v fork timestamp", f, cfg, newcfg) + } + // Specialty checks: + if cfg.Active(forks.DAO, num, time) && Get[DAOForkSupport](cfg) != Get[DAOForkSupport](newcfg) { + return newBlockCompatError2("DAO fork support flag", f, cfg, newcfg) + } + chainID := (*big.Int)(Get[*ChainID](cfg)) + newChainID := (*big.Int)(Get[*ChainID](newcfg)) + if cfg.Active(forks.TangerineWhistle, num, time) && !configBlockEqual(chainID, newChainID) { + return newBlockCompatError2("EIP158 chain ID", f, cfg, newcfg) + } + } + return nil +} + +func newBlockCompatError2(what string, f forks.Fork, storedcfg, newcfg *Config2) *ConfigCompatError { + err := &ConfigCompatError{What: what} + if storedcfg.Scheduled(f) { + err.StoredBlock = new(big.Int).SetUint64(storedcfg.activation[f]) + } + if newcfg.Scheduled(f) { + err.NewBlock = new(big.Int).SetUint64(newcfg.activation[f]) + } + // Need to rewind to one block before the earliest possible activation. + rew, _ := minActivation(f, storedcfg, newcfg) + if rew > 0 { + err.RewindToBlock = rew - 1 + } + return err +} + +func newTimestampCompatError2(what string, f forks.Fork, storedcfg, newcfg *Config2) *ConfigCompatError { + err := &ConfigCompatError{What: what} + if storedcfg.Scheduled(f) { + t := storedcfg.activation[f] + err.StoredTime = &t + } + if newcfg.Scheduled(f) { + t := newcfg.activation[f] + err.NewTime = &t + } + // Need to rewind to before the earliest possible activation. + rew, _ := minActivation(f, storedcfg, newcfg) + if rew > 0 { + err.RewindToTime = rew - 1 + } + return err +} + +// minActivation the earliest possible activation block/time for the given fork +// across two configurations. +func minActivation(f forks.Fork, cfg1, cfg2 *Config2) (act uint64, scheduled bool) { + switch { + case !cfg1.Scheduled(f): + act, scheduled = cfg2.activation[f] + case !cfg2.Scheduled(f): + act, scheduled = cfg1.activation[f] + default: + act = min(cfg1.activation[f], cfg2.activation[f]) + scheduled = true + } + return +} + +// activationEqual checks whether a fork has the same activation two configurations. +// Note this also returns true when it isn't scheduled at all. +func activationEqual(f forks.Fork, cfg1, cfg2 *Config2) bool { + return cfg1.Scheduled(f) == cfg2.Scheduled(f) && cfg1.activation[f] == cfg2.activation[f] +} + +// sanityCheckCanonOrder verifies forks.CanonOrder is defined sensibly. +// This exists to ensure library code doesn't mess with this slice in an incompatible way. +func sanityCheckCanonOrder() { + if len(forks.CanonOrder) == 0 { + panic("forks.CanonOrder is empty") + } + if forks.CanonOrder[0] != forks.Frontier { + panic("forks.CanonOrder must start with Frontier") + } +} + +func cloneBig(x *big.Int) *big.Int { + if x == nil { + return nil + } + return new(big.Int).Set(x) +} diff --git a/params/config2_presets.go b/params/config2_presets.go new file mode 100644 index 0000000000..557cb5d0ac --- /dev/null +++ b/params/config2_presets.go @@ -0,0 +1,115 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package params + +import ( + "math" + "math/big" + + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/params/forks" +) + +var ( + MainnetConfig2 *Config2 + AllEthashProtocolChanges2 *Config2 + MergedTestChainConfig2 *Config2 +) + +func init() { + MainnetConfig2 = NewConfig2( + Activations{ + forks.Homestead: 1_150_000, + forks.DAO: 1_920_000, + forks.TangerineWhistle: 2_463_000, + forks.SpuriousDragon: 2_675_000, + forks.Byzantium: 4_370_000, + forks.Constantinople: 7_280_000, + forks.Petersburg: 7_280_000, + forks.Istanbul: 9_069_000, + forks.MuirGlacier: 9_200_000, + forks.Berlin: 12_244_000, + forks.London: 12_965_000, + forks.ArrowGlacier: 13_773_000, + forks.GrayGlacier: 15_050_000, + // time-based forks + forks.Shanghai: 1681338455, + forks.Cancun: 1710338135, + forks.Prague: 1746612311, + }, + (*ChainID)(big.NewInt(1)), + (*TerminalTotalDifficulty)(MainnetTerminalTotalDifficulty), + DepositContractAddress(common.HexToAddress("0x00000000219ab540356cbb839cbe05303d7705fa")), + DAOForkSupport(true), + BlobSchedule{ + forks.Cancun: *DefaultCancunBlobConfig, + forks.Prague: *DefaultPragueBlobConfig, + }, + ) + + // AllEthashProtocolChanges2 contains every protocol change (EIPs) introduced + // and accepted by the Ethereum core developers into the Ethash consensus. + AllEthashProtocolChanges2 = NewConfig2( + Activations{ + forks.Homestead: 0, + forks.TangerineWhistle: 0, + forks.SpuriousDragon: 0, + forks.Byzantium: 0, + forks.Constantinople: 0, + forks.Petersburg: 0, + forks.Istanbul: 0, + forks.MuirGlacier: 0, + forks.Berlin: 0, + forks.London: 0, + forks.ArrowGlacier: 0, + forks.GrayGlacier: 0, + }, + (*ChainID)(big.NewInt(1337)), + (*TerminalTotalDifficulty)(big.NewInt(math.MaxInt64)), + ) + + // MergedTestChainConfig2 contains every protocol change (EIPs) introduced + // and accepted by the Ethereum core developers for testing purposes. + MergedTestChainConfig2 = NewConfig2( + Activations{ + forks.Homestead: 0, + forks.TangerineWhistle: 0, + forks.SpuriousDragon: 0, + forks.Byzantium: 0, + forks.Constantinople: 0, + forks.Petersburg: 0, + forks.Istanbul: 0, + forks.MuirGlacier: 0, + forks.Berlin: 0, + forks.London: 0, + forks.ArrowGlacier: 0, + forks.GrayGlacier: 0, + forks.Paris: 0, + forks.Shanghai: 0, + forks.Cancun: 0, + forks.Prague: 0, + forks.Osaka: 0, + }, + (*ChainID)(big.NewInt(1)), + (*TerminalTotalDifficulty)(big.NewInt(0)), + BlobSchedule{ + forks.Cancun: *DefaultCancunBlobConfig, + forks.Prague: *DefaultPragueBlobConfig, + forks.Osaka: *DefaultOsakaBlobConfig, + }, + ) +} diff --git a/params/forks/forks.go b/params/forks/forks.go index 5c9612a625..9d6a42f68a 100644 --- a/params/forks/forks.go +++ b/params/forks/forks.go @@ -16,60 +16,132 @@ package forks +import "fmt" + // Fork is a numerical identifier of specific network upgrades (forks). -type Fork int +type Fork uint32 const ( - Frontier Fork = iota - FrontierThawing - Homestead - DAO - TangerineWhistle // a.k.a. the EIP150 fork - SpuriousDragon // a.k.a. the EIP155 fork - Byzantium - Constantinople - Petersburg - Istanbul - MuirGlacier - Berlin - London - ArrowGlacier - GrayGlacier - Paris - Shanghai - Cancun - Prague - Osaka + Frontier Fork = iota | blockBased + FrontierThawing Fork = iota | blockBased | optional + Homestead Fork = iota | blockBased + DAO Fork = iota | blockBased | optional + TangerineWhistle Fork = iota | blockBased // a.k.a. the EIP150 fork + SpuriousDragon Fork = iota | blockBased // a.k.a. the EIP155/EIP158 fork + Byzantium Fork = iota | blockBased + Constantinople Fork = iota | blockBased + Petersburg Fork = iota | blockBased + Istanbul Fork = iota | blockBased + MuirGlacier Fork = iota | blockBased | optional + Berlin Fork = iota | blockBased + London Fork = iota | blockBased + ArrowGlacier Fork = iota | blockBased | optional + GrayGlacier Fork = iota | blockBased | optional + Paris Fork = iota | blockBased | ismerge + Shanghai Fork = iota + Cancun Fork = iota | hasBlobs + Prague Fork = iota | hasBlobs + Osaka Fork = iota | hasBlobs + Verkle Fork = iota | optional ) +var CanonOrder = []Fork{ + Frontier, + FrontierThawing, + Homestead, + DAO, + TangerineWhistle, + SpuriousDragon, + Byzantium, + Constantinople, + Petersburg, + Istanbul, + MuirGlacier, + Berlin, + London, + ArrowGlacier, + GrayGlacier, + Paris, + Shanghai, + Cancun, + Prague, + Osaka, + Verkle, +} + +const ( + // Config bits: these bits are set on specific fork enum values and encode metadata + // about the fork. + blockBased = 1 << 31 + ismerge = 1 << 30 + optional = 1 << 29 + hasBlobs = 1 << 28 + + // The config bits can be stripped using this bit mask. + unconfigMask = ^Fork(0) >> 8 +) + +// IsMerge returns true for the merge fork. +func (f Fork) IsMerge() bool { + return f&ismerge != 0 +} + +// Optional reports whether the fork can be left out of the config. +func (f Fork) Optional() bool { + return f&optional != 0 +} + +// BlockBased reports whether the fork is scheduled by block number instead of timestamp. +// This is true for pre-merge forks. +func (f Fork) BlockBased() bool { + return f&blockBased != 0 +} + +// HasBlobs reports whether the fork must have a corresponding blob count configuration. +func (f Fork) HasBlobs() bool { + return f&hasBlobs != 0 +} + // String implements fmt.Stringer. func (f Fork) String() string { s, ok := forkToString[f] if !ok { - return "Unknown fork" + return fmt.Sprintf("unknownFork(%#x)", f) } return s } var forkToString = map[Fork]string{ Frontier: "Frontier", - FrontierThawing: "Frontier Thawing", Homestead: "Homestead", - DAO: "DAO", - TangerineWhistle: "Tangerine Whistle", - SpuriousDragon: "Spurious Dragon", + DAO: "DAOFork", + TangerineWhistle: "TangerineWhistle", + SpuriousDragon: "SpuriousDragon", Byzantium: "Byzantium", Constantinople: "Constantinople", Petersburg: "Petersburg", Istanbul: "Istanbul", - MuirGlacier: "Muir Glacier", + MuirGlacier: "MuirGlacier", Berlin: "Berlin", London: "London", - ArrowGlacier: "Arrow Glacier", - GrayGlacier: "Gray Glacier", + ArrowGlacier: "ArrowGlacier", + GrayGlacier: "GrayGlacier", Paris: "Paris", Shanghai: "Shanghai", Cancun: "Cancun", Prague: "Prague", Osaka: "Osaka", } + +var forkFromString = make(map[string]Fork, len(forkToString)) + +func init() { + for f, name := range forkToString { + forkFromString[name] = f + } +} + +func ByName(name string) (Fork, bool) { + f, ok := forkFromString[name] + return f, ok +} diff --git a/params/registry.go b/params/registry.go new file mode 100644 index 0000000000..aed4888b8b --- /dev/null +++ b/params/registry.go @@ -0,0 +1,85 @@ +// Copyright 2025 The go-ethereum Authors +// This file is part of the go-ethereum library. +// +// The go-ethereum library is free software: you can redistribute it and/or modify +// it under the terms of the GNU Lesser General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// The go-ethereum library is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +// GNU Lesser General Public License for more details. +// +// You should have received a copy of the GNU Lesser General Public License +// along with the go-ethereum library. If not, see . + +package params + +import ( + "fmt" + "reflect" +) + +type ParameterType any + +type paramInfo struct { + rtype reflect.Type + name string + optional bool + defaultVal any +} + +var ( + paramRegistry = map[reflect.Type]*paramInfo{} + paramRegistryByName = map[string]*paramInfo{} +) + +type Parameter[T ParameterType] struct { + Name string + Optional bool + Default T +} + +// Get retrieves the value of a chain parameter. +func Get[T ParameterType](cfg *Config2) T { + for _, p := range cfg.param { + if v, ok := p.(T); ok { + return v + } + } + // get default + var z T + info, ok := findParam(z) + if !ok { + panic(fmt.Sprintf("unknown parameter type %T", z)) + } + return info.defaultVal.(T) +} + +func Define[T ParameterType](def Parameter[T]) { + var z T + info, defined := paramRegistryByName[def.Name] + if defined { + panic(fmt.Sprintf("chain parameter %q already registered with type %v", info.name, info.rtype)) + } + rtype := reflect.TypeOf(z) + info, defined = paramRegistry[rtype] + if defined { + panic(fmt.Sprintf("chain parameter of type %v already registered with name %q", rtype, info.name)) + } + info = ¶mInfo{ + rtype: rtype, + name: def.Name, + optional: def.Optional, + defaultVal: def.Default, + } + paramRegistry[rtype] = info + paramRegistryByName[def.Name] = info +} + +func findParam(v any) (*paramInfo, bool) { + rtype := reflect.TypeOf(v) + info, ok := paramRegistry[rtype] + return info, ok +}