Skip to content

Commit

Permalink
Merge pull request #6597 from onflow/janez/version-boundary-to-cadenc…
Browse files Browse the repository at this point in the history
…e-port

Expose version boundary to cadence interface - port
  • Loading branch information
j1010001 authored Oct 25, 2024
2 parents 1c2acbd + 03025cb commit cc866e6
Show file tree
Hide file tree
Showing 26 changed files with 776 additions and 174 deletions.
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

0 comments on commit cc866e6

Please sign in to comment.