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 minimum required version to cadence interface #6560

Merged
merged 15 commits into from
Oct 28, 2024

Conversation

janezpodhostnik
Copy link
Contributor

@janezpodhostnik janezpodhostnik commented Oct 11, 2024

This adds a minimum required version to the cadence interface.

This is a cadence version mapped from the flow go version as defined in the Node version beacon contract.

How it works:

Defining the version

FVM gettting the version

  • The FVM gets the minimum required flow-go version by calling that contract function and caching the result (using the same style of cache as the programs cache) at the start of every transaction (reading it together with the metering settings)
  • The cache will invalidate whenever the system account changes. This is currently every block, but fixing that issue is a separate task.
  • The FVM gets this version on all transactions, so using it does not read extra registers (and change the execution receipts)

FVM serving the version to cadence

  • Cadence can request the minimum required cadence version through the cadence runtime interface
  • The FVM translates the flow-go version into a cadence version via a hard-coded (manually up-kept) map. This map only needs to be added to if the cadence code needs to be aware of a version switch for a certain version. (we don't need to hard-code the entire version mapping, just the versions that cadence intends to use)

Rolling node upgrades using cadence minimum required version

We are running flow-go v3.0.0 (cadence v15.0.0) and we want to deploy a (small) change

  1. In cadence add an if statement:
    if minimumRequiredVersion < "v16.0.0" {
      // run v15.0.0 code
    else {
      // run v16.0.0 code
    }
    
  2. call this cadence version v16.0.0
  3. In the FVM add to the hard-coded mapping that flow-go version v4.0.0 will map to cadence v17.0.0 and upgrade cadence version to v16.0.0.
  4. call this flow-go v4.0.0 (make sure that v4.0.0 does not contain any other breaking changes)
  5. make a rolling deploy to all nodes from flow-go v3.0.0 to v4.0.0
  6. add a version boundary to the NodeVersionBeacon for v4.0.0
  7. once the version boundary is reached the cadence code should switch to the else branch and run cadence v16.0.0 code. Any nodes that have failed to do a rolling upgrade to flow-go v4.0.0 will simply stop, due to the existing HCU behavior.
  8. cleanup can be performed by removing the if-else in cadence (only keeping the v16.0.0 code) and removing the version mapping in FVM, then using a rolling deploy to upgrade the nodes again.

@codecov-commenter
Copy link

codecov-commenter commented Oct 11, 2024

Codecov Report

Attention: Patch coverage is 51.80723% with 120 lines in your changes missing coverage. Please review.

Project coverage is 41.12%. Comparing base (8d5a3b3) to head (b2ee0e9).

Files with missing lines Patch % Lines
...vironment/mock/minimum_cadence_required_version.go 0.00% 27 Missing ⚠️
model/convert/service_event.go 55.93% 20 Missing and 6 partials ⚠️
fvm/environment/mock/environment.go 0.00% 22 Missing ⚠️
fvm/executionParameters.go 68.33% 15 Missing and 4 partials ⚠️
fvm/environment/minimum_required_version.go 57.14% 9 Missing ⚠️
fvm/storage/derived/derived_block_data.go 33.33% 6 Missing ⚠️
fvm/context.go 0.00% 5 Missing ⚠️
fvm/transactionInvoker.go 70.00% 2 Missing and 1 partial ⚠️
fvm/storage/state/transaction_state.go 0.00% 2 Missing ⚠️
module/state_synchronization/indexer/util.go 75.00% 1 Missing ⚠️
Additional details and impacted files
@@           Coverage Diff            @@
##           master    #6560    +/-   ##
========================================
  Coverage   41.12%   41.12%            
========================================
  Files        2036     2038     +2     
  Lines      180114   180257   +143     
========================================
+ Hits        74067    74131    +64     
- Misses      99833    99905    +72     
- Partials     6214     6221     +7     
Flag Coverage Δ
unittests 41.12% <51.80%> (+<0.01%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@@ -287,11 +287,17 @@ func (c *IndexerCore) updateProgramCache(header *flow.Header, events []flow.Even

tx.AddInvalidator(&accessInvalidator{
programs: &programInvalidator{
invalidated: updatedContracts,
invalidated: updatedContracts,
invalidateAll: hasAuthorizedTransaction(collections, c.serviceAddress),
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This was missing

@janezpodhostnik janezpodhostnik changed the title Expose version boundary to cadence interface Expose minimum required version to cadence interface Oct 22, 2024
@janezpodhostnik janezpodhostnik self-assigned this Oct 22, 2024
@janezpodhostnik janezpodhostnik marked this pull request as ready for review October 22, 2024 17:05
@j1010001 j1010001 requested a review from SupunS October 22, 2024 17:25
Copy link
Member

@turbolent turbolent left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Mainly just looked at how this will integrate with Cadence, e.g. https://github.com/dapperlabs/cadence-internal/pull/270

fvm/environment/system_contracts.go Show resolved Hide resolved
@@ -336,3 +357,39 @@ func GetExecutionMemoryLimit(

return uint64(memoryLimitRaw), nil
}

func GetMinimumExecutionVersion(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This method seems have nothing to do with Minimum, no?

Suggested change
func GetMinimumExecutionVersion(
func GetCurrentExecutionVersion(

Please also add comments.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The version as defined on the Node version beacon is not the current version, but the minimum required version. That is why I called the method this way. I will add a comment.

@@ -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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Optional:

This introduce unnecessary complexity to the GetMinimumExecutionVersion function.

And create a shortcut for tests. Can we mock the the smart contract call in tests instead?

I saw there are also other WithXXX functions having the same pattern which is to use some flag variables only for testing. IMO, this is a bad practice. We shouldn't have production code skipping some logic (shortcuts) only for tests.

I'm ok for now, but better address this altogether in a separate PR.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For most test this was not used. I only used it for tests that are testing the programs cache loading and resetting. The reason I did that is that an extra contract is loading now, and those tests were off by 1. I think its cleaner for those tests to focus on what they were testing without this interference.

I agree its not the bast pattern, and I will try mocking in a separate PR.

However I am thinking of also using this setting for bootstrapping...

}
boundaries[i] = boundary
}

return boundaries, nil
}

func VersionBoundary(value cadence.Value) (
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please add comment

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added. Though this is not new code. Its just extracted from above.

@@ -62,6 +62,8 @@ type Environment interface {
error,
)

GetCurrentVersionBoundary() (cadence.Value, error)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
GetCurrentVersionBoundary() (cadence.Value, error)
// GetCurrentVersionBoundary returns the current version boundary stored in the VersionBeacon
// system contract.
// There are three cases:
// 1. If it's never set in the smart contract, the current value would be a zero version boundary value.
// 2. The current boundary value is set with a higher version than the node, and
// a higher height than the current block to be executed.
// 3. The current boundary value is set with a lower or same version than the node, and
//. a lower height than the current block to be executed.
GetCurrentVersionBoundary() (cadence.Value, error)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a comment, but this is a different function. This one just exposes the one on SystemContracts to the Environment interface

fvm/environment/minimum_required_version.go Outdated Show resolved Hide resolved
return cadenceVersion.String(), nil
}

func mapToCadenceVersion(version semver.Version) semver.Version {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Too vague, it's better to be more explicit.

Suggested change
func mapToCadenceVersion(version semver.Version) semver.Version {
func mapToCadenceVersion(flowGoVersion semver.Version) semver.Version {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add lots of test cases to this function to describe the behavior?

I would suggest to let it take the fvmToCadenceVersionMapping as input, so that it's easy to write unittests.

Suggested change
func mapToCadenceVersion(version semver.Version) semver.Version {
func mapToCadenceVersion(version semver.Version, fvmToCadenceVersionMapping []VersionMapEntry{) semver.Version {

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There are quite a few cases:

  1. when version is 0.0.0, and fvmToCadenceVersionMapping is empty
  2. when version is 0.0.0, and fvmToCadenceVersionMapping is [0.37.0 : 1.0.0]
  3. when version is 0.37.20, and fvmToCadenceVersionMapping is [0.37.0: 1.0.0]
  4. when version is 0.37.20, and fvmToCadenceVersionMapping is [v0.37.24 : 1.0.0]
  5. when version is 0.37.20, and fvmToCadenceVersionMapping is [v0.37.24 : 1.0.0, v0.37.25 : 1.0.1]
  6. when version is 0.37.20, and fvmToCadenceVersionMapping is [v0.37.19 : 1.0.0, v0.37.20 : 1.0.1, v0.37.21 : 1.0.2, v0.37.20 : 1.0.3]

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

added tests

Comment on lines 54 to 57
type VersionMapEntry struct {
FlowGoVersion semver.Version
CadenceVersion semver.Version
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
type VersionMapEntry struct {
FlowGoVersion semver.Version
CadenceVersion semver.Version
}
// MinVersionMapEntry defines a version map between flow-go and cadence version,
// such that cadence at that version must be run with a flow-go version that is greater
// or equal to the given min version.
type MinVersionMapEntry struct {
MinFlowGoVersion semver.Version
CadenceVersion semver.Version
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically the version map doesn't need to know that this is a mapping between minimums.

return semver.Version{}, fmt.Errorf("could not parse current version boundary: %w", err)
}

semVer, err := semver.NewVersion(boundary.Version)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I feel the name "current" in GetCurrentVersionBoundary is confusing, because there are two cases at least:

  1. boundary.Version > the version the node is currently running at.
    This means there is a up coming version change at the future height, which means in this case boundary.Height is greater than the height of the block to be executed
  2. boundary.Version <= the version the node is currently running at.
    In this case, there could be no version beacon was set in the past, or if set, the boundary.Height must be lower than the Height of the block to be executed.

However, we just return the Version value here without the Height, it means we won't distinguish these two cases. Is that right?

I think we might need two variables:

  1. boundary.Version
  2. IsVersionForUnreachedFutureHeight bool

IsVersionForUnreachedFutureHeight is true if the version is a higher version for the future height.
IsVersionForUnreachedFutureHeight is false if the version is a lower version for the past height or same height as the block to be executed.

I think we only need to map version when IsVersionForUnreachedFutureHeight is true, right?

And since there are two cases here, regardless I think it's better explain each case in comments for the map version function .

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that is why current is in the name, because it is the current version boundary that is in effect (boundary.Height <= current height). so there is just one case, and that is boundary.Version <= the version the node is currently running at. The reason is that is that if we were in the situation where boundary.Version > the version the execution of this transaction would not be permitted (that logic is in stop control)

I already added extra comments to GetCurrentVersionBoundary but I'll add some more here.

// return 0.0.0 if there is no mapping for the version
var cadenceVersion = semver.Version{}

greaterThanOrEqualTo := func(version semver.Version, versionToCompare semver.Version) bool {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we can move this pure function outside

)

func Test_MapToCadenceVersion(t *testing.T) {
v0 := semver.Version{}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
v0 := semver.Version{}
cadenceV0 := semver.Version{}

Comment on lines 12 to 36
flowV1 := semver.Version{
Major: 1,
Minor: 2,
Patch: 3,
PreRelease: "rc.1",
}
flowV2 := semver.Version{
Major: 2,
Minor: 2,
Patch: 3,
PreRelease: "rc.1",
}

cadenceV1 := semver.Version{
Major: 2,
Minor: 1,
Patch: 3,
PreRelease: "rc.2",
}
cadenceV2 := semver.Version{
Major: 12,
Minor: 0,
Patch: 0,
PreRelease: "",
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
flowV1 := semver.Version{
Major: 1,
Minor: 2,
Patch: 3,
PreRelease: "rc.1",
}
flowV2 := semver.Version{
Major: 2,
Minor: 2,
Patch: 3,
PreRelease: "rc.1",
}
cadenceV1 := semver.Version{
Major: 2,
Minor: 1,
Patch: 3,
PreRelease: "rc.2",
}
cadenceV2 := semver.Version{
Major: 12,
Minor: 0,
Patch: 0,
PreRelease: "",
}
flowV1 := semver.Version{
Major: 0,
Minor: 37,
Patch: 0,
}
flowV2 := semver.Version{
Major: 0,
Minor: 37,
Patch: 1,
}
cadenceV1 := semver.Version{
Major: 1,
Minor: 0,
Patch: 0,
}
cadenceV2 := semver.Version{
Major: 1,
Minor: 0,
Patch: 1,
}

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍 I changed the versions a little bit, because in the tests I bump the patch version and I dont want overlap

return cadenceVersion.String(), nil
}

func mapToCadenceVersion(flowGoVersion semver.Version, versionMapping []VersionMapEntry) semver.Version {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func mapToCadenceVersion(flowGoVersion semver.Version, versionMapping []VersionMapEntry) semver.Version {
// find the highest VersionEntry from the versionMapping that is below the given flowGoVersion, and
// return its CadenceVersion
func mapToCadenceVersion(flowGoVersion semver.Version, versionMapping []VersionMapEntry) semver.Version {

func (c minimumRequiredVersion) MinimumRequiredVersion() (string, error) {
executionParameters := c.txnPreparer.ExecutionParameters()

cadenceVersion := mapToCadenceVersion(executionParameters.ExecutionVersion, fvmToCadenceVersionMapping)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
cadenceVersion := mapToCadenceVersion(executionParameters.ExecutionVersion, fvmToCadenceVersionMapping)
// given the minimum required version defined by executionParameters.ExecutionVersion,
// find the highest verion entry in the version mapping that satisfies the minimum version
// requirement, and returns its cadence version
cadenceVersion := mapToCadenceVersion(executionParameters.ExecutionVersion, fvmToCadenceVersionMapping)

Comment on lines 11 to 13
type MinimumRequiredVersion interface {
MinimumRequiredVersion() (string, error)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be more specific?

Suggested change
type MinimumRequiredVersion interface {
MinimumRequiredVersion() (string, error)
}
type MinimumRequiredCadenceVersion interface {
MinimumRequiredCadenceVersion() (string, error)
}

Copy link
Contributor Author

@janezpodhostnik janezpodhostnik Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is a bit tricky. This method satisfies the cadence runtime interface (defined by cadence) and from a cadence perspective it does not make sense to name it a MinimumRequiredCadenceVersion so its just named MinimumRequiredVersion and in the FVM ve just have to implement that interface method.

I'll add a comment

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Maybe we keep the method name as MinimumRequiredVersion, but rename the interface as MinimumCadenceRequiredVersion ?

if greaterThanOrEqualTo(flowGoVersion, entry.FlowGoVersion) {
cadenceVersion = entry.CadenceVersion
} else {
break
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why break here?

It seems we always just look at the very first version entry only, in that case, why not just having one version entry instead of a mapping?

If we are going to hardcode a version map entry, I would say we just hard code the default one:

var DefaultVersionMapEntry = VersionMapEntry{ 
  FlowGoVersion: semver.Version{"0.0.0"}, CadenceVersion: semver.Version{"0.0.0"} }

// change this with versions FlowGoVersion: F, and CadenceVersion: C, where
// CadenceVersion C had a feature toggle which won't be enabled if FlowGoVersion is below F.
var RequiredVersionMapEntry = VersionMapEntry{ 
  FlowGoVersion: semver.Version{"0.0.0"}, CadenceVersion: semver.Version{"0.0.0"} }

then we can simplify this function into:

func mapToCadenceVersion(flowGoVersion semver.Version, versionMapping VersionMapEntry) semver.Version {
  if greateThanOrEqualTo(flowGoVersion, versionMapping.FlowGoVersion) {
    return versionMapping.CadenceVersion
  }
  return DefaultVersionEntry.CadenceVersion
}

@@ -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"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

Copy link
Contributor

@ramtinms ramtinms left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't have much context of how Cadence is gonna use the exposed versions but the code looks good to me

Copy link
Member

@zhangchiqing zhangchiqing left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good. I added some explaination as comment for MinimumRequiredVersion. Hopefully it makes the whole mechanism a bit more clear

Comment on lines 11 to 13
type MinimumRequiredVersion interface {
MinimumRequiredVersion() (string, error)
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see. Maybe we keep the method name as MinimumRequiredVersion, but rename the interface as MinimumCadenceRequiredVersion ?

}
}

func (c minimumRequiredVersion) MinimumRequiredVersion() (string, error) {
Copy link
Member

@zhangchiqing zhangchiqing Oct 25, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
func (c minimumRequiredVersion) MinimumRequiredVersion() (string, error) {
// The returned cadence version can be used by cadnece 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 minimumRequiredVersion) MinimumRequiredVersion() (string, error) {

@janezpodhostnik janezpodhostnik added this pull request to the merge queue Oct 25, 2024
@github-merge-queue github-merge-queue bot removed this pull request from the merge queue due to failed status checks Oct 25, 2024
Copy link
Member

@fxamacker fxamacker left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM! I reviewed for general Go programming as I'm not very familiar with this part of code yet.

@janezpodhostnik janezpodhostnik added this pull request to the merge queue Oct 28, 2024
Merged via the queue into master with commit a6b157c Oct 28, 2024
55 checks passed
@janezpodhostnik janezpodhostnik deleted the janez/version-boundary-to-cadence branch October 28, 2024 15:37
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants