diff --git a/api/results.go b/api/results.go new file mode 100644 index 0000000000000000000000000000000000000000..e339a483767b2eed9342b7846b81e8aa38f0dd7a --- /dev/null +++ b/api/results.go @@ -0,0 +1,209 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// +package api + +import ( + "fmt" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/globals" + "gitlab.com/elixxir/client/network/gateway" + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/comms/network" + ds "gitlab.com/elixxir/comms/network/dataStructures" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/comms/connect" + "gitlab.com/xx_network/primitives/id" + "time" +) + +// Enum of possible round results to pass back +type RoundResult uint + +const ( + TimeOut RoundResult = iota + Failed + Succeeded +) + +func (rr RoundResult) String() string { + switch rr { + case TimeOut: + return "TimeOut" + case Failed: + return "Failed" + case Succeeded: + return "Succeeded" + default: + return fmt.Sprintf("UNKNOWN RESULT: %d", rr) + } +} + +// Callback interface which reports the requested rounds. +// Designed such that the caller may decide how much detail they need. +// allRoundsSucceeded: +// Returns false if any rounds in the round map were unsuccessful. +// Returns true if ALL rounds were successful +// timedOut: +// Returns true if any of the rounds timed out while being monitored +// Returns false if all rounds statuses were returned +// rounds contains a mapping of all previously requested rounds to +// their respective round results +type RoundEventCallback func(allRoundsSucceeded, timedOut bool, rounds map[id.Round]RoundResult) + +// Comm interface for RequestHistoricalRounds. +// Constructed for testability with getRoundResults +type historicalRoundsComm interface { + RequestHistoricalRounds(host *connect.Host, + message *pb.HistoricalRounds) (*pb.HistoricalRoundsResponse, error) + GetHost(hostId *id.ID) (*connect.Host, bool) +} + +// Adjudicates on the rounds requested. Checks if they are +// older rounds or in progress rounds. +func (c *Client) GetRoundResults(roundList []id.Round, timeout time.Duration, + roundCallback RoundEventCallback) error { + + sendResults := make(chan ds.EventReturn, len(roundList)) + + return c.getRoundResults(roundList, timeout, roundCallback, + sendResults, c.comms) +} + +// Helper function which does all the logic for GetRoundResults +func (c *Client) getRoundResults(roundList []id.Round, timeout time.Duration, + roundCallback RoundEventCallback, sendResults chan ds.EventReturn, + commsInterface historicalRoundsComm) error { + + networkInstance := c.network.GetInstance() + + // Generate a message to track all older rounds + historicalRequest := &pb.HistoricalRounds{ + Rounds: []uint64{}, + } + + // Generate all tracking structures for rounds + roundEvents := c.GetRoundEvents() + roundsResults := make(map[id.Round]RoundResult) + allRoundsSucceeded := true + numResults := 0 + + // Parse and adjudicate every round + for _, rnd := range roundList { + // Every round is timed out by default, until proven to have finished + roundsResults[rnd] = TimeOut + roundInfo, err := networkInstance.GetRound(rnd) + // If we have the round in the buffer + if err == nil { + // Check if the round is done (completed or failed) or in progress + if states.Round(roundInfo.State) == states.COMPLETED { + roundsResults[rnd] = Succeeded + } else if states.Round(roundInfo.State) == states.FAILED { + roundsResults[rnd] = Failed + allRoundsSucceeded = false + } else { + // If in progress, add a channel monitoring its status + roundEvents.AddRoundEventChan(rnd, sendResults, + timeout-time.Millisecond, states.COMPLETED, states.FAILED) + numResults++ + } + } else { + jww.DEBUG.Printf("Failed to ger round [%d] in buffer: %v", rnd, err) + // Update oldest round (buffer may have updated externally) + oldestRound := networkInstance.GetOldestRoundID() + if rnd < oldestRound { + // If round is older that oldest round in our buffer + // Add it to the historical round request (performed later) + historicalRequest.Rounds = append(historicalRequest.Rounds, uint64(rnd)) + numResults++ + } else { + // Otherwise, monitor it's progress + roundEvents.AddRoundEventChan(rnd, sendResults, + timeout-time.Millisecond, states.COMPLETED, states.FAILED) + numResults++ + } + } + } + + // Find out what happened to old (historical) rounds if any are needed + if len(historicalRequest.Rounds) > 0 { + go c.getHistoricalRounds(historicalRequest, networkInstance, sendResults, commsInterface) + } + + // Determine the results of all rounds requested + go func() { + // Create the results timer + timer := time.NewTimer(timeout) + for { + + // If we know about all rounds, return + if numResults == 0 { + roundCallback(allRoundsSucceeded, false, roundsResults) + return + } + + // Wait for info about rounds or the timeout to occur + select { + case <-timer.C: + roundCallback(false, true, roundsResults) + return + case roundReport := <-sendResults: + numResults-- + // Skip if the round is nil (unknown from historical rounds) + // they default to timed out, so correct behavior is preserved + if roundReport.RoundInfo == nil || roundReport.TimedOut { + allRoundsSucceeded = false + } else { + // If available, denote the result + roundId := id.Round(roundReport.RoundInfo.ID) + if states.Round(roundReport.RoundInfo.State) == states.COMPLETED { + roundsResults[roundId] = Succeeded + } else { + roundsResults[roundId] = Failed + allRoundsSucceeded = false + + } + } + } + } + }() + + return nil +} + +// Helper function which asynchronously pings a random gateway until +// it gets information on it's requested historical rounds +func (c *Client) getHistoricalRounds(msg *pb.HistoricalRounds, + instance *network.Instance, sendResults chan ds.EventReturn, + comms historicalRoundsComm) { + + var resp *pb.HistoricalRoundsResponse + + for { + // Find a gateway to request about the roundRequests + gwHost, err := gateway.Get(instance.GetPartialNdf().Get(), comms, c.rng.GetStream()) + if err != nil { + globals.Log.FATAL.Panicf("Failed to track network, NDF has corrupt "+ + "data: %s", err) + } + + // If an error, retry with (potentially) a different gw host. + // If no error from received gateway request, exit loop + // and process rounds + resp, err = comms.RequestHistoricalRounds(gwHost, msg) + if err == nil { + break + } + } + + // Process historical rounds, sending back to the caller thread + for _, ri := range resp.Rounds { + sendResults <- ds.EventReturn{ + ri, + false, + } + } +} diff --git a/api/results_test.go b/api/results_test.go new file mode 100644 index 0000000000000000000000000000000000000000..54433f2e9b8bcfd91e3d83e5745bdc1d3ceecc75 --- /dev/null +++ b/api/results_test.go @@ -0,0 +1,249 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// +package api + +import ( + pb "gitlab.com/elixxir/comms/mixmessages" + ds "gitlab.com/elixxir/comms/network/dataStructures" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/primitives/id" + "testing" + "time" +) + +const numRounds = 10 + +// Happy path +func TestClient_GetRoundResults(t *testing.T) { + // Populate a round list to request + var roundList []id.Round + for i := 0; i < numRounds; i++ { + roundList = append(roundList, id.Round(i)) + } + + // Pre-populate the results channel with successful rounds + sendResults := make(chan ds.EventReturn, len(roundList)) + for i := 0; i < numRounds; i++ { + sendResults <- ds.EventReturn{ + RoundInfo: &pb.RoundInfo{ + ID: uint64(i), + State: uint32(states.COMPLETED), + }, + TimedOut: false, + } + } + + // Create a new copy of the test client for this test + client, err := newTestingClient(t) + if err != nil { + t.Errorf("Failed in setup: %v", err) + } + + // Construct the round call back function signature + var successfulRounds, timeout bool + receivedRCB := func(allRoundsSucceeded, timedOut bool, rounds map[id.Round]RoundResult) { + successfulRounds = allRoundsSucceeded + timeout = timedOut + } + + // Call the round results + err = client.getRoundResults(roundList, time.Duration(10)*time.Millisecond, + receivedRCB, sendResults, NewNoHistoricalRoundsComm()) + if err != nil { + t.Errorf("Error in happy path: %v", err) + } + + // Sleep to allow the report to come through the pipeline + time.Sleep(1 * time.Second) + + // If any rounds timed out or any round failed, the happy path has failed + if timeout || !successfulRounds { + t.Errorf("Unexpected round failures in happy path. "+ + "Expected all rounds to succeed with no timeouts."+ + "\n\tTimedOut: %v"+ + "\n\tallRoundsSucceeded: %v", timeout, successfulRounds) + } + +} + +// Checks that an two failed rounds (one timed out, one failure) +// affects the values in the report. +// Kept separately to ensure uncoupled failed rounds +// affect both report booleans +func TestClient_GetRoundResults_FailedRounds(t *testing.T) { + // Populate a round list to request + var roundList []id.Round + for i := 0; i < numRounds; i++ { + roundList = append(roundList, id.Round(i)) + } + + // Pre-populate the results channel with mostly successful rounds + sendResults := make(chan ds.EventReturn, len(roundList)) + for i := 0; i < numRounds; i++ { + // Last two rounds will have a failure and a timeout respectively + result := ds.EventReturn{ + RoundInfo: &pb.RoundInfo{ + ID: uint64(i), + State: uint32(states.COMPLETED), + }, + TimedOut: false, + } + if i == numRounds-2 { + result.RoundInfo.State = uint32(states.FAILED) + } + + sendResults <- result + + } + + // Create a new copy of the test client for this test + client, err := newTestingClient(t) + if err != nil { + t.Errorf("Failed in setup: %v", err) + } + + // Construct the round call back function signature + var successfulRounds, timeout bool + receivedRCB := func(allRoundsSucceeded, timedOut bool, rounds map[id.Round]RoundResult) { + successfulRounds = allRoundsSucceeded + timeout = timedOut + } + + // Call the round results + err = client.getRoundResults(roundList, time.Duration(10)*time.Millisecond, + receivedRCB, sendResults, NewNoHistoricalRoundsComm()) + if err != nil { + t.Errorf("Error in happy path: %v", err) + } + + // Sleep to allow the report to come through the pipeline + time.Sleep(2 * time.Second) + + // If no rounds have failed, this test has failed + if successfulRounds { + t.Errorf("Expected some rounds to fail. "+ + "\n\tTimedOut: %v"+ + "\n\tallRoundsSucceeded: %v", timeout, successfulRounds) + } + +} + +// Use the historical rounds interface which actually sends back rounds +func TestClient_GetRoundResults_HistoricalRounds(t *testing.T) { + // Populate a round list to request + var roundList []id.Round + for i := 0; i < numRounds; i++ { + roundList = append(roundList, id.Round(i)) + } + + // Pre-populate the results channel with successful rounds + sendResults := make(chan ds.EventReturn, len(roundList)-2) + for i := 0; i < numRounds; i++ { + // Skip sending rounds intended for historical rounds comm + if i == failedHistoricalRoundID || + i == completedHistoricalRoundID { + continue + } + + sendResults <- ds.EventReturn{ + RoundInfo: &pb.RoundInfo{ + ID: uint64(i), + State: uint32(states.COMPLETED), + }, + TimedOut: false, + } + } + + // Create a new copy of the test client for this test + client, err := newTestingClient(t) + if err != nil { + t.Errorf("Failed in setup: %v", err) + } + + // Overpopulate the round buffer, ensuring a circle back of the ring buffer + for i := 1; i <= ds.RoundInfoBufLen+completedHistoricalRoundID+1; i++ { + ri := &pb.RoundInfo{ID: uint64(i)} + if err = signRoundInfo(ri); err != nil { + t.Errorf("Failed to sign round in set up: %v", err) + } + + err = client.network.GetInstance().RoundUpdate(ri) + if err != nil { + t.Errorf("Failed to upsert round in set up: %v", err) + } + + } + + // Construct the round call back function signature + var successfulRounds, timeout bool + receivedRCB := func(allRoundsSucceeded, timedOut bool, rounds map[id.Round]RoundResult) { + successfulRounds = allRoundsSucceeded + timeout = timedOut + } + + // Call the round results + err = client.getRoundResults(roundList, time.Duration(10)*time.Millisecond, + receivedRCB, sendResults, NewHistoricalRoundsComm()) + if err != nil { + t.Errorf("Error in happy path: %v", err) + } + + // Sleep to allow the report to come through the pipeline + time.Sleep(2 * time.Second) + + // If no round failed, this test has failed + if successfulRounds { + t.Errorf("Expected historical rounds to have round failures"+ + "\n\tTimedOut: %v"+ + "\n\tallRoundsSucceeded: %v", timeout, successfulRounds) + } +} + +// Force some timeouts by not populating the entire results channel +func TestClient_GetRoundResults_Timeout(t *testing.T) { + // Populate a round list to request + var roundList []id.Round + for i := 0; i < numRounds; i++ { + roundList = append(roundList, id.Round(i)) + } + + // Create a broken channel which will never send, + // forcing a timeout + var sendResults chan ds.EventReturn + sendResults = nil + + // Create a new copy of the test client for this test + client, err := newTestingClient(t) + if err != nil { + t.Errorf("Failed in setup: %v", err) + } + + // Construct the round call back function signature + var successfulRounds, timeout bool + receivedRCB := func(allRoundsSucceeded, timedOut bool, rounds map[id.Round]RoundResult) { + successfulRounds = allRoundsSucceeded + timeout = timedOut + } + + // Call the round results + err = client.getRoundResults(roundList, time.Duration(10)*time.Millisecond, + receivedRCB, sendResults, NewNoHistoricalRoundsComm()) + if err != nil { + t.Errorf("Error in happy path: %v", err) + } + + // Sleep to allow the report to come through the pipeline + time.Sleep(2 * time.Second) + + // If no rounds have timed out , this test has failed + if !timeout { + t.Errorf("Expected all rounds to timeout with no valid round reporter."+ + "\n\tTimedOut: %v"+ + "\n\tallRoundsSucceeded: %v", timeout, successfulRounds) + } + +} diff --git a/api/utilsInterfaces_test.go b/api/utilsInterfaces_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6eb4c466a51845e756f275483271a785b643feaa --- /dev/null +++ b/api/utilsInterfaces_test.go @@ -0,0 +1,117 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// +package api + +import ( + "gitlab.com/elixxir/client/interfaces" + "gitlab.com/elixxir/client/interfaces/message" + "gitlab.com/elixxir/client/interfaces/params" + "gitlab.com/elixxir/client/stoppable" + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/comms/network" + cE2e "gitlab.com/elixxir/crypto/e2e" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/comms/connect" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" +) + +// Mock comm struct which returns no historical round data +type noHistoricalRounds struct{} + +// Constructor for noHistoricalRounds +func NewNoHistoricalRoundsComm() *noHistoricalRounds { + return &noHistoricalRounds{} +} + +// Returns no rounds back +func (ht *noHistoricalRounds) RequestHistoricalRounds(host *connect.Host, + message *pb.HistoricalRounds) (*pb.HistoricalRoundsResponse, error) { + return nil, nil +} + +// Built for interface adherence +func (ht *noHistoricalRounds) GetHost(hostId *id.ID) (*connect.Host, bool) { + return nil, false +} + +// Generate a mock comm which returns some historical round data +type historicalRounds struct{} + +// Constructor for historicalRounds comm interface +func NewHistoricalRoundsComm() *historicalRounds { + return &historicalRounds{} +} + +// Round IDs to return on mock historicalRounds comm +const failedHistoricalRoundID = 7 +const completedHistoricalRoundID = 8 + +// Mock comms endpoint which returns historical rounds +func (ht *historicalRounds) RequestHistoricalRounds(host *connect.Host, + message *pb.HistoricalRounds) (*pb.HistoricalRoundsResponse, error) { + // Return one successful and one failed mock round + failedRound := &pb.RoundInfo{ + ID: failedHistoricalRoundID, + State: uint32(states.FAILED), + } + + completedRound := &pb.RoundInfo{ + ID: completedHistoricalRoundID, + State: uint32(states.COMPLETED), + } + + return &pb.HistoricalRoundsResponse{ + Rounds: []*pb.RoundInfo{failedRound, completedRound}, + }, nil +} + +// Build for interface adherence +func (ht *historicalRounds) GetHost(hostId *id.ID) (*connect.Host, bool) { + return nil, true +} + +// Contains a test implementation of the networkManager interface. +type testNetworkManagerGeneric struct { + instance *network.Instance +} + +/* Below methods built for interface adherence */ +func (t *testNetworkManagerGeneric) GetHealthTracker() interfaces.HealthTracker { + return nil +} +func (t *testNetworkManagerGeneric) Follow() (stoppable.Stoppable, error) { + return nil, nil +} +func (t *testNetworkManagerGeneric) CheckGarbledMessages() { + return +} +func (t *testNetworkManagerGeneric) SendE2E(m message.Send, p params.E2E) ( + []id.Round, cE2e.MessageID, error) { + rounds := []id.Round{id.Round(0), id.Round(1), id.Round(2)} + return rounds, cE2e.MessageID{}, nil + +} +func (t *testNetworkManagerGeneric) SendUnsafe(m message.Send, p params.Unsafe) ([]id.Round, error) { + return nil, nil +} +func (t *testNetworkManagerGeneric) SendCMIX(message format.Message, rid *id.ID, p params.CMIX) (id.Round, ephemeral.Id, error) { + return id.Round(0), ephemeral.Id{}, nil +} +func (t *testNetworkManagerGeneric) GetInstance() *network.Instance { + return t.instance +} +func (t *testNetworkManagerGeneric) RegisterWithPermissioning(string) ([]byte, error) { + return nil, nil +} +func (t *testNetworkManagerGeneric) GetRemoteVersion() (string, error) { + return "test", nil +} +func (t *testNetworkManagerGeneric) GetStoppable() stoppable.Stoppable { + return &stoppable.Multi{} +} diff --git a/api/utils_test.go b/api/utils_test.go new file mode 100644 index 0000000000000000000000000000000000000000..74360610803c2107181f87efa0f29363d6da3f80 --- /dev/null +++ b/api/utils_test.go @@ -0,0 +1,151 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package api + +import ( + "github.com/pkg/errors" + "gitlab.com/elixxir/client/globals" + "gitlab.com/elixxir/client/interfaces/params" + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/comms/network" + "gitlab.com/elixxir/comms/testkeys" + "gitlab.com/xx_network/comms/connect" + "gitlab.com/xx_network/comms/signature" + "gitlab.com/xx_network/crypto/signature/rsa" + "gitlab.com/xx_network/crypto/tls" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/ndf" + "gitlab.com/xx_network/primitives/utils" + "testing" +) + +func newTestingClient(face interface{}) (*Client, error) { + switch face.(type) { + case *testing.T, *testing.M, *testing.B, *testing.PB: + break + default: + globals.Log.FATAL.Panicf("InitTestingSession is restricted to testing only. Got %T", face) + } + + def := getNDF(face) + marshalledDef, _ := def.Marshal() + storageDir := "ignore.1" + password := []byte("hunter2") + err := NewClient(string(marshalledDef), storageDir, password, "AAAA") + if err != nil { + return nil, errors.Errorf("Could not construct a mock client: %v", err) + } + + c, err := OpenClient(storageDir, password, params.GetDefaultNetwork()) + if err != nil { + return nil, errors.Errorf("Could not open a mock client: %v", err) + } + + commsManager := connect.NewManagerTesting(face) + + cert, err := utils.ReadFile(testkeys.GetNodeCertPath()) + if err != nil { + globals.Log.FATAL.Panicf("Failed to create new test Instance: %v", err) + } + + commsManager.AddHost(&id.Permissioning, "", cert, connect.GetDefaultHostParams()) + instanceComms := &connect.ProtoComms{ + Manager: commsManager, + } + + thisInstance, err := network.NewInstanceTesting(instanceComms, def, def, nil, nil, face) + if err != nil { + return nil, nil + } + + c.network = &testNetworkManagerGeneric{instance: thisInstance} + + return c, nil +} + +// Helper function which generates an ndf for testing +func getNDF(face interface{}) *ndf.NetworkDefinition { + switch face.(type) { + case *testing.T, *testing.M, *testing.B, *testing.PB: + break + default: + globals.Log.FATAL.Panicf("InitTestingSession is restricted to testing only. Got %T", face) + } + + cert, _ := utils.ReadFile(testkeys.GetNodeCertPath()) + nodeID := id.NewIdFromBytes([]byte("gateway"), face) + return &ndf.NetworkDefinition{ + Registration: ndf.Registration{ + TlsCertificate: string(cert), + }, + Nodes: []ndf.Node{ + { + ID: nodeID.Bytes(), + Address: "", + TlsCertificate: string(cert), + }, + }, + Gateways: []ndf.Gateway{ + { + ID: nodeID.Bytes(), + Address: "", + TlsCertificate: string(cert), + }, + }, + E2E: ndf.Group{ + Prime: "E2EE983D031DC1DB6F1A7A67DF0E9A8E5561DB8E8D49413394C049B" + + "7A8ACCEDC298708F121951D9CF920EC5D146727AA4AE535B0922C688B55B3DD2AE" + + "DF6C01C94764DAB937935AA83BE36E67760713AB44A6337C20E7861575E745D31F" + + "8B9E9AD8412118C62A3E2E29DF46B0864D0C951C394A5CBBDC6ADC718DD2A3E041" + + "023DBB5AB23EBB4742DE9C1687B5B34FA48C3521632C4A530E8FFB1BC51DADDF45" + + "3B0B2717C2BC6669ED76B4BDD5C9FF558E88F26E5785302BEDBCA23EAC5ACE9209" + + "6EE8A60642FB61E8F3D24990B8CB12EE448EEF78E184C7242DD161C7738F32BF29" + + "A841698978825B4111B4BC3E1E198455095958333D776D8B2BEEED3A1A1A221A6E" + + "37E664A64B83981C46FFDDC1A45E3D5211AAF8BFBC072768C4F50D7D7803D2D4F2" + + "78DE8014A47323631D7E064DE81C0C6BFA43EF0E6998860F1390B5D3FEACAF1696" + + "015CB79C3F9C2D93D961120CD0E5F12CBB687EAB045241F96789C38E89D796138E" + + "6319BE62E35D87B1048CA28BE389B575E994DCA755471584A09EC723742DC35873" + + "847AEF49F66E43873", + Generator: "2", + }, + CMIX: ndf.Group{ + Prime: "9DB6FB5951B66BB6FE1E140F1D2CE5502374161FD6538DF1648218642F0B5C48" + + "C8F7A41AADFA187324B87674FA1822B00F1ECF8136943D7C55757264E5A1A44F" + + "FE012E9936E00C1D3E9310B01C7D179805D3058B2A9F4BB6F9716BFE6117C6B5" + + "B3CC4D9BE341104AD4A80AD6C94E005F4B993E14F091EB51743BF33050C38DE2" + + "35567E1B34C3D6A5C0CEAA1A0F368213C3D19843D0B4B09DCB9FC72D39C8DE41" + + "F1BF14D4BB4563CA28371621CAD3324B6A2D392145BEBFAC748805236F5CA2FE" + + "92B871CD8F9C36D3292B5509CA8CAA77A2ADFC7BFD77DDA6F71125A7456FEA15" + + "3E433256A2261C6A06ED3693797E7995FAD5AABBCFBE3EDA2741E375404AE25B", + Generator: "5C7FF6B06F8F143FE8288433493E4769C4D988ACE5BE25A0E24809670716C613" + + "D7B0CEE6932F8FAA7C44D2CB24523DA53FBE4F6EC3595892D1AA58C4328A06C4" + + "6A15662E7EAA703A1DECF8BBB2D05DBE2EB956C142A338661D10461C0D135472" + + "085057F3494309FFA73C611F78B32ADBB5740C361C9F35BE90997DB2014E2EF5" + + "AA61782F52ABEB8BD6432C4DD097BC5423B285DAFB60DC364E8161F4A2A35ACA" + + "3A10B1C4D203CC76A470A33AFDCBDD92959859ABD8B56E1725252D78EAC66E71" + + "BA9AE3F1DD2487199874393CD4D832186800654760E1E34C09E4D155179F9EC0" + + "DC4473F996BDCE6EED1CABED8B6F116F7AD9CF505DF0F998E34AB27514B0FFE7", + }, + } +} + +// Signs a passed round info with the key tied to the test node cert +// used throughout utils and other tests +func signRoundInfo(ri *pb.RoundInfo) error { + privKeyFromFile := testkeys.LoadFromPath(testkeys.GetNodeKeyPath()) + + pk, err := tls.LoadRSAPrivateKey(string(privKeyFromFile)) + if err != nil { + return errors.Errorf("Couldn't load private key: %+v", err) + } + + ourPrivateKey := &rsa.PrivateKey{PrivateKey: *pk} + + return signature.Sign(ri, ourPrivateKey) + +} diff --git a/bindings/callback.go b/bindings/callback.go index 68ca5646e0122f8a08b1d5d27ca580e2783e603c..4fc0431a41718961405f040fed2a0691aa86b44d 100644 --- a/bindings/callback.go +++ b/bindings/callback.go @@ -38,7 +38,7 @@ type RoundEventCallback interface { // RoundEventHandler handles round events happening on the cMix network. type MessageDeliveryCallback interface { - EventCallback(msgID []byte, delivered, timedOut bool) + EventCallback(msgID []byte, delivered, timedOut bool, roundResults []byte) } // AuthRequestCallback notifies the register whenever they receive an auth diff --git a/bindings/client.go b/bindings/client.go index 7b026bd913960409189070363fb87d7bad92ed34..866721af2ed32316a3191c0b60d7bfbdfe82691a 100644 --- a/bindings/client.go +++ b/bindings/client.go @@ -16,9 +16,7 @@ import ( "gitlab.com/elixxir/client/interfaces/contact" "gitlab.com/elixxir/client/interfaces/message" "gitlab.com/elixxir/client/interfaces/params" - "gitlab.com/elixxir/client/interfaces/utility" "gitlab.com/elixxir/comms/mixmessages" - ds "gitlab.com/elixxir/comms/network/dataStructures" "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/primitives/id" "time" @@ -298,7 +296,7 @@ func (c *Client) RegisterRoundEventsHandler(rid int, cb RoundEventCallback, // RegisterMessageDeliveryCB allows the caller to get notified if the rounds a // message was sent in successfully completed. Under the hood, this uses the same -// interface as RegisterRoundEventsHandler, but provides a convent way to use +// interface as RegisterRoundEventsHandler, but provides a convenient way to use // the interface in its most common form, looking up the result of message // retrieval // @@ -307,37 +305,30 @@ func (c *Client) RegisterRoundEventsHandler(rid int, cb RoundEventCallback, // This function takes the marshaled send report to ensure a memory leak does // not occur as a result of both sides of the bindings holding a reference to // the same pointer. -func (c *Client) RegisterMessageDeliveryCB(marshaledSendReport []byte, - mdc MessageDeliveryCallback, timeoutMS int) (*Unregister, error) { +func (c *Client) WaitForRoundCompletion(marshaledSendReport []byte, + mdc MessageDeliveryCallback, timeoutMS int) error { sr, err := UnmarshalSendReport(marshaledSendReport) if err != nil { - return nil, errors.New(fmt.Sprintf("Failed to "+ - "RegisterMessageDeliveryCB: %+v", err)) + return errors.New(fmt.Sprintf("Failed to "+ + "WaitForRoundCompletion callback due to bad Send Report: %+v", err)) } - /*check message delivery*/ - sendResults := make(chan ds.EventReturn, len(sr.rl.list)) - roundEvents := c.api.GetRoundEvents() + f := func(allRoundsSucceeded, timedOut bool, rounds map[id.Round]api.RoundResult){ + results := make([]byte, len(sr.rl.list)) - reventObjs := make([]*ds.EventCallback, len(sr.rl.list)) + for i, r := range sr.rl.list{ + if result, exists := rounds[r]; exists{ + results[i] = byte(result) + } + } - for i, r := range sr.rl.list { - reventObjs[i] = roundEvents.AddRoundEventChan(r, sendResults, - time.Duration(timeoutMS)*time.Millisecond, states.COMPLETED, - states.FAILED) + mdc.EventCallback(sr.mid.Marshal(), allRoundsSucceeded, timedOut, results) } - go func() { - success, _, numTmeout := utility.TrackResults(sendResults, len(sr.rl.list)) - if !success { - mdc.EventCallback(sr.mid[:], false, numTmeout > 0) - } else { - mdc.EventCallback(sr.mid[:], true, false) - } - }() + timeout := time.Duration(timeoutMS)*time.Millisecond - return newRoundListUnregister(sr.rl.list, reventObjs, roundEvents), nil + return c.api.GetRoundResults(sr.rl.list, timeout, f) } // Returns a user object from which all information about the current user diff --git a/cmd/root.go b/cmd/root.go index 6a20d202be592f7d2d50b826af9ec3ca14b69cb5..8dcdb2b3ab4a2cc169d5924b234f949f9a6664c0 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -134,16 +134,32 @@ var rootCmd = &cobra.Command{ for i := 0; i < sendCnt; i++ { fmt.Printf("Sending to %s: %s\n", recipientID, msgBody) var roundIDs []id.Round + var roundTimeout time.Duration if unsafe { roundIDs, err = client.SendUnsafe(msg, paramsUnsafe) + roundTimeout = paramsUnsafe.Timeout } else { roundIDs, _, err = client.SendE2E(msg, paramsE2E) + roundTimeout = paramsE2E.Timeout } if err != nil { jww.FATAL.Panicf("%+v", err) } + + // Construct the callback function which prints out the rounds' results + f := func(allRoundsSucceeded, timedOut bool, + rounds map[id.Round]api.RoundResult) { + printRoundResults(allRoundsSucceeded, timedOut, rounds, roundIDs, msg) + } + + // Have the client report back the round results + err = client.GetRoundResults(roundIDs, roundTimeout, f) + if err != nil { + jww.FATAL.Panicf("%+v", err) + } + jww.INFO.Printf("RoundIDs: %+v\n", roundIDs) time.Sleep(sendDelay * time.Millisecond) } @@ -181,6 +197,46 @@ var rootCmd = &cobra.Command{ }, } +// Helper function which prints the round resuls +func printRoundResults(allRoundsSucceeded, timedOut bool, + rounds map[id.Round]api.RoundResult, roundIDs []id.Round, msg message.Send) { + + // Done as string slices for easy and human readable printing + successfulRounds := make([]string, 0) + failedRounds := make([]string, 0) + timedOutRounds := make([]string, 0) + + for _, r := range roundIDs { + // Group all round reports into a category based on their + // result (successful, failed, or timed out) + if result, exists := rounds[r]; exists { + if result == api.Succeeded { + successfulRounds = append(successfulRounds, strconv.Itoa(int(r))) + } else if result == api.Failed { + failedRounds = append(failedRounds, strconv.Itoa(int(r))) + } else { + timedOutRounds = append(timedOutRounds, strconv.Itoa(int(r))) + } + } + } + + jww.INFO.Printf("Result of sending message \"%s\" to \"%v\":", + msg.Payload, msg.Recipient) + + // Print out all rounds results, if they are populated + if len(successfulRounds) > 0 { + jww.INFO.Printf("\tRound(s) %v successful", strings.Join(successfulRounds, ",")) + } + if len(failedRounds) > 0 { + jww.ERROR.Printf("\tRound(s) %v failed", strings.Join(failedRounds, ",")) + } + if len(timedOutRounds) > 0 { + jww.ERROR.Printf("\tRound(s) %v timed " + + "\n\tout (no network resolution could be found)", strings.Join(timedOutRounds, ",")) + } + +} + func createClient() *api.Client { initLog(viper.GetBool("verbose"), viper.GetString("log")) jww.INFO.Printf(Version()) @@ -582,8 +638,9 @@ func init() { "Identity code (optional)") viper.BindPFlag("regcode", rootCmd.Flags().Lookup("regcode")) - rootCmd.Flags().StringP("message", "m", "", "Message to send") - viper.BindPFlag("message", rootCmd.Flags().Lookup("message")) + rootCmd.PersistentFlags().StringP("message", "m", "", + "Message to send") + viper.BindPFlag("message", rootCmd.PersistentFlags().Lookup("message")) rootCmd.Flags().UintP("sendid", "", 0, "Use precanned user id (must be between 1 and 40, inclusive)") diff --git a/cmd/single.go b/cmd/single.go index e3c2ad639a5b7b0cd160d29023f06a407f0c2b61..a7c8fafcc17af98cd355bdbbc2de807fd2f48432 100644 --- a/cmd/single.go +++ b/cmd/single.go @@ -116,10 +116,6 @@ func init() { "Path to contact file to send message to.") _ = viper.BindPFlag("contact", singleCmd.Flags().Lookup("contact")) - singleCmd.Flags().StringP("message", "m", "", - "Message to send.") - _ = viper.BindPFlag("message", singleCmd.Flags().Lookup("message")) - singleCmd.Flags().Uint8("maxMessages", 1, "The max number of single-use response messages.") _ = viper.BindPFlag("maxMessages", singleCmd.Flags().Lookup("maxMessages")) diff --git a/go.mod b/go.mod index c83caf94f41f20efd27b7b383eb92a04fb947ce5..0792504a91217fe7d064204d8894e114382df4a5 100644 --- a/go.mod +++ b/go.mod @@ -17,7 +17,7 @@ require ( github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/viper v1.7.1 gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 - gitlab.com/elixxir/comms v0.0.4-0.20210217173658-d751b9c00e5a + gitlab.com/elixxir/comms v0.0.4-0.20210218234550-f2e03b19bdb2 gitlab.com/elixxir/crypto v0.0.7-0.20210216174551-f806f79610eb gitlab.com/elixxir/ekv v0.1.4 gitlab.com/elixxir/primitives v0.0.3-0.20210216174458-2a23825c1eb1 diff --git a/go.sum b/go.sum index 1d690892a15314e6af21a02b0f7d6f50122548a1..0d535ad56dce99718be355c34655e8d41088e32e 100644 --- a/go.sum +++ b/go.sum @@ -251,12 +251,12 @@ github.com/zeebo/pcg v1.0.0 h1:dt+dx+HvX8g7Un32rY9XWoYnd0NmKmrIzpHF7qiTDj0= github.com/zeebo/pcg v1.0.0/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228 h1:Gi6rj4mAlK0BJIk1HIzBVMjWNjIUfstrsXC2VqLYPcA= gitlab.com/elixxir/bloomfilter v0.0.0-20200930191214-10e9ac31b228/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k= -gitlab.com/elixxir/comms v0.0.4-0.20210215180544-d672a6bfd53a h1:PrNMCPpAQQbSDlkHNBAIgjBWDVjTg7LDyPUlajGttUo= -gitlab.com/elixxir/comms v0.0.4-0.20210215180544-d672a6bfd53a/go.mod h1:ZXv+fpI/kRCzxxX6p4JXlonJVDl49t4+v71kEkBipgM= -gitlab.com/elixxir/comms v0.0.4-0.20210216174714-dc3e5c456e20 h1:XMKvdEDh/ThSowYroZ4PGY/eMNm1xV9ov4rf1UkgLCY= -gitlab.com/elixxir/comms v0.0.4-0.20210216174714-dc3e5c456e20/go.mod h1:GCbfPWB7VF5ZeDsLBCwfy0JiquG4OK6gsRjaIS66+yg= -gitlab.com/elixxir/comms v0.0.4-0.20210217173658-d751b9c00e5a h1:DYzWv9ffCssipLjwoCfe1zln7Mt+V155PY4rzAjflC0= -gitlab.com/elixxir/comms v0.0.4-0.20210217173658-d751b9c00e5a/go.mod h1:GCbfPWB7VF5ZeDsLBCwfy0JiquG4OK6gsRjaIS66+yg= +gitlab.com/elixxir/comms v0.0.4-0.20210210215855-f8a4b9751c59 h1:/MSWVvLFV8Z2H37l+9fC3z5aO/fXktwd3RKFT21dvcM= +gitlab.com/elixxir/comms v0.0.4-0.20210210215855-f8a4b9751c59/go.mod h1:ZXv+fpI/kRCzxxX6p4JXlonJVDl49t4+v71kEkBipgM= +gitlab.com/elixxir/comms v0.0.4-0.20210212194414-4c36bb47fa96 h1:66NNOMK9zQqW3xmGYll8wB/BKD5jMHOyYjpgwQD7b+Q= +gitlab.com/elixxir/comms v0.0.4-0.20210212194414-4c36bb47fa96/go.mod h1:ZXv+fpI/kRCzxxX6p4JXlonJVDl49t4+v71kEkBipgM= +gitlab.com/elixxir/comms v0.0.4-0.20210218234550-f2e03b19bdb2 h1:p5GunVi5sP9atTw3DKBkgV6k3eR9iTyI6m9GbUr8hhA= +gitlab.com/elixxir/comms v0.0.4-0.20210218234550-f2e03b19bdb2/go.mod h1:GCbfPWB7VF5ZeDsLBCwfy0JiquG4OK6gsRjaIS66+yg= gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4 h1:28ftZDeYEko7xptCZzeFWS1Iam95dj46TWFVVlKmw6A= gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4/go.mod h1:ucm9SFKJo+K0N2GwRRpaNr+tKXMIOVWzmyUD0SbOu2c= gitlab.com/elixxir/crypto v0.0.3 h1:znCt/x2bL4y8czTPaaFkwzdgSgW3BJc/1+dxyf1jqVw= @@ -295,8 +295,8 @@ gitlab.com/xx_network/primitives v0.0.2 h1:r45yKenJ9e7PylI1ZXJ1Es09oYNaYXjxVy9+u gitlab.com/xx_network/primitives v0.0.2/go.mod h1:cs0QlFpdMDI6lAo61lDRH2JZz+3aVkHy+QogOB6F/qc= gitlab.com/xx_network/primitives v0.0.4-0.20210210215152-e4534abf3ae5 h1:skLonqleopw1osf3xBQTNX9NjTV/OQP6AM4HKpLNUdc= gitlab.com/xx_network/primitives v0.0.4-0.20210210215152-e4534abf3ae5/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE= -gitlab.com/xx_network/primitives v0.0.4-0.20210215191517-2f56b21d6ed7 h1:Q4zU46rz3zf+9q6syhA8rk4SS9TQwsk4ggUJ68IiKv8= -gitlab.com/xx_network/primitives v0.0.4-0.20210215191517-2f56b21d6ed7/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE= +gitlab.com/xx_network/primitives v0.0.4-0.20210212180522-50ec526a6c12 h1:dOQS9tzT4fllDhU37pbJhAAW8qlB0HDjuf74rvzOZQQ= +gitlab.com/xx_network/primitives v0.0.4-0.20210212180522-50ec526a6c12/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE= gitlab.com/xx_network/primitives v0.0.4-0.20210215192713-e32335847d4f h1:0wFEYIHuPkWJuDkbDXNrwC5yGwkd7Mugt2BwcTqQbFY= gitlab.com/xx_network/primitives v0.0.4-0.20210215192713-e32335847d4f/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE= gitlab.com/xx_network/ring v0.0.2 h1:TlPjlbFdhtJrwvRgIg4ScdngMTaynx/ByHBRZiXCoL0=