Skip to content

Commit

Permalink
PID-1367: support status param for onchain revocation (#137)
Browse files Browse the repository at this point in the history
* support status param for onchain revocation
  • Loading branch information
ilya-korotya authored Oct 3, 2023
1 parent 5ffd319 commit 4cbc378
Show file tree
Hide file tree
Showing 14 changed files with 793 additions and 271 deletions.
23 changes: 23 additions & 0 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 All @@ -55,6 +56,7 @@
"@typescript-eslint/eslint-plugin": "^5.41.0",
"chai": "^4.3.7",
"chai-as-promised": "^7.1.1",
"chai-spies": "^1.0.0",
"chokidar": "^3.5.3",
"esbuild": "^0.15.15",
"eslint-config-prettier": "^8.8.0",
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>} _configs - list of ethereum network connections
*/
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 parameter is not present in credentialStatus.id'
);
}
const stateBigInt = newHashFromHex(stateHex).bigInt();
if (!isGenesisState(issuer, stateBigInt)) {
throw new Error(
`latest state not found and state parameter ${stateHex} is not genesis state`
);
}
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);
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 parameter ${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();
}
14 changes: 8 additions & 6 deletions src/identity/identity-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -289,8 +289,7 @@ export interface IIdentityWallet {
* @param {TreeState} treeState - contains state to upgrade
* @returns `{Promise<void>}`
*/
updateIdentityState(issuerDID: DID, published:boolean, treeState?: TreeState): Promise<void>;

updateIdentityState(issuerDID: DID, published: boolean, treeState?: TreeState): Promise<void>;
}

/**
Expand All @@ -317,7 +316,7 @@ export class IdentityWallet implements IIdentityWallet {
private readonly _kms: KMS,
private readonly _storage: IDataStorage,
private readonly _credentialWallet: ICredentialWallet
) { }
) {}

/**
* {@inheritDoc IIdentityWallet.createIdentity}
Expand Down Expand Up @@ -921,15 +920,18 @@ export class IdentityWallet implements IIdentityWallet {
}

/** {@inheritDoc IIdentityWallet.updateIdentityState} */
async updateIdentityState(issuerDID: DID, published:boolean, treeState?: TreeState): Promise<void> {
async updateIdentityState(
issuerDID: DID,
published: boolean,
treeState?: TreeState
): Promise<void> {
const latestTreeState = await this.getDIDTreeModel(issuerDID);

await this._storage.identity.saveIdentity({
did: issuerDID.string(),
state: treeState ? treeState.state : latestTreeState.state,
state: treeState?.state ?? latestTreeState.state,
isStatePublished: published,
isStateGenesis: false
});
}

}
2 changes: 1 addition & 1 deletion src/proof/proof-service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -284,7 +284,7 @@ export class ProofService implements IProofService {
const txId = await stateStorage.publishState(proof, ethSigner);

await this._identityWallet.updateIdentityState(did, true, newTreeState);

return txId;
}

Expand Down
Loading

0 comments on commit 4cbc378

Please sign in to comment.