Skip to content

Commit

Permalink
publish genesis state (#133)
Browse files Browse the repository at this point in the history
* publish genesis state to RHS in case of Iden3ReverseSparseMerkleTreeProof status
  • Loading branch information
ilya-korotya authored Sep 26, 2023
1 parent dd82c0f commit 5ffd319
Show file tree
Hide file tree
Showing 6 changed files with 227 additions and 56 deletions.
46 changes: 36 additions & 10 deletions src/credentials/credential-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -66,13 +66,15 @@ export interface CredentialRequest {
* id: string;
* nonce?: number;
* type: CredentialStatusType;
* issuerState?: string;
* }}
* @memberof CredentialRequest
*/
revocationOpts: {
id: string;
nonce?: number;
type: CredentialStatusType;
issuerState?: string;
};
}

Expand Down Expand Up @@ -342,19 +344,43 @@ export class CredentialWallet implements ICredentialWallet {
type: VerifiableConstants.JSON_SCHEMA_VALIDATOR
};

const id =
request.revocationOpts.type === CredentialStatusType.SparseMerkleTreeProof
? `${request.revocationOpts.id.replace(/\/$/, '')}/${request.revocationOpts.nonce}`
: request.revocationOpts.id;

cr.credentialStatus = {
id,
revocationNonce: request.revocationOpts.nonce,
type: request.revocationOpts.type
};
cr.credentialStatus = this.buildCredentialStatus(request);

return cr;
};

/**
* Builds credential status
* @param {CredentialRequest} request
* @returns `CredentialStatus`
*/
private buildCredentialStatus(request: CredentialRequest): CredentialStatus {
const credentialStatus: CredentialStatus = {
id: request.revocationOpts.id,
type: request.revocationOpts.type,
revocationNonce: request.revocationOpts.nonce
};

switch (request.revocationOpts.type) {
case CredentialStatusType.SparseMerkleTreeProof:
return {
...credentialStatus,
id: `${credentialStatus.id.replace(/\/$/, '')}/${credentialStatus.revocationNonce}`
};
case CredentialStatusType.Iden3ReverseSparseMerkleTreeProof:
return {
...credentialStatus,
id: request.revocationOpts.issuerState
? `${credentialStatus.id.replace(/\/$/, '')}/node?state=${
request.revocationOpts.issuerState
}`
: `${credentialStatus.id.replace(/\/$/, '')}`
};
default:
return credentialStatus;
}
}

/**
* {@inheritDoc ICredentialWallet.findById}
*/
Expand Down
100 changes: 69 additions & 31 deletions src/credentials/status/reverse-sparse-merkle-tree.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,12 @@ import {
NodeAux,
ZERO_HASH,
setBitBigEndian,
testBit
testBit,
newHashFromHex
} from '@iden3/js-merkletree';
import { IStateStorage } from '../../storage';
import { CredentialStatusResolver, CredentialStatusResolveOptions } from './resolver';
import { CredentialStatus, RevocationStatus } from '../../verifiable';
import { CredentialStatus, IssuerData, RevocationStatus } from '../../verifiable';
import { strMTHex } from '../../circuits';
import { VerifiableConstants, CredentialStatusType } from '../../verifiable/constants';

Expand Down Expand Up @@ -124,28 +125,12 @@ export class RHSResolver implements CredentialStatusResolver {
}

try {
return await this.getStatus(credentialStatus, credentialStatusResolveOptions.issuerDID);
return await this.getStatus(
credentialStatus,
credentialStatusResolveOptions.issuerDID,
credentialStatusResolveOptions.issuerData
);
} catch (e: unknown) {
const errMsg = (e as { reason: string })?.reason ?? (e as Error).message ?? (e as string);
if (
!!credentialStatusResolveOptions.issuerData &&
errMsg.includes(VerifiableConstants.ERRORS.IDENTITY_DOES_NOT_EXIST) &&
isIssuerGenesis(
credentialStatusResolveOptions.issuerDID.string(),
credentialStatusResolveOptions.issuerData.state.value
)
) {
return {
mtp: new Proof(),
issuer: {
state: credentialStatusResolveOptions.issuerData.state.value,
revocationTreeRoot: credentialStatusResolveOptions.issuerData.state.revocationTreeRoot,
rootOfRoots: credentialStatusResolveOptions.issuerData.state.rootOfRoots,
claimsTreeRoot: credentialStatusResolveOptions.issuerData.state.claimsTreeRoot
}
};
}

if (credentialStatus?.statusIssuer?.type === CredentialStatusType.SparseMerkleTreeProof) {
try {
return await (await fetch(credentialStatus.id)).json();
Expand All @@ -161,21 +146,64 @@ export class RHSResolver implements CredentialStatusResolver {
* 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
issuerDID: DID,
issuerData?: IssuerData
): Promise<RevocationStatus> {
const id = DID.idFromDID(issuerDID);
const latestStateInfo = await this._state.getLatestStateById(id.bigInt());

let latestState: bigint;
try {
const latestStateInfo = await this._state.getLatestStateById(id.bigInt());
latestState = latestStateInfo?.state || BigInt(0);
} 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 {
throw e;
}
}

const rhsHost = credentialStatus.id.split('/node')[0];
const hashedRevNonce = newHashFromBigInt(BigInt(credentialStatus.revocationNonce ?? 0));
const hashedIssuerRoot = newHashFromBigInt(BigInt(latestStateInfo?.state ?? 0));
return await this.getRevocationStatusFromRHS(
hashedRevNonce,
hashedIssuerRoot,
credentialStatus.id
);
const hashedIssuerRoot = newHashFromBigInt(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?: IssuerData
): RevocationStatus {
if (!!issuerData && isIssuerGenesis(issuerDID.string(), 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 is empty`);
}

/**
Expand Down Expand Up @@ -280,6 +308,16 @@ export class RHSResolver implements CredentialStatusResolver {
}
return p;
}

/**
* 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');
}
}

/**
Expand Down
37 changes: 33 additions & 4 deletions src/identity/identity-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,7 @@ import {
CredentialStatusType,
ProofQuery
} from '../verifiable';
import { CredentialRequest, ICredentialWallet } from '../credentials';
import { pushHashesToRHS, TreesModel } from '../credentials/rhs';
import { CredentialRequest, ICredentialWallet, pushHashesToRHS, TreesModel } from '../credentials';
import { TreeState } from '../circuits';
import { byteEncoder } from '../utils';
import { Options, getDocumentLoader } from '@iden3/js-jsonld-merklization';
Expand Down Expand Up @@ -223,6 +222,20 @@ export interface IIdentityWallet {
*/
publishStateToRHS(issuerDID: DID, rhsURL: string, revokedNonces?: number[]): Promise<void>;

/**
*
*
* @param {TreesModel} treeModel - trees model to publish
* @param {string} rhsURL - reverse hash service URL
* @param {number[]} [revokedNonces] - revoked nonces for the period from the last published
* @returns `Promise<void>`
*/
publishSpecificStateToRHS(
treeModel: TreesModel,
rhsURL: string,
revokedNonces?: number[]
): Promise<void>;

/**
* Extracts core claim from signature or merkle tree proof. If both proof persists core claim must be the same
*
Expand Down Expand Up @@ -382,7 +395,8 @@ export class IdentityWallet implements IIdentityWallet {
revocationOpts: {
nonce: revNonce,
id: opts.revocationOpts.id.replace(/\/$/, ''),
type: opts.revocationOpts.type
type: opts.revocationOpts.type,
issuerState: currentState.hex()
}
};

Expand Down Expand Up @@ -421,6 +435,10 @@ export class IdentityWallet implements IIdentityWallet {

credential.proof = [mtpProof];

if (opts.revocationOpts.type === CredentialStatusType.Iden3ReverseSparseMerkleTreeProof) {
await this.publishStateToRHS(did, opts.revocationOpts.id);
}

await this._storage.identity.saveIdentity({
did: did.string(),
state: currentState,
Expand Down Expand Up @@ -630,6 +648,9 @@ export class IdentityWallet implements IIdentityWallet {
const jsonSchema = schema as JSONSchema;
let credential: W3CCredential = new W3CCredential();

const issuerRoots = await this.getDIDTreeModel(issuerDID);
req.revocationOpts.issuerState = issuerRoots.state.hex();

req.revocationOpts.nonce =
typeof req.revocationOpts.nonce === 'number'
? req.revocationOpts.nonce
Expand Down Expand Up @@ -817,10 +838,18 @@ export class IdentityWallet implements IIdentityWallet {
return credentials;
}

/** {@inheritDoc IIdentityWallet.publishSpecificStateToRHS} */
async publishSpecificStateToRHS(
treeModel: TreesModel,
rhsURL: string,
revokedNonces?: number[]
): Promise<void> {
await pushHashesToRHS(treeModel.state, treeModel, rhsURL, revokedNonces);
}

/** {@inheritDoc IIdentityWallet.publishStateToRHS} */
async publishStateToRHS(issuerDID: DID, rhsURL: string, revokedNonces?: number[]): Promise<void> {
const treeState = await this.getDIDTreeModel(issuerDID);

await pushHashesToRHS(
treeState.state,
{
Expand Down
28 changes: 28 additions & 0 deletions tests/credentials/mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -83,3 +83,31 @@ export const cred4 = createTestCredential({
expirationDate: '2023-11-11',
issuanceDate: '2022-11-11'
});

export const cred5 = createTestCredential({
id: 'test4',
'@context': ['context4'],
credentialSchema: {
id: 'credentialSchemaId',
type: 'credentialSchemaType'
},
proof: ['some proof4'],
type: ['type4'],
credentialStatus: {
id: 'https://rhs-staging.polygonid.me',
type: 'Iden3ReverseSparseMerkleTreeProof',
nonce: 10
},
issuer: 'issuer4',
credentialSubject: {
countOfFines: 0,
country: {
name: 'Spain',
code: 'ES',
insured: true,
hasOwnPackage: 'false'
}
},
expirationDate: '2023-11-11',
issuanceDate: '2022-11-11'
});
1 change: 1 addition & 0 deletions tests/handlers/fetch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -179,6 +179,7 @@ describe('fetch', () => {
};
packageMgr.pack = async (): Promise<Uint8Array> => byteEncoder.encode(mockedToken);
fetchHandler = new FetchHandler(packageMgr);
fetchMock.spy();
fetchMock.post(agentUrl, JSON.parse(mockedCredResponse));
});

Expand Down
Loading

0 comments on commit 5ffd319

Please sign in to comment.