From f17caaa7f2b71e0119b3226b9fe87ab1b0213fa9 Mon Sep 17 00:00:00 2001 From: Benjamin Wenger <ben@elixxir.ioo> Date: Fri, 23 Sep 2022 08:31:53 -0700 Subject: [PATCH] in progress impl --- channels/channelMessages.proto | 22 ++---- channels/eventModel.go | 73 +++++++++++++++----- channels/identityStore.go | 31 +++++++++ channels/interface.go | 23 ++++++- channels/manager.go | 81 +++++++++++++++++++--- channels/nameService.go | 1 + channels/nickname.go | 107 +++++++++++++++++++++++++++++ channels/sendTracker.go | 53 +++++++++++++- cmix/identity/receptionID/store.go | 3 +- go.mod | 2 +- go.sum | 2 + 11 files changed, 347 insertions(+), 51 deletions(-) create mode 100644 channels/identityStore.go create mode 100644 channels/nickname.go diff --git a/channels/channelMessages.proto b/channels/channelMessages.proto index 91dfa6d06..f64265e9a 100644 --- a/channels/channelMessages.proto +++ b/channels/channelMessages.proto @@ -26,7 +26,11 @@ message ChannelMessage{ // Payload is the actual message payload. It will be processed differently // based on the PayloadType. - bytes Payload = 4; + 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; } // UserMessage is a message sent by a user who is a member within the channel. @@ -36,29 +40,13 @@ message UserMessage { // ChannelMessage. bytes Message = 1; - // ValidationSignature is the signature validating this user owns their - // username and may send messages to the channel under this username. This - // signature is provided by UD and may be validated by all members of the - // channel. - // - // ValidationSignature = Sig(UD_ECCPrivKey, Username | ECCPublicKey | UsernameLease) - bytes ValidationSignature = 2; - // 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; - // Username is the username the user has registered with the channel and - // with UD. - string Username = 4; - // ECCPublicKey is the user's EC Public key. This is provided by the // network. bytes ECCPublicKey = 5; - - // UsernameLease is the lease that has been provided to the username. This - // value is provide by UD. - int64 UsernameLease = 6; } diff --git a/channels/eventModel.go b/channels/eventModel.go index 9ebb58f48..dbff99e5d 100644 --- a/channels/eventModel.go +++ b/channels/eventModel.go @@ -9,6 +9,7 @@ package channels import ( "github.com/golang/protobuf/proto" + "github.com/golang/protobuf/ptypes/timestamp" jww "github.com/spf13/jwalterweatherman" "gitlab.com/elixxir/client/cmix/identity/receptionID" "gitlab.com/elixxir/primitives/states" @@ -28,7 +29,8 @@ const AdminUsername = "Admin" type SentStatus uint8 const ( - Sent SentStatus = iota + Unsent SentStatus = iota + Sent Delivered Failed ) @@ -46,9 +48,20 @@ type EventModel interface { // 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 which it may 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 ReceiveMessage(channelID *id.ID, messageID cryptoChannel.MessageID, - senderUsername, text string, timestamp time.Time, lease time.Duration, - round rounds.Round, status SentStatus) + nickname, codename, extension, color, text string, + timestamp time.Time, lease time.Duration, round rounds.Round, + 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 @@ -56,10 +69,20 @@ type EventModel interface { // // 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 which it may 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 ReceiveReply(channelID *id.ID, messageID cryptoChannel.MessageID, - reactionTo cryptoChannel.MessageID, senderUsername string, - text string, timestamp time.Time, lease time.Duration, - round rounds.Round, status SentStatus) + reactionTo cryptoChannel.MessageID, nickname, codename, extension, + color, text string, timestamp time.Time, lease time.Duration, + round rounds.Round, 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. @@ -69,14 +92,29 @@ type EventModel interface { // 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. + // + // the api needs to return a uuid of the message which it may 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 ReceiveReaction(channelID *id.ID, messageID cryptoChannel.MessageID, - reactionTo cryptoChannel.MessageID, senderUsername string, - reaction string, timestamp time.Time, lease time.Duration, - round rounds.Round, status SentStatus) + reactionTo cryptoChannel.MessageID, nickname, codename, extension, + color, reaction string, timestamp time.Time, lease time.Duration, + round rounds.Round, status SentStatus) uint64 // UpdateSentStatus is called whenever the sent status of a message has // changed. - UpdateSentStatus(messageID cryptoChannel.MessageID, status SentStatus) + // + // 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) @@ -89,12 +127,14 @@ type EventModel interface { // types. Default ones for Text, Reaction, and AdminText. type MessageTypeReceiveMessage func(channelID *id.ID, messageID cryptoChannel.MessageID, messageType MessageType, - senderUsername string, content []byte, timestamp time.Time, - lease time.Duration, round rounds.Round, status SentStatus) + nickname, codename, extension, color string, content []byte, + timestamp time.Time, lease time.Duration, round rounds.Round, + status SentStatus) // updateStatusFunc is a function type for EventModel.UpdateSentStatus so it can // be mocked for testing where used. -type updateStatusFunc func(messageID cryptoChannel.MessageID, status SentStatus) +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. @@ -150,7 +190,9 @@ type triggerEventFunc func(chID *id.ID, umi *userMessageInternal, // // It will call the appropriate MessageTypeHandler assuming one exists. func (e *events) triggerEvent(chID *id.ID, umi *userMessageInternal, - receptionID receptionID.EphemeralIdentity, round rounds.Round, + Identity cryptoChannel.Identity, ts timestamp.Timestamp, + receptionID receptionID.EphemeralIdentity, + round rounds.Round, status SentStatus) { um := umi.GetUserMessage() cm := umi.GetChannelMessage() @@ -168,9 +210,6 @@ func (e *events) triggerEvent(chID *id.ID, umi *userMessageInternal, return } - // Modify the timestamp to reduce the chance message order will be ambiguous - ts := mutateTimestamp(round.Timestamps[states.QUEUED], umi.GetMessageID()) - // Call the listener. This is already in an instanced event, no new thread needed. listener(chID, umi.GetMessageID(), messageType, um.Username, cm.Payload, ts, time.Duration(cm.Lease), round, status) diff --git a/channels/identityStore.go b/channels/identityStore.go new file mode 100644 index 000000000..2432a86c8 --- /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/interface.go b/channels/interface.go index 622ee6802..fd029b16e 100644 --- a/channels/interface.go +++ b/channels/interface.go @@ -9,6 +9,7 @@ package channels import ( "gitlab.com/elixxir/client/cmix" + "gitlab.com/elixxir/client/cmix/pickup/store" "gitlab.com/elixxir/client/cmix/rounds" cryptoBroadcast "gitlab.com/elixxir/crypto/broadcast" cryptoChannel "gitlab.com/elixxir/crypto/channel" @@ -27,6 +28,13 @@ var ValidForever = time.Duration(math.MaxInt64) type Manager interface { + // GetIdentity returns the public identity associated with this channel manager + GetIdentity() store.Identity + + // 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 @@ -42,8 +50,8 @@ type Manager interface { // 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) ( + 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 @@ -106,4 +114,15 @@ type Manager interface { // 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) + + // GetNickname returns the nickname for the given channel if it exists + GetNickname(ch *id.ID) (nickname string, exists bool) } diff --git a/channels/manager.go b/channels/manager.go index 4547cf57f..71181eec1 100644 --- a/channels/manager.go +++ b/channels/manager.go @@ -11,12 +11,16 @@ package channels import ( + "crypto/ed25519" + "encoding/base64" + "fmt" "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" @@ -24,20 +28,27 @@ import ( "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 - name NameService + kv *versioned.KV + net Client + rng *fastRNG.StreamGenerator // Events model *events + // nicknames + *nicknameManager + //send tracker st *sendTracker @@ -64,19 +75,52 @@ type Client interface { RemoveHealthCallback(uint64) } -// NewManager creates a new channel.Manager. It prefixes the KV with the -// username so that multiple instances for multiple users will not error. -func NewManager(kv *versioned.KV, net Client, - rng *fastRNG.StreamGenerator, name NameService, model EventModel) Manager { +// NewManager creates a new channel.Manager from a private identity. It +// prefixes the KV with a tag derived from the public key which can be retried +// for reloading using Manage.GetStorageTag. +func NewManager(identity cryptoChannel.PrivateIdentity, kv *versioned.KV, + net Client, rng *fastRNG.StreamGenerator, model EventModel) (Manager, error) { + + // Prefix the kv with the username so multiple can be run + kv = kv.Prefix(getStorageTag(identity.PubKey)) + + if err := storeIdentity(kv, identity); err != nil { + return nil, 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, name NameService, model EventModel) (Manager, + error) { // Prefix the kv with the username so multiple can be run - kv = kv.Prefix(name.GetUsername()) + kv = kv.Prefix(storageTag) + + //load the identity + identity, err := loadIdentity(kv) + if err != nil { + return nil, err + } + + m := setupManager(identity, kv, net, rng, model) + + return &m +} + +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, - name: name, broadcastMaker: broadcast.NewBroadcastChannel, } @@ -87,6 +131,8 @@ func NewManager(kv *versioned.KV, net Client, m.loadChannels() + m.nicknameManager = loadOrNewNicknameManager(kv) + return &m } @@ -163,3 +209,18 @@ func (m *manager) ReplayChannel(chID *id.ID) error { 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 +} + +func getStorageTag(pub ed25519.PublicKey) string { + return fmt.Sprintf(storageTagFormat, base64.StdEncoding.EncodeToString(pub)) +} diff --git a/channels/nameService.go b/channels/nameService.go index f1d93b876..d77fa9414 100644 --- a/channels/nameService.go +++ b/channels/nameService.go @@ -14,6 +14,7 @@ import ( // NameService is an interface which encapsulates // the user identity channel tracking service. +// NameService is currently unused type NameService interface { // GetUsername returns the username. diff --git a/channels/nickname.go b/channels/nickname.go new file mode 100644 index 000000000..a7dfb4521 --- /dev/null +++ b/channels/nickname.go @@ -0,0 +1,107 @@ +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 nm.kv.Exists(err) { + jww.FATAL.Panicf("Failed to load nicknameManager: %+v", err) + } + + return nm, nil + +} + +// 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 nil +} + +// DeleteNickname removes the nickname for a given channel, using the codename +// for that channel instead +func (nm *nicknameManager) DeleteNickname(ch *id.ID) { + nm.mux.Lock() + defer nm.mux.Unlock() + + delete(nm.byChannel, *ch) +} + +// save stores the nickname manager to disk. It must occur under the mux. +func (nm *nicknameManager) save() error { + data, err := json.Marshal(&nm.byChannel) + 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 + } + return json.Unmarshal(obj.Data, &nm.byChannel) +} + +// IsNicknameValid checks if a nickname is valid +// +// rules +// - a Nickname must not be longer than 24 characters +// todo: add character filtering +func IsNicknameValid(nm string) error { + if len([]rune(nm)) > 24 { + return errors.New("nicknames must be 24 characters in length or less") + } +} diff --git a/channels/sendTracker.go b/channels/sendTracker.go index 3913b666d..f471cd60d 100644 --- a/channels/sendTracker.go +++ b/channels/sendTracker.go @@ -23,7 +23,11 @@ import ( const ( sendTrackerStorageKey = "sendTrackerStorageKey" sendTrackerStorageVersion = 0 - getRoundResultsTimeout = 60 * time.Second + + 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 @@ -34,6 +38,7 @@ type tracked struct { MsgID cryptoChannel.MessageID ChannelID *id.ID RoundID id.Round + UUID uint64 } // the sendTracker tracks outbound messages and denotes when they are delivered @@ -45,6 +50,8 @@ type sendTracker struct { byMessageID map[cryptoChannel.MessageID]*tracked + unsent map[uint64]*tracked + mux sync.RWMutex trigger triggerEventFunc @@ -69,6 +76,7 @@ func loadSendTracker(net Client, kv *versioned.KV, trigger triggerEventFunc, st := &sendTracker{ byRound: make(map[id.Round][]*tracked), byMessageID: make(map[cryptoChannel.MessageID]*tracked), + unsent: make(map[uint64]*tracked), trigger: trigger, adminTrigger: adminTrigger, updateStatus: updateStatus, @@ -81,6 +89,13 @@ func loadSendTracker(net Client, kv *versioned.KV, trigger triggerEventFunc, }*/ st.load() + //denote all unsent messages as failed and clear + for uuid := range st.unsent { + updateStatus(uuid, cryptoChannel.MessageID{}, + 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) { @@ -89,6 +104,7 @@ func loadSendTracker(net Client, kv *versioned.KV, trigger triggerEventFunc, } net.RemoveHealthCallback(callBackID) for rid := range st.byRound { + rr := &roundResults{ round: rid, st: st, @@ -102,15 +118,32 @@ func loadSendTracker(net Client, kv *versioned.KV, trigger triggerEventFunc, // store writes the list of rounds that have been func (st *sendTracker) store() error { + + //save sent messages data, err := json.Marshal(&st.byRound) if err != nil { return err } - return st.kv.Set(sendTrackerStorageKey, &versioned.Object{ + err = st.kv.Set(sendTrackerStorageKey, &versioned.Object{ Version: sendTrackerStorageVersion, Timestamp: time.Now(), Data: data, }) + if err != nil { + return err + } + + //save unsent messages + data, err = json.Marshal(&st.unsent) + if err != nil { + return err + } + + return st.kv.Set(sendTrackerUnsentStorageKey, &versioned.Object{ + Version: sendTrackerUnsentStorageVersion, + Timestamp: time.Now(), + Data: data, + }) } // load will get the stored rounds to be checked from disk and builds @@ -132,9 +165,25 @@ func (st *sendTracker) load() error { 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 } +func denotePendingSend(channelID *id.ID, + umi *userMessageInternal, round rounds.Round) { + +} + // send tracks a generic send message func (st *sendTracker) send(channelID *id.ID, umi *userMessageInternal, round rounds.Round) { diff --git a/cmix/identity/receptionID/store.go b/cmix/identity/receptionID/store.go index 967c22e03..78400d30f 100644 --- a/cmix/identity/receptionID/store.go +++ b/cmix/identity/receptionID/store.go @@ -29,8 +29,7 @@ const ( receptionStoreStorageVersion = 0 ) -var InvalidRequestedNumIdentities = errors.New("cannot get less than one identity(s)") - +var InvalidRequestedNumIdentities = errors.New("cannot get less than one identity(s)")code type Store struct { // Identities which are being actively checked active []*registration diff --git a/go.mod b/go.mod index 227ba6e5c..b02daa4ba 100644 --- a/go.mod +++ b/go.mod @@ -15,7 +15,7 @@ require ( github.com/stretchr/testify v1.8.0 gitlab.com/elixxir/bloomfilter v0.0.0-20211222005329-7d931ceead6f gitlab.com/elixxir/comms v0.0.4-0.20220913220502-eed192f654bd - gitlab.com/elixxir/crypto v0.0.7-0.20220920002307-5541473e9aa7 + gitlab.com/elixxir/crypto v0.0.7-0.20220923135535-fb10d313632a 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.20220913215811-c4bf83b27de3 diff --git a/go.sum b/go.sum index c534518a5..662bf87b4 100644 --- a/go.sum +++ b/go.sum @@ -636,6 +636,8 @@ gitlab.com/elixxir/crypto v0.0.3/go.mod h1:ZNgBOblhYToR4m8tj4cMvJ9UsJAUKq+p0gCp0 gitlab.com/elixxir/crypto v0.0.7-0.20220913220142-ab0771bad0af/go.mod h1:QF8SzsrYh9Elip9EUYUDAhPjqO9DGrrrQxYHvn+VXok= gitlab.com/elixxir/crypto v0.0.7-0.20220920002307-5541473e9aa7 h1:9IsBtL8zcUG86XcfNUVIKcnlL5tyKlyQt1cJ5nogr1U= gitlab.com/elixxir/crypto v0.0.7-0.20220920002307-5541473e9aa7/go.mod h1:QF8SzsrYh9Elip9EUYUDAhPjqO9DGrrrQxYHvn+VXok= +gitlab.com/elixxir/crypto v0.0.7-0.20220923135535-fb10d313632a h1:n12oIGF9GzfA/0ZTggCjubTxOZPF8p9WINfkdko9e7E= +gitlab.com/elixxir/crypto v0.0.7-0.20220923135535-fb10d313632a/go.mod h1:QF8SzsrYh9Elip9EUYUDAhPjqO9DGrrrQxYHvn+VXok= 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