Skip to content

Commit 74f4d5c

Browse files
abdulmthaybarsayanAybars Göktuğ Ayanrflechtner
authored
feat: Implement 'revoke' for Issuer (#930)
* feat: revoke in issuer * fix: review notes * fix: merge-fix * fix: relocation of revoke func * feat: improve revocation implementation * fix: lint issues * fix: remove unused docs * Apply suggestions from code review Co-authored-by: Raphael Flechtner <39338561+rflechtner@users.noreply.github.com> * chore: improvements * fix: code review * chore: remove unnecessary docs * fix: docs * chore: remove test result file --------- Co-authored-by: Aybars Göktuğ Ayan <aybarsayan@gmail.com> Co-authored-by: Aybars Göktuğ Ayan <aybarsgoktugayan@Aybarss-MacBook-Air.local> Co-authored-by: Raphael Flechtner <39338561+rflechtner@users.noreply.github.com>
1 parent 76f490e commit 74f4d5c

5 files changed

Lines changed: 253 additions & 93 deletions

File tree

packages/credentials/src/V1/KiltAttestationProofV1.ts

Lines changed: 3 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -30,30 +30,20 @@ import type {
3030
FrameSystemEventRecord,
3131
RuntimeCommonAuthorizationAuthorizationId,
3232
} from '@kiltprotocol/augment-api'
33-
import { Blockchain } from '@kiltprotocol/chain-helpers'
3433
import { ConfigService } from '@kiltprotocol/config'
35-
import {
36-
authorizeTx,
37-
fromChain,
38-
getFullDid,
39-
signersForDid,
40-
validateDid,
41-
} from '@kiltprotocol/did'
34+
import { fromChain, getFullDid, validateDid } from '@kiltprotocol/did'
4235
import type {
4336
Did,
4437
ICType,
4538
IDelegationNode,
46-
KiltAddress,
4739
SharedArguments,
48-
SignerInterface,
4940
} from '@kiltprotocol/types'
50-
import { Caip19, JsonSchema, SDKErrors, Signers } from '@kiltprotocol/utils'
41+
import { Caip19, JsonSchema, SDKErrors } from '@kiltprotocol/utils'
5142

5243
import { CTypeLoader } from '../ctype/CTypeLoader.js'
5344
import * as CType from '../ctype/index.js'
5445
import {
5546
IssuerOptions,
56-
SimplifiedTransactionResult,
5747
// eslint-disable-next-line @typescript-eslint/no-unused-vars
5848
SubmitOverride,
5949
} from '../interfaces.js'
@@ -67,6 +57,7 @@ import {
6757
assertMatchingConnection,
6858
credentialIdFromRootHash,
6959
credentialIdToRootHash,
60+
defaultTxSubmit,
7061
delegationIdFromAttesterDelegation,
7162
ExpandedContents,
7263
getDelegationNodeIdForCredential,
@@ -662,56 +653,6 @@ export function finalizeProof(
662653
}
663654
}
664655

665-
async function defaultTxSubmit({
666-
didDocument,
667-
call,
668-
signers,
669-
submitter,
670-
}: SharedArguments & {
671-
call: Extrinsic
672-
}): Promise<SimplifiedTransactionResult> {
673-
let submitterAddress: KiltAddress
674-
let accountSigners: SignerInterface[] = []
675-
if (typeof submitter === 'string') {
676-
submitterAddress = submitter
677-
accountSigners = (
678-
await Promise.all(
679-
signers.map((keypair) =>
680-
'algorithm' in keypair
681-
? [keypair]
682-
: Signers.getSignersForKeypair({ keypair })
683-
)
684-
)
685-
).flat()
686-
} else if ('algorithm' in submitter) {
687-
submitterAddress = submitter.id
688-
accountSigners = [submitter]
689-
} else {
690-
accountSigners = await Signers.getSignersForKeypair({
691-
keypair: submitter,
692-
})
693-
submitterAddress = accountSigners[0].id as KiltAddress
694-
}
695-
696-
let extrinsic = await authorizeTx(
697-
didDocument,
698-
call,
699-
await signersForDid(didDocument, ...signers),
700-
submitterAddress
701-
)
702-
703-
if (!extrinsic.isSigned) {
704-
extrinsic = await extrinsic.signAsync(submitterAddress, {
705-
signer: Signers.getPolkadotSigner(accountSigners),
706-
})
707-
}
708-
const result = await Blockchain.submitSignedTx(extrinsic, {
709-
resolveOn: Blockchain.IS_FINALIZED,
710-
})
711-
const blockHash = result.status.asFinalized
712-
return { block: { hash: blockHash.toHex() } }
713-
}
714-
715656
/**
716657
*
717658
* Creates a complete {@link KiltAttestationProofV1} for issuing a new credential.

packages/credentials/src/V1/KiltRevocationStatusV1.ts

Lines changed: 77 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,26 +6,97 @@
66
*/
77

88
import { u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util'
9-
import { base58Decode, base58Encode } from '@polkadot/util-crypto'
9+
import { base58Encode } from '@polkadot/util-crypto'
1010
import type { ApiPromise } from '@polkadot/api'
1111
import type { U8aLike } from '@polkadot/util/types'
12-
1312
import { ConfigService } from '@kiltprotocol/config'
14-
import type { Caip2ChainId } from '@kiltprotocol/types'
13+
import type { Caip2ChainId, SharedArguments } from '@kiltprotocol/types'
1514
import { Caip2, SDKErrors } from '@kiltprotocol/utils'
16-
15+
import { Extrinsic } from '@polkadot/types/interfaces/'
1716
import * as CType from '../ctype/index.js'
1817
import * as Attestation from '../attestation/index.js'
1918
import {
20-
assertMatchingConnection,
19+
defaultTxSubmit,
2120
getDelegationNodeIdForCredential,
21+
getRootHashFromStatusId,
2222
} from './common.js'
23+
import type { IssuerOptions } from '../interfaces.js'
2324
import type { KiltCredentialV1, KiltRevocationStatusV1 } from './types.js'
2425

2526
export type Interface = KiltRevocationStatusV1
2627

2728
export const STATUS_TYPE = 'KiltRevocationStatusV1'
2829

30+
/**
31+
* Revokes a Verifiable Credential containing a KiltRevocationStatusV1.
32+
*
33+
* @param credentialStatus The `credentialStatus` property of the Verifiable Credential.
34+
* @param issuer
35+
* @param issuer.didDocument The DID Document of the issuer revoking the credential.
36+
* @param issuer.signers Array of signer interfaces for credential authorization.
37+
* @param issuer.submitter The submitter can be one of:
38+
* - A MultibaseKeyPair for signing transactions.
39+
* - A `KeyringPair` for blockchain interactions.
40+
* The submitter will be used to cover transaction fees and blockchain operations.
41+
* @param opts Additional parameters.
42+
* @param opts.api An optional polkadot-js/api instance connected to the blockchain network on which the credential is anchored.
43+
*/
44+
export async function revoke(
45+
credentialStatus: KiltRevocationStatusV1,
46+
issuer: IssuerOptions,
47+
opts: { api?: ApiPromise } = {}
48+
): Promise<void> {
49+
const rootHash = getRootHashFromStatusId(credentialStatus, opts)
50+
const { api = ConfigService.get('api') } = opts
51+
const { didDocument, signers, submitter } = issuer
52+
53+
// TODO: Support revocations through delegation.
54+
// In this case, the second parameter in this function would needs to be populated.
55+
const call = api.tx.attestation.revoke(rootHash, null)
56+
57+
const args: Pick<SharedArguments, 'didDocument' | 'api' | 'signers'> & {
58+
call: Extrinsic
59+
} = {
60+
didDocument,
61+
signers,
62+
api,
63+
call,
64+
}
65+
const transactionPromise =
66+
typeof submitter === 'function'
67+
? submitter(args)
68+
: defaultTxSubmit({
69+
...args,
70+
submitter,
71+
})
72+
73+
const result = await transactionPromise
74+
if ('status' in result) {
75+
let error: Error | undefined
76+
switch (result.status) {
77+
case 'confirmed':
78+
return
79+
case 'failed':
80+
error = result.asFailed.error
81+
break
82+
case 'rejected':
83+
error = result.asRejected.error
84+
break
85+
case 'unknown':
86+
error = result.asUnknown.error
87+
break
88+
default:
89+
break
90+
}
91+
throw (
92+
error ??
93+
new SDKErrors.SDKError(
94+
`Revocation failed with transaction status ${result?.status}`
95+
)
96+
)
97+
}
98+
}
99+
29100
/**
30101
* Check attestation and revocation status of a credential at the latest block available.
31102
*
@@ -39,24 +110,8 @@ export async function check(
39110
opts: { api?: ApiPromise } = {}
40111
): Promise<void> {
41112
const { credentialStatus } = credential
42-
if (credentialStatus?.type !== STATUS_TYPE)
43-
throw new TypeError(
44-
`The credential must have a credentialStatus of type ${STATUS_TYPE}`
45-
)
113+
const rootHash = getRootHashFromStatusId(credentialStatus, opts)
46114
const { api = ConfigService.get('api') } = opts
47-
const { assetNamespace, assetReference, assetInstance } =
48-
assertMatchingConnection(api, credential)
49-
if (assetNamespace !== 'kilt' || assetReference !== 'attestation') {
50-
throw new Error(
51-
`Cannot handle revocation status checks for asset type ${assetNamespace}:${assetReference}`
52-
)
53-
}
54-
if (!assetInstance) {
55-
throw new SDKErrors.CredentialMalformedError(
56-
"The attestation record's CAIP-19 identifier must contain an asset index ('token_id') decoding to the credential root hash"
57-
)
58-
}
59-
const rootHash = base58Decode(assetInstance)
60115
const encoded = await api.query.attestation.attestations(rootHash)
61116
if (encoded.isNone)
62117
throw new SDKErrors.CredentialUnverifiableError(

packages/credentials/src/V1/common.ts

Lines changed: 105 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,26 @@
88
import type { ApiPromise } from '@polkadot/api'
99
import { base58Decode, base58Encode } from '@polkadot/util-crypto'
1010
import { hexToU8a } from '@polkadot/util'
11+
import { ConfigService } from '@kiltprotocol/config'
1112

12-
import type { HexString } from '@kiltprotocol/types'
13-
import { Caip19, Caip2, SDKErrors } from '@kiltprotocol/utils'
13+
import type {
14+
HexString,
15+
KiltAddress,
16+
SharedArguments,
17+
SignerInterface,
18+
} from '@kiltprotocol/types'
19+
import { Caip19, Caip2, SDKErrors, Signers } from '@kiltprotocol/utils'
1420

15-
import type { KiltAttesterDelegationV1, KiltCredentialV1 } from './types.js'
21+
import { authorizeTx, signersForDid } from '@kiltprotocol/did'
22+
import { Blockchain } from '@kiltprotocol/chain-helpers'
23+
import { Extrinsic } from '@polkadot/types/interfaces'
24+
import type { SimplifiedTransactionResult } from '../interfaces.js'
25+
import type {
26+
KiltAttesterDelegationV1,
27+
KiltCredentialV1,
28+
KiltRevocationStatusV1,
29+
} from './types.js'
30+
import { STATUS_TYPE } from './KiltRevocationStatusV1.js'
1631

1732
export const spiritnetGenesisHash = hexToU8a(
1833
'0x411f057b9107718c9624d6aa4a3f23c1653898297f3d4d529d9bb6511a39dd21'
@@ -154,3 +169,90 @@ export function credentialIdFromRootHash(
154169
const bytes = typeof rootHash === 'string' ? hexToU8a(rootHash) : rootHash
155170
return `${KILT_CREDENTIAL_IRI_PREFIX}${base58Encode(bytes, false)}`
156171
}
172+
173+
/**
174+
* @param root0
175+
* @param root0.didDocument DID Document of the authorizing DID.
176+
* @param root0.call Extrinsic to be submitted.
177+
* @param root0.signers An array of signer interfaces, each allowing to request signatures made with a key associated with the issuer DID Document.
178+
* @param root0.submitter Submitter to cover the transaction.
179+
* @private
180+
*/
181+
export async function defaultTxSubmit({
182+
didDocument,
183+
call,
184+
signers,
185+
submitter,
186+
}: SharedArguments & {
187+
call: Extrinsic
188+
}): Promise<SimplifiedTransactionResult> {
189+
let submitterAddress: KiltAddress
190+
let accountSigners: SignerInterface[] = []
191+
if (typeof submitter === 'string') {
192+
submitterAddress = submitter
193+
accountSigners = (
194+
await Promise.all(
195+
signers.map((keypair) =>
196+
'algorithm' in keypair
197+
? [keypair]
198+
: Signers.getSignersForKeypair({ keypair })
199+
)
200+
)
201+
).flat()
202+
} else if ('algorithm' in submitter) {
203+
submitterAddress = submitter.id
204+
accountSigners = [submitter]
205+
} else {
206+
accountSigners = await Signers.getSignersForKeypair({
207+
keypair: submitter,
208+
})
209+
submitterAddress = accountSigners[0].id as KiltAddress
210+
}
211+
212+
let extrinsic = await authorizeTx(
213+
didDocument,
214+
call,
215+
await signersForDid(didDocument, ...signers),
216+
submitterAddress
217+
)
218+
219+
if (!extrinsic.isSigned) {
220+
extrinsic = await extrinsic.signAsync(submitterAddress, {
221+
signer: Signers.getPolkadotSigner(accountSigners),
222+
})
223+
}
224+
const result = await Blockchain.submitSignedTx(extrinsic, {
225+
resolveOn: Blockchain.IS_FINALIZED,
226+
})
227+
const blockHash = result.status.asFinalized
228+
return { block: { hash: blockHash.toHex() } }
229+
}
230+
231+
/**
232+
* @param credentialStatus Credential revocation status.
233+
* @param opts
234+
* @param opts.api Overrides the userd Kilt API.
235+
*/
236+
export function getRootHashFromStatusId(
237+
credentialStatus: KiltRevocationStatusV1,
238+
opts: { api?: ApiPromise } = {}
239+
) {
240+
if (credentialStatus?.type !== STATUS_TYPE)
241+
throw new TypeError(
242+
`The credential must have a credentialStatus of type ${STATUS_TYPE}`
243+
)
244+
const { api = ConfigService.get('api') } = opts
245+
const { assetNamespace, assetReference, assetInstance } =
246+
assertMatchingConnection(api, { credentialStatus })
247+
if (assetNamespace !== 'kilt' || assetReference !== 'attestation') {
248+
throw new Error(
249+
`Cannot handle revocation status checks for asset type ${assetNamespace}:${assetReference}`
250+
)
251+
}
252+
if (!assetInstance) {
253+
throw new SDKErrors.CredentialMalformedError(
254+
"The attestation record's CAIP-19 identifier must contain an asset index ('token_id') decoding to the credential root hash"
255+
)
256+
}
257+
return base58Decode(assetInstance)
258+
}

0 commit comments

Comments
 (0)