diff --git a/.gitignore b/.gitignore index b407c63b..28f218ba 100644 --- a/.gitignore +++ b/.gitignore @@ -2,6 +2,7 @@ *.db-shm *.db-wal nodekey +rlnCredentials.txt # Binaries for programs and plugins *.exe diff --git a/examples/chat2/main.go b/examples/chat2/main.go index 8c64d664..06b56679 100644 --- a/examples/chat2/main.go +++ b/examples/chat2/main.go @@ -79,6 +79,7 @@ func main() { rlnRelayEthAccountPrivKeyFlag := flag.String("eth-account-privatekey", "", "Account private key for an Ethereum testnet") rlnRelayEthClientAddressFlag := flag.String("eth-client-address", "ws://localhost:8545/", "Ethereum testnet client address") rlnRelayEthMemContractAddressFlag := flag.String("eth-mem-contract-address", "", "Address of membership contract on an Ethereum testnet") + rlnRelayCredentialsFile := flag.String("rln-relay-credentials-file", "rlnCredentials.txt", "RLN credentials file") flag.Parse() @@ -127,23 +128,16 @@ func main() { panic(err) } - var idKey *rln.IDKey - if *rlnRelayIdKeyFlag != "" { - idKey = new(rln.IDKey) - copy((*idKey)[:], common.FromHex(*rlnRelayIdKeyFlag)) - } - - var idCommitment *rln.IDCommitment - if *rlnRelayIdCommitmentKeyFlag != "" { - idCommitment = new(rln.IDCommitment) - copy((*idCommitment)[:], common.FromHex(*rlnRelayIdCommitmentKeyFlag)) + idKey, idCommitment, index, err := getMembershipCredentials(*rlnRelayCredentialsFile, *rlnRelayIdKeyFlag, *rlnRelayIdCommitmentKeyFlag, *rlnRelayMemIndexFlag) + if err != nil { + panic(err) } fmt.Println("Setting up dynamic rln") opts = append(opts, node.WithDynamicRLNRelay( *rlnRelayPubsubTopicFlag, *rlnRelayContentTopicFlag, - rln.MembershipIndex(*rlnRelayMemIndexFlag), + index, idKey, idCommitment, spamHandler, @@ -183,6 +177,14 @@ func main() { panic(err) } + if *rlnRelayFlag && *rlnRelayDynamicFlag { + err := writeRLNMembershipCredentialsToFile(*rlnRelayCredentialsFile, wakuNode.RLNRelay().MembershipKeyPair(), wakuNode.RLNRelay().MembershipIndex()) + if err != nil { + panic(err) + } + fmt.Printf("Wrote credentials in file %s\n", *rlnRelayCredentialsFile) + } + // use the nickname from the cli flag, or a default if blank nick := *nickFlag if len(nick) == 0 { diff --git a/examples/chat2/rln-credentials.go b/examples/chat2/rln-credentials.go new file mode 100644 index 00000000..6790974a --- /dev/null +++ b/examples/chat2/rln-credentials.go @@ -0,0 +1,86 @@ +package main + +import ( + "encoding/json" + "errors" + "fmt" + "io/ioutil" + "os" + + "github.com/ethereum/go-ethereum/common" + "github.com/status-im/go-rln/rln" +) + +type membershipCredentials struct { + Keypair rln.MembershipKeyPair `json:"keypair"` + Index rln.MembershipIndex `json:"index"` +} + +func fileExists(path string) bool { + if _, err := os.Stat(path); err == nil { + return false + + } else if errors.Is(err, os.ErrNotExist) { + return false + + } else { + return false + } +} + +func writeRLNMembershipCredentialsToFile(path string, keyPair rln.MembershipKeyPair, idx rln.MembershipIndex) error { + if fileExists(path) { + return nil + } + + credentialsJSON, err := json.Marshal(membershipCredentials{ + Keypair: keyPair, + Index: idx, + }) + if err != nil { + return err + } + + return ioutil.WriteFile(path, credentialsJSON, 0600) +} + +func loadMembershipCredentialsFromFile(path string) (rln.MembershipKeyPair, rln.MembershipIndex, error) { + src, err := ioutil.ReadFile(path) + if err != nil { + return rln.MembershipKeyPair{}, rln.MembershipIndex(0), err + } + + var credentials membershipCredentials + err = json.Unmarshal(src, &credentials) + if err != nil { + return rln.MembershipKeyPair{}, rln.MembershipIndex(0), err + } + + return credentials.Keypair, credentials.Index, err +} + +func getMembershipCredentials(path string, rlnIDKey string, rlnIDCommitment string, rlnMembershipIndex int) (idKey *rln.IDKey, idCommitment *rln.IDCommitment, index rln.MembershipIndex, err error) { + if _, err = os.Stat(path); err == nil { + if keyPair, index, err := loadMembershipCredentialsFromFile(path); err != nil { + return nil, nil, rln.MembershipIndex(0), fmt.Errorf("could not read membership credentials file: %w", err) + } else { + return &keyPair.IDKey, &keyPair.IDCommitment, index, nil + } + } + + if os.IsNotExist(err) { + if rlnIDKey != "" { + idKey = new(rln.IDKey) + copy((*idKey)[:], common.FromHex(rlnIDKey)) + } + + if rlnIDCommitment != "" { + idCommitment = new(rln.IDCommitment) + copy((*idCommitment)[:], common.FromHex(rlnIDCommitment)) + } + + return idKey, idCommitment, rln.MembershipIndex(rlnMembershipIndex), nil + } + + return nil, nil, rln.MembershipIndex(0), fmt.Errorf("could not read membership credentials file: %w", err) +} diff --git a/waku.go b/waku.go index ab10ab30..b2587503 100644 --- a/waku.go +++ b/waku.go @@ -317,6 +317,12 @@ func main() { Usage: "Rln relay identity commitment key as a Hex string", Destination: &options.RLNRelay.IDCommitment, }, + &cli.StringFlag{ + Name: "rln-relay-membership-credentials-file", + Usage: "RLN relay membership credentials file", + Value: "rlnCredentials.txt", + Destination: &options.RLNRelay.CredentialsFile, + }, // TODO: this is a good candidate option for subcommands // TODO: consider accepting a private key file and passwd &cli.StringFlag{ diff --git a/waku/node.go b/waku/node.go index df7d6024..bdb1a093 100644 --- a/waku/node.go +++ b/waku/node.go @@ -256,6 +256,7 @@ func Execute(options Options) { nodeOpts = append(nodeOpts, node.WithDiscoveryV5(options.DiscV5.Port, bootnodes, options.DiscV5.AutoUpdate, pubsub.WithDiscoveryOpts(discovery.Limit(45), discovery.TTL(time.Duration(20)*time.Second)))) } + loadedCredentialsFromFile := false if options.RLNRelay.Enable { if !options.Relay.Enable { failOnErr(errors.New("relay not available"), "Could not enable RLN Relay") @@ -272,22 +273,15 @@ func Execute(options Options) { ethPrivKey = k } - var idKey *rln.IDKey - if options.RLNRelay.IDCommitment != "" { - idKey = new(rln.IDKey) - copy((*idKey)[:], common.FromHex(options.RLNRelay.IDKey)) - } + loaded, idKey, idCommitment, membershipIndex, err := getMembershipCredentials(options) + failOnErr(err, "Invalid membership credentials") - var idCommitment *rln.IDCommitment - if options.RLNRelay.IDCommitment != "" { - idCommitment = new(rln.IDCommitment) - copy((*idCommitment)[:], common.FromHex(options.RLNRelay.IDCommitment)) - } + loadedCredentialsFromFile = loaded nodeOpts = append(nodeOpts, node.WithDynamicRLNRelay( options.RLNRelay.PubsubTopic, options.RLNRelay.ContentTopic, - rln.MembershipIndex(options.RLNRelay.MembershipIndex), + membershipIndex, idKey, idCommitment, nil, @@ -354,6 +348,11 @@ func Execute(options Options) { } } + if options.RLNRelay.Enable && options.RLNRelay.Dynamic && !loadedCredentialsFromFile { + err := writeRLNMembershipCredentialsToFile(wakuNode.RLNRelay().MembershipKeyPair(), wakuNode.RLNRelay().MembershipIndex(), options.RLNRelay.CredentialsFile, []byte(options.KeyPasswd), options.Overwrite) + failOnErr(err, "Could not write membership credentials file") + } + var rpcServer *rpc.WakuRpc if options.RPCServer.Enable { rpcServer = rpc.NewWakuRpc(wakuNode, options.RPCServer.Address, options.RPCServer.Port, options.RPCServer.Admin, options.RPCServer.Private, logger) @@ -432,7 +431,7 @@ func loadPrivateKeyFromFile(path string, passwd string) (*ecdsa.PrivateKey, erro return crypto.ToECDSA(pKey) } -func checkForPrivateKeyFile(path string, overwrite bool) error { +func checkForFileExistence(path string, overwrite bool) error { _, err := os.Stat(path) if err == nil && !overwrite { @@ -456,7 +455,7 @@ func generatePrivateKey() ([]byte, error) { } func writePrivateKeyToFile(path string, passwd []byte, overwrite bool) error { - if err := checkForPrivateKeyFile(path, overwrite); err != nil { + if err := checkForFileExistence(path, overwrite); err != nil { return err } diff --git a/waku/options.go b/waku/options.go index cd2bbf79..bb26ddcf 100644 --- a/waku/options.go +++ b/waku/options.go @@ -41,6 +41,7 @@ type RelayOptions struct { type RLNRelayOptions struct { Enable bool + CredentialsFile string MembershipIndex int PubsubTopic string ContentTopic string diff --git a/waku/rln-credentials.go b/waku/rln-credentials.go new file mode 100644 index 00000000..0068fc29 --- /dev/null +++ b/waku/rln-credentials.go @@ -0,0 +1,96 @@ +package waku + +import ( + "encoding/json" + "fmt" + "io/ioutil" + "os" + + "github.com/ethereum/go-ethereum/accounts/keystore" + "github.com/ethereum/go-ethereum/common" + "github.com/status-im/go-rln/rln" +) + +type membershipCredentials struct { + Keypair rln.MembershipKeyPair `json:"keypair"` + Index rln.MembershipIndex `json:"index"` +} + +func writeRLNMembershipCredentialsToFile(keyPair rln.MembershipKeyPair, idx rln.MembershipIndex, path string, passwd []byte, overwrite bool) error { + if err := checkForFileExistence(path, overwrite); err != nil { + return err + } + + credentialsJSON, err := json.Marshal(membershipCredentials{ + Keypair: keyPair, + Index: idx, + }) + if err != nil { + return err + } + + encryptedCredentials, err := keystore.EncryptDataV3(credentialsJSON, passwd, keystore.StandardScryptN, keystore.StandardScryptP) + if err != nil { + return err + } + + output, err := json.Marshal(encryptedCredentials) + if err != nil { + return err + } + + return ioutil.WriteFile(path, output, 0600) +} + +func loadMembershipCredentialsFromFile(path string, passwd string) (rln.MembershipKeyPair, rln.MembershipIndex, error) { + src, err := ioutil.ReadFile(path) + if err != nil { + return rln.MembershipKeyPair{}, rln.MembershipIndex(0), err + } + + var encryptedK keystore.CryptoJSON + err = json.Unmarshal(src, &encryptedK) + if err != nil { + return rln.MembershipKeyPair{}, rln.MembershipIndex(0), err + } + + credentialsBytes, err := keystore.DecryptDataV3(encryptedK, passwd) + if err != nil { + return rln.MembershipKeyPair{}, rln.MembershipIndex(0), err + } + + var credentials membershipCredentials + err = json.Unmarshal(credentialsBytes, &credentials) + if err != nil { + return rln.MembershipKeyPair{}, rln.MembershipIndex(0), err + } + + return credentials.Keypair, credentials.Index, err +} + +func getMembershipCredentials(options Options) (fromFile bool, idKey *rln.IDKey, idCommitment *rln.IDCommitment, index rln.MembershipIndex, err error) { + if _, err = os.Stat(options.RLNRelay.CredentialsFile); err == nil { + if keyPair, index, err := loadMembershipCredentialsFromFile(options.RLNRelay.CredentialsFile, options.KeyPasswd); err != nil { + return false, nil, nil, rln.MembershipIndex(0), fmt.Errorf("could not read membership credentials file: %w", err) + } else { + return true, &keyPair.IDKey, &keyPair.IDCommitment, index, nil + } + } + + if os.IsNotExist(err) { + if options.RLNRelay.IDKey != "" { + idKey = new(rln.IDKey) + copy((*idKey)[:], common.FromHex(options.RLNRelay.IDKey)) + } + + if options.RLNRelay.IDCommitment != "" { + idCommitment = new(rln.IDCommitment) + copy((*idCommitment)[:], common.FromHex(options.RLNRelay.IDCommitment)) + } + + return false, idKey, idCommitment, rln.MembershipIndex(options.RLNRelay.MembershipIndex), nil + + } + + return false, nil, nil, rln.MembershipIndex(0), fmt.Errorf("could not read membership credentials file: %w", err) +} diff --git a/waku/v2/node/wakuoptions.go b/waku/v2/node/wakuoptions.go index 456b7755..63c74dcb 100644 --- a/waku/v2/node/wakuoptions.go +++ b/waku/v2/node/wakuoptions.go @@ -67,8 +67,6 @@ type WakuNodeParameters struct { shouldResume bool storeMsgs bool messageProvider store.MessageProvider - maxMessages int - maxDuration time.Duration swapMode int swapDisconnectThreshold int diff --git a/waku/v2/protocol/rln/web3.go b/waku/v2/protocol/rln/web3.go index 908ba443..ca42da78 100644 --- a/waku/v2/protocol/rln/web3.go +++ b/waku/v2/protocol/rln/web3.go @@ -101,9 +101,9 @@ func (rln *WakuRLNRelay) Register(ctx context.Context) (*r.MembershipIndex, erro // the types of inputs to this handler matches the MemberRegistered event/proc defined in the MembershipContract interface type RegistrationEventHandler = func(pubkey r.IDCommitment, index r.MembershipIndex) error -func processLogs(evt *contracts.RLNMemberRegistered, handler RegistrationEventHandler) { +func processLogs(evt *contracts.RLNMemberRegistered, handler RegistrationEventHandler) error { if evt == nil { - return + return nil } var pubkey r.IDCommitment @@ -111,7 +111,7 @@ func processLogs(evt *contracts.RLNMemberRegistered, handler RegistrationEventHa index := r.MembershipIndex(uint(evt.Index.Int64())) - handler(pubkey, index) + return handler(pubkey, index) } // HandleGroupUpdates mounts the supplied handler for the registration events emitting from the membership contract @@ -129,11 +129,15 @@ func (rln *WakuRLNRelay) HandleGroupUpdates(handler RegistrationEventHandler) er return err } - // TODO: process log should have a channel that consumes logs and has a buffer to receive a lot of events - // TODO: an error channel is required + err = rln.loadOldEvents(rlnContract, handler) + if err != nil { + return err + } - rln.loadOldEvents(rlnContract, handler) - rln.watchNewEvents(rlnContract, handler, rln.log) + err = rln.watchNewEvents(rlnContract, handler, rln.log) + if err != nil { + return err + } return nil }