diff --git a/README.md b/README.md index 1c169b50dd2d0a5d491a2137b2591a4d838b08f0..dc58f697ff36740f76504073c92ae3d96676b16d 100644 --- a/README.md +++ b/README.md @@ -1,7 +1,8 @@ # xx network Client -[Repository](https://git.xx.network/elixxir/client) | [Go Doc](https://pkg.go.dev/gitlab.com/elixxir/client/xxdk) | [Examples](https://git.xx.network/elixxir/xxdk-examples/-/tree/master) - +[Repository](https://git.xx.network/elixxir/client) +| [Go Doc](https://pkg.go.dev/gitlab.com/elixxir/client/xxdk) +| [Examples](https://git.xx.network/elixxir/xxdk-examples/-/tree/master) The client is a library and related command-line tool that facilitates making full-featured xx clients for all platforms. It interfaces with the cMix system, enabling access to all xx network messaging features, including @@ -11,7 +12,8 @@ This repository contains everything necessary to implement the xx network messag contains features to extend the base messaging protocols. The command-line tool accompanying the client library can be built for any platform supported by Go. The libraries are -built for iOS and Android using [gomobile](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile). +built for iOS and Android using [gomobile](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile). The libraries are built +for web assembly using the repository [xxdk-wasm](https://git.xx.network/elixxir/xxdk-wasm). For library writers, the client requires a writable folder to store data, functions for receiving and approving requests for creating secure end-to-end messaging channels, discovering users, and receiving different types of messages. @@ -48,7 +50,7 @@ $ GOOS=darwin GOARCH=amd64 CGO_ENABLED=0 go build -ldflags '-w -s' -o client.dar ### Fetching an NDF -All actions performed with the client require a current +All actions performed with the client require a current [network definition file (NDF)](https://xxdk-dev.xx.network/technical-glossary/#network-definition-file-ndf). The NDF is downloadable from the command line or [via an access point](https://xxdk-dev.xx.network/quick-reference#func-downloadandverifysignedndfwithurl) in the Client @@ -78,16 +80,17 @@ $ ./client getndf --env mainnet | jq . > ndf.json ``` Sample content of `ndf.json`: + ```json { - "Timestamp": "2021-01-29T01:19:49.227246827Z", - "Gateways": [ - { - "Id": "BRM+Iotl6ujIGhjRddZMBdauapS7Z6jL0FJGq7IkUdYB", - "Address": ":8440", - "Tls_certificate": "-----BEGIN CERTIFICATE-----\nMIIDbDCCAlSgAwIBAgIJAOUNtZneIYECMA0GCSqGSIb3DQEBBQUAMGgxCzAJBgNV\nBAYTAlVTMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQx\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJp\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\n6m52PyzMNV+2N21IPppKwA==\n-----END CERTIFICATE-----\n" - } - ] + "Timestamp": "2021-01-29T01:19:49.227246827Z", + "Gateways": [ + { + "Id": "BRM+IoTl6ujIGhjRddZMBdaUapS7Z6jL0FJGq7IkUdYB", + "Address": ":8440", + "Tls_certificate": "-----BEGIN CERTIFICATE-----\nMIIDbDCCAlSgAwIBAgIJA8UNtZneIYE2MA0GCSqGSIb3DQE3BQU8MGgxCzAJBgNV\nBaYTAlVTmRMwEQYDvQQiDApDYWxpZm9ybmLhMRIwEAYfVQqHDAlDbGFyZW1vbnQx\nGzAZBgNVBAoMElByaXZhdGVncml0eSBDb3JwLjETMBeGA1UEAwwKKi5jbWl4LnJp\ncDAeFw0xOTAzMDUxODM1NDNaFw0yOTAzMDIxODM1NDNaMGgxCzAJBgNVBAYTAlVT\nMRMwEQYDVQQIDApDYWxpZm9ybmlhMRIwEAYDVQQHDAlDbGFyZW1vbnQxGzAZBgNV\nBAoMElByaXZhdGVncml0eSBDb3JwLjETMBEGA1UEAwwKKi5jbWl4LnJpcDCCASIw\nDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAPP0WyVkfZA/CEd2DgKpcudn0oDh\nDwsjmx8LBDWsUgQzyLrFiVigfUmUefknUH3dTJjmiJtGqLsayCnWdqWLHPJYvFfs\nWYW0IGF93UG/4N5UAWO4okC3CYgKSi4ekpfw2zgZq0gmbzTnXcHF9gfmQ7jJUKSE\ntJPSNzXq+PZeJTC9zJAb4Lj8QzH18rDM8DaL2y1ns0Y2Hu0edBFn/OqavBJKb/uA\nm3AEjqeOhC7EQUjVamWlTBPt40+B/6aFJX5BYm2JFkRsGBIyBVL46MvC02MgzTT9\nbJIJfwqmBaTruwemNgzGu7Jk03hqqS1TUEvSI6/x8bVoba3orcKkf9HsDjECAwEA\nAaMZMBcwFQYDVR0RBA4wDIIKKi5jbWl4LnJpcDANBgkqhkiG9w0BAQUFAAOCAQEA\nneUocN4AbcQAC1+b3To8u5UGdaGxhcGyZBlAoenRVdjXK3lTjsMdMWb4QctgNfIf\nU/zuUn2mxTmF/ekP0gCCgtleZr9+DYKU5hlXk8K10uKxGD6EvoiXZzlfeUuotgp2\nqvI3ysOm/hvCfyEkqhfHtbxjV7j7v7eQFPbvNaXbLa0yr4C4vMK/Z09Ui9JrZ/Z4\ncyIkxfC6/rOqAirSdIp09EGiw7GM8guHyggE4IiZrDslT8V3xIl985cbCxSxeW1R\ntgH4rdEXuVe9+31oJhmXOE9ux2jCop9tEJMgWg7HStrJ5plPbb+HmjoX3nBO04E5\n6m52PyzMNV+2N21IPppKwA==\n-----END CERTIFICATE-----\n" + } + ] } ``` @@ -99,9 +102,9 @@ $ ./client getndf --help ### Sending Safe Messages Between Two (2) Users -> 💡 **Note:** For information on receiving messages and troubleshooting authenticated channel requests, -see [Receiving Messages](#receiving-messages) and -> [Confirming authenticated channel requests](#confirming-authenticated-channel-requests). +> 💡 **Note:** For information on receiving messages and troubleshooting authenticated channel requests, see +> [Receiving Messages](#receiving-messages) +> and [Confirming authenticated channel requests](#confirming-authenticated-channel-requests). To send messages with end-to-end encryption, you must first establish a connection or [authenticated channel](https://xxdk-dev.xx.network/technical-glossary#authenticated-channel) with the other user. @@ -148,7 +151,7 @@ Received 0 * `-s`: The storage directory for client session data. * `--writeContact`: Output the user's contact information to this file. * `--destfile` is used to specify the recipient. You can also use `--destid b64:...` using the user's base64 ID, which -is printed in the logs. + is printed in the logs. * `--unsafe`: Send message without encryption (necessary whenever you have not already established an e2e channel). * `--unsafe-channel-creation` Auto-create and auto-accept channel requests. * `-m`: The message to send. @@ -158,7 +161,7 @@ This is why we've used the `--unsafe` flag when creating the user contact files. However, when sending between users, the flag is dropped in exchange for `--unsafe-channel-creation`. For the authenticated channel creation to be considered "safe", the user should be prompted. You can do this by -explicitly accepting the channel creation when sending a request with `--send-auth-request` (while excluding the +explicitly accepting the channel creation when sending a request with `--send-auth-request` (while excluding the `--unsafe-channel-creation` flag) or explicitly accepting a request with `--accept-channel`: ```shell @@ -189,8 +192,8 @@ See [Sending Safe Messages Between Two (2) Users](#sending-safe-messages-between Setting up an authenticated channel between clients is a back-and-forth process that happens in sequence. One client sends a request and waits for the other to accept it. -See the previous section, [Sending safe messages between 2 users](#sending-safe-messages-between-two-2-users), for example, -commands showing how to set up an end-to-end connection between clients before sending messages. +See the previous section, [Sending safe messages between 2 users](#sending-safe-messages-between-two-2-users), for +example, commands showing how to set up an end-to-end connection between clients before sending messages. As with received messages, there is no command for checking for authenticated channel requests; you'll be notified of any pending requests whenever the client is run. @@ -226,7 +229,7 @@ request. This means your client has not received the request. If one has been se Full usage of the client can be found with `client --help`: -```shell +```text $ ./client --help Runs a client for cMix anonymous communication platform @@ -296,15 +299,19 @@ Flags: Use "client [command] --help" for more information about a command. ``` ->💡 **Note:** The client cannot be used on the xx network with pre-canned user IDs. +> 💡 **Note:** The client cannot be used on the xx network with pre-canned user IDs. ## Library Overview -The xx client is designed to be a go library (and, by extension, a C library). +The xx client is designed to be a Go library (and, by extension, a C library). Support is also present for Go mobile to build Android and iOS libraries. In addition, we bind all exported symbols from the bindings package for use on mobile platforms. +This library is also supported by WebAssembly through the [xxdk-wasm](https://git.xx.network/elixxir/xxdk-wasm) +repository. xxdk-wasm wraps the bindings package in this repository so that they can be used by Javascript when compiled +for WebAssembly. + ### Implementation Notes Clients must perform the same actions *in the same order* as shown in `cmd/root.go`. Specifically, certain handlers need @@ -323,6 +330,10 @@ and functions exposed by the Client API. The main entry point for developing with the client is `xxdk/cmix` (or `bindings/cmix`). We recommend using the [documentation in the Go package directory](https://pkg.go.dev/gitlab.com/elixxir/client/xxdk). +If you are developing with the client through the browser and Javascript, refer to the +[xxdk-wasm](https://git.xx.network/elixxir/xxdk-wasm) repository, which wraps the `bindings/cmix` package. You may also +want to refer to the [Go documentation](https://pkg.go.dev/gitlab.com/elixxir/xxdk-wasm/wasm). + Looking at the API will, for example, show you there is a `RoundEvents` callback registration function, which lets your client see round events. @@ -373,6 +384,7 @@ gomobile for binding should include a shell script that creates the bindings. Fo machine with Xcode installed. Important reference info: + 1. [Setting up gomobile and subcommands](https://pkg.go.dev/golang.org/x/mobile/cmd/gomobile) 2. [Reference cycles, type restrictions](https://pkg.go.dev/golang.org/x/mobile/cmd/gobind) diff --git a/auth/confirm.go b/auth/confirm.go index bc87fa182aace4b7c1b3a9db9544b8532ef2a963..0b7278c6e1f08b8535384d729b95c46eba650b04 100644 --- a/auth/confirm.go +++ b/auth/confirm.go @@ -132,7 +132,7 @@ func (s *state) confirm(partner contact.Contact, serviceTag string) ( /*send message*/ if err = s.store.StoreConfirmation(partner.ID, baseFmt.Marshal(), - mac, fp); err == nil { + mac, fp); err != nil { jww.WARN.Printf("Failed to store confirmation for replay "+ "for relationship between %s and %s, cannot be replayed: %+v", partner.ID, s.e2e.GetReceptionID(), err) @@ -171,8 +171,8 @@ func sendAuthConfirm(net cmixClient, partner *id.ID, } em := fmt.Sprintf("Confirm Request with %s (msgDigest: %s) sent on round %d", - partner, format.DigestContents(payload), sentRound) + partner, format.DigestContents(payload), sentRound.ID) jww.INFO.Print(em) event.Report(1, "Auth", "SendConfirm", em) - return sentRound, nil + return sentRound.ID, nil } diff --git a/auth/interface.go b/auth/interface.go index b0eb8fcea491a9e48e82b6045099eda02f9f5200..c515973886b9387d3442ee6f134aa47a428bb0ea 100644 --- a/auth/interface.go +++ b/auth/interface.go @@ -141,7 +141,7 @@ type cmixClient interface { DeleteFingerprint(identity *id.ID, fingerprint format.Fingerprint) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) + rounds.Round, ephemeral.Id, error) } // e2eHandler is a sub-interface of e2e.Handler containing diff --git a/auth/request.go b/auth/request.go index 91641307430d69f9e51097124680cd4697468285..17db1b2f64572d73a7601e606d72fa1d2fb5c2b1 100644 --- a/auth/request.go +++ b/auth/request.go @@ -164,10 +164,10 @@ func (s *state) request(partner contact.Contact, myfacts fact.FactList, } em := fmt.Sprintf("Auth Request with %s (msgDigest: %s) sent"+ - " on round %d", partner.ID, format.DigestContents(contents), round) + " on round %d", partner.ID, format.DigestContents(contents), round.ID) jww.INFO.Print(em) s.event.Report(1, "Auth", "RequestSent", em) - return round, nil + return round.ID, nil } diff --git a/auth/state_test.go b/auth/state_test.go index 31a85612297e22467a837ad6f9899a4c8489dd60..5dc12aca17520550e4857647e40c32bed212fcc4 100644 --- a/auth/state_test.go +++ b/auth/state_test.go @@ -69,8 +69,8 @@ func (mnm *mockNetManager) AddFingerprint(identity *id.ID, fingerprint format.Fi func (mnm *mockNetManager) DeleteFingerprint(identity *id.ID, fingerprint format.Fingerprint) {} func (mnm *mockNetManager) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) { - return id.Round(5), ephemeral.Id{}, nil + rounds.Round, ephemeral.Id, error) { + return rounds.Round{ID: 5}, ephemeral.Id{}, nil } type mockE2E struct { diff --git a/bindings/broadcast.go b/bindings/broadcast.go deleted file mode 100644 index 8d5255fa054ec1361a36fc10753bb8a6ada15271..0000000000000000000000000000000000000000 --- a/bindings/broadcast.go +++ /dev/null @@ -1,216 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -package bindings - -import ( - "encoding/json" - "github.com/pkg/errors" - "gitlab.com/elixxir/client/broadcast" - "gitlab.com/elixxir/client/cmix" - "gitlab.com/elixxir/client/cmix/identity/receptionID" - "gitlab.com/elixxir/client/cmix/rounds" - cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" - "gitlab.com/xx_network/crypto/signature/rsa" - "gitlab.com/xx_network/primitives/id/ephemeral" -) - -// Channel is a bindings-level struct encapsulating the broadcast.Channel client -// object. -type Channel struct { - ch broadcast.Channel -} - -// ChannelDef is the bindings representation of an elixxir/crypto -// broadcast.Channel object. -// -// Example JSON: -// { -// "Name": "My broadcast channel", -// "Description": "A broadcast channel for me to test things", -// "Salt": "gpUqW7N22sffMXsvPLE7BA==", -// "PubKey": "LS0tLS1CRUdJTiBSU0EgUFVCTElDIEtFWS0tLS0tCk1DZ0NJUUN2YkZVckJKRFpqT3Y0Y0MvUHZZdXNvQkFtUTFkb3Znb044aHRuUjA2T3F3SURBUUFCCi0tLS0tRU5EIFJTQSBQVUJMSUMgS0VZLS0tLS0=" -// } -type ChannelDef struct { - Name string - Description string - Salt []byte - PubKey []byte -} - -// BroadcastMessage is the bindings representation of a broadcast message. -// -// BroadcastMessage Example JSON: -// { -// "RoundID":42, -// "EphID":[0,0,0,0,0,0,24,61], -// "RoundURL":"https://dashboard.xx.network/rounds/25?xxmessenger=true", -// "Payload":"SGVsbG8sIGJyb2FkY2FzdCBmcmllbmRzIQ==" -// } -type BroadcastMessage struct { - BroadcastReport - Payload []byte -} - -// BroadcastReport is the bindings representation of the info on how a broadcast -// message was sent -// -// BroadcastReport Example JSON: -// { -// "Rounds": [25, 26, 29], -// "EphID":[0,0,0,0,0,0,24,61], -// "RoundURL":"https://dashboard.xx.network/rounds/25?xxmessenger=true" -// } -type BroadcastReport struct { - RoundsList - RoundURL string - EphID ephemeral.Id -} - -// BroadcastListener is the public function type bindings can use to listen for -// broadcast messages. -// -// Parameters: -// - []byte - the JSON marshalled bytes of the BroadcastMessage object, which -// can be passed into WaitForRoundResult to see if the broadcast succeeded. -type BroadcastListener interface { - Callback([]byte, error) -} - -// NewBroadcastChannel creates a bindings-layer broadcast channel and starts -// listening for new messages. -// -// Parameters: -// - cmixId - internal ID of cmix -// - channelDefinition - JSON marshalled ChannelDef object -func NewBroadcastChannel(cmixId int, channelDefinition []byte) (*Channel, error) { - c, err := cmixTrackerSingleton.get(cmixId) - if err != nil { - return nil, err - } - - def := &ChannelDef{} - err = json.Unmarshal(channelDefinition, def) - if err != nil { - return nil, errors.WithMessage(err, "Failed to unmarshal underlying channel definition") - } - - channelID, err := cryptoBroadcast.NewChannelID(def.Name, def.Description, def.Salt, def.PubKey) - if err != nil { - return nil, errors.WithMessage(err, "Failed to generate channel ID") - } - chanPubLoaded, err := rsa.LoadPublicKeyFromPem(def.PubKey) - if err != nil { - return nil, errors.WithMessage(err, "Failed to load public key") - } - - ch, err := broadcast.NewBroadcastChannel(cryptoBroadcast.Channel{ - ReceptionID: channelID, - Name: def.Name, - Description: def.Description, - Salt: def.Salt, - RsaPubKey: chanPubLoaded, - }, c.api.GetCmix(), c.api.GetRng()) - if err != nil { - return nil, errors.WithMessage(err, "Failed to create broadcast channel client") - } - - return &Channel{ch: ch}, nil -} - -// Listen registers a BroadcastListener for a given method. This allows users to -// handle incoming broadcast messages. -// -// Parameters: -// - l - BroadcastListener object -// - method - int corresponding to broadcast.Method constant, 0 for symmetric -// or 1 for asymmetric -func (c *Channel) Listen(l BroadcastListener, method int) error { - broadcastMethod := broadcast.Method(method) - listen := func(payload []byte, - receptionID receptionID.EphemeralIdentity, round rounds.Round) { - l.Callback(json.Marshal(&BroadcastMessage{ - BroadcastReport: BroadcastReport{ - RoundsList: makeRoundsList(round.ID), - RoundURL: getRoundURL(round.ID), - EphID: receptionID.EphId, - }, - Payload: payload, - })) - } - return c.ch.RegisterListener(listen, broadcastMethod) -} - -// Broadcast sends a given payload over the broadcast channel using symmetric -// broadcast. -// -// Returns: -// - []byte - the JSON marshalled bytes of the BroadcastReport object, which -// can be passed into Cmix.WaitForRoundResult to see if the broadcast -// succeeded. -func (c *Channel) Broadcast(payload []byte) ([]byte, error) { - rid, eid, err := c.ch.Broadcast(payload, cmix.GetDefaultCMIXParams()) - if err != nil { - return nil, err - } - return json.Marshal(BroadcastReport{ - RoundsList: makeRoundsList(rid), - RoundURL: getRoundURL(rid), - EphID: eid, - }) -} - -// BroadcastAsymmetric sends a given payload over the broadcast channel using -// asymmetric broadcast. This mode of encryption requires a private key. -// -// Returns: -// - []byte - the JSON marshalled bytes of the BroadcastReport object, which -// can be passed into WaitForRoundResult to see if the broadcast succeeded. -func (c *Channel) BroadcastAsymmetric(payload, pk []byte) ([]byte, error) { - pkLoaded, err := rsa.LoadPrivateKeyFromPem(pk) - if err != nil { - return nil, err - } - rid, eid, err := c.ch.BroadcastAsymmetric(pkLoaded, payload, cmix.GetDefaultCMIXParams()) - if err != nil { - return nil, err - } - return json.Marshal(BroadcastReport{ - RoundsList: makeRoundsList(rid), - RoundURL: getRoundURL(rid), - EphID: eid, - }) -} - -// MaxPayloadSize returns the maximum possible payload size which can be -// broadcast. -func (c *Channel) MaxPayloadSize() int { - return c.ch.MaxPayloadSize() -} - -// MaxAsymmetricPayloadSize returns the maximum possible payload size which can -// be broadcast. -func (c *Channel) MaxAsymmetricPayloadSize() int { - return c.ch.MaxAsymmetricPayloadSize() -} - -// Get returns the result of calling json.Marshal on a ChannelDef based on the -// underlying crypto broadcast.Channel. -func (c *Channel) Get() ([]byte, error) { - def := c.ch.Get() - return json.Marshal(&ChannelDef{ - Name: def.Name, - Description: def.Description, - Salt: def.Salt, - PubKey: rsa.CreatePublicKeyPem(def.RsaPubKey), - }) -} - -// Stop stops the channel from listening for more messages. -func (c *Channel) Stop() { - c.ch.Stop() -} diff --git a/bindings/broadcast_test.go b/bindings/broadcast_test.go deleted file mode 100644 index bb7d93d52431b545d536b53742f49ee75ba786f4..0000000000000000000000000000000000000000 --- a/bindings/broadcast_test.go +++ /dev/null @@ -1,75 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -package bindings - -import ( - "encoding/json" - "gitlab.com/elixxir/crypto/cmix" - "gitlab.com/xx_network/crypto/csprng" - "gitlab.com/xx_network/crypto/signature/rsa" - "gitlab.com/xx_network/primitives/id" - "gitlab.com/xx_network/primitives/id/ephemeral" - "testing" - "time" -) - -func TestChannelDef_JSON(t *testing.T) { - rng := csprng.NewSystemRNG() - rng.SetSeed([]byte("rng")) - pk, _ := rsa.GenerateKey(rng, 256) - cd := ChannelDef{ - Name: "My broadcast channel", - Description: "A broadcast channel for me to test things", - Salt: cmix.NewSalt(rng, 16), - PubKey: rsa.CreatePublicKeyPem(pk.GetPublic()), - } - - cdJson, err := json.Marshal(cd) - if err != nil { - t.Errorf("Failed to marshal channel def: %+v", err) - } - t.Log(string(cdJson)) -} - -func TestBroadcastMessage_JSON(t *testing.T) { - uid := id.NewIdFromString("zezima", id.User, t) - eid, _, _, err := ephemeral.GetId(uid, 16, time.Now().UnixNano()) - if err != nil { - t.Errorf("Failed to form ephemeral ID: %+v", err) - } - bm := BroadcastMessage{ - BroadcastReport: BroadcastReport{ - RoundsList: makeRoundsList(42), - EphID: eid, - }, - Payload: []byte("Hello, broadcast friends!"), - } - bmJson, err := json.Marshal(bm) - if err != nil { - t.Errorf("Failed to marshal broadcast message: %+v", err) - } - t.Log(string(bmJson)) -} - -func TestBroadcastReport_JSON(t *testing.T) { - uid := id.NewIdFromString("zezima", id.User, t) - eid, _, _, err := ephemeral.GetId(uid, 16, time.Now().UnixNano()) - if err != nil { - t.Errorf("Failed to form ephemeral ID: %+v", err) - } - br := BroadcastReport{ - RoundsList: makeRoundsList(42), - EphID: eid, - } - - brJson, err := json.Marshal(br) - if err != nil { - t.Errorf("Failed to marshal broadcast report: %+v", err) - } - t.Log(string(brJson)) -} diff --git a/bindings/channels.go b/bindings/channels.go new file mode 100644 index 0000000000000000000000000000000000000000..0a6014332ec077516bc7e57d7ae36a2c58bc2f23 --- /dev/null +++ b/bindings/channels.go @@ -0,0 +1,1603 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "crypto/ed25519" + "encoding/base64" + "encoding/json" + "sync" + "time" + + "github.com/pkg/errors" + "gitlab.com/elixxir/client/channels" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/storage/utility" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/client/xxdk" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/rsa" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "gitlab.com/xx_network/primitives/netTime" +) + +//////////////////////////////////////////////////////////////////////////////// +// Singleton Tracker // +//////////////////////////////////////////////////////////////////////////////// + +// channelManagerTrackerSingleton is used to track ChannelsManager objects +// so that they can be referenced by ID back over the bindings. +var channelManagerTrackerSingleton = &channelManagerTracker{ + tracked: make(map[int]*ChannelsManager), + count: 0, +} + +// channelManagerTracker is a singleton used to keep track of extant +// ChannelsManager objects, preventing race conditions created by passing it +// over the bindings. +type channelManagerTracker struct { + tracked map[int]*ChannelsManager + count int + mux sync.RWMutex +} + +// make create a ChannelsManager from an [channels.Manager], assigns it a unique +// ID, and adds it to the channelManagerTracker. +func (cmt *channelManagerTracker) make(c channels.Manager) *ChannelsManager { + cmt.mux.Lock() + defer cmt.mux.Unlock() + + chID := cmt.count + cmt.count++ + + cmt.tracked[chID] = &ChannelsManager{ + api: c, + id: chID, + } + + return cmt.tracked[chID] +} + +// get an ChannelsManager from the channelManagerTracker given its ID. +func (cmt *channelManagerTracker) get(id int) (*ChannelsManager, error) { + cmt.mux.RLock() + defer cmt.mux.RUnlock() + + c, exist := cmt.tracked[id] + if !exist { + return nil, errors.Errorf( + "Cannot get ChannelsManager for ID %d, does not exist", id) + } + + return c, nil +} + +// delete removes a ChannelsManager from the channelManagerTracker. +func (cmt *channelManagerTracker) delete(id int) { + cmt.mux.Lock() + defer cmt.mux.Unlock() + + delete(cmt.tracked, id) +} + +//////////////////////////////////////////////////////////////////////////////// +// Basic Channel API // +//////////////////////////////////////////////////////////////////////////////// + +// ChannelsManager is a bindings-layer struct that wraps a [channels.Manager] +// interface. +type ChannelsManager struct { + api channels.Manager + id int +} + +// GetID returns the channelManagerTracker ID for the ChannelsManager object. +func (cm *ChannelsManager) GetID() int { + return cm.id +} + +// GenerateChannelIdentity creates a new private channel identity +// ([channel.PrivateIdentity]). The public component can be retrieved as JSON +// via [GetPublicChannelIdentityFromPrivate]. +// +// Parameters: +// - cmixID - The tracked cmix object ID. This can be retrieved using +// [Cmix.GetID]. +// +// Returns: +// - Marshalled bytes of [channel.PrivateIdentity]. +func GenerateChannelIdentity(cmixID int) ([]byte, error) { + // Get user from singleton + user, err := cmixTrackerSingleton.get(cmixID) + if err != nil { + return nil, err + } + + rng := user.api.GetRng().GetStream() + defer rng.Close() + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + return nil, err + } + return pi.Marshal(), nil +} + +// ConstructIdentity constructs a [channel.Identity] from a user's public key +// and codeset version. +// +// Parameters: +// - pubKey - The Ed25519 public key. +// - codesetVersion - The version of the codeset used to generate the identity. +// +// Returns: +// - JSON of [channel.Identity]. +func ConstructIdentity(pubKey []byte, codesetVersion int) ([]byte, error) { + identity, err := cryptoChannel.ConstructIdentity( + pubKey, uint8(codesetVersion)) + if err != nil { + return nil, err + } + return json.Marshal(identity) +} + +// ImportPrivateIdentity generates a new [channel.PrivateIdentity] from exported +// data. +// +// Parameters: +// - password - The password used to encrypt the identity. +// - data - The encrypted data. +// +// Returns: +// - JSON of [channel.PrivateIdentity]. +func ImportPrivateIdentity(password string, data []byte) ([]byte, error) { + pi, err := cryptoChannel.ImportPrivateIdentity(password, data) + if err != nil { + return nil, err + } + return pi.Marshal(), nil +} + +// GetPublicChannelIdentity constructs a public identity ([channel.Identity]) +// from a bytes version and returns it JSON marshaled. +// +// Parameters: +// - marshaledPublic - Bytes of the public identity ([channel.Identity]). +// +// Returns: +// - JSON of the constructed [channel.Identity]. +func GetPublicChannelIdentity(marshaledPublic []byte) ([]byte, error) { + i, err := cryptoChannel.UnmarshalIdentity(marshaledPublic) + if err != nil { + return nil, err + } + return json.Marshal(&i) +} + +// GetPublicChannelIdentityFromPrivate returns the public identity +// ([channel.Identity]) contained in the given private identity +// ([channel.PrivateIdentity]). +// +// Parameters: +// - marshaledPrivate - Bytes of the private identity +// (channel.PrivateIdentity]). +// +// Returns: +// - JSON of the public [channel.Identity]. +func GetPublicChannelIdentityFromPrivate(marshaledPrivate []byte) ([]byte, error) { + pi, err := cryptoChannel.UnmarshalPrivateIdentity(marshaledPrivate) + if err != nil { + return nil, err + } + return json.Marshal(&pi.Identity) +} + +// NewChannelsManagerGoEventModel creates a new [ChannelsManager] from a new +// private identity ([channel.PrivateIdentity]). This is not compatible with +// GoMobile Bindings because it receives the go event model. +// +// This is for creating a manager for an identity for the first time. For +// generating a new one channel identity, use [GenerateChannelIdentity]. To +// reload this channel manager, use [LoadChannelsManagerGoEventModel], passing +// in the storage tag retrieved by [ChannelsManager.GetStorageTag]. +// +// Parameters: +// - cmixID - The tracked Cmix object ID. This can be retrieved using +// [Cmix.GetID]. +// - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity]) +// that is generated by [GenerateChannelIdentity]. +// - goEvent - A function that initialises and returns the event model that is +// not compatible with GoMobile bindings. +func NewChannelsManagerGoEventModel(cmixID int, privateIdentity []byte, + goEventBuilder channels.EventModelBuilder) (*ChannelsManager, error) { + pi, err := cryptoChannel.UnmarshalPrivateIdentity(privateIdentity) + if err != nil { + return nil, err + } + + // Get user from singleton + user, err := cmixTrackerSingleton.get(cmixID) + if err != nil { + return nil, err + } + + // Construct new channels manager + m, err := channels.NewManager(pi, user.api.GetStorage().GetKV(), + user.api.GetCmix(), user.api.GetRng(), goEventBuilder) + if err != nil { + return nil, err + } + + // Add channel to singleton and return + return channelManagerTrackerSingleton.make(m), nil +} + +// LoadChannelsManagerGoEventModel loads an existing ChannelsManager. This is not +// compatible with GoMobile Bindings because it receives the go event model. +// This is for creating a manager for an identity for the first time. +// The channel manager should have first been created with +// NewChannelsManagerGoEventModel and then the storage tag can be retrieved +// with ChannelsManager.GetStorageTag +// +// Parameters: +// - cmixID - The tracked cmix object ID. This can be retrieved using +// [Cmix.GetID]. +// - storageTag - retrieved with ChannelsManager.GetStorageTag +// - goEvent - A function that initialises and returns the event model that is +// not compatible with GoMobile bindings. +func LoadChannelsManagerGoEventModel(cmixID int, storageTag string, + goEventBuilder channels.EventModelBuilder) (*ChannelsManager, error) { + + // Get user from singleton + user, err := cmixTrackerSingleton.get(cmixID) + if err != nil { + return nil, err + } + + // Construct new channels manager + m, err := channels.LoadManager(storageTag, user.api.GetStorage().GetKV(), + user.api.GetCmix(), user.api.GetRng(), goEventBuilder) + if err != nil { + return nil, err + } + + // Add channel to singleton and return + return channelManagerTrackerSingleton.make(m), nil +} + +// NewChannelsManager creates a new [ChannelsManager] from a new private +// identity ([channel.PrivateIdentity]). +// +// This is for creating a manager for an identity for the first time. For +// generating a new one channel identity, use [GenerateChannelIdentity]. To +// reload this channel manager, use [LoadChannelsManager], passing in the +// storage tag retrieved by [ChannelsManager.GetStorageTag]. +// +// Parameters: +// - cmixID - The tracked Cmix object ID. This can be retrieved using +// [Cmix.GetID]. +// - privateIdentity - Bytes of a private identity ([channel.PrivateIdentity]) +// that is generated by [GenerateChannelIdentity]. +// - event - An interface that contains a function that initialises and returns +// the event model that is bindings-compatible. +func NewChannelsManager(cmixID int, privateIdentity []byte, + eventBuilder EventModelBuilder) (*ChannelsManager, error) { + pi, err := cryptoChannel.UnmarshalPrivateIdentity(privateIdentity) + if err != nil { + return nil, err + } + + // Get user from singleton + user, err := cmixTrackerSingleton.get(cmixID) + if err != nil { + return nil, err + } + + eb := func(path string) (channels.EventModel, error) { + return NewEventModel(eventBuilder.Build(path)), nil + } + + // Construct new channels manager + m, err := channels.NewManager(pi, user.api.GetStorage().GetKV(), + user.api.GetCmix(), user.api.GetRng(), eb) + if err != nil { + return nil, err + } + + // Add channel to singleton and return + return channelManagerTrackerSingleton.make(m), nil +} + +// LoadChannelsManager loads an existing [ChannelsManager]. +// +// This is for loading a manager for an identity that has already been created. +// The channel manager should have previously been created with +// [NewChannelsManager] and the storage is retrievable with +// [ChannelsManager.GetStorageTag]. +// +// Parameters: +// - cmixID - The tracked cmix object ID. This can be retrieved using +// [Cmix.GetID]. +// - storageTag - The storage tag associated with the previously created +// channel manager and retrieved with [ChannelsManager.GetStorageTag]. +// - event - An interface that contains a function that initialises and returns +// the event model that is bindings-compatible. +func LoadChannelsManager(cmixID int, storageTag string, + eventBuilder EventModelBuilder) (*ChannelsManager, error) { + + // Get user from singleton + user, err := cmixTrackerSingleton.get(cmixID) + if err != nil { + return nil, err + } + + eb := func(path string) (channels.EventModel, error) { + return NewEventModel(eventBuilder.Build(path)), nil + } + + // Construct new channels manager + m, err := channels.LoadManager(storageTag, user.api.GetStorage().GetKV(), + user.api.GetCmix(), user.api.GetRng(), eb) + if err != nil { + return nil, err + } + + // Add channel to singleton and return + return channelManagerTrackerSingleton.make(m), nil +} + +// ChannelGeneration contains information about a newly generated channel. It +// contains the public channel info formatted in pretty print and the private +// key for the channel in PEM format. +// +// Example JSON: +// { +// "Channel": "\u003cSpeakeasy-v2:Test_Channel|description:Channel description.|level:Public|secrets:8AS3SczFvAYZftWuj4ZkOM9muFPIwq/0HuVCUJgTK8w=|GpPl1510/G07J4RfdYX9J5plTX3WNLVm+uuGmCwgFeU=|5|1|mRfdUGM6WxWjjCuLzO+2+zc3BQh2zMT2CHD8ZnBwpVI=\u003e", +// "PrivateKey": "-----BEGIN RSA PRIVATE KEY-----\nMDECAQACBgDMIU9LpQIDAQABAgYAvCd9ewECAw0tzQIDD305AgMFu4UCAwd+kQID\nAQxc\n-----END RSA PRIVATE KEY-----" +// } +type ChannelGeneration struct { + Channel string + PrivateKey string +} + +// GenerateChannel is used to create a channel a new channel of which you are +// the admin. It is only for making new channels, not joining existing ones. +// +// It returns a pretty print of the channel and the private key. +// +// Parameters: +// - cmixID - The tracked cmix object ID. This can be retrieved using +// [Cmix.GetID]. +// - name - The name of the new channel. The name must be between 3 and 24 +// characters inclusive. It can only include upper and lowercase unicode +// letters, digits 0 through 9, and underscores (_). It cannot be changed +// once a channel is created. +// - description - The description of a channel. The description is optional +// but cannot be longer than 144 characters and can include all unicode +// characters. It cannot be changed once a channel is created. +// - privacyLevel - The broadcast.PrivacyLevel of the channel. 0 = public, +// 1 = private, and 2 = secret. Refer to the comment below for more +// information. +// +// Returns: +// - []byte - [ChannelGeneration] describes a generated channel. It contains +// both the public channel info and the private key for the channel in PEM +// format. +// +// The [broadcast.PrivacyLevel] of a channel indicates the level of channel +// information revealed when sharing it via URL. For any channel besides public +// channels, the secret information is encrypted and a password is required to +// share and join a channel. +// - A privacy level of [broadcast.Public] reveals all the information +// including the name, description, privacy level, public key and salt. +// - A privacy level of [broadcast.Private] reveals only the name and +// description. +// - A privacy level of [broadcast.Secret] reveals nothing. +func GenerateChannel(cmixID int, name, description string, privacyLevel int) ([]byte, error) { + // Get cmix from singleton so its rng can be used + cmix, err := cmixTrackerSingleton.get(cmixID) + if err != nil { + return nil, err + } + + stream := cmix.api.GetRng().GetStream() + defer stream.Close() + level := cryptoBroadcast.PrivacyLevel(privacyLevel) + c, pk, err := cryptoBroadcast.NewChannel(name, description, level, + cmix.api.GetCmix().GetMaxMessageLength(), stream) + if err != nil { + return nil, err + } + + gen := ChannelGeneration{ + Channel: c.PrettyPrint(), + PrivateKey: string(pk.MarshalPem()), + } + + err = saveChannelPrivateKey(cmix, c.ReceptionID, pk) + if err != nil { + return nil, err + } + + return json.Marshal(&gen) +} + +const ( + channelPrivateKeyStoreVersion = 0 + channelPrivateKeyStoreKey = "channelPrivateKey" +) + +func saveChannelPrivateKey(cmix *Cmix, channelID *id.ID, pk rsa.PrivateKey) error { + return cmix.api.GetStorage().Set( + makeChannelPrivateKeyStoreKey(channelID), + &versioned.Object{ + Version: channelPrivateKeyStoreVersion, + Timestamp: netTime.Now(), + Data: pk.MarshalPem(), + }) +} + +// GetSavedChannelPrivateKeyUNSAFE loads the private key from storage for the +// given channel ID. +// +// NOTE: This function is unsafe and only for debugging purposes only. +// +// Parameters: +// - cmixID - ID of [Cmix] object in tracker. +// - channelIdBase64 - The [id.ID] of the channel in base 64 encoding. +// +// Returns: +// - The PEM file of the private key. +func GetSavedChannelPrivateKeyUNSAFE(cmixID int, channelIdBase64 string) (string, error) { + cmix, err := cmixTrackerSingleton.get(cmixID) + if err != nil { + return "", err + } + + channelIdBytes, err := base64.StdEncoding.DecodeString(channelIdBase64) + if err != nil { + return "", errors.Errorf("failed to decode channel ID: %+v", err) + } + + channelID, err := id.Unmarshal(channelIdBytes) + if err != nil { + return "", errors.Errorf("invalid channel ID: %+v", err) + } + + privKey, err := loadChannelPrivateKey(cmix, channelID) + if err != nil { + return "", errors.Errorf( + "failed to load private key from storage: %+v", err) + } + + return string(privKey.MarshalPem()), nil +} + +func loadChannelPrivateKey(cmix *Cmix, channelID *id.ID) (rsa.PrivateKey, error) { + obj, err := cmix.api.GetStorage().Get( + makeChannelPrivateKeyStoreKey(channelID)) + if err != nil { + return nil, err + } + + return rsa.GetScheme().UnmarshalPrivateKeyPEM(obj.Data) +} + +func makeChannelPrivateKeyStoreKey(channelID *id.ID) string { + return channelPrivateKeyStoreKey + "/" + channelID.String() +} + +// DecodePublicURL decodes the channel URL into a channel pretty print. This +// function can only be used for public channel URLs. To get the privacy level +// of a channel URL, use [GetShareUrlType]. +// +// Parameters: +// - url - The channel's share URL. Should be received from another user or +// generated via [GetShareURL]. +// +// Returns: +// - The channel pretty print. +func DecodePublicURL(url string) (string, error) { + c, err := cryptoBroadcast.DecodeShareURL(url, "") + if err != nil { + return "", err + } + + return c.PrettyPrint(), nil +} + +// DecodePrivateURL decodes the channel URL, using the password, into a channel +// pretty print. This function can only be used for private or secret channel +// URLs. To get the privacy level of a channel URL, use [GetShareUrlType]. +// +// Parameters: +// - url - The channel's share URL. Should be received from another user or +// generated via [GetShareURL]. +// - password - The password needed to decrypt the secret data in the URL. +// +// Returns: +// - The channel pretty print. +func DecodePrivateURL(url, password string) (string, error) { + c, err := cryptoBroadcast.DecodeShareURL(url, password) + if err != nil { + return "", err + } + + return c.PrettyPrint(), nil +} + +// GetChannelJSON returns the JSON of the channel for the given pretty print. +// +// Parameters: +// - prettyPrint - The pretty print of the channel. +// +// Returns: +// - JSON of the [broadcast.Channel] object. +// +// Example JSON of [broadcast.Channel]: +// { +// "ReceptionID": "Ja/+Jh+1IXZYUOn+IzE3Fw/VqHOscomD0Q35p4Ai//kD", +// "Name": "My_Channel", +// "Description": "Here is information about my channel.", +// "Salt": "+tlrU/htO6rrV3UFDfpQALUiuelFZ+Cw9eZCwqRHk+g=", +// "RsaPubKeyHash": "PViT1mYkGBj6AYmE803O2RpA7BX24EjgBdldu3pIm4o=", +// "RsaPubKeyLength": 5, +// "RSASubPayloads": 1, +// "Secret": "JxZt/wPx2luoPdHY6jwbXqNlKnixVU/oa9DgypZOuyI=", +// "Level": 0 +// } +func GetChannelJSON(prettyPrint string) ([]byte, error) { + c, err := cryptoBroadcast.NewChannelFromPrettyPrint(prettyPrint) + if err != nil { + return nil, nil + } + + return json.Marshal(c) +} + +// ChannelInfo contains information about a channel. +// +// Example of ChannelInfo JSON: +// { +// "Name": "Test Channel", +// "Description": "This is a test channel", +// "ChannelID": "RRnpRhmvXtW9ugS1nILJ3WfttdctDvC2jeuH43E0g/0D", +// } +type ChannelInfo struct { + Name string + Description string + ChannelID string +} + +// GetChannelInfo returns the info about a channel from its public description. +// +// Parameters: +// - prettyPrint - The pretty print of the channel. +// +// The pretty print will be of the format: +// <Speakeasy-v2:Test_Channel|description:Channel description.|level:Public|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=> +// +// Returns: +// - []byte - JSON of [ChannelInfo], which describes all relevant channel info. +func GetChannelInfo(prettyPrint string) ([]byte, error) { + _, bytes, err := getChannelInfo(prettyPrint) + return bytes, err +} + +func getChannelInfo(prettyPrint string) (*cryptoBroadcast.Channel, []byte, error) { + c, err := cryptoBroadcast.NewChannelFromPrettyPrint(prettyPrint) + if err != nil { + return nil, nil, err + } + ci := &ChannelInfo{ + Name: c.Name, + Description: c.Description, + ChannelID: c.ReceptionID.String(), + } + bytes, err := json.Marshal(ci) + if err != nil { + return nil, nil, err + } + return c, bytes, nil +} + +// JoinChannel joins the given channel. It will fail if the channel has already +// been joined. +// +// Parameters: +// - channelPretty - A portable channel string. Should be received from +// another user or generated via GenerateChannel. +// +// The pretty print will be of the format: +// <Speakeasy-v2:Test_Channel|description:Channel description.|level:Public|secrets:+oHcqDbJPZaT3xD5NcdLY8OjOMtSQNKdKgLPmr7ugdU=|rCI0wr01dHFStjSFMvsBzFZClvDIrHLL5xbCOPaUOJ0=|493|1|7cBhJxVfQxWo+DypOISRpeWdQBhuQpAZtUbQHjBm8NQ=> +// +// Returns: +// - []byte - JSON of [ChannelInfo], which describes all relevant channel info. +func (cm *ChannelsManager) JoinChannel(channelPretty string) ([]byte, error) { + c, info, err := getChannelInfo(channelPretty) + if err != nil { + return nil, err + } + + // Join the channel using the API + err = cm.api.JoinChannel(c) + + return info, err +} + +// GetChannels returns the IDs of all channels that have been joined. +// +// Returns: +// - []byte - A JSON marshalled list of IDs. +// +// JSON Example: +// { +// "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVID", +// "15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWAgD" +// } +func (cm *ChannelsManager) GetChannels() ([]byte, error) { + channelIds := cm.api.GetChannels() + return json.Marshal(channelIds) +} + +// LeaveChannel leaves the given channel. It will return an error if the +// channel was not previously joined. +// +// Parameters: +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). +func (cm *ChannelsManager) LeaveChannel(marshalledChanId []byte) error { + // Unmarshal channel ID + channelId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return err + } + + // Leave the channel + return cm.api.LeaveChannel(channelId) +} + +// ReplayChannel replays all messages from the channel within the network's +// memory (~3 weeks) over the event model. +// +// Parameters: +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). +func (cm *ChannelsManager) ReplayChannel(marshalledChanId []byte) error { + + // Unmarshal channel ID + chanId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return err + } + + // Replay channel + return cm.api.ReplayChannel(chanId) +} + +//////////////////////////////////////////////////////////////////////////////// +// Channel Share URL // +//////////////////////////////////////////////////////////////////////////////// + +// ShareURL is returned from ChannelsManager.GetShareURL. It includes the +// channel's share URL and password, if it needs one. +// +// JSON example for a public channel: +// { +// "url": "https://internet.speakeasy.tech/?0Name=My_Channel&Description=Here+is+information+about+my+channel.&2Level=Public&e=3CCvzK8diF%2B6vUZetyZkcyemoiI8uFLGSh%2B%2F9%2Bh5YQE%3D&k=zBakjn1Snay7AMr2CZ%2BCoWCHbe9TrtQqAVftIDi9Fjs%3D&l=5&m=0&p=1&s=Seyvx%2F5%2FOVTj5LClUG42AuLamDnqrbtMOoLymyIpFqY%3D&v=0", +// "password": "" +// } +// +// JSON example for a private channel: +// { +// "url": "https://internet.speakeasy.tech/?0Name=My_Channel&1Description=Here+is+information+about+my+channel.&d=i%2FwBAK6i89YT3LjPYb5%2BMmog5Gjk2unYzYt25y%2BmZH3%2Bo08oUIHHEoC7JYjk50Q2%2BMcSj6fQh%2BW3LBvWv02f1g60PLXZ1H8OS2rqoxBhwHvTpNgXRdUIErbk6q3ljIdjtSqJtWIzAx5no%2F96jaIBsob0U9jDE1jgsU8XNGxDz3TeKcdTOFiUpnh4R%2BALcys%3D&m=1&v=0", +// "password": "easter boaster musket catalyze unproven vendetta plated grinning" +// } +// +// JSON example for a secret channel: +// { +// "url": "https://internet.speakeasy.tech/?d=xRORDN8lt%2BI2SAn%2F21ZpOzj50J3HOV1GkMsPkhtgoYyQUqpPBZhhKpewzuDI%2B3wTQlpANLDMtFVL4J7y2lBpvIz9LQ5%2F6CoRdVkoXbG7uRqv6wscYdwWPYZBARC2cJSyeVad6RbxnoZ65Z0dtEVEff328ri3ZpaMBlP%2BpUH928pcVHibALW7Bw04Rkmh%2FWx6wJGw%2FU0gTHo02UlYFHh4G9CC%2BIU1x13BmEuW6Hyk6Ty9BlHt29QbsQ7uU30RwzQOyg8%3D&m=2&v=0", +// "password": "florist angled valid snarl discharge endearing harbor hazy" +// } +type ShareURL struct { + URL string `json:"url"` + Password string `json:"password"` +} + +// GetShareURL generates a URL that can be used to share this channel with +// others on the given host. +// +// A URL comes in one of three forms based on the privacy level set when +// generating the channel. Each privacy level hides more information than the +// last with the lowest level revealing everything and the highest level +// revealing nothing. For any level above the lowest, a password is returned, +// which will be required when decoding the URL. +// +// The maxUses is the maximum number of times this URL can be used to join a +// channel. If it is set to 0, then it can be shared unlimited times. The max +// uses is set as a URL parameter using the key [broadcast.MaxUsesKey]. Note +// that this number is also encoded in the secret data for private and secret +// URLs, so if the number is changed in the URL, is will be verified when +// calling [ChannelsManager.JoinChannelFromURL]. There is no enforcement for +// public URLs. +// +// Parameters: +// - cmixID - The tracked Cmix object ID. +// - host - The URL to append the channel info to. +// - maxUses - The maximum number of uses the link can be used (0 for +// unlimited). +// - marshalledChanId - A marshalled channel ID ([id.ID]). +// +// Returns: +// - JSON of ShareURL. +func (cm *ChannelsManager) GetShareURL(cmixID int, host string, maxUses int, + marshalledChanId []byte) ([]byte, error) { + + // Unmarshal channel ID + chanId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return nil, err + } + + // Get the channel from the ID + ch, err := cm.api.GetChannel(chanId) + if err != nil { + return nil, err + } + + // Get user from singleton + user, err := cmixTrackerSingleton.get(cmixID) + if err != nil { + return nil, err + } + + // Generate share URL and password + rng := user.api.GetRng().GetStream() + url, password, err := ch.ShareURL(host, maxUses, rng) + rng.Close() + if err != nil { + return nil, err + } + + su := ShareURL{ + URL: url, + Password: password, + } + + return json.Marshal(su) +} + +// GetShareUrlType determines the [broadcast.PrivacyLevel] of the channel URL. +// If the URL is an invalid channel URL, an error is returned. +// +// Parameters: +// - url - The channel share URL. +// +// Returns: +// - An int that corresponds to the [broadcast.PrivacyLevel] as outlined below. +// +// Possible returns: +// 0 = public channel +// 1 = private channel +// 2 = secret channel +func GetShareUrlType(url string) (int, error) { + level, err := cryptoBroadcast.GetShareUrlType(url) + return int(level), err +} + +//////////////////////////////////////////////////////////////////////////////// +// Channel Sending Methods & Reports // +//////////////////////////////////////////////////////////////////////////////// + +// ChannelSendReport is the bindings' representation of the return values of +// ChannelsManager's Send operations. +// +// JSON Example: +// { +// "MessageId": "0kitNxoFdsF4q1VMSI/xPzfCnGB2l+ln2+7CTHjHbJw=", +// "Rounds":[1,5,9], +// "EphId": 0 +// } +type ChannelSendReport struct { + MessageId []byte + RoundsList + EphId int64 +} + +// SendGeneric is used to send a raw message over a channel. In general, it +// should be wrapped in a function that defines the wire protocol. If the final +// message, before being sent over the wire, is too long, this will return an +// error. Due to the underlying encoding using compression, it isn't possible to +// define the largest payload that can be sent, but it will always be possible +// to send a payload of 802 bytes at minimum. The meaning of validUntil depends +// on the use case. +// +// Parameters: +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). +// - messageType - The message type of the message. This will be a valid +// [channels.MessageType]. +// - message - The contents of the message. This need not be of data type +// string, as the message could be a specified format that the channel may +// recognize. +// - leaseTimeMS - The lease of the message. This will be how long the message +// is valid until, in milliseconds. As per the channels.Manager +// documentation, this has different meanings depending on the use case. +// These use cases may be generic enough that they will not be enumerated +// here. +// - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty, +// and GetDefaultCMixParams will be used internally. +// +// Returns: +// - []byte - A JSON marshalled ChannelSendReport. +func (cm *ChannelsManager) SendGeneric(marshalledChanId []byte, + messageType int, message []byte, leaseTimeMS int64, + cmixParamsJSON []byte) ([]byte, error) { + + // Unmarshal channel ID and parameters + chanId, params, err := parseChannelsParameters( + marshalledChanId, cmixParamsJSON) + if err != nil { + return nil, err + } + + msgTy := channels.MessageType(messageType) + + // Send message + chanMsgId, rnd, ephId, err := cm.api.SendGeneric(chanId, + msgTy, message, time.Duration(leaseTimeMS), + params.CMIX) + if err != nil { + return nil, err + } + + // Construct send report + return constructChannelSendReport(chanMsgId, rnd.ID, ephId) +} + +// SendAdminGeneric is used to send a raw message over a channel encrypted with +// admin keys, identifying it as sent by the admin. In general, it should be +// wrapped in a function that defines the wire protocol. If the final message, +// before being sent over the wire, is too long, this will return an error. The +// message must be at most 510 bytes long. +// +// Parameters: +// - adminPrivateKey - The PEM-encoded admin RSA private key. +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). +// - messageType - The message type of the message. This will be a valid +// [channels.MessageType]. +// - message - The contents of the message. The message should be at most 510 +// bytes. This need not be of data type string, as the message could be a +// specified format that the channel may recognize. +// - leaseTimeMS - The lease of the message. This will be how long the message +// is valid until, in milliseconds. As per the channels.Manager +// documentation, this has different meanings depending on the use case. +// These use cases may be generic enough that they will not be enumerated +// here. +// - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty, +// and GetDefaultCMixParams will be used internally. +// +// Returns: +// - []byte - A JSON marshalled ChannelSendReport. +func (cm *ChannelsManager) SendAdminGeneric(adminPrivateKey, + marshalledChanId []byte, + messageType int, message []byte, leaseTimeMS int64, + cmixParamsJSON []byte) ([]byte, error) { + + // Load private key from file + rsaPrivKey, err := rsa.GetScheme().UnmarshalPrivateKeyPEM(adminPrivateKey) + if err != nil { + return nil, err + } + + // Unmarshal channel ID and parameters + chanId, params, err := parseChannelsParameters( + marshalledChanId, cmixParamsJSON) + if err != nil { + return nil, err + } + + msgTy := channels.MessageType(messageType) + + // Send admin message + chanMsgId, rnd, ephId, err := cm.api.SendAdminGeneric(rsaPrivKey, + chanId, msgTy, message, time.Duration(leaseTimeMS), + params.CMIX) + + // Construct send report + return constructChannelSendReport(chanMsgId, rnd.ID, ephId) +} + +// SendMessage is used to send a formatted message over a channel. +// Due to the underlying encoding using compression, it isn't possible to define +// the largest payload that can be sent, but it will always be possible to send +// a payload of 798 bytes at minimum. +// +// The message will auto delete validUntil after the round it is sent in, +// lasting forever if [channels.ValidForever] is used. +// +// Parameters: +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). +// - message - The contents of the message. The message should be at most 510 +// bytes. This is expected to be Unicode, and thus a string data type is +// expected +// - leaseTimeMS - The lease of the message. This will be how long the message +// is valid until, in milliseconds. As per the channels.Manager +// documentation, this has different meanings depending on the use case. +// These use cases may be generic enough that they will not be enumerated +// here. +// - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be +// empty, and GetDefaultCMixParams will be used internally. +// +// Returns: +// - []byte - A JSON marshalled ChannelSendReport +func (cm *ChannelsManager) SendMessage(marshalledChanId []byte, + message string, leaseTimeMS int64, cmixParamsJSON []byte) ([]byte, error) { + + // Unmarshal channel ID and parameters + chanId, params, err := parseChannelsParameters( + marshalledChanId, cmixParamsJSON) + if err != nil { + return nil, err + } + + // Send message + chanMsgId, rnd, ephId, err := cm.api.SendMessage(chanId, message, + time.Duration(leaseTimeMS), params.CMIX) + if err != nil { + return nil, err + } + + // Construct send report + return constructChannelSendReport(chanMsgId, rnd.ID, ephId) +} + +// SendReply is used to send a formatted message over a channel. Due to the +// underlying encoding using compression, it isn't possible to define the +// largest payload that can be sent, but it will always be possible to send a +// payload of 766 bytes at minimum. +// +// If the message ID the reply is sent to is nonexistent, the other side will +// post the message as a normal message and not a reply. The message will auto +// delete validUntil after the round it is sent in, lasting forever if +// [channels.ValidForever] is used. +// +// Parameters: +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). +// - message - The contents of the message. The message should be at most 510 +// bytes. This is expected to be Unicode, and thus a string data type is +// expected. +// - messageToReactTo - The marshalled [channel.MessageID] of the message you +// wish to reply to. This may be found in the ChannelSendReport if replying +// to your own. Alternatively, if reacting to another user's message, you may +// retrieve it via the ChannelMessageReceptionCallback registered using +// RegisterReceiveHandler. +// - leaseTimeMS - The lease of the message. This will be how long the message +// is valid until, in milliseconds. As per the channels.Manager +// documentation, this has different meanings depending on the use case. +// These use cases may be generic enough that they will not be enumerated +// here. +// - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty, +// and GetDefaultCMixParams will be used internally. +// +// Returns: +// - []byte - A JSON marshalled ChannelSendReport +func (cm *ChannelsManager) SendReply(marshalledChanId []byte, + message string, messageToReactTo []byte, leaseTimeMS int64, + cmixParamsJSON []byte) ([]byte, error) { + + // Unmarshal channel ID and parameters + chanId, params, err := parseChannelsParameters( + marshalledChanId, cmixParamsJSON) + if err != nil { + return nil, err + } + + // Unmarshal message ID + msgId := cryptoChannel.MessageID{} + copy(msgId[:], messageToReactTo) + + // Send Reply + chanMsgId, rnd, ephId, err := cm.api.SendReply(chanId, message, + msgId, time.Duration(leaseTimeMS), params.CMIX) + if err != nil { + return nil, err + } + + // Construct send report + return constructChannelSendReport(chanMsgId, rnd.ID, ephId) +} + +// SendReaction is used to send a reaction to a message over a channel. +// The reaction must be a single emoji with no other characters, and will +// be rejected otherwise. +// Users will drop the reaction if they do not recognize the reactTo message. +// +// Parameters: +// - marshalledChanId - A JSON marshalled channel ID ([id.ID]). +// - reaction - The user's reaction. This should be a single emoji with no +// other characters. As such, a Unicode string is expected. +// - messageToReactTo - The marshalled [channel.MessageID] of the message you +// wish to reply to. This may be found in the ChannelSendReport if replying +// to your own. Alternatively, if reacting to another user's message, you may +// retrieve it via the ChannelMessageReceptionCallback registered using +// RegisterReceiveHandler. +// - cmixParamsJSON - A JSON marshalled [xxdk.CMIXParams]. This may be empty, +// and GetDefaultCMixParams will be used internally. +// +// Returns: +// - []byte - A JSON marshalled ChannelSendReport. +func (cm *ChannelsManager) SendReaction(marshalledChanId []byte, + reaction string, messageToReactTo []byte, + cmixParamsJSON []byte) ([]byte, error) { + + // Unmarshal channel ID and parameters + chanId, params, err := parseChannelsParameters( + marshalledChanId, cmixParamsJSON) + if err != nil { + return nil, err + } + + // Unmarshal message ID + msgId := cryptoChannel.MessageID{} + copy(msgId[:], messageToReactTo) + + // Send reaction + chanMsgId, rnd, ephId, err := cm.api.SendReaction(chanId, + reaction, msgId, params.CMIX) + if err != nil { + return nil, err + } + + // Construct send report + return constructChannelSendReport(chanMsgId, rnd.ID, ephId) +} + +// GetIdentity returns the marshaled public identity ([channel.Identity]) that +// the channel is using. +func (cm *ChannelsManager) GetIdentity() ([]byte, error) { + i := cm.api.GetIdentity() + return json.Marshal(&i) +} + +// ExportPrivateIdentity encrypts and exports the private identity to a portable +// string. +func (cm *ChannelsManager) ExportPrivateIdentity(password string) ([]byte, error) { + return cm.api.ExportPrivateIdentity(password) +} + +// GetStorageTag returns the storage tag needed to reload the manager. +func (cm *ChannelsManager) GetStorageTag() string { + return cm.api.GetStorageTag() +} + +// SetNickname sets the nickname for a given channel. The nickname must be valid +// according to [IsNicknameValid]. +func (cm *ChannelsManager) SetNickname(newNick string, ch []byte) error { + chid, err := id.Unmarshal(ch) + if err != nil { + return err + } + return cm.api.SetNickname(newNick, chid) +} + +// DeleteNickname deletes the nickname for a given channel. +func (cm *ChannelsManager) DeleteNickname(ch []byte) error { + chid, err := id.Unmarshal(ch) + if err != nil { + return err + } + return cm.api.DeleteNickname(chid) +} + +// GetNickname returns the nickname set for a given channel. Returns an error if +// there is no nickname set. +func (cm *ChannelsManager) GetNickname(ch []byte) (string, error) { + chid, err := id.Unmarshal(ch) + if err != nil { + return "", err + } + nick, exists := cm.api.GetNickname(chid) + if !exists { + return "", errors.New("no nickname found for the given channel") + } + + return nick, nil +} + +// IsNicknameValid checks if a nickname is valid. +// +// Rules: +// 1. A nickname must not be longer than 24 characters. +// 2. A nickname must not be shorter than 1 character. +func IsNicknameValid(nick string) error { + return channels.IsNicknameValid(nick) +} + +// parseChannelsParameters is a helper function for the Send functions. It +// parses the channel ID and the passed in parameters into their respective +// objects. These objects are passed into the API via the internal send +// functions. +func parseChannelsParameters(marshalledChanId, cmixParamsJSON []byte) ( + *id.ID, xxdk.CMIXParams, error) { + // Unmarshal channel ID + chanId, err := id.Unmarshal(marshalledChanId) + if err != nil { + return nil, xxdk.CMIXParams{}, err + } + + // Unmarshal cmix params + params, err := parseCMixParams(cmixParamsJSON) + if err != nil { + return nil, xxdk.CMIXParams{}, err + } + + return chanId, params, nil +} + +// constructChannelSendReport is a helper function which returns a JSON +// marshalled ChannelSendReport. +func constructChannelSendReport(channelMessageId cryptoChannel.MessageID, + roundId id.Round, ephId ephemeral.Id) ([]byte, error) { + // Construct send report + chanSendReport := ChannelSendReport{ + MessageId: channelMessageId.Bytes(), + RoundsList: makeRoundsList(roundId), + EphId: ephId.Int64(), + } + + // Marshal send report + return json.Marshal(chanSendReport) +} + +//////////////////////////////////////////////////////////////////////////////// +// Channel Receiving Logic and Callback Registration // +//////////////////////////////////////////////////////////////////////////////// + +// ReceivedChannelMessageReport is a report structure returned via the +// ChannelMessageReceptionCallback. This report gives the context for the +// channel the message was sent to and the message itself. This is returned via +// the callback as JSON marshalled bytes. +// +// JSON Example: +// { +// "ChannelId": "AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", +// "MessageId": "3S6DiVjWH9mLmjy1oaam/3x45bJQzOW6u2KgeUn59wA=", +// "ReplyTo":"cxMyGUFJ+Ff1Xp2X+XkIpOnNAQEZmv8SNP5eYH4tCik=", +// "MessageType": 42, +// "SenderUsername": "hunter2", +// "Content": "YmFuX2JhZFVTZXI=", +// "Timestamp": 1662502150335283000, +// "Lease": 25, +// "Rounds": [ 1, 4, 9], +// } +type ReceivedChannelMessageReport struct { + ChannelId []byte + MessageId []byte + MessageType int + Nickname string + PubKey []byte + Codeset int + Content []byte + Timestamp int64 + Lease int64 + RoundsList +} + +// ChannelMessageReceptionCallback is the callback that returns the context for +// a channel message via the Callback. +// It must return a unique UUID for the message by which it can be referenced +// later +type ChannelMessageReceptionCallback interface { + Callback(receivedChannelMessageReport []byte, err error) int +} + +// RegisterReceiveHandler is used to register handlers for non-default message +// types. They can be processed by modules. It is important that such modules +// sync up with the event model implementation. +// +// There can only be one handler per [channels.MessageType], and this will +// return an error on any re-registration. +// +// Parameters: +// - messageType - represents the [channels.MessageType] which will have a +// registered listener. +// - listenerCb - the callback which will be executed when a channel message +// of messageType is received. +func (cm *ChannelsManager) RegisterReceiveHandler(messageType int, + listenerCb ChannelMessageReceptionCallback) error { + + // Wrap callback around backend interface + cb := channels.MessageTypeReceiveMessage( + func(channelID *id.ID, + messageID cryptoChannel.MessageID, messageType channels.MessageType, + nickname string, content []byte, pubKey ed25519.PublicKey, + codeset uint8, timestamp time.Time, lease time.Duration, + round rounds.Round, status channels.SentStatus) uint64 { + + rcm := ReceivedChannelMessageReport{ + ChannelId: channelID.Marshal(), + MessageId: messageID.Bytes(), + MessageType: int(messageType), + Nickname: nickname, + PubKey: pubKey, + Codeset: int(codeset), + Content: content, + Timestamp: timestamp.UnixNano(), + Lease: int64(lease), + RoundsList: makeRoundsList(round.ID), + } + + return uint64(listenerCb.Callback(json.Marshal(rcm))) + }) + + // Register handler + return cm.api.RegisterReceiveHandler(channels.MessageType(messageType), cb) +} + +//////////////////////////////////////////////////////////////////////////////// +// Event Model Logic // +//////////////////////////////////////////////////////////////////////////////// + +// EventModelBuilder builds an event model +type EventModelBuilder interface { + Build(path string) EventModel +} + +// EventModel is an interface which an external party which uses the channels +// system passed an object which adheres to in order to get events on the +// channel. +type EventModel interface { + // JoinChannel is called whenever a channel is joined locally. + // + // Parameters: + // - channel - Returns the pretty print representation of a channel. + JoinChannel(channel string) + + // LeaveChannel is called whenever a channel is left locally. + // + // Parameters: + // - ChannelId - The marshalled channel [id.ID]. + LeaveChannel(channelID []byte) + + // ReceiveMessage is called whenever a message is received on a given + // channel. It may be called multiple times on the same message. It is + // incumbent on the user of the API to filter such called by message ID. + // + // Parameters: + // - channelID - The marshalled channel [id.ID]. + // - messageID - The bytes of the [channel.MessageID] of the received + // message. + // - nickname - The nickname of the sender of the message. + // - text - The content of the message. + // - timestamp - Time the message was received; represented as nanoseconds + // since unix epoch. + // - pubKey - The sender's Ed25519 public key. + // - codeset - The codeset version. + // - lease - The number of nanoseconds that the message is valid for. + // - roundId - The ID of the round that the message was received on. + // - mType - the type of the message, always 1 for this call + // - status - the [channels.SentStatus] of the message. + // + // Statuses will be enumerated as such: + // Sent = 0 + // Delivered = 1 + // Failed = 2 + // + // Returns a non-negative unique UUID for the message that it can be + // referenced by later with [EventModel.UpdateSentStatus]. + ReceiveMessage(channelID, messageID []byte, nickname, text string, + pubKey []byte, codeset int, timestamp, lease, roundId, mType, + status int64) int64 + + // ReceiveReply is called whenever a message is received that is a reply on + // a given channel. It may be called multiple times on the same message. It + // is incumbent on the user of the API to filter such called by message ID. + // + // Messages may arrive our of order, so a reply in theory can arrive before + // the initial message. As a result, it may be important to buffer replies. + // + // Parameters: + // - channelID - The marshalled channel [id.ID]. + // - messageID - The bytes of the [channel.MessageID] of the received + // message. + // - reactionTo - The [channel.MessageID] for the message that received a + // reply. + // - nickname - The nickname of the sender of the message. + // - text - The content of the message. + // - pubKey - The sender's Ed25519 public key. + // - codeset - The codeset version. + // - timestamp - Time the message was received; represented as nanoseconds + // since unix epoch. + // - lease - The number of nanoseconds that the message is valid for. + // - roundId - The ID of the round that the message was received on. + // - mType - the type of the message, always 1 for this call + // - status - the [channels.SentStatus] of the message. + // + // Statuses will be enumerated as such: + // Sent = 0 + // Delivered = 1 + // Failed = 2 + // + // Returns a non-negative unique UUID for the message that it can be + // referenced by later with [EventModel.UpdateSentStatus]. + ReceiveReply(channelID, messageID, reactionTo []byte, nickname, text string, + pubKey []byte, codeset int, timestamp, lease, roundId, mType, + status int64) int64 + + // ReceiveReaction is called whenever a reaction to a message is received + // on a given channel. It may be called multiple times on the same reaction. + // It is incumbent on the user of the API to filter such called by message + // ID. + // + // Messages may arrive our of order, so a reply in theory can arrive before + // the initial message. As a result, it may be important to buffer + // reactions. + // + // Parameters: + // - channelID - The marshalled channel [id.ID]. + // - messageID - The bytes of the [channel.MessageID] of the received + // message. + // - reactionTo - The [channel.MessageID] for the message that received a + // reply. + // - nickname - The nickname of the sender of the message. + // - reaction - The contents of the reaction message. + // - pubKey - The sender's Ed25519 public key. + // - codeset - The codeset version. + // - timestamp - Time the message was received; represented as nanoseconds + // since unix epoch. + // - lease - The number of nanoseconds that the message is valid for. + // - roundId - The ID of the round that the message was received on. + // - mType - the type of the message, always 1 for this call + // - status - the [channels.SentStatus] of the message. + // + // Statuses will be enumerated as such: + // Sent = 0 + // Delivered = 1 + // Failed = 2 + // + // Returns a non-negative unique uuid for the message by which it can be + // referenced later with UpdateSentStatus + ReceiveReaction(channelID, messageID, reactionTo []byte, nickname, + reaction string, pubKey []byte, codeset int, timestamp, lease, roundId, + mType, status int64) int64 + + // UpdateSentStatus is called whenever the sent status of a message has + // changed. + // + // Parameters: + // - messageID - The bytes of the [channel.MessageID] of the received + // message. + // - status - the [channels.SentStatus] of the message. + // + // Statuses will be enumerated as such: + // Sent = 0 + // Delivered = 1 + // Failed = 2 + UpdateSentStatus( + uuid int64, messageID []byte, timestamp, roundID, status int64) + + // unimplemented + // IgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) + // UnIgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) + // PinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID, end time.Time) + // UnPinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) +} + +// toEventModel is a wrapper which wraps an existing channels.EventModel object. +type toEventModel struct { + em EventModel +} + +// NewEventModel is a constructor for a toEventModel. This will take in an +// EventModel and wraps it around the toEventModel. +func NewEventModel(em EventModel) channels.EventModel { + return &toEventModel{em: em} +} + +// JoinChannel is called whenever a channel is joined locally. +func (tem *toEventModel) JoinChannel(channel *cryptoBroadcast.Channel) { + tem.em.JoinChannel(channel.PrettyPrint()) +} + +// LeaveChannel is called whenever a channel is left locally. +func (tem *toEventModel) LeaveChannel(channelID *id.ID) { + tem.em.LeaveChannel(channelID[:]) +} + +// ReceiveMessage is called whenever a message is received on a given channel. +// It may be called multiple times on the same message. It is incumbent on the +// user of the API to filter such called by message ID. +func (tem *toEventModel) ReceiveMessage(channelID *id.ID, + messageID cryptoChannel.MessageID, nickname, text string, + pubKey ed25519.PublicKey, codeset uint8, timestamp time.Time, + lease time.Duration, round rounds.Round, mType channels.MessageType, + status channels.SentStatus) uint64 { + + return uint64(tem.em.ReceiveMessage(channelID[:], messageID[:], nickname, + text, pubKey, int(codeset), timestamp.UnixNano(), int64(lease), + int64(round.ID), int64(mType), int64(status))) +} + +// ReceiveReply is called whenever a message is received that is a reply on a +// given channel. It may be called multiple times on the same message. It is +// incumbent on the user of the API to filter such called by message ID. +// +// Messages may arrive our of order, so a reply in theory can arrive before the +// initial message. As a result, it may be important to buffer replies. +func (tem *toEventModel) ReceiveReply(channelID *id.ID, + messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID, + nickname, text string, pubKey ed25519.PublicKey, codeset uint8, + timestamp time.Time, lease time.Duration, round rounds.Round, + mType channels.MessageType, status channels.SentStatus) uint64 { + + return uint64(tem.em.ReceiveReply(channelID[:], messageID[:], reactionTo[:], + nickname, text, pubKey, int(codeset), timestamp.UnixNano(), + int64(lease), int64(round.ID), int64(mType), int64(status))) + +} + +// ReceiveReaction is called whenever a reaction to a message is received on a +// given channel. It may be called multiple times on the same reaction. It is +// incumbent on the user of the API to filter such called by message ID. +// +// Messages may arrive our of order, so a reply in theory can arrive before the +// initial message. As a result, it may be important to buffer reactions. +func (tem *toEventModel) ReceiveReaction(channelID *id.ID, messageID cryptoChannel.MessageID, + reactionTo cryptoChannel.MessageID, nickname, reaction string, + pubKey ed25519.PublicKey, codeset uint8, timestamp time.Time, + lease time.Duration, round rounds.Round, mType channels.MessageType, + status channels.SentStatus) uint64 { + + return uint64(tem.em.ReceiveReaction(channelID[:], messageID[:], + reactionTo[:], nickname, reaction, pubKey, int(codeset), + timestamp.UnixNano(), int64(lease), int64(round.ID), int64(mType), + int64(status))) +} + +// UpdateSentStatus is called whenever the sent status of a message has changed. +func (tem *toEventModel) UpdateSentStatus(uuid uint64, + messageID cryptoChannel.MessageID, timestamp time.Time, round rounds.Round, + status channels.SentStatus) { + tem.em.UpdateSentStatus(int64(uuid), messageID[:], timestamp.UnixNano(), + int64(round.ID), int64(status)) +} + +//////////////////////////////////////////////////////////////////////////////// +// Channel ChannelDbCipher // +//////////////////////////////////////////////////////////////////////////////// + +// ChannelDbCipher is the bindings layer representation of the [channel.Cipher]. +type ChannelDbCipher struct { + api cryptoChannel.Cipher + salt []byte + id int +} + +// channelDbCipherTrackerSingleton is used to track ChannelDbCipher objects +// so that they can be referenced by ID back over the bindings. +var channelDbCipherTrackerSingleton = &channelDbCipherTracker{ + tracked: make(map[int]*ChannelDbCipher), + count: 0, +} + +// channelDbCipherTracker is a singleton used to keep track of extant +// ChannelDbCipher objects, preventing race conditions created by passing it +// over the bindings. +type channelDbCipherTracker struct { + tracked map[int]*ChannelDbCipher + count int + mux sync.RWMutex +} + +// create creates a ChannelDbCipher from a [channel.Cipher], assigns it a unique +// ID, and adds it to the channelDbCipherTracker. +func (ct *channelDbCipherTracker) create(c cryptoChannel.Cipher) *ChannelDbCipher { + ct.mux.Lock() + defer ct.mux.Unlock() + + chID := ct.count + ct.count++ + + ct.tracked[chID] = &ChannelDbCipher{ + api: c, + id: chID, + } + + return ct.tracked[chID] +} + +// get an ChannelDbCipher from the channelDbCipherTracker given its ID. +func (ct *channelDbCipherTracker) get(id int) (*ChannelDbCipher, error) { + ct.mux.RLock() + defer ct.mux.RUnlock() + + c, exist := ct.tracked[id] + if !exist { + return nil, errors.Errorf( + "Cannot get ChannelDbCipher for ID %d, does not exist", id) + } + + return c, nil +} + +// delete removes a ChannelDbCipher from the channelDbCipherTracker. +func (ct *channelDbCipherTracker) delete(id int) { + ct.mux.Lock() + defer ct.mux.Unlock() + + delete(ct.tracked, id) +} + +// GetChannelDbCipherTrackerFromID returns the ChannelDbCipher with the +// corresponding ID in the tracker. +func GetChannelDbCipherTrackerFromID(id int) (*ChannelDbCipher, error) { + return channelDbCipherTrackerSingleton.get(id) +} + +// NewChannelsDatabaseCipher constructs a ChannelDbCipher object. +// +// Parameters: +// - cmixID - The tracked [Cmix] object ID. +// - password - The password for storage. This should be the same password +// passed into [NewCmix]. +// - plaintTextBlockSize - The maximum size of a payload to be encrypted. +// A payload passed into [ChannelDbCipher.Encrypt] that is larger than +// plaintTextBlockSize will result in an error. +func NewChannelsDatabaseCipher(cmixID int, password []byte, + plaintTextBlockSize int) (*ChannelDbCipher, error) { + // Get user from singleton + user, err := cmixTrackerSingleton.get(cmixID) + if err != nil { + return nil, err + } + + // Generate RNG + stream := user.api.GetRng().GetStream() + + // Load or generate a salt + salt, err := utility.NewOrLoadSalt( + user.api.GetStorage().GetKV(), stream) + if err != nil { + return nil, err + } + + // Construct a cipher + c, err := cryptoChannel.NewCipher(password, salt, + plaintTextBlockSize, stream) + if err != nil { + return nil, err + } + + // Return a cipher + return channelDbCipherTrackerSingleton.create(c), nil +} + +// GetID returns the ID for this ChannelDbCipher in the channelDbCipherTracker. +func (c *ChannelDbCipher) GetID() int { + return c.id +} + +// Encrypt will encrypt the raw data. It will return a ciphertext. Padding is +// done on the plaintext so all encrypted data looks uniform at rest. +// +// Parameters: +// - plaintext - The data to be encrypted. This must be smaller than the block +// size passed into [NewChannelsDatabaseCipher]. If it is larger, this will +// return an error. +func (c *ChannelDbCipher) Encrypt(plaintext []byte) ([]byte, error) { + return c.api.Encrypt(plaintext) +} + +// Decrypt will decrypt the passed in encrypted value. The plaintext will +// be returned by this function. Any padding will be discarded within +// this function. +// +// Parameters: +// - ciphertext - the encrypted data returned by [ChannelDbCipher.Encrypt]. +func (c *ChannelDbCipher) Decrypt(ciphertext []byte) ([]byte, error) { + return c.api.Decrypt(ciphertext) +} diff --git a/bindings/cmix.go b/bindings/cmix.go index 422740df88391fcdd91e54848520d38dfeb28a8d..5b5683f8aedb3e0e57f087de749c5e090823f0c0 100644 --- a/bindings/cmix.go +++ b/bindings/cmix.go @@ -60,10 +60,6 @@ func NewCmix(ndfJSON, storageDir string, password []byte, registrationCode strin // subprocesses to perform network operations. func LoadCmix(storageDir string, password []byte, cmixParamsJSON []byte) (*Cmix, error) { - if len(cmixParamsJSON) == 0 { - jww.WARN.Printf("cMix params not specified, using defaults...") - cmixParamsJSON = GetDefaultCMixParams() - } params, err := parseCMixParams(cmixParamsJSON) if err != nil { diff --git a/bindings/delivery.go b/bindings/delivery.go index 6420196d4e16f34edae4510cb7d1d228f7e0b2d2..a645e9412dbbc99e49272843241f0dc22058c838 100644 --- a/bindings/delivery.go +++ b/bindings/delivery.go @@ -22,9 +22,9 @@ import ( // This should be used by any type of send report's GetRoundURL method. var dashboardBaseURL = "https://dashboard.xx.network" -// SetDashboardURL is a function which modifies the base dashboard URL -// that is returned as part of any send report. Internally, this is defaulted -// to "https://dashboard.xx.network". This should only be called if the user +// SetDashboardURL is a function which modifies the base dashboard URL that is +// returned as part of any send report. Internally, this is defaulted to +// "https://dashboard.xx.network". This should only be called if the user // explicitly wants to modify the dashboard URL. This function is not // thread-safe, and as such should only be called on setup. // @@ -50,7 +50,7 @@ type RoundsList struct { Rounds []uint64 } -// makeRoundsList converts a list of id.Round into a binding-compatable +// makeRoundsList converts a list of id.Round into a binding-compatible // RoundsList. func makeRoundsList(rounds ...id.Round) RoundsList { rl := RoundsList{make([]uint64, len(rounds))} @@ -140,7 +140,7 @@ func (c *Cmix) WaitForRoundResult( timeout := time.Duration(timeoutMS) * time.Millisecond - err = c.api.GetCmix().GetRoundResults(timeout, f, rl...) + c.api.GetCmix().GetRoundResults(timeout, f, rl...) - return err + return nil } diff --git a/bindings/e2eHandler.go b/bindings/e2eHandler.go index 5ed379257b3fe30089f2643a8c3616526112ceaf..4b63407520f8e1aa756f2e749e4b0971fc72fdd2 100644 --- a/bindings/e2eHandler.go +++ b/bindings/e2eHandler.go @@ -47,6 +47,19 @@ func (e *E2e) GetReceptionID() []byte { return e.api.GetE2E().GetReceptionID().Marshal() } +// DeleteContact removes a partner from E2e's storage. +// +// Parameters: +// - partnerID - the marshalled bytes of id.ID. +func (e *E2e) DeleteContact(partnerID []byte) error { + partner, err := id.Unmarshal(partnerID) + if err != nil { + return err + } + + return e.api.DeleteContact(partner) +} + // GetAllPartnerIDs returns a list of all partner IDs that the user has an E2E // relationship with. // diff --git a/bindings/fileTransfer.go b/bindings/fileTransfer.go index dd3f01e92fb0004d0a73a0c7139e9a95a130e97d..4b8b5adde300191ba85b524fdd1499c3319b1998 100644 --- a/bindings/fileTransfer.go +++ b/bindings/fileTransfer.go @@ -33,12 +33,12 @@ type FileTransfer struct { // // Example JSON: // { -// "TransferID":"B4Z9cwU18beRoGbk5xBjbcd5Ryi9ZUFA2UBvi8FOHWo=", -// "SenderID":"emV6aW1hAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD", -// "Preview":"aXQncyBtZSBhIHByZXZpZXc=", -// "Name":"testfile.txt", -// "Type":"text file", -// "Size":2048 +// "TransferID":"B4Z9cwU18beRoGbk5xBjbcd5Ryi9ZUFA2UBvi8FOHWo=", +// "SenderID":"emV6aW1hAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAD", +// "Preview":"aXQncyBtZSBhIHByZXZpZXc=", +// "Name":"testfile.txt", +// "Type":"text file", +// "Size":2048 // } type ReceivedFile struct { TransferID []byte // ID of the file transfer @@ -52,10 +52,10 @@ type ReceivedFile struct { // FileSend is a public struct that contains the file contents and its name, // type, and preview. // { -// "Name":"testfile.txt", -// "Type":"text file", -// "Preview":"aXQncyBtZSBhIHByZXZpZXc=", -// "Contents":"VGhpcyBpcyB0aGUgZnVsbCBjb250ZW50cyBvZiB0aGUgZmlsZSBpbiBieXRlcw==" +// "Name":"testfile.txt", +// "Type":"text file", +// "Preview":"aXQncyBtZSBhIHByZXZpZXc=", +// "Contents":"VGhpcyBpcyB0aGUgZnVsbCBjb250ZW50cyBvZiB0aGUgZmlsZSBpbiBieXRlcw==" // } type FileSend struct { Name string // Name of the file @@ -69,10 +69,10 @@ type FileSend struct { // // Example JSON: // { -// "Completed":false, -// "Transmitted":128, -// "Total":2048, -// "Err":null +// "Completed":false, +// "Transmitted":128, +// "Total":2048, +// "Err":null // } type Progress struct { Completed bool // Status of transfer (true if done) @@ -183,8 +183,8 @@ func InitFileTransfer(e2eID int, receiveFileCallback ReceiveFileCallback, // - retry - number of retries allowed // - callback - callback that reports file sending progress // - period - Duration (in ms) to wait between progress callbacks triggering. -// This value should depend on how frequently you want to receive -// updates, and should be tuned to your implementation. +// This value should depend on how frequently you want to receive updates, +// and should be tuned to your implementation. // // Returns: // - []byte - unique file transfer ID @@ -271,8 +271,8 @@ func (f *FileTransfer) CloseSend(tidBytes []byte) error { // - tidBytes - file transfer ID // - callback - callback that reports file reception progress // - period - Duration (in ms) to wait between progress callbacks triggering. -// This value should depend on how frequently you want to receive -// updates, and should be tuned to your implementation. +// This value should depend on how frequently you want to receive updates, +// and should be tuned to your implementation. func (f *FileTransfer) RegisterSentProgressCallback(tidBytes []byte, callback FileTransferSentProgressCallback, period int) error { cb := func(completed bool, arrived, total uint16, @@ -301,8 +301,8 @@ func (f *FileTransfer) RegisterSentProgressCallback(tidBytes []byte, // - tidBytes - file transfer ID // - callback - callback that reports file reception progress // - period - Duration (in ms) to wait between progress callbacks triggering. -// This value should depend on how frequently you want to receive -// updates, and should be tuned to your implementation. +// This value should depend on how frequently you want to receive updates, +// and should be tuned to your implementation. func (f *FileTransfer) RegisterReceivedProgressCallback(tidBytes []byte, callback FileTransferReceiveProgressCallback, period int) error { cb := func(completed bool, received, total uint16, diff --git a/bindings/follow.go b/bindings/follow.go index 362ace98f2c8c66f53717b405153c261f75b2597..6f892e4151356b6bfa0906f2f746542aaae4c99e 100644 --- a/bindings/follow.go +++ b/bindings/follow.go @@ -10,6 +10,7 @@ package bindings import ( "encoding/json" "fmt" + jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/cmix/message" "time" @@ -82,6 +83,29 @@ func (c *Cmix) WaitForNetwork(timeoutMS int) bool { return false } +// ReadyToSend determines if the network is ready to send messages on. It +// returns true if the network is healthy and if the client has registered with +// at least 70% of the nodes. Returns false otherwise. +func (c *Cmix) ReadyToSend() bool { + // Check if the network is currently healthy + if !c.api.GetCmix().IsHealthy() { + return false + } + + // If the network is healthy, then check the number of nodes that the client + // is currently registered with + numReg, total, err := c.api.GetNodeRegistrationStatus() + if err != nil { + jww.FATAL.Panicf("Failed to get node registration status: %+v", err) + } + + // FIXME: This is a fix put in place because not all nodes in the NDF are + // online. This should be fixed. + total = 340 + + return numReg >= total*7/10 +} + // NetworkFollowerStatus gets the state of the network follower. It returns a // status with the following values: // Stopped - 0 @@ -145,8 +169,8 @@ func (c *Cmix) IsHealthy() bool { // // JSON Example: // { -// "FileTransfer{BatchBuilderThread, FilePartSendingThread#0, FilePartSendingThread#1, FilePartSendingThread#2, FilePartSendingThread#3}", -// "MessageReception Worker 0" +// "FileTransfer{BatchBuilderThread, FilePartSendingThread#0, FilePartSendingThread#1, FilePartSendingThread#2, FilePartSendingThread#3}", +// "MessageReception Worker 0" // } func (c *Cmix) GetRunningProcesses() ([]byte, error) { return json.Marshal(c.api.GetRunningProcesses()) diff --git a/bindings/group.go b/bindings/group.go index 9ea24b1fdb76bbf89f4d5b04a8421b0cfe0ed960..b7faa628b8b1439b27b0f27413b7049e0f926a97 100644 --- a/bindings/group.go +++ b/bindings/group.go @@ -203,8 +203,8 @@ func (g *GroupChat) Send(groupId, message []byte, tag string) ([]byte, error) { // Construct send report sendReport := &GroupSendReport{ - RoundsList: makeRoundsList(round), - RoundURL: getRoundURL(round), + RoundURL: getRoundURL(round.ID), + RoundsList: makeRoundsList(round.ID), Timestamp: timestamp.UnixNano(), MessageID: msgID.Bytes(), } @@ -322,7 +322,7 @@ func (g *Group) Serialize() []byte { return g.g.Serialize() } -// DeserializeGroup converts the results of Group.Serialize() into a Group +// DeserializeGroup converts the results of Group.Serialize into a Group // so that its methods can be called. func DeserializeGroup(serializedGroupData []byte) (*Group, error) { grp, err := gs.DeserializeGroup(serializedGroupData) diff --git a/bindings/params.go b/bindings/params.go index 3b4b515b2dd2745995adbba039188466617e82cb..c1444e70320f8351514f6d3ddbb2c26872329b83 100644 --- a/bindings/params.go +++ b/bindings/params.go @@ -78,27 +78,42 @@ func GetDefaultE2eFileTransferParams() []byte { return data } +// parseE2eFileTransferParams is a helper function which parses a JSON +// marshalled [e2eFileTransfer.Params]. func parseE2eFileTransferParams(data []byte) (e2eFileTransfer.Params, error) { p := &e2eFileTransfer.Params{} return *p, json.Unmarshal(data, p) } +// parseSingleUseParams is a helper function which parses a JSON marshalled +// [single.RequestParams]. func parseSingleUseParams(data []byte) (single.RequestParams, error) { p := &single.RequestParams{} return *p, p.UnmarshalJSON(data) } +// parseFileTransferParams is a helper function which parses a JSON marshalled +// [fileTransfer.Params]. func parseFileTransferParams(data []byte) (fileTransfer.Params, error) { p := &fileTransfer.Params{} return *p, json.Unmarshal(data, p) } +// parseCMixParams is a helper function which parses a JSON marshalled +// [xxdk.CMIXParams]. func parseCMixParams(data []byte) (xxdk.CMIXParams, error) { + if len(data) == 0 { + jww.WARN.Printf("cMix params not specified, using defaults...") + data = GetDefaultCMixParams() + } + p := &xxdk.CMIXParams{} err := p.Unmarshal(data) return *p, err } +// parseE2EParams is a helper function which parses a JSON marshalled +// [xxdk.E2EParams]. func parseE2EParams(data []byte) (xxdk.E2EParams, error) { p := &xxdk.E2EParams{} err := p.Unmarshal(data) diff --git a/bindings/timeNow.go b/bindings/timeNow.go new file mode 100644 index 0000000000000000000000000000000000000000..6aad2298f81a9651bbd6c80eb3b70a4ac4c9966a --- /dev/null +++ b/bindings/timeNow.go @@ -0,0 +1,36 @@ +/////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +/////////////////////////////////////////////////////////////////////////////// + +package bindings + +import ( + "gitlab.com/xx_network/primitives/netTime" + "time" +) + +// SetTimeSource will set the time source that will be used when retrieving the +// current time using [netTime.Now]. This should be called BEFORE Login() +// and only be called once. Using this after Login is undefined behavior that +// may result in a crash. +// +// Parameters: +// - timeNow is an object which adheres to [netTime.TimeSource]. Specifically, +// this object should a NowMs() method which return a 64-bit integer value. +func SetTimeSource(timeNow netTime.TimeSource) { + netTime.SetTimeSource(timeNow) +} + +// SetOffset will set an internal offset variable. All calls to [netTime.Now] +// will have this offset applied to this value. +// +// Parameters: +// - offset is a time by which netTime.Now will be offset. This value may be +// negative or positive. This expects a 64-bit integer value which will +// represent the number in microseconds this offset will be. +func SetOffset(offset int64) { + netTime.SetOffset(time.Duration(offset) * time.Microsecond) +} diff --git a/broadcast/asymmetric.go b/broadcast/asymmetric.go deleted file mode 100644 index 37d5cac8417ccbdd98f8d3d28f1cfa6bf35b4816..0000000000000000000000000000000000000000 --- a/broadcast/asymmetric.go +++ /dev/null @@ -1,74 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -package broadcast - -import ( - "encoding/binary" - "github.com/pkg/errors" - "gitlab.com/elixxir/client/cmix" - "gitlab.com/elixxir/client/cmix/message" - "gitlab.com/xx_network/crypto/multicastRSA" - "gitlab.com/xx_network/primitives/id" - "gitlab.com/xx_network/primitives/id/ephemeral" -) - -const ( - asymmetricBroadcastServiceTag = "AsymmBcast" - asymmCMixSendTag = "AsymmetricBroadcast" - internalPayloadSizeLength = 2 -) - -// BroadcastAsymmetric broadcasts the payload to the channel. Requires a healthy network state to send -// Payload must be equal to bc.MaxAsymmetricPayloadSize, and the channel PrivateKey must be passed in -func (bc *broadcastClient) BroadcastAsymmetric(pk multicastRSA.PrivateKey, payload []byte, cMixParams cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) { - // Confirm network health - if !bc.net.IsHealthy() { - return 0, ephemeral.Id{}, errors.New(errNetworkHealth) - } - - // Check payload size - if len(payload) > bc.MaxAsymmetricPayloadSize() { - return 0, ephemeral.Id{}, - errors.Errorf(errPayloadSize, len(payload), bc.MaxAsymmetricPayloadSize()) - } - payloadLength := uint16(len(payload)) - - finalPayload := make([]byte, bc.maxAsymmetricPayloadSizeRaw()) - binary.BigEndian.PutUint16(finalPayload[:internalPayloadSizeLength], payloadLength) - copy(finalPayload[internalPayloadSizeLength:], payload) - - // Encrypt payload - encryptedPayload, mac, fp, err := bc.channel.EncryptAsymmetric(finalPayload, pk, bc.rng.GetStream()) - if err != nil { - return 0, ephemeral.Id{}, errors.WithMessage(err, "Failed to encrypt asymmetric broadcast message") - } - - // Create service using asymmetric broadcast service tag & channel reception ID - // Allows anybody with this info to listen for messages on this channel - service := message.Service{ - Identifier: bc.channel.ReceptionID.Bytes(), - Tag: asymmetricBroadcastServiceTag, - } - - if cMixParams.DebugTag == cmix.DefaultDebugTag { - cMixParams.DebugTag = asymmCMixSendTag - } - - // Create payload sized for sending over cmix - sizedPayload := make([]byte, bc.net.GetMaxMessageLength()) - // Read random data into sized payload - _, err = bc.rng.GetStream().Read(sizedPayload) - if err != nil { - return 0, ephemeral.Id{}, errors.WithMessage(err, "Failed to add random data to sized broadcast") - } - copy(sizedPayload[:len(encryptedPayload)], encryptedPayload) - - return bc.net.Send( - bc.channel.ReceptionID, fp, service, sizedPayload, mac, cMixParams) -} diff --git a/broadcast/broadcastClient.go b/broadcast/client.go similarity index 57% rename from broadcast/broadcastClient.go rename to broadcast/client.go index 71e9ab35a74d6e9c4597f082786aaa009702bee8..1b4891a00bdf4d8ae9ce6bf31c2b020b356fa114 100644 --- a/broadcast/broadcastClient.go +++ b/broadcast/client.go @@ -14,25 +14,30 @@ import ( "gitlab.com/elixxir/client/cmix/message" crypto "gitlab.com/elixxir/crypto/broadcast" "gitlab.com/elixxir/crypto/fastRNG" - "gitlab.com/xx_network/crypto/signature/rsa" ) -// broadcastClient implements the Channel interface for sending/receiving asymmetric or symmetric broadcast messages +// broadcastClient implements the [broadcast.Channel] interface for sending/ +// receiving asymmetric or symmetric broadcast messages. type broadcastClient struct { - channel crypto.Channel + channel *crypto.Channel net Client rng *fastRNG.StreamGenerator } -// NewBroadcastChannel creates a channel interface based on crypto.Channel, accepts net client connection & callback for received messages -func NewBroadcastChannel(channel crypto.Channel, net Client, rng *fastRNG.StreamGenerator) (Channel, error) { +type NewBroadcastChannelFunc func(channel *crypto.Channel, net Client, + rng *fastRNG.StreamGenerator) (Channel, error) + +// NewBroadcastChannel creates a channel interface based on [broadcast.Channel]. +// It accepts a Cmix client connection. +func NewBroadcastChannel(channel *crypto.Channel, net Client, + rng *fastRNG.StreamGenerator) (Channel, error) { bc := &broadcastClient{ channel: channel, net: net, rng: rng, } - if !bc.verifyID() { + if !channel.Verify() { return nil, errors.New("Failed ID verification for broadcast channel") } @@ -45,20 +50,21 @@ func NewBroadcastChannel(channel crypto.Channel, net Client, rng *fastRNG.Stream return bc, nil } -// RegisterListener adds a service to hear broadcast messages of a given type via the passed in callback +// RegisterListener registers a listener for broadcast messages. func (bc *broadcastClient) RegisterListener(listenerCb ListenerFunc, method Method) error { var tag string switch method { case Symmetric: tag = symmetricBroadcastServiceTag - case Asymmetric: - tag = asymmetricBroadcastServiceTag + case RSAToPublic: + tag = asymmetricRSAToPublicBroadcastServiceTag default: - return errors.Errorf("Cannot register listener for broadcast method %s", method) + return errors.Errorf( + "Cannot register listener for broadcast method %s", method) } p := &processor{ - c: &bc.channel, + c: bc.channel, cb: listenerCb, method: method, } @@ -72,8 +78,8 @@ func (bc *broadcastClient) RegisterListener(listenerCb ListenerFunc, method Meth return nil } -// Stop unregisters the listener callback and stops the channel's identity -// from being tracked. +// Stop unregisters the listener callback and stops the channel's identity from +// being tracked. func (bc *broadcastClient) Stop() { // Removes currently tracked identity bc.net.RemoveIdentity(bc.channel.ReceptionID) @@ -82,29 +88,27 @@ func (bc *broadcastClient) Stop() { bc.net.DeleteClientService(bc.channel.ReceptionID) } -// Get returns the underlying crypto.Channel object -func (bc *broadcastClient) Get() crypto.Channel { +// Get returns the underlying [broadcast.Channel] object. +func (bc *broadcastClient) Get() *crypto.Channel { return bc.channel } -// verifyID generates a symmetric ID based on the info in the channel & compares it to the one passed in -func (bc *broadcastClient) verifyID() bool { - gen, err := crypto.NewChannelID(bc.channel.Name, bc.channel.Description, bc.channel.Salt, rsa.CreatePublicKeyPem(bc.channel.RsaPubKey)) - if err != nil { - jww.FATAL.Panicf("[verifyID] Failed to generate verified channel ID") - return false - } - return bc.channel.ReceptionID.Cmp(gen) -} - +// MaxPayloadSize returns the maximum size for a symmetric broadcast payload. func (bc *broadcastClient) MaxPayloadSize() int { return bc.maxSymmetricPayload() } -func (bc *broadcastClient) MaxAsymmetricPayloadSize() int { - return bc.maxAsymmetricPayloadSizeRaw() - internalPayloadSizeLength +func (bc *broadcastClient) maxSymmetricPayload() int { + return bc.channel.GetMaxSymmetricPayloadSize(bc.net.GetMaxMessageLength()) +} + +// MaxRSAToPublicPayloadSize return the maximum payload size for a +// [broadcast.RSAToPublic] asymmetric payload. +func (bc *broadcastClient) MaxRSAToPublicPayloadSize() int { + return bc.maxRSAToPublicPayloadSizeRaw() - internalPayloadSizeLength } -func (bc *broadcastClient) maxAsymmetricPayloadSizeRaw() int { - return bc.channel.MaxAsymmetricPayloadSize() +func (bc *broadcastClient) maxRSAToPublicPayloadSizeRaw() int { + size, _, _ := bc.channel.GetRSAToPublicMessageLength() + return size } diff --git a/broadcast/interface.go b/broadcast/interface.go index dc112ea8bebf457bf44f722c5332f001f27b67d3..3c674c0db3438862296e8980afcb8177eb3d2a9a 100644 --- a/broadcast/interface.go +++ b/broadcast/interface.go @@ -13,40 +13,69 @@ import ( "gitlab.com/elixxir/client/cmix/message" "gitlab.com/elixxir/client/cmix/rounds" crypto "gitlab.com/elixxir/crypto/broadcast" - "gitlab.com/elixxir/primitives/format" - "gitlab.com/xx_network/crypto/multicastRSA" + "gitlab.com/elixxir/crypto/rsa" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" "time" ) -// ListenerFunc is registered when creating a new broadcasting channel -// and receives all new broadcast messages for the channel. +// ListenerFunc is registered when creating a new broadcasting channel and +// receives all new broadcast messages for the channel. type ListenerFunc func(payload []byte, receptionID receptionID.EphemeralIdentity, round rounds.Round) -// Channel is the public-facing interface to interact with broadcast channels +// Channel is the public-facing interface to interact with broadcast channels. type Channel interface { - // MaxPayloadSize returns the maximum size for a symmetric broadcast payload + // MaxPayloadSize returns the maximum size for a symmetric broadcast + // payload. MaxPayloadSize() int - // MaxAsymmetricPayloadSize returns the maximum size for an asymmetric broadcast payload - MaxAsymmetricPayloadSize() int + // MaxRSAToPublicPayloadSize returns the maximum size for an asymmetric + // broadcast payload. + MaxRSAToPublicPayloadSize() int - // Get returns the underlying crypto.Channel - Get() crypto.Channel + // Get returns the underlying [broadcast.Channel] object. + Get() *crypto.Channel - // Broadcast broadcasts the payload to the channel. The payload size must be - // equal to MaxPayloadSize. + // Broadcast broadcasts a payload to the channel. The payload must be of the + // size [Channel.MaxPayloadSize] or smaller. + // + // The network must be healthy to send. Broadcast(payload []byte, cMixParams cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) + rounds.Round, ephemeral.Id, error) - // BroadcastAsymmetric broadcasts an asymmetric payload to the channel. The payload size must be - // equal to MaxPayloadSize & private key for channel must be passed in - BroadcastAsymmetric(pk multicastRSA.PrivateKey, payload []byte, cMixParams cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) + // BroadcastWithAssembler broadcasts a payload over a channel with a payload + // assembled after the round is selected, allowing the round info to be + // included in the payload. + // + // The payload must be of the size [Channel.MaxPayloadSize] or smaller. + // + // The network must be healthy to send. + BroadcastWithAssembler(assembler Assembler, cMixParams cmix.CMIXParams) ( + rounds.Round, ephemeral.Id, error) - // RegisterListener registers a listener for broadcast messages + // BroadcastRSAtoPublic broadcasts the payload to the channel. + // + // The payload must be of the size [Channel.MaxRSAToPublicPayloadSize] or + // smaller and the channel [rsa.PrivateKey] must be passed in. + // + // The network must be healthy to send. + BroadcastRSAtoPublic(pk rsa.PrivateKey, payload []byte, + cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) + + // BroadcastRSAToPublicWithAssembler broadcasts the payload to the channel + // with a function that builds the payload based upon the ID of the selected + // round. + // + // The payload must be of the size [Channel.MaxRSAToPublicPayloadSize] or + // smaller and the channel [rsa.PrivateKey] must be passed in. + // + // The network must be healthy to send. + BroadcastRSAToPublicWithAssembler( + pk rsa.PrivateKey, assembler Assembler, + cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) + + // RegisterListener registers a listener for broadcast messages. RegisterListener(listenerCb ListenerFunc, method Method) error // Stop unregisters the listener callback and stops the channel's identity @@ -54,17 +83,19 @@ type Channel interface { Stop() } -// Client contains the methods from cmix.Client that are required by -// symmetricClient. +// Assembler is a function which allows a bre +type Assembler func(rid id.Round) (payload []byte, err error) + +// Client contains the methods from [cmix.Client] that are required by +// broadcastClient. type Client interface { - GetMaxMessageLength() int - Send(recipient *id.ID, fingerprint format.Fingerprint, - service message.Service, payload, mac []byte, - cMixParams cmix.CMIXParams) (id.Round, ephemeral.Id, error) + SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler, + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) IsHealthy() bool AddIdentity(id *id.ID, validUntil time.Time, persistent bool) AddService(clientID *id.ID, newService message.Service, response message.Processor) DeleteClientService(clientID *id.ID) RemoveIdentity(id *id.ID) + GetMaxMessageLength() int } diff --git a/broadcast/method.go b/broadcast/method.go index 8e93ea57dba204b2f616071e54d7317bcec96624..c154fc027b81a55f600c5f54fa3847aa7616ea89 100644 --- a/broadcast/method.go +++ b/broadcast/method.go @@ -12,15 +12,18 @@ type Method uint8 const ( Symmetric Method = iota - Asymmetric + RSAToPublic + RSAToPrivate ) func (m Method) String() string { switch m { case Symmetric: return "Symmetric" - case Asymmetric: - return "Asymmetric" + case RSAToPublic: + return "RSAToPublic" + case RSAToPrivate: + return "RSAToPrivate" default: return "Unknown" } diff --git a/broadcast/processor.go b/broadcast/processor.go index a387c9e43ea1f8cb376f90adc7d7589e6ef2fd16..8ee1f85d07ad093337711ef15e7f6e3ced3dd1f8 100644 --- a/broadcast/processor.go +++ b/broadcast/processor.go @@ -35,10 +35,8 @@ func (p *processor) Process(msg format.Message, var payload []byte var err error switch p.method { - case Asymmetric: - encPartSize := p.c.RsaPubKey.Size() // Size returned by multicast RSA encryption - encodedMessage := msg.GetContents()[:encPartSize] // Only one message is encoded, rest of it is random data - decodedMessage, decryptErr := p.c.DecryptAsymmetric(encodedMessage) + case RSAToPublic: + decodedMessage, decryptErr := p.c.DecryptRSAToPublic(msg.GetContents(), msg.GetMac(), msg.GetKeyFP()) if decryptErr != nil { jww.ERROR.Printf(errDecrypt, p.c.ReceptionID, p.c.Name, decryptErr) return @@ -53,10 +51,10 @@ func (p *processor) Process(msg format.Message, return } default: - jww.ERROR.Printf("Unrecognized broadcast method %d", p.method) + jww.FATAL.Panicf("Unrecognized broadcast method %d", p.method) } - go p.cb(payload, receptionID, round) + p.cb(payload, receptionID, round) } // String returns a string identifying the symmetricProcessor for debugging purposes. diff --git a/broadcast/processor_test.go b/broadcast/processor_test.go index 6e0aa8edec9d054dc8c3283fc2689fd7eae71593..b758c20dbfa8756a31cc554671ab884d6b312a56 100644 --- a/broadcast/processor_test.go +++ b/broadcast/processor_test.go @@ -7,20 +7,7 @@ package broadcast -import ( - "bytes" - "gitlab.com/elixxir/client/cmix/identity/receptionID" - "gitlab.com/elixxir/client/cmix/rounds" - crypto "gitlab.com/elixxir/crypto/broadcast" - "gitlab.com/elixxir/crypto/cmix" - "gitlab.com/elixxir/primitives/format" - "gitlab.com/xx_network/crypto/csprng" - "gitlab.com/xx_network/crypto/signature/rsa" - "gitlab.com/xx_network/primitives/id" - "testing" - "time" -) - +/* // Tests that process.Process properly decrypts the payload and passes it to the // callback. func Test_processor_Process(t *testing.T) { @@ -37,7 +24,7 @@ func Test_processor_Process(t *testing.T) { RsaPubKey: rsaPrivKey.GetPublic(), } - cbChan := make(chan []byte) + cbChan := make(chan []byte, 1) cb := func(payload []byte, _ receptionID.EphemeralIdentity, _ rounds.Round) { cbChan <- payload } @@ -67,4 +54,4 @@ func Test_processor_Process(t *testing.T) { case <-time.After(15 * time.Millisecond): t.Error("Timed out waiting for listener channel to be called.") } -} +}*/ diff --git a/broadcast/rsaToPublic.go b/broadcast/rsaToPublic.go new file mode 100644 index 0000000000000000000000000000000000000000..26c4d0924af04c6464610f20fe8040ac1aa7b082 --- /dev/null +++ b/broadcast/rsaToPublic.go @@ -0,0 +1,121 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +package broadcast + +import ( + "encoding/binary" + "github.com/pkg/errors" + "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/message" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/crypto/rsa" + "gitlab.com/elixxir/primitives/format" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" +) + +const ( + asymmetricRSAToPublicBroadcastServiceTag = "AsymmToPublicBcast" + asymmCMixSendTag = "AsymmetricBroadcast" + internalPayloadSizeLength = 2 +) + +// BroadcastRSAtoPublic broadcasts the payload to the channel. Requires a +// healthy network state to send Payload length less than or equal to +// bc.MaxRSAToPublicPayloadSize, and the channel PrivateKey must be passed in +// +// BroadcastRSAtoPublic broadcasts the payload to the channel. +// +// The payload must be of the size [broadcastClient.MaxRSAToPublicPayloadSize] +// or smaller and the channel [rsa.PrivateKey] must be passed in. +// +// The network must be healthy to send. +func (bc *broadcastClient) BroadcastRSAtoPublic(pk rsa.PrivateKey, + payload []byte, cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + // Confirm network health + + assemble := func(rid id.Round) ([]byte, error) { + return payload, nil + } + return bc.BroadcastRSAToPublicWithAssembler(pk, assemble, cMixParams) +} + +// BroadcastRSAToPublicWithAssembler broadcasts the payload to the channel +// with a function that builds the payload based upon the ID of the selected +// round. +// +// The payload must be of the size [broadcastClient.MaxRSAToPublicPayloadSize] +// or smaller and the channel [rsa.PrivateKey] must be passed in. +// +// The network must be healthy to send. +func (bc *broadcastClient) BroadcastRSAToPublicWithAssembler( + pk rsa.PrivateKey, assembler Assembler, + cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + // Confirm network health + if !bc.net.IsHealthy() { + return rounds.Round{}, ephemeral.Id{}, errors.New(errNetworkHealth) + } + + assemble := func(rid id.Round) (fp format.Fingerprint, + service message.Service, encryptedPayload, mac []byte, err error) { + payload, err := assembler(rid) + if err != nil { + return format.Fingerprint{}, message.Service{}, nil, + nil, err + } + // Check payload size + if len(payload) > bc.MaxRSAToPublicPayloadSize() { + return format.Fingerprint{}, message.Service{}, nil, + nil, errors.Errorf(errPayloadSize, len(payload), + bc.MaxRSAToPublicPayloadSize()) + } + payloadLength := uint16(len(payload)) + + finalPayload := make([]byte, bc.maxRSAToPublicPayloadSizeRaw()) + binary.BigEndian.PutUint16(finalPayload[:internalPayloadSizeLength], + payloadLength) + copy(finalPayload[internalPayloadSizeLength:], payload) + + // Encrypt payload + encryptedPayload, mac, fp, err = + bc.channel.EncryptRSAToPublic(finalPayload, pk, bc.net.GetMaxMessageLength(), + bc.rng.GetStream()) + if err != nil { + return format.Fingerprint{}, message.Service{}, nil, + nil, errors.WithMessage(err, "Failed to encrypt "+ + "asymmetric broadcast message") + } + + // Create service using asymmetric broadcast service tag & channel + // reception ID allows anybody with this info to listen for messages on + // this channel + service = message.Service{ + Identifier: bc.channel.ReceptionID.Bytes(), + Tag: asymmetricRSAToPublicBroadcastServiceTag, + } + + if cMixParams.DebugTag == cmix.DefaultDebugTag { + cMixParams.DebugTag = asymmCMixSendTag + } + + // Create payload sized for sending over cmix + sizedPayload := make([]byte, bc.net.GetMaxMessageLength()) + // Read random data into sized payload + _, err = bc.rng.GetStream().Read(sizedPayload) + if err != nil { + return format.Fingerprint{}, message.Service{}, nil, + nil, errors.WithMessage(err, "Failed to add "+ + "random data to sized broadcast") + } + copy(sizedPayload[:len(encryptedPayload)], encryptedPayload) + + return + } + + return bc.net.SendWithAssembler(bc.channel.ReceptionID, assemble, cMixParams) +} diff --git a/broadcast/asymmetric_test.go b/broadcast/rsaToPublic_test.go similarity index 60% rename from broadcast/asymmetric_test.go rename to broadcast/rsaToPublic_test.go index e817688ded54df75a0cad3da24bdb85624e5e8dd..9be17453b96db68ca8a735fdcd9234ccd7ad2e3e 100644 --- a/broadcast/asymmetric_test.go +++ b/broadcast/rsaToPublic_test.go @@ -10,56 +10,71 @@ package broadcast import ( "bytes" "fmt" + "reflect" + "sync" + "testing" + "time" + + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/message" "gitlab.com/elixxir/client/cmix/rounds" crypto "gitlab.com/elixxir/crypto/broadcast" - cMixCrypto "gitlab.com/elixxir/crypto/cmix" "gitlab.com/elixxir/crypto/fastRNG" - "gitlab.com/xx_network/crypto/csprng" - "gitlab.com/xx_network/crypto/signature/rsa" - "reflect" - "sync" - "testing" - "time" + "gitlab.com/elixxir/primitives/format" ) -// Tests that symmetricClient adheres to the Symmetric interface. +// Tests that broadcastClient adheres to the Channel interface. var _ Channel = (*broadcastClient)(nil) -// Tests that symmetricClient adheres to the Symmetric interface. +// Tests that cmix.Client adheres to the Client interface. var _ Client = (cmix.Client)(nil) +// Tests that mockProcessor adheres to the message.Processor interface. +var _ message.Processor = (*mockProcessor)(nil) + +// mockProcessor adheres to the message.Processor interface. +type mockProcessor struct { + messages []format.Message +} + +func newMockProcessor() *mockProcessor { + m := new(mockProcessor) + m.messages = make([]format.Message, 0) + return m +} +func (p *mockProcessor) Process(message format.Message, + _ receptionID.EphemeralIdentity, _ rounds.Round) { + p.messages = append(p.messages, message) +} +func (p *mockProcessor) String() string { return "hello" } + func Test_asymmetricClient_Smoke(t *testing.T) { cMixHandler := newMockCmixHandler() rngGen := fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG) - pk, err := rsa.GenerateKey(rngGen.GetStream(), 4096) - if err != nil { - t.Fatalf("Failed to generate priv key: %+v", err) - } - cname := "MyChannel" - cdesc := "This is my channel about stuff." - csalt := cMixCrypto.NewSalt(csprng.NewSystemRNG(), 32) - cpubkey := pk.GetPublic() - cid, err := crypto.NewChannelID(cname, cdesc, csalt, rsa.CreatePublicKeyPem(cpubkey)) - if err != nil { - t.Errorf("Failed to create channel ID: %+v", err) - } - channel := crypto.Channel{ - ReceptionID: cid, - Name: cname, - Description: cdesc, - Salt: csalt, - RsaPubKey: cpubkey, - } + cName := "MyChannel" + cDesc := "This is my channel about stuff." + packetPayloadLength := newMockCmix(cMixHandler).GetMaxMessageLength() + + channel, pk, _ := crypto.NewChannel( + cName, cDesc, crypto.Public, packetPayloadLength, rngGen.GetStream()) + cid := channel.ReceptionID + + // Must mutate cMixHandler such that it's processorMap contains a + // message.Processor + mockProc := newMockProcessor() + cMixHandler.processorMap[*cid] = make(map[string][]message.Processor) + cMixHandler.processorMap[*cid]["AsymmBcast"] = []message.Processor{mockProc} - const n = 5 + const n = 1 cbChans := make([]chan []byte, n) clients := make([]Channel, n) for i := range clients { cbChan := make(chan []byte, 10) - cb := func(payload []byte, _ receptionID.EphemeralIdentity, - _ rounds.Round) { + cb := func( + payload []byte, _ receptionID.EphemeralIdentity, _ rounds.Round) { cbChan <- payload } @@ -68,7 +83,7 @@ func Test_asymmetricClient_Smoke(t *testing.T) { t.Errorf("Failed to create broadcast channel: %+v", err) } - err = s.RegisterListener(cb, Asymmetric) + err = s.RegisterListener(cb, RSAToPublic) if err != nil { t.Errorf("Failed to register listener: %+v", err) } @@ -76,7 +91,7 @@ func Test_asymmetricClient_Smoke(t *testing.T) { cbChans[i] = cbChan clients[i] = s - // Test that Get returns the expected channel + // Test that Channel.Get returns the expected channel if !reflect.DeepEqual(s.Get(), channel) { t.Errorf("Cmix %d returned wrong channel."+ "\nexpected: %+v\nreceived: %+v", i, channel, s.Get()) @@ -85,7 +100,7 @@ func Test_asymmetricClient_Smoke(t *testing.T) { // Send broadcast from each client for i := range clients { - payload := make([]byte, clients[i].MaxAsymmetricPayloadSize()) + payload := make([]byte, clients[i].MaxRSAToPublicPayloadSize()) copy(payload, fmt.Sprintf("Hello from client %d of %d.", i, len(clients))) @@ -98,9 +113,9 @@ func Test_asymmetricClient_Smoke(t *testing.T) { select { case r := <-cbChan: if !bytes.Equal(payload, r) { - t.Errorf("Cmix %d failed to receive expected "+ - "payload from client %d."+ - "\nexpected: %q\nreceived: %q", j, i, payload, r) + t.Errorf("Cmix %d failed to receive expected payload "+ + "from client %d.\nexpected: %q\nreceived: %q", + j, i, payload, r) } case <-time.After(time.Second): t.Errorf("Cmix %d timed out waiting for broadcast "+ @@ -110,7 +125,8 @@ func Test_asymmetricClient_Smoke(t *testing.T) { } // Broadcast payload - _, _, err := clients[i].BroadcastAsymmetric(pk, payload, cmix.GetDefaultCMIXParams()) + _, _, err := clients[i].BroadcastRSAtoPublic( + pk, payload, cmix.GetDefaultCMIXParams()) if err != nil { t.Errorf("Cmix %d failed to send broadcast: %+v", i, err) } @@ -124,7 +140,7 @@ func Test_asymmetricClient_Smoke(t *testing.T) { clients[i].Stop() } - payload := make([]byte, clients[0].MaxAsymmetricPayloadSize()) + payload := make([]byte, clients[0].MaxRSAToPublicPayloadSize()) copy(payload, "This message should not get through.") // Start waiting on channels and error if anything is received @@ -142,7 +158,7 @@ func Test_asymmetricClient_Smoke(t *testing.T) { } // Broadcast payload - _, _, err = clients[0].BroadcastAsymmetric(pk, payload, cmix.GetDefaultCMIXParams()) + _, _, err := clients[0].BroadcastRSAtoPublic(pk, payload, cmix.GetDefaultCMIXParams()) if err != nil { t.Errorf("Cmix 0 failed to send broadcast: %+v", err) } diff --git a/broadcast/sizedBroadcast.go b/broadcast/sizedBroadcast.go deleted file mode 100644 index e7099e02b089b3b0c8eaddcd4903313ce9d79e2c..0000000000000000000000000000000000000000 --- a/broadcast/sizedBroadcast.go +++ /dev/null @@ -1,83 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -package broadcast - -import ( - "encoding/binary" - "github.com/pkg/errors" -) - -// Message field sizes. -const ( - sizeSize = 2 - sizedBroadcastMinSize = sizeSize -) - -// Error messages. -const ( - // NewSizedBroadcast - errNewSizedBroadcastMaxSize = "size of payload and its size %d too large to fit in max payload size %d" - - // DecodeSizedBroadcast - errDecodeSizedBroadcastDataLen = "size of data %d must be greater than %d" - errDecodeSizedBroadcastSize = "stated payload size %d larger than provided data %d" -) - -/* -+---------------------------+ -| cMix Message Contents | -+---------+-----------------+ -| Size | Payload | -| 2 bytes | remaining space | -+---------+-----------------+ -*/ - -// NewSizedBroadcast creates a new broadcast payload of size maxPayloadSize that -// contains the given payload so that it fits completely inside a broadcasted -// cMix message payload. The length of the payload is stored internally and used -// to strip extraneous padding when decoding the payload. -// The maxPayloadSize is the maximum size of the resulting payload. Returns an -// error when the provided payload cannot fit in the max payload size. -func NewSizedBroadcast(maxPayloadSize int, payload []byte) ([]byte, error) { - if len(payload)+sizedBroadcastMinSize > maxPayloadSize { - return nil, errors.Errorf(errNewSizedBroadcastMaxSize, - len(payload)+sizedBroadcastMinSize, maxPayloadSize) - } - - b := make([]byte, sizeSize) - binary.LittleEndian.PutUint16(b, uint16(len(payload))) - - sizedPayload := make([]byte, maxPayloadSize) - copy(sizedPayload, append(b, payload...)) - - return sizedPayload, nil -} - -// DecodeSizedBroadcast decodes the data into its original payload stripping off -// extraneous padding. -func DecodeSizedBroadcast(data []byte) ([]byte, error) { - if len(data) < sizedBroadcastMinSize { - return nil, errors.Errorf( - errDecodeSizedBroadcastDataLen, len(data), sizedBroadcastMinSize) - } - - size := binary.LittleEndian.Uint16(data[:sizeSize]) - if int(size) > len(data[sizeSize:]) { - return nil, errors.Errorf( - errDecodeSizedBroadcastSize, size, len(data[sizeSize:])) - } - - return data[sizeSize : size+sizeSize], nil -} - -// MaxSizedBroadcastPayloadSize returns the maximum size of a payload that can -// fit in a sized broadcast message for the given maximum cMix message payload -// size. -func MaxSizedBroadcastPayloadSize(maxPayloadSize int) int { - return maxPayloadSize - sizedBroadcastMinSize -} diff --git a/broadcast/sizedBroadcast_test.go b/broadcast/sizedBroadcast_test.go deleted file mode 100644 index d81b439c097574e0c58d35525da79d14a1334471..0000000000000000000000000000000000000000 --- a/broadcast/sizedBroadcast_test.go +++ /dev/null @@ -1,115 +0,0 @@ -//////////////////////////////////////////////////////////////////////////////// -// Copyright © 2022 xx foundation // -// // -// Use of this source code is governed by a license that can be found in the // -// LICENSE file. // -//////////////////////////////////////////////////////////////////////////////// - -package broadcast - -import ( - "bytes" - "fmt" - "testing" -) - -// Tests that a payload smaller than the max payload size encoded via -// NewSizedBroadcast and decoded via DecodeSizedBroadcast matches the original. -func TestNewSizedBroadcast_DecodeSizedBroadcast_SmallPayload(t *testing.T) { - const maxPayloadSize = 512 - payload := []byte("This is my payload message.") - - data, err := NewSizedBroadcast(maxPayloadSize, payload) - if err != nil { - t.Errorf("NewSizedBroadcast returned an error: %+v", err) - } - - decodedPayload, err := DecodeSizedBroadcast(data) - if err != nil { - t.Errorf("DecodeSizedBroadcast returned an error: %+v", err) - } - - if !bytes.Equal(payload, decodedPayload) { - t.Errorf("Decoded payload does not match original."+ - "\nexpected: %q\nreceived: %q", payload, decodedPayload) - } -} - -// Tests that a payload the same size as the max payload size encoded via -// NewSizedBroadcast and decoded via DecodeSizedBroadcast matches the original. -func TestNewSizedBroadcast_DecodeSizedBroadcast_FullSizesPayload(t *testing.T) { - payload := []byte("This is my payload message.") - maxPayloadSize := len(payload) + sizeSize - - data, err := NewSizedBroadcast(maxPayloadSize, payload) - if err != nil { - t.Errorf("NewSizedBroadcast returned an error: %+v", err) - } - - decodedPayload, err := DecodeSizedBroadcast(data) - if err != nil { - t.Errorf("DecodeSizedBroadcast returned an error: %+v", err) - } - - if !bytes.Equal(payload, decodedPayload) { - t.Errorf("Decoded payload does not match original."+ - "\nexpected: %q\nreceived: %q", payload, decodedPayload) - } -} - -// Error path: tests that NewSizedBroadcast returns an error when the payload is -// larger than the max payload size. -func TestNewSizedBroadcast_MaxPayloadSizeError(t *testing.T) { - payload := []byte("This is my payload message.") - maxPayloadSize := len(payload) - expectedErr := fmt.Sprintf(errNewSizedBroadcastMaxSize, - len(payload)+sizedBroadcastMinSize, maxPayloadSize) - - _, err := NewSizedBroadcast(maxPayloadSize, payload) - if err == nil || err.Error() != expectedErr { - t.Errorf("NewSizedBroadcast did not return the expected error when "+ - "the payload is too large.\nexpected: %s\nreceived: %+v", - expectedErr, err) - } -} - -// Error path: tests that DecodeSizedBroadcast returns an error when the length -// of the data is shorter than the minimum length of a sized broadcast. -func TestDecodeSizedBroadcast_DataTooShortError(t *testing.T) { - data := []byte{0} - expectedErr := fmt.Sprintf( - errDecodeSizedBroadcastDataLen, len(data), sizedBroadcastMinSize) - - _, err := DecodeSizedBroadcast(data) - if err == nil || err.Error() != expectedErr { - t.Errorf("DecodeSizedBroadcast did not return the expected error "+ - "when the data is too small.\nexpected: %s\nreceived: %+v", - expectedErr, err) - } -} - -// Error path: tests that DecodeSizedBroadcast returns an error when the payload -// size is larger than the actual payload contained in the data. -func TestDecodeSizedBroadcast_SizeMismatchError(t *testing.T) { - data := []byte{255, 0, 10} - expectedErr := fmt.Sprintf( - errDecodeSizedBroadcastSize, data[0], len(data[sizeSize:])) - - _, err := DecodeSizedBroadcast(data) - if err == nil || err.Error() != expectedErr { - t.Errorf("DecodeSizedBroadcast did not return the expected error "+ - "when the size is too large.\nexpected: %s\nreceived: %+v", - expectedErr, err) - } -} - -// Tests that MaxSizedBroadcastPayloadSize returns the correct max size. -func TestMaxSizedBroadcastPayloadSize(t *testing.T) { - maxPayloadSize := 512 - expectedSize := maxPayloadSize - sizedBroadcastMinSize - receivedSize := MaxSizedBroadcastPayloadSize(maxPayloadSize) - if receivedSize != expectedSize { - t.Errorf("Incorrect max paylaod size.\nexpected: %d\nreceived: %d", - expectedSize, receivedSize) - } -} diff --git a/broadcast/symmetric.go b/broadcast/symmetric.go index 9a6885c8bc59fbd5b0d9482e99bbeefcc404c6be..432c53c8cab2ae80ea16f915d0142136a0d1532d 100644 --- a/broadcast/symmetric.go +++ b/broadcast/symmetric.go @@ -11,13 +11,15 @@ import ( "github.com/pkg/errors" "gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/cmix/message" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" ) // Error messages. const ( - // symmetricClient.Broadcast + // broadcastClient.Broadcast errNetworkHealth = "cannot send broadcast when the network is not healthy" errPayloadSize = "size of payload %d must be less than %d" errBroadcastMethodType = "cannot call %s broadcast using %s channel" @@ -29,41 +31,68 @@ const ( symmetricBroadcastServiceTag = "SymmetricBroadcast" ) -// MaxSymmetricPayloadSize returns the maximum size for a broadcasted payload. -func (bc *broadcastClient) maxSymmetricPayload() int { - return bc.net.GetMaxMessageLength() +// Broadcast broadcasts a payload to a symmetric channel. The payload must be of +// size [broadcastClient.MaxPayloadSize] or smaller. +// +// The network must be healthy to send. +func (bc *broadcastClient) Broadcast(payload []byte, cMixParams cmix.CMIXParams) ( + rounds.Round, ephemeral.Id, error) { + assemble := func(rid id.Round) ([]byte, error) { + return payload, nil + } + return bc.BroadcastWithAssembler(assemble, cMixParams) } -// Broadcast broadcasts a payload over a symmetric channel. -// Network must be healthy to send -// Requires a payload of size bc.MaxSymmetricPayloadSize() -func (bc *broadcastClient) Broadcast(payload []byte, cMixParams cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) { +// BroadcastWithAssembler broadcasts a payload over a symmetric channel with a +// payload assembled after the round is selected, allowing the round info to be +// included in the payload. +// +// The payload must be of the size [Channel.MaxPayloadSize] or smaller. +// +// The network must be healthy to send. +func (bc *broadcastClient) BroadcastWithAssembler(assembler Assembler, cMixParams cmix.CMIXParams) ( + rounds.Round, ephemeral.Id, error) { if !bc.net.IsHealthy() { - return 0, ephemeral.Id{}, errors.New(errNetworkHealth) + return rounds.Round{}, ephemeral.Id{}, errors.New(errNetworkHealth) } - if len(payload) != bc.maxSymmetricPayload() { - return 0, ephemeral.Id{}, - errors.Errorf(errPayloadSize, len(payload), bc.maxSymmetricPayload()) - } + assemble := func(rid id.Round) (fp format.Fingerprint, + service message.Service, encryptedPayload, mac []byte, err error) { - // Encrypt payload - rng := bc.rng.GetStream() - encryptedPayload, mac, fp := bc.channel.EncryptSymmetric(payload, rng) - rng.Close() + //assemble the passed payload + payload, err := assembler(rid) + if err != nil { + return format.Fingerprint{}, message.Service{}, nil, nil, err + } - // Create service using symmetric broadcast service tag & channel reception ID - // Allows anybody with this info to listen for messages on this channel - service := message.Service{ - Identifier: bc.channel.ReceptionID.Bytes(), - Tag: symmetricBroadcastServiceTag, - } + if len(payload) > bc.maxSymmetricPayload() { + return format.Fingerprint{}, message.Service{}, nil, nil, + errors.Errorf(errPayloadSize, len(payload), bc.maxSymmetricPayload()) + } + + // Encrypt payload + rng := bc.rng.GetStream() + defer rng.Close() + encryptedPayload, mac, fp, err = bc.channel.EncryptSymmetric(payload, + bc.net.GetMaxMessageLength(), rng) + if err != nil { + return format.Fingerprint{}, message.Service{}, + nil, nil, err + } + + // Create service using symmetric broadcast service tag & channel reception ID + // Allows anybody with this info to listen for messages on this channel + service = message.Service{ + Identifier: bc.channel.ReceptionID.Bytes(), + Tag: symmetricBroadcastServiceTag, + } - if cMixParams.DebugTag == cmix.DefaultDebugTag { - cMixParams.DebugTag = symmCMixSendTag + if cMixParams.DebugTag == cmix.DefaultDebugTag { + cMixParams.DebugTag = symmCMixSendTag + } + return } - return bc.net.Send( - bc.channel.ReceptionID, fp, service, encryptedPayload, mac, cMixParams) + return bc.net.SendWithAssembler(bc.channel.ReceptionID, assemble, + cMixParams) } diff --git a/broadcast/symmetric_test.go b/broadcast/symmetric_test.go index c339838c68cd6b2b2cebadef08157bb31a5d735f..3386a71ead8fb2ffada89329017975216be91507 100644 --- a/broadcast/symmetric_test.go +++ b/broadcast/symmetric_test.go @@ -7,6 +7,7 @@ package broadcast +/* import ( "bytes" "fmt" @@ -14,10 +15,9 @@ import ( "gitlab.com/elixxir/client/cmix/identity/receptionID" "gitlab.com/elixxir/client/cmix/rounds" crypto "gitlab.com/elixxir/crypto/broadcast" - cMixCrypto "gitlab.com/elixxir/crypto/cmix" + "gitlab.com/elixxir/crypto/fastRNG" "gitlab.com/xx_network/crypto/csprng" - "gitlab.com/xx_network/crypto/signature/rsa" "reflect" "sync" "testing" @@ -38,19 +38,10 @@ func Test_symmetricClient_Smoke(t *testing.T) { rngGen := fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG) cname := "MyChannel" cdesc := "This is my channel about stuff." - csalt := cMixCrypto.NewSalt(csprng.NewSystemRNG(), 32) - cpubkey := newRsaPubKey(64, t) - cid, err := crypto.NewChannelID(cname, cdesc, csalt, rsa.CreatePublicKeyPem(cpubkey)) - if err != nil { - t.Errorf("Failed to create channel ID: %+v", err) - } - channel := crypto.Channel{ - ReceptionID: cid, - Name: cname, - Description: cdesc, - Salt: csalt, - RsaPubKey: cpubkey, - } + mCmix := newMockCmix(cMixHandler) + channel,_,_ := crypto.NewChannel(cname, cdesc, + mCmix.GetMaxMessageLength(), + rngGen.GetStream()) // Set up callbacks, callback channels, and the symmetric clients const n = 5 @@ -85,7 +76,7 @@ func Test_symmetricClient_Smoke(t *testing.T) { // Send broadcast from each client for i := range clients { - payload := make([]byte, newMockCmix(cMixHandler).GetMaxMessageLength()) + payload := make([]byte, clients[i].MaxPayloadSize()) copy(payload, fmt.Sprintf("Hello from client %d of %d.", i, len(clients))) @@ -142,10 +133,10 @@ func Test_symmetricClient_Smoke(t *testing.T) { } // Broadcast payload - _, _, err = clients[0].Broadcast(payload, cmix.GetDefaultCMIXParams()) + _, _, err := clients[0].Broadcast(payload, cmix.GetDefaultCMIXParams()) if err != nil { t.Errorf("Cmix 0 failed to send broadcast: %+v", err) } wg.Wait() -} +}*/ diff --git a/broadcast/utils_test.go b/broadcast/utils_test.go index 5ed5d22f099783ed5d47bdfff84451af96dc5378..ee1dfebc264c208e037bcbd45385bac6160fa154 100644 --- a/broadcast/utils_test.go +++ b/broadcast/utils_test.go @@ -8,33 +8,21 @@ package broadcast import ( + "sync" + "time" + + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/cmix/identity/receptionID" "gitlab.com/elixxir/client/cmix/message" "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/primitives/format" - "gitlab.com/xx_network/crypto/signature/rsa" - "gitlab.com/xx_network/primitives/id" - "gitlab.com/xx_network/primitives/id/ephemeral" - "math/rand" - "sync" - "testing" - "time" ) -// newRsaPubKey generates a new random RSA public key for testing. -func newRsaPubKey(seed int64, t *testing.T) *rsa.PublicKey { - prng := rand.New(rand.NewSource(seed)) - privKey, err := rsa.GenerateKey(prng, 64) - if err != nil { - t.Errorf("Failed to generate new RSA key: %+v", err) - } - - return privKey.GetPublic() -} - //////////////////////////////////////////////////////////////////////////////// -// Mock cMix // +// Mock cMix // //////////////////////////////////////////////////////////////////////////////// type mockCmixHandler struct { @@ -56,7 +44,7 @@ type mockCmix struct { func newMockCmix(handler *mockCmixHandler) *mockCmix { return &mockCmix{ - numPrimeBytes: 4096, + numPrimeBytes: 4096 / 8, health: true, handler: handler, } @@ -66,6 +54,30 @@ func (m *mockCmix) GetMaxMessageLength() int { return format.NewMessage(m.numPrimeBytes).ContentsSize() } +func (m *mockCmix) SendWithAssembler(recipient *id.ID, + assembler cmix.MessageAssembler, _ cmix.CMIXParams) ( + rounds.Round, ephemeral.Id, error) { + + fingerprint, service, payload, mac, err := assembler(42) + if err != nil { + panic(err) + } + + msg := format.NewMessage(m.numPrimeBytes) + msg.SetContents(payload) + msg.SetMac(mac) + msg.SetKeyFP(fingerprint) + + m.handler.Lock() + defer m.handler.Unlock() + + for _, p := range m.handler.processorMap[*recipient][service.Tag] { + p.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{}) + } + + return rounds.Round{}, ephemeral.Id{}, nil +} + func (m *mockCmix) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, _ cmix.CMIXParams) ( id.Round, ephemeral.Id, error) { diff --git a/channels/adminListener.go b/channels/adminListener.go new file mode 100644 index 0000000000000000000000000000000000000000..86f80142ef5345a3aad346907d96940097a281a6 --- /dev/null +++ b/channels/adminListener.go @@ -0,0 +1,71 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "github.com/golang/protobuf/proto" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/primitives/id" + "time" +) + +// adminListener adheres to the [broadcast.ListenerFunc] interface and is used +// when admin messages are received on the channel. +type adminListener struct { + chID *id.ID + trigger triggerAdminEventFunc + checkSent messageReceiveFunc +} + +// Listen is called when a message is received for the admin listener +func (al *adminListener) Listen(payload []byte, + receptionID receptionID.EphemeralIdentity, round rounds.Round) { + // Get the message ID + msgID := channel.MakeMessageID(payload, al.chID) + + // Decode the message as a channel message + cm := &ChannelMessage{} + if err := proto.Unmarshal(payload, cm); err != nil { + jww.WARN.Printf("Failed to unmarshal Channel Message from Admin on "+ + "channel %s", al.chID) + return + } + + //check if we sent the message, ignore triggering if we sent + if al.checkSent(msgID, round) { + return + } + + /* CRYPTOGRAPHICALLY RELEVANT CHECKS */ + + // Check the round to ensure that the message is not a replay + if id.Round(cm.RoundID) != round.ID { + jww.WARN.Printf("The round message %s send on %s referenced "+ + "(%d) was not the same as the round the message was found on (%d)", + msgID, al.chID, cm.RoundID, round.ID) + return + } + + // Replace the timestamp on the message if it is outside of the + // allowable range + ts := vetTimestamp(time.Unix(0, cm.LocalTimestamp), + round.Timestamps[states.QUEUED], msgID) + + // Submit the message to the event model for listening + if uuid, err := al.trigger(al.chID, cm, ts, msgID, receptionID, + round, Delivered); err != nil { + jww.WARN.Printf("Error in passing off trigger for admin "+ + "message (UUID: %d): %+v", uuid, err) + } + + return +} diff --git a/channels/adminListener_test.go b/channels/adminListener_test.go new file mode 100644 index 0000000000000000000000000000000000000000..cb571d5724dcd7318d7ab4dec78bd2c0fe14866c --- /dev/null +++ b/channels/adminListener_test.go @@ -0,0 +1,231 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "bytes" + "gitlab.com/xx_network/primitives/netTime" + "testing" + "time" + + "github.com/golang/protobuf/proto" + + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/primitives/id" +) + +type triggerAdminEventDummy struct { + gotData bool + + chID *id.ID + cm *ChannelMessage + msgID cryptoChannel.MessageID + receptionID receptionID.EphemeralIdentity + round rounds.Round +} + +func (taed *triggerAdminEventDummy) triggerAdminEvent(chID *id.ID, + cm *ChannelMessage, ts time.Time, messageID cryptoChannel.MessageID, + receptionID receptionID.EphemeralIdentity, round rounds.Round, + status SentStatus) (uint64, error) { + taed.gotData = true + + taed.chID = chID + taed.cm = cm + taed.msgID = messageID + taed.receptionID = receptionID + taed.round = round + + return 0, nil +} + +// Tests the happy path. +func TestAdminListener_Listen(t *testing.T) { + + // Build inputs + chID := &id.ID{} + chID[0] = 1 + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + cm := &ChannelMessage{ + Lease: int64(time.Hour), + RoundID: uint64(r.ID), + PayloadType: 42, + Payload: []byte("blarg"), + } + + cmSerial, err := proto.Marshal(cm) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + msgID := cryptoChannel.MakeMessageID(cmSerial, chID) + + // Build the listener + dummy := &triggerAdminEventDummy{} + + al := adminListener{ + chID: chID, + trigger: dummy.triggerAdminEvent, + checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false }, + } + + // Call the listener + al.Listen(cmSerial, receptionID.EphemeralIdentity{}, r) + + // Check the results + if !dummy.gotData { + t.Fatalf("No data returned after valid listen") + } + + if !dummy.chID.Cmp(chID) { + t.Errorf("Channel ID not correct: %s vs %s", dummy.chID, chID) + } + + if !bytes.Equal(cm.Payload, dummy.cm.Payload) { + t.Errorf("payload not correct: %s vs %s", cm.Payload, + dummy.cm.Payload) + } + + if !msgID.Equals(dummy.msgID) { + t.Errorf("messageIDs not correct: %s vs %s", msgID, + dummy.msgID) + } + + if r.ID != dummy.round.ID { + t.Errorf("rounds not correct: %s vs %s", r.ID, + dummy.round.ID) + } +} + +// Tests that the message is rejected when the round it came on doesn't match +// the round in the channel message. +func TestAdminListener_Listen_BadRound(t *testing.T) { + + // build inputs + chID := &id.ID{} + chID[0] = 1 + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + cm := &ChannelMessage{ + Lease: int64(time.Hour), + // Different from the round above + RoundID: 69, + PayloadType: 42, + Payload: []byte("blarg"), + } + + cmSerial, err := proto.Marshal(cm) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + // Build the listener + dummy := &triggerAdminEventDummy{} + + al := adminListener{ + chID: chID, + trigger: dummy.triggerAdminEvent, + checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false }, + } + + // Call the listener + al.Listen(cmSerial, receptionID.EphemeralIdentity{}, r) + + // check the results + if dummy.gotData { + t.Fatalf("payload handled when it should have failed due to " + + "a round issue") + } + +} + +// Tests that the message is rejected when the channel message is malformed. +func TestAdminListener_Listen_BadChannelMessage(t *testing.T) { + + // Build inputs + chID := &id.ID{} + chID[0] = 1 + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + cmSerial := []byte("blarg") + + // Build the listener + dummy := &triggerAdminEventDummy{} + + al := adminListener{ + chID: chID, + trigger: dummy.triggerAdminEvent, + checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false }, + } + + // Call the listener + al.Listen(cmSerial, receptionID.EphemeralIdentity{}, r) + + // Check the results + if dummy.gotData { + t.Fatalf("payload handled when it should have failed due to " + + "a malformed channel message") + } + +} + +// Tests that the message is rejected when the sized broadcast message is +// malformed. +func TestAdminListener_Listen_BadSizedBroadcast(t *testing.T) { + + // build inputs + chID := &id.ID{} + chID[0] = 1 + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + cm := &ChannelMessage{ + Lease: int64(time.Hour), + // Different from the round above + RoundID: 69, + PayloadType: 42, + Payload: []byte("blarg"), + } + + cmSerial, err := proto.Marshal(cm) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + // Remove half the sized broadcast to make it malformed + chMsgSerialSized := cmSerial[:len(cmSerial)/2] + + // Build the listener + dummy := &triggerAdminEventDummy{} + + al := adminListener{ + chID: chID, + trigger: dummy.triggerAdminEvent, + checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false }, + } + + // Call the listener + al.Listen(chMsgSerialSized, receptionID.EphemeralIdentity{}, r) + + // Check the results + if dummy.gotData { + t.Fatalf("payload handled when it should have failed due to " + + "a malformed sized broadcast") + } +} diff --git a/channels/channelMessages.pb.go b/channels/channelMessages.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..1a7a414fe860fcfbf9439ed5422a3e550c5a55a0 --- /dev/null +++ b/channels/channelMessages.pb.go @@ -0,0 +1,320 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.15.6 +// source: channelMessages.proto + +package channels + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// ChannelMessage is transmitted by the channel. Effectively it is a command for +// the channel sent by a user with admin access of the channel. +type ChannelMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Lease is the length that this channel message will take effect. + Lease int64 `protobuf:"varint,1,opt,name=Lease,proto3" json:"Lease,omitempty"` + // The round this message was sent on. + RoundID uint64 `protobuf:"varint,2,opt,name=RoundID,proto3" json:"RoundID,omitempty"` + // The type the below payload is. This may be some form of channel command, + // such as BAN<username1>. + PayloadType uint32 `protobuf:"varint,3,opt,name=PayloadType,proto3" json:"PayloadType,omitempty"` + // Payload is the actual message payload. It will be processed differently + // based on the PayloadType. + Payload []byte `protobuf:"bytes,4,opt,name=Payload,proto3" json:"Payload,omitempty"` + // nickname is the name which the user is using for this message + // it will not be longer than 24 characters + Nickname string `protobuf:"bytes,5,opt,name=Nickname,proto3" json:"Nickname,omitempty"` + // Nonce is 32 bits of randomness to ensure that two messages in the same + // round with that have the same nickname, payload, and lease will not have + // the same message ID. + Nonce []byte `protobuf:"bytes,6,opt,name=Nonce,proto3" json:"Nonce,omitempty"` + // LocalTimestamp is the timestamp when the "send call" is made based upon the + // local clock. If this differs by more than 5 seconds +/- from when the round + // it sent on is queued, then a random mutation on the queued time (+/- 200ms) + // will be used by local clients instead + LocalTimestamp int64 `protobuf:"varint,7,opt,name=LocalTimestamp,proto3" json:"LocalTimestamp,omitempty"` +} + +func (x *ChannelMessage) Reset() { + *x = ChannelMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_channelMessages_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *ChannelMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ChannelMessage) ProtoMessage() {} + +func (x *ChannelMessage) ProtoReflect() protoreflect.Message { + mi := &file_channelMessages_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ChannelMessage.ProtoReflect.Descriptor instead. +func (*ChannelMessage) Descriptor() ([]byte, []int) { + return file_channelMessages_proto_rawDescGZIP(), []int{0} +} + +func (x *ChannelMessage) GetLease() int64 { + if x != nil { + return x.Lease + } + return 0 +} + +func (x *ChannelMessage) GetRoundID() uint64 { + if x != nil { + return x.RoundID + } + return 0 +} + +func (x *ChannelMessage) GetPayloadType() uint32 { + if x != nil { + return x.PayloadType + } + return 0 +} + +func (x *ChannelMessage) GetPayload() []byte { + if x != nil { + return x.Payload + } + return nil +} + +func (x *ChannelMessage) GetNickname() string { + if x != nil { + return x.Nickname + } + return "" +} + +func (x *ChannelMessage) GetNonce() []byte { + if x != nil { + return x.Nonce + } + return nil +} + +func (x *ChannelMessage) GetLocalTimestamp() int64 { + if x != nil { + return x.LocalTimestamp + } + return 0 +} + +// UserMessage is a message sent by a user who is a member within the channel. +type UserMessage struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + // Message contains the contents of the message. This is typically what the + // end-user has submitted to the channel. This is a serialization of the + // ChannelMessage. + Message []byte `protobuf:"bytes,1,opt,name=Message,proto3" json:"Message,omitempty"` + // Signature is the signature proving this message has been sent by the + // owner of this user's public key. + // + // Signature = Sig(User_ECCPublicKey, Message) + Signature []byte `protobuf:"bytes,3,opt,name=Signature,proto3" json:"Signature,omitempty"` + // ECCPublicKey is the user's EC Public key. This is provided by the + // network. + ECCPublicKey []byte `protobuf:"bytes,5,opt,name=ECCPublicKey,proto3" json:"ECCPublicKey,omitempty"` +} + +func (x *UserMessage) Reset() { + *x = UserMessage{} + if protoimpl.UnsafeEnabled { + mi := &file_channelMessages_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *UserMessage) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*UserMessage) ProtoMessage() {} + +func (x *UserMessage) ProtoReflect() protoreflect.Message { + mi := &file_channelMessages_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use UserMessage.ProtoReflect.Descriptor instead. +func (*UserMessage) Descriptor() ([]byte, []int) { + return file_channelMessages_proto_rawDescGZIP(), []int{1} +} + +func (x *UserMessage) GetMessage() []byte { + if x != nil { + return x.Message + } + return nil +} + +func (x *UserMessage) GetSignature() []byte { + if x != nil { + return x.Signature + } + return nil +} + +func (x *UserMessage) GetECCPublicKey() []byte { + if x != nil { + return x.ECCPublicKey + } + return nil +} + +var File_channelMessages_proto protoreflect.FileDescriptor + +var file_channelMessages_proto_rawDesc = []byte{ + 0x0a, 0x15, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, + 0x73, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, + 0x73, 0x22, 0xd6, 0x01, 0x0a, 0x0e, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x12, 0x14, 0x0a, 0x05, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x05, 0x4c, 0x65, 0x61, 0x73, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x52, 0x6f, + 0x75, 0x6e, 0x64, 0x49, 0x44, 0x18, 0x02, 0x20, 0x01, 0x28, 0x04, 0x52, 0x07, 0x52, 0x6f, 0x75, + 0x6e, 0x64, 0x49, 0x44, 0x12, 0x20, 0x0a, 0x0b, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, 0x54, + 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x0b, 0x50, 0x61, 0x79, 0x6c, 0x6f, + 0x61, 0x64, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, + 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x50, 0x61, 0x79, 0x6c, 0x6f, 0x61, 0x64, + 0x12, 0x1a, 0x0a, 0x08, 0x4e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x4e, 0x69, 0x63, 0x6b, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x14, 0x0a, 0x05, + 0x4e, 0x6f, 0x6e, 0x63, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x05, 0x4e, 0x6f, 0x6e, + 0x63, 0x65, 0x12, 0x26, 0x0a, 0x0e, 0x4c, 0x6f, 0x63, 0x61, 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x73, + 0x74, 0x61, 0x6d, 0x70, 0x18, 0x07, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x4c, 0x6f, 0x63, 0x61, + 0x6c, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x22, 0x69, 0x0a, 0x0b, 0x55, 0x73, + 0x65, 0x72, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x12, 0x18, 0x0a, 0x07, 0x4d, 0x65, 0x73, + 0x73, 0x61, 0x67, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x07, 0x4d, 0x65, 0x73, 0x73, + 0x61, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, 0x65, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x09, 0x53, 0x69, 0x67, 0x6e, 0x61, 0x74, 0x75, 0x72, + 0x65, 0x12, 0x22, 0x0a, 0x0c, 0x45, 0x43, 0x43, 0x50, 0x75, 0x62, 0x6c, 0x69, 0x63, 0x4b, 0x65, + 0x79, 0x18, 0x05, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x0c, 0x45, 0x43, 0x43, 0x50, 0x75, 0x62, 0x6c, + 0x69, 0x63, 0x4b, 0x65, 0x79, 0x42, 0x24, 0x5a, 0x22, 0x67, 0x69, 0x74, 0x6c, 0x61, 0x62, 0x2e, + 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x6c, 0x69, 0x78, 0x78, 0x69, 0x72, 0x2f, 0x63, 0x6c, 0x69, 0x65, + 0x6e, 0x74, 0x2f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x62, 0x06, 0x70, 0x72, 0x6f, + 0x74, 0x6f, 0x33, +} + +var ( + file_channelMessages_proto_rawDescOnce sync.Once + file_channelMessages_proto_rawDescData = file_channelMessages_proto_rawDesc +) + +func file_channelMessages_proto_rawDescGZIP() []byte { + file_channelMessages_proto_rawDescOnce.Do(func() { + file_channelMessages_proto_rawDescData = protoimpl.X.CompressGZIP(file_channelMessages_proto_rawDescData) + }) + return file_channelMessages_proto_rawDescData +} + +var file_channelMessages_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_channelMessages_proto_goTypes = []interface{}{ + (*ChannelMessage)(nil), // 0: channels.ChannelMessage + (*UserMessage)(nil), // 1: channels.UserMessage +} +var file_channelMessages_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_channelMessages_proto_init() } +func file_channelMessages_proto_init() { + if File_channelMessages_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_channelMessages_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*ChannelMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_channelMessages_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*UserMessage); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_channelMessages_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_channelMessages_proto_goTypes, + DependencyIndexes: file_channelMessages_proto_depIdxs, + MessageInfos: file_channelMessages_proto_msgTypes, + }.Build() + File_channelMessages_proto = out.File + file_channelMessages_proto_rawDesc = nil + file_channelMessages_proto_goTypes = nil + file_channelMessages_proto_depIdxs = nil +} diff --git a/channels/channelMessages.proto b/channels/channelMessages.proto new file mode 100644 index 0000000000000000000000000000000000000000..d89337d366a8cc4cfc27948f9e37e7570eddb254 --- /dev/null +++ b/channels/channelMessages.proto @@ -0,0 +1,63 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +syntax = "proto3"; + +option go_package = "gitlab.com/elixxir/client/channels"; + +package channels; + +// ChannelMessage is transmitted by the channel. Effectively it is a command for +// the channel sent by a user with admin access of the channel. +message ChannelMessage{ + // Lease is the length that this channel message will take effect. + int64 Lease = 1; + + // The round this message was sent on. + uint64 RoundID = 2; + + // The type the below payload is. This may be some form of channel command, + // such as BAN<username1>. + uint32 PayloadType = 3; + + // Payload is the actual message payload. It will be processed differently + // based on the PayloadType. + bytes Payload = 4; + + // nickname is the name which the user is using for this message + // it will not be longer than 24 characters + string Nickname = 5; + + // Nonce is 32 bits of randomness to ensure that two messages in the same + // round with that have the same nickname, payload, and lease will not have + // the same message ID. + bytes Nonce = 6; + + // LocalTimestamp is the timestamp when the "send call" is made based upon the + // local clock. If this differs by more than 5 seconds +/- from when the round + // it sent on is queued, then a random mutation on the queued time (+/- 200ms) + // will be used by local clients instead + int64 LocalTimestamp = 7; +} + +// UserMessage is a message sent by a user who is a member within the channel. +message UserMessage { + // Message contains the contents of the message. This is typically what the + // end-user has submitted to the channel. This is a serialization of the + // ChannelMessage. + bytes Message = 1; + + // Signature is the signature proving this message has been sent by the + // owner of this user's public key. + // + // Signature = Sig(User_ECCPublicKey, Message) + bytes Signature = 3; + + // ECCPublicKey is the user's EC Public key. This is provided by the + // network. + bytes ECCPublicKey = 5; +} \ No newline at end of file diff --git a/channels/compileProtobuf.sh b/channels/compileProtobuf.sh new file mode 100644 index 0000000000000000000000000000000000000000..dff697ecd9976fa8330d7c45052f7b3ca8eef382 --- /dev/null +++ b/channels/compileProtobuf.sh @@ -0,0 +1,15 @@ +#!/bin/bash +################################################################################ +## Copyright © 2022 xx foundation ## +## ## +## Use of this source code is governed by a license that can be found in the ## +## LICENSE file. ## +################################################################################ + +# This script will compile the Protobuf file to a Go file (pb.go). +# This is meant to be called from the top level of the repo. + +cd ./channels/ || return + +protoc --go_out=. --go_opt=paths=source_relative ./channelMessages.proto +protoc --go_out=. --go_opt=paths=source_relative ./text.proto diff --git a/channels/dummyNameServer.go b/channels/dummyNameServer.go new file mode 100644 index 0000000000000000000000000000000000000000..8c14e40279d8091bf34f29a5a881246715c27c34 --- /dev/null +++ b/channels/dummyNameServer.go @@ -0,0 +1,109 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "crypto/ed25519" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/crypto/channel" + "gitlab.com/xx_network/primitives/netTime" + "io" + "time" +) + +// NewDummyNameService returns a dummy object adhering to the name service +// This neither produces valid signatures nor validates passed signatures. +// +// THIS IS FOR DEVELOPMENT AND DEBUGGING PURPOSES ONLY. +func NewDummyNameService(username string, rng io.Reader) (NameService, error) { + jww.WARN.Printf("Creating a Dummy Name Service. This is for " + + "development and debugging only. It does not produce valid " + + "signatures or verify passed signatures. YOU SHOULD NEVER SEE THIS " + + "MESSAGE IN PRODUCTION") + + dns := &dummyNameService{ + username: username, + lease: netTime.Now().Add(35 * 24 * time.Hour), + } + + //generate the private key + var err error + dns.public, dns.private, err = ed25519.GenerateKey(rng) + if err != nil { + return nil, err + } + + //generate a dummy user discover identity to produce a validation signature + //just sign with our own key, it wont be evaluated anyhow + dns.validationSig = channel.SignChannelLease(dns.public, dns.username, + dns.lease, dns.private) + + return dns, nil +} + +// dummyNameService is a dummy NameService implementation. This is NOT meant +// for use in production +type dummyNameService struct { + private ed25519.PrivateKey + public ed25519.PublicKey + username string + validationSig []byte + lease time.Time +} + +// GetUsername returns the username for the dummyNameService. This is what was +// passed in through NewDummyNameService. +// +// THIS IS FOR DEVELOPMENT AND DEBUGGING PURPOSES ONLY. +func (dns *dummyNameService) GetUsername() string { + return dns.username +} + +// GetChannelValidationSignature will return the dummy validation signature +// generated in through the constructor, NewDummyNameService. +// +// THIS IS FOR DEVELOPMENT AND DEBUGGING PURPOSES ONLY. +func (dns *dummyNameService) GetChannelValidationSignature() ([]byte, time.Time) { + jww.WARN.Printf("GetChannelValidationSignature called on Dummy Name " + + "Service, dummy signature from a random key returned - identity not " + + "proven. YOU SHOULD NEVER SEE THIS MESSAGE IN PRODUCTION") + return dns.validationSig, dns.lease +} + +// GetChannelPubkey returns the ed25519.PublicKey generates in the constructor, +// NewDummyNameService. +func (dns *dummyNameService) GetChannelPubkey() ed25519.PublicKey { + return dns.public +} + +// SignChannelMessage will sign the passed in message using the +// dummyNameService's private key. +// +// THIS IS FOR DEVELOPMENT AND DEBUGGING PURPOSES ONLY. +func (dns *dummyNameService) SignChannelMessage(message []byte) ( + signature []byte, err error) { + jww.WARN.Printf("SignChannelMessage called on Dummy Name Service, " + + "signature from a random key - identity not proven. YOU SHOULD " + + "NEVER SEE THIS MESSAGE IN PRODUCTION") + sig := ed25519.Sign(dns.private, message) + return sig, nil +} + +// ValidateChannelMessage will always return true, indicating the the channel +// message is valid. This will ignore the passed in arguments. As a result, +// these values may be dummy or precanned. +// +// THIS IS FOR DEVELOPMENT AND DEBUGGING PURPOSES ONLY. +func (dns *dummyNameService) ValidateChannelMessage(username string, lease time.Time, + pubKey ed25519.PublicKey, authorIDSignature []byte) bool { + //ignore the authorIDSignature + jww.WARN.Printf("ValidateChannelMessage called on Dummy Name Service, " + + "no validation done - identity not validated. YOU SHOULD NEVER SEE " + + "THIS MESSAGE IN PRODUCTION") + return true +} diff --git a/channels/dummyNameServer_test.go b/channels/dummyNameServer_test.go new file mode 100644 index 0000000000000000000000000000000000000000..183da2abaa10cbd811048023c6447f61f3261ae4 --- /dev/null +++ b/channels/dummyNameServer_test.go @@ -0,0 +1,126 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "crypto/ed25519" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/primitives/netTime" + "testing" +) + +const numTests = 10 + +// Smoke test. +func TestNewDummyNameService(t *testing.T) { + rng := csprng.NewSystemRNG() + username := "floridaMan" + _, err := NewDummyNameService(username, rng) + if err != nil { + t.Fatalf("NewDummyNameService error: %+v", err) + } + +} + +// Smoke test. +func TestDummyNameService_GetUsername(t *testing.T) { + rng := csprng.NewSystemRNG() + username := "floridaMan" + ns, err := NewDummyNameService(username, rng) + if err != nil { + t.Fatalf("NewDummyNameService error: %+v", err) + } + + if username != ns.GetUsername() { + t.Fatalf("GetUsername did not return expected value."+ + "\nExpected: %s"+ + "\nReceived: %s", username, ns.GetUsername()) + } + +} + +// Smoke test. +func TestDummyNameService_SignChannelMessage(t *testing.T) { + rng := csprng.NewSystemRNG() + username := "floridaMan" + ns, err := NewDummyNameService(username, rng) + if err != nil { + t.Fatalf("NewDummyNameService error: %+v", err) + } + + message := []byte("the secret is in the sauce.") + + signature, err := ns.SignChannelMessage(message) + if err != nil { + t.Fatalf("SignChannelMessage error: %v", err) + } + + if len(signature) != ed25519.SignatureSize { + t.Errorf("DummyNameService's SignChannelMessage did not return a "+ + "signature of expected size, according to ed25519 specifications."+ + "\nExpected: %d"+ + "\nReceived: %d", ed25519.SignatureSize, len(signature)) + } + +} + +// Smoke test. +func TestDummyNameService_GetChannelValidationSignature(t *testing.T) { + rng := csprng.NewSystemRNG() + username := "floridaMan" + ns, err := NewDummyNameService(username, rng) + if err != nil { + t.Fatalf("NewDummyNameService error: %+v", err) + } + + validationSig, _ := ns.GetChannelValidationSignature() + + if len(validationSig) != ed25519.SignatureSize { + t.Errorf("DummyNameService's GetChannelValidationSignature did not "+ + "return a validation signature of expected size, according to "+ + "ed25519 specifications."+ + "\nExpected: %d"+ + "\nReceived: %d", ed25519.SignatureSize, len(validationSig)) + } + +} + +// Smoke test. +func TestDummyNameService_ValidateChannelMessage(t *testing.T) { + rng := csprng.NewSystemRNG() + username := "floridaMan" + ns, err := NewDummyNameService(username, rng) + if err != nil { + t.Fatalf("NewDummyNameService error: %+v", err) + } + + for i := 0; i < numTests; i++ { + if !ns.ValidateChannelMessage(username, netTime.Now(), nil, nil) { + t.Errorf("ValidateChannelMessage returned false. This should " + + "only ever return true.") + } + } +} + +// Smoke test. +func TestDummyNameService_GetChannelPubkey(t *testing.T) { + rng := csprng.NewSystemRNG() + username := "floridaMan" + ns, err := NewDummyNameService(username, rng) + if err != nil { + t.Fatalf("NewDummyNameService error: %+v", err) + } + + if len(ns.GetChannelPubkey()) != ed25519.PublicKeySize { + t.Errorf("DummyNameService's GetChannelPubkey did not "+ + "return a validation signature of expected size, according to "+ + "ed25519 specifications."+ + "\nExpected: %d"+ + "\nReceived: %d", ed25519.PublicKeySize, ns.GetChannelPubkey()) + } +} diff --git a/channels/emoji.go b/channels/emoji.go new file mode 100644 index 0000000000000000000000000000000000000000..71c7ae4f66dd6b96e812521056f44a548fbf74c6 --- /dev/null +++ b/channels/emoji.go @@ -0,0 +1,37 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "bytes" + "regexp" +) + +//based on emojis found at https://unicode.org/emoji/charts/full-emoji-list.html +const findEmoji = `[\xA9\xAE\x{2000}-\x{3300}\x{1F000}-\x{1FBFF}]` + +var compiledFindEmoji = regexp.MustCompile(findEmoji) + +// ValidateReaction checks that the reaction only contains a single emoji. +func ValidateReaction(reaction string) error { + + //make sure it is only only character + reactRunes := []rune(reaction) + if len(reactRunes) > 1 { + return InvalidReaction + } + + reader := bytes.NewReader([]byte(reaction)) + + // make sure it has emojis + if !compiledFindEmoji.MatchReader(reader) { + return InvalidReaction + } + + return nil +} diff --git a/channels/emoji_test.go b/channels/emoji_test.go new file mode 100644 index 0000000000000000000000000000000000000000..d320b05a6b0a404f23d3412aca8fb6a1fea393ef --- /dev/null +++ b/channels/emoji_test.go @@ -0,0 +1,35 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "testing" +) + +func TestValidateReaction(t *testing.T) { + + testReactions := []string{"ðŸ†", "😂", "â¤", "🤣", "ðŸ‘", "ðŸ˜", "ðŸ™", "😘", "🥰", + "ðŸ˜", "😊", "☺", "A", "b", "AA", "1", "ðŸ†ðŸ†", "ðŸ†A", "ðŸ‘ðŸ‘ðŸ‘", "ðŸ‘😘A", + "O", "\u0000", "\u0011", "\u001F", "\u007F", "\u0080", "\u008A", + "\u009F"} + + expected := []error{ + nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, nil, + InvalidReaction, InvalidReaction, InvalidReaction, InvalidReaction, + InvalidReaction, InvalidReaction, InvalidReaction, InvalidReaction, + InvalidReaction, InvalidReaction, InvalidReaction, InvalidReaction, + InvalidReaction, InvalidReaction, InvalidReaction, InvalidReaction} + + for i, r := range testReactions { + err := ValidateReaction(r) + if err != expected[i] { + t.Errorf("Got incorrect response for `%s` (%d): "+ + "`%s` vs `%s`", r, i, err, expected[i]) + } + } +} diff --git a/channels/errors.go b/channels/errors.go new file mode 100644 index 0000000000000000000000000000000000000000..319f065966a899f5a64c05b41eda84caa887fd02 --- /dev/null +++ b/channels/errors.go @@ -0,0 +1,25 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import "github.com/pkg/errors" + +var ( + ChannelAlreadyExistsErr = errors.New( + "the channel cannot be added because it already exists") + ChannelDoesNotExistsErr = errors.New( + "the channel cannot be found") + MessageTooLongErr = errors.New( + "the passed message is too long") + WrongPrivateKey = errors.New( + "the passed private key does not match the channel") + MessageTypeAlreadyRegistered = errors.New("the given message type has " + + "already been registered") + InvalidReaction = errors.New( + "The reaction is not valid, it must be a single emoji") +) diff --git a/channels/eventModel.go b/channels/eventModel.go new file mode 100644 index 0000000000000000000000000000000000000000..153a6bd68df07144219fc249332c5c3f0f316db2 --- /dev/null +++ b/channels/eventModel.go @@ -0,0 +1,381 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "crypto/ed25519" + "encoding/base64" + "errors" + "fmt" + "github.com/golang/protobuf/proto" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "sync" + "time" + + "gitlab.com/elixxir/client/cmix/rounds" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/xx_network/primitives/id" +) + +// AdminUsername defines the displayed username of admin messages, which are +// unique users for every channel defined by the channel's private key. +const AdminUsername = "Admin" + +type SentStatus uint8 + +const ( + Unsent SentStatus = iota + Sent + Delivered + Failed +) + +var AdminFakePubKey = ed25519.PublicKey{} + +// EventModel is an interface which an external party which uses the channels +// system passed an object which adheres to in order to get events on the +// channel. +type EventModel interface { + // JoinChannel is called whenever a channel is joined locally. + JoinChannel(channel *cryptoBroadcast.Channel) + + // LeaveChannel is called whenever a channel is left locally. + LeaveChannel(channelID *id.ID) + + // ReceiveMessage is called whenever a message is received on a given + // channel. It may be called multiple times on the same message. It is + // incumbent on the user of the API to filter such called by message ID. + // + // The API needs to return a UUID of the message that can be referenced at a + // later time. + // + // messageID, timestamp, and round are all nillable and may be updated based + // upon the UUID at a later date. A time of time.Time{} will be passed for a + // nilled timestamp. + // + // Nickname may be empty, in which case the UI is expected to display the + // codename. + // + // Message type is included in the call; it will always be Text (1) for this + // call, but it may be required in downstream databases. + ReceiveMessage(channelID *id.ID, messageID cryptoChannel.MessageID, + nickname, text string, pubKey ed25519.PublicKey, codeset uint8, + timestamp time.Time, lease time.Duration, round rounds.Round, + mType MessageType, status SentStatus) uint64 + + // ReceiveReply is called whenever a message is received that is a reply on + // a given channel. It may be called multiple times on the same message. It + // is incumbent on the user of the API to filter such called by message ID. + // + // Messages may arrive our of order, so a reply, in theory, can arrive + // before the initial message. As a result, it may be important to buffer + // replies. + // + // The API needs to return a UUID of the message that can be referenced at a + // later time. + // + // messageID, timestamp, and round are all nillable and may be updated based + // upon the UUID at a later date. A time of time.Time{} will be passed for a + // nilled timestamp. + // + // Nickname may be empty, in which case the UI is expected to display the + // codename. + // + // Message type is included in the call; it will always be Text (1) for this + // call, but it may be required in downstream databases. + ReceiveReply(channelID *id.ID, messageID cryptoChannel.MessageID, + reactionTo cryptoChannel.MessageID, nickname, text string, + pubKey ed25519.PublicKey, codeset uint8, timestamp time.Time, + lease time.Duration, round rounds.Round, mType MessageType, + status SentStatus) uint64 + + // ReceiveReaction is called whenever a reaction to a message is received on + // a given channel. It may be called multiple times on the same reaction. It + // is incumbent on the user of the API to filter such called by message ID. + // + // Messages may arrive our of order, so a reply, in theory, can arrive + // before the initial message. As a result, it may be important to buffer + // replies. + // + // The API needs to return a UUID of the message that can be referenced at a + // later time. + // + // messageID, timestamp, and round are all nillable and may be updated based + // upon the UUID at a later date. A time of time.Time{} will be passed for a + // nilled timestamp. + // + // Nickname may be empty, in which case the UI is expected to display the + // codename. + // + // Message type is included in the call; it will always be Text (1) for this + // call, but it may be required in downstream databases. + ReceiveReaction(channelID *id.ID, messageID cryptoChannel.MessageID, + reactionTo cryptoChannel.MessageID, nickname, reaction string, + pubKey ed25519.PublicKey, codeset uint8, timestamp time.Time, + lease time.Duration, round rounds.Round, mType MessageType, + status SentStatus) uint64 + + // UpdateSentStatus is called whenever the sent status of a message has + // changed. + // + // messageID, timestamp, and round are all nillable and may be updated based + // upon the UUID at a later date. A time of time.Time{} will be passed for a + // nilled timestamp. If a nil value is passed, make no update. + UpdateSentStatus(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) + + // unimplemented + // IgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) + // UnIgnoreMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) + // PinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID, end time.Time) + // UnPinMessage(ChannelID *id.ID, MessageID cryptoChannel.MessageID) +} + +// MessageTypeReceiveMessage defines handlers for messages of various message +// types. Default ones for Text, Reaction, and AdminText. +// +// A unique UUID must be returned by which the message can be referenced later +// via [EventModel.UpdateSentStatus]. +// +// It must return a unique UUID for the message by which it can be referenced +// later. +type MessageTypeReceiveMessage func(channelID *id.ID, + messageID cryptoChannel.MessageID, messageType MessageType, + nickname string, content []byte, pubKey ed25519.PublicKey, codeset uint8, + timestamp time.Time, lease time.Duration, round rounds.Round, + status SentStatus) uint64 + +// updateStatusFunc is a function type for EventModel.UpdateSentStatus so it can +// be mocked for testing where used. +type updateStatusFunc func(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) + +// events is an internal structure that processes events and stores the handlers +// for those events. +type events struct { + model EventModel + registered map[MessageType]MessageTypeReceiveMessage + mux sync.RWMutex +} + +// initEvents initializes the event model and registers default message type +// handlers. +func initEvents(model EventModel) *events { + e := &events{ + model: model, + registered: make(map[MessageType]MessageTypeReceiveMessage), + mux: sync.RWMutex{}, + } + + // set up default message types + e.registered[Text] = e.receiveTextMessage + e.registered[AdminText] = e.receiveTextMessage + e.registered[Reaction] = e.receiveReaction + return e +} + +// RegisterReceiveHandler is used to register handlers for non default message +// types s they can be processed by modules. It is important that such modules +// sync up with the event model implementation. +// +// There can only be one handler per message type, and this will return an error +// on a multiple registration. +func (e *events) RegisterReceiveHandler(messageType MessageType, + listener MessageTypeReceiveMessage) error { + e.mux.Lock() + defer e.mux.Unlock() + + // check if the type is already registered + if _, exists := e.registered[messageType]; exists { + return MessageTypeAlreadyRegistered + } + + // register the message type + e.registered[messageType] = listener + jww.INFO.Printf("Registered Listener for Message Type %s", messageType) + return nil +} + +type triggerEventFunc func(chID *id.ID, umi *userMessageInternal, ts time.Time, + receptionID receptionID.EphemeralIdentity, round rounds.Round, + status SentStatus) (uint64, error) + +// triggerEvent is an internal function that is used to trigger message +// reception on a message received from a user (symmetric encryption). +// +// It will call the appropriate MessageTypeHandler assuming one exists. +func (e *events) triggerEvent(chID *id.ID, umi *userMessageInternal, + ts time.Time, _ receptionID.EphemeralIdentity, round rounds.Round, + status SentStatus) (uint64, error) { + um := umi.GetUserMessage() + cm := umi.GetChannelMessage() + messageType := MessageType(cm.PayloadType) + + // Check if the type is already registered + e.mux.RLock() + listener, exists := e.registered[messageType] + e.mux.RUnlock() + if !exists { + errStr := fmt.Sprintf("Received message from %x on channel %s in "+ + "round %d which could not be handled due to unregistered message "+ + "type %s; Contents: %v", um.ECCPublicKey, chID, round.ID, messageType, + cm.Payload) + jww.WARN.Printf(errStr) + return 0, errors.New(errStr) + } + + // Call the listener. This is already in an instanced event, no new thread needed. + uuid := listener(chID, umi.GetMessageID(), messageType, cm.Nickname, cm.Payload, + um.ECCPublicKey, 0, ts, time.Duration(cm.Lease), + round, status) + return uuid, nil +} + +type triggerAdminEventFunc func(chID *id.ID, cm *ChannelMessage, ts time.Time, + messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) + +// triggerAdminEvent is an internal function that is used to trigger message +// reception on a message received from the admin (asymmetric encryption). +// +// It will call the appropriate MessageTypeHandler assuming one exists. +func (e *events) triggerAdminEvent(chID *id.ID, cm *ChannelMessage, + ts time.Time, messageID cryptoChannel.MessageID, + _ receptionID.EphemeralIdentity, round rounds.Round, status SentStatus) ( + uint64, error) { + messageType := MessageType(cm.PayloadType) + + // check if the type is already registered + e.mux.RLock() + listener, exists := e.registered[messageType] + e.mux.RUnlock() + if !exists { + errStr := fmt.Sprintf("Received Admin message from %s on channel %s in "+ + "round %d which could not be handled due to unregistered message "+ + "type %s; Contents: %v", AdminUsername, chID, round.ID, messageType, + cm.Payload) + jww.WARN.Printf(errStr) + return 0, errors.New(errStr) + } + + // Call the listener. This is already in an instanced event, no new thread needed. + uuid := listener(chID, messageID, messageType, AdminUsername, cm.Payload, + AdminFakePubKey, 0, ts, + time.Duration(cm.Lease), round, status) + return uuid, nil +} + +// receiveTextMessage is the internal function that handles the reception of +// text messages. It handles both messages and replies and calls the correct +// function on the event model. +// +// If the message has a reply, but it is malformed, it will drop the reply and +// write to the log. +func (e *events) receiveTextMessage(channelID *id.ID, + messageID cryptoChannel.MessageID, messageType MessageType, + nickname string, content []byte, pubKey ed25519.PublicKey, codeset uint8, + timestamp time.Time, lease time.Duration, round rounds.Round, + status SentStatus) uint64 { + txt := &CMIXChannelText{} + + if err := proto.Unmarshal(content, txt); err != nil { + jww.ERROR.Printf("Failed to text unmarshal message %s from %x on "+ + "channel %s, type %s, ts: %s, lease: %s, round: %d: %+v", + messageID, pubKey, channelID, messageType, timestamp, lease, + round.ID, err) + return 0 + } + + if txt.ReplyMessageID != nil { + + if len(txt.ReplyMessageID) == cryptoChannel.MessageIDLen { + var replyTo cryptoChannel.MessageID + copy(replyTo[:], txt.ReplyMessageID) + tag := makeChaDebugTag(channelID, pubKey, content, SendReplyTag) + jww.INFO.Printf("[%s]Channels - Received reply from %s "+ + "to %s on %s", tag, base64.StdEncoding.EncodeToString(pubKey), + base64.StdEncoding.EncodeToString(txt.ReplyMessageID), + channelID) + return e.model.ReceiveReply(channelID, messageID, replyTo, + nickname, txt.Text, pubKey, codeset, timestamp, lease, round, Text, status) + + } else { + jww.ERROR.Printf("Failed process reply to for message %s from "+ + "public key %v (codeset %d) on channel %s, type %s, ts: %s, "+ + "lease: %s, round: %d, returning without reply", + messageID, pubKey, codeset, channelID, messageType, timestamp, + lease, round.ID) + // Still process the message, but drop the reply because it is + // malformed + } + } + + tag := makeChaDebugTag(channelID, pubKey, content, SendMessageTag) + jww.INFO.Printf("[%s]Channels - Received message from %s "+ + "to %s on %s", tag, base64.StdEncoding.EncodeToString(pubKey), + base64.StdEncoding.EncodeToString(txt.ReplyMessageID), + channelID) + + return e.model.ReceiveMessage(channelID, messageID, nickname, txt.Text, pubKey, codeset, + timestamp, lease, round, Text, status) +} + +// receiveReaction is the internal function that handles the reception of +// Reactions. +// +// It does edge checking to ensure the received reaction is just a single emoji. +// If the received reaction is not, the reaction is dropped. +// If the messageID for the message the reaction is to is malformed, the +// reaction is dropped. +func (e *events) receiveReaction(channelID *id.ID, + messageID cryptoChannel.MessageID, messageType MessageType, + nickname string, content []byte, pubKey ed25519.PublicKey, codeset uint8, + timestamp time.Time, lease time.Duration, round rounds.Round, + status SentStatus) uint64 { + react := &CMIXChannelReaction{} + if err := proto.Unmarshal(content, react); err != nil { + jww.ERROR.Printf("Failed to text unmarshal message %s from %x on "+ + "channel %s, type %s, ts: %s, lease: %s, round: %d: %+v", + messageID, pubKey, channelID, messageType, timestamp, lease, + round.ID, err) + return 0 + } + + // check that the reaction is a single emoji and ignore if it isn't + if err := ValidateReaction(react.Reaction); err != nil { + jww.ERROR.Printf("Failed process reaction %s from %x on channel "+ + "%s, type %s, ts: %s, lease: %s, round: %d, due to malformed "+ + "reaction (%s), ignoring reaction", + messageID, pubKey, channelID, messageType, timestamp, lease, + round.ID, err) + return 0 + } + + if react.ReactionMessageID != nil && len(react.ReactionMessageID) == cryptoChannel.MessageIDLen { + var reactTo cryptoChannel.MessageID + copy(reactTo[:], react.ReactionMessageID) + + tag := makeChaDebugTag(channelID, pubKey, content, SendReactionTag) + jww.INFO.Printf("[%s]Channels - Received reaction from %s "+ + "to %s on %s", tag, base64.StdEncoding.EncodeToString(pubKey), + base64.StdEncoding.EncodeToString(react.ReactionMessageID), + channelID) + + return e.model.ReceiveReaction(channelID, messageID, reactTo, nickname, + react.Reaction, pubKey, codeset, timestamp, lease, round, Reaction, status) + } else { + jww.ERROR.Printf("Failed process reaction %s from public key %v "+ + "(codeset %d) on channel %s, type %s, ts: %s, lease: %s, "+ + "round: %d, reacting to invalid message, ignoring reaction", + messageID, pubKey, codeset, channelID, messageType, timestamp, + lease, round.ID) + } + return 0 +} diff --git a/channels/eventModel_test.go b/channels/eventModel_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4edf780c46076867789a298c224bfb6194e073b0 --- /dev/null +++ b/channels/eventModel_test.go @@ -0,0 +1,906 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "bytes" + "crypto/ed25519" + "fmt" + "github.com/golang/protobuf/proto" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "math/rand" + "reflect" + "runtime" + "testing" + "time" +) + +type eventReceive struct { + channelID *id.ID + messageID cryptoChannel.MessageID + reactionTo cryptoChannel.MessageID + nickname string + content []byte + timestamp time.Time + lease time.Duration + round rounds.Round +} + +type MockEvent struct { + uuid uint64 + eventReceive +} + +func (m *MockEvent) getUUID() uint64 { + old := m.uuid + m.uuid++ + return old +} + +func (*MockEvent) JoinChannel(*cryptoBroadcast.Channel) {} +func (*MockEvent) LeaveChannel(*id.ID) {} +func (m *MockEvent) ReceiveMessage(channelID *id.ID, + messageID cryptoChannel.MessageID, nickname, text string, + _ ed25519.PublicKey, _ uint8, timestamp time.Time, lease time.Duration, + round rounds.Round, _ MessageType, _ SentStatus) uint64 { + m.eventReceive = eventReceive{ + channelID: channelID, + messageID: messageID, + reactionTo: cryptoChannel.MessageID{}, + nickname: nickname, + content: []byte(text), + timestamp: timestamp, + lease: lease, + round: round, + } + return m.getUUID() +} +func (m *MockEvent) ReceiveReply(channelID *id.ID, + messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID, + nickname, text string, _ ed25519.PublicKey, _ uint8, timestamp time.Time, + lease time.Duration, round rounds.Round, _ MessageType, _ SentStatus) uint64 { + fmt.Println(reactionTo) + m.eventReceive = eventReceive{ + channelID: channelID, + messageID: messageID, + reactionTo: reactionTo, + nickname: nickname, + content: []byte(text), + timestamp: timestamp, + lease: lease, + round: round, + } + return m.getUUID() +} +func (m *MockEvent) ReceiveReaction(channelID *id.ID, + messageID cryptoChannel.MessageID, reactionTo cryptoChannel.MessageID, + nickname, reaction string, _ ed25519.PublicKey, _ uint8, timestamp time.Time, + lease time.Duration, round rounds.Round, _ MessageType, _ SentStatus) uint64 { + m.eventReceive = eventReceive{ + channelID: channelID, + messageID: messageID, + reactionTo: reactionTo, + nickname: nickname, + content: []byte(reaction), + timestamp: timestamp, + lease: lease, + round: round, + } + return m.getUUID() +} + +func (m *MockEvent) UpdateSentStatus(uint64, cryptoChannel.MessageID, + time.Time, rounds.Round, SentStatus) { + // TODO implement me + panic("implement me") +} + +func Test_initEvents(t *testing.T) { + + me := &MockEvent{} + + e := initEvents(me) + + // verify the model is registered + if e.model != me { + t.Errorf("Event model is not registered") + } + + // check registered channels was created + if e.registered == nil { + t.Fatalf("Registered handlers is not registered") + } + + // check that all the default callbacks are registered + if len(e.registered) != 3 { + t.Errorf("The correct number of default handlers are not "+ + "registered; %d vs %d", len(e.registered), 3) + // If this fails, is means the default handlers have changed. edit the + // number here and add tests below. be suspicious if it goes down. + } + + if getFuncName(e.registered[Text]) != getFuncName(e.receiveTextMessage) { + t.Errorf("Text does not have recieveTextMessageRegistred") + } + + if getFuncName(e.registered[AdminText]) != getFuncName(e.receiveTextMessage) { + t.Errorf("AdminText does not have recieveTextMessageRegistred") + } + + if getFuncName(e.registered[Reaction]) != getFuncName(e.receiveReaction) { + t.Errorf("Reaction does not have recieveReaction") + } +} + +func TestEvents_RegisterReceiveHandler(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + // Test that a new reception handler can be registered. + mt := MessageType(42) + err := e.RegisterReceiveHandler(mt, e.receiveReaction) + if err != nil { + t.Fatalf("Failed to register '%s' when it should be "+ + "sucesfull: %+v", mt, err) + } + + // check that it is written + returnedHandler, exists := e.registered[mt] + if !exists { + t.Fatalf("Failed to get handler '%s' after registration", mt) + } + + // check that the correct function is written + if getFuncName(e.receiveReaction) != getFuncName(returnedHandler) { + t.Fatalf("Failed to get correct handler for '%s' after "+ + "registration, %s vs %s", mt, getFuncName(e.receiveReaction), + getFuncName(returnedHandler)) + } + + // test that writing to the same receive handler fails + err = e.RegisterReceiveHandler(mt, e.receiveTextMessage) + if err == nil { + t.Fatalf("Failed to register '%s' when it should be "+ + "sucesfull: %+v", mt, err) + } else if err != MessageTypeAlreadyRegistered { + t.Fatalf("Wrong error returned when reregierting message "+ + "tyle '%s': %+v", mt, err) + } + + // check that it is still written + returnedHandler, exists = e.registered[mt] + if !exists { + t.Fatalf("Failed to get handler '%s' after second "+ + "registration", mt) + } + + // check that the correct function is written + if getFuncName(e.receiveReaction) != getFuncName(returnedHandler) { + t.Fatalf("Failed to get correct handler for '%s' after "+ + "second registration, %s vs %s", mt, getFuncName(e.receiveReaction), + getFuncName(returnedHandler)) + } +} + +type dummyMessageTypeHandler struct { + triggered bool + channelID *id.ID + messageID cryptoChannel.MessageID + messageType MessageType + nickname string + content []byte + timestamp time.Time + lease time.Duration + round rounds.Round +} + +func (dmth *dummyMessageTypeHandler) dummyMessageTypeReceiveMessage( + channelID *id.ID, messageID cryptoChannel.MessageID, + messageType MessageType, nickname string, content []byte, + _ ed25519.PublicKey, _ uint8, timestamp time.Time, lease time.Duration, + round rounds.Round, _ SentStatus) uint64 { + dmth.triggered = true + dmth.channelID = channelID + dmth.messageID = messageID + dmth.messageType = messageType + dmth.nickname = nickname + dmth.content = content + dmth.timestamp = timestamp + dmth.lease = lease + dmth.round = round + return rand.Uint64() +} + +func TestEvents_triggerEvents(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + dummy := &dummyMessageTypeHandler{} + + // register the handler + mt := MessageType(42) + err := e.RegisterReceiveHandler(mt, dummy.dummyMessageTypeReceiveMessage) + if err != nil { + t.Fatalf("Error on registration, should not have happened: "+ + "%+v", err) + } + + // craft the input for the event + chID := &id.ID{} + chID[0] = 1 + + umi, _, _ := builtTestUMI(t, mt) + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + // call the trigger + _, err = e.triggerEvent(chID, umi, netTime.Now(), receptionID.EphemeralIdentity{}, r, Delivered) + if err != nil { + t.Fatalf(err.Error()) + } + // check that the event was triggered + if !dummy.triggered { + t.Errorf("The event was not triggered") + } + + // check the data is stored in the dummy + if !dummy.channelID.Cmp(chID) { + t.Errorf("The channel IDs do not match %s vs %s", + dummy.channelID, chID) + } + + if !dummy.messageID.Equals(umi.GetMessageID()) { + t.Errorf("The message IDs do not match %s vs %s", + dummy.messageID, umi.GetMessageID()) + } + + if dummy.messageType != mt { + t.Errorf("The message types do not match %s vs %s", + dummy.messageType, mt) + } + + if dummy.nickname != umi.channelMessage.Nickname { + t.Errorf("The usernames do not match %s vs %s", + dummy.nickname, umi.channelMessage.Nickname) + } + + if !bytes.Equal(dummy.content, umi.GetChannelMessage().Payload) { + t.Errorf("The payloads do not match %s vs %s", + dummy.content, umi.GetChannelMessage().Payload) + } + + if !withinMutationWindow(r.Timestamps[states.QUEUED], dummy.timestamp) { + t.Errorf("The timestamps do not match %s vs %s", + dummy.timestamp, r.Timestamps[states.QUEUED]) + } + + if dummy.lease != time.Duration(umi.GetChannelMessage().Lease) { + t.Errorf("The messge lease durations do not match %s vs %s", + dummy.lease, time.Duration(umi.GetChannelMessage().Lease)) + } + + if dummy.round.ID != r.ID { + t.Errorf("The messge round does not match %s vs %s", + dummy.round.ID, r.ID) + } +} + +func TestEvents_triggerEvents_noChannel(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + dummy := &dummyMessageTypeHandler{} + + // skip handler registration + mt := MessageType(1) + + // craft the input for the event + chID := &id.ID{} + chID[0] = 1 + + umi, _, _ := builtTestUMI(t, mt) + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + // call the trigger + _, err := e.triggerEvent(chID, umi, netTime.Now(), receptionID.EphemeralIdentity{}, r, Delivered) + if err != nil { + t.Fatalf(err.Error()) + } + + // check that the event was triggered + if dummy.triggered { + t.Errorf("The event was triggered when it is unregistered") + } +} + +func TestEvents_triggerAdminEvents(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + dummy := &dummyMessageTypeHandler{} + + // register the handler + mt := MessageType(42) + err := e.RegisterReceiveHandler(mt, dummy.dummyMessageTypeReceiveMessage) + if err != nil { + t.Fatalf("Error on registration, should not have happened: "+ + "%+v", err) + } + + // craft the input for the event + chID := &id.ID{} + chID[0] = 1 + + u, _, cm := builtTestUMI(t, mt) + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + msgID := cryptoChannel.MakeMessageID(u.userMessage.Message, chID) + + // call the trigger + _, err = e.triggerAdminEvent(chID, cm, netTime.Now(), msgID, receptionID.EphemeralIdentity{}, r, + Delivered) + if err != nil { + t.Fatalf(err.Error()) + } + + // check that the event was triggered + if !dummy.triggered { + t.Errorf("The admin event was not triggered") + } + + // check the data is stored in the dummy + if !dummy.channelID.Cmp(chID) { + t.Errorf("The channel IDs do not match %s vs %s", + dummy.channelID, chID) + } + + if !dummy.messageID.Equals(msgID) { + t.Errorf("The message IDs do not match %s vs %s", + dummy.messageID, msgID) + } + + if dummy.messageType != mt { + t.Errorf("The message types do not match %s vs %s", + dummy.messageType, mt) + } + + if dummy.nickname != AdminUsername { + t.Errorf("The usernames do not match %s vs %s", + dummy.nickname, AdminUsername) + } + + if !bytes.Equal(dummy.content, cm.Payload) { + t.Errorf("The payloads do not match %s vs %s", + dummy.content, cm.Payload) + } + + if !withinMutationWindow(r.Timestamps[states.QUEUED], dummy.timestamp) { + t.Errorf("The timestamps do not match %s vs %s", + dummy.timestamp, r.Timestamps[states.QUEUED]) + } + + if dummy.lease != time.Duration(cm.Lease) { + t.Errorf("The messge lease durations do not match %s vs %s", + dummy.lease, time.Duration(cm.Lease)) + } + + if dummy.round.ID != r.ID { + t.Errorf("The messge round does not match %s vs %s", + dummy.round.ID, r.ID) + } +} + +func TestEvents_triggerAdminEvents_noChannel(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + dummy := &dummyMessageTypeHandler{} + + mt := MessageType(1) + // skip handler registration + + // craft the input for the event + chID := &id.ID{} + chID[0] = 1 + + u, _, cm := builtTestUMI(t, mt) + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + msgID := cryptoChannel.MakeMessageID(u.userMessage.Message, chID) + + // call the trigger + _, err := e.triggerAdminEvent(chID, cm, netTime.Now(), msgID, receptionID.EphemeralIdentity{}, r, + Delivered) + if err != nil { + t.Fatalf(err.Error()) + } + + // check that the event was triggered + if dummy.triggered { + t.Errorf("The admin event was triggered when unregistered") + } +} + +func TestEvents_receiveTextMessage_Message(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + // craft the input for the event + chID := &id.ID{} + chID[0] = 1 + + textPayload := &CMIXChannelText{ + Version: 0, + Text: "They Don't Think It Be Like It Is, But It Do", + ReplyMessageID: nil, + } + + textMarshaled, err := proto.Marshal(textPayload) + if err != nil { + t.Fatalf("failed to marshael the message proto: %+v", err) + } + + msgID := cryptoChannel.MakeMessageID(textMarshaled, chID) + + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + senderNickname := "Alice" + ts := netTime.Now() + + lease := 69 * time.Minute + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + // call the handler + e.receiveTextMessage(chID, msgID, 0, senderNickname, + textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered) + + // check the results on the model + if !me.eventReceive.channelID.Cmp(chID) { + t.Errorf("Channel ID did not propogate correctly, %s vs %s", + me.eventReceive.channelID, chID) + } + + if !me.eventReceive.messageID.Equals(msgID) { + t.Errorf("Message ID did not propogate correctly, %s vs %s", + me.eventReceive.messageID, msgID) + } + + if !me.eventReceive.reactionTo.Equals(cryptoChannel.MessageID{}) { + t.Errorf("Reaction ID is not blank, %s", + me.eventReceive.reactionTo) + } + + if me.eventReceive.nickname != senderNickname { + t.Errorf("SenderID propogate correctly, %s vs %s", + me.eventReceive.nickname, senderNickname) + } + + if me.eventReceive.timestamp != ts { + t.Errorf("Message timestamp did not propogate correctly, %s vs %s", + me.eventReceive.timestamp, ts) + } + + if me.eventReceive.lease != lease { + t.Errorf("Message lease did not propogate correctly, %s vs %s", + me.eventReceive.lease, lease) + } + + if me.eventReceive.round.ID != r.ID { + t.Errorf("Message round did not propogate correctly, %d vs %d", + me.eventReceive.round.ID, r.ID) + } +} + +func TestEvents_receiveTextMessage_Reply(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + // craft the input for the event + chID := &id.ID{} + chID[0] = 1 + + replyMsgId := cryptoChannel.MakeMessageID([]byte("blarg"), chID) + + textPayload := &CMIXChannelText{ + Version: 0, + Text: "They Don't Think It Be Like It Is, But It Do", + ReplyMessageID: replyMsgId[:], + } + + textMarshaled, err := proto.Marshal(textPayload) + if err != nil { + t.Fatalf("failed to marshael the message proto: %+v", err) + } + + msgID := cryptoChannel.MakeMessageID(textMarshaled, chID) + + senderUsername := "Alice" + ts := netTime.Now() + + lease := 69 * time.Minute + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + // call the handler + e.receiveTextMessage(chID, msgID, Text, senderUsername, + textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered) + + // check the results on the model + if !me.eventReceive.channelID.Cmp(chID) { + t.Errorf("Channel ID did not propogate correctly, %s vs %s", + me.eventReceive.channelID, chID) + } + + if !me.eventReceive.messageID.Equals(msgID) { + t.Errorf("Message ID did not propogate correctly, %s vs %s", + me.eventReceive.messageID, msgID) + } + + if !me.eventReceive.reactionTo.Equals(replyMsgId) { + t.Errorf("Reaction ID is not equal to what was passed in, "+ + "%s vs %s", me.eventReceive.reactionTo, replyMsgId) + } + + if me.eventReceive.nickname != senderUsername { + t.Errorf("SenderID propogate correctly, %s vs %s", + me.eventReceive.nickname, senderUsername) + } + + if me.eventReceive.timestamp != ts { + t.Errorf("Message timestamp did not propogate correctly, "+ + "%s vs %s", me.eventReceive.timestamp, ts) + } + + if me.eventReceive.lease != lease { + t.Errorf("Message lease did not propogate correctly, %s vs %s", + me.eventReceive.lease, lease) + } + + if me.eventReceive.round.ID != r.ID { + t.Errorf("Message round did not propogate correctly, %d vs %d", + me.eventReceive.round.ID, r.ID) + } +} + +func TestEvents_receiveTextMessage_Reply_BadReply(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + // craft the input for the event + chID := &id.ID{} + chID[0] = 1 + + replyMsgId := []byte("blarg") + + textPayload := &CMIXChannelText{ + Version: 0, + Text: "They Don't Think It Be Like It Is, But It Do", + ReplyMessageID: replyMsgId[:], + } + + textMarshaled, err := proto.Marshal(textPayload) + if err != nil { + t.Fatalf("failed to marshael the message proto: %+v", err) + } + + msgID := cryptoChannel.MakeMessageID(textMarshaled, chID) + + senderUsername := "Alice" + ts := netTime.Now() + + lease := 69 * time.Minute + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + // call the handler + e.receiveTextMessage(chID, msgID, 0, senderUsername, + textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered) + + // check the results on the model + if !me.eventReceive.channelID.Cmp(chID) { + t.Errorf("Channel ID did not propogate correctly, %s vs %s", + me.eventReceive.channelID, chID) + } + + if !me.eventReceive.messageID.Equals(msgID) { + t.Errorf("Message ID did not propogate correctly, %s vs %s", + me.eventReceive.messageID, msgID) + } + + if !me.eventReceive.reactionTo.Equals(cryptoChannel.MessageID{}) { + t.Errorf("Reaction ID is not blank, %s", + me.eventReceive.reactionTo) + } + + if me.eventReceive.nickname != senderUsername { + t.Errorf("SenderID propogate correctly, %s vs %s", + me.eventReceive.nickname, senderUsername) + } + + if me.eventReceive.timestamp != ts { + t.Errorf("Message timestamp did not propogate correctly, "+ + "%s vs %s", me.eventReceive.timestamp, ts) + } + + if me.eventReceive.lease != lease { + t.Errorf("Message lease did not propogate correctly, %s vs %s", + me.eventReceive.lease, lease) + } + + if me.eventReceive.round.ID != r.ID { + t.Errorf("Message round did not propogate correctly, %d vs %d", + me.eventReceive.round.ID, r.ID) + } +} + +func TestEvents_receiveReaction(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + // craft the input for the event + chID := &id.ID{} + chID[0] = 1 + + replyMsgId := cryptoChannel.MakeMessageID([]byte("blarg"), chID) + + textPayload := &CMIXChannelReaction{ + Version: 0, + Reaction: "ðŸ†", + ReactionMessageID: replyMsgId[:], + } + + textMarshaled, err := proto.Marshal(textPayload) + if err != nil { + t.Fatalf("failed to marshael the message proto: %+v", err) + } + + msgID := cryptoChannel.MakeMessageID(textMarshaled, chID) + + senderUsername := "Alice" + ts := netTime.Now() + + lease := 69 * time.Minute + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + // call the handler + e.receiveReaction(chID, msgID, 0, senderUsername, + textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered) + + // check the results on the model + if !me.eventReceive.channelID.Cmp(chID) { + t.Errorf("Channel ID did not propogate correctly, %s vs %s", + me.eventReceive.channelID, chID) + } + + if !me.eventReceive.messageID.Equals(msgID) { + t.Errorf("Message ID did not propogate correctly, %s vs %s", + me.eventReceive.messageID, msgID) + } + + if !me.eventReceive.reactionTo.Equals(replyMsgId) { + t.Errorf("Reaction ID is not equal to what was passed in, "+ + "%s vs %s", me.eventReceive.reactionTo, replyMsgId) + } + + if me.eventReceive.nickname != senderUsername { + t.Errorf("SenderID propogate correctly, %s vs %s", + me.eventReceive.nickname, senderUsername) + } + + if me.eventReceive.timestamp != ts { + t.Errorf("Message timestamp did not propogate correctly, "+ + "%s vs %s", me.eventReceive.timestamp, ts) + } + + if me.eventReceive.lease != lease { + t.Errorf("Message lease did not propogate correctly, %s vs %s", + me.eventReceive.lease, lease) + } + + if me.eventReceive.round.ID != r.ID { + t.Errorf("Message round did not propogate correctly, %d vs %d", + me.eventReceive.round.ID, r.ID) + } +} + +func TestEvents_receiveReaction_InvalidReactionMessageID(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + // craft the input for the event + chID := &id.ID{} + chID[0] = 1 + + replyMsgId := []byte("blarg") + + textPayload := &CMIXChannelReaction{ + Version: 0, + Reaction: "ðŸ†", + ReactionMessageID: replyMsgId[:], + } + + textMarshaled, err := proto.Marshal(textPayload) + if err != nil { + t.Fatalf("failed to marshael the message proto: %+v", err) + } + + msgID := cryptoChannel.MakeMessageID(textMarshaled, chID) + + senderUsername := "Alice" + ts := netTime.Now() + + lease := 69 * time.Minute + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + // call the handler + e.receiveReaction(chID, msgID, 0, senderUsername, + textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered) + + // check the results on the model + if me.eventReceive.channelID != nil { + t.Errorf("Channel ID did propogated correctly when the reaction " + + "is bad") + } + + if me.eventReceive.messageID.Equals(msgID) { + t.Errorf("Message ID propogated correctly when the reaction is " + + "bad") + } + + if !me.eventReceive.reactionTo.Equals(cryptoChannel.MessageID{}) { + t.Errorf("Reaction ID propogated correctly when the reaction " + + "is bad") + } + + if me.eventReceive.nickname != "" { + t.Errorf("SenderID propogated correctly when the reaction " + + "is bad") + } + + if me.eventReceive.lease != 0 { + t.Errorf("Message lease propogated correctly when the " + + "reaction is bad") + } +} + +func TestEvents_receiveReaction_InvalidReactionContent(t *testing.T) { + me := &MockEvent{} + + e := initEvents(me) + + // craft the input for the event + chID := &id.ID{} + chID[0] = 1 + + replyMsgId := cryptoChannel.MakeMessageID([]byte("blarg"), chID) + + textPayload := &CMIXChannelReaction{ + Version: 0, + Reaction: "I'm not a reaction", + ReactionMessageID: replyMsgId[:], + } + + textMarshaled, err := proto.Marshal(textPayload) + if err != nil { + t.Fatalf("failed to marshael the message proto: %+v", err) + } + + msgID := cryptoChannel.MakeMessageID(textMarshaled, chID) + + senderUsername := "Alice" + ts := netTime.Now() + + lease := 69 * time.Minute + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + // call the handler + e.receiveReaction(chID, msgID, 0, senderUsername, + textMarshaled, pi.PubKey, pi.CodesetVersion, ts, lease, r, Delivered) + + // check the results on the model + if me.eventReceive.channelID != nil { + t.Errorf("Channel ID did propogated correctly when the reaction " + + "is bad") + } + + if me.eventReceive.messageID.Equals(msgID) { + t.Errorf("Message ID propogated correctly when the reaction is " + + "bad") + } + + if !me.eventReceive.reactionTo.Equals(cryptoChannel.MessageID{}) { + t.Errorf("Reaction ID propogated correctly when the reaction " + + "is bad") + } + + if me.eventReceive.nickname != "" { + t.Errorf("SenderID propogated correctly when the reaction " + + "is bad") + } + + if me.eventReceive.lease != 0 { + t.Errorf("Message lease propogated correctly when the " + + "reaction is bad") + } +} + +func getFuncName(i interface{}) string { + return runtime.FuncForPC(reflect.ValueOf(i).Pointer()).Name() +} diff --git a/channels/identityStore.go b/channels/identityStore.go new file mode 100644 index 0000000000000000000000000000000000000000..2432a86c8ee10449502319ae443e32a88c74a908 --- /dev/null +++ b/channels/identityStore.go @@ -0,0 +1,31 @@ +package channels + +import ( + "gitlab.com/elixxir/client/storage/versioned" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/xx_network/primitives/netTime" +) + +const ( + identityStoreStorageKey = "identityStoreStorageKey" + identityStoreStorageVersion = 0 +) + +func storeIdentity(kv *versioned.KV, ident cryptoChannel.PrivateIdentity) error { + data := ident.Marshal() + obj := &versioned.Object{ + Version: identityStoreStorageVersion, + Timestamp: netTime.Now(), + Data: data, + } + + return kv.Set(identityStoreStorageKey, obj) +} + +func loadIdentity(kv *versioned.KV) (cryptoChannel.PrivateIdentity, error) { + obj, err := kv.Get(identityStoreStorageKey, identityStoreStorageVersion) + if err != nil { + return cryptoChannel.PrivateIdentity{}, err + } + return cryptoChannel.UnmarshalPrivateIdentity(obj.Data) +} diff --git a/channels/identityStore_test.go b/channels/identityStore_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ec2d72f9b90d77accb42d757682fa83f333689f2 --- /dev/null +++ b/channels/identityStore_test.go @@ -0,0 +1,46 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "bytes" + "encoding/base64" + "gitlab.com/elixxir/client/storage/versioned" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/crypto/csprng" + "testing" +) + +func TestStoreLoadIdentity(t *testing.T) { + rng := &csprng.SystemRNG{} + privIdentity, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf("GenerateIdentity error: %+v", err) + } + + kv := versioned.NewKV(ekv.MakeMemstore()) + err = storeIdentity(kv, privIdentity) + if err != nil { + t.Fatalf("storeIdentity error: %+v", err) + } + + loadedIdentity, err := loadIdentity(kv) + if err != nil { + t.Fatalf("loadIdentity error: %+v", err) + } + + if !bytes.Equal(loadedIdentity.Marshal(), privIdentity.Marshal()) { + t.Fatalf("Failed to load private identity."+ + "\nExpected: %s"+ + "\nReceived: %s", + base64.StdEncoding.EncodeToString(privIdentity.Marshal()), + base64.StdEncoding.EncodeToString(loadedIdentity.Marshal())) + } + +} \ No newline at end of file diff --git a/channels/interface.go b/channels/interface.go new file mode 100644 index 0000000000000000000000000000000000000000..b7d5445ced2690ef80a77ca0b30f087f814de43f --- /dev/null +++ b/channels/interface.go @@ -0,0 +1,136 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "math" + "time" + + "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/rounds" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/rsa" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" +) + +// ValidForever is used as a validUntil lease when sending to denote the +// message or operation never expires. Note: A message relay must be +// present to enforce this otherwise things expire after 3 weeks due to +// network retention. +var ValidForever = time.Duration(math.MaxInt64) + +type Manager interface { + + // GetIdentity returns the public identity associated with this channel manager + GetIdentity() cryptoChannel.Identity + + // ExportPrivateIdentity encrypts and exports the private identity to a + // portable string. + ExportPrivateIdentity(password string) ([]byte, error) + + // GetStorageTag returns the tag at which this manager is store for loading + // it is derived from the public key + GetStorageTag() string + + // JoinChannel joins the given channel. It will fail if the channel has + // already been joined. + JoinChannel(channel *cryptoBroadcast.Channel) error + + // LeaveChannel leaves the given channel. It will return an error if the + // channel was not previously joined. + LeaveChannel(channelID *id.ID) error + + // SendGeneric is used to send a raw message over a channel. In general, it + // should be wrapped in a function which defines the wire protocol + // If the final message, before being sent over the wire, is too long, this will + // return an error. Due to the underlying encoding using compression, it isn't + // possible to define the largest payload that can be sent, but + // it will always be possible to send a payload of 802 bytes at minimum + // Them meaning of validUntil depends on the use case. + SendGeneric(channelID *id.ID, messageType MessageType, + msg []byte, validUntil time.Duration, params cmix.CMIXParams) ( + cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error) + + // SendAdminGeneric is used to send a raw message over a channel encrypted + // with admin keys, identifying it as sent by the admin. In general, it + // should be wrapped in a function which defines the wire protocol + // If the final message, before being sent over the wire, is too long, this will + // return an error. The message must be at most 510 bytes long. + SendAdminGeneric(privKey rsa.PrivateKey, channelID *id.ID, + messageType MessageType, msg []byte, validUntil time.Duration, + params cmix.CMIXParams) (cryptoChannel.MessageID, + rounds.Round, ephemeral.Id, error) + + // SendMessage is used to send a formatted message over a channel. + // Due to the underlying encoding using compression, it isn't + // possible to define the largest payload that can be sent, but + // it will always be possible to send a payload of 798 bytes at minimum + // The message will auto delete validUntil after the round it is sent in, + // lasting forever if ValidForever is used + SendMessage(channelID *id.ID, msg string, validUntil time.Duration, + params cmix.CMIXParams) ( + cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error) + + // SendReply is used to send a formatted message over a channel. + // Due to the underlying encoding using compression, it isn't + // possible to define the largest payload that can be sent, but + // it will always be possible to send a payload of 766 bytes at minimum. + // If the message ID the reply is sent to doesnt exist, the other side will + // post the message as a normal message and not a reply. + // The message will auto delete validUntil after the round it is sent in, + // lasting forever if ValidForever is used + SendReply(channelID *id.ID, msg string, replyTo cryptoChannel.MessageID, + validUntil time.Duration, params cmix.CMIXParams) ( + cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error) + + // SendReaction is used to send a reaction to a message over a channel. The + // reaction must be a single emoji with no other characters, and will be + // rejected otherwise. + // + // Clients will drop the reaction if they do not recognize the reactTo + // message. + SendReaction(channelID *id.ID, reaction string, + reactTo cryptoChannel.MessageID, params cmix.CMIXParams) ( + cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error) + + // RegisterReceiveHandler is used to register handlers for non default + // message types so that they can be processed by modules. It is important + // that such modules sync up with the event model implementation. + // + // There can only be one handler per message type, and this will return an + // error on a multiple registration. + RegisterReceiveHandler(messageType MessageType, + listener MessageTypeReceiveMessage) error + + // GetChannels returns the IDs of all channels that have been joined. Use + // getChannelsUnsafe if you already have taken the mux. + GetChannels() []*id.ID + + // GetChannel returns the underlying cryptographic structure for a given + // channel. + GetChannel(chID *id.ID) (*cryptoBroadcast.Channel, error) + + // ReplayChannel replays all messages from the channel within the network's + // memory (~3 weeks) over the event model. It does this by wiping the + // underlying state tracking for message pickup for the channel, causing all + // messages to be re-retrieved from the network + ReplayChannel(chID *id.ID) error + + // SetNickname sets the nickname for a channel after checking that the + // nickname is valid using IsNicknameValid. + SetNickname(newNick string, ch *id.ID) error + + // DeleteNickname removes the nickname for a given channel, using the + // codename for that channel instead. + DeleteNickname(ch *id.ID) error + + // GetNickname returns the nickname for the given channel if it exists. + GetNickname(ch *id.ID) (nickname string, exists bool) +} diff --git a/channels/joinedChannel.go b/channels/joinedChannel.go new file mode 100644 index 0000000000000000000000000000000000000000..1b6aa53e7f9b2539b983c418eab52368d7c129c0 --- /dev/null +++ b/channels/joinedChannel.go @@ -0,0 +1,271 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "encoding/json" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/broadcast" + "gitlab.com/elixxir/client/storage/versioned" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" +) + +const ( + joinedChannelsVersion = 0 + joinedChannelsKey = "JoinedChannelsKey" + joinedChannelVersion = 0 + joinedChannelKey = "JoinedChannelKey-" +) + +// store stores the list of joined channels to disk while taking the read lock. +func (m *manager) store() error { + m.mux.RLock() + defer m.mux.RUnlock() + return m.storeUnsafe() +} + +// storeUnsafe stores the list of joined channels to disk without taking the +// read lock. It must be used by another function that has already taken the +// read lock. +func (m *manager) storeUnsafe() error { + channelsList := m.getChannelsUnsafe() + + data, err := json.Marshal(&channelsList) + if err != nil { + return err + } + + obj := &versioned.Object{ + Version: joinedChannelsVersion, + Timestamp: netTime.Now(), + Data: data, + } + + return m.kv.Set(joinedChannelsKey, obj) +} + +// loadChannels loads all currently joined channels from disk and registers them +// for message reception. +func (m *manager) loadChannels() { + obj, err := m.kv.Get(joinedChannelsKey, joinedChannelsVersion) + if !m.kv.Exists(err) { + m.channels = make(map[id.ID]*joinedChannel) + return + } else if err != nil { + jww.FATAL.Panicf("Failed to load channels: %+v", err) + } + + chList := make([]*id.ID, 0, len(m.channels)) + if err = json.Unmarshal(obj.Data, &chList); err != nil { + jww.FATAL.Panicf("Failed to load channels: %+v", err) + } + + chMap := make(map[id.ID]*joinedChannel) + + for i := range chList { + jc, err := loadJoinedChannel( + chList[i], m.kv, m.net, m.rng, m.events, m.broadcastMaker, + m.st.MessageReceive) + if err != nil { + jww.FATAL.Panicf("Failed to load channel %s: %+v", chList[i], err) + } + chMap[*chList[i]] = jc + } + + m.channels = chMap +} + +// addChannel adds a channel. +func (m *manager) addChannel(channel *cryptoBroadcast.Channel) error { + m.mux.Lock() + defer m.mux.Unlock() + if _, exists := m.channels[*channel.ReceptionID]; exists { + return ChannelAlreadyExistsErr + } + + b, err := m.broadcastMaker(channel, m.net, m.rng) + if err != nil { + return err + } + + jc := &joinedChannel{b} + if err = jc.Store(m.kv); err != nil { + go b.Stop() + return err + } + + m.channels[*jc.broadcast.Get().ReceptionID] = jc + + if err = m.storeUnsafe(); err != nil { + go b.Stop() + return err + } + + // Connect to listeners + err = b.RegisterListener((&userListener{ + chID: channel.ReceptionID, + trigger: m.events.triggerEvent, + checkSent: m.st.MessageReceive, + }).Listen, broadcast.Symmetric) + if err != nil { + return err + } + + err = b.RegisterListener((&adminListener{ + chID: channel.ReceptionID, + trigger: m.events.triggerAdminEvent, + checkSent: m.st.MessageReceive, + }).Listen, broadcast.RSAToPublic) + if err != nil { + return err + } + + return nil +} + +// removeChannel deletes the channel with the given ID from the channel list and +// stops it from broadcasting. Returns ChannelDoesNotExistsErr error if the +// channel does not exist. +func (m *manager) removeChannel(channelID *id.ID) error { + m.mux.Lock() + defer m.mux.Unlock() + + ch, exists := m.channels[*channelID] + if !exists { + return ChannelDoesNotExistsErr + } + + ch.broadcast.Stop() + + delete(m.channels, *channelID) + + err := m.storeUnsafe() + if err != nil { + return err + } + + return ch.delete(m.kv) +} + +// getChannel returns the given channel. Returns ChannelDoesNotExistsErr error +// if the channel does not exist. +func (m *manager) getChannel(channelID *id.ID) (*joinedChannel, error) { + m.mux.RLock() + defer m.mux.RUnlock() + + jc, exists := m.channels[*channelID] + if !exists { + return nil, ChannelDoesNotExistsErr + } + + return jc, nil +} + +// getChannelsUnsafe returns the IDs of all channels that have been joined. This +// function is unsafe because it does not take the mux; only use this function +// when under a lock. +func (m *manager) getChannelsUnsafe() []*id.ID { + list := make([]*id.ID, 0, len(m.channels)) + for chID := range m.channels { + list = append(list, chID.DeepCopy()) + } + return list +} + +// joinedChannel holds channel info. It will expand to include admin data, so it +// will be treated as a struct for now. +type joinedChannel struct { + broadcast broadcast.Channel +} + +// joinedChannelDisk is the representation of joinedChannel for storage. +type joinedChannelDisk struct { + Broadcast *cryptoBroadcast.Channel +} + +// Store writes the given channel to a unique storage location within the EKV. +func (jc *joinedChannel) Store(kv *versioned.KV) error { + jcd := joinedChannelDisk{jc.broadcast.Get()} + data, err := json.Marshal(&jcd) + if err != nil { + return err + } + + obj := &versioned.Object{ + Version: joinedChannelVersion, + Timestamp: netTime.Now(), + Data: data, + } + + return kv.Set(makeJoinedChannelKey(jc.broadcast.Get().ReceptionID), obj) +} + +// loadJoinedChannel loads a given channel from ekv storage. +func loadJoinedChannel(chId *id.ID, kv *versioned.KV, net broadcast.Client, + rngGen *fastRNG.StreamGenerator, e *events, + broadcastMaker broadcast.NewBroadcastChannelFunc, mr messageReceiveFunc) (*joinedChannel, error) { + obj, err := kv.Get(makeJoinedChannelKey(chId), joinedChannelVersion) + if err != nil { + return nil, err + } + + jcd := &joinedChannelDisk{} + + err = json.Unmarshal(obj.Data, jcd) + if err != nil { + return nil, err + } + + b, err := initBroadcast(jcd.Broadcast, e, net, broadcastMaker, rngGen, mr) + + jc := &joinedChannel{broadcast: b} + return jc, nil +} + +// delete removes the channel from the kv. +func (jc *joinedChannel) delete(kv *versioned.KV) error { + return kv.Delete(makeJoinedChannelKey(jc.broadcast.Get().ReceptionID), + joinedChannelVersion) +} + +func makeJoinedChannelKey(chId *id.ID) string { + return joinedChannelKey + chId.HexEncode() +} + +func initBroadcast(c *cryptoBroadcast.Channel, + e *events, net broadcast.Client, + broadcastMaker broadcast.NewBroadcastChannelFunc, + rngGen *fastRNG.StreamGenerator, mr messageReceiveFunc) (broadcast.Channel, error) { + b, err := broadcastMaker(c, net, rngGen) + if err != nil { + return nil, err + } + + err = b.RegisterListener((&userListener{ + chID: c.ReceptionID, + trigger: e.triggerEvent, + checkSent: mr, + }).Listen, broadcast.Symmetric) + if err != nil { + return nil, err + } + + err = b.RegisterListener((&adminListener{ + chID: c.ReceptionID, + trigger: e.triggerAdminEvent, + checkSent: mr, + }).Listen, broadcast.RSAToPublic) + if err != nil { + return nil, err + } + + return b, nil +} diff --git a/channels/joinedChannel_test.go b/channels/joinedChannel_test.go new file mode 100644 index 0000000000000000000000000000000000000000..ca0ba9ba8319bb246c05f33e816bfbbb9f8b8b39 --- /dev/null +++ b/channels/joinedChannel_test.go @@ -0,0 +1,651 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2020 xx network SEZC // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "bytes" + "crypto/ed25519" + "encoding/binary" + "gitlab.com/elixxir/client/broadcast" + clientCmix "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/message" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/storage/versioned" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/crypto/rsa" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "math/rand" + "reflect" + "sort" + "strconv" + "testing" + "time" +) + +// Tests that manager.store stores the channel list in the ekv. +func Test_manager_store(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + + for i := 0; i < 10; i++ { + ch, _, err := newTestChannel( + "name_"+strconv.Itoa(i), "description_"+strconv.Itoa(i), + m.rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel %d: %+v", i, err) + } + + b, err := broadcast.NewBroadcastChannel(ch, m.net, m.rng) + if err != nil { + t.Errorf("Failed to make new broadcast channel: %+v", err) + } + + m.channels[*ch.ReceptionID] = &joinedChannel{b} + } + + err = m.store() + if err != nil { + t.Errorf("Error storing channels: %+v", err) + } + + _, err = m.kv.Get(joinedChannelsKey, joinedChannelsVersion) + if !ekv.Exists(err) { + t.Errorf("channel list not found in KV: %+v", err) + } +} + +// Tests that the manager.loadChannels loads all the expected channels from the +// ekv. +func Test_manager_loadChannels(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + + expected := make([]*joinedChannel, 10) + + for i := range expected { + ch, _, err := newTestChannel( + "name_"+strconv.Itoa(i), "description_"+strconv.Itoa(i), m.rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel %d: %+v", i, err) + } + + b, err := broadcast.NewBroadcastChannel(ch, m.net, m.rng) + if err != nil { + t.Errorf("Failed to make new broadcast channel: %+v", err) + } + + jc := &joinedChannel{b} + if err = jc.Store(m.kv); err != nil { + t.Errorf("Failed to store joinedChannel %d: %+v", i, err) + } + + chID := *ch.ReceptionID + m.channels[chID] = jc + expected[i] = jc + } + + err = m.store() + if err != nil { + t.Errorf("Error storing channels: %+v", err) + } + + newManager := &manager{ + channels: make(map[id.ID]*joinedChannel), + kv: m.kv, + net: m.net, + rng: m.rng, + broadcastMaker: m.broadcastMaker, + } + + newManager.loadChannels() + + for chID, loadedCh := range newManager.channels { + ch, exists := m.channels[chID] + if !exists { + t.Errorf("Channel %s does not exist.", &chID) + } + + if !reflect.DeepEqual(ch.broadcast, loadedCh.broadcast) { + t.Errorf("Channel %s does not match loaded channel."+ + "\nexpected: %+v\nreceived: %+v", &chID, ch.broadcast, loadedCh.broadcast) + } + } +} + +// Tests that manager.addChannel adds the channel to the map and stores it in +// the kv. +func Test_manager_addChannel(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + + ch, _, err := newTestChannel( + "name", "description", m.rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + err = m.addChannel(ch) + if err != nil { + t.Errorf("Failed to add new channel: %+v", err) + } + + if _, exists := m.channels[*ch.ReceptionID]; !exists { + t.Errorf("Channel %s not added to channel map.", ch.Name) + } + + _, err = m.kv.Get(makeJoinedChannelKey(ch.ReceptionID), joinedChannelVersion) + if err != nil { + t.Errorf("Failed to get joinedChannel from kv: %+v", err) + } + + _, err = m.kv.Get(joinedChannelsKey, joinedChannelsVersion) + if err != nil { + t.Errorf("Failed to get channels from kv: %+v", err) + } +} + +// Error path: tests that manager.addChannel returns ChannelAlreadyExistsErr +// when the channel was already added. +func Test_manager_addChannel_ChannelAlreadyExistsErr(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + + ch, _, err := newTestChannel( + "name", "description", m.rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + err = m.addChannel(ch) + if err != nil { + t.Errorf("Failed to add new channel: %+v", err) + } + + err = m.addChannel(ch) + if err == nil || err != ChannelAlreadyExistsErr { + t.Errorf("Received incorrect error when adding a channel that already "+ + "exists.\nexpected: %s\nreceived: %+v", ChannelAlreadyExistsErr, err) + } +} + +// Tests the manager.removeChannel deletes the channel from the map. +func Test_manager_removeChannel(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + + ch, _, err := newTestChannel( + "name", "description", m.rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + err = m.addChannel(ch) + if err != nil { + t.Errorf("Failed to add new channel: %+v", err) + } + + err = m.removeChannel(ch.ReceptionID) + if err != nil { + t.Errorf("Error removing channel: %+v", err) + } + + if _, exists := m.channels[*ch.ReceptionID]; exists { + t.Errorf("Channel %s was not remove from the channel map.", ch.Name) + } + + _, err = m.kv.Get(makeJoinedChannelKey(ch.ReceptionID), joinedChannelVersion) + if ekv.Exists(err) { + t.Errorf("joinedChannel not removed from kv: %+v", err) + } +} + +// Error path: tests that manager.removeChannel returns ChannelDoesNotExistsErr +// when the channel was never added. +func Test_manager_removeChannel_ChannelDoesNotExistsErr(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + + ch, _, err := newTestChannel( + "name", "description", m.rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + err = m.removeChannel(ch.ReceptionID) + if err == nil || err != ChannelDoesNotExistsErr { + t.Errorf("Received incorrect error when removing a channel that does "+ + "not exists.\nexpected: %s\nreceived: %+v", + ChannelDoesNotExistsErr, err) + } +} + +// Tests the manager.getChannel returns the expected channel. +func Test_manager_getChannel(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + + ch, _, err := newTestChannel( + "name", "description", m.rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + err = m.addChannel(ch) + if err != nil { + t.Errorf("Failed to add new channel: %+v", err) + } + + jc, err := m.getChannel(ch.ReceptionID) + if err != nil { + t.Errorf("Error getting channel: %+v", err) + } + + if !reflect.DeepEqual(ch, jc.broadcast.Get()) { + t.Errorf("Received unexpected channel.\nexpected: %+v\nreceived: %+v", + ch, jc.broadcast.Get()) + } +} + +// Error path: tests that manager.getChannel returns ChannelDoesNotExistsErr +// when the channel was never added. +func Test_manager_getChannel_ChannelDoesNotExistsErr(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + + ch, _, err := newTestChannel( + "name", "description", m.rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + _, err = m.getChannel(ch.ReceptionID) + if err == nil || err != ChannelDoesNotExistsErr { + t.Errorf("Received incorrect error when getting a channel that does "+ + "not exists.\nexpected: %s\nreceived: %+v", + ChannelDoesNotExistsErr, err) + } +} + +// Tests that manager.getChannels returns all the channels that were added to +// the map. +func Test_manager_getChannels(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + + expected := make([]*id.ID, 10) + + for i := range expected { + ch, _, err := newTestChannel( + "name_"+strconv.Itoa(i), "description_"+strconv.Itoa(i), m.rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel %d: %+v", i, err) + } + expected[i] = ch.ReceptionID + + err = m.addChannel(ch) + if err != nil { + t.Errorf("Failed to add new channel %d: %+v", i, err) + } + } + + channelIDs := m.getChannelsUnsafe() + + sort.SliceStable(expected, func(i, j int) bool { + return bytes.Compare(expected[i][:], expected[j][:]) == -1 + }) + sort.SliceStable(channelIDs, func(i, j int) bool { + return bytes.Compare(channelIDs[i][:], channelIDs[j][:]) == -1 + }) + + if !reflect.DeepEqual(expected, channelIDs) { + t.Errorf("ID list does not match expected.\nexpected: %v\nreceived: %v", + expected, channelIDs) + } +} + +// Tests that joinedChannel.Store saves the joinedChannel to the expected place +// in the ekv. +func Test_joinedChannel_Store(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG) + ch, _, err := newTestChannel( + "name", "description", rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + b, err := broadcast.NewBroadcastChannel(ch, new(mockBroadcastClient), rng) + if err != nil { + t.Errorf("Failed to create new broadcast channel: %+v", err) + } + + jc := &joinedChannel{b} + + err = jc.Store(kv) + if err != nil { + t.Errorf("Error storing joinedChannel: %+v", err) + } + + _, err = kv.Get(makeJoinedChannelKey(ch.ReceptionID), joinedChannelVersion) + if !ekv.Exists(err) { + t.Errorf("joinedChannel not found in KV: %+v", err) + } +} + +// Tests that loadJoinedChannel returns a joinedChannel from storage that +// matches the original. +func Test_loadJoinedChannel(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + + ch, _, err := newTestChannel( + "name", "description", m.rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + err = m.addChannel(ch) + if err != nil { + t.Errorf("Failed to add channel: %+v", err) + } + + loadedJc, err := loadJoinedChannel(ch.ReceptionID, m.kv, m.net, m.rng, + m.events, m.broadcastMaker, func(messageID cryptoChannel.MessageID, r rounds.Round) bool { + return false + }) + if err != nil { + t.Errorf("Failed to load joinedChannel: %+v", err) + } + + if !reflect.DeepEqual(ch, loadedJc.broadcast.Get()) { + t.Errorf("Loaded joinedChannel does not match original."+ + "\nexpected: %+v\nreceived: %+v", ch, loadedJc.broadcast.Get()) + } +} + +// Tests that joinedChannel.delete deletes the stored joinedChannel from the +// ekv. +func Test_joinedChannel_delete(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG) + ch, _, err := newTestChannel( + "name", "description", rng.GetStream(), cryptoBroadcast.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + b, err := broadcast.NewBroadcastChannel(ch, new(mockBroadcastClient), rng) + if err != nil { + t.Errorf("Failed to create new broadcast channel: %+v", err) + } + + jc := &joinedChannel{b} + + err = jc.Store(kv) + if err != nil { + t.Errorf("Error storing joinedChannel: %+v", err) + } + + err = jc.delete(kv) + if err != nil { + t.Errorf("Error deleting joinedChannel: %+v", err) + } + + _, err = kv.Get(makeJoinedChannelKey(ch.ReceptionID), joinedChannelVersion) + if ekv.Exists(err) { + t.Errorf("joinedChannel found in KV: %+v", err) + } +} + +// Consistency test of makeJoinedChannelKey. +func Test_makeJoinedChannelKey_Consistency(t *testing.T) { + values := map[*id.ID]string{ + id.NewIdFromUInt(0, id.User, t): "JoinedChannelKey-0x0000000000000000000000000000000000000000000000000000000000000000", + id.NewIdFromUInt(1, id.User, t): "JoinedChannelKey-0x0000000000000001000000000000000000000000000000000000000000000000", + id.NewIdFromUInt(2, id.User, t): "JoinedChannelKey-0x0000000000000002000000000000000000000000000000000000000000000000", + id.NewIdFromUInt(3, id.User, t): "JoinedChannelKey-0x0000000000000003000000000000000000000000000000000000000000000000", + id.NewIdFromUInt(4, id.User, t): "JoinedChannelKey-0x0000000000000004000000000000000000000000000000000000000000000000", + id.NewIdFromUInt(5, id.User, t): "JoinedChannelKey-0x0000000000000005000000000000000000000000000000000000000000000000", + id.NewIdFromUInt(6, id.User, t): "JoinedChannelKey-0x0000000000000006000000000000000000000000000000000000000000000000", + id.NewIdFromUInt(7, id.User, t): "JoinedChannelKey-0x0000000000000007000000000000000000000000000000000000000000000000", + id.NewIdFromUInt(8, id.User, t): "JoinedChannelKey-0x0000000000000008000000000000000000000000000000000000000000000000", + id.NewIdFromUInt(9, id.User, t): "JoinedChannelKey-0x0000000000000009000000000000000000000000000000000000000000000000", + } + + for chID, expected := range values { + key := makeJoinedChannelKey(chID) + + if expected != key { + t.Errorf("Unexpected key for ID %d.\nexpected: %s\nreceived: %s", + binary.BigEndian.Uint64(chID[:8]), expected, key) + } + } + +} + +// newTestChannel creates a new cryptoBroadcast.Channel in the same way that +// cryptoBroadcast.NewChannel does but with a smaller RSA key and salt to make +// tests run quicker. +func newTestChannel(name, description string, rng csprng.Source, + level cryptoBroadcast.PrivacyLevel) ( + *cryptoBroadcast.Channel, rsa.PrivateKey, error) { + c, pk, err := cryptoBroadcast.NewChannelVariableKeyUnsafe( + name, description, level, 1000, 512, rng) + return c, pk, err +} + +//////////////////////////////////////////////////////////////////////////////// +// Mock Broadcast Client // +//////////////////////////////////////////////////////////////////////////////// + +// mockBroadcastClient adheres to the broadcast.Client interface. +type mockBroadcastClient struct{} + +func (m *mockBroadcastClient) GetMaxMessageLength() int { return 123 } + +func (m *mockBroadcastClient) SendWithAssembler(*id.ID, + clientCmix.MessageAssembler, clientCmix.CMIXParams) ( + rounds.Round, ephemeral.Id, error) { + return rounds.Round{ID: id.Round(567)}, ephemeral.Id{}, nil +} + +func (m *mockBroadcastClient) IsHealthy() bool { return true } +func (m *mockBroadcastClient) AddIdentity(*id.ID, time.Time, bool) {} +func (m *mockBroadcastClient) AddService(*id.ID, message.Service, message.Processor) {} +func (m *mockBroadcastClient) DeleteClientService(*id.ID) {} +func (m *mockBroadcastClient) RemoveIdentity(*id.ID) {} +func (m *mockBroadcastClient) GetRoundResults(time.Duration, clientCmix.RoundEventCallback, ...id.Round) { +} +func (m *mockBroadcastClient) AddHealthCallback(func(bool)) uint64 { return 0 } +func (m *mockBroadcastClient) RemoveHealthCallback(uint64) {} + +//////////////////////////////////////////////////////////////////////////////// +// Mock EventModel // +//////////////////////////////////////////////////////////////////////////////// + +func mockEventModelBuilder(string) (EventModel, error) { + return &mockEventModel{}, nil +} + +// mockEventModel adheres to the EventModel interface. +type mockEventModel struct { + joinedCh *cryptoBroadcast.Channel + leftCh *id.ID +} + +func (m *mockEventModel) JoinChannel(c *cryptoBroadcast.Channel) { m.joinedCh = c } +func (m *mockEventModel) LeaveChannel(c *id.ID) { m.leftCh = c } + +func (m *mockEventModel) ReceiveMessage(*id.ID, cryptoChannel.MessageID, string, + string, ed25519.PublicKey, uint8, time.Time, time.Duration, rounds.Round, + MessageType, SentStatus) uint64 { + return 0 +} + +func (m *mockEventModel) ReceiveReply(*id.ID, cryptoChannel.MessageID, + cryptoChannel.MessageID, string, string, ed25519.PublicKey, uint8, + time.Time, time.Duration, rounds.Round, MessageType, SentStatus) uint64 { + return 0 +} + +func (m *mockEventModel) ReceiveReaction(*id.ID, cryptoChannel.MessageID, + cryptoChannel.MessageID, string, string, ed25519.PublicKey, uint8, + time.Time, time.Duration, rounds.Round, MessageType, SentStatus) uint64 { + return 0 +} + +func (m *mockEventModel) UpdateSentStatus(uint64, cryptoChannel.MessageID, + time.Time, rounds.Round, SentStatus) { + // TODO implement me + panic("implement me") +} diff --git a/channels/manager.go b/channels/manager.go new file mode 100644 index 0000000000000000000000000000000000000000..b6a474e2fff2f874a12fbbcb2d6333f3eeb4db9e --- /dev/null +++ b/channels/manager.go @@ -0,0 +1,262 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +// Package channels provides a channels implementation on top of broadcast +// which is capable of handing the user facing features of channels, including +// replies, reactions, and eventually admin commands. +package channels + +import ( + "crypto/ed25519" + "encoding/base64" + "fmt" + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/broadcast" + "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/message" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/storage/versioned" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "sync" + "time" +) + +const storageTagFormat = "channelManagerStorageTag-%s" + +type manager struct { + // Sender Identity + me cryptoChannel.PrivateIdentity + + // List of all channels + channels map[id.ID]*joinedChannel + mux sync.RWMutex + + // External references + kv *versioned.KV + net Client + rng *fastRNG.StreamGenerator + + // Events model + *events + + // Nicknames + *nicknameManager + + // Send tracker + st *sendTracker + + // Makes the function that is used to create broadcasts be a pointer so that + // it can be replaced in tests + broadcastMaker broadcast.NewBroadcastChannelFunc +} + +// Client contains the methods from cmix.Client that are required by the +// [Manager]. +type Client interface { + GetMaxMessageLength() int + SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler, + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) + IsHealthy() bool + AddIdentity(id *id.ID, validUntil time.Time, persistent bool) + AddService(clientID *id.ID, newService message.Service, + response message.Processor) + DeleteClientService(clientID *id.ID) + RemoveIdentity(id *id.ID) + GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, + roundList ...id.Round) + AddHealthCallback(f func(bool)) uint64 + RemoveHealthCallback(uint64) +} + +// EventModelBuilder initialises the event model using the given path. +type EventModelBuilder func(path string) (EventModel, error) + +// NewManager creates a new channel Manager from a [channel.PrivateIdentity]. It +// prefixes the KV with a tag derived from the public key that can be retried +// for reloading using [Manager.GetStorageTag]. +func NewManager(identity cryptoChannel.PrivateIdentity, kv *versioned.KV, + net Client, rng *fastRNG.StreamGenerator, modelBuilder EventModelBuilder) ( + Manager, error) { + + // Prefix the kv with the username so multiple can be run + storageTag := getStorageTag(identity.PubKey) + jww.INFO.Printf("NewManager(ID:%s-%s, tag:%s)", identity.Codename, + identity.PubKey, storageTag) + kv = kv.Prefix(storageTag) + + if err := storeIdentity(kv, identity); err != nil { + return nil, err + } + + model, err := modelBuilder(storageTag) + if err != nil { + return nil, errors.Errorf("Failed to build event model: %+v", err) + } + + m := setupManager(identity, kv, net, rng, model) + + return m, nil +} + +// LoadManager restores a channel Manager from disk stored at the given storage +// tag. +func LoadManager(storageTag string, kv *versioned.KV, net Client, + rng *fastRNG.StreamGenerator, modelBuilder EventModelBuilder) (Manager, error) { + + jww.INFO.Printf("LoadManager(tag:%s)", storageTag) + + // Prefix the kv with the username so multiple can be run + kv = kv.Prefix(storageTag) + + // Load the identity + identity, err := loadIdentity(kv) + if err != nil { + return nil, err + } + + model, err := modelBuilder(storageTag) + if err != nil { + return nil, errors.Errorf("Failed to build event model: %+v", err) + } + + m := setupManager(identity, kv, net, rng, model) + + return m, nil +} + +func setupManager(identity cryptoChannel.PrivateIdentity, kv *versioned.KV, + net Client, rng *fastRNG.StreamGenerator, model EventModel) *manager { + + m := manager{ + me: identity, + kv: kv, + net: net, + rng: rng, + broadcastMaker: broadcast.NewBroadcastChannel, + } + + m.events = initEvents(model) + + m.st = loadSendTracker(net, kv, m.events.triggerEvent, + m.events.triggerAdminEvent, model.UpdateSentStatus, rng) + + m.loadChannels() + + m.nicknameManager = loadOrNewNicknameManager(kv) + + return &m +} + +// JoinChannel joins the given channel. It will fail if the channel has already +// been joined. +func (m *manager) JoinChannel(channel *cryptoBroadcast.Channel) error { + jww.INFO.Printf("JoinChannel(%s[%s])", channel.Name, channel.ReceptionID) + err := m.addChannel(channel) + if err != nil { + return err + } + + go m.events.model.JoinChannel(channel) + + return nil +} + +// LeaveChannel leaves the given channel. It will return an error if the channel +// was not previously joined. +func (m *manager) LeaveChannel(channelID *id.ID) error { + jww.INFO.Printf("LeaveChannel(%s)", channelID) + err := m.removeChannel(channelID) + if err != nil { + return err + } + + go m.events.model.LeaveChannel(channelID) + + return nil +} + +// GetChannels returns the IDs of all channels that have been joined. Use +// getChannelsUnsafe if you already have taken the mux. +func (m *manager) GetChannels() []*id.ID { + jww.INFO.Printf("GetChannels") + m.mux.Lock() + defer m.mux.Unlock() + return m.getChannelsUnsafe() +} + +// GetChannel returns the underlying cryptographic structure for a given channel. +func (m *manager) GetChannel(chID *id.ID) (*cryptoBroadcast.Channel, error) { + jww.INFO.Printf("GetChannel(%s)", chID) + jc, err := m.getChannel(chID) + if err != nil { + return nil, err + } else if jc.broadcast == nil { + return nil, errors.New("broadcast.Channel on joinedChannel is nil") + } + return jc.broadcast.Get(), nil +} + +// ReplayChannel replays all messages from the channel within the network's +// memory (~3 weeks) over the event model. It does this by wiping the +// underlying state tracking for message pickup for the channel, causing all +// messages to be re-retrieved from the network +func (m *manager) ReplayChannel(chID *id.ID) error { + jww.INFO.Printf("ReplayChannel(%s)", chID) + m.mux.RLock() + defer m.mux.RUnlock() + + jc, exists := m.channels[*chID] + if !exists { + return ChannelDoesNotExistsErr + } + + c := jc.broadcast.Get() + + // Stop the broadcast that will completely wipe it from the underlying cmix + // object + jc.broadcast.Stop() + + // Re-instantiate the broadcast, re-registering it from scratch + b, err := initBroadcast(c, m.events, m.net, m.broadcastMaker, m.rng, + m.st.MessageReceive) + if err != nil { + return err + } + jc.broadcast = b + + return nil + +} + +// GetStorageTag returns the tag at which this manager is store for loading +// it is derived from the public key +func (m *manager) GetStorageTag() string { + return getStorageTag(m.me.PubKey) +} + +// GetIdentity returns the public identity associated with this channel manager +func (m *manager) GetIdentity() cryptoChannel.Identity { + return m.me.Identity +} + +// ExportPrivateIdentity encrypts and exports the private identity to a portable +// string. +func (m *manager) ExportPrivateIdentity(password string) ([]byte, error) { + jww.INFO.Printf("ExportPrivateIdentity()") + rng := m.rng.GetStream() + defer rng.Close() + return m.me.Export(password, rng) +} + +func getStorageTag(pub ed25519.PublicKey) string { + return fmt.Sprintf(storageTagFormat, base64.StdEncoding.EncodeToString(pub)) +} diff --git a/channels/manager_test.go b/channels/manager_test.go new file mode 100644 index 0000000000000000000000000000000000000000..e4213bd7c369143fec41c60895150bf79e0ea3a6 --- /dev/null +++ b/channels/manager_test.go @@ -0,0 +1,225 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "fmt" + "gitlab.com/elixxir/client/broadcast" + "gitlab.com/elixxir/client/storage/versioned" + broadcast2 "gitlab.com/elixxir/crypto/broadcast" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/primitives/id" + "math/rand" + "os" + "sync" + "testing" + "time" + + jww "github.com/spf13/jwalterweatherman" +) + +func TestMain(m *testing.M) { + // Many tests trigger WARN prints;, set the out threshold so the WARN prints + // can be seen in the logs + jww.SetStdoutThreshold(jww.LevelWarn) + os.Exit(m.Run()) +} + +func TestManager_JoinChannel(t *testing.T) { + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + mem := m.events.model.(*mockEventModel) + + ch, _, err := newTestChannel( + "name", "description", m.rng.GetStream(), broadcast2.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + err = m.JoinChannel(ch) + if err != nil { + t.Fatalf("Join Channel Errored: %s", err) + } + + if _, exists := m.channels[*ch.ReceptionID]; !exists { + t.Errorf("Channel %s not added to channel map.", ch.Name) + } + + //wait because the event model is called in another thread + time.Sleep(1 * time.Second) + + if mem.joinedCh == nil { + t.Errorf("the channel join call was not propogated to the event " + + "model") + } +} + +func TestManager_LeaveChannel(t *testing.T) { + + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + mFace, err := NewManager(pi, versioned.NewKV(ekv.MakeMemstore()), + new(mockBroadcastClient), + fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG), + mockEventModelBuilder) + if err != nil { + t.Errorf(err.Error()) + } + + m := mFace.(*manager) + mem := m.events.model.(*mockEventModel) + + ch, _, err := newTestChannel( + "name", "description", m.rng.GetStream(), broadcast2.Public) + if err != nil { + t.Errorf("Failed to create new channel: %+v", err) + } + + err = m.JoinChannel(ch) + if err != nil { + t.Fatalf("Join Channel Errored: %s", err) + } + + err = m.LeaveChannel(ch.ReceptionID) + if err != nil { + t.Fatalf("Leave Channel Errored: %s", err) + } + + if _, exists := m.channels[*ch.ReceptionID]; exists { + t.Errorf("Channel %s still in map.", ch.Name) + } + + //wait because the event model is called in another thread + time.Sleep(1 * time.Second) + + if mem.leftCh == nil { + t.Errorf("the channel join call was not propogated to the event " + + "model") + } +} + +func TestManager_GetChannels(t *testing.T) { + m := &manager{ + channels: make(map[id.ID]*joinedChannel), + mux: sync.RWMutex{}, + } + + rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG) + + numtests := 10 + + chList := make(map[id.ID]interface{}) + + for i := 0; i < 10; i++ { + name := fmt.Sprintf("testChannel_%d", numtests) + s := rng.GetStream() + tc, _, err := newTestChannel(name, "blarg", s, broadcast2.Public) + s.Close() + if err != nil { + t.Fatalf("failed to generate channel %s", name) + } + bc, err := broadcast.NewBroadcastChannel(tc, new(mockBroadcastClient), rng) + if err != nil { + t.Fatalf("failed to generate broadcast %s", name) + } + m.channels[*tc.ReceptionID] = &joinedChannel{broadcast: bc} + chList[*tc.ReceptionID] = nil + } + + receivedChList := m.GetChannels() + + for _, receivedCh := range receivedChList { + if _, exists := chList[*receivedCh]; !exists { + t.Errorf("Channel was not returned") + } + } +} + +func TestManager_GetChannel(t *testing.T) { + m := &manager{ + channels: make(map[id.ID]*joinedChannel), + mux: sync.RWMutex{}, + } + + rng := fastRNG.NewStreamGenerator(1, 1, csprng.NewSystemRNG) + + numtests := 10 + + chList := make([]*id.ID, 0, numtests) + + for i := 0; i < 10; i++ { + name := fmt.Sprintf("testChannel_%d", numtests) + s := rng.GetStream() + tc, _, err := newTestChannel(name, "blarg", s, broadcast2.Public) + s.Close() + if err != nil { + t.Fatalf("failed to generate channel %s", name) + } + bc, err := broadcast.NewBroadcastChannel(tc, new(mockBroadcastClient), rng) + if err != nil { + t.Fatalf("failed to generate broadcast %s", name) + } + m.channels[*tc.ReceptionID] = &joinedChannel{broadcast: bc} + chList = append(chList, tc.ReceptionID) + } + + for i, receivedCh := range chList { + ch, err := m.GetChannel(receivedCh) + if err != nil { + t.Errorf("Channel %d failed to be gotten", i) + } else if !ch.ReceptionID.Cmp(receivedCh) { + t.Errorf("Channel %d Get returned wrong channel", i) + } + } +} + +func TestManager_GetChannel_BadChannel(t *testing.T) { + m := &manager{ + channels: make(map[id.ID]*joinedChannel), + mux: sync.RWMutex{}, + } + + numtests := 10 + + chList := make([]*id.ID, 0, numtests) + + for i := 0; i < 10; i++ { + chId := &id.ID{} + chId[0] = byte(i) + chList = append(chList, chId) + } + + for i, receivedCh := range chList { + _, err := m.GetChannel(receivedCh) + if err == nil { + t.Errorf("Channel %d returned when it doesnt exist", i) + } + } +} diff --git a/channels/messageTypes.go b/channels/messageTypes.go new file mode 100644 index 0000000000000000000000000000000000000000..5ac6e2de3c914131655e484599171c5ac6114d90 --- /dev/null +++ b/channels/messageTypes.go @@ -0,0 +1,31 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import "fmt" + +type MessageType uint32 + +const ( + Text = MessageType(1) + AdminText = MessageType(2) + Reaction = MessageType(3) +) + +func (mt MessageType) String() string { + switch mt { + case Text: + return "Text" + case AdminText: + return "AdminText" + case Reaction: + return "Reaction" + default: + return fmt.Sprintf("Unknown messageType %d", mt) + } +} diff --git a/channels/messageTypes_test.go b/channels/messageTypes_test.go new file mode 100644 index 0000000000000000000000000000000000000000..170ebf0542de153bb23c2f2135c66ff597d633f7 --- /dev/null +++ b/channels/messageTypes_test.go @@ -0,0 +1,25 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import "testing" + +func TestMessageType_String(t *testing.T) { + expected := []string{"Text", "AdminText", "Reaction", "Unknown messageType 4", + "Unknown messageType 5", "Unknown messageType 6", "Unknown messageType 7", + "Unknown messageType 8", "Unknown messageType 9", + "Unknown messageType 10"} + + for i := 1; i <= 10; i++ { + mt := MessageType(i) + if mt.String() != expected[i-1] { + t.Errorf("Stringer failed on test %d, %s vs %s", i, + mt.String(), expected[i-1]) + } + } +} diff --git a/channels/messages.go b/channels/messages.go new file mode 100644 index 0000000000000000000000000000000000000000..0162c3c62f62c10c7c652d206e405e7a2383327b --- /dev/null +++ b/channels/messages.go @@ -0,0 +1,75 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "github.com/golang/protobuf/proto" + "gitlab.com/elixxir/crypto/channel" + "gitlab.com/xx_network/primitives/id" +) + +// userMessageInternal is the internal structure of a UserMessage protobuf. +type userMessageInternal struct { + userMessage *UserMessage + channelMessage *ChannelMessage + messageID channel.MessageID +} + +func newUserMessageInternal(ursMsg *UserMessage, chid *id.ID) (*userMessageInternal, error) { + chanMessage := &ChannelMessage{} + err := proto.Unmarshal(ursMsg.Message, chanMessage) + if err != nil { + return nil, err + } + + channelMessage := chanMessage + return &userMessageInternal{ + userMessage: ursMsg, + channelMessage: channelMessage, + messageID: channel.MakeMessageID(ursMsg.Message, chid), + }, nil +} + +func unmarshalUserMessageInternal(usrMsg []byte, chid *id.ID) (*userMessageInternal, error) { + + um := &UserMessage{} + if err := proto.Unmarshal(usrMsg, um); err != nil { + return nil, err + } + + chanMessage := &ChannelMessage{} + err := proto.Unmarshal(um.Message, chanMessage) + if err != nil { + return nil, err + } + + channelMessage := chanMessage + + return &userMessageInternal{ + userMessage: um, + channelMessage: channelMessage, + messageID: channel.MakeMessageID(um.Message, chid), + }, nil +} + +// GetUserMessage retrieves the UserMessage within +// userMessageInternal. +func (umi *userMessageInternal) GetUserMessage() *UserMessage { + return umi.userMessage +} + +// GetChannelMessage retrieves the ChannelMessage within +// userMessageInternal. +func (umi *userMessageInternal) GetChannelMessage() *ChannelMessage { + return umi.channelMessage +} + +// GetMessageID retrieves the messageID for the message. +func (umi *userMessageInternal) GetMessageID() channel.MessageID { + return umi.messageID +} diff --git a/channels/messages_test.go b/channels/messages_test.go new file mode 100644 index 0000000000000000000000000000000000000000..b5109bd30a535156edc2e80556b9db51d55313e4 --- /dev/null +++ b/channels/messages_test.go @@ -0,0 +1,166 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "github.com/golang/protobuf/proto" + "gitlab.com/elixxir/crypto/channel" + "gitlab.com/xx_network/primitives/id" + "reflect" + "testing" +) + +func TestUnmarshalUserMessageInternal(t *testing.T) { + internal, usrMsg, _ := builtTestUMI(t, 7) + + chID := &id.ID{} + + usrMsgMarshaled, err := proto.Marshal(usrMsg) + if err != nil { + t.Fatalf("Failed to marshal user message: %+v", err) + } + + umi, err := unmarshalUserMessageInternal(usrMsgMarshaled, chID) + if err != nil { + t.Fatalf("Failed to unmarshal user message: %+v", err) + } + + if !umi.GetMessageID().Equals(internal.messageID) { + t.Errorf("Message IDs were changed in the unmarshal "+ + "process, %s vs %s", internal.messageID, umi.GetMessageID()) + } +} + +func TestUnmarshalUserMessageInternal_BadUserMessage(t *testing.T) { + chID := &id.ID{} + _, err := unmarshalUserMessageInternal([]byte("Malformed"), chID) + if err == nil { + t.Fatalf("Error not returned on unmarshaling a bad user " + + "message") + } +} + +func TestUnmarshalUserMessageInternal_BadChannelMessage(t *testing.T) { + _, usrMsg, _ := builtTestUMI(t, 7) + + usrMsg.Message = []byte("Malformed") + + chID := &id.ID{} + + usrMsgMarshaled, err := proto.Marshal(usrMsg) + if err != nil { + t.Fatalf("Failed to marshal user message: %+v", err) + } + + _, err = unmarshalUserMessageInternal(usrMsgMarshaled, chID) + if err == nil { + t.Fatalf("Error not returned on unmarshaling a user message " + + "with a bad channel message") + } +} + +func TestNewUserMessageInternal_BadChannelMessage(t *testing.T) { + _, usrMsg, _ := builtTestUMI(t, 7) + + usrMsg.Message = []byte("Malformed") + + chID := &id.ID{} + + _, err := newUserMessageInternal(usrMsg, chID) + + if err == nil { + t.Fatalf("failed to produce error with malformed user message") + } +} + +func TestUserMessageInternal_GetChannelMessage(t *testing.T) { + internal, _, channelMsg := builtTestUMI(t, 7) + received := internal.GetChannelMessage() + + if !reflect.DeepEqual(received.Payload, channelMsg.Payload) || + received.Lease != channelMsg.Lease || + received.RoundID != channelMsg.RoundID || + received.PayloadType != channelMsg.PayloadType { + t.Fatalf("GetChannelMessage did not return expected data."+ + "\nExpected: %v"+ + "\nReceived: %v", channelMsg, received) + } +} + +func TestUserMessageInternal_GetUserMessage(t *testing.T) { + internal, usrMsg, _ := builtTestUMI(t, 7) + received := internal.GetUserMessage() + + if !reflect.DeepEqual(received.Message, usrMsg.Message) || + !reflect.DeepEqual(received.Signature, usrMsg.Signature) || + !reflect.DeepEqual(received.ECCPublicKey, usrMsg.ECCPublicKey) { + t.Fatalf("GetUserMessage did not return expected data."+ + "\nExpected: %v"+ + "\nReceived: %v", usrMsg, received) + } +} + +func TestUserMessageInternal_GetMessageID(t *testing.T) { + internal, usrMsg, _ := builtTestUMI(t, 7) + received := internal.GetMessageID() + + chID := &id.ID{} + + expected := channel.MakeMessageID(usrMsg.Message, chID) + + if !reflect.DeepEqual(expected, received) { + t.Fatalf("GetMessageID did not return expected data."+ + "\nExpected: %v"+ + "\nReceived: %v", expected, received) + } +} + +// Ensures the serialization hasn't changed, changing the message IDs. The +// protocol is tolerant of this because only the sender seralizes, but +// it would be good to know when this changes. If this test breaks, report it, +// but it should be safe to update the expected +func TestUserMessageInternal_GetMessageID_Consistency(t *testing.T) { + expected := "ChMsgID-LrGYLFCaPamZk44X+c/b08qtmJIorgNnoE68v1HYrf8=" + + internal, _, _ := builtTestUMI(t, 7) + + received := internal.GetMessageID() + + if expected != received.String() { + t.Fatalf("GetMessageID did not return expected data."+ + "\nExpected: %v"+ + "\nReceived: %v", expected, received) + } +} + +func builtTestUMI(t *testing.T, mt MessageType) (*userMessageInternal, *UserMessage, *ChannelMessage) { + channelMsg := &ChannelMessage{ + Lease: 69, + RoundID: 42, + PayloadType: uint32(mt), + Payload: []byte("ban_badUSer"), + Nickname: "paul", + } + + serialized, err := proto.Marshal(channelMsg) + if err != nil { + t.Fatalf("Marshal error: %v", err) + } + + usrMsg := &UserMessage{ + Message: serialized, + Signature: []byte("sig2"), + ECCPublicKey: []byte("key"), + } + + chID := &id.ID{} + + internal, _ := newUserMessageInternal(usrMsg, chID) + + return internal, usrMsg, channelMsg +} diff --git a/channels/mutateTimestamp.go b/channels/mutateTimestamp.go new file mode 100644 index 0000000000000000000000000000000000000000..8b8bf683e971635458124783b76754fa9e3638b5 --- /dev/null +++ b/channels/mutateTimestamp.go @@ -0,0 +1,76 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "gitlab.com/elixxir/crypto/channel" + "gitlab.com/xx_network/crypto/large" + "time" +) + +const ( + // tenMsInNs is a prime close to one million to ensure patterns dont + // arise due to cofactors with the message ID when doing the modulo + tenMsInNs = 10000019 + halfTenMsInNs = tenMsInNs / 2 + beforeGrace = 5 * time.Second + afterGrace = 2 * time.Second +) + +var tenMsInNsLargeInt = large.NewInt(tenMsInNs) + +// vetTimestamp determines which timestamp to use for a message. It will +// use the local timestamp provided in the message as long as it is within 5 +// seconds before the round and 2 second after the round. Otherwise, it will +// use the round timestamp via mutateTimestamp +func vetTimestamp(localTS, ts time.Time, msgID channel.MessageID) time.Time { + + before := ts.Add(-beforeGrace) + after := ts.Add(afterGrace) + + if localTS.Before(before) || localTS.After(after) { + return mutateTimestamp(ts, msgID) + } + + return localTS +} + +// mutateTimestamp is used to modify the the timestamps on all messages in a +// deterministic manner. This is because message ordering is done by timestamp +// and the timestamps come from the rounds, which means multiple messages can +// have the same timestamp due to being in the same round. The meaning of +// conversations can change depending on order, so while no explicit order +// can be discovered because to do so can leak potential ordering info for the +// mix, choosing an arbitrary order and having all clients agree will at least +// ensure that misunderstandings due to disagreements in order cannot occur +// +// In order to do this, this function mutates the timestamp of the round within +// +/- 5ms seeded based upon the message ID. +// It should be noted that this is only a reasonable assumption when the number +// of messages in a channel isn't too much. For example, under these conditions +// the birthday paradox of getting a collision if there are 10 messages for the +// channel in the same round is ~4*10^-6, but the chance if there are 50 +// messages is 10^-4, and if the entire round is full of messages for the +// channel (1000 messages), .0487. +func mutateTimestamp(ts time.Time, msgID channel.MessageID) time.Time { + + // Treat the message ID as a number and mod it by the number of ns in an ms + // to get an offset factor. Use a prime close to 1000000 to make sure there + // are no patterns in the output and reduce the chance of collision. While + // the fields do not align, so there is some bias towards some parts of the + // output field, that bias is too small to matter because log2(10000019) ~23 + // while the input field is 256. + offsetLarge := large.NewIntFromBytes(msgID.Bytes()) + offsetLarge.Mod(offsetLarge, tenMsInNsLargeInt) + + // subtract half the field size so on average (across many runs) the message + // timestamps are not changed + offset := offsetLarge.Int64() - halfTenMsInNs + + return time.Unix(0, ts.UnixNano()+offset) +} diff --git a/channels/mutateTimestamp_test.go b/channels/mutateTimestamp_test.go new file mode 100644 index 0000000000000000000000000000000000000000..93561868b10ac781ef2c86cc0b483a0fed1815f8 --- /dev/null +++ b/channels/mutateTimestamp_test.go @@ -0,0 +1,129 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "gitlab.com/xx_network/primitives/netTime" + "math/rand" + "testing" + "time" + + "gitlab.com/elixxir/crypto/channel" +) + +// withinMutationWindow is a utility test function to check if a mutated +// timestamp is within the allowable window +func withinMutationWindow(raw, mutated time.Time) bool { + lowerBound := raw.Add(-time.Duration(halfTenMsInNs)) + upperBound := raw.Add(time.Duration(halfTenMsInNs)) + + return mutated.After(lowerBound) && mutated.Before(upperBound) +} + +func abs(n int64) int64 { + if n < 0 { + return -n + } + return n +} + +func TestMutateTimestampDeltaAverage(t *testing.T) { + samples := 10000 + t1 := netTime.Now() + sum := int64(0) + + rng := rand.New(rand.NewSource(netTime.Now().UnixNano())) + + for i := 0; i < samples; i++ { + var msgID channel.MessageID + rng.Read(msgID[:]) + t2 := mutateTimestamp(t1, msgID) + delta := t2.Sub(t1) + sum += abs(int64(delta)) + } + + avg := sum / int64(samples) + diff := abs(avg - 2502865) + if diff > 30000 { + t.Fatal() + } +} + +const generationRange = beforeGrace + afterGrace + +// TestVetTimestamp_Happy tests that when the localTS is within +// the allowed range, it is unmodified +func TestVetTimestamp_Happy(t *testing.T) { + samples := 10000 + + rng := rand.New(rand.NewSource(netTime.Now().UnixNano())) + + for i := 0; i < samples; i++ { + + now := time.Now() + + tested := now.Add(-beforeGrace).Add(time.Duration(rng.Int63()) % generationRange) + + var msgID channel.MessageID + rng.Read(msgID[:]) + + result := vetTimestamp(tested, now, msgID) + + if !tested.Equal(result) { + t.Errorf("Timestamp was molested unexpectedly") + } + } +} + +// TestVetTimestamp_Happy tests that when the localTS is less than +// the allowed time period it is replaced +func TestVetTimestamp_BeforePeriod(t *testing.T) { + samples := 10000 + + rng := rand.New(rand.NewSource(netTime.Now().UnixNano())) + + for i := 0; i < samples; i++ { + + now := time.Now() + + tested := now.Add(-beforeGrace).Add(-time.Duration(rng.Int63()) % (100000 * time.Hour)) + + var msgID channel.MessageID + rng.Read(msgID[:]) + + result := vetTimestamp(tested, now, msgID) + + if tested.Equal(result) { + t.Errorf("Timestamp was unmolested unexpectedly") + } + } +} + +// TestVetTimestamp_Happy tests that when the localTS is greater than +// the allowed time period it is replaced +func TestVetTimestamp_AfterPeriod(t *testing.T) { + samples := 10000 + + rng := rand.New(rand.NewSource(netTime.Now().UnixNano())) + + for i := 0; i < samples; i++ { + + now := time.Now() + + tested := now.Add(afterGrace).Add(-time.Duration(rng.Int63()) % (100000 * time.Hour)) + + var msgID channel.MessageID + rng.Read(msgID[:]) + + result := vetTimestamp(tested, now, msgID) + + if tested.Equal(result) { + t.Errorf("Timestamp was unmolested unexpectedly") + } + } +} diff --git a/channels/nameService.go b/channels/nameService.go new file mode 100644 index 0000000000000000000000000000000000000000..d77fa9414a4f0fe1d8a32fae130936187f2fcf92 --- /dev/null +++ b/channels/nameService.go @@ -0,0 +1,38 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "crypto/ed25519" + "time" +) + +// NameService is an interface which encapsulates +// the user identity channel tracking service. +// NameService is currently unused +type NameService interface { + + // GetUsername returns the username. + GetUsername() string + + // GetChannelValidationSignature returns the validation + // signature and the time it was signed. + GetChannelValidationSignature() ([]byte, time.Time) + + // GetChannelPubkey returns the user's public key. + GetChannelPubkey() ed25519.PublicKey + + // SignChannelMessage returns the signature of the + // given message. + SignChannelMessage(message []byte) (signature []byte, err error) + + // ValidateChannelMessage validates that a received channel message's + // username lease is signed by the NameService + ValidateChannelMessage(username string, lease time.Time, + pubKey ed25519.PublicKey, authorIDSignature []byte) bool +} diff --git a/channels/nickname.go b/channels/nickname.go new file mode 100644 index 0000000000000000000000000000000000000000..5116b597f754c09ce1b6bae06ec878c5192debfe --- /dev/null +++ b/channels/nickname.go @@ -0,0 +1,146 @@ +package channels + +import ( + "encoding/json" + "errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "sync" +) + +const ( + nicknameStoreStorageKey = "nicknameStoreStorageKey" + nicknameStoreStorageVersion = 0 +) + +type nicknameManager struct { + byChannel map[id.ID]string + + mux sync.RWMutex + + kv *versioned.KV +} + +// loadOrNewNicknameManager returns the stored nickname manager if there is +// one or returns a new one +func loadOrNewNicknameManager(kv *versioned.KV) *nicknameManager { + nm := &nicknameManager{ + byChannel: make(map[id.ID]string), + kv: kv, + } + err := nm.load() + if err != nil && nm.kv.Exists(err) { + jww.FATAL.Panicf("Failed to load nicknameManager: %+v", err) + } + + return nm + +} + +// GetNickname returns the nickname for the given channel if it exists +func (nm *nicknameManager) GetNickname(ch *id.ID) ( + nickname string, exists bool) { + nm.mux.RLock() + defer nm.mux.RUnlock() + + nickname, exists = nm.byChannel[*ch] + return +} + +// SetNickname sets the nickname for a channel after checking that the nickname +// is valid using IsNicknameValid +func (nm *nicknameManager) SetNickname(newNick string, ch *id.ID) error { + nm.mux.Lock() + defer nm.mux.Unlock() + + if err := IsNicknameValid(newNick); err != nil { + return err + } + + nm.byChannel[*ch] = newNick + return nm.save() +} + +// DeleteNickname removes the nickname for a given channel, using the codename +// for that channel instead +func (nm *nicknameManager) DeleteNickname(ch *id.ID) error { + nm.mux.Lock() + defer nm.mux.Unlock() + + delete(nm.byChannel, *ch) + + return nm.save() +} + +// channelIDToNickname is a serialization structure. This is used by the save +// and load functions to serialize the nicknameManager's byChannel map. +type channelIDToNickname struct { + ChannelId id.ID + Nickname string +} + +// save stores the nickname manager to disk. The caller of this must +// hold the mux. +func (nm *nicknameManager) save() error { + list := make([]channelIDToNickname, 0) + for chId, nickname := range nm.byChannel { + list = append(list, channelIDToNickname{ + ChannelId: chId, + Nickname: nickname, + }) + } + + data, err := json.Marshal(list) + if err != nil { + return err + } + obj := &versioned.Object{ + Version: nicknameStoreStorageVersion, + Timestamp: netTime.Now(), + Data: data, + } + + return nm.kv.Set(nicknameStoreStorageKey, obj) +} + +// load restores the nickname manager from disk. +func (nm *nicknameManager) load() error { + obj, err := nm.kv.Get(nicknameStoreStorageKey, nicknameStoreStorageVersion) + if err != nil { + return err + } + + list := make([]channelIDToNickname, 0) + err = json.Unmarshal(obj.Data, &list) + if err != nil { + return err + } + + for i := range list { + current := list[i] + nm.byChannel[current.ChannelId] = current.Nickname + } + + return nil +} + +// IsNicknameValid checks if a nickname is valid +// +// rules +// - a nickname must not be longer than 24 characters +// - a nickname must not be shorter than 1 character +// todo: add character filtering +func IsNicknameValid(nick string) error { + runeNick := []rune(nick) + if len(runeNick) > 24 { + return errors.New("nicknames must be 24 characters in length or less") + } + + if len(runeNick) < 1 { + return errors.New("nicknames must be at least 1 character in length") + } + + return nil +} diff --git a/channels/nickname_test.go b/channels/nickname_test.go new file mode 100644 index 0000000000000000000000000000000000000000..cf535245f82bb1e4b5c4caf5ac0b65f6355b4653 --- /dev/null +++ b/channels/nickname_test.go @@ -0,0 +1,108 @@ +package channels + +import ( + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/primitives/id" + "strconv" + "testing" +) + +// Unit test. Tests that once you set a nickname with SetNickname, you can +// retrieve the nickname using GetNickname. +func TestNicknameManager_SetGetNickname(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + nm := loadOrNewNicknameManager(kv) + + for i := 0; i < numTests; i++ { + chId := id.NewIdFromUInt(uint64(i), id.User, t) + nickname := "nickname#" + strconv.Itoa(i) + err := nm.SetNickname(nickname, chId) + if err != nil { + t.Fatalf("SetNickname error when setting %s: %+v", nickname, err) + } + + received, _ := nm.GetNickname(chId) + if received != nickname { + t.Fatalf("GetNickname did not return expected values."+ + "\nExpected: %s"+ + "\nReceived: %s", nickname, received) + } + } +} + +// Unit test. Tests that once you set a nickname with SetNickname, you can +// retrieve the nickname using GetNickname after a reload. +func TestNicknameManager_SetGetNickname_Reload(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + nm := loadOrNewNicknameManager(kv) + + for i := 0; i < numTests; i++ { + chId := id.NewIdFromUInt(uint64(i), id.User, t) + nickname := "nickname#" + strconv.Itoa(i) + err := nm.SetNickname(nickname, chId) + if err != nil { + t.Fatalf("SetNickname error when setting %s: %+v", nickname, err) + } + } + + nm2 := loadOrNewNicknameManager(kv) + + for i := 0; i < numTests; i++ { + chId := id.NewIdFromUInt(uint64(i), id.User, t) + nick, exists := nm2.GetNickname(chId) + if !exists { + t.Fatalf("Nickname %d not found ", i) + } + expected := "nickname#" + strconv.Itoa(i) + if nick != expected { + t.Fatalf("Nickname %d not found, expected: %s, received: %s ", i, expected, nick) + } + } +} + +// Error case: Tests that nicknameManager.GetNickname returns a false boolean +// if no nickname has been set with the channel ID. +func TestNicknameManager_GetNickname_Error(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + nm := loadOrNewNicknameManager(kv) + + for i := 0; i < numTests; i++ { + chId := id.NewIdFromUInt(uint64(i), id.User, t) + _, exists := nm.GetNickname(chId) + if exists { + t.Fatalf("GetNickname expected error case: " + + "This should not retrieve nicknames for channel IDs " + + "that are not set.") + } + } +} + +// Unit test. Check that once you SetNickname and DeleteNickname, +// GetNickname returns a false boolean. +func TestNicknameManager_DeleteNickname(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + nm := loadOrNewNicknameManager(kv) + + for i := 0; i < numTests; i++ { + chId := id.NewIdFromUInt(uint64(i), id.User, t) + nickname := "nickname#" + strconv.Itoa(i) + err := nm.SetNickname(nickname, chId) + if err != nil { + t.Fatalf("SetNickname error when setting %s: %+v", nickname, err) + } + + err = nm.DeleteNickname(chId) + if err != nil { + t.Fatalf("DeleteNickname error: %+v", err) + } + + _, exists := nm.GetNickname(chId) + if exists { + t.Fatalf("GetNickname expected error case: " + + "This should not retrieve nicknames for channel IDs " + + "that are not set.") + } + } + +} diff --git a/channels/readme.md b/channels/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..9327e9635afd9c109054bf7b486d24c97a53c0bc --- /dev/null +++ b/channels/readme.md @@ -0,0 +1,21 @@ +Channels provides a channels implementation on top of broadcast which is capable of handing the user facing features of +channels, including replies, reactions, and eventually admin commands. + +on sending, data propagates as follows: +Send function (Example: SendMessage) - > SendGeneric -> +Broadcast.BroadcastWithAssembler -> cmix.SendWithAssembler + +on receiving messages propagate as follows: +cmix message pickup (by service)- > broadcast.Processor -> +userListener -> events.triggerEvent -> +messageTypeHandler (example: Text) -> +eventModel (example: ReceiveMessage) + +on sendingAdmin, data propagates as follows: +Send function - > SendAdminGeneric -> +Broadcast.BroadcastAsymmetricWithAssembler -> cmix.SendWithAssembler + +on receiving admin messages propagate as follows: +cmix message pickup (by service)- > broadcast.Processor -> adminListener -> +events.triggerAdminEvent -> messageTypeHandler (example: Text) -> +eventModel (example: ReceiveMessage) \ No newline at end of file diff --git a/channels/send.go b/channels/send.go new file mode 100644 index 0000000000000000000000000000000000000000..0e508d4cc6d6c952554d90849541ba6514f46463 --- /dev/null +++ b/channels/send.go @@ -0,0 +1,374 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "crypto/ed25519" + "encoding/base64" + "fmt" + "time" + + "github.com/pkg/errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/rounds" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/rsa" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "gitlab.com/xx_network/primitives/netTime" + "golang.org/x/crypto/blake2b" + "google.golang.org/protobuf/proto" +) + +const ( + cmixChannelTextVersion = 0 + cmixChannelReactionVersion = 0 + SendMessageTag = "ChMessage" + SendReplyTag = "ChReply" + SendReactionTag = "ChReaction" +) + +// The size of the nonce used in the message ID. +const messageNonceSize = 4 + +// SendGeneric is used to send a raw message over a channel. In general, it +// should be wrapped in a function which defines the wire protocol +// If the final message, before being sent over the wire, is too long, this will +// return an error. Due to the underlying encoding using compression, it isn't +// possible to define the largest payload that can be sent, but +// it will always be possible to send a payload of 802 bytes at minimum +func (m *manager) SendGeneric(channelID *id.ID, messageType MessageType, + msg []byte, validUntil time.Duration, params cmix.CMIXParams) ( + cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error) { + + // Note: We log sends on exit, and append what happened to the message + // this cuts down on clutter in the log. + sendPrint := fmt.Sprintf("[%s] Sending ch %s type %d at %s", + params.DebugTag, channelID, messageType, + netTime.Now()) + defer jww.INFO.Println(sendPrint) + + //find the channel + ch, err := m.getChannel(channelID) + if err != nil { + return cryptoChannel.MessageID{}, rounds.Round{}, + ephemeral.Id{}, err + } + + nickname, _ := m.GetNickname(channelID) + + var msgId cryptoChannel.MessageID + + chMsg := &ChannelMessage{ + Lease: validUntil.Nanoseconds(), + PayloadType: uint32(messageType), + Payload: msg, + Nickname: nickname, + Nonce: make([]byte, messageNonceSize), + LocalTimestamp: netTime.Now().UnixNano(), + } + + // Generate random nonce to be used for message ID + // generation. This makes it so two identical messages sent on + // the same round have different message IDs + rng := m.rng.GetStream() + n, err := rng.Read(chMsg.Nonce) + rng.Close() + if err != nil { + sendPrint += fmt.Sprintf(", failed to generate nonce: %+v", err) + return cryptoChannel.MessageID{}, rounds.Round{}, + ephemeral.Id{}, + errors.Errorf("Failed to generate nonce: %+v", err) + } else if n != messageNonceSize { + sendPrint += fmt.Sprintf(", got %d bytes for %d-byte nonce", n, + messageNonceSize) + return cryptoChannel.MessageID{}, rounds.Round{}, + ephemeral.Id{}, + errors.Errorf( + "Generated %d bytes for %d-byte nonce", n, + messageNonceSize) + } + + usrMsg := &UserMessage{ + ECCPublicKey: m.me.PubKey, + } + + //Note: we are not checking if message is too long before trying to + //find a round + + //Build the function pointer that will build the message + assemble := func(rid id.Round) ([]byte, error) { + + //Build the message + chMsg.RoundID = uint64(rid) + + //Serialize the message + chMsgSerial, err := proto.Marshal(chMsg) + if err != nil { + return nil, err + } + + //make the messageID + msgId = cryptoChannel.MakeMessageID(chMsgSerial, channelID) + + //Sign the message + messageSig := ed25519.Sign(*m.me.Privkey, chMsgSerial) + + usrMsg.Message = chMsgSerial + usrMsg.Signature = messageSig + + //Serialize the user message + usrMsgSerial, err := proto.Marshal(usrMsg) + if err != nil { + return nil, err + } + + return usrMsgSerial, nil + } + + sendPrint += fmt.Sprintf(", pending send %s", netTime.Now()) + uuid, err := m.st.denotePendingSend(channelID, &userMessageInternal{ + userMessage: usrMsg, + channelMessage: chMsg, + messageID: msgId, + }) + if err != nil { + sendPrint += fmt.Sprintf(", pending send failed %s", + err.Error()) + return cryptoChannel.MessageID{}, rounds.Round{}, + ephemeral.Id{}, err + } + + sendPrint += fmt.Sprintf(", broadcasting message %s", netTime.Now()) + r, ephid, err := ch.broadcast.BroadcastWithAssembler(assemble, params) + if err != nil { + sendPrint += fmt.Sprintf(", broadcast failed %s, %s", + netTime.Now(), err.Error()) + errDenote := m.st.failedSend(uuid) + if errDenote != nil { + sendPrint += fmt.Sprintf(", failed to denote failed "+ + "broadcast: %s", err.Error()) + } + return cryptoChannel.MessageID{}, rounds.Round{}, + ephemeral.Id{}, err + } + sendPrint += fmt.Sprintf(", broadcast succeeded %s, success!", + netTime.Now()) + err = m.st.send(uuid, msgId, r) + if err != nil { + sendPrint += fmt.Sprintf(", broadcast failed: %s ", err.Error()) + } + return msgId, r, ephid, err +} + +// SendAdminGeneric is used to send a raw message over a channel encrypted +// with admin keys, identifying it as sent by the admin. In general, it +// should be wrapped in a function which defines the wire protocol +// If the final message, before being sent over the wire, is too long, this will +// return an error. The message must be at most 510 bytes long. +func (m *manager) SendAdminGeneric(privKey rsa.PrivateKey, channelID *id.ID, + messageType MessageType, msg []byte, validUntil time.Duration, + params cmix.CMIXParams) (cryptoChannel.MessageID, rounds.Round, + ephemeral.Id, error) { + + // Note: We log sends on exit, and append what happened to the message + // this cuts down on clutter in the log. + sendPrint := fmt.Sprintf("[%s] Admin sending ch %s type %d at %s", + params.DebugTag, channelID, messageType, + netTime.Now()) + defer jww.INFO.Println(sendPrint) + + //find the channel + ch, err := m.getChannel(channelID) + if err != nil { + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err + } + + var msgId cryptoChannel.MessageID + chMsg := &ChannelMessage{ + Lease: validUntil.Nanoseconds(), + PayloadType: uint32(messageType), + Payload: msg, + Nickname: AdminUsername, + Nonce: make([]byte, messageNonceSize), + LocalTimestamp: netTime.Now().UnixNano(), + } + + // Generate random nonce to be used for message ID generation. This makes it + // so two identical messages sent on the same round have different message IDs + rng := m.rng.GetStream() + n, err := rng.Read(chMsg.Nonce) + rng.Close() + if err != nil { + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, + errors.Errorf("Failed to generate nonce: %+v", err) + } else if n != messageNonceSize { + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, + errors.Errorf( + "Generated %d bytes for %-byte nonce", n, messageNonceSize) + } + + // Note: we are not checking if message is too long before trying to + // find a round + + //Build the function pointer that will build the message + assemble := func(rid id.Round) ([]byte, error) { + + //Build the message + chMsg.RoundID = uint64(rid) + + //Serialize the message + chMsgSerial, err := proto.Marshal(chMsg) + if err != nil { + return nil, err + } + + msgId = cryptoChannel.MakeMessageID(chMsgSerial, channelID) + + //check if the message is too long + if len(chMsgSerial) > ch.broadcast.MaxRSAToPublicPayloadSize() { + return nil, MessageTooLongErr + } + + return chMsgSerial, nil + } + + sendPrint += fmt.Sprintf(", pending send %s", netTime.Now()) + uuid, err := m.st.denotePendingAdminSend(channelID, chMsg) + if err != nil { + sendPrint += fmt.Sprintf(", pending send failed %s", + err.Error()) + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err + } + + sendPrint += fmt.Sprintf(", broadcasting message %s", netTime.Now()) + r, ephid, err := ch.broadcast.BroadcastRSAToPublicWithAssembler(privKey, + assemble, params) + if err != nil { + sendPrint += fmt.Sprintf(", broadcast failed %s, %s", + netTime.Now(), err.Error()) + errDenote := m.st.failedSend(uuid) + if errDenote != nil { + sendPrint += fmt.Sprintf(", failed to denote failed "+ + "broadcast: %s", err.Error()) + jww.ERROR.Printf("Failed to update for a failed send to "+ + "%s: %+v", channelID, err) + } + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err + } + sendPrint += fmt.Sprintf(", broadcast succeeded %s, success!", + netTime.Now()) + err = m.st.send(uuid, msgId, r) + if err != nil { + sendPrint += fmt.Sprintf(", broadcast failed: %s ", err.Error()) + } + return msgId, r, ephid, err +} + +// SendMessage is used to send a formatted message over a channel. +// Due to the underlying encoding using compression, it isn't +// possible to define the largest payload that can be sent, but +// it will always be possible to send a payload of 798 bytes at minimum +func (m *manager) SendMessage(channelID *id.ID, msg string, + validUntil time.Duration, params cmix.CMIXParams) ( + cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error) { + tag := makeChaDebugTag(channelID, m.me.PubKey, []byte(msg), SendMessageTag) + jww.INFO.Printf("[%s]SendMessage(%s)", tag, channelID) + + txt := &CMIXChannelText{ + Version: cmixChannelTextVersion, + Text: msg, + ReplyMessageID: nil, + } + + params = params.SetDebugTag(tag) + + txtMarshaled, err := proto.Marshal(txt) + if err != nil { + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err + } + + return m.SendGeneric(channelID, Text, txtMarshaled, validUntil, + params) +} + +// SendReply is used to send a formatted message over a channel. +// Due to the underlying encoding using compression, it isn't +// possible to define the largest payload that can be sent, but +// it will always be possible to send a payload of 766 bytes at minimum. +// If the message ID the reply is sent to doesnt exist, the other side will +// post the message as a normal message and not a reply. +func (m *manager) SendReply(channelID *id.ID, msg string, + replyTo cryptoChannel.MessageID, validUntil time.Duration, + params cmix.CMIXParams) (cryptoChannel.MessageID, rounds.Round, + ephemeral.Id, error) { + tag := makeChaDebugTag(channelID, m.me.PubKey, []byte(msg), SendReplyTag) + jww.INFO.Printf("[%s]SendReply(%s, to %s)", tag, channelID, replyTo) + txt := &CMIXChannelText{ + Version: cmixChannelTextVersion, + Text: msg, + ReplyMessageID: replyTo[:], + } + + params = params.SetDebugTag(tag) + + txtMarshaled, err := proto.Marshal(txt) + if err != nil { + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err + } + + return m.SendGeneric(channelID, Text, txtMarshaled, validUntil, + params) +} + +// SendReaction is used to send a reaction to a message over a channel. +// The reaction must be a single emoji with no other characters, and will +// be rejected otherwise. +// Clients will drop the reaction if they do not recognize the reactTo message +func (m *manager) SendReaction(channelID *id.ID, reaction string, + reactTo cryptoChannel.MessageID, params cmix.CMIXParams) ( + cryptoChannel.MessageID, rounds.Round, ephemeral.Id, error) { + tag := makeChaDebugTag(channelID, m.me.PubKey, []byte(reaction), SendReactionTag) + jww.INFO.Printf("[%s]SendReply(%s, to %s)", tag, channelID, reactTo) + + if err := ValidateReaction(reaction); err != nil { + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err + } + + react := &CMIXChannelReaction{ + Version: cmixChannelReactionVersion, + Reaction: reaction, + ReactionMessageID: reactTo[:], + } + + params = params.SetDebugTag(tag) + + reactMarshaled, err := proto.Marshal(react) + if err != nil { + return cryptoChannel.MessageID{}, rounds.Round{}, ephemeral.Id{}, err + } + + return m.SendGeneric(channelID, Reaction, reactMarshaled, ValidForever, + params) +} + +// makeChaDebugTag is a debug helper that creates non-unique msg identifier +// This is set as the debug tag on messages and enables some level +// of tracing a message (if it's contents/chan/type are unique) +func makeChaDebugTag(channelID *id.ID, id ed25519.PublicKey, + msg []byte, baseTag string) string { + + h, _ := blake2b.New256(nil) + h.Write(channelID[:]) + h.Write(msg) + h.Write(id) + + tripcode := base64.RawStdEncoding.EncodeToString(h.Sum(nil))[:12] + return fmt.Sprintf("%s-%s", baseTag, tripcode) +} diff --git a/channels/sendTracker.go b/channels/sendTracker.go new file mode 100644 index 0000000000000000000000000000000000000000..f75819fe2ccea74308cce60ee9087a3b1fc40320 --- /dev/null +++ b/channels/sendTracker.go @@ -0,0 +1,505 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "encoding/json" + "errors" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/storage/versioned" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/netTime" + "sync" + "time" +) + +const ( + sendTrackerStorageKey = "sendTrackerStorageKey" + sendTrackerStorageVersion = 0 + + sendTrackerUnsentStorageKey = "sendTrackerUnsentStorageKey" + sendTrackerUnsentStorageVersion = 0 + + getRoundResultsTimeout = 60 * time.Second + // number of times it will attempt to get round status before the round + // is assumed to have failed. Tracking per round does not persist across + // runs + maxChecks = 3 + + oneSecond = 1000 * time.Millisecond +) + +type tracked struct { + MsgID cryptoChannel.MessageID + ChannelID *id.ID + RoundID id.Round + UUID uint64 +} + +type trackedList struct { + List []*tracked + RoundCompleted bool +} + +// the sendTracker tracks outbound messages and denotes when they are delivered +// to the event model. It also captures incoming messages and in the event they +// were sent by this user diverts them as status updates on the previously sent +// messages +type sendTracker struct { + byRound map[id.Round]trackedList + + byMessageID map[cryptoChannel.MessageID]*tracked + + unsent map[uint64]*tracked + + mux sync.RWMutex + + trigger triggerEventFunc + adminTrigger triggerAdminEventFunc + updateStatus updateStatusFunc + + net Client + + kv *versioned.KV + + rngSrc *fastRNG.StreamGenerator +} + +// messageReceiveFunc is a function type for sendTracker.MessageReceive so it +// can be mocked for testing where used +type messageReceiveFunc func(messageID cryptoChannel.MessageID, r rounds.Round) bool + +// loadSendTracker loads a sent tracker, restoring from disk. It will register a +// function with the cmix client, delayed on when the network goes healthy, +// which will attempt to discover the status of all rounds that are outstanding. +func loadSendTracker(net Client, kv *versioned.KV, trigger triggerEventFunc, + adminTrigger triggerAdminEventFunc, updateStatus updateStatusFunc, + rngSource *fastRNG.StreamGenerator) *sendTracker { + st := &sendTracker{ + byRound: make(map[id.Round]trackedList), + byMessageID: make(map[cryptoChannel.MessageID]*tracked), + unsent: make(map[uint64]*tracked), + trigger: trigger, + adminTrigger: adminTrigger, + updateStatus: updateStatus, + net: net, + kv: kv, + rngSrc: rngSource, + } + + /*if err := st.load(); !kv.Exists(err){ + jww.FATAL.Panicf("failed to load sent tracker: %+v", err) + }*/ + st.load() + + //denote all unsent messages as failed and clear + for uuid, t := range st.unsent { + updateStatus(uuid, t.MsgID, + time.Time{}, rounds.Round{}, Failed) + } + st.unsent = make(map[uint64]*tracked) + + //register to check all outstanding rounds when the network becomes healthy + var callBackID uint64 + callBackID = net.AddHealthCallback(func(f bool) { + if !f { + return + } + net.RemoveHealthCallback(callBackID) + for rid, oldTracked := range st.byRound { + + if oldTracked.RoundCompleted { + continue + } + + rr := &roundResults{ + round: rid, + st: st, + } + st.net.GetRoundResults(getRoundResultsTimeout, rr.callback, rr.round) + } + }) + + return st +} + +// store writes the list of rounds that have been +func (st *sendTracker) store() error { + + if err := st.storeSent(); err != nil { + return err + } + + return st.storeUnsent() +} + +func (st *sendTracker) storeSent() error { + + //save sent messages + data, err := json.Marshal(&st.byRound) + if err != nil { + return err + } + return st.kv.Set(sendTrackerStorageKey, &versioned.Object{ + Version: sendTrackerStorageVersion, + Timestamp: netTime.Now(), + Data: data, + }) +} + +// store writes the list of rounds that have been +func (st *sendTracker) storeUnsent() error { + //save unsent messages + data, err := json.Marshal(&st.unsent) + if err != nil { + return err + } + + return st.kv.Set(sendTrackerUnsentStorageKey, &versioned.Object{ + Version: sendTrackerUnsentStorageVersion, + Timestamp: netTime.Now(), + Data: data, + }) +} + +// load will get the stored rounds to be checked from disk and builds +// internal datastructures +func (st *sendTracker) load() error { + obj, err := st.kv.Get(sendTrackerStorageKey, sendTrackerStorageVersion) + if err != nil { + return err + } + + err = json.Unmarshal(obj.Data, &st.byRound) + if err != nil { + return err + } + + for rid := range st.byRound { + roundList := st.byRound[rid].List + for j := range roundList { + st.byMessageID[roundList[j].MsgID] = roundList[j] + } + } + + obj, err = st.kv.Get(sendTrackerUnsentStorageKey, sendTrackerUnsentStorageVersion) + if err != nil { + return err + } + + err = json.Unmarshal(obj.Data, &st.unsent) + if err != nil { + return err + } + + return nil +} + +// denotePendingSend is called before the pending send. It tracks the send +// internally and notifies the UI of the send +func (st *sendTracker) denotePendingSend(channelID *id.ID, + umi *userMessageInternal) (uint64, error) { + // for a timestamp for the message, use 1 second from now to + // approximate the lag due to round submission + ts := netTime.Now().Add(oneSecond) + + // create a random message id so there will not be collisions in a database + // that requires a unique message ID + stream := st.rngSrc.GetStream() + umi.messageID = cryptoChannel.MessageID{} + num, err := stream.Read(umi.messageID[:]) + if num != len(umi.messageID[:]) || err != nil { + jww.FATAL.Panicf("failed to get a random message ID, read "+ + "len: %d, err: %+v", num, err) + } + stream.Close() + + // submit the message to the UI + uuid, err := st.trigger(channelID, umi, ts, receptionID.EphemeralIdentity{}, + rounds.Round{}, Unsent) + if err != nil { + return 0, err + } + + // track the message on disk + st.handleDenoteSend(uuid, channelID, umi.messageID, + rounds.Round{}) + return uuid, nil +} + +// denotePendingAdminSend is called before the pending admin send. It tracks the +// send internally and notifies the UI of the send +func (st *sendTracker) denotePendingAdminSend(channelID *id.ID, + cm *ChannelMessage) (uint64, error) { + // for a timestamp for the message, use 1 second from now to + // approximate the lag due to round submission + ts := netTime.Now().Add(oneSecond) + + // create a random message id so there will not be collisions in a database + // that requires a unique message ID + stream := st.rngSrc.GetStream() + randMid := cryptoChannel.MessageID{} + num, err := stream.Read(randMid[:]) + if num != len(randMid[:]) || err != nil { + jww.FATAL.Panicf("failed to get a random message ID, read "+ + "len: %d, err: %+v", num, err) + } + stream.Close() + + // submit the message to the UI + uuid, err := st.adminTrigger(channelID, cm, ts, randMid, + receptionID.EphemeralIdentity{}, + rounds.Round{}, Unsent) + + if err != nil { + return 0, err + } + + // track the message on disk + st.handleDenoteSend(uuid, channelID, randMid, + rounds.Round{}) + return uuid, nil +} + +// handleDenoteSend does the nity gritty of editing internal structures +func (st *sendTracker) handleDenoteSend(uuid uint64, channelID *id.ID, + messageID cryptoChannel.MessageID, round rounds.Round) { + st.mux.Lock() + defer st.mux.Unlock() + + //skip if already added + _, existsMessage := st.unsent[uuid] + if existsMessage { + return + } + + st.unsent[uuid] = &tracked{messageID, channelID, round.ID, uuid} + + err := st.storeUnsent() + if err != nil { + jww.FATAL.Panicf(err.Error()) + } +} + +// send tracks a generic send message +func (st *sendTracker) send(uuid uint64, msgID cryptoChannel.MessageID, + round rounds.Round) error { + + // update the on disk message status + t, err := st.handleSend(uuid, msgID, round) + if err != nil { + return err + } + + // Modify the timestamp to reduce the chance message order will be ambiguous + ts := mutateTimestamp(round.Timestamps[states.QUEUED], msgID) + + //update the message on the UI + go st.updateStatus(t.UUID, msgID, ts, round, Sent) + return nil +} + +// send tracks a generic send message +func (st *sendTracker) failedSend(uuid uint64) error { + + // update the on disk message status + t, err := st.handleSendFailed(uuid) + if err != nil { + return err + } + + //update the message on the UI + go st.updateStatus(t.UUID, cryptoChannel.MessageID{}, time.Time{}, rounds.Round{}, Failed) + return nil +} + +// handleSend does the nity gritty of editing internal structures +func (st *sendTracker) handleSend(uuid uint64, + messageID cryptoChannel.MessageID, round rounds.Round) (*tracked, error) { + st.mux.Lock() + defer st.mux.Unlock() + + //check if in unsent + t, exists := st.unsent[uuid] + if !exists { + return nil, errors.New("cannot handle send on an unprepared message") + } + + _, existsMessage := st.byMessageID[messageID] + if existsMessage { + return nil, errors.New("cannot handle send on a message which was " + + "already sent") + } + + t.MsgID = messageID + t.RoundID = round.ID + + //add the roundID + roundsList, existsRound := st.byRound[round.ID] + roundsList.List = append(roundsList.List, t) + st.byRound[round.ID] = roundsList + + //add the round + st.byMessageID[messageID] = t + + if !existsRound { + rr := &roundResults{ + round: round.ID, + st: st, + } + st.net.GetRoundResults(getRoundResultsTimeout, rr.callback, rr.round) + } + + delete(st.unsent, uuid) + + //store the changed list to disk + err := st.store() + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + + return t, nil +} + +// handleSendFailed does the nity gritty of editing internal structures +func (st *sendTracker) handleSendFailed(uuid uint64) (*tracked, error) { + st.mux.Lock() + defer st.mux.Unlock() + + //check if in unsent + t, exists := st.unsent[uuid] + if !exists { + return nil, errors.New("cannot handle send on an unprepared message") + } + + delete(st.unsent, uuid) + + //store the changed list to disk + err := st.storeUnsent() + if err != nil { + jww.FATAL.Panicf(err.Error()) + } + + return t, nil +} + +// MessageReceive is used when a message is received to check if the message +// was sent by this user. If it was, the correct signal is sent to the event +// model and the function returns true, notifying the caller to not process +// the message +func (st *sendTracker) MessageReceive(messageID cryptoChannel.MessageID, round rounds.Round) bool { + st.mux.RLock() + + //skip if already added + _, existsMessage := st.byMessageID[messageID] + st.mux.RUnlock() + if !existsMessage { + return false + } + + st.mux.Lock() + defer st.mux.Unlock() + msgData, existsMessage := st.byMessageID[messageID] + if !existsMessage { + return false + } + + delete(st.byMessageID, messageID) + + roundList := st.byRound[msgData.RoundID] + if len(roundList.List) == 1 { + delete(st.byRound, msgData.RoundID) + } else { + newRoundList := make([]*tracked, 0, len(roundList.List)-1) + for i := range roundList.List { + if !roundList.List[i].MsgID.Equals(messageID) { + newRoundList = append(newRoundList, roundList.List[i]) + } + } + st.byRound[msgData.RoundID] = trackedList{ + List: newRoundList, + RoundCompleted: roundList.RoundCompleted, + } + } + + ts := mutateTimestamp(round.Timestamps[states.QUEUED], messageID) + go st.updateStatus(msgData.UUID, messageID, ts, + round, Delivered) + + if err := st.storeSent(); err != nil { + jww.FATAL.Panicf("failed to store the updated sent list: %+v", err) + } + + return true +} + +// roundResults represents a round which results are waiting on from the cmix layer +type roundResults struct { + round id.Round + st *sendTracker + numChecks uint +} + +// callback is called when results are known about a round. it will re-trigger +// the wait if it fails up to 'maxChecks' times. +func (rr *roundResults) callback(allRoundsSucceeded, timedOut bool, results map[id.Round]cmix.RoundResult) { + + rr.st.mux.Lock() + + //if the message was already handled, do nothing + registered, existsRound := rr.st.byRound[rr.round] + if !existsRound { + rr.st.mux.Unlock() + return + } + + status := Delivered + if !allRoundsSucceeded { + status = Failed + } + + if timedOut { + if rr.numChecks >= maxChecks { + jww.WARN.Printf("Channel messages sent on %d assumed to "+ + "have failed after %d attempts to get round status", rr.round, + maxChecks) + status = Failed + } else { + rr.numChecks++ + + rr.st.mux.Unlock() + + //retry if timed out + go rr.st.net.GetRoundResults(getRoundResultsTimeout, rr.callback, []id.Round{rr.round}...) + return + } + + } + + registered.RoundCompleted = true + rr.st.byRound[rr.round] = registered + if err := rr.st.store(); err != nil { + jww.FATAL.Panicf("failed to store update after "+ + "finalizing delivery of sent messages: %+v", err) + } + + rr.st.mux.Unlock() + if status == Failed { + for i := range registered.List { + round := results[rr.round].Round + go rr.st.updateStatus(registered.List[i].UUID, registered.List[i].MsgID, time.Time{}, + round, Failed) + } + } +} diff --git a/channels/sendTracker_test.go b/channels/sendTracker_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6948df3beee69e5ffeed5a119bda3a9240972131 --- /dev/null +++ b/channels/sendTracker_test.go @@ -0,0 +1,354 @@ +package channels + +import ( + "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/message" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/storage/versioned" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/ekv" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + "gitlab.com/xx_network/primitives/netTime" + "testing" + "time" +) + +type mockClient struct{} + +func (mc *mockClient) GetMaxMessageLength() int { + return 2048 +} +func (mc *mockClient) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler, + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + return rounds.Round{}, ephemeral.Id{}, nil +} +func (mc *mockClient) IsHealthy() bool { + return true +} +func (mc *mockClient) AddIdentity(id *id.ID, validUntil time.Time, persistent bool) {} +func (mc *mockClient) AddService(clientID *id.ID, newService message.Service, + response message.Processor) { +} +func (mc *mockClient) DeleteClientService(clientID *id.ID) {} +func (mc *mockClient) RemoveIdentity(id *id.ID) {} +func (mc *mockClient) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, + roundList ...id.Round) { +} +func (mc *mockClient) AddHealthCallback(f func(bool)) uint64 { + return 0 +} +func (mc *mockClient) RemoveHealthCallback(uint64) {} + +// Test MessageReceive basic logic +func TestSendTracker_MessageReceive(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + uuidNum := uint64(0) + rid := id.Round(2) + + r := rounds.Round{ + ID: rid, + Timestamps: make(map[states.Round]time.Time), + } + r.Timestamps[states.QUEUED] = time.Now() + trigger := func(chID *id.ID, umi *userMessageInternal, ts time.Time, + receptionID receptionID.EphemeralIdentity, round rounds.Round, + status SentStatus) (uint64, error) { + oldUUID := uuidNum + uuidNum++ + return oldUUID, nil + } + + updateStatus := func(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) { + } + + cid := id.NewIdFromString("channel", id.User, t) + + crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG) + + st := loadSendTracker(&mockClient{}, kv, trigger, nil, updateStatus, crng) + + mid := cryptoChannel.MakeMessageID([]byte("hello"), cid) + process := st.MessageReceive(mid, r) + if process { + t.Fatalf("Did not receive expected result from MessageReceive") + } + + uuid, err := st.denotePendingSend(cid, &userMessageInternal{ + userMessage: &UserMessage{}, + channelMessage: &ChannelMessage{ + Lease: netTime.Now().UnixNano(), + RoundID: uint64(rid), + PayloadType: 0, + Payload: []byte("hello"), + }}) + if err != nil { + t.Fatalf(err.Error()) + } + + err = st.send(uuid, mid, rounds.Round{ + ID: rid, + State: 1, + }) + if err != nil { + t.Fatalf(err.Error()) + } + process = st.MessageReceive(mid, r) + if !process { + t.Fatalf("Did not receive expected result from MessageReceive") + } + + cid2 := id.NewIdFromString("channel two", id.User, t) + uuid2, err := st.denotePendingSend(cid2, &userMessageInternal{ + userMessage: &UserMessage{}, + channelMessage: &ChannelMessage{ + Lease: netTime.Now().UnixNano(), + RoundID: uint64(rid), + PayloadType: 0, + Payload: []byte("hello again"), + }}) + if err != nil { + t.Fatalf(err.Error()) + } + + err = st.send(uuid2, mid, rounds.Round{ + ID: rid, + State: 1, + }) + process = st.MessageReceive(mid, r) + if !process { + t.Fatalf("Did not receive expected result from MessageReceive") + } +} + +// Test failedSend function, confirming that data is stored appropriately +// and callbacks are called +func TestSendTracker_failedSend(t *testing.T) { + triggerCh := make(chan SentStatus) + + kv := versioned.NewKV(ekv.MakeMemstore()) + + adminTrigger := func(chID *id.ID, cm *ChannelMessage, ts time.Time, + messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + } + + updateStatus := func(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) { + triggerCh <- status + } + + crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG) + + st := loadSendTracker(&mockClient{}, kv, nil, adminTrigger, updateStatus, crng) + + cid := id.NewIdFromString("channel", id.User, t) + mid := cryptoChannel.MakeMessageID([]byte("hello"), cid) + rid := id.Round(2) + uuid, err := st.denotePendingAdminSend(cid, &ChannelMessage{ + Lease: 0, + RoundID: uint64(rid), + PayloadType: 0, + Payload: []byte("hello"), + }) + if err != nil { + t.Fatalf(err.Error()) + } + + err = st.failedSend(uuid) + if err != nil { + t.Fatalf(err.Error()) + } + + timeout := time.NewTicker(time.Second * 5) + select { + case s := <-triggerCh: + if s != Failed { + t.Fatalf("Did not receive failed from failed message") + } + t.Log("Received over trigger chan") + case <-timeout.C: + t.Fatal("Timed out waiting for trigger chan") + } + + trackedRound, ok := st.byRound[rid] + if ok { + t.Fatal("Should not have found a tracked round") + } + if len(trackedRound.List) != 0 { + t.Fatal("Did not find expected number of trackedRounds") + } + + _, ok = st.byMessageID[mid] + if ok { + t.Error("Should not have found tracked message") + } + + _, ok = st.unsent[uuid] + if ok { + t.Fatal("Should not have found an unsent") + } +} + +// Test send tracker send function, confirming that data is stored appropriately +//// and callbacks are called +func TestSendTracker_send(t *testing.T) { + triggerCh := make(chan bool) + + kv := versioned.NewKV(ekv.MakeMemstore()) + trigger := func(chID *id.ID, umi *userMessageInternal, ts time.Time, + receptionID receptionID.EphemeralIdentity, round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + } + + updateStatus := func(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) { + triggerCh <- true + } + + crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG) + + st := loadSendTracker(&mockClient{}, kv, trigger, nil, updateStatus, crng) + + cid := id.NewIdFromString("channel", id.User, t) + mid := cryptoChannel.MakeMessageID([]byte("hello"), cid) + rid := id.Round(2) + uuid, err := st.denotePendingSend(cid, &userMessageInternal{ + userMessage: &UserMessage{}, + channelMessage: &ChannelMessage{ + Lease: 0, + RoundID: uint64(rid), + PayloadType: 0, + Payload: []byte("hello"), + }, + messageID: mid, + }) + if err != nil { + t.Fatalf(err.Error()) + } + + err = st.send(uuid, mid, rounds.Round{ + ID: rid, + State: 2, + }) + if err != nil { + t.Fatalf(err.Error()) + } + + timeout := time.NewTicker(time.Second * 5) + select { + case <-triggerCh: + t.Log("Received over trigger chan") + case <-timeout.C: + t.Fatal("Timed out waiting for trigger chan") + } + + trackedRound, ok := st.byRound[rid] + if !ok { + t.Fatal("Should have found a tracked round") + } + if len(trackedRound.List) != 1 { + t.Fatal("Did not find expected number of trackedRounds") + } + if trackedRound.List[0].MsgID != mid { + t.Fatalf("Did not find expected message ID in trackedRounds") + } + + trackedMsg, ok := st.byMessageID[mid] + if !ok { + t.Error("Should have found tracked message") + } + if trackedMsg.MsgID != mid { + t.Fatalf("Did not find expected message ID in byMessageID") + } +} + +// Test loading stored byRound map from storage +func TestSendTracker_load_store(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + + crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG) + + st := loadSendTracker(&mockClient{}, kv, nil, nil, nil, crng) + cid := id.NewIdFromString("channel", id.User, t) + mid := cryptoChannel.MakeMessageID([]byte("hello"), cid) + rid := id.Round(2) + st.byRound[rid] = trackedList{ + List: []*tracked{{MsgID: mid, ChannelID: cid, RoundID: rid}}, + RoundCompleted: false, + } + err := st.store() + if err != nil { + t.Fatalf("Failed to store byRound: %+v", err) + } + + st2 := loadSendTracker(&mockClient{}, kv, nil, nil, nil, crng) + if len(st2.byRound) != len(st.byRound) { + t.Fatalf("byRound was not properly loaded") + } +} + +func TestRoundResult_callback(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + triggerCh := make(chan bool) + update := func(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) { + triggerCh <- true + } + trigger := func(chID *id.ID, umi *userMessageInternal, ts time.Time, + receptionID receptionID.EphemeralIdentity, round rounds.Round, + status SentStatus) (uint64, error) { + return 0, nil + } + + crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG) + + st := loadSendTracker(&mockClient{}, kv, trigger, nil, update, crng) + + cid := id.NewIdFromString("channel", id.User, t) + mid := cryptoChannel.MakeMessageID([]byte("hello"), cid) + rid := id.Round(2) + uuid, err := st.denotePendingSend(cid, &userMessageInternal{ + userMessage: &UserMessage{}, + channelMessage: &ChannelMessage{ + Lease: 0, + RoundID: uint64(rid), + PayloadType: 0, + Payload: []byte("hello"), + }, + messageID: mid, + }) + if err != nil { + t.Fatalf(err.Error()) + } + + err = st.send(uuid, mid, rounds.Round{ + ID: rid, + State: 2, + }) + + rr := roundResults{ + round: rid, + st: st, + numChecks: 0, + } + + rr.callback(true, false, map[id.Round]cmix.RoundResult{rid: {cmix.Succeeded, rounds.Round{ + ID: rid, + State: 0, + }}}) + + timeout := time.NewTicker(time.Second * 5) + select { + case <-triggerCh: + t.Log("Received trigger") + case <-timeout.C: + t.Fatal("Did not receive update") + } +} diff --git a/channels/send_test.go b/channels/send_test.go new file mode 100644 index 0000000000000000000000000000000000000000..7c3205115a1947cf3f5a20b81d79f794959d57dd --- /dev/null +++ b/channels/send_test.go @@ -0,0 +1,618 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "bytes" + "crypto/ed25519" + "math/rand" + "sync" + "testing" + "time" + + "github.com/golang/protobuf/proto" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/client/storage/versioned" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/crypto/rsa" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/primitives/netTime" + + "gitlab.com/xx_network/primitives/id" + "gitlab.com/xx_network/primitives/id/ephemeral" + + "gitlab.com/elixxir/client/broadcast" + "gitlab.com/elixxir/client/cmix" + cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" +) + +const returnedRound = 42 + +type mockBroadcastChannel struct { + hasRun bool + + payload []byte + params cmix.CMIXParams + + pk rsa.PrivateKey + + crypto *cryptoBroadcast.Channel +} + +func (m *mockBroadcastChannel) MaxPayloadSize() int { + return 1024 +} + +func (m *mockBroadcastChannel) MaxRSAToPublicPayloadSize() int { + return 512 +} + +func (m *mockBroadcastChannel) Get() *cryptoBroadcast.Channel { + return m.crypto +} + +func (m *mockBroadcastChannel) Broadcast(payload []byte, cMixParams cmix.CMIXParams) ( + rounds.Round, ephemeral.Id, error) { + + m.hasRun = true + + m.payload = payload + m.params = cMixParams + + return rounds.Round{ID: 123}, ephemeral.Id{}, nil +} + +func (m *mockBroadcastChannel) BroadcastWithAssembler(assembler broadcast.Assembler, cMixParams cmix.CMIXParams) ( + rounds.Round, ephemeral.Id, error) { + m.hasRun = true + + var err error + + m.payload, err = assembler(returnedRound) + m.params = cMixParams + + return rounds.Round{ID: 123}, ephemeral.Id{}, err +} + +func (m *mockBroadcastChannel) BroadcastRSAtoPublic(pk rsa.PrivateKey, payload []byte, + cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + m.hasRun = true + + m.payload = payload + m.params = cMixParams + + m.pk = pk + return rounds.Round{ID: 123}, ephemeral.Id{}, nil +} + +func (m *mockBroadcastChannel) BroadcastRSAToPublicWithAssembler( + pk rsa.PrivateKey, assembler broadcast.Assembler, + cMixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + + m.hasRun = true + + var err error + + m.payload, err = assembler(returnedRound) + m.params = cMixParams + + m.pk = pk + + return rounds.Round{ID: 123}, ephemeral.Id{}, err +} + +func (m *mockBroadcastChannel) RegisterListener(listenerCb broadcast.ListenerFunc, method broadcast.Method) error { + return nil +} + +func (m *mockBroadcastChannel) Stop() { +} + +type mockNameService struct { + validChMsg bool +} + +func (m *mockNameService) GetUsername() string { + return "Alice" +} + +func (m *mockNameService) GetChannelValidationSignature() (signature []byte, lease time.Time) { + return []byte("fake validation sig"), netTime.Now() +} + +func (m *mockNameService) GetChannelPubkey() ed25519.PublicKey { + return []byte("fake pubkey") +} + +func (m *mockNameService) SignChannelMessage(message []byte) (signature []byte, err error) { + return []byte("fake sig"), nil +} + +func (m *mockNameService) ValidateChannelMessage(username string, lease time.Time, + pubKey ed25519.PublicKey, authorIDSignature []byte) bool { + return m.validChMsg +} + +func TestSendGeneric(t *testing.T) { + + nameService := new(mockNameService) + nameService.validChMsg = true + + rng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(rng) + if err != nil { + t.Fatalf(err.Error()) + } + + crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG) + + m := &manager{ + me: pi, + channels: make(map[id.ID]*joinedChannel), + mux: sync.RWMutex{}, + rng: fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG), + nicknameManager: &nicknameManager{ + byChannel: make(map[id.ID]string), + kv: nil, + }, + st: loadSendTracker(&mockBroadcastClient{}, + versioned.NewKV(ekv.MakeMemstore()), func(chID *id.ID, + umi *userMessageInternal, ts time.Time, + receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + }, func(chID *id.ID, cm *ChannelMessage, ts time.Time, + messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + }, func(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) { + }, crng), + } + + channelID := new(id.ID) + messageType := Text + msg := []byte("hello world") + validUntil := time.Hour + params := new(cmix.CMIXParams) + + mbc := &mockBroadcastChannel{} + + m.channels[*channelID] = &joinedChannel{ + broadcast: mbc, + } + + messageId, roundId, ephemeralId, err := m.SendGeneric( + channelID, + messageType, + msg, + validUntil, + *params) + if err != nil { + t.Logf("ERROR %v", err) + t.Fail() + } + t.Logf("messageId %v, roundId %v, ephemeralId %v", messageId, roundId, ephemeralId) + + // verify the message was handled correctly + + // decode the user message + umi, err := unmarshalUserMessageInternal(mbc.payload, channelID) + if err != nil { + t.Fatalf("Failed to decode the user message: %s", err) + } + + // do checks of the data + if !umi.GetMessageID().Equals(messageId) { + t.Errorf("The message IDs do not match. %s vs %s ", + umi.messageID, messageId) + } + + if !bytes.Equal(umi.GetChannelMessage().Payload, msg) { + t.Errorf("The payload does not match. %s vs %s ", + umi.GetChannelMessage().Payload, msg) + } + + if MessageType(umi.GetChannelMessage().PayloadType) != messageType { + t.Fatalf("Message types do not match, %s vs %s", + MessageType(umi.GetChannelMessage().PayloadType), messageType) + } + + if umi.GetChannelMessage().RoundID != returnedRound { + t.Errorf("The returned round is incorrect, %d vs %d", + umi.GetChannelMessage().RoundID, returnedRound) + } + +} + +func TestAdminGeneric(t *testing.T) { + + prng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(prng) + if err != nil { + t.Fatalf(err.Error()) + } + + crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG) + + m := &manager{ + channels: make(map[id.ID]*joinedChannel), + nicknameManager: &nicknameManager{ + byChannel: make(map[id.ID]string), + kv: nil, + }, + me: pi, + rng: fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG), + st: loadSendTracker(&mockBroadcastClient{}, + versioned.NewKV(ekv.MakeMemstore()), func(chID *id.ID, + umi *userMessageInternal, ts time.Time, + receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + }, func(chID *id.ID, cm *ChannelMessage, ts time.Time, + messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + }, func(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) { + }, crng), + } + + messageType := Text + msg := []byte("hello world") + validUntil := time.Hour + + rng := &csprng.SystemRNG{} + ch, priv, err := cryptoBroadcast.NewChannel( + "test", "test", cryptoBroadcast.Public, 1000, rng) + if err != nil { + t.Fatalf("Failed to generate channel: %+v", err) + } + + mbc := &mockBroadcastChannel{crypto: ch} + + m.channels[*ch.ReceptionID] = &joinedChannel{ + broadcast: mbc, + } + + messageId, roundId, ephemeralId, err := m.SendAdminGeneric(priv, + ch.ReceptionID, messageType, msg, validUntil, + cmix.GetDefaultCMIXParams()) + if err != nil { + t.Fatalf("Failed to SendAdminGeneric: %v", err) + } + t.Logf("messageId %v, roundId %v, ephemeralId %v", messageId, roundId, ephemeralId) + + // verify the message was handled correctly + + msgID := cryptoChannel.MakeMessageID(mbc.payload, ch.ReceptionID) + + if !msgID.Equals(messageId) { + t.Errorf("The message IDs do not match. %s vs %s ", + msgID, messageId) + } + + // decode the channel message + chMgs := &ChannelMessage{} + err = proto.Unmarshal(mbc.payload, chMgs) + if err != nil { + t.Fatalf("Failed to decode the channel message: %s", err) + } + + if !bytes.Equal(chMgs.Payload, msg) { + t.Errorf("Messages do not match, %s vs %s", chMgs.Payload, msg) + } + + if MessageType(chMgs.PayloadType) != messageType { + t.Errorf("Message types do not match, %s vs %s", + MessageType(chMgs.PayloadType), messageType) + } + + if chMgs.RoundID != returnedRound { + t.Errorf("The returned round is incorrect, %d vs %d", + chMgs.RoundID, returnedRound) + } +} + +func TestSendMessage(t *testing.T) { + + nameService := new(mockNameService) + nameService.validChMsg = true + + prng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(prng) + if err != nil { + t.Fatalf(err.Error()) + } + + crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG) + + m := &manager{ + me: pi, + channels: make(map[id.ID]*joinedChannel), + nicknameManager: &nicknameManager{ + byChannel: make(map[id.ID]string), + kv: nil, + }, + rng: fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG), + st: loadSendTracker(&mockBroadcastClient{}, + versioned.NewKV(ekv.MakeMemstore()), func(chID *id.ID, + umi *userMessageInternal, ts time.Time, + receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + }, func(chID *id.ID, cm *ChannelMessage, ts time.Time, + messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + }, func(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) { + }, crng), + } + + channelID := new(id.ID) + messageType := Text + msg := "hello world" + validUntil := time.Hour + params := new(cmix.CMIXParams) + + mbc := &mockBroadcastChannel{} + + m.channels[*channelID] = &joinedChannel{ + broadcast: mbc, + } + + messageId, roundId, ephemeralId, err := m.SendMessage( + channelID, + msg, + validUntil, + *params) + if err != nil { + t.Logf("ERROR %v", err) + t.Fail() + } + t.Logf("messageId %v, roundId %v, ephemeralId %v", messageId, roundId, ephemeralId) + + // verify the message was handled correctly + + // decode the user message + umi, err := unmarshalUserMessageInternal(mbc.payload, channelID) + if err != nil { + t.Fatalf("Failed to decode the user message: %s", err) + } + + // do checks of the data + if !umi.GetMessageID().Equals(messageId) { + t.Errorf("The message IDs do not match. %s vs %s ", + umi.messageID, messageId) + } + + if MessageType(umi.GetChannelMessage().PayloadType) != messageType { + t.Fatalf("Message types do not match, %s vs %s", + MessageType(umi.GetChannelMessage().PayloadType), messageType) + } + + if umi.GetChannelMessage().RoundID != returnedRound { + t.Errorf("The returned round is incorrect, %d vs %d", + umi.GetChannelMessage().RoundID, returnedRound) + } + + // decode the text message + txt := &CMIXChannelText{} + err = proto.Unmarshal(umi.GetChannelMessage().Payload, txt) + if err != nil { + t.Fatalf("Could not decode cmix channel text: %s", err) + } + + if txt.Text != msg { + t.Errorf("Content of message is incorrect: %s vs %s", txt.Text, msg) + } + + if txt.ReplyMessageID != nil { + t.Errorf("Reply ID on a text message is not nil") + } +} + +func TestSendReply(t *testing.T) { + + prng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(prng) + if err != nil { + t.Fatalf(err.Error()) + } + + crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG) + + m := &manager{ + me: pi, + channels: make(map[id.ID]*joinedChannel), + nicknameManager: &nicknameManager{ + byChannel: make(map[id.ID]string), + kv: nil, + }, + rng: fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG), + st: loadSendTracker(&mockBroadcastClient{}, + versioned.NewKV(ekv.MakeMemstore()), func(chID *id.ID, + umi *userMessageInternal, ts time.Time, + receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + }, func(chID *id.ID, cm *ChannelMessage, ts time.Time, + messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + }, func(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) { + }, crng), + } + + channelID := new(id.ID) + messageType := Text + msg := "hello world" + validUntil := time.Hour + params := new(cmix.CMIXParams) + + replyMsgID := cryptoChannel.MessageID{} + replyMsgID[0] = 69 + + mbc := &mockBroadcastChannel{} + + m.channels[*channelID] = &joinedChannel{ + broadcast: mbc, + } + + messageId, roundId, ephemeralId, err := m.SendReply( + channelID, msg, replyMsgID, validUntil, *params) + if err != nil { + t.Logf("ERROR %v", err) + t.Fail() + } + t.Logf("messageId %v, roundId %v, ephemeralId %v", messageId, roundId, ephemeralId) + + // verify the message was handled correctly + + // decode the user message + umi, err := unmarshalUserMessageInternal(mbc.payload, channelID) + if err != nil { + t.Fatalf("Failed to decode the user message: %s", err) + } + + // do checks of the data + if !umi.GetMessageID().Equals(messageId) { + t.Errorf("The message IDs do not match. %s vs %s ", + umi.messageID, messageId) + } + + if MessageType(umi.GetChannelMessage().PayloadType) != messageType { + t.Fatalf("Message types do not match, %s vs %s", + MessageType(umi.GetChannelMessage().PayloadType), messageType) + } + + if umi.GetChannelMessage().RoundID != returnedRound { + t.Errorf("The returned round is incorrect, %d vs %d", + umi.GetChannelMessage().RoundID, returnedRound) + } + + // decode the text message + txt := &CMIXChannelText{} + err = proto.Unmarshal(umi.GetChannelMessage().Payload, txt) + if err != nil { + t.Fatalf("Could not decode cmix channel text: %s", err) + } + + if txt.Text != msg { + t.Errorf("Content of message is incorrect: %s vs %s", txt.Text, msg) + } + + if !bytes.Equal(txt.ReplyMessageID, replyMsgID[:]) { + t.Errorf("The reply message ID is not what was passed in") + } +} + +func TestSendReaction(t *testing.T) { + + prng := rand.New(rand.NewSource(64)) + + pi, err := cryptoChannel.GenerateIdentity(prng) + if err != nil { + t.Fatalf(err.Error()) + } + + crng := fastRNG.NewStreamGenerator(100, 5, csprng.NewSystemRNG) + + m := &manager{ + me: pi, + nicknameManager: &nicknameManager{ + byChannel: make(map[id.ID]string), + kv: nil, + }, + rng: fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG), + channels: make(map[id.ID]*joinedChannel), + st: loadSendTracker(&mockBroadcastClient{}, + versioned.NewKV(ekv.MakeMemstore()), func(chID *id.ID, + umi *userMessageInternal, ts time.Time, + receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + }, func(chID *id.ID, cm *ChannelMessage, ts time.Time, + messageID cryptoChannel.MessageID, receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) (uint64, error) { + return 0, nil + }, func(uuid uint64, messageID cryptoChannel.MessageID, + timestamp time.Time, round rounds.Round, status SentStatus) { + }, crng), + } + + channelID := new(id.ID) + messageType := Reaction + msg := "ðŸ†" + params := new(cmix.CMIXParams) + + replyMsgID := cryptoChannel.MessageID{} + replyMsgID[0] = 69 + + mbc := &mockBroadcastChannel{} + + m.channels[*channelID] = &joinedChannel{ + broadcast: mbc, + } + + messageId, roundId, ephemeralId, err := m.SendReaction( + channelID, msg, replyMsgID, *params) + if err != nil { + t.Logf("ERROR %v", err) + t.Fail() + } + t.Logf("messageId %v, roundId %v, ephemeralId %v", messageId, roundId, ephemeralId) + + // verify the message was handled correctly + + // decode the user message + umi, err := unmarshalUserMessageInternal(mbc.payload, channelID) + if err != nil { + t.Fatalf("Failed to decode the user message: %s", err) + } + + // do checks of the data + if !umi.GetMessageID().Equals(messageId) { + t.Errorf("The message IDs do not match. %s vs %s ", + umi.messageID, messageId) + } + + if MessageType(umi.GetChannelMessage().PayloadType) != messageType { + t.Fatalf("Message types do not match, %s vs %s", + MessageType(umi.GetChannelMessage().PayloadType), messageType) + } + + if umi.GetChannelMessage().RoundID != returnedRound { + t.Errorf("The returned round is incorrect, %d vs %d", + umi.GetChannelMessage().RoundID, returnedRound) + } + + // decode the text message + txt := &CMIXChannelReaction{} + err = proto.Unmarshal(umi.GetChannelMessage().Payload, txt) + if err != nil { + t.Fatalf("Could not decode cmix channel text: %s", err) + } + + if txt.Reaction != msg { + t.Errorf("Content of message is incorrect: %s vs %s", txt.Reaction, msg) + } + + if !bytes.Equal(txt.ReactionMessageID, replyMsgID[:]) { + t.Errorf("The reply message ID is not what was passed in") + } +} diff --git a/channels/text.pb.go b/channels/text.pb.go new file mode 100644 index 0000000000000000000000000000000000000000..5eea7e0ed48b0a5decd88d14d002fbf24c03b39a --- /dev/null +++ b/channels/text.pb.go @@ -0,0 +1,259 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.27.1 +// protoc v3.15.6 +// source: text.proto + +package channels + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + reflect "reflect" + sync "sync" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// CMIXChannelText is the payload for sending normal text messages to channels +// the replyMessageID is nil when it is not a reply +type CMIXChannelText struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Text string `protobuf:"bytes,2,opt,name=text,proto3" json:"text,omitempty"` + ReplyMessageID []byte `protobuf:"bytes,3,opt,name=replyMessageID,proto3" json:"replyMessageID,omitempty"` +} + +func (x *CMIXChannelText) Reset() { + *x = CMIXChannelText{} + if protoimpl.UnsafeEnabled { + mi := &file_text_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CMIXChannelText) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CMIXChannelText) ProtoMessage() {} + +func (x *CMIXChannelText) ProtoReflect() protoreflect.Message { + mi := &file_text_proto_msgTypes[0] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CMIXChannelText.ProtoReflect.Descriptor instead. +func (*CMIXChannelText) Descriptor() ([]byte, []int) { + return file_text_proto_rawDescGZIP(), []int{0} +} + +func (x *CMIXChannelText) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *CMIXChannelText) GetText() string { + if x != nil { + return x.Text + } + return "" +} + +func (x *CMIXChannelText) GetReplyMessageID() []byte { + if x != nil { + return x.ReplyMessageID + } + return nil +} + +// CMIXChannelReaction is the payload for reactions. The reaction must be a +// single emoji and the reactionMessageID must be non nil and a real message +// in the channel +type CMIXChannelReaction struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Version uint32 `protobuf:"varint,1,opt,name=version,proto3" json:"version,omitempty"` + Reaction string `protobuf:"bytes,2,opt,name=reaction,proto3" json:"reaction,omitempty"` + ReactionMessageID []byte `protobuf:"bytes,3,opt,name=reactionMessageID,proto3" json:"reactionMessageID,omitempty"` +} + +func (x *CMIXChannelReaction) Reset() { + *x = CMIXChannelReaction{} + if protoimpl.UnsafeEnabled { + mi := &file_text_proto_msgTypes[1] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) + } +} + +func (x *CMIXChannelReaction) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*CMIXChannelReaction) ProtoMessage() {} + +func (x *CMIXChannelReaction) ProtoReflect() protoreflect.Message { + mi := &file_text_proto_msgTypes[1] + if protoimpl.UnsafeEnabled && x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use CMIXChannelReaction.ProtoReflect.Descriptor instead. +func (*CMIXChannelReaction) Descriptor() ([]byte, []int) { + return file_text_proto_rawDescGZIP(), []int{1} +} + +func (x *CMIXChannelReaction) GetVersion() uint32 { + if x != nil { + return x.Version + } + return 0 +} + +func (x *CMIXChannelReaction) GetReaction() string { + if x != nil { + return x.Reaction + } + return "" +} + +func (x *CMIXChannelReaction) GetReactionMessageID() []byte { + if x != nil { + return x.ReactionMessageID + } + return nil +} + +var File_text_proto protoreflect.FileDescriptor + +var file_text_proto_rawDesc = []byte{ + 0x0a, 0x0a, 0x74, 0x65, 0x78, 0x74, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x12, 0x08, 0x63, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, 0x22, 0x67, 0x0a, 0x0f, 0x43, 0x4d, 0x49, 0x58, 0x43, 0x68, + 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x54, 0x65, 0x78, 0x74, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, + 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x74, 0x65, 0x78, 0x74, 0x18, 0x02, 0x20, 0x01, 0x28, + 0x09, 0x52, 0x04, 0x74, 0x65, 0x78, 0x74, 0x12, 0x26, 0x0a, 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x79, + 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, + 0x0e, 0x72, 0x65, 0x70, 0x6c, 0x79, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x44, 0x22, + 0x79, 0x0a, 0x13, 0x43, 0x4d, 0x49, 0x58, 0x43, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x52, 0x65, + 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x18, 0x0a, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0d, 0x52, 0x07, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, + 0x12, 0x1a, 0x0a, 0x08, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x2c, 0x0a, 0x11, + 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, + 0x44, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x11, 0x72, 0x65, 0x61, 0x63, 0x74, 0x69, 0x6f, + 0x6e, 0x4d, 0x65, 0x73, 0x73, 0x61, 0x67, 0x65, 0x49, 0x44, 0x42, 0x24, 0x5a, 0x22, 0x67, 0x69, + 0x74, 0x6c, 0x61, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x65, 0x6c, 0x69, 0x78, 0x78, 0x69, 0x72, + 0x2f, 0x63, 0x6c, 0x69, 0x65, 0x6e, 0x74, 0x2f, 0x63, 0x68, 0x61, 0x6e, 0x6e, 0x65, 0x6c, 0x73, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, +} + +var ( + file_text_proto_rawDescOnce sync.Once + file_text_proto_rawDescData = file_text_proto_rawDesc +) + +func file_text_proto_rawDescGZIP() []byte { + file_text_proto_rawDescOnce.Do(func() { + file_text_proto_rawDescData = protoimpl.X.CompressGZIP(file_text_proto_rawDescData) + }) + return file_text_proto_rawDescData +} + +var file_text_proto_msgTypes = make([]protoimpl.MessageInfo, 2) +var file_text_proto_goTypes = []interface{}{ + (*CMIXChannelText)(nil), // 0: channels.CMIXChannelText + (*CMIXChannelReaction)(nil), // 1: channels.CMIXChannelReaction +} +var file_text_proto_depIdxs = []int32{ + 0, // [0:0] is the sub-list for method output_type + 0, // [0:0] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_text_proto_init() } +func file_text_proto_init() { + if File_text_proto != nil { + return + } + if !protoimpl.UnsafeEnabled { + file_text_proto_msgTypes[0].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CMIXChannelText); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + file_text_proto_msgTypes[1].Exporter = func(v interface{}, i int) interface{} { + switch v := v.(*CMIXChannelReaction); i { + case 0: + return &v.state + case 1: + return &v.sizeCache + case 2: + return &v.unknownFields + default: + return nil + } + } + } + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: file_text_proto_rawDesc, + NumEnums: 0, + NumMessages: 2, + NumExtensions: 0, + NumServices: 0, + }, + GoTypes: file_text_proto_goTypes, + DependencyIndexes: file_text_proto_depIdxs, + MessageInfos: file_text_proto_msgTypes, + }.Build() + File_text_proto = out.File + file_text_proto_rawDesc = nil + file_text_proto_goTypes = nil + file_text_proto_depIdxs = nil +} diff --git a/channels/text.proto b/channels/text.proto new file mode 100644 index 0000000000000000000000000000000000000000..26cc59ca76ed8b888b9969730ad9fe2e1fe80d8b --- /dev/null +++ b/channels/text.proto @@ -0,0 +1,29 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +syntax = "proto3"; + +option go_package = "gitlab.com/elixxir/client/channels"; + +package channels; + +// CMIXChannelText is the payload for sending normal text messages to channels +// the replyMessageID is nil when it is not a reply +message CMIXChannelText { + uint32 version = 1; + string text = 2; + bytes replyMessageID = 3; +} + +// CMIXChannelReaction is the payload for reactions. The reaction must be a +// single emoji and the reactionMessageID must be non nil and a real message +// in the channel +message CMIXChannelReaction { + uint32 version = 1; + string reaction = 2; + bytes reactionMessageID = 3; +} \ No newline at end of file diff --git a/channels/userListener.go b/channels/userListener.go new file mode 100644 index 0000000000000000000000000000000000000000..202e5fa7d93b4ba15485cc2549806e0b8b014dea --- /dev/null +++ b/channels/userListener.go @@ -0,0 +1,82 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "crypto/ed25519" + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/primitives/id" + "time" +) + +// the userListener adheres to the [broadcast.ListenerFunc] interface and is +// used when user messages are received on the channel +type userListener struct { + name NameService + chID *id.ID + trigger triggerEventFunc + checkSent messageReceiveFunc +} + +// Listen is called when a message is received for the user listener +func (ul *userListener) Listen(payload []byte, + receptionID receptionID.EphemeralIdentity, round rounds.Round) { + + //Decode the message as a user message + umi, err := unmarshalUserMessageInternal(payload, ul.chID) + if err != nil { + jww.WARN.Printf("Failed to unmarshal User Message on "+ + "channel %s", ul.chID) + return + } + + um := umi.GetUserMessage() + cm := umi.GetChannelMessage() + msgID := umi.GetMessageID() + + //check if we sent the message, ignore triggering if we sent + if ul.checkSent(msgID, round) { + return + } + + /*CRYPTOGRAPHICALLY RELEVANT CHECKS*/ + + // check the round to ensure the message is not a replay + if id.Round(cm.RoundID) != round.ID { + jww.WARN.Printf("The round message %s send on %d referenced "+ + "(%d) was not the same as the round the message was found on (%d)", + msgID, ul.chID, cm.RoundID, round.ID) + return + } + + // check that the user properly signed the message + if !ed25519.Verify(um.ECCPublicKey, um.Message, um.Signature) { + jww.WARN.Printf("Message %s on channel %s purportedly from %s "+ + "failed its user signature with signature %v", msgID, + ul.chID, cm.Nickname, um.Signature) + return + } + + // Replace the timestamp on the message if it is outside of the + // allowable range + ts := vetTimestamp(time.Unix(0, cm.LocalTimestamp), round.Timestamps[states.QUEUED], msgID) + + //TODO: Processing of the message relative to admin commands will be here + + //Submit the message to the event model for listening + if uuid, err := ul.trigger(ul.chID, umi, ts, receptionID, round, + Delivered); err != nil { + jww.WARN.Printf("Error in passing off trigger for "+ + "message (UUID: %d): %+v", uuid, err) + } + + return +} diff --git a/channels/userListener_test.go b/channels/userListener_test.go new file mode 100644 index 0000000000000000000000000000000000000000..23d8ae8c1188ef7c717680f4b6a980dace81e0cc --- /dev/null +++ b/channels/userListener_test.go @@ -0,0 +1,358 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file // +//////////////////////////////////////////////////////////////////////////////// + +package channels + +import ( + "bytes" + "crypto/ed25519" + "gitlab.com/xx_network/primitives/netTime" + "math/rand" + "testing" + "time" + + "github.com/golang/protobuf/proto" + + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" + cryptoChannel "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/primitives/states" + "gitlab.com/xx_network/primitives/id" +) + +type triggerEventDummy struct { + gotData bool + + chID *id.ID + umi *userMessageInternal + msgID cryptoChannel.MessageID + receptionID receptionID.EphemeralIdentity + round rounds.Round +} + +func (ted *triggerEventDummy) triggerEvent(chID *id.ID, umi *userMessageInternal, + ts time.Time, receptionID receptionID.EphemeralIdentity, round rounds.Round, + sent SentStatus) (uint64, error) { + ted.gotData = true + + ted.chID = chID + ted.umi = umi + ted.receptionID = receptionID + ted.round = round + ted.msgID = umi.GetMessageID() + + return 0, nil +} + +// Tests the happy path +func TestUserListener_Listen(t *testing.T) { + + //build inputs + chID := &id.ID{} + chID[0] = 1 + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + rng := rand.New(rand.NewSource(42)) + pub, priv, err := ed25519.GenerateKey(rng) + if err != nil { + t.Fatalf("failed to generate ed25519 keypair, cant run test") + } + + cm := &ChannelMessage{ + Lease: int64(time.Hour), + RoundID: uint64(r.ID), + PayloadType: 42, + Payload: []byte("blarg"), + } + + cmSerial, err := proto.Marshal(cm) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + msgID := cryptoChannel.MakeMessageID(cmSerial, chID) + + sig := ed25519.Sign(priv, cmSerial) + ns := &mockNameService{validChMsg: true} + + um := &UserMessage{ + Message: cmSerial, + Signature: sig, + ECCPublicKey: pub, + } + + umSerial, err := proto.Marshal(um) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + //build the listener + dummy := &triggerEventDummy{} + + al := userListener{ + chID: chID, + name: ns, + trigger: dummy.triggerEvent, + checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false }, + } + + //call the listener + al.Listen(umSerial, receptionID.EphemeralIdentity{}, r) + + //check the results + if !dummy.gotData { + t.Fatalf("No data returned after valid listen") + } + + if !dummy.chID.Cmp(chID) { + t.Errorf("Channel ID not correct: %s vs %s", dummy.chID, chID) + } + + if !bytes.Equal(um.Message, dummy.umi.userMessage.Message) { + t.Errorf("message not correct: %s vs %s", um.Message, + dummy.umi.userMessage.Message) + } + + if !msgID.Equals(dummy.msgID) { + t.Errorf("messageIDs not correct: %s vs %s", msgID, + dummy.msgID) + } + + if r.ID != dummy.round.ID { + t.Errorf("rounds not correct: %s vs %s", r.ID, + dummy.round.ID) + } +} + +//tests that the message is rejected when the user signature is invalid +func TestUserListener_Listen_BadUserSig(t *testing.T) { + + //build inputs + chID := &id.ID{} + chID[0] = 1 + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + rng := rand.New(rand.NewSource(42)) + pub, _, err := ed25519.GenerateKey(rng) + if err != nil { + t.Fatalf("failed to generate ed25519 keypair, cant run test") + } + + cm := &ChannelMessage{ + Lease: int64(time.Hour), + RoundID: uint64(r.ID), + PayloadType: 42, + Payload: []byte("blarg"), + } + + cmSerial, err := proto.Marshal(cm) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + _, badpriv, err := ed25519.GenerateKey(rng) + if err != nil { + t.Fatalf("failed to generate ed25519 keypair, cant run test") + } + + sig := ed25519.Sign(badpriv, cmSerial) + ns := &mockNameService{validChMsg: true} + + um := &UserMessage{ + Message: cmSerial, + Signature: sig, + ECCPublicKey: pub, + } + + umSerial, err := proto.Marshal(um) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + //build the listener + dummy := &triggerEventDummy{} + + al := userListener{ + chID: chID, + name: ns, + trigger: dummy.triggerEvent, + checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false }, + } + + //call the listener + al.Listen(umSerial, receptionID.EphemeralIdentity{}, r) + + //check the results + if dummy.gotData { + t.Fatalf("Data returned after invalid listen") + } +} + +//tests that the message is rejected when the round in the message does not +//match the round passed in +func TestUserListener_Listen_BadRound(t *testing.T) { + + //build inputs + chID := &id.ID{} + chID[0] = 1 + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + rng := rand.New(rand.NewSource(42)) + pub, priv, err := ed25519.GenerateKey(rng) + if err != nil { + t.Fatalf("failed to generate ed25519 keypair, cant run test") + } + + cm := &ChannelMessage{ + Lease: int64(time.Hour), + //make the round not match + RoundID: 69, + PayloadType: 42, + Payload: []byte("blarg"), + } + + cmSerial, err := proto.Marshal(cm) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + sig := ed25519.Sign(priv, cmSerial) + ns := &mockNameService{validChMsg: true} + + um := &UserMessage{ + Message: cmSerial, + Signature: sig, + ECCPublicKey: pub, + } + + umSerial, err := proto.Marshal(um) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + //build the listener + dummy := &triggerEventDummy{} + + al := userListener{ + chID: chID, + name: ns, + trigger: dummy.triggerEvent, + checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false }, + } + + //call the listener + al.Listen(umSerial, receptionID.EphemeralIdentity{}, r) + + //check the results + if dummy.gotData { + t.Fatalf("Data returned after invalid listen") + } +} + +//tests that the message is rejected when the user message is malformed +func TestUserListener_Listen_BadMessage(t *testing.T) { + + //build inputs + chID := &id.ID{} + chID[0] = 1 + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + ns := &mockNameService{validChMsg: true} + + umSerial := []byte("malformed") + + //build the listener + dummy := &triggerEventDummy{} + + al := userListener{ + chID: chID, + name: ns, + trigger: dummy.triggerEvent, + checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false }, + } + + //call the listener + al.Listen(umSerial, receptionID.EphemeralIdentity{}, r) + + //check the results + if dummy.gotData { + t.Fatalf("Data returned after invalid listen") + } +} + +//tests that the message is rejected when the sized broadcast is malformed +func TestUserListener_Listen_BadSizedBroadcast(t *testing.T) { + + //build inputs + chID := &id.ID{} + chID[0] = 1 + + r := rounds.Round{ID: 420, Timestamps: make(map[states.Round]time.Time)} + r.Timestamps[states.QUEUED] = netTime.Now() + + rng := rand.New(rand.NewSource(42)) + pub, priv, err := ed25519.GenerateKey(rng) + if err != nil { + t.Fatalf("failed to generate ed25519 keypair, cant run test") + } + + cm := &ChannelMessage{ + Lease: int64(time.Hour), + //make the round not match + RoundID: 69, + PayloadType: 42, + Payload: []byte("blarg"), + } + + cmSerial, err := proto.Marshal(cm) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + sig := ed25519.Sign(priv, cmSerial) + ns := &mockNameService{validChMsg: true} + + um := &UserMessage{ + Message: cmSerial, + Signature: sig, + ECCPublicKey: pub, + } + + umSerial, err := proto.Marshal(um) + if err != nil { + t.Fatalf("Failed to marshal proto: %+v", err) + } + + //remove half the sized broadcast to make it malformed + umSerial = umSerial[:len(umSerial)/2] + + //build the listener + dummy := &triggerEventDummy{} + + al := userListener{ + chID: chID, + name: ns, + trigger: dummy.triggerEvent, + checkSent: func(messageID cryptoChannel.MessageID, r rounds.Round) bool { return false }, + } + + //call the listener + al.Listen(umSerial, receptionID.EphemeralIdentity{}, r) + + //check the results + if dummy.gotData { + t.Fatalf("Data returned after invalid listen") + } +} diff --git a/cmd/broadcast.go b/cmd/broadcast.go index 9b757490df44dac7493b6522a698b9ae17d7fc99..df52c82f13ae29ae780670a9a502d68be86e2142 100644 --- a/cmd/broadcast.go +++ b/cmd/broadcast.go @@ -20,7 +20,7 @@ import ( "gitlab.com/elixxir/client/cmix/identity/receptionID" "gitlab.com/elixxir/client/cmix/rounds" crypto "gitlab.com/elixxir/crypto/broadcast" - "gitlab.com/xx_network/crypto/signature/rsa" + rsa2 "gitlab.com/elixxir/crypto/rsa" "gitlab.com/xx_network/primitives/utils" "sync" ) @@ -56,7 +56,7 @@ var broadcastCmd = &cobra.Command{ waitUntilConnected(connected) /* Set up underlying crypto broadcast.Channel */ var channel *crypto.Channel - var pk *rsa.PrivateKey + var pk rsa2.PrivateKey keyPath := viper.GetString(broadcastKeyPathFlag) path, err := utils.ExpandPath(viper.GetString(broadcastChanPathFlag)) if utils.Exists(path) { @@ -81,23 +81,26 @@ var broadcastCmd = &cobra.Command{ if viper.GetBool(broadcastNewFlag) { // Create a new broadcast channel - channel, pk, err = crypto.NewChannel(name, desc, user.GetRng().GetStream()) + channel, pk, err = crypto.NewChannel(name, desc, crypto.Public, + user.GetCmix().GetMaxMessageLength(), user.GetRng().GetStream()) if err != nil { jww.FATAL.Panicf("Failed to create new channel: %+v", err) } if keyPath != "" { - err = utils.WriteFile(keyPath, rsa.CreatePrivateKeyPem(pk), os.ModePerm, os.ModeDir) + err = utils.WriteFile(keyPath, pk.MarshalPem(), os.ModePerm, os.ModeDir) if err != nil { jww.ERROR.Printf("Failed to write private key to path %s: %+v", path, err) } } else { - fmt.Printf("Private key generated for channel: %+v", rsa.CreatePrivateKeyPem(pk)) + fmt.Printf("Private key generated for channel: %+v", pk.MarshalPem()) } fmt.Printf("New broadcast channel generated") } else { + //fixme: redo channels, should be using pretty print over cli + // Read rest of info from config & build object manually - pubKeyBytes := []byte(viper.GetString(broadcastRsaPubFlag)) + /*pubKeyBytes := []byte(viper.GetString(broadcastRsaPubFlag)) pubKey, err := rsa.LoadPublicKeyFromPem(pubKeyBytes) if err != nil { jww.FATAL.Panicf("Failed to load public key at path: %+v", err) @@ -115,7 +118,7 @@ var broadcastCmd = &cobra.Command{ Description: desc, Salt: salt, RsaPubKey: pubKey, - } + }*/ } // Save channel to disk @@ -142,7 +145,8 @@ var broadcastCmd = &cobra.Command{ if err != nil { jww.ERROR.Printf("Failed to read private key from %s: %+v", ep, err) } - pk, err = rsa.LoadPrivateKeyFromPem(keyBytes) + + pk, err = rsa2.GetScheme().UnmarshalPrivateKeyPEM(keyBytes) if err != nil { jww.ERROR.Printf("Failed to load private key %+v: %+v", keyBytes, err) } @@ -158,7 +162,7 @@ var broadcastCmd = &cobra.Command{ asymmetric := viper.GetString(broadcastAsymmetricFlag) // Connect to broadcast channel - bcl, err := broadcast.NewBroadcastChannel(*channel, user.GetCmix(), user.GetRng()) + bcl, err := broadcast.NewBroadcastChannel(channel, user.GetCmix(), user.GetRng()) // Create & register symmetric receiver callback receiveChan := make(chan []byte, 100) @@ -179,7 +183,7 @@ var broadcastCmd = &cobra.Command{ jww.INFO.Printf("Received asymmetric message from %s over round %d", receptionID, round.ID) asymmetricReceiveChan <- payload } - err = bcl.RegisterListener(acb, broadcast.Asymmetric) + err = bcl.RegisterListener(acb, broadcast.RSAToPublic) if err != nil { jww.FATAL.Panicf("Failed to register asymmetric listener: %+v", err) } @@ -205,39 +209,30 @@ var broadcastCmd = &cobra.Command{ /* Send symmetric broadcast */ if symmetric != "" { - // Create properly sized broadcast message - broadcastMessage, err := broadcast.NewSizedBroadcast(bcl.MaxPayloadSize(), []byte(symmetric)) - if err != nil { - jww.FATAL.Panicf("Failed to create sized broadcast: %+v", err) - } - rid, eid, err := bcl.Broadcast(broadcastMessage, cmix.GetDefaultCMIXParams()) + rid, eid, err := bcl.Broadcast([]byte(symmetric), cmix.GetDefaultCMIXParams()) if err != nil { jww.ERROR.Printf("Failed to send symmetric broadcast message: %+v", err) retries++ continue } fmt.Printf("Sent symmetric broadcast message: %s", symmetric) - jww.INFO.Printf("Sent symmetric broadcast message to %s over round %d", eid, rid) + jww.INFO.Printf("Sent symmetric broadcast message to %s over round %d", eid, rid.ID) } /* Send asymmetric broadcast */ if asymmetric != "" { // Create properly sized broadcast message - broadcastMessage, err := broadcast.NewSizedBroadcast(bcl.MaxAsymmetricPayloadSize(), []byte(asymmetric)) - if err != nil { - jww.FATAL.Panicf("Failed to create sized broadcast: %+v", err) - } if pk == nil { jww.FATAL.Panicf("CANNOT SEND ASYMMETRIC BROADCAST WITHOUT PRIVATE KEY") } - rid, eid, err := bcl.BroadcastAsymmetric(pk, broadcastMessage, cmix.GetDefaultCMIXParams()) + rid, eid, err := bcl.BroadcastRSAtoPublic(pk, []byte(asymmetric), cmix.GetDefaultCMIXParams()) if err != nil { jww.ERROR.Printf("Failed to send asymmetric broadcast message: %+v", err) retries++ continue } fmt.Printf("Sent asymmetric broadcast message: %s", asymmetric) - jww.INFO.Printf("Sent asymmetric broadcast message to %s over round %d", eid, rid) + jww.INFO.Printf("Sent asymmetric broadcast message to %s over round %d", eid, rid.ID) } wg.Done() @@ -261,23 +256,13 @@ var broadcastCmd = &cobra.Command{ select { case receivedPayload := <-asymmetricReceiveChan: receivedCount++ - receivedBroadcast, err := broadcast.DecodeSizedBroadcast(receivedPayload) - if err != nil { - jww.ERROR.Printf("Failed to decode sized broadcast: %+v", err) - continue - } - fmt.Printf("Asymmetric broadcast message received: %s\n", string(receivedBroadcast)) + fmt.Printf("Asymmetric broadcast message received: %s\n", string(receivedPayload)) if receivedCount == expectedCnt { done = true } case receivedPayload := <-receiveChan: receivedCount++ - receivedBroadcast, err := broadcast.DecodeSizedBroadcast(receivedPayload) - if err != nil { - jww.ERROR.Printf("Failed to decode sized broadcast: %+v", err) - continue - } - fmt.Printf("Symmetric broadcast message received: %s\n", string(receivedBroadcast)) + fmt.Printf("Symmetric broadcast message received: %s\n", string(receivedPayload)) if receivedCount == expectedCnt { done = true } diff --git a/cmd/group.go b/cmd/group.go index 8f8bb6a3932450fd9f68af0649ddb3b1ecef8f8a..0cf296b4cca2bdc8f2206eb40b4a15367df3a167 100644 --- a/cmd/group.go +++ b/cmd/group.go @@ -237,7 +237,7 @@ func sendGroup(groupIdString string, msg []byte, gm groupChat.GroupChat) { } jww.INFO.Printf("[GC] Sent to group %s on round %d at %s", - groupID, rid, timestamp) + groupID, rid.ID, timestamp) fmt.Printf("Sent message %q to group.\n", msg) } diff --git a/cmd/root.go b/cmd/root.go index 33ae2745e7a35176a14d1b1bc39f057cbaa85de6..81d197451d72494298a87613c71ec24a44c86d5f 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -671,14 +671,8 @@ func acceptChannelVerified(user *xxdk.E2e, recipientID *id.ID, rid := acceptChannel(user, recipientID) // Monitor rounds for results - err := user.GetCmix().GetRoundResults(roundTimeout, + user.GetCmix().GetRoundResults(roundTimeout, makeVerifySendsCallback(retryChan, done), rid) - if err != nil { - jww.DEBUG.Printf("Could not verify "+ - "confirmation message for relationship with %s were sent "+ - "successfully, resending messages...", recipientID) - continue - } select { case <-retryChan: @@ -712,14 +706,9 @@ func requestChannelVerified(user *xxdk.E2e, } // Monitor rounds for results - err = user.GetCmix().GetRoundResults(roundTimeout, + user.GetCmix().GetRoundResults(roundTimeout, makeVerifySendsCallback(retryChan, done), rid) - if err != nil { - jww.DEBUG.Printf("Could not verify auth request was sent " + - "successfully, resending...") - continue - } select { case <-retryChan: @@ -751,14 +740,9 @@ func resetChannelVerified(user *xxdk.E2e, recipientContact contact.Contact, } // Monitor rounds for results - err = user.GetCmix().GetRoundResults(roundTimeout, + user.GetCmix().GetRoundResults(roundTimeout, makeVerifySendsCallback(retryChan, done), rid) - if err != nil { - jww.DEBUG.Printf("Could not verify auth request was sent " + - "successfully, resending...") - continue - } select { case <-retryChan: diff --git a/cmd/utils.go b/cmd/utils.go index 3752dcc7e9e548181b3982db440c39c1a5d3abb5..ad6de14de40c1b265dca23bb9b996c0e8f4b5e9d 100644 --- a/cmd/utils.go +++ b/cmd/utils.go @@ -53,13 +53,8 @@ func verifySendSuccess(user *xxdk.E2e, paramsE2E e2e.Params, } // Monitor rounds for results - err := user.GetCmix().GetRoundResults( + user.GetCmix().GetRoundResults( paramsE2E.CMIXParams.Timeout, f, roundIDs...) - if err != nil { - jww.DEBUG.Printf("Could not verify messages were sent " + - "successfully, resending messages...") - return false - } select { case <-retryChan: diff --git a/cmd/version.go b/cmd/version.go index 328c9cb0e5bd41b410b01bf498bf0f59997e041c..fcfa3b927bbe2f5669ee881fce0dfd30beb3d20a 100644 --- a/cmd/version.go +++ b/cmd/version.go @@ -18,7 +18,7 @@ import ( ) // Change this value to set the version for this build -const currentVersion = "4.2.0" +const currentVersion = "4.3.0" func Version() string { out := fmt.Sprintf("Elixxir Client v%s -- %s\n\n", xxdk.SEMVER, diff --git a/cmix/attempts/histrogram.go b/cmix/attempts/histrogram.go new file mode 100644 index 0000000000000000000000000000000000000000..37e7292592aeae1cdda482a82b0fdcf5530933d9 --- /dev/null +++ b/cmix/attempts/histrogram.go @@ -0,0 +1,116 @@ +package attempts + +import ( + "fmt" + "sort" + "strconv" + "strings" + "sync" + "sync/atomic" +) + +const ( + maxHistogramSize = 100 + minElements = 3 + percentileNumerator = 66 + percentileDenominator = 99 + percentileDenominatorOffset = 49 + optimalAttemptsInitValue = -1 +) + +// SendAttemptTracker tracks the number of attempts it took to send a cMix +// message in order to predict how many attempt are needed. +type SendAttemptTracker interface { + // SubmitProbeAttempt feeds the number of attempts it took to send a cMix + // message into the tracker and updates the optimal number of attempts. + SubmitProbeAttempt(numAttemptsUntilSuccessful int) + + // GetOptimalNumAttempts returns the number of optimal sends. If there is + // insufficient data to calculate, then ready is false. + GetOptimalNumAttempts() (attempts int, ready bool) +} + +// sendAttempts tracks the number of attempts to send a cMix message. +type sendAttempts struct { + optimalAttempts *int32 + isFull bool + currentIndex int + numAttempts []int + lock sync.Mutex +} + +// NewSendAttempts initialises a new SendAttemptTracker. +func NewSendAttempts() SendAttemptTracker { + optimalAttempts := int32(optimalAttemptsInitValue) + sa := &sendAttempts{ + optimalAttempts: &optimalAttempts, + isFull: false, + currentIndex: 0, + numAttempts: make([]int, maxHistogramSize), + } + + return sa +} + +// SubmitProbeAttempt feeds the number of attempts it took to send a cMix +// message into the tracker and updates the optimal number of attempts. +func (sa *sendAttempts) SubmitProbeAttempt(numAttemptsUntilSuccessful int) { + sa.lock.Lock() + defer sa.lock.Unlock() + + sa.numAttempts[sa.currentIndex] = numAttemptsUntilSuccessful + sa.currentIndex++ + + if sa.currentIndex == len(sa.numAttempts) { + sa.currentIndex = 0 + sa.isFull = true + } + + sa.computeOptimalUnsafe() +} + +// GetOptimalNumAttempts returns the number of optimal sends. If there is +// insufficient data to calculate, then ready is false. +func (sa *sendAttempts) GetOptimalNumAttempts() (attempts int, ready bool) { + optimalAttempts := atomic.LoadInt32(sa.optimalAttempts) + + if optimalAttempts == optimalAttemptsInitValue { + return 0, false + } + + return int(optimalAttempts), true +} + +// computeOptimalUnsafe updates the optimal send attempts. +func (sa *sendAttempts) computeOptimalUnsafe() { + toCopy := maxHistogramSize + if !sa.isFull { + if sa.currentIndex < minElements { + return + } + toCopy = sa.currentIndex + } + + histogramCopy := make([]int, toCopy) + copy(histogramCopy, sa.numAttempts[:toCopy]) + sort.Ints(histogramCopy) + + i := ((toCopy * percentileNumerator) + percentileDenominatorOffset) / + percentileDenominator + optimal := histogramCopy[i] + atomic.StoreInt32(sa.optimalAttempts, int32(optimal)) +} + +// String prints the values in the sendAttempts in a human-readable form for +// debugging and logging purposes. This function adheres to the fmt.Stringer +// interface. +func (sa *sendAttempts) String() string { + fields := []string{ + "optimalAttempts:" + strconv.Itoa(int(atomic.LoadInt32(sa.optimalAttempts))), + "isFull:" + strconv.FormatBool(sa.isFull), + "currentIndex:" + strconv.Itoa(sa.currentIndex), + "numAttempts:" + fmt.Sprintf("%d", sa.numAttempts), + } + + return "{" + strings.Join(fields, " ") + "}" +} diff --git a/cmix/attempts/histrogram_test.go b/cmix/attempts/histrogram_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6934b1f332e44cd17dd5cf2030932a8ec364fbd3 --- /dev/null +++ b/cmix/attempts/histrogram_test.go @@ -0,0 +1,89 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +package attempts + +import ( + "math/rand" + "reflect" + "testing" +) + +// Tests that NewSendAttempts returns a new sendAttempts with the expected +// fields. +func TestNewSendAttempts(t *testing.T) { + optimalAttempts := int32(optimalAttemptsInitValue) + expected := &sendAttempts{ + optimalAttempts: &optimalAttempts, + isFull: false, + currentIndex: 0, + numAttempts: make([]int, maxHistogramSize), + } + + sa := NewSendAttempts() + + if !reflect.DeepEqual(expected, sa) { + t.Errorf("New SendAttemptTracker does not match expected."+ + "\nexpected: %+v\nreceivedL %+v", expected, sa) + } +} + +// Tests that sendAttempts.SubmitProbeAttempt properly increments and stores the +// attempts. +func Test_sendAttempts_SubmitProbeAttempt(t *testing.T) { + sa := NewSendAttempts().(*sendAttempts) + + for i := 0; i < maxHistogramSize+20; i++ { + sa.SubmitProbeAttempt(i) + + if sa.currentIndex != (i+1)%maxHistogramSize { + t.Errorf("Incorrect currentIndex (%d).\nexpected: %d\nreceived: %d", + i, (i+1)%maxHistogramSize, sa.currentIndex) + } else if sa.numAttempts[i%maxHistogramSize] != i { + t.Errorf("Incorrect numAttempts at %d.\nexpected: %d\nreceived: %d", + i, i, sa.numAttempts[i%maxHistogramSize]) + } else if i > maxHistogramSize && !sa.isFull { + t.Errorf("Should be marked full when numAttempts > %d.", + maxHistogramSize) + } + } +} + +// Tests sendAttempts.GetOptimalNumAttempts returns numbers close to 70% of the +// average of attempts feeding in. +func Test_sendAttempts_GetOptimalNumAttempts(t *testing.T) { + prng := rand.New(rand.NewSource(42)) + sa := NewSendAttempts().(*sendAttempts) + + attempts, ready := sa.GetOptimalNumAttempts() + if ready { + t.Errorf("Marked ready when no attempts have been made.") + } else if attempts != 0 { + t.Errorf("Incorrect number of attempt.\nexpected: %d\nreceived: %d", + 0, attempts) + } + + const n = 100 + factor := (n * 7) / 10 + for i := 0; i < 500; i++ { + sa.SubmitProbeAttempt(prng.Intn(n)) + attempts, ready = sa.GetOptimalNumAttempts() + + if (sa.currentIndex < minElements && !sa.isFull) && ready { + t.Errorf("Ready when less than %d attempts made (%d).", + minElements, i) + } else if sa.currentIndex >= minElements { + if !ready { + t.Errorf("Not ready when more than %d attempts made (%d).", + minElements, i) + } else if attempts < factor-25 || attempts > factor+25 { + t.Errorf("Attempts is not close to average (%d)."+ + "\naverage: %d\nattempts: %d", i, factor, attempts) + } + } + } +} diff --git a/cmix/client.go b/cmix/client.go index d6acf56eb258bb7dfb8b3b31ddb7446c6101e2f6..bc7eaeaecc7a6a8fdb28f189e2d9ad9488870f2d 100644 --- a/cmix/client.go +++ b/cmix/client.go @@ -11,6 +11,9 @@ package cmix // and intra-client state are accessible through the context object. import ( + "gitlab.com/elixxir/client/cmix/attempts" + "gitlab.com/elixxir/client/cmix/clockSkew" + "gitlab.com/xx_network/primitives/netTime" "math" "strconv" "sync/atomic" @@ -57,6 +60,8 @@ type client struct { comms *commClient.Comms // Contains the network instance instance *commNetwork.Instance + //contains the clock skew tracker + skewTracker clockSkew.Tracker // Parameters of the network param Params @@ -70,7 +75,8 @@ type client struct { address.Space identity.Tracker health.Monitor - crit *critical + crit *critical + attemptTracker attempts.SendAttemptTracker // Earliest tracked round earliestRound *uint64 @@ -97,16 +103,20 @@ func NewClient(params Params, comms *commClient.Comms, session storage.Session, tracker := uint64(0) earliest := uint64(0) + netTime.SetTimeSource(localTime{}) + // Create client object c := &client{ - param: params, - tracker: &tracker, - events: events, - earliestRound: &earliest, - session: session, - rng: rng, - comms: comms, - maxMsgLen: tmpMsg.ContentsSize(), + param: params, + tracker: &tracker, + events: events, + earliestRound: &earliest, + session: session, + rng: rng, + comms: comms, + maxMsgLen: tmpMsg.ContentsSize(), + skewTracker: clockSkew.New(params.ClockSkewClamp), + attemptTracker: attempts.NewSendAttempts(), } if params.VerboseRoundTracking { @@ -188,10 +198,15 @@ func (c *client) initialize(ndf *ndf.NetworkDefinition) error { // Set up critical message tracking (sendCmix only) critSender := func(msg format.Message, recipient *id.ID, params CMIXParams, - ) (id.Round, ephemeral.Id, error) { - return sendCmixHelper(c.Sender, msg, recipient, params, c.instance, + ) (rounds.Round, ephemeral.Id, error) { + compiler := func(round id.Round) (format.Message, error) { + return msg, nil + } + r, eid, _, sendErr := sendCmixHelper(c.Sender, compiler, recipient, params, c.instance, c.session.GetCmixGroup(), c.Registrar, c.rng, c.events, - c.session.GetTransmissionID(), c.comms) + c.session.GetTransmissionID(), c.comms, c.attemptTracker) + return r, eid, sendErr + } c.crit = newCritical(c.session.GetKV(), c.Monitor, diff --git a/cmix/clockSkew/timeTracker.go b/cmix/clockSkew/timeTracker.go new file mode 100644 index 0000000000000000000000000000000000000000..0d8e79a7e7d483bca198cddabe1444ce6e77899d --- /dev/null +++ b/cmix/clockSkew/timeTracker.go @@ -0,0 +1,153 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +// package clockSkew tracks local clock skew relative to gateways. +package clockSkew + +import ( + jww "github.com/spf13/jwalterweatherman" + "sync" + "time" + + "gitlab.com/xx_network/primitives/id" +) + +const maxHistogramSize = 50 +const day = 24 * time.Hour + +// Tracker tracks local clock skew relative to various +// gateways. +type Tracker interface { + // Add additional data to our aggregate clock skews. + Add(gwID *id.ID, startTime, rTs time.Time, rtt, gwD time.Duration) + + // Aggregate returns the average of the last n offsets. + Aggregate() time.Duration +} + +// gatewayDelays is a helper type used by the timeOffsetTracker below +// to keep track of the last maxHistogramSize number of durations. +type gatewayDelays struct { + lock sync.RWMutex + delays []*time.Duration + currentIndex int +} + +func newGatewayDelays() *gatewayDelays { + return &gatewayDelays{ + delays: make([]*time.Duration, maxHistogramSize), + currentIndex: 0, + } +} + +func (g *gatewayDelays) Add(d time.Duration) { + g.lock.Lock() + defer g.lock.Unlock() + + g.delays[g.currentIndex] = &d + g.currentIndex += 1 + if g.currentIndex == len(g.delays) { + g.currentIndex = 0 + } +} + +func (g *gatewayDelays) Average() time.Duration { + g.lock.RLock() + defer g.lock.RUnlock() + return average(g.delays) +} + +// timeOffsetTracker implements the Tracker +type timeOffsetTracker struct { + gatewayClockDelays *sync.Map // id.ID -> *gatewayDelays + + lock sync.RWMutex + offsets []*time.Duration + currentIndex int + clamp time.Duration +} + +// New returns an implementation of Tracker. +func New(clamp time.Duration) Tracker { + t := &timeOffsetTracker{ + gatewayClockDelays: new(sync.Map), + offsets: make([]*time.Duration, maxHistogramSize), + currentIndex: 0, + clamp: clamp, + } + return t +} + +// Add implements the Add method of the Tracker interface. +func (t *timeOffsetTracker) Add(gwID *id.ID, startTime, rTs time.Time, rtt, gwD time.Duration) { + if abs(startTime.Sub(rTs)) > day { + jww.WARN.Printf("Time data from %s dropped, more than an day off from"+ + " local time; local: %s, remote: %s", gwID, startTime, rTs) + return + } + + delay := (rtt - gwD) / 2 + + delays, _ := t.gatewayClockDelays.LoadOrStore(*gwID, newGatewayDelays()) + + gwdelays := delays.(*gatewayDelays) + gwdelays.Add(delay) + gwDelay := gwdelays.Average() + + offset := startTime.Sub(rTs.Add(-gwDelay)) + t.addOffset(offset) +} + +func abs(duration time.Duration) time.Duration { + if duration < 0 { + return -duration + } + return duration +} + +func (t *timeOffsetTracker) addOffset(offset time.Duration) { + t.lock.Lock() + defer t.lock.Unlock() + + t.offsets[t.currentIndex] = &offset + t.currentIndex += 1 + if t.currentIndex == len(t.offsets) { + t.currentIndex = 0 + } +} + +// Aggregate implements the Aggregate method fo the Tracker interface. +func (t *timeOffsetTracker) Aggregate() time.Duration { + t.lock.RLock() + defer t.lock.RUnlock() + + avg := average(t.offsets) + if avg < (-t.clamp) || avg > t.clamp { + return avg + } else { + return 0 + } + +} + +func average(durations []*time.Duration) time.Duration { + sum := int64(0) + count := int64(0) + for i := 0; i < len(durations); i++ { + if durations[i] == nil { + break + } + sum += int64(*durations[i]) + count += 1 + } + + if count == 0 { + return 0 + } + + return time.Duration(sum / count) +} diff --git a/cmix/clockSkew/timeTracker_test.go b/cmix/clockSkew/timeTracker_test.go new file mode 100644 index 0000000000000000000000000000000000000000..40872721592f13ee877c96e8c264f4f473e8ca5e --- /dev/null +++ b/cmix/clockSkew/timeTracker_test.go @@ -0,0 +1,84 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +// package clockSkew tracks local clock skew relative to gateways. +package clockSkew + +import ( + "crypto/rand" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "gitlab.com/xx_network/primitives/id" +) + +func TestTimeTrackerSmokeTest(t *testing.T) { + tracker := New(0) + gwID := &id.ID{} + _, err := rand.Read(gwID[:]) + require.NoError(t, err) + + startTime := time.Now().AddDate(0, 0, -1) // this time yesterday + rTs := startTime.Add(time.Second * 10) + rtt := time.Second * 10 + gwD := time.Second * 3 + + tracker.Add(gwID, startTime, rTs, rtt, gwD) + tracker.Add(gwID, startTime, rTs, rtt, gwD) + tracker.Add(gwID, startTime, rTs, rtt, gwD) + + aggregate := tracker.Aggregate() + + t.Logf("aggregate: %v", aggregate) +} + +func TestAverage(t *testing.T) { + t1 := time.Duration(int64(10)) + t2 := time.Duration(int64(20)) + t3 := time.Duration(int64(30)) + t4 := time.Duration(int64(1000)) + durations := make([]*time.Duration, 100) + durations[0] = &t1 + durations[1] = &t2 + durations[2] = &t3 + durations[3] = &t4 + avg := average(durations) + require.Equal(t, int(avg), 265) +} + +func TestGatewayDelayAverage(t *testing.T) { + t1 := time.Duration(int64(10)) + t2 := time.Duration(int64(20)) + t3 := time.Duration(int64(30)) + t4 := time.Duration(int64(1000)) + gwDelays := newGatewayDelays() + gwDelays.Add(t1) + gwDelays.Add(t2) + gwDelays.Add(t3) + gwDelays.Add(t4) + avg := gwDelays.Average() + require.Equal(t, int(avg), 265) +} + +func TestAddOffset(t *testing.T) { + tracker := &timeOffsetTracker{ + gatewayClockDelays: new(sync.Map), + offsets: make([]*time.Duration, maxHistogramSize), + currentIndex: 0, + } + offset := time.Second * 10 + + for i := 0; i < maxHistogramSize-1; i++ { + tracker.addOffset(offset) + require.Equal(t, i+1, tracker.currentIndex) + } + tracker.addOffset(offset) + require.Equal(t, 0, tracker.currentIndex) +} diff --git a/cmix/critical.go b/cmix/critical.go index 92de10bb0736c13a130c99735393cc5598e13071..6e9dd64e5a30d95a0933818c9b79e763ec67fe05 100644 --- a/cmix/critical.go +++ b/cmix/critical.go @@ -8,6 +8,7 @@ package cmix import ( + "gitlab.com/elixxir/client/cmix/rounds" "time" jww "github.com/spf13/jwalterweatherman" @@ -35,7 +36,7 @@ type roundEventRegistrar interface { // anonymous function to include the structures from client that critical is // not aware of. type criticalSender func(msg format.Message, recipient *id.ID, - params CMIXParams) (id.Round, ephemeral.Id, error) + params CMIXParams) (rounds.Round, ephemeral.Id, error) // critical is a structure that allows the auto resending of messages that must // be received. @@ -134,7 +135,7 @@ func (c *critical) evaluate(stop *stoppable.Single) { round, _, err := c.send(msg, recipient, params) // Pass to the handler - c.handle(msg, recipient, round, err) + c.handle(msg, recipient, round.ID, err) }(msg, localRid, params) } diff --git a/cmix/follow.go b/cmix/follow.go index ef21f5dfcf941f6619c154e681f0781508bd1009..051438d4f5e5456bce0f3338169ac855b99a3f29 100644 --- a/cmix/follow.go +++ b/cmix/follow.go @@ -26,6 +26,8 @@ import ( "bytes" "encoding/binary" "fmt" + "gitlab.com/elixxir/client/cmix/identity/receptionID" + "sync" "sync/atomic" "time" @@ -55,7 +57,7 @@ const ( type followNetworkComms interface { GetHost(hostId *id.ID) (*connect.Host, bool) SendPoll(host *connect.Host, message *pb.GatewayPoll) ( - *pb.GatewayPollResponse, error) + *pb.GatewayPollResponse, time.Time, time.Duration, error) RequestMessages(host *connect.Host, message *pb.GetMessages) ( *pb.GetMessagesResponse, error) } @@ -68,7 +70,10 @@ func (c *client) followNetwork(report ClientErrorReport, TrackTicker := time.NewTicker(debugTrackPeriod) rng := c.rng.GetStream() + // abandon tracks rounds which data was not found out about in + // the verbose rounds debugging mode abandon := func(round id.Round) { return } + dummyAbandon := func(round id.Round) { return } if c.verboseRounds != nil { abandon = func(round id.Round) { c.verboseRounds.denote(round, Abandoned) @@ -82,7 +87,57 @@ func (c *client) followNetwork(report ClientErrorReport, stop.ToStopped() return case <-ticker.C: - c.follow(report, rng, c.comms, stop, abandon) + operator := func(toTrack []receptionID.IdentityUse) error { + + // set up tracking tools + wg := &sync.WaitGroup{} + wg.Add(len(toTrack)) + + // trigger the first separately because it will get network state + // updates + go func() { + c.follow(toTrack[0], report, rng, c.comms, stop, abandon, + true) + wg.Done() + }() + + //trigger all others without getting network state updates + for i := 1; i < len(toTrack); i++ { + go func(index int) { + c.follow(toTrack[index], report, rng, c.comms, stop, + dummyAbandon, false) + wg.Done() + }(i) + } + + //wait for all to complete + wg.Wait() + return nil + } + + //denote the execution + atomic.AddUint64(c.tracker, 1) + + // track the message on every identity + stream := c.rng.GetStream() + err := c.Tracker.ForEach( + int(c.param.MaxParallelIdentityTracks), + stream, + c.Space.GetAddressSpaceWithoutWait(), + operator) + stream.Close() + + //update clock skew + estimatedSkew := c.skewTracker.Aggregate() + // invert the skew because we need to reverse it + netTime.SetOffset(-estimatedSkew) + + if err != nil { + jww.ERROR.Printf("failed to operate on identities to "+ + "track: %s", err) + continue + } + case <-TrackTicker.C: numPolls := atomic.SwapUint64(c.tracker, 0) if c.numLatencies != 0 { @@ -108,18 +163,10 @@ func (c *client) followNetwork(report ClientErrorReport, } } -// follow executes each iteration of the follower. -func (c *client) follow(report ClientErrorReport, rng csprng.Source, - comms followNetworkComms, stop *stoppable.Single, - abandon func(round id.Round)) { - - // Get the identity we will poll for - identity, err := c.GetEphemeralIdentity( - rng, c.Space.GetAddressSpaceWithoutWait()) - if err != nil { - jww.FATAL.Panicf( - "Failed to get an identity, this should be impossible: %+v", err) - } +// follow executes an iteration of the follower for a specific identity +func (c *client) follow(identity receptionID.IdentityUse, + report ClientErrorReport, rng csprng.Source, comms followNetworkComms, + stop *stoppable.Single, abandon func(round id.Round), getUpdates bool) { // While polling with a fake identity, it is necessary to have populated // earliestRound data. However, as with fake identities, we want the values @@ -130,8 +177,6 @@ func (c *client) follow(report ClientErrorReport, rng csprng.Source, identity.ER = fakeEr } - atomic.AddUint64(c.tracker, 1) - // Get client version for poll version := c.session.GetClientVersion() @@ -147,14 +192,23 @@ func (c *client) follow(report ClientErrorReport, rng csprng.Source, ClientVersion: []byte(version.String()), FastPolling: c.param.FastPolling, LastRound: uint64(identity.ER.Get()), + DisableUpdates: !getUpdates, } + var rtt time.Duration + var sendTo *id.ID + var startTime time.Time + result, err := c.SendToAny(func(host *connect.Host) (interface{}, error) { jww.DEBUG.Printf("Executing poll for %v(%s) range: %s-%s(%s) from %s", identity.EphId.Int64(), identity.Source, identity.StartValid, identity.EndValid, identity.EndValid.Sub(identity.StartValid), host.GetId()) - return comms.SendPoll(host, &pollReq) + var err error + var response *pb.GatewayPollResponse + response, startTime, rtt, err = comms.SendPoll(host, &pollReq) + sendTo = host.GetId() + return response, err }, stop) // Exit if the thread has been stopped @@ -181,6 +235,11 @@ func (c *client) follow(report ClientErrorReport, rng csprng.Source, pollResp := result.(*pb.GatewayPollResponse) + //execute clock skew update + c.skewTracker.Add(sendTo, startTime, + time.Unix(0, pollResp.ReceivedTs), + rtt, time.Duration(pollResp.GatewayDelay)) + // ---- Process Network State Update Data ---- gwRoundsState := &knownRounds.KnownRounds{} err = gwRoundsState.Unmarshal(pollResp.KnownRounds) @@ -299,6 +358,7 @@ func (c *client) follow(report ClientErrorReport, rng csprng.Source, if !hasMessage && c.verboseRounds != nil { c.verboseRounds.denote(rid, RoundState(NoMessageAvailable)) } + //jww.INFO.Printf("[LOOKUP] round %d checked for %d, has message: %v", rid, identity.EphId.Int64(), hasMessage) return hasMessage } @@ -349,10 +409,10 @@ func (c *client) follow(report ClientErrorReport, rng csprng.Source, gwRoundsState.RangeUnchecked( updatedEarliestRound, c.param.KnownRoundsThreshold, roundChecker) - jww.DEBUG.Printf("Processed RangeUnchecked, Oldest: %d, "+ + jww.DEBUG.Printf("Processed RangeUnchecked for %d, Oldest: %d, "+ "firstUnchecked: %d, last Checked: %d, threshold: %d, "+ "NewEarliestRemaining: %d, NumWithMessages: %d, NumUnknown: %d", - updatedEarliestRound, gwRoundsState.GetFirstUnchecked(), + identity.EphId.Int64(), updatedEarliestRound, gwRoundsState.GetFirstUnchecked(), gwRoundsState.GetLastChecked(), c.param.KnownRoundsThreshold, earliestRemaining, len(roundsWithMessages), len(roundsUnknown)) diff --git a/cmix/gateway/defaults.go b/cmix/gateway/defaults.go new file mode 100644 index 0000000000000000000000000000000000000000..9e19c49cf3577888da2c31daf5bed9d8eabf52fd --- /dev/null +++ b/cmix/gateway/defaults.go @@ -0,0 +1,9 @@ +//go:build !js || !wasm +// +build !js !wasm + +// This file is compiled for all architectures except WebAssembly. +package gateway + +const ( + MaxPoolSize = 20 +) diff --git a/cmix/gateway/defaults_js.go b/cmix/gateway/defaults_js.go new file mode 100644 index 0000000000000000000000000000000000000000..8eb93858dc95aefd6506ab7bed520f83ab274632 --- /dev/null +++ b/cmix/gateway/defaults_js.go @@ -0,0 +1,5 @@ +package gateway + +const ( + MaxPoolSize = 7 +) diff --git a/cmix/gateway/hostPool.go b/cmix/gateway/hostPool.go index 49875776c2ff735520e7662eea9a14cdb3a44811..31f9ba3ca400705a2ddf82268bc6e0b5f4d1c266 100644 --- a/cmix/gateway/hostPool.go +++ b/cmix/gateway/hostPool.go @@ -48,6 +48,7 @@ var errorsList = []string{ "Host is in cool down", grpc.ErrClientConnClosing.Error(), connect.TooManyProxyError, + "Failed to fetch", } // HostManager Interface allowing storage and retrieval of Host objects @@ -127,7 +128,7 @@ type poolParamsDisk struct { // DefaultPoolParams returns a default set of PoolParams. func DefaultPoolParams() PoolParams { p := PoolParams{ - MaxPoolSize: 30, + MaxPoolSize: MaxPoolSize, ProxyAttempts: 5, PoolSize: 0, MaxPings: 0, @@ -247,7 +248,7 @@ func newHostPool(poolParams PoolParams, rng *fastRNG.StreamGenerator, } } else { jww.WARN.Printf( - "Building new HostPool because no HostList stored: %+v", err) + "Building new HostPool because no HostList stored: %s", err.Error()) } // Build the initial HostPool and return @@ -307,25 +308,22 @@ func (h *HostPool) initialize(startIdx uint32) error { id *id.ID latency time.Duration } + numGatewaysToTry := h.poolParams.MaxPings numGateways := uint32(len(randomGateways)) if numGatewaysToTry > numGateways { numGatewaysToTry = numGateways } + resultList := make([]gatewayDuration, 0, numGatewaysToTry) // Begin trying gateways c := make(chan gatewayDuration, numGatewaysToTry) - exit := false - i := uint32(0) - for !exit { - for ; i < numGateways; i++ { - // Ran out of Hosts to try - if i >= numGateways { - exit = true - break - } + i := 0 + for exit := false; !exit; { + triedHosts := uint32(0) + for ; triedHosts < numGateways && i < len(randomGateways); i++ { // Select a gateway not yet selected gwId, err := randomGateways[i].GetGatewayId() if err != nil { @@ -334,10 +332,9 @@ func (h *HostPool) initialize(startIdx uint32) error { // Skip if already in HostPool if _, ok := h.hostMap[*gwId]; ok { - // Try another Host instead - numGatewaysToTry++ continue } + triedHosts++ go func() { // Obtain that GwId's Host object @@ -358,19 +355,22 @@ func (h *HostPool) initialize(startIdx uint32) error { // Collect ping results pingTimeout := 2 * h.poolParams.HostParams.PingTimeout timer := time.NewTimer(pingTimeout) + + newAppends := uint32(0) innerLoop: for { select { case gw := <-c: // Only add successful pings if gw.latency > 0 { + newAppends++ resultList = append(resultList, gw) jww.DEBUG.Printf("Adding HostPool result %d/%d: %s: %d", len(resultList), numGatewaysToTry, gw.id, gw.latency) } // Break if we have all needed slots - if uint32(len(resultList)) == numGatewaysToTry { + if newAppends == triedHosts { exit = true timer.Stop() break innerLoop @@ -382,6 +382,10 @@ func (h *HostPool) initialize(startIdx uint32) error { break innerLoop } } + + if i >= len(randomGateways) { + exit = true + } } // Sort the resultList by lowest latency diff --git a/cmix/gateway/sender.go b/cmix/gateway/sender.go index 98dffa6a2de35328d4b42f27d920144663de4095..3919fad03137a65591836be7a4358de14b0e9f68 100644 --- a/cmix/gateway/sender.go +++ b/cmix/gateway/sender.go @@ -77,11 +77,11 @@ func (s *sender) SendToAny(sendFunc func(*connect.Host) (interface{}, error), "with error %s", proxies[proxy].GetId(), err.Error()) } else { if checkReplaceErr != nil { - jww.WARN.Printf("Unable to SendToAny via %s: %s."+ + jww.WARN.Printf("Unable to SendToAny via %s: %s. "+ "Unable to replace host: %+v", proxies[proxy].GetId(), err.Error(), checkReplaceErr) } else { - jww.WARN.Printf("Unable to SendToAny via %s: %s."+ + jww.WARN.Printf("Unable to SendToAny via %s: %s. "+ "Did not replace host.", proxies[proxy].GetId(), err.Error()) } diff --git a/cmix/health/callback.go b/cmix/health/callback.go new file mode 100644 index 0000000000000000000000000000000000000000..26dc504327d2f649875df0cb4c0a6d965001051c --- /dev/null +++ b/cmix/health/callback.go @@ -0,0 +1,53 @@ +package health + +import "sync" + +type trackerCallback struct { + funcs map[uint64]func(isHealthy bool) + funcsID uint64 + + mux sync.RWMutex +} + +func initTrackerCallback() *trackerCallback { + return &trackerCallback{ + funcs: map[uint64]func(isHealthy bool){}, + funcsID: 0, + } +} + +// addHealthCallback adds a function to the list of tracker functions such that +// each function can be run after network changes. Returns a unique ID for the +// function. +func (t *trackerCallback) addHealthCallback(f func(isHealthy bool), health bool) uint64 { + var currentID uint64 + + t.mux.Lock() + t.funcs[t.funcsID] = f + currentID = t.funcsID + t.funcsID++ + t.mux.Unlock() + + go f(health) + + return currentID +} + +// RemoveHealthCallback removes the function with the given ID from the list of +// tracker functions so that it will no longer be run. +func (t *trackerCallback) RemoveHealthCallback(chanID uint64) { + t.mux.Lock() + delete(t.funcs, chanID) + t.mux.Unlock() +} + +// callback calls every function with the new health state +func (t *trackerCallback) callback(health bool) { + t.mux.Lock() + defer t.mux.Unlock() + + // Run all listening functions + for _, f := range t.funcs { + go f(health) + } +} diff --git a/cmix/health/tracker.go b/cmix/health/tracker.go index 537ec320cae8341b341653e4af07c93fc37c50ce..1476b788ce6feba441c6b2facb97ea884482bcae 100644 --- a/cmix/health/tracker.go +++ b/cmix/health/tracker.go @@ -11,8 +11,7 @@ package health import ( - "errors" - "sync" + "sync/atomic" "time" jww "github.com/spf13/jwalterweatherman" @@ -29,108 +28,130 @@ type Monitor interface { } type tracker struct { + // timeout parameter describes how long + // without good news until the network is considered unhealthy timeout time.Duration + // channel on which new status updates are received from the network handler heartbeat chan network.Heartbeat - funcs map[uint64]func(isHealthy bool) - channelsID uint64 - funcsID uint64 - - running bool - - // Determines the current health status - isHealthy bool + // denotes the last time news was heard. Both hold ns since unix epoc + // in an atomic + lastCompletedRound *int64 + lastWaitingRound *int64 // Denotes that the past health status wasHealthy is true if isHealthy has - // ever been true - wasHealthy bool - mux sync.RWMutex + // ever been true in an atomic. + wasHealthy *uint32 + + // stores registered callbacks to receive event updates + *trackerCallback } // Init creates a single HealthTracker thread, starts it, and returns a tracker // and a stoppable. func Init(instance *network.Instance, timeout time.Duration) Monitor { - tracker := newTracker(timeout) - instance.SetNetworkHealthChan(tracker.heartbeat) - return tracker + trkr := newTracker(timeout) + instance.SetNetworkHealthChan(trkr.heartbeat) + + return trkr } // newTracker builds and returns a new tracker object given a Context. func newTracker(timeout time.Duration) *tracker { - return &tracker{ - timeout: timeout, - funcs: map[uint64]func(isHealthy bool){}, - heartbeat: make(chan network.Heartbeat, 100), - isHealthy: false, - running: false, - } -} -// AddHealthCallback adds a function to the list of tracker functions such that -// each function can be run after network changes. Returns a unique ID for the -// function. -func (t *tracker) AddHealthCallback(f func(isHealthy bool)) uint64 { - var currentID uint64 + lastCompletedRound := int64(0) + lastWaitingRound := int64(0) - t.mux.Lock() - t.funcs[t.funcsID] = f - currentID = t.funcsID - t.funcsID++ - t.mux.Unlock() + wasHealthy := uint32(0) - go f(t.IsHealthy()) + t := &tracker{ + timeout: timeout, + heartbeat: make(chan network.Heartbeat, 100), + lastCompletedRound: &lastCompletedRound, + lastWaitingRound: &lastWaitingRound, + wasHealthy: &wasHealthy, + } + t.trackerCallback = initTrackerCallback() + return t +} - return currentID +// getLastCompletedRoundTimestamp atomically loads the completed round timestamp +// and converts it to a time object, then returns it +func (t *tracker) getLastCompletedRoundTimestamp() time.Time { + return time.Unix(0, atomic.LoadInt64(t.lastCompletedRound)) } -// RemoveHealthCallback removes the function with the given ID from the list of -// tracker functions so that it will no longer be run. -func (t *tracker) RemoveHealthCallback(chanID uint64) { - t.mux.Lock() - delete(t.funcs, chanID) - t.mux.Unlock() +// getLastWaitingRoundTimestamp atomically loads the waiting round timestamp +// and converts it to a time object, then returns it +func (t *tracker) getLastWaitingRoundTimestamp() time.Time { + return time.Unix(0, atomic.LoadInt64(t.lastWaitingRound)) } +// IsHealthy returns true if the network is healthy, which is +// defined as the client having knowledge of both valid queued rounds +// and completed rounds within the last tracker.timeout seconds func (t *tracker) IsHealthy() bool { - t.mux.RLock() - defer t.mux.RUnlock() + // use the system time instead of netTime.Now() which can + // include an offset because local monotonicity is what + // matters here, not correctness relative to absolute time + now := time.Now() + + completedRecently := false + if now.Sub(t.getLastCompletedRoundTimestamp()) < t.timeout { + completedRecently = true + } - return t.isHealthy + waitingRecently := false + if now.Sub(t.getLastWaitingRoundTimestamp()) < t.timeout { + waitingRecently = true + } + + return completedRecently && waitingRecently } -// WasHealthy returns true if isHealthy has ever been true. -func (t *tracker) WasHealthy() bool { - t.mux.RLock() - defer t.mux.RUnlock() +// updateHealth atomically updates the internal +// timestamps to now if there are new waiting / completed +// rounds +func (t *tracker) updateHealth(hasWaiting, hasCompleted bool) { + // use the system time instead of netTime.Now() which can + // include an offset because local monotonicity is what + // matters here, not correctness relative to absolute time + now := time.Now().UnixNano() + + if hasWaiting { + atomic.StoreInt64(t.lastWaitingRound, now) + } - return t.wasHealthy + if hasCompleted { + atomic.StoreInt64(t.lastCompletedRound, now) + } } -func (t *tracker) setHealth(h bool) { - t.mux.Lock() - // Only set wasHealthy to true if either - // wasHealthy is true or - // wasHealthy is false but h value is true - t.wasHealthy = t.wasHealthy || h - t.isHealthy = h - t.mux.Unlock() +// forceUnhealthy cleats the internal timestamps, forcing the +// tracker into unhealthy +func (t *tracker) forceUnhealthy() { + atomic.StoreInt64(t.lastWaitingRound, 0) + atomic.StoreInt64(t.lastCompletedRound, 0) +} - t.transmit(h) +// WasHealthy returns true if isHealthy has ever been true. +func (t *tracker) WasHealthy() bool { + return atomic.LoadUint32(t.wasHealthy) == 1 +} + +// AddHealthCallback adds a function to the list of tracker functions such that +// each function can be run after network changes. Returns a unique ID for the +// function. +func (t *tracker) AddHealthCallback(f func(isHealthy bool)) uint64 { + return t.addHealthCallback(f, t.IsHealthy()) } +// StartProcesses starts running the func (t *tracker) StartProcesses() (stoppable.Stoppable, error) { - t.mux.Lock() - if t.running { - t.mux.Unlock() - return nil, errors.New( - "cannot start health tracker threads, they are already running") - } - t.running = true - t.isHealthy = false - t.mux.Unlock() + atomic.StoreUint32(t.wasHealthy, 0) stop := stoppable.NewSingle("health tracker") @@ -139,45 +160,68 @@ func (t *tracker) StartProcesses() (stoppable.Stoppable, error) { return stop, nil } -// start starts a long-running thread used to monitor and report on network +// start begins a long-running thread used to monitor and report on network // health. func (t *tracker) start(stop *stoppable.Single) { + + // ensures wasHealthy is only set once + hasSetWasHealthy := false + + // denotation of the previous state in order to catch state changes + lastState := false + + // flag denoting required exit, allows final signaling + quit := false + + //ensured the timeout error is only printed once per timeout period + timedOut := true + for { - var heartbeat network.Heartbeat + + /* wait for an event */ select { case <-stop.Quit(): - t.mux.Lock() - t.isHealthy = false - t.running = false - t.mux.Unlock() + t.forceUnhealthy() - t.transmit(false) - stop.ToStopped() + // flag the quit instead of quitting here so the + // joint signaling handler code can be triggered + quit = true - return - case heartbeat = <-t.heartbeat: - // FIXME: There's no transition to unhealthy here and there needs to - // be after some number of bad polls - if healthy(heartbeat) { - t.setHealth(true) - } + case heartbeat := <-t.heartbeat: + t.updateHealth(heartbeat.HasWaitingRound, heartbeat.IsRoundComplete) + timedOut = false case <-time.After(t.timeout): - if !t.isHealthy { - jww.WARN.Printf("Network health tracker timed out, network " + - "is no longer healthy...") + if !timedOut { + jww.ERROR.Printf("Network health tracker timed out, network " + + "is no longer healthy, follower likely has stopped...") + } + timedOut = true + + // note: no need to force to unhealthy because by definition the + // timestamps will be stale + } + + /* handle the state change resulting from an event */ + + // send signals if the state has changed + newHealthState := t.IsHealthy() + if newHealthState != lastState { + // set was healthy if we are healthy and it was never set before + if newHealthState && !hasSetWasHealthy { + atomic.StoreUint32(t.wasHealthy, 1) + hasSetWasHealthy = true } - t.setHealth(false) + + //trigger downstream events + t.callback(newHealthState) + + lastState = newHealthState } - } -} -func (t *tracker) transmit(health bool) { - // Run all listening functions - for _, f := range t.funcs { - go f(health) + // quit if required to quit + if quit { + stop.ToStopped() + return + } } } - -func healthy(a network.Heartbeat) bool { - return a.IsRoundComplete -} diff --git a/cmix/identity/receptionID/store.go b/cmix/identity/receptionID/store.go index 37dfb8bba2212773c6c2de62fd3604ba3d307c1e..967c22e034270d726b2c4e216d460dc767e0dbd2 100644 --- a/cmix/identity/receptionID/store.go +++ b/cmix/identity/receptionID/store.go @@ -13,6 +13,7 @@ import ( jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/storage/versioned" "gitlab.com/xx_network/crypto/large" + "gitlab.com/xx_network/crypto/shuffle" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" "gitlab.com/xx_network/primitives/netTime" @@ -28,6 +29,8 @@ const ( receptionStoreStorageVersion = 0 ) +var InvalidRequestedNumIdentities = errors.New("cannot get less than one identity(s)") + type Store struct { // Identities which are being actively checked active []*registration @@ -61,7 +64,7 @@ func NewOrLoadStore(kv *versioned.KV) *Store { s, err := loadStore(kv) if err != nil { jww.WARN.Printf( - "ReceptionID store not found, creating a new one: %+v", err) + "ReceptionID store not found, creating a new one: %s", err.Error()) s = &Store{ active: []*registration{}, @@ -157,7 +160,15 @@ func (s *Store) makeStoredReferences() []storedReference { return identities[:i] } -func (s *Store) GetIdentity(rng io.Reader, addressSize uint8) (IdentityUse, error) { +// ForEach operates on 'n' identities randomly in a random order. +// if no identities exist, it will operate on a single fake identity +func (s *Store) ForEach(n int, rng io.Reader, + addressSize uint8, operate func([]IdentityUse) error) error { + + if n < 1 { + return InvalidRequestedNumIdentities + } + s.mux.Lock() defer s.mux.Unlock() @@ -166,26 +177,29 @@ func (s *Store) GetIdentity(rng io.Reader, addressSize uint8) (IdentityUse, erro // Remove any now expired identities s.prune(now) - var identity IdentityUse - var err error + var identities []IdentityUse // If the list is empty, then return a randomly generated identity to poll // with so that we can continue tracking the network and to further // obfuscate network identities. if len(s.active) == 0 { - identity, err = generateFakeIdentity(rng, addressSize, now) + fakeIdentity, err := generateFakeIdentity(rng, addressSize, now) if err != nil { jww.FATAL.Panicf( "Failed to generate a new ID when none available: %+v", err) } + identities = append(identities, fakeIdentity) + // otherwise, select identities to return using a fisher-yates } else { - identity, err = s.selectIdentity(rng, now) + var err error + identities, err = s.selectIdentities(n, rng, now) if err != nil { - jww.FATAL.Panicf("Failed to select an ID: %+v", err) + jww.FATAL.Panicf("Failed to select a list of IDs: %+v", err) } } - return identity, nil + // do the passed operation on all identities + return operate(identities) } func (s *Store) AddIdentity(identity Identity) error { @@ -228,10 +242,11 @@ func (s *Store) RemoveIdentity(ephID ephemeral.Id) { s.mux.Lock() defer s.mux.Unlock() - for i, inQuestion := range s.active { + for i := 0; i < len(s.active); i++ { + inQuestion := s.active[i] if inQuestion.EphId == ephID { s.active = append(s.active[:i], s.active[i+1:]...) - + delete(s.present, makeIdHash(inQuestion.EphId, inQuestion.Source)) err := inQuestion.Delete() if err != nil { jww.FATAL.Panicf("Failed to delete identity: %+v", err) @@ -244,6 +259,8 @@ func (s *Store) RemoveIdentity(ephID ephemeral.Id) { } } + i-- + return } } @@ -254,16 +271,20 @@ func (s *Store) RemoveIdentities(source *id.ID) { defer s.mux.Unlock() doSave := false - for i, inQuestion := range s.active { + for i := 0; i < len(s.active); i++ { + inQuestion := s.active[i] if inQuestion.Source.Cmp(source) { s.active = append(s.active[:i], s.active[i+1:]...) - + delete(s.present, makeIdHash(inQuestion.EphId, inQuestion.Source)) + jww.INFO.Printf("Removing Identity %s:%d from tracker", + inQuestion.Source, inQuestion.EphId.Int64()) err := inQuestion.Delete() if err != nil { jww.FATAL.Panicf("Failed to delete identity: %+v", err) } doSave = doSave || !inQuestion.Ephemeral + i-- } } if doSave { @@ -305,6 +326,7 @@ func (s *Store) prune(now time.Time) { pruned = append(pruned, inQuestion.EphId.Int64()) s.active = append(s.active[:i], s.active[i+1:]...) + delete(s.present, makeIdHash(inQuestion.EphId, inQuestion.Source)) i-- } @@ -320,6 +342,8 @@ func (s *Store) prune(now time.Time) { } } +// selectIdentity returns a random identity in an IdentityUse object and +// increments its usage if necessary func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error) { // Choose a member from the list var selected *registration @@ -327,7 +351,7 @@ func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error if len(s.active) == 1 { selected = s.active[0] } else { - seed := make([]byte, 32) + seed := make([]byte, 32) //use 256 bits of entropy for the seed if _, err := rng.Read(seed); err != nil { return IdentityUse{}, errors.WithMessage(err, "Failed to choose "+ "ID due to RNG failure") @@ -341,10 +365,6 @@ func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error selected = s.active[selectedNum.Uint64()] } - if now.After(selected.End) { - selected.ExtraChecks-- - } - jww.TRACE.Printf("Selected identity: EphId: %d ID: %s End: %s "+ "StartValid: %s EndValid: %s", selected.EphId.Int64(), selected.Source, @@ -352,11 +372,64 @@ func (s *Store) selectIdentity(rng io.Reader, now time.Time) (IdentityUse, error selected.StartValid.Format("01/02/06 03:04:05 pm"), selected.EndValid.Format("01/02/06 03:04:05 pm")) + return useIdentity(selected, now), nil +} + +// selectIdentities returns up to 'n' identities in an IdentityUse object +// selected via fisher-yates and increments their usage if necessary +func (s *Store) selectIdentities(n int, rng io.Reader, now time.Time) ([]IdentityUse, error) { + // Choose a member from the list + selected := make([]IdentityUse, 0, n) + + if len(s.active) == 1 { + selected = append(selected, useIdentity(s.active[0], now)) + } else { + + // make the seed + seed := make([]byte, 32) //use 256 bits of entropy for the seed + if _, err := rng.Read(seed); err != nil { + return nil, errors.WithMessage(err, "Failed to choose "+ + "ID due to RNG failure") + } + + // make the list to shuffle + registered := make([]*registration, 0, len(s.active)) + for i := 0; i < len(s.active); i++ { + registered = append(registered, s.active[i]) + } + + //shuffle the list via fisher-yates + registeredProxy := shuffle.SeededShuffle(len(s.active), seed) + + //convert the list to identity use + for i := 0; i < len(registered) && (i < n); i++ { + selected = append(selected, + useIdentity(registered[registeredProxy[i]], now)) + } + + } + + jww.TRACE.Printf("Selected %d identities, first identity: EphId: %d ID: %s End: %s "+ + "StartValid: %s EndValid: %s", len(selected), + selected[0].EphId.Int64(), selected[0].Source, + selected[0].End.Format("01/02/06 03:04:05 pm"), + selected[0].StartValid.Format("01/02/06 03:04:05 pm"), + selected[0].EndValid.Format("01/02/06 03:04:05 pm")) + + return selected, nil +} + +// useIdentity makes the public IdentityUse object from a private *registration +// and deals with denoting the usage in the *registration if nessessay +func useIdentity(selected *registration, now time.Time) IdentityUse { + if now.After(selected.End) { + selected.ExtraChecks-- + } return IdentityUse{ Identity: selected.Identity, Fake: false, UR: selected.UR, ER: selected.ER, CR: selected.CR, - }, nil + } } diff --git a/cmix/identity/receptionID/store_test.go b/cmix/identity/receptionID/store_test.go index 68ad9c12b63ed708c1e82bf08ded5cfccac74984..5161469cfd7c044eddade131e8f76b90c6c30678 100644 --- a/cmix/identity/receptionID/store_test.go +++ b/cmix/identity/receptionID/store_test.go @@ -9,10 +9,13 @@ package receptionID import ( "bytes" + "encoding/binary" "encoding/json" "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/crypto/hash" "gitlab.com/elixxir/ekv" "gitlab.com/xx_network/primitives/netTime" + "math" "math/rand" "reflect" "testing" @@ -148,27 +151,226 @@ func TestStore_makeStoredReferences(t *testing.T) { } } -func TestStore_GetIdentity(t *testing.T) { +func TestStore_GetIdentities(t *testing.T) { kv := versioned.NewKV(ekv.MakeMemstore()) s := NewOrLoadStore(kv) prng := rand.New(rand.NewSource(42)) - testID, err := generateFakeIdentity(prng, 15, netTime.Now()) + + numToTest := 100 + + idsGenerated := make(map[uint64]interface{}) + + for i := 0; i < numToTest; i++ { + testID, err := generateFakeIdentity(prng, 15, netTime.Now()) + if err != nil { + t.Fatalf("Failed to generate fake ID: %+v", err) + } + testID.Fake = false + if s.AddIdentity(testID.Identity) != nil { + t.Errorf("AddIdentity() produced an error: %+v", err) + } + + idsGenerated[getIDFp(testID.EphemeralIdentity)] = nil + + } + + //get one + var idu []IdentityUse + o := func(a []IdentityUse) error { + idu = a + return nil + } + err := s.ForEach(1, prng, 15, o) if err != nil { - t.Fatalf("Failed to generate fake ID: %+v", err) + t.Errorf("GetIdentity() produced an error: %+v", err) } - if s.AddIdentity(testID.Identity) != nil { - t.Errorf("AddIdentity() produced an error: %+v", err) + + if _, exists := idsGenerated[getIDFp(idu[0].EphemeralIdentity)]; !exists || + idu[0].Fake { + t.Errorf("An unknown or fake identity was returned") + } + + //get three + err = s.ForEach(3, prng, 15, o) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if len(idu) != 3 { + t.Errorf("the wrong number of identities was returned") + } + + for i := 0; i < len(idu); i++ { + if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists || + idu[i].Fake { + t.Errorf("An unknown or fake identity was returned") + } + } + + //get ten + err = s.ForEach(10, prng, 15, o) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if len(idu) != 10 { + t.Errorf("the wrong number of identities was returned") + } + + for i := 0; i < len(idu); i++ { + if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists || + idu[i].Fake { + t.Errorf("An unknown or fake identity was returned") + } + } + + //get fifty + err = s.ForEach(50, prng, 15, o) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if len(idu) != 50 { + t.Errorf("the wrong number of identities was returned") + } + + for i := 0; i < len(idu); i++ { + if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists || + idu[i].Fake { + t.Errorf("An unknown or fake identity was returned") + } } - idu, err := s.GetIdentity(prng, 15) + //get 100 + err = s.ForEach(100, prng, 15, o) if err != nil { t.Errorf("GetIdentity() produced an error: %+v", err) } - if !testID.Equal(idu.Identity) { - t.Errorf("GetIdentity() did not return the expected Identity."+ - "\nexpected: %s\nreceived: %s", testID, idu) + if len(idu) != 100 { + t.Errorf("the wrong number of identities was returned") } + + for i := 0; i < len(idu); i++ { + if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists || + idu[i].Fake { + t.Errorf("An unknown or fake identity was returned") + } + } + + //get 1000, should only return 100 + err = s.ForEach(1000, prng, 15, o) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + if len(idu) != 100 { + t.Errorf("the wrong number of identities was returned") + } + + for i := 0; i < len(idu); i++ { + if _, exists := idsGenerated[getIDFp(idu[i].EphemeralIdentity)]; !exists || + idu[i].Fake { + t.Errorf("An unknown or fake identity was returned") + } + } + + // get 100 a second time and make sure the order is not the same as a + // smoke test that the shuffle is working + var idu2 []IdentityUse + o2 := func(a []IdentityUse) error { + idu2 = a + return nil + } + + err = s.ForEach(1000, prng, 15, o2) + if err != nil { + t.Errorf("GetIdentity() produced an error: %+v", err) + } + + diferent := false + for i := 0; i < len(idu); i++ { + if !idu[i].Source.Cmp(idu2[i].Source) { + diferent = true + break + } + } + + if !diferent { + t.Errorf("The 2 100 shuffels retruned the same result, shuffling" + + " is likley not occuring") + } + +} + +func TestStore_GetIdentities_NoIdentities(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + s := NewOrLoadStore(kv) + prng := rand.New(rand.NewSource(42)) + + var idu []IdentityUse + o := func(a []IdentityUse) error { + idu = a + return nil + } + + err := s.ForEach(5, prng, 15, o) + if err != nil { + t.Errorf("GetIdentities() produced an error: %+v", err) + } + + if len(idu) != 1 { + t.Errorf("GetIdenties() did not return only one identity " + + "when looking for a fake") + } + + if !idu[0].Fake { + t.Errorf("GetIdenties() did not return a fake identity " + + "when only one is avalible") + } +} + +func TestStore_GetIdentities_BadNum(t *testing.T) { + kv := versioned.NewKV(ekv.MakeMemstore()) + s := NewOrLoadStore(kv) + prng := rand.New(rand.NewSource(42)) + + o := func(a []IdentityUse) error { + return nil + } + + err := s.ForEach(0, prng, 15, o) + if err == nil { + t.Errorf("GetIdentities() shoud error with bad num value") + } + + err = s.ForEach(-1, prng, 15, o) + if err == nil { + t.Errorf("GetIdentities() shoud error with bad num value") + } + + err = s.ForEach(-100, prng, 15, o) + if err == nil { + t.Errorf("GetIdentities() shoud error with bad num value") + } + + err = s.ForEach(-1000000, prng, 15, o) + if err == nil { + t.Errorf("GetIdentities() shoud error with bad num value") + } + + err = s.ForEach(math.MinInt64, prng, 15, o) + if err == nil { + t.Errorf("GetIdentities() shoud error with bad num value") + } +} + +func getIDFp(identity EphemeralIdentity) uint64 { + h, _ := hash.NewCMixHash() + h.Write(identity.EphId[:]) + h.Write(identity.Source.Bytes()) + r := h.Sum(nil) + return binary.BigEndian.Uint64(r) } func TestStore_AddIdentity(t *testing.T) { diff --git a/cmix/identity/tracker.go b/cmix/identity/tracker.go index f7afdb6d8b6d3221c84817850c65715dec76bc16..7ab08da034fbdd1dbbbfd35ff54b7fc72c273929 100644 --- a/cmix/identity/tracker.go +++ b/cmix/identity/tracker.go @@ -49,7 +49,8 @@ type Tracker interface { StartProcesses() stoppable.Stoppable AddIdentity(id *id.ID, validUntil time.Time, persistent bool) RemoveIdentity(id *id.ID) - GetEphemeralIdentity(rng io.Reader, addressSize uint8) (receptionID.IdentityUse, error) + ForEach(n int, rng io.Reader, addressSize uint8, + operator func([]receptionID.IdentityUse) error) error GetIdentity(get *id.ID) (TrackedID, error) } @@ -142,10 +143,14 @@ func (t *manager) RemoveIdentity(id *id.ID) { t.deleteIdentity <- id } -// GetEphemeralIdentity returns an ephemeral Identity to poll the network with. -func (t *manager) GetEphemeralIdentity(rng io.Reader, addressSize uint8) ( - receptionID.IdentityUse, error) { - return t.ephemeral.GetIdentity(rng, addressSize) +// ForEach passes a fisher-yates shuffled list of up to 'num' +// ephemeral identities into the operation function. It will pass a +// fake identity if none are available +// and less than 'num' if less than 'num' are available. +// 'num' must be positive non-zero +func (t *manager) ForEach(n int, rng io.Reader, addressSize uint8, + operator func([]receptionID.IdentityUse) error) error { + return t.ephemeral.ForEach(n, rng, addressSize, operator) } // GetIdentity returns a currently tracked identity @@ -207,9 +212,11 @@ func (t *manager) track(stop *stoppable.Single) { continue case deleteID := <-t.deleteIdentity: + removed := false for i := range t.tracked { inQuestion := t.tracked[i] if inQuestion.Source.Cmp(deleteID) { + removed = true t.tracked = append(t.tracked[:i], t.tracked[i+1:]...) t.save() // Requires manual deletion in case identity is deleted before expiration @@ -217,6 +224,9 @@ func (t *manager) track(stop *stoppable.Single) { break } } + if !removed { + jww.WARN.Printf("Identity %s failed to be removed from tracker", deleteID) + } case <-stop.Quit(): t.addrSpace.UnregisterAddressSpaceNotification(addressSpaceSizeChanTag) stop.ToStopped() diff --git a/cmix/interface.go b/cmix/interface.go index 896b05a02a9f7c68ba67756eae8f66bc04acde93..f6572fb1dbdf6811052cb5d541e05286288c20cb 100644 --- a/cmix/interface.go +++ b/cmix/interface.go @@ -58,7 +58,7 @@ type Client interface { // WARNING: Do not roll your own crypto. Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams CMIXParams) ( - id.Round, ephemeral.Id, error) + rounds.Round, ephemeral.Id, error) // SendMany sends many "raw" cMix message payloads to the provided // recipients all in the same round. @@ -85,7 +85,25 @@ type Client interface { // (along with the reason). Blocks until successful send or err. // WARNING: Do not roll your own crypto. SendMany(messages []TargetedCmixMessage, p CMIXParams) ( - id.Round, []ephemeral.Id, error) + rounds.Round, []ephemeral.Id, error) + + // SendWithAssembler sends a variable cmix payload to the provided recipient. + // The payload sent is based on the Complier function passed in, which accepts + // a round ID and returns the necessary payload data. + // Returns the round ID of the round the payload was sent or an error if it + // fails. + // This does not have end-to-end encryption on it and is used exclusively as + // a send for higher order cryptographic protocols. Do not use unless + // implementing a protocol on top. + // recipient - cMix ID of the recipient. + // assembler - MessageAssembler function, accepting round ID and returning + // fingerprint + // format.Fingerprint, service message.Service, payload, mac []byte + // Will return an error if the network is unhealthy or if it fails to send + // (along with the reason). Blocks until successful sends or errors. + // WARNING: Do not roll your own crypto. + SendWithAssembler(recipient *id.ID, assembler MessageAssembler, + cmixParams CMIXParams) (rounds.Round, ephemeral.Id, error) /* === Message Reception ================================================ */ /* Identities are all network identities which the client is currently @@ -236,7 +254,7 @@ type Client interface { // GetRoundResults adjudicates on the rounds requested. Checks if they are // older rounds or in progress rounds. GetRoundResults(timeout time.Duration, roundCallback RoundEventCallback, - roundList ...id.Round) error + roundList ...id.Round) // LookupHistoricalRound looks up the passed historical round on the network. // GetRoundResults does this lookup when needed, generally that is @@ -297,6 +315,17 @@ type Client interface { type ClientErrorReport func(source, message, trace string) +// MessageAssembler func accepts a round ID, returning fingerprint, service, +// payload & mac. This allows users to pass in a paylaod which will contain the +// round ID over which the message is sent. +type MessageAssembler func(rid id.Round) (fingerprint format.Fingerprint, + service message.Service, payload, mac []byte, err error) + +// messageAssembler is an internal wrapper around MessageAssembler which +// returns a format.message This is necessary to preserve the interaction +// between sendCmixHelper and critical messages +type messageAssembler func(rid id.Round) (format.Message, error) + type clientCommsInterface interface { followNetworkComms SendCmixCommsInterface diff --git a/cmix/localTime.go b/cmix/localTime.go new file mode 100644 index 0000000000000000000000000000000000000000..b0b51d4bd2034d71e06321a467ff28907adf9980 --- /dev/null +++ b/cmix/localTime.go @@ -0,0 +1,19 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +package cmix + +import "time" + +// describes a local time object which gets time +// from the local clock in milliseconds +type localTime struct{} + +func (localTime) NowMs() int64 { + t := time.Now() + return (t.UnixNano() + int64(time.Millisecond)/2 + 1) / int64(time.Millisecond) +} diff --git a/cmix/message/bundle.go b/cmix/message/bundle.go index 88db932017ff4f33b714d782444c5264f5f1800f..7e8b20fb943988e00e39e27bbc9dc68ed3145b14 100644 --- a/cmix/message/bundle.go +++ b/cmix/message/bundle.go @@ -8,8 +8,8 @@ package message import ( - "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/client/cmix/identity/receptionID" + "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/primitives/format" "gitlab.com/xx_network/primitives/id" ) diff --git a/cmix/message/meteredCmixMessageBuffer.go b/cmix/message/meteredCmixMessageBuffer.go index 79604962bb8cfe8d076e3516096c80f04d3ee42b..826f8cab50f4e645652229830a56afd459fd70ad 100644 --- a/cmix/message/meteredCmixMessageBuffer.go +++ b/cmix/message/meteredCmixMessageBuffer.go @@ -11,6 +11,7 @@ import ( "encoding/json" "time" + "github.com/golang/protobuf/proto" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/cmix/identity/receptionID" @@ -21,7 +22,6 @@ import ( "gitlab.com/elixxir/primitives/states" "gitlab.com/xx_network/primitives/netTime" "golang.org/x/crypto/blake2b" - "google.golang.org/protobuf/proto" ) type meteredCmixMessageHandler struct{} diff --git a/cmix/nodes/register.go b/cmix/nodes/register.go index 5d2249ec617e7d39af16bcdd58cc0bd0d7879558..8cada7d120e663cf9ef075d11fe72c3c409c7859 100644 --- a/cmix/nodes/register.go +++ b/cmix/nodes/register.go @@ -134,6 +134,8 @@ func registerWithNode(sender gateway.Sender, comms RegisterNodeCommsInterface, var transmissionKey *cyclic.Int var validUntil uint64 var keyId []byte + + start := time.Now() // TODO: should move this to a pre-canned user initialization if s.IsPrecanned() { userNum := int(s.GetTransmissionID().Bytes()[7]) @@ -156,7 +158,8 @@ func registerWithNode(sender gateway.Sender, comms RegisterNodeCommsInterface, r.add(nodeID, transmissionKey, validUntil, keyId) - jww.INFO.Printf("Completed registration with node %s", nodeID) + jww.INFO.Printf("Completed registration with node %s,"+ + " took %d", nodeID, time.Since(start)) return nil } diff --git a/cmix/nodes/register_test.go b/cmix/nodes/register_test.go index 7349cd6ec55e61c484251a412576d3e87745b771..8ed9e6c897869e8ad9b5142b20a0e66e992b5621 100644 --- a/cmix/nodes/register_test.go +++ b/cmix/nodes/register_test.go @@ -88,4 +88,3 @@ func TestRegisterWithNode(t *testing.T) { t.Fatalf("registerWithNode error: %+v", err) } } - diff --git a/cmix/nodes/registrar.go b/cmix/nodes/registrar.go index dc61852f183937b5f4f9eadcda2cdfacd28d20a9..5ca5f108d201de75fcb739c43ea1bf2cea24951b 100644 --- a/cmix/nodes/registrar.go +++ b/cmix/nodes/registrar.go @@ -24,15 +24,15 @@ import ( ) const InputChanLen = 1000 -const maxAttempts = 5 +const maxAttempts = 2 // Backoff for attempting to register with a cMix node. var delayTable = [5]time.Duration{ 0, - 5 * time.Second, 30 * time.Second, 60 * time.Second, 120 * time.Second, + 240 * time.Second, } // registrar is an implementation of the Registrar interface. diff --git a/cmix/nodes/request.go b/cmix/nodes/request.go index 499acb4550b2ff31ebf2e4a2d4a8f8c0ef1c707b..abee1dfff97fb6621bc7079635332de833f82e00 100644 --- a/cmix/nodes/request.go +++ b/cmix/nodes/request.go @@ -8,6 +8,9 @@ package nodes import ( + "io" + "time" + "github.com/golang/protobuf/proto" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" @@ -24,10 +27,8 @@ import ( "gitlab.com/xx_network/crypto/chacha" "gitlab.com/xx_network/crypto/csprng" "gitlab.com/xx_network/crypto/signature/rsa" - "gitlab.com/xx_network/crypto/tls" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/netTime" - "io" ) // requestKey is a helper function which constructs a ClientKeyRequest message. @@ -41,9 +42,9 @@ func requestKey(sender gateway.Sender, comms RegisterNodeCommsInterface, // Generate a Diffie-Hellman keypair grp := r.session.GetCmixGroup() + start := time.Now() prime := grp.GetPBytes() - keyLen := len(prime) - dhPrivBytes, err := csprng.GenerateInGroup(prime, keyLen, rng) + dhPrivBytes, err := csprng.GenerateInGroup(prime, 32, rng) if err != nil { return nil, nil, 0, err } @@ -66,9 +67,11 @@ func requestKey(sender gateway.Sender, comms RegisterNodeCommsInterface, // Request nonce message from gateway jww.INFO.Printf("Register: Requesting client key from "+ - "gateway %s", gatewayID) + "gateway %s, setup took %s", gatewayID, time.Since(start)) + start = time.Now() result, err := sender.SendToAny(func(host *connect.Host) (interface{}, error) { + startInternal := time.Now() keyResponse, err2 := comms.SendRequestClientKeyMessage(host, signedKeyReq) if err2 != nil { return nil, errors.WithMessagef(err2, @@ -78,9 +81,11 @@ func requestKey(sender gateway.Sender, comms RegisterNodeCommsInterface, return nil, errors.WithMessage(err2, "requestKey: clientKeyResponse error") } + jww.TRACE.Printf("just comm reg request took %s", time.Since(startInternal)) return keyResponse, nil }, stop) + jww.TRACE.Printf("full reg request took %s", time.Since(start)) if err != nil { return nil, nil, 0, err @@ -169,22 +174,8 @@ func processRequestResponse(signedKeyResponse *pb.SignedKeyResponse, h.Write(signedKeyResponse.KeyResponse) hashedResponse := h.Sum(nil) - // Load nodes certificate - gatewayCert, err := tls.LoadCertificate(ngw.Gateway.TlsCertificate) - if err != nil { - return nil, nil, 0, - errors.Errorf("Unable to load nodes's certificate: %+v", err) - } - - // Extract public key - nodePubKey, err := tls.ExtractPublicKey(gatewayCert) - if err != nil { - return nil, nil, 0, - errors.Errorf("Unable to load node's public key: %v", err) - } - // Verify the response signature - err = rsa.Verify(nodePubKey, opts.Hash, hashedResponse, + err := verifyNodeSignature(ngw.Gateway.TlsCertificate, opts.Hash, hashedResponse, signedKeyResponse.KeyResponseSignedByGateway.Signature, opts) if err != nil { return nil, nil, 0, @@ -202,11 +193,14 @@ func processRequestResponse(signedKeyResponse *pb.SignedKeyResponse, // Convert Node DH Public key to a cyclic.Int nodeDHPub := grp.NewIntFromBytes(keyResponse.NodeDHPubKey) + start := time.Now() // Construct the session key h.Reset() sessionKey := registration.GenerateBaseKey(grp, nodeDHPub, dhPrivKey, h) + jww.TRACE.Printf("DH for reg took %s", time.Since(start)) + // Verify the HMAC if !registration.VerifyClientHMAC(sessionKey.Bytes(), keyResponse.EncryptedClientKey, opts.Hash.New, diff --git a/cmix/nodes/verifyNodeSig.go b/cmix/nodes/verifyNodeSig.go new file mode 100644 index 0000000000000000000000000000000000000000..55ae44c551fceffc76c170ee213d3e7b7db3dc8b --- /dev/null +++ b/cmix/nodes/verifyNodeSig.go @@ -0,0 +1,37 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build !js || !wasm + +package nodes + +import ( + "crypto" + "github.com/pkg/errors" + "gitlab.com/xx_network/crypto/tls" + + "gitlab.com/xx_network/crypto/signature/rsa" +) + +func verifyNodeSignature(certContents string, hash crypto.Hash, + hashed []byte, sig []byte, opts *rsa.Options) error { + + // Load nodes certificate + gatewayCert, err := tls.LoadCertificate(certContents) + if err != nil { + return errors.Errorf("Unable to load nodes's certificate: %+v", err) + } + + // Extract public key + nodePubKey, err := tls.ExtractPublicKey(gatewayCert) + if err != nil { + return errors.Errorf("Unable to load node's public key: %v", err) + } + + // Verify the response signature + return rsa.Verify(nodePubKey, hash, hashed, sig, opts) +} diff --git a/cmix/nodes/verifyNodeSig_js.go b/cmix/nodes/verifyNodeSig_js.go new file mode 100644 index 0000000000000000000000000000000000000000..6bc339bd38273160ce8a13515225c944d2d7f3a8 --- /dev/null +++ b/cmix/nodes/verifyNodeSig_js.go @@ -0,0 +1,23 @@ +//////////////////////////////////////////////////////////////////////////////// +// Copyright © 2022 xx foundation // +// // +// Use of this source code is governed by a license that can be found in the // +// LICENSE file. // +//////////////////////////////////////////////////////////////////////////////// + +//go:build js && wasm + +package nodes + +import ( + "crypto" + + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/xx_network/crypto/signature/rsa" +) + +func verifyNodeSignature(pub string, hash crypto.Hash, + hashed []byte, sig []byte, opts *rsa.Options) error { + jww.WARN.Printf("node signature checking disabled for wasm") + return nil +} diff --git a/cmix/params.go b/cmix/params.go index 95344b5490b6b0e6ae55de290fd04a03712b42c9..3ef813f0d9ca6e7903696f83cb6fea72e62a7962 100644 --- a/cmix/params.go +++ b/cmix/params.go @@ -10,6 +10,7 @@ package cmix import ( "encoding/base64" "encoding/json" + "fmt" "time" "gitlab.com/elixxir/client/cmix/message" @@ -58,6 +59,14 @@ type Params struct { // times. ReplayRequests bool + // MaxParallelIdentityTracks is the maximum number of parallel identities + // the system will poll in one iteration of the follower + MaxParallelIdentityTracks uint + + // ClockSkewClamp is the window (+/-) in which clock skew is + // ignored and local time is used + ClockSkewClamp time.Duration + Rounds rounds.Params Pickup pickup.Params Message message.Params @@ -80,6 +89,7 @@ type paramsDisk struct { Pickup pickup.Params Message message.Params Historical rounds.Params + MaxParallelIdentityTracks uint } // GetDefaultParams returns a Params object containing the @@ -89,13 +99,15 @@ func GetDefaultParams() Params { TrackNetworkPeriod: 100 * time.Millisecond, MaxCheckedRounds: 500, RegNodesBufferLen: 1000, - NetworkHealthTimeout: 30 * time.Second, + NetworkHealthTimeout: 15 * time.Second, ParallelNodeRegistrations: 20, KnownRoundsThreshold: 1500, // 5 rounds/sec * 60 sec/min * 5 min FastPolling: true, VerboseRoundTracking: false, RealtimeOnly: false, ReplayRequests: true, + MaxParallelIdentityTracks: 20, + ClockSkewClamp: 50 * time.Millisecond, } n.Rounds = rounds.GetDefaultParams() n.Pickup = pickup.GetDefaultParams() @@ -135,6 +147,7 @@ func (p Params) MarshalJSON() ([]byte, error) { Pickup: p.Pickup, Message: p.Message, Historical: p.Historical, + MaxParallelIdentityTracks: p.MaxParallelIdentityTracks, } return json.Marshal(&pDisk) @@ -163,6 +176,7 @@ func (p *Params) UnmarshalJSON(data []byte) error { Pickup: pDisk.Pickup, Message: pDisk.Message, Historical: pDisk.Historical, + MaxParallelIdentityTracks: pDisk.MaxParallelIdentityTracks, } return nil @@ -206,6 +220,10 @@ type CMIXParams struct { // should only be used in cases where repeats cannot be different. Only used // in sendCmix, not sendManyCmix. Critical bool + + // Probe tells the client that this send can be used to test network performance, + // that outgoing latency is not important + Probe bool } // cMixParamsDisk will be the marshal-able and umarshal-able object. @@ -228,7 +246,8 @@ func GetDefaultCMIXParams() CMIXParams { DebugTag: DefaultDebugTag, // Unused stoppable so components that require one have a channel to // wait on - Stop: stoppable.NewSingle("cmixParamsDefault"), + Stop: stoppable.NewSingle("cmixParamsDefault"), + Probe: false, } } @@ -282,6 +301,18 @@ func (p *CMIXParams) UnmarshalJSON(data []byte) error { return nil } +// SetDebugTag appends the debug tag if one already exists, +// otherwise it just used the new debug tag +func (p CMIXParams) SetDebugTag(newTag string) CMIXParams { + if p.DebugTag != DefaultDebugTag { + p.DebugTag = fmt.Sprintf("%s-%s", p.DebugTag, newTag) + } else { + p.DebugTag = newTag + } + + return p +} + // NodeMap represents a map of nodes and whether they have been // blacklisted. This is designed for use with CMIXParams.BlacklistedNodes type NodeMap map[id.ID]bool diff --git a/cmix/results.go b/cmix/results.go index 7c510525ea6bb0ba6bd073892327dc2c8f421702..916bdb206610fc2c98ed0509acba705d6c9c1a33 100644 --- a/cmix/results.go +++ b/cmix/results.go @@ -65,19 +65,19 @@ type RoundEventCallback func(allRoundsSucceeded, timedOut bool, rounds map[id.Ro // GetRoundResults adjudicates on the rounds requested. Checks if they are // older rounds or in progress rounds. func (c *client) GetRoundResults(timeout time.Duration, - roundCallback RoundEventCallback, roundList ...id.Round) error { + roundCallback RoundEventCallback, roundList ...id.Round) { jww.INFO.Printf("GetRoundResults(%v, %s)", roundList, timeout) sendResults := make(chan ds.EventReturn, len(roundList)) - return c.getRoundResults(roundList, timeout, roundCallback, + c.getRoundResults(roundList, timeout, roundCallback, sendResults) } // 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) error { + roundCallback RoundEventCallback, sendResults chan ds.EventReturn) { networkInstance := c.GetInstance() @@ -221,6 +221,4 @@ func (c *client) getRoundResults(roundList []id.Round, timeout time.Duration, } }() - - return nil } diff --git a/cmix/sendCmix.go b/cmix/sendCmix.go index 28253c0c26b26beb19345048b668a3684aab5075..87f882333c1ee4a7883a2f6c966e82758d2a1163 100644 --- a/cmix/sendCmix.go +++ b/cmix/sendCmix.go @@ -9,6 +9,9 @@ package cmix import ( "fmt" + "gitlab.com/elixxir/client/cmix/attempts" + "gitlab.com/elixxir/client/cmix/rounds" + "gitlab.com/elixxir/primitives/states" "strings" "time" @@ -56,42 +59,87 @@ import ( // WARNING: Do not roll your own crypto. func (c *client) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams CMIXParams) ( - id.Round, ephemeral.Id, error) { + rounds.Round, ephemeral.Id, error) { + // create an internal assembler function to pass to sendWithAssembler + assembler := func(rid id.Round) (format.Fingerprint, message.Service, + []byte, []byte, error) { + return fingerprint, service, payload, mac, nil + } + return c.sendWithAssembler(recipient, assembler, cmixParams) +} + +// SendWithAssembler sends a variable cmix payload to the provided recipient. +// The payload sent is based on the Complier function passed in, which accepts +// a round ID and returns the necessary payload data. +// Returns the round ID of the round the payload was sent or an error if it +// fails. +// This does not have end-to-end encryption on it and is used exclusively as +// a send for higher order cryptographic protocols. Do not use unless +// implementing a protocol on top. +// recipient - cMix ID of the recipient. +// assembler - MessageAssembler function, accepting round ID and returning fingerprint +// format.Fingerprint, service message.Service, payload, mac []byte +// Will return an error if the network is unhealthy or if it fails to send +// (along with the reason). Blocks until successful sends or errors. +// WARNING: Do not roll your own crypto. +func (c *client) SendWithAssembler(recipient *id.ID, assembler MessageAssembler, cmixParams CMIXParams) ( + rounds.Round, ephemeral.Id, error) { + // Critical messaging and assembler-based message payloads are not compatible + if cmixParams.Critical { + return rounds.Round{}, ephemeral.Id{}, errors.New("Cannot send critical messages with a message assembler") + } + return c.sendWithAssembler(recipient, assembler, cmixParams) +} + +// sendWithAssembler wraps the passed in MessageAssembler in a messageAssembler for sendCmixHelper, +// and sets up critical message handling where applicable. +func (c *client) sendWithAssembler(recipient *id.ID, assembler MessageAssembler, cmixParams CMIXParams) ( + rounds.Round, ephemeral.Id, error) { if !c.Monitor.IsHealthy() { - return 0, ephemeral.Id{}, errors.New( + return rounds.Round{}, ephemeral.Id{}, errors.New( "Cannot send cmix message when the network is not healthy") } - if len(payload) != c.maxMsgLen { - return 0, ephemeral.Id{}, errors.Errorf( - "bad message length (%d, need %d)", - len(payload), c.maxMsgLen) - } + // Create an internal messageAssembler which returns a format.Message + assemblerFunc := func(rid id.Round) (format.Message, error) { + fingerprint, service, payload, mac, err := assembler(rid) - // Build message. Will panic if inputs are not correct. - msg := format.NewMessage(c.session.GetCmixGroup().GetP().ByteLen()) - msg.SetContents(payload) - msg.SetKeyFP(fingerprint) - msg.SetSIH(service.Hash(msg.GetContents())) - msg.SetMac(mac) + if err != nil { + return format.Message{}, err + } - jww.TRACE.Printf("sendCmix Contents: %v, KeyFP: %v, MAC: %v, SIH: %v", - msg.GetContents(), msg.GetKeyFP(), msg.GetMac(), - msg.GetSIH()) + if len(payload) != c.maxMsgLen { + return format.Message{}, errors.Errorf( + "bad message length (%d, need %d)", + len(payload), c.maxMsgLen) + } - if cmixParams.Critical { - c.crit.AddProcessing(msg, recipient, cmixParams) + // Build message. Will panic if inputs are not correct. + msg := format.NewMessage(c.session.GetCmixGroup().GetP().ByteLen()) + msg.SetContents(payload) + msg.SetKeyFP(fingerprint) + msg.SetSIH(service.Hash(msg.GetContents())) + msg.SetMac(mac) + + jww.TRACE.Printf("sendCmix Contents: %v, KeyFP: %v, MAC: %v, SIH: %v", + msg.GetContents(), msg.GetKeyFP(), msg.GetMac(), + msg.GetSIH()) + + if cmixParams.Critical { + c.crit.AddProcessing(msg, recipient, cmixParams) + } + return msg, nil } - rid, ephID, rtnErr := sendCmixHelper(c.Sender, msg, recipient, cmixParams, + r, ephID, msg, rtnErr := sendCmixHelper(c.Sender, assemblerFunc, recipient, cmixParams, c.instance, c.session.GetCmixGroup(), c.Registrar, c.rng, c.events, - c.session.GetTransmissionID(), c.comms) + c.session.GetTransmissionID(), c.comms, c.attemptTracker) if cmixParams.Critical { - c.crit.handle(msg, recipient, rid, rtnErr) + c.crit.handle(msg, recipient, r.ID, rtnErr) } - return rid, ephID, rtnErr + return r, ephID, rtnErr } // sendCmixHelper is a helper function for client.SendCMIX. @@ -104,13 +152,13 @@ func (c *client) Send(recipient *id.ID, fingerprint format.Fingerprint, // If the message is successfully sent, the ID of the round sent it is returned, // which can be registered with the network instance to get a callback on its // status. -func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID, +func sendCmixHelper(sender gateway.Sender, assembler messageAssembler, recipient *id.ID, cmixParams CMIXParams, instance *network.Instance, grp *cyclic.Group, nodes nodes.Registrar, rng *fastRNG.StreamGenerator, events event.Reporter, - senderId *id.ID, comms SendCmixCommsInterface) (id.Round, ephemeral.Id, error) { + senderId *id.ID, comms SendCmixCommsInterface, attemptTracker attempts.SendAttemptTracker) (rounds.Round, ephemeral.Id, format.Message, error) { if cmixParams.RoundTries == 0 { - return 0, ephemeral.Id{}, + return rounds.Round{}, ephemeral.Id{}, format.Message{}, errors.Errorf("invalid parameter set, "+ "RoundTries cannot be 0: %+v", cmixParams) } @@ -125,48 +173,57 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID, attempted = excludedRounds.NewSet() } - jww.INFO.Printf("[Send-%s] Looking for round to send cMix message to "+ - "%s (msgDigest: %s)", cmixParams.DebugTag, recipient, - msg.Digest()) - stream := rng.GetStream() defer stream.Close() - // Flip leading bits randomly to thwart a tagging attack. - // See cmix.SetGroupBits for more info. - cmix.SetGroupBits(msg, grp, stream) + numAttempts := 0 + if !cmixParams.Probe { + optimalAttempts, ready := attemptTracker.GetOptimalNumAttempts() + if ready { + numAttempts = optimalAttempts + jww.INFO.Printf("[Send-%s] Looking for round to send cMix message to "+ + "%s, sending non probe with %d optimalAttempts", cmixParams.DebugTag, recipient, numAttempts) + } else { + numAttempts = 4 + jww.INFO.Printf("[Send-%s] Looking for round to send cMix message to "+ + "%s, sending non probe with %d non optimalAttempts, insufficient data", + cmixParams.DebugTag, recipient, numAttempts) + } + } else { + jww.INFO.Printf("[Send-%s] Looking for round to send cMix message to "+ + "%s, sending probe with %d Attempts, insufficient data", + cmixParams.DebugTag, recipient, numAttempts) + defer attemptTracker.SubmitProbeAttempt(numAttempts) + } - for numRoundTries := uint( - 0); numRoundTries < cmixParams.RoundTries; numRoundTries++ { + for numRoundTries := uint(0); numRoundTries < cmixParams.RoundTries; numRoundTries, + numAttempts = numRoundTries+1, numAttempts+1 { elapsed := netTime.Since(timeStart) jww.TRACE.Printf("[Send-%s] try %d, elapsed: %s", cmixParams.DebugTag, numRoundTries, elapsed) if elapsed > cmixParams.Timeout { jww.INFO.Printf("[Send-%s] No rounds to send to %s "+ - "(msgDigest: %s) were found before timeout %s", - cmixParams.DebugTag, recipient, msg.Digest(), - cmixParams.Timeout) - return 0, ephemeral.Id{}, errors.New( - "Sending cmix message timed out") + "were found before timeout %s", + cmixParams.DebugTag, recipient, cmixParams.Timeout) + return rounds.Round{}, ephemeral.Id{}, format.Message{}, errors.New("Sending cmix message timed out") } if numRoundTries > 0 { - jww.INFO.Printf("[Send-%s] Attempt %d to find round"+ - " to send message to %s (msgDigest: %s)", - cmixParams.DebugTag, - numRoundTries+1, recipient, msg.Digest()) + jww.INFO.Printf("[Send-%s] Attempt %d to find round to send "+ + "message to %s", cmixParams.DebugTag, + numRoundTries+1, recipient) } + startSearch := netTime.Now() // Find the best round to send to, excluding attempted rounds remainingTime := cmixParams.Timeout - elapsed waitingRounds := instance.GetWaitingRounds() - bestRound, err := waitingRounds.GetUpcomingRealtime( - remainingTime, attempted, sendTimeBuffer) + bestRound, _, err := waitingRounds.GetUpcomingRealtime( + remainingTime, attempted, numAttempts, sendTimeBuffer) if err != nil { - jww.WARN.Printf("[Send-%s] GetUpcomingRealtime failed "+ - "(msgDigest: %s): %+v", cmixParams.DebugTag, - msg.Digest(), err) + jww.WARN.Printf("[Send-%s] failed to GetUpcomingRealtime: "+ + "%+v", cmixParams.DebugTag, err) } if bestRound == nil { @@ -176,8 +233,8 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID, continue } - jww.TRACE.Printf("[Send-%s] Best round found: %+v", - cmixParams.DebugTag, bestRound) + jww.DEBUG.Printf("[Send-%s] Best round found, took %s: %d", + cmixParams.DebugTag, netTime.Since(startSearch), bestRound.ID) // Determine whether the selected round contains any // nodes that are blacklisted by the CMIXParams object @@ -203,6 +260,16 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID, continue } + msg, err := assembler(id.Round(bestRound.ID)) + if err != nil { + jww.ERROR.Printf("Failed to compile message: %+v", err) + return rounds.Round{}, ephemeral.Id{}, format.Message{}, err + } + + // Flip leading bits randomly to thwart a tagging attack. + // See cmix.SetGroupBits for more info. + cmix.SetGroupBits(msg, grp, stream) + // Retrieve host and key information from round firstGateway, roundKeys, err := processRound( nodes, bestRound, recipient.String(), msg.Digest()) @@ -219,13 +286,16 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID, wrappedMsg, encMsg, ephID, err := buildSlotMessage(msg, recipient, firstGateway, stream, senderId, bestRound, roundKeys) if err != nil { - return 0, ephemeral.Id{}, err + return rounds.Round{}, ephemeral.Id{}, format.Message{}, err } + timeRoundStart := time.Unix(0, int64(bestRound.Timestamps[states.QUEUED])) + jww.INFO.Printf("[Send-%s] Sending to EphID %d (%s), on round %d "+ - "(msgDigest: %s, ecrMsgDigest: %s) via gateway %s", - cmixParams.DebugTag, ephID.Int64(), recipient, bestRound.ID, - msg.Digest(), encMsg.Digest(), firstGateway.String()) + "(msgDigest: %s, ecrMsgDigest: %s) via gateway %s starting "+ + "at %s (%s in the future)", cmixParams.DebugTag, ephID.Int64(), + recipient, bestRound.ID, msg.Digest(), encMsg.Digest(), + firstGateway.String(), timeRoundStart, netTime.Until(timeRoundStart)) // Send the payload sendFunc := func(host *connect.Host, target *id.ID, @@ -269,7 +339,7 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID, // Exit if the thread has been stopped if stoppable.CheckErr(err) { - return 0, ephemeral.Id{}, err + return rounds.Round{}, ephemeral.Id{}, format.Message{}, err } // If the comm errors or the message fails to send, continue retrying @@ -278,7 +348,7 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID, jww.ERROR.Printf("[Send-%s] SendCmix failed to send to "+ "EphID %d (%s) on round %d: %+v", cmixParams.DebugTag, ephID.Int64(), recipient, bestRound.ID, err) - return 0, ephemeral.Id{}, err + return rounds.Round{}, ephemeral.Id{}, format.Message{}, err } jww.ERROR.Printf("[Send-%s] SendCmix failed to send to "+ @@ -298,7 +368,7 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID, jww.INFO.Print(m) events.Report(1, "MessageSend", "Metric", m) - return id.Round(bestRound.ID), ephID, nil + return rounds.MakeRound(bestRound), ephID, msg, nil } else { jww.FATAL.Panicf("[Send-%s] Gateway %s returned no error, "+ "but failed to accept message when sending to EphID %d (%s) "+ @@ -307,6 +377,6 @@ func sendCmixHelper(sender gateway.Sender, msg format.Message, recipient *id.ID, } } - return 0, ephemeral.Id{}, + return rounds.Round{}, ephemeral.Id{}, format.Message{}, errors.New("failed to send the message, out of round retries") } diff --git a/cmix/sendCmixUtils.go b/cmix/sendCmixUtils.go index 146e63117d5cde18c1ee8d9dc794156eded72ad1..e2d8ea178920d52c3fd3dcfbd78b4386896d5b75 100644 --- a/cmix/sendCmixUtils.go +++ b/cmix/sendCmixUtils.go @@ -41,7 +41,7 @@ type SendCmixCommsInterface interface { } // How much in the future a round needs to be to send to it -const sendTimeBuffer = 1000 * time.Millisecond +const sendTimeBuffer = 150 * time.Millisecond const unrecoverableError = "failed with an unrecoverable error" // handlePutMessageError handles errors received from a PutMessage or a diff --git a/cmix/sendManyCmix.go b/cmix/sendManyCmix.go index 4fa79c930c231e48a86e7c90b5f6324d4bdc2f25..3dcd12dda6043620ee9c48f8fd33d394a4352ab6 100644 --- a/cmix/sendManyCmix.go +++ b/cmix/sendManyCmix.go @@ -9,6 +9,8 @@ package cmix import ( "fmt" + "gitlab.com/elixxir/client/cmix/attempts" + "gitlab.com/elixxir/client/cmix/rounds" "strings" "time" @@ -67,9 +69,9 @@ type TargetedCmixMessage struct { // (along with the reason). Blocks until successful send or err. // WARNING: Do not roll your own crypto func (c *client) SendMany(messages []TargetedCmixMessage, - p CMIXParams) (id.Round, []ephemeral.Id, error) { + p CMIXParams) (rounds.Round, []ephemeral.Id, error) { if !c.Monitor.IsHealthy() { - return 0, []ephemeral.Id{}, errors.New( + return rounds.Round{}, []ephemeral.Id{}, errors.New( "Cannot send cMix message when the network is not healthy") } @@ -89,7 +91,7 @@ func (c *client) SendMany(messages []TargetedCmixMessage, return sendManyCmixHelper(c.Sender, acms, p, c.instance, c.session.GetCmixGroup(), c.Registrar, c.rng, c.events, - c.session.GetTransmissionID(), c.comms) + c.session.GetTransmissionID(), c.comms, c.attemptTracker) } type assembledCmixMessage struct { @@ -108,12 +110,12 @@ type assembledCmixMessage struct { // If the message is successfully sent, the ID of the round sent it is returned, // which can be registered with the network instance to get a callback on its // status. -func sendManyCmixHelper(sender gateway.Sender, - msgs []assembledCmixMessage, param CMIXParams, instance *network.Instance, - grp *cyclic.Group, registrar nodes.Registrar, - rng *fastRNG.StreamGenerator, events event.Reporter, - senderId *id.ID, comms SendCmixCommsInterface) ( - id.Round, []ephemeral.Id, error) { +func sendManyCmixHelper(sender gateway.Sender, msgs []assembledCmixMessage, + param CMIXParams, instance *network.Instance, grp *cyclic.Group, + registrar nodes.Registrar, rng *fastRNG.StreamGenerator, + events event.Reporter, senderId *id.ID, comms SendCmixCommsInterface, + attemptTracker attempts.SendAttemptTracker) ( + rounds.Round, []ephemeral.Id, error) { timeStart := netTime.Now() var attempted excludedRounds.ExcludedRounds @@ -140,14 +142,36 @@ func sendManyCmixHelper(sender gateway.Sender, cmix.SetGroupBits(msgs[i].Message, grp, stream) } - for numRoundTries := uint(0); numRoundTries < param.RoundTries; numRoundTries++ { + numAttempts := 0 + if !param.Probe { + optimalAttempts, ready := attemptTracker.GetOptimalNumAttempts() + if ready { + numAttempts = optimalAttempts + jww.INFO.Printf("[SendMany-%s] Looking for round to send cMix "+ + "messages to %s, sending non probe with %d optimalAttempts", + param.DebugTag, recipientString, numAttempts) + } else { + numAttempts = 4 + jww.INFO.Printf("[SendMany-%s] Looking for round to send cMix "+ + "messages to %s, sending non probe with %d non optimalAttempts, "+ + "insufficient data", param.DebugTag, recipientString, numAttempts) + } + } else { + jww.INFO.Printf("[SendMany-%s] Looking for round to send cMix messages "+ + "to %s, sending probe with %d Attempts, insufficient data", + param.DebugTag, recipientString, numAttempts) + defer attemptTracker.SubmitProbeAttempt(numAttempts) + } + + for numRoundTries := uint(0); numRoundTries < param.RoundTries; numRoundTries, + numAttempts = numRoundTries+1, numAttempts+1 { elapsed := netTime.Since(timeStart) if elapsed > param.Timeout { jww.INFO.Printf("[SendMany-%s] No rounds to send to %s "+ "(msgDigest: %s) were found before timeout %s", param.DebugTag, recipientString, msgDigests, param.Timeout) - return 0, []ephemeral.Id{}, + return rounds.Round{}, []ephemeral.Id{}, errors.New("sending cMix message timed out") } @@ -160,8 +184,8 @@ func sendManyCmixHelper(sender gateway.Sender, remainingTime := param.Timeout - elapsed // Find the best round to send to, excluding attempted rounds - bestRound, _ := instance.GetWaitingRounds().GetUpcomingRealtime( - remainingTime, attempted, sendTimeBuffer) + bestRound, _, _ := instance.GetWaitingRounds().GetUpcomingRealtime( + remainingTime, attempted, numAttempts, sendTimeBuffer) if bestRound == nil { continue } @@ -210,7 +234,7 @@ func sendManyCmixHelper(sender gateway.Sender, stream.Close() jww.INFO.Printf("[SendMany-%s] Error building slot "+ "received: %v", param.DebugTag, err) - return 0, []ephemeral.Id{}, errors.Errorf("failed to build "+ + return rounds.Round{}, []ephemeral.Id{}, errors.Errorf("failed to build "+ "slot message for %s: %+v", msg.Recipient, err) } } @@ -259,7 +283,7 @@ func sendManyCmixHelper(sender gateway.Sender, // Exit if the thread has been stopped if stoppable.CheckErr(err) { - return 0, []ephemeral.Id{}, err + return rounds.Round{}, []ephemeral.Id{}, err } // If the comm errors or the message fails to send, continue retrying @@ -276,7 +300,7 @@ func sendManyCmixHelper(sender gateway.Sender, jww.INFO.Printf("[SendMany-%s] Error received: %v", param.DebugTag, err) } - return 0, []ephemeral.Id{}, err + return rounds.Round{}, []ephemeral.Id{}, err } // Return if it sends properly @@ -288,7 +312,7 @@ func sendManyCmixHelper(sender gateway.Sender, bestRound.ID, msgDigests) jww.INFO.Print(m) events.Report(1, "MessageSendMany", "Metric", m) - return id.Round(bestRound.ID), ephemeralIDs, nil + return rounds.MakeRound(bestRound), ephemeralIDs, nil } else { jww.FATAL.Panicf("[SendMany-%s] Gateway %s returned no "+ "error, but failed to accept message when sending to EphIDs "+ @@ -297,6 +321,6 @@ func sendManyCmixHelper(sender gateway.Sender, } } - return 0, []ephemeral.Id{}, + return rounds.Round{}, []ephemeral.Id{}, errors.New("failed to send the message, unknown error") } diff --git a/cmix/utils_test.go b/cmix/utils_test.go index 54f05de98e8251b98f23934f00df9e21337c62a9..0a0cb6d2264412e767de7840fc73f07230e0b2c6 100644 --- a/cmix/utils_test.go +++ b/cmix/utils_test.go @@ -8,6 +8,7 @@ package cmix import ( + "gitlab.com/elixxir/client/cmix/rounds" "time" "github.com/pkg/errors" @@ -192,14 +193,14 @@ func (mrr *mockRoundEventRegistrar) AddRoundEventChan(rid id.Round, eventChan ch // mockCriticalSender func mockCriticalSender(msg format.Message, recipient *id.ID, - params CMIXParams) (id.Round, ephemeral.Id, error) { - return id.Round(1), ephemeral.Id{}, nil + params CMIXParams) (rounds.Round, ephemeral.Id, error) { + return rounds.Round{ID: 1}, ephemeral.Id{}, nil } // mockFailCriticalSender func mockFailCriticalSender(msg format.Message, recipient *id.ID, - params CMIXParams) (id.Round, ephemeral.Id, error) { - return id.Round(1), ephemeral.Id{}, errors.New("Test error") + params CMIXParams) (rounds.Round, ephemeral.Id, error) { + return rounds.Round{ID: 1}, ephemeral.Id{}, errors.New("Test error") } // func newTestClient(t *testing.T) (*client, error) { diff --git a/connect/authenticated.go b/connect/authenticated.go index 45f8dac4a97c204d9c25e87d07bbe86558933328..c5493deb0da13d227017e6695789f9317da2c830 100644 --- a/connect/authenticated.go +++ b/connect/authenticated.go @@ -138,12 +138,8 @@ func connectWithAuthentication(conn Connection, timeStart time.Time, // Track the result of the round(s) we sent the // identity authentication message on - err = net.GetRoundResults(remainingTime, + net.GetRoundResults(remainingTime, roundCb, sendReport.RoundList...) - if err != nil { - return nil, errors.Errorf("could not track rounds for successful " + - "identity confirmation message delivery") - } // Block waiting for confirmation of the round(s) success (or timeout jww.DEBUG.Printf("AuthenticatedConnection waiting for authenticated "+ "connection with %s to be established...", recipient.ID.String()) diff --git a/connect/utils_test.go b/connect/utils_test.go index fbf1d560f5182dc13bba249d09c44bb75b5a7155..86f190808b95d44e80cff854d87be643339f2f0c 100644 --- a/connect/utils_test.go +++ b/connect/utils_test.go @@ -160,11 +160,17 @@ func (m *mockCmix) Follow(cmix.ClientErrorReport) (stoppable.Stoppable, error) { func (m *mockCmix) GetMaxMessageLength() int { return 4096 } func (m *mockCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte, - []byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) { - return 0, ephemeral.Id{}, nil + []byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + return rounds.Round{}, ephemeral.Id{}, nil } -func (m *mockCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { - return 0, []ephemeral.Id{}, nil + +func (m *mockCmix) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler, + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + return rounds.Round{}, ephemeral.Id{}, nil +} + +func (m *mockCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { + return rounds.Round{}, []ephemeral.Id{}, nil } func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool) {} func (m *mockCmix) RemoveIdentity(*id.ID) {} @@ -189,9 +195,8 @@ func (m *mockCmix) HasNode(*id.ID) bool func (m *mockCmix) NumRegisteredNodes() int { return 24 } func (m *mockCmix) TriggerNodeRegistration(*id.ID) {} -func (m *mockCmix) GetRoundResults(_ time.Duration, roundCallback cmix.RoundEventCallback, _ ...id.Round) error { +func (m *mockCmix) GetRoundResults(_ time.Duration, roundCallback cmix.RoundEventCallback, _ ...id.Round) { roundCallback(true, false, nil) - return nil } func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error { return nil } diff --git a/dummy/mockCmix_test.go b/dummy/mockCmix_test.go index b60b5fe4ea43e0536f212fb0db291db8d95fc3b5..2aca679d0702225c69d01e7449c028ddd57cd7ee 100644 --- a/dummy/mockCmix_test.go +++ b/dummy/mockCmix_test.go @@ -38,12 +38,26 @@ func newMockCmix(payloadSize int) cmix.Client { } } -func (m *mockCmix) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (id.Round, ephemeral.Id, error) { +func (m *mockCmix) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { m.Lock() defer m.Unlock() m.messages[*recipient] = generateMessage(m.payloadSize, fingerprint, service, payload, mac) - return 0, ephemeral.Id{}, nil + return rounds.Round{}, ephemeral.Id{}, nil +} + +func (m *mockCmix) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler, + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + m.Lock() + defer m.Unlock() + + fingerprint, service, payload, mac, err := assembler(42) + if err != nil { + return rounds.Round{}, ephemeral.Id{}, err + } + m.messages[*recipient] = generateMessage(m.payloadSize, fingerprint, service, payload, mac) + + return rounds.Round{}, ephemeral.Id{}, nil } func (m *mockCmix) GetMsgListLen() int { @@ -64,11 +78,10 @@ func (m mockCmix) Follow(report cmix.ClientErrorReport) (stoppable.Stoppable, er } func (m mockCmix) GetMaxMessageLength() int { - //TODO implement me - panic("implement me") + return 100 } -func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { +func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { //TODO implement me panic("implement me") } @@ -163,7 +176,7 @@ func (m mockCmix) TriggerNodeRegistration(nid *id.ID) { panic("implement me") } -func (m mockCmix) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) error { +func (m mockCmix) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) { //TODO implement me panic("implement me") } diff --git a/dummy/random.go b/dummy/random.go index 2a41d8243decc43ae56ae93068c5106a8f9371e7..2f1abc73ec290ead86c142f527e85e8db176a136 100644 --- a/dummy/random.go +++ b/dummy/random.go @@ -49,7 +49,7 @@ func (m *Manager) newRandomCmixMessage(rng csprng.Source) ( } // Generate random message payload - payloadSize := m.store.GetCmixGroup().GetP().ByteLen() + payloadSize := m.net.GetMaxMessageLength() payload, err = newRandomPayload(payloadSize, rng) if err != nil { return nil, format.Fingerprint{}, message.Service{}, nil, nil, @@ -79,13 +79,8 @@ func (m *Manager) newRandomCmixMessage(rng csprng.Source) ( // newRandomPayload generates a random payload of a random length // within the maxPayloadSize. func newRandomPayload(maxPayloadSize int, rng csprng.Source) ([]byte, error) { - // Generate random payload size - randomPayloadSize, err := randomInt(maxPayloadSize, rng) - if err != nil { - return nil, errors.Errorf(payloadSizeRngErr, err) - } - randomMsg, err := csprng.Generate(randomPayloadSize, rng) + randomMsg, err := csprng.Generate(maxPayloadSize, rng) if err != nil { return nil, err } diff --git a/dummy/random_test.go b/dummy/random_test.go index fab84c22fc0e552931026ec0f27cc0c07e511b32..a4336983df79a8a33e3f95e56763d85728962d6c 100644 --- a/dummy/random_test.go +++ b/dummy/random_test.go @@ -75,16 +75,16 @@ func Test_durationRng_Consistency(t *testing.T) { // when using a PRNG and that the result is not larger than the max payload. func Test_newRandomPayload_Consistency(t *testing.T) { expectedPayloads := []string{ - "l7ufS7Ry6J9bFITyUgnJ", - "Ut/Xm012Qpthegyfnw07pVsMwNYUTIiFNQ==", - "CD9h", - "GSnh", - "joE=", - "uoQ+6NY+jE/+HOvqVG2PrBPdGqwEzi6ih3xVec+ix44bC6+uiBuCpw==", - "qkNGWnhiBhaXiu0M48bE8657w+BJW1cS/v2+DBAoh+EA2s0tiF9pLLYH2gChHBxwcec=", - "suEpcF4nPwXJIyaCjisFbg==", - "R/3zREEO1MEWAj+o41drb+0n/4l0usDK/ZrQVpKxNhnnOJZN/ceejVNDc2Yc/WbXTw==", - "bkt1IQ==", + "U4x/lrFkvxuXu59LtHLon1sUhPJSCcnZND6SugndnVLf15tNdkKbYXoMn58NO6VbDMDWFEyIhTWEGsvgcJsHWA==", + "CD9h03W8ArQd9PkZKeGP2p5vguVOdI6B555LvW/jTNy6hD7o1j6MT/4c6+pUbY+sE90arATOLqKHfFV5z6LHjg==", + "GwuvrogbgqdREIpC7TyQPKpDRlp4YgYWl4rtDOPGxPOue8PgSVtXEv79vgwQKIfhANrNLYhfaSy2B9oAoRwccA==", + "ceeWotwtwlpbdLLhKXBeJz8FySMmgo4rBW44F2WOEGFJiUf980RBDtTBFgI/qONXa2/tJ/+JdLrAyv2a0FaSsQ==", + "NhnnOJZN/ceejVNDc2Yc/WbXT+weG4lJGrcjbkt1IWKQzyvrQsPKJzKFYPGqwGfOpui/RtSrK0aAQCxfsoIOiA==", + "XTJg8d6XgoPUoJo2+WwglBdG4+1NpkaprotPp7T8OiC6+hp17TJ6hriww5rxz9KztRIZ6nlTOr9EjSxHnTJgdQ==", + "M5BZFMjMHPCdo54Okp0CSry8sWk5e7c05+8KbgHxhU3rX+Qk/vesIQiR9ZdeKSqiuKoEfGHNszNz6+csJ6CYwA==", + "IZfa5rcyw1HfZo+HTiyfHOCcqGAX5+IXSDA/9BwbI+EcSO0XU51oX3byp5i8ZN4OXbKGSyrTwmzmOCNCdloT1g==", + "luUt92D2w0ZeKaDcpGrDoNVwEzvCFXH19UpkMQVRP9hCmxlK4bqfKoOGrnKzZh/oLCrGTb9GFRgk4jBTEmN8mQ==", + "wrh9bfDdXvKDZxkHLWcvYfqgvob0V5Iew3wORgzw1wPQfcX1ZhpFATNAmnEramar17plIkyiaXjZpc5i/rEagw==", } prng := NewPrng(42) diff --git a/dummy/send.go b/dummy/send.go index ac6b39796add64bbfed8be9ed8c3e3f8687904f8..77b0f4e7de569e36b985c60521c6083b40006ee3 100644 --- a/dummy/send.go +++ b/dummy/send.go @@ -125,6 +125,7 @@ func (m *Manager) sendMessage(index, totalMessages int, rng csprng.Source) error // Send message p := cmix.GetDefaultCMIXParams() + p.Probe = true _, _, err = m.net.Send(recipient, fp, service, payload, mac, p) if err != nil { return errors.Errorf("Failed to send message: %+v", err) diff --git a/e2e/fpGenerator_test.go b/e2e/fpGenerator_test.go index f3bd2b1dc45c77a5fab006d6f0df9be3a20d9cb8..249dc5bba79fc07fb38439a64b4266a658215ee0 100644 --- a/e2e/fpGenerator_test.go +++ b/e2e/fpGenerator_test.go @@ -118,11 +118,15 @@ func newMockFpgCmix() *mockFpgCmix { func (m *mockFpgCmix) Follow(cmix.ClientErrorReport) (stoppable.Stoppable, error) { return nil, nil } func (m *mockFpgCmix) GetMaxMessageLength() int { return 0 } -func (m *mockFpgCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte, []byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) { - return 0, ephemeral.Id{}, nil +func (m *mockFpgCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte, []byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + return rounds.Round{}, ephemeral.Id{}, nil } -func (m *mockFpgCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { - return 0, nil, nil +func (m *mockFpgCmix) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler, + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + return rounds.Round{}, ephemeral.Id{}, nil +} +func (m *mockFpgCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { + return rounds.Round{}, nil, nil } func (m *mockFpgCmix) AddIdentity(*id.ID, time.Time, bool) {} func (m *mockFpgCmix) RemoveIdentity(*id.ID) {} @@ -166,8 +170,7 @@ func (m *mockFpgCmix) RemoveHealthCallback(uint64) func (m *mockFpgCmix) HasNode(*id.ID) bool { return false } func (m *mockFpgCmix) NumRegisteredNodes() int { return 0 } func (m *mockFpgCmix) TriggerNodeRegistration(*id.ID) {} -func (m *mockFpgCmix) GetRoundResults(time.Duration, cmix.RoundEventCallback, ...id.Round) error { - return nil +func (m *mockFpgCmix) GetRoundResults(time.Duration, cmix.RoundEventCallback, ...id.Round) { } func (m *mockFpgCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error { return nil } func (m *mockFpgCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) { diff --git a/e2e/parse/partition/store.go b/e2e/parse/partition/store.go index 5ca9007a85a78f28b4566b2ec0be710f4305b0bc..713fc291a9bb530994c3598b26e595eea52a6b03 100644 --- a/e2e/parse/partition/store.go +++ b/e2e/parse/partition/store.go @@ -171,7 +171,7 @@ func (s *Store) loadActivePartitions() { defer s.mux.Unlock() obj, err := s.kv.Get(activePartitions, activePartitionVersion) if err != nil { - jww.DEBUG.Printf("Could not load active partitions: %+v", err) + jww.DEBUG.Printf("Could not load active partitions: %s", err.Error()) return } diff --git a/e2e/rekey/utils_test.go b/e2e/rekey/utils_test.go index 34b7de358b4fafc072a95da66fd274aad0d53365..0b9fd388668ff77f9cbfed8bc7dc6dc6572d3eeb 100644 --- a/e2e/rekey/utils_test.go +++ b/e2e/rekey/utils_test.go @@ -236,13 +236,18 @@ func (m *mockNetManager) GetMaxMessageLength() int { func (m *mockNetManager) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) { - return id.Round(0), ephemeral.Id{}, nil + rounds.Round, ephemeral.Id, error) { + return rounds.Round{}, ephemeral.Id{}, nil +} + +func (m *mockNetManager) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler, + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + return rounds.Round{}, ephemeral.Id{}, nil } func (m *mockNetManager) SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) ( - id.Round, []ephemeral.Id, error) { - return id.Round(0), nil, nil + rounds.Round, []ephemeral.Id, error) { + return rounds.Round{}, nil, nil } func (m *mockNetManager) AddIdentity(id *id.ID, validUntil time.Time, persistent bool) {} @@ -297,8 +302,7 @@ func (m *mockNetManager) NumRegisteredNodes() int { func (m *mockNetManager) TriggerNodeRegistration(nid *id.ID) {} func (m *mockNetManager) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, - roundList ...id.Round) error { - return nil + roundList ...id.Round) { } func (m *mockNetManager) LookupHistoricalRound( diff --git a/e2e/sendE2E.go b/e2e/sendE2E.go index 6b3d275e7bd196379a752b5a83cdd5994698473b..c05700842e7264a04358be6803e8ac4ed038afb9 100644 --- a/e2e/sendE2E.go +++ b/e2e/sendE2E.go @@ -170,8 +170,7 @@ func (m *manager) prepareSendE2E(mt catalog.MessageType, recipient *id.ID, thisSendFunc := func() { wg.Add(1) go func(i int) { - var err error - roundIds[i], _, err = m.net.Send(recipient, + r, _, err := m.net.Send(recipient, key.Fingerprint(), s, contentsEnc, mac, params.CMIXParams) if err != nil { @@ -179,6 +178,7 @@ func (m *manager) prepareSendE2E(mt catalog.MessageType, recipient *id.ID, "Send: %+v", err) errCh <- err } + roundIds[i] = r.ID wg.Done() }(localI) } diff --git a/e2e/sendUnsafe.go b/e2e/sendUnsafe.go index 1eb2cee40684bcedeff7029ce08c8510ad38aeb0..b59bd3243bfb6a9208c302761580cf6f90417983 100644 --- a/e2e/sendUnsafe.go +++ b/e2e/sendUnsafe.go @@ -72,13 +72,13 @@ func (m *manager) sendUnsafe(mt catalog.MessageType, recipient *id.ID, jww.TRACE.Printf("sendUnsafe contents: %v, fp: %v, mac: %v", payload, fp, unencryptedMAC) - var err error - roundIds[i], _, err = m.net.Send(recipient, fp, + r, _, err := m.net.Send(recipient, fp, srvc, payload, unencryptedMAC, params.CMIXParams) if err != nil { errCh <- err } + roundIds[i] = r.ID wg.Done() }(i, p) } diff --git a/e2e/utils_test.go b/e2e/utils_test.go index d3dcda2491dbb91232101557d9f804472753211a..6d8835b8c51edaf47ca2821521e9e55de95f5db3 100644 --- a/e2e/utils_test.go +++ b/e2e/utils_test.go @@ -181,7 +181,7 @@ func (m *mockCmix) GetMaxMessageLength() int { } func (m *mockCmix) Send(_ *id.ID, fp format.Fingerprint, srv message.Service, - payload, mac []byte, _ cmix.CMIXParams) (id.Round, ephemeral.Id, error) { + payload, mac []byte, _ cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { m.handler.Lock() defer m.handler.Unlock() @@ -193,21 +193,26 @@ func (m *mockCmix) Send(_ *id.ID, fp format.Fingerprint, srv message.Service, if m.handler.processorMap[fp] != nil { m.handler.processorMap[fp].Process( msg, receptionID.EphemeralIdentity{}, rounds.Round{}) - return 0, ephemeral.Id{}, nil + return rounds.Round{}, ephemeral.Id{}, nil } else if m.handler.serviceMap[srv.Tag] != nil { m.handler.serviceMap[srv.Tag].Process( msg, receptionID.EphemeralIdentity{}, rounds.Round{}) - return 0, ephemeral.Id{}, nil + return rounds.Round{}, ephemeral.Id{}, nil } m.t.Errorf("No processor found for fingerprint %s", fp) - return 0, ephemeral.Id{}, + return rounds.Round{}, ephemeral.Id{}, errors.Errorf("No processor found for fingerprint %s", fp) } -func (m *mockCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { - return 0, nil, nil +func (m *mockCmix) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler, + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + panic("implement me") +} + +func (m *mockCmix) SendMany([]cmix.TargetedCmixMessage, cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { + return rounds.Round{}, nil, nil } func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool) {} func (m *mockCmix) RemoveIdentity(*id.ID) {} @@ -245,8 +250,7 @@ func (m *mockCmix) RemoveHealthCallback(uint64) {} func (m *mockCmix) HasNode(*id.ID) bool { return true } func (m *mockCmix) NumRegisteredNodes() int { return 0 } func (m *mockCmix) TriggerNodeRegistration(*id.ID) {} -func (m *mockCmix) GetRoundResults(time.Duration, cmix.RoundEventCallback, ...id.Round) error { - return nil +func (m *mockCmix) GetRoundResults(time.Duration, cmix.RoundEventCallback, ...id.Round) { } func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error { return nil } func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) { diff --git a/fileTransfer/connect/utils_test.go b/fileTransfer/connect/utils_test.go index b79fd3f6bae97ca390b9c140560b1c924db3bdca..6c9de504f42e812ec9bfc7c2c9925667241daa71 100644 --- a/fileTransfer/connect/utils_test.go +++ b/fileTransfer/connect/utils_test.go @@ -115,12 +115,12 @@ func (m *mockCmix) GetMaxMessageLength() int { } func (m *mockCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte, - []byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) { + []byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { panic("implement me") } func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, - _ cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { + _ cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { m.handler.Lock() for _, targetedMsg := range messages { msg := format.NewMessage(m.numPrimeBytes) @@ -132,7 +132,12 @@ func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, rounds.Round{ID: 42}) } m.handler.Unlock() - return 42, []ephemeral.Id{}, nil + return rounds.Round{ID: 42}, []ephemeral.Id{}, nil +} + +func (m *mockCmix) SendWithAssembler(*id.ID, cmix.MessageAssembler, + cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + panic("implement me") } func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool) { panic("implement me") } @@ -184,18 +189,19 @@ func (m *mockCmix) NumRegisteredNodes() int { panic("implement me") } func (m *mockCmix) TriggerNodeRegistration(*id.ID) { panic("implement me") } func (m *mockCmix) GetRoundResults(_ time.Duration, - roundCallback cmix.RoundEventCallback, _ ...id.Round) error { + roundCallback cmix.RoundEventCallback, _ ...id.Round) { go roundCallback(true, false, map[id.Round]cmix.RoundResult{42: {}}) - return nil } func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error { panic("implement me") } -func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) { +func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), + *stoppable.Single) (interface{}, error) { panic("implement me") } -func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, *stoppable.Single, time.Duration) (interface{}, error) { +func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, + *stoppable.Single, time.Duration) (interface{}, error) { panic("implement me") } func (m *mockCmix) SetGatewayFilter(gateway.Filter) { panic("implement me") } diff --git a/fileTransfer/e2e/utils_test.go b/fileTransfer/e2e/utils_test.go index f212e42d0dcb33ffe44f46a402701d146ce163ed..7de4bba2c25ddb23103da9f04c4a6eac8898984c 100644 --- a/fileTransfer/e2e/utils_test.go +++ b/fileTransfer/e2e/utils_test.go @@ -117,12 +117,12 @@ func (m *mockCmix) GetMaxMessageLength() int { } func (m *mockCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte, - []byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) { + []byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { panic("implement me") } func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, - _ cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { + _ cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { m.handler.Lock() for _, targetedMsg := range messages { msg := format.NewMessage(m.numPrimeBytes) @@ -134,7 +134,12 @@ func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, rounds.Round{ID: 42}) } m.handler.Unlock() - return 42, []ephemeral.Id{}, nil + return rounds.Round{ID: 42}, []ephemeral.Id{}, nil +} + +func (m *mockCmix) SendWithAssembler(*id.ID, cmix.MessageAssembler, + cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + panic("implement me") } func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool) { panic("implement me") } @@ -186,18 +191,19 @@ func (m *mockCmix) NumRegisteredNodes() int { panic("implement me") } func (m *mockCmix) TriggerNodeRegistration(*id.ID) { panic("implement me") } func (m *mockCmix) GetRoundResults(_ time.Duration, - roundCallback cmix.RoundEventCallback, _ ...id.Round) error { + roundCallback cmix.RoundEventCallback, _ ...id.Round) { go roundCallback(true, false, map[id.Round]cmix.RoundResult{42: {}}) - return nil } func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error { panic("implement me") } -func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) { +func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), + *stoppable.Single) (interface{}, error) { panic("implement me") } -func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, *stoppable.Single, time.Duration) (interface{}, error) { +func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, + *stoppable.Single, time.Duration) (interface{}, error) { panic("implement me") } func (m *mockCmix) SetGatewayFilter(gateway.Filter) { panic("implement me") } diff --git a/fileTransfer/groupChat/utils_test.go b/fileTransfer/groupChat/utils_test.go index e9f865511a40a99bb436cfbd448257744289b27f..61dd18006d7d80b35712cc938267a2f5bd2ae4f7 100644 --- a/fileTransfer/groupChat/utils_test.go +++ b/fileTransfer/groupChat/utils_test.go @@ -112,12 +112,12 @@ func (m *mockCmix) GetMaxMessageLength() int { } func (m *mockCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte, - []byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) { + []byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { panic("implement me") } func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, - _ cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { + _ cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { m.handler.Lock() for _, targetedMsg := range messages { msg := format.NewMessage(m.numPrimeBytes) @@ -129,7 +129,12 @@ func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, rounds.Round{ID: 42}) } m.handler.Unlock() - return 42, []ephemeral.Id{}, nil + return rounds.Round{ID: 42}, []ephemeral.Id{}, nil +} + +func (m *mockCmix) SendWithAssembler(*id.ID, cmix.MessageAssembler, + cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + panic("implement me") } func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool) { panic("implement me") } @@ -181,18 +186,19 @@ func (m *mockCmix) NumRegisteredNodes() int { panic("implement me") } func (m *mockCmix) TriggerNodeRegistration(*id.ID) { panic("implement me") } func (m *mockCmix) GetRoundResults(_ time.Duration, - roundCallback cmix.RoundEventCallback, _ ...id.Round) error { + roundCallback cmix.RoundEventCallback, _ ...id.Round) { go roundCallback(true, false, map[id.Round]cmix.RoundResult{42: {}}) - return nil } func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error { panic("implement me") } -func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) { +func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), + *stoppable.Single) (interface{}, error) { panic("implement me") } -func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, *stoppable.Single, time.Duration) (interface{}, error) { +func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, + *stoppable.Single, time.Duration) (interface{}, error) { panic("implement me") } func (m *mockCmix) SetGatewayFilter(gateway.Filter) { panic("implement me") } @@ -230,14 +236,14 @@ func newMockGC(handler *mockGcHandler) *mockGC { } func (m *mockGC) Send(groupID *id.ID, tag string, message []byte) ( - id.Round, time.Time, group.MessageID, error) { + rounds.Round, time.Time, group.MessageID, error) { m.handler.Lock() defer m.handler.Unlock() m.handler.services[tag].Process(groupChat.MessageReceive{ GroupID: groupID, Payload: message, }, format.Message{}, receptionID.EphemeralIdentity{}, rounds.Round{}) - return 0, time.Time{}, group.MessageID{}, nil + return rounds.Round{}, time.Time{}, group.MessageID{}, nil } func (m *mockGC) AddService(tag string, p groupChat.Processor) error { diff --git a/fileTransfer/groupChat/wrapper.go b/fileTransfer/groupChat/wrapper.go index 8566d645026bb5c3335cae6680cfdb4666576005..73bbf2cb0e26677c19046be47e31c444a7a33718 100644 --- a/fileTransfer/groupChat/wrapper.go +++ b/fileTransfer/groupChat/wrapper.go @@ -9,6 +9,7 @@ package groupChat import ( "github.com/pkg/errors" + "gitlab.com/elixxir/client/cmix/rounds" ft "gitlab.com/elixxir/client/fileTransfer" "gitlab.com/elixxir/client/groupChat" ftCrypto "gitlab.com/elixxir/crypto/fileTransfer" @@ -44,7 +45,7 @@ type Wrapper struct { // by the Wrapper for easier testing. type gcManager interface { Send(groupID *id.ID, tag string, message []byte) ( - id.Round, time.Time, group.MessageID, error) + rounds.Round, time.Time, group.MessageID, error) AddService(tag string, p groupChat.Processor) error } diff --git a/fileTransfer/manager.go b/fileTransfer/manager.go index 3963a3ca9e7e0c5a2a61c529c2c855d6f148b192..f4ef20744369953459f0f49c1fc24c5b57e18726 100644 --- a/fileTransfer/manager.go +++ b/fileTransfer/manager.go @@ -13,6 +13,7 @@ import ( jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/cmix/message" + "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/client/e2e" "gitlab.com/elixxir/client/fileTransfer/callbackTracker" "gitlab.com/elixxir/client/fileTransfer/store" @@ -145,7 +146,7 @@ type FtE2e interface { // transfer manager for easier testing. type Cmix interface { GetMaxMessageLength() int - SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (id.Round, + SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) AddFingerprint(identity *id.ID, fingerprint format.Fingerprint, mp message.Processor) error @@ -155,7 +156,7 @@ type Cmix interface { AddHealthCallback(f func(bool)) uint64 RemoveHealthCallback(uint64) GetRoundResults(timeout time.Duration, - roundCallback cmix.RoundEventCallback, roundList ...id.Round) error + roundCallback cmix.RoundEventCallback, roundList ...id.Round) } // Storage interface matches a subset of the storage.Session methods used by the diff --git a/fileTransfer/send.go b/fileTransfer/send.go index fd736280c8f55e64b927002c5aeec42120784cd5..e4a107f7b3e588318056706dc2b8f6dba9bf3d02 100644 --- a/fileTransfer/send.go +++ b/fileTransfer/send.go @@ -163,13 +163,14 @@ func (m *manager) sendCmix(packet []store.Part) { } } - err = m.cmix.GetRoundResults( - roundResultsTimeout, m.roundResultsCallback(validParts), rid) + m.cmix.GetRoundResults( + roundResultsTimeout, m.roundResultsCallback(validParts), rid.ID) } // roundResultsCallback generates a network.RoundEventCallback that handles // all parts in the packet once the round succeeds or fails. -func (m *manager) roundResultsCallback(packet []store.Part) cmix.RoundEventCallback { +func (m *manager) roundResultsCallback( + packet []store.Part) cmix.RoundEventCallback { // Group file parts by transfer grouped := map[ftCrypto.TransferID][]store.Part{} for _, p := range packet { diff --git a/fileTransfer/store/received.go b/fileTransfer/store/received.go index 189025a711b89537022a99ec72d19eaf98e63c91..6ec51f2e0188bc4d1ab05b7ec2c2ee044588951c 100644 --- a/fileTransfer/store/received.go +++ b/fileTransfer/store/received.go @@ -100,7 +100,7 @@ func (r *Received) AddTransfer(key *ftCrypto.TransferKey, _, exists := r.transfers[*tid] if exists { - return nil, errors.Errorf(errAddExistingReceivedTransfer, tid) + return nil, errors.Errorf(errAddExistingReceivedTransfer, *tid) } rt, err := newReceivedTransfer( diff --git a/fileTransfer/store/received_test.go b/fileTransfer/store/received_test.go index 4839c6b70c5b0d1403837303b2f876fa7b13f0bf..3bcba24b6e32847945ce6d10961517aa14dded3a 100644 --- a/fileTransfer/store/received_test.go +++ b/fileTransfer/store/received_test.go @@ -204,8 +204,8 @@ func TestReceived_save(t *testing.T) { kv := versioned.NewKV(ekv.MakeMemstore()) r, _, _ := NewOrLoadReceived(kv) r.transfers = map[ftCrypto.TransferID]*ReceivedTransfer{ - ftCrypto.TransferID{0}: nil, ftCrypto.TransferID{1}: nil, - ftCrypto.TransferID{2}: nil, ftCrypto.TransferID{3}: nil, + {0}: nil, {1}: nil, + {2}: nil, {3}: nil, } err := r.save() diff --git a/fileTransfer/store/sent.go b/fileTransfer/store/sent.go index a7f89a598dd79ec14b421b0101d0dae5ab7e05cf..d5e22ad8e976bb7d21c7e5e0c100496c19b363b3 100644 --- a/fileTransfer/store/sent.go +++ b/fileTransfer/store/sent.go @@ -108,7 +108,7 @@ func (s *Sent) AddTransfer(recipient *id.ID, key *ftCrypto.TransferKey, _, exists := s.transfers[*tid] if exists { - return nil, errors.Errorf(errAddExistingSentTransfer, tid) + return nil, errors.Errorf(errAddExistingSentTransfer, *tid) } st, err := newSentTransfer( diff --git a/fileTransfer/store/sent_test.go b/fileTransfer/store/sent_test.go index c343685bf5c4456def286fcb33360b694839f980..f3f24e43e4670b3d317d43d3e88a6c21c2ae4ace 100644 --- a/fileTransfer/store/sent_test.go +++ b/fileTransfer/store/sent_test.go @@ -225,8 +225,8 @@ func TestSent_save(t *testing.T) { kv := versioned.NewKV(ekv.MakeMemstore()) s, _, _ := NewOrLoadSent(kv) s.transfers = map[ftCrypto.TransferID]*SentTransfer{ - ftCrypto.TransferID{0}: nil, ftCrypto.TransferID{1}: nil, - ftCrypto.TransferID{2}: nil, ftCrypto.TransferID{3}: nil, + {0}: nil, {1}: nil, + {2}: nil, {3}: nil, } err := s.save() diff --git a/fileTransfer/utils_test.go b/fileTransfer/utils_test.go index 32c34ab230cb63a8d74aee6c2e50931b85ce6592..ff7750fcbce0c8d99f260ffdd2c48bb6d64d14a7 100644 --- a/fileTransfer/utils_test.go +++ b/fileTransfer/utils_test.go @@ -166,12 +166,12 @@ func (m *mockCmix) GetMaxMessageLength() int { } func (m *mockCmix) Send(*id.ID, format.Fingerprint, message.Service, []byte, - []byte, cmix.CMIXParams) (id.Round, ephemeral.Id, error) { + []byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { panic("implement me") } func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, - _ cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { + _ cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { m.handler.Lock() defer m.handler.Unlock() round := m.round @@ -185,7 +185,12 @@ func (m *mockCmix) SendMany(messages []cmix.TargetedCmixMessage, receptionID.EphemeralIdentity{Source: targetedMsg.Recipient}, rounds.Round{ID: round}) } - return round, []ephemeral.Id{}, nil + return rounds.Round{ID: round}, []ephemeral.Id{}, nil +} + +func (m *mockCmix) SendWithAssembler(*id.ID, cmix.MessageAssembler, + cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + panic("implement me") } func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool) { panic("implement me") } @@ -237,18 +242,19 @@ func (m *mockCmix) NumRegisteredNodes() int { panic("implement me") } func (m *mockCmix) TriggerNodeRegistration(*id.ID) { panic("implement me") } func (m *mockCmix) GetRoundResults(_ time.Duration, - roundCallback cmix.RoundEventCallback, rids ...id.Round) error { + roundCallback cmix.RoundEventCallback, rids ...id.Round) { go roundCallback(true, false, map[id.Round]cmix.RoundResult{rids[0]: {}}) - return nil } func (m *mockCmix) LookupHistoricalRound(id.Round, rounds.RoundResultCallback) error { panic("implement me") } -func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), *stoppable.Single) (interface{}, error) { +func (m *mockCmix) SendToAny(func(host *connect.Host) (interface{}, error), + *stoppable.Single) (interface{}, error) { panic("implement me") } -func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, *stoppable.Single, time.Duration) (interface{}, error) { +func (m *mockCmix) SendToPreferred([]*id.ID, gateway.SendToPreferredFunc, + *stoppable.Single, time.Duration) (interface{}, error) { panic("implement me") } func (m *mockCmix) SetGatewayFilter(gateway.Filter) { panic("implement me") } diff --git a/go.mod b/go.mod index dbcf2c6ea9aeed6f58d7159cf847bd27eda87ae7..13f55b334e9e8a2f31d1bcc842f14bc4140a548f 100644 --- a/go.mod +++ b/go.mod @@ -12,35 +12,47 @@ require ( github.com/spf13/cobra v1.5.0 github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/viper v1.12.0 + github.com/stretchr/testify v1.8.0 gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f - gitlab.com/elixxir/comms v0.0.4-0.20220916183942-257688b39f0c - gitlab.com/elixxir/crypto v0.0.7-0.20220901215826-1ceaeb59081f + gitlab.com/elixxir/comms v0.0.4-0.20221024232930-61a6369c1f68 + gitlab.com/elixxir/crypto v0.0.7-0.20221024215625-33315c3de43e gitlab.com/elixxir/ekv v0.2.1 - gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6 - gitlab.com/xx_network/comms v0.0.4-0.20220916183430-cc3077fedf9f - gitlab.com/xx_network/crypto v0.0.5-0.20220902182733-69aad094b487 - gitlab.com/xx_network/primitives v0.0.4-0.20220902183448-319596e2fec8 + gitlab.com/elixxir/primitives v0.0.3-0.20221017172918-6176818d1aba + gitlab.com/xx_network/comms v0.0.4-0.20221017172508-09e33697dc15 + gitlab.com/xx_network/crypto v0.0.5-0.20221017172404-b384a8d8b171 + gitlab.com/xx_network/primitives v0.0.4-0.20221017171439-42169a3e5c0d go.uber.org/ratelimit v0.2.0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa - golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b - google.golang.org/grpc v1.48.0 + golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c + google.golang.org/grpc v1.49.0 google.golang.org/protobuf v1.28.1 ) require ( + git.xx.network/elixxir/grpc-web-go-client v0.0.0-20220908170150-ef04339ffe65 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/badoux/checkmail v1.2.1 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/elliotchance/orderedmap v1.4.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/klauspost/compress v1.11.7 // indirect github.com/klauspost/cpuid/v2 v2.1.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/cors v1.8.2 // indirect + github.com/sethvargo/go-diceware v0.3.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect + github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -50,10 +62,12 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/zeebo/blake3 v0.2.3 // indirect gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 // indirect + go.uber.org/atomic v1.10.0 // indirect golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect golang.org/x/text v0.3.7 // indirect - google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 // indirect + google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + nhooyr.io/websocket v1.8.7 // indirect ) diff --git a/go.sum b/go.sum index 16d580d24dacae4af5cdd67a6a41c22af883a5a5..31dcefda3e6f7209aa7a43976c0c2c65890427bd 100644 --- a/go.sum +++ b/go.sum @@ -54,10 +54,17 @@ cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RX cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= +git.xx.network/elixxir/grpc-web-go-client v0.0.0-20220908170150-ef04339ffe65 h1:ksB3ZiMeFplqlaCjMDqKegbGzDZdS5pU0Z5GgFeBdZ0= +git.xx.network/elixxir/grpc-web-go-client v0.0.0-20220908170150-ef04339ffe65/go.mod h1:uFKw2wmgtlYMdiIm08dM0Vj4XvX9ZKVCj71c8O7SAPo= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= github.com/DataDog/datadog-go v3.2.0+incompatible/go.mod h1:LButxg5PwREeZtORoXG3tL4fMGNddJ+vMq1mwgfaqoQ= +github.com/Knetic/govaluate v3.0.1-0.20171022003610-9aa49832a739+incompatible/go.mod h1:r7JcOSlj0wfOMncg0iLm8Leh48TZaKVeNIfJntJ2wa0= github.com/OneOfOne/xxhash v1.2.2/go.mod h1:HSdplMjZKSmBqAxg5vPj2TmRDmfkzw+cTzAElWljhcU= +github.com/Shopify/sarama v1.19.0/go.mod h1:FVkBWblsNy7DGZRfXLU0O9RCGt5g3g3yEuWXgklEdEo= +github.com/Shopify/toxiproxy v2.1.4+incompatible/go.mod h1:OXgGpZ6Cli1/URJOF1DMxUHB2q5Ap20/P/eIdh4G0pI= +github.com/VividCortex/gohistogram v1.0.0/go.mod h1:Pf5mBqqDxYaXu3hDrrU+w6nw50o/4+TcAqDqk/vUH7g= +github.com/afex/hystrix-go v0.0.0-20180502004556-fa1af6a1f4f5/go.mod h1:SkGFH1ia65gfNATL8TAiHDNxPzPdmEL5uirI2Uyuz6c= github.com/alecthomas/template v0.0.0-20160405071501-a0175ee3bccc/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/template v0.0.0-20190718012654-fb15b899a751/go.mod h1:LOuyumcjzFXgccqObfd/Ljyb9UuFJ6TxHnclSeseNhc= github.com/alecthomas/units v0.0.0-20151022065526-2efee857e7cf/go.mod h1:ybxpYRFXyAe+OPACYpWeL0wqObRcbAqCMya13uyzqw0= @@ -66,11 +73,17 @@ github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk5 github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 h1:MzBOUgng9orim59UnfUTLRjMpd09C5uEVQ6RPGeCaVI= github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129/go.mod h1:rFgpPQZYZ8vdbc+48xibu8ALc3yeyd64IhHS+PU6Yyg= github.com/antihax/optional v1.0.0/go.mod h1:uupD/76wgC+ih3iEmQUL+0Ugr19nfwCT1kdvxnR2qWY= +github.com/apache/thrift v0.12.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= +github.com/apache/thrift v0.13.0/go.mod h1:cp2SuWMxlEZw2r+iP2GNCdIi4C1qmUzdZFSVb+bacwQ= github.com/armon/circbuf v0.0.0-20150827004946-bbbad097214e/go.mod h1:3U/XgcO3hCbHZ8TKRvWD2dDTCfh9M9ya+I9JpbB7O8o= github.com/armon/go-metrics v0.0.0-20180917152333-f0300d1749da/go.mod h1:Q73ZrmVTwzkszR9V5SSuryQ31EELlFMUz1kKyl939pY= github.com/armon/go-metrics v0.3.10/go.mod h1:4O98XIr/9W0sxpJ8UaYkvjk10Iff7SnFrb4QAOwNTFc= github.com/armon/go-radix v0.0.0-20180808171621-7fddfc383310/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= github.com/armon/go-radix v1.0.0/go.mod h1:ufUuZ+zHj4x4TnLV4JWEpy2hxWSpsRywHrMgIH9cCH8= +github.com/aryann/difflib v0.0.0-20170710044230-e206f873d14a/go.mod h1:DAHtR1m6lCRdSC2Tm3DSWRPvIPr6xNKyeHdqDQSQT+A= +github.com/aws/aws-lambda-go v1.13.3/go.mod h1:4UKl9IzQMoD+QF79YdCuzCwp8VbmG4VAQwij/eHl5CU= +github.com/aws/aws-sdk-go v1.27.0/go.mod h1:KmX6BPdI08NWTb3/sm4ZGu5ShLoqVDhKgpiN924inxo= +github.com/aws/aws-sdk-go-v2 v0.18.0/go.mod h1:JWVYvqSMppoMJC0x5wdwiImzgXTI9FuZwxzkQq9wy+g= github.com/badoux/checkmail v1.2.1 h1:TzwYx5pnsV6anJweMx2auXdekBwGr/yt1GgalIx9nBQ= github.com/badoux/checkmail v1.2.1/go.mod h1:XroCOBU5zzZJcLvgwU15I+2xXyCdTWXyR9MGfRhBYy0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= @@ -79,6 +92,12 @@ github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6r github.com/bgentry/speakeasy v0.1.0/go.mod h1:+zsyZBPWlz7T6j88CTgSN5bM796AkVf0kBD4zp0CCIs= github.com/bwesterb/go-ristretto v1.2.0/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= github.com/bwesterb/go-ristretto v1.2.1/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= +github.com/casbin/casbin/v2 v2.1.2/go.mod h1:YcPU1XXisHhLzuxH9coDNf2FbKpjGlbCg3n9yuLkIJQ= +github.com/cenkalti/backoff v2.2.1+incompatible h1:tNowT99t7UNflLxfYYSlKYsBpXdEet03Pg2g16Swow4= +github.com/cenkalti/backoff v2.2.1+incompatible/go.mod h1:90ReRw6GdpyfrHakVjL/QHaoyV4aDUVVkXQJJJ3NXXM= +github.com/cenkalti/backoff/v4 v4.1.1/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= +github.com/cenkalti/backoff/v4 v4.1.3 h1:cFAlzYUlVYDysBEH2T5hyJZMh3+5+WCBvSnK6Q8UtC4= +github.com/cenkalti/backoff/v4 v4.1.3/go.mod h1:scbssz8iZGpm3xbr14ovlUdkxfGXNInqkPWOWmG2CLw= 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/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= @@ -87,6 +106,7 @@ github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5P github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= github.com/circonus-labs/circonus-gometrics v2.3.1+incompatible/go.mod h1:nmEj6Dob7S7YxXgwXpfOuvO54S+tGdZdw9fuRZt25Ag= github.com/circonus-labs/circonusllhist v0.1.3/go.mod h1:kMXHVDlOchFAehlya5ePtbp5jckzBHf4XRpQvBOLI+I= +github.com/clbanning/x2j v0.0.0-20191024224557-825249438eec/go.mod h1:jMjuTZXRI4dUb/I5gc9Hdhagfvm9+RyrPryS/auMzxE= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cloudflare/circl v1.1.0/go.mod h1:prBCrKB9DV4poKZY1l9zBXg2QJY7mvgRvtMxxK7fi4I= github.com/cloudflare/circl v1.2.0 h1:NheeISPSUcYftKlfrLuOo4T62FkmD4t4jviLfFFYaec= @@ -100,16 +120,32 @@ github.com/cncf/xds/go v0.0.0-20210805033703-aa0b78936158/go.mod h1:eXthEFrGJvWH github.com/cncf/xds/go v0.0.0-20210922020428-25de7278fc84/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211001041855-01bcc9b48dfe/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= github.com/cncf/xds/go v0.0.0-20211011173535-cb28da3451f1/go.mod h1:eXthEFrGJvWHgFFCl3hGmgk+/aYT6PnTQLykKQRLhEs= +github.com/cockroachdb/datadriven v0.0.0-20190809214429-80d97fb3cbaa/go.mod h1:zn76sxSg3SzpJ0PPJaLDCu+Bu0Lg3sKTORVIj19EIF8= +github.com/codahale/hdrhistogram v0.0.0-20161010025455-3a0bb77429bd/go.mod h1:sE/e/2PUdi/liOCUjSTXgM1o87ZssimdTWN964YiIeI= +github.com/coreos/go-semver v0.2.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= github.com/coreos/go-semver v0.3.0/go.mod h1:nnelYz7RCh+5ahJtPPxZlU+153eP4D4r3EedlOD2RNk= +github.com/coreos/go-systemd v0.0.0-20180511133405-39ca1b05acc7/go.mod h1:F5haX7vjVVG0kc13fIWeqUViNPyEJxv/OmvnBo0Yme4= github.com/coreos/go-systemd/v22 v22.3.2/go.mod h1:Y58oyj3AT4RCenI/lSvhwexgC+NSVTIJ3seZv2GcEnc= +github.com/coreos/pkg v0.0.0-20160727233714-3ac0863d7acf/go.mod h1:E3G3o1h8I7cfcXa63jLwjI0eiQQMgzzUDFVpN/nH/eA= +github.com/cpuguy83/go-md2man/v2 v2.0.0-20190314233015-f79a8a8ca69d/go.mod h1:maD7wRr/U5Z6m/iR4s+kqSMx2CaBsrgA7czyZG/E6dU= github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/creack/pty v1.1.7/go.mod h1:lj5s0c3V2DBrqTV7llrYr5NG6My20zk30Fl46Y7DoTY= github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 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/desertbit/timer v0.0.0-20180107155436-c41aec40b27f h1:U5y3Y5UE0w7amNe7Z5G/twsBW0KEalRQXZzf8ufSh9I= +github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f/go.mod h1:xH/i4TFMt8koVQZ6WFms69WAsDWr2XsYL3Hkl7jkoLE= +github.com/dgrijalva/jwt-go v3.2.0+incompatible/go.mod h1:E3ru+11k8xSBh+hMPgOLZmtrrCbhqsmaPHjLKYnJCaQ= +github.com/dustin/go-humanize v0.0.0-20171111073723-bb3d318650d4/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= github.com/dustin/go-humanize v1.0.0/go.mod h1:HtrtbFcZ19U5GC7JDqmcUSB87Iq5E25KnS6fMYU6eOk= +github.com/eapache/go-resiliency v1.1.0/go.mod h1:kFI+JgMyC7bLPUVY133qvEBtVayf5mFgVsvEsIPBvNs= +github.com/eapache/go-xerial-snappy v0.0.0-20180814174437-776d5712da21/go.mod h1:+020luEh2TKB4/GOp8oxxtq0Daoen/Cii55CzbTV6DU= +github.com/eapache/queue v1.1.0/go.mod h1:6eCeP0CKFpHLu8blIFXhExK/dRa7WDZfr6jVFPTqq+I= +github.com/edsrzf/mmap-go v1.0.0/go.mod h1:YO35OhQPt3KJa3ryjFM5Bs14WD66h8eGKpfaBNrHW5M= github.com/elliotchance/orderedmap v1.4.0 h1:wZtfeEONCbx6in1CZyE6bELEt/vFayMvsxqI5SgsR+A= github.com/elliotchance/orderedmap v1.4.0/go.mod h1:wsDwEaX5jEoyhbs7x93zk2H/qv0zwuhg4inXhDkYqys= +github.com/envoyproxy/go-control-plane v0.6.9/go.mod h1:SBwIajubJHhxtWwsL9s8ss4safvEdbitLhGGK48rN6g= 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= @@ -123,28 +159,54 @@ github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7 github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4= github.com/fatih/color v1.9.0/go.mod h1:eQcE1qtQxscV5RaZvpXrrb8Drkc3/DdQ+uUYCNjL+zU= github.com/fatih/color v1.13.0/go.mod h1:kLAiJbzzSOZDVNGyDpeOxJ47H46qBXwg5ILebYFFOfk= +github.com/franela/goblin v0.0.0-20200105215937-c9ffbefa60db/go.mod h1:7dvUGVsVBjqR7JHJk0brhHOZYGmfBYOrK0ZhYMEtBr4= +github.com/franela/goreq v0.0.0-20171204163338-bcd34c9993f8/go.mod h1:ZhphrRTfi2rbfLwlschooIH4+wKKDR4Pdxhh+TRoA20= github.com/frankban/quicktest v1.14.3 h1:FJKSZTDHjyhriyC81FLQ0LY93eSai0ZyR/ZIkd3ZUKE= github.com/frankban/quicktest v1.14.3/go.mod h1:mgiwOwqx65TmIk1wJ6Q7wvnVMocbUorkibMOrVTHZps= +github.com/fsnotify/fsnotify v1.4.7/go.mod h1:jwhsz4b93w/PPRr/qN1Yymfu8t87LnFCMoQvtojpjFo= github.com/fsnotify/fsnotify v1.4.9/go.mod h1:znqG4EE+3YCdAaPaxE2ZRY/06pZUdp0tY4IgpuI1SZQ= github.com/fsnotify/fsnotify v1.5.4 h1:jRbGcIw6P2Meqdwuo0H1p6JVLbL5DHKAKlYndzMwVZI= github.com/fsnotify/fsnotify v1.5.4/go.mod h1:OVB6XrOHzAwXMpEM7uPOzcehqUV2UqJxmVXmkdnm1bU= github.com/ghodss/yaml v1.0.0/go.mod h1:4dBDuWmgqj2HViK6kFavaiC9ZROes6MMH2rRYeMEF04= +github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= +github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= +github.com/gin-gonic/gin v1.6.3 h1:ahKqKTFpO5KTPHxWZjEdPScmYaGtLo8Y4DMHoEsnp14= +github.com/gin-gonic/gin v1.6.3/go.mod h1:75u5sXoLsGZoRN5Sgbi1eraJ4GU3++wFwWzhwvtwp4M= github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as= +github.com/go-kit/kit v0.10.0/go.mod h1:xUsJbQ/Fp4kEt7AFgCuvyX4a71u8h9jB8tj/ORgOZ7o= github.com/go-kit/log v0.1.0/go.mod h1:zbhenjAZHb184qTLMA9ZjW7ThYL0H2mk7Q6pNt4vbaY= github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE= github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk= github.com/go-logfmt/logfmt v0.5.0/go.mod h1:wCYkCAKZfumFQihp8CzCvQ3paCTfi41vtzG1KdI/P7A= +github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= +github.com/go-playground/locales v0.13.0 h1:HyWk6mgj5qFqCT5fjGBuRArbVDfE4hi8+e8ceBS/t7Q= +github.com/go-playground/locales v0.13.0/go.mod h1:taPMhCMXrRLJO55olJkUXHZBHCxTMfnGwq/HNwmWNS8= +github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD876Lmtgy7VtROAbHHXk8no= +github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA= +github.com/go-playground/validator/v10 v10.2.0 h1:KgJ0snyC2R9VXYN2rneOtQcw5aHQB1Vv0sFl1UcHBOY= +github.com/go-playground/validator/v10 v10.2.0/go.mod h1:uOYAAleCW8F/7oMFd6aG0GOhaH6EGOAJShg8Id5JGkI= +github.com/go-sql-driver/mysql v1.4.0/go.mod h1:zAC/RDZ24gD3HViQzih4MyKcchzm+sOG5ZlKdlhCg5w= github.com/go-stack/stack v1.8.0/go.mod h1:v0f6uXyyMGvRgIKkXu+yp6POWl0qKG85gN/melR3HDY= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee h1:s+21KNqlpePfkah2I+gwHF8xmJWRjooY+5248k6m4A0= +github.com/gobwas/httphead v0.0.0-20180130184737-2c6c146eadee/go.mod h1:L0fX3K22YWvt/FAX9NnzrNzcI4wNYi9Yku4O0LKYflo= +github.com/gobwas/pool v0.2.0 h1:QEmUOlnSjWtnpRGHF3SauEiOsy82Cup83Vf2LcMlnc8= +github.com/gobwas/pool v0.2.0/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw= +github.com/gobwas/ws v1.0.2 h1:CoAavW/wd/kulfZmSIBt6p24n4j7tHgNVCjsfHVNUbo= +github.com/gobwas/ws v1.0.2/go.mod h1:szmBTxLgaFppYjEmNtny/v3w89xOydFnnZMcgRRu/EM= github.com/godbus/dbus/v5 v5.0.4/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA= +github.com/gogo/googleapis v1.1.0/go.mod h1:gf4bu3Q80BeJ6H1S1vYPm8/ELATdvryBaNFGgqEef3s= github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.0/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ= +github.com/gogo/protobuf v1.2.1/go.mod h1:hp+jE20tsWTFYpLwKvXlhS1hjn+gTNwPg2I6zVXpSg4= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3 h1:zN2lZNZRflqFyxVaTIU61KNKQ9C0055u9CAfpmqUvo4= github.com/golang-collections/collections v0.0.0-20130729185459-604e922904d3/go.mod h1:nPpo7qLxd6XL3hWJG/O60sR8ZKfMCiIoNap5GvD12KU= github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= +github.com/golang/groupcache v0.0.0-20160516000752-02826c3e7903/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= @@ -176,6 +238,7 @@ github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaS github.com/golang/protobuf v1.5.1/go.mod h1:DopwsBzvsk0Fs44TXzsVbJyPhcCPeIwnvohx4u74HPM= github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= +github.com/golang/snappy v0.0.0-20180518054509-2e65f85255db/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/golang/snappy v0.0.3/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= @@ -215,6 +278,7 @@ github.com/google/pprof v0.0.0-20210601050228-01bbb1931b22/go.mod h1:kpwsk12EmLe github.com/google/pprof v0.0.0-20210609004039-a478d1d731e9/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/pprof v0.0.0-20210720184732-4bb14d4b1be1/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= +github.com/google/uuid v1.0.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= @@ -224,9 +288,22 @@ github.com/googleapis/gax-go/v2 v2.2.0/go.mod h1:as02EH8zWkzwUoLbBaFeQ+arQaj/Oth github.com/googleapis/gax-go/v2 v2.3.0/go.mod h1:b8LNqSzNabLiUpXKkY7HAR5jr6bIT99EXz9pXxye9YM= github.com/googleapis/gax-go/v2 v2.4.0/go.mod h1:XOTVJ59hdnfJLIP/dh8n5CGryZR2LxK9wbMD5+iXC6c= github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= +github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY= +github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= +github.com/gorilla/mux v1.6.2/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/mux v1.7.3/go.mod h1:1lud6UwP+6orDFRuTfBEV8e9/aOM/c4fVVCaMa2zaAs= +github.com/gorilla/websocket v0.0.0-20170926233335-4201258b820c/go.mod h1:E7qHFY5m1UJ88s3WnNqhKjPHQ0heANvMoAMk2YaljkQ= +github.com/gorilla/websocket v1.4.1/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc= +github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +github.com/grpc-ecosystem/go-grpc-middleware v1.0.1-0.20190118093823-f849b5445de4/go.mod h1:FiyG127CGDf3tlThmgyCl78X/SZQqEOJBCDaAfeWzPs= +github.com/grpc-ecosystem/go-grpc-middleware v1.2.2/go.mod h1:EaizFBKfUKtMIF5iaDEhniwNedqGo9FuLFzppDr3uwI= github.com/grpc-ecosystem/go-grpc-prometheus v1.2.0/go.mod h1:8NvIoxWQoOIhqOTXgfV/d3M/q6VIi02HzZEHgUlZvzk= +github.com/grpc-ecosystem/grpc-gateway v1.9.5/go.mod h1:vNeuVxBJEsws4ogUvrchl83t/GYV9WGTSLVdBhOQFDY= github.com/grpc-ecosystem/grpc-gateway v1.16.0/go.mod h1:BDjrQk3hbvj6Nolgz8mAMFbcEtjT1g+wF4CSlocrBnw= +github.com/hashicorp/consul/api v1.3.0/go.mod h1:MmDNSzIMUjNpY/mQ398R4bk2FnqQLoPndWW5VkKPlCE= github.com/hashicorp/consul/api v1.12.0/go.mod h1:6pVBMo0ebnYdt2S3H87XhekM/HHrUoTD2XXb/VrZVy0= +github.com/hashicorp/consul/sdk v0.3.0/go.mod h1:VKf9jXwCTEY1QZP2MOLRhb5i/I/ssyNV1vwHyQBF0x8= github.com/hashicorp/consul/sdk v0.8.0/go.mod h1:GBvyrGALthsZObzUGsfgHZQDXjg4lOjagTIwIR1vPms= github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4= github.com/hashicorp/go-cleanhttp v0.5.0/go.mod h1:JpRdi6/HCYpAwUzNwuwqhbovhLtngrth3wmdIIUrZ80= @@ -240,37 +317,61 @@ github.com/hashicorp/go-msgpack v0.5.3/go.mod h1:ahLV/dePpqEmjfWmKiqvPkv/twdG7iP github.com/hashicorp/go-multierror v1.0.0/go.mod h1:dHtQlpGsu+cZNNAkkCN/P3hoUDHhCYQXV3UM06sGGrk= github.com/hashicorp/go-multierror v1.1.0/go.mod h1:spPvp8C1qA32ftKqdAHm4hHTbPw+vmowP0z+KUhOZdA= github.com/hashicorp/go-retryablehttp v0.5.3/go.mod h1:9B5zBasrRhHXnJnui7y6sL7es7NDiJgTc6Er0maI1Xs= +github.com/hashicorp/go-rootcerts v1.0.0/go.mod h1:K6zTfqpRlCUIjkwsN4Z+hiSfzSTQa6eBIzfwKfwNnHU= github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8= github.com/hashicorp/go-sockaddr v1.0.0/go.mod h1:7Xibr9yA9JjQq1JpNB2Vw7kxv8xerXegt+ozgdvDeDU= github.com/hashicorp/go-syslog v1.0.0/go.mod h1:qPfqrKkXGihmCqbJM2mZgkZGvKG1dFdvsLplgctolz4= github.com/hashicorp/go-uuid v1.0.0/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= github.com/hashicorp/go-uuid v1.0.1/go.mod h1:6SBZvOh/SIDV7/2o3Jml5SYk/TvGqwFJ/bN7x4byOro= +github.com/hashicorp/go-version v1.0.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go-version v1.2.0/go.mod h1:fltr4n8CU8Ke44wwGCBoEymUuxUHl09ZGVZPK5anwXA= +github.com/hashicorp/go.net v0.0.1/go.mod h1:hjKkEWcCURg++eb33jQU7oqQcI9XDCnUzHA0oac0k90= github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= github.com/hashicorp/golang-lru v0.5.4/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= github.com/hashicorp/logutils v1.0.0/go.mod h1:QIAnNjmIWmVIIkWDTG1z5v++HQmx9WQRO+LraFDTW64= +github.com/hashicorp/mdns v1.0.0/go.mod h1:tL+uN++7HEJ6SQLQ2/p+z2pH24WQKWjBPkE0mNTz8vQ= github.com/hashicorp/mdns v1.0.4/go.mod h1:mtBihi+LeNXGtG8L9dX59gAEa12BDtBQSp4v/YAJqrc= +github.com/hashicorp/memberlist v0.1.3/go.mod h1:ajVTdAv/9Im8oMAAj5G31PhhMCZJV2pPBoIllUwCN7I= github.com/hashicorp/memberlist v0.3.0/go.mod h1:MS2lj3INKhZjWNqd3N0m3J+Jxf3DAOnAH9VT3Sh9MUE= +github.com/hashicorp/serf v0.8.2/go.mod h1:6hOLApaqBFA1NXqRQAsxw9QxuDEvNxSQRwA/JwenrHc= github.com/hashicorp/serf v0.9.6/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= github.com/hashicorp/serf v0.9.7/go.mod h1:TXZNMjZQijwlDvp+r0b63xZ45H7JmCmgg4gpTwn9UV4= +github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= +github.com/hudl/fargo v1.3.0/go.mod h1:y3CKSmjA+wD2gak7sUSXTAoopbhU08POFhmITJgmKTg= github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= +github.com/improbable-eng/grpc-web v0.12.0/go.mod h1:6hRR09jOEG81ADP5wCQju1z71g6OL4eEvELdran/3cs= +github.com/improbable-eng/grpc-web v0.14.1/go.mod h1:zEjGHa8DAlkoOXmswrNvhUGEYQA9UI7DhrGeHR1DMGU= +github.com/improbable-eng/grpc-web v0.15.0 h1:BN+7z6uNXZ1tQGcNAuaU1YjsLTApzkjt2tzCixLaUPQ= +github.com/improbable-eng/grpc-web v0.15.0/go.mod h1:1sy9HKV4Jt9aEs9JSnkWlRJPuPtwNr0l57L4f878wP8= github.com/inconshreveable/mousetrap v1.0.0 h1:Z8tu5sraLXCXIcARxBp/8cbvlwVa7Z1NHg9XEKhtSvM= github.com/inconshreveable/mousetrap v1.0.0/go.mod h1:PxqpIevigyE2G7u3NXJIT2ANytuPF1OarO4DADm73n8= +github.com/influxdata/influxdb1-client v0.0.0-20191209144304-8bf82d3c094d/go.mod h1:qj24IKcXYK6Iy9ceXlo3Tc+vtHo9lIhSX5JddghvEPo= +github.com/jmespath/go-jmespath v0.0.0-20180206201540-c2b33e8439af/go.mod h1:Nht3zPeWKUH0NzdCt2Blrr5ys8VGpn0CEB0cQHVjt7k= +github.com/jonboulle/clockwork v0.1.0/go.mod h1:Ii8DK3G1RaLaWxj9trq07+26W01tbo22gdxWY5EU2bo= github.com/jpillora/backoff v1.0.0/go.mod h1:J/6gKK9jxlEcS3zixgDgUAsiuZ7yrSoa/FX5e0EB2j4= github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU= +github.com/json-iterator/go v1.1.7/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.8/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.10/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= github.com/json-iterator/go v1.1.11/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4= +github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= +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= github.com/julienschmidt/httprouter v1.3.0/go.mod h1:JR6WtHb+2LUe8TCKY3cZOxFyyO8IZAc4RVcycCCAKdM= +github.com/kisielk/errcheck v1.1.0/go.mod h1:EZBBE59ingxPouuu3KfxchcWSUPOHkagtvWXihfKN4Q= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= +github.com/klauspost/compress v1.10.3/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= +github.com/klauspost/compress v1.11.7 h1:0hzRabrMN4tSTvMfnL3SCv1ZGeAP23ynzodBgaHeMeg= +github.com/klauspost/compress v1.11.7/go.mod h1:aoV0uJVorq1K+umq18yTdKaF57EivdYsUV+/s2qKfXs= github.com/klauspost/cpuid/v2 v2.0.4/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.12/go.mod h1:g2LTdtYhdyuGPqyWyv7qRAmj1WBqxuObKfj5c0PQa7c= @@ -289,8 +390,21 @@ github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/ktr0731/dept v0.1.3/go.mod h1:b1EtCEjbjGShAfhZue+BrFKTG7sQmK7aSD7Q6VcGvO0= +github.com/ktr0731/go-multierror v0.0.0-20171204182908-b7773ae21874/go.mod h1:ZWayuE/hCzOD96CJizvcYnqrbmTC7RAG332yNtlKj6w= +github.com/ktr0731/grpc-test v0.1.4/go.mod h1:v47616grayBYXQveGWxO3OwjLB3nEEnHsZuMTc73FM0= +github.com/ktr0731/grpc-test v0.1.12 h1:Yha+zH2hB48huOfbsEMfyG7FeHCrVWq4fYmHfr3iH3U= +github.com/ktr0731/grpc-test v0.1.12/go.mod h1:AP4+ZrqSzdDaUNhAsp2fye06MXO2fdYY6YQJifb588M= +github.com/ktr0731/grpc-web-go-client v0.2.8 h1:nUf9p+YWirmFwmH0mwtAWhuXvzovc+/3C/eAY2Fshnk= +github.com/ktr0731/grpc-web-go-client v0.2.8/go.mod h1:1Iac8gFJvC/DRfZoGnFZsfEbEq/wQFK+2Ve1o3pHkCQ= +github.com/ktr0731/modfile v1.11.2/go.mod h1:LzNwnHJWHbuDh3BO17lIqzqDldXqGu1HCydWH3SinE0= +github.com/leodido/go-urn v1.2.0 h1:hpXL4XnriNwQ/ABnpepYM/1vCLWNDfUNts8dX3xTG6Y= +github.com/leodido/go-urn v1.2.0/go.mod h1:+8+nEpDfqqsY+g338gtMEUOtuK+4dEMhiQEgxpxOKII= +github.com/lightstep/lightstep-tracer-common/golang/gogo v0.0.0-20190605223551-bc2310a04743/go.mod h1:qklhhLq1aX+mtWk9cPHPzaBjWImj5ULL6C7HFJtXQMM= +github.com/lightstep/lightstep-tracer-go v0.18.1/go.mod h1:jlF1pusYV4pidLvZ+XD0UBX0ZE6WURAspgAczcDHrL4= github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea h1:uyJ13zfy6l79CM3HnVhDalIyZ4RJAyVfDrbnfFeJoC4= github.com/liyue201/goqr v0.0.0-20200803022322-df443203d4ea/go.mod h1:w4pGU9PkiX2hAWyF0yuHEHmYTQFAd6WHzp6+IY7JVjE= +github.com/lyft/protoc-gen-validate v0.0.13/go.mod h1:XbGvPuh87YZc5TdIa2/I4pLk0QoUACkjt2znoq26NVQ= github.com/magiconair/properties v1.8.6 h1:5ibWZ6iY0NctNGWo87LalDlEZ6R41TqbbDamhfG/Qzo= github.com/magiconair/properties v1.8.6/go.mod h1:y3VJvCyxH9uVvJTWEGAELF3aiYNyPKd5NZ3oSwXrF60= github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU= @@ -299,48 +413,90 @@ github.com/mattn/go-colorable v0.1.6/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope github.com/mattn/go-colorable v0.1.9/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc= github.com/mattn/go-colorable v0.1.12/go.mod h1:u5H1YNBxpqRaxsYJYSkiCWKzEfiAb1Gb520KVy5xxl4= github.com/mattn/go-isatty v0.0.3/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/mattn/go-isatty v0.0.8/go.mod h1:Iq45c/XA43vh69/j3iqttzPXn0bhXyGjM0Hdxcsrc5s= github.com/mattn/go-isatty v0.0.10/go.mod h1:qgIWMr58cqv1PHHyhnkY9lrL7etaEgOFcMEpPG5Rm84= github.com/mattn/go-isatty v0.0.11/go.mod h1:PhnuNfih5lzO57/f3n+odYbM4JtupLOxQOAqxQCu2WE= github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU= +github.com/mattn/go-isatty v0.0.14 h1:yVuAays6BHfxijgZPzw+3Zlu5yQgKGP2/hcQbHb7S9Y= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= +github.com/mattn/go-runewidth v0.0.2/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= +github.com/miekg/dns v1.0.14/go.mod h1:W1PPwlIAgtquWBMBEV9nkV9Cazfe8ScdGz/Lj7v3Nrg= github.com/miekg/dns v1.1.26/go.mod h1:bPDLeHnStXmXAq1m/Ch/hvfNHr14JKNPMBo3VZKjuso= github.com/miekg/dns v1.1.41/go.mod h1:p6aan82bvRIyn+zDIv9xYNUpwa73JcSh9BKwknJysuI= github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +github.com/mitchellh/cli v1.0.0/go.mod h1:hNIlj7HEI86fIcpObd7a0FcrxTWetlwJDGcceTlRvqc= github.com/mitchellh/cli v1.1.0/go.mod h1:xcISNoH86gajksDmfB23e/pu+B+GeFRMYmoHXxx3xhI= +github.com/mitchellh/copystructure v1.0.0/go.mod h1:SNtv71yrdKgLRyLFxmLdkAbkKEFWgYaq1OVrnRcwhnw= +github.com/mitchellh/go-homedir v1.0.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= github.com/mitchellh/go-testing-interface v1.0.0/go.mod h1:kRemZodwjscx+RGhAo8eIhFbs2+BFgRtFPeD/KE+zxI= +github.com/mitchellh/gox v0.4.0/go.mod h1:Sd9lOJ0+aimLBi73mGofS1ycjY8lL3uZM3JPS42BGNg= +github.com/mitchellh/iochan v1.0.0/go.mod h1:JwYml1nuB7xOzsp52dPpHFffvOCDupsG0QubkSMEySY= github.com/mitchellh/mapstructure v0.0.0-20160808181253-ca63d7c062ee/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.1.2/go.mod h1:FVVH3fgwuzCH5S8UJGiWEs2h04kUh9fWfEaFds41c1Y= github.com/mitchellh/mapstructure v1.4.3/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/reflectwalk v1.0.0/go.mod h1:mSTlrgnPZtwu0c4WaC2kGObEpuNDbx0jmZXqmk4esnw= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= +github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/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/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/mr-tron/base58 v1.2.0/go.mod h1:BinMc/sQntlIE1frQmRFPUoPA1Zkr8VRgBdjWI2mNwc= github.com/multiformats/go-multihash v0.2.1/go.mod h1:WxoMcYG85AZVQUyRyo9s4wULvW5qrI9vb2Lt6evduFc= github.com/multiformats/go-varint v0.0.6/go.mod h1:3Ls8CIEsrijN6+B7PbrXRPxHRPuXSrVKRY101jdMZYE= github.com/mwitkow/go-conntrack v0.0.0-20161129095857-cc309e4a2223/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f h1:KUppIJq7/+SVif2QVs3tOP0zanoHgBEVAwHxUSIzRqU= github.com/mwitkow/go-conntrack v0.0.0-20190716064945-2f068394615f/go.mod h1:qRWi+5nqEBWmkhHvq77mSJWrCKwh8bxhgT7d/eI7P4U= +github.com/mwitkow/grpc-proxy v0.0.0-20181017164139-0f1106ef9c76/go.mod h1:x5OoJHDHqxHS801UIuhqGl6QdSAEJvtausosHSdazIo= +github.com/nats-io/jwt v0.3.0/go.mod h1:fRYCDE99xlTsqUzISS1Bi75UBJ6ljOJQOAAu5VglpSg= +github.com/nats-io/jwt v0.3.2/go.mod h1:/euKqTS1ZD+zzjYrY7pseZrTtWQSjujC7xjPc8wL6eU= +github.com/nats-io/nats-server/v2 v2.1.2/go.mod h1:Afk+wRZqkMQs/p45uXdrVLuab3gwv3Z8C4HTBu8GD/k= +github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzEE/Zbp4w= +github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ= github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= +github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= +github.com/oklog/run v1.0.0/go.mod h1:dlhp/R75TPv97u0XWUtDeV/lRKWPKSdTuV0TZvrmrQA= +github.com/olekukonko/tablewriter v0.0.0-20170122224234-a0225b3f23b5/go.mod h1:vsDQFd/mU46D+Z4whnwzcISnGGzXWMclvtLoiIKAKIo= +github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/ginkgo v1.7.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= +github.com/onsi/gomega v1.4.3/go.mod h1:ex+gbHU/CVuBBDIJjb2X0qEXbFg53c61hWP/1CpauHY= +github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= +github.com/opentracing-contrib/go-observer v0.0.0-20170622124052-a52f23424492/go.mod h1:Ngi6UdF0k5OKD5t5wlmGhe/EDKPoUM3BXZSSfIuJbis= +github.com/opentracing/basictracer-go v1.0.0/go.mod h1:QfBfYuafItcjQuMwinw9GhYKwFXS9KnPs5lxoYwgW74= +github.com/opentracing/opentracing-go v1.0.2/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/opentracing/opentracing-go v1.1.0/go.mod h1:UkNAQd3GIcIGf0SeVgPpRdFStlNbqXla1AfSYxPUl2o= +github.com/openzipkin-contrib/zipkin-go-opentracing v0.4.5/go.mod h1:/wsWhb9smxSfWAKL3wpBW7V8scJMt8N8gnaMCS9E/cA= +github.com/openzipkin/zipkin-go v0.1.6/go.mod h1:QgAqvLzwWbR/WpD4A3cGpPtJrZXNIiJc5AZX7/PBEpw= +github.com/openzipkin/zipkin-go v0.2.1/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/openzipkin/zipkin-go v0.2.2/go.mod h1:NaW6tEwdmWMaCDZzg8sh+IBNOxHMPnhQw8ySjnjRyN4= +github.com/pact-foundation/pact-go v1.0.4/go.mod h1:uExwJY4kCzNPcHRj+hCR/HBbOOIwwtUjcrb0b5/5kLM= github.com/pascaldekloe/goe v0.0.0-20180627143212-57f6aae5913c/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= github.com/pascaldekloe/goe v0.1.0/go.mod h1:lzWF7FIEvWOWxwDKqyGYQf6ZUaNfKdP144TG7ZOy1lc= +github.com/pborman/uuid v1.2.0/go.mod h1:X/NO0urCmaxf9VXbdlT7C2Yzkj2IKimNn4k+gtPdI/k= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo= github.com/pelletier/go-toml/v2 v2.0.2 h1:+jQXlF3scKIcSEKkdHzXhCTDLPFi5r1wnK6yPS+49Gw= github.com/pelletier/go-toml/v2 v2.0.2/go.mod h1:MovirKjgVRESsAvNZlAjtFwV867yGuwRkXbG66OzopI= +github.com/performancecopilot/speed v3.0.0+incompatible/go.mod h1:/CLtqpZ5gBg1M9iaPbIdPPGyKcA8hKdoy6hAWba7Yac= +github.com/pierrec/lz4 v1.0.2-0.20190131084431-473cd7ce01a1/go.mod h1:3/3N9NVKO0jef7pBehbT1qWhCMrIgbYNnFAZCqQ5LRc= +github.com/pierrec/lz4 v2.0.5+incompatible/go.mod h1:pdkljMzZIN41W+lC3N2tnIh5sFi+IEE17M5jbnwPHcY= github.com/pkg/errors v0.8.0/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pkg/errors v0.8.1/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= +github.com/pkg/profile v1.2.1/go.mod h1:hJw3o1OdXxsrSjjVksARp5W95eeEaEfptyVZyv6JUPA= github.com/pkg/profile v1.6.0 h1:hUDfIISABYI59DyeB3OTay/HxSRwTQ8rB/H83k6r5dM= github.com/pkg/profile v1.6.0/go.mod h1:qBsxPvzyUincmltOk6iyRVxHYg4adc0OFOv72ZdLa18= github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= @@ -349,36 +505,63 @@ github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZN github.com/posener/complete v1.1.1/go.mod h1:em0nMJCgc9GFtwrmVmEMR/ZL6WyhyjMBndrE9hABlRI= github.com/posener/complete v1.2.3/go.mod h1:WZIdtGGp+qx0sLrYKtIRAruyNpv6hFCicSgv7Sy7s/s= github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw= +github.com/prometheus/client_golang v0.9.3-0.20190127221311-3c4408c8b829/go.mod h1:p2iRAGwDERtqlqzRXnrOVns+ignqQo//hLXqYxZYVNs= github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo= +github.com/prometheus/client_golang v1.3.0/go.mod h1:hJaj2vgQTGQmVCsAACORcieXFeDPbaTKGT+JTgUa3og= github.com/prometheus/client_golang v1.4.0/go.mod h1:e9GMxYsXl05ICDXkRhurwBS4Q3OK1iX/F2sw+iXX5zU= github.com/prometheus/client_golang v1.7.1/go.mod h1:PY5Wy2awLA44sXw4AOSfFBetzPP4j5+D6mVACh+pe2M= github.com/prometheus/client_golang v1.11.1/go.mod h1:Z6t4BnS23TR94PD6BsDNk8yVqroYurpAkEiz0P2BEV0= github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= +github.com/prometheus/client_model v0.0.0-20190115171406-56726106282f/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo= github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/client_model v0.1.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= +github.com/prometheus/common v0.2.0/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4= +github.com/prometheus/common v0.7.0/go.mod h1:DjGbpBbp5NYNiECxcL/VnbXCCaQpKd3tt26CguLLsqA= github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4= github.com/prometheus/common v0.10.0/go.mod h1:Tlit/dnDKsSWFlCLTWaA1cyBgKHSMdTB80sz/V91rCo= +github.com/prometheus/common v0.15.0/go.mod h1:U+gB1OBLb1lF3O42bTCL+FK18tX9Oar16Clt/msog/s= github.com/prometheus/common v0.26.0/go.mod h1:M7rCNAaPfAosfx8veZJCuw84e35h3Cfd9VFqTh1DIvc= github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= +github.com/prometheus/procfs v0.0.0-20190117184657-bf6a532e95b1/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk= github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA= github.com/prometheus/procfs v0.0.8/go.mod h1:7Qr8sr6344vo1JqZ6HhLceV9o3AJ1Ff+GxbHq6oeK9A= github.com/prometheus/procfs v0.1.3/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= +github.com/prometheus/procfs v0.3.0/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU= github.com/prometheus/procfs v0.6.0/go.mod h1:cz+aTbrPOrUb4q7XlbU9ygM+/jj0fzG6c1xBZuNvfVA= +github.com/rakyll/statik v0.1.6/go.mod h1:OEi9wJV/fMUAGx1eNjq75DKDsJVuEv1U0oYdX6GX8Zs= +github.com/rcrowley/go-metrics v0.0.0-20181016184325-3113b8401b8a/go.mod h1:bCqnVzQkZxMG4s8nGwiZ5l3QUCyqpo9Y+/ZMZ9VjZe4= +github.com/rogpeppe/fastuuid v0.0.0-20150106093220-6724a57986af/go.mod h1:XWv6SoW27p1b0cqNHllgS5HIMJraePCO15w5zCzIWYg= github.com/rogpeppe/fastuuid v1.2.0/go.mod h1:jVj6XXZzXRy/MSR5jhDC/2q6DgLz+nrA6LYCDYWNEvQ= github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= github.com/rogpeppe/go-internal v1.6.1 h1:/FiVV8dS/e+YqF2JvO3yXRFbBLTIuSDkuC7aBOAvL+k= github.com/rogpeppe/go-internal v1.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/rs/cors v1.8.2 h1:KCooALfAYGs415Cwu5ABvv9n9509fSiG5SQJn/AQo4U= +github.com/rs/cors v1.8.2/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= +github.com/russross/blackfriday/v2 v2.0.1/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= github.com/ryanuber/columnize v0.0.0-20160712163229-9b3edd62028f/go.mod h1:sm1tb6uqfes/u+d4ooFouqFdy9/2g9QGwK3SQygK0Ts= github.com/sagikazarmark/crypt v0.6.0/go.mod h1:U8+INwJo3nBv1m6A/8OBXAq7Jnpspk5AxSgDyEQcea8= +github.com/samuel/go-zookeeper v0.0.0-20190923202752-2cc03de413da/go.mod h1:gi+0XIa01GRL2eRQVjQkKGqKF3SF9vZR/HnPullcV2E= github.com/sean-/seed v0.0.0-20170313163322-e2103e2c3529/go.mod h1:DxrIzT+xaE7yg65j358z/aeFdxmN0P9QXhEzd20vsDc= +github.com/sethvargo/go-diceware v0.3.0 h1:UVVEfmN/uF50JfWAN7nbY6CiAlp5xeSx+5U0lWKkMCQ= +github.com/sethvargo/go-diceware v0.3.0/go.mod h1:lH5Q/oSPMivseNdhMERAC7Ti5oOPqsaVddU1BcN1CY0= +github.com/shurcooL/sanitized_anchor_name v1.0.0/go.mod h1:1NzhyTcUVG4SuEtjjoZeVRXNmyL/1OwPU0+IJeTBvfc= github.com/sirupsen/logrus v1.2.0/go.mod h1:LxeOpSwHxABJmUn/MG1IvRgCAasNZTLOkJPxbbu5VWo= github.com/sirupsen/logrus v1.4.2/go.mod h1:tLMulIdttU9McNUspp0xgXVQah82FyeX6MwdIuYE2rE= github.com/sirupsen/logrus v1.6.0/go.mod h1:7uNnSEd1DgxDLC74fIahvMZmmYsHGZGEOFrfsX/uA88= +github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M= +github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc= +github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA= +github.com/soheilhy/cmux v0.1.4/go.mod h1:IM3LyeVVIOuxMH7sFAkER9+bJ4dT7Ms6E4xg4kGIyLM= +github.com/soheilhy/cmux v0.1.5 h1:jjzc5WVemNEDTLwv9tlmemhC73tI08BNOIGwBOo10Js= +github.com/soheilhy/cmux v0.1.5/go.mod h1:T7TcVDs9LWfQgPlPsdngu6I6QIoyIFZDDC6sNE1GqG0= +github.com/sony/gobreaker v0.4.1/go.mod h1:ZKptC7FHNvhBz7dN2LGjPVBz2sZJmc0/PkyDJOjmxWY= github.com/spaolacci/murmur3 v0.0.0-20180118202830-f09979ecbc72/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spaolacci/murmur3 v1.1.0/go.mod h1:JwIasOWyU6f++ZhiEuf87xNszmSA2myDM2Kzu9HwQUA= github.com/spf13/afero v1.8.2/go.mod h1:CtAatgMJh6bJEIs48Ay/FOnkljP3WeGUG0MC1RfAqwo= @@ -386,16 +569,22 @@ github.com/spf13/afero v1.9.2 h1:j49Hj62F0n+DaZ1dDCvhABaPNSGNkt32oRFxI33IEMw= github.com/spf13/afero v1.9.2/go.mod h1:iUV7ddyEEZPO5gA3zD4fJt6iStLlL+Lg4m2cihcDf8Y= github.com/spf13/cast v1.5.0 h1:rj3WzYc11XZaIZMPKmwP96zkFEnnAmV8s6XbB2aY32w= github.com/spf13/cast v1.5.0/go.mod h1:SpXXQ5YoyJw6s3/6cMTQuxvgRl3PCJiyaX9p6b155UU= +github.com/spf13/cobra v0.0.3/go.mod h1:1l0Ry5zgKvJasoi3XT1TypsSe7PqH0Sj9dhYf7v3XqQ= github.com/spf13/cobra v1.5.0 h1:X+jTBEBqF0bHN+9cSMgmfuvv2VHJ9ezmFNf9Y/XstYU= github.com/spf13/cobra v1.5.0/go.mod h1:dWXEIy2H428czQCjInthrTRUg7yKbok+2Qi/yBIJoUM= github.com/spf13/jwalterweatherman v1.1.0 h1:ue6voC5bR5F8YxI5S67j9i582FU4Qvo2bmqnqMYADFk= github.com/spf13/jwalterweatherman v1.1.0/go.mod h1:aNWZUN0dPAAO/Ljvb5BEdw96iTZ0EXowPYD95IqWIGo= +github.com/spf13/pflag v1.0.1/go.mod h1:DYY7MBk1bdzusC3SYhjObp+wFpr4gzcvqqNjLnInEg4= 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.12.0 h1:CZ7eSOd3kZoaYDLbXnmzgQI5RlciuXBMA+18HwHRfZQ= github.com/spf13/viper v1.12.0/go.mod h1:b6COn30jlNxbm/V2IqWiNWkJ+vZNiMNksliPCiuKtSI= +github.com/streadway/amqp v0.0.0-20190404075320-75d898a42a94/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/amqp v0.0.0-20190827072141-edfb9018d271/go.mod h1:AZpEONHx3DKn8O/DFsRAY58/XVQiIPMTMB1SddzLXVw= +github.com/streadway/handy v0.0.0-20190108123426-d5acb3125c2a/go.mod h1:qNTQ5P5JnDBl6z3cMAg/SywNDC5ABu5ApDIw6lUbRmI= 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/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= 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/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= @@ -403,11 +592,13 @@ github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5 github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.2 h1:4jaiDzPyXQvSd7D0EjG45355tLlV3VOECpq10pLC+8s= github.com/stretchr/testify v1.7.2/go.mod h1:R6va5+xMeoiuVRoj+gSkQ7d3FALtqAAGI1FQKckRals= +github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PKk= +github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/subosito/gotenv v1.3.0/go.mod h1:YzJjq/33h7nrwdY+iHMhEOEEbW0ovIz0tB6t6PwAXzs= github.com/subosito/gotenv v1.4.0 h1:yAzM1+SmVcz5R4tXGsNMu1jUl2aOJXoiWUCEwwnGrvs= github.com/subosito/gotenv v1.4.0/go.mod h1:mZd6rFysKEcUhUHXJk0C/08wAgyDBFuwEYL7vWWGaGo= +github.com/tmc/grpc-websocket-proxy v0.0.0-20170815181823-89b8d40f7ca8/go.mod h1:ncp9v5uamzpCO7NfCPTXjqaC+bZgJeR0sMTm6dMHP7U= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2 h1:5u+EJUQiosu3JFX0XS0qTf5FznsMOzTjGqavBGuCbo0= github.com/ttacon/builder v0.0.0-20170518171403-c099f663e1c2/go.mod h1:4kyMkleCiLkgY6z8gK5BkI01ChBtxR0ro3I1ZDcGM3w= github.com/ttacon/libphonenumber v1.2.1 h1:fzOfY5zUADkCkbIafAed11gL1sW+bJ26p6zWLBMElR4= @@ -415,6 +606,13 @@ github.com/ttacon/libphonenumber v1.2.1/go.mod h1:E0TpmdVMq5dyVlQ7oenAkhsLu86OkU github.com/tv42/httpunix v0.0.0-20150427012821-b75d8614f926/go.mod h1:9ESjWnEqriFuLhtthL60Sar/7RFoluCcXsuvEwTV5KM= github.com/tyler-smith/go-bip39 v1.1.0 h1:5eUemwrMargf3BSLRRCalXT93Ns6pQJIjYQN2nyfOP8= github.com/tyler-smith/go-bip39 v1.1.0/go.mod h1:gUYDtqQw1JS3ZJ8UWVcGTGqqr6YIN3CWg+kkNaLt55U= +github.com/ugorji/go v1.1.7 h1:/68gy2h+1mWMrwZFeD1kQialdSzAb432dtpeJ42ovdo= +github.com/ugorji/go v1.1.7/go.mod h1:kZn38zHttfInRq0xu/PH0az30d+z6vm202qpg1oXVMw= +github.com/ugorji/go/codec v1.1.7 h1:2SvQaVZ1ouYrrKKwoSk2pzd4A9evlKJb9oTL+OaLUSs= +github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLYF3GoBXY= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= +github.com/urfave/cli v1.22.1/go.mod h1:Gos4lmkARVdJ6EkW0WaNv/tZAAMe9V7XWyB60NtXRu0= +github.com/xiang90/probing v0.0.0-20190116061207-43a291ad63a2/go.mod h1:UETIi67q53MR2AWcXfiuqkDkRtnGDLqkBTpCHuJHxtU= github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= @@ -433,47 +631,59 @@ github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo= github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4= gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f h1:yXGvNBqzZwAhDYlSnxPRbgor6JWoOt1Z7s3z1O9JR40= gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f/go.mod h1:H6jztdm0k+wEV2QGK/KYA+MY9nj9Zzatux/qIvDDv3k= -gitlab.com/elixxir/comms v0.0.4-0.20220916183942-257688b39f0c h1:N++gLUKVOfwgk/YnVIkIXpXznkpGb34nAqIcePkXoL8= -gitlab.com/elixxir/comms v0.0.4-0.20220916183942-257688b39f0c/go.mod h1:Yy5EAFq/ua0rZ2g1c3F86c3ry8bnsCACSVjRmwdGuIM= +gitlab.com/elixxir/comms v0.0.4-0.20221017173926-4eaa6061dfaa h1:/FEpu0N0rAyq74FkvO3uY8BcQoWLSbVPhj/s5QfscZw= +gitlab.com/elixxir/comms v0.0.4-0.20221017173926-4eaa6061dfaa/go.mod h1:rW7xdbHntP2MoF3q+2+f+IR8OHol94MRyviotfR5rXg= +gitlab.com/elixxir/comms v0.0.4-0.20221021234520-a4f94f752e3e h1:Go3Ec+LOm8t6j8wVgI4GqTfuy+PQkyblZCYQU7yGB2E= +gitlab.com/elixxir/comms v0.0.4-0.20221021234520-a4f94f752e3e/go.mod h1:rW7xdbHntP2MoF3q+2+f+IR8OHol94MRyviotfR5rXg= +gitlab.com/elixxir/comms v0.0.4-0.20221023173239-c75420d94293 h1:QIiZYjdtwjBCYaO7dZI6K8cEaA73wtd/YKIWD1oeDWw= +gitlab.com/elixxir/comms v0.0.4-0.20221023173239-c75420d94293/go.mod h1:rW7xdbHntP2MoF3q+2+f+IR8OHol94MRyviotfR5rXg= +gitlab.com/elixxir/comms v0.0.4-0.20221023190124-3441c3fdc3de h1:1YnKkJn3a7xiftFBRqLK5os7C6uF4okMtDXZyLrzIuY= +gitlab.com/elixxir/comms v0.0.4-0.20221023190124-3441c3fdc3de/go.mod h1:rW7xdbHntP2MoF3q+2+f+IR8OHol94MRyviotfR5rXg= +gitlab.com/elixxir/comms v0.0.4-0.20221024012811-e6754f7740db h1:LQUde8pjIfQpVdg7trANu6o5uzZv9ADK13S8bkMgkBw= +gitlab.com/elixxir/comms v0.0.4-0.20221024012811-e6754f7740db/go.mod h1:NevrBdsi5wJvitUeMsid3xI1FrzzuzfxKy4Bapnhzao= +gitlab.com/elixxir/comms v0.0.4-0.20221024050701-bced94c1b026 h1:CdqvzyM91wN6u4MmGj0n+gKO/0tJabWPN3EQ4SFsZsg= +gitlab.com/elixxir/comms v0.0.4-0.20221024050701-bced94c1b026/go.mod h1:NevrBdsi5wJvitUeMsid3xI1FrzzuzfxKy4Bapnhzao= +gitlab.com/elixxir/comms v0.0.4-0.20221024232930-61a6369c1f68 h1:DKsCIo15pFed5QGVIw/sJ+5bwoR4Tyaq//hBiJUOWfs= +gitlab.com/elixxir/comms v0.0.4-0.20221024232930-61a6369c1f68/go.mod h1:NevrBdsi5wJvitUeMsid3xI1FrzzuzfxKy4Bapnhzao= gitlab.com/elixxir/crypto v0.0.0-20200804182833-984246dea2c4/go.mod h1:ucm9SFKJo+K0N2GwRRpaNr+tKXMIOVWzmyUD0SbOu2c= gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp07WQmhA= -gitlab.com/elixxir/crypto v0.0.7-0.20220606201132-c370d5039cea/go.mod h1:Oy+VWQ2Sa0Ybata3oTV+Yc46hkaDwAsuIMW0wJ01z2M= -gitlab.com/elixxir/crypto v0.0.7-0.20220901215826-1ceaeb59081f h1:G5I01Ob+ff0YjGyluQ2h3+I97qWtOyNRrJGIJkJWaW8= -gitlab.com/elixxir/crypto v0.0.7-0.20220901215826-1ceaeb59081f/go.mod h1:IYRYQwpS3zhTVtrir+kGGTqRCMI3It/i6mGW/mxezrM= +gitlab.com/elixxir/crypto v0.0.7-0.20221017173452-565da4101a3b/go.mod h1:1rftbwSVdy49LkBIkPr+w+P2mDOerYeBKoZuB3r0yqI= +gitlab.com/elixxir/crypto v0.0.7-0.20221022003355-d8a6158b32a7 h1:+8DHBxZxJcmJSmcUFK4ZjjXgwV3wSo9O4+4NCaLdO4c= +gitlab.com/elixxir/crypto v0.0.7-0.20221022003355-d8a6158b32a7/go.mod h1:P/S3pEPYl7fuHQ1m4mL2pIaCxAjYIXrJml/pnfofI+U= +gitlab.com/elixxir/crypto v0.0.7-0.20221024012326-cf941c375c1f h1:ku5gWZnvgs8TPHfGIOfKO5QPnRJl5fsSAic5H1Y/QRg= +gitlab.com/elixxir/crypto v0.0.7-0.20221024012326-cf941c375c1f/go.mod h1:P/S3pEPYl7fuHQ1m4mL2pIaCxAjYIXrJml/pnfofI+U= +gitlab.com/elixxir/crypto v0.0.7-0.20221024215625-33315c3de43e h1:CIb5XdBTSUf9U/MDWv9DrjRicSYPDLudETMUkxzNVjQ= +gitlab.com/elixxir/crypto v0.0.7-0.20221024215625-33315c3de43e/go.mod h1:P/S3pEPYl7fuHQ1m4mL2pIaCxAjYIXrJml/pnfofI+U= gitlab.com/elixxir/ekv v0.2.1 h1:dtwbt6KmAXG2Tik5d60iDz2fLhoFBgWwST03p7T+9Is= gitlab.com/elixxir/ekv v0.2.1/go.mod h1:USLD7xeDnuZEavygdrgzNEwZXeLQJK/w1a+htpN+JEU= gitlab.com/elixxir/primitives v0.0.0-20200731184040-494269b53b4d/go.mod h1:OQgUZq7SjnE0b+8+iIAT2eqQF+2IFHn73tOo+aV11mg= gitlab.com/elixxir/primitives v0.0.0-20200804170709-a1896d262cd9/go.mod h1:p0VelQda72OzoUckr1O+vPW0AiFe0nyKQ6gYcmFSuF8= gitlab.com/elixxir/primitives v0.0.0-20200804182913-788f47bded40/go.mod h1:tzdFFvb1ESmuTCOl1z6+yf6oAICDxH2NPUemVgoNLxc= gitlab.com/elixxir/primitives v0.0.1/go.mod h1:kNp47yPqja2lHSiS4DddTvFpB/4D9dB2YKnw5c+LJCE= -gitlab.com/elixxir/primitives v0.0.3-0.20220606195757-40f7a589347f/go.mod h1:9Bb2+u+CDSwsEU5Droo6saDAXuBDvLRjexpBhPAYxhA= -gitlab.com/elixxir/primitives v0.0.3-0.20220810173935-592f34a88326/go.mod h1:9Bb2+u+CDSwsEU5Droo6saDAXuBDvLRjexpBhPAYxhA= -gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6 h1:/cxxZBP5jTPDpC3zgOx9vV1ojmJyG8pYtkl3IbcewNQ= -gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6/go.mod h1:9Bb2+u+CDSwsEU5Droo6saDAXuBDvLRjexpBhPAYxhA= +gitlab.com/elixxir/primitives v0.0.3-0.20221017172918-6176818d1aba h1:skhrlmHdEQfuT/4Ip2IVcI4ToNzPlmAxTYlTk034Il0= +gitlab.com/elixxir/primitives v0.0.3-0.20221017172918-6176818d1aba/go.mod h1:Rqbl8ToZxPn86RyqrpjIE9FJbFHoJgQfg68iSOTPcz8= gitlab.com/xx_network/comms v0.0.0-20200805174823-841427dd5023/go.mod h1:owEcxTRl7gsoM8c3RQ5KAm5GstxrJp5tn+6JfQ4z5Hw= -gitlab.com/xx_network/comms v0.0.4-0.20220916183430-cc3077fedf9f h1:oomZumvJSgKvzOX7XGIkixHitxqUCCeB6YCNQxju6Dc= -gitlab.com/xx_network/comms v0.0.4-0.20220916183430-cc3077fedf9f/go.mod h1:TraR4sW+YxK/2CV+IQUNaAV61+ElrBV0kXd5HLEjM7M= +gitlab.com/xx_network/comms v0.0.4-0.20221017172508-09e33697dc15 h1:H2OptkCpcGAgplz5PYMqUiA4aH/LUcjc9nLbGM+vUus= +gitlab.com/xx_network/comms v0.0.4-0.20221017172508-09e33697dc15/go.mod h1:yBDaXAWiPGGv2zXQgP35Ks/ek800Uaah4roVFyvMkWU= gitlab.com/xx_network/crypto v0.0.3/go.mod h1:DF2HYvvCw9wkBybXcXAgQMzX+MiGbFPjwt3t17VRqRE= gitlab.com/xx_network/crypto v0.0.4/go.mod h1:+lcQEy+Th4eswFgQDwT0EXKp4AXrlubxalwQFH5O0Mk= -gitlab.com/xx_network/crypto v0.0.5-0.20220606200528-3f886fe49e81/go.mod h1:/SJf+R75E+QepdTLh0H1/udsovxx2Q5ru34q1v0umKk= -gitlab.com/xx_network/crypto v0.0.5-0.20220729193517-1e5e96f39f6e/go.mod h1:/SJf+R75E+QepdTLh0H1/udsovxx2Q5ru34q1v0umKk= -gitlab.com/xx_network/crypto v0.0.5-0.20220902182733-69aad094b487 h1:CsnSSVw2o4PXFKo4OFbivjoLA0RE/Ktx9hxvtQRGNjw= -gitlab.com/xx_network/crypto v0.0.5-0.20220902182733-69aad094b487/go.mod h1:/SJf+R75E+QepdTLh0H1/udsovxx2Q5ru34q1v0umKk= +gitlab.com/xx_network/crypto v0.0.5-0.20221017172404-b384a8d8b171 h1:zGrUU8U6pMl5J4yzKzJ3QOAoo6g0Nna7Z+vVxxijpLg= +gitlab.com/xx_network/crypto v0.0.5-0.20221017172404-b384a8d8b171/go.mod h1:sZ5bMKbb6v+Qx3zwXsWsbhVikOte/1ibflLpbCVuboQ= gitlab.com/xx_network/primitives v0.0.0-20200803231956-9b192c57ea7c/go.mod h1:wtdCMr7DPePz9qwctNoAUzZtbOSHSedcK++3Df3psjA= gitlab.com/xx_network/primitives v0.0.0-20200804183002-f99f7a7284da/go.mod h1:OK9xevzWCaPO7b1wiluVJGk7R5ZsuC7pHY5hteZFQug= gitlab.com/xx_network/primitives v0.0.2/go.mod h1:cs0QlFpdMDI6lAo61lDRH2JZz+3aVkHy+QogOB6F/qc= -gitlab.com/xx_network/primitives v0.0.4-0.20220222211843-901fa4a2d72b/go.mod h1:9imZHvYwNFobxueSvVtHneZLk9wTK7HQTzxPm+zhFhE= -gitlab.com/xx_network/primitives v0.0.4-0.20220324193139-b292d1ae6e7e/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo= -gitlab.com/xx_network/primitives v0.0.4-0.20220630163313-7890038258c6/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo= -gitlab.com/xx_network/primitives v0.0.4-0.20220712193914-aebd8544396e/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo= -gitlab.com/xx_network/primitives v0.0.4-0.20220902183448-319596e2fec8 h1:esjWkrzTD+bpTgT5NphhdpX/fbSpZRXLpPaXb4HqkXg= -gitlab.com/xx_network/primitives v0.0.4-0.20220902183448-319596e2fec8/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo= +gitlab.com/xx_network/primitives v0.0.4-0.20221017171439-42169a3e5c0d h1:rOcGGx5SrhBkbD1P8JqbtqBBeuGhOSZxETytNGJcf/U= +gitlab.com/xx_network/primitives v0.0.4-0.20221017171439-42169a3e5c0d/go.mod h1:AXVVFt7dDAeIUpOGPiStCcUIKsBXLWbmV/BgZ4T+tOo= gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 h1:eJZrXqHsMmmejEPWw8gNAt0I8CGAMNO/7C339Zco3TM= gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93/go.mod h1:aLzpP2TiZTQut/PVHR40EJAomzugDdHXetbieRClXIM= +go.etcd.io/bbolt v1.3.3/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= +go.etcd.io/etcd v0.0.0-20191023171146-3cf2f69b5738/go.mod h1:dnLIgRNXwCJa5e+c6mIZCrds/GIG4ncV9HhK5PX7jPg= go.etcd.io/etcd/api/v3 v3.5.4/go.mod h1:5GB2vv4A4AOn3yk7MftYGHkUfGtDHnEraIjym4dYz5A= go.etcd.io/etcd/client/pkg/v3 v3.5.4/go.mod h1:IJHfcCEKxYu1Os13ZdwCwIUTUVGYTSAM3YSwc9/Ac1g= go.etcd.io/etcd/client/v2 v2.305.4/go.mod h1:Ud+VUwIi9/uQHOMA+4ekToJ12lTxlv0zB/+DHwTGEbU= go.etcd.io/etcd/client/v3 v3.5.4/go.mod h1:ZaRkVgBZC+L+dLCjTcF1hRXpgZXQPOvnA/Ak/gq3kiY= +go.opencensus.io v0.20.1/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= +go.opencensus.io v0.20.2/go.mod h1:6WKK9ahsWS3RSO+PY9ZHZUfv2irvY6gN279GOPZjmmk= go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= @@ -482,16 +692,30 @@ go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= go.opencensus.io v0.23.0/go.mod h1:XItmlyltB5F7CS4xOC1DcqMoFqwtC6OG2xF7mCv7P7E= go.opentelemetry.io/proto/otlp v0.7.0/go.mod h1:PqfVotwruBrMGOCsRd/89rSnXhoiJIqeYNgFYFoEGnI= -go.uber.org/atomic v1.7.0 h1:ADUqmZGgLDDfbSL9ZmPxKTybcoEYHgpYfELNoN+7hsw= +go.uber.org/atomic v1.3.2/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.4.0/go.mod h1:gD2HeocX3+yG+ygLZcrzQJaqmWj9AIm7n08wl/qW/PE= +go.uber.org/atomic v1.5.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= +go.uber.org/atomic v1.6.0/go.mod h1:sABNBOSYdrvTF6hTgEIbc7YasKWGhgEQZyfxyTvoXHQ= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= +go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= +go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= +go.uber.org/goleak v0.10.0/go.mod h1:VCZuO8V8mFPlL0F5J5GK1rtHV3DrFcQ1R8ryq7FK0aI= +go.uber.org/goleak v1.1.10/go.mod h1:8a7PlsEVH3e/a/GLqe5IIrQx6GzcnRmZEufDUTk4A7A= +go.uber.org/multierr v1.1.0/go.mod h1:wR5kodmAFQ0UK8QlbwjlSNy0Z68gJhDJUG5sjR94q/0= +go.uber.org/multierr v1.3.0/go.mod h1:VgVr7evmIr6uPjLBxg28wmKNXyqE9akIJ5XnfpiKl+4= go.uber.org/multierr v1.6.0/go.mod h1:cdWPpRnG4AhwMwsgIHip0KRBQjJy5kYEpYjJxpXp9iU= go.uber.org/ratelimit v0.2.0 h1:UQE2Bgi7p2B85uP5dC2bbRtig0C+OeNRnNEafLjsLPA= go.uber.org/ratelimit v0.2.0/go.mod h1:YYBV4e4naJvhpitQrWJu1vCpgB7CboMe0qhltKt6mUg= +go.uber.org/tools v0.0.0-20190618225709-2cfd321de3ee/go.mod h1:vJERXedbb3MVM5f9Ejo0C68/HhF8uaILCdgjnY+goOA= +go.uber.org/zap v1.10.0/go.mod h1:vwi/ZaCAaUcBkycHslxD9B2zi4UTXhF60s6SWpuDF0Q= +go.uber.org/zap v1.13.0/go.mod h1:zwrFLgMcdUuIBviXEYEH1YKNaOBnKXsx2IPda5bBwHM= go.uber.org/zap v1.17.0/go.mod h1:MXVU+bhUf/A7Xi2HNOnopQOrmycQ5Ih87HtOu4q5SSo= golang.org/x/crypto v0.0.0-20180904163835-0709b304e793/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= +golang.org/x/crypto v0.0.0-20181029021203-45a5f77698d3/go.mod h1:6SG95UA2DQfeDnfUPMdvaQW0Q7yPrPDi9nlGo2tz2b4= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= +golang.org/x/crypto v0.0.0-20190701094942-4def268fd1a4/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3H3cr1v9wB50oz8l4C4h62xy7jSTY= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200510223506-06a226fb4e37/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= @@ -502,7 +726,6 @@ golang.org/x/crypto v0.0.0-20201221181555-eec23a3978ad/go.mod h1:jdWPYTVW3xRLrWP golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20211108221036-ceb1ce70b4fa/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220128200615-198e4374d7ed/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220315160706-3147a52a75dd/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220411220226-7b82a4e95df4/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= golang.org/x/crypto v0.0.0-20220518034528-6f7dac969898/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= @@ -519,6 +742,7 @@ golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u0 golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= +golang.org/x/exp v0.0.0-20200331195152-e8c3332aa8e5/go.mod h1:4M0jN8W1tt0AVLNr8HDosyJCDCDuyL9N9+3m7wDWgKw= golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= @@ -546,8 +770,13 @@ golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181023162649-9b4f9f5ad519/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181201002055-351d144fa1fc/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= +golang.org/x/net v0.0.0-20190125091013-d26f9f9a57f3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= @@ -558,6 +787,7 @@ golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= +golang.org/x/net v0.0.0-20190813141303-74dc4d7220e7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190923162816-aa69164e4478/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= @@ -566,6 +796,7 @@ golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLL golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 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/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= +golang.org/x/net v0.0.0-20200421231249-e086a090c8fd/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= @@ -576,6 +807,7 @@ golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81R golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= +golang.org/x/net v0.0.0-20201202161906-c7110b5ffcbb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210119194325-5f4716e94777/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= @@ -584,7 +816,7 @@ golang.org/x/net v0.0.0-20210316092652-d523dce5a7f4/go.mod h1:RBQZq4jEuRlivfhVLd golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210503060351-7fd8e65b6420/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= +golang.org/x/net v0.0.0-20210805182204-aaa1db679c0d/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220225172249-27dd8689420f/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= @@ -592,8 +824,8 @@ golang.org/x/net v0.0.0-20220325170049-de3da57026de/go.mod h1:CfG3xpIq0wQ8r1q4Su golang.org/x/net v0.0.0-20220412020605-290c469a71a5/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220425223048-2871e0cb64e4/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= golang.org/x/net v0.0.0-20220520000938-2e3eb7b945c2/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk= -golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b h1:3ogNYyK4oIQdIKzTu68hQrr4iuVxF3AxKl9Aj/eDrw0= -golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= +golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c h1:JVAXQ10yGGVbSyoer5VILysz6YKjdNT2bsvlayjqhes= +golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c/go.mod h1:YDH+HFinaLZZlnHAfSS6ZXJJ9M9t4Dl22yv3iI2vPwk= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= @@ -629,7 +861,11 @@ golang.org/x/sync v0.0.0-20220513210516-0976fa681c29/go.mod h1:RxMgew5VJxzue5/jJ golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181026203630-95b1ffbd15a5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181107165924-66b7b1311ac8/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20181116152217-5ac8a444bdc5/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20181122145206-62eef0e2fa9b/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-20190222072716-a9d3bda3a223/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -640,6 +876,7 @@ golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190922100055-0a153f010e69/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190924154521-2837fb4f24fe/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -647,6 +884,7 @@ golang.org/x/sys v0.0.0-20191005200804-aed5e4c7ecf9/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20191220142924-d4481acd189f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200106162015-b016eb3dc98e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -659,6 +897,7 @@ golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7w golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/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/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= @@ -694,6 +933,7 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc golang.org/x/sys v0.0.0-20210616094352-59db8d763f22/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210823070655-63515b42dcdf/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210908233432-aa78b53d3365/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -726,15 +966,19 @@ golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.7 h1:olpwvP2KacW1ZWvsR7uQhoyTYvKAupfQrRGBFM352Gk= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= +golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= +golang.org/x/tools v0.0.0-20180221164845-07fd8470d635/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-20180828015842-6cd1fcedba52/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= +golang.org/x/tools v0.0.0-20190328211700-ab21143f2384/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= @@ -745,6 +989,9 @@ golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20190907020128-2ca718005c18/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029041327-9cc4af7d6b2c/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191029190741-b9c20aec41a5/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +golang.org/x/tools v0.0.0-20191108193012-7d206e10da11/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= @@ -752,6 +999,7 @@ golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtn golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= +golang.org/x/tools v0.0.0-20200103221440-774c71fcf114/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= @@ -790,6 +1038,7 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220411194840-2f41105eb62f/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20220517211312-f3a8303e98df/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= +google.golang.org/api v0.3.1/go.mod h1:6wY9I6uQWHQ8EM57III9mq/AjF+i8G65rmVagqKMtkk= google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= @@ -830,6 +1079,7 @@ google.golang.org/api v0.75.0/go.mod h1:pU9QmyHLnzlpar1Mjt4IbapUCy8J+6HD6GeELN69 google.golang.org/api v0.78.0/go.mod h1:1Sg78yoMLOhlQTeF+ARBoytAcH1NNyyl390YMy6rKmw= google.golang.org/api v0.81.0/go.mod h1:FA6Mb/bZxj706H2j+j2d6mHEEaHBmbbWnkfvmorOCko= google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= +google.golang.org/appengine v1.2.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= @@ -841,6 +1091,7 @@ google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRn google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= +google.golang.org/genproto v0.0.0-20190530194941-fb225487d101/go.mod h1:z3L6/3dTEVtUr6QSP8miRzeRqwQOioJ9I66odjN4I7s= google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= @@ -851,12 +1102,14 @@ google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvx google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= +google.golang.org/genproto v0.0.0-20200204235621-fb4a7afc5178/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= +google.golang.org/genproto v0.0.0-20200423170343-7949de9c1215/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= google.golang.org/genproto v0.0.0-20200513103714-09dca8ec2884/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= @@ -872,6 +1125,7 @@ google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6D google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= +google.golang.org/genproto v0.0.0-20210126160654-44e461bb6506/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210222152913-aa3ee6e6a81c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= google.golang.org/genproto v0.0.0-20210303154014-9728d6b83eeb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= @@ -914,12 +1168,17 @@ google.golang.org/genproto v0.0.0-20220421151946-72621c1f0bd3/go.mod h1:8w6bsBMX google.golang.org/genproto v0.0.0-20220429170224-98d788798c3e/go.mod h1:8w6bsBMX6yCPbAVTeqQHvzxW0EIFigd5lZyahWgyfDo= google.golang.org/genproto v0.0.0-20220505152158-f39f71e6c8f3/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= google.golang.org/genproto v0.0.0-20220519153652-3a47de7e79bd/go.mod h1:RAyBrSAP7Fh3Nc84ghnVLDPuV51xc9agzmm4Ph6i0Q4= -google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 h1:QntLWYqZeuBtJkth3m/6DLznnI0AHJr+AgJXvVh/izw= -google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78/go.mod h1:iHe1svFLAZg9VWz891+QbRMwUv9O/1Ww+/mngYeThbc= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc h1:Nf+EdcTLHR8qDNN/KfkQL0u0ssxt9OhbaWCl5C0ucEI= +google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc/go.mod h1:dbqgFATTzChvnt+ujMdZwITVAJHFtfyN1qUhDqEiIlk= +google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= +google.golang.org/grpc v1.20.0/go.mod h1:chYK+tFQF0nDUGJgXMSgLCQk3phJEuONr2DCgLDdAQM= google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= +google.golang.org/grpc v1.21.0/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= +google.golang.org/grpc v1.22.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= +google.golang.org/grpc v1.23.1/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= @@ -929,6 +1188,7 @@ google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3Iji google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= +google.golang.org/grpc v1.32.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= google.golang.org/grpc v1.33.1/go.mod h1:fr5YgcSWrqhRRxogOsw7RzIpsmvOZ6IcH4kBYTpR3n0= google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= @@ -946,8 +1206,9 @@ google.golang.org/grpc v1.44.0/go.mod h1:k+4IHHFw41K8+bbowsex27ge2rCb65oeWqe4jJ5 google.golang.org/grpc v1.45.0/go.mod h1:lN7owxKUQEqMfSyQikvvk5tf/6zMPsrK+ONuO11+0rQ= google.golang.org/grpc v1.46.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= google.golang.org/grpc v1.46.2/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= -google.golang.org/grpc v1.48.0 h1:rQOsyJ/8+ufEDJd/Gdsz7HG220Mh9HAhFHRGnIjda0w= google.golang.org/grpc v1.48.0/go.mod h1:vN9eftEi1UMyUsIF80+uQXhHjbXYbm0uXoFCACuMGWk= +google.golang.org/grpc v1.49.0 h1:WTLtQzmQori5FUH25Pq4WT22oCsv8USpQ+F6rqtsmxw= +google.golang.org/grpc v1.49.0/go.mod h1:ZgQEeidpAuNRZ8iRrlBKXZQP1ghovWIVhdJRyCDK+GI= google.golang.org/grpc/cmd/protoc-gen-go-grpc v1.1.0/go.mod h1:6Kw0yEErY5E/yWrBtf03jp27GLLJujG4z/JK95pnjjw= 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= @@ -972,10 +1233,17 @@ gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8 gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/cheggaaa/pb.v1 v1.0.25/go.mod h1:V/YB90LKu/1FcN3WVnfiiE5oMCibMjukxqG/qStrOgw= gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= +gopkg.in/fsnotify.v1 v1.4.7/go.mod h1:Tz8NjZHkW78fSQdbUxIjBTcgA1z1m8ZHf0WmKUhAMys= +gopkg.in/gcfg.v1 v1.2.3/go.mod h1:yesOnuUOFQAhST5vPY4nbZsb/huCgGGXlipJsBn0b3o= gopkg.in/ini.v1 v1.66.4/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.6 h1:LATuAqN/shcYAOkv3wl2L4rkaKqkcgTBQjOyYDvcPKI= gopkg.in/ini.v1 v1.66.6/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/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= +gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= +gopkg.in/yaml.v2 v2.0.0-20170812160011-eb3733d160e7/go.mod h1:JAlM8MvJe8wmxCU4Bli9HhUf9+ttbYbLASfIpnQbh74= gopkg.in/yaml.v2 v2.2.1/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.3/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= @@ -991,6 +1259,7 @@ gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C gopkg.in/yaml.v3 v3.0.0/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +honnef.co/go/tools v0.0.0-20180728063816-88497007e858/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= @@ -999,7 +1268,12 @@ honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= lukechampine.com/blake3 v1.1.6/go.mod h1:tkKEOtDkNtklkXtLNEOGNq5tcV90tJiA1vAA12R78LA= +nhooyr.io/websocket v1.8.6/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= +nhooyr.io/websocket v1.8.7 h1:usjR2uOr/zjjkVMy0lW+PPohFok7PCow5sDjLgX4P4g= +nhooyr.io/websocket v1.8.7/go.mod h1:B70DZP8IakI65RVQ51MsWP/8jndNma26DVA/nFSCgW0= rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= +sigs.k8s.io/yaml v1.1.0/go.mod h1:UJmg0vDUVViEyp3mgSv9WPwZCDxu4rQW1olrI1uml+o= sigs.k8s.io/yaml v1.2.0/go.mod h1:yfXDCHCao9+ENCvLSE62v9VSji2MKu5jeNfTrofGhJc= +sourcegraph.com/sourcegraph/appdash v0.0.0-20190731080439-ebfcffb1b5c0/go.mod h1:hI742Nqp5OhwiqlzhgfbWU4mW4yO10fP+LoT9WOswdU= diff --git a/groupChat/interface.go b/groupChat/interface.go index 983d709d87db528c144d5583f1aa18419edd8b21..e9b14c911abe56c1072e571d552dca6047d35cbd 100644 --- a/groupChat/interface.go +++ b/groupChat/interface.go @@ -24,6 +24,7 @@ import ( "gitlab.com/elixxir/client/catalog" "gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/cmix/message" + "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/client/e2e" "gitlab.com/elixxir/client/e2e/ratchet/partner" sessionImport "gitlab.com/elixxir/client/e2e/ratchet/partner/session" @@ -68,7 +69,7 @@ type GroupChat interface { // The send fails if the message is too long. Returns the ID of the round // sent on and the timestamp of the message send. Send(groupID *id.ID, tag string, message []byte) ( - id.Round, time.Time, group.MessageID, error) + rounds.Round, time.Time, group.MessageID, error) // GetGroups returns a list of all registered GroupChat IDs. GetGroups() []*id.ID @@ -114,7 +115,7 @@ type groupE2e interface { // methods needed by GroupChat type groupCmix interface { SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) ( - id.Round, []ephemeral.Id, error) + rounds.Round, []ephemeral.Id, error) AddService( clientID *id.ID, newService message.Service, response message.Processor) DeleteService( diff --git a/groupChat/networkManager_test.go b/groupChat/networkManager_test.go index 9c6535d1c73f7114c50012557f993b405f1a261f..e87b3f655bca1e2d5ccbf3d6547a25c1b1ac471d 100644 --- a/groupChat/networkManager_test.go +++ b/groupChat/networkManager_test.go @@ -44,9 +44,9 @@ func newTestNetworkManager(sendErr int) cmix.Client { } } -func (tnm *testNetworkManager) SendMany(messages []cmix.TargetedCmixMessage, _ cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { +func (tnm *testNetworkManager) SendMany(messages []cmix.TargetedCmixMessage, _ cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { if tnm.sendErr == 1 { - return 0, nil, errors.New("SendManyCMIX error") + return rounds.Round{}, nil, errors.New("SendManyCMIX error") } tnm.Lock() @@ -63,7 +63,7 @@ func (tnm *testNetworkManager) SendMany(messages []cmix.TargetedCmixMessage, _ c receiveMessages = append(receiveMessages, receiveMsg) } tnm.receptionMessages = append(tnm.receptionMessages, receiveMessages) - return 0, nil, nil + return rounds.Round{}, nil, nil } func (*testNetworkManager) AddService(*id.ID, message.Service, message.Processor) {} @@ -78,7 +78,14 @@ func (tnm *testNetworkManager) Follow(report cmix.ClientErrorReport) (stoppable. panic("implement me") } -func (tnm *testNetworkManager) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (id.Round, ephemeral.Id, error) { +func (tnm *testNetworkManager) SendWithAssembler(recipient *id.ID, + assembler cmix.MessageAssembler, cmixParams cmix.CMIXParams) (rounds.Round, + ephemeral.Id, error) { + //TODO implement me + panic("implement me") +} + +func (tnm *testNetworkManager) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { //TODO implement me panic("implement me") } @@ -163,7 +170,7 @@ func (tnm *testNetworkManager) TriggerNodeRegistration(nid *id.ID) { panic("implement me") } -func (tnm *testNetworkManager) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) error { +func (tnm *testNetworkManager) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) { //TODO implement me panic("implement me") } diff --git a/groupChat/send.go b/groupChat/send.go index b75f6e209fde626c64f25686928ebbf0c8f01418..9486885e4feeefc17d7d618bfe768927079642c6 100644 --- a/groupChat/send.go +++ b/groupChat/send.go @@ -12,6 +12,7 @@ import ( jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/cmix" "gitlab.com/elixxir/client/cmix/message" + "gitlab.com/elixxir/client/cmix/rounds" gs "gitlab.com/elixxir/client/groupChat/groupStore" "gitlab.com/elixxir/crypto/group" "gitlab.com/xx_network/primitives/id" @@ -44,7 +45,7 @@ const ( // Send sends a message to all group members using Cmix.SendMany. // The send fails if the message is too long. func (m *manager) Send(groupID *id.ID, tag string, message []byte) ( - id.Round, time.Time, group.MessageID, error) { + rounds.Round, time.Time, group.MessageID, error) { if tag == "" { tag = defaultServiceTag @@ -53,7 +54,7 @@ func (m *manager) Send(groupID *id.ID, tag string, message []byte) ( // Get the relevant group g, exists := m.GetGroup(groupID) if !exists { - return 0, time.Time{}, group.MessageID{}, + return rounds.Round{}, time.Time{}, group.MessageID{}, errors.Errorf(newNoGroupErr, groupID) } @@ -63,7 +64,7 @@ func (m *manager) Send(groupID *id.ID, tag string, message []byte) ( // Create a cMix message for each group member groupMessages, msgId, err := m.newMessages(g, tag, message, timeNow) if err != nil { - return 0, time.Time{}, group.MessageID{}, + return rounds.Round{}, time.Time{}, group.MessageID{}, errors.Errorf(newCmixMsgErr, g.Name, g.ID, err) } @@ -72,7 +73,7 @@ func (m *manager) Send(groupID *id.ID, tag string, message []byte) ( param.DebugTag = "group.Message" rid, _, err := m.getCMix().SendMany(groupMessages, param) if err != nil { - return 0, time.Time{}, group.MessageID{}, + return rounds.Round{}, time.Time{}, group.MessageID{}, errors.Errorf(sendManyCmixErr, m.getReceptionIdentity().ID, g.Name, g.ID, err) } diff --git a/groupChat/send_test.go b/groupChat/send_test.go index 2b19472859973171c40409527c48412ddda4f306..c0d4f599f63e7e1ab853af0257c80cf5cb662a6e 100644 --- a/groupChat/send_test.go +++ b/groupChat/send_test.go @@ -56,7 +56,7 @@ func Test_manager_Send(t *testing.T) { reception.Process(msg, receptionID.EphemeralIdentity{ EphId: ephemeral.Id{1, 2, 3}, Source: &id.ID{4, 5, 6}, }, - rounds.Round{ID: roundId, Timestamps: timestamps}) + rounds.Round{ID: roundId.ID, Timestamps: timestamps}) select { case result := <-msgChan: if !result.SenderID.Cmp(m.getReceptionIdentity().ID) { diff --git a/groupChat/wrapper.go b/groupChat/wrapper.go index 4e9d73b3e46d4e94ea985ab572baf32fa57029eb..462811c8d20f606353c419f61bd079e2a8480875 100644 --- a/groupChat/wrapper.go +++ b/groupChat/wrapper.go @@ -8,6 +8,7 @@ package groupChat import ( + "gitlab.com/elixxir/client/cmix/rounds" gs "gitlab.com/elixxir/client/groupChat/groupStore" "gitlab.com/elixxir/crypto/group" "gitlab.com/xx_network/primitives/id" @@ -53,7 +54,7 @@ func (w *Wrapper) LeaveGroup(groupID *id.ID) error { // Send calls GroupChat.Send. func (w *Wrapper) Send(groupID *id.ID, message []byte, tag string) ( - id.Round, time.Time, group.MessageID, error) { + rounds.Round, time.Time, group.MessageID, error) { return w.gc.Send(groupID, tag, message) } diff --git a/single/interfaces.go b/single/interfaces.go index d8a28d9545d824a4dd3ac1f583ea454f45b2b896..8a6f965e8728b05b26a09c9f99fe8c248c19cc52 100644 --- a/single/interfaces.go +++ b/single/interfaces.go @@ -44,7 +44,7 @@ type RequestCmix interface { GetMaxMessageLength() int Send(recipient *id.ID, fingerprint format.Fingerprint, service cMixMsg.Service, payload, mac []byte, - cmixParams cmix.CMIXParams) (id.Round, ephemeral.Id, error) + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) GetInstance() *network.Instance } @@ -72,7 +72,7 @@ type Cmix interface { AddIdentity(id *id.ID, validUntil time.Time, persistent bool) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, payload, mac []byte, cmixParams cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) + rounds.Round, ephemeral.Id, error) AddService(clientID *id.ID, newService message.Service, response message.Processor) DeleteService(clientID *id.ID, toDelete message.Service, diff --git a/single/listener_test.go b/single/listener_test.go index c0c0ae24907b940278ba41c2e9928fb1116e2ea9..43006ae979f8a9801921566ebff95ac27ca73fec 100644 --- a/single/listener_test.go +++ b/single/listener_test.go @@ -255,7 +255,7 @@ func (m mockListenCmix) GetMaxMessageLength() int { func (m mockListenCmix) Send(recipient *id.ID, fingerprint format.Fingerprint, service cMixMsg.Service, payload, mac []byte, _ cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) { + rounds.Round, ephemeral.Id, error) { msg := format.NewMessage(m.numPrimeBytes) msg.SetContents(payload) msg.SetMac(mac) @@ -270,7 +270,7 @@ func (m mockListenCmix) Send(recipient *id.ID, fingerprint format.Fingerprint, p.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{}) } - return 0, ephemeral.Id{}, nil + return rounds.Round{}, ephemeral.Id{}, nil } func (m mockListenCmix) GetInstance() *network.Instance { diff --git a/single/receivedRequest.go b/single/receivedRequest.go index 70ff0dc6f1fe673f69f13d4bbd826aa55cfdcc8a..cc05610088cca70c26539b5df5a0a090616b67cc 100644 --- a/single/receivedRequest.go +++ b/single/receivedRequest.go @@ -104,11 +104,11 @@ func (r *Request) Respond(payload []byte, cMixParams cmix.CMIXParams, jww.DEBUG.Printf("[SU] Sent single-use response cMix message part "+ "%d of %d on round %d to %s (eph ID %d) (%s).", - i, len(parts), round, r.sender, ephID.Int64(), r.tag) - rounds[i] = round + i, len(parts), round.ID, r.sender, ephID.Int64(), r.tag) + rounds[i] = round.ID r.net.GetInstance().GetRoundEvents().AddRoundEventChan( - round, sendResults, timeout, states.COMPLETED, states.FAILED) + round.ID, sendResults, timeout, states.COMPLETED, states.FAILED) }(i, parts[i].Marshal()) } diff --git a/single/receivedRequest_test.go b/single/receivedRequest_test.go index d58b9a224fa147a34d96bbc0be4ca127dcdbc6f8..4d5762828038be1ba058cc7e59a4bc1dd8490fca 100644 --- a/single/receivedRequest_test.go +++ b/single/receivedRequest_test.go @@ -11,6 +11,7 @@ import ( "bytes" "gitlab.com/elixxir/client/cmix" cmixMsg "gitlab.com/elixxir/client/cmix/message" + "gitlab.com/elixxir/client/cmix/rounds" "gitlab.com/elixxir/client/single/message" "gitlab.com/elixxir/comms/network" "gitlab.com/elixxir/crypto/cyclic" @@ -186,14 +187,14 @@ func (m *mockRequestCmix) GetMaxMessageLength() int { func (m *mockRequestCmix) Send(_ *id.ID, fp format.Fingerprint, _ cmixMsg.Service, payload, mac []byte, _ cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) { + rounds.Round, ephemeral.Id, error) { msg := format.NewMessage(m.numPrimeBytes) msg.SetMac(mac) msg.SetKeyFP(fp) msg.SetContents(payload) m.sendPayload <- msg - return 0, ephemeral.Id{}, nil + return rounds.Round{}, ephemeral.Id{}, nil } func (m *mockRequestCmix) GetInstance() *network.Instance { diff --git a/single/request.go b/single/request.go index 01424229f1a6e275abcd5b13bc7df9d964315b0d..16429e835021a6eb5b570ba7d60362ae5ba0dca6 100644 --- a/single/request.go +++ b/single/request.go @@ -211,14 +211,14 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte, jww.DEBUG.Printf("[SU] Sent single-use request cMix message part "+ "%d of %d on round %d to %s (eph ID %d) (%s).", - 0, len(parts)+1, rid, recipient.ID, ephID.Int64(), tag) + 0, len(parts)+1, rid.ID, recipient.ID, ephID.Int64(), tag) var wg sync.WaitGroup wg.Add(len(parts)) failed := uint32(0) roundIDs := make([]id.Round, len(parts)+1) - roundIDs[0] = rid + roundIDs[0] = rid.ID for i, part := range parts { go func(i int, part []byte) { defer wg.Done() @@ -231,9 +231,7 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte, encryptedPayload := auth.Crypt(key, fp[:24], requestPart.Marshal()) mac := singleUse.MakeMAC(key, encryptedPayload) - var ephID ephemeral.Id - var err error - roundIDs[i], ephID, err = net.Send(recipient.ID, fp, + r, ephID, err := net.Send(recipient.ID, fp, cmixMsg.Service{}, encryptedPayload, mac, params.CmixParams) if err != nil { atomic.AddUint32(&failed, 1) @@ -242,6 +240,7 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte, i, len(part)+1, recipient.ID, tag, err) return } + roundIDs[i] = r.ID jww.DEBUG.Printf("[SU] Sent single-use request cMix message part "+ "%d of %d on round %d to %s (eph ID %d) (%s).", i, @@ -263,7 +262,7 @@ func TransmitRequest(recipient contact.Contact, tag string, payload []byte, remainingTimeout := params.Timeout - netTime.Since(timeStart) go waitForTimeout(timeoutKillChan, wrapper, remainingTimeout) - return []id.Round{rid}, sendingID, nil + return []id.Round{rid.ID}, sendingID, nil } // generateDhKeys generates a new public key and DH key. diff --git a/single/utils_test.go b/single/utils_test.go index e2657840d3be6112d414178fe5195bdce71a2bf8..003d68e54cf6ca822918d07ca77985323ea8e70a 100644 --- a/single/utils_test.go +++ b/single/utils_test.go @@ -118,7 +118,7 @@ func (m *mockCmix) AddIdentity(*id.ID, time.Time, bool) {} func (m *mockCmix) Send(recipient *id.ID, fp format.Fingerprint, ms message.Service, payload, mac []byte, _ cmix.CMIXParams) ( - id.Round, ephemeral.Id, error) { + rounds.Round, ephemeral.Id, error) { msg := format.NewMessage(m.numPrimeBytes) msg.SetMac(mac) @@ -155,7 +155,7 @@ func (m *mockCmix) Send(recipient *id.ID, fp format.Fingerprint, }) } - return 0, ephemeral.Id{}, nil + return rounds.Round{}, ephemeral.Id{}, nil } func serviceKey(ms message.Service) string { diff --git a/storage/utility/encryptionSalt.go b/storage/utility/encryptionSalt.go new file mode 100644 index 0000000000000000000000000000000000000000..18b60c15d4d9f3d39aba0d34fb7efb4877fd92e0 --- /dev/null +++ b/storage/utility/encryptionSalt.go @@ -0,0 +1,69 @@ +package utility + +import ( + jww "github.com/spf13/jwalterweatherman" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/xx_network/primitives/netTime" + "io" +) + +// Storage constats +const ( + saltKey = "encryptionSalt" + saltVersion = 0 + saltPrefix = "encryptionSaltPrefix" +) + +// saltSize is the defined size in bytes of the salt generated in +// newSalt. +const saltSize = 32 + +// NewOrLoadSalt will attempt to find a stored salt if one exists. +// If one does not exist in storage, a new one will be generated. The newly +// generated salt will be stored. +func NewOrLoadSalt(kv *versioned.KV, stream io.Reader) ([]byte, error) { + kv = kv.Prefix(saltPrefix) + salt, err := loadSalt(kv) + if err != nil { + jww.WARN.Printf("Failed to load salt, generating new one...") + salt, err = newSalt(kv, stream) + } + + return salt, err +} + +// loadSalt is a helper function which attempts to load a stored salt from +// memory. +func loadSalt(kv *versioned.KV) ([]byte, error) { + obj, err := kv.Get(saltKey, saltVersion) + if err != nil { + return nil, err + } + + return obj.Data, nil +} + +// newSalt generates a new random salt. This salt is stored and returned +// to the caller. +func newSalt(kv *versioned.KV, stream io.Reader) ([]byte, error) { + // Generate a new salt + salt := make([]byte, saltSize) + _, err := stream.Read(salt) + if err != nil { + return nil, err + } + + // Store salt in storage + obj := &versioned.Object{ + Version: saltVersion, + Timestamp: netTime.Now(), + Data: salt, + } + + err = kv.Set(saltKey, obj) + if err != nil { + return nil, err + } + + return salt, nil +} \ No newline at end of file diff --git a/storage/utility/encryptionSalt_test.go b/storage/utility/encryptionSalt_test.go new file mode 100644 index 0000000000000000000000000000000000000000..f041b9ded2673277022ae55ce3c356e9744ce712 --- /dev/null +++ b/storage/utility/encryptionSalt_test.go @@ -0,0 +1,49 @@ +package utility + +import ( + "bytes" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/ekv" + "gitlab.com/xx_network/crypto/csprng" + "testing" +) + +// Smoke test +func TestNewOrLoadSalt(t *testing.T) { + kv := ekv.MakeMemstore() + vkv := versioned.NewKV(kv) + + rng := csprng.NewSystemRNG() + + _, err := NewOrLoadSalt(vkv, rng) + if err != nil { + t.Fatalf("NewOrLoadSalt error: %+v", err) + } + +} + +// Test that calling NewOrLoadSalt twice returns the same +// salt that exists in storage. +func TestLoadSalt(t *testing.T) { + + kv := ekv.MakeMemstore() + vkv := versioned.NewKV(kv) + + rng := csprng.NewSystemRNG() + + original, err := NewOrLoadSalt(vkv, rng) + if err != nil { + t.Fatalf("NewOrLoadSalt error: %+v", err) + } + + loaded, err := NewOrLoadSalt(vkv, rng) + if err != nil { + t.Fatalf("NewOrLoadSalt error: %+v", err) + } + + // Test that loaded matches the original (ie a new one was not generated) + if !bytes.Equal(original, loaded) { + t.Fatalf("Failed to load salt.") + } + +} diff --git a/ud/channelIDTracking.go b/ud/channelIDTracking.go new file mode 100644 index 0000000000000000000000000000000000000000..e40f4a1e33cbfb5519f7f0bc4964df04c9cd01b8 --- /dev/null +++ b/ud/channelIDTracking.go @@ -0,0 +1,380 @@ +package ud + +import ( + "crypto/ed25519" + "encoding/json" + "errors" + "gitlab.com/xx_network/primitives/netTime" + "sync" + "time" + + jww "github.com/spf13/jwalterweatherman" + + "gitlab.com/elixxir/client/channels" + "gitlab.com/elixxir/client/stoppable" + "gitlab.com/elixxir/client/storage/versioned" + "gitlab.com/elixxir/client/xxdk" + "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/crypto/channel" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/xx_network/comms/connect" +) + +const ( + registrationDiskKey = "registrationDiskKey" + registrationDiskVersion = 0 + graceDuration = time.Hour +) + +var ErrChannelLeaseSignature = errors.New("failure to validate lease signature") + +// loadRegistrationDisk loads a registrationDisk from the kv +// and returns the registrationDisk. +func loadRegistrationDisk(kv *versioned.KV) (registrationDisk, error) { + obj, err := kv.Get(registrationDiskKey, registrationDiskVersion) + if err != nil { + return registrationDisk{}, err + } + return UnmarshallRegistrationDisk(obj.Data) +} + +// saveRegistrationDisk saves the given saveRegistrationDisk to +// the given kv. +func saveRegistrationDisk(kv *versioned.KV, reg registrationDisk) error { + regBytes, err := reg.Marshall() + if err != nil { + return err + } + obj := versioned.Object{ + Version: registrationDiskVersion, + Timestamp: netTime.Now(), + Data: regBytes, + } + return kv.Set(registrationDiskKey, &obj) +} + +// registrationDisk is used to encapsulate the channel user's key pair, +// lease and lease signature. +type registrationDisk struct { + rwmutex sync.RWMutex + + Registered bool + PublicKey ed25519.PublicKey + PrivateKey ed25519.PrivateKey + Lease int64 + Signature []byte +} + +// newRegistrationDisk creates a new newRegistrationDisk. +func newRegistrationDisk(publicKey ed25519.PublicKey, privateKey ed25519.PrivateKey, + lease time.Time, signature []byte) registrationDisk { + return registrationDisk{ + Lease: lease.UnixNano(), + PublicKey: publicKey, + PrivateKey: privateKey, + Signature: signature, + } +} + +func (r registrationDisk) IsRegistered() bool { + r.rwmutex.RLock() + defer r.rwmutex.RUnlock() + + return r.Registered +} + +// Update updates the registrationDisk that is currently +// stored on the kv with a new lease and lease signature. +func (r registrationDisk) Update(lease int64, signature []byte) { + r.rwmutex.Lock() + defer r.rwmutex.Unlock() + + r.Registered = true + r.Lease = lease + r.Signature = signature +} + +// Marshall marshalls the registrationDisk. +func (r registrationDisk) Marshall() ([]byte, error) { + r.rwmutex.RLock() + defer r.rwmutex.RUnlock() + + return json.Marshal(&r) +} + +// UnmarshallRegistrationDisk unmarshalls a registrationDisk +func UnmarshallRegistrationDisk(data []byte) (registrationDisk, error) { + var r registrationDisk + err := json.Unmarshal(data, &r) + if err != nil { + return registrationDisk{}, err + } + return r, nil +} + +// GetLease returns the current registrationDisk lease. +func (r registrationDisk) GetLease() time.Time { + r.rwmutex.RLock() + defer r.rwmutex.RUnlock() + + return time.Unix(0, r.Lease) +} + +// GetPublicKey returns the current public key. +func (r registrationDisk) GetPublicKey() ed25519.PublicKey { + r.rwmutex.RLock() + defer r.rwmutex.RUnlock() + + pubkey := make([]byte, ed25519.PublicKeySize) + copy(pubkey, r.PublicKey) + return pubkey +} + +// GetPrivateKey returns the current private key. +func (r registrationDisk) getPrivateKey() ed25519.PrivateKey { + r.rwmutex.RLock() + defer r.rwmutex.RUnlock() + + return r.PrivateKey +} + +// GetLeaseSignature returns the currentl signature and lease time. +func (r registrationDisk) GetLeaseSignature() ([]byte, time.Time) { + r.rwmutex.RLock() + defer r.rwmutex.RUnlock() + + return r.Signature, time.Unix(0, r.Lease) +} + +// clientIDTracker encapsulates the client channel lease and the +// repetitive scheduling of new lease registrations when the +// current lease expires. +type clientIDTracker struct { + kv *versioned.KV + + username string + + registrationDisk *registrationDisk + receptionIdentity *xxdk.ReceptionIdentity + + rngSource *fastRNG.StreamGenerator + + host *connect.Host + comms channelLeaseComms + udPubKey ed25519.PublicKey +} + +// clientIDTracker implements the NameService interface. +var _ channels.NameService = (*clientIDTracker)(nil) + +// newclientIDTracker creates a new clientIDTracker. +func newclientIDTracker(comms channelLeaseComms, host *connect.Host, username string, kv *versioned.KV, + receptionIdentity xxdk.ReceptionIdentity, udPubKey ed25519.PublicKey, rngSource *fastRNG.StreamGenerator) *clientIDTracker { + + reg, err := loadRegistrationDisk(kv) + if !kv.Exists(err) { + rng := rngSource.GetStream() + defer rng.Close() + + publicKey, privateKey, err := ed25519.GenerateKey(rng) + if err != nil { + jww.FATAL.Panic(err) + } + + reg = registrationDisk{ + PublicKey: publicKey, + PrivateKey: privateKey, + Lease: 0, + } + err = saveRegistrationDisk(kv, reg) + if err != nil { + jww.FATAL.Panic(err) + } + } else if err != nil { + jww.FATAL.Panic(err) + } + + c := &clientIDTracker{ + kv: kv, + rngSource: rngSource, + registrationDisk: ®, + receptionIdentity: &receptionIdentity, + username: username, + comms: comms, + host: host, + udPubKey: udPubKey, + } + + if !reg.IsRegistered() { + err = c.register() + if err != nil { + jww.FATAL.Panic(err) + } + } + + return c +} + +// Start starts the registration worker. +func (c *clientIDTracker) Start() (stoppable.Stoppable, error) { + stopper := stoppable.NewSingle("ud.ClientIDTracker") + go c.registrationWorker(stopper) + return stopper, nil +} + +func pow(base, exponent int) int { + if exponent == 0 { + return 1 + } + result := base + for i := 2; i <= exponent; i++ { + result *= base + } + return result +} + +// registrationWorker is meant to run in it's own goroutine +// periodically registering, getting a new lease. +func (c *clientIDTracker) registrationWorker(stopper *stoppable.Single) { + // start backoff at 32 seconds + base := 2 + exponent := 5 + waitTime := time.Second + maxBackoff := 300 + for { + if netTime.Now().After(c.registrationDisk.GetLease().Add(-graceDuration)) { + err := c.register() + if err != nil { + backoffSeconds := pow(base, exponent) + if backoffSeconds > maxBackoff { + backoffSeconds = maxBackoff + } else { + exponent += 1 + } + waitTime = time.Second * time.Duration(backoffSeconds) + } else { + waitTime = time.Second + } + } + + select { + case <-stopper.Quit(): + return + case <-time.After(c.registrationDisk.GetLease().Add(-graceDuration).Sub(netTime.Now())): + } + + // Avoid spamming the server in the event that it's service is down. + select { + case <-stopper.Quit(): + return + case <-time.After(waitTime): + } + } +} + +// GetUsername returns the username. +func (c *clientIDTracker) GetUsername() string { + return c.username +} + +// GetChannelValidationSignature returns the validation +// signature and the time it was signed. +func (c *clientIDTracker) GetChannelValidationSignature() ([]byte, time.Time) { + return c.registrationDisk.GetLeaseSignature() +} + +// GetChannelPubkey returns the user's public key. +func (c *clientIDTracker) GetChannelPubkey() ed25519.PublicKey { + return c.registrationDisk.GetPublicKey() +} + +// SignChannelMessage returns the signature of the given +// message. The ed25519 private key stored in the registrationDisk on the +// kv is used for signing. +func (c *clientIDTracker) SignChannelMessage(message []byte) ([]byte, error) { + privateKey := c.registrationDisk.getPrivateKey() + return ed25519.Sign(privateKey, message), nil +} + +// ValidateoChannelMessage +func (c *clientIDTracker) ValidateChannelMessage(username string, lease time.Time, pubKey ed25519.PublicKey, authorIDSignature []byte) bool { + return channel.VerifyChannelLease(authorIDSignature, pubKey, username, lease, c.udPubKey) +} + +// register causes a request for a new channel lease to be sent to +// the user discovery server. If successful in procuration of a new lease +// then it is written to the registrationDisk on the kv. +func (c *clientIDTracker) register() error { + lease, signature, err := c.requestChannelLease() + if err != nil { + return err + } + + c.registrationDisk.Update(lease, signature) + + return nil +} + +// requestChannelLease requests a new channel lease +// from the user discovery server. +func (c *clientIDTracker) requestChannelLease() (int64, []byte, error) { + ts := netTime.Now().UnixNano() + privKey, err := c.receptionIdentity.GetRSAPrivateKey() + if err != nil { + return 0, nil, err + } + + rng := c.rngSource.GetStream() + userPubKey := c.registrationDisk.GetPublicKey() + fSig, err := channel.SignChannelIdentityRequest(userPubKey, time.Unix(0, ts), privKey, rng) + if err != nil { + return 0, nil, err + } + rng.Close() + + msg := &mixmessages.ChannelLeaseRequest{ + UserID: c.receptionIdentity.ID.Marshal(), + UserEd25519PubKey: userPubKey, + Timestamp: ts, + UserPubKeyRSASignature: fSig, + } + + resp, err := c.comms.SendChannelLeaseRequest(c.host, msg) + if err != nil { + return 0, nil, err + } + + ok := channel.VerifyChannelLease(resp.UDLeaseEd25519Signature, + userPubKey, c.username, time.Unix(0, resp.Lease), c.udPubKey) + if !ok { + return 0, nil, ErrChannelLeaseSignature + } + + return resp.Lease, resp.UDLeaseEd25519Signature, err +} + +// StartChannelNameService creates a new clientIDTracker +// and returns a reference to it's type as the NameService interface. +// However, it's scheduler thread isn't started until it's Start +// method is called. +func (m *Manager) StartChannelNameService() (channels.NameService, error) { + + if m.nameService == nil { + udPubKeyBytes := m.user.GetCmix().GetInstance().GetPartialNdf().Get().UDB.DhPubKey + username, err := m.store.GetUsername() + if err != nil { + return nil, err + } + m.nameService = newclientIDTracker( + m.comms, + m.ud.host, + username, + m.getKv(), + m.user.GetReceptionIdentity(), + udPubKeyBytes, + m.getRng()) + + } + + return m.nameService, nil +} diff --git a/ud/channelIDTracking_test.go b/ud/channelIDTracking_test.go new file mode 100644 index 0000000000000000000000000000000000000000..52c4c9826949856a363f04c315a2fd5dc7449637 --- /dev/null +++ b/ud/channelIDTracking_test.go @@ -0,0 +1,137 @@ +package ud + +import ( + "crypto/ed25519" + "crypto/rand" + "testing" + "time" + + "github.com/stretchr/testify/require" + + "gitlab.com/xx_network/comms/connect" + "gitlab.com/xx_network/crypto/csprng" + "gitlab.com/xx_network/crypto/signature/rsa" + "gitlab.com/xx_network/primitives/id" + + "gitlab.com/elixxir/client/event" + "gitlab.com/elixxir/client/storage/versioned" + store "gitlab.com/elixxir/client/ud/store" + "gitlab.com/elixxir/crypto/fastRNG" + "gitlab.com/elixxir/ekv" +) + +func TestSignChannelMessage(t *testing.T) { + publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + reg := registrationDisk{ + PublicKey: publicKey, + PrivateKey: privateKey, + Lease: 0, + } + c := &clientIDTracker{ + registrationDisk: ®, + } + + message := []byte("hello world") + sig, err := c.SignChannelMessage(message) + require.NoError(t, err) + + require.True(t, ed25519.Verify(publicKey, message, sig)) +} + +func TestNewRegistrationDisk(t *testing.T) { + publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + lease := time.Now().UnixNano() + + signature := make([]byte, 64) + reg := newRegistrationDisk(publicKey, privateKey, time.Unix(0, lease), signature) + require.Equal(t, reg.PublicKey, publicKey) + require.Equal(t, reg.PrivateKey, privateKey) + require.Equal(t, reg.Signature, signature) + require.Equal(t, reg.Lease, lease) +} + +func TestLoadSaveRegistration(t *testing.T) { + publicKey, privateKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + lease := time.Now() + signature := make([]byte, 64) + reg := newRegistrationDisk(publicKey, privateKey, lease, signature) + + kv := versioned.NewKV(ekv.MakeMemstore()) + + registrationDisk, err := loadRegistrationDisk(kv) + require.Error(t, err) + require.False(t, kv.Exists(err)) + + err = saveRegistrationDisk(kv, reg) + require.NoError(t, err) + + registrationDisk, err = loadRegistrationDisk(kv) + require.NoError(t, err) + require.Equal(t, registrationDisk, reg) +} + +func TestChannelIDTracking(t *testing.T) { + rngGen := fastRNG.NewStreamGenerator(1000, 10, csprng.NewSystemRNG) + + // comms AddHost + stream := rngGen.GetStream() + privKey, err := rsa.GenerateKey(stream, 1024) + require.NoError(t, err) + + tnm := newTestNetworkManager(t) + managerkv := versioned.NewKV(ekv.MakeMemstore()) + udStore, err := store.NewOrLoadStore(managerkv) + m := &Manager{ + user: mockE2e{ + grp: getGroup(), + events: event.NewEventManager(), + rng: rngGen, + kv: managerkv, + network: tnm, + t: t, + key: privKey, + }, + store: udStore, + comms: &mockComms{}, + } + + netDef := m.getCmix().GetInstance().GetPartialNdf().Get() + udID, err := id.Unmarshal(netDef.UDB.ID) + require.NoError(t, err) + + params := connect.GetDefaultHostParams() + params.AuthEnabled = false + params.SendTimeout = 20 * time.Second + + host, err := m.comms.AddHost(udID, netDef.UDB.Address, + []byte(netDef.UDB.Cert), params) + require.NoError(t, err) + + // register + + kv := versioned.NewKV(ekv.MakeMemstore()) + comms := new(mockComms) + username := "Alice" + + udPubKey, udPrivKey, err := ed25519.GenerateKey(rand.Reader) + require.NoError(t, err) + + rsaPrivKey, err := m.user.GetReceptionIdentity().GetRSAPrivateKey() + require.NoError(t, err) + + comms.SetUserRSAPubKey(rsaPrivKey.GetPublic()) + comms.SetUDEd25519PrivateKey(&udPrivKey) + comms.SetUsername(username) + + myTestClientIDTracker := newclientIDTracker( + comms, host, username, + kv, m.user.GetReceptionIdentity(), + udPubKey, rngGen) + + err = myTestClientIDTracker.register() + require.NoError(t, err) +} diff --git a/ud/comms.go b/ud/comms.go index 160f7529aa9296235c63d7a32ad0de226be90112..df993f608cc1ccc08a5f3e670a8322e4d97ff819 100644 --- a/ud/comms.go +++ b/ud/comms.go @@ -44,6 +44,8 @@ type Comms interface { // object. This will be used to send to the UD service on the above // gRPC send functions. GetHost(hostId *id.ID) (*connect.Host, bool) + + channelLeaseComms } // removeFactComms is a sub-interface of the Comms interface for the @@ -75,3 +77,7 @@ type registerUserComms interface { type addFactComms interface { SendRegisterFact(host *connect.Host, message *pb.FactRegisterRequest) (*pb.FactRegisterResponse, error) } + +type channelLeaseComms interface { + SendChannelLeaseRequest(host *connect.Host, message *pb.ChannelLeaseRequest) (*pb.ChannelLeaseResponse, error) +} diff --git a/ud/lookup_test.go b/ud/lookup_test.go index 9542cfc4015981be3c342d50444e3c5eb234a7d9..4b4d61d65e4154cb94b831fe5c4bee44d1a01d11 100644 --- a/ud/lookup_test.go +++ b/ud/lookup_test.go @@ -50,7 +50,7 @@ func TestManager_Lookup(t *testing.T) { DhPubKey: publicKey, } - contacts := []*Contact{&Contact{ + contacts := []*Contact{{ UserID: expectedContact.ID.Bytes(), PubKey: expectedContact.DhPubKey.Bytes(), }} diff --git a/ud/manager.go b/ud/manager.go index 75604bd3ddee8848a23179c645742ea735ff3f16..9ea87cf84bef02982f8830ba8afa68b8149938dc 100644 --- a/ud/manager.go +++ b/ud/manager.go @@ -44,6 +44,10 @@ type Manager struct { // ud is the tracker for the contact information of the specified UD server. // This information is specified in Manager's constructors (NewOrLoad and NewManagerFromBackup). ud *userDiscovery + + // nameService adheres to the channels.NameService interface. This is + // implemented using the clientIDTracker. + nameService *clientIDTracker } // NewOrLoad loads an existing Manager from storage or creates a diff --git a/ud/mockComms_test.go b/ud/mockComms_test.go index 1a50717f55bf749b4681009ceba7325c3b749a45..b77efc11a502e72036b343708e993ede522f46ba 100644 --- a/ud/mockComms_test.go +++ b/ud/mockComms_test.go @@ -8,14 +8,24 @@ package ud import ( - pb "gitlab.com/elixxir/comms/mixmessages" + "crypto/ed25519" + "time" + "gitlab.com/xx_network/comms/connect" "gitlab.com/xx_network/comms/messages" + "gitlab.com/xx_network/crypto/signature/rsa" "gitlab.com/xx_network/primitives/id" + + pb "gitlab.com/elixxir/comms/mixmessages" + "gitlab.com/elixxir/crypto/channel" ) type mockComms struct { - udHost *connect.Host + udHost *connect.Host + userRsaPub *rsa.PublicKey + userEd25519PubKey []byte + udPrivKey *ed25519.PrivateKey + username string } func (m mockComms) SendRegisterUser(host *connect.Host, message *pb.UDBUserRegistration) (*messages.Ack, error) { @@ -51,3 +61,44 @@ func (m *mockComms) AddHost(hid *id.ID, address string, cert []byte, params conn func (m mockComms) GetHost(hostId *id.ID) (*connect.Host, bool) { return m.udHost, true } + +func (m *mockComms) SetUDEd25519PrivateKey(key *ed25519.PrivateKey) { + m.udPrivKey = key +} + +func (m *mockComms) SetUserRSAPubKey(userRsaPub *rsa.PublicKey) { + m.userRsaPub = userRsaPub +} + +func (m *mockComms) SetUsername(u string) { + m.username = u +} + +func (m mockComms) SendChannelLeaseRequest(host *connect.Host, message *pb.ChannelLeaseRequest) (*pb.ChannelLeaseResponse, error) { + + err := channel.VerifyChannelIdentityRequest(message.UserPubKeyRSASignature, + message.UserEd25519PubKey, + time.Now(), + time.Unix(0, message.Timestamp), + m.userRsaPub) + if err != nil { + panic(err) + } + + d, _ := time.ParseDuration("4h30m") + lease := time.Now().Add(d).UnixNano() + signature := channel.SignChannelLease(message.UserEd25519PubKey, m.username, + time.Unix(0, lease), *m.udPrivKey) + + if err != nil { + panic(err) + } + + response := &pb.ChannelLeaseResponse{ + Lease: lease, + UserEd25519PubKey: m.userEd25519PubKey, + UDLeaseEd25519Signature: signature, + } + + return response, nil +} diff --git a/ud/networkManager_test.go b/ud/networkManager_test.go index b10cd8f2361568dcc6b7a6b3ea7c3b780a911982..bd3630ebf9016e37535dbba6ea332c5676da4a1a 100644 --- a/ud/networkManager_test.go +++ b/ud/networkManager_test.go @@ -34,9 +34,44 @@ type testNetworkManager struct { responseProcessor message.Processor } +func (tnm *testNetworkManager) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler, + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + + msg := format.NewMessage(tnm.instance.GetE2EGroup().GetP().ByteLen()) + + var rid id.Round = 123 + ephemeralId := new(ephemeral.Id) + + fingerprint, service, payload, mac, err := assembler(rid) + if err != nil { + return rounds.Round{ID: rid}, *ephemeralId, err + } + + // Build message. Will panic if inputs are not correct. + msg.SetKeyFP(fingerprint) + msg.SetContents(payload) + msg.SetMac(mac) + msg.SetSIH(service.Hash(msg.GetContents())) + // If the recipient for a call to Send is UD, then this + // is the request pathway. Call the UD processor to simulate + // the UD picking up the request + if bytes.Equal(tnm.instance.GetFullNdf(). + Get().UDB.ID, + recipient.Bytes()) { + tnm.responseProcessor.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{}) + + } else { + // This should happen when the mock UD service Sends back a response. + // Calling process mocks up the requester picking up the response. + tnm.requestProcess.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{}) + } + + return rounds.Round{}, ephemeral.Id{}, nil +} + func (tnm *testNetworkManager) Send(recipient *id.ID, fingerprint format.Fingerprint, service message.Service, - payload, mac []byte, cmixParams cmix.CMIXParams) (id.Round, ephemeral.Id, error) { + payload, mac []byte, cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { msg := format.NewMessage(tnm.instance.GetE2EGroup().GetP().ByteLen()) // Build message. Will panic if inputs are not correct. msg.SetKeyFP(fingerprint) @@ -57,7 +92,7 @@ func (tnm *testNetworkManager) Send(recipient *id.ID, fingerprint format.Fingerp tnm.requestProcess.Process(msg, receptionID.EphemeralIdentity{}, rounds.Round{}) } - return 0, ephemeral.Id{}, nil + return rounds.Round{}, ephemeral.Id{}, nil } func (tnm *testNetworkManager) AddFingerprint(identity *id.ID, @@ -136,7 +171,7 @@ func (tnm *testNetworkManager) SendToAny(sendFunc func(host *connect.Host) (inte panic("implement me") } -func (tnm *testNetworkManager) SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { +func (tnm *testNetworkManager) SendMany(messages []cmix.TargetedCmixMessage, p cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { //TODO implement me panic("implement me") } @@ -196,7 +231,7 @@ func (tnm *testNetworkManager) TriggerNodeRegistration(nid *id.ID) { panic("implement me") } -func (tnm *testNetworkManager) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) error { +func (tnm *testNetworkManager) GetRoundResults(timeout time.Duration, roundCallback cmix.RoundEventCallback, roundList ...id.Round) { //TODO implement me panic("implement me") } diff --git a/xxdk/ndf.go b/xxdk/ndf.go index de1056622c25efd98bb252c5eee46f15efaaece0..1872276f9c0b9ff8a0eee745b05db2018e27fad2 100644 --- a/xxdk/ndf.go +++ b/xxdk/ndf.go @@ -9,6 +9,7 @@ package xxdk import ( "encoding/base64" + "github.com/golang/protobuf/proto" "github.com/pkg/errors" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/comms/client" @@ -18,7 +19,6 @@ import ( "gitlab.com/xx_network/crypto/tls" "gitlab.com/xx_network/primitives/id" "gitlab.com/xx_network/primitives/id/ephemeral" - "google.golang.org/protobuf/proto" "io/ioutil" "net/http" ) @@ -59,7 +59,7 @@ func DownloadNdfFromGateway(address string, cert []byte) ( } // Send poll request and receive response containing NDF - resp, err := comms.SendPoll(host, pollMsg) + resp, _, _, err := comms.SendPoll(host, pollMsg) if err != nil { return nil, err } diff --git a/xxdk/utilsInterfaces_test.go b/xxdk/utilsInterfaces_test.go index b1b93977c64fbf1fcdd8ce1635ceeb15bc3b0655..4fbf3852387c7c095934c2fbf65f388cfa046e2f 100644 --- a/xxdk/utilsInterfaces_test.go +++ b/xxdk/utilsInterfaces_test.go @@ -109,13 +109,20 @@ func (t *testNetworkManagerGeneric) AddFingerprint(identity *id.ID, fingerprint } func (t *testNetworkManagerGeneric) Send(*id.ID, format.Fingerprint, - message.Service, []byte, []byte, cmix.CMIXParams) (id.Round, + message.Service, []byte, []byte, cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { - return id.Round(0), ephemeral.Id{}, nil + return rounds.Round{}, ephemeral.Id{}, nil } + +func (t *testNetworkManagerGeneric) SendWithAssembler(recipient *id.ID, assembler cmix.MessageAssembler, + cmixParams cmix.CMIXParams) (rounds.Round, ephemeral.Id, error) { + + return rounds.Round{}, ephemeral.Id{}, nil +} + func (t *testNetworkManagerGeneric) SendMany(messages []cmix.TargetedCmixMessage, - p cmix.CMIXParams) (id.Round, []ephemeral.Id, error) { - return 0, []ephemeral.Id{}, nil + p cmix.CMIXParams) (rounds.Round, []ephemeral.Id, error) { + return rounds.Round{}, []ephemeral.Id{}, nil } func (t *testNetworkManagerGeneric) GetInstance() *network.Instance { return t.instance @@ -176,8 +183,7 @@ func (t *testNetworkManagerGeneric) GetIdentity(get *id.ID) ( return identity.TrackedID{}, nil } func (t *testNetworkManagerGeneric) GetRoundResults(timeout time.Duration, - roundCallback cmix.RoundEventCallback, roundList ...id.Round) error { - return nil + roundCallback cmix.RoundEventCallback, roundList ...id.Round) { } func (t *testNetworkManagerGeneric) HasNode(nid *id.ID) bool { return false } func (t *testNetworkManagerGeneric) IsHealthy() bool { return true } diff --git a/xxdk/version_vars.go b/xxdk/version_vars.go index 61bd87da81f93ce196ac84a628a11107357ecb24..76a41f587dcf4dfd5fa2e3ba8b7ebea7510c4d58 100644 --- a/xxdk/version_vars.go +++ b/xxdk/version_vars.go @@ -1,11 +1,11 @@ // Code generated by go generate; DO NOT EDIT. // This file was generated by robots at -// 2022-09-15 10:52:12.1953796 -0700 PDT m=+0.451573601 +// 2022-10-17 12:52:49.822998 -0500 CDT m=+0.037589931 package xxdk -const GITVERSION = `55b66f6b Update proto files` -const SEMVER = "4.2.0" +const GITVERSION = `a401d138 update deps` +const SEMVER = "4.3.0" const DEPENDENCIES = `module gitlab.com/elixxir/client go 1.17 @@ -20,35 +20,47 @@ require ( github.com/spf13/cobra v1.5.0 github.com/spf13/jwalterweatherman v1.1.0 github.com/spf13/viper v1.12.0 + github.com/stretchr/testify v1.8.0 gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f - gitlab.com/elixxir/comms v0.0.4-0.20220914220142-601071b77d78 - gitlab.com/elixxir/crypto v0.0.7-0.20220901215826-1ceaeb59081f + gitlab.com/elixxir/comms v0.0.4-0.20221017173926-4eaa6061dfaa + gitlab.com/elixxir/crypto v0.0.7-0.20221017173452-565da4101a3b gitlab.com/elixxir/ekv v0.2.1 - gitlab.com/elixxir/primitives v0.0.3-0.20220901220638-1acc75fabdc6 - gitlab.com/xx_network/comms v0.0.4-0.20220914220351-2e461edbfe48 - gitlab.com/xx_network/crypto v0.0.5-0.20220902182733-69aad094b487 - gitlab.com/xx_network/primitives v0.0.4-0.20220902183448-319596e2fec8 + gitlab.com/elixxir/primitives v0.0.3-0.20221017172918-6176818d1aba + gitlab.com/xx_network/comms v0.0.4-0.20221017172508-09e33697dc15 + gitlab.com/xx_network/crypto v0.0.5-0.20221017172404-b384a8d8b171 + gitlab.com/xx_network/primitives v0.0.4-0.20221017171439-42169a3e5c0d go.uber.org/ratelimit v0.2.0 golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa - golang.org/x/net v0.0.0-20220802222814-0bcc04d9c69b - google.golang.org/grpc v1.48.0 + golang.org/x/net v0.0.0-20220822230855-b0a4917ee28c + google.golang.org/grpc v1.49.0 google.golang.org/protobuf v1.28.1 ) require ( + git.xx.network/elixxir/grpc-web-go-client v0.0.0-20220908170150-ef04339ffe65 // indirect github.com/andres-erbsen/clock v0.0.0-20160526145045-9e14626cd129 // indirect github.com/badoux/checkmail v1.2.1 // indirect + github.com/cenkalti/backoff/v4 v4.1.3 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/desertbit/timer v0.0.0-20180107155436-c41aec40b27f // indirect github.com/elliotchance/orderedmap v1.4.0 // indirect github.com/fsnotify/fsnotify v1.5.4 // indirect + github.com/gorilla/websocket v1.5.0 // indirect github.com/hashicorp/hcl v1.0.0 // indirect + github.com/improbable-eng/grpc-web v0.15.0 // indirect github.com/inconshreveable/mousetrap v1.0.0 // indirect + github.com/klauspost/compress v1.11.7 // indirect github.com/klauspost/cpuid/v2 v2.1.0 // indirect github.com/magiconair/properties v1.8.6 // indirect github.com/mitchellh/go-homedir v1.1.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/pelletier/go-toml v1.9.5 // indirect github.com/pelletier/go-toml/v2 v2.0.2 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/rs/cors v1.8.2 // indirect + github.com/sethvargo/go-diceware v0.3.0 // indirect github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e // indirect + github.com/soheilhy/cmux v0.1.5 // indirect github.com/spf13/afero v1.9.2 // indirect github.com/spf13/cast v1.5.0 // indirect github.com/spf13/pflag v1.0.5 // indirect @@ -58,11 +70,13 @@ require ( github.com/tyler-smith/go-bip39 v1.1.0 // indirect github.com/zeebo/blake3 v0.2.3 // indirect gitlab.com/xx_network/ring v0.0.3-0.20220222211904-da613960ad93 // indirect + go.uber.org/atomic v1.10.0 // indirect golang.org/x/sys v0.0.0-20220731174439-a90be440212d // indirect golang.org/x/text v0.3.7 // indirect - google.golang.org/genproto v0.0.0-20220802133213-ce4fa296bf78 // indirect + google.golang.org/genproto v0.0.0-20220822174746-9e6da59bd2fc // indirect gopkg.in/ini.v1 v1.66.6 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + nhooyr.io/websocket v1.8.7 // indirect ) `