Skip to content

Commit

Permalink
feat: Implement 'revoke' for Issuer (#930)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* chore: improvements

* fix: code review

* chore: remove unnecessary docs

* fix: docs

* chore: remove test result file

---------

Co-authored-by: Aybars Göktuğ Ayan <[email protected]>
Co-authored-by: Aybars Göktuğ Ayan <[email protected]>
Co-authored-by: Raphael Flechtner <[email protected]>
  • Loading branch information
4 people authored Jan 30, 2025
1 parent 76f490e commit 74f4d5c
Show file tree
Hide file tree
Showing 5 changed files with 253 additions and 93 deletions.
65 changes: 3 additions & 62 deletions packages/credentials/src/V1/KiltAttestationProofV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,30 +30,20 @@ import type {
FrameSystemEventRecord,
RuntimeCommonAuthorizationAuthorizationId,
} from '@kiltprotocol/augment-api'
import { Blockchain } from '@kiltprotocol/chain-helpers'
import { ConfigService } from '@kiltprotocol/config'
import {
authorizeTx,
fromChain,
getFullDid,
signersForDid,
validateDid,
} from '@kiltprotocol/did'
import { fromChain, getFullDid, validateDid } from '@kiltprotocol/did'
import type {
Did,
ICType,
IDelegationNode,
KiltAddress,
SharedArguments,
SignerInterface,
} from '@kiltprotocol/types'
import { Caip19, JsonSchema, SDKErrors, Signers } from '@kiltprotocol/utils'
import { Caip19, JsonSchema, SDKErrors } from '@kiltprotocol/utils'

import { CTypeLoader } from '../ctype/CTypeLoader.js'
import * as CType from '../ctype/index.js'
import {
IssuerOptions,
SimplifiedTransactionResult,
// eslint-disable-next-line @typescript-eslint/no-unused-vars
SubmitOverride,
} from '../interfaces.js'
Expand All @@ -67,6 +57,7 @@ import {
assertMatchingConnection,
credentialIdFromRootHash,
credentialIdToRootHash,
defaultTxSubmit,
delegationIdFromAttesterDelegation,
ExpandedContents,
getDelegationNodeIdForCredential,
Expand Down Expand Up @@ -662,56 +653,6 @@ export function finalizeProof(
}
}

async function defaultTxSubmit({
didDocument,
call,
signers,
submitter,
}: SharedArguments & {
call: Extrinsic
}): Promise<SimplifiedTransactionResult> {
let submitterAddress: KiltAddress
let accountSigners: SignerInterface[] = []
if (typeof submitter === 'string') {
submitterAddress = submitter
accountSigners = (
await Promise.all(
signers.map((keypair) =>
'algorithm' in keypair
? [keypair]
: Signers.getSignersForKeypair({ keypair })
)
)
).flat()
} else if ('algorithm' in submitter) {
submitterAddress = submitter.id
accountSigners = [submitter]
} else {
accountSigners = await Signers.getSignersForKeypair({
keypair: submitter,
})
submitterAddress = accountSigners[0].id as KiltAddress
}

let extrinsic = await authorizeTx(
didDocument,
call,
await signersForDid(didDocument, ...signers),
submitterAddress
)

if (!extrinsic.isSigned) {
extrinsic = await extrinsic.signAsync(submitterAddress, {
signer: Signers.getPolkadotSigner(accountSigners),
})
}
const result = await Blockchain.submitSignedTx(extrinsic, {
resolveOn: Blockchain.IS_FINALIZED,
})
const blockHash = result.status.asFinalized
return { block: { hash: blockHash.toHex() } }
}

/**
*
* Creates a complete {@link KiltAttestationProofV1} for issuing a new credential.
Expand Down
99 changes: 77 additions & 22 deletions packages/credentials/src/V1/KiltRevocationStatusV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,97 @@
*/

import { u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util'
import { base58Decode, base58Encode } from '@polkadot/util-crypto'
import { base58Encode } from '@polkadot/util-crypto'
import type { ApiPromise } from '@polkadot/api'
import type { U8aLike } from '@polkadot/util/types'

import { ConfigService } from '@kiltprotocol/config'
import type { Caip2ChainId } from '@kiltprotocol/types'
import type { Caip2ChainId, SharedArguments } from '@kiltprotocol/types'
import { Caip2, SDKErrors } from '@kiltprotocol/utils'

import { Extrinsic } from '@polkadot/types/interfaces/'
import * as CType from '../ctype/index.js'
import * as Attestation from '../attestation/index.js'
import {
assertMatchingConnection,
defaultTxSubmit,
getDelegationNodeIdForCredential,
getRootHashFromStatusId,
} from './common.js'
import type { IssuerOptions } from '../interfaces.js'
import type { KiltCredentialV1, KiltRevocationStatusV1 } from './types.js'

export type Interface = KiltRevocationStatusV1

export const STATUS_TYPE = 'KiltRevocationStatusV1'

/**
* Revokes a Verifiable Credential containing a KiltRevocationStatusV1.
*
* @param credentialStatus The `credentialStatus` property of the Verifiable Credential.
* @param issuer
* @param issuer.didDocument The DID Document of the issuer revoking the credential.
* @param issuer.signers Array of signer interfaces for credential authorization.
* @param issuer.submitter The submitter can be one of:
* - A MultibaseKeyPair for signing transactions.
* - A `KeyringPair` for blockchain interactions.
* The submitter will be used to cover transaction fees and blockchain operations.
* @param opts Additional parameters.
* @param opts.api An optional polkadot-js/api instance connected to the blockchain network on which the credential is anchored.
*/
export async function revoke(
credentialStatus: KiltRevocationStatusV1,
issuer: IssuerOptions,
opts: { api?: ApiPromise } = {}
): Promise<void> {
const rootHash = getRootHashFromStatusId(credentialStatus, opts)
const { api = ConfigService.get('api') } = opts
const { didDocument, signers, submitter } = issuer

// TODO: Support revocations through delegation.
// In this case, the second parameter in this function would needs to be populated.
const call = api.tx.attestation.revoke(rootHash, null)

const args: Pick<SharedArguments, 'didDocument' | 'api' | 'signers'> & {
call: Extrinsic
} = {
didDocument,
signers,
api,
call,
}
const transactionPromise =
typeof submitter === 'function'
? submitter(args)
: defaultTxSubmit({
...args,
submitter,
})

const result = await transactionPromise
if ('status' in result) {
let error: Error | undefined
switch (result.status) {
case 'confirmed':
return
case 'failed':
error = result.asFailed.error
break
case 'rejected':
error = result.asRejected.error
break
case 'unknown':
error = result.asUnknown.error
break
default:
break
}
throw (
error ??
new SDKErrors.SDKError(
`Revocation failed with transaction status ${result?.status}`
)
)
}
}

/**
* Check attestation and revocation status of a credential at the latest block available.
*
Expand All @@ -39,24 +110,8 @@ export async function check(
opts: { api?: ApiPromise } = {}
): Promise<void> {
const { credentialStatus } = credential
if (credentialStatus?.type !== STATUS_TYPE)
throw new TypeError(
`The credential must have a credentialStatus of type ${STATUS_TYPE}`
)
const rootHash = getRootHashFromStatusId(credentialStatus, opts)
const { api = ConfigService.get('api') } = opts
const { assetNamespace, assetReference, assetInstance } =
assertMatchingConnection(api, credential)
if (assetNamespace !== 'kilt' || assetReference !== 'attestation') {
throw new Error(
`Cannot handle revocation status checks for asset type ${assetNamespace}:${assetReference}`
)
}
if (!assetInstance) {
throw new SDKErrors.CredentialMalformedError(
"The attestation record's CAIP-19 identifier must contain an asset index ('token_id') decoding to the credential root hash"
)
}
const rootHash = base58Decode(assetInstance)
const encoded = await api.query.attestation.attestations(rootHash)
if (encoded.isNone)
throw new SDKErrors.CredentialUnverifiableError(
Expand Down
108 changes: 105 additions & 3 deletions packages/credentials/src/V1/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,11 +8,26 @@
import type { ApiPromise } from '@polkadot/api'
import { base58Decode, base58Encode } from '@polkadot/util-crypto'
import { hexToU8a } from '@polkadot/util'
import { ConfigService } from '@kiltprotocol/config'

import type { HexString } from '@kiltprotocol/types'
import { Caip19, Caip2, SDKErrors } from '@kiltprotocol/utils'
import type {
HexString,
KiltAddress,
SharedArguments,
SignerInterface,
} from '@kiltprotocol/types'
import { Caip19, Caip2, SDKErrors, Signers } from '@kiltprotocol/utils'

import type { KiltAttesterDelegationV1, KiltCredentialV1 } from './types.js'
import { authorizeTx, signersForDid } from '@kiltprotocol/did'
import { Blockchain } from '@kiltprotocol/chain-helpers'
import { Extrinsic } from '@polkadot/types/interfaces'
import type { SimplifiedTransactionResult } from '../interfaces.js'
import type {
KiltAttesterDelegationV1,
KiltCredentialV1,
KiltRevocationStatusV1,
} from './types.js'
import { STATUS_TYPE } from './KiltRevocationStatusV1.js'

export const spiritnetGenesisHash = hexToU8a(
'0x411f057b9107718c9624d6aa4a3f23c1653898297f3d4d529d9bb6511a39dd21'
Expand Down Expand Up @@ -154,3 +169,90 @@ export function credentialIdFromRootHash(
const bytes = typeof rootHash === 'string' ? hexToU8a(rootHash) : rootHash
return `${KILT_CREDENTIAL_IRI_PREFIX}${base58Encode(bytes, false)}`
}

/**
* @param root0
* @param root0.didDocument DID Document of the authorizing DID.
* @param root0.call Extrinsic to be submitted.
* @param root0.signers An array of signer interfaces, each allowing to request signatures made with a key associated with the issuer DID Document.
* @param root0.submitter Submitter to cover the transaction.
* @private
*/
export async function defaultTxSubmit({
didDocument,
call,
signers,
submitter,
}: SharedArguments & {
call: Extrinsic
}): Promise<SimplifiedTransactionResult> {
let submitterAddress: KiltAddress
let accountSigners: SignerInterface[] = []
if (typeof submitter === 'string') {
submitterAddress = submitter
accountSigners = (
await Promise.all(
signers.map((keypair) =>
'algorithm' in keypair
? [keypair]
: Signers.getSignersForKeypair({ keypair })
)
)
).flat()
} else if ('algorithm' in submitter) {
submitterAddress = submitter.id
accountSigners = [submitter]
} else {
accountSigners = await Signers.getSignersForKeypair({
keypair: submitter,
})
submitterAddress = accountSigners[0].id as KiltAddress
}

let extrinsic = await authorizeTx(
didDocument,
call,
await signersForDid(didDocument, ...signers),
submitterAddress
)

if (!extrinsic.isSigned) {
extrinsic = await extrinsic.signAsync(submitterAddress, {
signer: Signers.getPolkadotSigner(accountSigners),
})
}
const result = await Blockchain.submitSignedTx(extrinsic, {
resolveOn: Blockchain.IS_FINALIZED,
})
const blockHash = result.status.asFinalized
return { block: { hash: blockHash.toHex() } }
}

/**
* @param credentialStatus Credential revocation status.
* @param opts
* @param opts.api Overrides the userd Kilt API.
*/
export function getRootHashFromStatusId(
credentialStatus: KiltRevocationStatusV1,
opts: { api?: ApiPromise } = {}
) {
if (credentialStatus?.type !== STATUS_TYPE)
throw new TypeError(
`The credential must have a credentialStatus of type ${STATUS_TYPE}`
)
const { api = ConfigService.get('api') } = opts
const { assetNamespace, assetReference, assetInstance } =
assertMatchingConnection(api, { credentialStatus })
if (assetNamespace !== 'kilt' || assetReference !== 'attestation') {
throw new Error(
`Cannot handle revocation status checks for asset type ${assetNamespace}:${assetReference}`
)
}
if (!assetInstance) {
throw new SDKErrors.CredentialMalformedError(
"The attestation record's CAIP-19 identifier must contain an asset index ('token_id') decoding to the credential root hash"
)
}
return base58Decode(assetInstance)
}
Loading

0 comments on commit 74f4d5c

Please sign in to comment.