featimprove start, types, import/export, add checks for netwrok, shortcuts for decoder encoder

This commit is contained in:
Sasha 2024-01-29 02:04:29 +01:00
parent bafbe01e52
commit aece0e3dc3
No known key found for this signature in database
7 changed files with 174 additions and 57 deletions

View File

@ -6,6 +6,7 @@ import {
} from "./constants.js";
import { createRLN } from "./create.js";
import { Keystore } from "./keystore/index.js";
import { extractMetaMaskSigner } from "./metamask.js";
import {
IdentityCredential,
Proof,
@ -29,4 +30,5 @@ export {
RLN_STORAGE_ABI,
RLN_REGISTRY_ABI,
SEPOLIA_CONTRACT,
extractMetaMaskSigner,
};

View File

@ -1,3 +1,5 @@
import { Keystore } from "./keystore.js";
import type { KeystoreEntity } from "./types.js";
export { Keystore };
export type { KeystoreEntity };

View File

@ -14,12 +14,12 @@ import _ from "lodash";
import { v4 as uuidV4 } from "uuid";
import { buildBigIntFromUint8Array } from "../byte_utils.js";
import type { IdentityCredential } from "../rln.js";
import { decryptEipKeystore, keccak256Checksum } from "./cipher.js";
import { isCredentialValid, isKeystoreValid } from "./schema_validator.js";
import type {
Keccak256Hash,
KeystoreEntity,
MembershipHash,
MembershipInfo,
Password,
@ -57,11 +57,6 @@ type KeystoreCreateOptions = {
appIdentifier?: string;
};
type IdentityOptions = {
identity: IdentityCredential;
membership: MembershipInfo;
};
export class Keystore {
private data: NwakuKeystore;
@ -105,7 +100,7 @@ export class Keystore {
}
public async addCredential(
options: IdentityOptions,
options: KeystoreEntity,
password: Password
): Promise<MembershipHash> {
const membershipHash: MembershipHash = Keystore.computeMembershipHash(
@ -138,7 +133,7 @@ export class Keystore {
public async readCredential(
membershipHash: MembershipHash,
password: Password
): Promise<null | IdentityOptions> {
): Promise<null | KeystoreEntity> {
const nwakuCredential = this.data.credentials[membershipHash];
if (!nwakuCredential) {
@ -235,9 +230,7 @@ export class Keystore {
};
}
private static fromBytesToIdentity(
bytes: Uint8Array
): null | IdentityOptions {
private static fromBytesToIdentity(bytes: Uint8Array): null | KeystoreEntity {
try {
const str = bytesToUtf8(bytes);
const obj = JSON.parse(str);
@ -302,7 +295,7 @@ export class Keystore {
// follows nwaku implementation
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/protocol_types.nim#L98
private static fromIdentityToBytes(options: IdentityOptions): Uint8Array {
private static fromIdentityToBytes(options: KeystoreEntity): Uint8Array {
return utf8ToBytes(
JSON.stringify({
treeIndex: options.membership.treeIndex,

View File

@ -1,3 +1,5 @@
import type { IdentityCredential } from "../rln.js";
export type MembershipHash = string;
export type Sha256Hash = string;
export type Keccak256Hash = string;
@ -10,3 +12,8 @@ export type MembershipInfo = {
address: string;
treeIndex: number;
};
export type KeystoreEntity = {
identity: IdentityCredential;
membership: MembershipInfo;
};

View File

@ -1,15 +1,16 @@
import { ethers } from "ethers";
export const extractMetaMaskAccount =
async (): Promise<ethers.providers.Web3Provider> => {
const ethereum = (window as any).ethereum;
export const extractMetaMaskSigner = async (): Promise<ethers.Signer> => {
const ethereum = (window as any).ethereum;
if (!ethereum) {
throw Error(
"Missing or invalid Ethereum provider. Please install a Web3 wallet such as MetaMask."
);
}
if (!ethereum) {
throw Error(
"Missing or invalid Ethereum provider. Please install a Web3 wallet such as MetaMask."
);
}
await ethereum.request({ method: "eth_requestAccounts" });
return new ethers.providers.Web3Provider(ethereum, "any");
};
await ethereum.request({ method: "eth_requestAccounts" });
const provider = new ethers.providers.Web3Provider(ethereum, "any");
return provider.getSigner();
};

View File

@ -1,12 +1,21 @@
import { createDecoder, createEncoder } from "@waku/core";
import type { IRateLimitProof } from "@waku/interfaces";
import type {
ContentTopic,
IDecodedMessage,
EncoderOptions as WakuEncoderOptions,
} from "@waku/interfaces";
import init from "@waku/zerokit-rln-wasm";
import * as zerokitRLN from "@waku/zerokit-rln-wasm";
import { ethers } from "ethers";
import { buildBigIntFromUint8Array, writeUIntLE } from "./byte_utils.js";
import type { RLNDecoder, RLNEncoder } from "./codec.js";
import { createRLNDecoder, createRLNEncoder } from "./codec.js";
import { SEPOLIA_CONTRACT } from "./constants.js";
import { dateToEpoch, epochIntToBytes } from "./epoch.js";
import { extractMetaMaskAccount } from "./metamask.js";
import type { KeystoreEntity } from "./keystore/index.js";
import { extractMetaMaskSigner } from "./metamask.js";
import verificationKey from "./resources/verification_key.js";
import { RLNContract } from "./rln_contract.js";
import * as wc from "./witness_calculator.js";
@ -164,34 +173,138 @@ export function sha256(input: Uint8Array): Uint8Array {
type StartRLNOptions = {
/**
* If not set - will extract MetaMask account and get provider from it.
* If not set - will extract MetaMask account and get signer from it.
*/
provider?: ethers.providers.Provider;
signer?: ethers.Signer;
/**
* If not set - will use default SEPOLIA_CONTRACT address.
*/
registryAddress?: string;
/**
* Credentials to use for generating proofs and connecting to the contract and network.
* If provided used for validating the network chainId and connecting to registry contract.
*/
credentials?: KeystoreEntity;
};
type RegisterMembershipOptions =
| { signature: string }
| { identity: IdentityCredential };
export class RLNInstance {
private _contract: null | RLNContract = null;
private started = false;
private starting = false;
private _contract: undefined | RLNContract;
private _signer: undefined | ethers.Signer;
private _credentials: undefined | KeystoreEntity;
constructor(
private zkRLN: number,
private witnessCalculator: WitnessCalculator
) {}
public get contract(): null | RLNContract {
public get contract(): undefined | RLNContract {
return this._contract;
}
public async start(options: StartRLNOptions = {}): Promise<void> {
const provider = options.provider || (await extractMetaMaskAccount());
const registryAddress = options.registryAddress || SEPOLIA_CONTRACT.address;
public get signer(): undefined | ethers.Signer {
return this._signer;
}
this._contract = await RLNContract.init(this, {
public async start(options: StartRLNOptions = {}): Promise<void> {
if (this.started || this.starting) {
return;
}
this.starting = true;
try {
const { signer, credentials, registryAddress } =
await this.determineStartOptions(options);
this._signer = signer!;
this._credentials = credentials;
this._contract = await RLNContract.init(this, {
registryAddress: registryAddress!,
signer: signer!,
});
this.started = true;
} finally {
this.starting = false;
}
}
private async determineStartOptions(
options: StartRLNOptions
): Promise<StartRLNOptions> {
let chainId = options.credentials?.membership.chainId;
const registryAddress =
options.credentials?.membership.address ||
options.registryAddress ||
SEPOLIA_CONTRACT.address;
if (registryAddress === SEPOLIA_CONTRACT.address) {
chainId = SEPOLIA_CONTRACT.chainId;
}
const signer = options.signer || (await extractMetaMaskSigner());
const currentChainId = await signer.getChainId();
if (chainId && chainId !== currentChainId) {
throw Error(
`Failed to start RLN contract, chain ID of contract is different from current one: contract-${chainId}, current network-${currentChainId}`
);
}
return {
signer,
registryAddress,
provider,
credentials: options.credentials,
};
}
public async registerMembership(
options: RegisterMembershipOptions
): Promise<undefined | KeystoreEntity> {
if (!this.contract) {
throw Error("RLN Contract is not initialized.");
}
if (!options.identity || !options.signature) {
throw Error("Missing signature or identity to register membership.");
}
let identity = options.identity;
if (options.signature) {
identity = await this.generateSeededIdentityCredential(signature);
}
return this.contract.registerWithIdentity(identity);
}
public createEncoder(options: WakuEncoderOptions): RLNEncoder {
if (!this._credentials) {
throw Error(
"Failed to create Encoder: missing RLN credentials. Use createRLNEncoder directly."
);
}
return createRLNEncoder({
encoder: createEncoder(options),
rlnInstance: this,
index: this._credentials.membership.treeIndex,
credential: this._credentials.identity,
});
}
public createDecoder(
contentTopic: ContentTopic
): RLNDecoder<IDecodedMessage> {
return createRLNDecoder({
rlnInstance: this,
decoder: createDecoder(contentTopic),
});
}

View File

@ -3,7 +3,8 @@ import { ethers } from "ethers";
import { zeroPadLE } from "./byte_utils.js";
import { RLN_REGISTRY_ABI, RLN_STORAGE_ABI } from "./constants.js";
import { IdentityCredential, RLNInstance } from "./rln.js";
import type { KeystoreEntity } from "./keystore/index.js";
import { type IdentityCredential, RLNInstance } from "./rln.js";
import { MerkleRootTracker } from "./root_tracker.js";
type Member = {
@ -11,10 +12,10 @@ type Member = {
index: ethers.BigNumber;
};
type Provider = ethers.Signer | ethers.providers.Provider;
type Signer = ethers.Signer;
type RLNContractOptions = {
provider: Provider;
signer: Signer;
registryAddress: string;
};
@ -47,7 +48,7 @@ export class RLNContract {
): Promise<RLNContract> {
const rlnContract = new RLNContract(rlnInstance, options);
await rlnContract.initStorageContract(options.provider);
await rlnContract.initStorageContract(options.signer);
await rlnContract.fetchMembers(rlnInstance);
rlnContract.subscribeToMembers(rlnInstance);
@ -56,20 +57,20 @@ export class RLNContract {
constructor(
rlnInstance: RLNInstance,
{ registryAddress, provider }: RLNContractOptions
{ registryAddress, signer }: RLNContractOptions
) {
const initialRoot = rlnInstance.getMerkleRoot();
this.registryContract = new ethers.Contract(
registryAddress,
RLN_REGISTRY_ABI,
provider
signer
);
this.merkleRootTracker = new MerkleRootTracker(5, initialRoot);
}
private async initStorageContract(
provider: Provider,
signer: Signer,
options: RLNStorageOptions = {}
): Promise<void> {
const storageIndex = options?.storageIndex
@ -85,7 +86,7 @@ export class RLNContract {
this.storageContract = new ethers.Contract(
storageAddress,
RLN_STORAGE_ABI,
provider
signer
);
this._membersFilter = this.storageContract.filters.MemberRegistered();
@ -207,19 +208,9 @@ export class RLNContract {
);
}
public async registerWithSignature(
rlnInstance: RLNInstance,
signature: string
): Promise<Member | undefined> {
const identityCredential =
await rlnInstance.generateSeededIdentityCredential(signature);
return this.registerWithKey(identityCredential);
}
public async registerWithKey(
credential: IdentityCredential
): Promise<Member | undefined> {
public async registerWithIdentity(
identity: IdentityCredential
): Promise<KeystoreEntity | undefined> {
if (this.storageIndex === undefined) {
throw Error(
"Cannot register credential, no storage contract index found."
@ -228,7 +219,7 @@ export class RLNContract {
const txRegisterResponse: ethers.ContractTransaction =
await this.registryContract["register(uint16,uint256)"](
this.storageIndex,
credential.IDCommitmentBigInt,
identity.IDCommitmentBigInt,
{ gasLimit: 100000 }
);
const txRegisterReceipt = await txRegisterResponse.wait();
@ -245,9 +236,17 @@ export class RLNContract {
memberRegistered.data
);
const network = await this.registryContract.provider.getNetwork();
const address = this.registryContract.address;
const membershipId = decodedData.index.toNumber();
return {
idCommitment: decodedData.idCommitment,
index: decodedData.index,
identity,
membership: {
address,
treeIndex: membershipId,
chainId: network.chainId,
},
};
}