Skip to content

Commit 99d9897

Browse files
authored
On chain RHS publishing (#160)
* On chain RHS publishing * Add credential status publisher registry
1 parent 558ca29 commit 99d9897

File tree

14 files changed

+761
-503
lines changed

14 files changed

+761
-503
lines changed

.github/workflows/ci.yaml

+2
Original file line numberDiff line numberDiff line change
@@ -58,4 +58,6 @@ jobs:
5858
WALLET_KEY: ${{ secrets.WALLET_KEY }}
5959
RPC_URL: ${{ secrets.RPC_URL }}
6060
RHS_URL: ${{ secrets.RHS_URL }}
61+
STATE_CONTRACT_ADDRESS: ${{ secrets.STATE_CONTRACT_ADDRESS }}
62+
RHS_CONTRACT_ADDRESS: ${{ secrets.RHS_CONTRACT_ADDRESS }}
6163
run: npm run test

README.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,11 @@ To run them, please set following variables:
3030

3131
```bash
3232
export WALLET_KEY="...key in hex format"
33-
export RPC_URL="...url to polygon network rpc node"
33+
export RPC_URL="...url to network rpc node"
3434
export RHS_URL="..reverse hash service url"
3535
export IPFS_URL="url for ipfs"
36+
export STATE_CONTRACT_ADDRESS="state contract address"
37+
export RHS_CONTRACT_ADDRESS="reverse hash service contract address"
3638

3739
```
3840

src/credentials/credential-wallet.ts

+21-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { DID } from '@iden3/js-iden3-core';
1+
import { DID, getChainId } from '@iden3/js-iden3-core';
22
import { IDataStorage } from '../storage/interfaces';
33
import {
44
W3CCredential,
@@ -335,7 +335,7 @@ export class CredentialWallet implements ICredentialWallet {
335335
type: VerifiableConstants.JSON_SCHEMA_VALIDATOR
336336
};
337337

338-
cr.credentialStatus = this.buildCredentialStatus(request);
338+
cr.credentialStatus = this.buildCredentialStatus(request, issuer);
339339

340340
return cr;
341341
};
@@ -345,7 +345,7 @@ export class CredentialWallet implements ICredentialWallet {
345345
* @param {CredentialRequest} request
346346
* @returns `CredentialStatus`
347347
*/
348-
private buildCredentialStatus(request: CredentialRequest): CredentialStatus {
348+
private buildCredentialStatus(request: CredentialRequest, issuer: DID): CredentialStatus {
349349
const credentialStatus: CredentialStatus = {
350350
id: request.revocationOpts.id,
351351
type: request.revocationOpts.type,
@@ -367,6 +367,24 @@ export class CredentialWallet implements ICredentialWallet {
367367
}`
368368
: `${credentialStatus.id.replace(/\/$/, '')}`
369369
};
370+
case CredentialStatusType.Iden3OnchainSparseMerkleTreeProof2023: {
371+
const issuerId = DID.idFromDID(issuer);
372+
const chainId = getChainId(DID.blockchainFromId(issuerId), DID.networkIdFromId(issuerId));
373+
const searchParams = [
374+
['revocationNonce', request.revocationOpts.nonce?.toString() || ''],
375+
['contractAddress', `${chainId}:${request.revocationOpts.id}`],
376+
['state', request.revocationOpts.issuerState || '']
377+
]
378+
.filter(([, value]) => Boolean(value))
379+
.map(([key, value]) => `${key}=${value}`)
380+
.join('&');
381+
382+
return {
383+
...credentialStatus,
384+
// `[did]:[methodid]:[chain]:[network]:[id]/credentialStatus?(revocationNonce=value)&[contractAddress=[chainID]:[contractAddress]]&(state=issuerState)`
385+
id: `${issuer.string()}/credentialStatus?${searchParams}`
386+
};
387+
}
370388
default:
371389
return credentialStatus;
372390
}

src/credentials/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ export * from './status/reverse-sparse-merkle-tree';
33
export * from './status/sparse-merkle-tree';
44
export * from './status/resolver';
55
export * from './status/agent-revocation';
6+
export * from './status/credential-status-publisher';
67
export * from './status/utils';
78
export * from './credential-wallet';
89
export * from './rhs';

src/credentials/rhs.ts

+32-22
Original file line numberDiff line numberDiff line change
@@ -1,15 +1,10 @@
1-
import {
2-
Hash,
3-
newHashFromBigInt,
4-
testBit,
5-
Merkletree,
6-
NodeLeaf,
7-
Siblings
8-
} from '@iden3/js-merkletree';
1+
import { Hash, testBit, Merkletree, NodeLeaf, Siblings } from '@iden3/js-merkletree';
92

103
import { NODE_TYPE_LEAF } from '@iden3/js-merkletree';
114
import { hashElems } from '@iden3/js-merkletree';
125
import { ProofNode } from './status/reverse-sparse-merkle-tree';
6+
import { Iden3SmtRhsCredentialStatusPublisher } from './status/credential-status-publisher';
7+
import { CredentialStatusType } from '../verifiable';
138
/**
149
* Interface to unite contains three trees: claim, revocation and rootOfRoots
1510
* Also contains the current state of identity
@@ -29,6 +24,7 @@ export interface TreesModel {
2924
* A reverse hash service (RHS) is a centralized or decentralized service for storing publicly available data about identity.
3025
* Such data are identity state and state of revocation tree and roots tree root tree.
3126
*
27+
* @deprecated Use `pushHashesToReverseHashService` instead.
3228
* @param {Hash} state - current state of identity
3329
* @param {TreesModel} trees - current trees of identity (claims, revocation, rootOfRoots )
3430
* @param {string} rhsUrl - URL of service
@@ -41,6 +37,28 @@ export async function pushHashesToRHS(
4137
rhsUrl: string,
4238
revokedNonces?: number[]
4339
): Promise<void> {
40+
const nodes = await getNodesRepresentation(revokedNonces, trees, state);
41+
const publisher = new Iden3SmtRhsCredentialStatusPublisher();
42+
await publisher.publish({
43+
nodes,
44+
credentialStatusType: CredentialStatusType.Iden3ReverseSparseMerkleTreeProof,
45+
rhsUrl: rhsUrl
46+
});
47+
}
48+
49+
/**
50+
* Retrieves the representation of nodes for generating a proof.
51+
*
52+
* @param revokedNonces - An array of revoked nonces.
53+
* @param trees - The TreesModel object containing the necessary trees.
54+
* @param state - The hash of the state.
55+
* @returns A Promise that resolves to an array of ProofNode objects.
56+
*/
57+
export async function getNodesRepresentation(
58+
revokedNonces: number[] | undefined,
59+
trees: TreesModel,
60+
state: Hash
61+
): Promise<ProofNode[]> {
4462
const nb = new NodesBuilder();
4563

4664
if (revokedNonces) {
@@ -60,16 +78,7 @@ export async function pushHashesToRHS(
6078
);
6179
}
6280

63-
if (nb.nodes.length > 0) {
64-
await saveNodes(nb.nodes, rhsUrl);
65-
}
66-
}
67-
68-
async function saveNodes(nodes: ProofNode[], nodeUrl: string): Promise<boolean> {
69-
const nodesJSON = nodes.map((n) => n.toJSON());
70-
const resp = await fetch(nodeUrl + '/node', { method: 'post', body: JSON.stringify(nodesJSON) });
71-
const status = resp.status;
72-
return status === 200;
81+
return nb.nodes;
7382
}
7483

7584
async function addRoRNode(nb: NodesBuilder, trees: TreesModel): Promise<void> {
@@ -78,6 +87,7 @@ async function addRoRNode(nb: NodesBuilder, trees: TreesModel): Promise<void> {
7887

7988
return nb.addKey(currentRootsTree, (await claimsTree.root()).bigInt());
8089
}
90+
8191
async function addRevocationNode(
8292
nb: NodesBuilder,
8393
trees: TreesModel,
@@ -105,9 +115,9 @@ class NodesBuilder {
105115
async addKey(tree: Merkletree, nodeKey: bigint): Promise<void> {
106116
const { value: nodeValue, siblings } = await tree.get(nodeKey);
107117

108-
const nodeKeyHash = newHashFromBigInt(nodeKey);
118+
const nodeKeyHash = Hash.fromBigInt(nodeKey);
109119

110-
const nodeValueHash = newHashFromBigInt(nodeValue);
120+
const nodeValueHash = Hash.fromBigInt(nodeValue);
111121

112122
const node = new NodeLeaf(nodeKeyHash, nodeValueHash);
113123
const newNodes: ProofNode[] = await buildNodesUp(siblings, node);
@@ -142,7 +152,7 @@ async function buildNodesUp(siblings: Siblings, node: NodeLeaf): Promise<ProofNo
142152
nodes[index] = new ProofNode();
143153
}
144154
nodes[sl].hash = prevHash;
145-
const hashOfOne = newHashFromBigInt(BigInt(1));
155+
const hashOfOne = Hash.fromBigInt(BigInt(1));
146156

147157
nodes[sl].children = [node.entry[0], node.entry[1], hashOfOne];
148158

@@ -157,7 +167,7 @@ async function buildNodesUp(siblings: Siblings, node: NodeLeaf): Promise<ProofNo
157167
nodes[i].children[0] = prevHash;
158168
nodes[i].children[1] = siblings[i];
159169
}
160-
nodes[i].hash = await hashElems([nodes[i].children[0].bigInt(), nodes[i].children[1].bigInt()]);
170+
nodes[i].hash = hashElems([nodes[i].children[0].bigInt(), nodes[i].children[1].bigInt()]);
161171

162172
prevHash = nodes[i].hash;
163173
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
import { TransactionReceipt } from 'ethers';
2+
import { JSONObject } from '../../iden3comm';
3+
import { OnChainRevocationStorage } from '../../storage';
4+
import { CredentialStatusType } from '../../verifiable';
5+
import { ProofNode } from './reverse-sparse-merkle-tree';
6+
7+
/**
8+
* Represents a credential status publisher.
9+
*/
10+
export interface ICredentialStatusPublisher {
11+
/**
12+
* Publishes the credential status.
13+
* @param params - The parameters for publishing the status.
14+
* @returns A promise that resolves when the status is published.
15+
*/
16+
publish(params: JSONObject): Promise<void>;
17+
}
18+
19+
/**
20+
* Registry for managing credential status publishers.
21+
*/
22+
export class CredentialStatusPublisherRegistry {
23+
private _publishers: Map<CredentialStatusType, ICredentialStatusPublisher[]> = new Map();
24+
25+
/**
26+
* Registers one or more credential status publishers for a given type.
27+
* @param type - The credential status type.
28+
* @param publisher - One or more credential status publishers.
29+
*/
30+
public register(type: CredentialStatusType, ...publisher: ICredentialStatusPublisher[]): void {
31+
const publishers = this._publishers.get(type) ?? [];
32+
publishers.push(...publisher);
33+
this._publishers.set(type, publishers);
34+
}
35+
36+
/**
37+
* Retrieves the credential status publishers for a given type.
38+
* @param type - The credential status type.
39+
* @returns An array of credential status publishers or undefined if none are registered for the given type.
40+
*/
41+
public get(type: CredentialStatusType): ICredentialStatusPublisher[] | undefined {
42+
return this._publishers.get(type);
43+
}
44+
}
45+
46+
/**
47+
* Implementation of the ICredentialStatusPublisher interface for publishing on-chain credential status.
48+
*/
49+
export class Iden3OnchainSmtCredentialStatusPublisher implements ICredentialStatusPublisher {
50+
constructor(private readonly _storage: OnChainRevocationStorage) {}
51+
52+
/**
53+
* Publishes the credential status to the blockchain.
54+
* @param params - The parameters for publishing the credential status.
55+
*/
56+
public async publish(params: {
57+
nodes: ProofNode[];
58+
credentialStatusType: CredentialStatusType;
59+
onChain?: { txCallback?: (tx: TransactionReceipt) => Promise<void> };
60+
}): Promise<void> {
61+
if (
62+
![CredentialStatusType.Iden3OnchainSparseMerkleTreeProof2023].includes(
63+
params.credentialStatusType
64+
)
65+
) {
66+
throw new Error(
67+
`On-chain publishing is not supported for credential status type ${params.credentialStatusType}`
68+
);
69+
}
70+
const nodesBigInts = params.nodes.map((n) => n.children.map((c) => c.bigInt()));
71+
72+
const txPromise = this._storage.saveNodes(nodesBigInts);
73+
74+
if (params.onChain?.txCallback) {
75+
const cb = params.onChain?.txCallback;
76+
txPromise.then((receipt) => cb(receipt));
77+
return;
78+
}
79+
80+
await txPromise;
81+
}
82+
}
83+
84+
/**
85+
* Implementation of the ICredentialStatusPublisher interface for publishing off-chain credential status.
86+
*/
87+
export class Iden3SmtRhsCredentialStatusPublisher implements ICredentialStatusPublisher {
88+
/**
89+
* Publishes the credential status to a specified node URL.
90+
* @param params - The parameters for publishing the credential status.
91+
* @param params.nodes - The proof nodes to be published.
92+
* @param params.rhsUrl - The URL of the node to publish the credential status to.
93+
* @returns A promise that resolves when the credential status is successfully published.
94+
* @throws An error if the publishing fails.
95+
*/
96+
public async publish(params: {
97+
nodes: ProofNode[];
98+
credentialStatusType: CredentialStatusType;
99+
100+
rhsUrl: string;
101+
}): Promise<void> {
102+
if (
103+
![CredentialStatusType.Iden3ReverseSparseMerkleTreeProof].includes(
104+
params.credentialStatusType
105+
)
106+
) {
107+
throw new Error(
108+
`On-chain publishing is not supported for credential status type ${params.credentialStatusType}`
109+
);
110+
}
111+
const nodesJSON = params.nodes.map((n) => n.toJSON());
112+
const resp = await fetch(params.rhsUrl + '/node', {
113+
method: 'post',
114+
body: JSON.stringify(nodesJSON)
115+
});
116+
if (resp.status !== 200) {
117+
throw new Error(`Failed to publish credential status. Status: ${resp.status}`);
118+
}
119+
}
120+
}

src/credentials/utils.ts

+14
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
import { DID } from '@iden3/js-iden3-core';
22
import { W3CCredential } from '../verifiable';
3+
import { PublicKey } from '@iden3/js-crypto';
4+
import { KmsKeyId, KmsKeyType, keyPath } from '../kms';
35

46
/**
57
* Retrieves the user DID from a given credential.
@@ -20,3 +22,15 @@ export const getUserDIDFromCredential = (issuerDID: DID, credential: W3CCredenti
2022
}
2123
return DID.parse(credential.credentialSubject.id);
2224
};
25+
26+
export const getKMSIdByAuthCredential = (credential: W3CCredential): KmsKeyId => {
27+
if (!credential.type.includes('AuthBJJCredential')) {
28+
throw new Error("can't sign with not AuthBJJCredential credential");
29+
}
30+
const x = credential.credentialSubject['x'] as unknown as string;
31+
const y = credential.credentialSubject['y'] as unknown as string;
32+
33+
const pb: PublicKey = new PublicKey([BigInt(x), BigInt(y)]);
34+
const kp = keyPath(KmsKeyType.BabyJubJub, pb.hex());
35+
return { type: KmsKeyType.BabyJubJub, id: kp };
36+
};

0 commit comments

Comments
 (0)