diff --git a/Makefile b/Makefile index d65d89637dd40574db7b61f5ee73ccc244ad6b20..7a1a72d8d62d73587bfe14eedcabe636c68404c2 100644 --- a/Makefile +++ b/Makefile @@ -26,4 +26,4 @@ update_master: master: clean update_master update build -release: clean update_release update build +release: clean update_release update build \ No newline at end of file diff --git a/README.md b/README.md index 3edddf47c3ca0fd020b67bab3713134a50d3ed67..19de47b5212c58b56e8054fcb100c4836baff899 100644 --- a/README.md +++ b/README.md @@ -16,8 +16,9 @@ logLevel: 1 logPath: "registration.log" # Path to the node topology permissioning info ndfOutputPath: "ndf.json" -# Batch size of each round -batchSize: 8 +# Minimum number of nodes to begin running rounds. this differs from the number of members +# in a team because some scheduling algorithms may require multiple teams worth of nodes at minimum +minimumNodes: 3 # UDB ID udbID: 1 @@ -34,11 +35,9 @@ dbPassword: "" dbName: "cmix_server" dbAddress: "" -# List of Node registration codes (in order of network placement) -registrationCodes: - - "1" - - "2" - - "3" +# Path to JSON file with list of Node registration codes (in order of network +# placement) +RegCodesFilePath: "regCodes.json" # List of client codes to be added to the database (for testing) clientRegCodes: @@ -64,5 +63,17 @@ groups: e2e: prime: "${e2e_prime}" generator: "${e2e_generator}" + +# Selection of scheduling algorithem to use. Options are: +# simple - Schedules multiple teams to maximize performance, does not randomly re-arrange teams, if only a single +# only scheduling a single team, will use numerical ordering data for AlphaNet +# secure - Schedules new teams randomly, has apropreate buffers to ensure unpredictability, designed for BetaNet +schedulingAlgorithm: "single" + +# Path to file with config for scheduling algorithem within the user directory +schedulingConfigPath: "schedulingConfig.json" + + + ``` diff --git a/cmd/control.go b/cmd/control.go deleted file mode 100644 index 4cdc6d85a2d0ade144fc383db55d4191496b4175..0000000000000000000000000000000000000000 --- a/cmd/control.go +++ /dev/null @@ -1,150 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2018 Privategrity Corporation / -// / -// All rights reserved. / -//////////////////////////////////////////////////////////////////////////////// - -// Handles control layer above the network state - -package cmd - -import ( - jww "github.com/spf13/jwalterweatherman" - pb "gitlab.com/elixxir/comms/mixmessages" - "gitlab.com/elixxir/crypto/signature" - "gitlab.com/elixxir/primitives/current" - "gitlab.com/elixxir/primitives/id" - "gitlab.com/elixxir/primitives/states" - "time" -) - -// Control thread for advancement of network state -func (m *RegistrationImpl) StateControl() { - s := m.State - for range s.Update { - - // Check whether the round state is ready to increment - nextState := states.Round(s.CurrentRound.State + 1) - numNodesInRound := uint32(len(s.CurrentRound.Topology)) - if *s.CurrentRound.NetworkStatus[nextState] == numNodesInRound { - // Increment the round state - err := m.incrementRoundState(nextState) - if err != nil { - // TODO: Error handling - jww.FATAL.Panicf("Unable to create next round: %+v", err) - } - } - - // Handle completion of a round - if s.GetCurrentRoundState() == states.COMPLETED { - // Create the new round - err := m.newRound(s.CurrentRound.GetTopology(), m.params.batchSize) - if err != nil { - // TODO: Error handling - jww.FATAL.Panicf("Unable to create next round: %+v", err) - } - } - } -} - -// Initiate the next round with a selection of nodes -func (m *RegistrationImpl) createNextRound() error { - // Build a topology (currently consisting of all nodes in network) - var topology []string - for _, node := range m.State.GetPartialNdf().Get().Nodes { - topology = append(topology, node.GetNodeId().String()) - } - - // Progress to the next round - return m.newRound(topology, m.params.batchSize) -} - -// Increments the state of the current round if needed -func (m *RegistrationImpl) incrementRoundState(state states.Round) error { - s := m.State - - // Handle state transitions - switch state { - case states.STANDBY: - s.CurrentRound.State = uint32(states.REALTIME) - // Handle timestamp edge case with realtime - s.CurrentRound.Timestamps[states.REALTIME] = uint64(time.Now().Add(2 * time.Second).Unix()) - case states.COMPLETED: - s.CurrentRound.State = uint32(states.COMPLETED) - s.CurrentRound.Timestamps[states.COMPLETED] = uint64(time.Now().Unix()) - default: - return nil - } - // Update current round state - s.CurrentUpdate += 1 - s.CurrentRound.UpdateID = uint64(s.CurrentUpdate) - - // Sign the new round object - err := signature.Sign(s.CurrentRound.RoundInfo, s.PrivateKey) - if err != nil { - return err - } - - // Insert update into the state tracker - return s.AddRoundUpdate(s.CurrentRound.RoundInfo) -} - -// Builds and inserts the next RoundInfo object into the internal state -func (m *RegistrationImpl) newRound(topology []string, batchSize uint32) error { - s := m.State - - // Build the new current round object - s.CurrentUpdate += 1 - s.CurrentRound.RoundInfo = &pb.RoundInfo{ - ID: uint64(s.RoundData.GetLastRoundID() + 1), - UpdateID: uint64(s.CurrentUpdate), - State: uint32(states.PRECOMPUTING), - BatchSize: batchSize, - Topology: topology, - Timestamps: make([]uint64, states.NUM_STATES), - } - s.CurrentRound.Timestamps[states.PRECOMPUTING] = uint64(time.Now().Unix()) - jww.DEBUG.Printf("Initializing round %d...", s.CurrentRound.ID) - - // Initialize network status - for i := range s.CurrentRound.NetworkStatus { - val := uint32(0) - s.CurrentRound.NetworkStatus[i] = &val - } - - // Initialize node states based on given topology - for _, nodeId := range topology { - newState := uint32(states.PENDING) - newId, err := id.NewNodeFromString(nodeId) - if err != nil { - return err - } - s.CurrentRound.NodeStatuses[*newId] = &newState - } - - // Sign the new round object - err := signature.Sign(s.CurrentRound.RoundInfo, s.PrivateKey) - if err != nil { - return err - } - - // Insert the new round object into the state tracker - err = s.RoundData.UpsertRound(s.CurrentRound.RoundInfo) - if err != nil { - return err - } - return s.AddRoundUpdate(s.CurrentRound.RoundInfo) -} - -// Attempt to update the internal state after a node polling operation -func (m *RegistrationImpl) updateState(id *id.Node, activity *current.Activity) error { - // Convert node activity to round state - roundState, err := activity.ConvertToRoundState() - if err != nil { - return err - } - - // Update node state - m.State.UpdateNodeState(id, roundState) - return nil -} diff --git a/cmd/control_test.go b/cmd/control_test.go deleted file mode 100644 index 92f985b607f3c78d87cf814167d9c21166362e68..0000000000000000000000000000000000000000 --- a/cmd/control_test.go +++ /dev/null @@ -1,234 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2018 Privategrity Corporation / -// / -// All rights reserved. / -//////////////////////////////////////////////////////////////////////////////// - -package cmd - -import ( - "encoding/base64" - "gitlab.com/elixxir/primitives/current" - "gitlab.com/elixxir/primitives/id" - "gitlab.com/elixxir/primitives/states" - "gitlab.com/elixxir/registration/storage" - "testing" -) - -// Happy path -func Test_createNextRound(t *testing.T) { - batchSize := uint32(1) - node1 := base64.StdEncoding.EncodeToString([]byte("TESTSTRING")) - - topology := []string{node1, node1} - - s, err := storage.NewState() - if err != nil { - t.Errorf("Unable to create state: %+v", err) - } - impl := &RegistrationImpl{ - State: s, - } - s.PrivateKey = getTestKey() - - err = impl.newRound(topology, batchSize) - if err != nil { - t.Errorf("Unexpected error creating round: %+v", err) - } - - // Check attributes - if s.CurrentRound.GetID() != 0 { - t.Errorf("Incorrect round ID") - } - if s.CurrentRound.GetUpdateID() != 1 { - t.Errorf("Incorrect update ID!") - } - if s.CurrentRound.GetState() != uint32(states.PRECOMPUTING) { - t.Errorf("Incorrect round state!") - } - if s.CurrentRound.GetBatchSize() != batchSize { - t.Errorf("Incorrect round batch size!") - } - if len(s.CurrentRound.Topology) != len(topology) { - t.Errorf("Incorrect round topology!") - } - - // Check node statuses - for _, status := range s.CurrentRound.NodeStatuses { - if *status != uint32(states.PENDING) { - t.Errorf("Incorrect node status!") - } - } - - // Check round signature - if s.CurrentRound.RoundInfo.GetSignature() == nil || - len(s.CurrentRound.RoundInfo.GetNonce()) < 1 { - t.Errorf("Incorrect round signature!") - } - - // Check state data - if _, err := s.RoundData.GetRound(0); err != nil { - t.Errorf("Incorrect round data: %+v", err) - } - - // Check state updates - if _, err := s.RoundUpdates.GetUpdate(0); err != nil { - t.Errorf("Incorrect round update data: %+v", err) - } -} - -// Full test -func Test_updateState(t *testing.T) { - // Create some node ids to be used for testing - node1Str := "TEST_STRING_111111111111" - node2Str := "TEST_STRING_888888888888" - node1 := base64.StdEncoding.EncodeToString([]byte(node1Str)) - node2 := base64.StdEncoding.EncodeToString([]byte(node2Str)) - topology := []string{node1, node2} - - newNodeId, err := id.NewNodeFromString(node1) - if err != nil { - t.Errorf("Failed to create new node id from string: %v", err) - t.Fail() - } - - newNodeId2, err := id.NewNodeFromString(node2) - if err != nil { - t.Errorf("Failed to create new node id from string: %v", err) - t.Fail() - } - - s, err := storage.NewState() - if err != nil { - t.Errorf("Unable to create state: %+v", err) - } - - impl := &RegistrationImpl{ - State: s, - params: &Params{ - batchSize: 1, - }, - } - - s.PrivateKey = getTestKey() - - go impl.StateControl() - - err = impl.newRound(topology, impl.params.batchSize) - if err != nil { - t.Errorf("Unexpected error creating round: %+v", err) - } - - // Test update without change in status - state := current.WAITING - - err = impl.updateState(newNodeId, &state) - if err != nil { - t.Errorf("Unexpected error updating node state: %+v", err) - } - - if *s.CurrentRound.NodeStatuses[*newNodeId] != uint32(states.PENDING) { - t.Errorf("Expected node status not to change!") - } - - // Test updating node statuses - state = current.PRECOMPUTING - err = impl.updateState(newNodeId, &state) - if err != nil { - t.Errorf("Unexpected error updating node state: %+v", err) - } - - if *s.CurrentRound.NodeStatuses[*newNodeId] != uint32(states.PRECOMPUTING) { - t.Errorf("Expected node status not to change!") - } - - err = impl.updateState(newNodeId2, &state) - if err != nil { - t.Errorf("Unexpected error updating node state: %+v", err) - } - - if *s.CurrentRound.NodeStatuses[*newNodeId2] != uint32(states.PRECOMPUTING) { - t.Errorf("Expected node status not to change!") - } - // Test updating node statuses that trigger an incrementation - state = current.STANDBY - err = impl.updateState(newNodeId, &state) - if err != nil { - t.Errorf("Unexpected error updating node state: %+v", err) - } - - err = impl.updateState(newNodeId2, &state) - if err != nil { - t.Errorf("Unexpected error updating node state: %+v", err) - } - - if s.CurrentRound.State != uint32(states.REALTIME) { - t.Errorf("Expected round to increment! Got %s", - states.Round(s.CurrentRound.State)) - } -} - -// Happy path -func Test_incrementRoundState(t *testing.T) { - // Create some node ids to be used for testing - node1Str := "TEST_STRING_111111111111" - node2Str := "TEST_STRING_888888888888" - node1 := base64.StdEncoding.EncodeToString([]byte(node1Str)) - node2 := base64.StdEncoding.EncodeToString([]byte(node2Str)) - topology := []string{node1, node2} - - s, err := storage.NewState() - if err != nil { - t.Errorf("Unable to create state: %+v", err) - } - - impl := &RegistrationImpl{ - State: s, - params: &Params{ - batchSize: 1, - }, - } - s.PrivateKey = getTestKey() - - err = impl.newRound(topology, impl.params.batchSize) - if err != nil { - t.Errorf("Unexpected error creating round: %+v", err) - } - - // Test incrementing to each state - err = impl.incrementRoundState(states.PENDING) - if err != nil { - t.Errorf("Unexpected error incrementing round state: %+v", err) - } - if s.CurrentUpdate != 1 { - t.Errorf("Unexpected round update occurred!") - } - err = impl.incrementRoundState(states.PRECOMPUTING) - if err != nil { - t.Errorf("Unexpected error incrementing round state: %+v", err) - } - if s.CurrentUpdate != 1 { - t.Errorf("Unexpected round update occurred!") - } - err = impl.incrementRoundState(states.STANDBY) - if err != nil { - t.Errorf("Unexpected error incrementing round state: %+v", err) - } - if s.CurrentUpdate != 2 { - t.Errorf("Round update failed to occur!") - } - err = impl.incrementRoundState(states.REALTIME) - if err != nil { - t.Errorf("Unexpected error incrementing round state: %+v", err) - } - if s.CurrentUpdate != 2 { - t.Errorf("Unexpected round update occurred!") - } - err = impl.incrementRoundState(states.COMPLETED) - if err != nil { - t.Errorf("Unexpected error incrementing round state: %+v", err) - } - if s.CurrentUpdate != 3 { - t.Errorf("Round update failed to occur!") - } -} diff --git a/cmd/impl.go b/cmd/impl.go index d2d859b83769e31b014e123dc39ad81759471441..8b2f14dc49cb812b2ef9dbbd2bca44efb078dcbc 100644 --- a/cmd/impl.go +++ b/cmd/impl.go @@ -21,23 +21,37 @@ import ( "gitlab.com/elixxir/primitives/ndf" "gitlab.com/elixxir/primitives/utils" "gitlab.com/elixxir/registration/storage" + "sync" "time" ) +//generally large buffer, should be roughly as many nodes as are expected +const nodeCompletionChanLen = 1000 + // The main registration instance object type RegistrationImpl struct { Comms *registration.Comms params *Params - State *storage.State + State *storage.NetworkState permissioningCert *x509.Certificate ndfOutputPath string - nodeCompleted chan struct{} NdfReady *uint32 certFromFile string registrationsRemaining *uint64 maxRegistrationAttempts uint64 + + //registration status trackers + numRegistered int + //FIXME: it is possible that polling lock and registration lock + // do the same job and could conflict. reconsiderations of this logic + // may be fruitful + registrationLock sync.Mutex + beginScheduling chan struct{} } +//function used to schedule nodes +type SchedulingAlgorithm func(params []byte, state *storage.NetworkState) error + // Params object for reading in configuration data type Params struct { Address string @@ -46,13 +60,13 @@ type Params struct { NdfOutputPath string NsCertPath string NsAddress string - NumNodesInNet int cmix ndf.Group e2e ndf.Group publicAddress string maxRegistrationAttempts uint64 registrationCountDuration time.Duration - batchSize uint32 + minimumNodes uint32 + udbId []byte } // toGroup takes a group represented by a map of string to string, @@ -75,7 +89,22 @@ func StartRegistration(params Params) (*RegistrationImpl, error) { // Initialize variables regRemaining := uint64(0) ndfReady := uint32(0) - state, err := storage.NewState() + + // Read in private key + key, err := utils.ReadFile(params.KeyPath) + if err != nil { + return nil, errors.Errorf("failed to read key at %+v: %+v", + params.KeyPath, err) + } + + pk, err := rsa.LoadPrivateKeyFromPem(key) + if err != nil { + return nil, errors.Errorf("Failed to parse permissioning server key: %+v. "+ + "PermissioningKey is %+v", err, pk) + } + + //initilize the state tracking object + state, err := storage.NewState(pk) if err != nil { return nil, err } @@ -87,8 +116,10 @@ func StartRegistration(params Params) (*RegistrationImpl, error) { maxRegistrationAttempts: params.maxRegistrationAttempts, registrationsRemaining: ®Remaining, ndfOutputPath: params.NdfOutputPath, - nodeCompleted: make(chan struct{}, len(RegistrationCodes)), NdfReady: &ndfReady, + + numRegistered: 0, + beginScheduling: make(chan struct{}), } // Create timer and channel to be used by routine that clears the number of @@ -99,18 +130,6 @@ func StartRegistration(params Params) (*RegistrationImpl, error) { regImpl.registrationCapacityRestRunner(ticker, done) }() - // Read in private key - key, err := utils.ReadFile(params.KeyPath) - if err != nil { - return nil, errors.Errorf("failed to read key at %+v: %+v", - params.KeyPath, err) - } - regImpl.State.PrivateKey, err = rsa.LoadPrivateKeyFromPem(key) - if err != nil { - return nil, errors.Errorf("Failed to parse permissioning server key: %+v. "+ - "PermissioningKey is %+v", err, regImpl.State.PrivateKey) - } - if !noTLS { // Read in TLS keys from files cert, err := utils.ReadFile(params.CertPath) @@ -127,6 +146,43 @@ func StartRegistration(params Params) (*RegistrationImpl, error) { } } + // Construct the NDF + networkDef := &ndf.NetworkDefinition{ + Registration: ndf.Registration{ + Address: RegParams.publicAddress, + TlsCertificate: regImpl.certFromFile, + }, + + Timestamp: time.Now(), + UDB: ndf.UDB{ID: RegParams.udbId}, + E2E: RegParams.e2e, + CMIX: RegParams.cmix, + // fixme: consider removing. this allows clients to remain agnostic of teaming order + // by forcing team order == ndf order for simple non-random + Nodes: make([]ndf.Node, params.minimumNodes), + Gateways: make([]ndf.Gateway, params.minimumNodes), + } + + // Assemble notification server information if configured + if RegParams.NsCertPath != "" && RegParams.NsAddress != "" { + nsCert, err := utils.ReadFile(RegParams.NsCertPath) + if err != nil { + return nil, errors.Errorf("unable to read notification certificate") + } + networkDef.Notification = ndf.Notification{ + Address: RegParams.NsAddress, + TlsCertificate: string(nsCert), + } + } else { + jww.WARN.Printf("Configured to run without notifications bot!") + } + + // update the internal state with the newly-formed NDF + err = regImpl.State.UpdateNdf(networkDef) + if err != nil { + return nil, err + } + // Start the communication server regImpl.Comms = registration.StartRegistrationServer(id.PERMISSIONING, params.Address, NewImplementation(regImpl), diff --git a/cmd/permissioning.go b/cmd/permissioning.go index 901716a55b4c0e2de6c7f20ca96be8bfd91b7314..e9874ed483ae0a6ea939d4d9d2b498ec5dcffc85 100644 --- a/cmd/permissioning.go +++ b/cmd/permissioning.go @@ -17,8 +17,8 @@ import ( "gitlab.com/elixxir/primitives/utils" "gitlab.com/elixxir/registration/certAuthority" "gitlab.com/elixxir/registration/storage" + "strconv" "sync/atomic" - "time" ) // Handle registration attempt by a Node @@ -26,7 +26,8 @@ func (m *RegistrationImpl) RegisterNode(ID []byte, ServerAddr, ServerTlsCert, GatewayAddr, GatewayTlsCert, RegistrationCode string) error { // Get proper ID string - idString := id.NewNodeFromBytes(ID).String() + nid := id.NewNodeFromBytes(ID) + idString := nid.String() // Check that the node hasn't already been registered nodeInfo, err := storage.PermissioningDb.GetNode(RegistrationCode) @@ -38,7 +39,7 @@ func (m *RegistrationImpl) RegisterNode(ID []byte, ServerAddr, ServerTlsCert, return errors.Errorf( "Node with registration code %+v has already been registered", RegistrationCode) } - + pk := &m.State.GetPrivateKey().PrivateKey // Load the node and gateway certs nodeCertificate, err := tls.LoadCertificate(ServerTlsCert) if err != nil { @@ -51,12 +52,12 @@ func (m *RegistrationImpl) RegisterNode(ID []byte, ServerAddr, ServerTlsCert, // Sign the node and gateway certs signedNodeCert, err := certAuthority.Sign(nodeCertificate, - m.permissioningCert, &(m.State.PrivateKey.PrivateKey)) + m.permissioningCert, pk) if err != nil { return errors.Errorf("failed to sign node certificate: %v", err) } signedGatewayCert, err := certAuthority.Sign(gatewayCertificate, - m.permissioningCert, &(m.State.PrivateKey.PrivateKey)) + m.permissioningCert, pk) if err != nil { return errors.Errorf("Failed to sign gateway certificate: %v", err) } @@ -70,106 +71,115 @@ func (m *RegistrationImpl) RegisterNode(ID []byte, ServerAddr, ServerTlsCert, jww.DEBUG.Printf("Inserted node %s into the database with code %s", idString, RegistrationCode) - // Obtain the number of registered nodes - _, err = storage.PermissioningDb.CountRegisteredNodes() + //add the node to the host object for authenticated communications + _, err = m.Comms.AddHost(idString, ServerAddr, []byte(ServerTlsCert), false, true) if err != nil { - return errors.Errorf("Unable to count registered Nodes: %+v", err) + return errors.Errorf("Could not register host for Server %s: %+v", ServerAddr, err) } - _, err = m.Comms.AddHost(idString, ServerAddr, []byte(ServerTlsCert), false, true) + //add the node to the node map to track its state + err = m.State.GetNodeMap().AddNode(nid, nodeInfo.Order) if err != nil { - return errors.Errorf("Could not register host for Server %s: %+v", ServerAddr, err) + return errors.WithMessage(err, "Could not register node with "+ + "state tracker") } - jww.DEBUG.Printf("Total number of expected nodes for registration"+ - " completion: %d", len(RegistrationCodes)) - m.nodeCompleted <- struct{}{} - return nil + // Notify registration thread + return m.completeNodeRegistration(RegistrationCode) } -// nodeRegistrationCompleter is a wrapper for completed node registration error handling -func nodeRegistrationCompleter(impl *RegistrationImpl) error { - // Wait for all Nodes to complete registration - for numNodes := uint32(0); numNodes < uint32(len(RegistrationCodes)); numNodes++ { - jww.INFO.Printf("Registered %d node(s)!", numNodes) - <-impl.nodeCompleted - } - // Assemble the completed topology - gateways, nodes, err := assembleNdf(RegistrationCodes) - if err != nil { - return errors.Errorf("unable to assemble topology: %+v", err) - } +// Handles including new registrations in the network +// fixme: we should split this function into what is relevant to registering a node and what is relevant +// to permissioning +func (m *RegistrationImpl) completeNodeRegistration(regCode string) error { + + m.registrationLock.Lock() + defer m.registrationLock.Unlock() - // Assemble the registration server information - registration := ndf.Registration{Address: RegParams.publicAddress, TlsCertificate: impl.certFromFile} + m.numRegistered++ - // Construct the NDF - networkDef := &ndf.NetworkDefinition{ - Registration: registration, - Timestamp: time.Now(), - Nodes: nodes, - Gateways: gateways, - UDB: udbParams, - E2E: RegParams.e2e, - CMIX: RegParams.cmix, + jww.INFO.Printf("Registered %d node(s)! %s", m.numRegistered, regCode) + + // Add the new node to the topology + networkDef := m.State.GetFullNdf().Get() + gateway, node, order, err := assembleNdf(regCode) + if err != nil { + jww.ERROR.Printf("unable to assemble topology: %+v", err) + return errors.Errorf("Could not complete registration") } - // Assemble notification server information if configured - if RegParams.NsCertPath != "" && RegParams.NsAddress != "" { - nsCert, err := utils.ReadFile(RegParams.NsCertPath) - if err != nil { - return errors.Errorf("unable to read notification certificate") - } - networkDef.Notification = ndf.Notification{Address: RegParams.NsAddress, TlsCertificate: string(nsCert)} - } else { - jww.WARN.Printf("Configured to run without notifications bot!") + // fixme: consider removing. this allows clients to remain agnostic of teaming order + // by forcing team order == ndf order for simple non-random + if order >= len(networkDef.Nodes) { + appendNdf(networkDef, order) } - // Update the internal state with the newly-formed NDF - err = impl.State.UpdateNdf(networkDef) - err = impl.createNextRound() + networkDef.Gateways[order] = gateway + networkDef.Nodes[order] = node + + // update the internal state with the newly-updated ndf + err = m.State.UpdateNdf(networkDef) if err != nil { return err } - // Output the completed topology to a JSON file and save marshall'ed json data - err = outputToJSON(networkDef, impl.ndfOutputPath) + // Output the current topology to a JSON file + err = outputToJSON(networkDef, m.ndfOutputPath) if err != nil { - return errors.Errorf("unable to output NDF JSON file: %+v", err) + jww.ERROR.Printf("unable to output NDF JSON file: %+v", err) + return errors.Errorf("Could not complete registration") + } + + // Kick off the network if the minimum number of nodes has been met + if uint32(m.numRegistered) == m.params.minimumNodes { + jww.INFO.Printf("Minimum number of nodes %d registered!", m.numRegistered) + + atomic.CompareAndSwapUint32(m.NdfReady, 0, 1) + + //signal that scheduling should begin + m.beginScheduling <- struct{}{} } - // Alert that registration is complete - atomic.CompareAndSwapUint32(impl.NdfReady, 0, 1) return nil } -// Assemble the completed topology from the database -func assembleNdf(codes []string) ([]ndf.Gateway, []ndf.Node, error) { - var gateways []ndf.Gateway - var nodes []ndf.Node - for _, registrationCode := range codes { - // Get node information for each registration code - nodeInfo, err := storage.PermissioningDb.GetNode(registrationCode) - if err != nil { - return nil, nil, errors.Errorf( - "unable to obtain node for registration"+ - " code %+v: %+v", registrationCode, err) - } - var node ndf.Node - node.ID = nodeInfo.Id - node.TlsCertificate = nodeInfo.NodeCertificate - node.Address = nodeInfo.ServerAddress - - var gateway ndf.Gateway - gateway.TlsCertificate = nodeInfo.GatewayCertificate - gateway.Address = nodeInfo.GatewayAddress - //Since we are appending them simultaneously, indexing corresponding - // gateway-node is just finding your index (as a gateway or a node) - gateways = append(gateways, gateway) - nodes = append(nodes, node) - } - jww.DEBUG.Printf("Assembled the network topology") - return gateways, nodes, nil +// helper function which appends the ndf to the maximum order +func appendNdf(definition *ndf.NetworkDefinition, order int) { + lengthDifference := (order % len(definition.Nodes)) + 1 + gwExtension := make([]ndf.Gateway, lengthDifference) + nodeExtension := make([]ndf.Node, lengthDifference) + definition.Nodes = append(definition.Nodes, nodeExtension...) + definition.Gateways = append(definition.Gateways, gwExtension...) + +} + +// Assemble information for the given registration code +func assembleNdf(code string) (ndf.Gateway, ndf.Node, int, error) { + + // Get node information for each registration code + nodeInfo, err := storage.PermissioningDb.GetNode(code) + if err != nil { + return ndf.Gateway{}, ndf.Node{}, 0, errors.Errorf( + "unable to obtain node for registration"+ + " code %+v: %+v", code, err) + } + + node := ndf.Node{ + ID: nodeInfo.Id, + Address: nodeInfo.ServerAddress, + TlsCertificate: nodeInfo.NodeCertificate, + } + gateway := ndf.Gateway{ + Address: nodeInfo.GatewayAddress, + TlsCertificate: nodeInfo.GatewayCertificate, + } + + order, err := strconv.Atoi(nodeInfo.Order) + if err != nil { + return ndf.Gateway{}, ndf.Node{}, 0, errors.Errorf("Unable to read node's info: %v", err) + } + + return gateway, node, order, nil } // outputNodeTopologyToJSON encodes the NodeTopology structure to JSON and diff --git a/cmd/poll.go b/cmd/poll.go index 8d3a900c37baf8a9161ccea1410ed243a6f0f1ec..b7e728e9f2178566c56b1736db6eaf46f2b954a7 100644 --- a/cmd/poll.go +++ b/cmd/poll.go @@ -9,7 +9,7 @@ package cmd import ( - "errors" + "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/comms/connect" pb "gitlab.com/elixxir/comms/mixmessages" @@ -42,34 +42,52 @@ func (m *RegistrationImpl) Poll(msg *pb.PermissioningPoll, jww.DEBUG.Printf("Returning a new NDF to a back-end server!") // Return the updated NDFs - response.FullNDF = m.State.FullNdfMsg - response.PartialNDF = m.State.PartialNdfMsg + response.FullNDF = m.State.GetFullNdf().GetPb() + response.PartialNDF = m.State.GetPartialNdf().GetPb() } - // Do not attempt to update or report state to unprepared nodes - if msg.Activity == uint32(current.NOT_STARTED) { + // Fetch latest round updates + response.Updates, err = m.State.GetUpdates(int(msg.LastUpdate)) + if err != nil { return } // Commit updates reported by the node if node involved in the current round - if m.State.IsRoundNode(auth.Sender.GetId()) { - jww.TRACE.Printf("Updating state for node %s: %+v", - auth.Sender.GetId(), msg) + jww.DEBUG.Printf("Updating state for node %s: %+v", + auth.Sender.GetId(), msg) - nodeId, err := id.NewNodeFromString(auth.Sender.GetId()) - if err != nil { - return nil, err - } - err = m.updateState( - nodeId, - (*current.Activity)(&msg.Activity)) - if err != nil { - return nil, err - } + //get the nodeState and update + nid, err := id.NewNodeFromString(auth.Sender.GetId()) + if err != nil { + err = errors.Errorf("could not decode node id of %s: %s", auth.Sender.GetId(), err) + return } - // Fetch latest round updates - response.Updates = m.State.GetUpdates(int(msg.LastUpdate)) + n := m.State.GetNodeMap().GetNode(nid) + if n == nil { + err = errors.Errorf("node %s could not be found in internal state tracker", nid) + return + } + + // when a node poll is received, the nodes polling lock is taken here. If + // there is no update, it is released in this endpoint, otherwise it is + // released in the scheduling algorithm which blocks all future polls until + // processing completes + n.GetPollingLock().Lock() + + // update does edge checking. It ensures the state change recieved was a + // valid one and the state fo the node and + // any associated round allows for that change. If the change was not + // acceptable, it is not recorded and an error is returned, which is + // propagated to the node + update, oldActivity, err := n.Update(current.Activity(msg.Activity)) + + //if an update ocured, report it to the control thread + if update { + err = m.State.NodeUpdateNotification(nid, oldActivity, current.Activity(msg.Activity)) + } else { + n.GetPollingLock().Unlock() + } return } @@ -78,7 +96,8 @@ func (m *RegistrationImpl) Poll(msg *pb.PermissioningPoll, func (m *RegistrationImpl) PollNdf(theirNdfHash []byte, auth *connect.Auth) ([]byte, error) { // Ensure the NDF is ready to be returned - if m.State.GetFullNdf() == nil || m.State.GetPartialNdf() == nil { + regComplete := atomic.LoadUint32(m.NdfReady) + if regComplete != 1 { return nil, errors.New(ndf.NO_NDF) } diff --git a/cmd/poll_test.go b/cmd/poll_test.go index de2d3b31443ba099c922ad6d14f481352dfc9dcc..33312ead4155b9a99ffc9f9920e79a3fee34e07e 100644 --- a/cmd/poll_test.go +++ b/cmd/poll_test.go @@ -8,17 +8,21 @@ package cmd import ( "bytes" + "fmt" "gitlab.com/elixxir/comms/connect" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/crypto/signature/rsa" "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/id" "gitlab.com/elixxir/primitives/ndf" "gitlab.com/elixxir/primitives/states" "gitlab.com/elixxir/primitives/utils" "gitlab.com/elixxir/registration/storage" + "gitlab.com/elixxir/registration/storage/node" "gitlab.com/elixxir/registration/testkeys" "sync/atomic" "testing" + "time" ) // TestFunc: Gets permissioning server test key @@ -31,32 +35,25 @@ func getTestKey() *rsa.PrivateKey { // Happy path func TestRegistrationImpl_Poll(t *testing.T) { + testID := id.NewNodeFromUInt(0, t) testString := "test" // Start registration server + testParams.KeyPath = testkeys.GetCAKeyPath() impl, err := StartRegistration(testParams) if err != nil { t.Errorf("Unable to start registration: %+v", err) } atomic.CompareAndSwapUint32(impl.NdfReady, 0, 1) - impl.State.PrivateKey = getTestKey() err = impl.State.UpdateNdf(&ndf.NetworkDefinition{ Registration: ndf.Registration{ Address: "420", TlsCertificate: "", }, }) - if err != nil { - t.Errorf("Unable to update ndf: %+v", err) - return - } - err = impl.newRound([]string{testString}, 1) - if err != nil { - t.Errorf("Unexpected error creating round: %+v", err) - } // Make a simple auth object that will pass the checks - testHost, _ := connect.NewHost(testString, testString, + testHost, _ := connect.NewHost(testID.String(), testString, make([]byte, 0), false, true) testAuth := &connect.Auth{ IsAuthenticated: true, @@ -73,6 +70,22 @@ func TestRegistrationImpl_Poll(t *testing.T) { Error: nil, } + err = impl.State.AddRoundUpdate( + &pb.RoundInfo{ + ID: 1, + State: uint32(states.PRECOMPUTING), + }) + + if err != nil { + t.Errorf("Could not add round update: %s", err) + } + + err = impl.State.GetNodeMap().AddNode(testID, "") + + if err != nil { + t.Errorf("Could nto add node: %s", err) + } + response, err := impl.Poll(testMsg, testAuth) if err != nil { t.Errorf("Unexpected error polling: %+v", err) @@ -90,9 +103,22 @@ func TestRegistrationImpl_Poll(t *testing.T) { // Error path: Ndf not ready func TestRegistrationImpl_PollNoNdf(t *testing.T) { + + // Read in private key + key, err := utils.ReadFile(testkeys.GetCAKeyPath()) + if err != nil { + t.Errorf("failed to read key at %+v: %+v", + testkeys.GetCAKeyPath(), err) + } + + pk, err := rsa.LoadPrivateKeyFromPem(key) + if err != nil { + t.Errorf("Failed to parse permissioning server key: %+v. "+ + "PermissioningKey is %+v", err, pk) + } // Start registration server ndfReady := uint32(0) - state, err := storage.NewState() + state, err := storage.NewState(pk) if err != nil { t.Errorf("Unable to create state: %+v", err) } @@ -113,7 +139,7 @@ func TestRegistrationImpl_PollFailAuth(t *testing.T) { // Start registration server ndfReady := uint32(1) - state, err := storage.NewState() + state, err := storage.NewState(getTestKey()) if err != nil { t.Errorf("Unable to create state: %+v", err) } @@ -121,7 +147,7 @@ func TestRegistrationImpl_PollFailAuth(t *testing.T) { State: state, NdfReady: &ndfReady, } - impl.State.PrivateKey = getTestKey() + err = impl.State.UpdateNdf(&ndf.NetworkDefinition{ Registration: ndf.Registration{ Address: "420", @@ -145,51 +171,58 @@ func TestRegistrationImpl_PollFailAuth(t *testing.T) { //Happy path func TestRegistrationImpl_PollNdf(t *testing.T) { + //Create database storage.PermissioningDb = storage.NewDatabase("test", "password", "regCodes", "0.0.0.0:6969") //Create reg codes and populate the database - strings := make([]string, 0) - strings = append(strings, "BBBB", "CCCC", "DDDD") - storage.PopulateNodeRegistrationCodes(strings) - RegistrationCodes = strings - RegParams = testParams + infos := make([]node.Info, 0) + infos = append(infos, node.Info{RegCode: "BBBB", Order: "0"}, + node.Info{RegCode: "CCCC", Order: "1"}, + node.Info{RegCode: "DDDD", Order: "2"}) + storage.PopulateNodeRegistrationCodes(infos) + RegParams = testParams + udbId := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4} + RegParams.udbId = udbId + RegParams.minimumNodes = 3 + fmt.Println("-A") // Start registration server - impl, err := StartRegistration(testParams) + impl, err := StartRegistration(RegParams) if err != nil { t.Errorf(err.Error()) } - //Start the other nodes - udbId := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4} - - udbParams.ID = udbId - - //Register 1st node - err = impl.RegisterNode([]byte("B"), nodeAddr, string(nodeCert), - "0.0.0.0:7900", string(gatewayCert), "BBBB") - if err != nil { - t.Errorf("Expected happy path, recieved error: %+v", err) - } - - //Register 2nd node - err = impl.RegisterNode([]byte("C"), "0.0.0.0:6901", string(nodeCert), - "0.0.0.0:7901", string(gatewayCert), "CCCC") - if err != nil { - t.Errorf("Expected happy path, recieved error: %+v", err) - } - - //Register 3rd node - err = impl.RegisterNode([]byte("D"), "0.0.0.0:6902", string(nodeCert), - "0.0.0.0:7902", string(gatewayCert), "DDDD") - if err != nil { - t.Errorf("Expected happy path, recieved error: %+v", err) - } - - err = nodeRegistrationCompleter(impl) - if err != nil { - t.Errorf(err.Error()) + go func() { + fmt.Println("A") + //Register 1st node + err = impl.RegisterNode([]byte("B"), nodeAddr, string(nodeCert), + "0.0.0.0:7900", string(gatewayCert), "BBBB") + if err != nil { + t.Errorf("Expected happy path, recieved error: %+v", err) + } + fmt.Println("B") + //Register 2nd node + err = impl.RegisterNode([]byte("C"), "0.0.0.0:6901", string(nodeCert), + "0.0.0.0:7901", string(gatewayCert), "CCCC") + if err != nil { + t.Errorf("Expected happy path, recieved error: %+v", err) + } + fmt.Println("C") + //Register 3rd node + err = impl.RegisterNode([]byte("D"), "0.0.0.0:6902", string(nodeCert), + "0.0.0.0:7902", string(gatewayCert), "DDDD") + if err != nil { + t.Errorf("Expected happy path, recieved error: %+v", err) + } + }() + + //wait for registration to complete + select { + case <-time.NewTimer(1000 * time.Millisecond).C: + t.Errorf("Node registration never completed") + t.FailNow() + case <-impl.beginScheduling: } observedNDFBytes, err := impl.PollNdf(nil, &connect.Auth{}) @@ -203,7 +236,8 @@ func TestRegistrationImpl_PollNdf(t *testing.T) { string(observedNDFBytes)) } if bytes.Compare(observedNDF.UDB.ID, udbId) != 0 { - t.Errorf("Failed to set udbID. Expected: %v, \nRecieved: %v", udbId, observedNDF.UDB.ID) + t.Errorf("Failed to set udbID. Expected: %v, \nRecieved: %v, \nNdf: %+v", + udbId, observedNDF.UDB.ID, observedNDF) } if observedNDF.Registration.Address != permAddr { @@ -212,7 +246,8 @@ func TestRegistrationImpl_PollNdf(t *testing.T) { } expectedNodeIDs := make([][]byte, 0) expectedNodeIDs = append(expectedNodeIDs, []byte("B"), []byte("C"), []byte("D")) - for i := range observedNDF.Nodes { + + for i := range expectedNodeIDs { if bytes.Compare(expectedNodeIDs[i], observedNDF.Nodes[i].ID) != 0 { t.Errorf("Could not build node %d's, id: Expected: %v \n Recieved: %v", i, expectedNodeIDs, observedNDF.Nodes[i].ID) @@ -229,22 +264,22 @@ func TestRegistrationImpl_PollNdf_NoNDF(t *testing.T) { storage.PermissioningDb = storage.NewDatabase("test", "password", "regCodes", "0.0.0.0:6969") //Create reg codes and populate the database - strings := make([]string, 0) - strings = append(strings, "BBBB", "CCCC") - storage.PopulateNodeRegistrationCodes(strings) - RegistrationCodes = strings + infos := make([]node.Info, 0) + infos = append(infos, node.Info{RegCode: "BBBB", Order: "0"}, + node.Info{RegCode: "CCCC", Order: "1"}, + node.Info{RegCode: "DDDD", Order: "2"}) + storage.PopulateNodeRegistrationCodes(infos) RegParams = testParams + //Setup udb configurations + udbId := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4} + RegParams.udbId = udbId + RegParams.minimumNodes = 3 // Start registration server impl, err := StartRegistration(testParams) if err != nil { t.Errorf(err.Error()) } - go nodeRegistrationCompleter(impl) - - //Setup udb configurations - udbId := []byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 4} - udbParams.ID = udbId //Register 1st node err = impl.RegisterNode([]byte("B"), nodeAddr, string(nodeCert), diff --git a/cmd/registration.go b/cmd/registration.go index c6849e24b92cbfa5a828ecc9837a98d8c862f6f6..22257cbc461fd6c3ffd415ee3176ec437c347c7d 100644 --- a/cmd/registration.go +++ b/cmd/registration.go @@ -63,7 +63,7 @@ func (m *RegistrationImpl) RegisterUser(registrationCode, pubKey string) ( h := sha256.New() h.Write([]byte(pubKey)) data := h.Sum(nil) - sig, err := rsa.Sign(rand.Reader, m.State.PrivateKey, crypto.SHA256, data, nil) + sig, err := rsa.Sign(rand.Reader, m.State.GetPrivateKey(), crypto.SHA256, data, nil) if err != nil { return make([]byte, 0), errors.Errorf( "Unable to sign client public key: %+v", err) diff --git a/cmd/registration_test.go b/cmd/registration_test.go index 47adb25074422dfbd0f898ea41186c0f0d3da0cf..634b5f4de354b80f6dd80a53f11fcdac66beec37 100644 --- a/cmd/registration_test.go +++ b/cmd/registration_test.go @@ -8,9 +8,10 @@ package cmd import ( "fmt" jww "github.com/spf13/jwalterweatherman" - "gitlab.com/elixxir/comms/node" + nodeComms "gitlab.com/elixxir/comms/node" "gitlab.com/elixxir/primitives/utils" "gitlab.com/elixxir/registration/storage" + "gitlab.com/elixxir/registration/storage/node" "gitlab.com/elixxir/registration/testkeys" "os" "testing" @@ -24,7 +25,7 @@ var permAddr = "0.0.0.0:5900" var testParams Params var gatewayCert []byte -var nodeComm *node.Comms +var nodeComm *nodeComms.Comms func TestMain(m *testing.M) { jww.SetStdoutThreshold(jww.LevelDebug) @@ -52,8 +53,9 @@ func TestMain(m *testing.M) { publicAddress: permAddr, maxRegistrationAttempts: 5, registrationCountDuration: time.Hour, + minimumNodes: 3, } - nodeComm = node.StartNode("tmp", nodeAddr, node.NewImplementation(), nodeCert, nodeKey) + nodeComm = nodeComms.StartNode("tmp", nodeAddr, nodeComms.NewImplementation(), nodeCert, nodeKey) runFunc := func() int { code := m.Run() @@ -101,10 +103,9 @@ func TestRegCodeExists_InsertRegCode(t *testing.T) { if err != nil { t.Errorf(err.Error()) } - impl.nodeCompleted = make(chan struct{}, 1) storage.PermissioningDb = storage.NewDatabase("test", "password", "regCodes", "0.0.0.0:6969") //Insert a sample regCode - err = storage.PermissioningDb.InsertNodeRegCode("AAAA") + err = storage.PermissioningDb.InsertNodeRegCode("AAAA", "0") if err != nil { t.Errorf("Failed to insert client reg code %+v", err) } @@ -153,28 +154,33 @@ func TestCompleteRegistration_HappyPath(t *testing.T) { //Crate database storage.PermissioningDb = storage.NewDatabase("test", "password", "regCodes", "0.0.0.0:6969") //Insert a sample regCode - strings := make([]string, 0) - strings = append(strings, "BBBB") - storage.PopulateNodeRegistrationCodes(strings) - RegistrationCodes = strings + infos := make([]node.Info, 0) + infos = append(infos, node.Info{RegCode: "BBBB", Order: "0"}) + storage.PopulateNodeRegistrationCodes(infos) + localParams := testParams + localParams.minimumNodes = 1 // Start registration server - impl, err := StartRegistration(testParams) + impl, err := StartRegistration(localParams) if err != nil { t.Errorf(err.Error()) } RegParams = testParams - err = impl.RegisterNode([]byte("test"), "0.0.0.0:6900", string(nodeCert), - "0.0.0.0:6900", string(nodeCert), "BBBB") - if err != nil { - t.Errorf("Expected happy path, recieved error: %+v", err) - return - } + go func() { + err = impl.RegisterNode([]byte("test"), "0.0.0.0:6900", string(nodeCert), + "0.0.0.0:6900", string(nodeCert), "BBBB") + if err != nil { + t.Errorf("Expected happy path, recieved error: %+v", err) + return + } + }() - err = nodeRegistrationCompleter(impl) - if err != nil { - t.Errorf("Expected happy path, recieved error: %+v", err) + select { + case <-time.NewTimer(200 * time.Millisecond).C: + t.Errorf("Registration failed to complete") + t.FailNow() + case <-impl.beginScheduling: } //Kill the connections for the next test @@ -187,10 +193,11 @@ func TestDoubleRegistration(t *testing.T) { storage.PermissioningDb = storage.NewDatabase("test", "password", "regCodes", "0.0.0.0:6969") //Create reg codes and populate the database - strings := make([]string, 0) - strings = append(strings, "AAAA", "BBBB", "CCCC") - storage.PopulateNodeRegistrationCodes(strings) - RegistrationCodes = strings + infos := make([]node.Info, 0) + infos = append(infos, node.Info{RegCode: "AAAA", Order: "0"}, + node.Info{RegCode: "BBBB", Order: "1"}, + node.Info{RegCode: "CCCC", Order: "2"}) + storage.PopulateNodeRegistrationCodes(infos) RegParams = testParams // Start registration server @@ -198,10 +205,9 @@ func TestDoubleRegistration(t *testing.T) { if err != nil { t.Errorf(err.Error()) } - go nodeRegistrationCompleter(impl) //Create a second node to register - nodeComm2 := node.StartNode("tmp", "0.0.0.0:6901", node.NewImplementation(), nodeCert, nodeKey) + nodeComm2 := nodeComms.StartNode("tmp", "0.0.0.0:6901", nodeComms.NewImplementation(), nodeCert, nodeKey) //Register 1st node err = impl.RegisterNode([]byte("test"), nodeAddr, string(nodeCert), @@ -229,38 +235,46 @@ func TestTopology_MultiNodes(t *testing.T) { storage.PermissioningDb = storage.NewDatabase("test", "password", "regCodes", "0.0.0.0:6969") //Create reg codes and populate the database - strings := make([]string, 0) - strings = append(strings, "BBBB", "CCCC") - storage.PopulateNodeRegistrationCodes(strings) - RegistrationCodes = strings - RegParams = testParams + infos := make([]node.Info, 0) + infos = append(infos, node.Info{RegCode: "AAAA", Order: "0"}, + node.Info{RegCode: "BBBB", Order: "1"}, + node.Info{RegCode: "CCCC", Order: "2"}) + + storage.PopulateNodeRegistrationCodes(infos) + + localParams := testParams + localParams.minimumNodes = 2 // Start registration server - impl, err := StartRegistration(testParams) + impl, err := StartRegistration(localParams) if err != nil { t.Errorf(err.Error()) } //Create a second node to register - nodeComm2 := node.StartNode("tmp", "0.0.0.0:6901", node.NewImplementation(), nodeCert, nodeKey) - - //Register 1st node - err = impl.RegisterNode([]byte("A"), nodeAddr, string(nodeCert), - nodeAddr, string(nodeCert), "BBBB") - if err != nil { - t.Errorf("Expected happy path, recieved error: %+v", err) - } - - //Register 2nd node - err = impl.RegisterNode([]byte("B"), "0.0.0.0:6901", string(gatewayCert), - "0.0.0.0:6901", string(gatewayCert), "CCCC") - if err != nil { - t.Errorf("Expected happy path, recieved error: %+v", err) - } - - err = nodeRegistrationCompleter(impl) - if err != nil { - t.Errorf(err.Error()) + nodeComm2 := nodeComms.StartNode("tmp", "0.0.0.0:6901", nodeComms.NewImplementation(), nodeCert, nodeKey) + + go func() { + //Register 1st node + err = impl.RegisterNode([]byte("A"), nodeAddr, string(nodeCert), + nodeAddr, string(nodeCert), "BBBB") + if err != nil { + t.Errorf("Expected happy path, recieved error: %+v", err) + } + + //Register 2nd node + err = impl.RegisterNode([]byte("B"), "0.0.0.0:6901", string(gatewayCert), + "0.0.0.0:6901", string(gatewayCert), "CCCC") + if err != nil { + t.Errorf("Expected happy path, recieved error: %+v", err) + } + }() + + select { + case <-time.NewTimer(200 * time.Millisecond).C: + t.Errorf("Registration failed to complete") + t.FailNow() + case <-impl.beginScheduling: } //Kill the connections for the next test @@ -335,7 +349,6 @@ func TestRegCodeExists_RegUser_Timer(t *testing.T) { if err != nil { t.Errorf(err.Error()) } - go nodeRegistrationCompleter(impl) // Initialize the database storage.PermissioningDb = storage.NewDatabase( diff --git a/cmd/root.go b/cmd/root.go index 3f468283689eba9e09f940a5df58c2fda23161f3..8d395db16a6102eaaa1af917da98e1425c66f2ab 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -17,8 +17,10 @@ import ( jww "github.com/spf13/jwalterweatherman" "github.com/spf13/viper" "gitlab.com/elixxir/comms/mixmessages" - "gitlab.com/elixxir/primitives/ndf" + "gitlab.com/elixxir/primitives/utils" + "gitlab.com/elixxir/registration/scheduling/simple" "gitlab.com/elixxir/registration/storage" + "gitlab.com/elixxir/registration/storage/node" "os" "path" "strconv" @@ -30,10 +32,8 @@ var ( cfgFile string logLevel uint // 0 = info, 1 = debug, >1 = trace noTLS bool - RegistrationCodes []string RegParams Params ClientRegCodes []string - udbParams ndf.UDB clientVersion string clientVersionLock sync.RWMutex disablePermissioning bool @@ -58,11 +58,11 @@ var rootCmd = &cobra.Command{ jww.FATAL.Panicf("Failed to create E2E group: %+v", err) } + // Parse config file options certPath := viper.GetString("certPath") keyPath := viper.GetString("keyPath") localAddress := fmt.Sprintf("0.0.0.0:%d", viper.GetInt("port")) - batchSize := viper.GetInt("batchSize") ndfOutputPath := viper.GetString("ndfOutputPath") setClientVersion(viper.GetString("clientVersion")) ipAddr := viper.GetString("publicAddress") @@ -90,17 +90,27 @@ var rootCmd = &cobra.Command{ ) // Populate Node registration codes into the database - RegistrationCodes = viper.GetStringSlice("registrationCodes") - storage.PopulateNodeRegistrationCodes(RegistrationCodes) + RegCodesFilePath := viper.GetString("regCodesFilePath") + regCodeInfos, err := node.LoadInfo(RegCodesFilePath) + if err != nil { + jww.FATAL.Panicf("Failed to load registration codes from the "+ + "file %s: %+v", RegCodesFilePath, err) + } + storage.PopulateNodeRegistrationCodes(regCodeInfos) ClientRegCodes = viper.GetStringSlice("clientRegCodes") storage.PopulateClientRegistrationCodes(ClientRegCodes, 1000) - //Fixme: Do we want the udbID to be specified in the yaml? - tmpSlice := make([]byte, 32) + udbId := make([]byte, 32) + udbId[len(udbId)-1] = byte(viper.GetInt("udbID")) + + //load the scheduling params file as a string + SchedulingConfigPath := viper.GetString("schedulingConfigPath") + SchedulingConfig, err := utils.ReadFile(SchedulingConfigPath) + if err != nil { + jww.FATAL.Panicf("Could not load Scheduling Config file: %v", err) + } - tmpSlice[len(tmpSlice)-1] = byte(viper.GetInt("udbID")) - udbParams.ID = tmpSlice // Populate params RegParams = Params{ Address: localAddress, @@ -114,7 +124,8 @@ var rootCmd = &cobra.Command{ NsCertPath: nsCertPath, maxRegistrationAttempts: maxRegistrationAttempts, registrationCountDuration: registrationCountDuration, - batchSize: uint32(batchSize), + udbId: udbId, + minimumNodes: viper.GetUint32("minimumNodes"), } jww.INFO.Println("Starting Permissioning Server...") @@ -125,15 +136,31 @@ var rootCmd = &cobra.Command{ jww.FATAL.Panicf(err.Error()) } - // Begin the thread which handles the completion of node registration - err = nodeRegistrationCompleter(impl) - if err != nil { - jww.FATAL.Panicf("Failed to complete node registration: %+v", err) - } - jww.INFO.Printf("Node registration complete!") + jww.INFO.Printf("Waiting for for %v nodes to register so "+ + "rounds can start", RegParams.minimumNodes) + + <-impl.beginScheduling + jww.INFO.Printf("Minnimum number of nodes %v have registered,"+ + "begining scheduling and round creation", RegParams.minimumNodes) + + // Begin scheduling algorithm + go func() { + var err error + algo := viper.GetString("schedulingAlgorithm") + jww.INFO.Printf("Beginning %s scheduling algorithm", algo) + switch algo { + case "simple": + err = simple.Scheduler(SchedulingConfig, impl.State) + case "secure": + err = errors.New("secure scheduling algorithm not yet implemented") + default: + err = errors.Errorf("schedulding algorithem %s unknown", algo) + } + jww.FATAL.Panicf("Scheduling Algorithm exited: %s", err) + }() - // Begin state control (loops forever) - impl.StateControl() + // Block forever to prevent the program ending + select {} }, } diff --git a/go.mod b/go.mod index df6af96228231e4de93319ae13ebdd0a60e6365c..ba2017ad0d8fcbaedea8c4c432b8bad61bfd326f 100644 --- a/go.mod +++ b/go.mod @@ -3,25 +3,33 @@ module gitlab.com/elixxir/registration go 1.13 require ( - github.com/fsnotify/fsnotify v1.4.7 + github.com/fsnotify/fsnotify v1.4.9 github.com/go-pg/pg v8.0.6+incompatible + github.com/golang/protobuf v1.4.0 // indirect github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 // indirect github.com/jinzhu/inflection v1.0.0 // indirect github.com/mitchellh/go-homedir v1.1.0 + github.com/mitchellh/mapstructure v1.2.2 // indirect github.com/onsi/ginkgo v1.12.0 // indirect github.com/onsi/gomega v1.9.0 // indirect - github.com/pelletier/go-toml v1.6.0 // indirect + github.com/pelletier/go-toml v1.7.0 // indirect github.com/pkg/errors v0.9.1 github.com/smartystreets/assertions v1.0.1 // indirect github.com/spf13/afero v1.2.2 // indirect github.com/spf13/cast v1.3.1 // indirect - github.com/spf13/cobra v0.0.6 + github.com/spf13/cobra v1.0.0 github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/pflag v1.0.5 // indirect - github.com/spf13/viper v1.6.2 + + github.com/spf13/viper v1.6.3 gitlab.com/elixxir/comms v0.0.0-20200415204952-6d63dd94a0ea gitlab.com/elixxir/crypto v0.0.0-20200410231849-90e859940f5d gitlab.com/elixxir/primitives v0.0.0-20200410231944-a57d71d577c9 - gopkg.in/ini.v1 v1.54.0 // indirect + golang.org/x/crypto v0.0.0-20200420192832-a76a400e3025 // indirect + golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e // indirect + golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f // indirect + google.golang.org/genproto v0.0.0-20200420144010-e5e8543f8aeb // indirect + google.golang.org/grpc v1.28.1 // indirect + gopkg.in/ini.v1 v1.55.0 // indirect mellium.im/sasl v0.0.0-20190815210834-e27ea4901008 // indirect ) diff --git a/go.sum b/go.sum index b912ef1545120bf3dbbcfc494d1babbc66437184..ddbf1290563485a8c2b6bf59e7be3e204dc26685 100644 --- a/go.sum +++ b/go.sum @@ -10,8 +10,10 @@ github.com/beorn7/perks v1.0.0/go.mod h1:KWe93zE9D1o94FZ5RNwFwVgaQK1VOXiVxmqh+Ce github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/cespare/xxhash v1.1.0/go.mod h1:XrSqR1VqqWfGrhpAt58auRo0WTKS1nRRg3ghfAqPWnc= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= +github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= github.com/coreos/bbolt v1.3.2/go.mod h1:iRUV2dpdMOn7Bo10OQBFzIJO9kkE559Wcmn+qkEiiKk= github.com/coreos/etcd v3.3.10+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= +github.com/coreos/etcd v3.3.13+incompatible/go.mod h1:uF7uidLiAD3TWHmW31ZFd/JWoc32PjwdhPthX9715RE= github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-systemd v0.0.0-20190321100706-95778dfbb74e/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/pkg v0.0.0-20180928190104-399ea9e2e55f/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= @@ -22,10 +24,14 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= github.com/dgryski/go-sip13 v0.0.0-20181026042036-e10d5fee7954/go.mod h1:vAd38F8PWV+bWy6jNmig1y/TA+kYO4g3RSRF0IAv0no= +github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= +github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/fsnotify/fsnotify v1.4.7 h1:IXs+QLmnXW2CcXuY+8Mzv/fWEsPGWxqefPtCP5CnV9I= github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= +github.com/fsnotify/fsnotify v1.4.9 h1:hsms1Qyu0jgnwNXIxa+/V/PDsU6CfLf6CNO8H7IWoS4= +github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= @@ -47,10 +53,19 @@ github.com/golang/protobuf v1.3.3 h1:gyjaxf+svBWX08ZjK86iN9geUJF0H6gp2IRKX6Nf6/I github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.4 h1:87PNWwrRvUSnqS4dlcBU/ftvOIBep4sYuBLlh6rX2wk= github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= +github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= +github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= +github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= +github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= +github.com/golang/protobuf v1.4.0 h1:oOuy+ugB+P/kBdUnG5QaMXSIyJ1q38wWSojYCb3z5VQ= +github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= +github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= +github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= github.com/google/go-cmp v0.4.0 h1:xsAVV57WRhGj6kEIi8ReJzQlHHqcBYCElAvkovg3B/4= github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1 h1:EGx4pi6eqNxGaHF6qqu48+N2wcFQ5qg5FXgOdqsJ5d8= github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= github.com/gopherjs/gopherjs v0.0.0-20200217142428-fce0ec30dd00 h1:l5lAOZEym3oK3SQ2HBHWsJUfbNBiTXJDeW2QDxw9AQ0= @@ -69,6 +84,7 @@ github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANyt github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= +github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/jtolds/gls v4.20.0+incompatible h1:xdiiI2gbIgH/gLH7ADydsJ1uDOEzR8yvV7C0MuV77Wo= github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU= github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w= @@ -93,6 +109,11 @@ github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/mapstructure v1.1.2 h1:fmNYVwqnSfB9mZU6OS2O6GsXM+wcskZDuKQzvN1EDeE= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= +github.com/mitchellh/mapstructure v1.2.2 h1:dxe5oCinTXiTIcfgmZecdCzPmAJKd46KsCWc35r0TV4= +github.com/mitchellh/mapstructure v1.2.2/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/reflect2 v0.0.0-20180701023420-4b7aa43c6742/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= +github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= @@ -105,8 +126,8 @@ github.com/onsi/gomega v1.9.0 h1:R1uwffexN6Pr340GtYRIdZmAiN4J+iw6WG4wog1DUXg= github.com/onsi/gomega v1.9.0/go.mod h1:Ho0h+IUsWyvy1OpqCwxlQ/21gkhVunqlU8fDGcoTdcA= github.com/pelletier/go-toml v1.2.0 h1:T5zMGML61Wp+FlcbWjRDT7yAxhJNAiPPLOFECq181zc= github.com/pelletier/go-toml v1.2.0/go.mod h1:5z9KED0ma1S8pY6P1sdut58dfprrGBbd/94hg7ilaic= -github.com/pelletier/go-toml v1.6.0 h1:aetoXYr0Tv7xRU/V4B4IZJ2QcbtMUFoNb3ORp7TzIK4= -github.com/pelletier/go-toml v1.6.0/go.mod h1:5N711Q9dKgbdkxHL+MEfF31hpT7l0S0s/t2kKREewys= +github.com/pelletier/go-toml v1.7.0 h1:7utD74fnzVc/cpcyy8sjrlFr5vYpypUixARcHIMIGuI= +github.com/pelletier/go-toml v1.7.0/go.mod h1:vwGMzjaWMwyfHwgIBhI2YUM4fB6nL6lVAvS1LBMMhTE= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= @@ -141,8 +162,8 @@ github.com/spf13/cast v1.3.0 h1:oget//CVOEoFewqQxwr0Ej5yjygnqGkvggSE/gB35Q8= github.com/spf13/cast v1.3.0/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= github.com/spf13/cast v1.3.1 h1:nFm6S0SMdyzrzcmThSipiEubIDy8WEXKNZ0UOgiRpng= github.com/spf13/cast v1.3.1/go.mod h1:Qx5cxh0v+4UWYiBimWS+eyWzqEqokIECu5etghLkUJE= -github.com/spf13/cobra v0.0.6 h1:breEStsVwemnKh2/s6gMvSdMEkwW0sK8vGStnlVBMCs= -github.com/spf13/cobra v0.0.6/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= +github.com/spf13/cobra v1.0.0 h1:6m/oheQuQ13N9ks4hubMG6BnvwOeaJrqSPLahSnczz8= +github.com/spf13/cobra v1.0.0/go.mod h1:/6GTrnGXV9HjY+aR4k0oJ5tcvakLuG6EuKReYlHNrgE= github.com/spf13/jwalterweatherman v1.0.0/go.mod h1:cQK4TGJAtQXfYWX+Ddv3mKDzgVb68N+wFjFa4jdeBTo= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= @@ -151,11 +172,12 @@ github.com/spf13/pflag v1.0.3/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnIn github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= github.com/spf13/viper v1.4.0/go.mod h1:PTJ7Z/lr49W6bUbkmS1V3by4uWynFiR9p7+dSq/yZzE= -github.com/spf13/viper v1.6.2 h1:7aKfF+e8/k68gda3LOjo5RxiUqddoFxVq4BKBPrxk5E= -github.com/spf13/viper v1.6.2/go.mod h1:t3iDnF5Jlj76alVNuyFBk5oUMCvsrkbvZK0WQdfDi5k= +github.com/spf13/viper v1.6.3 h1:pDDu1OyEDTKzpJwdq4TiuLyMsUgRa/BT5cn5O62NoHs= +github.com/spf13/viper v1.6.3/go.mod h1:jUMtyi0/lB5yZH/FjyGAoH7IMNrIhlBf6pXZmbMDvzw= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= +github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.4.0 h1:2E4SXV/wtOkTonXsotYi4li6zVWxYlZuYNCXe9XRJyk= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4= @@ -166,17 +188,13 @@ github.com/tmc/grpc-websocket-proxy v0.0.0-20190109142713-0ad062ec5ee5/go.mod h1 github.com/ugorji/go v1.1.4/go.mod h1:uQMGLiO92mf5W77hV/PUCpI3pbzQx3CRekS0kk+RGrc= github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/xordataexchange/crypt v0.0.3-0.20170626215501-b2862e3d0a77/go.mod h1:aYKd//L2LvnjZzWKhF00oedf4jCCReLcmhLdhm1A27Q= -gitlab.com/elixxir/comms v0.0.0-20200407204710-45e8b09c39b8 h1:W0AvXYUZPLXp9vh+pauMSVJHQnKxImr84owdV6sjmOU= -gitlab.com/elixxir/comms v0.0.0-20200407204710-45e8b09c39b8/go.mod h1:MwHh/nBO405n0ELW11tnetpDj1oh5Un+RFThxIngFJc= gitlab.com/elixxir/comms v0.0.0-20200415204952-6d63dd94a0ea h1:LVQuXjq26hjnuw2isxSpYHFBql80VLnhcrNQ+IJ4QH0= gitlab.com/elixxir/comms v0.0.0-20200415204952-6d63dd94a0ea/go.mod h1:EeRHiSIfVYqTaMZGb6eE2kz1Rw5zGXuuBow4Pi2VFg0= -gitlab.com/elixxir/crypto v0.0.0-20200229000841-b1ee7117a1d0 h1:2oq/Y1pA3bwCdlRjhFjoOtb2somDY+wYz2vuiOQkVVA= -gitlab.com/elixxir/crypto v0.0.0-20200229000841-b1ee7117a1d0/go.mod h1:QPClJr3F90ejz6iHaCZuhexytd6PP97dDnt93iRCTDo= +gitlab.com/elixxir/crypto v0.0.0-20200410231849-90e859940f5d h1:+g7tGMO3g20Su3pdTJg30n5XhEGZ3avEd4ccN33CtdU= gitlab.com/elixxir/crypto v0.0.0-20200410231849-90e859940f5d/go.mod h1:QPClJr3F90ejz6iHaCZuhexytd6PP97dDnt93iRCTDo= gitlab.com/elixxir/primitives v0.0.0-20200218211222-4193179f359c h1:hZy85jE7bPyTp1ap57g4cUilPLXLuebVbsFq/JwNN+U= gitlab.com/elixxir/primitives v0.0.0-20200218211222-4193179f359c/go.mod h1:REJMcwIcyxh74VSHqy4S9yYiaEsQYObOPglRExDpk14= -gitlab.com/elixxir/primitives v0.0.0-20200401200647-87f186de48f6 h1:YABap+4YeRE4kk6JRRtYRf52hWuMyqRJtiMGNsaksTY= -gitlab.com/elixxir/primitives v0.0.0-20200401200647-87f186de48f6/go.mod h1:nqC90Tt1jLDd2e35hVWCmTynTXBBEt3UfaSsIhHevmc= +gitlab.com/elixxir/primitives v0.0.0-20200410231944-a57d71d577c9 h1:KP/BQqOrLcoCah24VHzQBY8EbQHvBcsrhz/Yjs6vn4Y= gitlab.com/elixxir/primitives v0.0.0-20200410231944-a57d71d577c9/go.mod h1:nqC90Tt1jLDd2e35hVWCmTynTXBBEt3UfaSsIhHevmc= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= @@ -189,6 +207,8 @@ golang.org/x/crypto v0.0.0-20200117160349-530e935923ad/go.mod h1:LzIPMQfyMNhhGPh golang.org/x/crypto v0.0.0-20200221231518-2aa609cf4a9d/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59 h1:3zb4D3T4G8jdExgVU/95+vQXfpEPiMdCaZgmGVxjNHM= golang.org/x/crypto v0.0.0-20200323165209-0ec3e9974c59/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= +golang.org/x/crypto v0.0.0-20200420192832-a76a400e3025 h1:U821GPBRQKVHpr6Os5UDBgF2FveIjlZYbz+e2n5wmmQ= +golang.org/x/crypto v0.0.0-20200420192832-a76a400e3025/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= @@ -204,6 +224,8 @@ golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn golang.org/x/net v0.0.0-20190522155817-f3200d17e092/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= golang.org/x/net v0.0.0-20200301022130-244492dfa37a h1:GuSPYbZzB5/dcLNCwLQLsg3obCJtX9IJhpXkvY7kzk0= golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e h1:3G+cUijn7XD+S4eJFddp53Pv7+slrESplyjG25HgL+k= +golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= @@ -216,11 +238,15 @@ golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5h golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191120155948-bd437916bb0e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d h1:nc5K6ox/4lTFbMVSL9WRR81ixkcwXThoiF6yf+R9scA= golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs= @@ -242,13 +268,24 @@ google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoA google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20200309141739-5b75447e413d h1:VQ0pz4dAUaMWcQLM7tGY8Nk691kSrlGPyF5nSgAIw2g= google.golang.org/genproto v0.0.0-20200309141739-5b75447e413d/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200420144010-e5e8543f8aeb h1:nAFaltAMbNVA0rixtwvdnqgSVLX3HFUUvMkEklmzbYM= +google.golang.org/genproto v0.0.0-20200420144010-e5e8543f8aeb/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.21.0 h1:G+97AoqBnmZIT91cLG/EkCoK9NSelj64P8bOHHNmGn0= google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.1 h1:zvIju4sqAGvwKspUQOhwnpcqSbzi7/H6QomNNjTL4sk= google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= +google.golang.org/grpc v1.28.1 h1:C1QC6KzgSiLyBabDi87BbjaGreoRgGUF5nOyvfrAZ1k= +google.golang.org/grpc v1.28.1/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= +google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= +google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= +google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= +google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= +google.golang.org/protobuf v1.21.0 h1:qdOKuR/EIArgaWNjetjgTzgVTAZ+S/WXVrq9HW9zimw= +google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= gopkg.in/alecthomas/kingpin.v2 v2.2.6/go.mod h1:FMv+mEhP44yOT+4EoQTLFTRgOQ1FBLkstjWtayDeSgw= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= @@ -260,8 +297,8 @@ gopkg.in/fsnotify.v1 v1.4.7 h1:xOHLXZwVvI9hhs+cLKq5+I5onOuwQLhQwiu63xxlHs4= gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= gopkg.in/ini.v1 v1.51.0 h1:AQvPpx3LzTDM0AjnIRlVFwFFGC+npRopjZxLJj6gdno= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/ini.v1 v1.54.0 h1:oM5ElzbIi7gwLnNbPX2M25ED1vSAK3B6dex50eS/6Fs= -gopkg.in/ini.v1 v1.54.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= +gopkg.in/ini.v1 v1.55.0 h1:E8yzL5unfpW3M6fz/eB7Cb5MQAYSZ7GKo4Qth+N2sgQ= +gopkg.in/ini.v1 v1.55.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/resty.v1 v1.12.0/go.mod h1:mDo4pnntr5jdWRML875a/NmxYqAlA73dVijT2AXvQQo= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= diff --git a/scheduling/simple/createRound.go b/scheduling/simple/createRound.go new file mode 100644 index 0000000000000000000000000000000000000000..1d824b97d7beb84c88885cc52f1429c859adc7d5 --- /dev/null +++ b/scheduling/simple/createRound.go @@ -0,0 +1,75 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package simple + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/comms/connect" + "gitlab.com/elixxir/crypto/shuffle" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/registration/storage" + "gitlab.com/elixxir/registration/storage/node" + "strconv" +) + +// createRound.go contains the logic to construct a team for a round and +// add that round to the network state + +// createRound.go builds a team for a round out of a pool and round id and places +// this round into the network state +func createRound(params Params, pool *waitingPoll, roundID id.Round, + state *storage.NetworkState) (protoRound, error) { + //get the nodes for the team + nodes := pool.Clear() + + var newRound protoRound + + //build the topology + nodeMap := state.GetNodeMap() + nodeStateList := make([]*node.State, params.TeamSize) + orderedNodeList := make([]*id.Node, params.TeamSize) + + if params.RandomOrdering { + + // Input an incrementing array of ints + randomIndex := make([]uint64, params.TeamSize) + for i := range randomIndex { + randomIndex[i] = uint64(i) + } + + // Shuffle array of ints randomly using Fisher-Yates shuffle + // https://en.wikipedia.org/wiki/Fisher%E2%80%93Yates_shuffle + shuffle.Shuffle(&randomIndex) + for i, nid := range nodes { + n := nodeMap.GetNode(nid) + nodeStateList[i] = n + // Use the shuffled array as an indexing order for + // the nodes' topological order + orderedNodeList[randomIndex[i]] = nid + } + } else { + for i, nid := range nodes { + n := nodeMap.GetNode(nid) + nodeStateList[i] = n + position, err := strconv.Atoi(n.GetOrdering()) + if err != nil { + return protoRound{}, errors.WithMessagef(err, + "Could not parse ordering info ('%s') from node %s", + n.GetOrdering(), nid) + } + + orderedNodeList[position] = nid + } + } + + newRound.topology = connect.NewCircuit(orderedNodeList) + newRound.ID = roundID + newRound.batchSize = params.BatchSize + newRound.nodeStateList = nodeStateList + + return newRound, nil + +} diff --git a/scheduling/simple/createRound_test.go b/scheduling/simple/createRound_test.go new file mode 100644 index 0000000000000000000000000000000000000000..bbb7a77386a848188579e475d32c893bcd29f61d --- /dev/null +++ b/scheduling/simple/createRound_test.go @@ -0,0 +1,218 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package simple + +import ( + "crypto/rand" + "gitlab.com/elixxir/comms/connect" + "gitlab.com/elixxir/crypto/signature/rsa" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/registration/storage" + "gitlab.com/elixxir/registration/storage/node" + "reflect" + "strconv" + "testing" +) + +// Happy path +func TestCreateRound_NonRandom(t *testing.T) { + // Build params for scheduling + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + // Build network state + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + // Build node list + nodeList := make([]*id.Node, testParams.TeamSize) + nodeStateList := make([]*node.State, testParams.TeamSize) + + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nid := id.NewNodeFromUInt(i, t) + nodeList[i] = nid + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + nodeState := testState.GetNodeMap().GetNode(nid) + nodeStateList[i] = nodeState + + } + + expectedTopology := connect.NewCircuit(nodeList) + + // Build pool + testPool := &waitingPoll{ + pool: nodeList, + position: int(testParams.TeamSize), + } + + roundID := NewRoundID(0) + + testProtoRound, err := createRound(testParams, testPool, roundID.Get(), testState) + if err != nil { + t.Errorf("Happy path of createRound failed: %v", err) + } + + if testProtoRound.ID != roundID.Get() { + t.Errorf("ProtoRound's id returned unexpected value!"+ + "\n\tExpected: %d"+ + "\n\tReceived: %d", roundID.Get(), testProtoRound.ID) + } + + if !reflect.DeepEqual(testProtoRound.topology, expectedTopology) { + t.Errorf("ProtoRound's topology returned unexpected value!"+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", expectedTopology, testProtoRound.topology) + } + + if testParams.BatchSize != testProtoRound.batchSize { + t.Errorf("ProtoRound's batchsize returned unexpected value!"+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", testParams.BatchSize, testProtoRound.batchSize) + + } + if !reflect.DeepEqual(testProtoRound.nodeStateList, nodeStateList) { + t.Errorf("ProtoRound's nodeStateList returned unexpected value!"+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", nodeStateList, testProtoRound.nodeStateList) + + } + +} + +// Error path: Provide a node ordering that is invalid +func TestCreateRound_BadOrdering(t *testing.T) { + // Build scheduling params + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + // Build network state + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + // Build a node list that will be invalid + nodeList := make([]*id.Node, testParams.TeamSize) + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nodeList[i] = id.NewNodeFromUInt(i, t) + // Input an invalid ordering to node + err := testState.GetNodeMap().AddNode(nodeList[i], "BadNumber") + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + } + + // Build pool + testPool := &waitingPoll{ + pool: nodeList, + position: int(testParams.TeamSize), + } + + roundID := NewRoundID(0) + + // Invalid ordering will cause this to fail + _, err = createRound(testParams, testPool, roundID.Get(), testState) + if err != nil { + return + } + + t.Errorf("Expected error case: passed in an ordering to nodes which were not numbers should result " + + "in an error") + +} + +// Happy path for random ordering +func TestCreateRound_RandomOrdering(t *testing.T) { + // Build scheduling params + testParams := Params{ + TeamSize: 10, + BatchSize: 32, + RandomOrdering: true, + } + + // Build network state + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + // Build the nodes + nodeList := make([]*id.Node, testParams.TeamSize) + nodeStateList := make([]*node.State, testParams.TeamSize) + + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nid := id.NewNodeFromUInt(i, t) + nodeList[i] = nid + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + nodeState := testState.GetNodeMap().GetNode(nid) + nodeStateList[i] = nodeState + + } + + initialTopology := connect.NewCircuit(nodeList) + + testPool := &waitingPoll{ + pool: nodeList, + position: int(testParams.TeamSize), + } + + roundID := NewRoundID(0) + + testProtoRound, err := createRound(testParams, testPool, roundID.Get(), testState) + if err != nil { + t.Errorf("Happy path of createRound failed: %v", err) + } + + // Check that shuffling has actually occurred + // This has a chance to fail even when successful, however that chance is 1 in ~3.6 million + if reflect.DeepEqual(initialTopology, testProtoRound.topology) { + t.Errorf("Highly unlikely initial topology identical to resulting after shuffling. " + + "Possile shuffling is broken") + } + + if testProtoRound.ID != roundID.Get() { + t.Errorf("ProtoRound's id returned unexpected value!"+ + "\n\tExpected: %d"+ + "\n\tReceived: %d", roundID.Get(), testProtoRound.ID) + } + + if testParams.BatchSize != testProtoRound.batchSize { + t.Errorf("ProtoRound's batchsize returned unexpected value!"+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", testParams.BatchSize, testProtoRound.batchSize) + + } + if !reflect.DeepEqual(testProtoRound.nodeStateList, nodeStateList) { + t.Errorf("ProtoRound's nodeStateList returned unexpected value!"+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", nodeStateList, testProtoRound.nodeStateList) + + } + +} diff --git a/scheduling/simple/nodeStateChange.go b/scheduling/simple/nodeStateChange.go new file mode 100644 index 0000000000000000000000000000000000000000..a256067ba6197c3df1e7a4844d636ee137e7b36f --- /dev/null +++ b/scheduling/simple/nodeStateChange.go @@ -0,0 +1,100 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package simple + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/elixxir/registration/storage" + "time" +) + +// HandleNodeStateChange handles the node state changes. +// A node in waiting is added to the pool in preparation for precomputing. +// A node in standby is added to a round in preparation for realtime. +// A node in completed waits for all other nodes in the team to transition +// before the round is updated. +func HandleNodeStateChange(update *storage.NodeUpdateNotification, pool *waitingPoll, + state *storage.NetworkState, realtimeDelay time.Duration) error { + //get node and round information + n := state.GetNodeMap().GetNode(update.Node) + + // when a node poll is received, the nodes polling lock is taken. If there + // is no update, it is released in the endpoint, otherwise it is released + // here which blocks all future polls until processing completes + defer n.GetPollingLock().Unlock() + + hasRound, r := n.GetCurrentRound() + + switch update.To { + case current.NOT_STARTED: + // Do nothing + case current.WAITING: + + // Clear the round if node has one (it should unless it is + // coming from NOT_STARTED + if hasRound { + n.ClearRound() + } + err := pool.Add(update.Node) + if err != nil { + return errors.WithMessage(err, "Waiting pool should never fill") + } + case current.PRECOMPUTING: + // Do nothing + case current.STANDBY: + + if !hasRound { + return errors.Errorf("Node %s without round should "+ + "not be in %s state", update.Node, states.PRECOMPUTING) + } + + stateComplete := r.NodeIsReadyForTransition() + if stateComplete { + err := r.Update(states.REALTIME, time.Now().Add(realtimeDelay)) + if err != nil { + return errors.WithMessagef(err, + "Could not move round %v from %s to %s", + r.GetRoundID(), states.PRECOMPUTING, states.REALTIME) + } + err = state.AddRoundUpdate(r.BuildRoundInfo()) + if err != nil { + return errors.WithMessagef(err, "Could not issue "+ + "update for round %v transitioning from %s to %s", + r.GetRoundID(), states.PRECOMPUTING, states.REALTIME) + } + } + case current.REALTIME: + // Do nothing + case current.COMPLETED: + + if !hasRound { + return errors.Errorf("Node %s without round should "+ + "not be in %s state", update.Node, states.COMPLETED) + } + + n.ClearRound() + + stateComplete := r.NodeIsReadyForTransition() + if stateComplete { + err := r.Update(states.COMPLETED, time.Now()) + if err != nil { + return errors.WithMessagef(err, + "Could not move round %v from %s to %s", + r.GetRoundID(), states.REALTIME, states.COMPLETED) + } + err = state.AddRoundUpdate(r.BuildRoundInfo()) + if err != nil { + return errors.WithMessagef(err, "Could not issue "+ + "update for round %v transitioning from %s to %s", + r.GetRoundID(), states.REALTIME, states.COMPLETED) + } + } + } + + return nil +} diff --git a/scheduling/simple/nodeStateChange_test.go b/scheduling/simple/nodeStateChange_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4478d9adedf7befe5f9ac26fffb3421c761eda5a --- /dev/null +++ b/scheduling/simple/nodeStateChange_test.go @@ -0,0 +1,364 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package simple + +import ( + "crypto/rand" + "gitlab.com/elixxir/comms/connect" + "gitlab.com/elixxir/crypto/signature/rsa" + "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/registration/storage" + "gitlab.com/elixxir/registration/storage/round" + "strconv" + "testing" +) + +// Happy path for transitioning to waiting +func TestHandleNodeStateChance_Waiting(t *testing.T) { + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + nodeList := make([]*id.Node, testParams.TeamSize) + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nodeList[i] = id.NewNodeFromUInt(i, t) + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + } + + roundID := NewRoundID(0).Get() + + // Set a round for the node in order to fully test the code path for + // a waiting transition + roundState := round.NewState_Testing(roundID, 0, t) + _ = testState.GetNodeMap().GetNode(nodeList[0]).SetRound(roundState) + + // Unfilled poll s.t. we can add a node to the waiting pool + testPool := newWaitingPool(int(testParams.TeamSize)) + + testUpdate := &storage.NodeUpdateNotification{ + Node: nodeList[0], + From: current.NOT_STARTED, + To: current.WAITING} + + testState.GetNodeMap().GetNode(nodeList[0]).GetPollingLock().Lock() + + err = HandleNodeStateChange(testUpdate, testPool, testState, 0) + if err != nil { + t.Errorf("Happy path received error: %v", err) + } +} + +// Happy path for transitioning to waiting +func TestHandleNodeStateChance_WaitingError(t *testing.T) { + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + nodeList := make([]*id.Node, testParams.TeamSize) + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nodeList[i] = id.NewNodeFromUInt(i, t) + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + } + + // Unfilled poll s.t. we can add a node to the waiting pool + testPool := &waitingPoll{ + pool: nodeList, + position: int(testParams.TeamSize), + } + + testUpdate := &storage.NodeUpdateNotification{ + Node: nodeList[0], + From: current.NOT_STARTED, + To: current.WAITING} + + testState.GetNodeMap().GetNode(nodeList[0]).GetPollingLock().Lock() + + err = HandleNodeStateChange(testUpdate, testPool, testState, 0) + if err != nil { + return + } + + t.Errorf("Should fail when trying to insert node into a full pool") +} + +// Happy path +func TestHandleNodeStateChance_Standby(t *testing.T) { + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + nodeList := make([]*id.Node, testParams.TeamSize-1) + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nodeList[i] = id.NewNodeFromUInt(i, t) + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + } + circuit := connect.NewCircuit(nodeList) + + roundID := NewRoundID(0) + + roundState, err := testState.GetRoundMap().AddRound(roundID.Get(), testParams.BatchSize, circuit) + if err != nil { + t.Errorf("Failed to add round: %v", err) + } + + // Unfilled poll s.t. we can add a node to the waiting pool + testPool := newWaitingPool(int(testParams.TeamSize)) + + for i := range nodeList { + testUpdate := &storage.NodeUpdateNotification{ + Node: nodeList[i], + From: current.NOT_STARTED, + To: current.WAITING, + } + + testState.GetNodeMap().GetNode(nodeList[i]).GetPollingLock().Lock() + + err = HandleNodeStateChange(testUpdate, testPool, testState, 0) + if err != nil { + t.Errorf("Waiting pool is full for %d: %v", i, err) + } + } + + // Iterate through all the nodes so that all the nodes are ready for transition + for i := range nodeList { + _ = testState.GetNodeMap().GetNode(nodeList[i]).SetRound(roundState) + + testUpdate := &storage.NodeUpdateNotification{ + Node: nodeList[i], + From: current.WAITING, + To: current.STANDBY} + + testState.GetNodeMap().GetNode(nodeList[i]).GetPollingLock().Lock() + + err = HandleNodeStateChange(testUpdate, testPool, testState, 0) + if err != nil { + t.Errorf("Error in standby happy path: %v", err) + } + + } + +} + +// Error path: Do not give a round to the nodes +func TestHandleNodeStateChance_Standby_NoRound(t *testing.T) { + + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + nodeList := make([]*id.Node, testParams.TeamSize) + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nodeList[i] = id.NewNodeFromUInt(i, t) + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + } + + //roundID := NewRoundID(0) + + // Unfilled poll s.t. we can add a node to the waiting pool + testPool := &waitingPoll{ + pool: nodeList, + position: int(testParams.TeamSize), + } + + // Explicitly don't give node a round to reach an error state + //roundState := round.NewState_Testing(roundID.Get(), 0, t) + //_ = testState.GetNodeMap().GetNode(nodeList[0]).SetRound(roundState) + + // Iterate through all the nodes so that all the nodes are ready for transition + for i := range nodeList { + testUpdate := &storage.NodeUpdateNotification{ + Node: nodeList[i], + From: current.WAITING, + To: current.STANDBY} + + testState.GetNodeMap().GetNode(nodeList[i]).GetPollingLock().Lock() + + err = HandleNodeStateChange(testUpdate, testPool, testState, 0) + if err == nil { + t.Errorf("Expected error for %d was not received. Node should not have round", i) + } + + } + +} + +// Happy path +func TestHandleNodeStateChange_Completed(t *testing.T) { + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + nodeList := make([]*id.Node, testParams.TeamSize) + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nodeList[i] = id.NewNodeFromUInt(i, t) + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + } + circuit := connect.NewCircuit(nodeList) + + roundID := NewRoundID(0) + + roundState, err := testState.GetRoundMap().AddRound(roundID.Get(), testParams.BatchSize, circuit) + if err != nil { + t.Errorf("Failed to add round: %v", err) + } + + // Unfilled poll s.t. we can add a node to the waiting pool + testPool := newWaitingPool(int(testParams.TeamSize)) + + for i := range nodeList { + testUpdate := &storage.NodeUpdateNotification{ + Node: nodeList[i], + From: current.NOT_STARTED, + To: current.WAITING, + } + + testState.GetNodeMap().GetNode(nodeList[i]).GetPollingLock().Lock() + + err = HandleNodeStateChange(testUpdate, testPool, testState, 0) + if err != nil { + t.Errorf("Waiting pool is full for %d: %v", i, err) + } + } + + // Iterate through all the nodes so that all the nodes are ready for transition + for i := range nodeList { + _ = testState.GetNodeMap().GetNode(nodeList[i]).SetRound(roundState) + + testUpdate := &storage.NodeUpdateNotification{ + Node: nodeList[i], + From: current.REALTIME, + To: current.COMPLETED} + + testState.GetNodeMap().GetNode(nodeList[i]).GetPollingLock().Lock() + + err = HandleNodeStateChange(testUpdate, testPool, testState, 0) + if err != nil { + t.Errorf("Expected happy path for completed: %v", err) + } + + } +} + +// Error path: attempt to handle a node transition when nodes never had rounds +func TestHandleNodeStateChange_Completed_NoRound(t *testing.T) { + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + nodeList := make([]*id.Node, testParams.TeamSize) + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nodeList[i] = id.NewNodeFromUInt(i, t) + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + } + + //roundID := NewRoundID(0) + + // Unfilled poll s.t. we can add a node to the waiting pool + testPool := &waitingPoll{ + pool: nodeList, + position: int(testParams.TeamSize), + } + + // Iterate through all the nodes so that all the nodes are ready for transition + for i := range nodeList { + testUpdate := &storage.NodeUpdateNotification{ + Node: nodeList[i], + From: current.WAITING, + To: current.COMPLETED} + + testState.GetNodeMap().GetNode(nodeList[i]).GetPollingLock().Lock() + + err = HandleNodeStateChange(testUpdate, testPool, testState, 0) + if err == nil { + t.Errorf("Expected error for %d was not received. Node should not have round", i) + } + + } +} diff --git a/scheduling/simple/pool.go b/scheduling/simple/pool.go new file mode 100644 index 0000000000000000000000000000000000000000..2a57f310a0d09885177ea61ea36419f80d2bf6d5 --- /dev/null +++ b/scheduling/simple/pool.go @@ -0,0 +1,60 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package simple + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/primitives/id" +) + +// pool.go contains the logic for a waiting pool, a simple list of nodes + +// contains a list of nodes of a certain size. does not allow more to be added +// than its max size and all nodes must be removed at once. +type waitingPoll struct { + pool []*id.Node + + position int +} + +//creates an empty waiting of object of the designated size +func newWaitingPool(size int) *waitingPoll { + return &waitingPoll{ + pool: make([]*id.Node, size), + position: 0, + } +} + +// adds an element to the waiting pool if it is not full, otherwise returns an +// error +func (wp *waitingPoll) Add(nid *id.Node) error { + if wp.position == len(wp.pool) { + return errors.New("waiting pool is full") + } + + wp.pool[wp.position] = nid + wp.position += 1 + + return nil +} + +// returns all elements from the waiting pool and clears it +func (wp *waitingPoll) Clear() []*id.Node { + old := wp.pool + wp.pool = make([]*id.Node, len(wp.pool)) + wp.position = 0 + return old +} + +// returns the number of elements currently in the pool +func (wp *waitingPoll) Len() int { + return wp.position +} + +// returns the maximum size of the waiting pool +func (wp *waitingPoll) Size() int { + return len(wp.pool) +} diff --git a/scheduling/simple/pool_test.go b/scheduling/simple/pool_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d0116436c3332737b0801d5b842a8114a89192b0 --- /dev/null +++ b/scheduling/simple/pool_test.go @@ -0,0 +1,148 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package simple + +import ( + "gitlab.com/elixxir/primitives/id" + "reflect" + "testing" +) + +// Happy path +func TestNewPool(t *testing.T) { + testingSize := 5 + receivedPool := newWaitingPool(testingSize) + + if len(receivedPool.pool) != testingSize { + t.Errorf("Pool is not of expected size."+ + "\n\tExpected: %d"+ + "\n\tReceived: %d", testingSize, len(receivedPool.pool)) + } + + if receivedPool.position != 0 { + t.Errorf("Newly created pool should have a position of %d. "+ + "Received position is: %v", 0, receivedPool.position) + } +} + +// Happy path +func TestWaitingPoll_Add(t *testing.T) { + testingSize := 5 + testPoll := newWaitingPool(testingSize) + + // node id with arbitrary initializer + for i := 0; i < testingSize; i++ { + nid := id.NewNodeFromUInt(uint64(i), t) + + err := testPoll.Add(nid) + if err != nil { + t.Errorf("Should not receive error when adding first node to pool: %v", err) + } + if testPoll.position-1 != i { + t.Errorf("Position should increment when adding one node to pool."+ + "\n\tPosition: %d"+ + "\n\tExpected: %d", testPoll.position, i) + } + } + +} + +// Error path: Should not be able to insert a node past maximum +// pool size +func TestWaitingPoll_Add_Full(t *testing.T) { + testingSize := 5 + testPoll := newWaitingPool(testingSize) + + for i := 0; i < testingSize; i++ { + nid := id.NewNodeFromUInt(uint64(i), t) + testPoll.Add(nid) + } + + // Attempt to push an additional node in + nid := id.NewNodeFromUInt(uint64(5), t) + + err := testPoll.Add(nid) + if err == nil { + t.Errorf("Should not be able to add to a full pool") + } + + if testPoll.position != testingSize { + t.Errorf("Position should not increment past the maximin size") + } + +} + +// Happy path +func TestWaitingPoll_Clear(t *testing.T) { + testingSize := 5 + testPoll := newWaitingPool(testingSize) + + nodeList := make([]*id.Node, testingSize) + for i := 0; i < testingSize; i++ { + nid := id.NewNodeFromUInt(uint64(i), t) + testPoll.Add(nid) + nodeList[i] = nid + } + + receivedList := testPoll.Clear() + + if !reflect.DeepEqual(nodeList, receivedList) { + t.Errorf("Node list received from clear did not match expected node list."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", nodeList, receivedList) + } + + emptyNodeList := make([]*id.Node, testingSize) + if !reflect.DeepEqual(testPoll.pool, emptyNodeList) { + t.Errorf("After clearing, waiting pool should not contain any nodes."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", emptyNodeList, testPoll.pool) + } + + if testPoll.position != 0 { + t.Errorf("Waiting pool's position should be reset after clear."+ + "Expected: %v"+ + "Position: %v", 0, testPoll.position) + } +} + +// Happy path +func TestWaitingPoll_Size(t *testing.T) { + testingSize := 5 + testPoll := newWaitingPool(testingSize) + + nid := id.NewNodeFromUInt(uint64(1), t) + testPoll.Add(nid) + + receivedSize := testPoll.Size() + + if receivedSize != testingSize { + t.Errorf("Pool not of expected size") + } +} + +// Happy path +func TestWaitingPoll_Len(t *testing.T) { + testingSize := 5 + testPoll := newWaitingPool(testingSize) + + // node id with arbitrary initializer + for i := 0; i < testingSize; i++ { + nid := id.NewNodeFromUInt(uint64(i), t) + + err := testPoll.Add(nid) + if err != nil { + t.Errorf("Should not receive error when adding first node to pool: %v", err) + } + + receivedLen := testPoll.Len() + if receivedLen != i+1 { + t.Errorf("Expected position not received. "+ + "\n\tExpected: %d"+ + "\n\tReceived: %d", i+1, receivedLen) + } + } +} diff --git a/scheduling/simple/roundID.go b/scheduling/simple/roundID.go new file mode 100644 index 0000000000000000000000000000000000000000..84fd23a3df6e0ac3b1132cf5ffca74f445b54b21 --- /dev/null +++ b/scheduling/simple/roundID.go @@ -0,0 +1,33 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +//this file defines the structure which creates and tracks the RoundID + +package simple + +//tracks the round ID. Only allows itself to incremented forward by 1 +import "gitlab.com/elixxir/primitives/id" + +//creates teh RoundID structure +func NewRoundID(start id.Round) *RoundID { + roundID := RoundID(start) + return &roundID +} + +//defines the RoundID type +type RoundID id.Round + +//Returns the current count and increments to the next one +func (r *RoundID) Next() id.Round { + old := *r + *r += 1 + return id.Round(old) +} + +//returns the current count +func (r *RoundID) Get() id.Round { + return id.Round(*r) +} diff --git a/scheduling/simple/roundID_test.go b/scheduling/simple/roundID_test.go new file mode 100644 index 0000000000000000000000000000000000000000..2240f42a67abe222a76efdb65c9a5989fb0b8f02 --- /dev/null +++ b/scheduling/simple/roundID_test.go @@ -0,0 +1,41 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package simple + +import ( + "gitlab.com/elixxir/primitives/id" + "testing" +) + +func TestNewRoundID(t *testing.T) { + startVal := 1 + startRound := id.Round(startVal) + receivedRound := NewRoundID(startRound) + + // Test get + if receivedRound.Get() != startRound { + t.Errorf("New round does not have expected starting value. "+ + "\n\tReceived value: %d"+ + "\n\tExpected value: %d", receivedRound.Get(), startRound) + } + + oldRound := receivedRound.Next() + + // Test next returns old value + if oldRound != startRound { + t.Errorf("Next did not return expected old value."+ + "\n\tReceived value: %d"+ + "\n\tExpected value: %d", oldRound, startRound) + } + + newRound := receivedRound.Get() + // Test that next incremented value + if newRound != id.Round(startVal+1) { + t.Errorf("Next did not increment value."+ + "\n\tReceived value: %d"+ + "\n\tExpected value: %d", newRound, id.Round(startVal+1)) + } +} diff --git a/scheduling/simple/schedule.go b/scheduling/simple/schedule.go new file mode 100644 index 0000000000000000000000000000000000000000..2eba01e27a5c72651d775afa0e6f03a9a0ce8831 --- /dev/null +++ b/scheduling/simple/schedule.go @@ -0,0 +1,121 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package simple + +import ( + "encoding/json" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/comms/connect" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/registration/storage" + "gitlab.com/elixxir/registration/storage/node" + "time" +) + +// scheduler.go contains the business logic for scheduling a round + +type Params struct { + TeamSize uint32 + BatchSize uint32 + RandomOrdering bool + MinimumDelay time.Duration + //delay in ms for a realtime round to start + RealtimeDelay uint32 +} + +//internal structure which describes a round to be created +type protoRound struct { + topology *connect.Circuit + ID id.Round + nodeStateList []*node.State + batchSize uint32 +} + +//size of round creation channel, just sufficiently large enough to not be jammed +const newRoundChanLen = 100 + +// Scheduler constructs the teaming parameters and sets up the scheduling +func Scheduler(serialParam []byte, state *storage.NetworkState) error { + var params Params + err := json.Unmarshal(serialParam, ¶ms) + if err != nil { + return errors.WithMessage(err, "Could not extract parameters") + } + + return scheduler(params, state) +} + +// scheduler is a utility function which builds a round by handling a node's +// state changes then creating a team from the nodes in the pool +func scheduler(params Params, state *storage.NetworkState) error { + + // pool which tracks nodes which are not in a team + pool := newWaitingPool(int(params.TeamSize)) + + //tracks and incrememnts the round id + roundID := NewRoundID(0) + + //channel to send new rounds over to be created + newRoundChan := make(chan protoRound, newRoundChanLen) + + //channel which the round creation thread returns errors on + errorChan := make(chan error, 1) + + //calculate the realtime delay from params + rtDelay := time.Duration(params.RealtimeDelay) * time.Millisecond + + //begin the thread that starts rounds + go func() { + lastRound := time.Now() + var err error + for newRound := range newRoundChan { + // To avoid back-to-back teaming, we make sure to sleep until the minimum delay + if timeDiff := time.Now().Sub(lastRound); timeDiff < params.MinimumDelay*time.Millisecond { + time.Sleep(timeDiff) + } + lastRound = time.Now() + + err = startRound(newRound, state, errorChan) + if err != nil { + break + } + } + + jww.ERROR.Printf("Round creation thread should never exit: %s", err) + + }() + + //start receiving updates from nodes + for true { + var update *storage.NodeUpdateNotification + select { + case err := <-errorChan: + return err + case update = <-state.GetNodeUpdateChannel(): + } + + //handle the node's state change + err := HandleNodeStateChange(update, pool, state, rtDelay) + if err != nil { + return err + } + + //create a new round if the pool is full + if pool.Len() == int(params.TeamSize) { + newRound, err := createRound(params, pool, roundID.Next(), state) + if err != nil { + return err + } + + //send the round to the new round channel to be created + newRoundChan <- newRound + } + + } + + return errors.New("single scheduler should never exit") +} diff --git a/scheduling/simple/schedule_test.go b/scheduling/simple/schedule_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ca3f043d43280ae15eae484834536d2bd52342fe --- /dev/null +++ b/scheduling/simple/schedule_test.go @@ -0,0 +1,104 @@ +package simple + +import ( + "encoding/json" + "gitlab.com/elixxir/crypto/signature/rsa" + "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/primitives/utils" + "gitlab.com/elixxir/registration/storage" + "gitlab.com/elixxir/registration/testkeys" + "reflect" + "strconv" + "testing" + "time" +) + +// Happy path +func TestScheduler(t *testing.T) { + configJson, err := utils.ReadFile(testkeys.GetSchedulingConfig()) + if err != nil { + t.Errorf("Failed to open %s", testkeys.GetSchedulingConfig()) + } + + var testParams Params + err = json.Unmarshal(configJson, &testParams) + if err != nil { + t.Errorf("Could not extract parameters: %v", err) + } + + // Read in private key + key, err := utils.ReadFile(testkeys.GetCAKeyPath()) + if err != nil { + t.Errorf("failed to read key at %+v: %+v", + testkeys.GetCAKeyPath(), err) + } + + pk, err := rsa.LoadPrivateKeyFromPem(key) + if err != nil { + t.Errorf("Failed to parse permissioning server key: %+v. "+ + "PermissioningKey is %+v", err, pk) + } + // Start registration server + state, err := storage.NewState(pk) + if err != nil { + t.Errorf("Unable to create state: %+v", err) + } + + teamSize := 3 + + nodeList := make([]*id.Node, teamSize) + for i := 0; i < teamSize; i++ { + nid := id.NewNodeFromUInt(uint64(i), t) + nodeList[i] = nid + + err = state.GetNodeMap().AddNode(nid, strconv.Itoa(i)) + if err != nil { + t.Errorf("Failed to add node %d to map: %v", i, err) + } + state.GetNodeMap().GetNode(nodeList[i]).GetPollingLock().Lock() + err = state.NodeUpdateNotification(nid, current.NOT_STARTED, current.WAITING) + if err != nil { + t.Errorf("Failed to update node %d from %s to %s: %v", + i, current.NOT_STARTED, current.WAITING, err) + } + + } + + go func() { + err = Scheduler(configJson, state) + if err != nil { + t.Errorf("Scheduler failed with error: %v", err) + } + }() + + time.Sleep(1 * time.Second) + + roundInfo, err := state.GetUpdates(0) + + if len(roundInfo) == 0 { + t.Errorf("Expected round to start. " + + "Received no round info indicating this") + } + + if err != nil { + t.Errorf("Unexpected error retrieving round info: %v", err) + } + + receivedNodeList, err := id.NewNodeListFromStrings(roundInfo[0].Topology) + if err != nil { + t.Errorf("Failed to convert topology of round info: %v", err) + } + + if !reflect.DeepEqual(receivedNodeList, nodeList) { + t.Errorf("Node list received from round info was not expected."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", nodeList, receivedNodeList) + } + + if roundInfo[0].BatchSize != testParams.BatchSize { + t.Errorf("Batchsize in round info is unexpected value."+ + "\n\tExpected: %v"+ + "\n\tReceived: %v", testParams.BatchSize, roundInfo[0].BatchSize) + } +} diff --git a/scheduling/simple/startRound.go b/scheduling/simple/startRound.go new file mode 100644 index 0000000000000000000000000000000000000000..372f8ab98b0ab73ff12e4e4674364ec2723fdd59 --- /dev/null +++ b/scheduling/simple/startRound.go @@ -0,0 +1,55 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package simple + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/elixxir/registration/storage" + "time" +) + +// startRound is a function which takes the info from createRound and updates the +// node and network states in order to begin the round +func startRound(round protoRound, state *storage.NetworkState, errChan chan<- error) error { + + // Add the round to the manager + r, err := state.GetRoundMap().AddRound(round.ID, round.batchSize, round.topology) + if err != nil { + err = errors.WithMessagef(err, "Failed to create new round %v", round.ID) + errChan <- err + return err + } + + // Move the round to precomputing + err = r.Update(states.PRECOMPUTING, time.Now()) + if err != nil { + err = errors.WithMessagef(err, "Could not move new round into %s", states.PRECOMPUTING) + errChan <- err + return err + } + + // Issue the update to the network state + err = state.AddRoundUpdate(r.BuildRoundInfo()) + if err != nil { + err = errors.WithMessagef(err, "Could not issue "+ + "update to create round %v", r.GetRoundID()) + errChan <- err + return err + } + + // Tag all nodes to the round + for _, n := range round.nodeStateList { + err := n.SetRound(r) + if err != nil { + err = errors.WithMessagef(err, "could not add round %v to node %s", r.GetRoundID(), n.GetID()) + errChan <- err + return err + } + } + + return nil +} diff --git a/scheduling/simple/startRound_test.go b/scheduling/simple/startRound_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d5d554d425d52a26faaebcdac9f2b8bf2830fba6 --- /dev/null +++ b/scheduling/simple/startRound_test.go @@ -0,0 +1,201 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package simple + +import ( + "crypto/rand" + "gitlab.com/elixxir/crypto/signature/rsa" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/elixxir/registration/storage" + "gitlab.com/elixxir/registration/storage/node" + "gitlab.com/elixxir/registration/storage/round" + "strconv" + "testing" +) + +// Happy path +func TestStartRound(t *testing.T) { + // Build params for scheduling + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + // Build network state + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + // Build node list + nodeList := make([]*id.Node, testParams.TeamSize) + nodeStateList := make([]*node.State, testParams.TeamSize) + + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nid := id.NewNodeFromUInt(i, t) + nodeList[i] = nid + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + nodeState := testState.GetNodeMap().GetNode(nid) + nodeStateList[i] = nodeState + + } + + // Build pool + testPool := &waitingPoll{ + pool: nodeList, + position: int(testParams.TeamSize), + } + + roundID := NewRoundID(0) + + testProtoRound, err := createRound(testParams, testPool, roundID.Get(), testState) + if err != nil { + t.Errorf("Happy path of createRound failed: %v", err) + } + + errorChan := make(chan error, 1) + + err = startRound(testProtoRound, testState, errorChan) + if err != nil { + t.Errorf("Received error from startRound(): %v", err) + } + + if testState.GetRoundMap().GetRound(0).GetRoundState() != states.PRECOMPUTING { + t.Errorf("In unexpected state after round creation: %v", + testState.GetRoundMap().GetRound(0).GetRoundState()) + } +} + +// Error path +func TestStartRound_BadState(t *testing.T) { + // Build params for scheduling + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + // Build network state + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + // Build node list + nodeList := make([]*id.Node, testParams.TeamSize) + nodeStateList := make([]*node.State, testParams.TeamSize) + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nid := id.NewNodeFromUInt(i, t) + nodeList[i] = nid + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + nodeState := testState.GetNodeMap().GetNode(nid) + nodeStateList[i] = nodeState + + } + + // Build pool + testPool := &waitingPoll{ + pool: nodeList, + position: int(testParams.TeamSize), + } + + roundID := NewRoundID(0) + + // Manually set the state of the round + badState := round.NewState_Testing(roundID.Get(), states.COMPLETED, t) + testState.GetRoundMap().AddRound_Testing(badState, t) + + testProtoRound, err := createRound(testParams, testPool, roundID.Get(), testState) + if err != nil { + t.Errorf("Happy path of createRound failed: %v", err) + } + + errorChan := make(chan error, 1) + + err = startRound(testProtoRound, testState, errorChan) + if err == nil { + t.Errorf("Expected error. Artificially created round " + + "should make starting precomputing impossible") + } + + if testState.GetRoundMap().GetRound(0).GetRoundState() == states.PRECOMPUTING { + t.Errorf("Should not be in precomputing after artificially incrementign round") + } +} + +// Error path +func TestStartRound_BadNode(t *testing.T) { + // Build params for scheduling + testParams := Params{ + TeamSize: 5, + BatchSize: 32, + RandomOrdering: false, + } + + // Build network state + privKey, _ := rsa.GenerateKey(rand.Reader, 2048) + testState, err := storage.NewState(privKey) + if err != nil { + t.Errorf("Failed to create test state: %v", err) + t.FailNow() + } + + // Build node list + nodeList := make([]*id.Node, testParams.TeamSize) + nodeStateList := make([]*node.State, testParams.TeamSize) + for i := uint64(0); i < uint64(len(nodeList)); i++ { + nid := id.NewNodeFromUInt(i, t) + nodeList[i] = nid + err := testState.GetNodeMap().AddNode(nodeList[i], strconv.Itoa(int(i))) + if err != nil { + t.Errorf("Couldn't add node: %v", err) + t.FailNow() + } + nodeState := testState.GetNodeMap().GetNode(nid) + nodeStateList[i] = nodeState + + } + + // Build pool + testPool := &waitingPoll{ + pool: nodeList, + position: int(testParams.TeamSize), + } + + roundID := NewRoundID(0) + badState := round.NewState_Testing(roundID.Get(), states.COMPLETED, t) + + testProtoRound, err := createRound(testParams, testPool, roundID.Get(), testState) + if err != nil { + t.Errorf("Happy path of createRound failed: %v", err) + } + // Manually set the round of a node + testProtoRound.nodeStateList[0].SetRound(badState) + + errorChan := make(chan error, 1) + + err = startRound(testProtoRound, testState, errorChan) + if err == nil { + t.Log(err) + t.Errorf("Expected error. Artificially created round " + + "should make starting precomputing impossible") + } + +} diff --git a/storage/database.go b/storage/database.go index 2309e7f154138f7cd6cc5adcfcb97dde22a7cbce..7a3b5fd3c28f7b6f4b56227ddc4b95327cc7a3aa 100644 --- a/storage/database.go +++ b/storage/database.go @@ -12,7 +12,9 @@ import ( "github.com/go-pg/pg" "github.com/go-pg/pg/orm" jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/registration/storage/node" "sync" + "time" ) // Struct implementing the Database Interface with an underlying DB @@ -36,9 +38,7 @@ type nodeRegistration interface { InsertNode(id []byte, code, serverAddr, serverCert, gatewayAddress, gatewayCert string) error // Insert Node registration code into the database - InsertNodeRegCode(code string) error - // Count the number of Nodes currently registered - CountRegisteredNodes() (int, error) + InsertNodeRegCode(regCode, order string) error // Get Node information for the given Node registration code GetNode(code string) (*NodeInformation, error) } @@ -78,6 +78,8 @@ type NodeInformation struct { // Registration code acts as the primary key Code string `sql:",pk"` + // Node order string, this is a tag used by the algorithem + Order string // Node ID Id []byte // Server IP address @@ -88,6 +90,8 @@ type NodeInformation struct { NodeCertificate string // Gateway TLS public certificate in PEM string format GatewayCertificate string + // Date/time that the node was registered + DateRegistered time.Time } // Struct representing the User table in the database @@ -131,21 +135,21 @@ func NewDatabase(username, password, database, address string) Storage { regCodeDb := &DatabaseImpl{ db: db, } - nodeMap := nodeRegistration(&MapImpl{ - node: make(map[string]*NodeInformation), + nodeDb := nodeRegistration(&DatabaseImpl{ + db: db, }) jww.INFO.Println("Database backend initialized successfully!") return Storage{ clientRegistration: regCodeDb, - nodeRegistration: nodeMap, + nodeRegistration: nodeDb, } } // Create the database schema func createSchema(db *pg.DB) error { - for _, model := range []interface{}{&RegistrationCode{}, &User{}} { + for _, model := range []interface{}{&RegistrationCode{}, &User{}, &NodeInformation{}} { err := db.CreateTable(model, &orm.CreateTableOptions{ // Ignore create table if already exists? IfNotExists: true, @@ -179,9 +183,9 @@ func PopulateClientRegistrationCodes(codes []string, uses int) { } // Adds Node registration codes to the database -func PopulateNodeRegistrationCodes(codes []string) { - for _, code := range codes { - err := PermissioningDb.InsertNodeRegCode(code) +func PopulateNodeRegistrationCodes(infos []node.Info) { + for _, info := range infos { + err := PermissioningDb.InsertNodeRegCode(info.RegCode, info.Order) if err != nil { jww.ERROR.Printf("Unable to populate Node registration code: %+v", err) diff --git a/storage/node/map.go b/storage/node/map.go new file mode 100644 index 0000000000000000000000000000000000000000..2915a4b183232f012084a47d7a6ef20a5959500d --- /dev/null +++ b/storage/node/map.go @@ -0,0 +1,67 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package node + +import ( + "errors" + "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/id" + "sync" + "time" +) + +// Tracks state of an individual Node in the network +type StateMap struct { + mux sync.RWMutex + + nodeStates map[id.Node]*State +} + +func NewStateMap() *StateMap { + return &StateMap{ + nodeStates: make(map[id.Node]*State), + } +} + +// Adds a new node state to the structure. Will not overwrite an existing one. +func (nsm *StateMap) AddNode(id *id.Node, ordering string) error { + nsm.mux.Lock() + defer nsm.mux.Unlock() + + if _, ok := nsm.nodeStates[*id]; ok { + return errors.New("cannot add a node which already exists") + } + + nsm.nodeStates[*id] = + &State{ + activity: current.NOT_STARTED, + currentRound: nil, + lastPoll: time.Now(), + ordering: ordering, + id: id, + } + + return nil +} + +// Returns the State object for the given id if it exists +func (nsm *StateMap) GetNode(id *id.Node) *State { + nsm.mux.RLock() + defer nsm.mux.RUnlock() + return nsm.nodeStates[*id] +} + +// Returns the number of elements in the NodeMapo +func (nsm *StateMap) Len() int { + nsm.mux.RLock() + defer nsm.mux.RUnlock() + num := 0 + for range nsm.nodeStates { + num++ + } + return num +} diff --git a/storage/node/map_test.go b/storage/node/map_test.go new file mode 100644 index 0000000000000000000000000000000000000000..1f4b33c87c9504a07c350f259ae91e1486b6e699 --- /dev/null +++ b/storage/node/map_test.go @@ -0,0 +1,185 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package node + +import ( + "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/registration/storage/round" + "math/rand" + "strings" + "testing" + "time" +) + +//tests that newStateMap is correct +func TestNewStateMap(t *testing.T) { + sm := NewStateMap() + + if sm.nodeStates == nil { + t.Errorf("Internal map not initilized") + } +} + +//Tests a node is added correctly to the state map when it is +func TestStateMap_AddNode_Happy(t *testing.T) { + sm := &StateMap{ + nodeStates: make(map[id.Node]*State), + } + + nid := id.NewNodeFromUInt(2, t) + + err := sm.AddNode(nid, "") + + if err != nil { + t.Errorf("Error returned on valid addition of node: %s", err) + } + + n := sm.nodeStates[*nid] + + if n.activity != current.NOT_STARTED { + t.Errorf("New node state has wrong activity; "+ + "Expected: %s, Recieved: %s", current.NOT_STARTED, n.activity) + } + + if n.currentRound != nil { + t.Errorf("New node has a curent round set incorrectly") + } + + pollDelta := time.Now().Sub(n.lastPoll) + + if pollDelta < 0 || pollDelta > time.Millisecond { + t.Errorf("timestap on poll is at the wrong time: %v", n.lastPoll) + } +} + +//Tests a node is added correctly to the state map when it is +func TestStateMap_AddNode_Invalid(t *testing.T) { + sm := &StateMap{ + nodeStates: make(map[id.Node]*State), + } + + nid := id.NewNodeFromUInt(2, t) + r := round.NewState_Testing(42, 0, t) + + sm.nodeStates[*nid] = &State{ + activity: current.WAITING, + currentRound: r, + lastPoll: time.Now(), + } + + time.Sleep(1 * time.Millisecond) + + err := sm.AddNode(nid, "") + + if err == nil { + t.Errorf("Error not returned on invalid addition of node: %s", err) + } else if !strings.Contains(err.Error(), "cannot add a node which "+ + "already exists") { + t.Errorf("Incorrect error returned from failed AddNode: %s", err) + } + + n := sm.nodeStates[*nid] + + if n.activity != current.WAITING { + t.Errorf("Extant node state has wrong activity; "+ + "Expected: %s, Recieved: %s", current.WAITING, n.activity) + } + + if n.currentRound == nil || n.currentRound.GetRoundID() != r.GetRoundID() { + t.Errorf("New node has a curent round set incorrectly: "+ + "Expected: %+v; Recieved: %+v", r.GetRoundID(), n.currentRound.GetRoundID()) + + } + + pollDelta := time.Now().Sub(n.lastPoll) + + if pollDelta < time.Millisecond { + t.Errorf("timestap is too new: %v", n.lastPoll) + } +} + +//Tests a node is retrieved correctly when in the state map +func TestStateMap_GetNode_Valid(t *testing.T) { + sm := &StateMap{ + nodeStates: make(map[id.Node]*State), + } + + nid := id.NewNodeFromUInt(2, t) + r := round.NewState_Testing(42, 0, t) + + sm.nodeStates[*nid] = &State{ + activity: current.NOT_STARTED, + currentRound: r, + lastPoll: time.Now(), + } + + n := sm.GetNode(nid) + + if n == nil { + t.Errorf("No node returned when node exists") + } else { + if n.activity != current.NOT_STARTED { + t.Errorf("New node state has wrong activity; "+ + "Expected: %s, Recieved: %s", current.NOT_STARTED, n.activity) + } + + if n.currentRound == nil || n.currentRound.GetRoundID() != r.GetRoundID() { + t.Errorf("New node has a curent round set incorrectly: "+ + "Expected: %+v; Recieved: %+v", r.GetRoundID(), n.currentRound.GetRoundID()) + + } + + pollDelta := time.Now().Sub(n.lastPoll) + + if pollDelta < 0 || pollDelta > time.Millisecond { + t.Errorf("timestap on poll is at the wrong time: %v", n.lastPoll) + } + } + +} + +//Tests a node not is not returned when no node exists +func TestStateMap_GetNode_invalid(t *testing.T) { + sm := &StateMap{ + nodeStates: make(map[id.Node]*State), + } + + nid := id.NewNodeFromUInt(2, t) + + n := sm.GetNode(nid) + + if n != nil { + t.Errorf("Nnode returned when node does not exist") + } +} + +//Tests that len returns the correct value +func TestStateMap_Len(t *testing.T) { + rng := rand.New(rand.NewSource(42)) + + for i := 0; i < 20; i++ { + l := int(rng.Uint64() % 100) + + if i == 0 { + l = 0 + } + + sm := &StateMap{ + nodeStates: make(map[id.Node]*State), + } + + for j := 0; j < l; j++ { + sm.nodeStates[*id.NewNodeFromUInt(uint64(5*j+1), t)] = &State{} + } + + if sm.Len() != l { + t.Errorf("Len returned a length of %v when it should be %v", + sm.Len(), l) + } + } +} diff --git a/storage/node/registration.go b/storage/node/registration.go new file mode 100644 index 0000000000000000000000000000000000000000..328437256de102ad2b947a324dcd757ffa35c6e0 --- /dev/null +++ b/storage/node/registration.go @@ -0,0 +1,40 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package node + +import ( + "encoding/json" + "github.com/pkg/errors" + "gitlab.com/elixxir/primitives/utils" +) + +type Info struct { + RegCode string + Order string +} + +// LoadInfo opens a JSON file and marshals it into a slice of Info. An error is +// returned when an issue is encountered reading the JSON file or unmarshaling +// the data. +func LoadInfo(filePath string) ([]Info, error) { + // Data loaded from file will be stored here + var infos []Info + + // Open file and get the JSON data + jsonData, err := utils.ReadFile(filePath) + if err != nil { + return nil, errors.Errorf("Could not load JSON file: %v", err) + } + + // Unmarshal the JSON data + err = json.Unmarshal(jsonData, &infos) + if err != nil { + return nil, errors.Errorf("Could not unmarshal JSON: %v", err) + } + + return infos, nil +} diff --git a/storage/node/registration_test.go b/storage/node/registration_test.go new file mode 100644 index 0000000000000000000000000000000000000000..03f348cfec9674658046d9073b261052f2400c6d --- /dev/null +++ b/storage/node/registration_test.go @@ -0,0 +1,122 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package node + +import ( + "gitlab.com/elixxir/primitives/utils" + "os" + "reflect" + "testing" +) + +// Tests that LoadInfo() correctly reads and unmarshals data from a file. +func TestLoadInfo(t *testing.T) { + // Set up expected values + filePath := "testRegCodes.json" + testData := []byte("[{\"RegCode\":\"GMWJGSAPGA\",\"Order\":\"TLMODYLUMG\"" + + "},{\"RegCode\":\"UQDIWWMNPP\",\"Order\":\"QIMGEGMGKB\"},{\"RegCode\"" + + ":\"NPTNIWDYJD\",\"Order\":\"HVOYAFNNDF\"},{\"RegCode\":\"BKFGQDLCIV" + + "\",\"Order\":\"NBCSWTCNVU\"},{\"RegCode\":\"XDMIQJISQC\",\"Order\":" + + "\"DLVHPDCSFX\"},{\"RegCode\":\"ABPHMKKKPH\",\"Order\":\"XDQHJWRMVT\"" + + "}]") + testInfos := []Info{ + {RegCode: "GMWJGSAPGA", Order: "TLMODYLUMG"}, + {RegCode: "UQDIWWMNPP", Order: "QIMGEGMGKB"}, + {RegCode: "NPTNIWDYJD", Order: "HVOYAFNNDF"}, + {RegCode: "BKFGQDLCIV", Order: "NBCSWTCNVU"}, + {RegCode: "XDMIQJISQC", Order: "DLVHPDCSFX"}, + {RegCode: "ABPHMKKKPH", Order: "XDQHJWRMVT"}, + } + + // Defer deletion of the test JSON file + defer func() { + // Clean up temporary test file + err := os.RemoveAll(filePath) + if err != nil { + t.Fatalf("Error deleting test JSON file %s:\n\t%v", filePath, err) + } + }() + + // Create test JSON file + err := utils.WriteFile(filePath, testData, utils.FilePerms, utils.DirPerms) + if err != nil { + t.Fatalf("Error creating test JSON file %s:\n\t%v", filePath, err) + } + + // Call LoadInfo() + infos, err := LoadInfo(filePath) + if err != nil { + t.Errorf("LoadInfo() encountered an error loading the JSON from"+ + " %s:\n\t%v", filePath, err) + } + + // Compare received Info slice to expected + if !reflect.DeepEqual(infos, testInfos) { + t.Errorf("LoadInfo() marshaled the JSON data incorrectly."+ + "\n\texpected: %+v\n\treceived: %+v", testInfos, infos) + } +} + +// Tests that LoadInfo produces an error when given an invalid file path. +func TestLoadInfo_FileError(t *testing.T) { + // Set up expected values + filePath := "testRegCodes.json" + var testInfos []Info + expectedErr := "Could not load JSON file: open testRegCodes.json: The " + + "system cannot find the file specified." + + infos, err := LoadInfo(filePath) + if err == nil { + t.Errorf("LoadInfo() did not encounter an error when it should "+ + "have while loading the JSON from %s\n\texpected: %s\n\treceived: %v", + filePath, expectedErr, err) + } + + if !reflect.DeepEqual(infos, testInfos) { + t.Errorf("LoadInfo() should have returned empty []Info."+ + "\n\texpected: %+v\n\treceived: %+v", testInfos, infos) + } +} + +// Tests that LoadInfo() produces an error when given invalid JSON. +func TestLoadInfo_JsonError(t *testing.T) { + // Set up expected values + filePath := "testRegCodes.json" + testData := []byte("This is invalid JSON.") + expectedErr := "Could not unmarshal JSON: invalid character 'T' looking " + + "for beginning of value" + var testInfos []Info + + // Defer deletion of the test JSON file + defer func() { + // Clean up temporary test file + err := os.RemoveAll(filePath) + if err != nil { + t.Fatalf("Error deleting test JSON file %s:\n\t%v", filePath, err) + } + }() + + // Create test JSON file + err := utils.WriteFile(filePath, testData, utils.FilePerms, utils.DirPerms) + if err != nil { + t.Fatalf("Error creating test JSON file %s:\n\t%v", filePath, err) + } + + // Call LoadInfo() + infos, err := LoadInfo(filePath) + if err == nil { + t.Errorf("LoadInfo() did not encounter an error when it should "+ + "have while unmarshaling the JSON from %s\n\texpected: %s\n\treceived: %v", + filePath, expectedErr, err) + } + + // Compare received Info slice to expected + if !reflect.DeepEqual(infos, testInfos) { + t.Errorf("LoadInfo() should have returned empty []Info."+ + "\n\texpected: %+v\n\treceived: %+v", testInfos, infos) + } +} diff --git a/storage/node/state.go b/storage/node/state.go new file mode 100644 index 0000000000000000000000000000000000000000..91d4aa7507b7c5c7461e32f736291c7cbcb2f1d6 --- /dev/null +++ b/storage/node/state.go @@ -0,0 +1,171 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package node + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/registration/storage/round" + "gitlab.com/elixxir/registration/transition" + "sync" + "time" +) + +// Tracks state of an individual Node in the network +type State struct { + mux sync.RWMutex + + // Current activity as reported by the Node + activity current.Activity + + //nil if not in a round, otherwise holds the round the node is in + currentRound *round.State + + // Timestamp of the last time this Node polled + lastPoll time.Time + + // Order string to be used in team configuration + ordering string + + //holds valid state transitions + stateMap *[][]bool + + //id of the node + id *id.Node + + // when a node poll is received, this nodes polling lock is. If + // there is no update, it is released in this endpoint, otherwise it is + // released in the scheduling algorithm which blocks all future polls until + // processing completes + //FIXME: it is possible that polling lock and registration lock + // do the same job and could conflict. reconsiderations of this logic + // may be fruitfull + pollingLock sync.Mutex +} + +// updates to the passed in activity if it is different from the known activity +// returns true if the state changed and the state was it was reguardless +func (n *State) Update(newActivity current.Activity) (bool, current.Activity, error) { + // Get and lock n state + n.mux.Lock() + defer n.mux.Unlock() + + // update n poll timestamp + n.lastPoll = time.Now() + + oldActivity := n.activity + + //if the activity is the one that the node is already in, do nothing + if oldActivity == newActivity { + return false, oldActivity, nil + } + + //check that teh activity transition is valid + valid := transition.Node.IsValidTransition(newActivity, oldActivity) + + if !valid { + return false, oldActivity, + errors.Errorf("node update from %s to %s failed, "+ + "invalid transition", oldActivity, newActivity) + } + + // check that the state of the round the node is assoceated with is correct + // for the transition + if transition.Node.NeedsRound(newActivity) == transition.Yes { + if n.currentRound == nil { + return false, oldActivity, + errors.Errorf("node update from %s to %s failed, "+ + "requires the node be assigned a round", oldActivity, + newActivity) + } + + if n.currentRound.GetRoundState() != transition.Node.RequiredRoundState(newActivity) { + return false, oldActivity, + errors.Errorf("node update from %s to %s failed, "+ + "requires the node's be assigned a round to be in the "+ + "correct state; Assigned: %s, Expected: %s", oldActivity, + newActivity, n.currentRound.GetRoundState(), + transition.Node.RequiredRoundState(oldActivity)) + } + } + + //check that the node doesnt have a round if it shouldn't + if transition.Node.NeedsRound(newActivity) == transition.No && n.currentRound != nil { + return false, oldActivity, + errors.Errorf("node update from %s to %s failed, "+ + "requires the node not be assigned a round", oldActivity, + newActivity) + } + + // change the node's activity + n.activity = newActivity + + return true, oldActivity, nil +} + +// gets the current activity of the node +func (n *State) GetActivity() current.Activity { + n.mux.RLock() + defer n.mux.RUnlock() + return n.activity +} + +// gets the timestap of the last time the node polled +func (n *State) GetLastPoll() time.Time { + n.mux.RLock() + defer n.mux.RUnlock() + return n.lastPoll +} + +// Returns the polling lock +func (n *State) GetPollingLock() *sync.Mutex { + return &n.pollingLock +} + +// gets the ordering string for use in team formation +func (n *State) GetOrdering() string { + return n.ordering +} + +// gets the ID of the node +func (n *State) GetID() *id.Node { + return n.id +} + +// returns true and the round id if the node is assigned to a round, +// return false and nil if it is not +func (n *State) GetCurrentRound() (bool, *round.State) { + n.mux.RLock() + defer n.mux.RUnlock() + if n.currentRound == nil { + return false, nil + } else { + return true, n.currentRound + } +} + +// sets the node to not be in a round +func (n *State) ClearRound() { + n.mux.Lock() + defer n.mux.Unlock() + n.currentRound = nil +} + +// sets the node's round to the passed in round unless one is already set, +// in which case it errors +func (n *State) SetRound(r *round.State) error { + n.mux.Lock() + defer n.mux.Unlock() + if n.currentRound != nil { + return errors.New("could not set the Node's round when it is " + + "already set") + } + + n.currentRound = r + return nil +} diff --git a/storage/node/state_test.go b/storage/node/state_test.go new file mode 100644 index 0000000000000000000000000000000000000000..9b7ac6fe2a7e16d9dff2e79475faaeb2e7aeecba --- /dev/null +++ b/storage/node/state_test.go @@ -0,0 +1,437 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package node + +import ( + "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/elixxir/registration/storage/round" + "math" + "strings" + "testing" + "time" +) + +// tests that State update functions properly when the state it is updated +// to is not the one it is at +func TestNodeState_Update_Same(t *testing.T) { + ns := State{ + activity: current.WAITING, + lastPoll: time.Now(), + } + + time.Sleep(10 * time.Millisecond) + + before := time.Now() + + updated, old, err := ns.Update(current.WAITING) + timeDelta := ns.lastPoll.Sub(before) + if timeDelta > (1*time.Millisecond) || timeDelta < 0 { + t.Errorf("Time recorded is not between 0 and 1 ms from "+ + "checkpoint: %s", timeDelta) + } + + if err != nil { + t.Errorf("Node state update should not have errored: %s", err) + } + + if updated == true { + t.Errorf("Node state should not have updated") + } + + if old != current.WAITING { + t.Errorf("Node state returned the wrong old state") + } + + if ns.activity != current.WAITING { + t.Errorf("Internal node activity is not correct: "+ + "Expected: %s, Recieved: %s", current.WAITING, ns.activity) + } +} + +// tests that State update functions properly when the state it is updated +// to is not the one it is not at +func TestNodeState_Update_Invalid(t *testing.T) { + ns := State{ + activity: current.WAITING, + lastPoll: time.Now(), + } + + time.Sleep(10 * time.Millisecond) + + before := time.Now() + + updated, old, err := ns.Update(current.COMPLETED) + + if err == nil { + t.Errorf("Node state update returned no error on invalid state change") + } else if !strings.Contains(err.Error(), "invalid transition") { + t.Errorf("Node state update returned the wrong error on "+ + "invalid state change: %s", err) + } + + timeDelta := ns.lastPoll.Sub(before) + if timeDelta > (1*time.Millisecond) || timeDelta < 0 { + t.Errorf("Time recorded is not between 0 and 1 ms from "+ + "checkpoint: %s", timeDelta) + } + + if updated == true { + t.Errorf("Node state should not have updated") + } + + if old != current.WAITING { + t.Errorf("Node state returned the wrong old state") + } + + if ns.activity != current.WAITING { + t.Errorf("Internal node activity is not correct: "+ + "Expected: %s, Recieved: %s", current.WAITING, ns.activity) + } +} + +// tests that State update functions properly when the state it is updated +// to is not the one it is not at +func TestNodeState_Update_Valid_RequiresRound_RoundNil(t *testing.T) { + ns := State{ + activity: current.WAITING, + lastPoll: time.Now(), + } + + time.Sleep(10 * time.Millisecond) + + before := time.Now() + + updated, old, err := ns.Update(current.PRECOMPUTING) + + if err == nil { + t.Errorf("Node state update returned no error on invalid state change") + } else if !strings.Contains(err.Error(), "requires the node be assigned a round") { + t.Errorf("Node state update returned the wrong error on "+ + "state change requiring round but without one: %s", err) + } + + timeDelta := ns.lastPoll.Sub(before) + if timeDelta > (1*time.Millisecond) || timeDelta < 0 { + t.Errorf("Time recorded is not between 0 and 1 ms from "+ + "checkpoint: %s", timeDelta) + } + + if updated == true { + t.Errorf("Node state should not have updated") + } + + if old != current.WAITING { + t.Errorf("Node state returned the wrong old state") + } + + if ns.activity != current.WAITING { + t.Errorf("Internal node activity is not correct: "+ + "Expected: %s, Recieved: %s", current.WAITING, ns.activity) + } +} + +// tests that State update functions properly when the state it is updated +// to is not the one it is not at +func TestNodeState_Update_Valid_RequiresRound_Round_InvalidState(t *testing.T) { + ns := State{ + activity: current.WAITING, + lastPoll: time.Now(), + currentRound: round.NewState_Testing(42, states.FAILED, t), + } + + time.Sleep(10 * time.Millisecond) + + before := time.Now() + + updated, old, err := ns.Update(current.PRECOMPUTING) + + if err == nil { + t.Errorf("Node state update returned no error on invalid state change") + } else if !strings.Contains(err.Error(), "requires the node's be assigned a round to be in the") { + t.Errorf("Node state update returned the wrong error on "+ + "state change requiring round in teh correct state but in wrong one: %s", err) + } + + timeDelta := ns.lastPoll.Sub(before) + if timeDelta > (1*time.Millisecond) || timeDelta < 0 { + t.Errorf("Time recorded is not between 0 and 1 ms from "+ + "checkpoint: %s", timeDelta) + } + + if updated == true { + t.Errorf("Node state should not have updated") + } + + if old != current.WAITING { + t.Errorf("Node state returned the wrong old state") + } + + if ns.activity != current.WAITING { + t.Errorf("Internal node activity is not correct: "+ + "Expected: %s, Recieved: %s", current.WAITING, ns.activity) + } +} + +// tests that State update functions properly when the state it is updated +// to is not the one it is not at +func TestNodeState_Update_Valid_RequiresRound_Round_ValidState(t *testing.T) { + ns := State{ + activity: current.WAITING, + lastPoll: time.Now(), + currentRound: round.NewState_Testing(42, states.PRECOMPUTING, t), + } + + time.Sleep(10 * time.Millisecond) + + before := time.Now() + + updated, old, err := ns.Update(current.PRECOMPUTING) + + if err != nil { + t.Errorf("Node state update returned no error on valid state change: %s", err) + } + + timeDelta := ns.lastPoll.Sub(before) + if timeDelta > (1*time.Millisecond) || timeDelta < 0 { + t.Errorf("Time recorded is not between 0 and 1 ms from "+ + "checkpoint: %s", timeDelta) + } + + if updated == false { + t.Errorf("Node state should have updated") + } + + if old != current.WAITING { + t.Errorf("Node state returned the wrong old state") + } + + if ns.activity != current.PRECOMPUTING { + t.Errorf("Internal node activity is not correct: "+ + "Expected: %s, Recieved: %s", current.PRECOMPUTING, ns.activity) + } +} + +// tests that State update functions properly when the state it is updated +// to is not the one it is not at +func TestNodeState_Update_Valid_RequiresNoRound_HasRound(t *testing.T) { + ns := State{ + activity: current.COMPLETED, + lastPoll: time.Now(), + currentRound: round.NewState_Testing(42, states.PRECOMPUTING, t), + } + + time.Sleep(10 * time.Millisecond) + + before := time.Now() + + updated, old, err := ns.Update(current.WAITING) + + if err == nil { + t.Errorf("Node state update returned no error on invalid state change") + } else if !strings.Contains(err.Error(), "requires the node not be assigned a round") { + t.Errorf("Node state update returned the wrong error on "+ + "state change requiring no round but has one: %s", err) + } + + timeDelta := ns.lastPoll.Sub(before) + if timeDelta > (1*time.Millisecond) || timeDelta < 0 { + t.Errorf("Time recorded is not between 0 and 1 ms from "+ + "checkpoint: %s", timeDelta) + } + + if updated == true { + t.Errorf("Node state should not have updated") + } + + if old != current.COMPLETED { + t.Errorf("Node state returned the wrong old state") + } + + if ns.activity != current.COMPLETED { + t.Errorf("Internal node activity is not correct: "+ + "Expected: %s, Recieved: %s", current.COMPLETED, ns.activity) + } +} + +// tests that State update functions properly when the state it is updated +// to is not the one it is not at +func TestNodeState_Update_Valid_RequiresNoRound_NoRound(t *testing.T) { + ns := State{ + activity: current.COMPLETED, + lastPoll: time.Now(), + } + + time.Sleep(10 * time.Millisecond) + + before := time.Now() + + updated, old, err := ns.Update(current.WAITING) + + if err != nil { + t.Errorf("Node state update returned error on valid state change: %s", err) + } + + timeDelta := ns.lastPoll.Sub(before) + if timeDelta > (1*time.Millisecond) || timeDelta < 0 { + t.Errorf("Time recorded is not between 0 and 1 ms from "+ + "checkpoint: %s", timeDelta) + } + + if updated == false { + t.Errorf("Node state should have updated") + } + + if old != current.COMPLETED { + t.Errorf("Node state returned the wrong old state") + } + + if ns.activity != current.WAITING { + t.Errorf("Internal node activity is not correct: "+ + "Expected: %s, Recieved: %s", current.WAITING, ns.activity) + } +} + +//tests that GetActivity returns the correct activity +func TestNodeState_GetActivity(t *testing.T) { + for i := 0; i < 10; i++ { + ns := State{ + activity: current.Activity(i), + } + + a := ns.GetActivity() + + if a != current.Activity(i) { + t.Errorf("returned curent activity not as set"+ + "Expected: %v, Recieved: %v", a, i) + } + } +} + +//tests that GetActivity returns the correct activity +func TestNodeState_GetLastPoll(t *testing.T) { + ns := State{} + for i := 0; i < 10; i++ { + before := time.Now() + ns.lastPoll = before + lp := ns.GetLastPoll() + + if lp.Sub(before) != 0 { + t.Errorf("Last Poll returned the wrong datetime") + } + } +} + +//tests that GetActivity returns the correct activity +func TestNodeState_GetCurrentRound_Set(t *testing.T) { + r := round.NewState_Testing(42, 0, t) + ns := State{ + currentRound: r, + } + + success, rnd := ns.GetCurrentRound() + + if !success { + t.Errorf("No round is set when one should be") + } + + if rnd.GetRoundID() != r.GetRoundID() { + t.Errorf("Returned round is not correct: "+ + "Expected: %v, Recieved: %v", r.GetRoundID(), rnd.GetRoundID()) + } +} + +//tests that GetActivity returns the correct activity +func TestNodeState_GetCurrentRound_NotSet(t *testing.T) { + ns := State{} + + success, rnd := ns.GetCurrentRound() + + if success { + t.Errorf("round returned when none is set") + } + + if rnd != nil { + t.Errorf("Returned round is not error valuve: "+ + "Expected: %v, Recieved: %v", uint64(math.MaxUint64), rnd) + } +} + +//tests that clear round sets the tracked roundID to nil +func TestNodeState_ClearRound(t *testing.T) { + r := round.State{} + + ns := State{ + currentRound: &r, + } + + ns.ClearRound() + + if ns.currentRound != nil { + t.Errorf("The curent round was not nilled") + } +} + +//tests that clear round sets the tracked roundID to nil +func TestNodeState_SetRound_Valid(t *testing.T) { + r := round.NewState_Testing(42, 2, t) + + ns := State{ + currentRound: nil, + } + + err := ns.SetRound(r) + + if err != nil { + t.Errorf("SetRound returned an error which it should be "+ + "sucesfull: %s", err) + } + + if ns.currentRound == nil { + t.Errorf("Round not updated") + } +} + +//tests that clear round does not set the tracked roundID errors when one is set +func TestNodeState_SetRound_Invalid(t *testing.T) { + r := round.NewState_Testing(42, 0, t) + storedR := round.NewState_Testing(69, 0, t) + + ns := State{ + currentRound: storedR, + } + + err := ns.SetRound(r) + + if err == nil { + t.Errorf("SetRound did not an error which it should have failed") + } else if !strings.Contains(err.Error(), "could not set the Node's "+ + "round when it is already set") { + t.Errorf("Incorrect error returned from failed SetRound: %s", err) + } + + if ns.currentRound.GetRoundID() != 69 { + t.Errorf("Round not updated to the correct value; "+ + "Expected: %v, Recieved: %v", 69, ns.currentRound.GetRoundID()) + } +} + +// tests that teh returned ID is correct +func TestNodeState_GetID(t *testing.T) { + testID := id.NewNodeFromUInt(50, t) + ns := State{ + id: testID, + } + + retrievedID := ns.GetID() + + if !testID.Cmp(retrievedID) { + t.Errorf("Recieved incorrect id from GetID, "+ + "Expected: %s, Recieved: %s", testID, retrievedID) + } +} diff --git a/storage/permissioningDb.go b/storage/permissioningDb.go new file mode 100644 index 0000000000000000000000000000000000000000..f33613f37ff2477828369dbd740e2dc666a93d32 --- /dev/null +++ b/storage/permissioningDb.go @@ -0,0 +1,46 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +// Handles the database ORM for nodes + +package storage + +import ( + "time" +) + +// If Node registration code is valid, add Node information +func (m *DatabaseImpl) InsertNode(id []byte, code, serverAddr, serverCert, + gatewayAddress, gatewayCert string) error { + newNode := NodeInformation{ + Code: code, + Id: id, + ServerAddress: serverAddr, + GatewayAddress: gatewayAddress, + NodeCertificate: serverCert, + GatewayCertificate: gatewayCert, + DateRegistered: time.Now(), + } + return m.db.Update(&newNode) +} + +// Insert Node registration code into the database +func (m *DatabaseImpl) InsertNodeRegCode(regCode, order string) error { + newNode := NodeInformation{ + Code: regCode, + Order: order, + } + return m.db.Insert(&newNode) +} + +// Get Node information for the given Node registration code +func (m *DatabaseImpl) GetNode(code string) (*NodeInformation, error) { + newNode := &NodeInformation{ + Code: code, + } + err := m.db.Select(newNode) + return newNode, err +} diff --git a/storage/permissioningMap.go b/storage/permissioningMap.go index e05e2e0e937765c8def6ed8f446b05cce23676f8..08e1a2a139657be8a1cdcfe544ecf9d83d6f144f 100644 --- a/storage/permissioningMap.go +++ b/storage/permissioningMap.go @@ -33,34 +33,24 @@ func (m *MapImpl) InsertNode(id []byte, code, serverCert, serverAddress, } // Insert Node registration code into the database -func (m *MapImpl) InsertNodeRegCode(code string) error { +func (m *MapImpl) InsertNodeRegCode(regCode, order string) error { m.mut.Lock() - jww.INFO.Printf("Adding node registration code: %s", code) + jww.INFO.Printf("Adding node registration code: %s with Order Info: %s", + regCode, order) // Enforce unique registration code - if m.node[code] != nil { + if m.node[regCode] != nil { m.mut.Unlock() - return errors.Errorf("node registration code %s already exists", code) + return errors.Errorf("node registration code %s already exists", + regCode) } - m.node[code] = &NodeInformation{Code: code} + m.node[regCode] = + &NodeInformation{Code: regCode, Order: order} m.mut.Unlock() return nil } -// Count the number of Nodes currently registered -func (m *MapImpl) CountRegisteredNodes() (int, error) { - m.mut.Lock() - counter := 0 - for _, v := range m.node { - if v.Id != nil { - counter += 1 - } - } - m.mut.Unlock() - return counter, nil -} - // Get Node information for the given Node registration code func (m *MapImpl) GetNode(code string) (*NodeInformation, error) { m.mut.Lock() diff --git a/storage/permissioningMap_test.go b/storage/permissioningMap_test.go index 9dfa7476619a5da661b3c0e2497aee87a52ae862..28fc5e30a501d1c40dd73b80429965c021956df8 100644 --- a/storage/permissioningMap_test.go +++ b/storage/permissioningMap_test.go @@ -6,7 +6,9 @@ package storage -import "testing" +import ( + "testing" +) // Happy path func TestMapImpl_InsertNodeRegCode(t *testing.T) { @@ -16,12 +18,18 @@ func TestMapImpl_InsertNodeRegCode(t *testing.T) { // Attempt to load in a valid code code := "TEST" - err := m.InsertNodeRegCode(code) + Order := "BLARG" + err := m.InsertNodeRegCode(code, Order) // Verify the insert was successful if err != nil || m.node[code] == nil { t.Errorf("Expected to successfully insert node registration code") } + + if m.node[code].Order != Order { + t.Errorf("Order string incorret; Expected: %s, Recieved: %s", + Order, m.node[code].Order) + } } // Error Path: Duplicate node registration code @@ -35,7 +43,7 @@ func TestMapImpl_InsertNodeRegCode_Duplicate(t *testing.T) { m.node[code] = &NodeInformation{Code: code} // Attempt to load in a duplicate code - err := m.InsertNodeRegCode(code) + err := m.InsertNodeRegCode(code, "") // Verify the insert failed if err == nil { @@ -87,38 +95,6 @@ func TestMapImpl_InsertNode_Invalid(t *testing.T) { } } -// Full happy path -func TestMapImpl_CountRegisteredNodes(t *testing.T) { - m := &MapImpl{ - node: make(map[string]*NodeInformation), - } - - // Check that there are zero registered nodes - count, err := m.CountRegisteredNodes() - if err != nil || count != 0 { - t.Errorf("Expected no registered nodes") - } - - // Load in a registration code - code := "TEST" - m.node[code] = &NodeInformation{Code: code} - - // Check that adding an unregistered node still returns zero - count, err = m.CountRegisteredNodes() - if err != nil || count != 0 { - t.Errorf("Still expected no registered nodes") - } - - // Load in a node - m.node[code].Id = make([]byte, 0) - - // Check that adding a registered node increases the count - count, err = m.CountRegisteredNodes() - if err != nil || count != 1 { - t.Errorf("Expected a registered node") - } -} - // Happy path func TestMapImpl_GetNode(t *testing.T) { m := &MapImpl{ diff --git a/storage/registrationDb.go b/storage/registrationDb.go index 717c43d018f381b6129c9ac402eb7b0aaeab8837..d3ba5dc435950f0962d86b511523d853e76b90db 100644 --- a/storage/registrationDb.go +++ b/storage/registrationDb.go @@ -4,7 +4,7 @@ // All rights reserved. / //////////////////////////////////////////////////////////////////////////////// -// Handles the database ORM for registration codes +// Handles the database ORM for clients package storage diff --git a/storage/round/map.go b/storage/round/map.go new file mode 100644 index 0000000000000000000000000000000000000000..5ac36bf80a99a41ded7d44b4da51478a79ae16f5 --- /dev/null +++ b/storage/round/map.go @@ -0,0 +1,63 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package round + +import ( + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/comms/connect" + "gitlab.com/elixxir/primitives/id" + "sync" + "testing" + "time" +) + +// Tracks state of an individual Node in the network +type StateMap struct { + mux sync.RWMutex + + rounds map[id.Round]*State +} + +//creates a state map object +func NewStateMap() *StateMap { + return &StateMap{ + rounds: make(map[id.Round]*State), + } +} + +// Adds a new round state to the structure. Will not overwrite an existing one. +func (rsm *StateMap) AddRound(id id.Round, batchsize uint32, + topology *connect.Circuit) (*State, error) { + rsm.mux.Lock() + defer rsm.mux.Unlock() + + if _, ok := rsm.rounds[id]; ok { + return nil, errors.New("cannot add a round which already exists") + } + + rsm.rounds[id] = newState(id, batchsize, topology, time.Now()) + + return rsm.rounds[id], nil +} + +// Gets rounds from the state structure +func (rsm *StateMap) GetRound(id id.Round) *State { + rsm.mux.RLock() + rsm.mux.RUnlock() + return rsm.rounds[id] +} + +//adds rounds for testing without checks +func (rsm *StateMap) AddRound_Testing(state *State, t *testing.T) { + if t == nil { + jww.FATAL.Panic("Only for testing") + } + + rsm.rounds[state.GetRoundID()] = state + +} diff --git a/storage/round/map_test.go b/storage/round/map_test.go new file mode 100644 index 0000000000000000000000000000000000000000..5074e1541f5bdab8bdd4aa8a20bf4dcd655400e3 --- /dev/null +++ b/storage/round/map_test.go @@ -0,0 +1,123 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package round + +import ( + "gitlab.com/elixxir/comms/connect" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/primitives/states" + "testing" +) + +//tests that newStateMap is correct +func TestNewStateMap(t *testing.T) { + sm := NewStateMap() + + if sm.rounds == nil { + t.Errorf("Internal map not initilized") + } +} + +// Tests a round is added correctly to the state map when the round does not +// exist +func TestStateMap_AddRound_Happy(t *testing.T) { + sm := &StateMap{ + rounds: make(map[id.Round]*State), + } + + rid := id.Round(2) + + const numNodes = 5 + + rRtn, err := sm.AddRound(rid, 32, buildMockTopology(numNodes, t)) + + if err != nil { + t.Errorf("Error returned on valid addition of node: %s", err) + } + + r := sm.rounds[rid] + + if r == nil { + t.Errorf("round not returned when lookup is valid") + t.FailNow() + } + + if rRtn.GetRoundID() != rid { + t.Errorf("round from lookup returned with wrong id") + } + + if r.GetRoundID() != rid { + t.Errorf("round from lookup returned with wrong id") + } +} + +// Tests a round is not added correctly to the state map when the round +// already exists +func TestStateMap_AddNode_Invalid(t *testing.T) { + sm := &StateMap{ + rounds: make(map[id.Round]*State), + } + + rid := id.Round(2) + + const numNodes = 5 + + sm.rounds[rid] = &State{state: states.FAILED} + + rRtn, err := sm.AddRound(rid, 32, buildMockTopology(numNodes, t)) + + if err == nil { + t.Errorf("Error not returned on invalid addition of node: %s", err) + } + + if rRtn != nil { + t.Errorf("round returned when none create") + } + + if sm.rounds[rid].state != states.FAILED { + t.Errorf("the state of the round was overweritten") + } +} + +//Tests a node is retrieved correctly when in the state map +func TestStateMap_GetRound_Valid(t *testing.T) { + sm := &StateMap{ + rounds: make(map[id.Round]*State), + } + rid := id.Round(2) + sm.rounds[rid] = &State{} + + r := sm.GetRound(rid) + + if r == nil { + t.Errorf("Round not retrieved when valid") + } + +} + +//Tests a not is not returned when no node exists +func TestStateMap_GetNode_invalid(t *testing.T) { + sm := &StateMap{ + rounds: make(map[id.Round]*State), + } + rid := id.Round(2) + + r := sm.GetRound(rid) + + if r != nil { + t.Errorf("Round retrieved when invalid") + } +} + +func buildMockTopology(numNodes int, t *testing.T) *connect.Circuit { + nodeLst := make([]*id.Node, numNodes) + for i := 0; i < numNodes; i++ { + nid := id.NewNodeFromUInt(uint64(i+1), t) + nodeLst[i] = nid + } + return connect.NewCircuit(nodeLst) +} diff --git a/storage/round/state.go b/storage/round/state.go new file mode 100644 index 0000000000000000000000000000000000000000..4aebe8a77f6255f8408e5407c642af69daa3d2dc --- /dev/null +++ b/storage/round/state.go @@ -0,0 +1,142 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package round + +import ( + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/comms/connect" + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/primitives/states" + "math" + "sync" + "testing" + "time" +) + +// Tracks the current global state of a round +type State struct { + // round info to be used to produce new round infos + base *pb.RoundInfo + + //topology of the round + topology *connect.Circuit + + //state of the round + state states.Round + + // Number of nodes ready for the next transition + readyForTransition uint8 + + mux sync.RWMutex +} + +//creates a round state object +func newState(id id.Round, batchsize uint32, topology *connect.Circuit, pendingTs time.Time) *State { + strTopology := make([]string, topology.Len()) + for i := 0; i < topology.Len(); i++ { + strTopology[i] = topology.GetNodeAtIndex(i).String() + } + + //create the timestamps and populate the first one + timestamps := make([]uint64, states.NUM_STATES) + timestamps[states.PENDING] = uint64(pendingTs.Unix()) + + //build and return the round state object + return &State{ + base: &pb.RoundInfo{ + ID: uint64(id), + UpdateID: math.MaxUint64, + State: 0, + BatchSize: batchsize, + Topology: strTopology, + Timestamps: timestamps, + }, + topology: topology, + state: states.PENDING, + readyForTransition: 0, + mux: sync.RWMutex{}, + } +} + +//creates a round state object +func NewState_Testing(id id.Round, state states.Round, t *testing.T) *State { + if t == nil { + jww.FATAL.Panic("Only for testing") + } + //build and return the round state object + return &State{ + base: &pb.RoundInfo{ + ID: uint64(id), + }, + state: state, + readyForTransition: 0, + mux: sync.RWMutex{}, + } +} + +// Increments that another node is ready for a transition. +// and returns true and clears if the transition is ready +func (s *State) NodeIsReadyForTransition() bool { + s.mux.Lock() + defer s.mux.Unlock() + + s.readyForTransition++ + if int(s.readyForTransition) == s.topology.Len() { + s.readyForTransition = 0 + return true + } + return false +} + +// updates the round to a new state. states can only move forward, they cannot +// go in reverse or replace the same state +func (s *State) Update(state states.Round, stamp time.Time) error { + s.mux.Lock() + defer s.mux.Unlock() + + if state <= s.state { + return errors.New("round state must always update to a " + + "greater state") + } + + s.state = state + s.base.Timestamps[state] = uint64(stamp.Unix()) + return nil +} + +//returns an unsigned roundinfo with all fields filled in +func (s *State) BuildRoundInfo() *pb.RoundInfo { + s.mux.RLock() + defer s.mux.RUnlock() + return &pb.RoundInfo{ + ID: s.base.GetID(), + State: uint32(s.state), + BatchSize: s.base.GetBatchSize(), + Topology: s.base.GetTopology(), + Timestamps: s.base.GetTimestamps(), + } +} + +//returns the state of the round +func (s *State) GetRoundState() states.Round { + s.mux.RLock() + defer s.mux.RUnlock() + return s.state +} + +//returns the round's topology +func (s *State) GetTopology() *connect.Circuit { + return s.topology +} + +//returns the id of the round +func (s *State) GetRoundID() id.Round { + rid := id.Round(s.base.ID) + return rid +} diff --git a/storage/round/state_test.go b/storage/round/state_test.go new file mode 100644 index 0000000000000000000000000000000000000000..aeecb8028a8ef9eeb48bda2642a8ced872bce7c1 --- /dev/null +++ b/storage/round/state_test.go @@ -0,0 +1,350 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// + +package round + +import ( + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/primitives/states" + "math" + "reflect" + "strings" + "testing" + "time" +) + +func TestNewState(t *testing.T) { + rid := id.Round(42) + + const ( + batchSize = 32 + numNodes = 5 + ) + + topology := buildMockTopology(numNodes, t) + + ts := time.Now() + + ns := newState(rid, batchSize, topology, ts) + + if len(ns.base.Timestamps) != int(states.NUM_STATES) { + t.Errorf("Length of timestamps list is incorrect: "+ + "Expected: %v, Recieved: %v", numNodes, len(ns.base.Timestamps)) + t.FailNow() + } + + expectedTimestamps := make([]uint64, states.NUM_STATES) + expectedTimestamps[states.PENDING] = uint64(ts.Unix()) + + for i := states.Round(0); i < states.NUM_STATES; i++ { + if ns.base.Timestamps[i] != expectedTimestamps[i] { + t.Errorf("Pending timestamp for %s is incorrect; expected: %v, :"+ + "recieved: %v", i, expectedTimestamps[i], ns.base.Timestamps[i]) + } + } + + if len(ns.base.Topology) != numNodes { + t.Errorf("Toplogy in pb is the wrong length: "+ + "Expected: %v, Recieved: %v", numNodes, len(ns.base.Topology)) + t.FailNow() + } + + for i := 0; i < topology.Len(); i++ { + strId := topology.GetNodeAtIndex(i).String() + if ns.base.Topology[i] != strId { + t.Errorf("Topology string on index %v is incorrect"+ + "Expected: %s, Recieved: %s", i, strId, ns.base.Topology[i]) + } + } + + if !reflect.DeepEqual(topology, ns.topology) { + t.Errorf("Topology in round not the same as passed in") + } + + if ns.base.BatchSize != batchSize { + t.Errorf("BatchSize in pb is incorrect; "+ + "Expected: %v, Recieved: %v", batchSize, ns.base.BatchSize) + } + + if ns.base.ID != uint64(rid) { + t.Errorf("round ID in pb is incorrect; "+ + "Expected: %v, Recived: %v", rid, ns.base.ID) + } + + if ns.base.UpdateID != math.MaxUint64 { + t.Errorf("update ID in pb is incorrect; "+ + "Expected: %v, Recived: %v", uint64(math.MaxUint64), + ns.base.UpdateID) + } + + if ns.state != states.PENDING { + t.Errorf("State of round is incorrect; "+ + "Expected: %s, Recived: %s", states.PENDING, ns.state) + } + + if ns.readyForTransition != 0 { + t.Errorf("readyForTransmission is incorrect; "+ + "Expected: %v, Recived: %v", 0, ns.readyForTransition) + } + +} + +// tests all rollover and non rollover transitions +func TestState_NodeIsReadyForTransition(t *testing.T) { + rid := id.Round(42) + + const ( + batchSize = 32 + numNodes = 5 + ) + + topology := buildMockTopology(numNodes, t) + + ts := time.Now() + + ns := newState(rid, batchSize, topology, ts) + + if ns.readyForTransition != 0 { + t.Errorf("readyForTransmission is incorrect; "+ + "Expected: %v, Recived: %v", 0, ns.readyForTransition) + } + + //test all non roll over transitions + for i := 0; i < numNodes-1; i++ { + ready := ns.NodeIsReadyForTransition() + + if ready { + t.Errorf("state should not be ready for transition on the "+ + "%vth node", i) + } + + if int(ns.readyForTransition) != i+1 { + t.Errorf("Ready for transition counter not correct; "+ + "Expected: %v, recieved: %v", i+1, ns.readyForTransition) + } + } +} + +//test the state update increments properly when given a valid input +func TestState_Update_Forward(t *testing.T) { + rid := id.Round(42) + + const ( + batchSize = 32 + numNodes = 5 + ) + + topology := buildMockTopology(numNodes, t) + + ts := time.Now() + + ns := newState(rid, batchSize, topology, ts) + + for i := states.PRECOMPUTING; i < states.NUM_STATES; i++ { + time.Sleep(1 * time.Millisecond) + ts = time.Now() + err := ns.Update(i, ts) + if err != nil { + t.Errorf("state update failed on valid transition to %s", + i) + } + if ns.state != i { + t.Errorf("Transition to state %s failed, at state %s", i, + ns.state) + } + + if ns.base.Timestamps[i] != uint64(ts.Unix()) { + t.Errorf("Timestamp stored is incorrect. "+ + "Stored: %v, Expected: %v", ns.base.Timestamps[i], uint64(ts.Unix())) + } + } +} + +//test the state update errors properly when set the the same state it is at +func TestState_Update_Same(t *testing.T) { + rid := id.Round(42) + + const ( + batchSize = 32 + numNodes = 5 + ) + + topology := buildMockTopology(numNodes, t) + + ts := time.Now() + + ns := newState(rid, batchSize, topology, ts) + + for i := states.PENDING; i < states.NUM_STATES; i++ { + ns.state = i + ns.base.Timestamps[i] = math.MaxUint64 + time.Sleep(1 * time.Millisecond) + ts = time.Now() + err := ns.Update(i, ts) + if err == nil { + t.Errorf("state update succeded on invalid transition "+ + "to %s from %s", i, i) + } else if !strings.Contains(err.Error(), "round state must "+ + "always update to a greater state") { + t.Errorf("state update failed with incorrect error: %s", + err) + } + + if ns.state != i { + t.Errorf("State incorrect after lateral transition for state "+ + "%s resulted in final state of %s", i, ns.state) + } + + if ns.base.Timestamps[i] != math.MaxUint64 { + t.Errorf("Timestamp edited on failed update"+ + "Stored: %v, Expected: %v", ns.base.Timestamps[i], + uint64(math.MaxUint64)) + } + } +} + +// test the state update errors properly when set to a state less than the +// current one +func TestState_Update_Reverse(t *testing.T) { + rid := id.Round(42) + + const ( + batchSize = 32 + numNodes = 5 + ) + + topology := buildMockTopology(numNodes, t) + + ts := time.Now() + + ns := newState(rid, batchSize, topology, ts) + + for i := states.PRECOMPUTING; i < states.NUM_STATES; i++ { + ns.state = i + ns.base.Timestamps[i] = math.MaxUint64 + time.Sleep(1 * time.Millisecond) + ts = time.Now() + err := ns.Update(i-1, ts) + if err == nil { + t.Errorf("state update succeded on invalid transition "+ + "to %s from %s", i-1, i) + } else if !strings.Contains(err.Error(), "round state must "+ + "always update to a greater state") { + t.Errorf("state update failed with incorrect error: %s", + err) + } + + if ns.state != i { + t.Errorf("State incorrect after reverse transition to state "+ + "%s from %s resulting in final state of %s", i, i-1, ns.state) + } + + if ns.base.Timestamps[i] != math.MaxUint64 { + t.Errorf("Timestamp edited on failed update"+ + "Stored: %v, Expected: %v", ns.base.Timestamps[i], + uint64(math.MaxUint64)) + } + } +} + +func TestState_BuildRoundInfo(t *testing.T) { + rid := id.Round(42) + + const ( + batchSize = 32 + numNodes = 5 + ) + + topology := buildMockTopology(numNodes, t) + + ts := time.Now() + + ns := newState(rid, batchSize, topology, ts) + + ns.state = states.FAILED + + ri := ns.BuildRoundInfo() + + if len(ri.Timestamps) != int(states.NUM_STATES) { + t.Errorf("Length of timestamps list is incorrect: "+ + "Expected: %v, Recieved: %v", numNodes, len(ri.Timestamps)) + t.FailNow() + } + + expectedTimestamps := make([]uint64, states.NUM_STATES) + expectedTimestamps[states.PENDING] = uint64(ts.Unix()) + + for i := states.Round(0); i < states.NUM_STATES; i++ { + if ri.Timestamps[i] != expectedTimestamps[i] { + t.Errorf("Pending timestamp for %s is incorrect; expected: %v, :"+ + "recieved: %v", i, expectedTimestamps[i], ri.Timestamps[i]) + } + } + + if len(ns.base.Topology) != numNodes { + t.Errorf("Toplogy in pb is the wrong length: "+ + "Expected: %v, Recieved: %v", numNodes, len(ri.Topology)) + t.FailNow() + } + + for i := 0; i < topology.Len(); i++ { + strId := topology.GetNodeAtIndex(i).String() + if ri.Topology[i] != strId { + t.Errorf("Topology string on index %v is incorrect"+ + "Expected: %s, Recieved: %s", i, strId, ri.Topology[i]) + } + } + + if ri.UpdateID != 0 { + t.Errorf("update ID is incorrect; Expected: %v, Recieved: %v", + 0, ri.UpdateID) + } + + if ri.ID != uint64(rid) { + t.Errorf("Round ID is incorrect; Expected: %v, Recieved: %v", + rid, ri.ID) + } + + if ri.BatchSize != batchSize { + t.Errorf("Batchsize is incorrect; Expected: %v, Recieved: %v", + batchSize, ri.BatchSize) + } + + if ri.State != uint32(states.FAILED) { + t.Errorf("State is incorrect; Expected: %v, Recieved: %v", + states.FAILED, ri.State) + } +} + +//tests that GetRoundState returns the correct state +func TestState_GetRoundState(t *testing.T) { + for i := states.Round(0); i < states.NUM_STATES; i++ { + rs := State{state: i} + + s := rs.GetRoundState() + + if s != i { + t.Errorf("GetRoundState returned the incorrect state;"+ + "Expected: %s, Recieved: %s", i, s) + } + } +} + +//tests that GetTopology returns the correct topology +func TestState_GetTopology(t *testing.T) { + + const ( + numNodes = 5 + ) + + topology := buildMockTopology(numNodes, t) + + rs := State{topology: topology} + + if !reflect.DeepEqual(topology, rs.GetTopology()) { + t.Errorf("retruned topology did not match passed topology") + } +} diff --git a/storage/state.go b/storage/state.go index 789c7d78664ce1b829c48c50256872eac94d1805..31a5f1abd080d8801a53c4fc34fe510869630a2f 100644 --- a/storage/state.go +++ b/storage/state.go @@ -9,165 +9,191 @@ package storage import ( + "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" pb "gitlab.com/elixxir/comms/mixmessages" "gitlab.com/elixxir/comms/network/dataStructures" "gitlab.com/elixxir/crypto/signature" "gitlab.com/elixxir/crypto/signature/rsa" + "gitlab.com/elixxir/primitives/current" "gitlab.com/elixxir/primitives/id" "gitlab.com/elixxir/primitives/ndf" "gitlab.com/elixxir/primitives/states" - "sync/atomic" + "gitlab.com/elixxir/registration/storage/node" + "gitlab.com/elixxir/registration/storage/round" + "sync" ) -// Used for keeping track of NDF and Round state -type State struct { - // State parameters --- - PrivateKey *rsa.PrivateKey - - // Round state --- - CurrentRound *RoundState - CurrentUpdate int // Round update counter - RoundUpdates *dataStructures.Updates - RoundData *dataStructures.Data - Update chan struct{} // For triggering updates to top level - - // NDF state --- - partialNdf *dataStructures.Ndf - fullNdf *dataStructures.Ndf - PartialNdfMsg *pb.NDF - FullNdfMsg *pb.NDF -} +const updateBufferLength = 1000 + +// NetworkState structure used for keeping track of NDF and Round state. +type NetworkState struct { + // NetworkState parameters + privateKey *rsa.PrivateKey -// Tracks the current global state of a round -type RoundState struct { - // Tracks round information - *pb.RoundInfo + // Round state + rounds *round.StateMap + roundUpdates *dataStructures.Updates + roundUpdateID uint64 + roundUpdateLock sync.Mutex + roundData *dataStructures.Data + update chan *NodeUpdateNotification // For triggering updates to top level - // Keeps track of the state of each node - NodeStatuses map[id.Node]*uint32 + // Node NetworkState + nodes *node.StateMap - // Keeps track of the real state of the network - // as described by the cumulative states of nodes - // In other words, counts the number of nodes currently in each state - NetworkStatus [states.NUM_STATES]*uint32 + // NDF state + partialNdf *dataStructures.Ndf + fullNdf *dataStructures.Ndf } -// Returns a new State object -func NewState() (*State, error) { - state := &State{ - CurrentRound: &RoundState{ - RoundInfo: &pb.RoundInfo{ - Topology: make([]string, 0), // Set this to avoid segfault - State: uint32(states.COMPLETED), // Set this to start rounds - }, - NodeStatuses: make(map[id.Node]*uint32), - }, - CurrentUpdate: 0, - RoundUpdates: dataStructures.NewUpdates(), - RoundData: dataStructures.NewData(), - Update: make(chan struct{}), +// NodeUpdateNotification structure used to notify the control thread that the +// round state has updated. +type NodeUpdateNotification struct { + Node *id.Node + From current.Activity + To current.Activity +} + +// NewState returns a new NetworkState object. +func NewState(pk *rsa.PrivateKey) (*NetworkState, error) { + fullNdf, err := dataStructures.NewNdf(&ndf.NetworkDefinition{}) + if err != nil { + return nil, err + } + partialNdf, err := dataStructures.NewNdf(&ndf.NetworkDefinition{}) + if err != nil { + return nil, err + } + + state := &NetworkState{ + rounds: round.NewStateMap(), + roundUpdates: dataStructures.NewUpdates(), + update: make(chan *NodeUpdateNotification, updateBufferLength), + nodes: node.NewStateMap(), + fullNdf: fullNdf, + partialNdf: partialNdf, + privateKey: pk, + roundUpdateID: 0, } // Insert dummy update - err := state.AddRoundUpdate(&pb.RoundInfo{}) + err = state.AddRoundUpdate(&pb.RoundInfo{}) if err != nil { return nil, err } return state, nil } -// Returns the full NDF -func (s *State) GetFullNdf() *dataStructures.Ndf { +// GetFullNdf returns the full NDF. +func (s *NetworkState) GetFullNdf() *dataStructures.Ndf { return s.fullNdf } -// Returns the partial NDF -func (s *State) GetPartialNdf() *dataStructures.Ndf { +// GetPartialNdf returns the partial NDF. +func (s *NetworkState) GetPartialNdf() *dataStructures.Ndf { return s.partialNdf } -// Returns all updates after the given ID -func (s *State) GetUpdates(id int) []*pb.RoundInfo { - return s.RoundUpdates.GetUpdates(id) +// GetUpdates returns all of the updates after the given ID. +func (s *NetworkState) GetUpdates(id int) ([]*pb.RoundInfo, error) { + return s.roundUpdates.GetUpdates(id), nil } -// Returns true if given node ID is participating in the current round -func (s *State) IsRoundNode(id string) bool { - for _, nodeId := range s.CurrentRound.Topology { - if nodeId == id { - return true - } - } - return false -} +// AddRoundUpdate creates a copy of the round before inserting it into +// roundUpdates. +func (s *NetworkState) AddRoundUpdate(round *pb.RoundInfo) error { + s.roundUpdateLock.Lock() + defer s.roundUpdateLock.Unlock() -// Returns the state of the current round -func (s *State) GetCurrentRoundState() states.Round { - return states.Round(s.CurrentRound.State) -} - -// Makes a copy of the round before inserting into RoundUpdates -func (s *State) AddRoundUpdate(round *pb.RoundInfo) error { roundCopy := &pb.RoundInfo{ ID: round.GetID(), - UpdateID: round.GetUpdateID(), + UpdateID: s.roundUpdateID, State: round.GetState(), BatchSize: round.GetBatchSize(), Topology: round.GetTopology(), Timestamps: round.GetTimestamps(), - Signature: &pb.RSASignature{ - Nonce: round.GetNonce(), - Signature: round.GetSig(), - }, } + + s.roundUpdateID++ + + err := signature.Sign(roundCopy, s.privateKey) + if err != nil { + return errors.WithMessagef(err, "Could not add round update %v "+ + "due to failed signature", roundCopy.UpdateID) + } + jww.DEBUG.Printf("Round state updated to %s", states.Round(roundCopy.State)) - return s.RoundUpdates.AddRound(roundCopy) + return s.roundUpdates.AddRound(roundCopy) } -// Given a full NDF, updates internal NDF structures -func (s *State) UpdateNdf(newNdf *ndf.NetworkDefinition) (err error) { - s.fullNdf, err = dataStructures.NewNdf(newNdf) +// UpdateNdf updates internal NDF structures with the specified new NDF. +func (s *NetworkState) UpdateNdf(newNdf *ndf.NetworkDefinition) (err error) { + // Build NDF comms messages + fullNdfMsg := &pb.NDF{} + fullNdfMsg.Ndf, err = newNdf.Marshal() if err != nil { return } - s.partialNdf, err = dataStructures.NewNdf(newNdf.StripNdf()) + partialNdfMsg := &pb.NDF{} + partialNdfMsg.Ndf, err = newNdf.StripNdf().Marshal() if err != nil { return } - // Build NDF comms messages - s.FullNdfMsg = &pb.NDF{} - s.FullNdfMsg.Ndf, err = s.GetFullNdf().Get().Marshal() + // Sign NDF comms messages + err = signature.Sign(fullNdfMsg, s.privateKey) if err != nil { return } - s.PartialNdfMsg = &pb.NDF{} - s.PartialNdfMsg.Ndf, err = s.GetPartialNdf().Get().Marshal() + err = signature.Sign(partialNdfMsg, s.privateKey) if err != nil { return } - // Sign NDF comms messages - err = signature.Sign(s.FullNdfMsg, s.PrivateKey) + // Assign NDF comms messages + err = s.fullNdf.Update(fullNdfMsg) if err != nil { - return + return err } - return signature.Sign(s.PartialNdfMsg, s.PrivateKey) + return s.partialNdf.Update(partialNdfMsg) +} + +// GetPrivateKey returns the server's private key. +func (s *NetworkState) GetPrivateKey() *rsa.PrivateKey { + return s.privateKey } -// Updates the state of the given node with the new state provided -func (s *State) UpdateNodeState(id *id.Node, newState states.Round) { - // Attempt to update node state atomically - // If an update occurred, continue, else nothing will happen - old := atomic.SwapUint32(s.CurrentRound.NodeStatuses[*id], uint32(newState)) - if old != uint32(newState) { - // Node state was updated, increment state counter - atomic.AddUint32(s.CurrentRound.NetworkStatus[newState], 1) - - // Cue an update - s.Update <- struct{}{} +// GetRoundMap returns the map of rounds. +func (s *NetworkState) GetRoundMap() *round.StateMap { + return s.rounds +} + +// GetNodeMap returns the map of nodes. +func (s *NetworkState) GetNodeMap() *node.StateMap { + return s.nodes +} + +// NodeUpdateNotification sends a notification to the control thread of an +// update to a nodes state. +func (s *NetworkState) NodeUpdateNotification(node *id.Node, from, to current.Activity) error { + nun := NodeUpdateNotification{ + Node: node, + From: from, + To: to, } + + select { + case s.update <- &nun: + return nil + default: + return errors.New("Could not send update notification") + } +} + +// GetNodeUpdateChannel returns a channel to receive node updates on. +func (s *NetworkState) GetNodeUpdateChannel() <-chan *NodeUpdateNotification { + return s.update } diff --git a/storage/state_test.go b/storage/state_test.go index 8309d90afc02c4636c4bc312835e01badbd5fdca..3a198a2c4fc53e4f8e70243efe644c338b21967b 100644 --- a/storage/state_test.go +++ b/storage/state_test.go @@ -7,79 +7,486 @@ package storage import ( - jww "github.com/spf13/jwalterweatherman" + "crypto/rand" + "fmt" + "github.com/pkg/errors" pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/comms/network/dataStructures" + "gitlab.com/elixxir/crypto/signature" "gitlab.com/elixxir/crypto/signature/rsa" - "gitlab.com/elixxir/primitives/states" - "gitlab.com/elixxir/primitives/utils" - "gitlab.com/elixxir/registration/testkeys" - "os" + "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/id" + "gitlab.com/elixxir/primitives/ndf" + "gitlab.com/elixxir/registration/storage/node" + "gitlab.com/elixxir/registration/storage/round" + mrand "math/rand" + "reflect" + "strings" "testing" + "time" ) -func TestMain(m *testing.M) { - jww.SetStdoutThreshold(jww.LevelDebug) +// Tests that NewState() creates a new NetworkState with all the correctly +// initialised fields. None of the error paths of NewState() can be tested. +func TestNewState(t *testing.T) { + // Set up expected values + expectedRounds := round.NewStateMap() + expectedRoundUpdates := dataStructures.NewUpdates() + expectedNodes := node.NewStateMap() + expectedFullNdf, err := dataStructures.NewNdf(&ndf.NetworkDefinition{}) + if err != nil { + t.Fatalf("Failed to generate new NDF:\n%v", err) + } + expectedPartialNdf, err := dataStructures.NewNdf(&ndf.NetworkDefinition{}) + if err != nil { + t.Fatalf("Failed to generate new NDF:\n%v", err) + } + + // Generate private RSA key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + t.Fatalf("Failed to generate private key:\n%v", err) + } + + // Generate new NetworkState + state, err := NewState(privateKey) + if err != nil { + t.Errorf("NewState() produced an unexpected error:\n%v", err) + } - runFunc := func() int { - code := m.Run() - return code + // Test fields of NetworkState + if !reflect.DeepEqual(state.privateKey, privateKey) { + t.Errorf("NewState() produced a NetworkState with the wrong privateKey."+ + "\n\texpected: %v\n\treceived: %v", privateKey, &state.privateKey) } - os.Exit(runFunc()) + if !reflect.DeepEqual(state.rounds, expectedRounds) { + t.Errorf("NewState() produced a NetworkState with the wrong rounds."+ + "\n\texpected: %v\n\treceived: %v", expectedRounds, state.rounds) + } + + // Can't check roundUpdates directly because it contains pointers. Instead, + // check that it is nto nil and the + if state.roundUpdates == nil { + t.Errorf("NewState() produced a NetworkState with a nil roundUpdates."+ + "\n\texpected: %#v\n\treceived: %#v", expectedRoundUpdates, state.roundUpdates) + } + + lastUpdateID := state.roundUpdates.GetLastUpdateID() + if lastUpdateID != 0 { + t.Errorf("roundUpdates has the wrong lastUpdateID"+ + "\n\texpected: %#v\n\treceived: %#v", 0, lastUpdateID) + } + + if !reflect.DeepEqual(state.nodes, expectedNodes) { + t.Errorf("NewState() produced a NetworkState with the wrong nodes."+ + "\n\texpected: %v\n\treceived: %v", expectedNodes, state.nodes) + } + + if !reflect.DeepEqual(state.fullNdf, expectedFullNdf) { + t.Errorf("NewState() produced a NetworkState with the wrong fullNdf."+ + "\n\texpected: %v\n\treceived: %v", expectedFullNdf, state.fullNdf) + } + + if !reflect.DeepEqual(state.partialNdf, expectedPartialNdf) { + t.Errorf("NewState() produced a NetworkState with the wrong partialNdf."+ + "\n\texpected: %v\n\treceived: %v", expectedPartialNdf, state.partialNdf) + } } -// TestFunc: Gets permissioning server test key -func getTestKey() *rsa.PrivateKey { - permKeyBytes, _ := utils.ReadFile(testkeys.GetCAKeyPath()) +// Tests that NewState() produces an error when the private key size is too +// small. +func TestNewState_PrivateKeyError(t *testing.T) { + // Set up expected values + expectedErr := "Could not add round update 0 due to failed signature: " + + "Unable to sign message: crypto/rsa: key size too small for PSS " + + "signature" + + // Generate private RSA key + privateKey, err := rsa.GenerateKey(rand.Reader, 128) + if err != nil { + t.Fatalf("Failed to generate private key:\n%v", err) + } - testPermissioningKey, _ := rsa.LoadPrivateKeyFromPem(permKeyBytes) - return testPermissioningKey + // Generate new NetworkState + state, err := NewState(privateKey) + + // Test NewState() output + if err == nil || err.Error() != expectedErr { + t.Errorf("NewState() did not produce an error when expected."+ + "\n\texpected: %+v\n\treceived: %+v", expectedErr, err) + } + if state != nil { + t.Errorf("NewState() unexpedly produced a non-nil NetworkState when an error was produced."+ + "\n\texpected: %+v\n\treceived: %+v", nil, state) + } } -// Full test -func TestState_IsRoundNode(t *testing.T) { - s, err := NewState() +// Tests that GetFullNdf() returns the correct NDF for a newly created +// NetworkState. +func TestNetworkState_GetFullNdf(t *testing.T) { + // Set up expected values + expectedFullNdf, err := dataStructures.NewNdf(&ndf.NetworkDefinition{}) + if err != nil { + t.Fatalf("Failed to generate new NDF:\n%v", err) + } + + // Generate new and NetworkState + state, _, err := generateTestNetworkState() if err != nil { - t.Errorf("Unable to create state: %+v", err) + t.Fatalf("%+v", err) } - s.CurrentRound = &RoundState{ - RoundInfo: &pb.RoundInfo{}, + + // Call GetFullNdf() + fullNdf := state.GetFullNdf() + + if !reflect.DeepEqual(fullNdf, expectedFullNdf) { + t.Errorf("GetFullNdf() returned the wrong NDF."+ + "\n\texpected: %v\n\treceived: %v", expectedFullNdf, fullNdf) } - testString := "Test" +} - // Test false case - if s.IsRoundNode(testString) { - t.Errorf("Expected node not to be round node!") +// Tests that GetPartialNdf() returns the correct NDF for a newly created +// NetworkState. +func TestNetworkState_GetPartialNdf(t *testing.T) { + // Set up expected values + expectedPartialNdf, err := dataStructures.NewNdf(&ndf.NetworkDefinition{}) + if err != nil { + t.Fatalf("Failed to generate new NDF:\n%v", err) } - // Test true case - s.CurrentRound.Topology = []string{testString} - if !s.IsRoundNode(testString) { - t.Errorf("Expected node to be round node!") + // Generate new NetworkState + state, _, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) + } + + partialNdf := state.GetPartialNdf() + + if !reflect.DeepEqual(partialNdf, expectedPartialNdf) { + t.Errorf("GetPartialNdf() returned the wrong NDF."+ + "\n\texpected: %v\n\treceived: %v", expectedPartialNdf, partialNdf) + } +} + +// Smoke test of GetUpdates() by adding rounds and then calling GetUpdates(). +func TestNetworkState_GetUpdates(t *testing.T) { + // Generate new NetworkState + state, _, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) + } + + // Update the round three times and build expected values array + var expectedRoundInfo []*pb.RoundInfo + for i := 0; i < 3; i++ { + roundInfo := &pb.RoundInfo{ + ID: 0, + UpdateID: uint64(3 + i), + } + + err = state.roundUpdates.AddRound(roundInfo) + if err != nil { + t.Errorf("AddRound() produced an unexpected error:\n%+v", err) + } + + expectedRoundInfo = append(expectedRoundInfo, roundInfo) + } + + // Test GetUpdates() + roundInfo, err := state.GetUpdates(2) + if err != nil { + t.Errorf("GetUpdates() produced an unexpected error:\n%v", err) + } + + if !reflect.DeepEqual(roundInfo, expectedRoundInfo) { + t.Errorf("GetUpdates() returned an incorrect RoundInfo slice."+ + "\n\texpected: %+v\n\treceived: %+v", expectedRoundInfo, roundInfo) } } -// Full test -func TestState_GetCurrentRoundState(t *testing.T) { - s, err := NewState() +// Tests that AddRoundUpdate() by adding a round, checking that it is correct +// and verifying the signature. +func TestNetworkState_AddRoundUpdate(t *testing.T) { + // Expected Values + testUpdateID := uint64(1) + testRoundInfo := &pb.RoundInfo{ + ID: 0, + UpdateID: 5, + } + expectedRoundInfo := *testRoundInfo + expectedRoundInfo.UpdateID = testUpdateID + + // Generate new private RSA key and NetworkState + state, privateKey, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) + } + + state.roundUpdateID = 1 + + // Call AddRoundUpdate() + err = state.AddRoundUpdate(testRoundInfo) + if err != nil { + t.Errorf("AddRoundUpdate() unexpectedly produced an error:\n%+v", + err) + } + + // Test if the round was added + roundInfoArr, err := state.GetUpdates(0) + if err != nil { + t.Fatalf("GetUpdates() produced an unexpected error:\n%+v", err) + } + + roundInfo := roundInfoArr[0] + + // Make signatures equal because they will not be tested for equality + expectedRoundInfo.Signature = roundInfo.Signature + + // Check that the round info returned is correct. + if !reflect.DeepEqual(*roundInfo, expectedRoundInfo) { + t.Errorf("AddRoundUpdate() added incorrect roundInfo."+ + "\n\texpected: %#v\n\treceived: %#v", expectedRoundInfo, *roundInfo) + } + + // Verify signature + err = signature.Verify(roundInfo, privateKey.GetPublic()) if err != nil { - t.Errorf("Unable to create state: %+v", err) + t.Fatalf("Failed to verify RoundInfo signature:\n%+v", err) } +} - // Test nil case - if s.GetCurrentRoundState() != states.COMPLETED { - t.Errorf("Expected nil round to return completed state! Got %+v", - s.GetCurrentRoundState()) +// Tests that AddRoundUpdate() returns an error. +func TestNetworkState_AddRoundUpdate_Error(t *testing.T) { + // Expected Values + testRoundInfo := &pb.RoundInfo{ + ID: 0, + UpdateID: 5, } + expectedErr := "Could not add round update 1 due to failed signature: " + + "Unable to sign message: crypto/rsa: key size too small for PSS " + + "signature" - s.CurrentRound = &RoundState{ - RoundInfo: &pb.RoundInfo{ - State: uint32(states.FAILED), - }, + // Generate new NetworkState + state, _, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) } - // Test happy path - if s.GetCurrentRoundState() != states.FAILED { - t.Errorf("Expected proper state return! Got %d", s.GetCurrentRoundState()) + // Generate new invalid private key and insert into NetworkState + brokenPrivateKey, err := rsa.GenerateKey(rand.Reader, 128) + if err != nil { + t.Fatalf("Failed to generate private key:\n%v", err) + } + state.privateKey = brokenPrivateKey + + // Call AddRoundUpdate() + err = state.AddRoundUpdate(testRoundInfo) + + if err == nil || err.Error() != expectedErr { + t.Errorf("AddRoundUpdate() did not produce an error when expected."+ + "\n\texpected: %+v\n\treceived: %+v", expectedErr, err) } } + +// Tests that UpdateNdf() updates fullNdf and partialNdf correctly. +func TestNetworkState_UpdateNdf(t *testing.T) { + // Expected values + testNDF := &ndf.NetworkDefinition{} + + // Generate new NetworkState + state, _, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) + } + + // Update NDF + err = state.UpdateNdf(testNDF) + if err != nil { + t.Errorf("UpdateNdf() unexpectedly produced an error:\n%+v", err) + } + + if !reflect.DeepEqual(*state.fullNdf.Get(), *testNDF) { + t.Errorf("UpdateNdf() saved the wrong NDF fullNdf."+ + "\n\texpected: %#v\n\treceived: %#v", *testNDF, *state.fullNdf.Get()) + } + + if !reflect.DeepEqual(*state.partialNdf.Get(), *testNDF) { + t.Errorf("UpdateNdf() saved the wrong NDF partialNdf."+ + "\n\texpected: %#v\n\treceived: %#v", *testNDF, *state.partialNdf.Get()) + } +} + +// Tests that UpdateNdf() generates an error when injected with invalid private +// key. +func TestNetworkState_UpdateNdf_SignError(t *testing.T) { + // Expected values + testNDF := &ndf.NetworkDefinition{} + expectedErr := "Unable to sign message: crypto/rsa: key size too small " + + "for PSS signature" + + // Generate new NetworkState + state, _, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) + } + + // Generate new invalid private key and insert into NetworkState + brokenPrivateKey, err := rsa.GenerateKey(rand.Reader, 128) + if err != nil { + t.Fatalf("Failed to generate private key:\n%v", err) + } + state.privateKey = brokenPrivateKey + + // Update NDF + err = state.UpdateNdf(testNDF) + + if err == nil || err.Error() != expectedErr { + t.Errorf("UpdateNdf() did not produce an error when expected."+ + "\n\texpected: %+v\n\treceived: %+v", expectedErr, err) + } +} + +// Tests that GetPrivateKey() returns the correct private key. +func TestNetworkState_GetPrivateKey(t *testing.T) { + // Generate new private RSA key and NetworkState + state, expectedPrivateKey, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) + } + + // Get the private + privateKey := state.GetPrivateKey() + + if !reflect.DeepEqual(privateKey, expectedPrivateKey) { + t.Errorf("GetPrivateKey() produced an incorrect private key."+ + "\n\texpected: %+v\n\treceived: %+v", + expectedPrivateKey, privateKey) + } +} + +// Tests that GetRoundMap() returns the correct round StateMap. +func TestNetworkState_GetRoundMap(t *testing.T) { + // Generate new NetworkState + state, _, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) + } + + // Get the round map + roundMap := state.GetRoundMap() + + if !reflect.DeepEqual(roundMap, round.NewStateMap()) { + t.Errorf("GetRoundMap() produced an incorrect round map."+ + "\n\texpected: %+v\n\treceived: %+v", + round.NewStateMap(), roundMap) + } +} + +// Tests that GetNodeMap() returns the correct node StateMap. +func TestNetworkState_GetNodeMap(t *testing.T) { + // Generate new NetworkState + state, _, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) + } + + // Get the round map + nodeMap := state.GetNodeMap() + + if !reflect.DeepEqual(nodeMap, node.NewStateMap()) { + t.Errorf("GetNodeMap() produced an incorrect node map."+ + "\n\texpected: %+v\n\treceived: %+v", + node.NewStateMap(), nodeMap) + } +} + +// Tests that NodeUpdateNotification() correctly sends an update to the update +// channel and that GetNodeUpdateChannel() receives and returns it. +func TestNetworkState_NodeUpdateNotification(t *testing.T) { + // Test values + testNun := NodeUpdateNotification{ + Node: id.NewNodeFromUInt(mrand.Uint64(), t), + From: current.NOT_STARTED, + To: current.WAITING, + } + + // Generate new NetworkState + state, _, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) + } + + go func() { + err = state.NodeUpdateNotification(testNun.Node, testNun.From, testNun.To) + if err != nil { + t.Errorf("NodeUpdateNotification() produced an unexpected error:"+ + "\n%+v", err) + } + }() + + nodeUpdateNotifier := state.GetNodeUpdateChannel() + + select { + case testUpdate := <-nodeUpdateNotifier: + if !reflect.DeepEqual(*testUpdate, testNun) { + t.Errorf("GetNodeUpdateChannel() received the wrong "+ + "NodeUpdateNotification.\n\texpected: %v\n\t received: %v", + testNun, *testUpdate) + } + case <-time.After(time.Millisecond): + t.Error("Failed to receive node update.") + } +} + +// Tests that NodeUpdateNotification() correctly produces and error when the +// channel buffer is already filled. +func TestNetworkState_NodeUpdateNotification_Error(t *testing.T) { + // Test values + testNun := NodeUpdateNotification{ + Node: id.NewNodeFromUInt(mrand.Uint64(), t), + From: current.NOT_STARTED, + To: current.WAITING, + } + expectedError := errors.New("Could not send update notification") + + // Generate new NetworkState + state, _, err := generateTestNetworkState() + if err != nil { + t.Fatalf("%+v", err) + } + + // Fill buffer + for i := 0; i < updateBufferLength; i++ { + state.update <- &testNun + } + + go func() { + err = state.NodeUpdateNotification(testNun.Node, testNun.From, testNun.To) + if strings.Compare(err.Error(), expectedError.Error()) != 0 { + t.Errorf("NodeUpdateNotification() did not produce an error "+ + "when the channel buffer is full.\n\texpected: %v\n\treceived: %v", + expectedError, err) + } + }() + + time.Sleep(1 * time.Second) +} + +// generateTestNetworkState returns a newly generated NetworkState and private +// key. Errors created by generating the key or NetworkState are returned. +func generateTestNetworkState() (*NetworkState, *rsa.PrivateKey, error) { + // Generate new private RSA key + privateKey, err := rsa.GenerateKey(rand.Reader, 2048) + if err != nil { + return nil, privateKey, fmt.Errorf("Failed to generate private key:\n+%v", err) + } + + // Generate new NetworkState using the private key + state, err := NewState(privateKey) + if err != nil { + return state, privateKey, fmt.Errorf("NewState() produced an unexpected error:\n+%v", err) + } + + return state, privateKey, nil +} diff --git a/testkeys/keypath.go b/testkeys/keypath.go index c39f98040b3cd8ddeee2cc63311e986ee1392e6c..041bf287b9a6346f6960778234e368ae5543cae6 100644 --- a/testkeys/keypath.go +++ b/testkeys/keypath.go @@ -62,3 +62,7 @@ func GetClientPublicKey() string { func GetClientNdf() string { return filepath.Join(getDirForFile(), "clientNDF.json") } + +func GetSchedulingConfig() string { + return filepath.Join(getDirForFile(), "schedulingConfig.json") +} diff --git a/testkeys/ndf.json b/testkeys/ndf.json index afd4f1258ca2c3b7510eacf2fa4fc4535d868087..a65e1b87862bed5d3c3a08782d61c1748a1d8199 100644 --- a/testkeys/ndf.json +++ b/testkeys/ndf.json @@ -1 +1 @@ -{"Timestamp":"2020-04-14T09:54:12.711582937-07:00","Gateways":[{"Address":"0.0.0.0:6900","Tls_certificate":"-----BEGIN CERTIFICATE-----\n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\nUUmmazxBG0l69nnYeVOTgq66ML6/RCLlLQ==\n-----END CERTIFICATE-----\n"},{"Address":"0.0.0.0:6901","Tls_certificate":"-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n"}],"Nodes":[{"Id":"QQ==","Address":"0.0.0.0:6900","Tls_certificate":"-----BEGIN CERTIFICATE-----\n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\nUUmmazxBG0l69nnYeVOTgq66ML6/RCLlLQ==\n-----END CERTIFICATE-----\n"},{"Id":"Qg==","Address":"0.0.0.0:6901","Tls_certificate":"-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n"}],"Registration":{"Address":"0.0.0.0:5900","Tls_certificate":"-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n"},"Notification":{"Address":"","Tls_certificate":""},"Udb":{"Id":"AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAQ="},"E2e":{"Prime":"","Small_prime":"","Generator":""},"Cmix":{"Prime":"","Small_prime":"","Generator":""}} \ No newline at end of file +{"Timestamp":"2020-05-12T16:31:43.486656799-07:00","Gateways":[{"Address":"","Tls_certificate":""},{"Address":"0.0.0.0:6900","Tls_certificate":"-----BEGIN CERTIFICATE-----\n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\nUUmmazxBG0l69nnYeVOTgq66ML6/RCLlLQ==\n-----END CERTIFICATE-----\n"},{"Address":"0.0.0.0:6901","Tls_certificate":"-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n"}],"Nodes":[{"Id":null,"Address":"","Tls_certificate":""},{"Id":"QQ==","Address":"0.0.0.0:6900","Tls_certificate":"-----BEGIN CERTIFICATE-----\n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\nUUmmazxBG0l69nnYeVOTgq66ML6/RCLlLQ==\n-----END CERTIFICATE-----\n"},{"Id":"Qg==","Address":"0.0.0.0:6901","Tls_certificate":"-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n"}],"Registration":{"Address":"0.0.0.0:5900","Tls_certificate":"-----BEGIN CERTIFICATE-----\n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\n-----END CERTIFICATE-----\n"},"Notification":{"Address":"","Tls_certificate":""},"Udb":{"Id":null},"E2e":{"Prime":"","Small_prime":"","Generator":""},"Cmix":{"Prime":"","Small_prime":"","Generator":""}} \ No newline at end of file diff --git a/testkeys/schedulingConfig.json b/testkeys/schedulingConfig.json new file mode 100644 index 0000000000000000000000000000000000000000..4fbb8d4edd8b38a91c806fcf8b6ac1460bcee085 --- /dev/null +++ b/testkeys/schedulingConfig.json @@ -0,0 +1,6 @@ +{ + "TeamSize": 3, + "BatchSize": 32, + "RandomOrdering": false, + "MinimumDelay": 60 +} \ No newline at end of file diff --git a/transition/ControlState.go b/transition/ControlState.go new file mode 100644 index 0000000000000000000000000000000000000000..c1c547c4327ab73b45394215790d5432d9a6a135 --- /dev/null +++ b/transition/ControlState.go @@ -0,0 +1,83 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package transition + +import ( + "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/states" + "math" +) + +// ControlState.go contains the state transition information for nodes. + +const ( + No = 0 + Yes = 1 + Maybe = 2 + nilRoundState = math.MaxUint32 +) + +// Node is a global variable used as bookkeping for state transition information +var Node = newTransitions() + +type Transitions [current.NUM_STATES]transitionValidation + +// newTransition creates a transition table containing necessary information +// on state transitions +func newTransitions() Transitions { + t := Transitions{} + t[current.NOT_STARTED] = NewTransitionValidation(No, nilRoundState) + t[current.WAITING] = NewTransitionValidation(No, nilRoundState, current.NOT_STARTED, current.COMPLETED, current.ERROR) + t[current.PRECOMPUTING] = NewTransitionValidation(Yes, states.PRECOMPUTING, current.WAITING) + t[current.STANDBY] = NewTransitionValidation(Yes, states.PRECOMPUTING, current.PRECOMPUTING) + t[current.REALTIME] = NewTransitionValidation(Yes, states.REALTIME, current.STANDBY) + t[current.COMPLETED] = NewTransitionValidation(Yes, states.REALTIME, current.REALTIME) + t[current.ERROR] = NewTransitionValidation(Maybe, nilRoundState, current.NOT_STARTED, + current.WAITING, current.PRECOMPUTING, current.STANDBY, current.REALTIME, + current.COMPLETED) + + return t +} + +// IsValidTransition checks the transitionValidation to see if +// the attempted transition is valid +func (t Transitions) IsValidTransition(to, from current.Activity) bool { + return t[to].from[from] +} + +// NeedsRound checks if the state being transitioned to +// will need round updates +func (t Transitions) NeedsRound(to current.Activity) int { + return t[to].needsRound +} + +// RequiredRoundState looks up the required round needed prior to transition +func (t Transitions) RequiredRoundState(to current.Activity) states.Round { + return t[to].roundState +} + +// Transitional information used for each state +type transitionValidation struct { + from [current.NUM_STATES]bool + needsRound int + roundState states.Round +} + +// NewTransitionValidation sets the from attribute, +// denoting whether going from that to the objects current state +// is valid +func NewTransitionValidation(needsRound int, roundState states.Round, from ...current.Activity) transitionValidation { + tv := transitionValidation{} + + tv.needsRound = needsRound + tv.roundState = roundState + + for _, f := range from { + tv.from[f] = true + } + + return tv +} diff --git a/transition/controlState_test.go b/transition/controlState_test.go new file mode 100644 index 0000000000000000000000000000000000000000..964ec31ef1aea7b310a88818f34780ec4ec5ccca --- /dev/null +++ b/transition/controlState_test.go @@ -0,0 +1,80 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2018 Privategrity Corporation / +// / +// All rights reserved. / +//////////////////////////////////////////////////////////////////////////////// +package transition + +import ( + "gitlab.com/elixxir/primitives/current" + "gitlab.com/elixxir/primitives/states" + "reflect" + "testing" +) + +// Tests the valid transition states +func TestTransitions_IsValidTransition(t *testing.T) { + testTransition := newTransitions() + + var expectedTransition = make([][]bool, current.NUM_STATES, current.NUM_STATES) + + expectedTransition[current.NOT_STARTED] = []bool{false, false, false, false, false, false, false, false} + expectedTransition[current.WAITING] = []bool{true, false, false, false, false, true, true, false} + expectedTransition[current.PRECOMPUTING] = []bool{false, true, false, false, false, false, false, false} + expectedTransition[current.STANDBY] = []bool{false, false, true, false, false, false, false, false} + expectedTransition[current.REALTIME] = []bool{false, false, false, true, false, false, false, false} + expectedTransition[current.COMPLETED] = []bool{false, false, false, false, true, false, false, false} + expectedTransition[current.ERROR] = []bool{true, true, true, true, true, true, false, false} + expectedTransition[current.CRASH] = make([]bool, current.NUM_STATES) + + for i := uint32(0); i < uint32(current.NUM_STATES); i++ { + receivedTransitions := make([]bool, len(expectedTransition)) + + for k := uint32(0); k < uint32(current.NUM_STATES); k++ { + //fmt.Printf("iter %d: %v\n", i, current.Activity(i)) + ok := testTransition.IsValidTransition(current.Activity(i), current.Activity(k)) + receivedTransitions[k] = ok + } + + if !reflect.DeepEqual(expectedTransition[current.Activity(i)], receivedTransitions) { + t.Errorf("State transitions for %s did not match expected.\n\tExpected: %v\n\tReceived: %v", + current.Activity(i), expectedTransition[current.Activity(i)], receivedTransitions) + } + + } + +} + +// Checks the look up function for NeedsRound produces expected results +func TestTransitions_NeedsRound(t *testing.T) { + testTransition := newTransitions() + + expectedNeedsRound := []int{0, 0, 1, 1, 1, 1, 2} + receivedNeedsRound := make([]int, len(expectedNeedsRound)) + for i := uint32(0); i < uint32(current.CRASH); i++ { + receivedNeedsRound[i] = testTransition.NeedsRound(current.Activity(i)) + } + + if !reflect.DeepEqual(expectedNeedsRound, receivedNeedsRound) { + t.Errorf("NeedsRound did not match expected.\n\tExpected: %v\n\tReceived: %v", + expectedNeedsRound, receivedNeedsRound) + } +} + +// Tests the lok for function for RequiredRoundState produces expected results +func TestTransitions_RequiredRoundState(t *testing.T) { + testTransition := newTransitions() + + expectedRoundState := []states.Round{nilRoundState, nilRoundState, 1, 1, 3, 3, nilRoundState} + receivedRoundState := make([]states.Round, len(expectedRoundState)) + + for i := uint32(0); i < uint32(current.CRASH); i++ { + receivedRoundState[i] = testTransition.RequiredRoundState(current.Activity(i)) + } + + if !reflect.DeepEqual(expectedRoundState, receivedRoundState) { + t.Errorf("NeedsRound did not match expected.\n\tExpected: %v\n\tReceived: %v", + expectedRoundState, receivedRoundState) + } + +}