@0xpolygonid/js-sdk
Version:
SDK to work with Polygon ID
705 lines (637 loc) • 23.3 kB
text/typescript
import { JsonRpcProvider, Signer, Contract, TransactionRequest, ethers } from 'ethers';
import { EthConnectionConfig } from './state';
import { IOnChainZKPVerifier } from '../interfaces/onchain-zkp-verifier';
import {
AuthMethod,
AuthProof,
AuthProofEthIdentity,
AuthProofZKP,
ContractInvokeTransactionData,
CrossChainProof,
JsonDocumentObjectValue,
processProofResponse,
ZeroKnowledgeInvokeResponse,
ZeroKnowledgeProofResponse
} from '../../iden3comm';
import abi from './abi/ZkpVerifier.json';
import { IVerifierABI } from '@iden3/universal-verifier-v2-abi';
import { TransactionService } from '../../blockchain';
import { BytesHelper, chainIDfromDID, DID, Id } from '@iden3/js-iden3-core';
import {
AtomicQueryMTPV2OnChainPubSignals,
AtomicQuerySigV2OnChainPubSignals,
AtomicQueryV3OnChainPubSignals,
AuthV2PubSignals,
CircuitId,
StatesInfo
} from '../../circuits';
import { byteEncoder, bytesToHex, DIDDocumentSignature, resolveDidDocument } from '../../utils';
import { GlobalStateUpdate, IdentityStateUpdate } from '../entities/state';
import { Hash } from '@iden3/js-merkletree';
import { packZkpProof, prepareZkpProof } from './common';
const maxGasLimit = 10000000n;
/**
* Supported function signature for SubmitZKPResponse
*/
export enum FunctionSignatures {
/**
* solidity identifier for function signature:
* function submitZKPResponse(uint64 requestId, uint256[] calldata inputs,
* uint256[2] calldata a, uint256[2][2] calldata b, uint256[2] calldata c) public
*/
SubmitZKPResponseV1 = 'b68967e2',
//function submitZKPResponseV2(tuple[](uint64 requestId,bytes zkProof,bytes data),bytes crossChainProof)
SubmitZKPResponseV2 = 'ade09fcd',
//function submitResponse(tuple(string authMethod,bytes proof),tuple(uint256 requestId,bytes proof,bytes metadata)[],bytes crossChainProof)
SubmitResponse = '06c86a91'
}
/**
* OnChainZKPVerifierOptions represents OnChainZKPVerifier options
*/
export type OnChainZKPVerifierOptions = {
didResolverUrl?: string;
};
type ProofPreparationResult = {
requestId: string | number;
proof: ZeroKnowledgeProofResponse;
encoded: string;
metadata: string;
};
export type TxPreparationResultSubmitResponse = {
authProof: { raw: AuthProof; encoded: string };
crossChainProof: { raw: CrossChainProof; encoded: string };
proofs: ProofPreparationResult[];
};
export const toTxDataArgs = function (res: TxPreparationResultSubmitResponse) {
return [
{
authMethod: res.authProof.raw.authMethod,
proof: res.authProof.encoded
},
res.proofs.map((p) => {
return {
requestId: p.requestId,
proof: p.encoded,
metadata: p.metadata
};
}),
res.crossChainProof.encoded
];
};
type OnChainZKPVerifierCircuitId =
| CircuitId.AuthV2
| CircuitId.AtomicQueryMTPV2OnChain
| CircuitId.AtomicQuerySigV2OnChain
| CircuitId.AtomicQueryV3OnChain;
/**
* OnChainZKPVerifier is a class that allows to interact with the OnChainZKPVerifier contract
* and submitZKPResponse.
*
* @beta
* @class OnChainZKPVerifier
*/
export class OnChainZKPVerifier implements IOnChainZKPVerifier {
/**
* supported circuits
*/
private static readonly _supportedCircuits: OnChainZKPVerifierCircuitId[] = [
CircuitId.AuthV2,
CircuitId.AtomicQueryMTPV2OnChain,
CircuitId.AtomicQuerySigV2OnChain,
CircuitId.AtomicQueryV3OnChain
];
private static readonly _supportedCircuitsPubSignalsMap = {
[CircuitId.AtomicQueryMTPV2OnChain]: AtomicQueryMTPV2OnChainPubSignals,
[CircuitId.AtomicQuerySigV2OnChain]: AtomicQuerySigV2OnChainPubSignals,
[CircuitId.AtomicQueryV3OnChain]: AtomicQueryV3OnChainPubSignals,
[CircuitId.AuthV2]: AuthV2PubSignals
};
/**
*
* Creates an instance of OnChainZKPVerifier.
* @beta
* @param {EthConnectionConfig[]} - array of ETH configs
*/
constructor(
private readonly _configs: EthConnectionConfig[],
private readonly _opts?: OnChainZKPVerifierOptions
) {}
public static async prepareTxArgsSubmitV1(
txData: ContractInvokeTransactionData,
zkProofResponse: ZeroKnowledgeProofResponse
): Promise<JsonDocumentObjectValue[]> {
if (txData.method_id.replace('0x', '') !== FunctionSignatures.SubmitZKPResponseV1) {
throw new Error(
`prepareTxArgsSubmitV1 function doesn't implement requested method id. Only '0x${FunctionSignatures.SubmitZKPResponseV1}' is supported.`
);
}
const requestID = zkProofResponse.id;
const inputs = zkProofResponse.pub_signals;
const preparedZkpProof = prepareZkpProof(zkProofResponse.proof);
const payload = [requestID, inputs, preparedZkpProof.a, preparedZkpProof.b, preparedZkpProof.c];
return payload;
}
/**
* {@inheritDoc IOnChainZKPVerifier.prepareTxArgsSubmitV1}
*/
public async prepareTxArgsSubmitV1(
txData: ContractInvokeTransactionData,
zkProofResponse: ZeroKnowledgeProofResponse
): Promise<JsonDocumentObjectValue[]> {
return OnChainZKPVerifier.prepareTxArgsSubmitV1(txData, zkProofResponse);
}
/**
* {@inheritDoc IOnChainZKPVerifier.submitZKPResponse}
*/
public async submitZKPResponse(
ethSigner: Signer,
txData: ContractInvokeTransactionData,
zkProofResponses: ZeroKnowledgeProofResponse[]
): Promise<Map<string, ZeroKnowledgeProofResponse>> {
const chainConfig = this._configs.find((i) => i.chainId == txData.chain_id);
if (!chainConfig) {
throw new Error(`config for chain id ${txData.chain_id} was not found`);
}
if (txData.method_id.replace('0x', '') !== FunctionSignatures.SubmitZKPResponseV1) {
throw new Error(
`submitZKPResponse function doesn't implement requested method id. Only '0x${FunctionSignatures.SubmitZKPResponseV1}' is supported.`
);
}
const provider = new JsonRpcProvider(chainConfig.url, chainConfig.chainId);
ethSigner = ethSigner.connect(provider);
const response = new Map<string, ZeroKnowledgeProofResponse>();
const feeData = await provider.getFeeData();
const maxFeePerGas = chainConfig.maxFeePerGas
? BigInt(chainConfig.maxFeePerGas)
: feeData.maxFeePerGas;
const maxPriorityFeePerGas = chainConfig.maxPriorityFeePerGas
? BigInt(chainConfig.maxPriorityFeePerGas)
: feeData.maxPriorityFeePerGas;
const verifierContract = new Contract(txData.contract_address, abi);
for (const zkProofResponse of zkProofResponses) {
const txArgs = await this.prepareTxArgsSubmitV1(txData, zkProofResponse);
const payload = await verifierContract.submitZKPResponse.populateTransaction(...txArgs);
const request: TransactionRequest = {
to: txData.contract_address,
data: payload.data,
maxFeePerGas,
maxPriorityFeePerGas
};
let gasLimit;
try {
gasLimit = await ethSigner.estimateGas(request);
} catch (e) {
gasLimit = maxGasLimit;
}
request.gasLimit = gasLimit;
const transactionService = new TransactionService(provider);
const { txnHash } = await transactionService.sendTransactionRequest(ethSigner, request);
response.set(txnHash, zkProofResponse);
}
return response;
}
/**
* {@inheritDoc IOnChainZKPVerifier.submitZKPResponseV2}
*/
public async submitZKPResponseV2(
ethSigner: Signer,
txData: ContractInvokeTransactionData,
zkProofResponses: ZeroKnowledgeProofResponse[]
): Promise<Map<string, ZeroKnowledgeProofResponse[]>> {
const chainConfig = this._configs.find((i) => i.chainId == txData.chain_id);
if (!chainConfig) {
throw new Error(`config for chain id ${txData.chain_id} was not found`);
}
if (txData.method_id.replace('0x', '') !== FunctionSignatures.SubmitZKPResponseV2) {
throw new Error(
`submitZKPResponseV2 function doesn't implement requested method id. Only '0x${FunctionSignatures.SubmitZKPResponseV2}' is supported.`
);
}
if (!this._opts?.didResolverUrl) {
throw new Error(`did resolver url required for crosschain verification`);
}
const provider = new JsonRpcProvider(chainConfig.url, chainConfig.chainId);
ethSigner = ethSigner.connect(provider);
const txDataArgs = await this.prepareTxArgsSubmitV2(txData, zkProofResponses);
const feeData = await provider.getFeeData();
const maxFeePerGas = chainConfig.maxFeePerGas
? BigInt(chainConfig.maxFeePerGas)
: feeData.maxFeePerGas;
const maxPriorityFeePerGas = chainConfig.maxPriorityFeePerGas
? BigInt(chainConfig.maxPriorityFeePerGas)
: feeData.maxPriorityFeePerGas;
const verifierContract = new Contract(txData.contract_address, abi);
const txRequestData = await verifierContract.submitZKPResponseV2.populateTransaction(
...txDataArgs
);
const request: TransactionRequest = {
to: txData.contract_address,
data: txRequestData.data,
maxFeePerGas,
maxPriorityFeePerGas
};
let gasLimit;
try {
gasLimit = await ethSigner.estimateGas(request);
} catch (e) {
gasLimit = maxGasLimit;
}
request.gasLimit = gasLimit;
const transactionService = new TransactionService(provider);
const { txnHash } = await transactionService.sendTransactionRequest(ethSigner, request);
return new Map<string, ZeroKnowledgeProofResponse[]>().set(txnHash, zkProofResponses);
}
/**
* {@inheritDoc IOnChainVerifierMultiQuery.submitResponse}
*/
public async submitResponse(
ethSigner: Signer,
txData: ContractInvokeTransactionData,
responses: ZeroKnowledgeProofResponse[],
authProof: AuthProof
): Promise<Map<string, ZeroKnowledgeInvokeResponse>> {
const chainConfig = this._configs.find((i) => i.chainId == txData.chain_id);
if (!chainConfig) {
throw new Error(`config for chain id ${txData.chain_id} was not found`);
}
if (txData.method_id.replace('0x', '') !== FunctionSignatures.SubmitResponse) {
throw new Error(
`submitResponse function doesn't implement requested method id. Only '0x${FunctionSignatures.SubmitResponse}' is supported.`
);
}
if (!this._opts?.didResolverUrl) {
throw new Error(`did resolver url required for crosschain verification`);
}
const provider = new JsonRpcProvider(chainConfig.url, chainConfig.chainId);
ethSigner = ethSigner.connect(provider);
const txPreparationResult = await this.prepareTxArgsSubmit(txData, responses, authProof);
const feeData = await provider.getFeeData();
const maxFeePerGas = chainConfig.maxFeePerGas
? BigInt(chainConfig.maxFeePerGas)
: feeData.maxFeePerGas;
const maxPriorityFeePerGas = chainConfig.maxPriorityFeePerGas
? BigInt(chainConfig.maxPriorityFeePerGas)
: feeData.maxPriorityFeePerGas;
const verifierContract = new Contract(txData.contract_address, IVerifierABI);
const txRequestData = await verifierContract.submitResponse.populateTransaction(
...txPreparationResult.txDataArgs
);
const request: TransactionRequest = {
to: txData.contract_address,
data: txRequestData.data,
maxFeePerGas,
maxPriorityFeePerGas
};
let gasLimit;
try {
gasLimit = await ethSigner.estimateGas(request);
} catch (e) {
gasLimit = maxGasLimit;
}
request.gasLimit = gasLimit;
const transactionService = new TransactionService(provider);
const { txnHash } = await transactionService.sendTransactionRequest(ethSigner, request);
// return multiple responses for all the responses (single and grouped)
return new Map<string, ZeroKnowledgeInvokeResponse>().set(txnHash, {
authProof: txPreparationResult.result.authProof.raw,
crossChainProof: txPreparationResult.result.crossChainProof.raw,
responses: txPreparationResult.result.proofs.map((m) => m.proof)
});
}
public static async prepareTxArgsSubmit(
resolverUrl: string,
txData: ContractInvokeTransactionData,
responses: ZeroKnowledgeProofResponse[],
authProof: AuthProof
): Promise<{ result: TxPreparationResultSubmitResponse; txDataArgs: JsonDocumentObjectValue[] }> {
if (txData.method_id.replace('0x', '') !== FunctionSignatures.SubmitResponse) {
throw new Error(
`submit cross chain doesn't implement requested method id. Only '0x${FunctionSignatures.SubmitResponse}' is supported.`
);
}
const gistUpdatesArr: GlobalStateUpdate[] = [];
const stateUpdatesArr: IdentityStateUpdate[] = [];
const payloadResponses = [];
const emptyBytes = '0x';
let encodedAuthProof = '';
switch (authProof.authMethod) {
case AuthMethod.AUTHV2: {
const preparedZkpProof = prepareZkpProof((authProof as AuthProofZKP).zkp.proof);
encodedAuthProof = packZkpProof(
(authProof as AuthProofZKP).zkp.pub_signals,
preparedZkpProof.a,
preparedZkpProof.b,
preparedZkpProof.c
);
break;
}
case AuthMethod.ETH_IDENTITY: {
encodedAuthProof = packEthIdentityProof((authProof as AuthProofEthIdentity).userDid);
break;
}
default:
throw new Error('auth proof must use method AuthV2 or ethIdentity');
}
// Process all the responses
for (const zkProof of responses) {
this.checkSupportedCircuit(zkProof.circuitId as CircuitId);
const { requestId, zkProofEncoded, metadata } = processProofResponse(zkProof);
payloadResponses.push({
proof: zkProof,
requestId: requestId,
encoded: zkProofEncoded,
metadata: metadata
});
}
// Process all zkProofs and prepare cross chain proofs
const allZkProofs = responses.map((zkProof) => ({
circuitId: zkProof.circuitId as OnChainZKPVerifierCircuitId,
pub_signals: zkProof.pub_signals
}));
if (authProof.authMethod == AuthMethod.AUTHV2) {
allZkProofs.push({
circuitId: (authProof as AuthProofZKP).zkp.circuitId as OnChainZKPVerifierCircuitId,
pub_signals: (authProof as AuthProofZKP).zkp.pub_signals
});
}
for (const zkProof of allZkProofs) {
const { gistUpdateResolutions, stateUpdateResolutions } = this.getUpdateResolutions(
resolverUrl,
txData.chain_id,
zkProof.circuitId,
zkProof.pub_signals
);
if (gistUpdateResolutions.length > 0) {
gistUpdatesArr.push(...((await Promise.all(gistUpdateResolutions)) as GlobalStateUpdate[]));
}
if (stateUpdateResolutions.length > 0) {
stateUpdatesArr.push(
...((await Promise.all(stateUpdateResolutions)) as IdentityStateUpdate[])
);
}
}
const encodedCrossChainProof =
gistUpdatesArr.length || stateUpdatesArr.length
? this.packCrossChainProofs(gistUpdatesArr, stateUpdatesArr)
: emptyBytes;
const preparationResult = {
authProof: { raw: authProof, encoded: encodedAuthProof },
proofs: payloadResponses,
crossChainProof: {
raw: {
globalStateProofs: gistUpdatesArr || [],
identityStateProofs: stateUpdatesArr || []
},
encoded: encodedCrossChainProof
}
};
return { result: preparationResult, txDataArgs: toTxDataArgs(preparationResult) };
}
public async prepareTxArgsSubmit(
txData: ContractInvokeTransactionData,
responses: ZeroKnowledgeProofResponse[],
authProof: AuthProof
): Promise<{ result: TxPreparationResultSubmitResponse; txDataArgs: JsonDocumentObjectValue[] }> {
if (!this._opts?.didResolverUrl) {
throw new Error(`did resolver url required for crosschain verification`);
}
return OnChainZKPVerifier.prepareTxArgsSubmit(
this._opts.didResolverUrl,
txData,
responses,
authProof
);
}
private static checkSupportedCircuit(circuitId: CircuitId) {
if (!this._supportedCircuits.includes(circuitId as OnChainZKPVerifierCircuitId)) {
throw new Error(`Circuit ${circuitId} not supported by OnChainZKPVerifier`);
}
}
private static getCrossChainResolvers(
source: {
id: Id;
root?: Hash;
state?: Hash;
}[],
txDataChainId: number,
type: 'gist' | 'state',
didResolverUrl: string
) {
return [
...new Set(
source.map((info) =>
JSON.stringify({
id: info.id.string(),
[type]: type === 'gist' ? info.root?.string() : info.state?.string()
})
)
)
].reduce((acc: Promise<unknown>[], s: string) => {
const info = JSON.parse(s);
const id = Id.fromString(info.id);
const chainId = chainIDfromDID(DID.parseFromId(id));
if (txDataChainId === chainId) {
return acc;
}
const promise = this.resolveDidDocumentEip712MessageAndSignature(
DID.parseFromId(Id.fromString(info.id)),
didResolverUrl,
{
[type]: Hash.fromString(info[type])
}
);
return [...acc, promise];
}, []);
}
public static async prepareTxArgsSubmitV2(
resolverUrl: string,
txData: ContractInvokeTransactionData,
zkProofResponses: ZeroKnowledgeProofResponse[]
): Promise<JsonDocumentObjectValue[]> {
if (txData.method_id.replace('0x', '') !== FunctionSignatures.SubmitZKPResponseV2) {
throw new Error(
`submit cross chain doesn't implement requested method id. Only '0x${FunctionSignatures.SubmitZKPResponseV2}' is supported.`
);
}
const gistUpdatesArr = [];
const stateUpdatesArr = [];
const payloadResponses = [];
const emptyBytes = '0x';
for (const zkProof of zkProofResponses) {
this.checkSupportedCircuit(zkProof.circuitId as CircuitId);
const { requestId, zkProofEncoded, metadata } = processProofResponse(zkProof);
payloadResponses.push({
requestId: requestId,
zkProof: zkProofEncoded,
data: metadata
});
const { gistUpdateResolutions, stateUpdateResolutions } = this.getUpdateResolutions(
resolverUrl,
txData.chain_id,
zkProof.circuitId as OnChainZKPVerifierCircuitId,
zkProof.pub_signals
);
if (gistUpdateResolutions.length > 0) {
gistUpdatesArr.push(...((await Promise.all(gistUpdateResolutions)) as GlobalStateUpdate[]));
}
if (stateUpdateResolutions.length > 0) {
stateUpdatesArr.push(
...((await Promise.all(stateUpdateResolutions)) as IdentityStateUpdate[])
);
}
}
const crossChainProofEncoded =
gistUpdatesArr.length || stateUpdatesArr.length
? this.packCrossChainProofs(gistUpdatesArr, stateUpdatesArr)
: emptyBytes;
return [payloadResponses, crossChainProofEncoded];
}
public async prepareTxArgsSubmitV2(
txData: ContractInvokeTransactionData,
zkProofResponses: ZeroKnowledgeProofResponse[]
): Promise<JsonDocumentObjectValue[]> {
if (!this._opts?.didResolverUrl) {
throw new Error(`did resolver url required for crosschain verification`);
}
return OnChainZKPVerifier.prepareTxArgsSubmitV2(
this._opts.didResolverUrl,
txData,
zkProofResponses
);
}
private static getUpdateResolutions(
resolverUrl: string,
chainId: number,
proofCircuitId: OnChainZKPVerifierCircuitId,
inputs: string[]
) {
const stateInfo = this.getOnChainGistRootStatePubSignals(proofCircuitId, inputs);
const gistUpdateResolutions = this.getCrossChainResolvers(
stateInfo.gists,
chainId,
'gist',
resolverUrl
);
const stateUpdateResolutions = this.getCrossChainResolvers(
stateInfo.states,
chainId,
'state',
resolverUrl
);
return { gistUpdateResolutions, stateUpdateResolutions };
}
private static packCrossChainProofs(
gistUpdateArr: GlobalStateUpdate[],
stateUpdateArr: IdentityStateUpdate[]
) {
const proofs = [];
for (const globalStateUpdate of gistUpdateArr) {
proofs.push({
proofType: 'globalStateProof',
proof: this.packGlobalStateMsg(globalStateUpdate)
});
}
for (const stateUpdate of stateUpdateArr) {
proofs.push({
proofType: 'stateProof',
proof: this.packIdentityStateMsg(stateUpdate)
});
}
return new ethers.AbiCoder().encode(
['tuple(' + 'string proofType,' + 'bytes proof' + ')[]'],
[proofs]
);
}
public static packGlobalStateMsg(msg: GlobalStateUpdate): string {
return new ethers.AbiCoder().encode(
[
'tuple(' +
'tuple(' +
'uint256 timestamp,' +
'bytes2 idType,' +
'uint256 root,' +
'uint256 replacedAtTimestamp' +
') globalStateMsg,' +
'bytes signature,' +
')'
],
[msg]
);
}
private static packIdentityStateMsg(msg: IdentityStateUpdate): string {
return new ethers.AbiCoder().encode(
[
'tuple(' +
'tuple(' +
'uint256 timestamp,' +
'uint256 id,' +
'uint256 state,' +
'uint256 replacedAtTimestamp' +
') idStateMsg,' +
'bytes signature,' +
')'
],
[msg]
);
}
private static getOnChainGistRootStatePubSignals(
onChainCircuitId: OnChainZKPVerifierCircuitId,
inputs: string[]
): StatesInfo {
const PubSignals = this._supportedCircuitsPubSignalsMap[onChainCircuitId];
if (!PubSignals) {
throw new Error(`Circuit ${onChainCircuitId} not supported by OnChainZKPVerifier`);
}
const atomicQueryPubSignals = new PubSignals();
const encodedInputs = byteEncoder.encode(JSON.stringify(inputs));
atomicQueryPubSignals.pubSignalsUnmarshal(encodedInputs);
return atomicQueryPubSignals.getStatesInfo();
}
private static async resolveDidDocumentEip712MessageAndSignature(
did: DID,
resolverUrl: string,
opts?: {
state?: Hash;
gist?: Hash;
}
) {
const didDoc = await resolveDidDocument(did, resolverUrl, {
...opts,
signature: DIDDocumentSignature.EthereumEip712Signature2021
});
if (!didDoc.didResolutionMetadata.proof?.length) {
throw new Error('No proof found in resolved DID document');
}
const message = didDoc.didResolutionMetadata.proof[0].eip712.message;
const signature = didDoc.didResolutionMetadata.proof[0].proofValue;
const isGistRequest = opts?.gist && !opts.state;
if (isGistRequest) {
return {
globalStateMsg: {
timestamp: message.timestamp,
idType: message.idType,
root: message.root,
replacedAtTimestamp: message.replacedAtTimestamp
},
signature
};
}
return {
idStateMsg: {
timestamp: message.timestamp,
id: message.id,
state: message.state,
replacedAtTimestamp: message.replacedAtTimestamp
},
signature
};
}
}
/**
* Packs an Ethereum identity proof from a Decentralized Identifier (DID).
* @param did - Decentralized Identifier (DID) to pack.
* @returns A hexadecimal string representing the packed DID identity proof.
*/
export const packEthIdentityProof = (did: DID): string => {
return `0x${bytesToHex(BytesHelper.intToBytes(DID.idFromDID(did).bigInt()))}`;
};