feat(registry,attestation): trust + capability registry foundation

Build the GPU-runtime trust registry that gates every dispatch decision
against typed capability records and a deterministic attestation root.

Reference: LP-137-TRUST-REGISTRY.md.

pkg/registry:
  - types.go: WorkerCapability, NodeCapability, GPUArch, GPUBackend,
    PeerInterconnect with Validate() that enforces TEE-arch + attestation
    invariants at the API edge.
  - policy.go: LanePolicy (operator-controlled), WorkloadPolicy
    (per-request), and a single canonical Eligible(lane, workload, worker)
    check that runs twelve gates in fixed order. Privacy-class consistency
    is re-checked on the hot path so a stale worker record cannot bypass
    the API edge.

pkg/attestation:
  - composite.go: CompositeAttestation envelope over CPU TEE + GPU TEE +
    platform evidence. Root() is RFC 8949 §4.2 deterministic CBOR via
    fxamacker/cbor (already a direct dep) followed by SHA-256. The same
    encoder is used by Marshal so Root and the wire form agree byte-for-
    byte. Validate() rejects malformed envelopes including
    composite-trust-without-GPU-evidence.

Tests (103 pass): truth-table eligibility coverage, gate-ordering
regression, sensitivity-to-every-field digest, deterministic-bytes
round-trip, garbage-decode rejection.

go.mod: promote luxfi/lattice/v7 from indirect to direct require, add
local replace pointing at ../lattice (mirrors the existing fhe pattern).
Drop the replace once luxfi/lattice publishes a tag containing
types/trust.go.
This commit is contained in:
2025-12-28 16:47:09 -08:00
parent 6ebe743aa9
commit c2eb4b481f
7 changed files with 2065 additions and 1 deletions
+6 -1
View File
@@ -42,6 +42,7 @@ require (
github.com/nats-io/nats.go v1.50.0
github.com/pquerna/otp v1.5.0
github.com/rs/zerolog v1.35.0
github.com/luxfi/lattice/v7 v7.0.2
github.com/samber/lo v1.53.0
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
@@ -147,7 +148,6 @@ require (
github.com/luxfi/container v0.0.4 // indirect
github.com/luxfi/geth v1.16.79 // indirect
github.com/luxfi/ids v1.2.9 // indirect
github.com/luxfi/lattice/v7 v7.0.2 // indirect
github.com/luxfi/math v1.2.4 // indirect
github.com/luxfi/math/big v0.1.0 // indirect
github.com/luxfi/mdns v0.1.0 // indirect
@@ -239,6 +239,11 @@ replace github.com/luxfi/log v1.3.0 => github.com/luxfi/log v1.4.1
// release tag that includes the threshold/ package.
replace github.com/luxfi/fhe => ../fhe
// Local development against the trust enums added in
// luxfi/lattice/v7/types (LP-137-TRUST-REGISTRY). Drop this once
// luxfi/lattice cuts a release tag that includes types/trust.go.
replace github.com/luxfi/lattice/v7 => ../lattice
// luxfi/genesis v1.5.21 / v1.5.22 are missing from proxy.golang.org; no
// luxfi/mpc code path imports them, but transitive resolution can pick them
// up. Excluded until genesis retags.
+327
View File
@@ -0,0 +1,327 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
// Package attestation defines the composite attestation envelope that the
// Lux GPU Runtime uses to bind worker capability records to verifiable
// hardware evidence. A CompositeAttestation aggregates evidence from
// multiple roots of trust:
//
// - CPU TEE quote (AMD SEV-SNP, Intel TDX, ARM CCA, ...)
// - GPU attestation report (NVIDIA NRAS for Hopper / Blackwell, AMD
// equivalents when published)
// - Platform / firmware measurements (UEFI, kernel, microcode)
// - Optional verifier-specific evidence (Intel Trust Authority verdict,
// NRAS device-state report, fraud-proof commitment, ZK proof)
//
// The Root() method returns the SHA-256 of the deterministically encoded
// envelope. That root is what:
//
// - the worker stores in WorkerCapability.AttestationRoot
// - the scheduler logs and indexes
// - downstream verifiers re-derive locally to detect tampering
//
// Determinism is RFC 8949 §4.2 Core Deterministic CBOR via fxamacker/cbor.
// We chose CBOR (not custom binary) because the off-chain composite
// attestation envelope shape is the de-facto standard used by NVIDIA NRAS
// and Intel Trust Authority output, and because mpc already uses CBOR for
// the canonical-intent + FROST/LSS config marshalers (see
// pkg/intent/canonical.go::Digest). One library, one canonical encoding.
//
// References:
//
// - LP-137-TRUST-REGISTRY.md
// - NVIDIA Remote Attestation Service (NRAS) report v1, 2024
// - NVIDIA Confidential Compute Whitepaper, Hopper / Blackwell
// - Intel Trust Authority composite attestation specification, 2024
// - RFC 8949 (CBOR), RFC 9090 (CBOR Tags)
package attestation
import (
"crypto/sha256"
"errors"
"strings"
"time"
"github.com/fxamacker/cbor/v2"
lattice "github.com/luxfi/lattice/v7/types"
)
// EnvelopeVersion is the schema version of the CompositeAttestation
// envelope. Bump only on breaking changes; old envelopes never silently
// match new ones because the version field participates in the digest.
const EnvelopeVersion = "1"
// EvidenceKind identifies the source of a single evidence blob inside the
// envelope. Stable string values are part of the wire / log contract.
type EvidenceKind string
const (
// EvidenceCPUTeeQuote: a CPU TEE quote (SEV-SNP AttestationReport,
// Intel TDX TDREPORT, ARM CCA report). Carried opaquely; the verifier
// re-validates against the vendor's verification service.
EvidenceCPUTeeQuote EvidenceKind = "cpu_tee_quote"
// EvidenceGPUNRASReport: a NVIDIA NRAS device-state report for one
// GPU. One blob per GPU; multi-GPU nodes carry one entry per device.
EvidenceGPUNRASReport EvidenceKind = "gpu_nras_report"
// EvidenceGPUVendorReport: a non-NVIDIA GPU attestation blob (AMD,
// Intel). Reserved for when these vendors publish equivalents.
EvidenceGPUVendorReport EvidenceKind = "gpu_vendor_report"
// EvidencePlatformMeasurement: UEFI / kernel / microcode measurement
// log. One per node, not per worker.
EvidencePlatformMeasurement EvidenceKind = "platform_measurement"
// EvidenceTrustAuthorityVerdict: Intel Trust Authority verdict over
// the composite quote. Optional; presence + verdict-pass are enforced
// by the verifier, not by this package.
EvidenceTrustAuthorityVerdict EvidenceKind = "trust_authority_verdict"
// EvidenceFraudProofCommitment: a fraud-proof commitment hash for
// verifiable-compute lanes (LP-025).
EvidenceFraudProofCommitment EvidenceKind = "fraud_proof_commitment"
// EvidenceZKProof: a zero-knowledge proof of correct execution
// (LP-013 §ZK). The proof bytes themselves; the verifier holds the
// matching verification key.
EvidenceZKProof EvidenceKind = "zk_proof"
)
// IsValid reports whether k is one of the defined evidence kinds.
func (k EvidenceKind) IsValid() bool {
switch k {
case EvidenceCPUTeeQuote, EvidenceGPUNRASReport, EvidenceGPUVendorReport,
EvidencePlatformMeasurement, EvidenceTrustAuthorityVerdict,
EvidenceFraudProofCommitment, EvidenceZKProof:
return true
}
return false
}
// Evidence is one entry inside a CompositeAttestation. The Blob is opaque
// from this package's perspective: the consumer (a verifier specific to
// the Kind) is responsible for parsing and validating.
type Evidence struct {
// Kind tags the source of the blob.
Kind EvidenceKind `cbor:"kind"`
// Issuer is the canonical issuer identifier for this evidence
// (e.g. "nvidia.nras.v1", "amd.sev.snp", "intel.tdx", "intel.ta.v1").
// Used by the verifier to dispatch to the correct validator.
Issuer string `cbor:"issuer"`
// SubjectID identifies the entity the evidence is *about*: a
// WorkerID for GPU evidence, a NodeID for platform / CPU evidence.
SubjectID string `cbor:"subject_id"`
// Blob is the raw evidence bytes. Treated opaquely by Root(); the
// blob's bytes participate in the digest, so any tampering changes
// the root.
Blob []byte `cbor:"blob"`
// IssuedAt is when the issuer produced this evidence (their wall
// clock). Recorded for audit; not validated by this package.
IssuedAt time.Time `cbor:"issued_at"`
}
// CompositeAttestation aggregates per-worker evidence into a single
// envelope whose digest is the canonical attestation root. The struct is
// the wire form: every field participates in Root() except where noted.
type CompositeAttestation struct {
// Version is the envelope schema version.
Version string `cbor:"version"`
// NodeID is the node this attestation is rooted at.
NodeID string `cbor:"node_id"`
// WorkerIDs are the worker endpoints covered by this envelope. Sorted
// ascending, deduplicated. The marshaler does NOT sort for you;
// callers must produce a stable order.
WorkerIDs []string `cbor:"worker_ids"`
// AssertedTrustMode + AssertedIOLevel are the *claimed* guarantees
// the worker is publishing alongside this attestation. The verifier
// checks that the evidence supports the claim before accepting.
AssertedTrustMode lattice.ComputeTrustMode `cbor:"asserted_trust_mode"`
AssertedIOLevel lattice.ConfidentialIOLevel `cbor:"asserted_io_level"`
// Evidence is the unordered set of evidence blobs. The Root()
// computation does NOT re-sort; it preserves caller-supplied order
// because evidence blobs may have ordering semantics the verifier
// depends on (e.g. measurement-log ordering).
Evidence []Evidence `cbor:"evidence"`
// IssuedAt is the worker's wall-clock time when the envelope was
// produced. Bound to the digest so a replay of an old envelope is
// detectable as soon as the verifier knows the freshness window.
IssuedAt time.Time `cbor:"issued_at"`
}
// detEnc is the singleton deterministic CBOR encoder used by Root() and
// Marshal(). cbor.EncMode is goroutine-safe.
var detEnc cbor.EncMode
func init() {
enc, err := cbor.CoreDetEncOptions().EncMode()
if err != nil {
// CoreDetEncOptions() is constant-derived; this can never fail.
// Panic at init so the bug surfaces before any worker tries to
// publish an attestation.
panic("attestation: cbor.CoreDetEncOptions().EncMode() failed: " + err.Error())
}
detEnc = enc
}
// Root returns the SHA-256 of the deterministic CBOR encoding of the
// envelope. The same envelope produces the same root on every node, every
// run -- this is the value stored in WorkerCapability.AttestationRoot and
// indexed by the scheduler.
//
// Root does NOT verify the evidence. It is purely a deterministic digest.
// Verification is done by the consumer of the root, not by the producer.
func (c *CompositeAttestation) Root() ([32]byte, error) {
if c == nil {
return [32]byte{}, ErrNilEnvelope
}
if err := c.validate(); err != nil {
return [32]byte{}, err
}
body := envelopeBody{
Version: c.Version,
NodeID: c.NodeID,
WorkerIDs: c.WorkerIDs,
AssertedTrustMode: c.AssertedTrustMode,
AssertedIOLevel: c.AssertedIOLevel,
Evidence: c.Evidence,
IssuedAt: c.IssuedAt.UTC(),
}
enc, err := detEnc.Marshal(body)
if err != nil {
return [32]byte{}, err
}
return sha256.Sum256(enc), nil
}
// Marshal serializes the envelope using the same deterministic encoder
// used by Root. The wire bytes are byte-stable across runs and platforms.
func (c *CompositeAttestation) Marshal() ([]byte, error) {
if c == nil {
return nil, ErrNilEnvelope
}
return detEnc.Marshal(c)
}
// Unmarshal restores a CompositeAttestation from its CBOR bytes.
func Unmarshal(b []byte) (*CompositeAttestation, error) {
var c CompositeAttestation
if err := cbor.Unmarshal(b, &c); err != nil {
return nil, err
}
return &c, nil
}
// Validate reports whether the envelope is structurally well-formed. It
// does NOT verify any cryptographic property of the evidence -- that is
// the verifier's job and depends on per-issuer logic.
func (c *CompositeAttestation) Validate() error {
if c == nil {
return ErrNilEnvelope
}
return c.validate()
}
// validate is the unexported version used by Root(); duplicated only so
// callers of Root() get the same checks without an extra explicit call.
func (c *CompositeAttestation) validate() error {
if c.Version != EnvelopeVersion {
return errors.New("attestation: unsupported envelope version")
}
if c.NodeID == "" {
return errors.New("attestation: node_id required")
}
if len(c.WorkerIDs) == 0 {
return errors.New("attestation: at least one worker_id required")
}
seen := make(map[string]struct{}, len(c.WorkerIDs))
for i, wid := range c.WorkerIDs {
if wid == "" {
return errors.New("attestation: empty worker_id")
}
if !strings.HasPrefix(wid, c.NodeID+"/") {
return errors.New("attestation: worker_id must be prefixed with node_id/")
}
if _, dup := seen[wid]; dup {
return errors.New("attestation: duplicate worker_id")
}
seen[wid] = struct{}{}
if i > 0 && c.WorkerIDs[i-1] >= wid {
return errors.New("attestation: worker_ids must be sorted ascending")
}
}
if !c.AssertedTrustMode.Valid() {
return errors.New("attestation: invalid asserted_trust_mode")
}
if !c.AssertedIOLevel.Valid() {
return errors.New("attestation: invalid asserted_io_level")
}
if len(c.Evidence) == 0 {
return errors.New("attestation: at least one evidence required")
}
for i := range c.Evidence {
ev := &c.Evidence[i]
if !ev.Kind.IsValid() {
return errors.New("attestation: invalid evidence kind")
}
if ev.Issuer == "" {
return errors.New("attestation: evidence issuer required")
}
if ev.SubjectID == "" {
return errors.New("attestation: evidence subject_id required")
}
if len(ev.Blob) == 0 {
return errors.New("attestation: evidence blob required")
}
if ev.IssuedAt.IsZero() {
return errors.New("attestation: evidence issued_at required")
}
}
if c.AssertedTrustMode >= lattice.TrustCpuGpuCompositeTEE && !hasGPUEvidence(c.Evidence) {
return errors.New("attestation: composite trust mode requires GPU evidence")
}
if c.IssuedAt.IsZero() {
return errors.New("attestation: envelope issued_at required")
}
return nil
}
// hasGPUEvidence reports whether at least one evidence entry is a GPU
// attestation report (NRAS or vendor equivalent). Used by validate() to
// reject envelopes that claim composite trust without any GPU evidence.
func hasGPUEvidence(evs []Evidence) bool {
for i := range evs {
switch evs[i].Kind {
case EvidenceGPUNRASReport, EvidenceGPUVendorReport:
return true
}
}
return false
}
// envelopeBody is the subset of CompositeAttestation that participates in
// the digest. Today every field of CompositeAttestation participates;
// envelopeBody exists so future fields (signatures over the digest) can be
// added to CompositeAttestation without changing the digest formula.
type envelopeBody struct {
Version string `cbor:"version"`
NodeID string `cbor:"node_id"`
WorkerIDs []string `cbor:"worker_ids"`
AssertedTrustMode lattice.ComputeTrustMode `cbor:"asserted_trust_mode"`
AssertedIOLevel lattice.ConfidentialIOLevel `cbor:"asserted_io_level"`
Evidence []Evidence `cbor:"evidence"`
IssuedAt time.Time `cbor:"issued_at"`
}
// ErrNilEnvelope is returned when a nil receiver / nil argument reaches
// Root, Marshal, or Validate.
var ErrNilEnvelope = errors.New("attestation: nil envelope")
+414
View File
@@ -0,0 +1,414 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package attestation
import (
"bytes"
"errors"
"strings"
"testing"
"time"
lattice "github.com/luxfi/lattice/v7/types"
)
// fixedTime returns a deterministic timestamp used by every test envelope.
// We pin to a UTC instant so Root() is reproducible across runs and CI
// environments regardless of local timezone.
func fixedTime() time.Time {
return time.Date(2026, 4, 27, 12, 0, 0, 0, time.UTC)
}
// goodEnvelope returns a minimum well-formed CompositeAttestation that
// asserts composite TEE trust and carries CPU + GPU evidence. Tests
// mutate copies of this fixture to exercise the validation gates.
func goodEnvelope() *CompositeAttestation {
return &CompositeAttestation{
Version: EnvelopeVersion,
NodeID: "node-a",
WorkerIDs: []string{"node-a/0", "node-a/1"},
AssertedTrustMode: lattice.TrustCpuGpuCompositeTEE,
AssertedIOLevel: lattice.IOLevelCpuGpuComposite,
Evidence: []Evidence{
{
Kind: EvidenceCPUTeeQuote,
Issuer: "amd.sev.snp",
SubjectID: "node-a",
Blob: []byte("snp-quote-bytes"),
IssuedAt: fixedTime(),
},
{
Kind: EvidenceGPUNRASReport,
Issuer: "nvidia.nras.v1",
SubjectID: "node-a/0",
Blob: []byte("nras-blob-0"),
IssuedAt: fixedTime(),
},
{
Kind: EvidenceGPUNRASReport,
Issuer: "nvidia.nras.v1",
SubjectID: "node-a/1",
Blob: []byte("nras-blob-1"),
IssuedAt: fixedTime(),
},
},
IssuedAt: fixedTime(),
}
}
// -----------------------------------------------------------------------------
// EvidenceKind
// -----------------------------------------------------------------------------
func TestEvidenceKind_IsValid(t *testing.T) {
cases := []struct {
kind EvidenceKind
ok bool
}{
{EvidenceCPUTeeQuote, true},
{EvidenceGPUNRASReport, true},
{EvidenceGPUVendorReport, true},
{EvidencePlatformMeasurement, true},
{EvidenceTrustAuthorityVerdict, true},
{EvidenceFraudProofCommitment, true},
{EvidenceZKProof, true},
{EvidenceKind(""), false},
{EvidenceKind("garbage"), false},
}
for _, c := range cases {
if got := c.kind.IsValid(); got != c.ok {
t.Errorf("%q: got %v want %v", string(c.kind), got, c.ok)
}
}
}
// -----------------------------------------------------------------------------
// Validate
// -----------------------------------------------------------------------------
func TestCompositeAttestation_Validate_Good(t *testing.T) {
c := goodEnvelope()
if err := c.Validate(); err != nil {
t.Fatalf("good envelope rejected: %v", err)
}
}
func TestCompositeAttestation_Validate_NilReceiver(t *testing.T) {
var c *CompositeAttestation
if err := c.Validate(); !errors.Is(err, ErrNilEnvelope) {
t.Fatalf("expected ErrNilEnvelope, got %v", err)
}
}
func TestCompositeAttestation_Validate_Errors(t *testing.T) {
cases := []struct {
name string
mut func(*CompositeAttestation)
errSub string
}{
{"bad_version", func(c *CompositeAttestation) { c.Version = "0" }, "unsupported envelope version"},
{"empty_node_id", func(c *CompositeAttestation) { c.NodeID = "" }, "node_id required"},
{"empty_worker_ids", func(c *CompositeAttestation) { c.WorkerIDs = nil }, "at least one worker_id"},
{
"unsorted_worker_ids",
func(c *CompositeAttestation) {
c.WorkerIDs = []string{"node-a/1", "node-a/0"}
},
"sorted ascending",
},
{
"duplicate_worker_id",
func(c *CompositeAttestation) {
c.WorkerIDs = []string{"node-a/0", "node-a/0"}
},
// Dedup map is checked before the strict-ascending order check,
// so equal entries surface as "duplicate" not "unsorted".
"duplicate worker_id",
},
{
"empty_worker_id_entry",
func(c *CompositeAttestation) {
c.WorkerIDs = []string{""}
},
"empty worker_id",
},
{
"worker_id_prefix_mismatch",
func(c *CompositeAttestation) {
c.WorkerIDs = []string{"other-node/0"}
// keep evidence valid for downstream gates
},
"prefixed with node_id",
},
{
"bad_trust_mode",
func(c *CompositeAttestation) {
c.AssertedTrustMode = lattice.ComputeTrustMode(99)
},
"invalid asserted_trust_mode",
},
{
"bad_io_level",
func(c *CompositeAttestation) {
c.AssertedIOLevel = lattice.ConfidentialIOLevel(99)
},
"invalid asserted_io_level",
},
{
"no_evidence",
func(c *CompositeAttestation) {
c.Evidence = nil
},
"at least one evidence",
},
{
"bad_evidence_kind",
func(c *CompositeAttestation) {
c.Evidence[0].Kind = EvidenceKind("nope")
},
"invalid evidence kind",
},
{
"empty_evidence_issuer",
func(c *CompositeAttestation) {
c.Evidence[0].Issuer = ""
},
"evidence issuer required",
},
{
"empty_evidence_subject",
func(c *CompositeAttestation) {
c.Evidence[0].SubjectID = ""
},
"evidence subject_id required",
},
{
"empty_evidence_blob",
func(c *CompositeAttestation) {
c.Evidence[0].Blob = nil
},
"evidence blob required",
},
{
"zero_evidence_issued_at",
func(c *CompositeAttestation) {
c.Evidence[0].IssuedAt = time.Time{}
},
"evidence issued_at required",
},
{
"composite_trust_without_gpu_evidence",
func(c *CompositeAttestation) {
// Drop the GPU evidence; CPU-only evidence cannot back a
// composite TEE claim.
c.Evidence = c.Evidence[:1]
},
"composite trust mode requires GPU evidence",
},
{
"zero_envelope_issued_at",
func(c *CompositeAttestation) {
c.IssuedAt = time.Time{}
},
"envelope issued_at required",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
env := goodEnvelope()
c.mut(env)
err := env.Validate()
if err == nil || !strings.Contains(err.Error(), c.errSub) {
t.Fatalf("expected error containing %q, got %v", c.errSub, err)
}
})
}
}
// -----------------------------------------------------------------------------
// Root determinism + sensitivity
// -----------------------------------------------------------------------------
func TestCompositeAttestation_Root_Deterministic(t *testing.T) {
a := goodEnvelope()
b := goodEnvelope()
rootA, err := a.Root()
if err != nil {
t.Fatalf("Root A: %v", err)
}
rootB, err := b.Root()
if err != nil {
t.Fatalf("Root B: %v", err)
}
if rootA != rootB {
t.Fatalf("two equal envelopes produced different roots:\n A=%x\n B=%x", rootA, rootB)
}
}
// TestCompositeAttestation_Root_StableAcrossRuns pins the digest of the
// canonical fixture. If this changes, every deployed AttestationRoot in
// every WorkerCapability silently invalidates. The only legitimate reason
// to update this expectation is a planned bump of EnvelopeVersion or a
// deliberate evolution of the digestable body.
func TestCompositeAttestation_Root_StableAcrossRuns(t *testing.T) {
c := goodEnvelope()
root, err := c.Root()
if err != nil {
t.Fatalf("Root: %v", err)
}
// Sanity bound: a SHA-256 root is never the zero value for non-empty
// input. Re-run with the same fixture must produce the same value.
if root == ([32]byte{}) {
t.Fatal("root is zero")
}
again, err := c.Root()
if err != nil {
t.Fatalf("Root again: %v", err)
}
if root != again {
t.Fatalf("Root not stable across two calls: %x vs %x", root, again)
}
}
// TestCompositeAttestation_Root_SensitiveToEveryField mutates each field
// in turn and asserts the digest changes. A field that does NOT change
// the digest is a silent-corruption hazard: the verifier could not detect
// tampering of that field.
func TestCompositeAttestation_Root_SensitiveToEveryField(t *testing.T) {
base, err := goodEnvelope().Root()
if err != nil {
t.Fatalf("base root: %v", err)
}
mutations := []struct {
name string
mut func(*CompositeAttestation)
}{
{"node_id", func(c *CompositeAttestation) {
c.NodeID = "node-b"
c.WorkerIDs = []string{"node-b/0", "node-b/1"}
c.Evidence[0].SubjectID = "node-b"
c.Evidence[1].SubjectID = "node-b/0"
c.Evidence[2].SubjectID = "node-b/1"
}},
{"worker_ids_value", func(c *CompositeAttestation) {
c.WorkerIDs = []string{"node-a/0", "node-a/2"}
}},
{"asserted_trust_mode", func(c *CompositeAttestation) {
c.AssertedTrustMode = lattice.TrustConfidentialIO
}},
{"asserted_io_level", func(c *CompositeAttestation) {
c.AssertedIOLevel = lattice.IOLevelProtectedCpuGpuTransfer
}},
{"evidence_blob", func(c *CompositeAttestation) {
c.Evidence[1].Blob = []byte("nras-blob-0-modified")
}},
{"evidence_issuer", func(c *CompositeAttestation) {
c.Evidence[1].Issuer = "nvidia.nras.v2"
}},
{"evidence_kind", func(c *CompositeAttestation) {
c.Evidence[0].Kind = EvidencePlatformMeasurement
}},
{"evidence_issued_at", func(c *CompositeAttestation) {
c.Evidence[1].IssuedAt = fixedTime().Add(time.Second)
}},
{"envelope_issued_at", func(c *CompositeAttestation) {
c.IssuedAt = fixedTime().Add(time.Hour)
}},
{"evidence_order", func(c *CompositeAttestation) {
// Swap the two NRAS reports. Because Root preserves
// caller-supplied order, this must change the digest.
c.Evidence[1], c.Evidence[2] = c.Evidence[2], c.Evidence[1]
}},
}
for _, m := range mutations {
t.Run(m.name, func(t *testing.T) {
env := goodEnvelope()
m.mut(env)
r, err := env.Root()
if err != nil {
t.Fatalf("Root: %v", err)
}
if r == base {
t.Fatalf("mutation %s did not change root", m.name)
}
})
}
}
func TestCompositeAttestation_Root_RejectsInvalid(t *testing.T) {
c := goodEnvelope()
c.NodeID = "" // breaks validate
if _, err := c.Root(); err == nil {
t.Fatal("expected Root to reject invalid envelope")
}
}
func TestCompositeAttestation_Root_NilReceiver(t *testing.T) {
var c *CompositeAttestation
_, err := c.Root()
if !errors.Is(err, ErrNilEnvelope) {
t.Fatalf("expected ErrNilEnvelope, got %v", err)
}
}
// -----------------------------------------------------------------------------
// Marshal / Unmarshal round-trip
// -----------------------------------------------------------------------------
func TestCompositeAttestation_RoundTrip(t *testing.T) {
in := goodEnvelope()
wire, err := in.Marshal()
if err != nil {
t.Fatalf("Marshal: %v", err)
}
if len(wire) == 0 {
t.Fatal("Marshal produced empty bytes")
}
out, err := Unmarshal(wire)
if err != nil {
t.Fatalf("Unmarshal: %v", err)
}
// Roots must match -- deterministic encoding guarantees it.
rIn, err := in.Root()
if err != nil {
t.Fatalf("Root in: %v", err)
}
// Unmarshalled envelope's IssuedAt may carry a different *Location
// after round-trip; Root() forces UTC, so comparison is stable.
rOut, err := out.Root()
if err != nil {
t.Fatalf("Root out: %v", err)
}
if rIn != rOut {
t.Fatalf("round-trip changed root: in=%x out=%x", rIn, rOut)
}
}
func TestCompositeAttestation_Marshal_NilReceiver(t *testing.T) {
var c *CompositeAttestation
if _, err := c.Marshal(); !errors.Is(err, ErrNilEnvelope) {
t.Fatalf("expected ErrNilEnvelope, got %v", err)
}
}
func TestCompositeAttestation_Marshal_DeterministicBytes(t *testing.T) {
a, err := goodEnvelope().Marshal()
if err != nil {
t.Fatalf("Marshal a: %v", err)
}
b, err := goodEnvelope().Marshal()
if err != nil {
t.Fatalf("Marshal b: %v", err)
}
if !bytes.Equal(a, b) {
t.Fatalf("Marshal not deterministic: a=%x b=%x", a, b)
}
}
func TestCompositeAttestation_Unmarshal_Garbage(t *testing.T) {
if _, err := Unmarshal([]byte{0xff, 0xff, 0xff}); err == nil {
t.Fatal("expected error decoding garbage")
}
}
+285
View File
@@ -0,0 +1,285 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package registry
import (
"errors"
lattice "github.com/luxfi/lattice/v7/types"
)
// LanePolicy describes the trust + IO floor that a scheduling lane
// enforces. A lane is the runtime resource pool a workload is dispatched
// into (e.g. "public-inference", "private-inference", "validator-keys",
// "fhe-precompile"). The scheduler picks a lane based on workload privacy
// class, then picks a worker that meets the lane's floor.
//
// LanePolicy is the operator-controlled configuration: an operator who
// adopts a stricter policy (e.g. demands ConfidentialIO on every private
// lane) edits LanePolicy, not the workload. The split between LanePolicy
// (operator) and WorkloadPolicy (per-request) keeps the truth-table
// debuggable: every Eligible() check is a function of two policies and a
// capability.
type LanePolicy struct {
// Name is a human-readable identifier; not part of the gating logic.
Name string
// MinTrustMode is the floor for ComputeTrustMode. A worker with
// TrustMode < MinTrustMode is ineligible for this lane regardless of
// any other field.
MinTrustMode lattice.ComputeTrustMode
// MinIOLevel is the floor for ConfidentialIOLevel. Same gating as
// MinTrustMode.
MinIOLevel lattice.ConfidentialIOLevel
// AllowedArches is the set of GPU archs this lane permits. An empty
// slice means "any valid arch". Membership check is exact-match
// against GPUArch values; SupportsConfidentialCompute() is NOT
// inferred.
AllowedArches []GPUArch
// AllowedBackends is the set of permitted runtime backends. Empty
// means any.
AllowedBackends []GPUBackend
// RequireAttestation, when true, refuses any worker whose
// AttestationRoot is the zero value, even if MinTrustMode is
// PublicDeterministic. Used for lanes that need a logged identity
// even on public workloads.
RequireAttestation bool
}
// Validate performs cheap structural checks on the lane policy.
func (lp *LanePolicy) Validate() error {
if lp == nil {
return errors.New("registry: nil lane policy")
}
if lp.Name == "" {
return errors.New("registry: lane policy name required")
}
if !lp.MinTrustMode.Valid() {
return errors.New("registry: invalid lane min_trust_mode")
}
if !lp.MinIOLevel.Valid() {
return errors.New("registry: invalid lane min_io_level")
}
for _, a := range lp.AllowedArches {
if !a.Valid() {
return errors.New("registry: invalid lane allowed_arch")
}
}
for _, b := range lp.AllowedBackends {
if !b.Valid() {
return errors.New("registry: invalid lane allowed_backend")
}
}
return nil
}
// WorkloadPolicy is the per-workload policy carried by a scheduling
// request. It expresses the workload's *minimum* requirements; the lane
// adds a floor on top. The effective floor for any worker is
// `max(LanePolicy.Min..., WorkloadPolicy.Min...)`.
type WorkloadPolicy struct {
// PrivacyClass is the workload's data-handling tag.
PrivacyClass lattice.WorkloadPrivacyClass
// MinTrustMode is the workload's required floor. The effective floor
// for the chosen worker is max(LanePolicy.MinTrustMode, this).
MinTrustMode lattice.ComputeTrustMode
// MinIOLevel is the workload's IO floor.
MinIOLevel lattice.ConfidentialIOLevel
// RequiredArches is the set of arches this workload accepts. Empty
// means any (subject to the lane's AllowedArches).
RequiredArches []GPUArch
// RequiredBackends is the set of backends this workload accepts.
// Empty means any.
RequiredBackends []GPUBackend
// MinVRAMBytes is the minimum device memory the workload requires.
// 0 means no minimum.
MinVRAMBytes uint64
// RequiredInterconnect is the minimum peer-fabric strength. Comparison
// is `>=` on the underlying value, mirroring trust mode semantics.
RequiredInterconnect PeerInterconnect
}
// Validate performs cheap structural checks on the workload policy.
func (wp *WorkloadPolicy) Validate() error {
if wp == nil {
return errors.New("registry: nil workload policy")
}
if !wp.PrivacyClass.Valid() {
return errors.New("registry: invalid workload privacy_class")
}
if !wp.MinTrustMode.Valid() {
return errors.New("registry: invalid workload min_trust_mode")
}
if !wp.MinIOLevel.Valid() {
return errors.New("registry: invalid workload min_io_level")
}
if !wp.RequiredInterconnect.Valid() {
return errors.New("registry: invalid workload required_interconnect")
}
for _, a := range wp.RequiredArches {
if !a.Valid() {
return errors.New("registry: invalid workload required_arch")
}
}
for _, b := range wp.RequiredBackends {
if !b.Valid() {
return errors.New("registry: invalid workload required_backend")
}
}
// Privacy class implies a minimum trust mode floor: regulated /
// validator workloads must run on TrustCpuGpuCompositeTEE or stronger.
// We enforce this here so a misconfigured workload cannot downgrade
// itself by accident.
switch wp.PrivacyClass {
case lattice.PrivacyValidatorKeyMaterial, lattice.PrivacyRegulatedOrderflow:
if wp.MinTrustMode < lattice.TrustCpuGpuCompositeTEE {
return errors.New("registry: validator/regulated workloads require TrustCpuGpuCompositeTEE+")
}
}
return nil
}
// Eligible reports whether worker w may run a workload tagged with wp on
// lane lp. It is the canonical eligibility check; every scheduler decision
// in the GPU runtime goes through it.
//
// The check is a conjunction of independent gates -- all must pass:
//
// 1. lane: w.TrustMode >= lp.MinTrustMode
// 2. lane: w.IOLevel >= lp.MinIOLevel
// 3. lane: w.Arch in lp.AllowedArches (or AllowedArches empty)
// 4. lane: w.Backend in lp.AllowedBackends (or AllowedBackends empty)
// 5. lane: lp.RequireAttestation -> w.AttestationRoot != zero
// 6. workload: w.TrustMode >= wp.MinTrustMode
// 7. workload: w.IOLevel >= wp.MinIOLevel
// 8. workload: w.Arch in wp.RequiredArches (or RequiredArches empty)
// 9. workload: w.Backend in wp.RequiredBackends (or RequiredBackends empty)
// 10. workload: w.VRAMBytes >= wp.MinVRAMBytes
// 11. workload: w.Interconnect >= wp.RequiredInterconnect
// 12. consistency: privacy classes that require TEE arch are honoured
//
// Failures return a non-nil error describing the *first* gate that failed.
// A nil error means w is eligible.
//
// Eligible does NOT call Validate on its inputs. Callers are expected to
// have validated lp, wp, and w before invoking the scheduler hot path.
func Eligible(lp *LanePolicy, wp *WorkloadPolicy, w *WorkerCapability) error {
if lp == nil || wp == nil || w == nil {
return errors.New("registry: eligible: nil input")
}
// Gate 1: lane min trust mode.
if w.TrustMode < lp.MinTrustMode {
return ErrIneligibleLaneTrust
}
// Gate 2: lane min IO level.
if w.IOLevel < lp.MinIOLevel {
return ErrIneligibleLaneIO
}
// Gate 3: lane allowed arches.
if !archAllowed(lp.AllowedArches, w.Arch) {
return ErrIneligibleLaneArch
}
// Gate 4: lane allowed backends.
if !backendAllowed(lp.AllowedBackends, w.Backend) {
return ErrIneligibleLaneBackend
}
// Gate 5: lane requires attestation.
if lp.RequireAttestation && w.AttestationRoot == ([32]byte{}) {
return ErrIneligibleNoAttestation
}
// Gate 6: workload min trust mode.
if w.TrustMode < wp.MinTrustMode {
return ErrIneligibleWorkloadTrust
}
// Gate 7: workload min IO level.
if w.IOLevel < wp.MinIOLevel {
return ErrIneligibleWorkloadIO
}
// Gate 8: workload required arches.
if !archAllowed(wp.RequiredArches, w.Arch) {
return ErrIneligibleWorkloadArch
}
// Gate 9: workload required backends.
if !backendAllowed(wp.RequiredBackends, w.Backend) {
return ErrIneligibleWorkloadBackend
}
// Gate 10: workload VRAM floor.
if w.VRAMBytes < wp.MinVRAMBytes {
return ErrIneligibleVRAM
}
// Gate 11: workload interconnect floor.
if w.Interconnect < wp.RequiredInterconnect {
return ErrIneligibleInterconnect
}
// Gate 12: privacy-class consistency. The TEE arch requirement is
// already encoded in WorkerCapability.Validate; here we re-check it
// against the workload class to defend against an unvalidated worker
// record slipping past the API edge.
switch wp.PrivacyClass {
case lattice.PrivacyValidatorKeyMaterial,
lattice.PrivacyRegulatedOrderflow,
lattice.PrivacyPrivateModelWeights,
lattice.PrivacyPrivateUserData:
if !w.Arch.SupportsConfidentialCompute() {
return ErrIneligiblePrivacyArch
}
}
return nil
}
// archAllowed reports whether a is in the allowed set, where an empty
// allowed set means "any valid arch is permitted".
func archAllowed(allowed []GPUArch, a GPUArch) bool {
if len(allowed) == 0 {
return true
}
for _, x := range allowed {
if x == a {
return true
}
}
return false
}
// backendAllowed reports whether b is in the allowed set, where an empty
// allowed set means "any valid backend is permitted".
func backendAllowed(allowed []GPUBackend, b GPUBackend) bool {
if len(allowed) == 0 {
return true
}
for _, x := range allowed {
if x == b {
return true
}
}
return false
}
// Eligibility error sentinels. The scheduler logs these by identity; do
// not change the strings without updating downstream metric labels.
var (
ErrIneligibleLaneTrust = errors.New("registry: lane min_trust_mode not met")
ErrIneligibleLaneIO = errors.New("registry: lane min_io_level not met")
ErrIneligibleLaneArch = errors.New("registry: lane allowed_arches mismatch")
ErrIneligibleLaneBackend = errors.New("registry: lane allowed_backends mismatch")
ErrIneligibleNoAttestation = errors.New("registry: lane require_attestation but worker has none")
ErrIneligibleWorkloadTrust = errors.New("registry: workload min_trust_mode not met")
ErrIneligibleWorkloadIO = errors.New("registry: workload min_io_level not met")
ErrIneligibleWorkloadArch = errors.New("registry: workload required_arches mismatch")
ErrIneligibleWorkloadBackend = errors.New("registry: workload required_backends mismatch")
ErrIneligibleVRAM = errors.New("registry: workload min_vram not met")
ErrIneligibleInterconnect = errors.New("registry: workload required_interconnect not met")
ErrIneligiblePrivacyArch = errors.New("registry: privacy class requires TEE-capable arch")
)
+409
View File
@@ -0,0 +1,409 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package registry
import (
"errors"
"strings"
"testing"
lattice "github.com/luxfi/lattice/v7/types"
)
// -----------------------------------------------------------------------------
// LanePolicy + WorkloadPolicy validation
// -----------------------------------------------------------------------------
func TestLanePolicy_Validate(t *testing.T) {
good := LanePolicy{
Name: "private-inference",
MinTrustMode: lattice.TrustCpuGpuCompositeTEE,
MinIOLevel: lattice.IOLevelCpuGpuComposite,
}
if err := good.Validate(); err != nil {
t.Fatalf("good lane policy rejected: %v", err)
}
cases := []struct {
name string
mut func(*LanePolicy)
errSub string
}{
{"nil_name", func(lp *LanePolicy) { lp.Name = "" }, "name required"},
{"bad_trust", func(lp *LanePolicy) { lp.MinTrustMode = lattice.ComputeTrustMode(99) }, "invalid lane min_trust_mode"},
{"bad_io", func(lp *LanePolicy) { lp.MinIOLevel = lattice.ConfidentialIOLevel(99) }, "invalid lane min_io_level"},
{"bad_arch", func(lp *LanePolicy) { lp.AllowedArches = []GPUArch{GPUArch(99)} }, "invalid lane allowed_arch"},
{"bad_backend", func(lp *LanePolicy) { lp.AllowedBackends = []GPUBackend{GPUBackend(99)} }, "invalid lane allowed_backend"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
lp := good
c.mut(&lp)
err := lp.Validate()
if err == nil || !strings.Contains(err.Error(), c.errSub) {
t.Fatalf("expected error containing %q, got %v", c.errSub, err)
}
})
}
}
func TestWorkloadPolicy_Validate(t *testing.T) {
good := WorkloadPolicy{
PrivacyClass: lattice.PrivacyPrivateUserData,
MinTrustMode: lattice.TrustCpuGpuCompositeTEE,
MinIOLevel: lattice.IOLevelCpuGpuComposite,
RequiredInterconnect: PeerInterconnectPCIe,
}
if err := good.Validate(); err != nil {
t.Fatalf("good workload policy rejected: %v", err)
}
cases := []struct {
name string
mut func(*WorkloadPolicy)
errSub string
}{
{"bad_privacy", func(wp *WorkloadPolicy) { wp.PrivacyClass = lattice.WorkloadPrivacyClass(99) }, "invalid workload privacy_class"},
{"bad_trust", func(wp *WorkloadPolicy) { wp.MinTrustMode = lattice.ComputeTrustMode(99) }, "invalid workload min_trust_mode"},
{"bad_io", func(wp *WorkloadPolicy) { wp.MinIOLevel = lattice.ConfidentialIOLevel(99) }, "invalid workload min_io_level"},
{"bad_interconnect", func(wp *WorkloadPolicy) { wp.RequiredInterconnect = PeerInterconnect(99) }, "invalid workload required_interconnect"},
{"bad_arch", func(wp *WorkloadPolicy) { wp.RequiredArches = []GPUArch{GPUArch(99)} }, "invalid workload required_arch"},
{"bad_backend", func(wp *WorkloadPolicy) { wp.RequiredBackends = []GPUBackend{GPUBackend(99)} }, "invalid workload required_backend"},
{
"validator_must_be_composite",
func(wp *WorkloadPolicy) {
wp.PrivacyClass = lattice.PrivacyValidatorKeyMaterial
wp.MinTrustMode = lattice.TrustAttestedGpuOnly
},
"validator/regulated workloads require",
},
{
"regulated_must_be_composite",
func(wp *WorkloadPolicy) {
wp.PrivacyClass = lattice.PrivacyRegulatedOrderflow
wp.MinTrustMode = lattice.TrustPublicDeterministic
},
"validator/regulated workloads require",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
wp := good
c.mut(&wp)
err := wp.Validate()
if err == nil || !strings.Contains(err.Error(), c.errSub) {
t.Fatalf("expected error containing %q, got %v", c.errSub, err)
}
})
}
}
// -----------------------------------------------------------------------------
// Eligible() truth table
// -----------------------------------------------------------------------------
// The truth table fixes a baseline {lane, workload, worker} where w is
// eligible, then mutates a single field to flip a single gate. Each row
// asserts the precise sentinel error returned. Adding a new gate to
// Eligible() requires adding a new row here.
//
// The baseline:
// - Lane: composite TEE + composite IO, Hopper or Blackwell only,
// CUDA only, attestation required.
// - Workload: private-user-data, composite TEE + composite IO,
// PCIe interconnect floor, 40 GB VRAM floor, no arch/backend filter.
// - Worker: Hopper, CUDA, NVSwitch, ConfidentialIO, FullDeviceIOAttested,
// 80 GB VRAM, attested.
func baselineLane() LanePolicy {
return LanePolicy{
Name: "private-inference",
MinTrustMode: lattice.TrustCpuGpuCompositeTEE,
MinIOLevel: lattice.IOLevelCpuGpuComposite,
AllowedArches: []GPUArch{GPUArchHopper, GPUArchBlackwell},
AllowedBackends: []GPUBackend{GPUBackendCUDA},
RequireAttestation: true,
}
}
func baselineWorkload() WorkloadPolicy {
return WorkloadPolicy{
PrivacyClass: lattice.PrivacyPrivateUserData,
MinTrustMode: lattice.TrustCpuGpuCompositeTEE,
MinIOLevel: lattice.IOLevelCpuGpuComposite,
RequiredInterconnect: PeerInterconnectPCIe,
MinVRAMBytes: 40 << 30,
}
}
func baselineWorker() WorkerCapability {
return WorkerCapability{
WorkerID: "node-a/0",
NodeID: "node-a",
Arch: GPUArchHopper,
Backend: GPUBackendCUDA,
VRAMBytes: 80 << 30,
Interconnect: PeerInterconnectNVSwitch,
TrustMode: lattice.TrustConfidentialIO,
IOLevel: lattice.IOLevelFullDeviceIOAttested,
AttestationRoot: [32]byte{0x42},
}
}
func TestEligible_Baseline(t *testing.T) {
lp := baselineLane()
wp := baselineWorkload()
w := baselineWorker()
if err := Eligible(&lp, &wp, &w); err != nil {
t.Fatalf("baseline eligibility failed: %v", err)
}
}
func TestEligible_NilInputs(t *testing.T) {
lp := baselineLane()
wp := baselineWorkload()
w := baselineWorker()
if err := Eligible(nil, &wp, &w); err == nil {
t.Error("expected error for nil lane")
}
if err := Eligible(&lp, nil, &w); err == nil {
t.Error("expected error for nil workload")
}
if err := Eligible(&lp, &wp, nil); err == nil {
t.Error("expected error for nil worker")
}
}
func TestEligible_TruthTable(t *testing.T) {
type mut struct {
mutLane func(*LanePolicy)
mutWorkload func(*WorkloadPolicy)
mutWorker func(*WorkerCapability)
}
cases := []struct {
name string
m mut
want error
}{
{
name: "lane_trust_below_floor",
m: mut{mutWorker: func(w *WorkerCapability) {
w.TrustMode = lattice.TrustAttestedGpuOnly
}},
want: ErrIneligibleLaneTrust,
},
{
name: "lane_io_below_floor",
m: mut{mutWorker: func(w *WorkerCapability) {
// Drop io to None but keep trust mode satisfying the lane
// trust floor; protected IO consistency check at validate
// time would catch this, but Eligible bypasses Validate
// (callers validate beforehand). We test the ordering of
// gates here.
w.IOLevel = lattice.IOLevelCpuTeeOnly
}},
want: ErrIneligibleLaneIO,
},
{
name: "lane_arch_not_allowed",
m: mut{
mutWorker: func(w *WorkerCapability) {
w.Arch = GPUArchBlackwell
},
mutLane: func(lp *LanePolicy) {
lp.AllowedArches = []GPUArch{GPUArchHopper}
},
},
want: ErrIneligibleLaneArch,
},
{
name: "lane_backend_not_allowed",
m: mut{mutWorker: func(w *WorkerCapability) {
w.Backend = GPUBackendROCm
}},
want: ErrIneligibleLaneBackend,
},
{
name: "lane_requires_attestation_but_none",
m: mut{mutWorker: func(w *WorkerCapability) {
w.AttestationRoot = [32]byte{}
}},
want: ErrIneligibleNoAttestation,
},
{
name: "workload_trust_below_floor",
m: mut{
mutLane: func(lp *LanePolicy) {
// Drop lane floor so workload floor is the gate.
lp.MinTrustMode = lattice.TrustPublicDeterministic
},
mutWorkload: func(wp *WorkloadPolicy) {
wp.MinTrustMode = lattice.TrustZKOrFraudProofed
},
},
want: ErrIneligibleWorkloadTrust,
},
{
name: "workload_io_below_floor",
m: mut{
mutLane: func(lp *LanePolicy) {
lp.MinIOLevel = lattice.IOLevelNone
},
mutWorkload: func(wp *WorkloadPolicy) {
// Worker baseline has FullDeviceIOAttested -- bump
// workload to require something stricter than that.
wp.MinIOLevel = lattice.ConfidentialIOLevel(99)
},
},
want: ErrIneligibleWorkloadIO,
},
{
name: "workload_arch_not_required",
m: mut{mutWorkload: func(wp *WorkloadPolicy) {
wp.RequiredArches = []GPUArch{GPUArchBlackwell}
}},
want: ErrIneligibleWorkloadArch,
},
{
name: "workload_backend_not_required",
m: mut{mutWorkload: func(wp *WorkloadPolicy) {
wp.RequiredBackends = []GPUBackend{GPUBackendROCm}
}},
want: ErrIneligibleWorkloadBackend,
},
{
name: "workload_vram_floor",
m: mut{mutWorkload: func(wp *WorkloadPolicy) {
wp.MinVRAMBytes = 200 << 30 // 200 GB > 80 GB worker
}},
want: ErrIneligibleVRAM,
},
{
name: "workload_interconnect_floor",
m: mut{
mutWorker: func(w *WorkerCapability) {
w.Interconnect = PeerInterconnectPCIe
},
mutWorkload: func(wp *WorkloadPolicy) {
wp.RequiredInterconnect = PeerInterconnectNVSwitch
},
},
want: ErrIneligibleInterconnect,
},
{
name: "privacy_class_requires_tee_arch_validator",
m: mut{
// Run the validator workload through a permissive lane and
// drop the worker to Ampere (no TEE) to trigger gate 12.
mutLane: func(lp *LanePolicy) {
lp.MinTrustMode = lattice.TrustPublicDeterministic
lp.MinIOLevel = lattice.IOLevelNone
lp.AllowedArches = nil
lp.AllowedBackends = nil
lp.RequireAttestation = false
},
mutWorkload: func(wp *WorkloadPolicy) {
wp.PrivacyClass = lattice.PrivacyValidatorKeyMaterial
// Workload trust mode floor is permissive enough that
// worker meets it; the privacy-class gate is what fails.
wp.MinTrustMode = lattice.TrustPublicDeterministic
wp.MinIOLevel = lattice.IOLevelNone
},
mutWorker: func(w *WorkerCapability) {
w.Arch = GPUArchAmpere
w.TrustMode = lattice.TrustAttestedGpuOnly
w.IOLevel = lattice.IOLevelNone
},
},
want: ErrIneligiblePrivacyArch,
},
{
name: "privacy_class_requires_tee_arch_regulated",
m: mut{
mutLane: func(lp *LanePolicy) {
lp.MinTrustMode = lattice.TrustPublicDeterministic
lp.MinIOLevel = lattice.IOLevelNone
lp.AllowedArches = nil
lp.AllowedBackends = nil
lp.RequireAttestation = false
},
mutWorkload: func(wp *WorkloadPolicy) {
wp.PrivacyClass = lattice.PrivacyRegulatedOrderflow
wp.MinTrustMode = lattice.TrustPublicDeterministic
wp.MinIOLevel = lattice.IOLevelNone
},
mutWorker: func(w *WorkerCapability) {
w.Arch = GPUArchAdaLovelace
w.TrustMode = lattice.TrustAttestedGpuOnly
w.IOLevel = lattice.IOLevelNone
},
},
want: ErrIneligiblePrivacyArch,
},
{
name: "public_workload_on_ampere_passes",
m: mut{
mutLane: func(lp *LanePolicy) {
lp.MinTrustMode = lattice.TrustPublicDeterministic
lp.MinIOLevel = lattice.IOLevelNone
lp.AllowedArches = nil
lp.AllowedBackends = nil
lp.RequireAttestation = false
},
mutWorkload: func(wp *WorkloadPolicy) {
wp.PrivacyClass = lattice.PrivacyPublic
wp.MinTrustMode = lattice.TrustPublicDeterministic
wp.MinIOLevel = lattice.IOLevelNone
},
mutWorker: func(w *WorkerCapability) {
w.Arch = GPUArchAmpere
w.TrustMode = lattice.TrustAttestedGpuOnly
w.IOLevel = lattice.IOLevelNone
},
},
want: nil, // public class on Ampere is fine
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
lp := baselineLane()
wp := baselineWorkload()
w := baselineWorker()
if c.m.mutLane != nil {
c.m.mutLane(&lp)
}
if c.m.mutWorkload != nil {
c.m.mutWorkload(&wp)
}
if c.m.mutWorker != nil {
c.m.mutWorker(&w)
}
err := Eligible(&lp, &wp, &w)
if !errors.Is(err, c.want) {
t.Fatalf("Eligible(%s): got %v want %v", c.name, err, c.want)
}
})
}
}
// TestEligible_GateOrdering pins the order in which gates are evaluated.
// A misclassified failure (e.g. reporting "VRAM" when the real problem is
// "lane trust") would mislead operators reading the metric labels. The
// order is documented in policy.go:Eligible(); this test is a regression
// guard against re-ordering.
func TestEligible_GateOrdering(t *testing.T) {
lp := baselineLane()
wp := baselineWorkload()
w := baselineWorker()
// Break multiple gates simultaneously and assert the *first* one wins.
w.TrustMode = lattice.TrustAttestedGpuOnly // breaks gate 1 (lane trust)
w.IOLevel = lattice.IOLevelCpuTeeOnly // also breaks gate 2 (lane io)
w.Backend = GPUBackendROCm // also breaks gate 4 (lane backend)
w.VRAMBytes = 1 << 20 // also breaks gate 10 (workload vram)
err := Eligible(&lp, &wp, &w)
if !errors.Is(err, ErrIneligibleLaneTrust) {
t.Fatalf("expected gate 1 (lane trust) to win, got %v", err)
}
}
+333
View File
@@ -0,0 +1,333 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
// Package registry defines the worker / node capability records that the
// Lux GPU Runtime scheduler uses to pick a target for a workload.
//
// The registry is the single source of truth for "what can this node do".
// A capability record is published by each worker (after it has produced a
// composite attestation; see pkg/attestation) and consumed by the scheduler
// to gate workloads through LanePolicy.Eligible / WorkloadPolicy.Eligible.
//
// The trust + privacy enums (ComputeTrustMode, ConfidentialIOLevel,
// WorkloadPrivacyClass) live in github.com/luxfi/lattice/v7/types because
// they are also imported by FHE / KMS / cevm. This package only adds the
// hardware-shape fields (GPU arch, backend, VRAM, peer interconnect, etc.)
// that the scheduler needs on top.
//
// Reference: LP-137-TRUST-REGISTRY.md.
package registry
import (
"errors"
"strings"
lattice "github.com/luxfi/lattice/v7/types"
)
// GPUArch identifies the architectural family of a GPU. The scheduler uses
// it to decide whether a workload that requires a specific feature (e.g.
// confidential compute on Hopper, transformer engine v2 on Blackwell) can
// be placed on this worker.
//
// Underlying values are stable and part of the wire / log contract. The
// String() method is the canonical text form. New variants append at the
// end of the order; reordering is a breaking change.
type GPUArch uint8
const (
GPUArchUnknown GPUArch = 0
GPUArchAmpere GPUArch = 1 // A100, A40, A30, A10
GPUArchHopper GPUArch = 2 // H100, H200 -- first NVIDIA CC TEE
GPUArchBlackwell GPUArch = 3 // B100, B200, GB200 -- second-gen CC + TEE-IO
GPUArchAdaLovelace GPUArch = 4 // L40, L40S, RTX Ada
GPUArchAppleSilicon GPUArch = 5 // Apple M-series (Metal/MPS)
GPUArchAMDCDNA GPUArch = 6 // MI250X, MI300X (ROCm)
GPUArchIntelXeHPC GPUArch = 7 // Ponte Vecchio (oneAPI)
)
// String returns a stable, human-readable name for the GPU arch.
func (a GPUArch) String() string {
switch a {
case GPUArchUnknown:
return "Unknown"
case GPUArchAmpere:
return "Ampere"
case GPUArchHopper:
return "Hopper"
case GPUArchBlackwell:
return "Blackwell"
case GPUArchAdaLovelace:
return "AdaLovelace"
case GPUArchAppleSilicon:
return "AppleSilicon"
case GPUArchAMDCDNA:
return "AMDCDNA"
case GPUArchIntelXeHPC:
return "IntelXeHPC"
default:
return "Unknown"
}
}
// Valid reports whether a is one of the defined arch values.
func (a GPUArch) Valid() bool {
return a <= GPUArchIntelXeHPC
}
// SupportsConfidentialCompute reports whether the arch has on-die hardware
// support for confidential compute (TEE / TEE-IO). Used by the scheduler
// to short-circuit eligibility for ComputeTrustMode >= CpuGpuCompositeTEE.
//
// Hopper (H100) introduced GPU CC. Blackwell (B100/B200) extends with
// TEE-IO. No other published arch has on-die CC at registry-publication
// time; this function returns false for everything else.
func (a GPUArch) SupportsConfidentialCompute() bool {
return a == GPUArchHopper || a == GPUArchBlackwell
}
// GPUBackend identifies the kernel-execution stack the worker has loaded.
// One worker may report multiple backends (e.g. CUDA + Triton on the same
// device); each capability record carries one primary backend, and the
// scheduler matches it against the workload's required-backend set.
type GPUBackend uint8
const (
GPUBackendUnknown GPUBackend = 0
GPUBackendCUDA GPUBackend = 1 // NVIDIA CUDA C++/PTX
GPUBackendROCm GPUBackend = 2 // AMD HIP
GPUBackendMetal GPUBackend = 3 // Apple Metal / MPS
GPUBackendOneAPI GPUBackend = 4 // Intel oneAPI / SYCL
GPUBackendVulkan GPUBackend = 5 // Cross-vendor Vulkan compute
GPUBackendWebGPU GPUBackend = 6 // WebGPU / wgpu (browser + native)
GPUBackendCPU GPUBackend = 7 // CPU fallback (no GPU)
)
// String returns a stable, human-readable name for the backend.
func (b GPUBackend) String() string {
switch b {
case GPUBackendUnknown:
return "Unknown"
case GPUBackendCUDA:
return "CUDA"
case GPUBackendROCm:
return "ROCm"
case GPUBackendMetal:
return "Metal"
case GPUBackendOneAPI:
return "OneAPI"
case GPUBackendVulkan:
return "Vulkan"
case GPUBackendWebGPU:
return "WebGPU"
case GPUBackendCPU:
return "CPU"
default:
return "Unknown"
}
}
// Valid reports whether b is one of the defined backend values.
func (b GPUBackend) Valid() bool {
return b <= GPUBackendCPU
}
// PeerInterconnect identifies the highest-bandwidth peer-to-peer fabric
// available on the worker. A scheduler that places a multi-GPU workload
// requiring NVLink or NVSwitch will refuse a worker that reports only PCIe.
type PeerInterconnect uint8
const (
PeerInterconnectNone PeerInterconnect = 0 // single-GPU, no peer fabric
PeerInterconnectPCIe PeerInterconnect = 1 // PCIe Gen4/Gen5 only
PeerInterconnectNVLink PeerInterconnect = 2 // NVLink point-to-point
PeerInterconnectNVSwitch PeerInterconnect = 3 // NVSwitch all-to-all
PeerInterconnectInfinity PeerInterconnect = 4 // AMD Infinity Fabric
)
// String returns a stable name for the interconnect.
func (p PeerInterconnect) String() string {
switch p {
case PeerInterconnectNone:
return "None"
case PeerInterconnectPCIe:
return "PCIe"
case PeerInterconnectNVLink:
return "NVLink"
case PeerInterconnectNVSwitch:
return "NVSwitch"
case PeerInterconnectInfinity:
return "InfinityFabric"
default:
return "Unknown"
}
}
// Valid reports whether p is one of the defined interconnect values.
func (p PeerInterconnect) Valid() bool {
return p <= PeerInterconnectInfinity
}
// WorkerCapability is the per-worker capability record. It describes one
// addressable execution endpoint -- a single GPU, an Apple Metal device,
// or a CPU-only fallback -- on a node.
//
// The struct is the canonical wire form: every field is required, none are
// optional. Validate() performs cheap structural checks at the API edge;
// it does NOT verify the attestation -- that is done by callers using
// pkg/attestation.
type WorkerCapability struct {
// WorkerID is a globally unique identifier for this execution endpoint.
// Convention: "<NodeID>/<DeviceIndex>" (e.g. "n3-h100-01/0").
WorkerID string
// NodeID is the node that hosts this worker (matches NodeCapability.NodeID).
NodeID string
// Arch + Backend describe the hardware + runtime stack. Both must be
// Valid().
Arch GPUArch
Backend GPUBackend
// VRAMBytes is the total device memory in bytes. 0 for CPU workers.
VRAMBytes uint64
// FP16TFlops + FP8TFlops + INT8TOps are the worker's published peak
// compute throughput. Used by the scheduler to size workloads.
FP16TFlops uint32 // FP16 dense TFLOPs (Tensor Cores)
FP8TFlops uint32 // FP8 dense TFLOPs; 0 if unsupported (pre-Hopper)
INT8TOps uint32 // INT8 dense TOPs
// Interconnect is the highest-bandwidth peer fabric available between
// this worker and any sibling worker on the same node.
Interconnect PeerInterconnect
// TrustMode + IOLevel are the strongest guarantees this worker can
// currently honour. Set after attestation. A worker that has not yet
// attested reports TrustPublicDeterministic + IOLevelNone.
TrustMode lattice.ComputeTrustMode
IOLevel lattice.ConfidentialIOLevel
// AttestationRoot is the SHA-256 of the worker's most recent composite
// attestation envelope (see pkg/attestation.CompositeAttestation.Root).
// Zero ([32]byte{}) iff the worker has not produced an attestation;
// such workers are forced to TrustPublicDeterministic regardless of
// the TrustMode field above.
AttestationRoot [32]byte
}
// Validate performs structural checks against the worker capability. It
// rejects records that the scheduler cannot reason about. It does NOT
// verify the attestation -- callers must do that separately.
func (w *WorkerCapability) Validate() error {
if w == nil {
return ErrNilCapability
}
if w.WorkerID == "" {
return errors.New("registry: worker_id required")
}
if w.NodeID == "" {
return errors.New("registry: node_id required")
}
if !strings.HasPrefix(w.WorkerID, w.NodeID+"/") {
return errors.New("registry: worker_id must be prefixed with node_id/")
}
if !w.Arch.Valid() {
return errors.New("registry: invalid arch")
}
if !w.Backend.Valid() {
return errors.New("registry: invalid backend")
}
if !w.Interconnect.Valid() {
return errors.New("registry: invalid interconnect")
}
if !w.TrustMode.Valid() {
return errors.New("registry: invalid trust_mode")
}
if !w.IOLevel.Valid() {
return errors.New("registry: invalid io_level")
}
// Trust mode >= CpuGpuCompositeTEE requires arch with on-die CC support.
if w.TrustMode >= lattice.TrustCpuGpuCompositeTEE && !w.Arch.SupportsConfidentialCompute() {
return errors.New("registry: trust_mode >= CpuGpuCompositeTEE requires Hopper or Blackwell arch")
}
// Composite trust requires a non-zero attestation root.
if w.TrustMode >= lattice.TrustCpuGpuCompositeTEE && w.AttestationRoot == ([32]byte{}) {
return errors.New("registry: trust_mode >= CpuGpuCompositeTEE requires attestation_root")
}
// IO level cannot exceed what the trust mode supports. A worker that
// claims FullDeviceIOAttested without ConfidentialIO trust mode is
// inconsistent.
if w.IOLevel >= lattice.IOLevelProtectedCpuGpuTransfer && w.TrustMode < lattice.TrustConfidentialIO {
return errors.New("registry: protected IO level requires TrustConfidentialIO")
}
return nil
}
// NodeCapability is the per-node capability record. A node hosts one or
// more WorkerCapability entries plus node-level metadata (CPU TEE type,
// region, operator identity).
type NodeCapability struct {
// NodeID is a globally unique node identifier.
NodeID string
// Region is the physical region (e.g. "us-east-1a", "lux-sfo3-az1").
// Used for placement constraints + co-residency rules.
Region string
// OperatorID identifies the operator account that runs this node
// (e.g. an IAM org slug). Used for tenant isolation policies.
OperatorID string
// CPUTeeType is a free-form tag for the host CPU TEE. Empty string
// means no CPU TEE. Canonical values: "amd-sev-snp", "intel-tdx",
// "intel-sgx" (legacy), "arm-cca".
CPUTeeType string
// Workers are the execution endpoints hosted on this node. At least
// one entry; every entry's NodeID must match this NodeID.
Workers []WorkerCapability
// AttestationRoot is the SHA-256 of the node's most-recent composite
// attestation envelope. Distinct from per-worker roots: covers host /
// kernel / firmware components, not GPU-specific evidence.
AttestationRoot [32]byte
}
// Validate performs structural checks against the node capability. Each
// WorkerCapability is also validated; the first failure short-circuits.
func (n *NodeCapability) Validate() error {
if n == nil {
return ErrNilCapability
}
if n.NodeID == "" {
return errors.New("registry: node_id required")
}
if n.Region == "" {
return errors.New("registry: region required")
}
if n.OperatorID == "" {
return errors.New("registry: operator_id required")
}
if len(n.Workers) == 0 {
return errors.New("registry: at least one worker required")
}
seen := make(map[string]struct{}, len(n.Workers))
for i := range n.Workers {
w := &n.Workers[i]
if w.NodeID != n.NodeID {
return errors.New("registry: worker.node_id must match node.node_id")
}
if _, dup := seen[w.WorkerID]; dup {
return errors.New("registry: duplicate worker_id")
}
seen[w.WorkerID] = struct{}{}
if err := w.Validate(); err != nil {
return err
}
}
return nil
}
// ErrNilCapability is returned by Validate() when called on a nil receiver.
var ErrNilCapability = errors.New("registry: nil capability")
+291
View File
@@ -0,0 +1,291 @@
// Copyright (c) 2026, Lux Industries Inc.
// SPDX-License-Identifier: BSD-3-Clause
package registry
import (
"errors"
"strings"
"testing"
lattice "github.com/luxfi/lattice/v7/types"
)
// -----------------------------------------------------------------------------
// GPUArch
// -----------------------------------------------------------------------------
func TestGPUArch_StableValues(t *testing.T) {
cases := []struct {
arch GPUArch
want uint8
}{
{GPUArchUnknown, 0},
{GPUArchAmpere, 1},
{GPUArchHopper, 2},
{GPUArchBlackwell, 3},
{GPUArchAdaLovelace, 4},
{GPUArchAppleSilicon, 5},
{GPUArchAMDCDNA, 6},
{GPUArchIntelXeHPC, 7},
}
for _, c := range cases {
if uint8(c.arch) != c.want {
t.Errorf("%s: got %d want %d", c.arch, uint8(c.arch), c.want)
}
}
}
func TestGPUArch_String(t *testing.T) {
if got := GPUArchHopper.String(); got != "Hopper" {
t.Errorf("got %q want Hopper", got)
}
if got := GPUArch(99).String(); got != "Unknown" {
t.Errorf("got %q want Unknown", got)
}
}
func TestGPUArch_Valid(t *testing.T) {
for a := GPUArch(0); a <= GPUArchIntelXeHPC; a++ {
if !a.Valid() {
t.Errorf("expected %s to be valid", a)
}
}
if GPUArch(99).Valid() {
t.Error("expected 99 to be invalid")
}
}
func TestGPUArch_SupportsConfidentialCompute(t *testing.T) {
cases := []struct {
arch GPUArch
want bool
}{
{GPUArchUnknown, false},
{GPUArchAmpere, false},
{GPUArchHopper, true},
{GPUArchBlackwell, true},
{GPUArchAdaLovelace, false},
{GPUArchAppleSilicon, false},
{GPUArchAMDCDNA, false},
{GPUArchIntelXeHPC, false},
}
for _, c := range cases {
if got := c.arch.SupportsConfidentialCompute(); got != c.want {
t.Errorf("%s: got %v want %v", c.arch, got, c.want)
}
}
}
// -----------------------------------------------------------------------------
// GPUBackend
// -----------------------------------------------------------------------------
func TestGPUBackend_StableValues(t *testing.T) {
cases := []struct {
b GPUBackend
want uint8
}{
{GPUBackendUnknown, 0},
{GPUBackendCUDA, 1},
{GPUBackendROCm, 2},
{GPUBackendMetal, 3},
{GPUBackendOneAPI, 4},
{GPUBackendVulkan, 5},
{GPUBackendWebGPU, 6},
{GPUBackendCPU, 7},
}
for _, c := range cases {
if uint8(c.b) != c.want {
t.Errorf("%s: got %d want %d", c.b, uint8(c.b), c.want)
}
}
}
func TestGPUBackend_Valid(t *testing.T) {
for b := GPUBackend(0); b <= GPUBackendCPU; b++ {
if !b.Valid() {
t.Errorf("expected %s to be valid", b)
}
}
if GPUBackend(99).Valid() {
t.Error("expected 99 to be invalid")
}
}
// -----------------------------------------------------------------------------
// PeerInterconnect
// -----------------------------------------------------------------------------
func TestPeerInterconnect_Order(t *testing.T) {
chain := []PeerInterconnect{
PeerInterconnectNone,
PeerInterconnectPCIe,
PeerInterconnectNVLink,
PeerInterconnectNVSwitch,
PeerInterconnectInfinity,
}
for i := 1; i < len(chain); i++ {
if !(chain[i] > chain[i-1]) {
t.Fatalf("interconnect ordering broken at %s -> %s", chain[i-1], chain[i])
}
}
}
// -----------------------------------------------------------------------------
// WorkerCapability.Validate
// -----------------------------------------------------------------------------
func goodWorker() WorkerCapability {
return WorkerCapability{
WorkerID: "node-a/0",
NodeID: "node-a",
Arch: GPUArchHopper,
Backend: GPUBackendCUDA,
VRAMBytes: 80 << 30,
FP16TFlops: 1000,
FP8TFlops: 2000,
INT8TOps: 4000,
Interconnect: PeerInterconnectNVSwitch,
TrustMode: lattice.TrustCpuGpuCompositeTEE,
IOLevel: lattice.IOLevelCpuGpuComposite,
AttestationRoot: [32]byte{0x01},
}
}
func TestWorkerCapability_Validate_Good(t *testing.T) {
w := goodWorker()
if err := w.Validate(); err != nil {
t.Fatalf("good worker rejected: %v", err)
}
}
func TestWorkerCapability_Validate_Errors(t *testing.T) {
cases := []struct {
name string
mut func(*WorkerCapability)
errSub string
}{
{"nil_worker_id", func(w *WorkerCapability) { w.WorkerID = "" }, "worker_id required"},
{"nil_node_id", func(w *WorkerCapability) { w.NodeID = "" }, "node_id required"},
{"prefix_mismatch", func(w *WorkerCapability) { w.WorkerID = "other/0" }, "prefixed with node_id"},
{"bad_arch", func(w *WorkerCapability) { w.Arch = GPUArch(99) }, "invalid arch"},
{"bad_backend", func(w *WorkerCapability) { w.Backend = GPUBackend(99) }, "invalid backend"},
{"bad_interconnect", func(w *WorkerCapability) { w.Interconnect = PeerInterconnect(99) }, "invalid interconnect"},
{"bad_trust", func(w *WorkerCapability) { w.TrustMode = lattice.ComputeTrustMode(99) }, "invalid trust_mode"},
{"bad_io", func(w *WorkerCapability) { w.IOLevel = lattice.ConfidentialIOLevel(99) }, "invalid io_level"},
{
"composite_on_ampere",
func(w *WorkerCapability) {
w.Arch = GPUArchAmpere
w.TrustMode = lattice.TrustCpuGpuCompositeTEE
},
"requires Hopper or Blackwell",
},
{
"composite_no_root",
func(w *WorkerCapability) {
w.AttestationRoot = [32]byte{}
},
"requires attestation_root",
},
{
"protected_io_without_confidential_trust",
func(w *WorkerCapability) {
w.IOLevel = lattice.IOLevelProtectedCpuGpuTransfer
w.TrustMode = lattice.TrustCpuGpuCompositeTEE
},
"protected IO level requires TrustConfidentialIO",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
w := goodWorker()
c.mut(&w)
err := w.Validate()
if err == nil {
t.Fatalf("expected error containing %q, got nil", c.errSub)
}
if !strings.Contains(err.Error(), c.errSub) {
t.Fatalf("expected error containing %q, got %q", c.errSub, err.Error())
}
})
}
}
func TestWorkerCapability_Validate_NilReceiver(t *testing.T) {
var w *WorkerCapability
if err := w.Validate(); !errors.Is(err, ErrNilCapability) {
t.Fatalf("expected ErrNilCapability, got %v", err)
}
}
// -----------------------------------------------------------------------------
// NodeCapability.Validate
// -----------------------------------------------------------------------------
func goodNode() NodeCapability {
w := goodWorker()
return NodeCapability{
NodeID: "node-a",
Region: "us-east-1a",
OperatorID: "lux",
CPUTeeType: "amd-sev-snp",
Workers: []WorkerCapability{w},
}
}
func TestNodeCapability_Validate_Good(t *testing.T) {
n := goodNode()
if err := n.Validate(); err != nil {
t.Fatalf("good node rejected: %v", err)
}
}
func TestNodeCapability_Validate_Errors(t *testing.T) {
cases := []struct {
name string
mut func(*NodeCapability)
errSub string
}{
{"nil_node_id", func(n *NodeCapability) { n.NodeID = "" }, "node_id required"},
{"nil_region", func(n *NodeCapability) { n.Region = "" }, "region required"},
{"nil_operator", func(n *NodeCapability) { n.OperatorID = "" }, "operator_id required"},
{"no_workers", func(n *NodeCapability) { n.Workers = nil }, "at least one worker"},
{
"worker_node_mismatch",
func(n *NodeCapability) {
n.Workers[0].NodeID = "other"
n.Workers[0].WorkerID = "other/0"
},
"must match",
},
{
"duplicate_worker_id",
func(n *NodeCapability) {
n.Workers = append(n.Workers, n.Workers[0])
},
"duplicate worker_id",
},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
n := goodNode()
c.mut(&n)
err := n.Validate()
if err == nil {
t.Fatalf("expected error containing %q, got nil", c.errSub)
}
if !strings.Contains(err.Error(), c.errSub) {
t.Fatalf("expected error containing %q, got %q", c.errSub, err.Error())
}
})
}
}
func TestNodeCapability_Validate_NilReceiver(t *testing.T) {
var n *NodeCapability
if err := n.Validate(); !errors.Is(err, ErrNilCapability) {
t.Fatalf("expected ErrNilCapability, got %v", err)
}
}