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:  &regRemaining,
 		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, &params)
+	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-----\nMIIGHTCCBAWgAwIBAgIUI9a3/pEOezSEuJHyfBI49/JbVmgwDQYJKoZIhvcNAQEL\nBQAwgZIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJQ2xhcmVt\nb250MRAwDgYDVQQKDAdFbGl4eGlyMRQwEgYDVQQLDAtEZXZlbG9wbWVudDEZMBcG\nA1UEAwwQZ2F0ZXdheS5jbWl4LnJpcDEfMB0GCSqGSIb3DQEJARYQYWRtaW5AZWxp\neHhpci5pbzAeFw0xOTA4MTcxOTA0MjdaFw0yMDA4MTYxOTA0MjdaMIGSMQswCQYD\nVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCUNsYXJlbW9udDEQMA4GA1UE\nCgwHRWxpeHhpcjEUMBIGA1UECwwLRGV2ZWxvcG1lbnQxGTAXBgNVBAMMEGdhdGV3\nYXkuY21peC5yaXAxHzAdBgkqhkiG9w0BCQEWEGFkbWluQGVsaXh4aXIuaW8wggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDt8kLGWKdyz9YBJFtJ/m8bG0QW\nKyyOkEiKBh9TkVL2Avxu/BCZ0qKmQ99x8qPE62xpdrUjyNS4IVnqv4Wf5QgMMfjS\nv481njbBXRgIdn5F/pM7e/QqH3ZVQd8efQT+5OlWLmIu7770cVNAGAwd2rnDZykF\n5tcJ1SvpQjt0uEp3qcG9a/R+x2bkP1XvjZY1jC2P4sAFfZ01eOOwY9HoWAdBB6R/\nzmTjXVFdWh7BM2TId27NPMTmxPgPBwugpLndiD07b/GpPLsWB+BDIiM+XyWE84Rb\n9Pj89TkVmfvL61Wsw/u+Qv65h8sRXws4I6zT4EcYQbJX24xaR1EzdFT4A43vDg0F\noVY4TI327stN52t/NOu19D0TRko8eoJTtbrk0E+sTWepu8RaA5bAeuEdcC5ccOQy\nuTeOV/lpnimN9a2FOUJi3YmLxrPePQoLOMkY6JJ/lVCspIPAX4NS8xwE0kDa2hWZ\nIRujY/zozw4vLWlNn0QP0EMYFgy07iOu9k3+GMW07l4XLoselJcdAWF5Ts4nEHIC\nJMX9rH/nbvmKppRTG7OPVTEouNTVj8nwz7i18Lkp9Ix/W2gftFFuz1vpDopB51ek\nhlAP+ushVPdOYgTqO4i3E3x/GabyGY0gZAgWkPnhys5R65powmxfkBNc05EViFFl\nMDttrSDZ7geeLsA0xwIDAQABo2kwZzAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQW\nBBQk0oLPgVCbhySovRMHttfRk/30QjAfBgNVHSMEGDAWgBQk0oLPgVCbhySovRMH\nttfRk/30QjAUBgNVHREEDTALgglmb28uY28udWswDQYJKoZIhvcNAQELBQADggIB\nAIVkAFyCUZKVjQ7dTp7Q6II36UIet2KXoVfPqGnd0aDluV8OY4xg4l+scc1lEFna\no24DWfK2HKxVWSJAihJb1wRf0e7yWUhsiLCzyYgkjHihphBlGmKDXb3Ghl+kud2q\nDPf2m+rhQyfw7Y/W1xMCD5lQtTULPLPVqif452cyzyKKtqu8GBIqWRAI69N1i+p5\nqGYOGidOxSqOSPEgise765VhGgElwg1YcFsxql5dNu1eQ1hoQNJYHf2cWtrMpelZ\nYLrooPUfUlxnTYESkW9vE9zn67utqEi7N5RXQ8HOSKVNBPwpIdTKHiyTnEZf6nDK\n4fJcIAeUrKBuvzYhbqfKQBZzg071YG9ADnPkzjxz7v5zak9QfWJVH7fFOiEJ6Mzx\n48DS9vw9SIjJqe/fJP/9gkgI+YFEL635llzN9tRwB0dj7mQbtoAjuhoQ8wqCeAU9\nq7EgxK3l4fjIWvg9oYwt11fLGljLnH+Q5XK12TCsBkjlt9zY7ID3F0sjfM8ixxD2\nkbDiEjiNO3Xu5x1QHNMP1e9gflm8IcpQNpzgShhObvTiUZh80qlt7q1vqekHDzYz\nFZGGEdXJCT7whzKhshBwBV4c8XCT9Ec8MDUuyZpMuoo+G1azhwSRuvjgIrFsNa46\nGkO9xZKhVC5yeirzJK1xH3qZN2Kc1CxbeRHSQcjYVAN1\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-----\nMIIGFTCCA/2gAwIBAgIUNdjL0qGKH2CyhoIsuY7biOomzJswDQYJKoZIhvcNAQEL\nBQAwgZIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJQ2xhcmVt\nb250MRAwDgYDVQQKDAdFbGl4eGlyMRQwEgYDVQQLDAtEZXZlbG9wbWVudDEZMBcG\nA1UEAwwQZ2F0ZXdheS5jbWl4LnJpcDEfMB0GCSqGSIb3DQEJARYQYWRtaW5AZWxp\neHhpci5pbzAeFw0xOTA4MTUyMjE2MDVaFw0yMDA4MTQyMjE2MDVaMIGKMQswCQYD\nVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCUNsYXJlbW9udDEQMA4GA1UE\nCgwHRWxpeHhpcjEUMBIGA1UECwwLRGV2ZWxvcG1lbnQxETAPBgNVBAMMCGNtaXgu\ncmlwMR8wHQYJKoZIhvcNAQkBFhBhZG1pbkBlbGl4eGlyLmlvMIICIjANBgkqhkiG\n9w0BAQEFAAOCAg8AMIICCgKCAgEAmXX3piDILK9CMAVIQ4Pjei0yVdcV56CldzSK\n1gP/Pd4PmQyDJXUmlq6q/3KgX8xHqKohBlrY0drJtL1jniEkeNHY5SVeYfctMZgR\nD50xjwMy84lLIVvUe4qdODyGY+xoQABJZu2rMVQHuz2PVSmW4QDdXURqpBRgdWv+\nAb8Ga7rHHKQ/6IoMACstujefkVkt5SyFJOBhyXftdUSHtkGzBxmOppyPj4WH5fe9\niRmMyQi0IfJZXTPZL+GSNhEibbrk5di4kxNIFgh5Hpx9xtKzD5dmCkq1gt3iRNRf\nvAXG7JIuGToVZfxThB9LBNf5ftJ0nYwRfKugB9lbsRJAsuyB5KgVvpvrWslZSGtV\nPnXtBuhBsgjfMAis4CgTk9Y9J8TaNIFvmUivJptylSyCdS/ByRGijlViepW8107I\nWzK3xSLQUbcVa363q3B8SrIFgTekq4T29q85PJsYGDNaMIhVDcTJAbJLYYiowp3E\n8x64psHtAKEkt4KYK9U9UDB2yZ07wMToHHcy5SFxtvS30penl2KwQ5o0HEg0eCHr\nmkAOkqKK3V3C4U6hfcY+bUMmvvlR1oZ6iETggby9t93Lxtp5JiH4qwz/JEwbFT91\n8BcdINz1JiPOV6Quq9Rbrqw003bvl/41Tc5MMYD2ZxN5gpPEZYpBFFsoEn+lVvt2\nS19cKP8CAwEAAaNpMGcwDwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUdq9qt1zj\nbpnjYEqtFSDxu+9aScgwHwYDVR0jBBgwFoAUJNKCz4FQm4ckqL0TB7bX0ZP99EIw\nFAYDVR0RBA0wC4IJZm9vLmNvLnVrMA0GCSqGSIb3DQEBCwUAA4ICAQDZF3p17fGB\nMngnKDTEC/HhPlfwaP2dv1K5wQjhuBcCItiXOHk90sTHY33IdHj3eLn2hOLf6iR1\ngY7jdgCf9RGQxDcKmq8Y72mJ/mln0OpPofUgNaJ8At07NC9fXk1kWgrtbS3EUtXQ\nPgYqRqo7IknYcz8pZq5SLxHEcsXB0RwKynzcDrVRPmlsXNX+rXzqyio0vO2kGVWJ\nht7mPTXZnTXLJEHGVE0wb93FDl0errOqeEFhvtuROyqV7/Z6cQ/ZKnpTD9Ckjzuw\nbJpIuKLcJ6mc4o4pzmEH4E9irwOObsQ8i6K/28/csAz4f+d6wBdIaPvHC803xfY7\n6azKtUBG+YwsWce5QN+++h9tDSVIrFjCFVXE5JPO/XxLne6rtg00hWFmgUyCzjb1\nyyXVMaNmPQcPuZl3mAJ+2Lo6UPMTdMwSRn8UJ3kpLwXq3b7QqK/nFZGp0h9iN7et\n+W0SjLhjRd+stIhi12Ox3Ze1r+fzVFlo/pZlwDHN2wa3phZ24MAX/+HILXxSi2Ez\nOfzOK3YvlWH4ZWntM0Iw/ASHTderJkTuVFhmi+hhraMcND/M/risUeRWgcH2QMD8\ny9DTWujbk4VeKSc5kEK1clGHUWgO28+o+0x4nddAYEeZJq1gMfg4Q61BE6ckLihl\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-----\nMIIGHTCCBAWgAwIBAgIUI9a3/pEOezSEuJHyfBI49/JbVmgwDQYJKoZIhvcNAQEL\nBQAwgZIxCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJDQTESMBAGA1UEBwwJQ2xhcmVt\nb250MRAwDgYDVQQKDAdFbGl4eGlyMRQwEgYDVQQLDAtEZXZlbG9wbWVudDEZMBcG\nA1UEAwwQZ2F0ZXdheS5jbWl4LnJpcDEfMB0GCSqGSIb3DQEJARYQYWRtaW5AZWxp\neHhpci5pbzAeFw0xOTA4MTcxOTA0MjdaFw0yMDA4MTYxOTA0MjdaMIGSMQswCQYD\nVQQGEwJVUzELMAkGA1UECAwCQ0ExEjAQBgNVBAcMCUNsYXJlbW9udDEQMA4GA1UE\nCgwHRWxpeHhpcjEUMBIGA1UECwwLRGV2ZWxvcG1lbnQxGTAXBgNVBAMMEGdhdGV3\nYXkuY21peC5yaXAxHzAdBgkqhkiG9w0BCQEWEGFkbWluQGVsaXh4aXIuaW8wggIi\nMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDt8kLGWKdyz9YBJFtJ/m8bG0QW\nKyyOkEiKBh9TkVL2Avxu/BCZ0qKmQ99x8qPE62xpdrUjyNS4IVnqv4Wf5QgMMfjS\nv481njbBXRgIdn5F/pM7e/QqH3ZVQd8efQT+5OlWLmIu7770cVNAGAwd2rnDZykF\n5tcJ1SvpQjt0uEp3qcG9a/R+x2bkP1XvjZY1jC2P4sAFfZ01eOOwY9HoWAdBB6R/\nzmTjXVFdWh7BM2TId27NPMTmxPgPBwugpLndiD07b/GpPLsWB+BDIiM+XyWE84Rb\n9Pj89TkVmfvL61Wsw/u+Qv65h8sRXws4I6zT4EcYQbJX24xaR1EzdFT4A43vDg0F\noVY4TI327stN52t/NOu19D0TRko8eoJTtbrk0E+sTWepu8RaA5bAeuEdcC5ccOQy\nuTeOV/lpnimN9a2FOUJi3YmLxrPePQoLOMkY6JJ/lVCspIPAX4NS8xwE0kDa2hWZ\nIRujY/zozw4vLWlNn0QP0EMYFgy07iOu9k3+GMW07l4XLoselJcdAWF5Ts4nEHIC\nJMX9rH/nbvmKppRTG7OPVTEouNTVj8nwz7i18Lkp9Ix/W2gftFFuz1vpDopB51ek\nhlAP+ushVPdOYgTqO4i3E3x/GabyGY0gZAgWkPnhys5R65powmxfkBNc05EViFFl\nMDttrSDZ7geeLsA0xwIDAQABo2kwZzAdBgNVHQ4EFgQUJNKCz4FQm4ckqL0TB7bX\n0ZP99EIwHwYDVR0jBBgwFoAUJNKCz4FQm4ckqL0TB7bX0ZP99EIwDwYDVR0TAQH/\nBAUwAwEB/zAUBgNVHREEDTALgglmb28uY28udWswDQYJKoZIhvcNAQELBQADggIB\nABsV5qEXpa6B5B15u7qPaHOQupCXgGgJexd3n9XyGjs3g8mDMd7C/gFGYaUZkBOS\nogTJhj/0/IYJARmYpO4KTzpXqN1+wC6Y768ToUnEvHaRMNSaQJk5ZLtr+40ZYtp8\niC5wWX1pjJUdZKjDuEtpI7vmgLOWT23yy1MPBZhtOBOW4mKzJ2x4qjC748bIIgrg\ns5c3D5RftTWuw7yRRehBKBB2AqSJ3PAlDrJZ01eP3wRo7rAFBg90OLi8Mn1eGzIo\nX1YaATuq6x0Gfx6X5vRzIiZwfZ4WHBR/GwA1QZcbDmXU+a+u/uaAvYG7jZFt/ioV\nFLDWo12j516p87kjME2SGaYJqmFvklY5BupA4V6Hd6qWYyoXd1XIF8QyjYOzW3NF\nTCfE0H45R2Q1P2TiMZywwyvvSLAPLfurgqLQD9k/UjDxVvCamms6pUSAy3FxFOm5\ntizV/hkTa3gcFsZRjdLsM0CGgcMvInWTJn7b88O35je6+rgCz1OfUrVrCgTSovMC\nodzJ9EXYZrmDL/ZOGps/jXmkH4Fv+mgNgespMSJE1D+MteKCz6ghsidDEvELpRnN\n5ku8G5fFkEb1eK4W5Ug9Cxx9A7IUFU1PLgrR5CdHCSlw7cuCctz7jHiJS0c1P3u2\nQAjnEg9B8/6c3Pmhf4aAC7vQ6eCr+H7lqFllZBn4wPHs\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)
+	}
+
+}