diff --git a/op-devstack/dsl/interop_filter.go b/op-devstack/dsl/interop_filter.go new file mode 100644 index 00000000000..e1ec058ee51 --- /dev/null +++ b/op-devstack/dsl/interop_filter.go @@ -0,0 +1,36 @@ +package dsl + +import ( + "github.com/ethereum-optimism/optimism/op-devstack/stack" +) + +type InteropFilter struct { + commonImpl + inner stack.InteropFilter +} + +// NewInteropFilter creates a new InteropFilter DSL wrapper +func NewInteropFilter(inner stack.InteropFilter) *InteropFilter { + return &InteropFilter{ + commonImpl: commonFromT(inner.T()), + inner: inner, + } +} + +// Escape returns the underlying stack.InteropFilter +func (f *InteropFilter) Escape() stack.InteropFilter { + return f.inner +} + +// GetFailsafeEnabled returns whether failsafe is enabled +func (f *InteropFilter) GetFailsafeEnabled() bool { + enabled, err := f.inner.AdminAPI().GetFailsafeEnabled(f.ctx) + f.require.NoError(err, "failed to get failsafe enabled") + return enabled +} + +// CheckAccessList validates interop executing messages +func (f *InteropFilter) CheckAccessList(inboxEntries [][]byte) error { + // TODO: implement when needed for tests + return nil +} diff --git a/op-devstack/presets/minimal_with_interop_filter.go b/op-devstack/presets/minimal_with_interop_filter.go new file mode 100644 index 00000000000..cd661b08479 --- /dev/null +++ b/op-devstack/presets/minimal_with_interop_filter.go @@ -0,0 +1,32 @@ +package presets + +import ( + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/dsl" + "github.com/ethereum-optimism/optimism/op-devstack/shim" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-devstack/stack/match" + "github.com/ethereum-optimism/optimism/op-devstack/sysgo" +) + +type MinimalWithInteropFilter struct { + Minimal + + InteropFilter *dsl.InteropFilter +} + +func WithMinimalWithInteropFilter() stack.CommonOption { + return stack.MakeCommon(sysgo.DefaultMinimalSystemWithInteropFilter(&sysgo.DefaultMinimalSystemWithInteropFilterIDs{})) +} + +func NewMinimalWithInteropFilter(t devtest.T) *MinimalWithInteropFilter { + system := shim.NewSystem(t) + orch := Orchestrator() + orch.Hydrate(system) + minimal := minimalFromSystem(t, system, orch) + interopFilter := system.InteropFilter(match.Assume(t, match.FirstInteropFilter)) + return &MinimalWithInteropFilter{ + Minimal: *minimal, + InteropFilter: dsl.NewInteropFilter(interopFilter), + } +} diff --git a/op-devstack/shim/interop_filter.go b/op-devstack/shim/interop_filter.go new file mode 100644 index 00000000000..3d461cf5bcd --- /dev/null +++ b/op-devstack/shim/interop_filter.go @@ -0,0 +1,46 @@ +package shim + +import ( + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-service/sources" +) + +type InteropFilterConfig struct { + CommonConfig + ID stack.InteropFilterID + Client client.RPC +} + +type rpcInteropFilter struct { + commonImpl + id stack.InteropFilterID + + client client.RPC + api apis.InteropFilterAPI +} + +var _ stack.InteropFilter = (*rpcInteropFilter)(nil) + +func NewInteropFilter(cfg InteropFilterConfig) stack.InteropFilter { + cfg.T = cfg.T.WithCtx(stack.ContextWithID(cfg.T.Ctx(), cfg.ID)) + return &rpcInteropFilter{ + commonImpl: newCommon(cfg.CommonConfig), + id: cfg.ID, + client: cfg.Client, + api: sources.NewInteropFilterClient(cfg.Client), + } +} + +func (r *rpcInteropFilter) ID() stack.InteropFilterID { + return r.id +} + +func (r *rpcInteropFilter) AdminAPI() apis.InteropFilterAdminAPI { + return r.api +} + +func (r *rpcInteropFilter) QueryAPI() apis.InteropFilterQueryAPI { + return r.api +} diff --git a/op-devstack/shim/system.go b/op-devstack/shim/system.go index 72c0468dbb3..2d782a7c73d 100644 --- a/op-devstack/shim/system.go +++ b/op-devstack/shim/system.go @@ -32,9 +32,10 @@ type presetSystem struct { // tracks all networks, and ensures there are no networks with the same eth.ChainID networks locks.RWMap[eth.ChainID, stack.Network] - supervisors locks.RWMap[stack.SupervisorID, stack.Supervisor] - sequencers locks.RWMap[stack.TestSequencerID, stack.TestSequencer] - syncTesters locks.RWMap[stack.SyncTesterID, stack.SyncTester] + supervisors locks.RWMap[stack.SupervisorID, stack.Supervisor] + sequencers locks.RWMap[stack.TestSequencerID, stack.TestSequencer] + syncTesters locks.RWMap[stack.SyncTesterID, stack.SyncTester] + interopFilters locks.RWMap[stack.InteropFilterID, stack.InteropFilter] } var _ stack.ExtensibleSystem = (*presetSystem)(nil) @@ -125,6 +126,24 @@ func (p *presetSystem) AddSyncTester(v stack.SyncTester) { p.require().True(p.syncTesters.SetIfMissing(v.ID(), v), "sync tester %s must not already exist", v.ID()) } +func (p *presetSystem) InteropFilter(m stack.InteropFilterMatcher) stack.InteropFilter { + v, ok := findMatch(m, p.interopFilters.Get, p.InteropFilters) + p.require().True(ok, "must find interop filter %s", m) + return v +} + +func (p *presetSystem) AddInteropFilter(v stack.InteropFilter) { + p.require().True(p.interopFilters.SetIfMissing(v.ID(), v), "interop filter %s must not already exist", v.ID()) +} + +func (p *presetSystem) InteropFilterIDs() []stack.InteropFilterID { + return stack.SortInteropFilterIDs(p.interopFilters.Keys()) +} + +func (p *presetSystem) InteropFilters() []stack.InteropFilter { + return stack.SortInteropFilters(p.interopFilters.Values()) +} + func (p *presetSystem) SuperchainIDs() []stack.SuperchainID { return stack.SortSuperchainIDs(p.superchains.Keys()) } diff --git a/op-devstack/stack/interop_filter.go b/op-devstack/stack/interop_filter.go new file mode 100644 index 00000000000..35107a39697 --- /dev/null +++ b/op-devstack/stack/interop_filter.go @@ -0,0 +1,58 @@ +package stack + +import ( + "log/slog" + + "github.com/ethereum-optimism/optimism/op-service/apis" +) + +// InteropFilterID identifies an InteropFilter by name, is type-safe, and can be value-copied and used as map key. +type InteropFilterID genericID + +var _ GenericID = (*InteropFilterID)(nil) + +const InteropFilterKind Kind = "InteropFilter" + +func (id InteropFilterID) String() string { + return genericID(id).string(InteropFilterKind) +} + +func (id InteropFilterID) Kind() Kind { + return InteropFilterKind +} + +func (id InteropFilterID) LogValue() slog.Value { + return slog.StringValue(id.String()) +} + +func (id InteropFilterID) MarshalText() ([]byte, error) { + return genericID(id).marshalText(InteropFilterKind) +} + +func (id *InteropFilterID) UnmarshalText(data []byte) error { + return (*genericID)(id).unmarshalText(InteropFilterKind, data) +} + +func SortInteropFilterIDs(ids []InteropFilterID) []InteropFilterID { + return copyAndSortCmp(ids) +} + +func SortInteropFilters(elems []InteropFilter) []InteropFilter { + return copyAndSort(elems, lessElemOrdered[InteropFilterID, InteropFilter]) +} + +var _ InteropFilterMatcher = InteropFilterID("") + +func (id InteropFilterID) Match(elems []InteropFilter) []InteropFilter { + return findByID(id, elems) +} + +// InteropFilter is a lightweight service that validates interop executing messages. +// It provides a subset of supervisor functionality focused on transaction filtering. +type InteropFilter interface { + Common + ID() InteropFilterID + + AdminAPI() apis.InteropFilterAdminAPI + QueryAPI() apis.InteropFilterQueryAPI +} diff --git a/op-devstack/stack/match/first.go b/op-devstack/stack/match/first.go index 572b49e335c..ace09ca64ca 100644 --- a/op-devstack/stack/match/first.go +++ b/op-devstack/stack/match/first.go @@ -21,3 +21,4 @@ var FirstCluster = First[stack.ClusterID, stack.Cluster]() var FirstFaucet = First[stack.FaucetID, stack.Faucet]() var FirstSyncTester = First[stack.SyncTesterID, stack.SyncTester]() +var FirstInteropFilter = First[stack.InteropFilterID, stack.InteropFilter]() diff --git a/op-devstack/stack/matcher.go b/op-devstack/stack/matcher.go index afe97cfeff9..5f098099608 100644 --- a/op-devstack/stack/matcher.go +++ b/op-devstack/stack/matcher.go @@ -62,3 +62,5 @@ type L2ELMatcher = Matcher[L2ELNodeID, L2ELNode] type FaucetMatcher = Matcher[FaucetID, Faucet] type SyncTesterMatcher = Matcher[SyncTesterID, SyncTester] + +type InteropFilterMatcher = Matcher[InteropFilterID, InteropFilter] diff --git a/op-devstack/stack/system.go b/op-devstack/stack/system.go index d96b91b67f6..00270277a18 100644 --- a/op-devstack/stack/system.go +++ b/op-devstack/stack/system.go @@ -19,12 +19,14 @@ type System interface { Supervisor(m SupervisorMatcher) Supervisor TestSequencer(id TestSequencerMatcher) TestSequencer + InteropFilter(m InteropFilterMatcher) InteropFilter SuperchainIDs() []SuperchainID ClusterIDs() []ClusterID L1NetworkIDs() []L1NetworkID L2NetworkIDs() []L2NetworkID SupervisorIDs() []SupervisorID + InteropFilterIDs() []InteropFilterID Superchains() []Superchain Clusters() []Cluster @@ -32,6 +34,7 @@ type System interface { L2Networks() []L2Network Supervisors() []Supervisor TestSequencers() []TestSequencer + InteropFilters() []InteropFilter } // ExtensibleSystem is an extension-interface to add new components to the system. @@ -46,6 +49,7 @@ type ExtensibleSystem interface { AddSupervisor(v Supervisor) AddTestSequencer(v TestSequencer) AddSyncTester(v SyncTester) + AddInteropFilter(v InteropFilter) } type TimeTravelClock interface { diff --git a/op-devstack/sysgo/interop_filter.go b/op-devstack/sysgo/interop_filter.go new file mode 100644 index 00000000000..89bca09d06a --- /dev/null +++ b/op-devstack/sysgo/interop_filter.go @@ -0,0 +1,158 @@ +package sysgo + +import ( + "context" + "sync" + "time" + + "github.com/ethereum/go-ethereum/log" + + "github.com/ethereum-optimism/optimism/op-devstack/devtest" + "github.com/ethereum-optimism/optimism/op-devstack/shim" + "github.com/ethereum-optimism/optimism/op-devstack/stack" + "github.com/ethereum-optimism/optimism/op-interop-filter/filter" + "github.com/ethereum-optimism/optimism/op-interop-filter/flags" + "github.com/ethereum-optimism/optimism/op-service/client" + oplog "github.com/ethereum-optimism/optimism/op-service/log" + opmetrics "github.com/ethereum-optimism/optimism/op-service/metrics" + "github.com/ethereum-optimism/optimism/op-service/oppprof" + oprpc "github.com/ethereum-optimism/optimism/op-service/rpc" + "github.com/ethereum-optimism/optimism/op-service/testutils/tcpproxy" +) + +// InteropFilterService wraps the interop filter service for sysgo +type InteropFilterService struct { + mu sync.Mutex + + id stack.InteropFilterID + userRPC string + + cfg *filter.Config + p devtest.P + logger log.Logger + + service *filter.Service + + proxy *tcpproxy.Proxy +} + +var _ stack.Lifecycle = (*InteropFilterService)(nil) + +func (s *InteropFilterService) hydrate(sys stack.ExtensibleSystem) { + tlog := sys.Logger().New("id", s.id) + filterClient, err := client.NewRPC(sys.T().Ctx(), tlog, s.userRPC, client.WithLazyDial()) + sys.T().Require().NoError(err) + sys.T().Cleanup(filterClient.Close) + + sys.AddInteropFilter(shim.NewInteropFilter(shim.InteropFilterConfig{ + CommonConfig: shim.NewCommonConfig(sys.T()), + ID: s.id, + Client: filterClient, + })) +} + +func (s *InteropFilterService) UserRPC() string { + return s.userRPC +} + +func (s *InteropFilterService) Start() { + s.mu.Lock() + defer s.mu.Unlock() + if s.service != nil { + s.logger.Warn("InteropFilter already started") + return + } + + if s.proxy == nil { + s.proxy = tcpproxy.New(s.logger.New("proxy", "interop-filter")) + s.p.Require().NoError(s.proxy.Start()) + s.p.Cleanup(func() { + s.proxy.Close() + }) + s.userRPC = "http://" + s.proxy.Addr() + } + + srv, err := filter.NewService(context.Background(), s.cfg, s.logger) + s.p.Require().NoError(err) + + s.service = srv + s.logger.Info("Starting interop filter") + err = srv.Start(context.Background()) + s.p.Require().NoError(err, "interop filter failed to start") + s.logger.Info("Started interop filter") + s.proxy.SetUpstream(ProxyAddr(s.p.Require(), "http://"+srv.RPC().Endpoint())) +} + +func (s *InteropFilterService) Stop() { + s.mu.Lock() + defer s.mu.Unlock() + if s.service == nil { + s.logger.Warn("InteropFilter already stopped") + return + } + ctx, cancel := context.WithCancel(context.Background()) + cancel() // force-quit + s.logger.Info("Closing interop filter") + closeErr := s.service.Stop(ctx) + s.logger.Info("Closed interop filter", "err", closeErr) + + s.service = nil +} + +// WithInteropFilter adds an interop filter service to the orchestrator. +// It will connect to the specified L2 EL nodes and serve checkAccessList requests. +func WithInteropFilter(filterID stack.InteropFilterID, l2ELs []stack.L2ELNodeID) stack.Option[*Orchestrator] { + return stack.AfterDeploy(func(orch *Orchestrator) { + p := orch.P().WithCtx(stack.ContextWithID(orch.P().Ctx(), filterID)) + require := p.Require() + + require.Nil(orch.interopFilter, "can only support a single interop-filter in sysgo") + + // Build L2 RPC list from EL nodes + l2RPCs := make([]flags.L2RPC, 0, len(l2ELs)) + for _, elID := range l2ELs { + el, ok := orch.l2ELs.Get(elID) + require.True(ok, "need L2 EL for interop filter", elID) + chainID, ok := elID.ChainID().Uint64() + require.True(ok, "chain ID must fit in uint64") + l2RPCs = append(l2RPCs, flags.L2RPC{ + ChainID: chainID, + RPCURL: el.UserRPC(), + }) + } + + cfg := &filter.Config{ + L2RPCs: l2RPCs, + DataDir: p.TempDir(), + BackfillDuration: 1 * time.Minute, // Short backfill for tests + Version: "dev", + LogConfig: oplog.CLIConfig{ + Level: log.LevelDebug, + Format: oplog.FormatText, + }, + MetricsConfig: opmetrics.CLIConfig{ + Enabled: false, + }, + PprofConfig: oppprof.CLIConfig{ + ListenEnabled: false, + }, + RPC: oprpc.CLIConfig{ + ListenAddr: "127.0.0.1", + ListenPort: 0, // Auto-assign port + }, + } + + plog := p.Logger() + filterService := &InteropFilterService{ + id: filterID, + userRPC: "", // set on start + cfg: cfg, + p: p, + logger: plog, + service: nil, // set on start + } + orch.interopFilter = filterService + filterService.Start() + orch.p.Cleanup(filterService.Stop) + }) +} diff --git a/op-devstack/sysgo/orchestrator.go b/op-devstack/sysgo/orchestrator.go index 90e0b261ebf..bd9164483b8 100644 --- a/op-devstack/sysgo/orchestrator.go +++ b/op-devstack/sysgo/orchestrator.go @@ -54,8 +54,9 @@ type Orchestrator struct { // service name => prometheus endpoints to scrape l2MetricsEndpoints locks.RWMap[string, []PrometheusMetricsTarget] - syncTester *SyncTesterService - faucet *FaucetService + syncTester *SyncTesterService + faucet *FaucetService + interopFilter *InteropFilterService controlPlane *ControlPlane @@ -138,6 +139,9 @@ func (o *Orchestrator) Hydrate(sys stack.ExtensibleSystem) { if o.syncTester != nil { o.syncTester.hydrate(sys) } + if o.interopFilter != nil { + o.interopFilter.hydrate(sys) + } o.faucet.hydrate(sys) o.sysHook.PostHydrate(sys) } diff --git a/op-devstack/sysgo/system.go b/op-devstack/sysgo/system.go index 1fa516fe381..76f71fd94a8 100644 --- a/op-devstack/sysgo/system.go +++ b/op-devstack/sysgo/system.go @@ -278,6 +278,70 @@ func DefaultMinimalSystemWithSyncTester(dest *DefaultMinimalSystemWithSyncTester return opt } +// DefaultMinimalSystemWithInteropFilterIDs extends DefaultMinimalSystemIDs with InteropFilter +type DefaultMinimalSystemWithInteropFilterIDs struct { + DefaultMinimalSystemIDs + + InteropFilter stack.InteropFilterID +} + +func NewDefaultMinimalSystemWithInteropFilterIDs(l1ID, l2ID eth.ChainID) DefaultMinimalSystemWithInteropFilterIDs { + minimal := NewDefaultMinimalSystemIDs(l1ID, l2ID) + return DefaultMinimalSystemWithInteropFilterIDs{ + DefaultMinimalSystemIDs: minimal, + InteropFilter: stack.InteropFilterID("interop-filter"), + } +} + +// DefaultMinimalSystemWithInteropFilter creates a minimal system with an interop filter. +// This is useful for testing interop transaction filtering without a full supervisor. +func DefaultMinimalSystemWithInteropFilter(dest *DefaultMinimalSystemWithInteropFilterIDs) stack.Option[*Orchestrator] { + l1ID := eth.ChainIDFromUInt64(900) + l2ID := eth.ChainIDFromUInt64(901) + ids := NewDefaultMinimalSystemWithInteropFilterIDs(l1ID, l2ID) + + opt := stack.Combine[*Orchestrator]() + opt.Add(stack.BeforeDeploy(func(o *Orchestrator) { + o.P().Logger().Info("Setting up") + })) + + opt.Add(WithMnemonicKeys(devkeys.TestMnemonic)) + + opt.Add(WithDeployer(), + WithDeployerOptions( + WithLocalContractSources(), + WithCommons(ids.L1.ChainID()), + WithPrefundedL2(ids.L1.ChainID(), ids.L2.ChainID()), + ), + ) + + opt.Add(WithL1Nodes(ids.L1EL, ids.L1CL)) + + opt.Add(WithL2ELNode(ids.L2EL)) + opt.Add(WithL2CLNode(ids.L2CL, ids.L1CL, ids.L1EL, ids.L2EL, L2CLSequencer())) + + opt.Add(WithBatcher(ids.L2Batcher, ids.L1EL, ids.L2CL, ids.L2EL)) + opt.Add(WithProposer(ids.L2Proposer, ids.L1EL, &ids.L2CL, nil)) + + opt.Add(WithFaucets([]stack.L1ELNodeID{ids.L1EL}, []stack.L2ELNodeID{ids.L2EL})) + + opt.Add(WithTestSequencer(ids.TestSequencer, ids.L1CL, ids.L2CL, ids.L1EL, ids.L2EL)) + + opt.Add(WithL2Challenger(ids.L2Challenger, ids.L1EL, ids.L1CL, nil, nil, &ids.L2CL, []stack.L2ELNodeID{ + ids.L2EL, + })) + + opt.Add(WithInteropFilter(ids.InteropFilter, []stack.L2ELNodeID{ids.L2EL})) + + opt.Add(WithL2MetricsDashboard()) + + opt.Add(stack.Finally(func(orch *Orchestrator) { + *dest = ids + })) + + return opt +} + type DefaultSingleChainInteropSystemIDs struct { L1 stack.L1NetworkID L1EL stack.L1ELNodeID diff --git a/op-interop-filter/filter/chain_ingester.go b/op-interop-filter/filter/chain_ingester.go index 0e2fb6d3154..973d8e0017d 100644 --- a/op-interop-filter/filter/chain_ingester.go +++ b/op-interop-filter/filter/chain_ingester.go @@ -161,11 +161,7 @@ func (c *ChainIngester) initLogsDB() error { var dbPath string if c.cfg.DataDir != "" { - chainDir := filepath.Join(c.cfg.DataDir, fmt.Sprintf("chain-%d", chainIDUint)) - if err := os.MkdirAll(chainDir, 0755); err != nil { - return fmt.Errorf("failed to create chain dir: %w", err) - } - dbPath = filepath.Join(chainDir, "logs.db") + dbPath = filepath.Join(c.cfg.DataDir, fmt.Sprintf("chain-%d", chainIDUint), "logs.db") } else { // Use fresh temp directory if no data dir specified // Remove any stale data from previous runs diff --git a/op-interop-filter/filter/service.go b/op-interop-filter/filter/service.go index 59393981888..75e97b17c6c 100644 --- a/op-interop-filter/filter/service.go +++ b/op-interop-filter/filter/service.go @@ -227,3 +227,8 @@ func (s *Service) Stop(ctx context.Context) error { func (s *Service) Stopped() bool { return s.stopped.Load() } + +// RPC returns the RPC server for accessing the endpoint +func (s *Service) RPC() *oprpc.Server { + return s.rpcServer +} diff --git a/op-service/apis/interop_filter.go b/op-service/apis/interop_filter.go new file mode 100644 index 00000000000..f88fa542b9f --- /dev/null +++ b/op-service/apis/interop_filter.go @@ -0,0 +1,27 @@ +package apis + +import ( + "context" + + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// InteropFilterAPI is the RPC API interface for the interop filter service. +// It implements a subset of the supervisor API for transaction filtering. +type InteropFilterAPI interface { + InteropFilterAdminAPI + InteropFilterQueryAPI +} + +// InteropFilterAdminAPI provides admin methods for the interop filter. +type InteropFilterAdminAPI interface { + GetFailsafeEnabled(ctx context.Context) (bool, error) +} + +// InteropFilterQueryAPI provides query methods for the interop filter. +type InteropFilterQueryAPI interface { + CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error +} diff --git a/op-service/sources/interop_filter_client.go b/op-service/sources/interop_filter_client.go new file mode 100644 index 00000000000..cf1183e0172 --- /dev/null +++ b/op-service/sources/interop_filter_client.go @@ -0,0 +1,47 @@ +package sources + +import ( + "context" + "fmt" + + "github.com/ethereum/go-ethereum/common" + + "github.com/ethereum-optimism/optimism/op-service/apis" + "github.com/ethereum-optimism/optimism/op-service/client" + "github.com/ethereum-optimism/optimism/op-supervisor/supervisor/types" +) + +// InteropFilterClient is an RPC client for the interop filter service. +type InteropFilterClient struct { + client client.RPC +} + +var _ apis.InteropFilterAPI = (*InteropFilterClient)(nil) + +// NewInteropFilterClient creates a new InteropFilterClient. +func NewInteropFilterClient(client client.RPC) *InteropFilterClient { + return &InteropFilterClient{ + client: client, + } +} + +// GetFailsafeEnabled returns whether failsafe is enabled. +func (cl *InteropFilterClient) GetFailsafeEnabled(ctx context.Context) (bool, error) { + var enabled bool + err := cl.client.CallContext(ctx, &enabled, "admin_getFailsafeEnabled") + if err != nil { + return false, fmt.Errorf("failed to get failsafe mode for interop filter: %w", err) + } + return enabled, nil +} + +// CheckAccessList validates interop executing messages. +func (cl *InteropFilterClient) CheckAccessList(ctx context.Context, inboxEntries []common.Hash, + minSafety types.SafetyLevel, executingDescriptor types.ExecutingDescriptor) error { + return cl.client.CallContext(ctx, nil, "supervisor_checkAccessList", inboxEntries, minSafety, executingDescriptor) +} + +// Close closes the underlying RPC client. +func (cl *InteropFilterClient) Close() { + cl.client.Close() +}