Skip to content
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

feat: new high-level credentialing interfaces #811

Merged
merged 25 commits into from
Dec 5, 2023
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
7378a18
refactor!: move credential related functionality in one folder
rflechtner Nov 16, 2023
b0db96d
chore: lint fix on utils
rflechtner Nov 20, 2023
b246704
feat: allow signing of transactions with SignerInterface
rflechtner Nov 20, 2023
45d0530
refactor: use Date for timestamp
rflechtner Nov 21, 2023
b3c7e08
refactor!: update issue submission options
rflechtner Nov 21, 2023
44343be
refactor!: separate presentation creation and signing
rflechtner Nov 23, 2023
341f4b8
feat: credential issuance interfaces
rflechtner Nov 23, 2023
f6e5c12
feat: holder credential functions
rflechtner Nov 27, 2023
5342e78
refactor: switch to json pointer instead of path
rflechtner Nov 27, 2023
da60f26
chore: improve typing, use getProof
rflechtner Nov 27, 2023
14ddff5
feat: verifier interfaces
rflechtner Nov 28, 2023
1ed90a9
fix: jsonpointer syntax
rflechtner Nov 29, 2023
2a33d17
fix: don't throw if proofPurpose is not set
rflechtner Nov 29, 2023
7c4bb11
fix: getExtrinsicSigner logic
rflechtner Nov 29, 2023
e384e67
fix: tests
rflechtner Nov 29, 2023
6d37fbe
test: fix tests
rflechtner Nov 29, 2023
33b2589
Merge branch 'vc-refactor' into rf-credential-interface
rflechtner Nov 29, 2023
cd2e05f
chore: linting
rflechtner Nov 29, 2023
06a7349
chore: don't rename imports & align status strings
rflechtner Dec 5, 2023
1641d6c
chore: no legitimations on createCredential
rflechtner Dec 5, 2023
bdbbc86
chore: improve readability of verifyPresentation
rflechtner Dec 5, 2023
7ef04c6
chore: use json-pointer typedefs
rflechtner Dec 5, 2023
117d39d
chore: remove comment
rflechtner Dec 5, 2023
ff47e4a
fix: integration tests
rflechtner Dec 5, 2023
cb1394c
Merge remote-tracking branch 'origin/vc-refactor' into rf-credential-…
rflechtner Dec 5, 2023
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 16 additions & 3 deletions packages/chain-helpers/src/blockchain/Blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,16 @@ import type { BN } from '@polkadot/util'
import type {
ISubmittableResult,
KeyringPair,
KiltAddress,
SignerInterface,
SubmittableExtrinsic,
SubscriptionPromise,
} from '@kiltprotocol/types'

import { SubmittableResult } from '@polkadot/api'

import { ConfigService } from '@kiltprotocol/config'
import { SDKErrors } from '@kiltprotocol/utils'
import { SDKErrors, Signers } from '@kiltprotocol/utils'

import { ErrorHandler } from '../errorhandling/index.js'
import { makeSubscriptionPromise } from './SubscriptionPromise.js'
Expand Down Expand Up @@ -167,6 +169,11 @@ export async function submitSignedTx(

export const dispatchTx = submitSignedTx

export type TransactionSigner = SignerInterface<
'Ecrecover-Secp256k1-Blake2b' | 'Sr25519' | 'Ed25519',
Dudleyneedham marked this conversation as resolved.
Show resolved Hide resolved
KiltAddress
>

/**
* Signs and submits the SubmittableExtrinsic with optional resolution and rejection criteria.
*
Expand All @@ -178,13 +185,19 @@ export const dispatchTx = submitSignedTx
*/
export async function signAndSubmitTx(
tx: SubmittableExtrinsic,
signer: KeyringPair,
signer: KeyringPair | TransactionSigner,
Dudleyneedham marked this conversation as resolved.
Show resolved Hide resolved
{
tip,
...opts
}: Partial<SubscriptionPromise.Options> & Partial<{ tip: AnyNumber }> = {}
): Promise<ISubmittableResult> {
const signedTx = await tx.signAsync(signer, { tip })
const signedTx =
'address' in signer
? await tx.signAsync(signer, { tip })
: await tx.signAsync(signer.id, {
tip,
signer: Signers.getPolkadotSigner([signer]),
})
return submitSignedTx(signedTx, opts)
}

Expand Down
3 changes: 2 additions & 1 deletion packages/core/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@
"@polkadot/keyring": "^12.0.0",
"@polkadot/types": "^10.4.0",
"@polkadot/util": "^12.0.0",
"@polkadot/util-crypto": "^12.0.0"
"@polkadot/util-crypto": "^12.0.0",
"json-pointer": "^0.6.2"
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ import {
timestamp,
cType,
legacyCredential,
} from '../../../../tests/testUtils/testData.js'
} from '../../../../../tests/testUtils/testData.js'
import {
finalizeProof,
initializeProof,
Expand Down Expand Up @@ -136,7 +136,7 @@ describe('issuance', () => {
subject,
cType: cType.$id,
issuer: attestation.owner,
timestamp: 0,
timestamp: new Date(0),
})

it('create a proof via initialize and finalize', async () => {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,18 +24,19 @@ import type { QueryableStorageEntry } from '@polkadot/api/types'
import type { Option, u64, Vec } from '@polkadot/types'
import type {
AccountId,
EventRecord,
Extrinsic,
Hash,
} from '@polkadot/types/interfaces/types.js'
import type { IEventData, Signer } from '@polkadot/types/types'
import type { IEventData } from '@polkadot/types/types'

import {
authorizeTx,
authorizeTx as didAuthorizeTx,
Dudleyneedham marked this conversation as resolved.
Show resolved Hide resolved
getFullDid,
validateDid,
fromChain as didFromChain,
} from '@kiltprotocol/did'
import { JsonSchema, SDKErrors, Caip19 } from '@kiltprotocol/utils'
import { JsonSchema, SDKErrors, Caip19, Signers } from '@kiltprotocol/utils'
import { ConfigService } from '@kiltprotocol/config'
import { Blockchain } from '@kiltprotocol/chain-helpers'
import type {
Expand Down Expand Up @@ -80,6 +81,8 @@ import type {
import { CTypeLoader } from '../ctype/CTypeLoader.js'
import { KiltRevocationStatusV1 } from './index.js'

export type Interface = KiltAttestationProofV1

/**
* Type for backwards-compatible Kilt proof suite.
*/
Expand Down Expand Up @@ -338,7 +341,11 @@ async function verifyLegitimation(
export async function verify(
credentialInput: Omit<KiltCredentialV1, 'proof'>,
proof: KiltAttestationProofV1,
opts: { api?: ApiPromise; cTypes?: ICType[]; loadCTypes?: CTypeLoader } = {}
opts: {
api?: ApiPromise
cTypes?: ICType[]
loadCTypes?: CTypeLoader | false
} = {}
): Promise<void> {
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { proof: _, ...credential } = credentialInput as KiltCredentialV1
Expand Down Expand Up @@ -508,7 +515,7 @@ export function applySelectiveDisclosure(
const { statements: statementsOriginal } = normalizeClaims(expandedContents)
if (statementsOriginal.length !== proofInput.salt.length)
throw new SDKErrors.ProofMalformedError(
'Violated expectation: number of normalized statements === number of salts'
'Violated expectation: number of normalized statements !== number of salts'
)
// 2. Filter credentialSubject for claims to be revealed
const reducedSubject = Object.entries(credentialSubject).reduce(
Expand Down Expand Up @@ -640,50 +647,54 @@ export function finalizeProof(
blockHash,
timestamp,
genesisHash = spiritnetGenesisHash,
}: { blockHash: Uint8Array; timestamp: number; genesisHash?: Uint8Array }
}: { blockHash: Uint8Array; timestamp: Date; genesisHash?: Uint8Array }
): KiltCredentialV1 {
const rootHash = calculateRootHash(credential, proof)
return {
...credential,
id: credentialIdFromRootHash(rootHash),
credentialStatus: fromGenesisAndRootHash(genesisHash, rootHash),
issuanceDate: new Date(timestamp).toISOString(),
issuanceDate: timestamp.toISOString(),
proof: { ...proof, block: base58Encode(blockHash) },
}
}

export type AttestationHandler = (
tx: Extrinsic,
api: ApiPromise
) => Promise<{
blockHash: Uint8Array
timestamp?: number
}>

export type TxHandler = {
account: KiltAddress
signAndSubmit?: AttestationHandler
signer?: Signer
export interface TransactionResult {
status: 'inBlock' | 'finalized'
Dudleyneedham marked this conversation as resolved.
Show resolved Hide resolved
Dudleyneedham marked this conversation as resolved.
Show resolved Hide resolved
includedAt: { blockHash: Uint8Array; blockHeight?: BigInt; blockTime?: Date }
events?: EventRecord[] // do we need that?
Dudleyneedham marked this conversation as resolved.
Show resolved Hide resolved
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is nice to have. It helps people a lot and only way to know about this is through polkadot js. I think keep it.

}

export type IssueOpts = {
type CustomHandlers = {
authorizeTx: (tx: Extrinsic) => Promise<Extrinsic>
submitTx: (tx: Extrinsic) => Promise<TransactionResult>
}
type SignersAndSubmitter = {
submitterAccount: KiltAddress
signers: readonly SignerInterface[]
transactionHandler: TxHandler
} & Parameters<typeof authorizeTx>[4]

function makeDefaultTxSubmit(
transactionHandler: TxHandler
): AttestationHandler {
return async (tx, api) => {
const signed = await api.tx(tx).signAsync(transactionHandler.account, {
signer: transactionHandler.signer,
})
const result = await Blockchain.submitSignedTx(signed, {
resolveOn: Blockchain.IS_FINALIZED,
})
const blockHash = result.status.asFinalized
return { blockHash }
}
}
export type IssueOpts =
| (CustomHandlers & Partial<SignersAndSubmitter>)
| (Partial<CustomHandlers> & SignersAndSubmitter)

async function defaultTxSubmit(
tx: Extrinsic,
submitterAccount: KiltAddress,
signers: readonly SignerInterface[],
api: ApiPromise
): Promise<TransactionResult> {
const extrinsic = api.tx(tx)
const signed = extrinsic.isSigned
? extrinsic
: await extrinsic.signAsync(submitterAccount, {
signer: Signers.getPolkadotSigner(signers),
})
const result = await Blockchain.submitSignedTx(signed, {
resolveOn: Blockchain.IS_FINALIZED,
})
const blockHash = result.status.asFinalized
const { events } = result
return { status: 'finalized', includedAt: { blockHash }, events }
}

/**
Expand All @@ -692,18 +703,20 @@ function makeDefaultTxSubmit(
*
* @param credential A [[KiltCredentialV1]] for which a proof shall be created.
* @param issuer The DID or DID Document of the DID acting as the issuer.
* @param opts Additional parameters.
* @param opts.signers An array of signer interfaces related to the issuer's keys. The function selects the appropriate handlers for all signatures required for issuance (e.g., authorizing the on-chain anchoring of the credential).
* @param opts.transactionHandler Object containing the submitter `address` that's going to cover the transaction fees as well as either a `signer` or `signAndSubmit` callback handling extrinsic signing and submission.
* The signAndSubmit callback receives an unsigned extrinsic and is expected to return the `blockHash` and (optionally) `timestamp` when the extrinsic was included in a block.
* This callback must thus take care of signing and submitting the extrinsic to the KILT blockchain as well as noting the inclusion block.
* If only the `signer` is given, a default callback will be constructed to take care of submitting the signed extrinsic using the cached blockchain api object.
* @param options Additional parameters.
* @param options.signers An array of signer interfaces related to the issuer's keys. The function selects the appropriate handlers for all signatures required for issuance (e.g., authorizing the on-chain anchoring of the credential).
* This can be omitted if both a custom authorizeTx & submitTx are given.
* @param options.submitterAccount The account which counter-signs the transaction to cover the transaction fees.
* Can be omitted if both a custom authorizeTx & submitTx are given.
* @param options.authorizeTx Allows overriding the function that takes a transaction and adds authorization by signing it with keys associated with the issuer DID.
* @param options.submitTx Allows overriding the function that takes the DID-signed transaction and submits it to a blockchain node, tracking its inclusion in a block.
* It is expected to at least return the hash of the block at which the transaction was processed.
* @returns The credential where `id`, `credentialStatus`, and `issuanceDate` have been updated based on the on-chain attestation record, containing a finalized proof.
*/
export async function issue(
credential: Omit<UnissuedCredential, 'issuer'>,
issuer: Did | DidDocument,
{ signers, transactionHandler, ...otherParams }: IssueOpts
options: IssueOpts
): Promise<KiltCredentialV1> {
const updatedCredential = {
...credential,
Expand All @@ -712,20 +725,39 @@ export async function issue(
const [proof, callArgs] = initializeProof(updatedCredential)
const api = ConfigService.get('api')
const call = api.tx.attestation.add(...callArgs)
const txSubmissionHandler =
transactionHandler.signAndSubmit ?? makeDefaultTxSubmit(transactionHandler)

const didSigned = await authorizeTx(
issuer,
call,
signers,
transactionHandler.account,
otherParams
)

const { signers, submitterAccount, authorizeTx, submitTx, ...otherParams } =
rflechtner marked this conversation as resolved.
Show resolved Hide resolved
options

if (!(authorizeTx && submitTx) && !(signers && submitterAccount)) {
throw new Error(
'`signers` and `submitterAccount` are required options if authorizeTx or submitTx are not given'
)
}

/* eslint-disable @typescript-eslint/no-non-null-assertion -- we've checked the appropriate combination of parameters above, but typescript does not follow */
const didSigned = authorizeTx
? await authorizeTx(call)
: await didAuthorizeTx(
Dudleyneedham marked this conversation as resolved.
Show resolved Hide resolved
issuer,
call,
signers!,
submitterAccount!,
otherParams
)
Dudleyneedham marked this conversation as resolved.
Show resolved Hide resolved

const transactionPromise = submitTx
? submitTx(didSigned)
: defaultTxSubmit(didSigned, submitterAccount!, signers!, api)
/* eslint-enable @typescript-eslint/no-non-null-assertion */

const {
blockHash,
timestamp = (await api.query.timestamp.now.at(blockHash)).toNumber(),
} = await txSubmissionHandler(didSigned, api)
includedAt: { blockHash, blockTime },
} = await transactionPromise

const timestamp =
blockTime ??
new Date((await api.query.timestamp.now.at(blockHash)).toNumber())
return finalizeProof(updatedCredential, proof, {
blockHash,
timestamp,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import * as CType from '../ctype/index.js'
import {
credential as VC,
cType,
} from '../../../../tests/testUtils/testData.js'
} from '../../../../../tests/testUtils/testData.js'
import {
credentialSchema,
validateStructure,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ export {
getDelegationNodeIdForCredential as getDelegationId,
} from './common.js'

export type Interface = KiltCredentialV1

/**
* Credential context URL required for Kilt credentials.
*/
Expand Down Expand Up @@ -219,7 +221,7 @@ interface CredentialInput {
claims: ICredential['claim']['contents']
cType: ICType['$id']
issuer: Did
timestamp?: number
timestamp?: Date
chainGenesisHash?: Uint8Array
claimHash?: ICredential['rootHash']
legitimations?: Array<KiltCredentialV1 | KiltCredentialV1['id']>
Expand All @@ -243,7 +245,7 @@ export function fromInput(
* @param input.claims A record of claims about the subject.
* @param input.cType The CType (or alternatively its id) to which the claims conform.
* @param input.issuer The issuer of the credential.
* @param input.timestamp Timestamp of a block at which the credential can be verified, in milliseconds since January 1, 1970, UTC (UNIX epoch).
* @param input.timestamp Timestamp of a block at which the credential can be verified.
* @param input.chainGenesisHash Optional: Genesis hash of the chain against which this credential is verifiable. Defaults to the spiritnet genesis hash.
* @param input.claimHash Optional: digest of the credential contents needed to produce a credential id.
* @param input.legitimations Optional: array of credentials (or credential ids) which function as legitimations to this credential.
Expand All @@ -255,7 +257,7 @@ export function fromInput({
claims,
cType,
issuer,
timestamp = Date.now(),
timestamp = new Date(),
chainGenesisHash = spiritnetGenesisHash,
claimHash,
legitimations,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,8 @@ import {
} from './common.js'
import type { KiltCredentialV1, KiltRevocationStatusV1 } from './types.js'

export type Interface = KiltRevocationStatusV1

export const STATUS_TYPE = 'KiltRevocationStatusV1'

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,18 @@ export interface UnsignedVc {
/**
* Claims about the subjects of the credential.
*/
credentialSubject: { id?: string }
credentialSubject: {
id?: string
[key: string]: unknown
}
/**
* The entity that issued the credential.
*/
issuer: string
/**
* When the credential was issued.
*/
issuanceDate: string
issuanceDate?: string
/**
* If true, this credential can only be presented and used by its subject.
*/
Expand All @@ -85,6 +88,7 @@ export interface UnsignedVc {
}

export interface VerifiableCredential extends UnsignedVc {
issuanceDate: string
/**
* Cryptographic proof that makes the credential tamper-evident.
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ import { ConfigService } from '@kiltprotocol/config'
import type { CTypeHash, Did, IAttestation } from '@kiltprotocol/types'
import { SDKErrors } from '@kiltprotocol/utils'

import { ApiMocks } from '../../../../tests/testUtils'
import { ApiMocks } from '../../../../../tests/testUtils'
import * as Attestation from './Attestation'

let mockedApi: any
Expand Down
Loading
Loading