diff --git a/.changelog/3038.breaking.md b/.changelog/3038.breaking.md new file mode 100644 index 00000000000..352cb322382 --- /dev/null +++ b/.changelog/3038.breaking.md @@ -0,0 +1 @@ +go/registry: Make TLS address required for RoleConsensusRPC diff --git a/.changelog/3038.feature.1.md b/.changelog/3038.feature.1.md new file mode 100644 index 00000000000..b96c836899f --- /dev/null +++ b/.changelog/3038.feature.1.md @@ -0,0 +1,5 @@ +go/control: Add registration status to node status + +This updates the response returned by the `GetStatus` method exposed by the +node controller service to include a `Registration` field that contains +information about the node's current registration. diff --git a/.changelog/3038.feature.2.md b/.changelog/3038.feature.2.md new file mode 100644 index 00000000000..848fd4ed9d5 --- /dev/null +++ b/.changelog/3038.feature.2.md @@ -0,0 +1 @@ +go/consensus: Add IsValidator to reported node status diff --git a/.changelog/3038.feature.3.md b/.changelog/3038.feature.3.md new file mode 100644 index 00000000000..568643224b6 --- /dev/null +++ b/.changelog/3038.feature.3.md @@ -0,0 +1,6 @@ +go/control: Add identity status to node status + +This updates the response returned by the `GetStatus` method exposed by the +node controller service to include an `Identity` field that contains +information about the public keys used to identify a node in different +contexts. diff --git a/go/consensus/api/api.go b/go/consensus/api/api.go index edd76f1491f..c43c0cc0694 100644 --- a/go/consensus/api/api.go +++ b/go/consensus/api/api.go @@ -124,6 +124,9 @@ type Status struct { GenesisHeight int64 `json:"genesis_height"` // GenesisHash is the hash of the genesis block. GenesisHash []byte `json:"genesis_hash"` + + // IsValidator returns whether the current node is part of the validator set. + IsValidator bool `json:"is_validator"` } // Backend is an interface that a consensus backend must provide. diff --git a/go/consensus/tendermint/tendermint.go b/go/consensus/tendermint/tendermint.go index 29a02e784a1..d669d7076ac 100644 --- a/go/consensus/tendermint/tendermint.go +++ b/go/consensus/tendermint/tendermint.go @@ -26,6 +26,7 @@ import ( tmproxy "github.com/tendermint/tendermint/proxy" tmcli "github.com/tendermint/tendermint/rpc/client/local" tmrpctypes "github.com/tendermint/tendermint/rpc/core/types" + tmstate "github.com/tendermint/tendermint/state" tmtypes "github.com/tendermint/tendermint/types" tmdb "github.com/tendermint/tm-db" @@ -719,38 +720,57 @@ func (t *tendermintService) GetTransactions(ctx context.Context, height int64) ( } func (t *tendermintService) GetStatus(ctx context.Context) (*consensusAPI.Status, error) { + status := &consensusAPI.Status{ + ConsensusVersion: version.ConsensusProtocol.String(), + Backend: api.BackendName, + } + // Genesis block is hardcoded as block 1, since tendermint doesn't have // a genesis block as such, but some external tooling expects there to be // one, so here we are. // This may soon change if the following tendermint issue gets fixed: // https://github.com/tendermint/tendermint/issues/2543 + status.GenesisHeight = 1 genBlk, err := t.GetBlock(ctx, 1) - if err != nil { - return nil, err + switch err { + case nil: + status.GenesisHash = genBlk.Hash + default: + // We may not be able to fetch the genesis block in case it has been pruned. } + // Latest block. latestBlk, err := t.GetBlock(ctx, consensusAPI.HeightLatest) - if err != nil { - return nil, err - } - + switch err { + case nil: + status.LatestHeight = latestBlk.Height + status.LatestHash = latestBlk.Hash + status.LatestTime = latestBlk.Time + case consensusAPI.ErrNoCommittedBlocks: + // No committed blocks yet. + default: + return nil, fmt.Errorf("failed to fetch current block: %w", err) + } + + // List of consensus peers. tmpeers := t.node.Switch().Peers().List() peers := make([]string, 0, len(tmpeers)) for _, tmpeer := range tmpeers { p := string(tmpeer.ID()) + "@" + tmpeer.RemoteAddr().String() peers = append(peers, p) } + status.NodePeers = peers - return &consensusAPI.Status{ - ConsensusVersion: version.ConsensusProtocol.String(), - Backend: api.BackendName, - NodePeers: peers, - LatestHeight: latestBlk.Height, - LatestHash: latestBlk.Hash, - LatestTime: latestBlk.Time, - GenesisHeight: 1, // See above for an explanation why this is 1. - GenesisHash: genBlk.Hash, - }, nil + // Check if the local node is in the validator set for the latest (uncommitted) block. + vals, err := tmstate.LoadValidators(t.stateDb, status.LatestHeight+1) + if err != nil { + return nil, fmt.Errorf("failed to load validator set: %w", err) + } + consensusPk := t.consensusSigner.Public() + consensusAddr := []byte(crypto.PublicKeyToTendermint(&consensusPk).Address()) + status.IsValidator = vals.HasAddress(consensusAddr) + + return status, nil } func (t *tendermintService) WatchBlocks(ctx context.Context) (<-chan *consensusAPI.Block, pubsub.ClosableSubscription, error) { diff --git a/go/control/api/api.go b/go/control/api/api.go index 4256ec8723d..094d2aff836 100644 --- a/go/control/api/api.go +++ b/go/control/api/api.go @@ -3,8 +3,12 @@ package api import ( "context" + "time" + "github.com/oasisprotocol/oasis-core/go/common/crypto/signature" "github.com/oasisprotocol/oasis-core/go/common/errors" + "github.com/oasisprotocol/oasis-core/go/common/identity" + "github.com/oasisprotocol/oasis-core/go/common/node" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" epochtime "github.com/oasisprotocol/oasis-core/go/epochtime/api" upgrade "github.com/oasisprotocol/oasis-core/go/upgrade/api" @@ -47,17 +51,56 @@ type Status struct { // SoftwareVersion is the oasis-node software version. SoftwareVersion string `json:"software_version"` + // Identity is the identity of the node. + Identity IdentityStatus `json:"identity"` + // Consensus is the status overview of the consensus layer. Consensus consensus.Status `json:"consensus"` + + // Registration is the node's registration status. + Registration RegistrationStatus `json:"registration"` +} + +// IdentityStatus is the current node identity status, listing all the public keys that identify +// this node in different contexts. +type IdentityStatus struct { + // Node is the node identity public key. + Node signature.PublicKey `json:"node"` + + // P2P is the public key used for p2p communication. + P2P signature.PublicKey `json:"p2p"` + + // Consensus is the consensus public key. + Consensus signature.PublicKey `json:"consensus"` + + // TLS is the public key used for TLS connections. + TLS signature.PublicKey `json:"tls"` +} + +// RegistrationStatus is the node registration status. +type RegistrationStatus struct { + // LastRegistration is the time of the last successful registration with the consensus registry + // service. In case the node did not successfully register yet, it will be the zero timestamp. + LastRegistration time.Time `json:"last_registration"` + + // Descriptor is the node descriptor that the node successfully registered with. In case the + // node did not successfully register yet, it will be nil. + Descriptor *node.Node `json:"descriptor,omitempty"` } -// ControlledNode is an interface the node presents for shutting itself down. +// ControlledNode is an internal interface that the controlled oasis-node must provide. type ControlledNode interface { // RequestShutdown is the method called by the control server to trigger node shutdown. RequestShutdown() (<-chan struct{}, error) // Ready returns a channel that is closed once node is ready. Ready() <-chan struct{} + + // GetIdentity returns the node's identity. + GetIdentity() *identity.Identity + + // GetRegistrationStatus returns the node's current registration status. + GetRegistrationStatus(ctx context.Context) (*RegistrationStatus, error) } // DebugModuleName is the module name for the debug controller service. diff --git a/go/control/control.go b/go/control/control.go index 6d6658888fe..f40d9292829 100644 --- a/go/control/control.go +++ b/go/control/control.go @@ -3,6 +3,7 @@ package control import ( "context" + "fmt" "github.com/oasisprotocol/oasis-core/go/common/version" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" @@ -83,12 +84,26 @@ func (c *nodeController) CancelUpgrade(ctx context.Context) error { func (c *nodeController) GetStatus(ctx context.Context) (*control.Status, error) { cs, err := c.consensus.GetStatus(ctx) if err != nil { - return nil, err + return nil, fmt.Errorf("failed to get consensus status: %w", err) } + rs, err := c.node.GetRegistrationStatus(ctx) + if err != nil { + return nil, fmt.Errorf("failed to get registration status: %w", err) + } + + ident := c.node.GetIdentity() + return &control.Status{ SoftwareVersion: version.SoftwareVersion, - Consensus: *cs, + Identity: control.IdentityStatus{ + Node: ident.NodeSigner.Public(), + P2P: ident.P2PSigner.Public(), + Consensus: ident.ConsensusSigner.Public(), + TLS: ident.GetTLSSigner().Public(), + }, + Consensus: *cs, + Registration: *rs, }, nil } diff --git a/go/oasis-node/cmd/debug/txsource/workload/queries.go b/go/oasis-node/cmd/debug/txsource/workload/queries.go index dacdb8cdea3..fcad946dafb 100644 --- a/go/oasis-node/cmd/debug/txsource/workload/queries.go +++ b/go/oasis-node/cmd/debug/txsource/workload/queries.go @@ -17,6 +17,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/node" "github.com/oasisprotocol/oasis-core/go/common/quantity" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" + control "github.com/oasisprotocol/oasis-core/go/control/api" epochtime "github.com/oasisprotocol/oasis-core/go/epochtime/api" registry "github.com/oasisprotocol/oasis-core/go/registry/api" runtimeClient "github.com/oasisprotocol/oasis-core/go/runtime/client/api" @@ -57,6 +58,7 @@ type queries struct { stakingParams staking.ConsensusParameters schedulerParams scheduler.ConsensusParameters + control control.NodeController staking staking.Backend consensus consensus.ClientBackend registry registry.Backend @@ -439,6 +441,19 @@ func (q *queries) doRuntimeQueries(ctx context.Context, rng *rand.Rand) error { return nil } +func (q *queries) doControlQueries(ctx context.Context, rng *rand.Rand) error { + q.logger.Debug("Doing node control queries") + + _, err := q.control.GetStatus(ctx) + if err != nil { + return fmt.Errorf("control.GetStatus error: %w", err) + } + + q.logger.Debug("Done node control queries") + + return nil +} + func (q *queries) doQueries(ctx context.Context, rng *rand.Rand) error { block, err := q.consensus.GetBlock(ctx, consensus.HeightLatest) if err != nil { @@ -470,6 +485,9 @@ func (q *queries) doQueries(ctx context.Context, rng *rand.Rand) error { "height_latest", block.Height, ) + if err := q.doControlQueries(ctx, rng); err != nil { + return fmt.Errorf("control queries error: %w", err) + } if err := q.doConsensusQueries(ctx, rng, height); err != nil { return fmt.Errorf("consensus queries error: %w", err) } @@ -499,6 +517,7 @@ func (q *queries) Run(gracefulExit context.Context, rng *rand.Rand, conn *grpc.C q.logger = logging.GetLogger("cmd/txsource/workload/queries") + q.control = control.NewNodeControllerClient(conn) q.consensus = cnsc q.registry = registry.NewRegistryClient(conn) q.runtime = runtimeClient.NewRuntimeClient(conn) diff --git a/go/oasis-node/cmd/node/control.go b/go/oasis-node/cmd/node/control.go new file mode 100644 index 00000000000..9406625e52e --- /dev/null +++ b/go/oasis-node/cmd/node/control.go @@ -0,0 +1,37 @@ +package node + +import ( + "context" + + "github.com/oasisprotocol/oasis-core/go/common/identity" + control "github.com/oasisprotocol/oasis-core/go/control/api" +) + +var _ control.ControlledNode = (*Node)(nil) + +// Implements control.ControlledNode. +func (n *Node) RequestShutdown() (<-chan struct{}, error) { + if err := n.RegistrationWorker.RequestDeregistration(); err != nil { + return nil, err + } + // This returns only the registration worker's event channel, + // otherwise the caller (usually the control grpc server) will only + // get notified once everything is already torn down - perhaps + // including the server. + return n.RegistrationWorker.Quit(), nil +} + +// Implements control.ControlledNode. +func (n *Node) Ready() <-chan struct{} { + return n.readyCh +} + +// Implements control.ControlledNode. +func (n *Node) GetIdentity() *identity.Identity { + return n.Identity +} + +// Implements control.ControlledNode. +func (n *Node) GetRegistrationStatus(ctx context.Context) (*control.RegistrationStatus, error) { + return n.RegistrationWorker.GetRegistrationStatus(ctx) +} diff --git a/go/oasis-node/cmd/node/node.go b/go/oasis-node/cmd/node/node.go index 0034cc2c655..0e0d6ed9286 100644 --- a/go/oasis-node/cmd/node/node.go +++ b/go/oasis-node/cmd/node/node.go @@ -69,12 +69,8 @@ import ( workerStorage "github.com/oasisprotocol/oasis-core/go/worker/storage" ) -var ( - _ controlAPI.ControlledNode = (*Node)(nil) - - // Flags has the configuration flags. - Flags = flag.NewFlagSet("", flag.ContinueOnError) -) +// Flags has the configuration flags. +var Flags = flag.NewFlagSet("", flag.ContinueOnError) const exportsSubDir = "exports" @@ -161,22 +157,6 @@ func (n *Node) Wait() { n.svcMgr.Wait() } -func (n *Node) RequestShutdown() (<-chan struct{}, error) { - if err := n.RegistrationWorker.RequestDeregistration(); err != nil { - return nil, err - } - // This returns only the registration worker's event channel, - // otherwise the caller (usually the control grpc server) will only - // get notified once everything is already torn down - perhaps - // including the server. - return n.RegistrationWorker.Quit(), nil -} - -// Ready returns the ready channel which gets closed once the node is ready. -func (n *Node) Ready() <-chan struct{} { - return n.readyCh -} - func (n *Node) waitReady(logger *logging.Logger) { if n.NodeController == nil { logger.Error("failed while waiting for node: node controller not initialized") diff --git a/go/oasis-test-runner/scenario/e2e/early_query.go b/go/oasis-test-runner/scenario/e2e/early_query.go index a8fe7f81b2e..8fbb715692d 100644 --- a/go/oasis-test-runner/scenario/e2e/early_query.go +++ b/go/oasis-test-runner/scenario/e2e/early_query.go @@ -76,5 +76,17 @@ func (sc *earlyQueryImpl) Run(childEnv *env.Env) error { return fmt.Errorf("GetTransactions query should fail with ErrNoCommittedBlocks (got: %s)", err) } + // GetStatus. + status, err := sc.net.Controller().GetStatus(ctx) + if err != nil { + return fmt.Errorf("failed to get status for node: %w", err) + } + if status.Consensus.LatestHeight != 0 { + return fmt.Errorf("node reports non-zero latest height before chain is initialized") + } + if !status.Consensus.IsValidator { + return fmt.Errorf("node does not report itself to be a validator at genesis") + } + return nil } diff --git a/go/oasis-test-runner/scenario/e2e/node_shutdown.go b/go/oasis-test-runner/scenario/e2e/node_shutdown.go index 97c2e8a3688..7e36a8804bd 100644 --- a/go/oasis-test-runner/scenario/e2e/node_shutdown.go +++ b/go/oasis-test-runner/scenario/e2e/node_shutdown.go @@ -50,7 +50,7 @@ func (sc *nodeShutdownImpl) Run(childEnv *env.Env) error { return err } - sc.logger.Info("requesting node shutdown") + sc.logger.Info("waiting for the node to become ready") computeWorker := sc.runtimeImpl.net.ComputeWorkers()[0] // Wait for the node to be ready since we didn't wait for any clients. @@ -58,10 +58,20 @@ func (sc *nodeShutdownImpl) Run(childEnv *env.Env) error { if err != nil { return err } - if err = nodeCtrl.WaitSync(context.Background()); err != nil { + if err = nodeCtrl.WaitReady(context.Background()); err != nil { return err } + // Make sure that the GetStatus endpoint returns sensible values. + status, err := nodeCtrl.GetStatus(context.Background()) + if err != nil { + return fmt.Errorf("failed to get status for node: %w", err) + } + if status.Registration.Descriptor == nil { + return fmt.Errorf("node has not registered") + } + + sc.logger.Info("requesting node shutdown") args := []string{ "control", "shutdown", "--log.level", "debug", diff --git a/go/registry/api/api.go b/go/registry/api/api.go index e284cf7d262..0721fc57392 100644 --- a/go/registry/api/api.go +++ b/go/registry/api/api.go @@ -166,9 +166,10 @@ var ( ConsensusAddressRequiredRoles = node.RoleValidator // TLSAddressRequiredRoles are the Node roles that require TLS Address. - TLSAddressRequiredRoles = (node.RoleComputeWorker | + TLSAddressRequiredRoles = node.RoleComputeWorker | node.RoleStorageWorker | - node.RoleKeyManager) + node.RoleKeyManager | + node.RoleConsensusRPC // P2PAddressRequiredRoles are the Node roles that require P2P Address. P2PAddressRequiredRoles = node.RoleComputeWorker diff --git a/go/worker/registration/worker.go b/go/worker/registration/worker.go index da2b034e1f3..9e50461ecf9 100644 --- a/go/worker/registration/worker.go +++ b/go/worker/registration/worker.go @@ -22,6 +22,7 @@ import ( "github.com/oasisprotocol/oasis-core/go/common/node" "github.com/oasisprotocol/oasis-core/go/common/persistent" consensus "github.com/oasisprotocol/oasis-core/go/consensus/api" + control "github.com/oasisprotocol/oasis-core/go/control/api" epochtime "github.com/oasisprotocol/oasis-core/go/epochtime/api" "github.com/oasisprotocol/oasis-core/go/oasis-node/cmd/common/flags" registry "github.com/oasisprotocol/oasis-core/go/registry/api" @@ -169,6 +170,8 @@ type Worker struct { // nolint: maligned roleProviders []*roleProvider registerCh chan struct{} + + status control.RegistrationStatus } // DebugForceallowUnroutableAddresses allows unroutable addresses. @@ -461,6 +464,16 @@ func (w *Worker) registrationStopped() { } } +// GetRegistrationStatus returns the node's current registration status. +func (w *Worker) GetRegistrationStatus(ctx context.Context) (*control.RegistrationStatus, error) { + w.RLock() + defer w.RUnlock() + + status := new(control.RegistrationStatus) + *status = w.status + return status, nil +} + // InitialRegistrationCh returns the initial registration channel. func (w *Worker) InitialRegistrationCh() chan struct{} { return w.initialRegCh @@ -704,6 +717,12 @@ func (w *Worker) registerNode(epoch epochtime.EpochTime, hook RegisterNodeHook) return err } + // Update the registration status on successful registration. + w.RLock() + w.status.LastRegistration = time.Now() + w.status.Descriptor = &nodeDesc + w.RUnlock() + w.logger.Info("node registered with the registry") return nil }