feat(config): move Fevm.Events->Events, implement soft deprecation

Closes: https://github.com/filecoin-project/lotus/issues/11679

* Introduce a `moved:"To.New.Config"` tag which prints a stderr warning when
  you use one of these, but will move any set value to the new location if the
	new location isn't already set itself.
* Look for `X is DEPRECATED` to hold certain fields back from documentation.
* Use `toml:"omitempty"` to prevent the default config output from having these
  deprecated values.
This commit is contained in:
Rod Vagg
2024-03-22 07:00:28 +01:00
committed by Phi-rjan
parent 82a45185dd
commit d35ebe5bc0
11 changed files with 343 additions and 118 deletions
+3 -3
View File
@@ -266,13 +266,13 @@ func ConfigFullNode(c interface{}) Option {
// Actor event filtering support
Override(new(events.EventHelperAPI), From(new(modules.EventHelperAPI))),
Override(new(*filter.EventFilterManager), modules.EventFilterManager(cfg.Fevm)),
Override(new(*filter.EventFilterManager), modules.EventFilterManager(cfg.Events)),
// in lite-mode Eth api is provided by gateway
ApplyIf(isFullNode,
If(cfg.Fevm.EnableEthRPC,
Override(new(full.EthModuleAPI), modules.EthModuleAPI(cfg.Fevm)),
Override(new(full.EthEventAPI), modules.EthEventHandler(cfg.Fevm)),
Override(new(full.EthEventAPI), modules.EthEventHandler(cfg.Events, cfg.Fevm.EnableEthRPC)),
),
If(!cfg.Fevm.EnableEthRPC,
Override(new(full.EthModuleAPI), &full.EthModuleDummy{}),
@@ -282,7 +282,7 @@ func ConfigFullNode(c interface{}) Option {
ApplyIf(isFullNode,
If(cfg.Events.EnableActorEventsAPI,
Override(new(full.ActorEventAPI), modules.ActorEventHandler(cfg.Events.EnableActorEventsAPI, cfg.Fevm)),
Override(new(full.ActorEventAPI), modules.ActorEventHandler(cfg.Events)),
),
If(!cfg.Events.EnableActorEventsAPI,
Override(new(full.ActorEventAPI), &full.ActorEventDummy{}),
+5
View File
@@ -74,6 +74,11 @@ func run() error {
name := f[0]
typ := f[1]
if len(comment) > 0 && strings.HasPrefix(comment[0], fmt.Sprintf("%s is DEPRECATED", name)) {
// don't document deprecated fields
continue
}
out[currentType] = append(out[currentType], field{
Name: name,
Type: typ,
+7 -9
View File
@@ -110,17 +110,15 @@ func DefaultFullNode() *FullNode {
Fevm: FevmConfig{
EnableEthRPC: false,
EthTxHashMappingLifetimeDays: 0,
Events: Events{
DisableRealTimeFilterAPI: false,
DisableHistoricFilterAPI: false,
FilterTTL: Duration(time.Hour * 24),
MaxFilters: 100,
MaxFilterResults: 10000,
MaxFilterHeightRange: 2880, // conservative limit of one day
},
},
Events: EventsConfig{
EnableActorEventsAPI: false,
DisableRealTimeFilterAPI: false,
DisableHistoricFilterAPI: false,
EnableActorEventsAPI: false,
FilterTTL: Duration(time.Hour * 24),
MaxFilters: 100,
MaxFilterResults: 10000,
MaxFilterHeightRange: 2880, // conservative limit of one day
},
}
}
+13 -15
View File
@@ -357,13 +357,13 @@ see https://lotus.filecoin.io/storage-providers/advanced-configurations/market/#
Comment: ``,
},
},
"Events": {
"EventsConfig": {
{
Name: "DisableRealTimeFilterAPI",
Type: "bool",
Comment: `DisableRealTimeFilterAPI will disable the RealTimeFilterAPI that can create and query filters for actor events as they are emitted.
The API is enabled when EnableEthRPC or Events.EnableActorEventsAPI is true, but can be disabled selectively with this flag.`,
The API is enabled when Fevm.EnableEthRPC or EnableActorEventsAPI is true, but can be disabled selectively with this flag.`,
},
{
Name: "DisableHistoricFilterAPI",
@@ -371,7 +371,16 @@ The API is enabled when EnableEthRPC or Events.EnableActorEventsAPI is true, but
Comment: `DisableHistoricFilterAPI will disable the HistoricFilterAPI that can create and query filters for actor events
that occurred in the past. HistoricFilterAPI maintains a queryable index of events.
The API is enabled when EnableEthRPC or Events.EnableActorEventsAPI is true, but can be disabled selectively with this flag.`,
The API is enabled when Fevm.EnableEthRPC or EnableActorEventsAPI is true, but can be disabled selectively with this flag.`,
},
{
Name: "EnableActorEventsAPI",
Type: "bool",
Comment: `EnableActorEventsAPI enables the Actor events API that enables clients to consume events
emitted by (smart contracts + built-in Actors).
This will also enable the RealTimeFilterAPI and HistoricFilterAPI by default, but they can be
disabled by setting their respective Disable* options.`,
},
{
Name: "FilterTTL",
@@ -409,17 +418,6 @@ the database must already exist and be writeable. If a relative path is provided
relative to the CWD (current working directory).`,
},
},
"EventsConfig": {
{
Name: "EnableActorEventsAPI",
Type: "bool",
Comment: `EnableActorEventsAPI enables the Actor events API that enables clients to consume events
emitted by (smart contracts + built-in Actors).
This will also enable the RealTimeFilterAPI and HistoricFilterAPI by default, but they can be
disabled by setting their respective Disable* options in Fevm.Events.`,
},
},
"FaultReporterConfig": {
{
Name: "EnableConsensusFaultReporter",
@@ -474,7 +472,7 @@ Set to 0 to keep all mappings`,
},
{
Name: "Events",
Type: "Events",
Type: "DeprecatedEvents",
Comment: ``,
},
+132 -10
View File
@@ -18,12 +18,9 @@ import (
// FromFile loads config from a specified file overriding defaults specified in
// the def parameter. If file does not exist or is empty defaults are assumed.
func FromFile(path string, opts ...LoadCfgOpt) (interface{}, error) {
var loadOpts cfgLoadOpts
var err error
for _, opt := range opts {
if err = opt(&loadOpts); err != nil {
return nil, xerrors.Errorf("failed to apply load cfg option: %w", err)
}
loadOpts, err := applyOpts(opts...)
if err != nil {
return nil, err
}
var def interface{}
if loadOpts.defaultCfg != nil {
@@ -56,16 +53,43 @@ func FromFile(path string, opts ...LoadCfgOpt) (interface{}, error) {
return nil, xerrors.Errorf("config failed validation: %w", err)
}
}
return FromReader(buf, def)
return FromReader(buf, def, opts...)
}
// FromReader loads config from a reader instance.
func FromReader(reader io.Reader, def interface{}) (interface{}, error) {
cfg := def
_, err := toml.NewDecoder(reader).Decode(cfg)
func FromReader(reader io.Reader, def interface{}, opts ...LoadCfgOpt) (interface{}, error) {
loadOpts, err := applyOpts(opts...)
if err != nil {
return nil, err
}
cfg := def
md, err := toml.NewDecoder(reader).Decode(cfg)
if err != nil {
return nil, err
}
// find any fields with a tag: `moved:"New.Config.Location"` and move any set values there over to
// the new location if they are not already set there.
movedFields := findMovedFields(nil, cfg)
var warningOut io.Writer = os.Stderr
if loadOpts.warningWriter != nil {
warningOut = loadOpts.warningWriter
}
for _, d := range movedFields {
if md.IsDefined(d.Field...) {
fmt.Fprintf(
warningOut,
"WARNING: Use of deprecated configuration option '%s' will be removed in a future release, use '%s' instead\n",
strings.Join(d.Field, "."),
strings.Join(d.NewField, "."))
if !md.IsDefined(d.NewField...) {
// new value isn't set but old is, we should move what the user set there
if err := moveFieldValue(cfg, d.Field, d.NewField); err != nil {
return nil, fmt.Errorf("failed to move field value: %w", err)
}
}
}
}
err = envconfig.Process("LOTUS", cfg)
if err != nil {
@@ -75,14 +99,105 @@ func FromReader(reader io.Reader, def interface{}) (interface{}, error) {
return cfg, nil
}
// move a value from the location in the valPtr struct specified by oldPath, to the location
// specified by newPath; where the path is an array of nested field names.
func moveFieldValue(valPtr interface{}, oldPath []string, newPath []string) error {
oldValue, err := getFieldValue(valPtr, oldPath)
if err != nil {
return err
}
val := reflect.ValueOf(valPtr).Elem()
for {
field := val.FieldByName(newPath[0])
if !field.IsValid() {
return fmt.Errorf("unexpected error fetching field value")
}
if len(newPath) == 1 {
if field.Kind() != oldValue.Kind() {
return fmt.Errorf("unexpected error, old kind != new kind")
}
// set field on val to be the new one, and we're done
field.Set(oldValue)
return nil
}
if field.Kind() != reflect.Struct {
return fmt.Errorf("unexpected error fetching field value, is not a struct")
}
newPath = newPath[1:]
val = field
}
}
// recursively iterate into `path` to find the terminal value
func getFieldValue(val interface{}, path []string) (reflect.Value, error) {
if reflect.ValueOf(val).Kind() == reflect.Ptr {
val = reflect.ValueOf(val).Elem().Interface()
}
field := reflect.ValueOf(val).FieldByName(path[0])
if !field.IsValid() {
return reflect.Value{}, fmt.Errorf("unexpected error fetching field value")
}
if len(path) > 1 {
if field.Kind() != reflect.Struct {
return reflect.Value{}, fmt.Errorf("unexpected error fetching field value, is not a struct")
}
return getFieldValue(field.Interface(), path[1:])
}
return field, nil
}
type movedField struct {
Field []string
NewField []string
}
// inspect the fields recursively within a struct and find any with "moved" tags
func findMovedFields(path []string, val interface{}) []movedField {
dep := make([]movedField, 0)
if reflect.ValueOf(val).Kind() == reflect.Ptr {
val = reflect.ValueOf(val).Elem().Interface()
}
t := reflect.TypeOf(val)
if t.Kind() != reflect.Struct {
return nil
}
for i := 0; i < t.NumField(); i++ {
field := t.Field(i)
// could also do a "deprecated" in here
if idx := field.Tag.Get("moved"); idx != "" && idx != "-" {
dep = append(dep, movedField{
Field: append(path, field.Name),
NewField: strings.Split(idx, "."),
})
}
if field.Type.Kind() == reflect.Struct && reflect.ValueOf(val).FieldByName(field.Name).IsValid() {
deps := findMovedFields(append(path, field.Name), reflect.ValueOf(val).FieldByName(field.Name).Interface())
dep = append(dep, deps...)
}
}
return dep
}
type cfgLoadOpts struct {
defaultCfg func() (interface{}, error)
canFallbackOnDefault func() error
validate func(string) error
warningWriter io.Writer
}
type LoadCfgOpt func(opts *cfgLoadOpts) error
func applyOpts(opts ...LoadCfgOpt) (cfgLoadOpts, error) {
var loadOpts cfgLoadOpts
var err error
for _, opt := range opts {
if err = opt(&loadOpts); err != nil {
return loadOpts, fmt.Errorf("failed to apply load cfg option: %w", err)
}
}
return loadOpts, nil
}
func SetDefault(f func() (interface{}, error)) LoadCfgOpt {
return func(opts *cfgLoadOpts) error {
opts.defaultCfg = f
@@ -104,6 +219,13 @@ func SetValidate(f func(string) error) LoadCfgOpt {
}
}
func SetWarningWriter(w io.Writer) LoadCfgOpt {
return func(opts *cfgLoadOpts) error {
opts.warningWriter = w
return nil
}
}
func NoDefaultForSplitstoreTransition() error {
return xerrors.Errorf("FullNode config not found and fallback to default disallowed while we transition to splitstore discard default. Use `lotus config default` to set this repo up with a default config. Be sure to set `EnableSplitstore` to `false` if you are running a full archive node")
}
+75
View File
@@ -8,6 +8,7 @@ import (
"time"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
func fullNodeDefault() (interface{}, error) { return DefaultFullNode(), nil }
@@ -138,3 +139,77 @@ func TestFailToFallbackToDefault(t *testing.T) {
_, err = FromFile(nonExistantFileName, SetDefault(fullNodeDefault), SetCanFallbackOnDefault(NoDefaultForSplitstoreTransition))
assert.Error(t, err)
}
func TestPrintDeprecated(t *testing.T) {
type ChildCfg struct {
Field string `moved:"Bang"`
NewField string
}
type Old struct {
Thing1 int `moved:"New.Thing1"`
Thing2 int `moved:"New.Thing2"`
}
type New struct {
Thing1 int
Thing2 int
}
type ParentCfg struct {
Child ChildCfg
Old Old
New New
Foo int
Baz string `moved:"Child.NewField"`
Boom int `moved:"Foo"`
Bang string
}
t.Run("warning output", func(t *testing.T) {
cfg := `
Baz = "baz"
Foo = 100
[Child]
Field = "bip"
NewField = "bop"
`
warningWriter := bytes.NewBuffer(nil)
v, err := FromReader(bytes.NewReader([]byte(cfg)), &ParentCfg{Boom: 200, Bang: "300"}, SetWarningWriter(warningWriter))
require.NoError(t, err)
require.Equal(t, &ParentCfg{
Child: ChildCfg{
Field: "bip",
NewField: "bop",
},
Baz: "baz",
Foo: 100,
Boom: 200,
Bang: "bip",
}, v)
require.Regexp(t, `\WChild\.Field\W.+use 'Bang' instead`, warningWriter.String())
require.Regexp(t, `\WBaz\W.+use 'Child\.NewField' instead`, warningWriter.String())
require.NotContains(t, warningWriter.String(), "don't use this at all")
require.NotContains(t, warningWriter.String(), "Boom")
})
defaultNew := New{Thing1: 42, Thing2: 800}
testCases := []struct {
name string
cfg string
expected New
}{
{"simple", ``, defaultNew},
{"set new", "[New]\nThing1 = 101\nThing2 = 102\n", New{Thing1: 101, Thing2: 102}},
// should move old to new fields if new isn't set
{"set old", "[Old]\nThing1 = 101\nThing2 = 102\n", New{Thing1: 101, Thing2: 102}},
}
for _, tc := range testCases {
tc := tc
t.Run(tc.name, func(t *testing.T) {
v, err := FromReader(bytes.NewReader([]byte(tc.cfg)), &ParentCfg{New: defaultNew})
require.NoError(t, err)
require.Equal(t, tc.expected, v.(*ParentCfg).New)
})
}
}
+33 -12
View File
@@ -791,19 +791,48 @@ type FevmConfig struct {
// Set to 0 to keep all mappings
EthTxHashMappingLifetimeDays int
Events Events
Events DeprecatedEvents `toml:"Events,omitempty"`
}
type Events struct {
type DeprecatedEvents struct {
// DisableRealTimeFilterAPI is DEPRECATED and will be removed in a future release. Use Events.DisableRealTimeFilterAPI instead.
DisableRealTimeFilterAPI bool `moved:"Events.DisableRealTimeFilterAPI" toml:"DisableRealTimeFilterAPI,omitempty"`
// DisableHistoricFilterAPI is DEPRECATED and will be removed in a future release. Use Events.DisableHistoricFilterAPI instead.
DisableHistoricFilterAPI bool `moved:"Events.DisableHistoricFilterAPI" toml:"DisableHistoricFilterAPI,omitempty"`
// FilterTTL is DEPRECATED and will be removed in a future release. Use Events.FilterTTL instead.
FilterTTL Duration `moved:"Events.FilterTTL" toml:"FilterTTL,omitzero"`
// MaxFilters is DEPRECATED and will be removed in a future release. Use Events.MaxFilters instead.
MaxFilters int `moved:"Events.MaxFilters" toml:"MaxFilters,omitzero"`
// MaxFilterResults is DEPRECATED and will be removed in a future release. Use Events.MaxFilterResults instead.
MaxFilterResults int `moved:"Events.MaxFilterResults" toml:"MaxFilterResults,omitzero"`
// MaxFilterHeightRange is DEPRECATED and will be removed in a future release. Use Events.MaxFilterHeightRange instead.
MaxFilterHeightRange uint64 `moved:"Events.MaxFilterHeightRange" toml:"MaxFilterHeightRange,omitzero"`
// DatabasePath is DEPRECATED and will be removed in a future release. Use Events.DatabasePath instead.
DatabasePath string `moved:"Events.DatabasePath" toml:"DatabasePath,omitempty"`
}
type EventsConfig struct {
// DisableRealTimeFilterAPI will disable the RealTimeFilterAPI that can create and query filters for actor events as they are emitted.
// The API is enabled when EnableEthRPC or Events.EnableActorEventsAPI is true, but can be disabled selectively with this flag.
// The API is enabled when Fevm.EnableEthRPC or EnableActorEventsAPI is true, but can be disabled selectively with this flag.
DisableRealTimeFilterAPI bool
// DisableHistoricFilterAPI will disable the HistoricFilterAPI that can create and query filters for actor events
// that occurred in the past. HistoricFilterAPI maintains a queryable index of events.
// The API is enabled when EnableEthRPC or Events.EnableActorEventsAPI is true, but can be disabled selectively with this flag.
// The API is enabled when Fevm.EnableEthRPC or EnableActorEventsAPI is true, but can be disabled selectively with this flag.
DisableHistoricFilterAPI bool
// EnableActorEventsAPI enables the Actor events API that enables clients to consume events
// emitted by (smart contracts + built-in Actors).
// This will also enable the RealTimeFilterAPI and HistoricFilterAPI by default, but they can be
// disabled by setting their respective Disable* options.
EnableActorEventsAPI bool
// FilterTTL specifies the time to live for actor event filters. Filters that haven't been accessed longer than
// this time become eligible for automatic deletion.
FilterTTL Duration
@@ -830,14 +859,6 @@ type Events struct {
// Set upper bound on index size
}
type EventsConfig struct {
// EnableActorEventsAPI enables the Actor events API that enables clients to consume events
// emitted by (smart contracts + built-in Actors).
// This will also enable the RealTimeFilterAPI and HistoricFilterAPI by default, but they can be
// disabled by setting their respective Disable* options in Fevm.Events.
EnableActorEventsAPI bool
}
type IndexConfig struct {
// EXPERIMENTAL FEATURE. USE WITH CAUTION
// EnableMsgIndex enables indexing of messages on chain.
+15 -15
View File
@@ -32,17 +32,17 @@ type EventHelperAPI struct {
var _ events.EventHelperAPI = &EventHelperAPI{}
func EthEventHandler(cfg config.FevmConfig) func(helpers.MetricsCtx, repo.LockedRepo, fx.Lifecycle, *filter.EventFilterManager, *store.ChainStore, *stmgr.StateManager, EventHelperAPI, *messagepool.MessagePool, full.StateAPI, full.ChainAPI) (*full.EthEventHandler, error) {
func EthEventHandler(cfg config.EventsConfig, enableEthRPC bool) func(helpers.MetricsCtx, repo.LockedRepo, fx.Lifecycle, *filter.EventFilterManager, *store.ChainStore, *stmgr.StateManager, EventHelperAPI, *messagepool.MessagePool, full.StateAPI, full.ChainAPI) (*full.EthEventHandler, error) {
return func(mctx helpers.MetricsCtx, r repo.LockedRepo, lc fx.Lifecycle, fm *filter.EventFilterManager, cs *store.ChainStore, sm *stmgr.StateManager, evapi EventHelperAPI, mp *messagepool.MessagePool, stateapi full.StateAPI, chainapi full.ChainAPI) (*full.EthEventHandler, error) {
ctx := helpers.LifecycleCtx(mctx, lc)
ee := &full.EthEventHandler{
Chain: cs,
MaxFilterHeightRange: abi.ChainEpoch(cfg.Events.MaxFilterHeightRange),
MaxFilterHeightRange: abi.ChainEpoch(cfg.MaxFilterHeightRange),
SubscribtionCtx: ctx,
}
if !cfg.EnableEthRPC || cfg.Events.DisableRealTimeFilterAPI {
if !enableEthRPC || cfg.DisableRealTimeFilterAPI {
// all event functionality is disabled
// the historic filter API relies on the real time one
return ee, nil
@@ -53,21 +53,21 @@ func EthEventHandler(cfg config.FevmConfig) func(helpers.MetricsCtx, repo.Locked
StateAPI: stateapi,
ChainAPI: chainapi,
}
ee.FilterStore = filter.NewMemFilterStore(cfg.Events.MaxFilters)
ee.FilterStore = filter.NewMemFilterStore(cfg.MaxFilters)
// Start garbage collection for filters
lc.Append(fx.Hook{
OnStart: func(context.Context) error {
go ee.GC(ctx, time.Duration(cfg.Events.FilterTTL))
go ee.GC(ctx, time.Duration(cfg.FilterTTL))
return nil
},
})
ee.TipSetFilterManager = &filter.TipSetFilterManager{
MaxFilterResults: cfg.Events.MaxFilterResults,
MaxFilterResults: cfg.MaxFilterResults,
}
ee.MemPoolFilterManager = &filter.MemPoolFilterManager{
MaxFilterResults: cfg.Events.MaxFilterResults,
MaxFilterResults: cfg.MaxFilterResults,
}
ee.EventFilterManager = fm
@@ -94,22 +94,22 @@ func EthEventHandler(cfg config.FevmConfig) func(helpers.MetricsCtx, repo.Locked
}
}
func EventFilterManager(cfg config.FevmConfig) func(helpers.MetricsCtx, repo.LockedRepo, fx.Lifecycle, *store.ChainStore, *stmgr.StateManager, EventHelperAPI, full.ChainAPI) (*filter.EventFilterManager, error) {
func EventFilterManager(cfg config.EventsConfig) func(helpers.MetricsCtx, repo.LockedRepo, fx.Lifecycle, *store.ChainStore, *stmgr.StateManager, EventHelperAPI, full.ChainAPI) (*filter.EventFilterManager, error) {
return func(mctx helpers.MetricsCtx, r repo.LockedRepo, lc fx.Lifecycle, cs *store.ChainStore, sm *stmgr.StateManager, evapi EventHelperAPI, chainapi full.ChainAPI) (*filter.EventFilterManager, error) {
ctx := helpers.LifecycleCtx(mctx, lc)
// Enable indexing of actor events
var eventIndex *filter.EventIndex
if !cfg.Events.DisableHistoricFilterAPI {
if !cfg.DisableHistoricFilterAPI {
var dbPath string
if cfg.Events.DatabasePath == "" {
if cfg.DatabasePath == "" {
sqlitePath, err := r.SqlitePath()
if err != nil {
return nil, err
}
dbPath = filepath.Join(sqlitePath, "events.db")
} else {
dbPath = cfg.Events.DatabasePath
dbPath = cfg.DatabasePath
}
var err error
@@ -144,7 +144,7 @@ func EventFilterManager(cfg config.FevmConfig) func(helpers.MetricsCtx, repo.Loc
return *actor.Address, true
},
MaxFilterResults: cfg.Events.MaxFilterResults,
MaxFilterResults: cfg.MaxFilterResults,
}
lc.Append(fx.Hook{
@@ -162,10 +162,10 @@ func EventFilterManager(cfg config.FevmConfig) func(helpers.MetricsCtx, repo.Loc
}
}
func ActorEventHandler(enable bool, fevmCfg config.FevmConfig) func(helpers.MetricsCtx, repo.LockedRepo, fx.Lifecycle, *filter.EventFilterManager, *store.ChainStore, *stmgr.StateManager, EventHelperAPI, *messagepool.MessagePool, full.StateAPI, full.ChainAPI) (*full.ActorEventHandler, error) {
func ActorEventHandler(cfg config.EventsConfig) func(helpers.MetricsCtx, repo.LockedRepo, fx.Lifecycle, *filter.EventFilterManager, *store.ChainStore, *stmgr.StateManager, EventHelperAPI, *messagepool.MessagePool, full.StateAPI, full.ChainAPI) (*full.ActorEventHandler, error) {
return func(mctx helpers.MetricsCtx, r repo.LockedRepo, lc fx.Lifecycle, fm *filter.EventFilterManager, cs *store.ChainStore, sm *stmgr.StateManager, evapi EventHelperAPI, mp *messagepool.MessagePool, stateapi full.StateAPI, chainapi full.ChainAPI) (*full.ActorEventHandler, error) {
if !enable || fevmCfg.Events.DisableRealTimeFilterAPI {
if !cfg.EnableActorEventsAPI || cfg.DisableRealTimeFilterAPI {
fm = nil
}
@@ -173,7 +173,7 @@ func ActorEventHandler(enable bool, fevmCfg config.FevmConfig) func(helpers.Metr
cs,
fm,
time.Duration(build.BlockDelaySecs)*time.Second,
abi.ChainEpoch(fevmCfg.Events.MaxFilterHeightRange),
abi.ChainEpoch(cfg.MaxFilterHeightRange),
), nil
}
}