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
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
Loading