Skip to content

feat(sdk): gate supernode candidates by minimum version (>= 2.5.0)#293

Merged
mateeullahmalik merged 2 commits into
masterfrom
feat/sdk-gate-min-supernode-version
May 12, 2026
Merged

feat(sdk): gate supernode candidates by minimum version (>= 2.5.0)#293
mateeullahmalik merged 2 commits into
masterfrom
feat/sdk-gate-min-supernode-version

Conversation

@mateeullahmalik
Copy link
Copy Markdown
Collaborator

Summary

Adds an SDK-side compatibility gate that refuses to upload to supernodes running a version older than pkg/version.MinSupernodeVersion (currently 2.5.0).

Closes the only real activation-hazard surfaced in the v1.11.1 → v1.12.0 upgrade-survival review: an SDK client that has adopted the post-upgrade contract (e.g. LEP-5 AvailabilityCommitment at register, requiring ChunkProof[] at finalize) being paired with a pre-LEP-5 supernode during the asynchronous mainnet rollout window.

Why

Side Upgrade timing
Chain Atomic at the halt height — no staging possible
Supernode fleet Asynchronous over hours-to-days — operators control their own boxes; we cannot force a schedule

Without a gate, a v1.12.0 client paired with a v2.4.x supernode submits an action that no SN can finalize. The action sits in PROCESSING until expiry → per-action data loss, no chain-wide impact, but visibly broken uploads during the rollout window.

The SDK is the only layer that can read both sides (chain params + candidate SN's reported version) and refuse the pairing. This PR is that gate.

What

File Change
pkg/version/min_supernode.go New const MinSupernodeVersion = "2.5.0" — single source of truth
pkg/version/compatibility.go New IsCompatibleSupernodeVersion(reported string) bool using Masterminds/semver/v3. Floor is compared as 2.5.0-0 so all 2.5.0-rc* / -beta / +meta pre-releases are eligible. Fail-closed on empty / unparseable input.
pkg/version/compatibility_test.go 25 table-driven cases covering all four invariants below
sdk/task/task.go filterEligibleSupernodesParallel consults StatusResponse.version from the existing per-candidate GetSupernodeStatus probe (no new RPC). Rejected nodes get a clear reason: supernode version X is below SDK minimum 2.5.0.

Invariants

# Invariant Enforcement point Test
I1 No SN with version < 2.5.0 reaches the upload step filterEligibleSupernodesParallel (sole filter entry) 8 rows (floor, above-floor, way-above; pre-floor patch/minor/major)
I2 Empty / unparseable / missing version → ineligible (fail-closed) Same point 6 rows (empty, whitespace, garbage, partial, letters, negative)
I3 Single source of truth for the version floor pkg/version/min_supernode.go const Grep verifies no inline "2.5.0" literal outside pkg/version
I4 2.5.0-rc*, -beta, +build of the floor base are eligible Semver comparison vs 2.5.0-0 7 rows incl. 2.5.0-rc1, v2.5.0-rc2, 2.5.0-rc2+build.5, -beta, -alpha.1, -0 boundary, and negative 2.4.99-rc1

Test output

=== RUN   TestIsCompatibleSupernodeVersion
--- PASS: TestIsCompatibleSupernodeVersion (0.00s)
    --- PASS: floor_exact_2.5.0
    --- PASS: floor_with_v_prefix
    --- PASS: patch_above_floor
    --- PASS: rc1_of_floor              <-- 2.5.0-rc1 is eligible
    --- PASS: rc2_of_floor_with_v       <-- v2.5.0-rc2 is eligible
    --- PASS: rc_with_build_meta        <-- 2.5.0-rc2+build.5 is eligible
    --- PASS: rc_of_pre-floor_version   <-- 2.4.99-rc1 is rejected
    --- PASS: one_patch_below_floor     <-- 2.4.72 is rejected
    --- PASS: empty_string              <-- fail-closed
    --- PASS: garbage                   <-- fail-closed
    ... (25 cases total)
=== RUN   TestMinSupernodeVersion_IsValidSemver
--- PASS: TestMinSupernodeVersion_IsValidSemver
ok      github.com/LumeraProtocol/supernode/v2/pkg/version
ok      github.com/LumeraProtocol/supernode/v2/sdk/task

Rollout semantics

  • 2.5.0-rc1 is eligible — this is intentional and confirmed with @mateeullahmalik. The forthcoming SN release will tag as v2.5.0-rc1 first; the gate must let it through.
  • 2.4.72 and earlier are rejected — that is the entire point.
  • v2.5.0 is eligible — leading v is tolerated.

What is NOT in this PR

  • Chain-side enforcement — out of scope. SDK is the available lever.
  • sdk-go / sdk-js / sdk-rs direct changessdk-go picks this up automatically on the next supernode dep bump (it imports pkg/version transitively via sdk/task). sdk-js and sdk-rs need their own gates (follow-up issues to file).
  • Full live-devnet E2E — deferred until a real v2.5.0-rc1 supernode binary is cut. Today there is no SN tag at or above the floor, so a live test would just confirm that the gate rejects everything (which the unit tests already prove deterministically). A live E2E should be run as part of the v2.5.0-rc1 release validation against a mixed-version fleet (one stale v2.4.72 + one v2.5.0-rc1). I'll run it then via the cascade-register-rs-snapi skill.

Risks & mitigations

Risk Mitigation
Status RPC returns empty version (regression in a future SN build) Fail-closed: such an SN is rejected. Visible in logs with a clear reason.
Some future SN release reports version via a different field Caught immediately — every cascade will fail with version <unreported> is below SDK minimum 2.5.0. No silent passthrough.
Const drift over time (someone hard-codes "2.5.0" elsewhere) I3 invariant — grep before merge of any future PR. CI lint could enforce.
Floor accidentally set to an unparseable string TestMinSupernodeVersion_IsValidSemver guards this — package will not compile cleanly.

Rollback

Revert is safe and clean: the change is additive within an already-failing branch of filterEligibleSupernodesParallel. Reverting removes the version check; the existing peers / health / balance gates remain.

Add an SDK-side compatibility gate that refuses to upload to supernodes
running a version older than pkg/version.MinSupernodeVersion (2.5.0).

WHY
---
The chain upgrades atomically at the halt height; the supernode fleet
upgrades asynchronously over hours-to-days. During a rollout window an
SDK client that already speaks the post-upgrade contract (e.g. LEP-5
AvailabilityCommitment at register, requiring ChunkProof[] at finalize
on v1.12.0) can be paired with a supernode that lacks the matching code
path. The action then stalls in PROCESSING until expiry: per-action
data loss, no chain-wide impact, but a poor UX during rollouts.

The SDK is the single layer that can read both sides (chain params and
the candidate supernode's reported version) and refuse the pairing.
Mainnet operators choose their own upgrade timing, so a client-side
gate is the only available lever.

WHAT
----
* pkg/version/min_supernode.go — single source of truth constant
  MinSupernodeVersion = "2.5.0". Pre-release suffixes on the floor
  base (e.g. "2.5.0-rc1") are treated as eligible by design.
* pkg/version/compatibility.go — IsCompatibleSupernodeVersion(string)
  using Masterminds/semver/v3. Compares against floor "2.5.0-0" so any
  2.5.0 pre-release is accepted. Fail-closed on empty / unparseable
  input: a supernode that cannot prove its version is presumed stale.
* sdk/task/task.go — filterEligibleSupernodesParallel now consults the
  StatusResponse.version returned by the existing per-candidate
  GetSupernodeStatus probe (no additional RPC). Failing nodes are
  rejected with a clear reason that includes both reported and minimum
  versions.

INVARIANTS
----------
| # | Invariant                                              | Enforcement          |
|---|--------------------------------------------------------|----------------------|
| I1| No SN < MinSupernodeVersion reaches upload             | filter (sole entry)  |
| I2| Missing / unparseable version is rejected (fail-closed)| same point           |
| I3| Single source of truth for the floor                   | pkg/version constant |
| I4| 2.5.0-rc* / -beta / +build accepted                    | semver -0 floor      |

TESTS
-----
* pkg/version/compatibility_test.go covers 25 rows including the floor,
  patch/minor/major above, all rc/beta/alpha forms of 2.5.0, the -0
  boundary, pre-floor rcs (must reject), empty / whitespace / garbage,
  semver-padded inputs ("2.5", "2"), and a self-compatibility guard
  that protects against an unparseable MinSupernodeVersion constant.

NOT IN THIS PR
--------------
* Chain-side enforcement (out of scope; SDK is the available lever).
* sdk-go / sdk-js / sdk-rs — sdk-go inherits this automatically on
  next dep bump; the other SDKs need their own gates (follow-ups).
* End-to-end live devnet test: deferred until a v2.5.0-rc1 supernode
  binary exists. The 25-row unit-test matrix proves the comparator
  contract today.
The cascade-e2e CI job was failing on this branch with
"no eligible supernodes to register" (SDK event:
sdk:supernodes_found count=0 total=3).

Root cause: tests/scripts/setup-supernodes.sh built the test binary
with -ldflags="-s -w" only, leaving supernode/cmd.Version at its
default "dev". The supernode metrics collector parses "dev" via
supernode/supernode_metrics/metrics_collection.go::parseVersion,
which falls back to [2,0,0] for unparseable inputs. StatusResponse
then reports version="dev"; the new SDK gate
(pkg/version.IsCompatibleSupernodeVersion) rejects it as it is
below MinSupernodeVersion=2.5.0, and filterEligibleSupernodesParallel
drops every candidate.

Fix: inject SUPERNODE_TEST_VERSION (default "2.5.0-test") via -X
into cmd.Version, exactly mirroring the production Makefile's
LDFLAGS pattern. The "-test" suffix is a valid semver prerelease
on the 2.5.0 base, so the gate accepts it.

Sibling audit: only one go-build call site exists in
tests/scripts/setup-supernodes.sh (setup_primary); setup_secondary
copies the primary binary. The tests/system/e2e_sn_manager_test.go
buildSN call is for the sn-manager-e2e-tests job, which is currently
commented out in .github/workflows/tests.yml and does not exercise
the cascade SDK gate. Out of scope for this fix.

Local validation:
  go build -ldflags=\"-X .../cmd.Version=2.5.0-test\" ...
  ./supernode version  -> "Version: 2.5.0-test"
  IsCompatibleSupernodeVersion("2.5.0-test") -> true
@mateeullahmalik mateeullahmalik requested a review from j-rafique May 12, 2026 14:19
@mateeullahmalik mateeullahmalik self-assigned this May 12, 2026
@mateeullahmalik mateeullahmalik merged commit 8a38557 into master May 12, 2026
7 checks passed
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.

2 participants