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
97 changes: 75 additions & 22 deletions packages/credentials/src/V1/KiltRevocationStatusV1.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,95 @@
*/

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/types.js'
import * as CType from '../ctype/index.js'
import * as Attestation from '../attestation/index.js'
import {
assertMatchingConnection,
defaultTxSubmit,
getDelegationNodeIdForCredential,
getRootHash,
} 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 opts Additional parameters.
* @param opts.api An optional polkadot-js/api instance connected to the blockchain network on which the credential is anchored.
* @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.
*/
export async function revoke(
credentialStatus: KiltRevocationStatusV1,
issuer: IssuerOptions,
opts: { api?: ApiPromise } = {}
): Promise<void> {
const rootHash = getRootHash(credentialStatus, opts)
const { api = ConfigService.get('api') } = opts
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,
})

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 +108,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 = getRootHash(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 { 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,
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
* @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() } }
}

/**
* @param credentialStatus
* @param issuer
* @param opts
* @param opts.api
*/
export function getRootHash(
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