Merge pull request #84 from zk-passport/android-witnesscalc

Moving to PolygonID's stack on Android
This commit is contained in:
turboblitz
2024-05-12 19:34:07 +09:00
committed by GitHub
9 changed files with 248 additions and 162 deletions

View File

@@ -19,7 +19,7 @@ import { YStack } from 'tamagui';
import { prove } from './src/utils/prover';
import { useToastController } from '@tamagui/toast';
import * as amplitude from '@amplitude/analytics-react-native';
import { checkForZkey } from './src/utils/zkeyDownload';
import { downloadZkey, IsZkeyDownloading, ShowWarningModalProps } from './src/utils/zkeyDownload';
import RNFS from 'react-native-fs';
global.Buffer = Buffer;
@@ -40,8 +40,16 @@ function App(): JSX.Element {
const [proof, setProof] = useState<Proof | null>(null);
const [mintText, setMintText] = useState<string>("");
const [majority, setMajority] = useState<number>(18);
const [zkeydownloadStatus, setDownloadStatus] = useState<"not_started" | "downloading" | "completed" | "error">("not_started");
const [showWarning, setShowWarning] = useState(false);
const [isZkeyDownloading, setIsZkeyDownloading] = useState<IsZkeyDownloading>({
register_sha256WithRSAEncryption_65537: false,
disclose: false,
proof_of_passport: false,
});
const [showWarningModal, setShowWarningModal] = useState<ShowWarningModalProps>({
show: false,
circuit: "",
size: 0,
});
const [disclosure, setDisclosure] = useState({
issuing_state: false,
@@ -65,12 +73,16 @@ function App(): JSX.Element {
};
useEffect(() => {
checkForZkey({
setDownloadStatus,
setShowWarning,
toast
})
amplitude.init(AMPLITUDE_KEY);
// downloadZkey("register_sha256WithRSAEncryption_65537"); // temporary, might move after nfc scanning
// downloadZkey("disclose");
downloadZkey(
"proof_of_passport",
isZkeyDownloading,
setIsZkeyDownloading,
setShowWarningModal,
toast
);
}, []);
const handleStartCameraScan = async () => {
@@ -144,10 +156,10 @@ function App(): JSX.Element {
setDateOfExpiry={setDateOfExpiry}
majority={majority}
setMajority={setMajority}
zkeydownloadStatus={zkeydownloadStatus}
showWarning={showWarning}
setShowWarning={setShowWarning}
setDownloadStatus={setDownloadStatus}
isZkeyDownloading={isZkeyDownloading}
showWarningModal={showWarningModal}
setShowWarningModal={setShowWarningModal}
setIsZkeyDownloading={setIsZkeyDownloading}
/>
</YStack>
</YStack>

View File

@@ -1,4 +1,4 @@
import React, { useState, useEffect, Profiler } from 'react';
import React, { useState, useEffect } from 'react';
import { YStack, XStack, Text, Button, Tabs, Sheet, Label, Fieldset, Input, Switch, H2, Image, useWindowDimensions, H4, H3, ScrollView } from 'tamagui'
import { HelpCircle, IterationCw, VenetianMask, Cog, CheckCircle2, ExternalLink } from '@tamagui/lucide-icons';
import X from '../images/x.png'
@@ -17,7 +17,7 @@ import { bgColor, blueColorLight, borderColor, componentBgColor, textColor1, tex
import MintScreen from './MintScreen';
import { ToastViewport, useToastController } from '@tamagui/toast';
import { ToastMessage } from '../components/ToastMessage';
import { downloadZkey } from '../utils/zkeyDownload';
import { CircuitName, fetchZkey, IsZkeyDownloading, ShowWarningModalProps } from '../utils/zkeyDownload';
interface MainScreenProps {
onStartCameraScan: () => void;
@@ -43,10 +43,10 @@ interface MainScreenProps {
setDateOfExpiry: (date: string) => void;
majority: number;
setMajority: (age: number) => void;
zkeydownloadStatus: string;
showWarning: boolean;
setShowWarning: (value: boolean) => void;
setDownloadStatus: (value: "not_started" | "downloading" | "completed" | "error") => void;
isZkeyDownloading: IsZkeyDownloading;
showWarningModal: ShowWarningModalProps;
setShowWarningModal: (value: ShowWarningModalProps) => void;
setIsZkeyDownloading: (value: IsZkeyDownloading) => void;
}
const MainScreen: React.FC<MainScreenProps> = ({
@@ -73,10 +73,10 @@ const MainScreen: React.FC<MainScreenProps> = ({
setDateOfExpiry,
majority,
setMajority,
zkeydownloadStatus,
showWarning,
setShowWarning,
setDownloadStatus
isZkeyDownloading,
showWarningModal,
setShowWarningModal,
setIsZkeyDownloading
}) => {
const [NFCScanIsOpen, setNFCScanIsOpen] = useState(false);
const [SettingsIsOpen, setSettingsIsOpen] = useState(false);
@@ -442,7 +442,9 @@ const MainScreen: React.FC<MainScreenProps> = ({
setEns={setEns}
majority={majority}
setMajority={setMajority}
zkeydownloadStatus={zkeydownloadStatus}
isZkeyDownloading={isZkeyDownloading}
setIsZkeyDownloading={setIsZkeyDownloading}
setShowWarningModal={setShowWarningModal}
/>
</Tabs.Content>
<Tabs.Content value="mint" f={1}>
@@ -457,20 +459,26 @@ const MainScreen: React.FC<MainScreenProps> = ({
</Tabs.Content>
</Tabs>
</YStack>
<Modal visible={showWarning} animationType="slide" transparent={true}>
<Modal visible={showWarningModal.show} animationType="slide" transparent={true}>
<YStack bc="#161616" p={20} ai="center" jc="center" position="absolute" top={0} bottom={0} left={0} right={0}>
<YStack bc="#343434" p={20} borderRadius={10} ai="center" jc="center">
<Text fontWeight="bold" fontSize={18} color="#a0a0a0">👋 Hi</Text>
<Text mt={10} textAlign="center" color="#a0a0a0">
The app needs to download a large file (300MB). Please make sure you're connected to a Wi-Fi network before continuing.
The app needs to download a large file ({(showWarningModal.size / 1024 / 1024).toFixed(2)}MB). Please make sure you're connected to a Wi-Fi network before continuing.
</Text>
<XStack mt={20}>
<Button onPress={() => {
downloadZkey(
setDownloadStatus,
fetchZkey(
showWarningModal.circuit as CircuitName,
isZkeyDownloading,
setIsZkeyDownloading,
toast
);
setShowWarning(false)
setShowWarningModal({
show: false,
circuit: '',
size: 0,
})
}} bc="#4caf50" borderRadius={5} padding={10}>
<Text color="#ffffff">Continue</Text>
</Button>

View File

@@ -12,7 +12,7 @@ import { useToastController } from '@tamagui/toast'
import { ethers } from 'ethers';
import { Platform } from 'react-native';
import { formatAttribute } from '../utils/utils';
import { Proof } from '../../../common/src/utils/types';
import { downloadZkey, IsZkeyDownloading, ShowWarningModalProps } from '../utils/zkeyDownload';
interface ProveScreenProps {
selectedApp: App | null;
@@ -28,7 +28,9 @@ interface ProveScreenProps {
setEns: (ens: string) => void;
majority: number;
setMajority: (age: number) => void;
zkeydownloadStatus: string;
isZkeyDownloading: IsZkeyDownloading;
setIsZkeyDownloading: (value: IsZkeyDownloading) => void;
setShowWarningModal: (value: ShowWarningModalProps) => void;
}
const ProveScreen: React.FC<ProveScreenProps> = ({
@@ -45,13 +47,33 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
setEns,
majority,
setMajority,
zkeydownloadStatus
isZkeyDownloading,
setIsZkeyDownloading,
setShowWarningModal
}) => {
const { height } = useWindowDimensions();
const [inputValue, setInputValue] = useState(DEFAULT_ADDRESS ?? '');
const provider = new ethers.JsonRpcProvider(`https://eth-mainnet.g.alchemy.com/v2/lpOn3k6Fezetn1e5QF-iEsn-J0C6oGE0`);
const toast = useToastController()
const circuit = "proof_of_passport" //later, pass this as input
useEffect(() => {
launchZkeyDownloadIfRequired();
}, [])
async function launchZkeyDownloadIfRequired() {
if (!isZkeyDownloading[circuit]) {
// this already checks if downloading is required
downloadZkey(
circuit,
isZkeyDownloading,
setIsZkeyDownloading,
setShowWarningModal,
toast
);
}
}
useEffect(() => {
if (ens != '' && inputValue == '') {
@@ -236,7 +258,7 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
</YStack >
</YStack >
<Button
disabled={zkeydownloadStatus != "completed" || (address == ethers.ZeroAddress)}
disabled={isZkeyDownloading[circuit] || (address == ethers.ZeroAddress)}
borderWidth={1.3}
borderColor={borderColor}
borderRadius={100}
@@ -245,20 +267,13 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
backgroundColor={address == ethers.ZeroAddress ? "#cecece" : "#3185FC"}
alignSelf='center'
>
{zkeydownloadStatus === "downloading" ? (
{isZkeyDownloading[circuit] ? (
<XStack ai="center" gap="$1">
<Spinner />
<Text color={textColor1} fow="bold">
Downloading ZK proving key
</Text>
</XStack>
) : zkeydownloadStatus === "error" ? (
<XStack ai="center" gap="$1">
<Spinner />
<Text color={textColor1} fow="bold">
Error downloading ZK proving key
</Text>
</XStack>
) : generatingProof ? (
<XStack ai="center" gap="$1">
<Spinner />
@@ -278,7 +293,6 @@ const ProveScreen: React.FC<ProveScreenProps> = ({
</Button>
{(height > 750) && <Text fontSize={10} color={generatingProof ? "#a0a0a0" : "#161616"} py="$2" alignSelf='center'>This operation can take up to 2 mn, phone may freeze during this time</Text>}
</YStack >
</YStack >
);
};

View File

@@ -4,7 +4,7 @@ import groth16ExportSolidityCallData from '../../utils/snarkjs';
import contractAddresses from "../../deployments/addresses.json";
import proofOfPassportArtefact from "../../deployments/ProofOfPassport.json";
import { Steps } from './utils';
import { AWS_ENDPOINT } from '../../../common/src/constants/constants';
import { RELAYER_URL } from '../../../common/src/constants/constants';
import { Proof } from "../../../common/src/utils/types";
interface MinterProps {
@@ -42,7 +42,7 @@ export const mint = async ({ proof, setStep, setMintText, toast }: MinterProps)
.mint.populateTransaction(...callData);
console.log('transactionRequest', transactionRequest);
const response = await axios.post(AWS_ENDPOINT, {
const response = await axios.post(RELAYER_URL, {
chain: "sepolia",
tx_data: transactionRequest
});

View File

@@ -1,7 +1,7 @@
import { NativeModules, Platform } from 'react-native';
import { revealBitmapFromMapping } from '../../../common/src/utils/revealBitmap';
import { generateCircuitInputs } from '../../../common/src/utils/generateInputs';
import { Steps } from './utils';
import { parseProofAndroid, Steps } from './utils';
import { PassportData, Proof } from '../../../common/src/utils/types';
import * as amplitude from '@amplitude/analytics-react-native';
import RNFS from 'react-native-fs';
@@ -14,7 +14,7 @@ interface ProverProps {
setStep: (value: number) => void;
setGeneratingProof: (value: boolean) => void;
setProofTime: (value: number) => void;
setProof: (value: Proof | null) => void;
setProof: (proof: Proof) => void;
toast: any
}
@@ -128,24 +128,4 @@ const generateProof = async (
} catch (err: any) {
console.log('err', err);
}
};
const parseProofAndroid = (response: string) => {
const match = response.match(/ZkProof\(proof=Proof\(pi_a=\[(.*?)\], pi_b=\[\[(.*?)\], \[(.*?)\], \[1, 0\]\], pi_c=\[(.*?)\], protocol=groth16, curve=bn128\), pub_signals=\[(.*?)\]\)/);
if (!match) throw new Error('Invalid input format');
const [, pi_a, pi_b_1, pi_b_2, pi_c, pub_signals] = match;
return {
proof: {
a: pi_a.split(',').map((n: string) => n.trim()),
b: [
pi_b_1.split(',').map((n: string) => n.trim()),
pi_b_2.split(',').map((n: string) => n.trim()),
],
c: pi_c.split(',').map((n: string) => n.trim()),
},
pub_signals: pub_signals.split(',').map((n: string) => n.trim())
} as Proof;
};

View File

@@ -1,6 +1,7 @@
// Function to extract information from a two-line MRZ.
import { countryCodes } from "../../../common/src/constants/constants";
import { Proof } from "../../../common/src/utils/types";
// The actual parsing would depend on the standard being used (TD1, TD2, TD3, MRVA, MRVB).
export function extractMRZInfo(mrzString: string) {
@@ -54,4 +55,24 @@ export function formatAttribute(key: string, attribute: string) {
return countryCodes[attribute as keyof typeof countryCodes]
}
return attribute;
}
}
export const parseProofAndroid = (response: string) => {
const match = response.match(/ZkProof\(proof=Proof\(pi_a=\[(.*?)\], pi_b=\[\[(.*?)\], \[(.*?)\], \[1, 0\]\], pi_c=\[(.*?)\], protocol=groth16, curve=bn128\), pub_signals=\[(.*?)\]\)/);
if (!match) throw new Error('Invalid input format');
const [, pi_a, pi_b_1, pi_b_2, pi_c, pub_signals] = match;
return {
proof: {
a: pi_a.split(',').map((n: string) => n.trim()),
b: [
pi_b_1.split(',').map((n: string) => n.trim()),
pi_b_2.split(',').map((n: string) => n.trim()),
],
c: pi_c.split(',').map((n: string) => n.trim()),
},
pub_signals: pub_signals.split(',').map((n: string) => n.trim())
} as Proof;
};

View File

@@ -1,81 +1,186 @@
import {
NativeModules,
Platform,
} from 'react-native';
import RNFS from 'react-native-fs';
import { ARKZKEY_URL, ZKEY_NAME, ZKEY_URL } from '../../../common/src/constants/constants';
import * as amplitude from '@amplitude/analytics-react-native';
import NetInfo from '@react-native-community/netinfo';
import axios from 'axios';
import { unzip } from 'react-native-zip-archive';
const localZkeyPath = RNFS.DocumentDirectoryPath + '/proof_of_passport.zkey';
const localZipPath = RNFS.DocumentDirectoryPath + '/proof_of_passport.zip';
const localUrlPath = RNFS.DocumentDirectoryPath + '/zkey_url.txt';
// this should not change, instead update the zkey on the bucket
const zkeyZipUrls = {
register_sha256WithRSAEncryption_65537: "qweqwe",
disclose: "qweqwe",
proof_of_passport: `https://d8o9bercqupgk.cloudfront.net/proof_of_passport.zkey.zip`,
};
async function initMopro() {
if (Platform.OS === 'android') {
const res = await NativeModules.Prover.runInitAction()
console.log('Mopro init res:', res)
}
export type CircuitName = keyof typeof zkeyZipUrls;
export type ShowWarningModalProps = {
show: boolean,
circuit: CircuitName | "",
size: number,
}
export type IsZkeyDownloading = {
[circuit in CircuitName]: boolean;
}
// each time we download a zkey, we store the size of the zip file in a file named <circuit_name>_zip_size.txt
// we assume a new zkey zip will always have a different size
// the downloadZkey function downloads a zkey if either:
// 1. the zkey file does not exist
// 2. <circuit_name>_zip_size.txt does not show the same size as the server response (zkey is outdated)
// 3. the zkey is currently downloading
// 4. the commitment is already registered and the function is called for a register zkey. If it's the case, there is no need to download the latest zkey.
// => this should be fine is the function is never called after the commitment is registered.
export async function downloadZkey(
setDownloadStatus: (value: "not_started" | "downloading" | "completed" | "error") => void,
circuit: CircuitName,
isZkeyDownloading: IsZkeyDownloading,
setIsZkeyDownloading: (value: IsZkeyDownloading) => void,
setShowWarningModal: (value: ShowWarningModalProps) => void,
toast: any
) {
console.log('launching zkey download')
setDownloadStatus('downloading');
amplitude.track('Downloading zkey...');
const downloadRequired = await isDownloadRequired(circuit, isZkeyDownloading);
if (!downloadRequired) {
console.log(`zkey for ${circuit} already downloaded`)
amplitude.track(`zkey for ${circuit} already downloaded`);
return;
}
const networkInfo = await NetInfo.fetch();
console.log('Network type:', networkInfo.type)
if (networkInfo.type === 'wifi') {
fetchZkey(
circuit,
isZkeyDownloading,
setIsZkeyDownloading,
toast
);
} else {
setShowWarningModal({
show: true,
circuit: circuit,
size: 0,
});
}
}
export async function isDownloadRequired(
circuit: CircuitName,
isZkeyDownloading: IsZkeyDownloading
) {
if (isZkeyDownloading[circuit]) {
return false;
}
const fileExists = await RNFS.exists(`${RNFS.DocumentDirectoryPath}/${circuit}.zkey`);
if (!fileExists) {
return true;
}
let storedZipSize = 0;
try {
storedZipSize = Number(await RNFS.readFile(`${RNFS.DocumentDirectoryPath}/${circuit}_zip_size.txt`, 'utf8'));
} catch (error) {
console.log(`${circuit}_zip_size.txt file not found, so assuming zkey is outdated.`);
return true;
}
console.log('storedZipSize:', storedZipSize)
const response = await axios.head(zkeyZipUrls[circuit]);
const expectedSize = parseInt(response.headers['content-length'], 10);
console.log('expectedSize:', expectedSize)
const isZipComplete = storedZipSize === expectedSize;
console.log('isZipComplete:', isZipComplete)
if (!isZipComplete) {
return true;
}
return false
}
export async function fetchZkey(
circuit: CircuitName,
isZkeyDownloading: IsZkeyDownloading,
setIsZkeyDownloading: (value: IsZkeyDownloading) => void,
toast: any
) {
console.log(`fetching zkey for ${circuit} ...`)
amplitude.track(`fetching zkey for ${circuit} ...`);
setIsZkeyDownloading({
...isZkeyDownloading,
[circuit]: true,
});
const startTime = Date.now();
const url = Platform.OS === 'android' ? ARKZKEY_URL : ZKEY_URL
let previousPercentComplete = -1;
const options = {
fromUrl: url,
toFile: localZipPath,
fromUrl: zkeyZipUrls[circuit],
toFile: `${RNFS.DocumentDirectoryPath}/${circuit}.zkey.zip`,
background: true,
begin: () => {
console.log('Download has begun');
},
progress: (res: any) => {
const percentComplete = Math.floor((res.bytesWritten / res.contentLength) * 100);
if (percentComplete !== previousPercentComplete) {
if (percentComplete % 5 === 0 && percentComplete !== previousPercentComplete) {
console.log(`${percentComplete}%`);
previousPercentComplete = percentComplete;
}
},
}
};
RNFS.downloadFile(options).promise
.then(async () => {
console.log('Download complete');
RNFS.readDir(RNFS.DocumentDirectoryPath)
.then((result) => {
console.log('Directory contents before:', result);
console.log('Directory contents before unzipping:', result);
})
const unzipPath = RNFS.DocumentDirectoryPath;
await unzip(localZipPath, unzipPath);
const oldPath = `${unzipPath}/${ZKEY_NAME}${Platform.OS === 'android' ? '.arkzkey' : '.zkey'}`;
const newPath = `${unzipPath}/proof_of_passport.zkey`;
await RNFS.moveFile(oldPath, newPath);
await unzip(`${RNFS.DocumentDirectoryPath}/${circuit}.zkey.zip`, RNFS.DocumentDirectoryPath);
RNFS.readDir(RNFS.DocumentDirectoryPath)
.then((result) => {
console.log('Directory contents after:', result);
console.log('Directory contents after unzipping:', result);
})
console.log('Unzip complete');
setDownloadStatus('completed')
const endTime = Date.now();
amplitude.track('zkey download succeeded, took ' + ((endTime - startTime) / 1000) + ' seconds');
RNFS.writeFile(localUrlPath, url, 'utf8');
initMopro()
setIsZkeyDownloading({
...isZkeyDownloading,
[circuit]: false,
});
amplitude.track('zkey download succeeded, took ' + ((Date.now() - startTime) / 1000) + ' seconds');
const zipSize = await RNFS.stat(`${RNFS.DocumentDirectoryPath}/${circuit}.zkey.zip`);
console.log('zipSize:', zipSize.size);
RNFS.writeFile(`${RNFS.DocumentDirectoryPath}/${circuit}_zip_size.txt`, zipSize.size.toString(), 'utf8');
console.log('zip size written to file');
// delete the zip file
RNFS.unlink(`${RNFS.DocumentDirectoryPath}/${circuit}.zkey.zip`)
.then(() => {
console.log('zip file deleted');
})
.catch((error) => {
console.error(error);
});
})
.catch((error) => {
console.error(error);
setDownloadStatus('error');
setIsZkeyDownloading({
...isZkeyDownloading,
[circuit]: false,
});
amplitude.track('zkey download failed: ' + error.message);
toast.show('Error', {
message: `Error: ${error.message}`,
@@ -84,55 +189,4 @@ export async function downloadZkey(
},
});
});
}
interface CheckForZkeyProps {
setDownloadStatus: (value: "not_started" | "downloading" | "completed" | "error") => void;
setShowWarning: (value: boolean) => void;
toast: any
}
export async function checkForZkey({
setDownloadStatus,
setShowWarning,
toast
}: CheckForZkeyProps) {
console.log('local zip path:', localZipPath)
const url = Platform.OS === 'android' ? ARKZKEY_URL : ZKEY_URL
let storedUrl = '';
try {
storedUrl = await RNFS.readFile(localUrlPath, 'utf8');
} catch (error) {
console.log('zkey_url.txt file not found, so assuming zkey is outdated.');
}
const fileExists = await RNFS.exists(localZipPath);
const fileInfo = fileExists ? await RNFS.stat(localZipPath) : null;
const response = await axios.head(url);
const expectedSize = parseInt(response.headers['content-length'], 10);
const isFileComplete = fileInfo && fileInfo.size === expectedSize;
console.log('expectedSize:', expectedSize)
console.log('fileInfo.size:', fileInfo?.size)
console.log('isFileComplete:', isFileComplete)
if (!isFileComplete || url !== storedUrl) {
const state = await NetInfo.fetch();
console.log('Network start type:', state.type)
if (state.type === 'wifi') {
downloadZkey(
setDownloadStatus,
toast
)
} else {
setShowWarning(true);
}
} else {
console.log('zkey already downloaded')
amplitude.track('zkey already downloaded');
setDownloadStatus('completed');
initMopro()
}
}
}

View File

@@ -1,7 +1,4 @@
export const AWS_ENDPOINT = "https://0pw5u65m3a.execute-api.eu-north-1.amazonaws.com/api-stage/mint"
export const ZKEY_NAME = "proof_of_passport_final_v01"
export const ZKEY_URL = `https://d8o9bercqupgk.cloudfront.net/${ZKEY_NAME}.zkey.zip`
export const RELAYER_URL = "https://0pw5u65m3a.execute-api.eu-north-1.amazonaws.com/api-stage/mint"
export const TREE_DEPTH = 16

View File

@@ -280,9 +280,9 @@ describe("Proof of Passport", function () {
console.log('transactionRequest', transactionRequest);
const apiEndpoint = process.env.AWS_ENDPOINT;
const apiEndpoint = process.env.RELAYER_URL;
if (!apiEndpoint) {
throw new Error('AWS_ENDPOINT env variable is not set');
throw new Error('RELAYER_URL env variable is not set');
}
const response = await axios.post(apiEndpoint, {
chain: "mumbai",