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

[Access] Add support for ignoring version beacon events for compatible versions #6535

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading