Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

POC/WIP: feat/1098 - Fetch and store MASP params in background #1114

Draft
wants to merge 5 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/namadillo/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
.pnp.js
/build
/dist
/public/sw
.env
/test-results/
/playwright-report/
Expand Down
8 changes: 3 additions & 5 deletions apps/namadillo/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -43,10 +43,11 @@
"release:dry-run": "release-it --verbose --dry-run --ci",
"release:no-npm": "release-it --verbose --no-npm.publish --ci",
"start:proxy": "node ./scripts/startProxies.js",
"dev": "vite",
"dev": "yarn dev:sw& vite",
"dev:sw": "tsc --watch --project tsconfig.sw.json",
"preview": "vite preview",
"dev:local": "NODE_ENV=development NAMADA_INTERFACE_LOCAL=\"true\" yarn dev",
"dev:proxy": "NAMADA_INTERFACE_PROXY=true && ./scripts/start-proxies.sh && yarn dev:local",
"dev:proxy": "NAMADA_INTERFACE_PROXY=true && ./scripts/start-proxies.sh && yarn dev",
"build": "NODE_ENV=production && yarn wasm:build && vite build",
"build:only": "NODE_ENV=production && vite build",
"lint": "eslint src --ext .ts,.tsx",
Expand All @@ -57,7 +58,6 @@
"test:watch": "yarn wasm:build:test && yarn jest --watchAll=true",
"test:coverage": "yarn wasm:build:test && yarn test --coverage",
"test:ci": "jest",
"test:watch-only": "yarn jest --watchAll=true",
"e2e-test": "PLAYWRIGHT_BASE_URL=http://localhost:3000 yarn playwright test",
"e2e-test:headed": "PLAYWRIGHT_BASE_URL=http://localhost:3000 yarn playwright test --project=chromium --headed",
"wasm:build": "node ./scripts/build.js --release",
Expand Down Expand Up @@ -107,12 +107,10 @@
"eslint-plugin-react-hooks": "^4.6.0",
"globals": "^15.9.0",
"history": "^5.3.0",
"identity-obj-proxy": "^3.0.0",
"jest": "^29.7.0",
"jest-create-mock-instance": "^2.0.0",
"jest-environment-jsdom": "^29.7.0",
"jest-fetch-mock": "^3.0.3",
"jest-transformer-svg": "^2.0.2",
"local-cors-proxy": "^1.1.0",
"postcss": "^8.4.32",
"release-it": "^17.0.1",
Expand Down
25 changes: 4 additions & 21 deletions apps/namadillo/scripts/startProxies.js
Original file line number Diff line number Diff line change
@@ -1,31 +1,14 @@
const { exec } = require("child_process");
require("dotenv").config();

const {
NAMADA_INTERFACE_NAMADA_ALIAS = "Namada",
NAMADA_INTERFACE_NAMADA_URL,
NAMADA_INTERFACE_COSMOS_ALIAS = "Cosmos",
NAMADA_INTERFACE_COSMOS_URL,
NAMADA_INTERFACE_ETH_ALIAS = "Ethereum",
NAMADA_INTERFACE_ETH_URL,
} = process.env;
const MASP_MPC_URL =
"https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup";

const proxyConfigs = [
{
alias: NAMADA_INTERFACE_NAMADA_ALIAS,
url: NAMADA_INTERFACE_NAMADA_URL,
alias: "MASP MPC URL",
url: MASP_MPC_URL,
proxyPort: 8010,
},
{
alias: NAMADA_INTERFACE_COSMOS_ALIAS,
url: NAMADA_INTERFACE_COSMOS_URL,
proxyPort: 8011,
},
{
alias: NAMADA_INTERFACE_ETH_ALIAS,
url: NAMADA_INTERFACE_ETH_URL,
proxyPort: 8012,
},
];

proxyConfigs.forEach(({ alias, url, proxyPort }) => {
Expand Down
51 changes: 51 additions & 0 deletions apps/namadillo/src/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ import { IndexerLoader } from "App/Setup/IndexerLoader";
import { TomlConfigLoader } from "App/Setup/TomlConfigLoader";
import "./tailwind.css";

const { NAMADA_INTERFACE_PROXY: isProxy = "false" } = process.env;

const router = getRouter();

const container = document.getElementById("root");
Expand All @@ -40,3 +42,52 @@ if (container) {
);
});
}

if ("serviceWorker" in navigator) {
const swConfig: Record<string, string> = {
isProxy: isProxy,
};
const swUrl = `/sw/sw.js?${new URLSearchParams(swConfig).toString()}`;

window.addEventListener("load", () => {
navigator.serviceWorker
.register(swUrl, {
scope: "/sw/",
})
.then((registration) => {
console.log("Service Worker registered: ", registration);

const msgChannel = new MessageChannel();
registration.active?.postMessage(
{
type: "INIT_PORT",
},
[msgChannel.port2]
);

msgChannel.port1.onmessage = (event) => {
switch (event.type) {
case "namadillo:hasMaspParamsResponse":
console.warn(`${event.data.param}: ${event.data.hasMaspParam}`);
break;
}
};

registration.active?.postMessage({
type: "namadillo:hasMaspParams",
param: "masp-output.params",
});
registration.active?.postMessage({
type: "namadillo:hasMaspParams",
param: "masp-spend.params",
});
registration.active?.postMessage({
type: "namadillo:hasMaspParams",
param: "masp-convert.params",
});
})
.catch((error) => {
console.warn("Service Worker registration failed: ", error);
});
});
}
33 changes: 33 additions & 0 deletions apps/namadillo/sw/constants.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
/**
* Define constants for MASP Params Storage
*/
const STORAGE_PREFIX = "/namadillo";

enum MaspParam {
Output = "masp-output.params",
Convert = "masp-convert.params",
Spend = "masp-spend.params",
}

const MASP_PARAM_LEN: Record<MaspParam, { length: number; sha256sum: string }> =
{
[MaspParam.Output]: {
length: 16398620,
sha256sum:
"ed8b5d354017d808cfaf7b31eca5c511936e65ef6d276770251f5234ec5328b8",
},
[MaspParam.Spend]: {
length: 49848572,
sha256sum:
"62b3c60ca54bd99eb390198e949660624612f7db7942db84595fa9f1b4a29fd8",
},
[MaspParam.Convert]: {
length: 22570940,
sha256sum:
"8e049c905e0e46f27662c7577a4e3480c0047ee1171f7f6d9c5b0de757bf71f1",
},
};

enum MsgType {
FetchParam = "fetch-params",
}
6 changes: 6 additions & 0 deletions apps/namadillo/sw/crypto.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
const sha256Hash = async (msg: Uint8Array): Promise<string> => {
const hashBuffer = await crypto.subtle.digest("SHA-256", msg);
const hashArray = Array.from(new Uint8Array(hashBuffer));
// Return hash as hex
return hashArray.map((byte) => byte.toString(16).padStart(2, "0")).join("");
};
109 changes: 109 additions & 0 deletions apps/namadillo/sw/indexedDb.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,109 @@
/**
* Establish types for IndexedDB
*/
interface KVStore<T> {
get<U extends T>(key: string): Promise<U | undefined>;
set<U extends T>(key: string, data: U | null): Promise<void>;
prefix(): string;
}

let durability: IDBTransactionMode = "readwrite";

class IndexedDBKVStore<T> implements KVStore<T> {
protected cachedDB?: IDBDatabase;

constructor(protected readonly _prefix: string) {}

public async get<U extends T>(key: string): Promise<U | undefined> {
const tx = (await this.getDB()).transaction([this.prefix()], "readonly");
const store = tx.objectStore(this.prefix());

return new Promise((resolve, reject) => {
const request = store.get(key);
request.onerror = (event) => {
event.stopPropagation();

reject(event.target);
};
request.onsuccess = () => {
if (!request.result) {
resolve(undefined);
} else {
resolve(request.result.data);
}
};
});
}

public async set<U extends T>(key: string, data: U | null): Promise<void> {
if (data === null) {
const tx = (await this.getDB()).transaction([this.prefix()], durability, {
durability: "strict",
});
const store = tx.objectStore(this.prefix());

return new Promise((resolve, reject) => {
const request = store.delete(key);
request.onerror = (event) => {
event.stopPropagation();

reject(event.target);
};
request.onsuccess = () => {
resolve();
};
});
} else {
const tx = (await this.getDB()).transaction([this.prefix()], durability, {
durability: "strict",
});
const store = tx.objectStore(this.prefix());

return new Promise((resolve, reject) => {
const request = store.put({
key,
data,
});
request.onerror = (event) => {
event.stopPropagation();

reject(event.target);
};
request.onsuccess = () => {
resolve();
};
});
}
}

public prefix(): string {
return this._prefix;
}

protected async getDB(): Promise<IDBDatabase> {
if (this.cachedDB) {
return this.cachedDB;
}

return new Promise((resolve, reject) => {
const request = indexedDB.open(this.prefix());
request.onerror = (event) => {
event.stopPropagation();
reject(event.target);
};

request.onupgradeneeded = (event) => {
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
const db = event.target.result;

db.createObjectStore(this.prefix(), { keyPath: "key" });
};

request.onsuccess = () => {
this.cachedDB = request.result;
resolve(request.result);
};
});
}
}
90 changes: 90 additions & 0 deletions apps/namadillo/sw/masp.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
type MaspParamBytes = {
param: MaspParam;
bytes: Uint8Array;
};

const fetchMaspParam = async (
maspParam: MaspParam,
onRead?: (value?: Uint8Array) => void,
onComplete?: () => void
): Promise<MaspParamBytes> => {
return fetch([MASP_MPC_URL, maspParam].join("/"))
.then(async (response) => {
if (response.ok) {
const reader = response.body?.getReader();
if (!reader) {
throw new Error("No readable stream returned!");
}
return new ReadableStream({
start(controller) {
return pump();
function pump() {
return reader?.read().then(({ done, value }) => {
// Invoke callback if provided
if (onRead && value) onRead(value);
// When no more data needs to be consumed, close the stream
if (done) {
controller.close();
// Invoke callback if provided
if (onComplete) onComplete();
return;
}
// Enqueue the next data chunk into our target stream
controller.enqueue(value);
return pump();
});
}
},
});
}
})
.then((stream) => new Response(stream))
.then((response) => response.blob())
.then(async (blob) => {
const arrayBuffer = await blob.arrayBuffer();
return {
param: maspParam,
bytes: new Uint8Array(arrayBuffer),
};
});
};

const storeMaspParam = async ({
param,
bytes,
}: MaspParamBytes): Promise<MaspParamBytes> => {
console.info(`Storing ${param}...`);
await store.set(param, bytes);
return {
param,
bytes,
};
};

const validateMaspParamBytes = async ({
param,
bytes,
}: MaspParamBytes): Promise<MaspParamBytes> => {
const { length, sha256sum } = MASP_PARAM_LEN[param];

// Reject if invalid length (incomplete download or invalid)
console.log(`Validating data length for ${param}, expecting ${length}...`);

if (length !== bytes.length) {
return Promise.reject(
`Invalid data length! Expected ${length}, received ${bytes.length}!`
);
}

// Reject if invalid hash (otherwise invalid data)
console.log(`Validating sha256sum for ${param}, expecting ${sha256sum}...`);
const hash = await sha256Hash(bytes);

if (hash !== sha256sum) {
return Promise.reject(
`Invalid sha256sum! Expected ${sha256sum}, received ${hash}!`
);
}

return { param, bytes };
};
Loading
Loading