diff --git a/engine/execution/computation/computer/computer_test.go b/engine/execution/computation/computer/computer_test.go index 4eade023273..0d7899100ad 100644 --- a/engine/execution/computation/computer/computer_test.go +++ b/engine/execution/computation/computer/computer_test.go @@ -705,6 +705,7 @@ func TestBlockExecutor_ExecuteBlock(t *testing.T) { }, ), ), + fvm.WithReadVersionFromNodeVersionBeacon(false), ) vm := fvm.NewVirtualMachine() @@ -816,7 +817,9 @@ func TestBlockExecutor_ExecuteBlock(t *testing.T) { runtime.Config{}, func(_ runtime.Config) runtime.Runtime { return rt - }))) + })), + fvm.WithReadVersionFromNodeVersionBeacon(false), + ) vm := fvm.NewVirtualMachine() @@ -929,7 +932,9 @@ func TestBlockExecutor_ExecuteBlock(t *testing.T) { runtime.Config{}, func(_ runtime.Config) runtime.Runtime { return rt - }))) + })), + fvm.WithReadVersionFromNodeVersionBeacon(false), + ) vm := fvm.NewVirtualMachine() diff --git a/fvm/context.go b/fvm/context.go index 4007f22286f..fd198633b54 100644 --- a/fvm/context.go +++ b/fvm/context.go @@ -392,3 +392,12 @@ func WithEVMTracer(tracer debug.EVMTracer) Option { return ctx } } + +// WithReadVersionFromNodeVersionBeacon sets whether the version from the node version beacon should be read +// this should only be disabled for testing +func WithReadVersionFromNodeVersionBeacon(enabled bool) Option { + return func(ctx Context) Context { + ctx.ReadVersionFromNodeVersionBeacon = enabled + return ctx + } +} diff --git a/fvm/environment/derived_data_invalidator.go b/fvm/environment/derived_data_invalidator.go index 77659ed984d..754a1e37513 100644 --- a/fvm/environment/derived_data_invalidator.go +++ b/fvm/environment/derived_data_invalidator.go @@ -25,7 +25,7 @@ func (u ContractUpdates) Any() bool { type DerivedDataInvalidator struct { ContractUpdates - MeterParamOverridesUpdated bool + ExecutionParametersUpdated bool } var _ derived.TransactionInvalidator = DerivedDataInvalidator{} @@ -37,16 +37,16 @@ func NewDerivedDataInvalidator( ) DerivedDataInvalidator { return DerivedDataInvalidator{ ContractUpdates: contractUpdates, - MeterParamOverridesUpdated: meterParamOverridesUpdated( + ExecutionParametersUpdated: executionParametersUpdated( executionSnapshot, meterStateRead), } } -// meterParamOverridesUpdated returns true if the meter param overrides have been updated +// executionParametersUpdated returns true if the meter param overrides have been updated // this is done by checking if the registers needed to compute the meter param overrides // have been touched in the execution snapshot -func meterParamOverridesUpdated( +func executionParametersUpdated( executionSnapshot *snapshot.ExecutionSnapshot, meterStateRead *snapshot.ExecutionSnapshot, ) bool { @@ -73,8 +73,8 @@ func (invalidator DerivedDataInvalidator) ProgramInvalidator() derived.ProgramIn return ProgramInvalidator{invalidator} } -func (invalidator DerivedDataInvalidator) MeterParamOverridesInvalidator() derived.MeterParamOverridesInvalidator { - return MeterParamOverridesInvalidator{invalidator} +func (invalidator DerivedDataInvalidator) ExecutionParametersInvalidator() derived.ExecutionParametersInvalidator { + return ExecutionParametersInvalidator{invalidator} } type ProgramInvalidator struct { @@ -82,7 +82,7 @@ type ProgramInvalidator struct { } func (invalidator ProgramInvalidator) ShouldInvalidateEntries() bool { - return invalidator.MeterParamOverridesUpdated || + return invalidator.ExecutionParametersUpdated || invalidator.ContractUpdates.Any() } @@ -91,7 +91,7 @@ func (invalidator ProgramInvalidator) ShouldInvalidateEntry( program *derived.Program, _ *snapshot.ExecutionSnapshot, ) bool { - if invalidator.MeterParamOverridesUpdated { + if invalidator.ExecutionParametersUpdated { // if meter parameters changed we need to invalidate all programs return true } @@ -124,18 +124,18 @@ func (invalidator ProgramInvalidator) ShouldInvalidateEntry( return false } -type MeterParamOverridesInvalidator struct { +type ExecutionParametersInvalidator struct { DerivedDataInvalidator } -func (invalidator MeterParamOverridesInvalidator) ShouldInvalidateEntries() bool { - return invalidator.MeterParamOverridesUpdated +func (invalidator ExecutionParametersInvalidator) ShouldInvalidateEntries() bool { + return invalidator.ExecutionParametersUpdated } -func (invalidator MeterParamOverridesInvalidator) ShouldInvalidateEntry( +func (invalidator ExecutionParametersInvalidator) ShouldInvalidateEntry( _ struct{}, - _ derived.MeterParamOverrides, + _ derived.StateExecutionParameters, _ *snapshot.ExecutionSnapshot, ) bool { - return invalidator.MeterParamOverridesUpdated + return invalidator.ExecutionParametersUpdated } diff --git a/fvm/environment/derived_data_invalidator_test.go b/fvm/environment/derived_data_invalidator_test.go index 5db306642d0..147c03fb57a 100644 --- a/fvm/environment/derived_data_invalidator_test.go +++ b/fvm/environment/derived_data_invalidator_test.go @@ -82,7 +82,7 @@ func TestDerivedDataProgramInvalidator(t *testing.T) { }) t.Run("meter parameters invalidator invalidates all entries", func(t *testing.T) { invalidator := environment.DerivedDataInvalidator{ - MeterParamOverridesUpdated: true, + ExecutionParametersUpdated: true, }.ProgramInvalidator() require.True(t, invalidator.ShouldInvalidateEntries()) @@ -207,23 +207,23 @@ func TestDerivedDataProgramInvalidator(t *testing.T) { func TestMeterParamOverridesInvalidator(t *testing.T) { invalidator := environment.DerivedDataInvalidator{}. - MeterParamOverridesInvalidator() + ExecutionParametersInvalidator() require.False(t, invalidator.ShouldInvalidateEntries()) require.False(t, invalidator.ShouldInvalidateEntry( struct{}{}, - derived.MeterParamOverrides{}, + derived.StateExecutionParameters{}, nil)) invalidator = environment.DerivedDataInvalidator{ ContractUpdates: environment.ContractUpdates{}, - MeterParamOverridesUpdated: true, - }.MeterParamOverridesInvalidator() + ExecutionParametersUpdated: true, + }.ExecutionParametersInvalidator() require.True(t, invalidator.ShouldInvalidateEntries()) require.True(t, invalidator.ShouldInvalidateEntry( struct{}{}, - derived.MeterParamOverrides{}, + derived.StateExecutionParameters{}, nil)) } @@ -265,7 +265,11 @@ func TestMeterParamOverridesUpdated(t *testing.T) { txnState, err := blockDatabase.NewTransaction(0, state.DefaultParameters()) require.NoError(t, err) - computer := fvm.NewMeterParamOverridesComputer(ctx, txnState) + computer := fvm.NewExecutionParametersComputer( + ctx.Logger, + ctx, + txnState, + ) overrides, err := computer.Compute(txnState, struct{}{}) require.NoError(t, err) @@ -300,7 +304,7 @@ func TestMeterParamOverridesUpdated(t *testing.T) { environment.ContractUpdates{}, snapshot, meterStateRead) - require.Equal(t, expected, invalidator.MeterParamOverridesUpdated) + require.Equal(t, expected, invalidator.ExecutionParametersUpdated) } executionSnapshot, err = txnState.FinalizeMainTransaction() diff --git a/fvm/environment/env.go b/fvm/environment/env.go index 5b01728111c..e23e9c64deb 100644 --- a/fvm/environment/env.go +++ b/fvm/environment/env.go @@ -62,6 +62,12 @@ type Environment interface { error, ) + // GetCurrentVersionBoundary executes the getCurrentVersionBoundary function on the NodeVersionBeacon contract. + // the function will return the version boundary (version, block height) that is currently in effect. + // the version boundary currently in effect is the highest one not above the current block height. + // if there is no existing version boundary lower than the current block height, the function will return version 0 and block height 0. + GetCurrentVersionBoundary() (cadence.Value, error) + // AccountInfo GetAccount(address flow.Address) (*flow.Account, error) GetAccountKeys(address flow.Address) ([]flow.AccountPublicKey, error) diff --git a/fvm/environment/facade_env.go b/fvm/environment/facade_env.go index 67e20e02394..57b01d1a853 100644 --- a/fvm/environment/facade_env.go +++ b/fvm/environment/facade_env.go @@ -38,6 +38,7 @@ type facadeEnvironment struct { ValueStore *SystemContracts + MinimumCadenceRequiredVersion UUIDGenerator AccountLocalIDGenerator @@ -107,6 +108,9 @@ func newFacadeEnvironment( ), SystemContracts: systemContracts, + MinimumCadenceRequiredVersion: NewMinimumCadenceRequiredVersion( + txnState, + ), UUIDGenerator: NewUUIDGenerator( tracer, diff --git a/fvm/environment/minimum_required_version.go b/fvm/environment/minimum_required_version.go new file mode 100644 index 00000000000..3095c33cda9 --- /dev/null +++ b/fvm/environment/minimum_required_version.go @@ -0,0 +1,99 @@ +package environment + +import ( + "github.com/coreos/go-semver/semver" + + "github.com/onflow/flow-go/fvm/storage/state" +) + +// MinimumCadenceRequiredVersion returns the minimum required cadence version for the current environment +// in semver format. +type MinimumCadenceRequiredVersion interface { + MinimumRequiredVersion() (string, error) +} + +type minimumCadenceRequiredVersion struct { + txnPreparer state.NestedTransactionPreparer +} + +func NewMinimumCadenceRequiredVersion( + txnPreparer state.NestedTransactionPreparer, +) MinimumCadenceRequiredVersion { + return minimumCadenceRequiredVersion{ + txnPreparer: txnPreparer, + } +} + +// MinimumRequiredVersion The returned cadence version can be used by cadence runtime for supporting feature flag. +// The feature flag in cadence allows ENs to produce consistent results even if running with +// different cadence versions at the same height, which is useful for rolling out cadence +// upgrade without all ENs restarting all together. +// For instance, we would like to grade cadence from v1 to v3, where v3 has a new cadence feature. +// We first make a cadence v2 that has feature flag only turned on when the MinimumRequiredVersion() +// method returns v2 or above. +// So cadence v2 with the feature flag turned off will produce the same result as v1 which doesn't have the feature. +// And cadence v2 with the feature flag turned on will also produce the same result as v3 which has the feature. +// The feature flag allows us to roll out cadence v2 to all ENs which was running v1. +// And we use the MinimumRequiredVersion to control when the feature flag should be switched from off to on. +// And the switching should happen at the same height for all ENs. +// +// The height-based switch over can be done by using VersionBeacon, however, the VersionBeacon only +// defines the flow-go version, not cadence version. +// So we first read the current minimum required flow-go version from the VersionBeacon control, +// and map it to the cadence version to be used by cadence to decide feature flag status. +// +// For instance, let’s say all ENs are running flow-go v0.37.0 with cadence v1. +// We first create a version mapping entry for flow-go v0.37.1 to cadence v2, and roll out v0.37.1 to all ENs. +// v0.37.1 ENs will produce the same result as v0.37.0 ENs, because the current version beacon still returns v0.37.0, +// which maps zero cadence version, and cadence will keep the feature flag off. +// +// After all ENs have upgraded to v0.37.1, we send out a version beacon to switch to v0.37.1 at a future height, +// let’s say height 1000. +// Then what happens is that: +// 1. ENs running v0.37.0 will crash after height 999, until upgrade to higher version +// 2. ENs running v0.37.1 will execute with cadence v2 with feature flag off up until height 999, and from height 1000, +// the feature flag will be on, which means all v0.37.1 ENs will again produce consistent results for blocks above 1000. +// +// After height 1000 have been sealed, we can roll out v0.37.2 to all ENs with cadence v3, and it will produce the consistent +// result as v0.37.1. +func (c minimumCadenceRequiredVersion) MinimumRequiredVersion() (string, error) { + executionParameters := c.txnPreparer.ExecutionParameters() + + // map the minimum required flow-go version to a minimum required cadence version + cadenceVersion := mapToCadenceVersion(executionParameters.ExecutionVersion, minimumFvmToMinimumCadenceVersionMapping) + + return cadenceVersion.String(), nil +} + +func mapToCadenceVersion(flowGoVersion semver.Version, versionMapping FlowGoToCadenceVersionMapping) semver.Version { + if versionGreaterThanOrEqualTo(flowGoVersion, versionMapping.FlowGoVersion) { + return versionMapping.CadenceVersion + } else { + return semver.Version{} + } +} + +func versionGreaterThanOrEqualTo(version semver.Version, other semver.Version) bool { + return version.Compare(other) >= 0 +} + +type FlowGoToCadenceVersionMapping struct { + FlowGoVersion semver.Version + CadenceVersion semver.Version +} + +// This could also be a map, but ist not needed because we only expect one entry at a give time +// we won't be fixing 2 separate issues at 2 separate version with one deploy. +var minimumFvmToMinimumCadenceVersionMapping = FlowGoToCadenceVersionMapping{ + // Leaving this example in, so it's easier to understand + // + // FlowGoVersion: *semver.New("0.37.0"), + // CadenceVersion: *semver.New("1.0.0"), + // +} + +func SetFVMToCadenceVersionMappingForTestingOnly(mapping FlowGoToCadenceVersionMapping) { + minimumFvmToMinimumCadenceVersionMapping = mapping +} + +var _ MinimumCadenceRequiredVersion = (*minimumCadenceRequiredVersion)(nil) diff --git a/fvm/environment/minimum_required_version_test.go b/fvm/environment/minimum_required_version_test.go new file mode 100644 index 00000000000..a72e10567df --- /dev/null +++ b/fvm/environment/minimum_required_version_test.go @@ -0,0 +1,63 @@ +package environment + +import ( + "testing" + + "github.com/coreos/go-semver/semver" + "github.com/stretchr/testify/require" +) + +func Test_MapToCadenceVersion(t *testing.T) { + flowV0 := semver.Version{} + cadenceV0 := semver.Version{} + flowV1 := semver.Version{ + Major: 0, + Minor: 37, + Patch: 0, + } + cadenceV1 := semver.Version{ + Major: 1, + Minor: 0, + Patch: 0, + } + + mapping := FlowGoToCadenceVersionMapping{ + FlowGoVersion: flowV1, + CadenceVersion: cadenceV1, + } + + t.Run("no mapping, v0", func(t *testing.T) { + version := mapToCadenceVersion(flowV0, FlowGoToCadenceVersionMapping{}) + + require.Equal(t, cadenceV0, version) + }) + + t.Run("v0", func(t *testing.T) { + version := mapToCadenceVersion(flowV0, mapping) + + require.Equal(t, semver.Version{}, version) + }) + t.Run("v1 - delta", func(t *testing.T) { + + v := flowV1 + v.Patch -= 1 + + version := mapToCadenceVersion(v, mapping) + + require.Equal(t, cadenceV0, version) + }) + t.Run("v1", func(t *testing.T) { + version := mapToCadenceVersion(flowV1, mapping) + + require.Equal(t, cadenceV1, version) + }) + t.Run("v1 + delta", func(t *testing.T) { + + v := flowV1 + v.BumpPatch() + + version := mapToCadenceVersion(v, mapping) + + require.Equal(t, cadenceV1, version) + }) +} diff --git a/fvm/environment/mock/environment.go b/fvm/environment/mock/environment.go index 1766579d621..445dcbd49d8 100644 --- a/fvm/environment/mock/environment.go +++ b/fvm/environment/mock/environment.go @@ -907,6 +907,36 @@ func (_m *Environment) GetCurrentBlockHeight() (uint64, error) { return r0, r1 } +// GetCurrentVersionBoundary provides a mock function with given fields: +func (_m *Environment) GetCurrentVersionBoundary() (cadence.Value, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for GetCurrentVersionBoundary") + } + + var r0 cadence.Value + var r1 error + if rf, ok := ret.Get(0).(func() (cadence.Value, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() cadence.Value); ok { + r0 = rf() + } else { + if ret.Get(0) != nil { + r0 = ret.Get(0).(cadence.Value) + } + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + // GetInterpreterSharedState provides a mock function with given fields: func (_m *Environment) GetInterpreterSharedState() *interpreter.SharedState { ret := _m.Called() diff --git a/fvm/environment/mock/minimum_cadence_required_version.go b/fvm/environment/mock/minimum_cadence_required_version.go new file mode 100644 index 00000000000..d3bff30e08b --- /dev/null +++ b/fvm/environment/mock/minimum_cadence_required_version.go @@ -0,0 +1,52 @@ +// Code generated by mockery v2.43.2. DO NOT EDIT. + +package mock + +import mock "github.com/stretchr/testify/mock" + +// MinimumCadenceRequiredVersion is an autogenerated mock type for the MinimumCadenceRequiredVersion type +type MinimumCadenceRequiredVersion struct { + mock.Mock +} + +// MinimumRequiredVersion provides a mock function with given fields: +func (_m *MinimumCadenceRequiredVersion) MinimumRequiredVersion() (string, error) { + ret := _m.Called() + + if len(ret) == 0 { + panic("no return value specified for MinimumRequiredVersion") + } + + var r0 string + var r1 error + if rf, ok := ret.Get(0).(func() (string, error)); ok { + return rf() + } + if rf, ok := ret.Get(0).(func() string); ok { + r0 = rf() + } else { + r0 = ret.Get(0).(string) + } + + if rf, ok := ret.Get(1).(func() error); ok { + r1 = rf() + } else { + r1 = ret.Error(1) + } + + return r0, r1 +} + +// NewMinimumCadenceRequiredVersion creates a new instance of MinimumCadenceRequiredVersion. It also registers a testing interface on the mock and a cleanup function to assert the mocks expectations. +// The first argument is typically a *testing.T value. +func NewMinimumCadenceRequiredVersion(t interface { + mock.TestingT + Cleanup(func()) +}) *MinimumCadenceRequiredVersion { + mock := &MinimumCadenceRequiredVersion{} + mock.Mock.Test(t) + + t.Cleanup(func() { mock.AssertExpectations(t) }) + + return mock +} diff --git a/fvm/environment/programs_test.go b/fvm/environment/programs_test.go index e39d224c9d4..b0368db49e0 100644 --- a/fvm/environment/programs_test.go +++ b/fvm/environment/programs_test.go @@ -156,7 +156,9 @@ func Test_Programs(t *testing.T) { fvm.WithAuthorizationChecksEnabled(false), fvm.WithSequenceNumberCheckAndIncrementEnabled(false), fvm.WithCadenceLogging(true), - fvm.WithDerivedBlockData(derivedBlockData)) + fvm.WithDerivedBlockData(derivedBlockData), + // disable reading version from node version beacon otherwise it loads an extra contract + fvm.WithReadVersionFromNodeVersionBeacon(false)) var contractASnapshot *snapshot.ExecutionSnapshot var contractBSnapshot *snapshot.ExecutionSnapshot @@ -613,7 +615,9 @@ func Test_ProgramsDoubleCounting(t *testing.T) { fvm.WithSequenceNumberCheckAndIncrementEnabled(false), fvm.WithCadenceLogging(true), fvm.WithDerivedBlockData(derivedBlockData), - fvm.WithMetricsReporter(metrics)) + fvm.WithMetricsReporter(metrics), + // disable reading version from node version beacon otherwise it loads an extra contract + fvm.WithReadVersionFromNodeVersionBeacon(false)) t.Run("deploy contracts and ensure cache is empty", func(t *testing.T) { // deploy contract A diff --git a/fvm/environment/system_contracts.go b/fvm/environment/system_contracts.go index 22860dd38ea..e8e9598aa3c 100644 --- a/fvm/environment/system_contracts.go +++ b/fvm/environment/system_contracts.go @@ -312,3 +312,21 @@ func (sys *SystemContracts) AccountsStorageCapacity( }, ) } + +var getCurrentVersionBoundarySpec = ContractFunctionSpec{ + AddressFromChain: ServiceAddress, + LocationName: systemcontracts.ContractNameNodeVersionBeacon, + FunctionName: systemcontracts.ContractVersionBeacon_getCurrentVersionBoundary, + ArgumentTypes: []sema.Type{}, +} + +// GetCurrentVersionBoundary executes the getCurrentVersionBoundary function on the NodeVersionBeacon contract. +// the function will return the version boundary (version, block height) that is currently in effect. +// the version boundary currently in effect is the highest one not above the current block height. +// if there is no existing version boundary lower than the current block height, the function will return version 0 and block height 0. +func (sys *SystemContracts) GetCurrentVersionBoundary() (cadence.Value, error) { + return sys.Invoke( + getCurrentVersionBoundarySpec, + []cadence.Value{}, + ) +} diff --git a/fvm/executionParameters.go b/fvm/executionParameters.go index 34e3f4594cb..db88f42ef5f 100644 --- a/fvm/executionParameters.go +++ b/fvm/executionParameters.go @@ -5,6 +5,11 @@ import ( "fmt" "math" + "github.com/coreos/go-semver/semver" + "github.com/rs/zerolog" + + "github.com/onflow/flow-go/model/convert" + "github.com/onflow/flow-go/fvm/storage/snapshot" "github.com/onflow/cadence" @@ -52,75 +57,82 @@ func getBasicMeterParameters( return params } -// getBodyMeterParameters returns the set of meter parameters used for -// transaction/script body execution. -func getBodyMeterParameters( +// getExecutionParameters returns the set of meter parameters used for +// transaction/script body execution and the minimum required version as defined by the +// NodeVersionBeacon contract. +func getExecutionParameters( + log zerolog.Logger, ctx Context, proc Procedure, txnState storage.TransactionPreparer, -) ( - meter.MeterParameters, - *snapshot.ExecutionSnapshot, - error, -) { - procParams := getBasicMeterParameters(ctx, proc) +) (state.ExecutionParameters, *snapshot.ExecutionSnapshot, error) { + meterParams := getBasicMeterParameters(ctx, proc) - overrides, meterStateRead, err := txnState.GetMeterParamOverrides( + executionParams, executionParamsStateRead, err := txnState.GetStateExecutionParameters( txnState, - NewMeterParamOverridesComputer(ctx, txnState)) + NewExecutionParametersComputer(log, ctx, txnState)) if err != nil { - return procParams, nil, err + return state.ExecutionParameters{ + MeterParameters: meterParams, + ExecutionVersion: semver.Version{}, + }, nil, err } - if overrides.ComputationWeights != nil { - procParams = procParams.WithComputationWeights( - overrides.ComputationWeights) + if executionParams.ComputationWeights != nil { + meterParams = meterParams.WithComputationWeights( + executionParams.ComputationWeights) } - if overrides.MemoryWeights != nil { - procParams = procParams.WithMemoryWeights(overrides.MemoryWeights) + if executionParams.MemoryWeights != nil { + meterParams = meterParams.WithMemoryWeights(executionParams.MemoryWeights) } - if overrides.MemoryLimit != nil { - procParams = procParams.WithMemoryLimit(*overrides.MemoryLimit) + if executionParams.MemoryLimit != nil { + meterParams = meterParams.WithMemoryLimit(*executionParams.MemoryLimit) } // NOTE: The memory limit (and interaction limit) may be overridden by the // environment. We need to ignore the override in that case. if proc.ShouldDisableMemoryAndInteractionLimits(ctx) { - procParams = procParams.WithMemoryLimit(math.MaxUint64). + meterParams = meterParams.WithMemoryLimit(math.MaxUint64). WithStorageInteractionLimit(math.MaxUint64) } - return procParams, meterStateRead, nil + return state.ExecutionParameters{ + MeterParameters: meterParams, + ExecutionVersion: executionParams.ExecutionVersion, + }, executionParamsStateRead, nil } -type MeterParamOverridesComputer struct { +type ExecutionParametersComputer struct { + log zerolog.Logger ctx Context txnState storage.TransactionPreparer } -func NewMeterParamOverridesComputer( +func NewExecutionParametersComputer( + log zerolog.Logger, ctx Context, txnState storage.TransactionPreparer, -) MeterParamOverridesComputer { - return MeterParamOverridesComputer{ +) ExecutionParametersComputer { + return ExecutionParametersComputer{ + log: log, ctx: ctx, txnState: txnState, } } -func (computer MeterParamOverridesComputer) Compute( +func (computer ExecutionParametersComputer) Compute( _ state.NestedTransactionPreparer, _ struct{}, ) ( - derived.MeterParamOverrides, + derived.StateExecutionParameters, error, ) { - var overrides derived.MeterParamOverrides + var overrides derived.StateExecutionParameters var err error computer.txnState.RunWithAllLimitsDisabled(func() { - overrides, err = computer.getMeterParamOverrides() + overrides, err = computer.getExecutionParameters() }) if err != nil { @@ -132,8 +144,8 @@ func (computer MeterParamOverridesComputer) Compute( return overrides, nil } -func (computer MeterParamOverridesComputer) getMeterParamOverrides() ( - derived.MeterParamOverrides, +func (computer ExecutionParametersComputer) getExecutionParameters() ( + derived.StateExecutionParameters, error, ) { // Check that the service account exists because all the settings are @@ -147,7 +159,7 @@ func (computer MeterParamOverridesComputer) getMeterParamOverrides() ( computer.ctx.EnvironmentParams, computer.txnState) - overrides := derived.MeterParamOverrides{} + overrides := derived.StateExecutionParameters{} // set the property if no error, but if the error is a fatal error then // return it @@ -204,6 +216,15 @@ func (computer MeterParamOverridesComputer) getMeterParamOverrides() ( return overrides, err } + executionVersion, err := GetMinimumRequiredExecutionVersion(computer.log, computer.ctx, env) + err = setIfOk( + "execution version", + err, + func() { overrides.ExecutionVersion = executionVersion }) + if err != nil { + return overrides, err + } + return overrides, nil } @@ -336,3 +357,40 @@ func GetExecutionMemoryLimit( return uint64(memoryLimitRaw), nil } + +func GetMinimumRequiredExecutionVersion( + log zerolog.Logger, + ctx Context, + env environment.Environment, +) (semver.Version, error) { + if !ctx.ReadVersionFromNodeVersionBeacon { + return semver.Version{}, nil + } + + // the current version boundary defines a block height and a minimum required version that is required past that block height. + value, err := env.GetCurrentVersionBoundary() + + if err != nil { + return semver.Version{}, fmt.Errorf("could not get current version boundary: %w", err) + } + + boundary, err := convert.VersionBoundary(value) + + if err != nil { + return semver.Version{}, fmt.Errorf("could not parse current version boundary: %w", err) + } + + semVer, err := semver.NewVersion(boundary.Version) + if err != nil { + // This could be problematic, if the version is not a valid semver version. The NodeVersionBeacon should prevent + // this, but it could have bugs. + // Erroring here gives us no way to recover as no transactions would work anymore, + // instead return the version as 0.0.0 and log the error, allowing us to recover. + // this would mean that any if-statements that were relying on a higher version would fail, + // but that is preferable to all transactions halting. + log.Error().Err(err).Msg("could not parse version boundary. Version boundary as defined in the NodeVersionBeacon contract is not a valid semver version!") + return semver.Version{}, nil + } + + return *semVer, nil +} diff --git a/fvm/fvm_test.go b/fvm/fvm_test.go index cb0d270f725..fb88f78f287 100644 --- a/fvm/fvm_test.go +++ b/fvm/fvm_test.go @@ -8,6 +8,15 @@ import ( "math" "strings" "testing" + "time" + + "github.com/rs/zerolog" + + "github.com/coreos/go-semver/semver" + "github.com/onflow/flow-core-contracts/lib/go/templates" + + "github.com/onflow/flow-go/fvm/storage" + "github.com/onflow/flow-go/fvm/storage/state" cadenceStdlib "github.com/onflow/cadence/stdlib" @@ -3404,3 +3413,193 @@ func TestCrypto(t *testing.T) { test(t, fmt.Sprintf("import Crypto from %s", cryptoContractAddress)) }) } + +func Test_MinimumRequiredVersion(t *testing.T) { + + chain := flow.Emulator.Chain() + sc := systemcontracts.SystemContractsForChain(chain.ChainID()) + log := zerolog.New(zerolog.NewTestWriter(t)) + + getVersion := func(ctx fvm.Context, snapshotTree snapshot.SnapshotTree) string { + blockDatabase := storage.NewBlockDatabase( + snapshotTree, + 0, + nil) + + txnState, err := blockDatabase.NewTransaction(0, state.DefaultParameters()) + require.NoError(t, err) + + executionParams, _, err := txnState.GetStateExecutionParameters( + txnState, + fvm.NewExecutionParametersComputer(log, ctx, txnState)) + require.NoError(t, err) + + // this will set the parameters to the txnState. + // this is done at the beginning of a transaction/script + txnId, err := txnState.BeginNestedTransactionWithMeterParams( + state.ExecutionParameters{ + ExecutionVersion: executionParams.ExecutionVersion, + }) + require.NoError(t, err) + + mrv := environment.NewMinimumCadenceRequiredVersion(txnState) + + v, err := mrv.MinimumRequiredVersion() + + require.NoError(t, err) + _, err = txnState.CommitNestedTransaction(txnId) + require.NoError(t, err) + + return v + } + + insertVersionBoundary := func(newVersion semver.Version, currentHeight, insertHeight uint64, ctx fvm.Context, snapshotTree snapshot.SnapshotTree, vm fvm.VM, txIndex uint32) snapshot.SnapshotTree { + setVersionBoundaryScript := templates.GenerateSetVersionBoundaryScript(sc.AsTemplateEnv()) + tx := flow.NewTransactionBody(). + SetScript(setVersionBoundaryScript). + SetProposalKey(sc.FlowServiceAccount.Address, 0, 0). + AddAuthorizer(sc.FlowServiceAccount.Address). + SetPayer(sc.FlowServiceAccount.Address) + + tx. + AddArgument(jsoncdc.MustEncode(cadence.UInt8(newVersion.Major))). + AddArgument(jsoncdc.MustEncode(cadence.UInt8(newVersion.Minor))). + AddArgument(jsoncdc.MustEncode(cadence.UInt8(newVersion.Patch))). + AddArgument(jsoncdc.MustEncode(cadence.String(newVersion.PreRelease))) + + tx.AddArgument(jsoncdc.MustEncode(cadence.UInt64(insertHeight))) + + startHeader := flow.Header{ + Height: currentHeight, + ChainID: chain.ChainID(), + Timestamp: time.Now().UTC(), + } + + blocks := new(envMock.Blocks) + ctxWithBlock := fvm.NewContextFromParent( + ctx, + fvm.WithBlockHeader(&startHeader), + fvm.WithBlocks(blocks), + ) + + executionSnapshot, output, err := vm.Run( + ctxWithBlock, + fvm.Transaction(tx, txIndex), + snapshotTree) + + require.NoError(t, err) + require.NoError(t, output.Err) + return snapshotTree.Append(executionSnapshot) + } + + runSystemTxToUpdateNodeVersionBeaconContract := func(atHeight uint64, ctx fvm.Context, snapshotTree snapshot.SnapshotTree, vm fvm.VM, txIndex uint32) snapshot.SnapshotTree { + txBody := flow.NewTransactionBody(). + SetScript([]byte(fmt.Sprintf(` + import NodeVersionBeacon from %s + + transaction { + prepare(serviceAccount: auth(BorrowValue) &Account) { + + let versionBeaconHeartbeat = serviceAccount.storage + .borrow<&NodeVersionBeacon.Heartbeat>(from: NodeVersionBeacon.HeartbeatStoragePath) + ?? panic("Couldn't borrow NodeVersionBeacon.Heartbeat Resource") + versionBeaconHeartbeat.heartbeat() + } + } + `, + sc.NodeVersionBeacon.Address.HexWithPrefix()))). + SetProposalKey(sc.FlowServiceAccount.Address, 0, 0). + AddAuthorizer(sc.FlowServiceAccount.Address). + SetPayer(sc.FlowServiceAccount.Address) + + endHeader := flow.Header{ + Height: atHeight, + ChainID: chain.ChainID(), + Timestamp: time.Now().UTC(), + } + + blocks := new(envMock.Blocks) + ctxWithBlock := fvm.NewContextFromParent(ctx, + fvm.WithBlockHeader(&endHeader), + fvm.WithBlocks(blocks), + ) + + executionSnapshot, output, err := vm.Run( + ctxWithBlock, + fvm.Transaction(txBody, txIndex), + snapshotTree) + + require.NoError(t, err) + require.NoError(t, output.Err) + + return snapshotTree.Append(executionSnapshot) + } + + t.Run("minimum required version", newVMTest(). + withContextOptions( + fvm.WithChain(chain), + fvm.WithAuthorizationChecksEnabled(false), + fvm.WithSequenceNumberCheckAndIncrementEnabled(false), + ). + run(func( + t *testing.T, + vm fvm.VM, + chain flow.Chain, + ctx fvm.Context, + snapshotTree snapshot.SnapshotTree, + ) { + // default version is empty + require.Equal(t, semver.Version{}.String(), getVersion(ctx, snapshotTree)) + + // define mapping for flow go version to cadence version + flowVersion1 := semver.Version{ + Major: 1, + Minor: 2, + Patch: 3, + PreRelease: "rc.1", + } + cadenceVersion1 := semver.Version{ + Major: 2, + Minor: 1, + Patch: 3, + PreRelease: "rc.2", + } + environment.SetFVMToCadenceVersionMappingForTestingOnly( + environment.FlowGoToCadenceVersionMapping{ + FlowGoVersion: flowVersion1, + CadenceVersion: cadenceVersion1, + }) + + h0 := uint64(100) // starting height + hv1 := uint64(2000) // version boundary height + + txIndex := uint32(0) + + // insert version boundary 1 + snapshotTree = insertVersionBoundary(flowVersion1, h0, hv1, ctx, snapshotTree, vm, txIndex) + txIndex += 1 + + // so far no change: + require.Equal(t, semver.Version{}.String(), getVersion(ctx, snapshotTree)) + + // system transaction needs to run to update the flowVersion on chain + snapshotTree = runSystemTxToUpdateNodeVersionBeaconContract(hv1-1, ctx, snapshotTree, vm, txIndex) + txIndex += 1 + + // no change: + require.Equal(t, semver.Version{}.String(), getVersion(ctx, snapshotTree)) + + // system transaction needs to run to update the flowVersion on chain + snapshotTree = runSystemTxToUpdateNodeVersionBeaconContract(hv1, ctx, snapshotTree, vm, txIndex) + txIndex += 1 + + // switch to cadence version 1 + require.Equal(t, cadenceVersion1.String(), getVersion(ctx, snapshotTree)) + + // system transaction needs to run to update the flowVersion on chain + snapshotTree = runSystemTxToUpdateNodeVersionBeaconContract(hv1+1, ctx, snapshotTree, vm, txIndex) + + // still cadence version 1 + require.Equal(t, cadenceVersion1.String(), getVersion(ctx, snapshotTree)) + })) +} diff --git a/fvm/script.go b/fvm/script.go index 9a5a2551f4d..4d8a86323b8 100644 --- a/fvm/script.go +++ b/fvm/script.go @@ -173,7 +173,8 @@ func (executor *scriptExecutor) Execute() error { } func (executor *scriptExecutor) execute() error { - meterParams, _, err := getBodyMeterParameters( + executionParams, _, err := getExecutionParameters( + executor.env.Logger(), executor.ctx, executor.proc, executor.txnState) @@ -182,7 +183,7 @@ func (executor *scriptExecutor) execute() error { } txnId, err := executor.txnState.BeginNestedTransactionWithMeterParams( - meterParams) + executionParams) if err != nil { return err } diff --git a/fvm/storage/derived/derived_block_data.go b/fvm/storage/derived/derived_block_data.go index 96233359333..14deca315b7 100644 --- a/fvm/storage/derived/derived_block_data.go +++ b/fvm/storage/derived/derived_block_data.go @@ -3,12 +3,11 @@ package derived import ( "fmt" - "github.com/onflow/flow-go/fvm/storage/snapshot" - "github.com/onflow/cadence/common" "github.com/onflow/cadence/interpreter" "github.com/onflow/flow-go/fvm/storage/logical" + "github.com/onflow/flow-go/fvm/storage/snapshot" "github.com/onflow/flow-go/fvm/storage/state" ) @@ -23,11 +22,12 @@ type DerivedTransactionPreparer interface { ) GetProgram(location common.AddressLocation) (*Program, bool) - GetMeterParamOverrides( + // GetStateExecutionParameters returns parameters needed for execution from the state. + GetStateExecutionParameters( txnState state.NestedTransactionPreparer, - getMeterParamOverrides ValueComputer[struct{}, MeterParamOverrides], + getMeterParamOverrides ValueComputer[struct{}, StateExecutionParameters], ) ( - MeterParamOverrides, + StateExecutionParameters, *snapshot.ExecutionSnapshot, error, ) @@ -46,7 +46,7 @@ type Program struct { type DerivedBlockData struct { programs *DerivedDataTable[common.AddressLocation, *Program] - meterParamOverrides *DerivedDataTable[struct{}, MeterParamOverrides] + meterParamOverrides *DerivedDataTable[struct{}, StateExecutionParameters] } // DerivedTransactionData is the derived data scratch space for a single @@ -59,7 +59,7 @@ type DerivedTransactionData struct { // There's only a single entry in this table. For simplicity, we'll use // struct{} as the entry's key. - meterParamOverrides *TableTransaction[struct{}, MeterParamOverrides] + executionParameters *TableTransaction[struct{}, StateExecutionParameters] } func NewEmptyDerivedBlockData( @@ -72,7 +72,7 @@ func NewEmptyDerivedBlockData( ](initialSnapshotTime), meterParamOverrides: NewEmptyTable[ struct{}, - MeterParamOverrides, + StateExecutionParameters, ](initialSnapshotTime), } } @@ -87,14 +87,14 @@ func (block *DerivedBlockData) NewChildDerivedBlockData() *DerivedBlockData { func (block *DerivedBlockData) NewSnapshotReadDerivedTransactionData() *DerivedTransactionData { return &DerivedTransactionData{ programs: block.programs.NewSnapshotReadTableTransaction(), - meterParamOverrides: block.meterParamOverrides.NewSnapshotReadTableTransaction(), + executionParameters: block.meterParamOverrides.NewSnapshotReadTableTransaction(), } } func (block *DerivedBlockData) NewCachingSnapshotReadDerivedTransactionData() *DerivedTransactionData { return &DerivedTransactionData{ programs: block.programs.NewCachingSnapshotReadTableTransaction(), - meterParamOverrides: block.meterParamOverrides.NewCachingSnapshotReadTableTransaction(), + executionParameters: block.meterParamOverrides.NewCachingSnapshotReadTableTransaction(), } } @@ -121,7 +121,7 @@ func (block *DerivedBlockData) NewDerivedTransactionData( return &DerivedTransactionData{ programs: txnPrograms, - meterParamOverrides: txnMeterParamOverrides, + executionParameters: txnMeterParamOverrides, }, nil } @@ -178,19 +178,19 @@ func (transaction *DerivedTransactionData) AddInvalidator( } transaction.programs.AddInvalidator(invalidator.ProgramInvalidator()) - transaction.meterParamOverrides.AddInvalidator( - invalidator.MeterParamOverridesInvalidator()) + transaction.executionParameters.AddInvalidator( + invalidator.ExecutionParametersInvalidator()) } -func (transaction *DerivedTransactionData) GetMeterParamOverrides( +func (transaction *DerivedTransactionData) GetStateExecutionParameters( txnState state.NestedTransactionPreparer, - getMeterParamOverrides ValueComputer[struct{}, MeterParamOverrides], + getMeterParamOverrides ValueComputer[struct{}, StateExecutionParameters], ) ( - MeterParamOverrides, + StateExecutionParameters, *snapshot.ExecutionSnapshot, error, ) { - return transaction.meterParamOverrides.GetWithStateOrCompute( + return transaction.executionParameters.GetWithStateOrCompute( txnState, struct{}{}, getMeterParamOverrides) @@ -202,7 +202,7 @@ func (transaction *DerivedTransactionData) Validate() error { return fmt.Errorf("programs validate failed: %w", err) } - err = transaction.meterParamOverrides.Validate() + err = transaction.executionParameters.Validate() if err != nil { return fmt.Errorf("meter param overrides validate failed: %w", err) } @@ -216,7 +216,7 @@ func (transaction *DerivedTransactionData) Commit() error { return fmt.Errorf("programs commit failed: %w", err) } - err = transaction.meterParamOverrides.Commit() + err = transaction.executionParameters.Commit() if err != nil { return fmt.Errorf("meter param overrides commit failed: %w", err) } diff --git a/fvm/storage/derived/invalidator.go b/fvm/storage/derived/invalidator.go index a17bded4c67..9467c169bdc 100644 --- a/fvm/storage/derived/invalidator.go +++ b/fvm/storage/derived/invalidator.go @@ -1,6 +1,7 @@ package derived import ( + "github.com/coreos/go-semver/semver" "github.com/onflow/cadence/common" "github.com/onflow/flow-go/fvm/meter" @@ -12,17 +13,23 @@ type MeterParamOverrides struct { MemoryLimit *uint64 // nil indicates no override } +// StateExecutionParameters are parameters needed for execution defined in the execution state. +type StateExecutionParameters struct { + MeterParamOverrides + ExecutionVersion semver.Version +} + type ProgramInvalidator TableInvalidator[ common.AddressLocation, *Program, ] -type MeterParamOverridesInvalidator TableInvalidator[ +type ExecutionParametersInvalidator TableInvalidator[ struct{}, - MeterParamOverrides, + StateExecutionParameters, ] type TransactionInvalidator interface { ProgramInvalidator() ProgramInvalidator - MeterParamOverridesInvalidator() MeterParamOverridesInvalidator + ExecutionParametersInvalidator() ExecutionParametersInvalidator } diff --git a/fvm/storage/state/execution_state.go b/fvm/storage/state/execution_state.go index 455650ada25..12968e4b0a6 100644 --- a/fvm/storage/state/execution_state.go +++ b/fvm/storage/state/execution_state.go @@ -4,6 +4,8 @@ import ( "fmt" "math" + "github.com/coreos/go-semver/semver" + "github.com/onflow/cadence/common" "github.com/onflow/crypto/hash" @@ -28,7 +30,8 @@ type ExecutionState struct { finalized bool *spockState - meter *meter.Meter + meter *meter.Meter + executionVersion semver.Version // NOTE: parent and child state shares the same limits controller *limitsController @@ -41,6 +44,11 @@ type StateParameters struct { maxValueSizeAllowed uint64 } +type ExecutionParameters struct { + meter.MeterParameters + ExecutionVersion semver.Version +} + func DefaultParameters() StateParameters { return StateParameters{ MeterParameters: meter.DefaultParameters(), @@ -130,19 +138,20 @@ func NewExecutionStateWithSpockStateHasher( // NewChildWithMeterParams generates a new child state using the provide meter // parameters. func (state *ExecutionState) NewChildWithMeterParams( - params meter.MeterParameters, + params ExecutionParameters, ) *ExecutionState { return &ExecutionState{ finalized: false, spockState: state.spockState.NewChild(), - meter: meter.NewMeter(params), + meter: meter.NewMeter(params.MeterParameters), + executionVersion: params.ExecutionVersion, limitsController: state.limitsController, } } // NewChild generates a new child state using the parent's meter parameters. func (state *ExecutionState) NewChild() *ExecutionState { - return state.NewChildWithMeterParams(state.meter.MeterParameters) + return state.NewChildWithMeterParams(state.ExecutionParameters()) } // InteractionUsed returns the amount of ledger interaction (total ledger byte read + total ledger byte written) @@ -352,6 +361,13 @@ func (state *ExecutionState) checkSize( return nil } +func (state *ExecutionState) ExecutionParameters() ExecutionParameters { + return ExecutionParameters{ + MeterParameters: state.meter.MeterParameters, + ExecutionVersion: state.executionVersion, + } +} + func (state *ExecutionState) readSetSize() int { return state.spockState.readSetSize() } diff --git a/fvm/storage/state/transaction_state.go b/fvm/storage/state/transaction_state.go index 1e5c563baf1..1b00bf78973 100644 --- a/fvm/storage/state/transaction_state.go +++ b/fvm/storage/state/transaction_state.go @@ -45,6 +45,9 @@ type Meter interface { type NestedTransactionPreparer interface { Meter + // ExecutionParameters returns the execution parameters + ExecutionParameters() ExecutionParameters + // NumNestedTransactions returns the number of uncommitted nested // transactions. Note that the main transaction is not considered a // nested transaction. @@ -84,7 +87,7 @@ type NestedTransactionPreparer interface { // the provided meter parameters. This returns error if the current nested // transaction is program restricted. BeginNestedTransactionWithMeterParams( - params meter.MeterParameters, + params ExecutionParameters, ) ( NestedTransactionId, error, @@ -200,6 +203,10 @@ func (txnState *transactionState) current() nestedTransactionStackFrame { return txnState.nestedTransactions[txnState.NumNestedTransactions()] } +func (txnState *transactionState) ExecutionParameters() ExecutionParameters { + return txnState.current().ExecutionParameters() +} + func (txnState *transactionState) NumNestedTransactions() int { return len(txnState.nestedTransactions) - 1 } @@ -267,7 +274,7 @@ func (txnState *transactionState) BeginNestedTransaction() ( } func (txnState *transactionState) BeginNestedTransactionWithMeterParams( - params meter.MeterParameters, + params ExecutionParameters, ) ( NestedTransactionId, error, diff --git a/fvm/storage/state/transaction_state_test.go b/fvm/storage/state/transaction_state_test.go index e41ff7951f9..f8b18b5d186 100644 --- a/fvm/storage/state/transaction_state_test.go +++ b/fvm/storage/state/transaction_state_test.go @@ -104,7 +104,9 @@ func TestUnrestrictedNestedTransactionDifferentMeterParams(t *testing.T) { require.Equal(t, uint(math.MaxUint), mainState.TotalMemoryLimit()) id1, err := txn.BeginNestedTransactionWithMeterParams( - meter.DefaultParameters().WithMemoryLimit(1)) + state.ExecutionParameters{ + MeterParameters: meter.DefaultParameters().WithMemoryLimit(1), + }) require.NoError(t, err) nestedState1 := id1.StateForTestingOnly() @@ -112,7 +114,9 @@ func TestUnrestrictedNestedTransactionDifferentMeterParams(t *testing.T) { require.Equal(t, uint(1), nestedState1.TotalMemoryLimit()) id2, err := txn.BeginNestedTransactionWithMeterParams( - meter.DefaultParameters().WithMemoryLimit(2)) + state.ExecutionParameters{ + MeterParameters: meter.DefaultParameters().WithMemoryLimit(2), + }) require.NoError(t, err) nestedState2 := id2.StateForTestingOnly() diff --git a/fvm/systemcontracts/system_contracts.go b/fvm/systemcontracts/system_contracts.go index 3760044698e..ad9f66c4a65 100644 --- a/fvm/systemcontracts/system_contracts.go +++ b/fvm/systemcontracts/system_contracts.go @@ -65,6 +65,7 @@ const ( ContractStorageFeesFunction_calculateAccountCapacity = "calculateAccountCapacity" ContractStorageFeesFunction_getAccountsCapacityForTransactionStorageCheck = "getAccountsCapacityForTransactionStorageCheck" ContractStorageFeesFunction_defaultTokenAvailableBalance = "defaultTokenAvailableBalance" + ContractVersionBeacon_getCurrentVersionBoundary = "getCurrentVersionBoundary" // These are the account indexes of system contracts as deployed by the default bootstrapping. // On long-running networks some of these contracts might have been deployed after bootstrapping, diff --git a/fvm/transactionInvoker.go b/fvm/transactionInvoker.go index 3b97964db74..6edf2ec5405 100644 --- a/fvm/transactionInvoker.go +++ b/fvm/transactionInvoker.go @@ -34,6 +34,8 @@ type TransactionExecutorParams struct { // Note: This is disabled only by tests TransactionBodyExecutionEnabled bool + + ReadVersionFromNodeVersionBeacon bool } func DefaultTransactionExecutorParams() TransactionExecutorParams { @@ -42,6 +44,7 @@ func DefaultTransactionExecutorParams() TransactionExecutorParams { SequenceNumberCheckAndIncrementEnabled: true, AccountKeyWeightThreshold: AccountKeyWeightThreshold, TransactionBodyExecutionEnabled: true, + ReadVersionFromNodeVersionBeacon: true, } } @@ -68,7 +71,7 @@ type transactionExecutor struct { // the state reads needed to compute the metering parameters // this is used to invalidate the metering parameters if a transaction // writes to any of those registers - meterStateRead *snapshot.ExecutionSnapshot + executionStateRead *snapshot.ExecutionSnapshot cadenceRuntime *reusableRuntime.ReusableCadenceRuntime txnBodyExecutor runtime.Executor @@ -199,27 +202,27 @@ func (executor *transactionExecutor) preprocessTransactionBody() error { return err } } - // get meter parameters - meterParams, meterStateRead, err := getBodyMeterParameters( + executionParameters, executionStateRead, err := getExecutionParameters( + executor.env.Logger(), executor.ctx, executor.proc, executor.txnState) if err != nil { - return fmt.Errorf("error getting meter parameters: %w", err) + return fmt.Errorf("error getting execution parameters: %w", err) } - if len(meterStateRead.WriteSet) != 0 { + if len(executionStateRead.WriteSet) != 0 { // this should never happen // and indicates an implementation error - panic("getting metering parameters should not write to registers") + panic("getting execution parameters should not write to registers") } - // we need to save the meter state read for invalidation purposes - executor.meterStateRead = meterStateRead + // we need to save the execution state read for invalidation purposes + executor.executionStateRead = executionStateRead txnId, err := executor.txnState.BeginNestedTransactionWithMeterParams( - meterParams) + executionParameters) if err != nil { return err } @@ -403,7 +406,8 @@ func (executor *transactionExecutor) normalExecution() ( invalidator = environment.NewDerivedDataInvalidator( contractUpdates, bodySnapshot, - executor.meterStateRead) + executor.executionStateRead, + ) // Check if all account storage limits are ok // diff --git a/model/convert/service_event.go b/model/convert/service_event.go index f8edf54aeae..7482f2ecc6f 100644 --- a/model/convert/service_event.go +++ b/model/convert/service_event.go @@ -1170,69 +1170,12 @@ func convertVersionBoundaries(array cadence.Array) ( boundaries := make([]flow.VersionBoundary, len(array.Values)) for i, cadenceVal := range array.Values { - boundary, err := DecodeCadenceValue( - fmt.Sprintf(".Values[%d]", i), - cadenceVal, - func(structVal cadence.Struct) ( - flow.VersionBoundary, - error, - ) { - if structVal.Type() == nil { - return flow.VersionBoundary{}, fmt.Errorf("VersionBoundary struct doesn't have type") - } - - fields := cadence.FieldsMappedByName(structVal) - - const expectedFieldCount = 2 - if len(fields) < expectedFieldCount { - return flow.VersionBoundary{}, fmt.Errorf( - "incorrect number of fields (%d != %d)", - len(fields), - expectedFieldCount, - ) - } - - blockHeightValue, err := getField[cadence.Value](fields, "blockHeight") - if err != nil { - return flow.VersionBoundary{}, fmt.Errorf("failed to decode VersionBoundary struct: %w", err) - } - - versionValue, err := getField[cadence.Value](fields, "version") - if err != nil { - return flow.VersionBoundary{}, fmt.Errorf("failed to decode VersionBoundary struct: %w", err) - } - - height, err := DecodeCadenceValue( - ".blockHeight", - blockHeightValue, - func(cadenceVal cadence.UInt64) ( - uint64, - error, - ) { - return uint64(cadenceVal), nil - }, - ) - if err != nil { - return flow.VersionBoundary{}, err - } - - version, err := DecodeCadenceValue( - ".version", - versionValue, - convertSemverVersion, - ) - if err != nil { - return flow.VersionBoundary{}, err - } - - return flow.VersionBoundary{ - BlockHeight: height, - Version: version, - }, nil - }, - ) + boundary, err := VersionBoundary(cadenceVal) if err != nil { - return nil, err + return nil, decodeError{ + location: fmt.Sprintf(".Values[%d]", i), + err: err, + } } boundaries[i] = boundary } @@ -1240,6 +1183,75 @@ func convertVersionBoundaries(array cadence.Array) ( return boundaries, nil } +// VersionBoundary decodes a single version boundary from the given Cadence value. +func VersionBoundary(value cadence.Value) ( + flow.VersionBoundary, + error, +) { + boundary, err := DecodeCadenceValue( + "VersionBoundary", + value, + func(structVal cadence.Struct) ( + flow.VersionBoundary, + error, + ) { + if structVal.Type() == nil { + return flow.VersionBoundary{}, fmt.Errorf("VersionBoundary struct doesn't have type") + } + + fields := cadence.FieldsMappedByName(structVal) + + const expectedFieldCount = 2 + if len(fields) < expectedFieldCount { + return flow.VersionBoundary{}, fmt.Errorf( + "incorrect number of fields (%d != %d)", + len(fields), + expectedFieldCount, + ) + } + + blockHeightValue, err := getField[cadence.Value](fields, "blockHeight") + if err != nil { + return flow.VersionBoundary{}, fmt.Errorf("failed to decode VersionBoundary struct: %w", err) + } + + versionValue, err := getField[cadence.Value](fields, "version") + if err != nil { + return flow.VersionBoundary{}, fmt.Errorf("failed to decode VersionBoundary struct: %w", err) + } + + height, err := DecodeCadenceValue( + ".blockHeight", + blockHeightValue, + func(cadenceVal cadence.UInt64) ( + uint64, + error, + ) { + return uint64(cadenceVal), nil + }, + ) + if err != nil { + return flow.VersionBoundary{}, err + } + + version, err := DecodeCadenceValue( + ".version", + versionValue, + convertSemverVersion, + ) + if err != nil { + return flow.VersionBoundary{}, err + } + + return flow.VersionBoundary{ + BlockHeight: height, + Version: version, + }, nil + }, + ) + return boundary, err +} + func convertSemverVersion(structVal cadence.Struct) ( string, error, diff --git a/module/state_synchronization/indexer/indexer_core.go b/module/state_synchronization/indexer/indexer_core.go index 9f15cc05e34..22a6d16ea2a 100644 --- a/module/state_synchronization/indexer/indexer_core.go +++ b/module/state_synchronization/indexer/indexer_core.go @@ -287,9 +287,10 @@ func (c *IndexerCore) updateProgramCache(header *flow.Header, events []flow.Even tx.AddInvalidator(&accessInvalidator{ programs: &programInvalidator{ - invalidated: updatedContracts, + invalidated: updatedContracts, + invalidateAll: hasAuthorizedTransaction(collections, c.serviceAddress), }, - meterParamOverrides: &meterParamOverridesInvalidator{ + executionParameters: &executionParametersInvalidator{ invalidateAll: hasAuthorizedTransaction(collections, c.serviceAddress), }, }) diff --git a/module/state_synchronization/indexer/util.go b/module/state_synchronization/indexer/util.go index d75f5541a02..5526776716b 100644 --- a/module/state_synchronization/indexer/util.go +++ b/module/state_synchronization/indexer/util.go @@ -92,15 +92,15 @@ var _ derived.TransactionInvalidator = (*accessInvalidator)(nil) // accessInvalidator is a derived.TransactionInvalidator that invalidates programs and meter param overrides. type accessInvalidator struct { programs *programInvalidator - meterParamOverrides *meterParamOverridesInvalidator + executionParameters *executionParametersInvalidator } func (inv *accessInvalidator) ProgramInvalidator() derived.ProgramInvalidator { return inv.programs } -func (inv *accessInvalidator) MeterParamOverridesInvalidator() derived.MeterParamOverridesInvalidator { - return inv.meterParamOverrides +func (inv *accessInvalidator) ExecutionParametersInvalidator() derived.ExecutionParametersInvalidator { + return inv.executionParameters } var _ derived.ProgramInvalidator = (*programInvalidator)(nil) @@ -121,17 +121,17 @@ func (inv *programInvalidator) ShouldInvalidateEntry(location common.AddressLoca return inv.invalidateAll || ok } -var _ derived.MeterParamOverridesInvalidator = (*meterParamOverridesInvalidator)(nil) +var _ derived.ExecutionParametersInvalidator = (*executionParametersInvalidator)(nil) -// meterParamOverridesInvalidator is a derived.MeterParamOverridesInvalidator that invalidates meter param overrides. -type meterParamOverridesInvalidator struct { +// executionParametersInvalidator is a derived.ExecutionParametersInvalidator that invalidates meter param overrides and execution version. +type executionParametersInvalidator struct { invalidateAll bool } -func (inv *meterParamOverridesInvalidator) ShouldInvalidateEntries() bool { +func (inv *executionParametersInvalidator) ShouldInvalidateEntries() bool { return inv.invalidateAll } -func (inv *meterParamOverridesInvalidator) ShouldInvalidateEntry(_ struct{}, _ derived.MeterParamOverrides, _ *snapshot.ExecutionSnapshot) bool { +func (inv *executionParametersInvalidator) ShouldInvalidateEntry(_ struct{}, _ derived.StateExecutionParameters, _ *snapshot.ExecutionSnapshot) bool { return inv.invalidateAll } diff --git a/module/trace/constants.go b/module/trace/constants.go index 9e8ab96f3ad..1241ce765a0 100644 --- a/module/trace/constants.go +++ b/module/trace/constants.go @@ -185,11 +185,9 @@ const ( FVMEnvRandomSourceHistoryProvider SpanName = "fvm.env.randomSourceHistoryProvider" FVMEnvCreateAccount SpanName = "fvm.env.createAccount" FVMEnvAddAccountKey SpanName = "fvm.env.addAccountKey" - FVMEnvAddEncodedAccountKey SpanName = "fvm.env.addEncodedAccountKey" FVMEnvAccountKeysCount SpanName = "fvm.env.accountKeysCount" FVMEnvGetAccountKey SpanName = "fvm.env.getAccountKey" FVMEnvRevokeAccountKey SpanName = "fvm.env.revokeAccountKey" - FVMEnvRevokeEncodedAccountKey SpanName = "fvm.env.revokeEncodedAccountKey" FVMEnvUpdateAccountContractCode SpanName = "fvm.env.updateAccountContractCode" FVMEnvGetAccountContractCode SpanName = "fvm.env.getAccountContractCode" FVMEnvRemoveAccountContractCode SpanName = "fvm.env.removeAccountContractCode"