mirror of
https://github.com/logos-messaging/js-rln.git
synced 2026-01-02 13:43:06 +00:00
feat!: add new operations and improve start (#93)
* featimprove start, types, import/export, add checks for netwrok, shortcuts for decoder encoder * fix type issue * update tests * provide ability to use Keystore as a seed for credentials * fix types * up test * initialize keystore by default * add keys operation to Keystore
This commit is contained in:
parent
bafbe01e52
commit
77ba0a6d5d
@ -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,
|
||||
};
|
||||
|
||||
@ -1,3 +1,5 @@
|
||||
import { Keystore } from "./keystore.js";
|
||||
import type { DecryptedCredentials, EncryptedCredentials } from "./types.js";
|
||||
|
||||
export { Keystore };
|
||||
export type { EncryptedCredentials, DecryptedCredentials };
|
||||
|
||||
@ -172,8 +172,8 @@ describe("Keystore", () => {
|
||||
});
|
||||
|
||||
it("should fail to create store from invalid string", () => {
|
||||
expect(Keystore.fromString("/asdq}")).to.eq(null);
|
||||
expect(Keystore.fromString('{ "name": "it" }')).to.eq(null);
|
||||
expect(Keystore.fromString("/asdq}")).to.eq(undefined);
|
||||
expect(Keystore.fromString('{ "name": "it" }')).to.eq(undefined);
|
||||
});
|
||||
|
||||
it("shoud create store from valid string", async () => {
|
||||
@ -308,6 +308,6 @@ describe("Keystore", () => {
|
||||
const store = Keystore.fromObject(NWAKU_KEYSTORE as any);
|
||||
|
||||
const result = await store.readCredential("wrong-hash", "wrong-password");
|
||||
expect(result).to.eq(null);
|
||||
expect(result).to.eq(undefined);
|
||||
});
|
||||
});
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -81,7 +76,9 @@ export class Keystore {
|
||||
return new Keystore(options);
|
||||
}
|
||||
|
||||
public static fromString(str: string): Keystore | null {
|
||||
// should be valid JSON string that contains Keystore file
|
||||
// https://github.com/waku-org/nwaku/blob/f05528d4be3d3c876a8b07f9bb7dfaae8aa8ec6e/waku/waku_keystore/keyfile.nim#L376
|
||||
public static fromString(str: string): undefined | Keystore {
|
||||
try {
|
||||
const obj = JSON.parse(str);
|
||||
|
||||
@ -92,7 +89,7 @@ export class Keystore {
|
||||
return new Keystore(obj);
|
||||
} catch (err) {
|
||||
console.error("Cannot create Keystore from string:", err);
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -105,7 +102,7 @@ export class Keystore {
|
||||
}
|
||||
|
||||
public async addCredential(
|
||||
options: IdentityOptions,
|
||||
options: KeystoreEntity,
|
||||
password: Password
|
||||
): Promise<MembershipHash> {
|
||||
const membershipHash: MembershipHash = Keystore.computeMembershipHash(
|
||||
@ -138,11 +135,11 @@ export class Keystore {
|
||||
public async readCredential(
|
||||
membershipHash: MembershipHash,
|
||||
password: Password
|
||||
): Promise<null | IdentityOptions> {
|
||||
): Promise<undefined | KeystoreEntity> {
|
||||
const nwakuCredential = this.data.credentials[membershipHash];
|
||||
|
||||
if (!nwakuCredential) {
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
|
||||
const eipKeystore = Keystore.fromCredentialToEip(nwakuCredential);
|
||||
@ -167,6 +164,14 @@ export class Keystore {
|
||||
return this.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Read array of hashes of current credentials
|
||||
* @returns array of keys of credentials in current Keystore
|
||||
*/
|
||||
public keys(): string[] {
|
||||
return Object.keys(this.toObject().credentials || {});
|
||||
}
|
||||
|
||||
private static isValidNwakuStore(obj: unknown): boolean {
|
||||
if (!isKeystoreValid(obj)) {
|
||||
return false;
|
||||
@ -237,7 +242,7 @@ export class Keystore {
|
||||
|
||||
private static fromBytesToIdentity(
|
||||
bytes: Uint8Array
|
||||
): null | IdentityOptions {
|
||||
): undefined | KeystoreEntity {
|
||||
try {
|
||||
const str = bytesToUtf8(bytes);
|
||||
const obj = JSON.parse(str);
|
||||
@ -271,7 +276,7 @@ export class Keystore {
|
||||
};
|
||||
} catch (err) {
|
||||
console.error("Cannot parse bytes to Nwaku Credentials:", err);
|
||||
return null;
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
@ -302,7 +307,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,
|
||||
|
||||
@ -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,25 @@ export type MembershipInfo = {
|
||||
address: string;
|
||||
treeIndex: number;
|
||||
};
|
||||
|
||||
export type KeystoreEntity = {
|
||||
identity: IdentityCredential;
|
||||
membership: MembershipInfo;
|
||||
};
|
||||
|
||||
export type DecryptedCredentials = KeystoreEntity;
|
||||
|
||||
export type EncryptedCredentials = {
|
||||
/**
|
||||
* Valid JSON string that contains Keystore
|
||||
*/
|
||||
keystore: string;
|
||||
/**
|
||||
* ID of credentials from provided Keystore to use
|
||||
*/
|
||||
id: string;
|
||||
/**
|
||||
* Password to decrypt credentials provided
|
||||
*/
|
||||
password: Password;
|
||||
};
|
||||
|
||||
@ -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();
|
||||
};
|
||||
|
||||
180
src/rln.ts
180
src/rln.ts
@ -1,12 +1,26 @@
|
||||
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 { Keystore } from "./keystore/index.js";
|
||||
import type {
|
||||
DecryptedCredentials,
|
||||
EncryptedCredentials,
|
||||
} from "./keystore/index.js";
|
||||
import { Password } from "./keystore/types.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 +178,180 @@ 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?: EncryptedCredentials | DecryptedCredentials;
|
||||
};
|
||||
|
||||
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 keystore = Keystore.create();
|
||||
private _credentials: undefined | DecryptedCredentials;
|
||||
|
||||
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, registryAddress } = await this.determineStartOptions(
|
||||
options
|
||||
);
|
||||
|
||||
this._signer = signer!;
|
||||
this._contract = await RLNContract.init(this, {
|
||||
registryAddress: registryAddress!,
|
||||
signer: signer!,
|
||||
});
|
||||
this.started = true;
|
||||
} finally {
|
||||
this.starting = false;
|
||||
}
|
||||
}
|
||||
|
||||
private async determineStartOptions(
|
||||
options: StartRLNOptions
|
||||
): Promise<StartRLNOptions> {
|
||||
const credentials = await this.decryptCredentialsIfNeeded(
|
||||
options.credentials
|
||||
);
|
||||
|
||||
let chainId = credentials?.membership.chainId;
|
||||
const registryAddress =
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
private async decryptCredentialsIfNeeded(
|
||||
credentials?: EncryptedCredentials | DecryptedCredentials
|
||||
): Promise<undefined | DecryptedCredentials> {
|
||||
if (!credentials) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ("identity" in credentials) {
|
||||
this._credentials = credentials;
|
||||
return credentials;
|
||||
}
|
||||
|
||||
const keystore = Keystore.fromString(credentials.keystore);
|
||||
|
||||
if (!keystore) {
|
||||
throw Error("Failed to start RLN: cannot read Keystore provided.");
|
||||
}
|
||||
|
||||
this.keystore = keystore;
|
||||
this._credentials = await keystore.readCredential(
|
||||
credentials.id,
|
||||
credentials.password
|
||||
);
|
||||
|
||||
return this._credentials;
|
||||
}
|
||||
|
||||
public async registerMembership(
|
||||
options: RegisterMembershipOptions
|
||||
): Promise<undefined | DecryptedCredentials> {
|
||||
if (!this.contract) {
|
||||
throw Error("RLN Contract is not initialized.");
|
||||
}
|
||||
|
||||
let identity = "identity" in options && options.identity;
|
||||
|
||||
if ("signature" in options) {
|
||||
identity = await this.generateSeededIdentityCredential(options.signature);
|
||||
}
|
||||
|
||||
if (!identity) {
|
||||
throw Error("Missing signature or identity to register membership.");
|
||||
}
|
||||
|
||||
return this.contract.registerWithIdentity(identity);
|
||||
}
|
||||
|
||||
/**
|
||||
* Changes credentials in use by relying on provided Keystore earlier in rln.start
|
||||
* @param id: string, hash of credentials to select from Keystore
|
||||
* @param password: string or bytes to use to decrypt credentials from Keystore
|
||||
*/
|
||||
public async useCredentials(id: string, password: Password): Promise<void> {
|
||||
this._credentials = await this.keystore?.readCredential(id, password);
|
||||
}
|
||||
|
||||
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),
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@ -16,7 +16,7 @@ describe("RLN Contract abstraction", () => {
|
||||
const voidSigner = new ethers.VoidSigner(rln.SEPOLIA_CONTRACT.address);
|
||||
const rlnContract = new rln.RLNContract(rlnInstance, {
|
||||
registryAddress: rln.SEPOLIA_CONTRACT.address,
|
||||
provider: voidSigner,
|
||||
signer: voidSigner,
|
||||
});
|
||||
|
||||
rlnContract["storageContract"] = {
|
||||
@ -32,7 +32,7 @@ describe("RLN Contract abstraction", () => {
|
||||
chai.expect(insertMemberSpy).to.have.been.called();
|
||||
});
|
||||
|
||||
it("should register a member by signature", async () => {
|
||||
it("should register a member", async () => {
|
||||
const mockSignature =
|
||||
"0xdeb8a6b00a8e404deb1f52d3aa72ed7f60a2ff4484c737eedaef18a0aacb2dfb4d5d74ac39bb71fa358cf2eb390565a35b026cc6272f2010d4351e17670311c21c";
|
||||
|
||||
@ -40,7 +40,7 @@ describe("RLN Contract abstraction", () => {
|
||||
const voidSigner = new ethers.VoidSigner(rln.SEPOLIA_CONTRACT.address);
|
||||
const rlnContract = new rln.RLNContract(rlnInstance, {
|
||||
registryAddress: rln.SEPOLIA_CONTRACT.address,
|
||||
provider: voidSigner,
|
||||
signer: voidSigner,
|
||||
});
|
||||
|
||||
rlnContract["storageIndex"] = 1;
|
||||
@ -57,7 +57,9 @@ describe("RLN Contract abstraction", () => {
|
||||
"register(uint16,uint256)"
|
||||
);
|
||||
|
||||
await rlnContract.registerWithSignature(rlnInstance, mockSignature);
|
||||
const identity =
|
||||
rlnInstance.generateSeededIdentityCredential(mockSignature);
|
||||
await rlnContract.registerWithIdentity(identity);
|
||||
|
||||
chai.expect(contractSpy).to.have.been.called();
|
||||
});
|
||||
|
||||
@ -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 { DecryptedCredentials } 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<DecryptedCredentials | 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,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user