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

PID-1367: support status param for onchain revocation #137

Merged
merged 3 commits into from
Oct 3, 2023
Merged
Show file tree
Hide file tree
Changes from 2 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
30 changes: 22 additions & 8 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
"@microsoft/api-extractor": "^7.9.0",
"@types/chai": "^4.3.4",
"@types/chai-as-promised": "^7.1.5",
"@types/chai-spies": "^1.0.4",
"@types/elliptic": "^6.4.14",
"@types/fs-extra": "^11.0.1",
"@types/jsonld": "^1.5.9",
Expand Down Expand Up @@ -78,6 +79,7 @@
"ajv-formats": "^2.1.1",
"base58-js": "^1.0.4",
"buffer-browserify": "^0.2.5",
"chai-spies": "^1.0.0",
Kolezhniuk marked this conversation as resolved.
Show resolved Hide resolved
"cross-sha256": "^1.2.0",
"crypto-browserify": "^3.12.0",
"did-jwt": "^6.11.6",
Expand Down
1 change: 1 addition & 0 deletions src/credentials/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,6 @@ export * from './status/reverse-sparse-merkle-tree';
export * from './status/sparse-merkle-tree';
export * from './status/resolver';
export * from './status/agent-revocation';
export * from './status/utils';
export * from './credential-wallet';
export * from './rhs';
105 changes: 73 additions & 32 deletions src/credentials/status/on-chain-revocation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,12 @@ import { EthConnectionConfig } from '../../storage/blockchain';
import { CredentialStatusResolver, CredentialStatusResolveOptions } from './resolver';
import { OnChainRevocationStorage } from '../../storage/blockchain/onchain-revocation';
import { DID, Id } from '@iden3/js-iden3-core';
import { getChainIdByDIDsParts } from '../../storage/blockchain';
import { utils } from 'ethers';

import { VerifiableConstants } from '../../verifiable/constants';
import { isGenesisState } from './utils';
import { newHashFromHex } from '@iden3/js-merkletree';
import { EthStateStorage } from '../../../src/storage/blockchain/state';
import { getChainId } from '../../storage/blockchain';
import { IStateStorage, IOnchainRevocationStore } from '../../storage';
/**
* OnChainIssuer is a class that allows to interact with the onchain contract
* and build the revocation status.
Expand All @@ -18,8 +21,7 @@ export class OnChainResolver implements CredentialStatusResolver {
*
* Creates an instance of OnChainIssuer.
* @public
* @param {Array<EthConnectionConfig>} - onchain contract address
* @param {string} - list of EthConnectionConfig
* @param {Array<EthConnectionConfig>} - list of ethereum network connections
Kolezhniuk marked this conversation as resolved.
Show resolved Hide resolved
*/
constructor(private readonly _configs: EthConnectionConfig[]) {}

Expand Down Expand Up @@ -52,15 +54,48 @@ export class OnChainResolver implements CredentialStatusResolver {
credentialStatus: CredentialStatus,
issuer: DID
): Promise<RevocationStatus> {
const { contractAddress, chainId, revocationNonce } =
const { contractAddress, chainId, revocationNonce, stateHex } =
this.extractCredentialStatusInfo(credentialStatus);
if (revocationNonce !== credentialStatus.revocationNonce) {
throw new Error('revocationNonce does not match');
}
const networkConfig = this.networkByChainId(chainId);
const onChainCaller = new OnChainRevocationStorage(networkConfig, contractAddress);

const issuerId = DID.idFromDID(issuer);
let latestIssuerState: bigint;
try {
const ethStorage = this._getStateStorageForIssuer(issuerId);
const latestStateInfo = await ethStorage.getLatestStateById(issuerId.bigInt());
if (!latestStateInfo.state) {
throw new Error('state contract returned empty state');
}
latestIssuerState = latestStateInfo.state;
} catch (e) {
const errMsg = (e as { reason: string })?.reason ?? (e as Error).message ?? (e as string);
if (!errMsg.includes(VerifiableConstants.ERRORS.IDENTITY_DOES_NOT_EXIST)) {
throw e;
}

if (!stateHex) {
throw new Error(
'latest state not found and state prameter is not present in credentialStatus.id'
Kolezhniuk marked this conversation as resolved.
Show resolved Hide resolved
);
}
const stateBigInt = newHashFromHex(stateHex).bigInt();
if (!isGenesisState(issuer, stateBigInt)) {
throw new Error(
`latest state not found and state prameter ${stateHex} is not genesis state`
ilya-korotya marked this conversation as resolved.
Show resolved Hide resolved
);
}
latestIssuerState = stateBigInt;
}

const id = DID.idFromDID(issuer);
const revocationStatus = await onChainCaller.getRevocationStatus(id.bigInt(), revocationNonce);
const onChainCaller = this._getOnChainRevocationStorageForIssuer(chainId, contractAddress);
const revocationStatus = await onChainCaller.getRevocationStatusByIdAndState(
id.bigInt(),
latestIssuerState,
revocationNonce
);
return revocationStatus;
}

Expand All @@ -74,7 +109,7 @@ export class OnChainResolver implements CredentialStatusResolver {
contractAddress: string;
chainId: number;
revocationNonce: number;
issuer: string;
stateHex: string;
} {
if (!credentialStatus.id) {
throw new Error('credentialStatus id is empty');
Expand All @@ -85,29 +120,18 @@ export class OnChainResolver implements CredentialStatusResolver {
throw new Error('invalid credentialStatus id');
}

const issuer = idParts[0];
const issuerDID = DID.parse(issuer);

const idURL = new URL(credentialStatus.id);

// if contractAddress is not present in id as param, then it should be parsed from DID
let contractAddress = idURL.searchParams.get('contractAddress');
let chainId: number;
if (!contractAddress) {
const issuerId = DID.idFromDID(issuerDID);
const ethAddr = Id.ethAddressFromId(issuerId);
contractAddress = utils.getAddress(utils.hexDataSlice(ethAddr, 0));
const blockchain = DID.blockchainFromId(issuerId);
const network = DID.networkIdFromId(issuerId);
chainId = getChainIdByDIDsParts(issuerDID.method, blockchain, network);
} else {
const parts = contractAddress.split(':');
if (parts.length != 2) {
throw new Error('invalid contract address encoding. should be chainId:contractAddress');
}
chainId = parseInt(parts[0], 10);
contractAddress = parts[1];
const stateHex = idURL.searchParams.get('state') || '';
const contractIdentifier = idURL.searchParams.get('contractAddress');
if (!contractIdentifier) {
throw new Error('contractAddress not found in credentialStatus.id field');
}
const parts = contractIdentifier.split(':');
if (parts.length != 2) {
throw new Error('invalid contract address encoding. should be chainId:contractAddress');
}
const chainId = parseInt(parts[0], 10);
const contractAddress = parts[1];

// if revocationNonce is not present in id as param, then it should be extract from credentialStatus
const rv = idURL.searchParams.get('revocationNonce') || credentialStatus.revocationNonce;
Expand All @@ -116,7 +140,7 @@ export class OnChainResolver implements CredentialStatusResolver {
}
const revocationNonce = typeof rv === 'number' ? rv : parseInt(rv, 10);

return { contractAddress, chainId, revocationNonce, issuer };
return { contractAddress, chainId, revocationNonce, stateHex };
}

networkByChainId(chainId: number): EthConnectionConfig {
Expand All @@ -126,4 +150,21 @@ export class OnChainResolver implements CredentialStatusResolver {
}
return network;
}

// TODO (illia-korotia): is dirty hack for mock in tests.
// need to pass to constructor list of state stores not list of network configs
private _getStateStorageForIssuer(issuerId: Id): IStateStorage {
const issuerChainId = getChainId(DID.blockchainFromId(issuerId), DID.networkIdFromId(issuerId));
const ethStorage = new EthStateStorage(this.networkByChainId(issuerChainId));
return ethStorage;
}

private _getOnChainRevocationStorageForIssuer(
chainId: number,
contractAddress: string
): IOnchainRevocationStore {
const networkConfig = this.networkByChainId(chainId);
const onChainCaller = new OnChainRevocationStorage(networkConfig, contractAddress);
return onChainCaller;
}
}
35 changes: 21 additions & 14 deletions src/credentials/status/reverse-sparse-merkle-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { CredentialStatusResolver, CredentialStatusResolveOptions } from './reso
import { CredentialStatus, IssuerData, RevocationStatus } from '../../verifiable';
import { strMTHex } from '../../circuits';
import { VerifiableConstants, CredentialStatusType } from '../../verifiable/constants';
import { isGenesisState } from './utils';

/**
* ProofNode is a partial Reverse Hash Service result
Expand Down Expand Up @@ -154,27 +155,31 @@ export class RHSResolver implements CredentialStatusResolver {
issuerDID: DID,
issuerData?: IssuerData
): Promise<RevocationStatus> {
const id = DID.idFromDID(issuerDID);
const issuerId = DID.idFromDID(issuerDID);

let latestState: bigint;
try {
const latestStateInfo = await this._state.getLatestStateById(id.bigInt());
latestState = latestStateInfo?.state || BigInt(0);
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 as { reason: string })?.reason ?? (e as Error).message ?? (e as string);
Kolezhniuk marked this conversation as resolved.
Show resolved Hide resolved
if (errMsg.includes(VerifiableConstants.ERRORS.IDENTITY_DOES_NOT_EXIST)) {
const currentState = this.extractState(credentialStatus.id);
if (!currentState) {
return this.getRevocationStatusFromIssuerData(issuerDID, issuerData);
}
const currentStateBigInt = newHashFromHex(currentState).bigInt();
if (!isGenesisStateId(id.bigInt(), currentStateBigInt, id.type())) {
throw new Error(`state ${currentState} is not genesis`);
}
latestState = currentStateBigInt;
} else {
if (!errMsg.includes(VerifiableConstants.ERRORS.IDENTITY_DOES_NOT_EXIST)) {
throw e;
}
const stateHex = this.extractState(credentialStatus.id);
if (!stateHex) {
return this.getRevocationStatusFromIssuerData(issuerDID, issuerData);
}
const currentStateBigInt = newHashFromHex(stateHex).bigInt();
if (!isGenesisState(issuerDID, currentStateBigInt)) {
throw new Error(
`latest state not found and state prameter ${stateHex} is not genesis state`
);
}
latestState = currentStateBigInt;
}

const rhsHost = credentialStatus.id.split('/node')[0];
Expand Down Expand Up @@ -321,6 +326,7 @@ export class RHSResolver implements CredentialStatusResolver {
}

/**
* @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)
Expand All @@ -338,6 +344,7 @@ export function isIssuerGenesis(issuer: string, state: string): boolean {
}

/**
* @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
Expand Down
19 changes: 19 additions & 0 deletions src/credentials/status/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
import { buildDIDType, DID, Id, BytesHelper } from '@iden3/js-iden3-core';
/**
* Checks if state is genesis state
*
* @param {string} did - did
* @param {bigint|string} state - hash on bigInt or hex string format
* @returns boolean
*/
export function isGenesisState(did: DID, state: bigint | string): boolean {
if (typeof state === 'string') {
state = BytesHelper.bytesToInt(BytesHelper.hexToBytes(state));
}
const id = DID.idFromDID(did);
const { method, blockchain, networkId } = DID.decodePartsFromId(id);
const type = buildDIDType(method, blockchain, networkId);
const idFromState = Id.idGenesisFromIdenState(type, state);

return id.bigInt().toString() === idFromState.bigInt().toString();
}
Loading