diff --git a/.changelog/2581.feature.md b/.changelog/2581.feature.md new file mode 100644 index 00000000000..a1d11195570 --- /dev/null +++ b/.changelog/2581.feature.md @@ -0,0 +1,9 @@ +Version signed entity, node and runtime descriptors + +This introduces a DescriptorVersion field to all entity, node and runtime +descriptors to support future updates and handling of legacy descriptors at +genesis. + +All new registrations only accept the latest version while initializing from +genesis is also allowed with an older version to support a dump/restore +upgrade. diff --git a/go/common/entity/entity.go b/go/common/entity/entity.go index de54d24e107..9ac3f13a5ca 100644 --- a/go/common/entity/entity.go +++ b/go/common/entity/entity.go @@ -28,9 +28,24 @@ var ( _ prettyprint.PrettyPrinter = (*SignedEntity)(nil) ) +const ( + // LatestEntityDescriptorVersion is the latest entity descriptor version that should be used for + // all new descriptors. Using earlier versions may be rejected. + LatestEntityDescriptorVersion = 1 + + // Minimum and maximum descriptor versions that are allowed. + minEntityDescriptorVersion = 0 + maxEntityDescriptorVersion = LatestEntityDescriptorVersion +) + // Entity represents an entity that controls one or more Nodes and or // services. -type Entity struct { +type Entity struct { // nolint: maligned + // DescriptorVersion is the entity descriptor version. + // + // It should be bumped whenever breaking changes are made to the descriptor. + DescriptorVersion uint16 `json:"v,omitempty"` + // ID is the public key identifying the entity. ID signature.PublicKey `json:"id"` @@ -44,6 +59,29 @@ type Entity struct { AllowEntitySignedNodes bool `json:"allow_entity_signed_nodes"` } +// ValidateBasic performs basic descriptor validity checks. +func (e *Entity) ValidateBasic(strictVersion bool) error { + switch strictVersion { + case true: + // Only the latest version is allowed. + if e.DescriptorVersion != LatestEntityDescriptorVersion { + return fmt.Errorf("invalid entity descriptor version (expected: %d got: %d)", + LatestEntityDescriptorVersion, + e.DescriptorVersion, + ) + } + case false: + // A range of versions is allowed. + if e.DescriptorVersion < minEntityDescriptorVersion || e.DescriptorVersion > maxEntityDescriptorVersion { + return fmt.Errorf("invalid entity descriptor version (min: %d max: %d)", + minEntityDescriptorVersion, + maxEntityDescriptorVersion, + ) + } + } + return nil +} + // String returns a string representation of itself. func (e Entity) String() string { return "" @@ -109,7 +147,8 @@ func Generate(baseDir string, signerFactory signature.SignerFactory, template *E return nil, nil, err } ent := &Entity{ - ID: signer.Public(), + DescriptorVersion: LatestEntityDescriptorVersion, + ID: signer.Public(), } if template != nil { ent.Nodes = template.Nodes @@ -166,6 +205,7 @@ func SignEntity(signer signature.Signer, context signature.Context, entity *Enti func init() { testEntitySigner = memorySigner.NewTestSigner("ekiden test entity key seed") + testEntity.DescriptorVersion = LatestEntityDescriptorVersion testEntity.ID = testEntitySigner.Public() testEntity.AllowEntitySignedNodes = true } diff --git a/go/common/node/node.go b/go/common/node/node.go index 338383f3330..ab85135cded 100644 --- a/go/common/node/node.go +++ b/go/common/node/node.go @@ -35,8 +35,23 @@ var ( _ prettyprint.PrettyPrinter = (*MultiSignedNode)(nil) ) +const ( + // LatestNodeDescriptorVersion is the latest node descriptor version that should be used for all + // new descriptors. Using earlier versions may be rejected. + LatestNodeDescriptorVersion = 1 + + // Minimum and maximum descriptor versions that are allowed. + minNodeDescriptorVersion = 0 + maxNodeDescriptorVersion = LatestNodeDescriptorVersion +) + // Node represents public connectivity information about an Oasis node. -type Node struct { +type Node struct { // nolint: maligned + // DescriptorVersion is the node descriptor version. + // + // It should be bumped whenever breaking changes are made to the descriptor. + DescriptorVersion uint16 `json:"v,omitempty"` + // ID is the public key identifying the node. ID signature.PublicKey `json:"id"` @@ -116,6 +131,29 @@ func (m RolesMask) String() string { return strings.Join(ret, ",") } +// ValidateBasic performs basic descriptor validity checks. +func (n *Node) ValidateBasic(strictVersion bool) error { + switch strictVersion { + case true: + // Only the latest version is allowed. + if n.DescriptorVersion != LatestNodeDescriptorVersion { + return fmt.Errorf("invalid node descriptor version (expected: %d got: %d)", + LatestNodeDescriptorVersion, + n.DescriptorVersion, + ) + } + case false: + // A range of versions is allowed. + if n.DescriptorVersion < minNodeDescriptorVersion || n.DescriptorVersion > maxNodeDescriptorVersion { + return fmt.Errorf("invalid node descriptor version (min: %d max: %d)", + minNodeDescriptorVersion, + maxNodeDescriptorVersion, + ) + } + } + return nil +} + // AddRoles adds the Node roles func (n *Node) AddRoles(r RolesMask) { n.Roles |= r diff --git a/go/consensus/tendermint/apps/keymanager/genesis.go b/go/consensus/tendermint/apps/keymanager/genesis.go index 97839c30507..9509e916225 100644 --- a/go/consensus/tendermint/apps/keymanager/genesis.go +++ b/go/consensus/tendermint/apps/keymanager/genesis.go @@ -31,7 +31,7 @@ func (app *keymanagerApplication) InitChain(ctx *tmapi.Context, request types.Re regSt := doc.Registry rtMap := make(map[common.Namespace]*registry.Runtime) for _, v := range regSt.Runtimes { - rt, err := registry.VerifyRegisterRuntimeArgs(®St.Parameters, ctx.Logger(), v, true) + rt, err := registry.VerifyRegisterRuntimeArgs(®St.Parameters, ctx.Logger(), v, true, false) if err != nil { ctx.Logger().Error("InitChain: Invalid runtime", "err", err, diff --git a/go/consensus/tendermint/apps/registry/genesis.go b/go/consensus/tendermint/apps/registry/genesis.go index 3aea74b644c..1fe3c6ea467 100644 --- a/go/consensus/tendermint/apps/registry/genesis.go +++ b/go/consensus/tendermint/apps/registry/genesis.go @@ -49,7 +49,7 @@ func (app *registryApplication) InitChain(ctx *abciAPI.Context, request types.Re if v == nil { return fmt.Errorf("registry: genesis runtime index %d is nil", i) } - rt, err := registry.VerifyRegisterRuntimeArgs(&st.Parameters, ctx.Logger(), v, ctx.IsInitChain()) + rt, err := registry.VerifyRegisterRuntimeArgs(&st.Parameters, ctx.Logger(), v, ctx.IsInitChain(), false) if err != nil { return err } diff --git a/go/consensus/tendermint/apps/registry/state/state_test.go b/go/consensus/tendermint/apps/registry/state/state_test.go index ac68d4cf916..43b074b1c57 100644 --- a/go/consensus/tendermint/apps/registry/state/state_test.go +++ b/go/consensus/tendermint/apps/registry/state/state_test.go @@ -40,7 +40,8 @@ func TestNodeUpdate(t *testing.T) { // Create a new node. n := node.Node{ - ID: nodeSigner.Public(), + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: nodeSigner.Public(), P2P: node.P2PInfo{ ID: p2pSigner1.Public(), }, diff --git a/go/consensus/tendermint/apps/registry/transactions.go b/go/consensus/tendermint/apps/registry/transactions.go index b8e1bd9b53c..09fa2f06384 100644 --- a/go/consensus/tendermint/apps/registry/transactions.go +++ b/go/consensus/tendermint/apps/registry/transactions.go @@ -18,7 +18,7 @@ func (app *registryApplication) registerEntity( state *registryState.MutableState, sigEnt *entity.SignedEntity, ) error { - ent, err := registry.VerifyRegisterEntityArgs(ctx.Logger(), sigEnt, ctx.IsInitChain()) + ent, err := registry.VerifyRegisterEntityArgs(ctx.Logger(), sigEnt, ctx.IsInitChain(), false) if err != nil { return err } @@ -198,6 +198,7 @@ func (app *registryApplication) registerNode( // nolint: gocyclo untrustedEntity, ctx.Now(), ctx.IsInitChain(), + false, epoch, state, state, @@ -503,7 +504,7 @@ func (app *registryApplication) registerRuntime( // nolint: gocyclo return registry.ErrForbidden } - rt, err := registry.VerifyRegisterRuntimeArgs(params, ctx.Logger(), sigRt, ctx.IsInitChain()) + rt, err := registry.VerifyRegisterRuntimeArgs(params, ctx.Logger(), sigRt, ctx.IsInitChain(), false) if err != nil { return err } diff --git a/go/consensus/tendermint/apps/staking/slashing_test.go b/go/consensus/tendermint/apps/staking/slashing_test.go index 5e248a9270a..b12716836e5 100644 --- a/go/consensus/tendermint/apps/staking/slashing_test.go +++ b/go/consensus/tendermint/apps/staking/slashing_test.go @@ -50,8 +50,9 @@ func TestOnEvidenceDoubleSign(t *testing.T) { // Add node. nodeSigner := memorySigner.NewTestSigner("node test signer") nod := &node.Node{ - ID: nodeSigner.Public(), - EntityID: ent.ID, + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: nodeSigner.Public(), + EntityID: ent.ID, Consensus: node.ConsensusInfo{ ID: consensusID, }, diff --git a/go/genesis/genesis_test.go b/go/genesis/genesis_test.go index d6764d4ddc5..13de540b926 100644 --- a/go/genesis/genesis_test.go +++ b/go/genesis/genesis_test.go @@ -160,6 +160,7 @@ func TestGenesisSanityCheck(t *testing.T) { // Note that this test entity has no nodes by design, those will be added // later by various tests. testEntity := &entity.Entity{ + DescriptorVersion: entity.LatestEntityDescriptorVersion, ID: validPK, AllowEntitySignedNodes: true, } @@ -167,9 +168,10 @@ func TestGenesisSanityCheck(t *testing.T) { kmRuntimeID := hex2ns("4000000000000000ffffffffffffffffffffffffffffffffffffffffffffffff", false) testKMRuntime := ®istry.Runtime{ - ID: kmRuntimeID, - EntityID: testEntity.ID, - Kind: registry.KindKeyManager, + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, + ID: kmRuntimeID, + EntityID: testEntity.ID, + Kind: registry.KindKeyManager, AdmissionPolicy: registry.RuntimeAdmissionPolicy{ EntityWhitelist: ®istry.EntityWhitelistRuntimeAdmissionPolicy{ Entities: map[signature.PublicKey]bool{ @@ -182,10 +184,11 @@ func TestGenesisSanityCheck(t *testing.T) { testRuntimeID := hex2ns("0000000000000000000000000000000000000000000000000000000000000001", false) testRuntime := ®istry.Runtime{ - ID: testRuntimeID, - EntityID: testEntity.ID, - Kind: registry.KindCompute, - KeyManager: &testKMRuntime.ID, + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, + ID: testRuntimeID, + EntityID: testEntity.ID, + Kind: registry.KindCompute, + KeyManager: &testKMRuntime.ID, Executor: registry.ExecutorParameters{ GroupSize: 1, RoundTimeout: 1 * time.Second, @@ -223,10 +226,11 @@ func TestGenesisSanityCheck(t *testing.T) { var testAddress node.Address _ = testAddress.UnmarshalText([]byte("127.0.0.1:1234")) testNode := &node.Node{ - ID: nodeSigner.Public(), - EntityID: testEntity.ID, - Expiration: 10, - Roles: node.RoleValidator, + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: nodeSigner.Public(), + EntityID: testEntity.ID, + Expiration: 10, + Roles: node.RoleValidator, Committee: node.CommitteeInfo{ Certificate: dummyCert.Certificate[0], Addresses: []node.CommitteeAddress{ diff --git a/go/oasis-node/cmd/debug/byzantine/registry.go b/go/oasis-node/cmd/debug/byzantine/registry.go index 19185efa1fd..86f67445e54 100644 --- a/go/oasis-node/cmd/debug/byzantine/registry.go +++ b/go/oasis-node/cmd/debug/byzantine/registry.go @@ -42,9 +42,10 @@ func registryRegisterNode(svc service.TendermintService, id *identity.Identity, } nodeDesc := &node.Node{ - ID: id.NodeSigner.Public(), - EntityID: entityID, - Expiration: 1000, + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: id.NodeSigner.Public(), + EntityID: entityID, + Expiration: 1000, Committee: node.CommitteeInfo{ Certificate: id.GetTLSCertificate().Certificate[0], Addresses: committeeAddresses, diff --git a/go/oasis-node/cmd/debug/txsource/workload/registration.go b/go/oasis-node/cmd/debug/txsource/workload/registration.go index 848762db24a..71023edc7fa 100644 --- a/go/oasis-node/cmd/debug/txsource/workload/registration.go +++ b/go/oasis-node/cmd/debug/txsource/workload/registration.go @@ -42,9 +42,10 @@ type registration struct { func getRuntime(entityID signature.PublicKey, id common.Namespace) *registry.Runtime { rt := ®istry.Runtime{ - ID: id, - EntityID: entityID, - Kind: registry.KindCompute, + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, + ID: id, + EntityID: entityID, + Kind: registry.KindCompute, Executor: registry.ExecutorParameters{ GroupSize: 1, RoundTimeout: 1 * time.Second, @@ -94,10 +95,11 @@ func getNodeDesc(rng *rand.Rand, nodeIdentity *identity.Identity, entityID signa } nodeDesc := node.Node{ - ID: nodeIdentity.NodeSigner.Public(), - EntityID: entityID, - Expiration: 0, - Roles: availableRoles[rng.Intn(len(availableRoles))], + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: nodeIdentity.NodeSigner.Public(), + EntityID: entityID, + Expiration: 0, + Roles: availableRoles[rng.Intn(len(availableRoles))], Committee: node.CommitteeInfo{ Certificate: nodeIdentity.GetTLSCertificate().Certificate[0], Addresses: []node.CommitteeAddress{ @@ -204,7 +206,8 @@ func (r *registration) Run( // nolint: gocyclo } ent := &entity.Entity{ - ID: entityAccs[i].signer.Public(), + DescriptorVersion: entity.LatestEntityDescriptorVersion, + ID: entityAccs[i].signer.Public(), } // Generate entity node identities. diff --git a/go/oasis-node/cmd/registry/entity/entity.go b/go/oasis-node/cmd/registry/entity/entity.go index e014e733056..27a4d2a53c1 100644 --- a/go/oasis-node/cmd/registry/entity/entity.go +++ b/go/oasis-node/cmd/registry/entity/entity.go @@ -374,6 +374,7 @@ func loadOrGenerateEntity(dataDir string, generate bool) (*entity.Entity, signat if generate { template := &entity.Entity{ + DescriptorVersion: entity.LatestEntityDescriptorVersion, AllowEntitySignedNodes: viper.GetBool(cfgAllowEntitySignedNodes), } diff --git a/go/oasis-node/cmd/registry/node/node.go b/go/oasis-node/cmd/registry/node/node.go index fca3f77e7d8..97e1dbf5b8b 100644 --- a/go/oasis-node/cmd/registry/node/node.go +++ b/go/oasis-node/cmd/registry/node/node.go @@ -176,9 +176,10 @@ func doInit(cmd *cobra.Command, args []string) { // nolint: gocyclo } n := &node.Node{ - ID: nodeIdentity.NodeSigner.Public(), - EntityID: entityID, - Expiration: viper.GetUint64(CfgExpiration), + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: nodeIdentity.NodeSigner.Public(), + EntityID: entityID, + Expiration: viper.GetUint64(CfgExpiration), Committee: node.CommitteeInfo{ Certificate: nodeIdentity.GetTLSCertificate().Certificate[0], NextCertificate: nextCert, diff --git a/go/oasis-node/cmd/registry/runtime/runtime.go b/go/oasis-node/cmd/registry/runtime/runtime.go index a919adbffda..f499d1e5c0b 100644 --- a/go/oasis-node/cmd/registry/runtime/runtime.go +++ b/go/oasis-node/cmd/registry/runtime/runtime.go @@ -353,11 +353,12 @@ func runtimeFromFlags() (*registry.Runtime, signature.Signer, error) { } rt := ®istry.Runtime{ - ID: id, - EntityID: signer.Public(), - Genesis: gen, - Kind: kind, - TEEHardware: teeHardware, + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, + ID: id, + EntityID: signer.Public(), + Genesis: gen, + Kind: kind, + TEEHardware: teeHardware, Version: registry.VersionInfo{ Version: version.FromU64(viper.GetUint64(CfgVersion)), }, diff --git a/go/oasis-node/node_test.go b/go/oasis-node/node_test.go index 6aee2cd3409..fb72476122d 100644 --- a/go/oasis-node/node_test.go +++ b/go/oasis-node/node_test.go @@ -79,6 +79,7 @@ var ( } testRuntime = ®istry.Runtime{ + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, // ID: default value, // EntityID: test entity, Kind: registry.KindCompute, diff --git a/go/oasis-test-runner/oasis/runtime.go b/go/oasis-test-runner/oasis/runtime.go index 4aad76d5b40..0a9379f8a00 100644 --- a/go/oasis-test-runner/oasis/runtime.go +++ b/go/oasis-test-runner/oasis/runtime.go @@ -115,15 +115,16 @@ func (rt *Runtime) GetGenesisStatePath() string { // NewRuntime provisions a new runtime and adds it to the network. func (net *Network) NewRuntime(cfg *RuntimeCfg) (*Runtime, error) { descriptor := registry.Runtime{ - ID: cfg.ID, - EntityID: cfg.Entity.entity.ID, - Kind: cfg.Kind, - TEEHardware: cfg.TEEHardware, - Executor: cfg.Executor, - Merge: cfg.Merge, - TxnScheduler: cfg.TxnScheduler, - Storage: cfg.Storage, - AdmissionPolicy: cfg.AdmissionPolicy, + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, + ID: cfg.ID, + EntityID: cfg.Entity.entity.ID, + Kind: cfg.Kind, + TEEHardware: cfg.TEEHardware, + Executor: cfg.Executor, + Merge: cfg.Merge, + TxnScheduler: cfg.TxnScheduler, + Storage: cfg.Storage, + AdmissionPolicy: cfg.AdmissionPolicy, } rtDir, err := net.baseDir.NewSubDir("runtime-" + cfg.ID.String()) diff --git a/go/oasis-test-runner/scenario/e2e/registry_cli.go b/go/oasis-test-runner/scenario/e2e/registry_cli.go index 9e8820aea1c..7ec8fd8e8b5 100644 --- a/go/oasis-test-runner/scenario/e2e/registry_cli.go +++ b/go/oasis-test-runner/scenario/e2e/registry_cli.go @@ -402,9 +402,10 @@ func (r *registryCLIImpl) newTestNode(entityID signature.PublicKey) (*node.Node, } testNode := node.Node{ - ID: signature.PublicKey{}, // ID is generated afterwards. - EntityID: entityID, - Expiration: 42, + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: signature.PublicKey{}, // ID is generated afterwards. + EntityID: entityID, + Expiration: 42, Committee: node.CommitteeInfo{ Certificate: []byte{}, // Certificate is generated afterwards. Addresses: testCommitteeAddresses, @@ -605,8 +606,9 @@ func (r *registryCLIImpl) testRuntime(childEnv *env.Env, cli *cli.Helpers) error return fmt.Errorf("TestEntity: %w", err) } testRuntime := registry.Runtime{ - EntityID: testEntity.ID, - Kind: registry.KindCompute, + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, + EntityID: testEntity.ID, + Kind: registry.KindCompute, Executor: registry.ExecutorParameters{ GroupSize: 1, GroupBackupSize: 2, diff --git a/go/registry/api/api.go b/go/registry/api/api.go index cd92d84b086..af043b208ce 100644 --- a/go/registry/api/api.go +++ b/go/registry/api/api.go @@ -340,7 +340,7 @@ type RuntimeLookup interface { } // VerifyRegisterEntityArgs verifies arguments for RegisterEntity. -func VerifyRegisterEntityArgs(logger *logging.Logger, sigEnt *entity.SignedEntity, isGenesis bool) (*entity.Entity, error) { +func VerifyRegisterEntityArgs(logger *logging.Logger, sigEnt *entity.SignedEntity, isGenesis bool, isSanityCheck bool) (*entity.Entity, error) { var ent entity.Entity if sigEnt == nil { return nil, ErrInvalidArgument @@ -368,6 +368,13 @@ func VerifyRegisterEntityArgs(logger *logging.Logger, sigEnt *entity.SignedEntit ) return nil, ErrInvalidArgument } + if err := ent.ValidateBasic(!isGenesis && !isSanityCheck); err != nil { + logger.Error("RegisterEntity: invalid entity descriptor", + "entity", ent, + "err", err, + ) + return nil, ErrInvalidArgument + } // Ensure the node list has no duplicates. nodesMap := make(map[signature.PublicKey]bool) @@ -402,6 +409,7 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo entity *entity.Entity, now time.Time, isGenesis bool, + isSanityCheck bool, epoch epochtime.EpochTime, runtimeLookup RuntimeLookup, nodeLookup NodeLookup, @@ -425,6 +433,13 @@ func VerifyRegisterNodeArgs( // nolint: gocyclo ) return nil, nil, ErrInvalidSignature } + if err := n.ValidateBasic(!isGenesis && !isSanityCheck); err != nil { + logger.Error("RegisterNode: invalid node descriptor", + "node", n, + "err", err, + ) + return nil, nil, ErrInvalidArgument + } // This should never happen, unless there's a bug in the caller. if !entity.ID.Equal(n.EntityID) { @@ -1039,6 +1054,7 @@ func VerifyRegisterRuntimeArgs( logger *logging.Logger, sigRt *SignedRuntime, isGenesis bool, + isSanityCheck bool, ) (*Runtime, error) { var rt Runtime if sigRt == nil { @@ -1067,6 +1083,13 @@ func VerifyRegisterRuntimeArgs( ) return nil, ErrInvalidArgument } + if err := rt.ValidateBasic(!isGenesis && !isSanityCheck); err != nil { + logger.Error("RegisterRuntime: invalid runtime descriptor", + "runtime", rt, + "err", err, + ) + return nil, ErrInvalidArgument + } switch rt.Kind { case KindCompute: diff --git a/go/registry/api/runtime.go b/go/registry/api/runtime.go index 750397aec9c..5f96db89da7 100644 --- a/go/registry/api/runtime.go +++ b/go/registry/api/runtime.go @@ -171,8 +171,23 @@ type RuntimeAdmissionPolicy struct { EntityWhitelist *EntityWhitelistRuntimeAdmissionPolicy `json:"entity_whitelist,omitempty"` } +const ( + // LatestRuntimeDescriptorVersion is the latest entity descriptor version that should be used + // for all new descriptors. Using earlier versions may be rejected. + LatestRuntimeDescriptorVersion = 1 + + // Minimum and maximum descriptor versions that are allowed. + minRuntimeDescriptorVersion = 0 + maxRuntimeDescriptorVersion = LatestRuntimeDescriptorVersion +) + // Runtime represents a runtime. -type Runtime struct { +type Runtime struct { // nolint: maligned + // DescriptorVersion is the runtime descriptor version. + // + // It should be bumped whenever breaking changes are made to the descriptor. + DescriptorVersion uint16 `json:"v,omitempty"` + // ID is a globally unique long term identifier of the runtime. ID common.Namespace `json:"id"` @@ -212,14 +227,37 @@ type Runtime struct { AdmissionPolicy RuntimeAdmissionPolicy `json:"admission_policy"` } +// ValidateBasic performs basic descriptor validity checks. +func (r *Runtime) ValidateBasic(strictVersion bool) error { + switch strictVersion { + case true: + // Only the latest version is allowed. + if r.DescriptorVersion != LatestRuntimeDescriptorVersion { + return fmt.Errorf("invalid runtime descriptor version (expected: %d got: %d)", + LatestRuntimeDescriptorVersion, + r.DescriptorVersion, + ) + } + case false: + // A range of versions is allowed. + if r.DescriptorVersion < minRuntimeDescriptorVersion || r.DescriptorVersion > maxRuntimeDescriptorVersion { + return fmt.Errorf("invalid runtime descriptor version (min: %d max: %d)", + minRuntimeDescriptorVersion, + maxRuntimeDescriptorVersion, + ) + } + } + return nil +} + // String returns a string representation of itself. -func (c Runtime) String() string { - return "" +func (r Runtime) String() string { + return "" } // IsCompute returns true iff the runtime is a generic compute runtime. -func (c *Runtime) IsCompute() bool { - return c.Kind == KindCompute +func (r *Runtime) IsCompute() bool { + return r.Kind == KindCompute } // SignedRuntime is a signed blob containing a CBOR-serialized Runtime. diff --git a/go/registry/api/sanity_check.go b/go/registry/api/sanity_check.go index 0de296565eb..29e9212686d 100644 --- a/go/registry/api/sanity_check.go +++ b/go/registry/api/sanity_check.go @@ -77,7 +77,7 @@ func (g *Genesis) SanityCheck( func SanityCheckEntities(logger *logging.Logger, entities []*entity.SignedEntity) (map[signature.PublicKey]*entity.Entity, error) { seenEntities := make(map[signature.PublicKey]*entity.Entity) for _, signedEnt := range entities { - entity, err := VerifyRegisterEntityArgs(logger, signedEnt, true) + entity, err := VerifyRegisterEntityArgs(logger, signedEnt, true, true) if err != nil { return nil, fmt.Errorf("entity sanity check failed: %w", err) } @@ -98,7 +98,7 @@ func SanityCheckRuntimes( // First go through all runtimes and perform general sanity checks. seenRuntimes := []*Runtime{} for _, signedRt := range runtimes { - rt, err := VerifyRegisterRuntimeArgs(params, logger, signedRt, isGenesis) + rt, err := VerifyRegisterRuntimeArgs(params, logger, signedRt, isGenesis, true) if err != nil { return nil, fmt.Errorf("runtime sanity check failed: %w", err) } @@ -107,7 +107,7 @@ func SanityCheckRuntimes( seenSuspendedRuntimes := []*Runtime{} for _, signedRt := range suspendedRuntimes { - rt, err := VerifyRegisterRuntimeArgs(params, logger, signedRt, isGenesis) + rt, err := VerifyRegisterRuntimeArgs(params, logger, signedRt, isGenesis, true) if err != nil { return nil, fmt.Errorf("runtime sanity check failed: %w", err) } @@ -174,6 +174,7 @@ func SanityCheckNodes( entity, time.Now(), isGenesis, + true, epoch, runtimesLookup, nodeLookup, diff --git a/go/registry/tests/tester.go b/go/registry/tests/tester.go index 2a8ea4b18de..1fed56544ea 100644 --- a/go/registry/tests/tester.go +++ b/go/registry/tests/tester.go @@ -78,7 +78,13 @@ func testRegistryEntityNodes( // nolint: gocyclo require := require.New(t) for _, v := range entities { - err = v.Register(consensus) + // First try registering invalid cases and make sure they fail. + for _, inv := range v.invalidBefore { + err = v.Register(consensus, inv.signed) + require.Error(err, inv.descr) + } + + err = v.Register(consensus, v.SignedRegistration) require.NoError(err, "RegisterEntity") select { @@ -612,11 +618,18 @@ type TestEntity struct { Signer signature.Signer SignedRegistration *entity.SignedEntity + + invalidBefore []*invalidEntityRegistration } -// Register attempts to register the entity. -func (ent *TestEntity) Register(consensus consensusAPI.Backend) error { - return consensusAPI.SignAndSubmitTx(context.Background(), consensus, ent.Signer, api.NewRegisterEntityTx(0, nil, ent.SignedRegistration)) +type invalidEntityRegistration struct { + descr string + signed *entity.SignedEntity +} + +// Register attempts to register an entity. +func (ent *TestEntity) Register(consensus consensusAPI.Backend, sigEnt *entity.SignedEntity) error { + return consensusAPI.SignAndSubmitTx(context.Background(), consensus, ent.Signer, api.NewRegisterEntityTx(0, nil, sigEnt)) } // Deregister attempts to deregister the entity. @@ -714,11 +727,12 @@ func (ent *TestEntity) NewTestNodes(nCompute int, nStorage int, idNonce []byte, } nod.Node = &node.Node{ - ID: nod.Signer.Public(), - EntityID: ent.Entity.ID, - Expiration: uint64(expiration), - Runtimes: runtimes, - Roles: role, + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: nod.Signer.Public(), + EntityID: ent.Entity.ID, + Expiration: uint64(expiration), + Runtimes: runtimes, + Roles: role, } addr := node.Address{ TCPAddr: net.TCPAddr{ @@ -975,11 +989,12 @@ func (ent *TestEntity) NewTestNodes(nCompute int, nStorage int, idNonce []byte, // Add another Re-Registration with different address field. nod.UpdatedNode = &node.Node{ - ID: nod.Signer.Public(), - EntityID: ent.Entity.ID, - Expiration: uint64(expiration), - Runtimes: runtimes, - Roles: role, + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: nod.Signer.Public(), + EntityID: ent.Entity.ID, + Expiration: uint64(expiration), + Runtimes: runtimes, + Roles: role, } addr = node.Address{ TCPAddr: net.TCPAddr{ @@ -1005,13 +1020,14 @@ func (ent *TestEntity) NewTestNodes(nCompute int, nStorage int, idNonce []byte, newRuntimes := append([]*node.Runtime(nil), runtimes...) newRuntimes = append(newRuntimes, &node.Runtime{ID: publicKeyToNamespace(testRuntimeSigner.Public(), false)}) newNode := &node.Node{ - ID: nod.Signer.Public(), - EntityID: ent.Entity.ID, - Expiration: uint64(expiration), - Runtimes: newRuntimes, - Roles: role, - P2P: nod.Node.P2P, - Committee: nod.Node.Committee, + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: nod.Signer.Public(), + EntityID: ent.Entity.ID, + Expiration: uint64(expiration), + Runtimes: newRuntimes, + Roles: role, + P2P: nod.Node.P2P, + Committee: nod.Node.Committee, } newNode.P2P.ID = invalidIdentity.P2PSigner.Public() newNode.Consensus.ID = invalidIdentity.ConsensusSigner.Public() @@ -1032,6 +1048,28 @@ func (ent *TestEntity) NewTestNodes(nCompute int, nStorage int, idNonce []byte, } nod.invalidReReg = append(nod.invalidReReg, invalid14) + // Add a registration with an old version. + invalid15 := &invalidNodeRegistration{ + descr: "Registering with an old descriptor should fail", + } + invNode15 := *nod.Node + invNode15.DescriptorVersion = 0 + invalid15.signed, err = node.MultiSignNode( + []signature.Signer{ + nodeIdentity.NodeSigner, + ent.Signer, + nodeIdentity.ConsensusSigner, + nodeIdentity.P2PSigner, + nodeIdentity.GetTLSSigner(), + }, + api.RegisterNodeSignatureContext, + &invNode15, + ) + if err != nil { + return nil, err + } + nod.invalidBefore = append(nod.invalidBefore, invalid15) + nodes = append(nodes, &nod) } @@ -1053,15 +1091,27 @@ func NewTestEntities(seed []byte, n int) ([]*TestEntity, error) { return nil, err } ent.Entity = &entity.Entity{ + DescriptorVersion: entity.LatestEntityDescriptorVersion, ID: ent.Signer.Public(), AllowEntitySignedNodes: true, } - signed, err := signature.SignSigned(ent.Signer, api.RegisterEntitySignatureContext, ent.Entity) + ent.SignedRegistration, err = entity.SignEntity(ent.Signer, api.RegisterEntitySignatureContext, ent.Entity) + if err != nil { + return nil, err + } + + // Add a registration with an old version. + invalid1 := &invalidEntityRegistration{ + descr: "Registering with an old descriptor should fail", + } + invEnt1 := *ent.Entity + invEnt1.DescriptorVersion = 0 + invalid1.signed, err = entity.SignEntity(ent.Signer, api.RegisterEntitySignatureContext, &invEnt1) if err != nil { return nil, err } - ent.SignedRegistration = &entity.SignedEntity{Signed: *signed} + ent.invalidBefore = append(ent.invalidBefore, invalid1) entities = append(entities, &ent) } @@ -1171,7 +1221,7 @@ func BulkPopulate(t *testing.T, backend api.Backend, consensus consensusAPI.Back entities, err := NewTestEntities(seed, 1) require.NoError(err, "NewTestEntities") entity := entities[0] - err = entity.Register(consensus) + err = entity.Register(consensus, entity.SignedRegistration) require.NoError(err, "RegisterEntity") select { case ev := <-entityCh: @@ -1322,9 +1372,10 @@ func NewTestRuntime(seed []byte, ent *TestEntity, isKeyManager bool) (*TestRunti var rt TestRuntime rt.Signer = ent.Signer rt.Runtime = &api.Runtime{ - ID: id, - EntityID: ent.Entity.ID, - Kind: api.KindCompute, + DescriptorVersion: api.LatestRuntimeDescriptorVersion, + ID: id, + EntityID: ent.Entity.ID, + Kind: api.KindCompute, Executor: api.ExecutorParameters{ GroupSize: 3, GroupBackupSize: 5, diff --git a/go/roothash/api/commitment/pool_test.go b/go/roothash/api/commitment/pool_test.go index 8e87a649866..1a12b3dd690 100644 --- a/go/roothash/api/commitment/pool_test.go +++ b/go/roothash/api/commitment/pool_test.go @@ -61,8 +61,9 @@ type staticNodeLookup struct { func (n *staticNodeLookup) Node(ctx context.Context, id signature.PublicKey) (*node.Node, error) { return &node.Node{ - ID: id, - Runtimes: []*node.Runtime{n.runtime}, + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: id, + Runtimes: []*node.Runtime{n.runtime}, }, nil } @@ -109,9 +110,10 @@ func TestPoolSingleCommitment(t *testing.T) { _ = rtID.UnmarshalHex("0000000000000000000000000000000000000000000000000000000000000000") rt := ®istry.Runtime{ - ID: rtID, - Kind: registry.KindCompute, - TEEHardware: node.TEEHardwareInvalid, + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, + ID: rtID, + Kind: registry.KindCompute, + TEEHardware: node.TEEHardwareInvalid, } // Generate a commitment signing key. @@ -211,9 +213,10 @@ func TestPoolSingleCommitmentTEE(t *testing.T) { _ = rtID.UnmarshalHex("0000000000000000000000000000000000000000000000000000000000000000") rt := ®istry.Runtime{ - ID: rtID, - Kind: registry.KindCompute, - TEEHardware: node.TEEHardwareIntelSGX, + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, + ID: rtID, + Kind: registry.KindCompute, + TEEHardware: node.TEEHardwareIntelSGX, } // Generate a commitment signing key. @@ -442,9 +445,10 @@ func TestPoolSerialization(t *testing.T) { _ = rtID.UnmarshalHex("0000000000000000000000000000000000000000000000000000000000000000") rt := ®istry.Runtime{ - ID: rtID, - Kind: registry.KindCompute, - TEEHardware: node.TEEHardwareInvalid, + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, + ID: rtID, + Kind: registry.KindCompute, + TEEHardware: node.TEEHardwareInvalid, } // Generate a commitment signing key. @@ -1088,9 +1092,10 @@ func generateMockCommittee(t *testing.T) ( _ = rtID.UnmarshalHex("0000000000000000000000000000000000000000000000000000000000000000") rt = ®istry.Runtime{ - ID: rtID, - Kind: registry.KindCompute, - TEEHardware: node.TEEHardwareInvalid, + DescriptorVersion: registry.LatestRuntimeDescriptorVersion, + ID: rtID, + Kind: registry.KindCompute, + TEEHardware: node.TEEHardwareInvalid, } // Generate commitment signing keys. diff --git a/go/worker/registration/worker.go b/go/worker/registration/worker.go index 311eb6598a6..988e5b08032 100644 --- a/go/worker/registration/worker.go +++ b/go/worker/registration/worker.go @@ -569,9 +569,10 @@ func (w *Worker) registerNode(epoch epochtime.EpochTime, hook RegisterNodeHook) } nodeDesc := node.Node{ - ID: identityPublic, - EntityID: w.entityID, - Expiration: uint64(epoch) + 2, + DescriptorVersion: node.LatestNodeDescriptorVersion, + ID: identityPublic, + EntityID: w.entityID, + Expiration: uint64(epoch) + 2, Committee: node.CommitteeInfo{ Certificate: w.identity.GetTLSCertificate().Certificate[0], NextCertificate: nextCert,