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

publish genesis state #133

Merged
merged 5 commits into from
Sep 26, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
44 changes: 34 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,41 @@ 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:
credentialStatus.id = `${credentialStatus.id.replace(/\/$/, '')}/${
credentialStatus.revocationNonce
}`;
break;
case CredentialStatusType.Iden3ReverseSparseMerkleTreeProof:
credentialStatus.id = request.revocationOpts.issuerState
? `${credentialStatus.id.replace(/\/$/, '')}/node?state=${
request.revocationOpts.issuerState
}`
: `${credentialStatus.id.replace(/\/$/, '')}`;
break;
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would expect default keyword which throws Error

Copy link
Contributor Author

@ilya-korotya ilya-korotya Sep 13, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For SparseMerkleTreeProof, Iden3commRevocationStatusV1, Iden3ReverseSparseMerkleTreeProof types we have to modify the id field. All other types remain unchanged

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please add default keyword to switch statement which will throw an Error in case for some reasons request.revocationOpts.type none of available cases

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

How I understand this regexp cuts end '/'. We should cut the last '/' for the agent endpoint also to be sure that SDK will not create a URL like https/agent//

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Kolezhniuk @vmidylli about the default case. I disagree that we should return an error if we have an unknown type.

  1. What if the user implements a custom revocation type?
  2. We have strict typing to protect against dummy users.
  3. SDK should be flexible. All validation should be on the user side. Because we cannot know all the use cases of this SDK

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@ilya-korotya ok, in case we don't need to throw an Error, I would expect default statement in any case where you need to use switch to make logic flow explicitly

default:
        return credentialStatus;

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

/**
   * 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:
      case CredentialStatusType.Iden3commRevocationStatusV1:
        return {
          ...credentialStatus,
          id: `${request.revocationOpts.id.replace(/\/$/, '')}/${request.revocationOpts.nonce}`
        };
      case CredentialStatusType.Iden3ReverseSparseMerkleTreeProof:
        return {
          ...credentialStatus,
          id: request.revocationOpts.issuerState
            ? `${request.revocationOpts.id}/node?state=${request.revocationOpts.issuerState}`
            : `${request.revocationOpts.id}`
        };
      default:
        return credentialStatus;
    }
  }

}

return credentialStatus;
}

/**
* {@inheritDoc ICredentialWallet.findById}
*/
Expand Down
2 changes: 1 addition & 1 deletion src/credentials/rhs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ export interface TreesModel {
* @param {number[]} [revokedNonces] - revoked nonces since last published info
* @returns void
*/
export async function pushHashesToRHS(
export async function publishStateToRHS(
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are you sure that we want to check public interface here? I think it is better to live it as is.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@vmidyllic we discussed it. And we decided to add export to this function and call it inside on identity-wallet.ts

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

it was exported already, so this is a problem of renaming already exported function

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I returned old name

state: Hash,
trees: TreesModel,
rhsUrl: string,
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
60 changes: 55 additions & 5 deletions src/identity/identity-wallet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,8 +34,12 @@ import {
CredentialStatusType,
ProofQuery
} from '../verifiable';
import { CredentialRequest, ICredentialWallet } from '../credentials';
import { pushHashesToRHS, TreesModel } from '../credentials/rhs';
import {
CredentialRequest,
ICredentialWallet,
publishStateToRHS,
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 +227,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 @@ -370,7 +388,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 @@ -409,6 +428,26 @@ export class IdentityWallet implements IIdentityWallet {

credential.proof = [mtpProof];

if (opts.revocationOpts.type === CredentialStatusType.Iden3ReverseSparseMerkleTreeProof) {
const revocationTree = await this._storage.mt.getMerkleTreeByIdentifierAndType(
did.string(),
MerkleTreeType.Revocations
);

const rootOfRootsTree = await this._storage.mt.getMerkleTreeByIdentifierAndType(
did.string(),
MerkleTreeType.Roots
);

const trees: TreesModel = {
state: currentState,
claimsTree: claimsTree,
revocationTree: revocationTree,
rootsTree: rootOfRootsTree
};
await publishStateToRHS(currentState, trees, opts.revocationOpts.id);
}

await this._storage.identity.saveIdentity({
did: did.string(),
state: currentState,
Expand Down Expand Up @@ -618,6 +657,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 @@ -803,11 +845,19 @@ export class IdentityWallet implements IIdentityWallet {
return credentials;
}

/** {@inheritDoc IIdentityWallet.publishSpecificStateToRHS} */
async publishSpecificStateToRHS(
treeModel: TreesModel,
rhsURL: string,
revokedNonces?: number[]
): Promise<void> {
await publishStateToRHS(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(
await publishStateToRHS(
treeState.state,
{
revocationTree: treeState.revocationTree,
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