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

fix: block parsing logic for public credentials #697

Merged
merged 8 commits into from
Dec 16, 2022
Merged
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
84 changes: 83 additions & 1 deletion packages/core/src/__integrationtests__/PublicCredentials.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ let attesterKey: KeyTool

let api: ApiPromise
// Generate a random asset ID
const assetId: AssetDidUri = `did:asset:eip155:1.erc20:${randomAsHex(20)}`
let assetId: AssetDidUri = `did:asset:eip155:1.erc20:${randomAsHex(20)}`
let latestCredential: IPublicCredentialInput

async function issueCredential(
Expand Down Expand Up @@ -498,6 +498,88 @@ describe('When there is an issued public credential', () => {
})
})

describe('When there is a batch which contains a credential creation', () => {
beforeAll(async () => {
assetId = `did:asset:eip155:1.erc20:${randomAsHex(20)}`
const credential1 = {
claims: {
name: `Certified NFT collection with id ${UUID.generate()}`,
},
cTypeHash: CType.getHashForSchema(nftNameCType),
delegationId: null,
subject: assetId,
}
const credential2 = {
claims: {
name: `Certified NFT collection with id ${UUID.generate()}`,
},
cTypeHash: CType.getHashForSchema(nftNameCType),
delegationId: null,
subject: assetId,
}
const credential3 = {
claims: {
name: `Certified NFT collection with id ${UUID.generate()}`,
},
cTypeHash: CType.getHashForSchema(nftNameCType),
delegationId: null,
subject: assetId,
}
// A batchAll with a DID call, and a nested batch with a second DID call and a nested forceBatch batch with a third DID call.
const currentAttesterNonce = Did.documentFromChain(
await api.query.did.did(Did.toChain(attester.uri))
).lastTxCounter
const batchTx = api.tx.utility.batchAll([
await Did.authorizeTx(
attester.uri,
api.tx.publicCredentials.add(PublicCredential.toChain(credential1)),
attesterKey.getSignCallback(attester),
tokenHolder.address,
{ txCounter: currentAttesterNonce.addn(1) }
),
api.tx.utility.batch([
await Did.authorizeTx(
attester.uri,
api.tx.publicCredentials.add(PublicCredential.toChain(credential2)),
attesterKey.getSignCallback(attester),
tokenHolder.address,
{ txCounter: currentAttesterNonce.addn(2) }
),
api.tx.utility.forceBatch([
await Did.authorizeTx(
attester.uri,
api.tx.publicCredentials.add(PublicCredential.toChain(credential3)),
attesterKey.getSignCallback(attester),
tokenHolder.address,
{ txCounter: currentAttesterNonce.addn(3) }
),
]),
]),
])
await submitTx(batchTx, tokenHolder)
})

it('should correctly parse the block and retrieve the original credentials', async () => {
const encodedCredentials = await api.call.publicCredentials.getBySubject(
assetId,
null
)
const retrievedCredentials = await PublicCredential.credentialsFromChain(
encodedCredentials
)
expect(retrievedCredentials.length).toEqual(3)
await expect(
PublicCredential.verifyCredential(retrievedCredentials[0])
).resolves.not.toThrow()
await expect(
PublicCredential.verifyCredential(retrievedCredentials[1])
).resolves.not.toThrow()
await expect(
PublicCredential.verifyCredential(retrievedCredentials[2])
).resolves.not.toThrow()
})
})

afterAll(async () => {
await disconnect()
})
83 changes: 58 additions & 25 deletions packages/core/src/publicCredential/PublicCredential.chain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import { validateUri } from '@kiltprotocol/asset-did'
import { SDKErrors } from '@kiltprotocol/utils'

import { getIdForCredential } from './PublicCredential.js'
import { isBatch } from '../utils.js'

export interface EncodedPublicCredential {
ctypeHash: CTypeHash
Expand Down Expand Up @@ -62,11 +63,7 @@ export function toChain(

// Flatten any nested batch calls into a single list of calls.
function flattenCalls(api: ApiPromise, call: Call): Call[] {
if (
api.tx.utility.batch.is(call) ||
api.tx.utility.batchAll.is(call) ||
api.tx.utility.forceBatch.is(call)
) {
if (isBatch(api, call)) {
// Inductive case
return call.args[0].flatMap((c) => flattenCalls(api, c))
}
Expand Down Expand Up @@ -117,6 +114,30 @@ async function retrievePublicCredentialCreationExtrinsicFromBlock(
return lastPublicCredentialCreationExtrinsic?.extrinsic ?? null
}

// Given a (nested) call, flattens them and filter by calls that are of type `api.tx.publicCredentials.add`.
function extractPublicCredentialCreationCallsFromDidCall(
api: ApiPromise,
call: Call
): Array<GenericCall<typeof api.tx.publicCredentials.add.args>> {
const extrinsicCalls = flattenCalls(api, call)
return extrinsicCalls.filter(
(c): c is GenericCall<typeof api.tx.publicCredentials.add.args> =>
api.tx.publicCredentials.add.is(c)
)
}

// Given a (nested) call, flattens them and filter by calls that are of type `api.tx.did.submitDidCall`.
function extractDidCallsFromBatchCall(
api: ApiPromise,
call: Call
): Array<GenericCall<typeof api.tx.did.submitDidCall.args>> {
const extrinsicCalls = flattenCalls(api, call)
return extrinsicCalls.filter(
(c): c is GenericCall<typeof api.tx.did.submitDidCall.args> =>
api.tx.did.submitDidCall.is(c)
)
}

/**
* Decodes the public credential details returned by `api.call.publicCredentials.getById()`.
*
Expand Down Expand Up @@ -147,31 +168,40 @@ export async function credentialFromChain(
)
}

if (!api.tx.did.submitDidCall.is(extrinsic)) {
if (!isBatch(api, extrinsic) && !api.tx.did.submitDidCall.is(extrinsic)) {
throw new SDKErrors.PublicCredentialError(
'Extrinsic should be a did.submitDidCall extrinsic'
'Extrinsic should be either a `did.submitDidCall` extrinsic or a batch with at least a `did.submitDidCall` extrinsic'
)
}

const extrinsicCalls = flattenCalls(api, extrinsic.args[0].call)
const extrinsicDidOrigin = didFromChain(extrinsic.args[0].did)
// If we're dealing with a batch, flatten any nested `submit_did_call` calls,
// otherwise the extrinsic is itself a submit_did_call, so just take it.
const didCalls = isBatch(api, extrinsic)
? extrinsic.args[0].flatMap((batchCall) =>
extractDidCallsFromBatchCall(api, batchCall)
)
: [extrinsic]

const credentialCreationCalls = extrinsicCalls.filter(
(call): call is GenericCall<typeof api.tx.publicCredentials.add.args> =>
api.tx.publicCredentials.add.is(call)
)
// From the list of DID calls, only consider public_credentials::add calls, bundling each of them with their DID submitter.
ntn-x2 marked this conversation as resolved.
Show resolved Hide resolved
// Re-create the issued public credential for each call identified.
const callCredentialsContent = credentialCreationCalls.map((call) =>
credentialInputFromChain(call.args[0])
)
// If more than a call is present, it always considers the last one as the valid one.
// It returns a list of [reconstructedCredential, attesterDid].
const callCredentialsContent = didCalls.flatMap((didCall) => {
const publicCredentialCalls =
extractPublicCredentialCreationCallsFromDidCall(api, didCall.args[0].call)
return publicCredentialCalls.map(
(c) =>
[
credentialInputFromChain(c.args[0]),
didFromChain(didCall.args[0].did),
] as const
)
})

// If more than one call is present, it always considers the last one as the valid one, and takes its submitter.
const lastRightCredentialCreationCall = callCredentialsContent
.reverse()
.find((credentialInput) => {
const reconstructedId = getIdForCredential(
credentialInput,
extrinsicDidOrigin
)
.find(([c, s]) => {
ntn-x2 marked this conversation as resolved.
Show resolved Hide resolved
const reconstructedId = getIdForCredential(c, s)
return reconstructedId === credentialId
})

Expand All @@ -180,10 +210,13 @@ export async function credentialFromChain(
'Block should always contain the full credential, eventually.'
)
}

const [credentialInput, submitter] = lastRightCredentialCreationCall
ntn-x2 marked this conversation as resolved.
Show resolved Hide resolved

return {
...lastRightCredentialCreationCall,
attester: extrinsicDidOrigin,
id: getIdForCredential(lastRightCredentialCreationCall, extrinsicDidOrigin),
...credentialInput,
attester: submitter,
id: getIdForCredential(credentialInput, submitter),
blockNumber,
revoked: revoked.toPrimitive(),
}
Expand Down
34 changes: 34 additions & 0 deletions packages/core/src/utils.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
/**
* Copyright (c) 2018-2022, BOTLabs GmbH.
*
* This source code is licensed under the BSD 4-Clause "Original" license
* found in the LICENSE file in the root directory of this source tree.
*/

import type { ApiPromise } from '@polkadot/api'
import type { Call, Extrinsic } from '@polkadot/types/interfaces'

import { GenericExtrinsic } from '@polkadot/types'

/**
* Checks wheather the provided extrinsic or call represents a batch.
*
* @param api The [[ApiPromise]].
* @param extrinsic The input [[Extrinsic]] or [[Call]].
*
* @returns True if it's a batch, false otherwise.
*/
export function isBatch(
api: ApiPromise,
extrinsic: Extrinsic | Call
): extrinsic is GenericExtrinsic<
| typeof api.tx.utility.batch.args
| typeof api.tx.utility.batchAll.args
| typeof api.tx.utility.forceBatch.args
> {
return (
api.tx.utility.batch.is(extrinsic) ||
api.tx.utility.batchAll.is(extrinsic) ||
api.tx.utility.forceBatch.is(extrinsic)
)
}