Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose version boundary to cadence interface - port #6597

Merged
merged 14 commits into from
Oct 25, 2024
9 changes: 7 additions & 2 deletions engine/execution/computation/computer/computer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -705,6 +705,7 @@ func TestBlockExecutor_ExecuteBlock(t *testing.T) {
},
),
),
fvm.WithReadVersionFromNodeVersionBeacon(false),
)

vm := fvm.NewVirtualMachine()
Expand Down Expand Up @@ -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()

Expand Down Expand Up @@ -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()

Expand Down
9 changes: 9 additions & 0 deletions fvm/context.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
}
28 changes: 14 additions & 14 deletions fvm/environment/derived_data_invalidator.go
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ func (u ContractUpdates) Any() bool {
type DerivedDataInvalidator struct {
ContractUpdates

MeterParamOverridesUpdated bool
ExecutionParametersUpdated bool
}

var _ derived.TransactionInvalidator = DerivedDataInvalidator{}
Expand All @@ -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 {
Expand All @@ -73,16 +73,16 @@ 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 {
DerivedDataInvalidator
}

func (invalidator ProgramInvalidator) ShouldInvalidateEntries() bool {
return invalidator.MeterParamOverridesUpdated ||
return invalidator.ExecutionParametersUpdated ||
invalidator.ContractUpdates.Any()
}

Expand All @@ -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
}
Expand Down Expand Up @@ -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
}
20 changes: 12 additions & 8 deletions fvm/environment/derived_data_invalidator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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())
Expand Down Expand Up @@ -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))
}

Expand Down Expand Up @@ -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)
Expand Down Expand Up @@ -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()
Expand Down
6 changes: 6 additions & 0 deletions fvm/environment/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
4 changes: 4 additions & 0 deletions fvm/environment/facade_env.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ type facadeEnvironment struct {
ValueStore

*SystemContracts
MinimumCadenceRequiredVersion

UUIDGenerator
AccountLocalIDGenerator
Expand Down Expand Up @@ -103,6 +104,9 @@ func newFacadeEnvironment(
),

SystemContracts: systemContracts,
MinimumCadenceRequiredVersion: NewMinimumCadenceRequiredVersion(
txnState,
),

UUIDGenerator: NewUUIDGenerator(
tracer,
Expand Down
99 changes: 99 additions & 0 deletions fvm/environment/minimum_required_version.go
Original file line number Diff line number Diff line change
@@ -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)
63 changes: 63 additions & 0 deletions fvm/environment/minimum_required_version_test.go
Original file line number Diff line number Diff line change
@@ -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)
})
}
Loading
Loading