diff --git a/cmd/waku/rlngenerate/command_rln.go b/cmd/waku/rlngenerate/command_rln.go index c689d9a4..bb751b55 100644 --- a/cmd/waku/rlngenerate/command_rln.go +++ b/cmd/waku/rlngenerate/command_rln.go @@ -28,13 +28,13 @@ var Command = cli.Command{ Name: "generate-rln-credentials", Usage: "Generate credentials for usage with RLN", Action: func(cCtx *cli.Context) error { - err := verifyFlags() - if err != nil { + if options.ETHPrivateKey == nil { + err := errors.New("a private key must be specified") logger.Error("validating option flags", zap.Error(err)) return cli.Exit(err, 1) } - err = execute(context.Background()) + err := execute(context.Background()) if err != nil { logger.Error("registering RLN credentials", zap.Error(err)) return cli.Exit(err, 1) @@ -45,24 +45,6 @@ var Command = cli.Command{ Flags: flags, } -func verifyFlags() error { - if options.CredentialsPath == "" { - logger.Warn("keystore: no credentials path set, using default path", zap.String("path", keystore.RLN_CREDENTIALS_FILENAME)) - options.CredentialsPath = keystore.RLN_CREDENTIALS_FILENAME - } - - if options.CredentialsPassword == "" { - logger.Warn("keystore: no credentials password set, using default password", zap.String("password", keystore.RLN_CREDENTIALS_PASSWORD)) - options.CredentialsPassword = keystore.RLN_CREDENTIALS_PASSWORD - } - - if options.ETHPrivateKey == nil { - return errors.New("a private key must be specified") - } - - return nil -} - func execute(ctx context.Context) error { ethClient, err := ethclient.Dial(options.ETHClientAddress) if err != nil { @@ -122,15 +104,20 @@ func execute(ctx context.Context) error { } func persistCredentials(identityCredential *rln.IdentityCredential, membershipIndex rln.MembershipIndex, chainID *big.Int) error { + appKeystore, err := keystore.New(options.CredentialsPath, dynamic.RLNAppInfo, logger) + if err != nil { + return err + } + membershipGroup := keystore.MembershipGroup{ TreeIndex: membershipIndex, MembershipContract: keystore.MembershipContract{ - ChainId: fmt.Sprintf("0x%X", chainID.Int64()), + ChainID: fmt.Sprintf("0x%X", chainID.Int64()), Address: options.MembershipContractAddress.String(), }, } - membershipGroupIndex, err := keystore.AddMembershipCredentials(options.CredentialsPath, identityCredential, membershipGroup, options.CredentialsPassword, dynamic.RLNAppInfo, keystore.DefaultSeparator) + membershipGroupIndex, err := appKeystore.AddMembershipCredentials(identityCredential, membershipGroup, options.CredentialsPassword) if err != nil { return fmt.Errorf("failed to persist credentials: %w", err) } diff --git a/cmd/waku/rlngenerate/flags.go b/cmd/waku/rlngenerate/flags.go index 460e9a72..9301c024 100644 --- a/cmd/waku/rlngenerate/flags.go +++ b/cmd/waku/rlngenerate/flags.go @@ -13,12 +13,12 @@ var flags = []cli.Flag{ &cli.PathFlag{ Name: "cred-path", Usage: "RLN relay membership credentials file", - Value: keystore.RLN_CREDENTIALS_FILENAME, + Value: keystore.DefaultCredentialsFilename, Destination: &options.CredentialsPath, }, &cli.StringFlag{ Name: "cred-password", - Value: keystore.RLN_CREDENTIALS_PASSWORD, + Value: keystore.DefaultCredentialsPassword, Usage: "Password for encrypting RLN credentials", Destination: &options.CredentialsPassword, }, diff --git a/waku/v2/node/wakunode2_rln.go b/waku/v2/node/wakunode2_rln.go index 568c9751..0b34334b 100644 --- a/waku/v2/node/wakunode2_rln.go +++ b/waku/v2/node/wakunode2_rln.go @@ -12,6 +12,7 @@ import ( "github.com/waku-org/go-waku/waku/v2/protocol/rln" "github.com/waku-org/go-waku/waku/v2/protocol/rln/group_manager/dynamic" "github.com/waku-org/go-waku/waku/v2/protocol/rln/group_manager/static" + "github.com/waku-org/go-waku/waku/v2/protocol/rln/keystore" r "github.com/waku-org/go-zerokit-rln/rln" ) @@ -44,14 +45,18 @@ func (w *WakuNode) setupRLNRelay() error { } else { w.log.Info("setting up waku-rln-relay in on-chain mode") + appKeystore, err := keystore.New(w.opts.keystorePath, dynamic.RLNAppInfo, w.log) + if err != nil { + return err + } + groupManager, err = dynamic.NewDynamicGroupManager( w.opts.rlnETHClientAddress, w.opts.rlnMembershipContractAddress, w.opts.rlnRelayMemIndex, - w.opts.keystorePath, + appKeystore, w.opts.keystorePassword, w.opts.keystoreIndex, - true, w.opts.prometheusReg, w.log, ) diff --git a/waku/v2/protocol/rln/group_manager/dynamic/dynamic.go b/waku/v2/protocol/rln/group_manager/dynamic/dynamic.go index 2046fc93..86ca665e 100644 --- a/waku/v2/protocol/rln/group_manager/dynamic/dynamic.go +++ b/waku/v2/protocol/rln/group_manager/dynamic/dynamic.go @@ -51,8 +51,7 @@ type DynamicGroupManager struct { chainId *big.Int rlnContract *contracts.RLN - saveKeystore bool - keystorePath string + appKeystore *keystore.AppKeystore keystorePassword string keystoreIndex uint @@ -122,35 +121,21 @@ func NewDynamicGroupManager( ethClientAddr string, memContractAddr common.Address, membershipGroupIndex uint, - keystorePath string, + appKeystore *keystore.AppKeystore, keystorePassword string, keystoreIndex uint, - saveKeystore bool, reg prometheus.Registerer, log *zap.Logger, ) (*DynamicGroupManager, error) { log = log.Named("rln-dynamic") - path := keystorePath - if path == "" { - log.Warn("keystore: no credentials path set, using default path", zap.String("path", keystore.RLN_CREDENTIALS_FILENAME)) - path = keystore.RLN_CREDENTIALS_FILENAME - } - - password := keystorePassword - if password == "" { - log.Warn("keystore: no credentials password set, using default password", zap.String("password", keystore.RLN_CREDENTIALS_PASSWORD)) - password = keystore.RLN_CREDENTIALS_PASSWORD - } - return &DynamicGroupManager{ membershipGroupIndex: membershipGroupIndex, membershipContractAddress: memContractAddr, ethClientAddress: ethClientAddr, eventHandler: handler, - saveKeystore: saveKeystore, - keystorePath: path, - keystorePassword: password, + appKeystore: appKeystore, + keystorePassword: keystorePassword, keystoreIndex: keystoreIndex, log: log, metrics: newMetrics(reg), @@ -196,39 +181,9 @@ func (gm *DynamicGroupManager) Start(ctx context.Context, rlnInstance *rln.RLN, return err } - if gm.identityCredential == nil && gm.keystorePassword != "" && gm.keystorePath != "" { - start := time.Now() - credentials, err := keystore.GetMembershipCredentials(gm.log, - gm.keystorePath, - gm.keystorePassword, - RLNAppInfo, - nil, - []keystore.MembershipContract{{ - ChainId: fmt.Sprintf("0x%X", gm.chainId), - Address: gm.membershipContractAddress.Hex(), - }}) - if err != nil { - return err - } - gm.metrics.RecordMembershipCredentialsImportDuration(time.Since(start)) - - if len(credentials) != 0 { - if int(gm.keystoreIndex) <= len(credentials)-1 { - credential := credentials[gm.keystoreIndex] - gm.identityCredential = credential.IdentityCredential - if int(gm.membershipGroupIndex) <= len(credential.MembershipGroups)-1 { - gm.membershipIndex = &credential.MembershipGroups[gm.membershipGroupIndex].TreeIndex - } else { - return errors.New("invalid membership group index") - } - } else { - return errors.New("invalid keystore index") - } - } - } - - if gm.identityCredential == nil || gm.membershipIndex == nil { - return errors.New("no credentials available") + err = gm.loadCredential() + if err != nil { + return err } if err = gm.HandleGroupUpdates(ctx, gm.eventHandler); err != nil { @@ -238,6 +193,39 @@ func (gm *DynamicGroupManager) Start(ctx context.Context, rlnInstance *rln.RLN, return nil } +func (gm *DynamicGroupManager) loadCredential() error { + start := time.Now() + + credentials, err := gm.appKeystore.GetMembershipCredentials( + gm.keystorePassword, + nil, + []keystore.MembershipContract{{ + ChainID: fmt.Sprintf("0x%X", gm.chainId), + Address: gm.membershipContractAddress.Hex(), + }}) + if err != nil { + return err + } + gm.metrics.RecordMembershipCredentialsImportDuration(time.Since(start)) + + if len(credentials) == 0 { + return errors.New("no credentials available") + } + + if int(gm.keystoreIndex) > len(credentials)-1 { + return errors.New("invalid keystore index") + } + + if int(gm.membershipGroupIndex) > len(credentials[gm.keystoreIndex].MembershipGroups)-1 { + return errors.New("invalid membership group index") + } + + gm.identityCredential = credentials[gm.keystoreIndex].IdentityCredential + gm.membershipIndex = &credentials[gm.keystoreIndex].MembershipGroups[gm.membershipGroupIndex].TreeIndex + + return nil +} + func (gm *DynamicGroupManager) InsertMembers(toInsert *om.OrderedMap) error { for pair := toInsert.Oldest(); pair != nil; pair = pair.Next() { events := pair.Value.([]*contracts.RLNMemberRegistered) // TODO: should these be sortered by index? we assume all members arrive in order diff --git a/waku/v2/protocol/rln/keystore/keystore.go b/waku/v2/protocol/rln/keystore/keystore.go index 184bac12..031aa3b8 100644 --- a/waku/v2/protocol/rln/keystore/keystore.go +++ b/waku/v2/protocol/rln/keystore/keystore.go @@ -5,7 +5,6 @@ import ( "encoding/json" "errors" "fmt" - "io/ioutil" "os" "sort" @@ -14,130 +13,49 @@ import ( "go.uber.org/zap" ) -const RLN_CREDENTIALS_FILENAME = "rlnKeystore.json" -const RLN_CREDENTIALS_PASSWORD = "password" +// DefaultCredentialsFilename is the default filename for the rln credentials keystore +const DefaultCredentialsFilename = "rlnKeystore.json" -type MembershipContract struct { - ChainId string `json:"chainId"` - Address string `json:"address"` -} +// DefaultCredentialsPassword contains the default password used when no password is specified +const DefaultCredentialsPassword = "password" -type MembershipGroup struct { - MembershipContract MembershipContract `json:"membershipContract"` - TreeIndex rln.MembershipIndex `json:"treeIndex"` -} +// New creates a new instance of a rln credentials keystore +func New(keystorePath string, appInfo AppInfo, logger *zap.Logger) (*AppKeystore, error) { + logger = logger.Named("rln-keystore") -type MembershipCredentials struct { - IdentityCredential *rln.IdentityCredential `json:"identityCredential"` - MembershipGroups []MembershipGroup `json:"membershipGroups"` -} - -type AppInfo struct { - Application string `json:"application"` - AppIdentifier string `json:"appIdentifier"` - Version string `json:"version"` -} - -type AppKeystore struct { - Application string `json:"application"` - AppIdentifier string `json:"appIdentifier"` - Credentials []AppKeystoreCredential `json:"credentials"` - Version string `json:"version"` -} - -type AppKeystoreCredential struct { - Crypto keystore.CryptoJSON `json:"crypto"` -} - -const DefaultSeparator = "\n" - -func (m MembershipCredentials) Equals(other MembershipCredentials) bool { - if !rln.IdentityCredentialEquals(*m.IdentityCredential, *other.IdentityCredential) { - return false - } - - for _, x := range m.MembershipGroups { - found := false - for _, y := range other.MembershipGroups { - if x.Equals(y) { - found = true - break - } - } - if !found { - return false - } - } - - return true -} - -func (m MembershipGroup) Equals(other MembershipGroup) bool { - return m.MembershipContract.Equals(other.MembershipContract) && m.TreeIndex == other.TreeIndex -} - -func (m MembershipContract) Equals(other MembershipContract) bool { - return m.Address == other.Address && m.ChainId == other.ChainId -} - -func CreateAppKeystore(path string, appInfo AppInfo, separator string) error { - if separator == "" { - separator = DefaultSeparator - } - - keystore := AppKeystore{ - Application: appInfo.Application, - AppIdentifier: appInfo.AppIdentifier, - Version: appInfo.Version, - } - - b, err := json.Marshal(keystore) - if err != nil { - return err - } - - b = append(b, []byte(separator)...) - - buffer := new(bytes.Buffer) - - err = json.Compact(buffer, b) - if err != nil { - return err - } - - return ioutil.WriteFile(path, buffer.Bytes(), 0600) -} - -func LoadAppKeystore(path string, appInfo AppInfo, separator string) (AppKeystore, error) { - if separator == "" { - separator = DefaultSeparator + path := keystorePath + if path == "" { + logger.Warn("keystore: no credentials path set, using default path", zap.String("path", DefaultCredentialsFilename)) + path = DefaultCredentialsFilename } _, err := os.Stat(path) if err != nil { if os.IsNotExist(err) { // If no keystore exists at path we create a new empty one with passed keystore parameters - err = CreateAppKeystore(path, appInfo, separator) + err = createAppKeystore(path, appInfo, defaultSeparator) if err != nil { - return AppKeystore{}, err + return nil, err } } else { - return AppKeystore{}, err + return nil, err } } src, err := os.ReadFile(path) if err != nil { - return AppKeystore{}, err + return nil, err } - for _, keystoreBytes := range bytes.Split(src, []byte(separator)) { + for _, keystoreBytes := range bytes.Split(src, []byte(defaultSeparator)) { if len(keystoreBytes) == 0 { continue } - keystore := AppKeystore{} - err := json.Unmarshal(keystoreBytes, &keystore) + keystore := new(AppKeystore) + keystore.logger = logger + keystore.path = path + err := json.Unmarshal(keystoreBytes, keystore) if err != nil { continue } @@ -147,53 +65,15 @@ func LoadAppKeystore(path string, appInfo AppInfo, separator string) (AppKeystor } } - return AppKeystore{}, errors.New("no keystore found") + return nil, errors.New("no keystore found") } -func filterCredential(credential MembershipCredentials, filterIdentityCredentials []MembershipCredentials, filterMembershipContracts []MembershipContract) *MembershipCredentials { - if len(filterIdentityCredentials) != 0 { - found := false - for _, filterCreds := range filterIdentityCredentials { - if filterCreds.Equals(credential) { - found = true - } - } - if !found { - return nil - } - } - - if len(filterMembershipContracts) != 0 { - var membershipGroupsIntersection []MembershipGroup - for _, filterContract := range filterMembershipContracts { - for _, credentialGroups := range credential.MembershipGroups { - if filterContract.Equals(credentialGroups.MembershipContract) { - membershipGroupsIntersection = append(membershipGroupsIntersection, credentialGroups) - } - } - } - if len(membershipGroupsIntersection) != 0 { - // If we have a match on some groups, we return the credential with filtered groups - return &MembershipCredentials{ - IdentityCredential: credential.IdentityCredential, - MembershipGroups: membershipGroupsIntersection, - } - } else { - return nil - } - } - - // We hit this return only if - // - filterIdentityCredentials.len() == 0 and filterMembershipContracts.len() == 0 (no filter) - // - filterIdentityCredentials.len() != 0 and filterMembershipContracts.len() == 0 (filter only on identity credential) - // Indeed, filterMembershipContracts.len() != 0 will have its exclusive return based on all values of membershipGroupsIntersection.len() - return &credential -} - -func GetMembershipCredentials(logger *zap.Logger, credentialsPath string, password string, appInfo AppInfo, filterIdentityCredentials []MembershipCredentials, filterMembershipContracts []MembershipContract) ([]MembershipCredentials, error) { - k, err := LoadAppKeystore(credentialsPath, appInfo, DefaultSeparator) - if err != nil { - return nil, err +// GetMembershipCredentials decrypts and retrieves membership credentials from the keystore applying filters +func (k *AppKeystore) GetMembershipCredentials(keystorePassword string, filterIdentityCredentials []MembershipCredentials, filterMembershipContracts []MembershipContract) ([]MembershipCredentials, error) { + password := keystorePassword + if password == "" { + k.logger.Warn("keystore: no credentials password set, using default password", zap.String("password", DefaultCredentialsPassword)) + password = DefaultCredentialsPassword } var result []MembershipCredentials @@ -220,12 +100,7 @@ func GetMembershipCredentials(logger *zap.Logger, credentialsPath string, passwo } // AddMembershipCredentials inserts a membership credential to the keystore matching the application, appIdentifier and version filters. -func AddMembershipCredentials(path string, newIdentityCredential *rln.IdentityCredential, newMembershipGroup MembershipGroup, password string, appInfo AppInfo, separator string) (membershipGroupIndex uint, err error) { - k, err := LoadAppKeystore(path, appInfo, DefaultSeparator) - if err != nil { - return 0, err - } - +func (k *AppKeystore) AddMembershipCredentials(newIdentityCredential *rln.IdentityCredential, newMembershipGroup MembershipGroup, password string) (membershipGroupIndex uint, err error) { // A flag to tell us if the keystore contains a credential associated to the input identity credential, i.e. membershipCredential found := false for i, existingCredentials := range k.Credentials { @@ -275,7 +150,7 @@ func AddMembershipCredentials(path string, newIdentityCredential *rln.IdentityCr } // we update the original credential field in keystoreCredentials - k.Credentials[i] = AppKeystoreCredential{Crypto: encryptedCredentials} + k.Credentials[i] = appKeystoreCredential{Crypto: encryptedCredentials} found = true @@ -309,28 +184,23 @@ func AddMembershipCredentials(path string, newIdentityCredential *rln.IdentityCr return 0, err } - k.Credentials = append(k.Credentials, AppKeystoreCredential{Crypto: encryptedCredentials}) + k.Credentials = append(k.Credentials, appKeystoreCredential{Crypto: encryptedCredentials}) membershipGroupIndex = uint(len(newCredential.MembershipGroups) - 1) } - return membershipGroupIndex, save(k, path, separator) + return membershipGroupIndex, save(k, k.path) } -// Safely saves a Keystore's JsonNode to disk. -// If exists, the destination file is renamed with extension .bkp; the file is written at its destination and the .bkp file is removed if write is successful, otherwise is restored -func save(keystore AppKeystore, path string, separator string) error { - // We first backup the current keystore - _, err := os.Stat(path) - if err == nil { - err := os.Rename(path, path+".bkp") - if err != nil { - return err - } +func createAppKeystore(path string, appInfo AppInfo, separator string) error { + if separator == "" { + separator = defaultSeparator } - if separator == "" { - separator = DefaultSeparator + keystore := AppKeystore{ + Application: appInfo.Application, + AppIdentifier: appInfo.AppIdentifier, + Version: appInfo.Version, } b, err := json.Marshal(keystore) @@ -342,6 +212,75 @@ func save(keystore AppKeystore, path string, separator string) error { buffer := new(bytes.Buffer) + err = json.Compact(buffer, b) + if err != nil { + return err + } + + return os.WriteFile(path, buffer.Bytes(), 0600) +} + +func filterCredential(credential MembershipCredentials, filterIdentityCredentials []MembershipCredentials, filterMembershipContracts []MembershipContract) *MembershipCredentials { + if len(filterIdentityCredentials) != 0 { + found := false + for _, filterCreds := range filterIdentityCredentials { + if filterCreds.Equals(credential) { + found = true + } + } + if !found { + return nil + } + } + + if len(filterMembershipContracts) != 0 { + var membershipGroupsIntersection []MembershipGroup + for _, filterContract := range filterMembershipContracts { + for _, credentialGroups := range credential.MembershipGroups { + if filterContract.Equals(credentialGroups.MembershipContract) { + membershipGroupsIntersection = append(membershipGroupsIntersection, credentialGroups) + } + } + } + if len(membershipGroupsIntersection) != 0 { + // If we have a match on some groups, we return the credential with filtered groups + return &MembershipCredentials{ + IdentityCredential: credential.IdentityCredential, + MembershipGroups: membershipGroupsIntersection, + } + } else { + return nil + } + } + + // We hit this return only if + // - filterIdentityCredentials.len() == 0 and filterMembershipContracts.len() == 0 (no filter) + // - filterIdentityCredentials.len() != 0 and filterMembershipContracts.len() == 0 (filter only on identity credential) + // Indeed, filterMembershipContracts.len() != 0 will have its exclusive return based on all values of membershipGroupsIntersection.len() + return &credential +} + +// Safely saves a Keystore's JsonNode to disk. +// If exists, the destination file is renamed with extension .bkp; the file is written at its destination and the .bkp file is removed if write is successful, otherwise is restored +func save(keystore *AppKeystore, path string) error { + // We first backup the current keystore + _, err := os.Stat(path) + if err == nil { + err := os.Rename(path, path+".bkp") + if err != nil { + return err + } + } + + b, err := json.Marshal(keystore) + if err != nil { + return err + } + + b = append(b, []byte(defaultSeparator)...) + + buffer := new(bytes.Buffer) + err = json.Compact(buffer, b) if err != nil { restoreErr := os.Rename(path, path+".bkp") @@ -351,7 +290,7 @@ func save(keystore AppKeystore, path string, separator string) error { return err } - err = ioutil.WriteFile(path, buffer.Bytes(), 0600) + err = os.WriteFile(path, buffer.Bytes(), 0600) if err != nil { restoreErr := os.Rename(path, path+".bkp") if restoreErr != nil { diff --git a/waku/v2/protocol/rln/keystore/types.go b/waku/v2/protocol/rln/keystore/types.go new file mode 100644 index 00000000..8ff0184a --- /dev/null +++ b/waku/v2/protocol/rln/keystore/types.go @@ -0,0 +1,81 @@ +package keystore + +import ( + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/waku-org/go-zerokit-rln/rln" + "go.uber.org/zap" +) + +// MembershipContract contains information about a membership smart contract address and the chain in which it is deployed +type MembershipContract struct { + ChainID string `json:"chainId"` + Address string `json:"address"` +} + +// Equals is used to compare MembershipContract +func (m MembershipContract) Equals(other MembershipContract) bool { + return m.Address == other.Address && m.ChainID == other.ChainID +} + +// MembershipGroup contains information about the index in which a credential is stored in the merkle tree and the contract associated to this credential +type MembershipGroup struct { + MembershipContract MembershipContract `json:"membershipContract"` + TreeIndex rln.MembershipIndex `json:"treeIndex"` +} + +// Equals is used to compare MembershipGroup +func (m MembershipGroup) Equals(other MembershipGroup) bool { + return m.MembershipContract.Equals(other.MembershipContract) && m.TreeIndex == other.TreeIndex +} + +// MembershipCredentials contains all the information about an RLN Identity Credential and membership group it belongs to +type MembershipCredentials struct { + IdentityCredential *rln.IdentityCredential `json:"identityCredential"` + MembershipGroups []MembershipGroup `json:"membershipGroups"` +} + +// Equals is used to compare MembershipCredentials +func (m MembershipCredentials) Equals(other MembershipCredentials) bool { + if !rln.IdentityCredentialEquals(*m.IdentityCredential, *other.IdentityCredential) { + return false + } + + for _, x := range m.MembershipGroups { + found := false + for _, y := range other.MembershipGroups { + if x.Equals(y) { + found = true + break + } + } + if !found { + return false + } + } + + return true +} + +// AppInfo is a helper structure that contains information about the application that uses these credentials +type AppInfo struct { + Application string `json:"application"` + AppIdentifier string `json:"appIdentifier"` + Version string `json:"version"` +} + +// AppKeystore represents the membership credentials to be used in RLN +type AppKeystore struct { + Application string `json:"application"` + AppIdentifier string `json:"appIdentifier"` + Credentials []appKeystoreCredential `json:"credentials"` + Version string `json:"version"` + + path string + logger *zap.Logger +} + +type appKeystoreCredential struct { + Crypto keystore.CryptoJSON `json:"crypto"` +} + +const defaultSeparator = "\n"