Skip to content

feat: Implement 'revoke' for Issuer #930

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

Merged
merged 14 commits into from
Jan 30, 2025
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
79 changes: 75 additions & 4 deletions packages/credentials/src/V1/KiltRevocationStatusV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,23 +9,94 @@ import { u8aEq, u8aToHex, u8aToU8a } from '@polkadot/util'
import { base58Decode, 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/types.js'
import * as CType from '../ctype/index.js'
import * as Attestation from '../attestation/index.js'
import {
assertMatchingConnection,
defaultTxSubmit,
getDelegationNodeIdForCredential,
} from './common.js'
import type { KiltCredentialV1, KiltRevocationStatusV1 } from './types.js'
import type { IssuerOptions } from '../interfaces.js'
import type { KiltCredentialV1 } from './types.js'

import { type KiltRevocationStatusV1 } from './types.js'

export type Interface = KiltRevocationStatusV1

export const STATUS_TYPE = 'KiltRevocationStatusV1'

/**
* @param credentialStatus The credential status propoerty of the Verifiable credential.
* @param opts Additional parameters.
* @param opts.api An optional polkadot-js/api instance connected to the blockchain network on which the credential is anchored.
* @param params.issuer Interfaces for interacting with the issuer identity.
* @param params.issuer.didDocument The DID Document of the issuer revoking the credential.
* @param params.issuer.signers Array of signer interfaces for credential authorization.
* @param params.issuer.submitter The submitter can be one of:
* - A MultibaseKeyPair for signing transactions
* - A Ed25519 type keypair for blockchain interactions
* The submitter will be used to cover transaction fees and blockchain operations.
* @param issuer
*/
export async function revoke(
credentialStatus: KiltRevocationStatusV1,
issuer: IssuerOptions,
opts: { api?: ApiPromise } = {}
): Promise<void> {
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"
)
}
const rootHash = base58Decode(assetInstance)

const { didDocument, signers, submitter } = issuer

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,
})

let result = await transactionPromise
if ('status' in result) {
if (result.status !== 'confirmed') {
throw new SDKErrors.SDKError(
`Unexpected transaction status ${result.status}; the transaction should be "confirmed" for issuance to continue`
)
}
result = result.asConfirmed
}
}

/**
* Check attestation and revocation status of a credential at the latest block available.
*
Expand Down
70 changes: 68 additions & 2 deletions packages/credentials/src/V1/common.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,9 +9,18 @@ import type { ApiPromise } from '@polkadot/api'
import { base58Decode, base58Encode } from '@polkadot/util-crypto'
import { hexToU8a } from '@polkadot/util'

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 { Extrinsic } from '@polkadot/types/interfaces/types.js'
import { authorizeTx, signersForDid } from '@kiltprotocol/did'
import { Blockchain } from '@kiltprotocol/chain-helpers'
import { SimplifiedTransactionResult } from '../interfaces.js'
import type { KiltAttesterDelegationV1, KiltCredentialV1 } from './types.js'

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

/**
* @param root0
* @param root0.didDocument
* @param root0.call
* @param root0.signers
* @param root0.submitter
*/
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() } }
}
54 changes: 48 additions & 6 deletions packages/credentials/src/issuer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,17 @@
* found in the LICENSE file in the root directory of this source tree.
*/

import type { Did, ICType, IClaimContents } from '@kiltprotocol/types'

import { SDKErrors } from '@kiltprotocol/utils'
import { KiltAttestationProofV1, KiltCredentialV1 } from './V1/index.js'
import type { UnsignedVc, VerifiableCredential } from './V1/types.js'
import type { CTypeLoader } from './ctype/index.js'
// eslint-disable-next-line @typescript-eslint/no-unused-vars
import type { IssuerOptions, SubmitOverride } from './interfaces.js'
import type { Did, ICType, IClaimContents } from '@kiltprotocol/types'
import type { IssuerOptions } from './interfaces.js'
import type { CTypeLoader } from './ctype/index.js'
import type { UnsignedVc, VerifiableCredential } from './V1/types.js'
import {
KiltAttestationProofV1,
KiltCredentialV1,
KiltRevocationStatusV1,
} from './V1/index.js'

export type { IssuerOptions }

Expand Down Expand Up @@ -134,3 +137,42 @@ export async function issue({
)
}
}

/**
* Revokes a Kilt credential on the blockchain, making it invalid.
*
* @param params Holds all named parameters.
* @param params.credential A credential document.
* @param params.issuer Interfaces for interacting with the issuer identity for the purpose of revoking the credential.
* @param params.issuer.didDocument The DID Document of the issuer.
* @param params.issuer.signers An array of signer interfaces, each allowing to request signatures made with a key associated with the issuer DID Document.
* The function will select the first signer that matches requirements around signature algorithm and relationship of the key to the DID as given by the DID Document.
* @param params.issuer.submitter Some proof types require making transactions to effect state changes on the KILT blockchain.
* The blockchain account whose address is specified here will be used to cover all transaction fees and deposits due for this operation.
* As transactions to the blockchain need to be signed, `signers` is expected to contain a signer interface where the `id` matches this address.
*
* Alternatively, you can pass a {@link SubmitOverride} callback that takes care of Did-authorizing and submitting the transaction.
* If you are using a service that helps you submit and pay for transactions, this is your point of integration to it.
*
* @throws If the credential format is invalid or the revocation fails.
*/
export async function revoke({
credential,
issuer,
}: {
credential: VerifiableCredential
issuer: IssuerOptions
}): Promise<VerifiableCredential> {
const status = credential.credentialStatus
if (!status || status.type !== KiltRevocationStatusV1.STATUS_TYPE) {
throw new SDKErrors.SDKError(
`Only credential status type ${KiltRevocationStatusV1.STATUS_TYPE} is currently supported.`
)
}
await KiltRevocationStatusV1.revoke(
status as KiltRevocationStatusV1.Interface,
issuer,
{}
)
return credential
}
21 changes: 21 additions & 0 deletions tests/bundle/bundle-test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,27 @@ async function runAll() {

console.log('presentation verified')

// ┏━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Revoke credential ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━┛
// Before revocation, credential status is valid.
let credentialStatus = await Kilt.Verifier.checkStatus({ credential })
if (!credentialStatus.verified) {
throw new Error('credential already revoked')
}
// Revoke a previously issued credential on chain.
await Kilt.Issuer.revoke({
issuer: { didDocument, signers, submitter },
credential,
})
console.log('credential revoked')

// After revocation, credential status is invalid.
credentialStatus = await Kilt.Verifier.checkStatus({ credential })
if (credentialStatus.verified) {
throw new Error('credential did not get revoked')
}

// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
// ┃ Remove a Verification Method ┃
// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
Expand Down
Loading