Repository: bacalhau-project/bacalhau (language: python)

Issue:
Compute Nodes broadcast `NodeInfo` with `unknown` approval status which overrides previous approvals/rejections
Auto-approved/pending nodes receive unknown status since compute nodes broadcast empty approval state to requester node. There are several solutions to address this:
- shoe-horn in a fix to the requester that ignores the status from compute nodes
- remove the approval state and connected state from the `NodeInfo` structure as details are unknown to compute node. It's only relevant in the context of a requester node.


Discussion / hints:

Fix patch:
diff --git a/cmd/cli/agent/node.go b/cmd/cli/agent/node.go
index 4fe10f36c5..8781a76c63 100644
--- a/cmd/cli/agent/node.go
+++ b/cmd/cli/agent/node.go
@@ -43,7 +43,7 @@ func (o *NodeOptions) runNode(cmd *cobra.Command, _ []string) error {
 		return fmt.Errorf("could not get server node: %w", err)
 	}
 
-	writeErr := output.OutputOneNonTabular(cmd, o.OutputOpts, response.NodeInfo)
+	writeErr := output.OutputOneNonTabular(cmd, o.OutputOpts, response.NodeState)
 	if writeErr != nil {
 		return fmt.Errorf("failed to write node: %w", writeErr)
 	}
diff --git a/cmd/cli/job/describe.go b/cmd/cli/job/describe.go
index 736291370a..e3244b6b45 100644
--- a/cmd/cli/job/describe.go
+++ b/cmd/cli/job/describe.go
@@ -6,15 +6,16 @@ import (
 	"slices"
 	"time"
 
-	"github.com/bacalhau-project/bacalhau/pkg/lib/collections"
-	"github.com/bacalhau-project/bacalhau/pkg/models"
-	"github.com/bacalhau-project/bacalhau/pkg/util/idgen"
 	"github.com/jedib0t/go-pretty/v6/table"
 	"github.com/jedib0t/go-pretty/v6/text"
 	"github.com/samber/lo"
 	"github.com/spf13/cobra"
 	"k8s.io/kubectl/pkg/util/i18n"
 
+	"github.com/bacalhau-project/bacalhau/pkg/lib/collections"
+	"github.com/bacalhau-project/bacalhau/pkg/models"
+	"github.com/bacalhau-project/bacalhau/pkg/util/idgen"
+
 	"github.com/bacalhau-project/bacalhau/cmd/util"
 	"github.com/bacalhau-project/bacalhau/cmd/util/flags/cliflags"
 	"github.com/bacalhau-project/bacalhau/cmd/util/output"
diff --git a/cmd/cli/node/columns.go b/cmd/cli/node/columns.go
index 5e14ea579d..9663922797 100644
--- a/cmd/cli/node/columns.go
+++ b/cmd/cli/node/columns.go
@@ -4,35 +4,36 @@ import (
 	"fmt"
 	"strings"
 
-	"github.com/bacalhau-project/bacalhau/cmd/util/output"
-	"github.com/bacalhau-project/bacalhau/pkg/model"
-	"github.com/bacalhau-project/bacalhau/pkg/models"
-	"github.com/bacalhau-project/bacalhau/pkg/util/idgen"
 	"github.com/c2h5oh/datasize"
 	"github.com/jedib0t/go-pretty/v6/table"
 	"github.com/jedib0t/go-pretty/v6/text"
 	"github.com/samber/lo"
 	"golang.org/x/exp/slices"
+
+	"github.com/bacalhau-project/bacalhau/cmd/util/output"
+	"github.com/bacalhau-project/bacalhau/pkg/model"
+	"github.com/bacalhau-project/bacalhau/pkg/models"
+	"github.com/bacalhau-project/bacalhau/pkg/util/idgen"
 )
 
-var alwaysColumns = []output.TableColumn[*models.NodeInfo]{
+var alwaysColumns = []output.TableColumn[*models.NodeState]{
 	{
 		ColumnConfig: table.ColumnConfig{Name: "id"},
-		Value:        func(node *models.NodeInfo) string { return idgen.ShortNodeID(node.ID()) },
+		Value:        func(node *models.NodeState) string { return idgen.ShortNodeID(node.Info.ID()) },
 	},
 	{
 		ColumnConfig: table.ColumnConfig{Name: "type"},
-		Value:        func(ni *models.NodeInfo) string { return ni.NodeType.String() },
+		Value:        func(ni *models.NodeState) string { return ni.Info.NodeType.String() },
 	},
 	{
 		ColumnConfig: table.ColumnConfig{Name: "approval"},
-		Value:        func(ni *models.NodeInfo) string { return ni.Approval.String() },
+		Value:        func(ni *models.NodeState) string { return ni.Membership.String() },
 	},
 	{
 		ColumnConfig: table.ColumnConfig{Name: "status"},
-		Value: func(ni *models.NodeInfo) string {
-			if ni.ComputeNodeInfo != nil {
-				return ni.State.String()
+		Value: func(ni *models.NodeState) string {
+			if ni.Info.ComputeNodeInfo != nil {
+				return ni.Connection.String()
 			}
 
 			return "" // nothing for requester nodes
@@ -40,12 +41,12 @@ var alwaysColumns = []output.TableColumn[*models.NodeInfo]{
 	},
 }
 
-var toggleColumns = map[string][]output.TableColumn[*models.NodeInfo]{
+var toggleColumns = map[string][]output.TableColumn[*models.NodeState]{
 	"labels": {
 		{
 			ColumnConfig: table.ColumnConfig{Name: "labels", WidthMax: 50, WidthMaxEnforcer: text.WrapSoft},
-			Value: func(ni *models.NodeInfo) string {
-				labels := lo.MapToSlice(ni.Labels, func(key, val string) string { return fmt.Sprintf("%s=%s", key, val) })
+			Value: func(ni *models.NodeState) string {
+				labels := lo.MapToSlice(ni.Info.Labels, func(key, val string) string { return fmt.Sprintf("%s=%s", key, val) })
 				slices.Sort(labels)
 				return strings.Join(labels, " ")
 			},
@@ -54,20 +55,20 @@ var toggleColumns = map[string][]output.TableColumn[*models.NodeInfo]{
 	"version": {
 		{
 			ColumnConfig: table.ColumnConfig{Name: "version"},
-			Value: func(ni *models.NodeInfo) string {
-				return ni.BacalhauVersion.GitVersion
+			Value: func(ni *models.NodeState) string {
+				return ni.Info.BacalhauVersion.GitVersion
 			},
 		},
 		{
 			ColumnConfig: table.ColumnConfig{Name: "architecture"},
-			Value: func(ni *models.NodeInfo) string {
-				return ni.BacalhauVersion.GOARCH
+			Value: func(ni *models.NodeState) string {
+				return ni.Info.BacalhauVersion.GOARCH
 			},
 		},
 		{
 			ColumnConfig: table.ColumnConfig{Name: "os"},
-			Value: func(ni *models.NodeInfo) string {
-				return ni.BacalhauVersion.GOOS
+			Value: func(ni *models.NodeState) string {
+				return ni.Info.BacalhauVersion.GOOS
 			},
 		},
 	},
@@ -123,11 +124,11 @@ func maxLen(val []string) int {
 	return lo.Max(lo.Map[string, int](val, func(item string, index int) int { return len(item) })) + 1
 }
 
-func ifComputeNode(getFromCNInfo func(*models.ComputeNodeInfo) string) func(*models.NodeInfo) string {
-	return func(ni *models.NodeInfo) string {
-		if ni.ComputeNodeInfo == nil {
+func ifComputeNode(getFromCNInfo func(*models.ComputeNodeInfo) string) func(state *models.NodeState) string {
+	return func(ni *models.NodeState) string {
+		if ni.Info.ComputeNodeInfo == nil {
 			return ""
 		}
-		return getFromCNInfo(ni.ComputeNodeInfo)
+		return getFromCNInfo(ni.Info.ComputeNodeInfo)
 	}
 }
diff --git a/cmd/cli/serve/util.go b/cmd/cli/serve/util.go
index e178f827a4..d3650094cf 100644
--- a/cmd/cli/serve/util.go
+++ b/cmd/cli/serve/util.go
@@ -7,16 +7,17 @@ import (
 	"path/filepath"
 	"time"
 
+	pkgerrors "github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+	"github.com/samber/lo"
+	"github.com/spf13/viper"
+
 	"github.com/bacalhau-project/bacalhau/pkg/compute/store"
 	"github.com/bacalhau-project/bacalhau/pkg/compute/store/boltdb"
 	"github.com/bacalhau-project/bacalhau/pkg/jobstore"
 	boltjobstore "github.com/bacalhau-project/bacalhau/pkg/jobstore/boltdb"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/util/idgen"
-	pkgerrors "github.com/pkg/errors"
-	"github.com/rs/zerolog/log"
-	"github.com/samber/lo"
-	"github.com/spf13/viper"
 
 	"github.com/bacalhau-project/bacalhau/cmd/util/flags/configflags"
 	"github.com/bacalhau-project/bacalhau/pkg/orchestrator/transformer"
@@ -127,9 +128,9 @@ func GetRequesterConfig(ctx context.Context, createJobStore bool) (node.Requeste
 	}
 
 	if cfg.ManualNodeApproval {
-		requesterConfig.DefaultApprovalState = models.NodeApprovals.PENDING
+		requesterConfig.DefaultApprovalState = models.NodeMembership.PENDING
 	} else {
-		requesterConfig.DefaultApprovalState = models.NodeApprovals.APPROVED
+		requesterConfig.DefaultApprovalState = models.NodeMembership.APPROVED
 	}
 
 	return requesterConfig, nil
diff --git a/ops/aws/canary/lambda/cmd/alarm_slack_handler/utils.go b/ops/aws/canary/lambda/cmd/alarm_slack_handler/utils.go
index 1664b3546d..812b8536b1 100644
--- a/ops/aws/canary/lambda/cmd/alarm_slack_handler/utils.go
+++ b/ops/aws/canary/lambda/cmd/alarm_slack_handler/utils.go
@@ -2,20 +2,21 @@ package main
 
 import (
 	"encoding/json"
+	"os"
+	"strconv"
+	"time"
+
 	"github.com/aws/aws-lambda-go/events"
 	"github.com/aws/aws-sdk-go/aws"
 	"github.com/aws/aws-sdk-go/aws/session"
 	"github.com/aws/aws-sdk-go/service/secretsmanager"
 	"github.com/slack-go/slack"
-	"os"
-	"strconv"
-	"time"
 )
 
 func mustGetSlackSecret() slackSecretType {
 	secretName := os.Getenv("SLACK_SECRET_NAME")
 
-	//Create a Secrets Manager client
+	// Create a Secrets Manager client
 	sess, err := session.NewSession()
 	if err != nil {
 		panic(err)
diff --git a/pkg/compute/management_client.go b/pkg/compute/management_client.go
index c0c9946cd0..11bbb990e1 100644
--- a/pkg/compute/management_client.go
+++ b/pkg/compute/management_client.go
@@ -57,11 +57,12 @@ func NewManagementClient(params *ManagementClientParams) *ManagementClient {
 }
 
 func (m *ManagementClient) getNodeInfo(ctx context.Context) models.NodeInfo {
-	return m.nodeInfoDecorator.DecorateNodeInfo(ctx, models.NodeInfo{
+	ni := m.nodeInfoDecorator.DecorateNodeInfo(ctx, models.NodeInfo{
 		NodeID:   m.nodeID,
 		NodeType: models.NodeTypeCompute,
 		Labels:   m.labelsProvider.GetLabels(ctx),
 	})
+	return ni
 }
 
 // RegisterNode sends a registration request to the requester node. If we successfully
diff --git a/pkg/devstack/option.go b/pkg/devstack/option.go
index 9de52c60b3..208a9e01d3 100644
--- a/pkg/devstack/option.go
+++ b/pkg/devstack/option.go
@@ -124,7 +124,7 @@ func (o *DevStackConfig) Validate() error {
 
 func WithAutoNodeApproval() ConfigOption {
 	return func(cfg *DevStackConfig) {
-		cfg.RequesterConfig.DefaultApprovalState = models.NodeApprovals.APPROVED
+		cfg.RequesterConfig.DefaultApprovalState = models.NodeMembership.APPROVED
 	}
 }
 
diff --git a/pkg/libp2p/transport/libp2p.go b/pkg/libp2p/transport/libp2p.go
index 3fe9fd623a..2e258e4c53 100644
--- a/pkg/libp2p/transport/libp2p.go
+++ b/pkg/libp2p/transport/libp2p.go
@@ -6,6 +6,13 @@ import (
 	"fmt"
 	"time"
 
+	libp2p_pubsub "github.com/libp2p/go-libp2p-pubsub"
+	"github.com/libp2p/go-libp2p/core/host"
+	basichost "github.com/libp2p/go-libp2p/p2p/host/basic"
+	routedhost "github.com/libp2p/go-libp2p/p2p/host/routed"
+	"github.com/libp2p/go-libp2p/p2p/protocol/identify"
+	"github.com/multiformats/go-multiaddr"
+
 	"github.com/bacalhau-project/bacalhau/pkg/compute"
 	pkgconfig "github.com/bacalhau-project/bacalhau/pkg/config"
 	"github.com/bacalhau-project/bacalhau/pkg/lib/validate"
@@ -18,12 +25,6 @@ import (
 	"github.com/bacalhau-project/bacalhau/pkg/system"
 	core_transport "github.com/bacalhau-project/bacalhau/pkg/transport"
 	"github.com/bacalhau-project/bacalhau/pkg/transport/bprotocol"
-	libp2p_pubsub "github.com/libp2p/go-libp2p-pubsub"
-	"github.com/libp2p/go-libp2p/core/host"
-	basichost "github.com/libp2p/go-libp2p/p2p/host/basic"
-	routedhost "github.com/libp2p/go-libp2p/p2p/host/routed"
-	"github.com/libp2p/go-libp2p/p2p/protocol/identify"
-	"github.com/multiformats/go-multiaddr"
 )
 
 const NodeInfoTopic = "bacalhau-node-info"
@@ -46,7 +47,7 @@ type Libp2pTransport struct {
 	Host              host.Host
 	computeProxy      *bprotocol.ComputeProxy
 	callbackProxy     *bprotocol.CallbackProxy
-	nodeInfoPubSub    pubsub.PubSub[models.NodeInfo]
+	nodeInfoPubSub    pubsub.PubSub[models.NodeState]
 	nodeInfoDecorator models.NodeInfoDecorator
 }
 
@@ -71,7 +72,7 @@ func NewLibp2pTransport(ctx context.Context,
 	}
 
 	// PubSub to publish node info to the network
-	nodeInfoPubSub, err := libp2p.NewPubSub[models.NodeInfo](libp2p.PubSubParams{
+	nodeInfoPubSub, err := libp2p.NewPubSub[models.NodeState](libp2p.PubSubParams{
 		Host:      libp2pHost,
 		TopicName: NodeInfoTopic,
 		PubSub:    gossipSub,
@@ -129,8 +130,8 @@ func NewLibp2pTransport(ctx context.Context,
 
 func (t *Libp2pTransport) RegisterNodeInfoConsumer(ctx context.Context, nodeInfoStore routing.NodeInfoStore) error {
 	// register consumers of node info published over gossipSub
-	nodeInfoSubscriber := pubsub.NewChainedSubscriber[models.NodeInfo](true)
-	nodeInfoSubscriber.Add(pubsub.SubscriberFunc[models.NodeInfo](nodeInfoStore.Add))
+	nodeInfoSubscriber := pubsub.NewChainedSubscriber[models.NodeState](true)
+	nodeInfoSubscriber.Add(pubsub.SubscriberFunc[models.NodeState](nodeInfoStore.Add))
 
 	return t.nodeInfoPubSub.Subscribe(ctx, nodeInfoSubscriber)
 }
@@ -181,7 +182,7 @@ func (t *Libp2pTransport) ManagementProxy() compute.ManagementEndpoint {
 }
 
 // NodeInfoPubSub returns the node info pubsub.
-func (t *Libp2pTransport) NodeInfoPubSub() pubsub.PubSub[models.NodeInfo] {
+func (t *Libp2pTransport) NodeInfoPubSub() pubsub.PubSub[models.NodeState] {
 	return t.nodeInfoPubSub
 }
 
diff --git a/pkg/models/node_approval.go b/pkg/models/node_approval.go
index 93f8e141d6..0f5dc5fcf2 100644
--- a/pkg/models/node_approval.go
+++ b/pkg/models/node_approval.go
@@ -4,93 +4,95 @@ import (
 	"fmt"
 )
 
-type NodeApproval struct {
-	approval
+// TODO if we ever pass a pointer to this type and use `==` comparison on it we're gonna have a bad time
+// implement an `Equal()` method for this type and default to it.
+type NodeMembershipState struct {
+	membership
 }
 
-type approval int
+type membership int
 
 const (
-	unknown approval = iota
+	unknown membership = iota
 	pending
 	approved
 	rejected
 )
 
 var (
-	strApprovalArray = [...]string{
+	strMembershipArray = [...]string{
 		pending:  "PENDING",
 		approved: "APPROVED",
 		rejected: "REJECTED",
 	}
 
-	typeApprovalMap = map[string]approval{
+	typeMembershipMap = map[string]membership{
 		"PENDING":  pending,
 		"APPROVED": approved,
 		"REJECTED": rejected,
 	}
 )
 
-func (t approval) String() string {
-	return strApprovalArray[t]
+func (t membership) String() string {
+	return strMembershipArray[t]
 }
 
-func Parse(a any) NodeApproval {
+func Parse(a any) NodeMembershipState {
 	switch v := a.(type) {
-	case NodeApproval:
+	case NodeMembershipState:
 		return v
 	case string:
-		return NodeApproval{stringToApproval(v)}
+		return NodeMembershipState{stringToApproval(v)}
 	case fmt.Stringer:
-		return NodeApproval{stringToApproval(v.String())}
+		return NodeMembershipState{stringToApproval(v.String())}
 	case int:
-		return NodeApproval{approval(v)}
+		return NodeMembershipState{membership(v)}
 	case int64:
-		return NodeApproval{approval(int(v))}
+		return NodeMembershipState{membership(int(v))}
 	case int32:
-		return NodeApproval{approval(int(v))}
+		return NodeMembershipState{membership(int(v))}
 	}
-	return NodeApproval{unknown}
+	return NodeMembershipState{unknown}
 }
 
-func stringToApproval(s string) approval {
-	if v, ok := typeApprovalMap[s]; ok {
+func stringToApproval(s string) membership {
+	if v, ok := typeMembershipMap[s]; ok {
 		return v
 	}
 	return unknown
 }
 
-func (t approval) IsValid() bool {
-	return t >= approval(1) && t <= approval(len(strApprovalArray))
+func (t membership) IsValid() bool {
+	return t >= membership(1) && t <= membership(len(strMembershipArray))
 }
 
-type approvalsContainer struct {
-	UNKNOWN  NodeApproval
-	PENDING  NodeApproval
-	APPROVED NodeApproval
-	REJECTED NodeApproval
+type membershipContainer struct {
+	UNKNOWN  NodeMembershipState
+	PENDING  NodeMembershipState
+	APPROVED NodeMembershipState
+	REJECTED NodeMembershipState
 }
 
-var NodeApprovals = approvalsContainer{
-	UNKNOWN:  NodeApproval{unknown},
-	PENDING:  NodeApproval{pending},
-	APPROVED: NodeApproval{approved},
-	REJECTED: NodeApproval{rejected},
+var NodeMembership = membershipContainer{
+	UNKNOWN:  NodeMembershipState{unknown},
+	PENDING:  NodeMembershipState{pending},
+	APPROVED: NodeMembershipState{approved},
+	REJECTED: NodeMembershipState{rejected},
 }
 
-func (c approvalsContainer) All() []NodeApproval {
-	return []NodeApproval{
+func (c membershipContainer) All() []NodeMembershipState {
+	return []NodeMembershipState{
 		c.PENDING,
 		c.APPROVED,
 		c.REJECTED,
 	}
 }
 
-func (t NodeApproval) MarshalJSON() ([]byte, error) {
+func (t NodeMembershipState) MarshalJSON() ([]byte, error) {
 	return []byte(`"` + t.String() + `"`), nil
 }
 
-func (t *NodeApproval) UnmarshalJSON(b []byte) error {
+func (t *NodeMembershipState) UnmarshalJSON(b []byte) error {
 	val := string(trimQuotes(b))
 	*t = Parse(val)
 	return nil
diff --git a/pkg/models/node_connection.go b/pkg/models/node_connection.go
new file mode 100644
index 0000000000..a8fe879f35
--- /dev/null
+++ b/pkg/models/node_connection.go
@@ -0,0 +1,97 @@
+package models
+
+import (
+	"fmt"
+)
+
+// TODO if we ever pass a pointer to this type and use `==` comparison on it we're gonna have a bad time
+// implement an `Equal()` method for this type and default to it.
+type NodeConnectionState struct {
+	connection
+}
+
+type connection int
+
+// To add a new state (for instance, a state beyond which the node is considered
+// lost) then:
+// * add it to the end of the list in the const below
+// * add it to strConnectionArray and typeConnectionMap
+// * add it to the livenessContainer and corresponding NodeStates var.
+// * add it to the All() method in the livenessContainer
+const (
+	connected connection = iota
+	disconnected
+)
+
+var (
+	strConnectionArray = [...]string{
+		connected:    "CONNECTED",
+		disconnected: "DISCONNECTED",
+	}
+
+	typeConnectionMap = map[string]connection{
+		"CONNECTED":    connected,
+		"DISCONNECTED": disconnected,
+	}
+)
+
+func (t connection) String() string {
+	return strConnectionArray[t]
+}
+
+func ParseConnection(a any) NodeConnectionState {
+	switch v := a.(type) {
+	case NodeConnectionState:
+		return v
+	case string:
+		return NodeConnectionState{stringToConnection(v)}
+	case fmt.Stringer:
+		return NodeConnectionState{stringToConnection(v.String())}
+	case int:
+		return NodeConnectionState{connection(v)}
+	case int64:
+		return NodeConnectionState{connection(int(v))}
+	case int32:
+		return NodeConnectionState{connection(int(v))}
+	}
+	return NodeConnectionState{disconnected}
+}
+
+func stringToConnection(s string) connection {
+	if v, ok := typeConnectionMap[s]; ok {
+		return v
+	}
+	return disconnected
+}
+
+func (t connection) IsValid() bool {
+	return t >= connection(1) && t <= connection(len(strConnectionArray))
+}
+
+type livenessContainer struct {
+	CONNECTED    NodeConnectionState
+	DISCONNECTED NodeConnectionState
+	HEALTHY      NodeConnectionState
+}
+
+var NodeStates = livenessContainer{
+	CONNECTED:    NodeConnectionState{connected},
+	DISCONNECTED: NodeConnectionState{disconnected},
+}
+
+func (c livenessContainer) All() []NodeConnectionState {
+	return []NodeConnectionState{
+		c.CONNECTED,
+		c.DISCONNECTED,
+	}
+}
+
+func (s NodeConnectionState) MarshalJSON() ([]byte, error) {
+	return []byte(`"` + s.String() + `"`), nil
+}
+
+func (s *NodeConnectionState) UnmarshalJSON(b []byte) error {
+	val := string(trimQuotes(b))
+	*s = ParseConnection(val)
+	return nil
+}
diff --git a/pkg/models/node_info.go b/pkg/models/node_info.go
index 1a0a5a5dc2..793ef499ca 100644
--- a/pkg/models/node_info.go
+++ b/pkg/models/node_info.go
@@ -38,8 +38,8 @@ func (e *NodeType) UnmarshalText(text []byte) (err error) {
 	return
 }
 
-type NodeInfoProvider interface {
-	GetNodeInfo(ctx context.Context) NodeInfo
+type NodeStateProvider interface {
+	GetNodeState(ctx context.Context) NodeState
 }
 
 type LabelsProvider interface {
@@ -74,17 +74,17 @@ func (n NoopNodeInfoDecorator) DecorateNodeInfo(ctx context.Context, nodeInfo No
 	return nodeInfo
 }
 
-// NodeInfo
-// TODO: add Validate() method to NodeInfo and make sure it is called in all the places where it is initialized
+// NodeInfo contains metadata about a node on the network. Compute nodes share their NodeInfo with Requester nodes
+// to further its view of the networks conditions. ComputeNodeInfo is non-nil iff the NodeType is NodeTypeCompute.
+// TODO(walid): add Validate() method to NodeInfo and make sure it is called in all the places where it is initialized
 type NodeInfo struct {
+	// TODO replace all access on this field with the `ID()` method
 	NodeID          string            `json:"NodeID"`
 	PeerInfo        *peer.AddrInfo    `json:"PeerInfo,omitempty" yaml:",omitempty"`
 	NodeType        NodeType          `json:"NodeType"`
 	Labels          map[string]string `json:"Labels"`
 	ComputeNodeInfo *ComputeNodeInfo  `json:"ComputeNodeInfo,omitempty" yaml:",omitempty"`
 	BacalhauVersion BuildVersionInfo  `json:"BacalhauVersion"`
-	Approval        NodeApproval      `json:"Approval"`
-	State           NodeState         `json:"State"`
 }
 
 // ID returns the node ID
@@ -102,6 +102,8 @@ func (n NodeInfo) IsComputeNode() bool {
 	return n.NodeType == NodeTypeCompute
 }
 
+// ComputeNodeInfo contains metadata about the current state and abilities of a compute node. Compute Nodes share
+// this state with Requester nodes by including it in the NodeInfo they share across the network.
 type ComputeNodeInfo struct {
 	ExecutionEngines   []string  `json:"ExecutionEngines"`
 	Publishers         []string  `json:"Publishers"`
diff --git a/pkg/models/node_state.go b/pkg/models/node_state.go
index 94aea6101b..c9130216ef 100644
--- a/pkg/models/node_state.go
+++ b/pkg/models/node_state.go
@@ -1,95 +1,9 @@
 package models
 
-import (
-	"fmt"
-)
-
+// NodeState contains metadata about the state of a node on the network. Requester nodes maintain a NodeState for
+// each node they are aware of. The NodeState represents a Requester nodes view of another node on the network.
 type NodeState struct {
-	liveness
-}
-
-type liveness int
-
-// To add a new state (for instance, a state beyond which the node is considered
-// lost) then:
-// * add it to the end of the list in the const below
-// * add it to strLivenessArray and typeLivenessMap
-// * add it to the livenessContainer and corresponding NodeStates var.
-// * add it to the All() method in the livenessContainer
-const (
-	connected liveness = iota
-	disconnected
-)
-
-var (
-	strLivenessArray = [...]string{
-		connected:    "CONNECTED",
-		disconnected: "DISCONNECTED",
-	}
-
-	typeLivenessMap = map[string]liveness{
-		"CONNECTED":    connected,
-		"DISCONNECTED": disconnected,
-	}
-)
-
-func (t liveness) String() string {
-	return strLivenessArray[t]
-}
-
-func ParseState(a any) NodeState {
-	switch v := a.(type) {
-	case NodeState:
-		return v
-	case string:
-		return NodeState{stringToLiveness(v)}
-	case fmt.Stringer:
-		return NodeState{stringToLiveness(v.String())}
-	case int:
-		return NodeState{liveness(v)}
-	case int64:
-		return NodeState{liveness(int(v))}
-	case int32:
-		return NodeState{liveness(int(v))}
-	}
-	return NodeState{disconnected}
-}
-
-func stringToLiveness(s string) liveness {
-	if v, ok := typeLivenessMap[s]; ok {
-		return v
-	}
-	return disconnected
-}
-
-func (t liveness) IsValid() bool {
-	return t >= liveness(1) && t <= liveness(len(strLivenessArray))
-}
-
-type livenessContainer struct {
-	CONNECTED    NodeState
-	DISCONNECTED NodeState
-	HEALTHY      NodeState
-}
-
-var NodeStates = livenessContainer{
-	CONNECTED:    NodeState{connected},
-	DISCONNECTED: NodeState{disconnected},
-}
-
-func (c livenessContainer) All() []NodeState {
-	return []NodeState{
-		c.CONNECTED,
-		c.DISCONNECTED,
-	}
-}
-
-func (s NodeState) MarshalJSON() ([]byte, error) {
-	return []byte(`"` + s.String() + `"`), nil
-}
-
-func (s *NodeState) UnmarshalJSON(b []byte) error {
-	val := string(trimQuotes(b))
-	*s = ParseState(val)
-	return nil
+	Info       NodeInfo            `json:"Info"`
+	Membership NodeMembershipState `json:"Membership"`
+	Connection NodeConnectionState `json:"Connection"`
 }
diff --git a/pkg/nats/transport/nats.go b/pkg/nats/transport/nats.go
index f249c322b1..48cb0d91ac 100644
--- a/pkg/nats/transport/nats.go
+++ b/pkg/nats/transport/nats.go
@@ -6,6 +6,10 @@ import (
 	"fmt"
 	"strings"
 
+	"github.com/nats-io/nats-server/v2/server"
+	"github.com/nats-io/nats.go"
+	"github.com/rs/zerolog/log"
+
 	"github.com/bacalhau-project/bacalhau/pkg/compute"
 	"github.com/bacalhau-project/bacalhau/pkg/lib/validate"
 	"github.com/bacalhau-project/bacalhau/pkg/model"
@@ -16,9 +20,6 @@ import (
 	"github.com/bacalhau-project/bacalhau/pkg/pubsub"
 	"github.com/bacalhau-project/bacalhau/pkg/routing"
 	core_transport "github.com/bacalhau-project/bacalhau/pkg/transport"
-	"github.com/nats-io/nats-server/v2/server"
-	"github.com/nats-io/nats.go"
-	"github.com/rs/zerolog/log"
 )
 
 const NodeInfoSubjectPrefix = "node.info."
@@ -85,7 +86,7 @@ type NATSTransport struct {
 	natsClient        *nats_helper.ClientManager
 	computeProxy      compute.Endpoint
 	callbackProxy     compute.Callback
-	nodeInfoPubSub    pubsub.PubSub[models.NodeInfo]
+	nodeInfoPubSub    pubsub.PubSub[models.NodeState]
 	nodeInfoDecorator models.NodeInfoDecorator
 	managementProxy   compute.ManagementEndpoint
 }
@@ -149,7 +150,7 @@ func NewNATSTransport(ctx context.Context,
 	}
 
 	// PubSub to publish and consume node info messages
-	nodeInfoPubSub, err := nats_pubsub.NewPubSub[models.NodeInfo](nats_pubsub.PubSubParams{
+	nodeInfoPubSub, err := nats_pubsub.NewPubSub[models.NodeState](nats_pubsub.PubSubParams{
 		Conn:                nc.Client,
 		Subject:             NodeInfoSubjectPrefix + config.NodeID,
 		SubscriptionSubject: NodeInfoSubjectPrefix + "*",
@@ -206,8 +207,8 @@ func CreateClient(ctx context.Context, config *NATSTransportConfig) (*nats_helpe
 
 func (t *NATSTransport) RegisterNodeInfoConsumer(ctx context.Context, infostore routing.NodeInfoStore) error {
 	// subscribe to nodeInfo subject and add nodeInfo to nodeInfoStore
-	nodeInfoSubscriber := pubsub.NewChainedSubscriber[models.NodeInfo](true)
-	nodeInfoSubscriber.Add(pubsub.SubscriberFunc[models.NodeInfo](infostore.Add))
+	nodeInfoSubscriber := pubsub.NewChainedSubscriber[models.NodeState](true)
+	nodeInfoSubscriber.Add(pubsub.SubscriberFunc[models.NodeState](infostore.Add))
 	return t.nodeInfoPubSub.Subscribe(ctx, nodeInfoSubscriber)
 }
 
@@ -256,7 +257,7 @@ func (t *NATSTransport) ManagementProxy() compute.ManagementEndpoint {
 }
 
 // NodeInfoPubSub returns the node info pubsub.
-func (t *NATSTransport) NodeInfoPubSub() pubsub.PubSub[models.NodeInfo] {
+func (t *NATSTransport) NodeInfoPubSub() pubsub.PubSub[models.NodeState] {
 	return t.nodeInfoPubSub
 }
 
diff --git a/pkg/node/config_defaults.go b/pkg/node/config_defaults.go
index 283c834f04..2eb4e54252 100644
--- a/pkg/node/config_defaults.go
+++ b/pkg/node/config_defaults.go
@@ -75,7 +75,7 @@ var DefaultRequesterConfig = RequesterConfigParams{
 		NodeDisconnectedAfter:   types.Duration(30 * time.Second), //nolint:gomnd
 	},
 
-	DefaultApprovalState: models.NodeApprovals.APPROVED,
+	DefaultApprovalState: models.NodeMembership.APPROVED,
 }
 
 var TestRequesterConfig = RequesterConfigParams{
@@ -111,7 +111,7 @@ var TestRequesterConfig = RequesterConfigParams{
 		NodeDisconnectedAfter:   types.Duration(30 * time.Second), //nolint:gomnd
 	},
 
-	DefaultApprovalState: models.NodeApprovals.APPROVED,
+	DefaultApprovalState: models.NodeMembership.APPROVED,
 }
 
 func getRequesterConfigParams() RequesterConfigParams {
diff --git a/pkg/node/config_requester.go b/pkg/node/config_requester.go
index e45df01f3d..d6b683c11c 100644
--- a/pkg/node/config_requester.go
+++ b/pkg/node/config_requester.go
@@ -5,11 +5,12 @@ import (
 	"net/url"
 	"time"
 
-	"github.com/bacalhau-project/bacalhau/pkg/config/types"
-	"github.com/bacalhau-project/bacalhau/pkg/jobstore"
 	"github.com/imdario/mergo"
 	"github.com/rs/zerolog/log"
 
+	"github.com/bacalhau-project/bacalhau/pkg/config/types"
+	"github.com/bacalhau-project/bacalhau/pkg/jobstore"
+
 	"github.com/bacalhau-project/bacalhau/pkg/model"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/orchestrator"
@@ -56,7 +57,7 @@ type RequesterConfigParams struct {
 	// When new nodes join the cluster, what state do they have? By default, APPROVED, and
 	// for tests, APPROVED. We will provide an option to set this to PENDING for production
 	// or for when operators are ready to control node approval.
-	DefaultApprovalState models.NodeApproval
+	DefaultApprovalState models.NodeMembershipState
 
 	ControlPlaneSettings types.RequesterControlPlaneConfig
 }
diff --git a/pkg/node/heartbeat/server.go b/pkg/node/heartbeat/server.go
index 296b38423d..cc9443da57 100644
--- a/pkg/node/heartbeat/server.go
+++ b/pkg/node/heartbeat/server.go
@@ -27,7 +27,7 @@ type HeartbeatServer struct {
 	clock             clock.Clock
 	subscription      *natsPubSub.PubSub[Heartbeat]
 	pqueue            *collections.HashedPriorityQueue[string, TimestampedHeartbeat]
-	livenessMap       *concurrency.StripedMap[models.NodeState]
+	livenessMap       *concurrency.StripedMap[models.NodeConnectionState]
 	checkFrequency    time.Duration
 	disconnectedAfter time.Duration
 }
@@ -64,7 +64,7 @@ func NewServer(params HeartbeatServerParams) (*HeartbeatServer, error) {
 		clock:             clk,
 		subscription:      subscription,
 		pqueue:            pqueue,
-		livenessMap:       concurrency.NewStripedMap[models.NodeState](0), // no particular stripe count for now
+		livenessMap:       concurrency.NewStripedMap[models.NodeConnectionState](0), // no particular stripe count for now
 		checkFrequency:    params.CheckFrequency,
 		disconnectedAfter: params.NodeDisconnectedAfter,
 	}, nil
@@ -135,22 +135,22 @@ func (h *HeartbeatServer) CheckQueue(ctx context.Context) {
 
 // markNode will mark a node as being in a certain state. This will be used to update the node's
 // info to include the liveness state.
-func (h *HeartbeatServer) markNodeAs(nodeID string, state models.NodeState) {
+func (h *HeartbeatServer) markNodeAs(nodeID string, state models.NodeConnectionState) {
 	h.livenessMap.Put(nodeID, state)
 }
 
 // UpdateNode will add the liveness for specific nodes to their NodeInfo
-func (h *HeartbeatServer) UpdateNodeInfo(nodeInfo *models.NodeInfo) {
-	if liveness, ok := h.livenessMap.Get(nodeInfo.NodeID); ok {
-		nodeInfo.State = liveness
+func (h *HeartbeatServer) UpdateNodeInfo(state *models.NodeState) {
+	if liveness, ok := h.livenessMap.Get(state.Info.NodeID); ok {
+		state.Connection = liveness
 	} else {
 		// We've never seen this, so we'll mark it as unknown
-		nodeInfo.State = models.NodeStates.DISCONNECTED
+		state.Connection = models.NodeStates.DISCONNECTED
 	}
 }
 
 // FilterNodeInfos will return only those NodeInfos that have the requested liveness
-func (h *HeartbeatServer) FilterNodeInfos(nodeInfos []*models.NodeInfo, state models.NodeState) []*models.NodeInfo {
+func (h *HeartbeatServer) FilterNodeInfos(nodeInfos []*models.NodeInfo, state models.NodeConnectionState) []*models.NodeInfo {
 	result := make([]*models.NodeInfo, 0)
 	for _, nodeInfo := range nodeInfos {
 		if liveness, ok := h.livenessMap.Get(nodeInfo.NodeID); ok {
diff --git a/pkg/node/manager/node_manager.go b/pkg/node/manager/node_manager.go
index a5233ce53a..bb6079c310 100644
--- a/pkg/node/manager/node_manager.go
+++ b/pkg/node/manager/node_manager.go
@@ -4,15 +4,16 @@ import (
 	"context"
 	"fmt"
 
+	"github.com/libp2p/go-libp2p/core/peer"
+	"github.com/pkg/errors"
+	"github.com/rs/zerolog/log"
+
 	"github.com/bacalhau-project/bacalhau/pkg/compute"
 	"github.com/bacalhau-project/bacalhau/pkg/lib/concurrency"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/models/requests"
 	"github.com/bacalhau-project/bacalhau/pkg/node/heartbeat"
 	"github.com/bacalhau-project/bacalhau/pkg/routing"
-	"github.com/libp2p/go-libp2p/core/peer"
-	"github.com/pkg/errors"
-	"github.com/rs/zerolog/log"
 )
 
 const (
@@ -24,16 +25,16 @@ const (
 // also provides operations for querying and managing compute
 // node information.
 type NodeManager struct {
-	nodeInfo             routing.NodeInfoStore
+	store                routing.NodeInfoStore
 	resourceMap          *concurrency.StripedMap[models.Resources]
 	heartbeats           *heartbeat.HeartbeatServer
-	defaultApprovalState models.NodeApproval
+	defaultApprovalState models.NodeMembershipState
 }
 
 type NodeManagerParams struct {
 	NodeInfo             routing.NodeInfoStore
 	Heartbeats           *heartbeat.HeartbeatServer
-	DefaultApprovalState models.NodeApproval
+	DefaultApprovalState models.NodeMembershipState
 }
 
 // NewNodeManager constructs a new node manager and returns a pointer
@@ -41,7 +42,7 @@ type NodeManagerParams struct {
 func NewNodeManager(params NodeManagerParams) *NodeManager {
 	return &NodeManager{
 		resourceMap:          concurrency.NewStripedMap[models.Resources](resourceMapLockCount),
-		nodeInfo:             params.NodeInfo,
+		store:                params.NodeInfo,
 		heartbeats:           params.Heartbeats,
 		defaultApprovalState: params.DefaultApprovalState,
 	}
@@ -68,10 +69,10 @@ func (n *NodeManager) Start(ctx context.Context) error {
 // Register is part of the implementation of the ManagementEndpoint
 // interface. It is used to register a compute node with the cluster.
 func (n *NodeManager) Register(ctx context.Context, request requests.RegisterRequest) (*requests.RegisterResponse, error) {
-	existing, err := n.nodeInfo.Get(ctx, request.Info.NodeID)
+	existing, err := n.store.Get(ctx, request.Info.NodeID)
 	if err == nil {
 		// If we have already seen this node and rejected it, then let the node know
-		if existing.Approval == models.NodeApprovals.REJECTED {
+		if existing.Membership == models.NodeMembership.REJECTED {
 			return &requests.RegisterResponse{
 				Accepted: false,
 				Reason:   "node has been rejected",
@@ -86,10 +87,13 @@ func (n *NodeManager) Register(ctx context.Context, request requests.RegisterReq
 		}, nil
 	}
 
-	request.Info.Approval = n.defaultApprovalState
-
-	if err := n.nodeInfo.Add(ctx, request.Info); err != nil {
-		return nil, errors.Wrap(err, "failed to save nodeinfo during node registration")
+	if err := n.store.Add(ctx, models.NodeState{
+		Info:       request.Info,
+		Membership: n.defaultApprovalState,
+		// NB(forrest): by virtue of a compute node calling this endpoint we can consider it connected
+		Connection: models.NodeStates.CONNECTED,
+	}); err != nil {
+		return nil, errors.Wrap(err, "failed to save nodestate during node registration")
 	}
 
 	return &requests.RegisterResponse{
@@ -98,9 +102,9 @@ func (n *NodeManager) Register(ctx context.Context, request requests.RegisterReq
 }
 
 // UpdateInfo is part of the implementation of the ManagementEndpoint
-// interface. It is used to update the node info for a particular node
+// interface. It is used to update the node state for a particular node
 func (n *NodeManager) UpdateInfo(ctx context.Context, request requests.UpdateInfoRequest) (*requests.UpdateInfoResponse, error) {
-	existing, err := n.nodeInfo.Get(ctx, request.Info.NodeID)
+	existing, err := n.store.Get(ctx, request.Info.NodeID)
 
 	if errors.Is(err, routing.ErrNodeNotFound{}) {
 		return &requests.UpdateInfoResponse{
@@ -110,10 +114,10 @@ func (n *NodeManager) UpdateInfo(ctx context.Context, request requests.UpdateInf
 	}
 
 	if err != nil {
-		return nil, errors.Wrap(err, "failed to get nodeinfo during node registration")
+		return nil, errors.Wrap(err, "failed to get nodestate during node registration")
 	}
 
-	if existing.Approval == models.NodeApprovals.REJECTED {
+	if existing.Membership == models.NodeMembership.REJECTED {
 		return &requests.UpdateInfoResponse{
 			Accepted: false,
 			Reason:   "node registration rejected",
@@ -121,8 +125,14 @@ func (n *NodeManager) UpdateInfo(ctx context.Context, request requests.UpdateInf
 	}
 
 	// TODO: Add a Put endpoint that takes the revision into account?
-	if err := n.nodeInfo.Add(ctx, request.Info); err != nil {
-		return nil, errors.Wrap(err, "failed to save nodeinfo during node registration")
+	if err := n.store.Add(ctx, models.NodeState{
+		Info: request.Info,
+		// the nodes approval state is assumed to be approved here, but re-use existing state
+		Membership: existing.Membership,
+		// TODO can we assume the node is connected here?
+		Connection: models.NodeStates.CONNECTED,
+	}); err != nil {
+		return nil, errors.Wrap(err, "failed to save nodestate during node registration")
 	}
 
 	return &requests.UpdateInfoResponse{
@@ -134,12 +144,12 @@ func (n *NodeManager) UpdateInfo(ctx context.Context, request requests.UpdateInf
 // is used to augment information about the available resources for each node.
 func (n *NodeManager) UpdateResources(ctx context.Context,
 	request requests.UpdateResourcesRequest) (*requests.UpdateResourcesResponse, error) {
-	existing, err := n.nodeInfo.Get(ctx, request.NodeID)
+	existing, err := n.store.Get(ctx, request.NodeID)
 	if errors.Is(err, routing.ErrNodeNotFound{}) {
 		return nil, fmt.Errorf("unable to update resources for missing node: %s", request.NodeID)
 	}
 
-	if existing.Approval == models.NodeApprovals.REJECTED {
+	if existing.Membership == models.NodeMembership.REJECTED {
 		log.Ctx(ctx).Debug().Msg("not updating resources for rejected node ")
 		return &requests.UpdateResourcesResponse{}, nil
 	}
@@ -154,44 +164,44 @@ func (n *NodeManager) UpdateResources(ctx context.Context,
 
 // ---- Implementation of routing.NodeInfoStore ----
 func (n *NodeManager) FindPeer(ctx context.Context, peerID peer.ID) (peer.AddrInfo, error) {
-	return n.nodeInfo.FindPeer(ctx, peerID)
+	return n.store.FindPeer(ctx, peerID)
 }
 
-func (n *NodeManager) Add(ctx context.Context, nodeInfo models.NodeInfo) error {
-	return n.nodeInfo.Add(ctx, nodeInfo)
+func (n *NodeManager) Add(ctx context.Context, nodeInfo models.NodeState) error {
+	return n.store.Add(ctx, nodeInfo)
 }
 
-func (n *NodeManager) addToInfo(ctx context.Context, info *models.NodeInfo) {
-	resources, found := n.resourceMap.Get(info.NodeID)
-	if found && info.ComputeNodeInfo != nil {
-		info.ComputeNodeInfo.AvailableCapacity = resources
+func (n *NodeManager) addToInfo(ctx context.Context, state *models.NodeState) {
+	resources, found := n.resourceMap.Get(state.Info.NodeID)
+	if found && state.Info.ComputeNodeInfo != nil {
+		state.Info.ComputeNodeInfo.AvailableCapacity = resources
 	}
 
 	if n.heartbeats != nil {
-		n.heartbeats.UpdateNodeInfo(info)
+		n.heartbeats.UpdateNodeInfo(state)
 	}
 }
 
-func (n *NodeManager) Get(ctx context.Context, nodeID string) (models.NodeInfo, error) {
-	info, err := n.nodeInfo.Get(ctx, nodeID)
+func (n *NodeManager) Get(ctx context.Context, nodeID string) (models.NodeState, error) {
+	nodeState, err := n.store.Get(ctx, nodeID)
 	if err != nil {
-		return models.NodeInfo{}, err
+		return models.NodeState{}, err
 	}
-	n.addToInfo(ctx, &info)
-	return info, nil
+	n.addToInfo(ctx, &nodeState)
+	return nodeState, nil
 }
 
-func (n *NodeManager) GetByPrefix(ctx context.Context, prefix string) (models.NodeInfo, error) {
-	info, err := n.nodeInfo.GetByPrefix(ctx, prefix)
+func (n *NodeManager) GetByPrefix(ctx context.Context, prefix string) (models.NodeState, error) {
+	state, err := n.store.GetByPrefix(ctx, prefix)
 	if err != nil {
-		return models.NodeInfo{}, err
+		return models.NodeState{}, err
 	}
-	n.addToInfo(ctx, &info)
-	return info, nil
+	n.addToInfo(ctx, &state)
+	return state, nil
 }
 
-func (n *NodeManager) List(ctx context.Context, filters ...routing.NodeInfoFilter) ([]models.NodeInfo, error) {
-	items, err := n.nodeInfo.List(ctx, filters...)
+func (n *NodeManager) List(ctx context.Context, filters ...routing.NodeStateFilter) ([]models.NodeState, error) {
+	items, err := n.store.List(ctx, filters...)
 	if err != nil {
 		return nil, err
 	}
@@ -204,7 +214,7 @@ func (n *NodeManager) List(ctx context.Context, filters ...routing.NodeInfoFilte
 }
 
 func (n *NodeManager) Delete(ctx context.Context, nodeID string) error {
-	return n.nodeInfo.Delete(ctx, nodeID)
+	return n.store.Delete(ctx, nodeID)
 }
 
 // ---- Implementation of node actions ----
@@ -213,20 +223,20 @@ func (n *NodeManager) Delete(ctx context.Context, nodeID string) error {
 // reason for the approval (for audit). The return values denote success and any
 // failure of the operation as a human readable string.
 func (n *NodeManager) ApproveAction(ctx context.Context, nodeID string, reason string) (bool, string) {
-	info, err := n.nodeInfo.GetByPrefix(ctx, nodeID)
+	state, err := n.store.GetByPrefix(ctx, nodeID)
 	if err != nil {
 		return false, err.Error()
 	}
 
-	if info.Approval == models.NodeApprovals.APPROVED {
+	if state.Membership == models.NodeMembership.APPROVED {
 		return false, "node already approved"
 	}
 
-	info.Approval = models.NodeApprovals.APPROVED
+	state.Membership = models.NodeMembership.APPROVED
 	log.Ctx(ctx).Info().Str("reason", reason).Msgf("node %s approved", nodeID)
 
-	if err := n.nodeInfo.Add(ctx, info); err != nil {
-		return false, "failed to save nodeinfo during node approval"
+	if err := n.store.Add(ctx, state); err != nil {
+		return false, "failed to save nodestate during node approval"
 	}
 
 	return true, ""
@@ -236,20 +246,20 @@ func (n *NodeManager) ApproveAction(ctx context.Context, nodeID string, reason s
 // reason for the rejection (for audit). The return values denote success and any
 // failure of the operation as a human readable string.
 func (n *NodeManager) RejectAction(ctx context.Context, nodeID string, reason string) (bool, string) {
-	info, err := n.nodeInfo.GetByPrefix(ctx, nodeID)
+	state, err := n.store.GetByPrefix(ctx, nodeID)
 	if err != nil {
 		return false, err.Error()
 	}
 
-	if info.Approval == models.NodeApprovals.REJECTED {
+	if state.Membership == models.NodeMembership.REJECTED {
 		return false, "node already rejected"
 	}
 
-	info.Approval = models.NodeApprovals.REJECTED
+	state.Membership = models.NodeMembership.REJECTED
 	log.Ctx(ctx).Info().Str("reason", reason).Msgf("node %s rejected", nodeID)
 
-	if err := n.nodeInfo.Add(ctx, info); err != nil {
-		return false, "failed to save nodeinfo during node rejection"
+	if err := n.store.Add(ctx, state); err != nil {
+		return false, "failed to save nodestate during node rejection"
 	}
 
 	return true, ""
@@ -259,13 +269,13 @@ func (n *NodeManager) RejectAction(ctx context.Context, nodeID string, reason st
 // reason for the rejection (for audit). The return values denote success and any
 // failure of the operation as a human readable string.
 func (n *NodeManager) DeleteAction(ctx context.Context, nodeID string, reason string) (bool, string) {
-	info, err := n.nodeInfo.GetByPrefix(ctx, nodeID)
+	state, err := n.store.GetByPrefix(ctx, nodeID)
 	if err != nil {
 		return false, err.Error()
 	}
 
-	if err := n.nodeInfo.Delete(ctx, info.NodeID); err != nil {
-		return false, fmt.Sprintf("failed to delete nodeinfo: %s", err)
+	if err := n.store.Delete(ctx, state.Info.NodeID); err != nil {
+		return false, fmt.Sprintf("failed to delete nodestate: %s", err)
 	}
 
 	return true, ""
diff --git a/pkg/node/node.go b/pkg/node/node.go
index 50944234e2..9ac0bcd2a6 100644
--- a/pkg/node/node.go
+++ b/pkg/node/node.go
@@ -427,11 +427,11 @@ func NewNode(
 
 	// Create a node info provider for LibP2P, and specify the default node approval state
 	// of Approved to avoid confusion as approval state is not used for this transport type.
-	nodeInfoProvider := routing.NewNodeInfoProvider(routing.NodeInfoProviderParams{
+	nodeInfoProvider := routing.NewNodeStateProvider(routing.NodeStateProviderParams{
 		NodeID:              config.NodeID,
 		LabelsProvider:      labelsProvider,
 		BacalhauVersion:     *version.Get(),
-		DefaultNodeApproval: models.NodeApprovals.APPROVED,
+		DefaultNodeApproval: models.NodeMembership.APPROVED,
 	})
 	nodeInfoProvider.RegisterNodeInfoDecorator(transportLayer.NodeInfoDecorator())
 	if computeNode != nil {
@@ -439,14 +439,14 @@ func NewNode(
 	}
 
 	shared.NewEndpoint(shared.EndpointParams{
-		Router:           apiServer.Router,
-		NodeID:           config.NodeID,
-		NodeInfoProvider: nodeInfoProvider,
+		Router:            apiServer.Router,
+		NodeID:            config.NodeID,
+		NodeStateProvider: nodeInfoProvider,
 	})
 
 	agent.NewEndpoint(agent.EndpointParams{
 		Router:             apiServer.Router,
-		NodeInfoProvider:   nodeInfoProvider,
+		NodeStateProvider:  nodeInfoProvider,
 		DebugInfoProviders: debugInfoProviders,
 	})
 
@@ -463,18 +463,19 @@ func NewNode(
 		// NB(forrest): this must be done last to avoid eager publishing before nodes are constructed
 		// TODO(forrest) [fixme] we should fix this to make it less racy in testing
 		nodeInfoPublisher = routing.NewNodeInfoPublisher(routing.NodeInfoPublisherParams{
-			PubSub:           transportLayer.NodeInfoPubSub(),
-			NodeInfoProvider: nodeInfoProvider,
-			IntervalConfig:   nodeInfoPublisherInterval,
+			PubSub:            transportLayer.NodeInfoPubSub(),
+			NodeStateProvider: nodeInfoProvider,
+			IntervalConfig:    nodeInfoPublisherInterval,
 		})
 	} else {
 		// We want to register the current requester node to the node store
 		if config.IsRequesterNode {
-			nodeInfo := nodeInfoProvider.GetNodeInfo(ctx)
-			nodeInfo.Approval = models.NodeApprovals.APPROVED
-			err := tracingInfoStore.Add(ctx, nodeInfo)
-			if err != nil {
+			nodeState := nodeInfoProvider.GetNodeState(ctx)
+			// TODO what is the liveness here? We are adding ourselves so I assume connected?
+			nodeState.Membership = models.NodeMembership.APPROVED
+			if err := tracingInfoStore.Add(ctx, nodeState); err != nil {
 				log.Ctx(ctx).Error().Err(err).Msg("failed to add requester node to the node store")
+				return nil, fmt.Errorf("registering node to the node store: %w", err)
 			}
 		}
 	}
diff --git a/pkg/node/requester.go b/pkg/node/requester.go
index 8c7319fe3d..cf1a88b655 100644
--- a/pkg/node/requester.go
+++ b/pkg/node/requester.go
@@ -3,6 +3,8 @@ package node
 import (
 	"context"
 
+	"github.com/rs/zerolog/log"
+
 	"github.com/bacalhau-project/bacalhau/pkg/authn"
 	"github.com/bacalhau-project/bacalhau/pkg/job"
 	"github.com/bacalhau-project/bacalhau/pkg/lib/backoff"
@@ -23,7 +25,6 @@ import (
 	s3helper "github.com/bacalhau-project/bacalhau/pkg/s3"
 	"github.com/bacalhau-project/bacalhau/pkg/translation"
 	"github.com/bacalhau-project/bacalhau/pkg/util"
-	"github.com/rs/zerolog/log"
 
 	"github.com/bacalhau-project/bacalhau/pkg/compute"
 	"github.com/bacalhau-project/bacalhau/pkg/eventhandler"
@@ -72,14 +73,11 @@ func NewRequesterNode(
 
 	jobStore := requesterConfig.JobStore
 
+	// TODO(forrest) [simplify]: given the current state of the code this interface obfuscates what is happening here,
+	// there isn't any "node discovery" happening here, we are simply listing a node store.
+	// The todo here is to simply pass a node store where it's needed instead of this chain wrapping a discoverer wrapping
+	// a store...
 	// compute node discoverer
-	nodeDiscoveryChain := discovery.NewChain(true)
-	nodeDiscoveryChain.Add(
-		discovery.NewStoreNodeDiscoverer(discovery.StoreNodeDiscovererParams{
-			Store: nodeInfoStore,
-		}),
-	)
-
 	log.Ctx(ctx).
 		Info().
 		Msgf("Nodes joining the cluster will be assigned approval state: %s", requesterConfig.DefaultApprovalState.String())
@@ -103,7 +101,7 @@ func NewRequesterNode(
 
 	// node selector
 	nodeSelector := selector.NewNodeSelector(selector.NodeSelectorParams{
-		NodeDiscoverer: nodeDiscoveryChain,
+		NodeDiscoverer: nodeInfoStore,
 		NodeRanker:     nodeRankerChain,
 	})
 
@@ -260,7 +258,7 @@ func NewRequesterNode(
 
 	// register debug info providers for the /debug endpoint
 	debugInfoProviders := []model.DebugInfoProvider{
-		discovery.NewDebugInfoProvider(nodeDiscoveryChain),
+		discovery.NewDebugInfoProvider(nodeInfoStore),
 	}
 
 	// register requester public http apis
@@ -269,7 +267,7 @@ func NewRequesterNode(
 		Requester:          endpoint,
 		DebugInfoProviders: debugInfoProviders,
 		JobStore:           jobStore,
-		NodeDiscoverer:     nodeDiscoveryChain,
+		NodeDiscoverer:     nodeInfoStore,
 	})
 
 	orchestrator_endpoint.NewEndpoint(orchestrator_endpoint.EndpointParams{
@@ -329,7 +327,7 @@ func NewRequesterNode(
 		Endpoint:           endpoint,
 		localCallback:      endpoint,
 		EndpointV2:         endpointV2,
-		NodeDiscoverer:     nodeDiscoveryChain,
+		NodeDiscoverer:     nodeInfoStore,
 		NodeInfoStore:      nodeInfoStore,
 		JobStore:           jobStore,
 		nodeManager:        nodeManager,
diff --git a/pkg/orchestrator/interfaces.go b/pkg/orchestrator/interfaces.go
index 3e57367b83..e137456b71 100644
--- a/pkg/orchestrator/interfaces.go
+++ b/pkg/orchestrator/interfaces.go
@@ -6,6 +6,7 @@ import (
 	"time"
 
 	"github.com/bacalhau-project/bacalhau/pkg/models"
+	"github.com/bacalhau-project/bacalhau/pkg/routing"
 )
 
 // EvaluationBroker is used to manage brokering of evaluations. When an evaluation is
@@ -89,8 +90,9 @@ type Planner interface {
 }
 
 // NodeDiscoverer discovers nodes in the network that are suitable to execute a job.
+// NodeDiscoverer is a subset of the routing.NodeInfoStore interface.
 type NodeDiscoverer interface {
-	ListNodes(ctx context.Context) ([]models.NodeInfo, error)
+	List(ctx context.Context, filter ...routing.NodeStateFilter) ([]models.NodeState, error)
 }
 
 // NodeRanker ranks nodes based on their suitability to execute a job.
diff --git a/pkg/orchestrator/selection/discovery/chained.go b/pkg/orchestrator/selection/discovery/chained.go
deleted file mode 100644
index 51f96bd1d8..0000000000
--- a/pkg/orchestrator/selection/discovery/chained.go
+++ /dev/null
@@ -1,63 +0,0 @@
-package discovery
-
-import (
-	"context"
-	"errors"
-
-	"github.com/bacalhau-project/bacalhau/pkg/models"
-	"github.com/bacalhau-project/bacalhau/pkg/orchestrator"
-	pkgerrors "github.com/pkg/errors"
-	"github.com/rs/zerolog/log"
-	"golang.org/x/exp/maps"
-)
-
-type Chain struct {
-	discoverers  []orchestrator.NodeDiscoverer
-	ignoreErrors bool
-}
-
-func NewChain(ignoreErrors bool) *Chain {
-	return &Chain{
-		ignoreErrors: ignoreErrors,
-	}
-}
-
-func (c *Chain) Add(discoverer ...orchestrator.NodeDiscoverer) {
-	c.discoverers = append(c.discoverers, discoverer...)
-}
-
-func (c *Chain) ListNodes(ctx context.Context) ([]models.NodeInfo, error) {
-	return c.chainDiscovery(ctx, "ListNodes", func(r orchestrator.NodeDiscoverer) ([]models.NodeInfo, error) {
-		return r.ListNodes(ctx)
-	})
-}
-
-func (c *Chain) chainDiscovery(
-	ctx context.Context,
-	caller string,
-	getNodes func(orchestrator.NodeDiscoverer) ([]models.NodeInfo, error),
-) ([]models.NodeInfo, error) {
-	var err error
-	uniqueNodes := make(map[string]models.NodeInfo, 0)
-	for _, discoverer := range c.discoverers {
-		nodeInfos, discoverErr := getNodes(discoverer)
-		err = errors.Join(err, pkgerrors.Wrapf(discoverErr, "error finding nodes from %T", discoverer))
-		currentNodesCount := len(uniqueNodes)
-		for _, nodeInfo := range nodeInfos {
-			if _, ok := uniqueNodes[nodeInfo.ID()]; !ok {
-				uniqueNodes[nodeInfo.ID()] = nodeInfo
-			}
-		}
-		log.Ctx(ctx).Debug().Msgf("[%s] found %d more nodes by %T", caller, len(uniqueNodes)-currentNodesCount, discoverer)
-	}
-
-	if err != nil && c.ignoreErrors {
-		log.Ctx(ctx).Warn().Err(err).Msg("ignoring error finding nodes")
-		err = nil
-	}
-
-	return maps.Values(uniqueNodes), err
-}
-
-// compile-time interface assertions
-var _ orchestrator.NodeDiscoverer = (*Chain)(nil)
diff --git a/pkg/orchestrator/selection/discovery/fixed.go b/pkg/orchestrator/selection/discovery/fixed.go
deleted file mode 100644
index 656e8dac58..0000000000
--- a/pkg/orchestrator/selection/discovery/fixed.go
+++ /dev/null
@@ -1,26 +0,0 @@
-package discovery
-
-import (
-	"context"
-
-	"github.com/bacalhau-project/bacalhau/pkg/models"
-)
-
-// node discoverer that always returns the same set of nodes
-type fixedDiscoverer struct {
-	peerIDs []models.NodeInfo
-}
-
-func NewFixedDiscoverer(peerIDs ...models.NodeInfo) *fixedDiscoverer {
-	return &fixedDiscoverer{
-		peerIDs: peerIDs,
-	}
-}
-
-func (f *fixedDiscoverer) FindNodes(context.Context, models.Job) ([]models.NodeInfo, error) {
-	return f.peerIDs, nil
-}
-
-func (f *fixedDiscoverer) ListNodes(context.Context) ([]models.NodeInfo, error) {
-	return f.peerIDs, nil
-}
diff --git a/pkg/orchestrator/selection/discovery/info_provider.go b/pkg/orchestrator/selection/discovery/info_provider.go
index 605ec4d7fb..9e0151989a 100644
--- a/pkg/orchestrator/selection/discovery/info_provider.go
+++ b/pkg/orchestrator/selection/discovery/info_provider.go
@@ -17,7 +17,7 @@ func NewDebugInfoProvider(discoverer orchestrator.NodeDiscoverer) model.DebugInf
 
 // GetDebugInfo implements models.DebugInfoProvider
 func (p *discoveredNodesProvider) GetDebugInfo(ctx context.Context) (info model.DebugInfo, err error) {
-	nodes, err := p.discoverer.ListNodes(ctx)
+	nodes, err := p.discoverer.List(ctx)
 	info.Component = "DiscoveredNodes"
 	info.Info = nodes
 	return info, err
diff --git a/pkg/orchestrator/selection/discovery/store.go b/pkg/orchestrator/selection/discovery/store.go
deleted file mode 100644
index 431a5f9868..0000000000
--- a/pkg/orchestrator/selection/discovery/store.go
+++ /dev/null
@@ -1,31 +0,0 @@
-package discovery
-
-import (
-	"context"
-
-	"github.com/bacalhau-project/bacalhau/pkg/models"
-	"github.com/bacalhau-project/bacalhau/pkg/orchestrator"
-	"github.com/bacalhau-project/bacalhau/pkg/routing"
-)
-
-type StoreNodeDiscovererParams struct {
-	Store routing.NodeInfoStore
-}
-
-type StoreNodeDiscoverer struct {
-	store routing.NodeInfoStore
-}
-
-func NewStoreNodeDiscoverer(params StoreNodeDiscovererParams) *StoreNodeDiscoverer {
-	return &StoreNodeDiscoverer{
-		store: params.Store,
-	}
-}
-
-// ListNodes implements orchestrator.NodeDiscoverer
-func (d *StoreNodeDiscoverer) ListNodes(ctx context.Context) ([]models.NodeInfo, error) {
-	return d.store.List(ctx)
-}
-
-// compile time check that StoreNodeDiscoverer implements NodeDiscoverer
-var _ orchestrator.NodeDiscoverer = (*StoreNodeDiscoverer)(nil)
diff --git a/pkg/orchestrator/selection/selector/node_selector.go b/pkg/orchestrator/selection/selector/node_selector.go
index 80cd1a2703..3dd3d4cf27 100644
--- a/pkg/orchestrator/selection/selector/node_selector.go
+++ b/pkg/orchestrator/selection/selector/node_selector.go
@@ -3,6 +3,7 @@ package selector
 import (
 	"context"
 	"errors"
+	"fmt"
 	"sort"
 
 	"github.com/rs/zerolog/log"
@@ -32,7 +33,16 @@ func NewNodeSelector(params NodeSelectorParams) *NodeSelector {
 }
 
 func (n NodeSelector) AllNodes(ctx context.Context) ([]models.NodeInfo, error) {
-	return n.nodeDiscoverer.ListNodes(ctx)
+	nodeStates, err := n.nodeDiscoverer.List(ctx)
+	if err != nil {
+		return nil, fmt.Errorf("failed to list discovered nodes: %w", err)
+	}
+	// extract slice of models.NodeInfo from slice of routing.NodeConnectionState
+	nodeInfos := make([]models.NodeInfo, 0, len(nodeStates))
+	for _, ns := range nodeStates {
+		nodeInfos = append(nodeInfos, ns.Info)
+	}
+	return nodeInfos, nil
 }
 
 func (n NodeSelector) AllMatchingNodes(ctx context.Context,
@@ -73,32 +83,42 @@ func (n NodeSelector) TopMatchingNodes(ctx context.Context,
 func (n NodeSelector) rankAndFilterNodes(ctx context.Context,
 	job *models.Job,
 	constraints *orchestrator.NodeSelectionConstraints) (selected, rejected []orchestrator.NodeRank, err error) {
-	listed, err := n.nodeDiscoverer.ListNodes(ctx)
+	listed, err := n.nodeDiscoverer.List(ctx)
 	if err != nil {
 		return nil, nil, err
 	}
 
-	nodeIDs := lo.Filter(listed, func(nodeInfo models.NodeInfo, index int) bool {
-		if nodeInfo.NodeType != models.NodeTypeCompute {
+	// filter node states to return a slice of nodes that are:
+	// - compute nodes
+	// - approved to executor jobs
+	// - connected (alive)
+	nodeStates := lo.Filter(listed, func(nodeState models.NodeState, index int) bool {
+		if nodeState.Info.NodeType != models.NodeTypeCompute {
 			return false
 		}
 
-		if constraints.RequireApproval && nodeInfo.Approval != models.NodeApprovals.APPROVED {
+		if constraints.RequireApproval && nodeState.Membership != models.NodeMembership.APPROVED {
 			return false
 		}
 
-		if constraints.RequireConnected && nodeInfo.State != models.NodeStates.CONNECTED {
+		if constraints.RequireConnected && nodeState.Connection != models.NodeStates.CONNECTED {
 			return false
 		}
 
 		return true
 	})
 
-	if len(nodeIDs) == 0 {
+	if len(nodeStates) == 0 {
 		return nil, nil, errors.New("unable to find any connected and approved nodes")
 	}
 
-	rankedNodes, err := n.nodeRanker.RankNodes(ctx, *job, nodeIDs)
+	// extract the nodeInfo from the slice of node states for ranking
+	nodeInfos := make([]models.NodeInfo, 0, len(nodeStates))
+	for _, ns := range nodeStates {
+		nodeInfos = append(nodeInfos, ns.Info)
+	}
+
+	rankedNodes, err := n.nodeRanker.RankNodes(ctx, *job, nodeInfos)
 	if err != nil {
 		return nil, nil, err
 	}
diff --git a/pkg/publicapi/apimodels/agent.go b/pkg/publicapi/apimodels/agent.go
index cf76b0a84c..4705a1b656 100644
--- a/pkg/publicapi/apimodels/agent.go
+++ b/pkg/publicapi/apimodels/agent.go
@@ -28,5 +28,5 @@ type GetAgentNodeRequest struct {
 
 type GetAgentNodeResponse struct {
 	BaseGetResponse
-	*models.NodeInfo
+	*models.NodeState
 }
diff --git a/pkg/publicapi/apimodels/node.go b/pkg/publicapi/apimodels/node.go
index 39933fdd28..76f5b77aae 100644
--- a/pkg/publicapi/apimodels/node.go
+++ b/pkg/publicapi/apimodels/node.go
@@ -1,8 +1,9 @@
 package apimodels
 
 import (
-	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"k8s.io/apimachinery/pkg/labels"
+
+	"github.com/bacalhau-project/bacalhau/pkg/models"
 )
 
 type GetNodeRequest struct {
@@ -12,7 +13,7 @@ type GetNodeRequest struct {
 
 type GetNodeResponse struct {
 	BaseGetResponse
-	Node *models.NodeInfo
+	Node *models.NodeState
 }
 
 type ListNodesRequest struct {
@@ -43,7 +44,7 @@ func (o *ListNodesRequest) ToHTTPRequest() *HTTPRequest {
 
 type ListNodesResponse struct {
 	BaseListResponse
-	Nodes []*models.NodeInfo
+	Nodes []*models.NodeState
 }
 
 type PutNodeRequest struct {
diff --git a/pkg/publicapi/endpoint/agent/endpoint.go b/pkg/publicapi/endpoint/agent/endpoint.go
index ad0e5a82ea..4bfc56840c 100644
--- a/pkg/publicapi/endpoint/agent/endpoint.go
+++ b/pkg/publicapi/endpoint/agent/endpoint.go
@@ -3,31 +3,32 @@ package agent
 import (
 	"net/http"
 
+	"github.com/labstack/echo/v4"
+	"github.com/rs/zerolog/log"
+
 	"github.com/bacalhau-project/bacalhau/pkg/model"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/publicapi/apimodels"
 	"github.com/bacalhau-project/bacalhau/pkg/publicapi/middleware"
 	"github.com/bacalhau-project/bacalhau/pkg/version"
-	"github.com/labstack/echo/v4"
-	"github.com/rs/zerolog/log"
 )
 
 type EndpointParams struct {
 	Router             *echo.Echo
-	NodeInfoProvider   models.NodeInfoProvider
+	NodeStateProvider  models.NodeStateProvider
 	DebugInfoProviders []model.DebugInfoProvider
 }
 
 type Endpoint struct {
 	router             *echo.Echo
-	nodeInfoProvider   models.NodeInfoProvider
+	nodeStateProvider  models.NodeStateProvider
 	debugInfoProviders []model.DebugInfoProvider
 }
 
 func NewEndpoint(params EndpointParams) *Endpoint {
 	e := &Endpoint{
 		router:             params.Router,
-		nodeInfoProvider:   params.NodeInfoProvider,
+		nodeStateProvider:  params.NodeStateProvider,
 		debugInfoProviders: params.DebugInfoProviders,
 	}
 
@@ -80,9 +81,9 @@ func (e *Endpoint) version(c echo.Context) error {
 //	@Failure	500	{object}	string
 //	@Router		/api/v1/agent/node [get]
 func (e *Endpoint) node(c echo.Context) error {
-	nodeInfo := e.nodeInfoProvider.GetNodeInfo(c.Request().Context())
+	nodeState := e.nodeStateProvider.GetNodeState(c.Request().Context())
 	return c.JSON(http.StatusOK, apimodels.GetAgentNodeResponse{
-		NodeInfo: &nodeInfo,
+		NodeState: &nodeState,
 	})
 }
 
diff --git a/pkg/publicapi/endpoint/orchestrator/node.go b/pkg/publicapi/endpoint/orchestrator/node.go
index 7533a35768..4704e15afd 100644
--- a/pkg/publicapi/endpoint/orchestrator/node.go
+++ b/pkg/publicapi/endpoint/orchestrator/node.go
@@ -19,12 +19,12 @@ func (e *Endpoint) getNode(c echo.Context) error {
 	if c.Param("id") == "" {
 		return echo.NewHTTPError(http.StatusBadRequest, "missing node id")
 	}
-	job, err := e.nodeManager.GetByPrefix(ctx, c.Param("id"))
+	nodeState, err := e.nodeManager.GetByPrefix(ctx, c.Param("id"))
 	if err != nil {
 		return err
 	}
 	return c.JSON(http.StatusOK, apimodels.GetNodeResponse{
-		Node: &job,
+		Node: &nodeState,
 	})
 }
 
@@ -44,9 +44,9 @@ func (e *Endpoint) listNodes(c echo.Context) error {
 		return err
 	}
 
-	capacity := func(node *models.NodeInfo) *models.Resources {
-		if node.ComputeNodeInfo != nil {
-			return &node.ComputeNodeInfo.AvailableCapacity
+	capacity := func(node *models.NodeState) *models.Resources {
+		if node.Info.ComputeNodeInfo != nil {
+			return &node.Info.ComputeNodeInfo.AvailableCapacity
 		}
 		return &models.Resources{}
 	}
@@ -59,7 +59,7 @@ func (e *Endpoint) listNodes(c echo.Context) error {
 
 	if args.Reverse {
 		baseSortFnc := sortFnc
-		sortFnc = func(a, b *models.NodeInfo) int {
+		sortFnc = func(a, b *models.NodeState) int {
 			x := baseSortFnc(a, b)
 			if x == -1 {
 				return 1
@@ -81,17 +81,17 @@ func (e *Endpoint) listNodes(c echo.Context) error {
 	args.FilterByStatus = strings.ToUpper(args.FilterByStatus)
 
 	// filter nodes, first by status, then by label selectors
-	res := make([]*models.NodeInfo, 0)
+	res := make([]*models.NodeState, 0)
 	for i, node := range allNodes {
-		if args.FilterByApproval != "" && args.FilterByApproval != node.Approval.String() {
+		if args.FilterByApproval != "" && args.FilterByApproval != node.Membership.String() {
 			continue
 		}
 
-		if args.FilterByStatus != "" && args.FilterByStatus != node.State.String() {
+		if args.FilterByStatus != "" && args.FilterByStatus != node.Connection.String() {
 			continue
 		}
 
-		if selector.Matches(labels.Set(node.Labels)) {
+		if selector.Matches(labels.Set(node.Info.Labels)) {
 			res = append(res, &allNodes[i])
 		}
 	}
@@ -110,34 +110,36 @@ func (e *Endpoint) listNodes(c echo.Context) error {
 	})
 }
 
-type resourceFunc func(node *models.NodeInfo) *models.Resources
-type sortFunc func(a, b *models.NodeInfo) int
+type resourceFunc func(node *models.NodeState) *models.Resources
+type sortFunc func(a, b *models.NodeState) int
 
 func (e *Endpoint) getSortFunction(orderBy string, capacity resourceFunc) sortFunc {
 	switch orderBy {
 	case "id", "":
-		return func(a, b *models.NodeInfo) int { return util.Compare[string]{}.Cmp(a.ID(), b.ID()) }
+		return func(a, b *models.NodeState) int { return util.Compare[string]{}.Cmp(a.Info.ID(), b.Info.ID()) }
 	case "type":
-		return func(a, b *models.NodeInfo) int { return util.Compare[models.NodeType]{}.Cmp(a.NodeType, b.NodeType) }
+		return func(a, b *models.NodeState) int {
+			return util.Compare[models.NodeType]{}.Cmp(a.Info.NodeType, b.Info.NodeType)
+		}
 	case "available_cpu":
-		return func(a, b *models.NodeInfo) int {
+		return func(a, b *models.NodeState) int {
 			return util.Compare[float64]{}.CmpRev(capacity(a).CPU, capacity(b).CPU)
 		}
 	case "available_memory":
-		return func(a, b *models.NodeInfo) int {
+		return func(a, b *models.NodeState) int {
 			return util.Compare[uint64]{}.CmpRev(capacity(a).Memory, capacity(b).Memory)
 		}
 	case "available_disk":
-		return func(a, b *models.NodeInfo) int {
+		return func(a, b *models.NodeState) int {
 			return util.Compare[uint64]{}.CmpRev(capacity(a).Disk, capacity(b).Disk)
 		}
 	case "available_gpu":
-		return func(a, b *models.NodeInfo) int {
+		return func(a, b *models.NodeState) int {
 			return util.Compare[uint64]{}.CmpRev(capacity(a).GPU, capacity(b).GPU)
 		}
 	case "approval", "status":
-		return func(a, b *models.NodeInfo) int {
-			return util.Compare[string]{}.Cmp(a.Approval.String(), b.Approval.String())
+		return func(a, b *models.NodeState) int {
+			return util.Compare[string]{}.Cmp(a.Membership.String(), b.Membership.String())
 		}
 	default:
 	}
diff --git a/pkg/publicapi/endpoint/requester/endpoints_nodes.go b/pkg/publicapi/endpoint/requester/endpoints_nodes.go
index 5a98e80ad5..1b31ca034b 100644
--- a/pkg/publicapi/endpoint/requester/endpoints_nodes.go
+++ b/pkg/publicapi/endpoint/requester/endpoints_nodes.go
@@ -17,7 +17,7 @@ import (
 //	@Router		/api/v1/requester/nodes [get]
 func (s *Endpoint) nodes(c echo.Context) error {
 	ctx := c.Request().Context()
-	nodes, err := s.nodeDiscoverer.ListNodes(ctx)
+	nodes, err := s.nodeDiscoverer.List(ctx)
 	if err != nil {
 		return echo.NewHTTPError(http.StatusInternalServerError, err.Error())
 	}
diff --git a/pkg/publicapi/endpoint/shared/endpoint.go b/pkg/publicapi/endpoint/shared/endpoint.go
index 0235e474a3..6b2fe1f2c6 100644
--- a/pkg/publicapi/endpoint/shared/endpoint.go
+++ b/pkg/publicapi/endpoint/shared/endpoint.go
@@ -3,30 +3,31 @@ package shared
 import (
 	"net/http"
 
+	"github.com/labstack/echo/v4"
+
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/publicapi/apimodels/legacymodels"
 	"github.com/bacalhau-project/bacalhau/pkg/publicapi/middleware"
 	"github.com/bacalhau-project/bacalhau/pkg/version"
-	"github.com/labstack/echo/v4"
 )
 
 type EndpointParams struct {
-	Router           *echo.Echo
-	NodeID           string
-	NodeInfoProvider models.NodeInfoProvider
+	Router            *echo.Echo
+	NodeID            string
+	NodeStateProvider models.NodeStateProvider
 }
 
 type Endpoint struct {
-	router           *echo.Echo
-	nodeID           string
-	nodeInfoProvider models.NodeInfoProvider
+	router            *echo.Echo
+	nodeID            string
+	nodeStateProvider models.NodeStateProvider
 }
 
 func NewEndpoint(params EndpointParams) *Endpoint {
 	e := &Endpoint{
-		router:           params.Router,
-		nodeID:           params.NodeID,
-		nodeInfoProvider: params.NodeInfoProvider,
+		router:            params.Router,
+		nodeID:            params.NodeID,
+		nodeStateProvider: params.NodeStateProvider,
 	}
 
 	// JSON group
@@ -74,7 +75,7 @@ func (e *Endpoint) id(c echo.Context) error {
 //	@Failure	500	{object}	string
 //	@Router		/api/v1/node_info [get]
 func (e *Endpoint) nodeInfo(c echo.Context) error {
-	return c.JSON(http.StatusOK, e.nodeInfoProvider.GetNodeInfo(c.Request().Context()))
+	return c.JSON(http.StatusOK, e.nodeStateProvider.GetNodeState(c.Request().Context()))
 }
 
 // version godoc
diff --git a/pkg/routing/inmemory/inmemory.go b/pkg/routing/inmemory/inmemory.go
index 666fe8b849..397d431ccd 100644
--- a/pkg/routing/inmemory/inmemory.go
+++ b/pkg/routing/inmemory/inmemory.go
@@ -6,16 +6,16 @@ import (
 	"sync"
 	"time"
 
-	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/libp2p/go-libp2p/core/peer"
 	"github.com/rs/zerolog/log"
 
+	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/routing"
 )
 
 // TODO: replace the manual and lazy eviction with a more efficient caching library
 type nodeInfoWrapper struct {
-	models.NodeInfo
+	models.NodeState
 	evictAt time.Time
 }
 
@@ -36,48 +36,48 @@ func NewNodeStore(params NodeStoreParams) *NodeStore {
 	}
 }
 
-func (r *NodeStore) Add(ctx context.Context, nodeInfo models.NodeInfo) error {
+func (r *NodeStore) Add(ctx context.Context, state models.NodeState) error {
 	r.mu.Lock()
 	defer r.mu.Unlock()
 
 	// add or update the node info
-	nodeID := nodeInfo.ID()
+	nodeID := state.Info.ID()
 	r.nodeInfoMap[nodeID] = nodeInfoWrapper{
-		NodeInfo: nodeInfo,
-		evictAt:  time.Now().Add(r.ttl),
+		NodeState: state,
+		evictAt:   time.Now().Add(r.ttl),
 	}
 
-	log.Ctx(ctx).Trace().Msgf("Added node info %+v", nodeInfo)
+	log.Ctx(ctx).Trace().Msgf("Added node state %+v", state)
 	return nil
 }
 
-func (r *NodeStore) Get(ctx context.Context, nodeID string) (models.NodeInfo, error) {
+func (r *NodeStore) Get(ctx context.Context, nodeID string) (models.NodeState, error) {
 	r.mu.RLock()
 	defer r.mu.RUnlock()
 	infoWrapper, ok := r.nodeInfoMap[nodeID]
 	if !ok {
-		return models.NodeInfo{}, routing.NewErrNodeNotFound(nodeID)
+		return models.NodeState{}, routing.NewErrNodeNotFound(nodeID)
 	}
 	if time.Now().After(infoWrapper.evictAt) {
 		go r.evict(ctx, infoWrapper)
-		return models.NodeInfo{}, routing.NewErrNodeNotFound(nodeID)
+		return models.NodeState{}, routing.NewErrNodeNotFound(nodeID)
 	}
-	return infoWrapper.NodeInfo, nil
+	return infoWrapper.NodeState, nil
 }
 
-func (r *NodeStore) GetByPrefix(ctx context.Context, prefix string) (models.NodeInfo, error) {
+func (r *NodeStore) GetByPrefix(ctx context.Context, prefix string) (models.NodeState, error) {
 	r.mu.RLock()
 	defer r.mu.RUnlock()
-	nodeInfo, err := r.Get(ctx, prefix)
+	state, err := r.Get(ctx, prefix)
 
 	// we found a node with the exact ID
 	if err == nil {
-		return nodeInfo, nil
+		return state, nil
 	}
 	// return the error if it's not a node not found error
 	var errNotFound routing.ErrNodeNotFound
 	if !errors.As(err, &errNotFound) {
-		return models.NodeInfo{}, err
+		return models.NodeState{}, err
 	}
 
 	// look for a node with the prefix. if there are multiple nodes with the same prefix, return ErrMultipleNodesFound error
@@ -96,14 +96,14 @@ func (r *NodeStore) GetByPrefix(ctx context.Context, prefix string) (models.Node
 	}
 
 	if len(nodeIDsWithPrefix) == 0 {
-		return models.NodeInfo{}, routing.NewErrNodeNotFound(prefix)
+		return models.NodeState{}, routing.NewErrNodeNotFound(prefix)
 	}
 
 	if len(nodeIDsWithPrefix) > 1 {
-		return models.NodeInfo{}, routing.NewErrMultipleNodesFound(prefix, nodeIDsWithPrefix)
+		return models.NodeState{}, routing.NewErrMultipleNodesFound(prefix, nodeIDsWithPrefix)
 	}
 
-	return r.nodeInfoMap[nodeIDsWithPrefix[0]].NodeInfo, nil
+	return r.nodeInfoMap[nodeIDsWithPrefix[0]].NodeState, nil
 }
 
 func (r *NodeStore) FindPeer(ctx context.Context, peerID peer.ID) (peer.AddrInfo, error) {
@@ -113,40 +113,40 @@ func (r *NodeStore) FindPeer(ctx context.Context, peerID peer.ID) (peer.AddrInfo
 	if !ok {
 		return peer.AddrInfo{}, nil
 	}
-	if infoWrapper.PeerInfo != nil && len(infoWrapper.PeerInfo.Addrs) > 0 {
-		return *infoWrapper.PeerInfo, nil
+	if infoWrapper.Info.PeerInfo != nil && len(infoWrapper.Info.PeerInfo.Addrs) > 0 {
+		return *infoWrapper.Info.PeerInfo, nil
 	}
 	return peer.AddrInfo{}, nil
 }
 
-func (r *NodeStore) List(ctx context.Context, filters ...routing.NodeInfoFilter) ([]models.NodeInfo, error) {
+func (r *NodeStore) List(ctx context.Context, filters ...routing.NodeStateFilter) ([]models.NodeState, error) {
 	r.mu.RLock()
 	defer r.mu.RUnlock()
 
-	megaFilter := func(info models.NodeInfo) bool {
+	megaFilter := func(state models.NodeState) bool {
 		for _, filter := range filters {
-			if !filter(info) {
+			if !filter(state) {
 				return false
 			}
 		}
 		return true
 	}
 
-	var nodeInfos []models.NodeInfo
+	var nodeStates []models.NodeState
 	var toEvict []nodeInfoWrapper
-	for _, nodeInfo := range r.nodeInfoMap {
-		if time.Now().After(nodeInfo.evictAt) {
-			toEvict = append(toEvict, nodeInfo)
+	for _, nodeState := range r.nodeInfoMap {
+		if time.Now().After(nodeState.evictAt) {
+			toEvict = append(toEvict, nodeState)
 		} else {
-			if megaFilter(nodeInfo.NodeInfo) {
-				nodeInfos = append(nodeInfos, nodeInfo.NodeInfo)
+			if megaFilter(nodeState.NodeState) {
+				nodeStates = append(nodeStates, nodeState.NodeState)
 			}
 		}
 	}
 	if len(toEvict) > 0 {
 		go r.evict(ctx, toEvict...)
 	}
-	return nodeInfos, nil
+	return nodeStates, nil
 }
 
 func (r *NodeStore) Delete(ctx context.Context, nodeID string) error {
@@ -155,13 +155,13 @@ func (r *NodeStore) Delete(ctx context.Context, nodeID string) error {
 	return r.doDelete(ctx, nodeID)
 }
 
-func (r *NodeStore) evict(ctx context.Context, infoWrappers ...nodeInfoWrapper) {
+func (r *NodeStore) evict(ctx context.Context, stateWrappers ...nodeInfoWrapper) {
 	r.mu.Lock()
 	defer r.mu.Unlock()
-	for _, infoWrapper := range infoWrappers {
-		nodeID := infoWrapper.ID()
+	for _, stateWrapper := range stateWrappers {
+		nodeID := stateWrapper.Info.ID()
 		nodeInfo, ok := r.nodeInfoMap[nodeID]
-		if !ok || nodeInfo.evictAt != infoWrapper.evictAt {
+		if !ok || nodeInfo.evictAt != stateWrapper.evictAt {
 			return // node info already evicted or has been updated since it was scheduled for eviction
 		}
 		err := r.doDelete(ctx, nodeID)
diff --git a/pkg/routing/kvstore/kvstore.go b/pkg/routing/kvstore/kvstore.go
index 5e8bbd3ef7..f288adbb3c 100644
--- a/pkg/routing/kvstore/kvstore.go
+++ b/pkg/routing/kvstore/kvstore.go
@@ -58,59 +58,59 @@ func (n *NodeStore) FindPeer(ctx context.Context, peerID peer.ID) (peer.AddrInfo
 	// We are temporarily matching the code of the inmemory.NodeStore which never returns an
 	// error for this method.
 	nodeID := peerID.String()
-	info, err := n.Get(ctx, nodeID)
+	state, err := n.Get(ctx, nodeID)
 	if err != nil {
 		return peer.AddrInfo{}, nil
 	}
 
-	return *info.PeerInfo, nil
+	return *state.Info.PeerInfo, nil
 }
 
-// Add adds a node info to the repo.
-func (n *NodeStore) Add(ctx context.Context, nodeInfo models.NodeInfo) error {
-	data, err := json.Marshal(nodeInfo)
+// Add adds a node state to the repo.
+func (n *NodeStore) Add(ctx context.Context, state models.NodeState) error {
+	data, err := json.Marshal(state)
 	if err != nil {
-		return pkgerrors.Wrap(err, "failed to marshal node info adding to node store")
+		return pkgerrors.Wrap(err, "failed to marshal node state adding to node store")
 	}
 
-	_, err = n.kv.Put(ctx, nodeInfo.ID(), data)
+	_, err = n.kv.Put(ctx, state.Info.ID(), data)
 	if err != nil {
-		return pkgerrors.Wrap(err, "failed to write node info to node store")
+		return pkgerrors.Wrap(err, "failed to write node state to node store")
 	}
 
 	return nil
 }
 
-// Get returns the node info for the given node ID.
-func (n *NodeStore) Get(ctx context.Context, nodeID string) (models.NodeInfo, error) {
+// Get returns the node state for the given node ID.
+func (n *NodeStore) Get(ctx context.Context, nodeID string) (models.NodeState, error) {
 	entry, err := n.kv.Get(ctx, nodeID)
 	if err != nil {
 		if pkgerrors.Is(err, jetstream.ErrKeyNotFound) {
-			return models.NodeInfo{}, routing.NewErrNodeNotFound(nodeID)
+			return models.NodeState{}, routing.NewErrNodeNotFound(nodeID)
 		}
 
-		return models.NodeInfo{}, pkgerrors.Wrap(err, "failed to get node info from node store")
+		return models.NodeState{}, pkgerrors.Wrap(err, "failed to get node state from node store")
 	}
 
-	var node models.NodeInfo
+	var node models.NodeState
 	err = json.Unmarshal(entry.Value(), &node)
 	if err != nil {
-		return models.NodeInfo{}, pkgerrors.Wrap(err, "failed to unmarshal node info from node store")
+		return models.NodeState{}, pkgerrors.Wrap(err, "failed to unmarshal node state from node store")
 	}
 
 	return node, nil
 }
 
-// GetByPrefix returns the node info for the given node ID.
+// GetByPrefix returns the node state for the given node ID.
 // Supports both full and short node IDs band currently iterates through all of the
 // keys to find matches, due to NATS KVStore not supporting prefix searches (yet).
-func (n *NodeStore) GetByPrefix(ctx context.Context, prefix string) (models.NodeInfo, error) {
+func (n *NodeStore) GetByPrefix(ctx context.Context, prefix string) (models.NodeState, error) {
 	keys, err := n.kv.Keys(ctx)
 	if err != nil {
 		if pkgerrors.Is(err, jetstream.ErrNoKeysFound) {
-			return models.NodeInfo{}, routing.NewErrNodeNotFound(prefix)
+			return models.NodeState{}, routing.NewErrNodeNotFound(prefix)
 		}
-		return models.NodeInfo{}, pkgerrors.Wrap(err, "failed to get by prefix when listing keys")
+		return models.NodeState{}, pkgerrors.Wrap(err, "failed to get by prefix when listing keys")
 	}
 
 	// Filter the list down to just the matching keys
@@ -119,38 +119,38 @@ func (n *NodeStore) GetByPrefix(ctx context.Context, prefix string) (models.Node
 	})
 
 	if len(keys) == 0 {
-		return models.NodeInfo{}, routing.NewErrNodeNotFound(prefix)
+		return models.NodeState{}, routing.NewErrNodeNotFound(prefix)
 	} else if len(keys) > 1 {
-		return models.NodeInfo{}, routing.NewErrMultipleNodesFound(prefix, keys)
+		return models.NodeState{}, routing.NewErrMultipleNodesFound(prefix, keys)
 	}
 
 	return n.Get(ctx, keys[0])
 }
 
 // List returns a list of nodes
-func (n *NodeStore) List(ctx context.Context, filters ...routing.NodeInfoFilter) ([]models.NodeInfo, error) {
+func (n *NodeStore) List(ctx context.Context, filters ...routing.NodeStateFilter) ([]models.NodeState, error) {
 	keys, err := n.kv.Keys(ctx)
 	if err != nil {
 		// Return an empty list rather than an error if there are no keys in the bucket
 		if pkgerrors.Is(err, jetstream.ErrNoKeysFound) {
-			return []models.NodeInfo{}, nil
+			return []models.NodeState{}, nil
 		}
-		return nil, pkgerrors.Wrap(err, "failed to list node info from node store")
+		return nil, pkgerrors.Wrap(err, "failed to list node state from node store")
 	}
 
 	var mErr error
 
 	// Create a mega filter that combines all the filters into one
-	megaFilter := func(info models.NodeInfo) bool {
+	megaFilter := func(state models.NodeState) bool {
 		for _, filter := range filters {
-			if !filter(info) {
+			if !filter(state) {
 				return false
 			}
 		}
 		return true
 	}
 
-	nodes := make([]models.NodeInfo, 0, len(keys))
+	nodes := make([]models.NodeState, 0, len(keys))
 	for _, key := range keys {
 		node, err := n.Get(ctx, key)
 		if err != nil {
@@ -165,10 +165,10 @@ func (n *NodeStore) List(ctx context.Context, filters ...routing.NodeInfoFilter)
 	return nodes, mErr
 }
 
-// Delete deletes a node info from the repo.
+// Delete deletes a node state from the repo.
 func (n *NodeStore) Delete(ctx context.Context, nodeID string) error {
 	if err := n.kv.Purge(ctx, nodeID); err != nil {
-		return pkgerrors.Wrap(err, "failed to purge node info from node store")
+		return pkgerrors.Wrap(err, "failed to purge node state from node store")
 	}
 
 	return nil
diff --git a/pkg/routing/node_info_provider.go b/pkg/routing/node_info_provider.go
index 507d7fedbc..3b9a65a3b7 100644
--- a/pkg/routing/node_info_provider.go
+++ b/pkg/routing/node_info_provider.go
@@ -6,23 +6,23 @@ import (
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 )
 
-type NodeInfoProviderParams struct {
+type NodeStateProviderParams struct {
 	NodeID              string
 	LabelsProvider      models.LabelsProvider
 	BacalhauVersion     models.BuildVersionInfo
-	DefaultNodeApproval models.NodeApproval
+	DefaultNodeApproval models.NodeMembershipState
 }
 
-type NodeInfoProvider struct {
+type NodeStateProvider struct {
 	nodeID              string
 	labelsProvider      models.LabelsProvider
 	bacalhauVersion     models.BuildVersionInfo
 	nodeInfoDecorators  []models.NodeInfoDecorator
-	defaultNodeApproval models.NodeApproval
+	defaultNodeApproval models.NodeMembershipState
 }
 
-func NewNodeInfoProvider(params NodeInfoProviderParams) *NodeInfoProvider {
-	provider := &NodeInfoProvider{
+func NewNodeStateProvider(params NodeStateProviderParams) *NodeStateProvider {
+	provider := &NodeStateProvider{
 		nodeID:              params.NodeID,
 		labelsProvider:      params.LabelsProvider,
 		bacalhauVersion:     params.BacalhauVersion,
@@ -32,35 +32,48 @@ func NewNodeInfoProvider(params NodeInfoProviderParams) *NodeInfoProvider {
 
 	// If we were not given a default approval, we default to PENDING
 	if !provider.defaultNodeApproval.IsValid() {
-		provider.defaultNodeApproval = models.NodeApprovals.PENDING
+		provider.defaultNodeApproval = models.NodeMembership.PENDING
 	}
 
 	return provider
 }
 
 // RegisterNodeInfoDecorator registers a node info decorator with the node info provider.
-func (n *NodeInfoProvider) RegisterNodeInfoDecorator(decorator models.NodeInfoDecorator) {
+func (n *NodeStateProvider) RegisterNodeInfoDecorator(decorator models.NodeInfoDecorator) {
 	n.nodeInfoDecorators = append(n.nodeInfoDecorators, decorator)
 }
 
-func (n *NodeInfoProvider) GetNodeInfo(ctx context.Context) models.NodeInfo {
-	res := models.NodeInfo{
+func (n *NodeStateProvider) GetNodeState(ctx context.Context) models.NodeState {
+	info := models.NodeInfo{
 		NodeID:          n.nodeID,
 		BacalhauVersion: n.bacalhauVersion,
 		Labels:          n.labelsProvider.GetLabels(ctx),
 		NodeType:        models.NodeTypeRequester,
-		Approval:        n.defaultNodeApproval,
 	}
 	for _, decorator := range n.nodeInfoDecorators {
-		res = decorator.DecorateNodeInfo(ctx, res)
+		info = decorator.DecorateNodeInfo(ctx, info)
 	}
 
-	if !res.Approval.IsValid() {
-		res.Approval = models.NodeApprovals.PENDING
+	state := models.NodeState{
+		Info:       info,
+		Membership: n.defaultNodeApproval,
+		// NB(forrest): we are returning NodeState about ourselves (the Requester)
+		// the concept of a disconnected requester node could only exist from the
+		// perspective of a ComputeNode or another RequesterNode.
+		// We don't support multiple requester nodes nor querying the state of one from a Compute node. (yet)
+		// So we allways say we are connected here.
+
+		// This is all pretty funky and my comment here will hopefully become outdates at some-point and need adjusting,
+		// but for now: "you can tell the requester node is connected because of the way it is".
+		Connection: models.NodeStates.CONNECTED,
+	}
+
+	if !state.Membership.IsValid() {
+		state.Membership = models.NodeMembership.PENDING
 	}
 
-	return res
+	return state
 }
 
 // compile-time interface check
-var _ models.NodeInfoProvider = &NodeInfoProvider{}
+var _ models.NodeStateProvider = &NodeStateProvider{}
diff --git a/pkg/routing/node_info_publisher.go b/pkg/routing/node_info_publisher.go
index 6f99ba0cdb..719f0d0ca5 100644
--- a/pkg/routing/node_info_publisher.go
+++ b/pkg/routing/node_info_publisher.go
@@ -5,10 +5,11 @@ import (
 	"sync"
 	"time"
 
+	"github.com/rs/zerolog/log"
+
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/pubsub"
 	"github.com/bacalhau-project/bacalhau/pkg/system"
-	"github.com/rs/zerolog/log"
 )
 
 type NodeInfoPublisherIntervalConfig struct {
@@ -34,26 +35,26 @@ func (n NodeInfoPublisherIntervalConfig) IsEagerPublishEnabled() bool {
 }
 
 type NodeInfoPublisherParams struct {
-	PubSub           pubsub.Publisher[models.NodeInfo]
-	NodeInfoProvider models.NodeInfoProvider
-	IntervalConfig   NodeInfoPublisherIntervalConfig
+	PubSub            pubsub.Publisher[models.NodeState]
+	NodeStateProvider models.NodeStateProvider
+	IntervalConfig    NodeInfoPublisherIntervalConfig
 }
 
 type NodeInfoPublisher struct {
-	pubSub           pubsub.Publisher[models.NodeInfo]
-	nodeInfoProvider models.NodeInfoProvider
-	intervalConfig   NodeInfoPublisherIntervalConfig
-	stopped          bool
-	stopChannel      chan struct{}
-	stopOnce         sync.Once
+	pubSub            pubsub.Publisher[models.NodeState]
+	nodeStateProvider models.NodeStateProvider
+	intervalConfig    NodeInfoPublisherIntervalConfig
+	stopped           bool
+	stopChannel       chan struct{}
+	stopOnce          sync.Once
 }
 
 func NewNodeInfoPublisher(params NodeInfoPublisherParams) *NodeInfoPublisher {
 	p := &NodeInfoPublisher{
-		pubSub:           params.PubSub,
-		nodeInfoProvider: params.NodeInfoProvider,
-		intervalConfig:   params.IntervalConfig,
-		stopChannel:      make(chan struct{}),
+		pubSub:            params.PubSub,
+		nodeStateProvider: params.NodeStateProvider,
+		intervalConfig:    params.IntervalConfig,
+		stopChannel:       make(chan struct{}),
 	}
 
 	go func() {
@@ -71,7 +72,7 @@ func (n *NodeInfoPublisher) Publish(ctx context.Context) error {
 	ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/routing.NodeInfoPublisher.publish")
 	defer span.End()
 
-	return n.pubSub.Publish(ctx, n.nodeInfoProvider.GetNodeInfo(ctx))
+	return n.pubSub.Publish(ctx, n.nodeStateProvider.GetNodeState(ctx))
 }
 
 func (n *NodeInfoPublisher) eagerPublishBackgroundTask() {
diff --git a/pkg/routing/tracing/tracing.go b/pkg/routing/tracing/tracing.go
index 8e23bceb3a..c1fabb03c4 100644
--- a/pkg/routing/tracing/tracing.go
+++ b/pkg/routing/tracing/tracing.go
@@ -22,7 +22,7 @@ func NewNodeStore(delegate routing.NodeInfoStore) *NodeStore {
 	}
 }
 
-func (r *NodeStore) Add(ctx context.Context, nodeInfo models.NodeInfo) error {
+func (r *NodeStore) Add(ctx context.Context, state models.NodeState) error {
 	ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/routing.NodeInfoStore.Add") //nolint:govet
 	defer span.End()
 
@@ -31,14 +31,14 @@ func (r *NodeStore) Add(ctx context.Context, nodeInfo models.NodeInfo) error {
 		dur := stopwatch()
 		log.Ctx(ctx).Trace().
 			Dur("duration", dur).
-			Str("node", nodeInfo.ID()).
+			Str("node", state.Info.ID()).
 			Msg("node added")
 	}()
 
-	return r.delegate.Add(ctx, nodeInfo)
+	return r.delegate.Add(ctx, state)
 }
 
-func (r *NodeStore) Get(ctx context.Context, nodeID string) (models.NodeInfo, error) {
+func (r *NodeStore) Get(ctx context.Context, nodeID string) (models.NodeState, error) {
 	ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/routing.NodeInfoStore.Get") //nolint:govet
 	defer span.End()
 
@@ -54,7 +54,7 @@ func (r *NodeStore) Get(ctx context.Context, nodeID string) (models.NodeInfo, er
 	return r.delegate.Get(ctx, nodeID)
 }
 
-func (r *NodeStore) GetByPrefix(ctx context.Context, prefix string) (models.NodeInfo, error) {
+func (r *NodeStore) GetByPrefix(ctx context.Context, prefix string) (models.NodeState, error) {
 	ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/routing.NodeInfoStore.GetByPrefix") //nolint:govet
 	defer span.End()
 
@@ -74,7 +74,7 @@ func (r *NodeStore) FindPeer(ctx context.Context, peerID peer.ID) (peer.AddrInfo
 	return r.delegate.FindPeer(ctx, peerID)
 }
 
-func (r *NodeStore) List(ctx context.Context, filters ...routing.NodeInfoFilter) ([]models.NodeInfo, error) {
+func (r *NodeStore) List(ctx context.Context, filters ...routing.NodeStateFilter) ([]models.NodeState, error) {
 	ctx, span := system.NewSpan(ctx, system.GetTracer(), "pkg/routing.NodeInfoStore.List") //nolint:govet
 	defer span.End()
 
diff --git a/pkg/routing/types.go b/pkg/routing/types.go
index 75cf0aadeb..1facd0c611 100644
--- a/pkg/routing/types.go
+++ b/pkg/routing/types.go
@@ -3,33 +3,35 @@ package routing
 import (
 	"context"
 
-	"github.com/bacalhau-project/bacalhau/pkg/models"
 	libp2p_routing "github.com/libp2p/go-libp2p/core/routing"
+
+	"github.com/bacalhau-project/bacalhau/pkg/models"
 )
 
+// TODO rename this interface to NodeStore, it tracks more than their info
 type NodeInfoStore interface {
 	// TODO: Remove this interface once we switch to nats
 	libp2p_routing.PeerRouting
 
 	// Add adds a node info to the repo.
-	Add(ctx context.Context, nodeInfo models.NodeInfo) error
+	Add(ctx context.Context, nodeInfo models.NodeState) error
 
 	// Get returns the node info for the given node ID.
-	Get(ctx context.Context, nodeID string) (models.NodeInfo, error)
+	Get(ctx context.Context, nodeID string) (models.NodeState, error)
 
 	// GetByPrefix returns the node info for the given node ID.
 	// Supports both full and short node IDs.
-	GetByPrefix(ctx context.Context, prefix string) (models.NodeInfo, error)
+	GetByPrefix(ctx context.Context, prefix string) (models.NodeState, error)
 
 	// List returns a list of nodes
-	List(ctx context.Context, filters ...NodeInfoFilter) ([]models.NodeInfo, error)
+	List(ctx context.Context, filters ...NodeStateFilter) ([]models.NodeState, error)
 
 	// Delete deletes a node info from the repo.
 	Delete(ctx context.Context, nodeID string) error
 }
 
-// NodeInfoFilter is a function that filters node info
-// when listing nodes. It returns true if the node info
-// should be returned, and false if the node info should
+// NodeStateFilter is a function that filters node state
+// when listing nodes. It returns true if the node state
+// should be returned, and false if the node state should
 // be ignored.
-type NodeInfoFilter func(models.NodeInfo) bool
+type NodeStateFilter func(models.NodeState) bool
diff --git a/pkg/swagger/docs.go b/pkg/swagger/docs.go
index 07187d003a..8aac73dfaf 100644
--- a/pkg/swagger/docs.go
+++ b/pkg/swagger/docs.go
@@ -2831,7 +2831,7 @@ const docTemplate = `{
         "models.NodeInfo": {
             "type": "object",
             "properties": {
-                "Approval": {
+                "Membership": {
                     "$ref": "#/definitions/models.NodeApproval"
                 },
                 "BacalhauVersion": {
diff --git a/pkg/transport/interfaces.go b/pkg/transport/interfaces.go
index 8079eeb935..7c18fde790 100644
--- a/pkg/transport/interfaces.go
+++ b/pkg/transport/interfaces.go
@@ -23,7 +23,7 @@ type TransportLayer interface {
 
 	// NodeInfoPubSub enables compute nodes to publish their info and capabilities
 	// to orchestrator nodes for job matching and discovery.
-	NodeInfoPubSub() pubsub.PubSub[models.NodeInfo]
+	NodeInfoPubSub() pubsub.PubSub[models.NodeState]
 
 	// NodeInfoDecorator enables transport layer to enrich node info with data
 	// required for request routing


Test patch:
diff --git a/cmd/cli/agent/node_test.go b/cmd/cli/agent/node_test.go
index f23a63243b..ffe404c2d7 100644
--- a/cmd/cli/agent/node_test.go
+++ b/cmd/cli/agent/node_test.go
@@ -5,9 +5,10 @@ package agent_test
 import (
 	"testing"
 
+	"github.com/stretchr/testify/suite"
+
 	"github.com/bacalhau-project/bacalhau/pkg/lib/marshaller"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
-	"github.com/stretchr/testify/suite"
 
 	cmdtesting "github.com/bacalhau-project/bacalhau/cmd/testing"
 	"github.com/bacalhau-project/bacalhau/cmd/util/output"
@@ -25,18 +26,18 @@ func (s *NodeSuite) TestNodeJSONOutput() {
 	_, out, err := s.ExecuteTestCobraCommand("agent", "node", "--output", string(output.JSONFormat))
 	s.Require().NoError(err, "Could not request node with json output.")
 
-	nodeInfo := &models.NodeInfo{}
+	nodeInfo := &models.NodeState{}
 	err = marshaller.JSONUnmarshalWithMax([]byte(out), &nodeInfo)
 	s.Require().NoError(err, "Could not unmarshall the output into json - %+v", err)
-	s.Require().Equal(s.Node.ID, nodeInfo.ID(), "Node ID does not match in json.")
+	s.Require().Equal(s.Node.ID, nodeInfo.Info.ID(), "Node ID does not match in json.")
 }
 
 func (s *NodeSuite) TestNodeYAMLOutput() {
 	_, out, err := s.ExecuteTestCobraCommand("agent", "node")
 	s.Require().NoError(err, "Could not request node with yaml output.")
 
-	nodeInfo := &models.NodeInfo{}
+	nodeInfo := &models.NodeState{}
 	err = marshaller.YAMLUnmarshalWithMax([]byte(out), &nodeInfo)
 	s.Require().NoError(err, "Could not unmarshall the output into yaml - %+v", err)
-	s.Require().Equal(s.Node.ID, nodeInfo.ID(), "Node ID does not match in yaml.")
+	s.Require().Equal(s.Node.ID, nodeInfo.Info.ID(), "Node ID does not match in yaml.")
 }
diff --git a/pkg/node/heartbeat/heartbeat_test.go b/pkg/node/heartbeat/heartbeat_test.go
index 9ecb1e9bc2..42fe31eb41 100644
--- a/pkg/node/heartbeat/heartbeat_test.go
+++ b/pkg/node/heartbeat/heartbeat_test.go
@@ -9,12 +9,13 @@ import (
 	"testing"
 	"time"
 
-	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/benbjohnson/clock"
 	"github.com/nats-io/nats-server/v2/server"
 	natsserver "github.com/nats-io/nats-server/v2/test"
 	"github.com/nats-io/nats.go"
 	"github.com/stretchr/testify/suite"
+
+	"github.com/bacalhau-project/bacalhau/pkg/models"
 )
 
 const (
@@ -74,7 +75,7 @@ func (s *HeartbeatTestSuite) TestSendHeartbeat() {
 		name           string
 		includeInitial bool
 		heartbeats     []time.Duration
-		expectedState  models.NodeState
+		expectedState  models.NodeConnectionState
 		waitUntil      time.Duration
 	}
 
@@ -116,13 +117,13 @@ func (s *HeartbeatTestSuite) TestSendHeartbeat() {
 	}
 
 	for i, tc := range testcases {
-		nodeInfo := models.NodeInfo{
-			NodeID: "node-" + strconv.Itoa(i),
+		nodeState := models.NodeState{
+			Info: models.NodeInfo{NodeID: "node-" + strconv.Itoa(i)},
 		}
 
 		s.T().Run(tc.name, func(t *testing.T) {
 			// Wait for the first heartbeat to be sent
-			client, err := NewClient(s.client, nodeInfo.NodeID, TestTopic)
+			client, err := NewClient(s.client, nodeState.Info.NodeID, TestTopic)
 			s.Require().NoError(err)
 			defer client.Close(ctx)
 
@@ -147,8 +148,8 @@ func (s *HeartbeatTestSuite) TestSendHeartbeat() {
 
 			s.clock.Add(tc.waitUntil)
 
-			server.UpdateNodeInfo(&nodeInfo)
-			s.Require().Equal(nodeInfo.State, tc.expectedState, fmt.Sprintf("incorrect state in %s", tc.name))
+			server.UpdateNodeInfo(&nodeState)
+			s.Require().Equal(nodeState.Connection, tc.expectedState, fmt.Sprintf("incorrect state in %s", tc.name))
 		})
 	}
 }
diff --git a/pkg/orchestrator/scheduler/batch_job_test.go b/pkg/orchestrator/scheduler/batch_job_test.go
index 598e98521e..bc34c88148 100644
--- a/pkg/orchestrator/scheduler/batch_job_test.go
+++ b/pkg/orchestrator/scheduler/batch_job_test.go
@@ -6,14 +6,15 @@ import (
 	"context"
 	"testing"
 
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/suite"
+	"go.uber.org/mock/gomock"
+
 	"github.com/bacalhau-project/bacalhau/pkg/jobstore"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/orchestrator"
 	"github.com/bacalhau-project/bacalhau/pkg/orchestrator/retry"
 	"github.com/bacalhau-project/bacalhau/pkg/test/mock"
-	"github.com/google/uuid"
-	"github.com/stretchr/testify/suite"
-	"go.uber.org/mock/gomock"
 )
 
 const (
@@ -69,11 +70,11 @@ func (s *BatchJobSchedulerTestSuite) TestProcess_ShouldCreateEnoughExecutions()
 
 	// we need 3 executions. discover enough nodes
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), nodeIDs[0]),
-		*mockNodeInfo(s.T(), nodeIDs[1]),
-		*mockNodeInfo(s.T(), nodeIDs[2]),
-		*mockNodeInfo(s.T(), nodeIDs[3]),
-		*mockNodeInfo(s.T(), nodeIDs[4]),
+		*fakeNodeInfo(s.T(), nodeIDs[0]),
+		*fakeNodeInfo(s.T(), nodeIDs[1]),
+		*fakeNodeInfo(s.T(), nodeIDs[2]),
+		*fakeNodeInfo(s.T(), nodeIDs[3]),
+		*fakeNodeInfo(s.T(), nodeIDs[4]),
 	}
 	s.mockNodeSelection(job, nodeInfos, job.Count)
 
@@ -97,8 +98,8 @@ func (s *BatchJobSchedulerTestSuite) TestProcess_AlreadyEnoughExecutions() {
 
 	// mock active executions' nodes to be healthy
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[execAskForBid].NodeID),
-		*mockNodeInfo(s.T(), executions[execBidAccepted].NodeID),
+		*fakeNodeInfo(s.T(), executions[execAskForBid].NodeID),
+		*fakeNodeInfo(s.T(), executions[execBidAccepted].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 
@@ -125,9 +126,9 @@ func (s *BatchJobSchedulerTestSuite) TestProcess_RejectExtraExecutions() {
 
 	// mock active executions' nodes to be healthy
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[0].NodeID),
-		*mockNodeInfo(s.T(), executions[1].NodeID),
-		*mockNodeInfo(s.T(), executions[2].NodeID),
+		*fakeNodeInfo(s.T(), executions[0].NodeID),
+		*fakeNodeInfo(s.T(), executions[1].NodeID),
+		*fakeNodeInfo(s.T(), executions[2].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 
@@ -151,8 +152,8 @@ func (s *BatchJobSchedulerTestSuite) TestProcess_TooManyExecutions() {
 
 	// mock active executions' nodes to be healthy
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[execAskForBid].NodeID),
-		*mockNodeInfo(s.T(), executions[execBidAccepted].NodeID),
+		*fakeNodeInfo(s.T(), executions[execAskForBid].NodeID),
+		*fakeNodeInfo(s.T(), executions[execBidAccepted].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 	matcher := NewPlanMatcher(s.T(), PlanMatcherParams{
@@ -172,8 +173,8 @@ func (s *BatchJobSchedulerTestSuite) TestProcessFail_NotEnoughExecutions() {
 
 	// we need 3 executions. discover fewer nodes
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), nodeIDs[0]),
-		*mockNodeInfo(s.T(), nodeIDs[1]),
+		*fakeNodeInfo(s.T(), nodeIDs[0]),
+		*fakeNodeInfo(s.T(), nodeIDs[1]),
 	}
 	s.mockNodeSelection(job, nodeInfos, job.Count)
 
@@ -221,8 +222,8 @@ func (s *BatchJobSchedulerTestSuite) TestFailUnhealthyExecs_ShouldMarkExecutions
 
 	// mock node discoverer to exclude the node in BidAccepted state
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[execAskForBid].NodeID),
-		*mockNodeInfo(s.T(), executions[execCanceled].NodeID),
+		*fakeNodeInfo(s.T(), executions[execAskForBid].NodeID),
+		*fakeNodeInfo(s.T(), executions[execCanceled].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 	s.mockNodeSelection(job, nodeInfos, 1)
@@ -285,8 +286,8 @@ func (s *BatchJobSchedulerTestSuite) TestProcess_ShouldMarkJobAsFailed_NoRetry()
 
 	// mark askForBid exec as lost so we attempt to retry
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[execBidAccepted].NodeID),
-		*mockNodeInfo(s.T(), executions[execCompleted].NodeID),
+		*fakeNodeInfo(s.T(), executions[execBidAccepted].NodeID),
+		*fakeNodeInfo(s.T(), executions[execCompleted].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 
diff --git a/pkg/orchestrator/scheduler/daemon_job_test.go b/pkg/orchestrator/scheduler/daemon_job_test.go
index 69cb8033f0..5f74a87077 100644
--- a/pkg/orchestrator/scheduler/daemon_job_test.go
+++ b/pkg/orchestrator/scheduler/daemon_job_test.go
@@ -6,13 +6,14 @@ import (
 	"context"
 	"testing"
 
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/suite"
+	"go.uber.org/mock/gomock"
+
 	"github.com/bacalhau-project/bacalhau/pkg/jobstore"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/orchestrator"
 	"github.com/bacalhau-project/bacalhau/pkg/test/mock"
-	"github.com/google/uuid"
-	"github.com/stretchr/testify/suite"
-	"go.uber.org/mock/gomock"
 )
 
 type DaemonJobSchedulerTestSuite struct {
@@ -48,9 +49,9 @@ func (s *DaemonJobSchedulerTestSuite) TestProcess_ShouldCreateNewExecutions() {
 	s.jobStore.EXPECT().GetExecutions(gomock.Any(), jobstore.GetExecutionsOptions{JobID: job.ID}).Return(executions, nil)
 
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), nodeIDs[0]),
-		*mockNodeInfo(s.T(), nodeIDs[1]),
-		*mockNodeInfo(s.T(), nodeIDs[2]),
+		*fakeNodeInfo(s.T(), nodeIDs[0]),
+		*fakeNodeInfo(s.T(), nodeIDs[1]),
+		*fakeNodeInfo(s.T(), nodeIDs[2]),
 	}
 	s.nodeSelector.EXPECT().AllMatchingNodes(gomock.Any(), gomock.Any(), gomock.Any()).Return(nodeInfos, nil)
 
@@ -97,7 +98,7 @@ func (s *DaemonJobSchedulerTestSuite) TestProcess_ShouldMarkLostExecutionsOnUnhe
 
 	// mock node discoverer to exclude the first node
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[1].NodeID),
+		*fakeNodeInfo(s.T(), executions[1].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 	s.nodeSelector.EXPECT().AllMatchingNodes(gomock.Any(), job, gomock.Any()).Return(nodeInfos, nil)
@@ -126,7 +127,7 @@ func (s *DaemonJobSchedulerTestSuite) TestProcess_ShouldNOTMarkJobAsFailed() {
 
 	// mock node discoverer to exclude the first node
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[1].NodeID),
+		*fakeNodeInfo(s.T(), executions[1].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 	s.nodeSelector.EXPECT().AllMatchingNodes(gomock.Any(), job, gomock.Any()).Return(nodeInfos, nil)
diff --git a/pkg/orchestrator/scheduler/ops_job_test.go b/pkg/orchestrator/scheduler/ops_job_test.go
index a32f7d6cdf..c9813071c3 100644
--- a/pkg/orchestrator/scheduler/ops_job_test.go
+++ b/pkg/orchestrator/scheduler/ops_job_test.go
@@ -6,13 +6,14 @@ import (
 	"context"
 	"testing"
 
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/suite"
+	"go.uber.org/mock/gomock"
+
 	"github.com/bacalhau-project/bacalhau/pkg/jobstore"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/orchestrator"
 	"github.com/bacalhau-project/bacalhau/pkg/test/mock"
-	"github.com/google/uuid"
-	"github.com/stretchr/testify/suite"
-	"go.uber.org/mock/gomock"
 )
 
 type OpsJobSchedulerTestSuite struct {
@@ -48,9 +49,9 @@ func (s *OpsJobSchedulerTestSuite) TestProcess_ShouldCreateNewExecutions() {
 	s.jobStore.EXPECT().GetExecutions(gomock.Any(), jobstore.GetExecutionsOptions{JobID: job.ID}).Return(executions, nil)
 
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), nodeIDs[0]),
-		*mockNodeInfo(s.T(), nodeIDs[1]),
-		*mockNodeInfo(s.T(), nodeIDs[2]),
+		*fakeNodeInfo(s.T(), nodeIDs[0]),
+		*fakeNodeInfo(s.T(), nodeIDs[1]),
+		*fakeNodeInfo(s.T(), nodeIDs[2]),
 	}
 	s.mockNodeSelection(job, nodeInfos)
 
@@ -93,7 +94,7 @@ func (s *OpsJobSchedulerTestSuite) TestProcess_ShouldMarkLostExecutionsOnUnhealt
 
 	// mock node discoverer to exclude the first node
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[1].NodeID),
+		*fakeNodeInfo(s.T(), executions[1].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 
@@ -118,7 +119,7 @@ func (s *OpsJobSchedulerTestSuite) TestProcess_ShouldMarkJobAsFailed() {
 
 	// mock node discoverer to exclude the first node
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[1].NodeID),
+		*fakeNodeInfo(s.T(), executions[1].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 
diff --git a/pkg/orchestrator/scheduler/service_job_test.go b/pkg/orchestrator/scheduler/service_job_test.go
index 48c9fd6fc0..9fcac97b15 100644
--- a/pkg/orchestrator/scheduler/service_job_test.go
+++ b/pkg/orchestrator/scheduler/service_job_test.go
@@ -6,14 +6,15 @@ import (
 	"context"
 	"testing"
 
+	"github.com/google/uuid"
+	"github.com/stretchr/testify/suite"
+	"go.uber.org/mock/gomock"
+
 	"github.com/bacalhau-project/bacalhau/pkg/jobstore"
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/orchestrator"
 	"github.com/bacalhau-project/bacalhau/pkg/orchestrator/retry"
 	"github.com/bacalhau-project/bacalhau/pkg/test/mock"
-	"github.com/google/uuid"
-	"github.com/stretchr/testify/suite"
-	"go.uber.org/mock/gomock"
 )
 
 const (
@@ -61,11 +62,11 @@ func (s *ServiceJobSchedulerTestSuite) TestProcess_ShouldCreateEnoughExecutions(
 
 	// we need 3 executions. discover enough nodes
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), nodeIDs[0]),
-		*mockNodeInfo(s.T(), nodeIDs[1]),
-		*mockNodeInfo(s.T(), nodeIDs[2]),
-		*mockNodeInfo(s.T(), nodeIDs[3]),
-		*mockNodeInfo(s.T(), nodeIDs[4]),
+		*fakeNodeInfo(s.T(), nodeIDs[0]),
+		*fakeNodeInfo(s.T(), nodeIDs[1]),
+		*fakeNodeInfo(s.T(), nodeIDs[2]),
+		*fakeNodeInfo(s.T(), nodeIDs[3]),
+		*fakeNodeInfo(s.T(), nodeIDs[4]),
 	}
 	s.mockNodeSelection(job, nodeInfos, job.Count)
 
@@ -89,9 +90,9 @@ func (s *ServiceJobSchedulerTestSuite) TestProcess_AlreadyEnoughExecutions() {
 
 	// mock active executions' nodes to be healthy
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[execServiceAskForBid].NodeID),
-		*mockNodeInfo(s.T(), executions[execServiceBidAccepted1].NodeID),
-		*mockNodeInfo(s.T(), executions[execServiceBidAccepted2].NodeID),
+		*fakeNodeInfo(s.T(), executions[execServiceAskForBid].NodeID),
+		*fakeNodeInfo(s.T(), executions[execServiceBidAccepted1].NodeID),
+		*fakeNodeInfo(s.T(), executions[execServiceBidAccepted2].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 
@@ -118,9 +119,9 @@ func (s *ServiceJobSchedulerTestSuite) TestProcess_RejectExtraExecutions() {
 
 	// mock active executions' nodes to be healthy
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[0].NodeID),
-		*mockNodeInfo(s.T(), executions[1].NodeID),
-		*mockNodeInfo(s.T(), executions[2].NodeID),
+		*fakeNodeInfo(s.T(), executions[0].NodeID),
+		*fakeNodeInfo(s.T(), executions[1].NodeID),
+		*fakeNodeInfo(s.T(), executions[2].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 
@@ -145,9 +146,9 @@ func (s *ServiceJobSchedulerTestSuite) TestProcess_TooManyExecutions() {
 
 	// mock active executions' nodes to be healthy
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[execServiceAskForBid].NodeID),
-		*mockNodeInfo(s.T(), executions[execServiceBidAccepted1].NodeID),
-		*mockNodeInfo(s.T(), executions[execServiceBidAccepted2].NodeID),
+		*fakeNodeInfo(s.T(), executions[execServiceAskForBid].NodeID),
+		*fakeNodeInfo(s.T(), executions[execServiceBidAccepted1].NodeID),
+		*fakeNodeInfo(s.T(), executions[execServiceBidAccepted2].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 	matcher := NewPlanMatcher(s.T(), PlanMatcherParams{
@@ -167,8 +168,8 @@ func (s *ServiceJobSchedulerTestSuite) TestProcessFail_NotEnoughExecutions() {
 
 	// we need 3 executions. discover fewer nodes
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), nodeIDs[0]),
-		*mockNodeInfo(s.T(), nodeIDs[1]),
+		*fakeNodeInfo(s.T(), nodeIDs[0]),
+		*fakeNodeInfo(s.T(), nodeIDs[1]),
 	}
 	s.mockNodeSelection(job, nodeInfos, job.Count)
 
@@ -217,8 +218,8 @@ func (s *ServiceJobSchedulerTestSuite) TestFailUnhealthyExecs_ShouldMarkExecutio
 
 	// mock node discoverer to exclude the node in BidAccepted state
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[execServiceAskForBid].NodeID),
-		*mockNodeInfo(s.T(), executions[execServiceCanceled].NodeID),
+		*fakeNodeInfo(s.T(), executions[execServiceAskForBid].NodeID),
+		*fakeNodeInfo(s.T(), executions[execServiceCanceled].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 	s.mockNodeSelection(job, nodeInfos, 2)
@@ -250,11 +251,11 @@ func (s *ServiceJobSchedulerTestSuite) TestProcess_TreatCompletedExecutionsAsFai
 
 	// discover all nodes to avoid treating active executions as unhealthy
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), nodeIDs[0]),
-		*mockNodeInfo(s.T(), nodeIDs[1]),
-		*mockNodeInfo(s.T(), nodeIDs[2]),
-		*mockNodeInfo(s.T(), nodeIDs[3]),
-		*mockNodeInfo(s.T(), nodeIDs[4]),
+		*fakeNodeInfo(s.T(), nodeIDs[0]),
+		*fakeNodeInfo(s.T(), nodeIDs[1]),
+		*fakeNodeInfo(s.T(), nodeIDs[2]),
+		*fakeNodeInfo(s.T(), nodeIDs[3]),
+		*fakeNodeInfo(s.T(), nodeIDs[4]),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 	s.mockNodeSelection(job, nodeInfos, 2)
@@ -302,8 +303,8 @@ func (s *ServiceJobSchedulerTestSuite) TestProcess_ShouldMarkJobAsFailed_NoRetry
 
 	// mark askForBid exec as lost so we attempt to retry
 	nodeInfos := []models.NodeInfo{
-		*mockNodeInfo(s.T(), executions[execServiceBidAccepted1].NodeID),
-		*mockNodeInfo(s.T(), executions[execServiceBidAccepted2].NodeID),
+		*fakeNodeInfo(s.T(), executions[execServiceBidAccepted1].NodeID),
+		*fakeNodeInfo(s.T(), executions[execServiceBidAccepted2].NodeID),
 	}
 	s.nodeSelector.EXPECT().AllNodes(gomock.Any()).Return(nodeInfos, nil)
 
diff --git a/pkg/orchestrator/scheduler/utils_test.go b/pkg/orchestrator/scheduler/utils_test.go
index 2845a20dbc..598abdd308 100644
--- a/pkg/orchestrator/scheduler/utils_test.go
+++ b/pkg/orchestrator/scheduler/utils_test.go
@@ -120,10 +120,8 @@ func (m PlanMatcher) String() string {
 		m.JobState, m.Evaluation, m.NewExecutionsNodes, m.StoppedExecutions, m.ApprovedExecutions)
 }
 
-func mockNodeInfo(t *testing.T, nodeID string) *models.NodeInfo {
+func fakeNodeInfo(t *testing.T, nodeID string) *models.NodeInfo {
 	return &models.NodeInfo{
-		NodeID:   nodeID,
-		Approval: models.NodeApprovals.APPROVED,
-		State:    models.NodeStates.CONNECTED,
+		NodeID: nodeID,
 	}
 }
diff --git a/pkg/orchestrator/selection/discovery/chained_test.go b/pkg/orchestrator/selection/discovery/chained_test.go
deleted file mode 100644
index b542a5eb77..0000000000
--- a/pkg/orchestrator/selection/discovery/chained_test.go
+++ /dev/null
@@ -1,83 +0,0 @@
-//go:build unit || !integration
-
-package discovery
-
-import (
-	"context"
-	"errors"
-	"testing"
-
-	"github.com/bacalhau-project/bacalhau/pkg/models"
-	"github.com/stretchr/testify/suite"
-)
-
-type ChainedSuite struct {
-	suite.Suite
-	chain   *Chain
-	peerID1 models.NodeInfo
-	peerID2 models.NodeInfo
-	peerID3 models.NodeInfo
-}
-
-func (s *ChainedSuite) SetupSuite() {
-	s.peerID1 = models.NodeInfo{NodeID: "peerID1"}
-	s.peerID2 = models.NodeInfo{NodeID: "peerID2"}
-	s.peerID3 = models.NodeInfo{NodeID: "peerID3"}
-}
-
-func (s *ChainedSuite) SetupTest() {
-	s.chain = NewChain(false) // don't ignore errors
-}
-
-func TestChainedSuite(t *testing.T) {
-	suite.Run(t, new(ChainedSuite))
-}
-
-func (s *ChainedSuite) TestListNodes() {
-	s.chain.Add(NewFixedDiscoverer(s.peerID1))
-	s.chain.Add(NewFixedDiscoverer(s.peerID2))
-	s.chain.Add(NewFixedDiscoverer(s.peerID3))
-
-	peerIDs, err := s.chain.ListNodes(context.Background())
-	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{s.peerID1, s.peerID2, s.peerID3}, peerIDs)
-}
-
-func (s *ChainedSuite) TestListNodes_Overlap() {
-	s.chain.Add(NewFixedDiscoverer(s.peerID1, s.peerID2))
-	s.chain.Add(NewFixedDiscoverer(s.peerID2, s.peerID3))
-
-	peerIDs, err := s.chain.ListNodes(context.Background())
-	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{s.peerID1, s.peerID2, s.peerID3}, peerIDs)
-}
-
-func (s *ChainedSuite) TestHandle_Error() {
-	s.chain.Add(NewFixedDiscoverer(s.peerID1, s.peerID2))
-	s.chain.Add(newBadDiscoverer())
-	s.chain.Add(NewFixedDiscoverer(s.peerID3))
-	_, err := s.chain.ListNodes(context.Background())
-	s.Error(err)
-}
-
-func (s *ChainedSuite) TestHandle_IgnoreError() {
-	s.chain.ignoreErrors = true
-	s.chain.Add(NewFixedDiscoverer(s.peerID1, s.peerID2))
-	s.chain.Add(newBadDiscoverer())
-	s.chain.Add(NewFixedDiscoverer(s.peerID3))
-
-	peerIDs, err := s.chain.ListNodes(context.Background())
-	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{s.peerID1, s.peerID2, s.peerID3}, peerIDs)
-}
-
-// node discoverer that always returns an error
-type badDiscoverer struct{}
-
-func newBadDiscoverer() *badDiscoverer {
-	return &badDiscoverer{}
-}
-
-func (b *badDiscoverer) ListNodes(context.Context) ([]models.NodeInfo, error) {
-	return nil, errors.New("bad discoverer")
-}
diff --git a/pkg/orchestrator/selection/discovery/store_test.go b/pkg/orchestrator/selection/discovery/store_test.go
deleted file mode 100644
index be35d0d05a..0000000000
--- a/pkg/orchestrator/selection/discovery/store_test.go
+++ /dev/null
@@ -1,70 +0,0 @@
-//go:build unit || !integration
-
-package discovery
-
-import (
-	"context"
-	"math"
-	"testing"
-
-	"github.com/bacalhau-project/bacalhau/pkg/models"
-	"github.com/bacalhau-project/bacalhau/pkg/routing/inmemory"
-	"github.com/bacalhau-project/bacalhau/pkg/test/mock"
-	"github.com/stretchr/testify/suite"
-)
-
-type StoreNodeDiscovererSuite struct {
-	suite.Suite
-	discoverer *StoreNodeDiscoverer
-	store      *inmemory.NodeStore
-}
-
-func (s *StoreNodeDiscovererSuite) SetupTest() {
-	s.store = inmemory.NewNodeStore(inmemory.NodeStoreParams{
-		TTL: math.MaxInt64,
-	})
-	s.discoverer = NewStoreNodeDiscoverer(StoreNodeDiscovererParams{
-		Store: s.store,
-	})
-}
-
-func TestStoreNodeDiscovererSuite(t *testing.T) {
-	suite.Run(t, new(StoreNodeDiscovererSuite))
-}
-
-func (s *StoreNodeDiscovererSuite) TestListNodes() {
-	ctx := context.Background()
-	nodeInfo1 := generateNodeInfo("node1", models.EngineDocker)
-	s.NoError(s.store.Add(ctx, nodeInfo1))
-
-	// both nodes are returned when asked for docker nodes
-	job := mock.Job()
-	job.Task().Engine.Type = models.EngineDocker
-
-	peerIDs, err := s.discoverer.ListNodes(context.Background())
-	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, peerIDs)
-
-	nodeInfo2 := generateNodeInfo("node2", models.EngineDocker, models.EngineWasm)
-	s.NoError(s.store.Add(ctx, nodeInfo2))
-	// only node2 is returned when asked for noop nodes
-	peerIDs, err = s.discoverer.ListNodes(context.Background())
-	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1, nodeInfo2}, peerIDs)
-}
-
-func (s *StoreNodeDiscovererSuite) TestListNodes_Empty() {
-	peerIDs, err := s.discoverer.ListNodes(context.Background())
-	s.NoError(err)
-	s.Empty(peerIDs)
-}
-
-func generateNodeInfo(id string, engines ...string) models.NodeInfo {
-	return models.NodeInfo{
-		NodeID:   id,
-		NodeType: models.NodeTypeCompute,
-		ComputeNodeInfo: &models.ComputeNodeInfo{
-			ExecutionEngines: engines,
-		},
-	}
-}
diff --git a/pkg/publicapi/test/agent_test.go b/pkg/publicapi/test/agent_test.go
index 2a1315cd91..c6a06eb9bc 100644
--- a/pkg/publicapi/test/agent_test.go
+++ b/pkg/publicapi/test/agent_test.go
@@ -4,11 +4,7 @@ package test
 
 import (
 	"context"
-	"testing"
 
-	"github.com/stretchr/testify/require"
-
-	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/publicapi/apimodels"
 	"github.com/bacalhau-project/bacalhau/pkg/version"
 )
@@ -36,12 +32,15 @@ func (s *ServerSuite) TestAgentNode() {
 	resp, err := s.client.Agent().Node(ctx, &apimodels.GetAgentNodeRequest{})
 	s.Require().NoError(err)
 	s.Require().NotEmpty(resp)
-	s.Require().NotNil(resp.NodeInfo)
+	s.Require().NotNil(resp.NodeState)
 
-	node := s.requesterNode
-	expectedNode, err := node.RequesterNode.NodeInfoStore.Get(context.Background(), s.requesterNode.ID)
+	requesterNode := s.requesterNode
+	// NB(forrest): we are only asserting NodeInfos are equal (which excludes approvals and liveness from NodeState)
+	// because we are asking the requester's NodeInfoStore for the NodeState it contains on itself (s.requesterNode.ID)
+	// and since the requester doesn't send heartbeat messages to itself it will consider itself disconnected
+	expectedNode, err := requesterNode.RequesterNode.NodeInfoStore.Get(context.Background(), s.requesterNode.ID)
 	s.Require().NoError(err)
-	equalNodeInfo(s.T(), expectedNode, *resp.NodeInfo)
+	s.Require().Equal(expectedNode.Info, resp.Info)
 }
 
 func (s *ServerSuite) TestAgentNodeCompute() {
@@ -49,26 +48,5 @@ func (s *ServerSuite) TestAgentNodeCompute() {
 	resp, err := s.computeClient.Agent().Node(ctx, &apimodels.GetAgentNodeRequest{})
 	s.Require().NoError(err)
 	s.Require().NotEmpty(resp)
-	s.Require().NotNil(resp.NodeInfo)
-}
-
-func equalNodeInfo(t *testing.T, a, b models.NodeInfo) {
-	require.Equal(t, a.BacalhauVersion, b.BacalhauVersion)
-	require.Equal(t, a.ID(), b.ID())
-	require.Equal(t, a.NodeType, b.NodeType)
-	require.Equal(t, a.Labels, b.Labels)
-
-	if a.ComputeNodeInfo == nil {
-		require.Nil(t, b.ComputeNodeInfo)
-		return
-	}
-	require.ElementsMatch(t, a.ComputeNodeInfo.ExecutionEngines, b.ComputeNodeInfo.ExecutionEngines)
-	require.ElementsMatch(t, a.ComputeNodeInfo.Publishers, b.ComputeNodeInfo.Publishers)
-	require.ElementsMatch(t, a.ComputeNodeInfo.StorageSources, b.ComputeNodeInfo.StorageSources)
-	require.Equal(t, a.ComputeNodeInfo.MaxCapacity, b.ComputeNodeInfo.MaxCapacity)
-	require.Equal(t, a.ComputeNodeInfo.AvailableCapacity, b.ComputeNodeInfo.AvailableCapacity)
-	require.Equal(t, a.ComputeNodeInfo.MaxJobRequirements, b.ComputeNodeInfo.MaxJobRequirements)
-	require.Equal(t, a.ComputeNodeInfo.RunningExecutions, b.ComputeNodeInfo.RunningExecutions)
-	require.Equal(t, a.ComputeNodeInfo.RunningExecutions, b.ComputeNodeInfo.RunningExecutions)
-
+	s.Require().NotNil(resp.NodeState)
 }
diff --git a/pkg/routing/inmemory/inmemory_test.go b/pkg/routing/inmemory/inmemory_test.go
index 3d5c55aeb1..91dd46337c 100644
--- a/pkg/routing/inmemory/inmemory_test.go
+++ b/pkg/routing/inmemory/inmemory_test.go
@@ -8,10 +8,11 @@ import (
 	"testing"
 	"time"
 
+	"github.com/stretchr/testify/suite"
+
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/routing"
 	"github.com/bacalhau-project/bacalhau/pkg/routing/inmemory"
-	"github.com/stretchr/testify/suite"
 )
 
 var nodeIDs = []string{
@@ -37,17 +38,17 @@ func TestInMemoryNodeStoreSuite(t *testing.T) {
 
 func (s *InMemoryNodeStoreSuite) Test_Get() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
+	nodeInfo1 := generateNodeState(nodeIDs[1], models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
 	// test Get
-	res1, err := s.store.Get(ctx, nodeInfo0.ID())
+	res1, err := s.store.Get(ctx, nodeInfo0.Info.ID())
 	s.NoError(err)
 	s.Equal(nodeInfo0, res1)
 
-	res2, err := s.store.Get(ctx, nodeInfo1.ID())
+	res2, err := s.store.Get(ctx, nodeInfo1.Info.ID())
 	s.NoError(err)
 	s.Equal(nodeInfo1, res2)
 }
@@ -62,7 +63,7 @@ func (s *InMemoryNodeStoreSuite) Test_GetNotFound() {
 
 func (s *InMemoryNodeStoreSuite) Test_GetByPrefix_SingleMatch() {
 	ctx := context.Background()
-	nodeInfo := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
+	nodeInfo := generateNodeState(nodeIDs[0], models.EngineDocker)
 	s.NoError(s.store.Add(ctx, nodeInfo))
 
 	res, err := s.store.GetByPrefix(ctx, "QmdZQ7")
@@ -72,8 +73,8 @@ func (s *InMemoryNodeStoreSuite) Test_GetByPrefix_SingleMatch() {
 
 func (s *InMemoryNodeStoreSuite) Test_GetByPrefix_MultipleMatches() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
+	nodeInfo1 := generateNodeState(nodeIDs[1], models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
@@ -95,7 +96,7 @@ func (s *InMemoryNodeStoreSuite) Test_GetByPrefix_ExpiredNode() {
 		TTL: 10 * time.Millisecond,
 	})
 
-	nodeInfo := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
+	nodeInfo := generateNodeState(nodeIDs[0], models.EngineDocker)
 	s.NoError(store.Add(ctx, nodeInfo))
 
 	// Wait for the item to expire
@@ -108,44 +109,44 @@ func (s *InMemoryNodeStoreSuite) Test_GetByPrefix_ExpiredNode() {
 
 func (s *InMemoryNodeStoreSuite) Test_List() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
+	nodeInfo1 := generateNodeState(nodeIDs[1], models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
 	// test List
 	allNodeInfos, err := s.store.List(ctx)
 	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo0, nodeInfo1}, allNodeInfos)
+	s.ElementsMatch([]models.NodeState{nodeInfo0, nodeInfo1}, allNodeInfos)
 }
 
 func (s *InMemoryNodeStoreSuite) Test_ListWithFilters() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
+	nodeInfo1 := generateNodeState(nodeIDs[1], models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
 	// Match one record
-	filterPartialID := func(node models.NodeInfo) bool {
-		return strings.HasPrefix(node.ID(), string(nodeIDs[0][0:8]))
+	filterPartialID := func(node models.NodeState) bool {
+		return strings.HasPrefix(node.Info.ID(), string(nodeIDs[0][0:8]))
 	}
 	nodes, err := s.store.List(ctx, filterPartialID)
 	s.NoError(err)
 	s.Equal(1, len(nodes))
-	s.Equal(nodeIDs[0], nodes[0].ID())
+	s.Equal(nodeIDs[0], nodes[0].Info.ID())
 
 	// Match all records
-	filterPartialID = func(node models.NodeInfo) bool {
-		return strings.HasPrefix(node.ID(), "Qm")
+	filterPartialID = func(node models.NodeState) bool {
+		return strings.HasPrefix(node.Info.ID(), "Qm")
 	}
 	nodes, err = s.store.List(ctx, filterPartialID)
 	s.NoError(err)
 	s.Equal(2, len(nodes))
 
 	// Match no records
-	filterPartialID = func(node models.NodeInfo) bool {
-		return strings.HasPrefix(node.ID(), "XYZ")
+	filterPartialID = func(node models.NodeState) bool {
+		return strings.HasPrefix(node.Info.ID(), "XYZ")
 	}
 	nodes, err = s.store.List(ctx, filterPartialID)
 	s.NoError(err)
@@ -154,19 +155,19 @@ func (s *InMemoryNodeStoreSuite) Test_ListWithFilters() {
 
 func (s *InMemoryNodeStoreSuite) Test_Delete() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineDocker, models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
+	nodeInfo1 := generateNodeState(nodeIDs[1], models.EngineDocker, models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
 	// delete first node
-	s.NoError(s.store.Delete(ctx, nodeInfo0.ID()))
+	s.NoError(s.store.Delete(ctx, nodeInfo0.Info.ID()))
 	nodes, err := s.store.List(ctx)
 	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, nodes)
+	s.ElementsMatch([]models.NodeState{nodeInfo1}, nodes)
 
 	// delete second node
-	s.NoError(s.store.Delete(ctx, nodeInfo1.ID()))
+	s.NoError(s.store.Delete(ctx, nodeInfo1.Info.ID()))
 	nodes, err = s.store.List(ctx)
 	s.NoError(err)
 	s.Empty(nodes)
@@ -174,21 +175,21 @@ func (s *InMemoryNodeStoreSuite) Test_Delete() {
 
 func (s *InMemoryNodeStoreSuite) Test_Replace() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineWasm)
-	nodeInfo1.NodeID = nodeInfo0.NodeID
+	nodeInfo1 := generateNodeState(nodeIDs[0], models.EngineWasm)
+	nodeInfo1.Info.NodeID = nodeInfo0.Info.NodeID
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
-	res, err := s.store.Get(ctx, nodeInfo0.ID())
+	res, err := s.store.Get(ctx, nodeInfo0.Info.ID())
 	s.NoError(err)
 	s.Equal(nodeInfo1, res)
 
 	// test List
 	allNodeInfos, err := s.store.List(ctx)
 	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, allNodeInfos)
+	s.ElementsMatch([]models.NodeState{nodeInfo1}, allNodeInfos)
 }
 
 func (s *InMemoryNodeStoreSuite) Test_Eviction() {
@@ -197,22 +198,28 @@ func (s *InMemoryNodeStoreSuite) Test_Eviction() {
 		TTL: ttl,
 	})
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 
 	// test Get
-	res, err := s.store.Get(ctx, nodeInfo0.ID())
+	res, err := s.store.Get(ctx, nodeInfo0.Info.ID())
 	s.NoError(err)
 	s.Equal(nodeInfo0, res)
 
 	// wait for eviction
 	time.Sleep(ttl + 100*time.Millisecond)
-	_, err = s.store.Get(ctx, nodeInfo0.ID())
+	_, err = s.store.Get(ctx, nodeInfo0.Info.ID())
 	s.Error(err)
 	s.IsType(routing.ErrNodeNotFound{}, err)
 }
 
-func generateNodeInfo(t *testing.T, peerID string, engines ...string) models.NodeInfo {
+func generateNodeState(peerID string, engines ...string) models.NodeState {
+	return models.NodeState{
+		Info: generateNodeInfo(peerID, engines...),
+	}
+}
+
+func generateNodeInfo(peerID string, engines ...string) models.NodeInfo {
 	return models.NodeInfo{
 		NodeID:   peerID,
 		NodeType: models.NodeTypeCompute,
diff --git a/pkg/routing/kvstore/kvstore_test.go b/pkg/routing/kvstore/kvstore_test.go
index 78cc42a95b..1acffa1c36 100644
--- a/pkg/routing/kvstore/kvstore_test.go
+++ b/pkg/routing/kvstore/kvstore_test.go
@@ -11,10 +11,11 @@ import (
 	natsserver "github.com/nats-io/nats-server/v2/test"
 	"github.com/nats-io/nats.go"
 
+	"github.com/stretchr/testify/suite"
+
 	"github.com/bacalhau-project/bacalhau/pkg/models"
 	"github.com/bacalhau-project/bacalhau/pkg/routing"
 	"github.com/bacalhau-project/bacalhau/pkg/routing/kvstore"
-	"github.com/stretchr/testify/suite"
 )
 
 const TEST_PORT = 8369
@@ -57,19 +58,19 @@ func TestKVNodeInfoStoreSuite(t *testing.T) {
 
 func (s *KVNodeInfoStoreSuite) Test_Get() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
+	nodeInfo1 := generateNodeState(nodeIDs[1], models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
 	// test Get
-	res1, err := s.store.Get(ctx, nodeInfo0.ID())
+	res1, err := s.store.Get(ctx, nodeInfo0.Info.ID())
 	s.NoError(err)
-	s.Equal(nodeInfo0.ID(), res1.ID())
+	s.Equal(nodeInfo0.Info.ID(), res1.Info.ID())
 
-	res2, err := s.store.Get(ctx, nodeInfo1.ID())
+	res2, err := s.store.Get(ctx, nodeInfo1.Info.ID())
 	s.NoError(err)
-	s.Equal(nodeInfo1.ID(), res2.ID())
+	s.Equal(nodeInfo1.Info.ID(), res2.Info.ID())
 }
 
 func (s *KVNodeInfoStoreSuite) Test_GetNotFound() {
@@ -81,7 +82,7 @@ func (s *KVNodeInfoStoreSuite) Test_GetNotFound() {
 
 func (s *KVNodeInfoStoreSuite) Test_GetByPrefix_SingleMatch() {
 	ctx := context.Background()
-	nodeInfo := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
+	nodeInfo := generateNodeState(nodeIDs[0], models.EngineDocker)
 	s.NoError(s.store.Add(ctx, nodeInfo))
 
 	res, err := s.store.GetByPrefix(ctx, "QmdZQ7")
@@ -91,8 +92,8 @@ func (s *KVNodeInfoStoreSuite) Test_GetByPrefix_SingleMatch() {
 
 func (s *KVNodeInfoStoreSuite) Test_GetByPrefix_MultipleMatches() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
+	nodeInfo1 := generateNodeState(nodeIDs[1], models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
@@ -111,7 +112,7 @@ func (s *KVNodeInfoStoreSuite) Test_GetByPrefix_NoMatch_Empty() {
 func (s *KVNodeInfoStoreSuite) Test_GetByPrefix_NoMatch_NotEmpty() {
 	ctx := context.Background()
 
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[1], models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 
 	_, err := s.store.GetByPrefix(ctx, "nonexistent")
@@ -121,44 +122,44 @@ func (s *KVNodeInfoStoreSuite) Test_GetByPrefix_NoMatch_NotEmpty() {
 
 func (s *KVNodeInfoStoreSuite) Test_List() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
+	nodeInfo1 := generateNodeState(nodeIDs[1], models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
 	// test List
 	allNodeInfos, err := s.store.List(ctx)
 	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo0, nodeInfo1}, allNodeInfos)
+	s.ElementsMatch([]models.NodeState{nodeInfo0, nodeInfo1}, allNodeInfos)
 }
 
 func (s *KVNodeInfoStoreSuite) Test_ListWithFilters() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
+	nodeInfo1 := generateNodeState(nodeIDs[1], models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
 	// Match one record
-	filterPartialID := func(node models.NodeInfo) bool {
-		return strings.HasPrefix(node.ID(), string(nodeIDs[0][0:8]))
+	filterPartialID := func(node models.NodeState) bool {
+		return strings.HasPrefix(node.Info.ID(), string(nodeIDs[0][0:8]))
 	}
 	nodes, err := s.store.List(ctx, filterPartialID)
 	s.NoError(err)
 	s.Equal(1, len(nodes))
-	s.Equal(nodeIDs[0], nodes[0].ID())
+	s.Equal(nodeIDs[0], nodes[0].Info.ID())
 
 	// Match all records
-	filterPartialID = func(node models.NodeInfo) bool {
-		return strings.HasPrefix(node.ID(), "Qm")
+	filterPartialID = func(node models.NodeState) bool {
+		return strings.HasPrefix(node.Info.ID(), "Qm")
 	}
 	nodes, err = s.store.List(ctx, filterPartialID)
 	s.NoError(err)
 	s.Equal(2, len(nodes))
 
 	// Match no records
-	filterPartialID = func(node models.NodeInfo) bool {
-		return strings.HasPrefix(node.ID(), "XYZ")
+	filterPartialID = func(node models.NodeState) bool {
+		return strings.HasPrefix(node.Info.ID(), "XYZ")
 	}
 	nodes, err = s.store.List(ctx, filterPartialID)
 	s.NoError(err)
@@ -167,20 +168,20 @@ func (s *KVNodeInfoStoreSuite) Test_ListWithFilters() {
 
 func (s *KVNodeInfoStoreSuite) Test_Delete() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[1], models.EngineDocker, models.EngineWasm)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
+	nodeInfo1 := generateNodeState(nodeIDs[1], models.EngineDocker, models.EngineWasm)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
 	// delete first node
-	s.NoError(s.store.Delete(ctx, nodeInfo0.ID()))
+	s.NoError(s.store.Delete(ctx, nodeInfo0.Info.ID()))
 
 	nodes, err := s.store.List(ctx)
 	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, nodes)
+	s.ElementsMatch([]models.NodeState{nodeInfo1}, nodes)
 
 	// delete second node
-	s.NoError(s.store.Delete(ctx, nodeInfo1.ID()))
+	s.NoError(s.store.Delete(ctx, nodeInfo1.Info.ID()))
 
 	nodes, err = s.store.List(ctx)
 	s.NoError(err)
@@ -189,24 +190,30 @@ func (s *KVNodeInfoStoreSuite) Test_Delete() {
 
 func (s *KVNodeInfoStoreSuite) Test_Replace() {
 	ctx := context.Background()
-	nodeInfo0 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineDocker)
+	nodeInfo0 := generateNodeState(nodeIDs[0], models.EngineDocker)
 	s.NoError(s.store.Add(ctx, nodeInfo0))
 
-	nodeInfo1 := generateNodeInfo(s.T(), nodeIDs[0], models.EngineWasm)
-	nodeInfo1.NodeID = nodeInfo0.NodeID
+	nodeInfo1 := generateNodeState(nodeIDs[0], models.EngineWasm)
+	nodeInfo1.Info.NodeID = nodeInfo0.Info.NodeID
 	s.NoError(s.store.Add(ctx, nodeInfo1))
 
-	res, err := s.store.Get(ctx, nodeInfo0.ID())
+	res, err := s.store.Get(ctx, nodeInfo0.Info.ID())
 	s.NoError(err)
 	s.Equal(nodeInfo1, res)
 
 	// test List
 	allNodeInfos, err := s.store.List(ctx)
 	s.NoError(err)
-	s.ElementsMatch([]models.NodeInfo{nodeInfo1}, allNodeInfos)
+	s.ElementsMatch([]models.NodeState{nodeInfo1}, allNodeInfos)
+}
+
+func generateNodeState(peerID string, engines ...string) models.NodeState {
+	return models.NodeState{
+		Info: generateNodeInfo(peerID, engines...),
+	}
 }
 
-func generateNodeInfo(t *testing.T, peerID string, engines ...string) models.NodeInfo {
+func generateNodeInfo(peerID string, engines ...string) models.NodeInfo {
 	return models.NodeInfo{
 		NodeID:   peerID,
 		NodeType: models.NodeTypeCompute,
diff --git a/pkg/test/teststack/stack.go b/pkg/test/teststack/stack.go
index 2376cce3ec..f319db6d2c 100644
--- a/pkg/test/teststack/stack.go
+++ b/pkg/test/teststack/stack.go
@@ -111,7 +111,7 @@ func allNodesDiscovered(t testing.TB, stack *devstack.DevStack) bool {
 		}
 
 		expectedNodes := stack.GetNodeIds()
-		discoveredNodes, err := node.RequesterNode.NodeDiscoverer.ListNodes(ctx)
+		discoveredNodes, err := node.RequesterNode.NodeDiscoverer.List(ctx)
 		require.NoError(t, err)
 
 		if len(discoveredNodes) < len(expectedNodes) {
@@ -121,7 +121,7 @@ func allNodesDiscovered(t testing.TB, stack *devstack.DevStack) bool {
 
 		discoveredNodeIDs := make([]string, len(discoveredNodes))
 		for i, discoveredNode := range discoveredNodes {
-			discoveredNodeIDs[i] = discoveredNode.ID()
+			discoveredNodeIDs[i] = discoveredNode.Info.ID()
 		}
 		require.ElementsMatch(t, expectedNodes, discoveredNodeIDs)
 	}
diff --git a/pkg/test/utils/node/utils.go b/pkg/test/utils/node/utils.go
index ff47f5d6e0..de8fb24e0e 100644
--- a/pkg/test/utils/node/utils.go
+++ b/pkg/test/utils/node/utils.go
@@ -22,15 +22,15 @@ func WaitForNodeDiscovery(t *testing.T, requesterNode *node.Requester, expectedN
 	loggingGap := 1 * time.Second
 	waitLoggingUntil := time.Now().Add(loggingGap)
 
-	var nodeInfos []models.NodeInfo
+	var nodeInfos []models.NodeState
 	for time.Now().Before(waitUntil) {
 		var err error
 		nodeInfos, err = requesterNode.NodeInfoStore.List(ctx)
 		require.NoError(t, err)
 
 		if time.Now().After(waitLoggingUntil) {
-			t.Logf("connected to %d peers: %v", len(nodeInfos), logger.ToSliceStringer(nodeInfos, func(t models.NodeInfo) string {
-				return t.ID()
+			t.Logf("connected to %d peers: %v", len(nodeInfos), logger.ToSliceStringer(nodeInfos, func(t models.NodeState) string {
+				return t.Info.ID()
 			}))
 			waitLoggingUntil = time.Now().Add(loggingGap)
 		}
@@ -40,7 +40,7 @@ func WaitForNodeDiscovery(t *testing.T, requesterNode *node.Requester, expectedN
 		time.Sleep(waitGaps)
 	}
 	require.FailNowf(t, fmt.Sprintf("requester node didn't read all node infos even after waiting for %s", waitDuration),
-		"expected 4 node infos, got %d. %+v", len(nodeInfos), logger.ToSliceStringer(nodeInfos, func(t models.NodeInfo) string {
-			return t.ID()
+		"expected 4 node infos, got %d. %+v", len(nodeInfos), logger.ToSliceStringer(nodeInfos, func(t models.NodeState) string {
+			return t.Info.ID()
 		}))
 }
diff --git a/test/labels.sh b/test/labels.sh
index 8a9ccec96e..f0ccebeac5 100755
--- a/test/labels.sh
+++ b/test/labels.sh
@@ -15,11 +15,11 @@ run_test() {
     done
 
     assert_equal 1 $(jq -rcM length <<< $stdout)
-    assert_not_equal 0 $(jq -rcM '.[0].Labels | length' <<< $stdout)
-    assert_equal false $(jq -rcM '.[0].Labels["Operating-System"] == null' <<< $stdout)
-    assert_equal false $(jq -rcM '.[0].Labels["Architecture"] == null' <<< $stdout)
-    assert_equal value $(jq -rcM '.[0].Labels["key"]' <<< $stdout)
-    assert_equal $WORD $(jq -rcM '.[0].Labels["random"]' <<< $stdout)
+    assert_not_equal 0 $(jq -rcM '.[0].Info.Labels | length' <<< $stdout)
+    assert_equal false $(jq -rcM '.[0].Info.Labels["Operating-System"] == null' <<< $stdout)
+    assert_equal false $(jq -rcM '.[0].Info.Labels["Architecture"] == null' <<< $stdout)
+    assert_equal value $(jq -rcM '.[0].Info.Labels["key"]' <<< $stdout)
+    assert_equal $WORD $(jq -rcM '.[0].Info.Labels["random"]' <<< $stdout)
 }
 
 testcase_receive_labels_about_requester_node_for_nats() {
@@ -32,7 +32,7 @@ testcase_receive_extra_labels_about_compute_node_for_nats() {
     subject bacalhau config set node.network.type nats
     assert_equal 0 $status
     run_test requester,compute
-    assert_equal false $(jq -rcM '.[0].Labels["git-lfs"] == null' <<< $stdout)
+    assert_equal false $(jq -rcM '.[0].Info.Labels["git-lfs"] == null' <<< $stdout)
 }
 
 testcase_receive_labels_about_requester_node_for_libp2p() {
@@ -45,5 +45,5 @@ testcase_receive_extra_labels_about_compute_node_for_libp2p() {
     subject bacalhau config set node.network.type libp2p
     assert_equal 0 $status
     run_test requester,compute
-    assert_equal false $(jq -rcM '.[0].Labels["git-lfs"] == null' <<< $stdout)
+    assert_equal false $(jq -rcM '.[0].Info.Labels["git-lfs"] == null' <<< $stdout)
 }