@0xpolygonid/js-sdk
Version:
SDK to work with Polygon ID
367 lines (332 loc) • 11 kB
text/typescript
import { buildDIDType, BytesHelper, DID, Id } from '@iden3/js-iden3-core';
import { Hash, Proof, NodeAux, ZERO_HASH, testBit } from '@iden3/js-merkletree';
import { IStateStorage } from '../../storage';
import { CredentialStatusResolver, CredentialStatusResolveOptions } from './resolver';
import { CredentialStatus, RevocationStatus, State } from '../../verifiable';
import { CredentialStatusType } from '../../verifiable/constants';
import { isEthereumIdentity, isGenesisState } from '../../utils';
import { IssuerResolver } from './sparse-merkle-tree';
import { isIdentityDoesNotExistError } from '../../storage/blockchain/errors';
/**
* ProofNode is a partial Reverse Hash Service result
* it contains the current node hash and its children
*
* @public
* @class ProofNode
*/
export class ProofNode {
/**
*
* Creates an instance of ProofNode.
* @param {Hash} [hash=ZERO_HASH] - current node hash
* @param {Hash[]} [children=[]] - children of the node
*/
constructor(public hash: Hash = ZERO_HASH, public children: Hash[] = []) {}
/**
* Determination of Node type
* Can be: Leaf, Middle or State node
*
* @returns NodeType
*/
nodeType(): NodeType {
if (this.children.length === 2) {
return NodeType.Middle;
}
if (this.children.length === 3 && this.children[2].hex() === Hash.fromBigInt(BigInt(1)).hex()) {
return NodeType.Leaf;
}
if (this.children.length === 3) {
return NodeType.State;
}
return NodeType.Unknown;
}
/**
* JSON Representation of ProofNode with a hex values
*
* @returns {*} - ProofNode with hexes
*/
toJSON() {
return {
hash: this.hash.hex(),
children: this.children.map((h) => h.hex())
};
}
/**
* Creates ProofNode Hashes from hex values
*
* @static
* @param {ProofNodeHex} hexNode
* @returns ProofNode
*/
static fromHex(hexNode: ProofNodeHex): ProofNode {
return new ProofNode(
Hash.fromHex(hexNode.hash),
hexNode.children.map((ch) => Hash.fromHex(ch))
);
}
}
interface ProofNodeHex {
hash: string;
children: string[];
}
interface NodeHexResponse {
node: ProofNodeHex;
status: string;
}
enum NodeType {
Unknown = 0,
Middle = 1,
Leaf = 2,
State = 3
}
/**
* RHSResolver is a class that allows to interact with the RHS service to get revocation status.
*
* @public
* @class RHSResolver
*/
export class RHSResolver implements CredentialStatusResolver {
constructor(private readonly _state: IStateStorage) {}
/**
* resolve is a method to resolve a credential status from the blockchain.
*
* @public
* @param {CredentialStatus} credentialStatus - credential status to resolve
* @param {CredentialStatusResolveOptions} credentialStatusResolveOptions - options for resolver
* @returns `{Promise<RevocationStatus>}`
*/
async resolve(
credentialStatus: CredentialStatus,
credentialStatusResolveOptions?: CredentialStatusResolveOptions
): Promise<RevocationStatus> {
if (!credentialStatusResolveOptions?.issuerDID) {
throw new Error('IssuerDID is not set in options');
}
try {
return await this.getStatus(
credentialStatus,
credentialStatusResolveOptions.issuerDID,
credentialStatusResolveOptions.issuerData,
credentialStatusResolveOptions.issuerGenesisState
);
} catch (e: unknown) {
if (credentialStatus?.statusIssuer?.type === CredentialStatusType.SparseMerkleTreeProof) {
try {
return await new IssuerResolver().resolve(credentialStatus.statusIssuer);
} catch (e) {
throw new Error(
`can't fetch revocation status from backup endpoint: ${(e as Error)?.message}`
);
}
}
throw new Error(`can't fetch revocation status: ${(e as Error)?.message}`);
}
}
/**
* Gets revocation status from rhs service.
* @param {CredentialStatus} credentialStatus
* @param {DID} issuerDID
* @param {IssuerData} issuerData
* @returns Promise<RevocationStatus>
*/
private async getStatus(
credentialStatus: CredentialStatus,
issuerDID: DID,
issuerData?: {
state: {
rootOfRoots: string;
claimsTreeRoot: string;
revocationTreeRoot: string;
value: string;
};
},
genesisState?: State
): Promise<RevocationStatus> {
const issuerId = DID.idFromDID(issuerDID);
let latestState: bigint;
try {
const latestStateInfo = await this._state.getLatestStateById(issuerId.bigInt());
if (!latestStateInfo.state) {
throw new Error('state contract returned empty state');
}
latestState = latestStateInfo.state;
} catch (e) {
if (!isIdentityDoesNotExistError(e)) {
throw e;
}
const stateHex = this.extractState(credentialStatus.id);
if (!stateHex) {
return this.getRevocationStatusFromIssuerData(issuerDID, issuerData, genesisState);
}
const currentStateBigInt = Hash.fromHex(stateHex).bigInt();
const isEthIdentity = isEthereumIdentity(issuerDID);
if (!isEthIdentity && !isGenesisState(issuerDID, currentStateBigInt)) {
throw new Error(
`latest state not found and state parameter ${stateHex} is not genesis state`
);
}
if (isEthIdentity) {
throw new Error(`State must be published for Ethereum based identity`);
}
latestState = currentStateBigInt;
}
const rhsHost = credentialStatus.id.split('/node')[0];
const hashedRevNonce = Hash.fromBigInt(BigInt(credentialStatus.revocationNonce ?? 0));
const hashedIssuerRoot = Hash.fromBigInt(latestState);
return await this.getRevocationStatusFromRHS(hashedRevNonce, hashedIssuerRoot, rhsHost);
}
/**
* Extract revocation status from issuer data.
* @param {DID} issuerDID
* @param {IssuerData} issuerData
*/
private getRevocationStatusFromIssuerData(
issuerDID: DID,
issuerData?: {
state: {
rootOfRoots: string;
claimsTreeRoot: string;
revocationTreeRoot: string;
value: string;
};
},
genesisState?: State
): RevocationStatus {
if (!!genesisState && isGenesisState(issuerDID, genesisState.value.bigInt())) {
return {
mtp: new Proof(),
issuer: {
state: genesisState.value.hex(),
revocationTreeRoot: genesisState.revocationTreeRoot.hex(),
rootOfRoots: genesisState.rootOfRoots.hex(),
claimsTreeRoot: genesisState.claimsTreeRoot.hex()
}
};
}
// legacy
if (!!issuerData && isGenesisState(issuerDID, issuerData.state.value)) {
return {
mtp: new Proof(),
issuer: {
state: issuerData.state.value,
revocationTreeRoot: issuerData.state.revocationTreeRoot,
rootOfRoots: issuerData.state.rootOfRoots,
claimsTreeRoot: issuerData.state.claimsTreeRoot
}
};
}
throw new Error(`issuer data / genesis state param is empty`);
}
/**
* Gets partial revocation status info from rhs service.
*
* @param {Hash} data - hash to fetch
* @param {Hash} issuerRoot - issuer root which is a part of url
* @param {string} rhsUrl - base URL for reverse hash service
* @returns Promise<RevocationStatus>
*/
private async getRevocationStatusFromRHS(
data: Hash,
issuerRoot: Hash,
rhsUrl: string
): Promise<RevocationStatus> {
if (!rhsUrl) throw new Error('HTTP reverse hash service URL is not specified');
const resp = await fetch(`${rhsUrl}/node/${issuerRoot.hex()}`);
const treeRoots = ((await resp.json()) as NodeHexResponse)?.node;
if (treeRoots.children.length !== 3) {
throw new Error('state should has tree children');
}
const s = issuerRoot.hex();
const [cTR, rTR, roTR] = treeRoots.children;
const rtrHashed = Hash.fromHex(rTR);
const nonRevProof = await this.rhsGenerateProof(rtrHashed, data, `${rhsUrl}/node`);
return {
mtp: nonRevProof,
issuer: {
state: s,
claimsTreeRoot: cTR,
revocationTreeRoot: rTR,
rootOfRoots: roTR
}
};
}
async rhsGenerateProof(treeRoot: Hash, key: Hash, rhsUrl: string): Promise<Proof> {
let existence = false;
const siblings: Hash[] = [];
let nodeAux: NodeAux;
const mkProof = () => new Proof({ siblings, existence, nodeAux });
let nextKey = treeRoot;
for (let depth = 0; depth < key.bytes.length * 8; depth++) {
if (nextKey.bytes.every((i) => i === 0)) {
return mkProof();
}
const data = await fetch(`${rhsUrl}/${nextKey.hex()}`);
const resp = ((await data.json()) as NodeHexResponse)?.node;
const n = ProofNode.fromHex(resp);
switch (n.nodeType()) {
case NodeType.Leaf:
if (key.bytes.every((b, index) => b === n.children[0].bytes[index])) {
existence = true;
return mkProof();
}
// We found a leaf whose entry didn't match hIndex
nodeAux = {
key: n.children[0],
value: n.children[1]
};
return mkProof();
case NodeType.Middle:
if (testBit(key.bytes, depth)) {
nextKey = n.children[1];
siblings.push(n.children[0]);
} else {
nextKey = n.children[0];
siblings.push(n.children[1]);
}
break;
default:
throw new Error(`found unexpected node type in tree ${n.hash.hex()}`);
}
}
throw new Error('tree depth is too high');
}
/**
* Get state param from rhs url
* @param {string} id
* @returns string | null
*/
private extractState(id: string): string | null {
const u = new URL(id);
return u.searchParams.get('state');
}
}
/**
* @deprecated The method should not be used. Use isGenesisState instead.
* Checks if issuer did is created from given state is genesis
*
* @param {string} issuer - did (string)
* @param {string} state - hex state
* @returns boolean
*/
export function isIssuerGenesis(issuer: string, state: string): boolean {
const did = DID.parse(issuer);
const id = DID.idFromDID(did);
const { method, blockchain, networkId } = DID.decodePartsFromId(id);
const arr = BytesHelper.hexToBytes(state);
const stateBigInt = BytesHelper.bytesToInt(arr);
const type = buildDIDType(method, blockchain, networkId);
return isGenesisStateId(DID.idFromDID(did).bigInt(), stateBigInt, type);
}
/**
* @deprecated The method should not be used. Use isGenesisState instead.
* Checks if id is created from given state and type is genesis
*
* @param {bigint} id
* @param {bigint} state
* @param {Uint8Array} type
* @returns boolean - returns if id is genesis
*/
export function isGenesisStateId(id: bigint, state: bigint, type: Uint8Array): boolean {
const idFromState = Id.idGenesisFromIdenState(type, state);
return id.toString() === idFromState.bigInt().toString();
}