Skip to content

Commit

Permalink
Merge pull request #6535 from onflow/petera/allow-version-control-ign…
Browse files Browse the repository at this point in the history
…ored-versions

[Access] Add support for ignoring version beacon events for compatible versions
  • Loading branch information
franklywatson authored Nov 7, 2024
2 parents b929eea + 0cd2759 commit fb6576e
Show file tree
Hide file tree
Showing 2 changed files with 197 additions and 21 deletions.
67 changes: 46 additions & 21 deletions engine/common/version/version_control.go
Original file line number Diff line number Diff line change
Expand Up @@ -32,10 +32,15 @@ type VersionControlConsumer func(height uint64, version *semver.Version)
// NoHeight represents the maximum possible height for blocks.
var NoHeight = uint64(0)

// defaultCompatibilityOverrides stores the list of version compatibility overrides.
// version beacon events who's Major.Minor.Patch version match an entry in this map will be ignored.
//
// IMPORTANT: only add versions to this list if you are certain that the cadence and fvm changes
// deployed during the HCU are backwards compatible for scripts.
var defaultCompatibilityOverrides = map[string]struct{}{}

// VersionControl manages the version control system for the node.
// It consumes BlockFinalized events and updates the node's version control based on the latest version beacon.
//
// VersionControl implements the protocol.Consumer and component.Component interfaces.
type VersionControl struct {
// Noop implements the protocol.Consumer interface with no operations.
psEvents.Noop
Expand Down Expand Up @@ -67,6 +72,10 @@ type VersionControl struct {
// startHeight and endHeight define the height boundaries for version compatibility.
startHeight *atomic.Uint64
endHeight *atomic.Uint64

// compatibilityOverrides stores the list of version compatibility overrides.
// version beacon events who's Major.Minor.Patch version match an entry in this map will be ignored.
compatibilityOverrides map[string]struct{}
}

var _ protocol.Consumer = (*VersionControl)(nil)
Expand Down Expand Up @@ -97,6 +106,7 @@ func NewVersionControl(
finalizedHeightNotifier: engine.NewNotifier(),
startHeight: atomic.NewUint64(NoHeight),
endHeight: atomic.NewUint64(NoHeight),
compatibilityOverrides: defaultCompatibilityOverrides,
}

if vc.nodeVersion == nil {
Expand Down Expand Up @@ -146,10 +156,7 @@ func (v *VersionControl) initBoundaries(
for {
vb, err := v.versionBeacons.Highest(processedHeight)
if err != nil && !errors.Is(err, storage.ErrNotFound) {
ctx.Throw(
fmt.Errorf(
"failed to get highest version beacon for version control: %w",
err))
ctx.Throw(fmt.Errorf("failed to get highest version beacon for version control: %w", err))
return err
}

Expand All @@ -175,17 +182,16 @@ func (v *VersionControl) initBoundaries(
if err == nil {
err = fmt.Errorf("boundary semantic version is nil")
}
ctx.Throw(
fmt.Errorf(
"failed to parse semver during version control setup: %w",
err))
ctx.Throw(fmt.Errorf("failed to parse semver during version control setup: %w", err))
return err
}

compResult := ver.Compare(*v.nodeVersion)
processedHeight = vb.SealHeight - 1

if compResult <= 0 {
if v.isOverridden(ver) {
continue
}

if ver.Compare(*v.nodeVersion) <= 0 {
v.startHeight.Store(boundary.BlockHeight)
v.log.Info().
Uint64("startHeight", boundary.BlockHeight).
Expand Down Expand Up @@ -295,10 +301,7 @@ func (v *VersionControl) blockFinalized(
Uint64("height", height).
Msg("Failed to get highest version beacon")

ctx.Throw(
fmt.Errorf(
"failed to get highest version beacon for version control: %w",
err))
ctx.Throw(fmt.Errorf("failed to get highest version beacon for version control: %w", err))
return
}

Expand Down Expand Up @@ -330,13 +333,14 @@ func (v *VersionControl) blockFinalized(
}
// this should never happen as we already validated the version beacon
// when indexing it
ctx.Throw(
fmt.Errorf(
"failed to parse semver: %w",
err))
ctx.Throw(fmt.Errorf("failed to parse semver: %w", err))
return
}

if v.isOverridden(ver) {
continue
}

if ver.Compare(*v.nodeVersion) > 0 {
newEndHeight = boundary.BlockHeight - 1

Expand Down Expand Up @@ -385,3 +389,24 @@ func (v *VersionControl) EndHeight() uint64 {

return endHeight
}

// isOverridden checks if the version is overridden by the compatibility overrides and can be ignored.
func (v *VersionControl) isOverridden(ver *semver.Version) bool {
normalizedVersion := &semver.Version{
Major: ver.Major,
Minor: ver.Minor,
Patch: ver.Patch,
}

_, ok := v.compatibilityOverrides[normalizedVersion.String()]
if !ok {
return false
}

v.log.Info().
Str("event_version", ver.String()).
Str("override_version", normalizedVersion.String()).
Msg("ignoring version beacon event matching compatibility override")

return true
}
151 changes: 151 additions & 0 deletions engine/common/version/version_control_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ type testCaseConfig struct {
nodeVersion string

versionEvents []*flow.SealedVersionBeacon
overrides map[string]struct{}
expectedStart uint64
expectedEnd uint64
}
Expand Down Expand Up @@ -184,6 +185,51 @@ func TestVersionControlInitialization(t *testing.T) {
expectedStart: sealedRootBlockHeight + 12,
expectedEnd: sealedRootBlockHeight + 13,
},
{
name: "start and end version set, start ignored due to override",
nodeVersion: "0.0.2",
versionEvents: []*flow.SealedVersionBeacon{
VersionBeaconEvent(sealedRootBlockHeight+10, flow.VersionBoundary{BlockHeight: sealedRootBlockHeight + 12, Version: "0.0.1"}),
VersionBeaconEvent(latestBlockHeight-10, flow.VersionBoundary{BlockHeight: latestBlockHeight - 8, Version: "0.0.3"}),
},
overrides: map[string]struct{}{"0.0.1": {}},
expectedStart: sealedRootBlockHeight,
expectedEnd: latestBlockHeight - 9,
},
{
name: "start and end version set, end ignored due to override",
nodeVersion: "0.0.2",
versionEvents: []*flow.SealedVersionBeacon{
VersionBeaconEvent(sealedRootBlockHeight+10, flow.VersionBoundary{BlockHeight: sealedRootBlockHeight + 12, Version: "0.0.1"}),
VersionBeaconEvent(latestBlockHeight-10, flow.VersionBoundary{BlockHeight: latestBlockHeight - 8, Version: "0.0.3"}),
},
overrides: map[string]struct{}{"0.0.3": {}},
expectedStart: sealedRootBlockHeight + 12,
expectedEnd: latestBlockHeight,
},
{
name: "start and end version set, middle envent ignored due to override",
nodeVersion: "0.0.2",
versionEvents: []*flow.SealedVersionBeacon{
VersionBeaconEvent(sealedRootBlockHeight+10, flow.VersionBoundary{BlockHeight: sealedRootBlockHeight + 12, Version: "0.0.1"}),
VersionBeaconEvent(latestBlockHeight-3, flow.VersionBoundary{BlockHeight: latestBlockHeight - 1, Version: "0.0.3"}),
VersionBeaconEvent(latestBlockHeight-10, flow.VersionBoundary{BlockHeight: latestBlockHeight - 8, Version: "0.0.4"}),
},
overrides: map[string]struct{}{"0.0.3": {}},
expectedStart: sealedRootBlockHeight + 12,
expectedEnd: latestBlockHeight - 9,
},
{
name: "pre-release version matches overrides",
nodeVersion: "0.0.2",
versionEvents: []*flow.SealedVersionBeacon{
VersionBeaconEvent(sealedRootBlockHeight+10, flow.VersionBoundary{BlockHeight: sealedRootBlockHeight + 12, Version: "0.0.1-pre-release.0"}),
VersionBeaconEvent(latestBlockHeight-10, flow.VersionBoundary{BlockHeight: latestBlockHeight - 8, Version: "0.0.3"}),
},
overrides: map[string]struct{}{"0.0.1": {}},
expectedStart: sealedRootBlockHeight,
expectedEnd: latestBlockHeight - 9,
},
}

for _, testCase := range testCases {
Expand Down Expand Up @@ -217,6 +263,7 @@ func TestVersionControlInitialization(t *testing.T) {
versionBeacons: versionBeacons,
sealedRootBlockHeight: sealedRootBlockHeight,
latestFinalizedBlockHeight: latestBlockHeight,
overrides: testCase.overrides,
signalerContext: irrecoverable.NewMockSignalerContext(t, ctx),
})

Expand Down Expand Up @@ -322,6 +369,72 @@ func generateChecks(testCase testCaseConfig, finalizedRootBlockHeight, latestBlo
return checks
}

// TestVersionBoundaryReceived tests the behavior of the VersionControl component when a new
// version beacon event is received.
func TestVersionBoundaryReceived(t *testing.T) {
signalCtx := irrecoverable.NewMockSignalerContext(t, context.Background())

contract := &versionBeaconContract{}

// Create version event for initial height
latestHeight := uint64(10)
boundaryHeight := uint64(13)

vc := createVersionControlComponent(t, versionComponentTestConfigs{
nodeVersion: "0.0.1",
versionBeacons: contract,
sealedRootBlockHeight: 0,
latestFinalizedBlockHeight: latestHeight,
overrides: map[string]struct{}{"0.0.2": {}}, // skip event at 0.0.2
signalerContext: signalCtx,
})

var assertUpdate func(height uint64, version *semver.Version)
var assertCallbackCalled, assertCallbackNotCalled func()

// Add a consumer to verify version updates
vc.AddVersionUpdatesConsumer(func(height uint64, version *semver.Version) {
assertUpdate(height, version)
})
assert.Len(t, vc.consumers, 1)

// At this point, both start and end heights are unset

// Add a new boundary, and finalize the block
latestHeight++ // 11
contract.AddBoundary(latestHeight, flow.VersionBoundary{BlockHeight: boundaryHeight, Version: "0.0.2"})

// This event should be skipped due to the override
assertUpdate, assertCallbackNotCalled = generateConsumerIgnoredAssertions(t)
vc.blockFinalized(signalCtx, latestHeight)
assertCallbackNotCalled()

// Next, add another new boundary and finalize the block
latestHeight++ // 12
contract.AddBoundary(latestHeight, flow.VersionBoundary{BlockHeight: boundaryHeight, Version: "0.0.3"})

assertUpdate, assertCallbackCalled = generateConsumerAssertions(t, boundaryHeight, semver.New("0.0.3"))
vc.blockFinalized(signalCtx, latestHeight)
assertCallbackCalled()

// Finally, finalize one more block to get past the boundary
latestHeight++ // 13
vc.blockFinalized(signalCtx, latestHeight)

// Check compatibility at key heights
compatible, err := vc.CompatibleAtBlock(10)
require.NoError(t, err)
assert.True(t, compatible)

compatible, err = vc.CompatibleAtBlock(12)
require.NoError(t, err)
assert.True(t, compatible)

compatible, err = vc.CompatibleAtBlock(13)
require.NoError(t, err)
assert.False(t, compatible)
}

// TestVersionBoundaryUpdated tests the behavior of the VersionControl component when the version is updated.
func TestVersionBoundaryUpdated(t *testing.T) {
signalCtx := irrecoverable.NewMockSignalerContext(t, context.Background())
Expand Down Expand Up @@ -487,6 +600,23 @@ func TestNotificationSkippedForCompatibleVersions(t *testing.T) {
assert.True(t, compatible)
}

// TestIsOverriden tests the isOverridden method of the VersionControl component correctly matches
// versions
func TestIsOverriden(t *testing.T) {
vc := &VersionControl{
compatibilityOverrides: map[string]struct{}{"0.0.1": {}},
}

assert.True(t, vc.isOverridden(semver.New("0.0.1")))
assert.True(t, vc.isOverridden(semver.New("0.0.1-pre-release")))

assert.False(t, vc.isOverridden(semver.New("0.0.2")))
assert.False(t, vc.isOverridden(semver.New("0.0.2-pre-release")))

assert.False(t, vc.isOverridden(semver.New("1.0.1")))
assert.False(t, vc.isOverridden(semver.New("0.1.1")))
}

func generateConsumerAssertions(
t *testing.T,
boundaryHeight uint64,
Expand All @@ -507,12 +637,29 @@ func generateConsumerAssertions(
return assertUpdate, assertCalled
}

func generateConsumerIgnoredAssertions(
t *testing.T,
) (func(uint64, *semver.Version), func()) {
called := false

assertUpdate := func(uint64, *semver.Version) {
called = true
}

assertNotCalled := func() {
assert.False(t, called)
}

return assertUpdate, assertNotCalled
}

// versionComponentTestConfigs contains custom tweaks for version control creation
type versionComponentTestConfigs struct {
nodeVersion string
versionBeacons storage.VersionBeacons
sealedRootBlockHeight uint64
latestFinalizedBlockHeight uint64
overrides map[string]struct{}
signalerContext *irrecoverable.MockSignalerContext
}

Expand All @@ -530,6 +677,10 @@ func createVersionControlComponent(
)
require.NoError(t, err)

if config.overrides != nil {
vc.compatibilityOverrides = config.overrides
}

// Start the VersionControl component.
vc.Start(config.signalerContext)
unittest.RequireComponentsReadyBefore(t, 2*time.Second, vc)
Expand Down

0 comments on commit fb6576e

Please sign in to comment.