Repository: Lilypad-Tech/lilypad
Pull request #544: feat: Add minimum version check
Author: login: bgins
Labels: name: feature
name: cla-signed
Created: 2025-03-18T21:18:29Z | Merged: 2025-03-19T22:00:05Z

### Summary

This pull request makes the following changes:

- [x] Add `SERVER_MINIMUM_VERSION` option and version config struct
- [x] Add minimum version check
- [x] Add version config and version check tests
- [x] Check version on resource offer and job offer post 

This pull request adds a minimum version check so we can ensure that job creators and resource providers stay up to date at a `SERVER_MINIMUM_VERSION` or newer.

### Task/Issue reference

Closes: #543 

### Test plan

We include the version at compile time, so testing this pull request will require a few compiled binaries. Start by downloading the `v2.13.0`, `v2.14.0`, and `v2.15.0` binaries from our releases page: https://github.com/Lilypad-Tech/lilypad/releases. Rename the binaries to `lilypad-v2.13.0`, `lilypad-v2.14.0`, and `lilypad-v2.15.0`

Start the base services. Start the solver with a `v2.14.0` minimum version.

```sh
./stack solver --server-minimum-version v2.14.0
```

We run the solver from source for this portion of testing because we are testing the `SERVER_MINIMUM_VERSION` setting and the compiled version does not matter.

> [!CAUTION]
> In this next portion of testing we use development-only private keys. Do not use these keys for production or any use case of consequence. Future reader you have been warned. 🔥 

Start a `v2.15.0` resource provider using our local dev private key.

```sh
WEB3_PRIVATE_KEY=0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a ./lilypad-v2.15.0 resource-provider --network dev --disable-pow true
```

Check the solver and the resource offer should increment to one resource offer.

Stop the resource provider and start the `v2.14.0` resource provider.

```sh
WEB3_PRIVATE_KEY=0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a ./lilypad-v2.14.0 resource-provider --network dev --disable-pow true
```

Once again, the resource offer should be accepted by the solver.

Stop the resource provider and start the `v2.13.0` resource provider.

```sh
WEB3_PRIVATE_KEY=0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a ./lilypad-v2.13.0 resource-provider --network dev --disable-pow true
```

The solver should start logging unsupported version errors and the resource provider will crash with the same error message after retries.

#### Semantic version

We fall back to a major version derived from the solver version when the minimum version is not set. To test this, we need to compile a binary for the solver at `v2.16.0` and another binary before `v2.0.0` for the resource provider.

```sh
go build -v -o lilypad-v2.16.0 -ldflags="-X 'github.com/lilypad-tech/lilypad/pkg/system.Version=v2.16.0'"
go build -v -o lilypad-v1.52.0 -ldflags="-X 'github.com/lilypad-tech/lilypad/pkg/system.Version=v1.52.0'"
```

Set the binaries to executable and then start the solver without a minimum version. The easiest way to do this is to temporarily update our `stack` solver command. Change this line:

https://github.com/Lilypad-Tech/lilypad/blob/922c3e79ced16022042affbca221b5c52389dfef/stack#L211

to

```sh
./lilypad-v2.16.0 solver --network dev "$@"
```

Then start the solver with `./stack solver`. This preserves our full local development configuration.

Now start `v1.52.0` resource provider:

```sh
WEB3_PRIVATE_KEY=0x47e179ec197488593b187f80a00eb0da91f1b9d0b13f8733639f19c30a34926a ./lilypad-v1.52.0 resource-provider --network dev --disable-pow true
```

The resource offer should be rejected because it is before the `v2` major version.

### Details

All versions assume a `v` prefix and will fail validation if it is missing.

#### Configs

The mininmum version can be set with an environment variable or a CLI option:

```sh
SERVER_MINIMUM_VERSION=v2.14.0 ./stack solver
./stack solver --server-minimum-version v2.14.0
```

#### Semantic versioning

The minimum version is a convenience until we move to strict semantic versioning. Once we move to strict semantic versioning, we can drop the `SERVER_MINIMUM_VERSION` config and use the solver version to determine the appropriate supported major version as minimum.

#### Local development

The solver does not have a version in local development. The `SERVER_MINIMUM_VERSION` config can be set to enforce a minimum, but the common case should avoid this because job creators and resource providers will also not have a version.


Diff:
diff --git a/cmd/lilypad/solver.go b/cmd/lilypad/solver.go
index e4e6153fa..6a07e1a55 100644
--- a/cmd/lilypad/solver.go
+++ b/cmd/lilypad/solver.go
@@ -79,7 +79,12 @@ func runSolver(cmd *cobra.Command, options solver.SolverOptions, network string,
 		return err
 	}
 
-	solverService, err := solver.NewSolver(options, solverStore, web3SDK, stats, tracer, meter)
+	versionConfig, err := system.NewVersionConfig(options.Server.AccessControl.MinimumVersion)
+	if err != nil {
+		return err
+	}
+
+	solverService, err := solver.NewSolver(options, solverStore, web3SDK, stats, tracer, meter, versionConfig)
 	if err != nil {
 		return err
 	}
diff --git a/pkg/http/types.go b/pkg/http/types.go
index c73dafca3..692e87ca6 100644
--- a/pkg/http/types.go
+++ b/pkg/http/types.go
@@ -15,6 +15,7 @@ type AccessControlOptions struct {
 	ValidationTokenKid              string
 	AnuraAddresses                  []string
 	OfferTimestampDiffSeconds       int
+	MinimumVersion                  string
 }
 
 type ValidationToken struct {
diff --git a/pkg/options/server.go b/pkg/options/server.go
index 32c7a0250..5973ebba0 100644
--- a/pkg/options/server.go
+++ b/pkg/options/server.go
@@ -26,6 +26,7 @@ func GetDefaultAccessControlOptions() http.AccessControlOptions {
 		ValidationTokenKid:              GetDefaultServeOptionString("SERVER_VALIDATION_TOKEN_KID", ""),
 		AnuraAddresses:                  GetDefaultServeOptionStringArray("SERVER_ANURA_ADDRESSES", []string{}),
 		OfferTimestampDiffSeconds:       GetDefaultServeOptionInt("SERVER_OFFER_TIMESTAMP_DIFF_SECONDS", 30),
+		MinimumVersion:                  GetDefaultServeOptionString("SERVER_MINIMUM_VERSION", ""),
 	}
 }
 
@@ -85,6 +86,10 @@ func AddServerCliFlags(cmd *cobra.Command, serverOptions *http.ServerOptions) {
 		&serverOptions.AccessControl.OfferTimestampDiffSeconds, "server-offer-timestamp-diff-seconds", serverOptions.AccessControl.OfferTimestampDiffSeconds,
 		`The diff before or after now when a job or resource offer must be received (SERVER_OFFER_TIMESTAMP_DIFF_SECONDS).`,
 	)
+	cmd.PersistentFlags().StringVar(
+		&serverOptions.AccessControl.MinimumVersion, "server-minimum-version", serverOptions.AccessControl.MinimumVersion,
+		`The minimum client version (SERVER_MINIMUM_VERSION).`,
+	)
 }
 
 func CheckServerOptions(options http.ServerOptions, storeType string) error {
diff --git a/pkg/solver/server.go b/pkg/solver/server.go
index 6b953681f..377e01417 100644
--- a/pkg/solver/server.go
+++ b/pkg/solver/server.go
@@ -30,12 +30,13 @@ import (
 )
 
 type solverServer struct {
-	options    http.ServerOptions
-	controller *SolverController
-	store      store.SolverStore
-	stats      stats.Stats
-	services   data.ServiceConfig
-	log        *zerolog.Logger
+	options       http.ServerOptions
+	controller    *SolverController
+	store         store.SolverStore
+	stats         stats.Stats
+	services      data.ServiceConfig
+	versionConfig *system.VersionConfig
+	log           *zerolog.Logger
 }
 
 func NewSolverServer(
@@ -43,14 +44,16 @@ func NewSolverServer(
 	controller *SolverController,
 	store store.SolverStore,
 	stats stats.Stats,
+	versionConfig *system.VersionConfig,
 	services data.ServiceConfig,
 ) (*solverServer, error) {
 	server := &solverServer{
-		options:    options,
-		controller: controller,
-		store:      store,
-		stats:      stats,
-		log:        system.GetLogger(system.SolverService),
+		options:       options,
+		controller:    controller,
+		store:         store,
+		stats:         stats,
+		versionConfig: versionConfig,
+		log:           system.GetLogger(system.SolverService),
 	}
 
 	metricsDashboard.Init(services.APIHost)
@@ -345,6 +348,17 @@ func (server *solverServer) addJobOffer(jobOffer data.JobOffer, res corehttp.Res
 		return nil, fmt.Errorf("job creator address does not match signer address")
 	}
 
+	versionHeader, _ := http.GetVersionFromHeaders(req)
+	minVersion, ok := server.versionConfig.IsSupported(versionHeader)
+	if !ok {
+		server.log.Debug().Str("cid", jobOffer.ID).
+			Str("address", jobOffer.JobCreator).
+			Str("version", versionHeader).
+			Str("minVersion", minVersion).
+			Msg("job offer rejected because job creator is running an unsupported version")
+		return nil, fmt.Errorf("Please update to minimum supported version %s or newer: https://github.com/Lilypad-Tech/lilypad/releases", minVersion)
+	}
+
 	offerRecent := isTimestampRecent(jobOffer.CreatedAt, server.options.AccessControl.OfferTimestampDiffSeconds*1000)
 	if !offerRecent {
 		server.log.Debug().Str("cid", jobOffer.ID).Str("address", jobOffer.JobCreator).Msg("job offer rejected because timestamp was not recent")
@@ -361,9 +375,6 @@ func (server *solverServer) addJobOffer(jobOffer data.JobOffer, res corehttp.Res
 }
 
 func (server *solverServer) addResourceOffer(resourceOffer data.ResourceOffer, res corehttp.ResponseWriter, req *corehttp.Request) (*data.ResourceOfferContainer, error) {
-	versionHeader, _ := http.GetVersionFromHeaders(req)
-	server.log.Debug().Str("version", versionHeader).Msg("resource provider adding offer with version header")
-
 	signerAddress, err := http.CheckSignature(req)
 	if err != nil {
 		server.log.Error().Err(err).Msg("error checking signature")
@@ -388,6 +399,17 @@ func (server *solverServer) addResourceOffer(resourceOffer data.ResourceOffer, r
 		}
 	}
 
+	versionHeader, _ := http.GetVersionFromHeaders(req)
+	minVersion, ok := server.versionConfig.IsSupported(versionHeader)
+	if !ok {
+		server.log.Debug().Str("cid", resourceOffer.ID).
+			Str("address", resourceOffer.ResourceProvider).
+			Str("version", versionHeader).
+			Str("minVersion", minVersion).
+			Msg("resource offer rejected because resource provider is running an unsupported version")
+		return nil, fmt.Errorf("Please update to minimum supported version %s or newer: https://github.com/Lilypad-Tech/lilypad/releases", minVersion)
+	}
+
 	offerRecent := isTimestampRecent(resourceOffer.CreatedAt, server.options.AccessControl.OfferTimestampDiffSeconds*1000)
 	if !offerRecent {
 		server.log.Debug().Str("cid", resourceOffer.ID).Str("address", resourceOffer.ResourceProvider).Msg("resource offer rejected because timestamp was not recent")
diff --git a/pkg/solver/solver.go b/pkg/solver/solver.go
index f8a8727ea..a1025a4eb 100644
--- a/pkg/solver/solver.go
+++ b/pkg/solver/solver.go
@@ -43,12 +43,13 @@ func NewSolver(
 	stats stats.Stats,
 	tracer trace.Tracer,
 	meter metric.Meter,
+	versionConfig *system.VersionConfig,
 ) (*Solver, error) {
 	controller, err := NewSolverController(web3SDK, store, options, tracer, meter)
 	if err != nil {
 		return nil, err
 	}
-	server, err := NewSolverServer(options.Server, controller, store, stats, options.Services)
+	server, err := NewSolverServer(options.Server, controller, store, stats, versionConfig, options.Services)
 	if err != nil {
 		return nil, err
 	}
diff --git a/pkg/system/version.go b/pkg/system/version.go
index 7054c045e..7d61aebd3 100644
--- a/pkg/system/version.go
+++ b/pkg/system/version.go
@@ -1,4 +1,66 @@
 package system
 
+import (
+	"fmt"
+
+	"github.com/rs/zerolog/log"
+	"golang.org/x/mod/semver"
+)
+
 var Version string
 var CommitSHA string
+
+type VersionConfig struct {
+	version        string
+	minimumVersion string
+}
+
+func NewVersionConfig(minimumVersion string) (*VersionConfig, error) {
+	log := GetLogger(SolverService)
+
+	if Version != "" && !semver.IsValid(Version) {
+		log.Error().Str("version", Version).Msg("invalid embedded version")
+		return nil, fmt.Errorf("invalid embedded version: %s", Version)
+	}
+
+	if minimumVersion != "" && !semver.IsValid(minimumVersion) {
+		log.Error().Str("minVersion", minimumVersion).Msg("invalid minimum version")
+		return nil, fmt.Errorf("invalid minimum version: %s", minimumVersion)
+	}
+
+	if Version != "" &&
+		minimumVersion != "" &&
+		semver.Compare(minimumVersion, Version) > 0 {
+		log.Error().Str("minVersion", minimumVersion).Str("version", Version).Msg("minimum version greater than embedded version")
+		return nil, fmt.Errorf("minimum version %s is greater than embedded version %s", minimumVersion, Version)
+	}
+
+	return &VersionConfig{
+		version:        Version,
+		minimumVersion: minimumVersion,
+	}, nil
+}
+
+func (vc *VersionConfig) IsSupported(version string) (string, bool) {
+	var minVersion string
+	if vc.minimumVersion != "" {
+		minVersion = vc.minimumVersion
+	} else if vc.version != "" {
+		// Use embedded major version when minimum version is not set.
+		// This option follows semantic versioning for breaking changes, so that the
+		// minimum version can be omitted when using semantic versioning.
+		minVersion = semver.Major(vc.version)
+	} else {
+		// Return supported when neither an embedded version nor minimum version is set.
+		// This condition supports local development.
+		return "", true
+	}
+
+	if !semver.IsValid(version) {
+		log.Warn().Msgf("invalid version %s", version)
+		return minVersion, false
+	}
+
+	result := semver.Compare(version, minVersion)
+	return minVersion, result >= 0 // True if version >= minVersion
+}
diff --git a/pkg/system/version_test.go b/pkg/system/version_test.go
new file mode 100644
index 000000000..bde4e6384
--- /dev/null
+++ b/pkg/system/version_test.go
@@ -0,0 +1,177 @@
+//go:build unit
+
+package system
+
+import (
+	"testing"
+)
+
+func TestIsSupported(t *testing.T) {
+	originalVersion := Version
+
+	tests := []struct {
+		name                string
+		embeddedVersion     string
+		minimumVersion      string
+		versionToCheck      string
+		expectedMinVersion  string
+		expectedIsSupported bool
+	}{
+		{
+			name:                "No embedded version nor minimum version returns true",
+			embeddedVersion:     "",
+			minimumVersion:      "",
+			versionToCheck:      "v0.1.0",
+			expectedMinVersion:  "",
+			expectedIsSupported: true,
+		},
+		{
+			name:                "No embedded version but has minimum version - newer version supported",
+			embeddedVersion:     "",
+			minimumVersion:      "v1.2.3",
+			versionToCheck:      "v1.3.0",
+			expectedMinVersion:  "v1.2.3",
+			expectedIsSupported: true,
+		},
+		{
+			name:                "No embedded version but has minimum version - older version not supported",
+			embeddedVersion:     "",
+			minimumVersion:      "v1.2.3",
+			versionToCheck:      "v1.1.0",
+			expectedMinVersion:  "v1.2.3",
+			expectedIsSupported: false,
+		},
+		{
+			name:                "No minimum version uses embedded major version - newer version supported",
+			embeddedVersion:     "v2.3.4",
+			minimumVersion:      "",
+			versionToCheck:      "v2.0.0",
+			expectedMinVersion:  "v2",
+			expectedIsSupported: true,
+		},
+		{
+			name:                "No minimum version uses embedded major version - older version not supported",
+			embeddedVersion:     "v2.3.4",
+			minimumVersion:      "",
+			versionToCheck:      "v1.9.9",
+			expectedMinVersion:  "v2",
+			expectedIsSupported: false,
+		},
+		{
+			name:                "Minimum version used when specified - newer version supported",
+			embeddedVersion:     "v3.4.5",
+			minimumVersion:      "v2.0.0",
+			versionToCheck:      "v2.1.0",
+			expectedMinVersion:  "v2.0.0",
+			expectedIsSupported: true,
+		},
+		{
+			name:                "Minimum version used when specified - exact match supported",
+			embeddedVersion:     "v3.4.5",
+			minimumVersion:      "v2.0.0",
+			versionToCheck:      "v2.0.0",
+			expectedMinVersion:  "v2.0.0",
+			expectedIsSupported: true,
+		},
+		{
+			name:                "Minimum version used when specified - older version not supported",
+			embeddedVersion:     "v3.4.5",
+			minimumVersion:      "v2.0.0",
+			versionToCheck:      "v1.9.9",
+			expectedMinVersion:  "v2.0.0",
+			expectedIsSupported: false,
+		},
+		{
+			name:                "Invalid version to check returns false",
+			embeddedVersion:     "v3.4.5",
+			minimumVersion:      "v2.0.0",
+			versionToCheck:      "invalid",
+			expectedMinVersion:  "v2.0.0",
+			expectedIsSupported: false,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			Version = tc.embeddedVersion
+
+			vc, err := NewVersionConfig(tc.minimumVersion)
+			if err != nil {
+				t.Fatalf("Failed to create VersionConfig: %v", err)
+			}
+
+			minVersion, isSupported := vc.IsSupported(tc.versionToCheck)
+
+			if minVersion != tc.expectedMinVersion {
+				t.Errorf("Expected min version %q, got %q", tc.expectedMinVersion, minVersion)
+			}
+
+			if isSupported != tc.expectedIsSupported {
+				t.Errorf("Expected isSupported to be %v, got %v", tc.expectedIsSupported, isSupported)
+			}
+		})
+	}
+
+	Version = originalVersion
+}
+
+func TestNewVersionConfig_Validation(t *testing.T) {
+	originalVersion := Version
+
+	tests := []struct {
+		name            string
+		embeddedVersion string
+		minimumVersion  string
+		expectError     bool
+	}{
+		{
+			name:            "Valid versions",
+			embeddedVersion: "v1.2.3",
+			minimumVersion:  "v1.0.0",
+			expectError:     false,
+		},
+		{
+			name:            "Invalid embedded version",
+			embeddedVersion: "1.2.3", // missing v prefix
+			minimumVersion:  "v1.0.0",
+			expectError:     true,
+		},
+		{
+			name:            "Invalid minimum version",
+			embeddedVersion: "v1.2.3",
+			minimumVersion:  "1.0.0", // missing v prefix
+			expectError:     true,
+		},
+		{
+			name:            "Minimum version greater than embedded version",
+			embeddedVersion: "v1.2.3",
+			minimumVersion:  "v2.0.0",
+			expectError:     true,
+		},
+		{
+			name:            "Empty embedded version is valid",
+			embeddedVersion: "",
+			minimumVersion:  "v1.0.0",
+			expectError:     false,
+		},
+		{
+			name:            "Empty minimum version is valid",
+			embeddedVersion: "v1.2.3",
+			minimumVersion:  "",
+			expectError:     false,
+		},
+	}
+
+	for _, tc := range tests {
+		t.Run(tc.name, func(t *testing.T) {
+			Version = tc.embeddedVersion
+
+			_, err := NewVersionConfig(tc.minimumVersion)
+			if (err != nil) != tc.expectError {
+				t.Errorf("Expected error: %v, got error: %v", tc.expectError, err)
+			}
+		})
+	}
+
+	Version = originalVersion
+}


Reviews:
user: login: narbs91
state: COMMENTED
author_association: COLLABORATOR
submitted_at: 2025-03-19T16:19:34Z
user: login: bgins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:23:53Z
user: login: bgins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:24:02Z
user: login: bgins
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T17:24:12Z
user: login: narbs91
body: LGTM!
state: APPROVED
author_association: COLLABORATOR
submitted_at: 2025-03-19T18:31:32Z
user: login: logan-wrld
body: AMAZING!!! TYSMMM
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-19T21:58:49Z

Review comments:
diff_hunk: @@ -345,6 +348,12 @@ func (server *solverServer) addJobOffer(jobOffer data.JobOffer, res corehttp.Res
 		return nil, fmt.Errorf("job creator address does not match signer address")
 	}
 
+	versionHeader, _ := http.GetVersionFromHeaders(req)
+	minVersion, ok := server.versionConfig.IsSupported(versionHeader)
+	if !ok {
+		return nil, fmt.Errorf("Please update to minimum supported version %s or newer: https://github.com/Lilypad-Tech/lilypad/releases", minVersion)
path: pkg/solver/server.go
user: login: narbs91
body: Let's add a log statement here too
created_at: 2025-03-19T16:13:43Z
author_association: COLLABORATOR
diff_hunk: @@ -388,6 +394,12 @@ func (server *solverServer) addResourceOffer(resourceOffer data.ResourceOffer, r
 		}
 	}
 
+	versionHeader, _ := http.GetVersionFromHeaders(req)
+	minVersion, ok := server.versionConfig.IsSupported(versionHeader)
+	if !ok {
+		return nil, fmt.Errorf("Please update to minimum supported version %s or newer: https://github.com/Lilypad-Tech/lilypad/releases", minVersion)
path: pkg/solver/server.go
user: login: narbs91
body: Same comment here around adding a log statement
created_at: 2025-03-19T16:14:39Z
author_association: COLLABORATOR
diff_hunk: @@ -1,4 +1,61 @@
 package system
 
+import (
+	"fmt"
+
+	"github.com/rs/zerolog/log"
+	"golang.org/x/mod/semver"
+)
+
 var Version string
 var CommitSHA string
+
+type VersionConfig struct {
+	version        string
+	minimumVersion string
+}
+
+func NewVersionConfig(minimumVersion string) (*VersionConfig, error) {
+	if Version != "" && !semver.IsValid(Version) {
+		return nil, fmt.Errorf("invalid embedded version: %s", Version)
+	}
+
+	if minimumVersion != "" && !semver.IsValid(minimumVersion) {
+		return nil, fmt.Errorf("invalid minimum version: %s", minimumVersion)
+	}
+
+	if Version != "" &&
+		minimumVersion != "" &&
+		semver.Compare(minimumVersion, Version) > 0 {
+		return nil, fmt.Errorf("minimum version %s is greater than embedded version %s", minimumVersion, Version)
path: pkg/system/version.go
user: login: narbs91
body: Let's add log statements inside these blocks at least initially to help us debug/troubleshoot
created_at: 2025-03-19T16:16:34Z
author_association: COLLABORATOR
diff_hunk: @@ -345,6 +348,12 @@ func (server *solverServer) addJobOffer(jobOffer data.JobOffer, res corehttp.Res
 		return nil, fmt.Errorf("job creator address does not match signer address")
 	}
 
+	versionHeader, _ := http.GetVersionFromHeaders(req)
+	minVersion, ok := server.versionConfig.IsSupported(versionHeader)
+	if !ok {
+		return nil, fmt.Errorf("Please update to minimum supported version %s or newer: https://github.com/Lilypad-Tech/lilypad/releases", minVersion)
path: pkg/solver/server.go
user: login: bgins
body: Yeah good call! Added in db9caa2b0b0609898642732b33941014cba8b448
created_at: 2025-03-19T17:23:53Z
author_association: CONTRIBUTOR
diff_hunk: @@ -388,6 +394,12 @@ func (server *solverServer) addResourceOffer(resourceOffer data.ResourceOffer, r
 		}
 	}
 
+	versionHeader, _ := http.GetVersionFromHeaders(req)
+	minVersion, ok := server.versionConfig.IsSupported(versionHeader)
+	if !ok {
+		return nil, fmt.Errorf("Please update to minimum supported version %s or newer: https://github.com/Lilypad-Tech/lilypad/releases", minVersion)
path: pkg/solver/server.go
user: login: bgins
body: Added in db9caa2b0b0609898642732b33941014cba8b448
created_at: 2025-03-19T17:24:02Z
author_association: CONTRIBUTOR
diff_hunk: @@ -1,4 +1,61 @@
 package system
 
+import (
+	"fmt"
+
+	"github.com/rs/zerolog/log"
+	"golang.org/x/mod/semver"
+)
+
 var Version string
 var CommitSHA string
+
+type VersionConfig struct {
+	version        string
+	minimumVersion string
+}
+
+func NewVersionConfig(minimumVersion string) (*VersionConfig, error) {
+	if Version != "" && !semver.IsValid(Version) {
+		return nil, fmt.Errorf("invalid embedded version: %s", Version)
+	}
+
+	if minimumVersion != "" && !semver.IsValid(minimumVersion) {
+		return nil, fmt.Errorf("invalid minimum version: %s", minimumVersion)
+	}
+
+	if Version != "" &&
+		minimumVersion != "" &&
+		semver.Compare(minimumVersion, Version) > 0 {
+		return nil, fmt.Errorf("minimum version %s is greater than embedded version %s", minimumVersion, Version)
path: pkg/system/version.go
user: login: bgins
body: Same, added in db9caa2b0b0609898642732b33941014cba8b448
created_at: 2025-03-19T17:24:11Z
author_association: CONTRIBUTOR

PR comments:
user: login: logan-wrld
created_at: 2025-03-19T07:12:43Z
body: Hey @bgins! Awesome testing instructions! The preparation and the first two steps(startup a v2.14 Solver using the minimum version cmd and a v2.15 RP And then... a 2.14 RP) were completed without any issues. 

After that, I was able to activate a v2.13 RP connected to the same solver(`./stack solver --server-minimum-version v2.14.0`) **without** the RP crashing. 

I left the RP running with the solver for over 30 minutes to see if the RP eventually timed out(it did not).

- From your instructions, the RP should have crashed correct? 

Below is a screenshot of the RP connecting to the Solver: 
![Screenshot from 2025-03-19 01-58-44](https://github.com/user-attachments/assets/acf5feca-29f8-4ec0-a27e-b9237934904f)

Just to see what happens when running a cowsay in this configuration, the Solver gives me this error `...server.go:2220 > error="Please update to minimum supported version v2.14.0 or newer: https://github.com/Lilypad-Tech/lilypad/releases" method POST=/api/v1/job_offers` 

![Screenshot from 2025-03-19 02-02-23](https://github.com/user-attachments/assets/42abc779-75ad-4d3e-9f6f-5babfbdc9874)
author_association: CONTRIBUTOR
user: login: bgins
created_at: 2025-03-19T15:46:24Z
body: > The preparation and the first two steps(startup a v2.14 Solver using the minimum version cmd and a v2.15 RP And then... a 2.14 RP) were completed without any issues.
> 
> After that, I was able to activate a v2.13 RP connected to the same solver(`./stack solver --server-minimum-version v2.14.0`) **without** the RP crashing.

The solver command in the second step looks correct, and the resource provider should have crashed well before 30 minutes. The solver should also start logging not supported messages immediately.

> Just to see what happens when running a cowsay in this configuration, the Solver gives me this error `...server.go:2220 > error="Please update to minimum supported version v2.14.0 or newer: https://github.com/Lilypad-Tech/lilypad/releases" method POST=/api/v1/job_offers`

Yeah, this is what the unsupported version case should look like.

Let's talk through the testing later today. Would like to check a couple details in your setup, and it'll be faster to discuss it live.


author_association: CONTRIBUTOR