diff --git a/cmd/access/node_builder/access_node_builder.go b/cmd/access/node_builder/access_node_builder.go index 6d48a429f57..d2394ccb44b 100644 --- a/cmd/access/node_builder/access_node_builder.go +++ b/cmd/access/node_builder/access_node_builder.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "os" "path" "path/filepath" @@ -147,6 +148,8 @@ type AccessNodeConfig struct { registersDBPath string checkpointFile string scriptExecutorConfig query.QueryConfig + scriptExecMinBlock uint64 + scriptExecMaxBlock uint64 } type PublicNetworkConfig struct { @@ -237,6 +240,8 @@ func DefaultAccessNodeConfig() *AccessNodeConfig { registersDBPath: filepath.Join(homedir, ".flow", "execution_state"), checkpointFile: cmd.NotSet, scriptExecutorConfig: query.NewDefaultConfig(), + scriptExecMinBlock: 0, + scriptExecMaxBlock: math.MaxUint64, } } @@ -1107,6 +1112,14 @@ func (builder *FlowAccessNodeBuilder) extraFlags() { "script-execution-timeout", defaultConfig.scriptExecutorConfig.ExecutionTimeLimit, "timeout value for locally executed scripts. default: 10s") + flags.Uint64Var(&builder.scriptExecMinBlock, + "script-execution-min-height", + defaultConfig.scriptExecMinBlock, + "lowest block height to allow for script execution. default: no limit") + flags.Uint64Var(&builder.scriptExecMaxBlock, + "script-execution-max-height", + defaultConfig.scriptExecMaxBlock, + "highest block height to allow for script execution. default: no limit") }).ValidateFlags(func() error { if builder.supportsObserver && (builder.PublicNetworkConfig.BindAddress == cmd.NotSet || builder.PublicNetworkConfig.BindAddress == "") { @@ -1421,7 +1434,7 @@ func (builder *FlowAccessNodeBuilder) Build() (cmd.Node, error) { return nil }). Module("backend script executor", func(node *cmd.NodeConfig) error { - builder.ScriptExecutor = backend.NewScriptExecutor() + builder.ScriptExecutor = backend.NewScriptExecutor(builder.Logger, builder.scriptExecMinBlock, builder.scriptExecMaxBlock) return nil }). Module("async register store", func(node *cmd.NodeConfig) error { diff --git a/engine/access/rpc/backend/script_executor.go b/engine/access/rpc/backend/script_executor.go index 12d64a0daa9..f0ec0a85c27 100644 --- a/engine/access/rpc/backend/script_executor.go +++ b/engine/access/rpc/backend/script_executor.go @@ -2,8 +2,9 @@ package backend import ( "context" - "sync" + "fmt" + "github.com/rs/zerolog" "go.uber.org/atomic" "github.com/onflow/flow-go/model/flow" @@ -12,6 +13,8 @@ import ( ) type ScriptExecutor struct { + log zerolog.Logger + // scriptExecutor is used to interact with execution state scriptExecutor *execution.Scripts @@ -21,25 +24,51 @@ type ScriptExecutor struct { // initialized is used to signal that the index and executor are ready initialized *atomic.Bool - // init is used to ensure that the object is initialized only once - init sync.Once + // minCompatibleHeight and maxCompatibleHeight are used to limit the block range that can be queried using local execution + // to ensure only blocks that are compatible with the node's current software version are allowed. + // Note: this is a temporary solution for cadence/fvm upgrades while version beacon support is added + minCompatibleHeight *atomic.Uint64 + maxCompatibleHeight *atomic.Uint64 } -func NewScriptExecutor() *ScriptExecutor { +func NewScriptExecutor(log zerolog.Logger, minHeight, maxHeight uint64) *ScriptExecutor { + logger := log.With().Str("component", "script-executor").Logger() + logger.Info(). + Uint64("min_height", minHeight). + Uint64("max_height", maxHeight). + Msg("script executor created") + return &ScriptExecutor{ - initialized: atomic.NewBool(false), + log: logger, + initialized: atomic.NewBool(false), + minCompatibleHeight: atomic.NewUint64(minHeight), + maxCompatibleHeight: atomic.NewUint64(maxHeight), } } +// SetMinCompatibleHeight sets the lowest block height (inclusive) that can be queried using local execution +// Use this to limit the executable block range supported by the node's current software version. +func (s *ScriptExecutor) SetMinCompatibleHeight(height uint64) { + s.minCompatibleHeight.Store(height) + s.log.Info().Uint64("height", height).Msg("minimum compatible height set") +} + +// SetMaxCompatibleHeight sets the highest block height (inclusive) that can be queried using local execution +// Use this to limit the executable block range supported by the node's current software version. +func (s *ScriptExecutor) SetMaxCompatibleHeight(height uint64) { + s.maxCompatibleHeight.Store(height) + s.log.Info().Uint64("height", height).Msg("maximum compatible height set") +} + // InitReporter initializes the indexReporter and script executor // This method can be called at any time after the ScriptExecutor object is created. Any requests // made to the other methods will return execution.ErrDataNotAvailable until this method is called. func (s *ScriptExecutor) InitReporter(indexReporter state_synchronization.IndexReporter, scriptExecutor *execution.Scripts) { - s.init.Do(func() { - defer s.initialized.Store(true) + if s.initialized.CompareAndSwap(false, true) { + s.log.Info().Msg("script executor initialized") s.indexReporter = indexReporter s.scriptExecutor = scriptExecutor - }) + } } // ExecuteAtBlockHeight executes provided script at the provided block height against a local execution state. @@ -49,8 +78,8 @@ func (s *ScriptExecutor) InitReporter(indexReporter state_synchronization.IndexR // - execution.ErrDataNotAvailable if the data for the block height is not available. this could be because // the height is not within the index block range, or the index is not ready. func (s *ScriptExecutor) ExecuteAtBlockHeight(ctx context.Context, script []byte, arguments [][]byte, height uint64) ([]byte, error) { - if !s.isDataAvailable(height) { - return nil, execution.ErrDataNotAvailable + if err := s.checkDataAvailable(height); err != nil { + return nil, err } return s.scriptExecutor.ExecuteAtBlockHeight(ctx, script, arguments, height) @@ -63,13 +92,29 @@ func (s *ScriptExecutor) ExecuteAtBlockHeight(ctx context.Context, script []byte // - execution.ErrDataNotAvailable if the data for the block height is not available. this could be because // the height is not within the index block range, or the index is not ready. func (s *ScriptExecutor) GetAccountAtBlockHeight(ctx context.Context, address flow.Address, height uint64) (*flow.Account, error) { - if !s.isDataAvailable(height) { - return nil, execution.ErrDataNotAvailable + if err := s.checkDataAvailable(height); err != nil { + return nil, err } return s.scriptExecutor.GetAccountAtBlockHeight(ctx, address, height) } -func (s *ScriptExecutor) isDataAvailable(height uint64) bool { - return s.initialized.Load() && height <= s.indexReporter.HighestIndexedHeight() && height >= s.indexReporter.LowestIndexedHeight() +func (s *ScriptExecutor) checkDataAvailable(height uint64) error { + if !s.initialized.Load() { + return fmt.Errorf("%w: script executor not initialized", execution.ErrDataNotAvailable) + } + + if height > s.indexReporter.HighestIndexedHeight() { + return fmt.Errorf("%w: block not indexed yet", execution.ErrDataNotAvailable) + } + + if height < s.indexReporter.LowestIndexedHeight() { + return fmt.Errorf("%w: block is before lowest indexed height", execution.ErrDataNotAvailable) + } + + if height > s.maxCompatibleHeight.Load() || height < s.minCompatibleHeight.Load() { + return fmt.Errorf("%w: node software is not compatible with version required to executed block", execution.ErrDataNotAvailable) + } + + return nil }