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: nested ctype val #919

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from

Conversation

aybarsayan
Copy link

fixes KILTProtocol/ticket#3632

Added nested CType validation support to validateSubject
Implemented automatic reference detection and blockchain fetching
Added support to handle both simple and complex CType structures
Updated tests for new functionality

Note: Improves credential validation by handling nested CTypes automatically.

How to test:

Please provide a brief step-by-step instruction.
If necessary provide information about dependencies (specific configuration, branches, database dumps, etc.)

  • Step 1
  • Step 2
  • etc.

Checklist:

  • I have verified that the code works
  • I have verified that the code is easy to understand
    • If not, I have left a well-balanced amount of inline comments
  • I have left the code in a better state
  • I have documented the changes (where applicable)
    • Either PR or Ticket to update the Docs
    • Link the PR/Ticket here

Copy link
Member

@Dudleyneedham Dudleyneedham left a comment

Choose a reason for hiding this comment

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

In general, this is bad practice to create a Named function into another Named function unless you are using it multiple times.

There is a time and place for doing it, but it isn't here.

If you are worried about the typing you can cast or try another approach.

const references = extractUniqueReferences(cType)
const referencedCTypes: ICType[] = []

for (const ref of references) {
Copy link
Member

Choose a reason for hiding this comment

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

I would suggest using a Promise.all here to reduce the code complexity and make this more efficient.

Additionally, change this from a for..loop with the await inside to a map. Using the map const you can use the Promise.all

Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, use array methods here please. E.g., await Promise.all(Array.from(references).map(ctypeId => loadCTypes(ctypeId)))

Copy link
Author

Choose a reason for hiding this comment

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

Fixed at: 77b9c3d

Comment on lines 331 to 349
// Helper function to check if CType is nested
function cTypeTypeFinder(cType: ICType): boolean {
function hasRef(obj: any): boolean {
if (typeof obj !== 'object' || obj === null) return false

if ('$ref' in obj) return true

return Object.values(obj).some(value => {
if (Array.isArray(value)) {
return value.some(item => hasRef(item))
}
if (typeof value === 'object') {
return hasRef(value)
}
return false
})
}
return hasRef(cType.properties)
}
Copy link
Member

Choose a reason for hiding this comment

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

No need to make another Named function inside this function.

Suggested change
// Helper function to check if CType is nested
function cTypeTypeFinder(cType: ICType): boolean {
function hasRef(obj: any): boolean {
if (typeof obj !== 'object' || obj === null) return false
if ('$ref' in obj) return true
return Object.values(obj).some(value => {
if (Array.isArray(value)) {
return value.some(item => hasRef(item))
}
if (typeof value === 'object') {
return hasRef(value)
}
return false
})
}
return hasRef(cType.properties)
}
// Helper function to check if CType is nested
function cTypeTypeFinder(cType: ICType): boolean {
if (typeof cType.properties !== 'object' || cType.properties === null) return false
if ('$ref' in cType.properties) return true
return Object.values(cType.properties).some(value => {
if (Array.isArray(value)) {
return value.some(item => hasRef(item))
}
if (typeof value === 'object') {
return hasRef(value)
}
return false
})
}

Copy link
Contributor

Choose a reason for hiding this comment

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

I understand that you're defining a function to call it recursively, but can't you just do that with the outer function directly? Also, more generally, I think all that you need is the second function; if you're already recursing through the CType, collect all the references, we will need them eventually.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed at: 77b9c3d

}

// Helper function to extract unique references from CType
function extractUniqueReferences(cType: ICType): Set<string> {
Copy link
Member

Choose a reason for hiding this comment

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

As above, change it.

Copy link
Contributor

Choose a reason for hiding this comment

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

Here I can somewhat see the point of having an inner function; alternatively, you could solve it by taking an optional second argument:

Suggested change
function extractUniqueReferences(cType: ICType): Set<string> {
function extractUniqueReferences(cType: ICType, references=new Set<string>()): Set<string> {

Copy link
Author

Choose a reason for hiding this comment

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

Fixed at: 77b9c3d

@@ -7,6 +7,7 @@

import { hexToU8a } from '@polkadot/util'
import { base58Encode } from '@polkadot/util-crypto'
import * as Kilt from "@kiltprotocol/sdk-js"
Copy link
Contributor

Choose a reason for hiding this comment

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

Because the credentials package is imported by the sdk-js package this creates a circular dependency and will not work. We also don't do blanket imports in the sdk implementation. Be specific, only import what you need. CType stuff is implemented in the credentials package, this should not be necessary.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed at: 77b9c3d

Comment on lines 331 to 349
// Helper function to check if CType is nested
function cTypeTypeFinder(cType: ICType): boolean {
function hasRef(obj: any): boolean {
if (typeof obj !== 'object' || obj === null) return false

if ('$ref' in obj) return true

return Object.values(obj).some(value => {
if (Array.isArray(value)) {
return value.some(item => hasRef(item))
}
if (typeof value === 'object') {
return hasRef(value)
}
return false
})
}
return hasRef(cType.properties)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

I understand that you're defining a function to call it recursively, but can't you just do that with the outer function directly? Also, more generally, I think all that you need is the second function; if you're already recursing through the CType, collect all the references, we will need them eventually.

function extractUniqueReferences(cType: ICType): Set<string> {
const references = new Set<string>()

function processValue(value: any) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Don't use any, use unknown

Copy link
Author

Choose a reason for hiding this comment

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

fixed at: 77b9c3d

}

// Helper function to extract unique references from CType
function extractUniqueReferences(cType: ICType): Set<string> {
Copy link
Contributor

Choose a reason for hiding this comment

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

Here I can somewhat see the point of having an inner function; alternatively, you could solve it by taking an optional second argument:

Suggested change
function extractUniqueReferences(cType: ICType): Set<string> {
function extractUniqueReferences(cType: ICType, references=new Set<string>()): Set<string> {

@@ -344,7 +406,7 @@ export async function validateSubject(
}: Pick<KiltCredentialV1, 'credentialSubject' | 'type'>,
{
cTypes = [],
loadCTypes = cachingCTypeLoader,
loadCTypes = newCachingCTypeLoader(),
Copy link
Contributor

Choose a reason for hiding this comment

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

what's the reason for this change?

Copy link
Author

Choose a reason for hiding this comment

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

Fixed at (i missed it that it already exists): 77b9c3d

Comment on lines 452 to 453
// Connect to blockchain
const api = Kilt.ConfigService.get('api')
Copy link
Contributor

Choose a reason for hiding this comment

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

shouldn't be needed, we have the loadCTypes argument to load ctypes

Copy link
Author

Choose a reason for hiding this comment

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

fixed at: 77b9c3d

Comment on lines 455 to 464
// Bizim eklediğimiz doğrulama mantığı
const isNested = cTypeTypeFinder(cType)

if (!isNested) {
await CType.verifyClaimAgainstNestedSchemas(
cType,
[],
claims
)
} else {
Copy link
Contributor

Choose a reason for hiding this comment

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

skip this step; call extractUniqueReferences directly, pass each ctype id to the ctype loader, then pass the resulting definitions to verifyClaimAgainstNestedSchemas. If there are no ctype references in the ctype, this result in an empty array, just like here.

const references = extractUniqueReferences(cType)
const referencedCTypes: ICType[] = []

for (const ref of references) {
Copy link
Contributor

Choose a reason for hiding this comment

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

Yes, use array methods here please. E.g., await Promise.all(Array.from(references).map(ctypeId => loadCTypes(ctypeId)))

Comment on lines 470 to 473
const referencedCType = await CType.fetchFromChain(ref as any)
if (referencedCType.cType) {
referencedCTypes.push(referencedCType.cType)
}
Copy link
Contributor

Choose a reason for hiding this comment

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

again, use the loadCTypes argument. If it can't be found, throw an error saying that the ctype contains a reference to another ctype which cannot be resolved.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed at: 77b9c3d

Comment on lines 419 to 422

// check that we have access to the right schema
let cType = cTypes?.find(({ $id }) => $id === credentialsCTypeId)
if (!cType) {
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to somehow handle both the ctypes passed in as the cTypes argument, and the loadCtypes function, if there is any. I suggest we create a combined ctype loader function from both, which first checks if the cType whose id is being looked up is contained in the cTypes array, and if not, forwards the id to the loadCtypes function, which then tries to look it up.

Hint: checking if we have a CType already in cTypes can be done quicker if we transform the array into a Map of ctype id to ctype definition once.

Copy link
Author

Choose a reason for hiding this comment

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

I am working on a new commit for this.

Copy link
Contributor

Choose a reason for hiding this comment

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

this is still tbd

Copy link
Contributor

@rflechtner rflechtner left a comment

Choose a reason for hiding this comment

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

Looking way better already, but there's still a few things that need to be tackled

const referencedCTypes = await Promise.all(
Array.from(references).map(async (ref) => {
try {
const referencedCType = await cachingCTypeLoader(ref as any)
Copy link
Contributor

Choose a reason for hiding this comment

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

please use the function passed as the loadCtypes argument

Copy link
Author

Choose a reason for hiding this comment

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

Fixed at: bbfced2

Comment on lines 478 to 479
// eslint-disable-next-line no-console
console.error('Validation error:', error)
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't use console statements in library code, that's why the lint rule exists

Copy link
Author

Choose a reason for hiding this comment

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

Fixed at: bbfced2

Comment on lines 461 to 462
// eslint-disable-next-line no-console
console.error(`Failed to fetch CType for reference ${ref}:`, error)
Copy link
Contributor

Choose a reason for hiding this comment

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

we don't use console statements in library code, that's why the lint rule exists

Copy link
Author

Choose a reason for hiding this comment

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

Fixed at: bbfced2

Comment on lines 330 to 345
// Check recursively if a value has references
const hasRef = (value: unknown): boolean => {
if (typeof value !== 'object' || value === null) {
return false
}

if ('$ref' in (value as Record<string, unknown>)) {
return true
}

if (Array.isArray(value)) {
return value.some((item) => hasRef(item))
}

return Object.values(value as Record<string, unknown>).some((v) => hasRef(v))
}
Copy link
Contributor

Choose a reason for hiding this comment

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

you're not using this function any more, are you?

Copy link
Author

Choose a reason for hiding this comment

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

Yes forget to remove, fixed at: bbfced2

if (Array.isArray(value)) {
value.forEach(processValue)
} else {
Object.values(objValue).forEach(processValue)
Copy link
Contributor

Choose a reason for hiding this comment

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

the point of passing down the set of references to extractUniqueReferences was that you can call extractUniqueReferences in recursion instead of an inner function. You don't necessarily need to do it that way, but if you're not using this parameter, you can also just pull it back into the function as a normal variable.

Copy link
Author

Choose a reason for hiding this comment

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

Fixed at: bbfced2

Comment on lines 419 to 422

// check that we have access to the right schema
let cType = cTypes?.find(({ $id }) => $id === credentialsCTypeId)
if (!cType) {
Copy link
Contributor

Choose a reason for hiding this comment

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

this is still tbd

Comment on lines +433 to +444
const references = extractUniqueReferences(cType)

const referencedCTypes = await Promise.all(
Array.from(references).map(async (ref) => {
if (typeof loadCTypes !== 'function') {
throw new Error(
`The definition for this credential's CType ${ref} has not been passed to the validator and CType loading has been disabled`
)
}
return loadCTypes(ref as any)
})
)
Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry that I'm only realising that now, but we have two more issues here that need to be addressed:

  1. There is a maximum recursion depth for synchronous code in JS, which means that deeply nested structures could end up triggering a max recursion depth reached error
  2. The CTypes that we fetch could also contain references, so we need to check their definitions too and fetch any CTypes that they depend on

In light of these issues I think it's a good idea to refactor extractUniqueReferences so that it is async and also loads the ctype definitions directly. This function could be named loadNestedCTypeDefinitions and would

  1. Take a CType definition cType and a CTypeLoader cTypeLoader as parameters
  2. Initialise an empty Set fetchedCTypeIds
  3. Initialise an empty Set fetchedCTypeDefinitions
  4. Define a subroutine processCTypeProperties which accepts ICType["properties"] and:
    1. iterates over all properties in the object
    2. If the property value is an array, call and await processCTypeProperties on each item in it and continue
    3. If the property key is $ref, extract the CType id from it
    4. If the extracted CType id is not yet in fetchedCTypeIds, push the id to it, then call cTypeLoader passing the id as an argument
    5. await the Promise returned by the cTypeLoader; if it's a CType, call processCTypeProperties on its properties; else, throw an error
  5. calls processCTypeProperties on the CType properties and awaits its completion
  6. returns fetchedCTypeDefinitions

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants