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

feat/592: Validate length and checksum of fetched masp param #1143

Merged
merged 3 commits into from
Oct 1, 2024
Merged
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
61 changes: 41 additions & 20 deletions apps/namadillo/src/hooks/useSdk.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import initSdk from "@heliax/namada-sdk/inline-init";
import { getSdk, Sdk } from "@heliax/namada-sdk/web";
import { QueryStatus, useQuery } from "@tanstack/react-query";
import { nativeTokenAddressAtom } from "atoms/chain";
import { rpcUrlAtom } from "atoms/settings";
import { getDefaultStore, useAtomValue } from "jotai";
Expand All @@ -13,7 +14,15 @@ import {
} from "react";
import Proxies from "../../scripts/proxies.json";

export const SdkContext = createContext<Sdk | undefined>(undefined);
type SdkContext = {
sdk?: Sdk;
maspParamsStatus: QueryStatus;
};

export const SdkContext = createContext<SdkContext>({
sdk: undefined,
maspParamsStatus: "pending",
});

const { VITE_PROXY: isProxied } = import.meta.env;

Expand Down Expand Up @@ -51,37 +60,49 @@ export const SdkProvider: FunctionComponent<PropsWithChildren> = ({
const [sdk, setSdk] = useState<Sdk>();
const nativeToken = useAtomValue(nativeTokenAddressAtom);

// fetchAndStoreMaspParams() returns nothing,
// so we return boolean on success for the query to succeed:
const fetchMaspParams = async (): Promise<boolean | void> => {
const { masp } = sdk!;

return masp.hasMaspParams().then(async (hasMaspParams) => {
if (hasMaspParams) {
await masp.loadMaspParams("").catch((e) => Promise.reject(e));
return true;
}
return masp
.fetchAndStoreMaspParams(paramsUrl)
.then(() => masp.loadMaspParams("").then(() => true))
.catch((e) => {
throw new Error(e);
});
});
};

const { status: maspParamsStatus } = useQuery({
queryKey: ["sdk"],
queryFn: fetchMaspParams,
retry: 3,
retryDelay: 3000,
});

useEffect(() => {
if (nativeToken.data) {
getSdkInstance().then((sdk) => {
setSdk(sdk);
const { masp } = sdk;
masp.hasMaspParams().then((hasMaspParams) => {
if (hasMaspParams) {
return masp.loadMaspParams("").catch((e) => console.error(`${e}`));
}
masp
.fetchAndStoreMaspParams(paramsUrl)
.then(() => masp.loadMaspParams(""))
.catch((e) => console.error(`${e}`));
});
});
}
}, [nativeToken.data]);

return (
<>
<SdkContext.Provider value={sdk}> {children} </SdkContext.Provider>
<SdkContext.Provider value={{ sdk, maspParamsStatus }}>
{children}
</SdkContext.Provider>
</>
);
};

export const useSdk = (): Sdk => {
const sdkContext = useContext(SdkContext);

if (!sdkContext) {
throw new Error("sdkContext has to be used within <SdkContext.Provider>");
}

return sdkContext;
export const useSdk = (): SdkContext => {
return useContext(SdkContext);
};
133 changes: 117 additions & 16 deletions packages/shared/lib/src/sdk/mod.ts
Original file line number Diff line number Diff line change
@@ -1,46 +1,147 @@
const PREFIX = "Namada::SDK";
const MASP_MPC_RELEASE_URL =
"https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup/";

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("");
};

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

type MaspParamBytes = {
param: MaspParam;
bytes: Uint8Array;
};

/**
* The following sha256 digests where produced by downloading the following:
* https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup/masp-convert.params
* https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup/masp-spend.params
* https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup/masp-output.params
*
* And running "sha256sum" against each file:
*
* > sha256sum masp-convert.params
* 8e049c905e0e46f27662c7577a4e3480c0047ee1171f7f6d9c5b0de757bf71f1 masp-convert.params
*
* > sha256sum masp-spend.params
* 62b3c60ca54bd99eb390198e949660624612f7db7942db84595fa9f1b4a29fd8 masp-spend.params
*
* > sha256sum masp-output.params
* ed8b5d354017d808cfaf7b31eca5c511936e65ef6d276770251f5234ec5328b8 masp-output.params
*
* Length is specified in bytes, and can be retrieved with:
*
* > wc -c < masp-convert.params
* 22570940
* > wc -c < masp-spend.params
* 49848572
* > wc -c < masp-output.params
* 16398620
*/
const MASP_PARAM_ATTR: Record<
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can we add a comment telling from where these numbers / hashes come from?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yep, thanks!

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

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

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

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

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

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

return bytes;
};

export async function hasMaspParams(): Promise<boolean> {
return (
(await has("masp-spend.params")) &&
(await has("masp-output.params")) &&
(await has("masp-convert.params"))
(await has(MaspParam.Spend)) &&
(await has(MaspParam.Output)) &&
(await has(MaspParam.Convert))
);
}

export async function fetchAndStoreMaspParams(
url?: string
): Promise<[void, void, void]> {
return Promise.all([
fetchAndStore("masp-spend.params", url),
fetchAndStore("masp-output.params", url),
fetchAndStore("masp-convert.params", url),
fetchAndStore(MaspParam.Spend, url),
fetchAndStore(MaspParam.Output, url),
fetchAndStore(MaspParam.Convert, url),
]);
}

export async function getMaspParams(): Promise<[unknown, unknown, unknown]> {
return Promise.all([
get("masp-spend.params"),
get("masp-output.params"),
get("masp-convert.params"),
get(MaspParam.Spend),
get(MaspParam.Output),
get(MaspParam.Convert),
]);
}

export async function fetchAndStore(
params: string,
param: MaspParam,
url?: string
): Promise<void> {
const data = await fetchParams(params, url);
await set(params, data);
return await fetchParams(param, url)
.then((data) => set(param, data))
.catch((e) => {
return Promise.reject(`Encountered errors fetching ${param}: ${e}`);
});
}

export async function fetchParams(
params: string,
url: string = "https://github.com/anoma/masp-mpc/releases/download/namada-trusted-setup/"
param: MaspParam,
url: string = MASP_MPC_RELEASE_URL
): Promise<Uint8Array> {
return fetch(`${url}${params}`)
return fetch(`${url}${param}`)
.then((response) => response.arrayBuffer())
.then((ab) => new Uint8Array(ab));
.then((ab) => {
const bytes = new Uint8Array(ab);
return validateMaspParamBytes({ param, bytes });
});
}

function getDB(): Promise<IDBDatabase> {
Expand Down
Loading