feat: create `generate-rln-credentials` subcommand

This commit is contained in:
Richard Ramos 2023-08-18 17:24:04 -04:00 committed by richΛrd
parent 8cc92dfdef
commit f088e49075
8 changed files with 479 additions and 74 deletions

View File

@ -6,6 +6,7 @@ import (
cli "github.com/urfave/cli/v2" cli "github.com/urfave/cli/v2"
"github.com/urfave/cli/v2/altsrc" "github.com/urfave/cli/v2/altsrc"
"github.com/waku-org/go-waku/cmd/waku/keygen" "github.com/waku-org/go-waku/cmd/waku/keygen"
"github.com/waku-org/go-waku/cmd/waku/rlngenerate"
"github.com/waku-org/go-waku/waku/v2/node" "github.com/waku-org/go-waku/waku/v2/node"
) )
@ -114,6 +115,7 @@ func main() {
}, },
Commands: []*cli.Command{ Commands: []*cli.Command{
&keygen.Command, &keygen.Command,
&rlngenerate.Command,
}, },
} }

View File

@ -0,0 +1,19 @@
//go:build !gowaku_rln
// +build !gowaku_rln
package rlngenerate
import (
"errors"
cli "github.com/urfave/cli/v2"
)
// Command generates a key file used to generate the node's peerID, encrypted with an optional password
var Command = cli.Command{
Name: "generate-rln-credentials",
Usage: "Generate credentials for usage with RLN",
Action: func(cCtx *cli.Context) error {
return errors.New("not available. Execute `make RLN=true` to add RLN support to go-waku")
},
}

View File

@ -0,0 +1,141 @@
//go:build gowaku_rln
// +build gowaku_rln
package rlngenerate
import (
"context"
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/ethclient"
cli "github.com/urfave/cli/v2"
"github.com/waku-org/go-waku/logging"
"github.com/waku-org/go-waku/waku/v2/protocol/rln/contracts"
"github.com/waku-org/go-waku/waku/v2/protocol/rln/group_manager/dynamic"
"github.com/waku-org/go-waku/waku/v2/protocol/rln/keystore"
"github.com/waku-org/go-waku/waku/v2/utils"
"github.com/waku-org/go-zerokit-rln/rln"
"go.uber.org/zap"
)
var options Options
var logger = utils.Logger().Named("rln-credentials")
// Command generates a key file used to generate the node's peerID, encrypted with an optional password
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 {
logger.Error("validating option flags", zap.Error(err))
return cli.Exit(err, 1)
}
err = execute(context.Background())
if err != nil {
logger.Error("registering RLN credentials", zap.Error(err))
return cli.Exit(err, 1)
}
return nil
},
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 {
return err
}
rlnInstance, err := rln.NewRLN()
if err != nil {
return err
}
chainID, err := ethClient.ChainID(ctx)
if err != nil {
return err
}
rlnContract, err := contracts.NewRLN(options.MembershipContractAddress, ethClient)
if err != nil {
return err
}
// prepare rln membership key pair
logger.Info("generating rln credential")
identityCredential, err := rlnInstance.MembershipKeyGen()
if err != nil {
return err
}
// register the rln-relay peer to the membership contract
membershipIndex, err := register(ctx, ethClient, rlnContract, identityCredential.IDCommitment, chainID)
if err != nil {
return err
}
// TODO: clean private key from memory
err = persistCredentials(identityCredential, membershipIndex, chainID)
if err != nil {
return err
}
if logger.Level() == zap.DebugLevel {
logger.Info("registered credentials into the membership contract",
logging.HexString("IDCommitment", identityCredential.IDCommitment[:]),
logging.HexString("IDNullifier", identityCredential.IDNullifier[:]),
logging.HexString("IDSecretHash", identityCredential.IDSecretHash[:]),
logging.HexString("IDTrapDoor", identityCredential.IDTrapdoor[:]),
zap.Uint("index", membershipIndex),
)
} else {
logger.Info("registered credentials into the membership contract", logging.HexString("idCommitment", identityCredential.IDCommitment[:]), zap.Uint("index", membershipIndex))
}
ethClient.Close()
return nil
}
func persistCredentials(identityCredential *rln.IdentityCredential, membershipIndex rln.MembershipIndex, chainID *big.Int) error {
membershipGroup := keystore.MembershipGroup{
TreeIndex: membershipIndex,
MembershipContract: keystore.MembershipContract{
ChainId: fmt.Sprintf("0x%X", chainID.Int64()),
Address: options.MembershipContractAddress.String(),
},
}
keystoreIndex, membershipGroupIndex, err := keystore.AddMembershipCredentials(options.CredentialsPath, identityCredential, membershipGroup, options.CredentialsPassword, dynamic.RLNAppInfo, keystore.DefaultSeparator)
if err != nil {
return fmt.Errorf("failed to persist credentials: %w", err)
}
logger.Info("persisted credentials succesfully", zap.Int("keystoreIndex", keystoreIndex), zap.Int("membershipGroupIndex", membershipGroupIndex))
return nil
}

View File

@ -0,0 +1,75 @@
//go:build gowaku_rln
// +build gowaku_rln
package rlngenerate
import (
cli "github.com/urfave/cli/v2"
wcli "github.com/waku-org/go-waku/waku/cliutils"
"github.com/waku-org/go-waku/waku/v2/protocol/rln/keystore"
)
var flags = []cli.Flag{
&cli.PathFlag{
Name: "cred-path",
Usage: "RLN relay membership credentials file",
Value: keystore.RLN_CREDENTIALS_FILENAME,
Destination: &options.CredentialsPath,
},
&cli.StringFlag{
Name: "cred-password",
Value: keystore.RLN_CREDENTIALS_PASSWORD,
Usage: "Password for encrypting RLN credentials",
Destination: &options.CredentialsPassword,
},
&cli.GenericFlag{
Name: "eth-account-private-key",
Usage: "Ethereum account private key used for registering in member contract",
Value: &wcli.PrivateKeyValue{
Value: &options.ETHPrivateKey,
},
},
&cli.StringFlag{
Name: "eth-client-address",
Usage: "Ethereum testnet client address",
Value: "ws://localhost:8545",
Destination: &options.ETHClientAddress,
},
&cli.GenericFlag{
Name: "eth-contract-address",
Usage: "Address of membership contract",
Value: &wcli.AddressValue{
Value: &options.MembershipContractAddress,
},
},
&cli.StringFlag{
Name: "eth-nonce",
Value: "",
Usage: "Set an specific ETH transaction nonce. Leave empty to calculate the nonce automatically",
Destination: &options.ETHNonce,
},
&cli.Uint64Flag{
Name: "eth-gas-limit",
Value: 0,
Usage: "Gas limit to set for the transaction execution (0 = estimate)",
Destination: &options.ETHGasLimit,
},
&cli.StringFlag{
Name: "eth-gas-price",
Value: "",
Usage: "Gas price in wei to use for the transaction execution (empty = gas price oracle)",
Destination: &options.ETHGasPrice,
},
&cli.StringFlag{
Name: "eth-gas-fee-cap",
Value: "",
Usage: "Gas fee cap in wei to use for the 1559 transaction execution (empty = gas price oracle)",
Destination: &options.ETHGasFeeCap,
},
&cli.StringFlag{
Name: "eth-gas-tip-cap",
Value: "",
Usage: "Gas priority fee cap in wei to use for the 1559 transaction execution (empty = gas price oracle)",
Destination: &options.ETHGasTipCap,
},
}

View File

@ -0,0 +1,21 @@
package rlngenerate
import (
"crypto/ecdsa"
"github.com/ethereum/go-ethereum/common"
)
// Options are settings used to create RLN credentials.
type Options struct {
CredentialsPath string
CredentialsPassword string
ETHPrivateKey *ecdsa.PrivateKey
ETHClientAddress string
MembershipContractAddress common.Address
ETHGasLimit uint64
ETHNonce string
ETHGasPrice string
ETHGasFeeCap string
ETHGasTipCap string
}

View File

@ -0,0 +1,125 @@
//go:build gowaku_rln
// +build gowaku_rln
package rlngenerate
import (
"context"
"errors"
"fmt"
"math/big"
"github.com/ethereum/go-ethereum/accounts/abi/bind"
"github.com/ethereum/go-ethereum/core/types"
"github.com/ethereum/go-ethereum/ethclient"
"github.com/ethereum/go-ethereum/log"
"github.com/waku-org/go-waku/logging"
"github.com/waku-org/go-waku/waku/v2/protocol/rln/contracts"
"github.com/waku-org/go-zerokit-rln/rln"
"go.uber.org/zap"
)
func getMembershipFee(ctx context.Context, rlnContract *contracts.RLN) (*big.Int, error) {
return rlnContract.MEMBERSHIPDEPOSIT(&bind.CallOpts{Context: ctx})
}
func register(ctx context.Context, ethClient *ethclient.Client, rlnContract *contracts.RLN, idComm rln.IDCommitment, chainID *big.Int) (rln.MembershipIndex, error) {
// check if the contract exists by calling a static function
membershipFee, err := getMembershipFee(ctx, rlnContract)
if err != nil {
return 0, err
}
auth, err := bind.NewKeyedTransactorWithChainID(options.ETHPrivateKey, chainID)
if err != nil {
return 0, err
}
auth.Value = membershipFee
auth.Context = ctx
auth.GasLimit = options.ETHGasLimit
var ok bool
if options.ETHNonce != "" {
nonce := &big.Int{}
auth.Nonce, ok = nonce.SetString(options.ETHNonce, 10)
if !ok {
return 0, errors.New("invalid nonce value")
}
}
if options.ETHGasFeeCap != "" {
gasFeeCap := &big.Int{}
auth.GasFeeCap, ok = gasFeeCap.SetString(options.ETHGasFeeCap, 10)
if !ok {
return 0, errors.New("invalid gas fee cap value")
}
}
if options.ETHGasTipCap != "" {
gasTipCap := &big.Int{}
auth.GasTipCap, ok = gasTipCap.SetString(options.ETHGasTipCap, 10)
if !ok {
return 0, errors.New("invalid gas tip cap value")
}
}
if options.ETHGasPrice != "" {
gasPrice := &big.Int{}
auth.GasPrice, ok = gasPrice.SetString(options.ETHGasPrice, 10)
if !ok {
return 0, errors.New("invalid gas price value")
}
}
log.Debug("registering an id commitment", zap.Binary("idComm", idComm[:]))
// registers the idComm into the membership contract whose address is in rlnPeer.membershipContractAddress
tx, err := rlnContract.Register(auth, rln.Bytes32ToBigInt(idComm))
if err != nil {
return 0, fmt.Errorf("transaction error: %w", err)
}
url := ""
switch chainID.Int64() {
case 1:
url = "https://etherscan.io"
case 5:
url = "https://goerli.etherscan.io"
case 11155111:
url = "https://sepolia.etherscan.io"
}
if url != "" {
logger.Info(fmt.Sprintf("transaction broadcasted, find details of your registration transaction in %s/tx/%s", url, tx.Hash()))
} else {
logger.Info("transaction broadcasted.", zap.String("transactionHash", tx.Hash().String()))
}
logger.Warn("waiting for transaction to be mined...")
txReceipt, err := bind.WaitMined(ctx, ethClient, tx)
if err != nil {
return 0, fmt.Errorf("transaction error: %w", err)
}
if txReceipt.Status != types.ReceiptStatusSuccessful {
return 0, errors.New("transaction reverted")
}
// the receipt topic holds the hash of signature of the raised events
evt, err := rlnContract.ParseMemberRegistered(*txReceipt.Logs[0])
if err != nil {
return 0, err
}
var eventIDComm rln.IDCommitment = rln.BigIntToBytes32(evt.Pubkey)
log.Debug("information extracted from tx log", zap.Uint64("blockNumber", evt.Raw.BlockNumber), logging.HexString("idCommitment", eventIDComm[:]), zap.Uint64("index", evt.Index.Uint64()))
if eventIDComm != idComm {
return 0, errors.New("invalid id commitment key")
}
return rln.MembershipIndex(uint(evt.Index.Int64())), nil
}

View File

@ -215,7 +215,7 @@ func (gm *DynamicGroupManager) Start(ctx context.Context, rlnInstance *rln.RLN,
if len(credentials) != 0 { if len(credentials) != 0 {
if int(gm.keystoreIndex) <= len(credentials)-1 { if int(gm.keystoreIndex) <= len(credentials)-1 {
credential := credentials[gm.keystoreIndex] credential := credentials[gm.keystoreIndex]
gm.identityCredential = &credential.IdentityCredential gm.identityCredential = credential.IdentityCredential
if int(gm.membershipGroupIndex) <= len(credential.MembershipGroups)-1 { if int(gm.membershipGroupIndex) <= len(credential.MembershipGroups)-1 {
gm.membershipIndex = &credential.MembershipGroups[gm.membershipGroupIndex].TreeIndex gm.membershipIndex = &credential.MembershipGroups[gm.membershipGroupIndex].TreeIndex
} else { } else {
@ -275,18 +275,15 @@ func (gm *DynamicGroupManager) persistCredentials() error {
return errors.New("no credentials to persist") return errors.New("no credentials to persist")
} }
keystoreCred := keystore.MembershipCredentials{ membershipGroup := keystore.MembershipGroup{
IdentityCredential: *gm.identityCredential, TreeIndex: *gm.membershipIndex,
MembershipGroups: []keystore.MembershipGroup{{ MembershipContract: keystore.MembershipContract{
TreeIndex: *gm.membershipIndex, ChainId: fmt.Sprintf("0x%X", gm.chainId),
MembershipContract: keystore.MembershipContract{ Address: gm.membershipContractAddress.String(),
ChainId: fmt.Sprintf("0x%X", gm.chainId), },
Address: gm.membershipContractAddress.String(),
},
}},
} }
err := keystore.AddMembershipCredentials(gm.keystorePath, []keystore.MembershipCredentials{keystoreCred}, gm.keystorePassword, RLNAppInfo, keystore.DefaultSeparator) _, _, err := keystore.AddMembershipCredentials(gm.keystorePath, gm.identityCredential, membershipGroup, gm.keystorePassword, RLNAppInfo, keystore.DefaultSeparator)
if err != nil { if err != nil {
return fmt.Errorf("failed to persist credentials: %w", err) return fmt.Errorf("failed to persist credentials: %w", err)
} }

View File

@ -7,6 +7,7 @@ import (
"fmt" "fmt"
"io/ioutil" "io/ioutil"
"os" "os"
"sort"
"github.com/ethereum/go-ethereum/accounts/keystore" "github.com/ethereum/go-ethereum/accounts/keystore"
"github.com/waku-org/go-zerokit-rln/rln" "github.com/waku-org/go-zerokit-rln/rln"
@ -27,8 +28,8 @@ type MembershipGroup struct {
} }
type MembershipCredentials struct { type MembershipCredentials struct {
IdentityCredential rln.IdentityCredential `json:"identityCredential"` IdentityCredential *rln.IdentityCredential `json:"identityCredential"`
MembershipGroups []MembershipGroup `json:"membershipGroups"` MembershipGroups []MembershipGroup `json:"membershipGroups"`
} }
type AppInfo struct { type AppInfo struct {
@ -51,7 +52,7 @@ type AppKeystoreCredential struct {
const DefaultSeparator = "\n" const DefaultSeparator = "\n"
func (m MembershipCredentials) Equals(other MembershipCredentials) bool { func (m MembershipCredentials) Equals(other MembershipCredentials) bool {
if !rln.IdentityCredentialEquals(m.IdentityCredential, other.IdentityCredential) { if !rln.IdentityCredentialEquals(*m.IdentityCredential, *other.IdentityCredential) {
return false return false
} }
@ -218,80 +219,104 @@ func GetMembershipCredentials(logger *zap.Logger, credentialsPath string, passwo
return result, nil return result, nil
} }
// Adds a sequence of membership credential to the keystore matching the application, appIdentifier and version filters. // Adds a membership credential to the keystore matching the application, appIdentifier and version filters.
func AddMembershipCredentials(path string, credentials []MembershipCredentials, password string, appInfo AppInfo, separator string) error { func AddMembershipCredentials(path string, newIdentityCredential *rln.IdentityCredential, newMembershipGroup MembershipGroup, password string, appInfo AppInfo, separator string) (keystoreIndex int, membershipGroupIndex int, err error) {
k, err := LoadAppKeystore(path, appInfo, DefaultSeparator) k, err := LoadAppKeystore(path, appInfo, DefaultSeparator)
if err != nil { if err != nil {
return err return 0, 0, err
} }
var credentialsToAdd []MembershipCredentials // A flag to tell us if the keystore contains a credential associated to the input identity credential, i.e. membershipCredential
for _, newCredential := range credentials { found := false
// A flag to tell us if the keystore contains a credential associated to the input identity credential, i.e. membershipCredential for i, existingCredentials := range k.Credentials {
found := -1 credentialsBytes, err := keystore.DecryptDataV3(existingCredentials.Crypto, password)
for i, existingCredentials := range k.Credentials {
credentialsBytes, err := keystore.DecryptDataV3(existingCredentials.Crypto, password)
if err != nil {
continue
}
var credentials MembershipCredentials
err = json.Unmarshal(credentialsBytes, &credentials)
if err != nil {
continue
}
if rln.IdentityCredentialEquals(credentials.IdentityCredential, newCredential.IdentityCredential) {
// idCredential is present in keystore. We add the input credential membership group to the one contained in the decrypted keystore credential (we deduplicate groups using sets)
allMemberships := append(credentials.MembershipGroups, newCredential.MembershipGroups...)
// we define the updated credential with the updated membership sets
updatedCredential := MembershipCredentials{
IdentityCredential: newCredential.IdentityCredential,
MembershipGroups: allMemberships,
}
// we re-encrypt creating a new keyfile
b, err := json.Marshal(updatedCredential)
if err != nil {
return err
}
encryptedCredentials, err := keystore.EncryptDataV3(b, []byte(password), keystore.StandardScryptN, keystore.StandardScryptP)
if err != nil {
return err
}
// we update the original credential field in keystoreCredentials
k.Credentials[i] = AppKeystoreCredential{Crypto: encryptedCredentials}
found = i
// We stop decrypting other credentials in the keystore
break
}
}
if found == -1 {
credentialsToAdd = append(credentialsToAdd, newCredential)
}
}
for _, c := range credentialsToAdd {
b, err := json.Marshal(c)
if err != nil { if err != nil {
return err continue
}
var credentials MembershipCredentials
err = json.Unmarshal(credentialsBytes, &credentials)
if err != nil {
continue
}
if rln.IdentityCredentialEquals(*credentials.IdentityCredential, *newIdentityCredential) {
// idCredential is present in keystore. We add the input credential membership group to the one contained in the decrypted keystore credential (we deduplicate groups using sets)
allMembershipsMap := make(map[MembershipGroup]struct{})
for _, m := range credentials.MembershipGroups {
allMembershipsMap[m] = struct{}{}
}
allMembershipsMap[newMembershipGroup] = struct{}{}
// We sort membership groups, otherwise we will not have deterministic results in tests
var allMemberships []MembershipGroup
for k := range allMembershipsMap {
allMemberships = append(allMemberships, k)
}
sort.Slice(allMemberships, func(i, j int) bool {
return allMemberships[i].MembershipContract.Address < allMemberships[j].MembershipContract.Address
})
// we define the updated credential with the updated membership sets
updatedCredential := MembershipCredentials{
IdentityCredential: newIdentityCredential,
MembershipGroups: allMemberships,
}
// we re-encrypt creating a new keyfile
b, err := json.Marshal(updatedCredential)
if err != nil {
return 0, 0, err
}
encryptedCredentials, err := keystore.EncryptDataV3(b, []byte(password), keystore.StandardScryptN, keystore.StandardScryptP)
if err != nil {
return 0, 0, err
}
// we update the original credential field in keystoreCredentials
k.Credentials[i] = AppKeystoreCredential{Crypto: encryptedCredentials}
found = true
// We setup the return values
membershipGroupIndex = len(allMemberships)
keystoreIndex = i
for mIdx, mg := range updatedCredential.MembershipGroups {
if mg.MembershipContract.Equals(newMembershipGroup.MembershipContract) {
membershipGroupIndex = mIdx
break
}
}
// We stop decrypting other credentials in the keystore
break
}
}
if !found { // Not found
newCredential := MembershipCredentials{
IdentityCredential: newIdentityCredential,
MembershipGroups: []MembershipGroup{newMembershipGroup},
}
b, err := json.Marshal(newCredential)
if err != nil {
return 0, 0, err
} }
encryptedCredentials, err := keystore.EncryptDataV3(b, []byte(password), keystore.StandardScryptN, keystore.StandardScryptP) encryptedCredentials, err := keystore.EncryptDataV3(b, []byte(password), keystore.StandardScryptN, keystore.StandardScryptP)
if err != nil { if err != nil {
return err return 0, 0, err
} }
k.Credentials = append(k.Credentials, AppKeystoreCredential{Crypto: encryptedCredentials}) k.Credentials = append(k.Credentials, AppKeystoreCredential{Crypto: encryptedCredentials})
keystoreIndex = len(k.Credentials) - 1
membershipGroupIndex = len(newCredential.MembershipGroups) - 1
} }
return save(k, path, separator) return keystoreIndex, membershipGroupIndex, save(k, path, separator)
} }
// Safely saves a Keystore's JsonNode to disk. // Safely saves a Keystore's JsonNode to disk.