@0xpolygonid/js-sdk
Version:
SDK to work with Polygon ID
298 lines (297 loc) • 11.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", { value: true });
exports.isGenesisStateId = exports.isIssuerGenesis = exports.RHSResolver = exports.ProofNode = void 0;
const js_iden3_core_1 = require("@iden3/js-iden3-core");
const js_merkletree_1 = require("@iden3/js-merkletree");
const constants_1 = require("../../verifiable/constants");
const utils_1 = require("../../utils");
const sparse_merkle_tree_1 = require("./sparse-merkle-tree");
/**
* ProofNode is a partial Reverse Hash Service result
* it contains the current node hash and its children
*
* @public
* @class ProofNode
*/
class ProofNode {
/**
*
* Creates an instance of ProofNode.
* @param {Hash} [hash=ZERO_HASH] - current node hash
* @param {Hash[]} [children=[]] - children of the node
*/
constructor(hash = js_merkletree_1.ZERO_HASH, children = []) {
this.hash = hash;
this.children = children;
}
/**
* Determination of Node type
* Can be: Leaf, Middle or State node
*
* @returns NodeType
*/
nodeType() {
if (this.children.length === 2) {
return NodeType.Middle;
}
if (this.children.length === 3 && this.children[2].hex() === js_merkletree_1.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) {
return new ProofNode(js_merkletree_1.Hash.fromHex(hexNode.hash), hexNode.children.map((ch) => js_merkletree_1.Hash.fromHex(ch)));
}
}
exports.ProofNode = ProofNode;
var NodeType;
(function (NodeType) {
NodeType[NodeType["Unknown"] = 0] = "Unknown";
NodeType[NodeType["Middle"] = 1] = "Middle";
NodeType[NodeType["Leaf"] = 2] = "Leaf";
NodeType[NodeType["State"] = 3] = "State";
})(NodeType || (NodeType = {}));
/**
* RHSResolver is a class that allows to interact with the RHS service to get revocation status.
*
* @public
* @class RHSResolver
*/
class RHSResolver {
constructor(_state) {
this._state = _state;
}
/**
* 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, credentialStatusResolveOptions) {
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) {
if (credentialStatus?.statusIssuer?.type === constants_1.CredentialStatusType.SparseMerkleTreeProof) {
try {
return await new sparse_merkle_tree_1.IssuerResolver().resolve(credentialStatus.statusIssuer);
}
catch (e) {
throw new Error(`can't fetch revocation status from backup endpoint: ${e?.message}`);
}
}
throw new Error(`can't fetch revocation status: ${e?.message}`);
}
}
/**
* Gets revocation status from rhs service.
* @param {CredentialStatus} credentialStatus
* @param {DID} issuerDID
* @param {IssuerData} issuerData
* @returns Promise<RevocationStatus>
*/
async getStatus(credentialStatus, issuerDID, issuerData, genesisState) {
const issuerId = js_iden3_core_1.DID.idFromDID(issuerDID);
let latestState;
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) {
const errMsg = e?.reason ?? e.message ?? e;
if (!errMsg.includes(constants_1.VerifiableConstants.ERRORS.IDENTITY_DOES_NOT_EXIST)) {
throw e;
}
const stateHex = this.extractState(credentialStatus.id);
if (!stateHex) {
return this.getRevocationStatusFromIssuerData(issuerDID, issuerData, genesisState);
}
const currentStateBigInt = js_merkletree_1.Hash.fromHex(stateHex).bigInt();
const isEthIdentity = (0, utils_1.isEthereumIdentity)(issuerDID);
if (!isEthIdentity && !(0, utils_1.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 = js_merkletree_1.Hash.fromBigInt(BigInt(credentialStatus.revocationNonce ?? 0));
const hashedIssuerRoot = js_merkletree_1.Hash.fromBigInt(latestState);
return await this.getRevocationStatusFromRHS(hashedRevNonce, hashedIssuerRoot, rhsHost);
}
/**
* Extract revocation status from issuer data.
* @param {DID} issuerDID
* @param {IssuerData} issuerData
*/
getRevocationStatusFromIssuerData(issuerDID, issuerData, genesisState) {
if (!!genesisState && (0, utils_1.isGenesisState)(issuerDID, genesisState.value.bigInt())) {
return {
mtp: new js_merkletree_1.Proof(),
issuer: {
state: genesisState.value.hex(),
revocationTreeRoot: genesisState.revocationTreeRoot.hex(),
rootOfRoots: genesisState.rootOfRoots.hex(),
claimsTreeRoot: genesisState.claimsTreeRoot.hex()
}
};
}
// legacy
if (!!issuerData && (0, utils_1.isGenesisState)(issuerDID, issuerData.state.value)) {
return {
mtp: new js_merkletree_1.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>
*/
async getRevocationStatusFromRHS(data, issuerRoot, rhsUrl) {
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())?.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 = js_merkletree_1.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, key, rhsUrl) {
let existence = false;
const siblings = [];
let nodeAux;
const mkProof = () => new js_merkletree_1.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())?.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 ((0, js_merkletree_1.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
*/
extractState(id) {
const u = new URL(id);
return u.searchParams.get('state');
}
}
exports.RHSResolver = RHSResolver;
/**
* @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
*/
function isIssuerGenesis(issuer, state) {
const did = js_iden3_core_1.DID.parse(issuer);
const id = js_iden3_core_1.DID.idFromDID(did);
const { method, blockchain, networkId } = js_iden3_core_1.DID.decodePartsFromId(id);
const arr = js_iden3_core_1.BytesHelper.hexToBytes(state);
const stateBigInt = js_iden3_core_1.BytesHelper.bytesToInt(arr);
const type = (0, js_iden3_core_1.buildDIDType)(method, blockchain, networkId);
return isGenesisStateId(js_iden3_core_1.DID.idFromDID(did).bigInt(), stateBigInt, type);
}
exports.isIssuerGenesis = isIssuerGenesis;
/**
* @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
*/
function isGenesisStateId(id, state, type) {
const idFromState = js_iden3_core_1.Id.idGenesisFromIdenState(type, state);
return id.toString() === idFromState.bigInt().toString();
}
exports.isGenesisStateId = isGenesisStateId;