From 8b99d3b4e17ed87857a5b37ba81af0a1c14262d3 Mon Sep 17 00:00:00 2001 From: Jernej Kos Date: Fri, 15 May 2020 14:10:46 +0200 Subject: [PATCH] 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. --- .changelog/2581.feature.md | 9 ++ go/common/entity/entity.go | 44 +++++++- go/common/node/node.go | 40 ++++++- .../tendermint/apps/keymanager/genesis.go | 2 +- .../tendermint/apps/registry/genesis.go | 2 +- .../apps/registry/state/state_test.go | 3 +- .../tendermint/apps/registry/transactions.go | 5 +- .../tendermint/apps/staking/slashing_test.go | 5 +- go/genesis/genesis_test.go | 26 +++-- go/oasis-node/cmd/debug/byzantine/registry.go | 7 +- .../debug/txsource/workload/registration.go | 19 ++-- go/oasis-node/cmd/registry/entity/entity.go | 1 + go/oasis-node/cmd/registry/node/node.go | 7 +- go/oasis-node/cmd/registry/runtime/runtime.go | 11 +- go/oasis-node/node_test.go | 1 + go/oasis-test-runner/oasis/runtime.go | 19 ++-- .../scenario/e2e/registry_cli.go | 12 +- go/registry/api/api.go | 25 ++++- go/registry/api/runtime.go | 48 +++++++- go/registry/api/sanity_check.go | 7 +- go/registry/tests/tester.go | 105 +++++++++++++----- go/roothash/api/commitment/pool_test.go | 33 +++--- go/worker/registration/worker.go | 7 +- 23 files changed, 331 insertions(+), 107 deletions(-) create mode 100644 .changelog/2581.feature.md 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,