feat: add support for Keystore and new contract (#280)

* add support for Keystore and new contract

* update mock

* up rln version
This commit is contained in:
Sasha 2023-10-20 13:01:40 +02:00 committed by GitHub
parent 736f3209be
commit 5f3b0e22b0
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
2 changed files with 236 additions and 109 deletions

View File

@ -42,45 +42,69 @@
<code class="value" id="latest-membership-id">Not loaded yet</code> <code class="value" id="latest-membership-id">Not loaded yet</code>
</div> </div>
<h2 class="mu1">Credentials</h2> <div class="row rcenter mu1">
<div>
<h2>Keystore</h2>
<span id="keystoreStatus"></span>
</div>
<div>
<button id="importKeystore" type="button">Import json</button>
<input type="file" id="importKeystoreInput" style="display: none" />
<button id="exportKeystore" type="button">Download json</button>
</div>
</div>
<hr /> <hr />
<div class="row mu1">
<label for="keystorePassword"
>Password(used for reading/saving into Keystore):</label
>
<input
id="keystorePassword"
name="keystorePassword"
type="text"
style="width: 100%"
/>
</div>
<div class="row"> <div class="row">
<div class="w50"> <div class="w50">
<h4>Generate new, or import existing, credentials from wallet:</h4> <h4>Generate new credentials from wallet:</h4>
<br /> <br />
<div id="import-from-wallet"> <button
<button id="import-from-wallet-button" type="button"> id="import-from-wallet-button"
Generate RLN Credentials type="button"
</button> style="width: 100%"
</div> >
Generate new credentials
</button>
<br /> <br />
<button disabled id="register-button" type="button"> <button disabled id="register-button" type="button">
Register Credentials in Contract Register credentials
</button> </button>
</div> </div>
<div class="w50"> <div class="w50">
<h4>Import existing credentials manually:</h4> <h4>Read from Keystore:</h4>
<div> <br />
<label for="membership-id" <select
>Membership ID (your index in the RLN smart contract):</label id="keystoreOptions"
> style="
<input id="membership-id" name="membership-id" type="text" /> width: 100%;
<label for="id-secret-hash">RLN Secret Hash (hex string):</label> overflow: hidden;
<input id="id-secret-hash" name="id-secret-hash" type="text" /> white-space: nowrap;
<label for="commitment-key">RLN Commitment Key (hex string):</label> text-overflow: ellipsis;
<input id="commitment-key" name="commitment-key" type="text" /> "
<label for="id-trapdoor">RLN ID Trapdoor (hex string):</label> ></select>
<input id="id-trapdoor" name="id-trapdoor" type="text" /> <br />
<label for="id-nullifier">RLN ID Nullifier (hex string):</label> <button id="readKeystore" type="button">Read credentials</button>
<input id="id-nullifier" name="id-nullifier" type="text" />
<button disabled id="import-manually-button" type="button">
Import RLN Credentials
</button>
</div>
</div> </div>
</div> </div>
<div class="row rcenter mu1"> <div class="row rcenter mu1">
<h4>Keystore Hash</h4>
<code class="value" id="keystoreHash">none</code>
</div>
<div class="row rcenter">
<h4>Membership id</h4> <h4>Membership id</h4>
<code class="value" id="id">none</code> <code class="value" id="id">none</code>
</div> </div>

View File

@ -8,11 +8,12 @@ import {
import { protobuf } from "https://taisukef.github.io/protobuf-es.js/dist/protobuf-es.js"; import { protobuf } from "https://taisukef.github.io/protobuf-es.js/dist/protobuf-es.js";
import { import {
create, create,
IdentityCredential, Keystore,
RLNDecoder, RLNDecoder,
RLNEncoder, RLNEncoder,
RLNContract, RLNContract,
} from "https://unpkg.com/@waku/rln@0.1.1/bundle/index.js"; SEPOLIA_CONTRACT,
} from "https://unpkg.com/@waku/rln@0.1.1-fa49e29/bundle/index.js";
import { ethers } from "https://unpkg.com/ethers@5.7.2/dist/ethers.esm.min.js"; import { ethers } from "https://unpkg.com/ethers@5.7.2/dist/ethers.esm.min.js";
const ContentTopic = "/toy-chat/2/luzhou/proto"; const ContentTopic = "/toy-chat/2/luzhou/proto";
@ -23,9 +24,6 @@ const ProtoChatMessage = new protobuf.Type("ChatMessage")
.add(new protobuf.Field("nick", 2, "string")) .add(new protobuf.Field("nick", 2, "string"))
.add(new protobuf.Field("text", 3, "bytes")); .add(new protobuf.Field("text", 3, "bytes"));
const rlnDeployBlk = 3193048;
const rlnAddress = "0x9C09146844C1326c2dBC41c451766C7138F88155";
const SIGNATURE_MESSAGE = const SIGNATURE_MESSAGE =
"The signature of this message will be used to generate your RLN credentials. Anyone accessing it may send messages on your behalf, please only share with the RLN dApp"; "The signature of this message will be used to generate your RLN credentials. Anyone accessing it may send messages on your behalf, please only share with the RLN dApp";
@ -56,14 +54,37 @@ async function initRLN(ui) {
const rlnInstance = await create(); const rlnInstance = await create();
ui.setRlnStatus("WASM Blob download in progress... done!"); ui.setRlnStatus("WASM Blob download in progress... done!");
const rlnContract = new RLNContract( const rlnContract = await RLNContract.init(rlnInstance, {
rlnInstance, { registryAddress: SEPOLIA_CONTRACT.address,
address: rlnAddress,
provider: provider.getSigner(), provider: provider.getSigner(),
}); });
result.contract = rlnContract; result.contract = rlnContract;
// Keystore logic
let keystore = initKeystore(ui);
ui.createKeystoreOptions(keystore);
ui.onKeystoreImport(async (text) => {
try {
keystore = Keystore.fromString(text);
ui.setKeystoreStatus("Imported keystore from json");
} catch (err) {
console.error("Failed to import keystore:", err);
ui.setKeystoreStatus("Failed to import, fallback to current keystore");
}
ui.createKeystoreOptions(keystore);
saveLocalKeystore(keystore);
});
ui.onKeystoreExport(async () => {
return keystore.toString();
});
ui.onKeystoreRead(async (hash, password) => {
return keystore.readCredential(hash, password);
});
// Wallet logic // Wallet logic
window.ethereum.on("accountsChanged", ui.setAccount); window.ethereum.on("accountsChanged", ui.setAccount);
window.ethereum.on("chainChanged", (chainId) => { window.ethereum.on("chainChanged", (chainId) => {
@ -85,7 +106,7 @@ async function initRLN(ui) {
const filter = rlnContract.contract.filters.MemberRegistered(); const filter = rlnContract.contract.filters.MemberRegistered();
ui.disableRetrieveButton(); ui.disableRetrieveButton();
await rlnContract.fetchMembers(rlnInstance, { fromBlock: rlnDeployBlk }); await rlnContract.fetchMembers(rlnInstance);
ui.enableRetrieveButton(); ui.enableRetrieveButton();
rlnContract.subscribeToMembers(rlnInstance); rlnContract.subscribeToMembers(rlnInstance);
@ -93,12 +114,12 @@ async function initRLN(ui) {
const last = rlnContract.members.at(-1); const last = rlnContract.members.at(-1);
if (last) { if (last) {
ui.setLastMember(last.index, last.pubkey); ui.setLastMember(last.index, last.idCommitment);
} }
// make sure we have subscriptions to keep updating last item // make sure we have subscriptions to keep updating last item
rlnContract.contract.on(filter, (_pubkey, _index, event) => { rlnContract.contract.on(filter, (_idCommitment, _index, event) => {
ui.setLastMember(event.args.index, event.args.pubkey); ui.setLastMember(event.args.index, event.args.idCommitment);
}); });
}); });
@ -106,31 +127,18 @@ async function initRLN(ui) {
let membershipId; let membershipId;
let credentials; let credentials;
ui.onManualImport((membershipId, credentials) => {
result.encoder = new RLNEncoder(
createEncoder({
ephemeral: false,
contentTopic: ContentTopic,
}),
rlnInstance,
membershipId,
credentials
);
ui.setMembershipInfo(membershipId, credentials);
ui.enableDialButton();
});
ui.onWalletImport(async () => { ui.onWalletImport(async () => {
const signer = provider.getSigner(); const signer = provider.getSigner();
signature = await signer.signMessage(SIGNATURE_MESSAGE); signature = await signer.signMessage(
`${SIGNATURE_MESSAGE}. Nonce: ${Math.ceil(Math.random() * 1000)}`
);
credentials = await rlnInstance.generateSeededIdentityCredential(signature); credentials = await rlnInstance.generateSeededIdentityCredential(signature);
const idCommitment = ethers.utils.hexlify(credentials.IDCommitment); const idCommitment = ethers.utils.hexlify(credentials.IDCommitment);
rlnContract.members.forEach((m) => { rlnContract.members.forEach((m) => {
if (m.pubkey._hex === idCommitment) { if (m.idCommitment === idCommitment) {
membershipId = m.index.toString(); membershipId = m.index.toString();
} }
}); });
@ -155,20 +163,42 @@ async function initRLN(ui) {
ui.onRegister(async () => { ui.onRegister(async () => {
ui.setRlnStatus("Trying to register..."); ui.setRlnStatus("Trying to register...");
const event = signature const memberInfo = signature
? await rlnContract.registerWithSignature(rlnInstance, signature) ? await rlnContract.registerWithSignature(rlnInstance, signature)
: await rlnContract.registerWithKey(credentials); : await rlnContract.registerWithKey(credentials);
// Update membershipId membershipId = memberInfo.index.toNumber();
membershipId = event.args.index.toNumber();
console.log( console.log(
"Obtained index for current membership credentials", "Obtained index for current membership credentials",
membershipId membershipId
); );
const password = ui.getKeystorePassword();
if (!password) {
ui.setKeystoreStatus("Cannot add credentials, no password.");
}
const keystoreHash = await keystore.addCredential(
{
membership: {
treeIndex: membershipId,
chainId: SEPOLIA_CONTRACT.chainId,
address: SEPOLIA_CONTRACT.address,
},
identity:
credentials ||
rlnInstance.generateSeededIdentityCredential(signature),
},
password
);
saveLocalKeystore(keystore);
ui.addKeystoreOption(keystoreHash);
ui.setKeystoreStatus(`Added credential to Keystore`);
ui.setRlnStatus("Successfully registered."); ui.setRlnStatus("Successfully registered.");
ui.setMembershipInfo(membershipId, credentials); ui.setMembershipInfo(membershipId, credentials, keystoreHash);
ui.enableDialButton(); ui.enableDialButton();
}); });
@ -284,17 +314,11 @@ function initUI() {
"retrieve-rln-details" "retrieve-rln-details"
); );
// Credentials Elements
const membershipIdInput = document.getElementById("membership-id");
const idSecretHashInput = document.getElementById("id-secret-hash");
const commitmentKeyInput = document.getElementById("commitment-key");
const idTrapdoorInput = document.getElementById("id-trapdoor");
const idNullifierInput = document.getElementById("id-nullifier");
const importManually = document.getElementById("import-manually-button");
const importFromWalletButton = document.getElementById( const importFromWalletButton = document.getElementById(
"import-from-wallet-button" "import-from-wallet-button"
); );
const keystoreHashDiv = document.getElementById("keystoreHash");
const idDiv = document.getElementById("id"); const idDiv = document.getElementById("id");
const secretHashDiv = document.getElementById("secret-hash"); const secretHashDiv = document.getElementById("secret-hash");
const commitmentDiv = document.getElementById("commitment"); const commitmentDiv = document.getElementById("commitment");
@ -313,36 +337,25 @@ function initUI() {
const sendingStatusSpan = document.getElementById("sending-status"); const sendingStatusSpan = document.getElementById("sending-status");
const messagesList = document.getElementById("messagesList"); const messagesList = document.getElementById("messagesList");
// Keystore
const importKeystoreBtn = document.getElementById("importKeystore");
const importKeystoreInput = document.getElementById("importKeystoreInput");
const exportKeystore = document.getElementById("exportKeystore");
const keystoreStatus = document.getElementById("keystoreStatus");
const keystorePassword = document.getElementById("keystorePassword");
const keystoreOptions = document.getElementById("keystoreOptions");
const readKeystoreButton = document.getElementById("readKeystore");
// set initial state // set initial state
keystoreHashDiv.innerText = "not registered yet";
idDiv.innerText = "not registered yet"; idDiv.innerText = "not registered yet";
registerButton.disabled = true; registerButton.disabled = true;
importManually.disabled = true;
textInput.disabled = true; textInput.disabled = true;
sendButton.disabled = true; sendButton.disabled = true;
dialButton.disabled = true; dialButton.disabled = true;
retrieveRLNDetailsButton.disabled = true; retrieveRLNDetailsButton.disabled = true;
nicknameInput.disabled = true; nicknameInput.disabled = true;
// monitor & enable buttons if needed
membershipIdInput.onchange = enableManualImportIfNeeded;
idSecretHashInput.onchange = enableManualImportIfNeeded;
commitmentKeyInput.onchange = enableManualImportIfNeeded;
idNullifierInput.onchange = enableManualImportIfNeeded;
idTrapdoorInput.onchange = enableManualImportIfNeeded;
function enableManualImportIfNeeded() {
const isValuesPresent =
idSecretHashInput.value &&
commitmentKeyInput.value &&
idNullifierInput.value &&
idTrapdoorInput.value &&
membershipIdInput.value;
if (isValuesPresent) {
importManually.disabled = false;
}
}
nicknameInput.onchange = enableChatIfNeeded; nicknameInput.onchange = enableChatIfNeeded;
nicknameInput.onblur = enableChatIfNeeded; nicknameInput.onblur = enableChatIfNeeded;
@ -353,22 +366,32 @@ function initUI() {
} }
} }
// Keystore
keystorePassword.onchange = enableRegisterIfNeeded;
keystorePassword.onblur = enableRegisterIfNeeded;
function enableRegisterIfNeeded() {
if (keystorePassword.value && commitmentDiv.innerText !== "none") {
registerButton.disabled = false;
}
}
return { return {
// UI for RLN // UI for RLN
setRlnStatus(text) { setRlnStatus(text) {
statusSpan.innerText = text; statusSpan.innerText = text;
}, },
setMembershipInfo(id, credential) { setMembershipInfo(id, credential, keystoreHash) {
keystoreHashDiv.innerText = keystoreHash || "not registered yet";
idDiv.innerText = id || "not registered yet"; idDiv.innerText = id || "not registered yet";
secretHashDiv.innerText = utils.bytesToHex(credential.IDSecretHash); secretHashDiv.innerText = utils.bytesToHex(credential.IDSecretHash);
commitmentDiv.innerText = utils.bytesToHex(credential.IDCommitment); commitmentDiv.innerText = utils.bytesToHex(credential.IDCommitment);
nullifierDiv.innerText = utils.bytesToHex(credential.IDNullifier); nullifierDiv.innerText = utils.bytesToHex(credential.IDNullifier);
trapdoorDiv.innerText = utils.bytesToHex(credential.IDTrapdoor); trapdoorDiv.innerText = utils.bytesToHex(credential.IDTrapdoor);
}, },
setLastMember(index, pubkey) { setLastMember(index, _idCommitment) {
try { try {
const idCommitment = ethers.utils.zeroPad( const idCommitment = ethers.utils.zeroPad(
ethers.utils.arrayify(pubkey), ethers.utils.arrayify(_idCommitment),
32 32
); );
const indexInt = index.toNumber(); const indexInt = index.toNumber();
@ -399,7 +422,15 @@ function initUI() {
retrieveRLNDetailsButton.disabled = true; retrieveRLNDetailsButton.disabled = true;
}, },
enableRegisterButtonForSepolia(chainId) { enableRegisterButtonForSepolia(chainId) {
registerButton.disabled = isSepolia(chainId) ? false : true; registerButton.disabled =
isSepolia(chainId) &&
keystorePassword.value &&
commitmentDiv.innerText !== "none"
? false
: true;
},
getKeystorePassword() {
return keystorePassword.value;
}, },
setAccount(accounts) { setAccount(accounts) {
addressDiv.innerText = accounts.length ? accounts[0] : ""; addressDiv.innerText = accounts.length ? accounts[0] : "";
@ -415,24 +446,6 @@ function initUI() {
await fn(); await fn();
}); });
}, },
onManualImport(fn) {
importManually.addEventListener("click", () => {
const idTrapdoor = utils.hexToBytes(idTrapdoorInput.value);
const idNullifier = utils.hexToBytes(idNullifierInput.value);
const idCommitment = utils.hexToBytes(commitmentKeyInput.value);
const idSecretHash = utils.hexToBytes(idSecretHashInput.value);
const membershipId = membershipIdInput.value;
const credentials = new IdentityCredential(
idTrapdoor,
idNullifier,
idSecretHash,
idCommitment
);
fn(membershipId, credentials);
});
},
onWalletImport(fn) { onWalletImport(fn) {
importFromWalletButton.addEventListener("click", async () => { importFromWalletButton.addEventListener("click", async () => {
await fn(); await fn();
@ -450,6 +463,68 @@ function initUI() {
} }
}); });
}, },
// Keystore
addKeystoreOption(id) {
const option = document.createElement("option");
option.innerText = id;
option.setAttribute("value", id);
keystoreOptions.appendChild(option);
},
createKeystoreOptions(keystore) {
const ids = Object.keys(keystore.toObject().credentials || {});
keystoreOptions.innerHTML = "";
ids.forEach((v) => this.addKeystoreOption(v));
},
onKeystoreRead(fn) {
readKeystoreButton.addEventListener("click", async (event) => {
event.preventDefault();
if (!keystoreOptions.value) {
throw Error("No value selected to read from Keystore");
}
const credentials = await fn(
keystoreOptions.value,
keystorePassword.value
);
this.setMembershipInfo(
credentials.membership.treeIndex,
credentials.identity,
keystoreOptions.value
);
});
},
setKeystoreStatus(text) {
keystoreStatus.innerText = text;
},
onKeystoreImport(fn) {
importKeystoreBtn.addEventListener("click", (event) => {
event.preventDefault();
importKeystoreInput.click();
});
importKeystoreInput.addEventListener("change", async (event) => {
const file = event.target.files[0];
if (!file) {
console.error("No file selected");
return;
}
const text = await file.text();
fn(text);
});
},
onKeystoreExport(fn) {
exportKeystore.addEventListener("click", async (event) => {
event.preventDefault();
const filename = "keystore.json";
const text = await fn();
const file = new File([text], filename, {
type: "application/json",
});
const link = document.createElement("a");
link.href = URL.createObjectURL(file);
link.download = filename;
link.click();
});
},
// UI for Waku // UI for Waku
setWakuStatus(text) { setWakuStatus(text) {
statusDiv.innerText = text; statusDiv.innerText = text;
@ -501,3 +576,31 @@ function initUI() {
function isSepolia(id) { function isSepolia(id) {
return id === 11155111; return id === 11155111;
} }
function initKeystore(ui) {
try {
const text = readLocalKeystore();
if (!text) {
ui.setKeystoreStatus("Initialized empty keystore");
return Keystore.create();
}
const keystore = Keystore.fromString(text);
if (!keystore) {
throw Error("Failed to create from string");
}
ui.setKeystoreStatus("Loaded from localStorage");
return keystore;
} catch (err) {
console.error("Failed to init keystore:", err);
ui.setKeystoreStatus("Initialized empty keystore");
return Keystore.create();
}
}
function readLocalKeystore() {
return localStorage.getItem("keystore");
}
function saveLocalKeystore(keystore) {
localStorage.setItem("keystore", keystore.toString());
}