mirror of
https://github.com/luxfi/cli.git
synced 2026-10-30 17:01:20 +00:00
325 lines
10 KiB
Go
325 lines
10 KiB
Go
// Copyright (C) 2022, Lux Industries Inc. All rights reserved.
|
|
// See the file LICENSE for licensing terms.
|
|
|
|
package utils
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"sort"
|
|
"strconv"
|
|
"sync"
|
|
|
|
"github.com/luxfi/cli/v2/pkg/application"
|
|
"github.com/luxfi/cli/v2/pkg/binutils"
|
|
"github.com/luxfi/cli/v2/pkg/constants"
|
|
"github.com/luxfi/cli/v2/pkg/models"
|
|
"github.com/luxfi/cli/v2/pkg/vm"
|
|
luxlog "github.com/luxfi/log"
|
|
"golang.org/x/mod/semver"
|
|
)
|
|
|
|
var (
|
|
binaryToVersion map[string]string
|
|
lock = &sync.Mutex{}
|
|
|
|
_ VersionMapper = &versionMapper{}
|
|
)
|
|
|
|
/*
|
|
VersionMapper keys and their usage:
|
|
* OnlyLuxKey: Used when running one node only (no compatibility required)
|
|
|
|
* MultiLux1Key Used for the update scenario where node is updated and
|
|
* MultiLux2Key both node versions need to be compatible.
|
|
* MultiLuxEVMKey This is the Lux EVM version compatible to the above scenario.
|
|
|
|
* LatestEVM2LuxKey Latest evm version
|
|
* LatestLux2EVMKey while this is the latest node compatible with that evm
|
|
|
|
* SoloEVMKey1 This is used when we want to test evm versions where compatibility
|
|
* SoloEVMKey2 needs to be between the two evm versions
|
|
(latest might not be compatible with second latest)
|
|
|
|
|
|
*/
|
|
|
|
// VersionMapper is an abstraction for retrieving version compatibility URLs
|
|
// allowing unit tests without requiring external http calls.
|
|
// The idea is to finally calculate which VM is compatible with which Lux,
|
|
// so that the e2e tests can always download and run the latest compatible versions,
|
|
// without having to manually update the e2e tests periodically.
|
|
type VersionMapper interface {
|
|
GetCompatURL(vmType models.VMType) string
|
|
GetLuxURL() string
|
|
GetApp() *application.Lux
|
|
GetLatestLuxByProtoVersion(app *application.Lux, rpcVersion int, url string) (string, error)
|
|
GetEligibleVersions(sortedVersions []string, repoName string, app *application.Lux) ([]string, error)
|
|
}
|
|
|
|
// NewVersionMapper returns the default VersionMapper for e2e tests
|
|
func NewVersionMapper() VersionMapper {
|
|
app := &application.Lux{
|
|
Downloader: application.NewDownloader(),
|
|
Log: luxlog.NewNoOpLogger(),
|
|
}
|
|
return &versionMapper{
|
|
app: app,
|
|
}
|
|
}
|
|
|
|
// versionMapper is the default implementation for version mapping.
|
|
// It downloads compatibility URLs from the actual github endpoints
|
|
type versionMapper struct {
|
|
app *application.Lux
|
|
}
|
|
|
|
// GetLatestLuxByProtoVersion returns the latest Lux version which
|
|
// runs with the specified rpcVersion, or an error if it can't be found
|
|
// (or other errors occurred)
|
|
func (*versionMapper) GetLatestLuxByProtoVersion(app *application.Lux, rpcVersion int, url string) (string, error) {
|
|
return vm.GetLatestLuxByProtocolVersion(app, rpcVersion, url)
|
|
}
|
|
|
|
// GetApp returns the Lux application instance
|
|
func (m *versionMapper) GetApp() *application.Lux {
|
|
return m.app
|
|
}
|
|
|
|
// GetCompatURL returns the compatibility URL for the given VM type
|
|
func (*versionMapper) GetCompatURL(vmType models.VMType) string {
|
|
switch vmType {
|
|
case models.EVM:
|
|
return constants.EVMRPCCompatibilityURL
|
|
case models.CustomVM:
|
|
// TODO: unclear yet what we should return here
|
|
return ""
|
|
default:
|
|
return ""
|
|
}
|
|
}
|
|
|
|
// GetLuxURL returns the compatibility URL for Lux
|
|
func (*versionMapper) GetLuxURL() string {
|
|
return constants.LuxCompatibilityURL
|
|
}
|
|
|
|
func (*versionMapper) GetEligibleVersions(sortedVersions []string, repoName string, app *application.Lux) ([]string, error) {
|
|
// get latest lux release to make sure we're not picking a release currently in progress but not available for download
|
|
latest, err := app.Downloader.GetLatestReleaseVersion(binutils.GetGithubLatestReleaseURL(
|
|
constants.LuxOrg,
|
|
repoName,
|
|
))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var eligible []string
|
|
for i, ver := range sortedVersions {
|
|
versionComparison := semver.Compare(ver, latest)
|
|
if versionComparison != 0 {
|
|
continue
|
|
}
|
|
eligible = sortedVersions[i:]
|
|
break
|
|
}
|
|
|
|
return eligible, nil
|
|
}
|
|
|
|
// GetVersionMapping returns a map of specific VMs resp. Lux e2e context keys
|
|
// to the actual version which corresponds to that key.
|
|
// This allows the e2e test to know what version to download and run.
|
|
// Returns an error if there was a problem reading the URL compatibility json
|
|
// or some other issue.
|
|
func GetVersionMapping(mapper VersionMapper) (map[string]string, error) {
|
|
// ginkgo can run tests in parallel. However, we really just need this mapping to be
|
|
// performed once for the whole duration of a test.
|
|
// Therefore we store the result in a global variable, and then lock
|
|
// the access to it.
|
|
lock.Lock()
|
|
defer lock.Unlock()
|
|
// if mapping has already been done, return it right away
|
|
if binaryToVersion != nil {
|
|
return binaryToVersion, nil
|
|
}
|
|
// get compatible versions for subnetEVM
|
|
// subnetEVMversions is a list of sorted EVM versions,
|
|
// subnetEVMmapping maps EVM versions to their RPC versions
|
|
subnetEVMversions, subnetEVMmapping, err := getVersions(mapper, models.EVM)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// evm publishes its upcoming new version in the compatibility json
|
|
// before the new version is actually a downloadable release
|
|
subnetEVMversions, err = mapper.GetEligibleVersions(subnetEVMversions, constants.EVMRepoName, mapper.GetApp())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// now get the node compatibility object
|
|
luxCompat, err := getLuxCompatibility(mapper)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// create the global mapping variable
|
|
binaryToVersion = make(map[string]string)
|
|
|
|
// sort lux compatibility by highest available RPC versions
|
|
// to lowest (the map can not be iterated in a sorted way)
|
|
rpcs := make([]int, 0, len(luxCompat))
|
|
for k := range luxCompat {
|
|
// cannot use string sort
|
|
kint, err := strconv.Atoi(k)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
rpcs = append(rpcs, kint)
|
|
}
|
|
sort.Sort(sort.Reverse(sort.IntSlice(rpcs)))
|
|
|
|
// iterate the rpc versions
|
|
// evaluate two node versions which are consecutive
|
|
// and run with the same RPC version.
|
|
// This is required for the for the "can deploy with multiple node versions" test
|
|
for _, rpcVersion := range rpcs {
|
|
versionAsString := strconv.Itoa(rpcVersion)
|
|
versionsForRPC := luxCompat[versionAsString]
|
|
// we need at least 2 versions for the same RPC version
|
|
if len(versionsForRPC) > 1 {
|
|
versionsForRPC = reverseSemverSort(versionsForRPC)
|
|
binaryToVersion[MultiLux1Key] = versionsForRPC[0]
|
|
binaryToVersion[MultiLux2Key] = versionsForRPC[1]
|
|
|
|
// now iterate the subnetEVMversions and find a
|
|
// evm version which is compatible with that RPC version.
|
|
// The above-mentioned test runs with this as well.
|
|
for _, evmVer := range subnetEVMversions {
|
|
if subnetEVMmapping[evmVer] == rpcVersion {
|
|
// we know there already exists at least one such combination.
|
|
// unless the compatibility JSON will start to be shortened in some way,
|
|
// we should always be able to find a matching evm
|
|
binaryToVersion[MultiLuxEVMKey] = evmVer
|
|
// found the version, break
|
|
break
|
|
}
|
|
}
|
|
// all good, don't need to look more
|
|
break
|
|
}
|
|
}
|
|
|
|
// when running Lux only, always use latest
|
|
binaryToVersion[OnlyLuxKey] = OnlyLuxValue
|
|
|
|
// now let's look for evm versions which are fit for the
|
|
// "can deploy multiple evm versions" test.
|
|
// We need two evm versions which run the same RPC version,
|
|
// and then a compatible Lux
|
|
//
|
|
// To avoid having to iterate again, we'll also fill the values
|
|
// for the **latest** compatible Lux and Lux EVM
|
|
for i, ver := range subnetEVMversions {
|
|
// safety check, should not happen, as we already know
|
|
// compatible versions exist
|
|
if i+1 == len(subnetEVMversions) {
|
|
return nil, errors.New("no compatible versions for subsequent EVM found")
|
|
}
|
|
first := ver
|
|
second := subnetEVMversions[i+1]
|
|
// we should be able to safely assume that for a given evm RPC version,
|
|
// there exists at least one compatible Lux.
|
|
// This means we can in any case use this to set the **latest** compatibility
|
|
soloLux, err := mapper.GetLatestLuxByProtoVersion(mapper.GetApp(), subnetEVMmapping[first], mapper.GetLuxURL())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
// Once latest compatibility has been set, we can skip this
|
|
if binaryToVersion[LatestEVM2LuxKey] == "" {
|
|
binaryToVersion[LatestEVM2LuxKey] = first
|
|
binaryToVersion[LatestLux2EVMKey] = soloLux
|
|
}
|
|
// first and second are compatible
|
|
if subnetEVMmapping[first] == subnetEVMmapping[second] {
|
|
binaryToVersion[SoloEVMKey1] = first
|
|
binaryToVersion[SoloEVMKey2] = second
|
|
binaryToVersion[SoloLuxKey] = soloLux
|
|
break
|
|
}
|
|
}
|
|
|
|
return binaryToVersion, nil
|
|
}
|
|
|
|
// getVersions gets compatible versions for the given VM type.
|
|
// Returns a correctly ordered list of semantic version strings,
|
|
// from latest to oldest, and a map of version to rpc
|
|
func getVersions(mapper VersionMapper, vmType models.VMType) ([]string, map[string]int, error) {
|
|
compat, err := getCompatibility(mapper, vmType)
|
|
if err != nil {
|
|
return nil, nil, err
|
|
}
|
|
mapping := compat.RPCChainVMProtocolVersion
|
|
if len(mapping) == 0 {
|
|
return nil, nil, errors.New("zero length rpcs")
|
|
}
|
|
versions := make([]string, len(mapping))
|
|
if len(versions) == 0 {
|
|
return nil, nil, errors.New("zero length versions")
|
|
}
|
|
i := 0
|
|
for v := range mapping {
|
|
versions[i] = v
|
|
i++
|
|
}
|
|
|
|
versions = reverseSemverSort(versions)
|
|
return versions, mapping, nil
|
|
}
|
|
|
|
// getCompatibility returns the compatibility object for the given VM type
|
|
func getCompatibility(mapper VersionMapper, vmType models.VMType) (models.VMCompatibility, error) {
|
|
compatibilityBytes, err := mapper.GetApp().GetDownloader().Download(mapper.GetCompatURL(vmType))
|
|
if err != nil {
|
|
return models.VMCompatibility{}, err
|
|
}
|
|
|
|
var parsedCompat models.VMCompatibility
|
|
if err = json.Unmarshal(compatibilityBytes, &parsedCompat); err != nil {
|
|
return models.VMCompatibility{}, err
|
|
}
|
|
return parsedCompat, nil
|
|
}
|
|
|
|
// getLuxCompatibility returns the compatibility for Lux
|
|
func getLuxCompatibility(mapper VersionMapper) (models.LuxCompatiblity, error) {
|
|
luxBytes, err := mapper.GetApp().GetDownloader().Download(mapper.GetLuxURL())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
var luxCompat models.LuxCompatiblity
|
|
if err = json.Unmarshal(luxBytes, &luxCompat); err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return luxCompat, nil
|
|
}
|
|
|
|
// For semantic version slices, we can't just reverse twice:
|
|
// the semver packages only has increasing `Sort`, while
|
|
// `sort.Sort(sort.Reverse(sort.StringSlice(sliceSortedWithSemverSort)))`
|
|
// again fails to sort correctly (as it will sort again with string sorting
|
|
// instead of semantic versioning)
|
|
func reverseSemverSort(slice []string) []string {
|
|
semver.Sort(slice)
|
|
reverse := make([]string, len(slice))
|
|
for i, s := range slice {
|
|
idx := len(slice) - (1 + i)
|
|
reverse[idx] = s
|
|
}
|
|
return reverse
|
|
}
|