diff --git a/.eslintignore b/.eslintignore index e31e345a4..61e59a3ca 100644 --- a/.eslintignore +++ b/.eslintignore @@ -3,4 +3,4 @@ dist lib jest.* coverage -*.d.ts +*.d.ts \ No newline at end of file diff --git a/packages/actors_api/src/actors/Claimer.ts b/packages/actors_api/src/actors/Claimer.ts index 33f630406..e0093b1a1 100644 --- a/packages/actors_api/src/actors/Claimer.ts +++ b/packages/actors_api/src/actors/Claimer.ts @@ -20,7 +20,7 @@ import type { IClaim, IMessage, IRequestAttestationForClaim, - IDelegationBaseNode, + IDelegationNode, IPublicIdentity, IRequestForAttestation, } from '@kiltprotocol/types' @@ -109,7 +109,7 @@ export function requestAttestation( attesterPublicIdentity: IPublicIdentity, option: { legitimations?: AttestedClaim[] - delegationId?: IDelegationBaseNode['id'] + delegationId?: IDelegationNode['id'] } = {} ): { message: Message diff --git a/packages/chain-helpers/package.json b/packages/chain-helpers/package.json index bbaaaf939..932412b7b 100644 --- a/packages/chain-helpers/package.json +++ b/packages/chain-helpers/package.json @@ -26,7 +26,7 @@ }, "dependencies": { "@kiltprotocol/config": "workspace:*", - "@kiltprotocol/type-definitions": "0.1.6", + "@kiltprotocol/type-definitions": "0.1.10", "@kiltprotocol/types": "workspace:*", "@kiltprotocol/utils": "workspace:*", "@polkadot/api": "^4.13.1", diff --git a/packages/chain-helpers/src/blockchainApiConnection/TypeRegistry.ts b/packages/chain-helpers/src/blockchainApiConnection/TypeRegistry.ts index 81621fe2f..27b8a4371 100644 --- a/packages/chain-helpers/src/blockchainApiConnection/TypeRegistry.ts +++ b/packages/chain-helpers/src/blockchainApiConnection/TypeRegistry.ts @@ -5,7 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import { types10 as KILT_TYPES } from '@kiltprotocol/type-definitions' +import { types17 as KILT_TYPES } from '@kiltprotocol/type-definitions' import { TypeRegistry } from '@polkadot/types' const TYPE_REGISTRY = new TypeRegistry() diff --git a/packages/chain-helpers/src/blockchainApiConnection/__mocks__/BlockchainApiConnection.ts b/packages/chain-helpers/src/blockchainApiConnection/__mocks__/BlockchainApiConnection.ts index d0f4e882f..b25b7d65c 100644 --- a/packages/chain-helpers/src/blockchainApiConnection/__mocks__/BlockchainApiConnection.ts +++ b/packages/chain-helpers/src/blockchainApiConnection/__mocks__/BlockchainApiConnection.ts @@ -31,10 +31,9 @@ * value setters `.mockReturnValue` or `.mockReturnValueOnce` on the method you want to modify: * ``` * const mocked_api = require('../blockchainApiConnection/BlockchainApiConnection').__mocked_api - * mocked_api.query.delegation.children.mockReturnValue( - * new Vec( - * 'Hash', - * ['0x123', '0x456', '0x789'] + * mocked_api.query.delegation.hierarchies.mockReturnValue( + * new Option( + * 'Hash' * ) * ) * ``` @@ -229,10 +228,10 @@ const __mocked_api: any = { }), }, delegation: { - createRoot: jest.fn((rootId, _ctypeHash) => { + createHierarchy: jest.fn((rootId, _ctypeHash) => { return __getMockSubmittableExtrinsic() }), - revokeRoot: jest.fn((rootId) => { + addDelegation: jest.fn((delegationId, parent_id, owner, permissions, signature) => { return __getMockSubmittableExtrinsic() }), revokeDelegation: jest.fn((delegationId) => { @@ -305,18 +304,14 @@ const __mocked_api: any = { }, delegation: { // default return value decodes to null, represents delegation not found - roots: jest.fn(async (rootId: string) => - mockChainQueryReturn('delegation', 'root') + hierarchies: jest.fn(async (rootId: string) => + mockChainQueryReturn('delegation', 'hierarchies') ), /* example return value: new Option( TYPE_REGISTRY, - Tuple.with(['Hash', AccountId, Bool]), - [ - '0x1234', // ctype hash - '4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', // Account - false, // revoked flag - ] + Hash, + '0x1234', // ctype hash ) */ @@ -327,28 +322,15 @@ const __mocked_api: any = { /* example return value: new Option( TYPE_REGISTRY, - Tuple.with(['DelegationNodeId','Option',AccountId,U32,Bool]), + Tuple.with(['DelegationNodeId','Option','Vec',DelegationDetails]), [ - '0x1234', // root-id - null, // parent-id? - '4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs', // Account - 0, // permissions - false, // revoked flag + '0x1234', // root-id + '0x1234', // parent-id? + '[0x2345,0x3456] // children ids + '{4r1WkS3t8rbCb11H8t3tJvGVCynwDXSUBiuGB6sLRHzCLCjs,false,0}', // {owner, revocation status, permissions} ] ) */ - - // default return value decodes to [], represents: no children found - children: jest.fn(async (id: string) => - mockChainQueryReturn('delegation', 'children') - ), - /* example return value: - new Vec( - TYPE_REGISTRY, - 'DelegationNodeId', - ['0x123', '0x456', '0x789'] - ) - */ }, did: { // default return value decodes to null, represents dID not found diff --git a/packages/chain-helpers/src/blockchainApiConnection/__mocks__/BlockchainQuery.ts b/packages/chain-helpers/src/blockchainApiConnection/__mocks__/BlockchainQuery.ts index 41859c7ba..91656c90c 100644 --- a/packages/chain-helpers/src/blockchainApiConnection/__mocks__/BlockchainQuery.ts +++ b/packages/chain-helpers/src/blockchainApiConnection/__mocks__/BlockchainQuery.ts @@ -13,7 +13,7 @@ const AccountId = TYPE_REGISTRY.getOrThrow('AccountId') type ChainQueryTypes = { attestation: 'attestations' | 'delegatedAttestations' ctype: 'cTYPEs' - delegation: 'root' | 'delegations' | 'children' + delegation: 'hierarchies' | 'delegations' did: 'dIDs' portablegabi: 'accumulatorList' | 'accumulatorCount' | 'accountState' } @@ -34,16 +34,14 @@ const chainQueryReturnTuples: { cTYPEs: AccountId, }, delegation: { - // Root-Delegation: root-id -> (ctype-hash, account, revoked) - root: TYPE_REGISTRY.getOrUnknown('DelegationRoot'), - // Delegations: delegation-id -> (root-id, parent-id?, account, permissions, revoked)? + // Delegation hierarchies: root-id -> (ctype-hash)? + hierarchies: TYPE_REGISTRY.getOrUnknown('DelegationHierarchyDetails'), + // Delegations: delegation-id -> (hierarchy-id, parent-id?, childrenIds, details)? delegations: TYPE_REGISTRY.getOrUnknown('DelegationNode'), - // Children: root-or-delegation-id -> [delegation-id] - children: TYPE_REGISTRY.getOrUnknown('DelegationNodeId'), }, attestation: { // Attestations: claim-hash -> (ctype-hash, attester-account, delegation-id?, revoked)? - attestations: TYPE_REGISTRY.getOrUnknown('Attestation'), + attestations: TYPE_REGISTRY.getOrUnknown('AttestationDetails'), // DelegatedAttestations: delegation-id -> [claim-hash] delegatedAttestations: TYPE_REGISTRY.getOrUnknown('Hash'), }, @@ -116,7 +114,6 @@ export function mockChainQueryReturn( return wrapInOption() } case 'delegation': { - if (innerQuery === 'children') return wrapInVec() return wrapInOption() } case 'did': { diff --git a/packages/chain-helpers/src/errorhandling/ExtrinsicError.ts b/packages/chain-helpers/src/errorhandling/ExtrinsicError.ts index 8658e8991..ab17215e6 100644 --- a/packages/chain-helpers/src/errorhandling/ExtrinsicError.ts +++ b/packages/chain-helpers/src/errorhandling/ExtrinsicError.ts @@ -80,27 +80,53 @@ export const ExtrinsicErrors = { code: 13002, message: 'delegation not found', }, - ERROR_ROOT_ALREADY_EXISTS: { code: 13003, message: 'root already exist' }, - ERROR_ROOT_NOT_FOUND: { code: 13004, message: 'root not found' }, + ERROR_DELEGATE_NOT_FOUND: { + code: 13003, + message: 'delegate not found', + }, + ERROR_HIERARCHY_ALREADY_EXISTS: { + code: 13004, + message: 'hierarchy already exist', + }, + ERROR_HIERARCHY_NOT_FOUND: { code: 13005, message: 'hierarchy not found' }, ERROR_MAX_DELEGATION_SEARCH_DEPTH_REACHED: { - code: 13005, + code: 13006, message: 'maximum delegation search depth reached', }, - ERROR_NOT_OWNER_OF_PARENT: { code: 13006, message: 'not owner of parent' }, - ERROR_NOT_OWNER_OF_ROOT: { code: 13007, message: 'not owner of root' }, - ERROR_PARENT_NOT_FOUND: { code: 13008, message: 'parent not found' }, + ERROR_NOT_OWNER_OF_PARENT: { code: 13007, message: 'not owner of parent' }, + ERROR_NOT_OWNER_OF_HIERARCHY: { + code: 13008, + message: 'not owner of hierarchy', + }, + ERROR_PARENT_NOT_FOUND: { code: 13009, message: 'parent not found' }, + ERROR_PARENT_REVOKED: { + code: 13010, + message: 'parent delegation revoked', + }, ERROR_NOT_PERMITTED_TO_REVOKE: { - code: 13009, + code: 13011, message: 'not permitted to revoke', }, ERROR_NOT_AUTHORIZED_TO_DELEGATE: { - code: 13010, + code: 13012, message: 'not authorized to delegate', }, ERROR_EXCEEDED_REVOCATION_BOUNDS: { - code: 13011, + code: 13013, message: 'exceeded revocation bounds', }, + ERROR_EXCEEDED_MAX_REVOCATIONS_ALLOWED: { + code: 13014, + message: 'exceeded max revocations allowed', + }, + ERROR_EXCEEDED_MAX_PARENT_CHECKS_ALLOWED: { + code: 13015, + message: 'exceeded max parent checks allowed', + }, + INTERNAL_ERROR: { + code: 13016, + message: 'an internal delegation module error occured', + }, UNKNOWN_ERROR: { code: 13100, message: 'an unknown delegation module error occured', @@ -163,15 +189,20 @@ export const PalletToExtrinsicErrors: IPalletToExtrinsicErrors = { 0: ExtrinsicErrors.Delegation.ERROR_DELEGATION_ALREADY_EXISTS, 1: ExtrinsicErrors.Delegation.ERROR_BAD_DELEGATION_SIGNATURE, 2: ExtrinsicErrors.Delegation.ERROR_DELEGATION_NOT_FOUND, - 3: ExtrinsicErrors.Delegation.ERROR_ROOT_ALREADY_EXISTS, - 4: ExtrinsicErrors.Delegation.ERROR_ROOT_NOT_FOUND, - 5: ExtrinsicErrors.Delegation.ERROR_MAX_DELEGATION_SEARCH_DEPTH_REACHED, - 6: ExtrinsicErrors.Delegation.ERROR_NOT_OWNER_OF_PARENT, - 7: ExtrinsicErrors.Delegation.ERROR_NOT_OWNER_OF_ROOT, - 8: ExtrinsicErrors.Delegation.ERROR_PARENT_NOT_FOUND, - 9: ExtrinsicErrors.Delegation.ERROR_NOT_PERMITTED_TO_REVOKE, - 10: ExtrinsicErrors.Delegation.ERROR_NOT_AUTHORIZED_TO_DELEGATE, - 11: ExtrinsicErrors.Delegation.ERROR_EXCEEDED_REVOCATION_BOUNDS, + 3: ExtrinsicErrors.Delegation.ERROR_DELEGATE_NOT_FOUND, + 4: ExtrinsicErrors.Delegation.ERROR_HIERARCHY_ALREADY_EXISTS, + 5: ExtrinsicErrors.Delegation.ERROR_HIERARCHY_NOT_FOUND, + 6: ExtrinsicErrors.Delegation.ERROR_MAX_DELEGATION_SEARCH_DEPTH_REACHED, + 7: ExtrinsicErrors.Delegation.ERROR_NOT_OWNER_OF_PARENT, + 8: ExtrinsicErrors.Delegation.ERROR_NOT_OWNER_OF_HIERARCHY, + 9: ExtrinsicErrors.Delegation.ERROR_PARENT_NOT_FOUND, + 10: ExtrinsicErrors.Delegation.ERROR_PARENT_REVOKED, + 11: ExtrinsicErrors.Delegation.ERROR_NOT_PERMITTED_TO_REVOKE, + 12: ExtrinsicErrors.Delegation.ERROR_NOT_AUTHORIZED_TO_DELEGATE, + 13: ExtrinsicErrors.Delegation.ERROR_EXCEEDED_REVOCATION_BOUNDS, + 14: ExtrinsicErrors.Delegation.ERROR_EXCEEDED_MAX_REVOCATIONS_ALLOWED, + 15: ExtrinsicErrors.Delegation.ERROR_EXCEEDED_MAX_PARENT_CHECKS_ALLOWED, + 16: ExtrinsicErrors.Delegation.INTERNAL_ERROR, [-1]: ExtrinsicErrors.Delegation.UNKNOWN_ERROR, }, [PalletIndex.DID]: { diff --git a/packages/core/src/__integrationtests__/Delegation.spec.ts b/packages/core/src/__integrationtests__/Delegation.spec.ts index 710e4a5a3..5a7702df5 100644 --- a/packages/core/src/__integrationtests__/Delegation.spec.ts +++ b/packages/core/src/__integrationtests__/Delegation.spec.ts @@ -9,23 +9,15 @@ * @group integration/delegation */ -import type { ICType } from '@kiltprotocol/types' +import type { ICType, IDelegationNode } from '@kiltprotocol/types' import { Permission } from '@kiltprotocol/types' -import { UUID } from '@kiltprotocol/utils' import { BlockchainUtils } from '@kiltprotocol/chain-helpers' -import { AttestedClaim, Identity } from '..' import Attestation from '../attestation/Attestation' -import { config, disconnect } from '../kilt' import Claim from '../claim/Claim' -import { - fetchChildren, - getAttestationHashes, - getChildIds, -} from '../delegation/Delegation.chain' -import { decodeDelegationNode } from '../delegation/DelegationDecoder' -import DelegationNode from '../delegation/DelegationNode' -import DelegationRootNode from '../delegation/DelegationRootNode' import RequestForAttestation from '../requestforattestation/RequestForAttestation' +import { AttestedClaim, Identity } from '..' +import { config, disconnect } from '../kilt' +import DelegationNode from '../delegation/DelegationNode' import { CtypeOnChain, DriversLicense, @@ -34,51 +26,51 @@ import { wannabeFaucet, WS_ADDRESS, } from './utils' +import { getAttestationHashes } from '../delegation/DelegationNode.chain' -async function writeRoot( +async function writeHierarchy( delegator: Identity, ctypeHash: ICType['hash'] -): Promise { - const root = new DelegationRootNode({ - id: UUID.generate(), - cTypeHash: ctypeHash, +): Promise { + const rootNode = DelegationNode.newRoot({ account: delegator.address, - revoked: false, + permissions: [Permission.DELEGATE], + cTypeHash: ctypeHash, }) - await root.store().then((tx) => + await rootNode.store().then((tx) => BlockchainUtils.signAndSubmitTx(tx, delegator, { resolveOn: BlockchainUtils.IS_IN_BLOCK, reSign: true, }) ) - return root + + return rootNode } + async function addDelegation( - parentNode: DelegationRootNode | DelegationNode, + hierarchyId: IDelegationNode['id'], + parentId: DelegationNode['id'], delegator: Identity, delegee: Identity, permissions: Permission[] = [Permission.ATTEST, Permission.DELEGATE] ): Promise { - const rootId = - parentNode instanceof DelegationRootNode ? parentNode.id : parentNode.rootId - const delegation = new DelegationNode({ - id: UUID.generate(), - rootId, + const delegationNode = DelegationNode.newNode({ + hierarchyId, + parentId, account: delegee.address, permissions, - parentId: parentNode.id, - revoked: false, }) - await delegation - .store(delegee.signStr(delegation.generateHash())) + + await delegationNode + .store(delegee.signStr(delegationNode.generateHash())) .then((tx) => BlockchainUtils.signAndSubmitTx(tx, delegator, { resolveOn: BlockchainUtils.IS_IN_BLOCK, reSign: true, }) ) - return delegation + return delegationNode } let root: Identity @@ -102,8 +94,13 @@ beforeAll(async () => { }, 30_000) it('should be possible to delegate attestation rights', async () => { - const rootNode = await writeRoot(root, DriversLicense.hash) - const delegatedNode = await addDelegation(rootNode, root, attester) + const rootNode = await writeHierarchy(root, DriversLicense.hash) + const delegatedNode = await addDelegation( + rootNode.id, + rootNode.id, + root, + attester + ) await Promise.all([ expect(rootNode.verify()).resolves.toBeTruthy(), expect(delegatedNode.verify()).resolves.toBeTruthy(), @@ -111,12 +108,17 @@ it('should be possible to delegate attestation rights', async () => { }, 60_000) describe('and attestation rights have been delegated', () => { - let rootNode: DelegationRootNode + let rootNode: DelegationNode let delegatedNode: DelegationNode beforeAll(async () => { - rootNode = await writeRoot(root, DriversLicense.hash) - delegatedNode = await addDelegation(rootNode, root, attester) + rootNode = await writeHierarchy(root, DriversLicense.hash) + delegatedNode = await addDelegation( + rootNode.id, + rootNode.id, + root, + attester + ) await Promise.all([ expect(rootNode.verify()).resolves.toBeTruthy(), @@ -181,9 +183,10 @@ describe('revocation', () => { }) it('delegator can revoke delegation', async () => { - const delegationRoot = await writeRoot(delegator, DriversLicense.hash) + const rootNode = await writeHierarchy(delegator, DriversLicense.hash) const delegationA = await addDelegation( - delegationRoot, + rootNode.id, + rootNode.id, delegator, firstDelegee ) @@ -199,14 +202,15 @@ describe('revocation', () => { }, 40_000) it('delegee cannot revoke root but can revoke own delegation', async () => { - const delegationRoot = await writeRoot(delegator, DriversLicense.hash) + const delegationRoot = await writeHierarchy(delegator, DriversLicense.hash) const delegationA = await addDelegation( - delegationRoot, + delegationRoot.id, + delegationRoot.id, delegator, firstDelegee ) await expect( - delegationRoot.revoke().then((tx) => + delegationRoot.revoke(firstDelegee.address).then((tx) => BlockchainUtils.signAndSubmitTx(tx, firstDelegee, { resolveOn: BlockchainUtils.IS_IN_BLOCK, reSign: true, @@ -227,19 +231,22 @@ describe('revocation', () => { }, 60_000) it('delegator can revoke root, revoking all delegations in tree', async () => { - const delegationRoot = await writeRoot(delegator, DriversLicense.hash) + let delegationRoot = await writeHierarchy(delegator, DriversLicense.hash) const delegationA = await addDelegation( - delegationRoot, + delegationRoot.id, + delegationRoot.id, delegator, firstDelegee ) const delegationB = await addDelegation( - delegationA, + delegationRoot.id, + delegationA.id, firstDelegee, secondDelegee ) + delegationRoot = await delegationRoot.getLatestState() await expect( - delegationRoot.revoke().then((tx) => + delegationRoot.revoke(delegator.address).then((tx) => BlockchainUtils.signAndSubmitTx(tx, delegator, { resolveOn: BlockchainUtils.IS_IN_BLOCK, reSign: true, @@ -256,31 +263,13 @@ describe('revocation', () => { }) describe('handling queries to data not on chain', () => { - it('getChildIds on empty', async () => { - return expect(getChildIds('0x012012012')).resolves.toEqual([]) - }) - it('DelegationNode query on empty', async () => { return expect(DelegationNode.query('0x012012012')).resolves.toBeNull() }) - it('DelegationRootNode.query on empty', async () => { - return expect(DelegationRootNode.query('0x012012012')).resolves.toBeNull() - }) - it('getAttestationHashes on empty', async () => { return expect(getAttestationHashes('0x012012012')).resolves.toEqual([]) }) - - it('fetchChildren on empty', async () => { - return expect( - fetchChildren(['0x012012012']).then((res) => - res.map((el) => { - return { id: el.id, codec: decodeDelegationNode(el.codec) } - }) - ) - ).resolves.toEqual([{ id: '0x012012012', codec: null }]) - }) }) afterAll(() => { diff --git a/packages/core/src/attestation/Attestation.ts b/packages/core/src/attestation/Attestation.ts index d31723740..4da2b3ab3 100644 --- a/packages/core/src/attestation/Attestation.ts +++ b/packages/core/src/attestation/Attestation.ts @@ -22,12 +22,12 @@ import type { SubmittableExtrinsic } from '@polkadot/api/promise/types' import type { IPublicIdentity, IAttestation, + IDelegationHierarchyDetails, IRequestForAttestation, CompressedAttestation, } from '@kiltprotocol/types' import { revoke, query, store } from './Attestation.chain' import AttestationUtils from './Attestation.utils' -import DelegationRootNode from '../delegation/DelegationRootNode' import DelegationNode from '../delegation/DelegationNode' export default class Attestation implements IAttestation { @@ -109,24 +109,25 @@ export default class Attestation implements IAttestation { * [STATIC] [ASYNC] Tries to query the delegationId and if successful query the rootId. * * @param delegationId - The Id of the Delegation stored in [[Attestation]]. - * @returns A promise of either null if querying was not successful or the affiliated [[DelegationRootNode]]. + * @returns A promise of either null if querying was not successful or the affiliated [[DelegationNode]]. */ - public static async getDelegationRoot( + public static async getDelegationDetails( delegationId: IAttestation['delegationId'] | null - ): Promise { - if (delegationId) { - const delegationNode: DelegationNode | null = await DelegationNode.query( - delegationId - ) - if (delegationNode) { - return delegationNode.getRoot() - } + ): Promise { + if (!delegationId) { + return null } - return null + const delegationNode: DelegationNode | null = await DelegationNode.query( + delegationId + ) + if (!delegationNode) { + return null + } + return delegationNode.getHierarchyDetails() } - public async getDelegationRoot(): Promise { - return Attestation.getDelegationRoot(this.delegationId) + public async getDelegationDetails(): Promise { + return Attestation.getDelegationDetails(this.delegationId) } /** diff --git a/packages/core/src/delegation/Delegation.chain.ts b/packages/core/src/delegation/Delegation.chain.ts deleted file mode 100644 index 2fc3a054a..000000000 --- a/packages/core/src/delegation/Delegation.chain.ts +++ /dev/null @@ -1,76 +0,0 @@ -/** - * Copyright 2018-2021 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. - */ - -/** - * @packageDocumentation - * @module DelegationBaseNode - */ - -import type { Option, Vec } from '@polkadot/types' -import type { Hash } from '@polkadot/types/interfaces' -import type { IDelegationBaseNode } from '@kiltprotocol/types' -import { DecoderUtils } from '@kiltprotocol/utils' -import { BlockchainApiConnection } from '@kiltprotocol/chain-helpers' -import type { CodecWithId, IChainDelegationNode } from './DelegationDecoder' - -function decodeDelegatedAttestations(queryResult: Option>): string[] { - DecoderUtils.assertCodecIsType(queryResult, ['Option>']) - return queryResult.unwrapOrDefault().map((hash) => hash.toHex()) -} - -/** - * @param id - * @internal - */ -export async function getAttestationHashes( - id: IDelegationBaseNode['id'] -): Promise { - const blockchain = await BlockchainApiConnection.getConnectionOrConnect() - const encodedHashes = await blockchain.api.query.attestation.delegatedAttestations< - Option> - >(id) - return decodeDelegatedAttestations(encodedHashes) -} - -/** - * @param id - * @internal - */ -export async function getChildIds( - id: IDelegationBaseNode['id'] -): Promise { - const blockchain = await BlockchainApiConnection.getConnectionOrConnect() - const childIds = await blockchain.api.query.delegation.children< - Option> - >(id) - DecoderUtils.assertCodecIsType(childIds, ['Option>']) - return childIds.unwrapOrDefault().map((hash) => hash.toHex()) -} - -/** - * @param childIds - * @internal - */ -export async function fetchChildren( - childIds: string[] -): Promise>>> { - const blockchain = await BlockchainApiConnection.getConnectionOrConnect() - const val: Array - >> = await Promise.all( - childIds.map(async (childId: string) => { - const queryResult = await blockchain.api.query.delegation.delegations< - Option - >(childId) - return { - id: childId, - codec: queryResult, - } - }) - ) - return val -} diff --git a/packages/core/src/delegation/Delegation.spec.ts b/packages/core/src/delegation/Delegation.spec.ts deleted file mode 100644 index d980ab9d2..000000000 --- a/packages/core/src/delegation/Delegation.spec.ts +++ /dev/null @@ -1,199 +0,0 @@ -/** - * Copyright 2018-2021 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. - */ - -/** - * @group unit/delegation - */ - -/* eslint-disable @typescript-eslint/ban-ts-comment */ - -import { Permission } from '@kiltprotocol/types' -import type { ICType, IDelegationBaseNode } from '@kiltprotocol/types' -import { Crypto, SDKErrors } from '@kiltprotocol/utils' -import { mockChainQueryReturn } from '@kiltprotocol/chain-helpers/lib/blockchainApiConnection/__mocks__/BlockchainQuery' -import { Identity } from '..' -import DelegationNode from './DelegationNode' -import Kilt from '../kilt/Kilt' -import errorCheck from './Delegation.utils' - -jest.mock( - '@kiltprotocol/chain-helpers/lib/blockchainApiConnection/BlockchainApiConnection' -) - -const blockchainApi = require('@kiltprotocol/chain-helpers/lib/blockchainApiConnection/BlockchainApiConnection') - .__mocked_api - -Kilt.config({ address: 'ws://testString' }) - -describe('Delegation', () => { - let identityAlice: Identity - let rootId: IDelegationBaseNode['id'] - let nodeId: IDelegationBaseNode['id'] - let cTypeHash: ICType['hash'] - let myDelegation: DelegationNode - let children: DelegationNode[] - let attestationHashes: string[] - beforeAll(async () => { - identityAlice = Identity.buildFromURI('//Alice') - rootId = Crypto.hashStr('rootId') - nodeId = Crypto.hashStr('myNodeId') - cTypeHash = - 'kilt:ctype:0xba15bf4960766b0a6ad7613aa3338edce95df6b22ed29dd72f6e72d740829b84' - - blockchainApi.query.attestation.delegatedAttestations.mockReturnValue( - mockChainQueryReturn('attestation', 'delegatedAttestations', [ - cTypeHash, - Crypto.hashStr('secondTest'), - Crypto.hashStr('thirdTest'), - ]) - ) - blockchainApi.query.delegation.root.mockReturnValue( - mockChainQueryReturn('delegation', 'root', [ - cTypeHash, - identityAlice.address, - false, - ]) - ) - - blockchainApi.query.delegation.delegations - // first call - .mockResolvedValueOnce( - mockChainQueryReturn('delegation', 'delegations', [ - rootId, - nodeId, - identityAlice.getPublicIdentity().address, - 2, - false, - ]) - ) - // second call - .mockResolvedValueOnce( - mockChainQueryReturn('delegation', 'delegations', [ - rootId, - nodeId, - identityAlice.getPublicIdentity().address, - 1, - false, - ]) - ) - // third call - .mockResolvedValueOnce( - mockChainQueryReturn('delegation', 'delegations', [ - rootId, - nodeId, - identityAlice.getPublicIdentity().address, - 1, - false, - ]) - ) - // default (any further calls) - .mockResolvedValue( - // Delegation: delegation-id -> (root-id, parent-id?, account, permissions, revoked) - mockChainQueryReturn('delegation', 'delegations') - ) - - blockchainApi.query.delegation.children.mockResolvedValue( - mockChainQueryReturn('delegation', 'children', [ - Crypto.hashStr('firstChild'), - Crypto.hashStr('secondChild'), - Crypto.hashStr('thirdChild'), - ]) - ) - }) - - it('get children', async () => { - myDelegation = new DelegationNode({ - id: nodeId, - rootId, - account: identityAlice.getPublicIdentity().address, - permissions: [Permission.ATTEST], - parentId: undefined, - revoked: false, - }) - children = await myDelegation.getChildren() - expect(children).toHaveLength(3) - expect(children[0]).toEqual({ - id: Crypto.hashStr('firstChild'), - rootId, - parentId: nodeId, - account: identityAlice.getPublicIdentity().address, - permissions: [Permission.DELEGATE], - revoked: false, - }) - expect(children[1]).toEqual({ - id: Crypto.hashStr('secondChild'), - rootId, - parentId: nodeId, - account: identityAlice.getPublicIdentity().address, - permissions: [Permission.ATTEST], - revoked: false, - }) - expect(children[2]).toEqual({ - id: Crypto.hashStr('thirdChild'), - rootId, - parentId: nodeId, - account: identityAlice.getPublicIdentity().address, - permissions: [Permission.ATTEST], - revoked: false, - }) - }) - it('get attestation hashes', async () => { - attestationHashes = await myDelegation.getAttestationHashes() - expect(attestationHashes).toHaveLength(3) - }) - - it('error check should throw errors on faulty delegation', async () => { - const malformedIdDelegation = { - id: nodeId.slice(13) + nodeId.slice(15), - account: identityAlice.address, - revoked: false, - } as IDelegationBaseNode - - const missingIdDelegation = { - id: nodeId, - account: identityAlice.address, - revoked: false, - } as IDelegationBaseNode - - // @ts-expect-error - delete missingIdDelegation.id - - const missingAccountDelegation = { - id: nodeId, - account: identityAlice.address, - revoked: false, - } as IDelegationBaseNode - - // @ts-expect-error - delete missingAccountDelegation.account - - const missingRevokedStatusDelegation = { - id: nodeId, - account: identityAlice.address, - revoked: false, - } as IDelegationBaseNode - - // @ts-expect-error - delete missingRevokedStatusDelegation.revoked - - expect(() => errorCheck(malformedIdDelegation)).toThrowError( - SDKErrors.ERROR_DELEGATION_ID_TYPE() - ) - - expect(() => errorCheck(missingIdDelegation)).toThrowError( - SDKErrors.ERROR_DELEGATION_ID_MISSING() - ) - - expect(() => errorCheck(missingAccountDelegation)).toThrowError( - SDKErrors.ERROR_OWNER_NOT_PROVIDED() - ) - - expect(() => errorCheck(missingRevokedStatusDelegation)).toThrowError( - new TypeError('revoked is expected to be a boolean') - ) - }) -}) diff --git a/packages/core/src/delegation/Delegation.ts b/packages/core/src/delegation/Delegation.ts deleted file mode 100644 index 4618d7238..000000000 --- a/packages/core/src/delegation/Delegation.ts +++ /dev/null @@ -1,160 +0,0 @@ -/** - * Copyright 2018-2021 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. - */ - -/** - * Delegations are the building blocks of top-down trust structures in KILT. An Attester can inherit trust through delegation from another attester ("top-down"). - * In order to model these trust hierarchies, a delegation is represented as a **node** in a **delegation tree**. - * - * A delegation object is stored on-chain, and can be revoked. A base node is created, a ID which may be used in a [[RequestForAttestation]]. - * A delegation can and may restrict permissions. - * - * Permissions: - * * Delegate. - * * Attest. - * - * @packageDocumentation - * @module DelegationBaseNode - * @preferred - */ - -import type { - IDelegationBaseNode, - SubmittableExtrinsic, -} from '@kiltprotocol/types' -import Attestation from '../attestation/Attestation' -import { query } from '../attestation/Attestation.chain' -import Identity from '../identity/Identity' -import { getAttestationHashes } from './Delegation.chain' -import DelegationNode from './DelegationNode' -import DelegationRootNode from './DelegationRootNode' -import errorCheck from './Delegation.utils' - -export default abstract class DelegationBaseNode - implements IDelegationBaseNode { - public id: IDelegationBaseNode['id'] - public account: IDelegationBaseNode['account'] - public revoked: IDelegationBaseNode['revoked'] = false - - /** - * Builds a new [DelegationBaseNode] instance. - * - * @param delegationBaseNodeInput - The base object from which to create the delegation base node. - */ - public constructor(delegationBaseNodeInput: IDelegationBaseNode) { - this.account = delegationBaseNodeInput.account - this.id = delegationBaseNodeInput.id - this.revoked = delegationBaseNodeInput.revoked - errorCheck(this) - } - - /** - * Fetches the root of the delegation tree. - * - * @returns Promise containing [[DelegationRootNode]]. - */ - public abstract getRoot(): Promise - - /** - * Fetches the parent delegation node. If the parent node is [null] this node is a direct child of the root node. - * - * @returns Promise containing the parent node or [null]. - */ - public abstract getParent(): Promise - - /** - * Fetches the children nodes of the current node. - * - * @returns Promise containing the resolved children nodes. - */ - public abstract getChildren(): Promise - - /** - * Fetches and resolves all attestations attested with this delegation node. - * - * @returns Promise containing all resolved attestations attested with this node. - */ - public async getAttestations(): Promise { - const attestationHashes = await this.getAttestationHashes() - const attestations = await Promise.all( - attestationHashes.map((claimHash: string) => { - return query(claimHash) - }) - ) - - return attestations.filter((value): value is Attestation => !!value) - } - - /** - * Fetches all hashes of attestations attested with this delegation node. - * - * @returns Promise containing all attestation hashes attested with this node. - */ - public async getAttestationHashes(): Promise { - return getAttestationHashes(this.id) - } - - /** - * Verifies this delegation node by querying it from chain and checking its [revoked] status. - * - * @returns Promise containing a boolean flag indicating if the verification succeeded. - */ - public abstract verify(): Promise - - /** - * Revokes this delegation node on chain. - * - * @param address The address of the identity used to revoke the delegation. - * @returns Promise containing a unsigned submittable transaction. - */ - public abstract revoke(address: string): Promise - - /** - * Checks on chain whether a identity with the given address is delegating to the current node. - * - * @param address The address of the identity. - * @returns An object containing a `node` owned by the identity if it is delegating, plus the number of `steps` traversed. `steps` is 0 if the address is owner of the current node. - */ - public async findAncestorOwnedBy( - address: Identity['address'] - ): Promise<{ steps: number; node: DelegationBaseNode | null }> { - if (this.account === address) { - return { - steps: 0, - node: this, - } - } - const parent = await this.getParent() - if (parent) { - const result = await parent.findAncestorOwnedBy(address) - result.steps += 1 - return result - } - return { - steps: 0, - node: null, - } - } - - /** - * Recursively counts all nodes in the branches below the current node (excluding the current node). - * - * @returns Promise resolving to the node count. - */ - public async subtreeNodeCount(): Promise { - const children = await this.getChildren() - if (children.length > 0) { - const childrensChildCounts = await Promise.all( - children.map((child) => child.subtreeNodeCount()) - ) - return ( - children.length + - childrensChildCounts.reduce((previous, current) => previous + current) - ) - } - return 0 - } -} diff --git a/packages/core/src/delegation/Delegation.utils.ts b/packages/core/src/delegation/Delegation.utils.ts deleted file mode 100644 index 3c5c8cef9..000000000 --- a/packages/core/src/delegation/Delegation.utils.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** - * Copyright 2018-2021 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 { IDelegationBaseNode } from '@kiltprotocol/types' -import { SDKErrors, DataUtils } from '@kiltprotocol/utils' -import { isHex } from '@polkadot/util' - -export default function errorCheck( - delegationBaseNode: IDelegationBaseNode -): void { - const { id, account, revoked } = delegationBaseNode - if (!id) { - throw SDKErrors.ERROR_DELEGATION_ID_MISSING() - } else if (typeof id !== 'string') { - throw SDKErrors.ERROR_DELEGATION_ID_TYPE() - } else if (!isHex(id)) { - throw SDKErrors.ERROR_DELEGATION_ID_TYPE() - } - - if (!account) { - throw SDKErrors.ERROR_OWNER_NOT_PROVIDED() - } else DataUtils.validateAddress(account, 'delegationNode owner') - - if (typeof revoked !== 'boolean') { - throw new TypeError('revoked is expected to be a boolean') - } -} diff --git a/packages/core/src/delegation/DelegationDecoder.ts b/packages/core/src/delegation/DelegationDecoder.ts index 1c41bfe2c..3ddc25ffe 100644 --- a/packages/core/src/delegation/DelegationDecoder.ts +++ b/packages/core/src/delegation/DelegationDecoder.ts @@ -6,7 +6,7 @@ */ /** - * When [[DelegationNode]]s or [[DelegationRootNode]]s are written on the blockchain, they're encoded. + * When a [[DelegationNode]] or a [[DelegationHierarchy]] is written on the blockchain, it is encoded. * DelegationDecoder helps to decode them when they're queried from the chain. * * The DelegationDecoder methods transform a Codec type into an object of a KILT type. @@ -18,45 +18,43 @@ /** * Dummy comment needed for correct doc display, do not remove. */ -import { Permission } from '@kiltprotocol/types' +import type { IDelegationNode } from '@kiltprotocol/types' +import { IDelegationHierarchyDetails, Permission } from '@kiltprotocol/types' import type { Option } from '@polkadot/types' -import type { IDelegationRootNode } from '@kiltprotocol/types' -import type { Struct } from '@polkadot/types/codec' +import type { BTreeSet, Struct } from '@polkadot/types/codec' import type { AccountId, Hash } from '@polkadot/types/interfaces/runtime' -import type { u32 } from '@polkadot/types/primitive' +import type { Bool, u32 } from '@polkadot/types/primitive' import { DecoderUtils } from '@kiltprotocol/utils' -import { DelegationNode } from '..' export type CodecWithId = { id: string codec: C } -export type RootDelegationRecord = Pick< - IDelegationRootNode, - 'cTypeHash' | 'account' | 'revoked' +export type DelegationHierarchyDetailsRecord = Pick< + IDelegationHierarchyDetails, + 'cTypeHash' > -export interface IChainDelegationRoot extends Struct { - readonly ctypeHash: Hash - readonly owner: AccountId - readonly revoked: boolean +export type CtypeHash = Hash + +export interface IChainDelegationHierarchyDetails extends Struct { + readonly ctypeHash: CtypeHash } -export function decodeRootDelegation( - encoded: Option -): RootDelegationRecord | null { - DecoderUtils.assertCodecIsType(encoded, ['Option']) - if (encoded.isSome) { - const delegationRoot = encoded.unwrap() - // TODO: check that root is none - return { - cTypeHash: delegationRoot.ctypeHash.toString(), - account: delegationRoot.owner.toString(), - revoked: delegationRoot.revoked.valueOf(), - } +export function decodeDelegationHierarchyDetails( + encoded: Option +): DelegationHierarchyDetailsRecord | null { + DecoderUtils.assertCodecIsType(encoded, [ + 'Option', + ]) + if (encoded.isNone) { + return null + } + const delegationHierarchyDetails = encoded.unwrap() + return { + cTypeHash: delegationHierarchyDetails.ctypeHash.toHex(), } - return null } /** @@ -79,37 +77,44 @@ function decodePermissions(bitset: number): Permission[] { return permissions } -export type DelegationNodeRecord = Pick< - DelegationNode, - 'rootId' | 'parentId' | 'account' | 'permissions' | 'revoked' -> +export type DelegationNodeRecord = Omit export type DelegationNodeId = Hash export interface IChainDelegationNode extends Struct { - readonly rootId: DelegationNodeId + readonly hierarchyRootId: DelegationNodeId readonly parent: Option - readonly owner: AccountId + readonly children: BTreeSet + readonly details: IChainDelegationDetails +} + +export type DelegationOwner = AccountId + +export interface IChainDelegationDetails extends Struct { + readonly owner: DelegationOwner + readonly revoked: Bool readonly permissions: u32 - readonly revoked: boolean } export function decodeDelegationNode( encoded: Option ): DelegationNodeRecord | null { DecoderUtils.assertCodecIsType(encoded, ['Option']) - if (encoded.isSome) { - const delegationNode = encoded.unwrap() + if (encoded.isNone) { + return null + } + const delegationNode = encoded.unwrap() - return { - rootId: delegationNode.rootId.toString(), - parentId: delegationNode.parent.isSome - ? delegationNode.parent.toString() - : undefined, - account: delegationNode.owner.toString(), - permissions: decodePermissions(delegationNode.permissions.toNumber()), - revoked: delegationNode.revoked.valueOf(), - } + return { + hierarchyId: delegationNode.hierarchyRootId.toHex(), + parentId: delegationNode.parent.isSome + ? delegationNode.parent.toHex() + : undefined, + childrenIds: [...delegationNode.children.keys()].map((id) => id.toHex()), + account: delegationNode.details.owner.toString(), + permissions: decodePermissions( + delegationNode.details.permissions.toNumber() + ), + revoked: delegationNode.details.revoked.valueOf(), } - return null } diff --git a/packages/core/src/delegation/DelegationHierarchyDetails.chain.ts b/packages/core/src/delegation/DelegationHierarchyDetails.chain.ts new file mode 100644 index 000000000..fa88d6aeb --- /dev/null +++ b/packages/core/src/delegation/DelegationHierarchyDetails.chain.ts @@ -0,0 +1,46 @@ +/** + * Copyright 2018-2021 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 { + IDelegationHierarchyDetails, + IDelegationNode, +} from '@kiltprotocol/types' +import type { Option } from '@polkadot/types' +import { BlockchainApiConnection } from '@kiltprotocol/chain-helpers' +import { + decodeDelegationHierarchyDetails, + DelegationHierarchyDetailsRecord, + IChainDelegationHierarchyDetails, +} from './DelegationDecoder' + +/** + * @packageDocumentation + * @module DelegationHierarchyDetails + */ + +/** + * @param rootId + * @internal + */ +// eslint-disable-next-line import/prefer-default-export +export async function query( + rootId: IDelegationNode['id'] +): Promise { + const blockchain = await BlockchainApiConnection.getConnectionOrConnect() + const decoded: DelegationHierarchyDetailsRecord | null = decodeDelegationHierarchyDetails( + await blockchain.api.query.delegation.delegationHierarchies< + Option + >(rootId) + ) + if (!decoded) { + return null + } + return { + ...decoded, + id: rootId, + } +} diff --git a/packages/core/src/delegation/DelegationNode.chain.ts b/packages/core/src/delegation/DelegationNode.chain.ts index 3dd3e7970..4c9310f0a 100644 --- a/packages/core/src/delegation/DelegationNode.chain.ts +++ b/packages/core/src/delegation/DelegationNode.chain.ts @@ -10,47 +10,62 @@ * @module DelegationNode */ -import type { Option } from '@polkadot/types' +import type { Option, Vec } from '@polkadot/types' import type { IDelegationNode, SubmittableExtrinsic } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' import { BlockchainApiConnection } from '@kiltprotocol/chain-helpers' -import DelegationBaseNode from './Delegation' -import { fetchChildren, getChildIds } from './Delegation.chain' -import { - CodecWithId, - decodeDelegationNode, - IChainDelegationNode, -} from './DelegationDecoder' +import type { Hash } from '@polkadot/types/interfaces' +import { DecoderUtils, SDKErrors } from '@kiltprotocol/utils' +import { decodeDelegationNode, IChainDelegationNode } from './DelegationDecoder' import DelegationNode from './DelegationNode' import { permissionsAsBitset } from './DelegationNode.utils' -const log = ConfigService.LoggingFactory.getLogger('DelegationBaseNode') +const log = ConfigService.LoggingFactory.getLogger('DelegationNode') + +/** + * @param delegation + * @internal + */ +export async function storeAsRoot( + delegation: DelegationNode +): Promise { + const blockchain = await BlockchainApiConnection.getConnectionOrConnect() + + if (!delegation.isRoot()) { + throw SDKErrors.ERROR_INVALID_ROOT_NODE + } + return blockchain.api.tx.delegation.createHierarchy( + delegation.hierarchyId, + await delegation.getCTypeHash() + ) +} /** * @param delegation * @param signature * @internal */ -export async function store( - delegation: IDelegationNode, +export async function storeAsDelegation( + delegation: DelegationNode, signature: string ): Promise { const blockchain = await BlockchainApiConnection.getConnectionOrConnect() - const includeParentId: boolean = delegation.parentId - ? delegation.parentId !== delegation.rootId - : false - const tx: SubmittableExtrinsic = blockchain.api.tx.delegation.addDelegation( + + if (delegation.isRoot()) { + throw SDKErrors.ERROR_INVALID_DELEGATION_NODE + } + + return blockchain.api.tx.delegation.addDelegation( delegation.id, - delegation.rootId, - includeParentId ? delegation.parentId : undefined, + delegation.parentId, delegation.account, permissionsAsBitset(delegation), signature ) - return tx } /** + * @param delegation * @param delegationId * @internal */ @@ -59,23 +74,17 @@ export async function query( ): Promise { const blockchain = await BlockchainApiConnection.getConnectionOrConnect() const decoded = decodeDelegationNode( - await blockchain.api.query.delegation.delegations< + await blockchain.api.query.delegation.delegationNodes< Option >(delegationId) ) - if (decoded) { - const root = new DelegationNode({ - id: delegationId, - rootId: decoded.rootId, - account: decoded.account, - permissions: decoded.permissions, - parentId: decoded.parentId, - revoked: decoded.revoked, - }) - - return root + if (!decoded) { + return null } - return null + return new DelegationNode({ + ...decoded, + id: delegationId, + }) } /** @@ -104,36 +113,47 @@ export async function revoke( /** * @param delegationNodeId + * @param delegationNode * @internal */ -// function lives here to avoid circular imports between DelegationBaseNode and DelegationNode export async function getChildren( - delegationNodeId: DelegationBaseNode['id'] + delegationNode: DelegationNode ): Promise { - log.info(` :: getChildren('${delegationNodeId}')`) - const childIds: string[] = await getChildIds(delegationNodeId) - const queryResults: Array - >> = await fetchChildren(childIds) - const children: DelegationNode[] = queryResults - .map((codec: CodecWithId>) => { - const decoded = decodeDelegationNode(codec.codec) - if (decoded) { - const child = new DelegationNode({ - id: codec.id, - rootId: decoded.rootId, - account: decoded.account, - permissions: decoded.permissions, - parentId: decoded.parentId, - revoked: decoded.revoked, - }) - return child + log.info(` :: getChildren('${delegationNode.id}')`) + const childrenNodes = await Promise.all( + delegationNode.childrenIds.map(async (childId) => { + const childNode = await query(childId) + if (!childNode) { + throw SDKErrors.ERROR_DELEGATION_ID_MISSING } - return null - }) - .filter((value): value is DelegationNode => { - return value !== null + return childNode }) - log.info(`children: ${JSON.stringify(children)}`) - return children + ) + log.info(`children: ${JSON.stringify(childrenNodes)}`) + return childrenNodes +} + +/** + * @param delegationNodeId + * @param queryResult + * @internal + */ +function decodeDelegatedAttestations(queryResult: Option>): string[] { + DecoderUtils.assertCodecIsType(queryResult, ['Option>']) + return queryResult.unwrapOrDefault().map((hash) => hash.toHex()) +} + +/** + * @param delegationNodeId + * @param id + * @internal + */ +export async function getAttestationHashes( + id: IDelegationNode['id'] +): Promise { + const blockchain = await BlockchainApiConnection.getConnectionOrConnect() + const encodedHashes = await blockchain.api.query.attestation.delegatedAttestations< + Option> + >(id) + return decodeDelegatedAttestations(encodedHashes) } diff --git a/packages/core/src/delegation/DelegationNode.spec.ts b/packages/core/src/delegation/DelegationNode.spec.ts index d9bb9b0ad..0901c9b75 100644 --- a/packages/core/src/delegation/DelegationNode.spec.ts +++ b/packages/core/src/delegation/DelegationNode.spec.ts @@ -11,465 +11,605 @@ /* eslint-disable @typescript-eslint/ban-ts-comment */ -import { IDelegationNode, Permission } from '@kiltprotocol/types' +import { + IDelegationNode, + IDelegationHierarchyDetails, + Permission, +} from '@kiltprotocol/types' import { encodeAddress } from '@polkadot/keyring' import { Crypto, SDKErrors } from '@kiltprotocol/utils' import Identity from '../identity' import DelegationNode from './DelegationNode' -import DelegationRootNode from './DelegationRootNode' import { permissionsAsBitset, errorCheck } from './DelegationNode.utils' -let childMap: Record = {} +let hierarchiesDetails: Record = {} let nodes: Record = {} -let rootNodes: Record = {} -jest.mock('./DelegationNode.chain', () => ({ - getChildren: jest.fn(async (id: string) => { - return childMap[id] || [] - }), - query: jest.fn(async (id: string) => nodes[id] || null), -})) +jest.mock('./DelegationNode.chain', () => { + return { + getChildren: jest.fn(async (node: DelegationNode) => + node.childrenIds.map((id) => nodes[id] || null) + ), + query: jest.fn(async (id: string) => nodes[id] || null), + storeAsRoot: jest.fn(async (node: DelegationNode) => { + nodes[node.id] = node + hierarchiesDetails[node.id] = { + id: node.id, + cTypeHash: await node.getCTypeHash(), + } + }), + revoke: jest.fn( + async ( + nodeId: IDelegationNode['id'], + // eslint-disable-next-line @typescript-eslint/no-unused-vars + maxDepth: number, + // eslint-disable-next-line @typescript-eslint/no-unused-vars + maxRevocations: number + ) => { + nodes[nodeId] = new DelegationNode({ + ...nodes[nodeId], + revoked: true, + }) + } + ), + } +}) -jest.mock('./DelegationRootNode.chain', () => ({ - query: jest.fn(async (id: string) => rootNodes[id] || null), +jest.mock('./DelegationHierarchyDetails.chain', () => ({ + query: jest.fn(async (id: string) => hierarchiesDetails[id] || null), })) -let identityAlice: Identity -let identityBob: Identity -let id: string -let successId: string -let failureId: string -let rootId: string -let parentId: string -let hashList: string[] -let addresses: string[] - -beforeAll(() => { - identityAlice = Identity.buildFromURI('//Alice') - identityBob = Identity.buildFromURI('//Bob') - successId = Crypto.hashStr('success') - rootId = Crypto.hashStr('rootId') - id = Crypto.hashStr('id') - parentId = Crypto.hashStr('parentId') - failureId = Crypto.hashStr('failure') - hashList = Array(10002) - .fill('') - .map((_val, index) => Crypto.hashStr(`${index + 1}`)) - addresses = Array(10002) - .fill('') - .map((_val, index) => - encodeAddress(Crypto.hash(`${index}`, 256), 38) - ) -}) - -describe('Delegation', () => { - it('delegation generate hash', () => { - const node = new DelegationNode({ - id, - rootId, - account: identityBob.address, - permissions: [Permission.DELEGATE], - parentId, - revoked: false, - }) - const hash: string = node.generateHash() - expect(hash).toBe( - '0x3f3dc0df7527013f2373f18f55cf87847df3249526e9b1d5aa75df8eeb5b7d6e' - ) - }) +describe('DelegationNode', () => { + let identityAlice: Identity + let identityBob: Identity + let id: string + let successId: string + let failureId: string + let hierarchyId: string + let parentId: string + let hashList: string[] + let addresses: string[] - it('delegation permissionsAsBitset', () => { - const node = new DelegationNode({ - id, - rootId, - account: identityBob.address, - permissions: [Permission.DELEGATE], - parentId, - revoked: false, - }) - const permissions: Uint8Array = permissionsAsBitset(node) - const expected: Uint8Array = new Uint8Array(4) - expected[0] = 2 - expect(permissions.toString()).toBe(expected.toString()) + beforeAll(() => { + identityAlice = Identity.buildFromURI('//Alice') + identityBob = Identity.buildFromURI('//Bob') + successId = Crypto.hashStr('success') + hierarchyId = Crypto.hashStr('rootId') + id = Crypto.hashStr('id') + parentId = Crypto.hashStr('parentId') + failureId = Crypto.hashStr('failure') + hashList = Array(10002) + .fill('') + .map((_val, index) => Crypto.hashStr(`${index + 1}`)) + addresses = Array(10002) + .fill('') + .map((_val, index) => + encodeAddress(Crypto.hash(`${index}`, 256), 38) + ) }) - it('delegation verify', async () => { - nodes = { - [successId]: new DelegationNode({ - id: successId, - rootId, - account: identityAlice.address, + describe('Delegation', () => { + it('delegation generate hash', () => { + const node = new DelegationNode({ + id, + hierarchyId, + parentId, + account: identityBob.address, + childrenIds: [], permissions: [Permission.DELEGATE], - parentId: undefined, revoked: false, - }), - [failureId]: { - ...nodes.success, - revoked: true, - id: failureId, - } as DelegationNode, - } + }) + const hash: string = node.generateHash() + expect(hash).toBe( + '0x3f3dc0df7527013f2373f18f55cf87847df3249526e9b1d5aa75df8eeb5b7d6e' + ) + }) - expect( - await new DelegationNode({ - id: successId, - rootId, - account: identityAlice.address, + it('delegation permissionsAsBitset', () => { + const node = new DelegationNode({ + id, + hierarchyId, + parentId, + account: identityBob.address, + childrenIds: [], permissions: [Permission.DELEGATE], - parentId: undefined, revoked: false, - }).verify() - ).toBe(true) + }) + const permissions: Uint8Array = permissionsAsBitset(node) + const expected: Uint8Array = new Uint8Array(4) + expected[0] = 2 + expect(permissions.toString()).toBe(expected.toString()) + }) - expect( - await new DelegationNode({ - id: failureId, - rootId, + it('delegation verify', async () => { + nodes = { + [successId]: new DelegationNode({ + id: successId, + hierarchyId, + account: identityAlice.address, + childrenIds: [], + permissions: [Permission.DELEGATE], + parentId: undefined, + revoked: false, + }), + [failureId]: { + ...nodes.success, + revoked: true, + id: failureId, + } as DelegationNode, + } + + expect( + await new DelegationNode({ + id: successId, + hierarchyId, + account: identityAlice.address, + childrenIds: [], + permissions: [Permission.DELEGATE], + parentId: undefined, + revoked: false, + }).verify() + ).toBe(true) + + expect( + await new DelegationNode({ + id: failureId, + hierarchyId, + account: identityAlice.address, + childrenIds: [], + permissions: [Permission.DELEGATE], + parentId: undefined, + revoked: false, + }).verify() + ).toBe(false) + }) + + it('get delegation root', async () => { + hierarchiesDetails = { + [hierarchyId]: { + id: hierarchyId, + cTypeHash: + 'kilt:ctype:0xba15bf4960766b0a6ad7613aa3338edce95df6b22ed29dd72f6e72d740829b84', + }, + } + + nodes = { + [hierarchyId]: new DelegationNode({ + id: hierarchyId, + hierarchyId, + account: identityAlice.address, + childrenIds: [], + permissions: [Permission.DELEGATE], + revoked: false, + }), + } + + const node: DelegationNode = new DelegationNode({ + id, + hierarchyId, account: identityAlice.address, + childrenIds: [], permissions: [Permission.DELEGATE], - parentId: undefined, revoked: false, - }).verify() - ).toBe(false) + }) + const hierarchyDetails = await node.getHierarchyDetails() + + expect(hierarchyDetails).toBeDefined() + expect(hierarchyDetails.cTypeHash).toBe( + 'kilt:ctype:0xba15bf4960766b0a6ad7613aa3338edce95df6b22ed29dd72f6e72d740829b84' + ) + }) }) - it('get delegation root', async () => { - rootNodes = { - [rootId]: new DelegationRootNode({ - id: rootId, - cTypeHash: - 'kilt:ctype:0xba15bf4960766b0a6ad7613aa3338edce95df6b22ed29dd72f6e72d740829b84', + describe('count subtree', () => { + let topNode: DelegationNode + const a1: string = Crypto.hashStr('a1') + const b1: string = Crypto.hashStr('b1') + const b2: string = Crypto.hashStr('b2') + const c1: string = Crypto.hashStr('c1') + const c2: string = Crypto.hashStr('c2') + const d1: string = Crypto.hashStr('d1') + beforeAll(() => { + topNode = new DelegationNode({ + id: a1, + hierarchyId, account: identityAlice.address, + childrenIds: [b1, b2], + permissions: [Permission.ATTEST, Permission.DELEGATE], revoked: false, - }), - } + }) + + nodes = { + [a1]: topNode, + [b1]: new DelegationNode({ + id: b1, + hierarchyId, + account: identityAlice.address, + permissions: [Permission.ATTEST, Permission.DELEGATE], + parentId: a1, + childrenIds: [c1, c2], + revoked: false, + }), + [b2]: new DelegationNode({ + id: b2, + hierarchyId, + account: identityAlice.address, + childrenIds: [], + permissions: [Permission.ATTEST, Permission.DELEGATE], + parentId: a1, + revoked: false, + }), + [c1]: new DelegationNode({ + id: c1, + hierarchyId, + account: identityAlice.address, + childrenIds: [d1], + permissions: [Permission.ATTEST, Permission.DELEGATE], + parentId: b1, + revoked: false, + }), + [c2]: new DelegationNode({ + id: c2, + hierarchyId, + account: identityAlice.address, + childrenIds: [], + permissions: [Permission.ATTEST, Permission.DELEGATE], + parentId: b1, + revoked: false, + }), + [d1]: new DelegationNode({ + id: d1, + hierarchyId, + account: identityAlice.address, + childrenIds: [], + permissions: [Permission.ATTEST, Permission.DELEGATE], + parentId: c1, + revoked: false, + }), + } + }) - const node: DelegationNode = new DelegationNode({ - id, - rootId, - account: identityAlice.address, - permissions: [Permission.DELEGATE], - revoked: false, + it('mocks work', async () => { + expect(topNode.id).toEqual(a1) + await expect( + topNode.getChildren().then((children) => { + return children.map((childNode) => childNode.id) + }) + ).resolves.toStrictEqual(topNode.childrenIds) + await expect(nodes[d1].getChildren()).resolves.toStrictEqual([]) + }) + + it('counts all subnodes', async () => { + await expect(topNode.subtreeNodeCount()).resolves.toStrictEqual(5) + }) + + it('counts smaller subtrees', async () => { + await expect(nodes[b2].subtreeNodeCount()).resolves.toStrictEqual(0) + await expect(nodes[b1].subtreeNodeCount()).resolves.toStrictEqual(3) + await expect(nodes[c1].subtreeNodeCount()).resolves.toStrictEqual(1) + await expect(nodes[c2].subtreeNodeCount()).resolves.toStrictEqual(0) + await expect(nodes[d1].subtreeNodeCount()).resolves.toStrictEqual(0) + }) + + it('counts all subnodes in deeply nested structure (100)', async () => { + const lastIndex = 100 + nodes = hashList + .slice(0, lastIndex + 1) + .reduce((previous, current, index) => { + return { + ...previous, + [current]: new DelegationNode({ + id: current, + hierarchyId, + account: identityAlice.address, + permissions: [Permission.DELEGATE], + childrenIds: index < lastIndex ? [hashList[index + 1]] : [], + parentId: hashList[index - 1], + revoked: false, + }), + } + }, {}) + await expect( + nodes[hashList[0]].subtreeNodeCount() + ).resolves.toStrictEqual(100) }) - const rootNode = await node.getRoot() - expect(rootNode).toBeDefined() - expect(rootNode.cTypeHash).toBe( - 'kilt:ctype:0xba15bf4960766b0a6ad7613aa3338edce95df6b22ed29dd72f6e72d740829b84' - ) + it('counts all subnodes in deeply nested structure (1000)', async () => { + const lastIndex = 1000 + nodes = hashList + .slice(0, lastIndex + 1) + .reduce((previous, current, index) => { + return { + ...previous, + [current]: new DelegationNode({ + id: current, + hierarchyId, + account: identityAlice.address, + permissions: [Permission.DELEGATE], + childrenIds: index < lastIndex ? [hashList[index + 1]] : [], + parentId: hashList[index - 1], + revoked: false, + }), + } + }, {}) + await expect( + nodes[hashList[0]].subtreeNodeCount() + ).resolves.toStrictEqual(1000) + }) + + it('counts all subnodes in deeply nested structure (10000)', async () => { + const lastIndex = 10000 + nodes = hashList + .slice(0, lastIndex + 1) + .reduce((previous, current, index) => { + return { + ...previous, + [current]: new DelegationNode({ + id: current, + hierarchyId, + account: identityAlice.address, + permissions: [Permission.DELEGATE], + childrenIds: index < lastIndex ? [hashList[index + 1]] : [], + parentId: hashList[index - 1], + revoked: false, + }), + } + }, {}) + await expect( + nodes[hashList[0]].subtreeNodeCount() + ).resolves.toStrictEqual(10000) + }) }) -}) -describe('count subtree', () => { - let topNode: DelegationNode - const a1: string = Crypto.hashStr('a1') - const b1: string = Crypto.hashStr('b1') - const b2: string = Crypto.hashStr('b2') - const c1: string = Crypto.hashStr('c1') - const c2: string = Crypto.hashStr('c2') - const d1: string = Crypto.hashStr('d1') - beforeAll(() => { - topNode = new DelegationNode({ - id: a1, - rootId, - account: identityAlice.address, - permissions: [Permission.ATTEST, Permission.DELEGATE], - revoked: false, + describe('count depth', () => { + beforeAll(() => { + nodes = hashList + .slice(0, 1000) + .map( + (nodeId, index) => + new DelegationNode({ + id: nodeId, + hierarchyId, + account: addresses[index], + permissions: [Permission.DELEGATE], + childrenIds: [], + parentId: hashList[index + 1], + revoked: false, + }) + ) + .reduce((result, node) => { + return { + ...result, + [node.id]: node, + } + }, {}) + + expect(Object.keys(nodes)).toHaveLength(1000) }) - nodes = { - [b1]: new DelegationNode({ - id: b1, - rootId, - account: identityAlice.address, - permissions: [Permission.ATTEST, Permission.DELEGATE], - parentId: a1, - revoked: false, - }), - [b2]: new DelegationNode({ - id: b2, - rootId, + it('counts steps from last child till select parent', async () => { + await Promise.all( + [0, 1, 5, 10, 75, 100, 300, 500, 999].map((i) => + expect( + nodes[hashList[0]].findAncestorOwnedBy(nodes[hashList[i]].account) + ).resolves.toMatchObject({ + steps: i, + node: nodes[hashList[i]], + }) + ) + ) + }) + + it('counts various distances within the hierarchy', async () => { + await Promise.all([ + expect( + nodes[hashList[1]].findAncestorOwnedBy(nodes[hashList[2]].account) + ).resolves.toMatchObject({ + steps: 1, + node: nodes[hashList[2]], + }), + expect( + nodes[hashList[250]].findAncestorOwnedBy(nodes[hashList[450]].account) + ).resolves.toMatchObject({ steps: 200, node: nodes[hashList[450]] }), + expect( + nodes[hashList[800]].findAncestorOwnedBy(nodes[hashList[850]].account) + ).resolves.toMatchObject({ + steps: 50, + node: nodes[hashList[850]], + }), + expect( + nodes[hashList[5]].findAncestorOwnedBy(nodes[hashList[955]].account) + ).resolves.toMatchObject({ + steps: 950, + node: nodes[hashList[955]], + }), + ]) + }) + + it('returns null if trying to count backwards', async () => { + await Promise.all([ + expect( + nodes[hashList[10]].findAncestorOwnedBy(nodes[hashList[5]].account) + ).resolves.toMatchObject({ + steps: 989, + node: null, + }), + expect( + nodes[hashList[99]].findAncestorOwnedBy(nodes[hashList[95]].account) + ).resolves.toMatchObject({ steps: 900, node: null }), + expect( + nodes[hashList[900]].findAncestorOwnedBy(nodes[hashList[500]].account) + ).resolves.toMatchObject({ steps: 99, node: null }), + ]) + }) + + it('returns null if looking for non-existent account', async () => { + const noOnesAddress = encodeAddress(Crypto.hash('-1', 256), 38) + await Promise.all([ + expect( + nodes[hashList[10]].findAncestorOwnedBy(noOnesAddress) + ).resolves.toMatchObject({ + steps: 989, + node: null, + }), + expect( + nodes[hashList[99]].findAncestorOwnedBy(noOnesAddress) + ).resolves.toMatchObject({ + steps: 900, + node: null, + }), + expect( + nodes[hashList[900]].findAncestorOwnedBy(noOnesAddress) + ).resolves.toMatchObject({ + steps: 99, + node: null, + }), + ]) + }) + + it('error check should throw errors on faulty delegation nodes', async () => { + const malformedPremissionsDelegationNode = { + id, + hierarchyId, account: identityAlice.address, - permissions: [Permission.ATTEST, Permission.DELEGATE], - parentId: a1, + childrenIds: [], + permissions: [], + parentId: undefined, revoked: false, - }), - [c1]: new DelegationNode({ - id: c1, - rootId, + } as IDelegationNode + + const missingRootIdDelegationNode = { + id, + hierarchyId, account: identityAlice.address, - permissions: [Permission.ATTEST, Permission.DELEGATE], - parentId: b1, + permissions: [Permission.DELEGATE], + parentId: undefined, revoked: false, - }), - [c2]: new DelegationNode({ - id: c2, - rootId, + } as IDelegationNode + + // @ts-expect-error + delete missingRootIdDelegationNode.hierarchyId + + const malformedRootIdDelegationNode = { + id, + hierarchyId: hierarchyId.slice(13) + hierarchyId.slice(15), account: identityAlice.address, - permissions: [Permission.ATTEST, Permission.DELEGATE], - parentId: b1, + permissions: [Permission.DELEGATE], + parentId: undefined, revoked: false, - }), - [d1]: new DelegationNode({ - id: d1, - rootId, + } as IDelegationNode + + const malformedParentIdDelegationNode = { + id, + hierarchyId, account: identityAlice.address, - permissions: [Permission.ATTEST, Permission.DELEGATE], - parentId: c1, + permissions: [Permission.DELEGATE], + parentId: 'malformed', revoked: false, - }), - } - childMap = { - [a1]: [nodes[b1], nodes[b2]], - [b1]: [nodes[c1], nodes[c2]], - [c1]: [nodes[d1]], - } - }) - - it('mocks work', async () => { - expect(topNode.id).toEqual(a1) - await expect(topNode.getChildren()).resolves.toBe(childMap[a1]) - await expect(nodes[d1].getChildren()).resolves.toStrictEqual([]) - }) - - it('counts all subnodes', async () => { - await expect(topNode.subtreeNodeCount()).resolves.toStrictEqual(5) - }) - - it('counts smaller subtrees', async () => { - await expect(nodes[b2].subtreeNodeCount()).resolves.toStrictEqual(0) - await expect(nodes[b1].subtreeNodeCount()).resolves.toStrictEqual(3) - await expect(nodes[c1].subtreeNodeCount()).resolves.toStrictEqual(1) - await expect(nodes[c2].subtreeNodeCount()).resolves.toStrictEqual(0) - await expect(nodes[d1].subtreeNodeCount()).resolves.toStrictEqual(0) - }) - - it('counts all subnodes in deeply nested childMap (100)', async () => { - childMap = hashList.slice(0, 101).reduce((previous, current, index) => { - return { - ...previous, - [current]: [ - new DelegationNode({ - id: hashList[index + 1], - rootId, - account: identityAlice.address, - permissions: [Permission.DELEGATE], - parentId: current, - revoked: false, - }), - ], - } - }, {}) - - await expect( - childMap[hashList[0]][0].subtreeNodeCount() - ).resolves.toStrictEqual(100) - }) + } as IDelegationNode - it('counts all subnodes in deeply nested childMap (1000)', async () => { - childMap = hashList.slice(0, 1001).reduce((previous, current, index) => { - return { - ...previous, - [current]: [ - new DelegationNode({ - id: hashList[index + 1], - rootId, - account: identityAlice.address, - permissions: [Permission.DELEGATE], - parentId: current, - revoked: false, - }), - ], - } - }, {}) - await expect( - childMap[hashList[0]][0].subtreeNodeCount() - ).resolves.toStrictEqual(1000) - }) - - it('counts all subnodes in deeply nested childMap (10000)', async () => { - childMap = hashList.slice(0, 10001).reduce((previous, current, index) => { - return { - ...previous, - [current]: [ - new DelegationNode({ - id: hashList[index + 1], - rootId, - account: identityAlice.address, - permissions: [Permission.DELEGATE], - parentId: current, - revoked: false, - }), - ], - } - }, {}) - await expect( - childMap[hashList[0]][0].subtreeNodeCount() - ).resolves.toStrictEqual(10000) - }) -}) + expect(() => errorCheck(malformedPremissionsDelegationNode)).toThrowError( + SDKErrors.ERROR_UNAUTHORIZED( + 'Must have at least one permission and no more then two' + ) + ) -describe('count depth', () => { - beforeAll(() => { - nodes = hashList - .slice(0, 1000) - .map( - (nodeId, index) => - new DelegationNode({ - id: nodeId, - rootId, - account: addresses[index], - permissions: [Permission.DELEGATE], - parentId: hashList[index + 1], - revoked: false, - }) + expect(() => errorCheck(missingRootIdDelegationNode)).toThrowError( + SDKErrors.ERROR_DELEGATION_ID_MISSING() ) - .reduce((result, node) => { - return { - ...result, - [node.id]: node, - } - }, {}) - - expect(Object.keys(nodes)).toHaveLength(1000) - }) - it('counts steps from last child till select parent', async () => { - await Promise.all( - [0, 1, 5, 10, 75, 100, 300, 500, 999].map((i) => - expect( - nodes[hashList[0]].findAncestorOwnedBy(nodes[hashList[i]].account) - ).resolves.toMatchObject({ - steps: i, - node: nodes[hashList[i]], - }) + expect(() => errorCheck(malformedRootIdDelegationNode)).toThrowError( + SDKErrors.ERROR_DELEGATION_ID_TYPE() ) - ) - }) - it('counts various distances within the hierarchy', async () => { - await Promise.all([ - expect( - nodes[hashList[1]].findAncestorOwnedBy(nodes[hashList[2]].account) - ).resolves.toMatchObject({ - steps: 1, - node: nodes[hashList[2]], - }), - expect( - nodes[hashList[250]].findAncestorOwnedBy(nodes[hashList[450]].account) - ).resolves.toMatchObject({ steps: 200, node: nodes[hashList[450]] }), - expect( - nodes[hashList[800]].findAncestorOwnedBy(nodes[hashList[850]].account) - ).resolves.toMatchObject({ - steps: 50, - node: nodes[hashList[850]], - }), - expect( - nodes[hashList[5]].findAncestorOwnedBy(nodes[hashList[955]].account) - ).resolves.toMatchObject({ - steps: 950, - node: nodes[hashList[955]], - }), - ]) + expect(() => errorCheck(malformedParentIdDelegationNode)).toThrowError( + SDKErrors.ERROR_DELEGATION_ID_TYPE() + ) + }) }) +}) - it('returns null if trying to count backwards', async () => { - await Promise.all([ - expect( - nodes[hashList[10]].findAncestorOwnedBy(nodes[hashList[5]].account) - ).resolves.toMatchObject({ - steps: 989, - node: null, - }), - expect( - nodes[hashList[99]].findAncestorOwnedBy(nodes[hashList[95]].account) - ).resolves.toMatchObject({ steps: 900, node: null }), - expect( - nodes[hashList[900]].findAncestorOwnedBy(nodes[hashList[500]].account) - ).resolves.toMatchObject({ steps: 99, node: null }), - ]) - }) +describe('DelegationHierarchy', () => { + let identityAlice: Identity + let ctypeHash: string + let ROOT_IDENTIFIER: string + let ROOT_SUCCESS: string - it('returns null if looking for non-existent account', async () => { - const noOnesAddress = encodeAddress(Crypto.hash('-1', 256), 38) - await Promise.all([ - expect( - nodes[hashList[10]].findAncestorOwnedBy(noOnesAddress) - ).resolves.toMatchObject({ - steps: 989, - node: null, - }), - expect( - nodes[hashList[99]].findAncestorOwnedBy(noOnesAddress) - ).resolves.toMatchObject({ - steps: 900, - node: null, - }), - expect( - nodes[hashList[900]].findAncestorOwnedBy(noOnesAddress) - ).resolves.toMatchObject({ - steps: 99, - node: null, - }), - ]) - }) + beforeAll(async () => { + identityAlice = Identity.buildFromURI('//Alice') + ctypeHash = `0x6b696c743a63747970653a307830303031000000000000000000000000000000` + ROOT_IDENTIFIER = Crypto.hashStr('1') + ROOT_SUCCESS = Crypto.hashStr('success') - it('error check should throw errors on faulty delegation nodes', async () => { - const malformedPremissionsDelegationNode = { - id, - rootId, + const revokedRootDelegationNode = new DelegationNode({ account: identityAlice.address, - permissions: [], - parentId: undefined, - revoked: false, - } as IDelegationNode - - const missingRootIdDelegationNode = { - id, - rootId, + childrenIds: [], + hierarchyId: ROOT_IDENTIFIER, + id: ROOT_IDENTIFIER, + permissions: [Permission.DELEGATE], + revoked: true, + }) + const notRevokedRootDelegationNode = new DelegationNode({ account: identityAlice.address, + childrenIds: [], + hierarchyId: ROOT_SUCCESS, + id: ROOT_SUCCESS, permissions: [Permission.DELEGATE], - parentId: undefined, revoked: false, - } as IDelegationNode + }) - // @ts-expect-error - delete missingRootIdDelegationNode.rootId + nodes = { + [ROOT_IDENTIFIER]: revokedRootDelegationNode, + [ROOT_SUCCESS]: notRevokedRootDelegationNode, + } - const malformedRootIdDelegationNode = { - id, - rootId: rootId.slice(13) + rootId.slice(15), - account: identityAlice.address, - permissions: [Permission.DELEGATE], - parentId: undefined, - revoked: false, - } as IDelegationNode + hierarchiesDetails = { + [ROOT_IDENTIFIER]: { id: ROOT_IDENTIFIER, cTypeHash: ctypeHash }, + [ROOT_SUCCESS]: { id: ROOT_SUCCESS, cTypeHash: ctypeHash }, + } + }) - const malformedParentIdDelegationNode = { - id, - rootId, + it('stores root delegation', async () => { + const rootDelegation = new DelegationNode({ account: identityAlice.address, + childrenIds: [], + hierarchyId: ROOT_IDENTIFIER, + id: ROOT_IDENTIFIER, permissions: [Permission.DELEGATE], - parentId: 'malformed', revoked: false, - } as IDelegationNode - - expect(() => errorCheck(malformedPremissionsDelegationNode)).toThrowError( - SDKErrors.ERROR_UNAUTHORIZED( - 'Must have at least one permission and no more then two' - ) - ) + }) + await rootDelegation.store() - expect(() => errorCheck(missingRootIdDelegationNode)).toThrowError( - SDKErrors.ERROR_DELEGATION_ID_MISSING() - ) + const rootNode = await DelegationNode.query(ROOT_IDENTIFIER) + if (rootNode) { + expect(rootNode.id).toBe(ROOT_IDENTIFIER) + } + }) - expect(() => errorCheck(malformedRootIdDelegationNode)).toThrowError( - SDKErrors.ERROR_DELEGATION_ID_TYPE() - ) + it('query root delegation', async () => { + const queriedDelegation = await DelegationNode.query(ROOT_IDENTIFIER) + expect(queriedDelegation).not.toBe(undefined) + if (queriedDelegation) { + expect(queriedDelegation.account).toBe(identityAlice.address) + expect(queriedDelegation.getCTypeHash()).resolves.toBe(ctypeHash) + expect(queriedDelegation.id).toBe(ROOT_IDENTIFIER) + } + }) - expect(() => errorCheck(malformedParentIdDelegationNode)).toThrowError( - SDKErrors.ERROR_DELEGATION_ID_TYPE() - ) + it('root delegation verify', async () => { + const aDelegationRootNode = new DelegationNode({ + account: identityAlice.address, + childrenIds: [], + hierarchyId: ROOT_IDENTIFIER, + id: ROOT_IDENTIFIER, + permissions: [Permission.DELEGATE], + revoked: false, + }) + await aDelegationRootNode.revoke(identityAlice.address) + const fetchedNodeRevocationStatus = DelegationNode.query( + ROOT_IDENTIFIER + ).then((node) => node?.revoked) + expect(fetchedNodeRevocationStatus).resolves.not.toBeNull() + expect(fetchedNodeRevocationStatus).resolves.toEqual(true) }) }) diff --git a/packages/core/src/delegation/DelegationNode.ts b/packages/core/src/delegation/DelegationNode.ts index 71ec75fd4..3282efc23 100644 --- a/packages/core/src/delegation/DelegationNode.ts +++ b/packages/core/src/delegation/DelegationNode.ts @@ -10,54 +10,215 @@ * * Starting from the root node, entities can delegate the right to issue attestations to Claimers for a certain CTYPE and also delegate the right to attest and to delegate further nodes. * + * A delegation object is stored on-chain, and can be revoked. + * + * A delegation can and may restrict permissions. + * + * Permissions: + * * Delegate. + * * Attest. + * * @packageDocumentation * @module DelegationNode * @preferred */ -import type { IDelegationNode, SubmittableExtrinsic } from '@kiltprotocol/types' -import { Crypto, SDKErrors } from '@kiltprotocol/utils' +import type { + IDelegationHierarchyDetails, + IDelegationNode, + IPublicIdentity, + SubmittableExtrinsic, +} from '@kiltprotocol/types' +import { Crypto, SDKErrors, UUID } from '@kiltprotocol/utils' import { ConfigService } from '@kiltprotocol/config' -import DelegationBaseNode from './Delegation' -import { getChildren, query, revoke, store } from './DelegationNode.chain' +import type { DelegationHierarchyDetailsRecord } from './DelegationDecoder' +import { query as queryAttestation } from '../attestation/Attestation.chain' +import { + getChildren, + getAttestationHashes, + query, + revoke, + storeAsDelegation, + storeAsRoot, +} from './DelegationNode.chain' +import { query as queryDetails } from './DelegationHierarchyDetails.chain' import * as DelegationNodeUtils from './DelegationNode.utils' -import DelegationRootNode from './DelegationRootNode' -import { query as queryRoot } from './DelegationRootNode.chain' +import Attestation from '../attestation/Attestation' +import Identity from '../identity/Identity' const log = ConfigService.LoggingFactory.getLogger('DelegationNode') -export default class DelegationNode extends DelegationBaseNode - implements IDelegationNode { +type NewDelegationNodeInput = Required< + Pick +> + +type NewDelegationRootInput = Pick & + DelegationHierarchyDetailsRecord + +export default class DelegationNode implements IDelegationNode { + public readonly id: IDelegationNode['id'] + public readonly hierarchyId: IDelegationNode['hierarchyId'] + public readonly parentId?: IDelegationNode['parentId'] + public readonly childrenIds: Array = [] + public readonly account: IPublicIdentity['address'] + public readonly permissions: IDelegationNode['permissions'] + private hierarchyDetails?: IDelegationHierarchyDetails + public readonly revoked: boolean + + // eslint-disable-next-line jsdoc/require-param /** - * [STATIC] Queries the delegation node with [delegationId]. + * Creates a new [DelegationNode] from an [IDelegationNode]. * - * @param delegationId The unique identifier of the desired delegation. - * @returns Promise containing the [[DelegationNode]] or [null]. */ - public static async query( - delegationId: string - ): Promise { - log.info(`:: query('${delegationId}')`) - const result = await query(delegationId) - log.info(`result: ${JSON.stringify(result)}`) - return result + public constructor({ + id, + hierarchyId, + parentId, + childrenIds, + account, + permissions, + revoked, + }: IDelegationNode) { + this.id = id + this.hierarchyId = hierarchyId + this.parentId = parentId + this.childrenIds = childrenIds + this.account = account + this.permissions = permissions + this.revoked = revoked + DelegationNodeUtils.errorCheck(this) } - public rootId: IDelegationNode['rootId'] - public parentId?: IDelegationNode['parentId'] - public permissions: IDelegationNode['permissions'] + /** + * Builds a new [DelegationNode] representing a regular delegation node ready to be submitted to the chain for creation. + * + * @param hierarchyId.hierarchyId + * @param hierarchyId - The delegation hierarchy under which to store the node. + * @param parentId - The parent node under which to store the node. + * @param account - The owner (i.e., delegate) of this delegation. + * @param permissions - The set of permissions associated with this delegation node. + * @param hierarchyId.parentId + * @param hierarchyId.account + * @param hierarchyId.permissions + * @returns A new [DelegationNode] with a randomly generated id. + */ + public static newNode({ + hierarchyId, + parentId, // Cannot be undefined here + account, + permissions, + }: NewDelegationNodeInput): DelegationNode { + return new DelegationNode({ + id: UUID.generate(), + hierarchyId, + parentId, + account, + permissions, + childrenIds: [], + revoked: false, + }) + } /** - * Creates a new [DelegationNode]. + * Builds a new [DelegationNode] representing a root delegation node ready to be submitted to the chain for creation. + * + * @param account - The address of this delegation (and of the whole hierarchy under it). + * @param permissions - The set of permissions associated with this delegation node. + * @param hierarchyDetails - The details associated with the delegation hierarchy (e.g. The CType hash of allowed attestations). * - * @param delegationNodeInput - The base object from which to create the delegation node. + * @returns A new [DelegationNode] with a randomly generated id. */ - public constructor(delegationNodeInput: IDelegationNode) { - super(delegationNodeInput) - this.permissions = delegationNodeInput.permissions - this.rootId = delegationNodeInput.rootId - this.parentId = delegationNodeInput.parentId - DelegationNodeUtils.errorCheck(this) + public static newRoot({ + account, + permissions, + cTypeHash, + }: NewDelegationRootInput): DelegationNode { + const nodeId = UUID.generate() + + const newNode = new DelegationNode({ + id: nodeId, + hierarchyId: nodeId, + account, + permissions, + childrenIds: [], + revoked: false, + }) + newNode.hierarchyDetails = { + id: nodeId, + cTypeHash, + } + + return newNode + } + + /** + * Lazily fetches the details of the hierarchy the node is part of and return its CType. + * + * @returns The CType hash associated with the delegation hierarchy. + */ + public async getCTypeHash(): Promise { + return this.getHierarchyDetails().then((details) => details.cTypeHash) + } + + /** + * [ASYNC] Fetches the details of the hierarchy this delegation node belongs to. + * + * @throws [[ERROR_HIERARCHY_QUERY]] when the hierarchy details could not be queried. + * @returns Promise containing the [[DelegationHierarchyDetails]] of this delegation node. + */ + public async getHierarchyDetails(): Promise { + if (!this.hierarchyDetails) { + const hierarchyDetails = await queryDetails(this.hierarchyId) + if (!hierarchyDetails) { + throw SDKErrors.ERROR_HIERARCHY_QUERY(this.hierarchyId) + } + this.hierarchyDetails = hierarchyDetails + return hierarchyDetails + } + return this.hierarchyDetails + } + + /** + * [ASYNC] Fetches the parent node of this delegation node. + * + * @returns Promise containing the parent as [[DelegationNode]] or [null]. + */ + public async getParent(): Promise { + return this.parentId ? query(this.parentId) : null + } + + /** + * [ASYNC] Fetches the children nodes of this delegation node. + * + * @returns Promise containing the children as an array of [[DelegationNode]], which is empty if there are no children. + */ + public async getChildren(): Promise { + return getChildren(this) + } + + /** + * [ASYNC] Fetches and resolves all attestations attested with this delegation node. + * + * @returns Promise containing all resolved attestations attested with this node. + */ + public async getAttestations(): Promise { + const attestationHashes = await this.getAttestationHashes() + const attestations = await Promise.all( + attestationHashes.map((claimHash: string) => { + return queryAttestation(claimHash) + }) + ) + + return attestations.filter((value): value is Attestation => !!value) + } + + /** + * [ASYNC] Fetches all hashes of attestations attested with this delegation node. + * + * @returns Promise containing all attestation hashes attested with this node. + */ + public async getAttestationHashes(): Promise { + return getAttestationHashes(this.id) } /** @@ -82,8 +243,8 @@ export default class DelegationNode extends DelegationBaseNode * @returns The hash representation of this delegation **as a hex string**. */ public generateHash(): string { - const propsToHash: Array = [this.id, this.rootId] - if (this.parentId && this.parentId !== this.rootId) { + const propsToHash: Array = [this.id, this.hierarchyId] + if (this.parentId) { propsToHash.push(this.parentId) } const uint8Props: Uint8Array[] = propsToHash.map((value) => { @@ -98,42 +259,38 @@ export default class DelegationNode extends DelegationBaseNode } /** - * [ASYNC] Fetches the root of this delegation node. + * [ASYNC] Syncronise the delegation node state with the latest state as stored on the blockchain. * - * @throws [[ERROR_ROOT_NODE_QUERY]] when the rootId could not be queried. - * @returns Promise containing the [[DelegationRootNode]] of this delegation node. + * @returns An updated instance of the same [DelegationNode] containing the up-to-date state fetched from the chain. */ - public async getRoot(): Promise { - const rootNode = await queryRoot(this.rootId) - if (!rootNode) { - throw SDKErrors.ERROR_ROOT_NODE_QUERY(this.rootId) + public async getLatestState(): Promise { + const newNodeState = await query(this.id) + if (!newNodeState) { + throw SDKErrors.ERROR_DELEGATION_ID_MISSING } - return rootNode + return newNodeState } /** - * [ASYNC] Fetches the parent node of this delegation node. + * [ASYNC] Stores the delegation node on chain. * - * @returns Promise containing the parent as [[DelegationBaseNode]] or [null]. + * @param signature Signature of the delegate to ensure it is done under the delegate's permission. + * @returns Promise containing an unsigned SubmittableExtrinsic. */ - - public async getParent(): Promise { - if (!this.parentId || this.parentId === this.rootId) { - // parent must be root - return this.getRoot() + public async store(signature?: string): Promise { + if (this.isRoot()) { + return storeAsRoot(this) + // eslint-disable-next-line no-else-return + } else { + if (!signature) { + throw SDKErrors.ERROR_DELEGATION_SIGNATURE_MISSING + } + return storeAsDelegation(this, signature) } - return query(this.parentId) } - /** - * [ASYNC] Stores the delegation node on chain. - * - * @param signature Signature of the delegate to ensure it is done under the delegate's permission. - * @returns Promise containing a unsigned SubmittableExtrinsic. - */ - public async store(signature: string): Promise { - log.info(`:: store(${this.id})`) - return store(this, signature) + isRoot(): boolean { + return this.id === this.hierarchyId && !this.parentId } /** @@ -146,6 +303,53 @@ export default class DelegationNode extends DelegationBaseNode return node !== null && !node.revoked } + /** + * [ASYNC] Checks on chain whether a identity with the given address is delegating to the current node. + * + * @param address The address of the identity. + * + * @returns An object containing a `node` owned by the identity if it is delegating, plus the number of `steps` traversed. `steps` is 0 if the address is owner of the current node. + */ + public async findAncestorOwnedBy( + address: Identity['address'] + ): Promise<{ steps: number; node: DelegationNode | null }> { + if (this.account === address) { + return { + steps: 0, + node: this, + } + } + const parent = await this.getParent() + if (parent) { + const result = await parent.findAncestorOwnedBy(address) + result.steps += 1 + return result + } + return { + steps: 0, + node: null, + } + } + + /** + * [ASYNC] Recursively counts all nodes that descend from the current node (excluding the current node). It is important to first refresh the state of the node from the chain. + * + * @returns Promise resolving to the node count. + */ + public async subtreeNodeCount(): Promise { + const children = await this.getChildren() + if (children.length === 0) { + return 0 + } + const childrensChildCounts = await Promise.all( + children.map((child) => child.subtreeNodeCount()) + ) + return ( + children.length + + childrensChildCounts.reduce((previous, current) => previous + current) + ) + } + /** * [ASYNC] Revokes the delegation node on chain. * @@ -159,16 +363,25 @@ export default class DelegationNode extends DelegationBaseNode `Identity with address ${address} is not among the delegators and may not revoke this node` ) } - const childCount = await this.subtreeNodeCount() - // must revoke all children and self - const revocationCount = childCount + 1 + const childrenCount = await this.subtreeNodeCount() log.debug( - `:: revoke(${this.id}) with maxRevocations=${revocationCount} and maxDepth = ${steps} through delegation node ${node?.id} and identity ${address}` + `:: revoke(${this.id}) with maxRevocations=${childrenCount} and maxDepth = ${steps} through delegation node ${node?.id} and identity ${address}` ) - return revoke(this.id, steps, revocationCount) + return revoke(this.id, steps, childrenCount) } - public async getChildren(): Promise { - return getChildren(this.id) + /** + * [STATIC] [ASYNC] Queries the delegation node with its [delegationId]. + * + * @param delegationId The unique identifier of the desired delegation. + * @returns Promise containing the [[DelegationNode]] or [null]. + */ + public static async query( + delegationId: string + ): Promise { + log.info(`:: query('${delegationId}')`) + const result = await query(delegationId) + log.info(`result: ${JSON.stringify(result)}`) + return result } } diff --git a/packages/core/src/delegation/DelegationNode.utils.ts b/packages/core/src/delegation/DelegationNode.utils.ts index 18335ce24..f9fb908b7 100644 --- a/packages/core/src/delegation/DelegationNode.utils.ts +++ b/packages/core/src/delegation/DelegationNode.utils.ts @@ -72,7 +72,7 @@ export async function countNodeDepth( } export function errorCheck(delegationNodeInput: IDelegationNode): void { - const { permissions, rootId, parentId } = delegationNodeInput + const { permissions, hierarchyId: rootId, parentId } = delegationNodeInput if (permissions.length === 0 || permissions.length > 3) { throw SDKErrors.ERROR_UNAUTHORIZED( diff --git a/packages/core/src/delegation/DelegationRootNode.chain.ts b/packages/core/src/delegation/DelegationRootNode.chain.ts deleted file mode 100644 index 89ad16fdb..000000000 --- a/packages/core/src/delegation/DelegationRootNode.chain.ts +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright 2018-2021 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. - */ - -/** - * @packageDocumentation - * @module DelegationRootNode - */ - -import type { Option } from '@polkadot/types' -import type { - IDelegationRootNode, - SubmittableExtrinsic, -} from '@kiltprotocol/types' -import { BlockchainApiConnection } from '@kiltprotocol/chain-helpers' -import { - decodeRootDelegation, - IChainDelegationRoot, - RootDelegationRecord, -} from './DelegationDecoder' -import DelegationRootNode from './DelegationRootNode' - -/** - * @param delegation - * @internal - */ -export async function store( - delegation: IDelegationRootNode -): Promise { - const blockchain = await BlockchainApiConnection.getConnectionOrConnect() - const tx: SubmittableExtrinsic = blockchain.api.tx.delegation.createRoot( - delegation.id, - delegation.cTypeHash - ) - return tx -} - -/** - * @param delegationId - * @internal - */ -export async function query( - delegationId: IDelegationRootNode['id'] -): Promise { - const blockchain = await BlockchainApiConnection.getConnectionOrConnect() - const decoded: RootDelegationRecord | null = decodeRootDelegation( - await blockchain.api.query.delegation.roots>( - delegationId - ) - ) - if (decoded) { - const root = new DelegationRootNode({ - id: delegationId, - cTypeHash: decoded.cTypeHash, - account: decoded.account, - revoked: decoded.revoked, - }) - return root - } - return null -} - -/** - * @internal - * - * Revokes a full delegation tree, also revoking all constituent nodes. - * - * @param delegation The [[DelegationRootNode]] node in the delegation tree at which to revoke. - * @param maxRevocations The maximum number of revocations that may be performed. Should be set to the number of nodes (including the root node) in the tree. Higher numbers result in a larger amount locked during the transaction, as each revocation adds to the fee that is charged. - * @returns Unsigned [[SubmittableExtrinsic]] ready to be signed and dispatched. - */ -export async function revoke( - delegation: IDelegationRootNode, - maxRevocations: number -): Promise { - const blockchain = await BlockchainApiConnection.getConnectionOrConnect() - const tx: SubmittableExtrinsic = blockchain.api.tx.delegation.revokeRoot( - delegation.id, - maxRevocations - ) - return tx -} diff --git a/packages/core/src/delegation/DelegationRootNode.spec.ts b/packages/core/src/delegation/DelegationRootNode.spec.ts deleted file mode 100644 index 0806e0626..000000000 --- a/packages/core/src/delegation/DelegationRootNode.spec.ts +++ /dev/null @@ -1,148 +0,0 @@ -/** - * Copyright 2018-2021 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. - */ - -/** - * @group unit/delegation - */ - -import { Crypto } from '@kiltprotocol/utils' -import { - BlockchainUtils, - BlockchainApiConnection, -} from '@kiltprotocol/chain-helpers' -import { mockChainQueryReturn } from '@kiltprotocol/chain-helpers/lib/blockchainApiConnection/__mocks__/BlockchainQuery' -import { Identity } from '..' - -import DelegationRootNode from './DelegationRootNode' -import Kilt from '../kilt/Kilt' - -jest.mock( - '@kiltprotocol/chain-helpers/lib/blockchainApiConnection/BlockchainApiConnection' -) - -describe('Delegation', () => { - let identityAlice: Identity - let ctypeHash: string - let ROOT_IDENTIFIER: string - let ROOT_SUCCESS: string - Kilt.config({ address: 'ws://testString' }) - - beforeAll(async () => { - identityAlice = Identity.buildFromURI('//Alice') - ctypeHash = `0x6b696c743a63747970653a307830303031000000000000000000000000000000` - ROOT_IDENTIFIER = Crypto.hashStr('1') - ROOT_SUCCESS = Crypto.hashStr('success') - - require('@kiltprotocol/chain-helpers/lib/blockchainApiConnection/BlockchainApiConnection').__mocked_api.query.delegation.root.mockReturnValue( - mockChainQueryReturn('delegation', 'root', [ - ctypeHash, - identityAlice.address, - false, - ]) - ) - require('@kiltprotocol/chain-helpers/lib/blockchainApiConnection/BlockchainApiConnection').__mocked_api.query.delegation.delegations.mockReturnValue( - mockChainQueryReturn('delegation', 'delegations', [ - ctypeHash, - null, - identityAlice.address, - 1, - false, - ]) - ) - }) - - it('stores root delegation', async () => { - const rootDelegation = new DelegationRootNode({ - id: ROOT_IDENTIFIER, - cTypeHash: ctypeHash, - account: identityAlice.address, - revoked: false, - }) - await rootDelegation - .store() - .then((tx) => - BlockchainUtils.signAndSubmitTx(tx, identityAlice, { reSign: true }) - ) - - const rootNode = await DelegationRootNode.query(ROOT_IDENTIFIER) - if (rootNode) { - expect(rootNode.id).toBe(ROOT_IDENTIFIER) - } - }) - - it('query root delegation', async () => { - const queriedDelegation = await DelegationRootNode.query(ROOT_IDENTIFIER) - expect(queriedDelegation).not.toBe(undefined) - if (queriedDelegation) { - expect(queriedDelegation.account).toBe(identityAlice.address) - expect(queriedDelegation.cTypeHash).toBe(ctypeHash) - expect(queriedDelegation.id).toBe(ROOT_IDENTIFIER) - } - }) - - it('root delegation verify', async () => { - require('@kiltprotocol/chain-helpers/lib/blockchainApiConnection/BlockchainApiConnection').__mocked_api.query.delegation.root = jest.fn( - async (rootId) => { - if (rootId === ROOT_SUCCESS) { - const tuple = mockChainQueryReturn('delegation', 'root', [ - ctypeHash, - identityAlice.address, - false, - ]) - - return Promise.resolve(tuple) - } - const tuple = mockChainQueryReturn('delegation', 'root', [ - ctypeHash, - identityAlice.address, - true, - ]) - - return Promise.resolve(tuple) - } - ) - - expect( - await new DelegationRootNode({ - id: ROOT_IDENTIFIER, - cTypeHash: ctypeHash, - account: identityAlice.address, - revoked: false, - }).verify() - ).toBe(false) - - expect( - await new DelegationRootNode({ - id: ROOT_SUCCESS, - cTypeHash: ctypeHash, - account: identityAlice.address, - revoked: true, - }).verify() - ).toBe(true) - }) - - it('root delegation verify', async () => { - const blockchain = await BlockchainApiConnection.getConnectionOrConnect() - - const aDelegationRootNode = new DelegationRootNode({ - id: ROOT_IDENTIFIER, - cTypeHash: ctypeHash, - account: identityAlice.address, - revoked: false, - }) - const revokeStatus = await aDelegationRootNode - .revoke() - .then((tx) => - BlockchainUtils.signAndSubmitTx(tx, identityAlice, { reSign: true }) - ) - expect(blockchain.api.tx.delegation.revokeRoot).toBeCalledWith( - ROOT_IDENTIFIER, - 1 - ) - expect(revokeStatus).toBeDefined() - }) -}) diff --git a/packages/core/src/delegation/DelegationRootNode.ts b/packages/core/src/delegation/DelegationRootNode.ts deleted file mode 100644 index 76fd9b7ab..000000000 --- a/packages/core/src/delegation/DelegationRootNode.ts +++ /dev/null @@ -1,102 +0,0 @@ -/** - * Copyright 2018-2021 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. - */ - -/** - * KILT enables top-down trust structures. - * On the lowest level, a delegation structure is always a **tree**. - * The root of this tree is DelegationRootNode. - * - * Apart from inheriting [[DelegationBaseNode]]'s structure, a DelegationRootNode has a [[cTypeHash]] property that refers to a specific [[CType]]. - * A DelegationRootNode is written on-chain, and can be queried by delegationId via the [[query]] method. - * - * @packageDocumentation - * @module DelegationRootNode - */ - -import type { - IDelegationRootNode, - SubmittableExtrinsic, -} from '@kiltprotocol/types' -import { ConfigService } from '@kiltprotocol/config' -import DelegationBaseNode from './Delegation' -import DelegationNode from './DelegationNode' -import { getChildren } from './DelegationNode.chain' -import { query, revoke, store } from './DelegationRootNode.chain' -import errorCheck from './DelegationRootNode.utils' - -const log = ConfigService.LoggingFactory.getLogger('DelegationRootNode') - -export default class DelegationRootNode extends DelegationBaseNode - implements IDelegationRootNode { - /** - * [STATIC] Queries the delegation root with ``delegationId``. - * - * @param delegationId Unique identifier of the delegation root. - * @returns Promise containing [[DelegationRootNode]] or [null]. - */ - public static async query( - delegationId: string - ): Promise { - log.info(`:: query('${delegationId}')`) - const result = await query(delegationId) - if (result) { - log.info(`result: ${JSON.stringify(result)}`) - } else { - log.info(`root node not found`) - } - - return result - } - - public cTypeHash: IDelegationRootNode['cTypeHash'] - - /** - * Creates a new [DelegationRootNode]. - * - * @param delegationRootNodeInput - The base object from which to create the delegation base node. - */ - public constructor(delegationRootNodeInput: IDelegationRootNode) { - super(delegationRootNodeInput) - this.cTypeHash = delegationRootNodeInput.cTypeHash - errorCheck(this) - } - - public getRoot(): Promise { - return Promise.resolve(this) - } - - /* eslint-disable class-methods-use-this */ - public getParent(): Promise { - return Promise.resolve(null) - } - /* eslint-enable class-methods-use-this */ - - /** - * Stores the delegation root node on chain. - * - * @returns Promise containing the unsigned SubmittableExtrinsic. - */ - public async store(): Promise { - log.debug(`:: store(${this.id})`) - return store(this) - } - - public async verify(): Promise { - const node = await query(this.id) - return node !== null && !node.revoked - } - - public async revoke(): Promise { - const childCount = await this.subtreeNodeCount() - log.debug(`:: revoke(${this.id})`) - return revoke(this, childCount + 1) - } - - public async getChildren(): Promise { - return getChildren(this.id) - } -} diff --git a/packages/core/src/delegation/DelegationRootNode.utils.ts b/packages/core/src/delegation/DelegationRootNode.utils.ts deleted file mode 100644 index 732f9c602..000000000 --- a/packages/core/src/delegation/DelegationRootNode.utils.ts +++ /dev/null @@ -1,19 +0,0 @@ -/** - * Copyright 2018-2021 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 { IDelegationRootNode } from '@kiltprotocol/types' -import { DataUtils, SDKErrors } from '@kiltprotocol/utils' - -export default function errorCheck( - delegationRootNodeInput: IDelegationRootNode -): void { - const { cTypeHash } = delegationRootNodeInput - - if (!cTypeHash) { - throw SDKErrors.ERROR_CTYPE_HASH_NOT_PROVIDED() - } else DataUtils.validateHash(cTypeHash, 'delegation root node ctype') -} diff --git a/packages/core/src/delegation/index.ts b/packages/core/src/delegation/index.ts index 26fbbe396..69f256508 100644 --- a/packages/core/src/delegation/index.ts +++ b/packages/core/src/delegation/index.ts @@ -5,14 +5,7 @@ * found in the LICENSE file in the root directory of this source tree. */ -import DelegationBaseNode from './Delegation' import DelegationNode from './DelegationNode' -import DelegationRootNode from './DelegationRootNode' import * as DelegationNodeUtils from './DelegationNode.utils' -export { - DelegationBaseNode, - DelegationNode, - DelegationRootNode, - DelegationNodeUtils, -} +export { DelegationNode, DelegationNodeUtils } diff --git a/packages/core/src/index.ts b/packages/core/src/index.ts index aea3aca7b..967d6ccd9 100644 --- a/packages/core/src/index.ts +++ b/packages/core/src/index.ts @@ -10,12 +10,7 @@ import AttestedClaim, { AttestedClaimUtils } from './attestedclaim' import { Balance, BalanceUtils } from './balance' import Claim, { ClaimUtils } from './claim' import { CType, CTypeMetadata, CTypeSchema, CTypeUtils } from './ctype' -import { - DelegationBaseNode, - DelegationNode, - DelegationRootNode, - DelegationNodeUtils, -} from './delegation' +import { DelegationNode, DelegationNodeUtils } from './delegation' import Did, { IDid, IDidDocument, @@ -50,10 +45,8 @@ export { AttestationUtils, AttestedClaim, AttestedClaimUtils, - DelegationBaseNode, DelegationNode, DelegationNodeUtils, - DelegationRootNode, Did, IDid, IDidDocument, diff --git a/packages/core/src/requestforattestation/RequestForAttestation.ts b/packages/core/src/requestforattestation/RequestForAttestation.ts index 5660d084b..b2d772df5 100644 --- a/packages/core/src/requestforattestation/RequestForAttestation.ts +++ b/packages/core/src/requestforattestation/RequestForAttestation.ts @@ -21,7 +21,7 @@ import type { IRequestForAttestation, CompressedRequestForAttestation, Hash, - IDelegationBaseNode, + IDelegationNode, IClaim, IAttestedClaim, } from '@kiltprotocol/types' @@ -46,7 +46,7 @@ function getHashRoot(leaves: Uint8Array[]): Uint8Array { export type Options = { legitimations?: AttestedClaim[] - delegationId?: IDelegationBaseNode['id'] + delegationId?: IDelegationNode['id'] } export default class RequestForAttestation implements IRequestForAttestation { @@ -138,7 +138,7 @@ export default class RequestForAttestation implements IRequestForAttestation { public claimHashes: string[] public claimNonceMap: Record public rootHash: Hash - public delegationId: IDelegationBaseNode['id'] | null + public delegationId: IDelegationNode['id'] | null /** * Builds a new [[RequestForAttestation]] instance. @@ -301,7 +301,7 @@ export default class RequestForAttestation implements IRequestForAttestation { private static getHashLeaves( claimHashes: Hash[], legitimations: IAttestedClaim[], - delegationId: IDelegationBaseNode['id'] | null + delegationId: IDelegationNode['id'] | null ): Uint8Array[] { const result: Uint8Array[] = [] claimHashes.forEach((item) => { diff --git a/packages/types/src/Attestation.ts b/packages/types/src/Attestation.ts index 6e44267de..1030d7549 100644 --- a/packages/types/src/Attestation.ts +++ b/packages/types/src/Attestation.ts @@ -10,14 +10,14 @@ * @module IAttestation */ import type { ICType } from './CType' -import type { IDelegationBaseNode } from './Delegation' +import type { IDelegationNode } from './Delegation' import type { IPublicIdentity } from './PublicIdentity' export interface IAttestation { claimHash: string cTypeHash: ICType['hash'] owner: IPublicIdentity['address'] - delegationId: IDelegationBaseNode['id'] | null + delegationId: IDelegationNode['id'] | null revoked: boolean } diff --git a/packages/types/src/Delegation.ts b/packages/types/src/Delegation.ts index a3489cfb3..f58f31b20 100644 --- a/packages/types/src/Delegation.ts +++ b/packages/types/src/Delegation.ts @@ -18,18 +18,17 @@ export enum Permission { DELEGATE = 1 << 1, // 0010 } -export interface IDelegationBaseNode { +export interface IDelegationNode { id: string + hierarchyId: IDelegationNode['id'] + parentId?: IDelegationNode['id'] + childrenIds: Array account: IPublicIdentity['address'] + permissions: Permission[] revoked: boolean } -export interface IDelegationRootNode extends IDelegationBaseNode { +export interface IDelegationHierarchyDetails { + id: IDelegationNode['id'] cTypeHash: ICType['hash'] } - -export interface IDelegationNode extends IDelegationBaseNode { - rootId: IDelegationBaseNode['id'] - parentId?: IDelegationBaseNode['id'] - permissions: Permission[] -} diff --git a/packages/types/src/Message.ts b/packages/types/src/Message.ts index e4c62a33d..da46a7147 100644 --- a/packages/types/src/Message.ts +++ b/packages/types/src/Message.ts @@ -20,7 +20,7 @@ import type { PartialClaim, } from './Claim' import type { ICType } from './CType' -import type { IDelegationBaseNode, IDelegationNode } from './Delegation' +import type { IDelegationNode } from './Delegation' import type { IPublicIdentity } from './PublicIdentity' import type { CompressedQuoteAgreed, IQuoteAgreement } from './Quote' import type { @@ -110,7 +110,7 @@ export interface IRejectTerms extends IMessageBodyBase { content: { claim: PartialClaim legitimations: IAttestedClaim[] - delegationId?: IDelegationBaseNode['id'] + delegationId?: IDelegationNode['id'] } type: MessageBodyType.REJECT_TERMS } @@ -238,8 +238,8 @@ export interface IRequestClaimsForCTypesContent { } export interface IDelegationData { - account: IDelegationBaseNode['account'] - id: IDelegationBaseNode['id'] + account: IDelegationNode['account'] + id: IDelegationNode['id'] parentId: IDelegationNode['id'] permissions: IDelegationNode['permissions'] isPCR: boolean @@ -261,7 +261,7 @@ export interface ISubmitDelegationApproval { } export interface IInformDelegationCreation { - delegationId: IDelegationBaseNode['id'] + delegationId: IDelegationNode['id'] isPCR: boolean } @@ -274,7 +274,7 @@ export type CompressedPartialClaim = [ export type CompressedRejectedTerms = [ CompressedPartialClaim, CompressedAttestedClaim[], - IDelegationBaseNode['id'] | undefined + IDelegationNode['id'] | undefined ] export type CompressedRequestClaimsForCTypesContent = [ @@ -290,8 +290,8 @@ export type CompressedRequestAttestationForClaimContent = [ ] export type CompressedDelegationData = [ - IDelegationBaseNode['account'], - IDelegationBaseNode['id'], + IDelegationNode['account'], + IDelegationNode['id'], IDelegationNode['id'], IDelegationNode['permissions'], boolean diff --git a/packages/types/src/RequestForAttestation.ts b/packages/types/src/RequestForAttestation.ts index 8afccc275..eea4e389a 100644 --- a/packages/types/src/RequestForAttestation.ts +++ b/packages/types/src/RequestForAttestation.ts @@ -12,7 +12,7 @@ import type { IAttestedClaim, CompressedAttestedClaim } from './AttestedClaim' import type { IClaim, CompressedClaim } from './Claim' -import type { IDelegationBaseNode } from './Delegation' +import type { IDelegationNode } from './Delegation' export type Hash = string @@ -26,7 +26,7 @@ export interface IRequestForAttestation { claimNonceMap: Record claimHashes: Hash[] claimerSignature: string - delegationId: IDelegationBaseNode['id'] | null + delegationId: IDelegationNode['id'] | null legitimations: IAttestedClaim[] rootHash: Hash } diff --git a/packages/types/src/Terms.ts b/packages/types/src/Terms.ts index af5badbaf..3c64e1b8b 100644 --- a/packages/types/src/Terms.ts +++ b/packages/types/src/Terms.ts @@ -12,7 +12,7 @@ import type { IAttestedClaim, CompressedAttestedClaim } from './AttestedClaim' import type { CompressedCType, ICType } from './CType' -import type { IDelegationBaseNode } from './Delegation' +import type { IDelegationNode } from './Delegation' import type { IQuoteAttesterSigned, CompressedQuoteAttesterSigned, @@ -23,7 +23,7 @@ import type { PartialClaim } from './Claim' export interface ITerms { claim: PartialClaim legitimations: IAttestedClaim[] - delegationId?: IDelegationBaseNode['id'] + delegationId?: IDelegationNode['id'] quote?: IQuoteAttesterSigned prerequisiteClaims?: ICType['hash'] cTypes?: ICType[] @@ -32,7 +32,7 @@ export interface ITerms { export type CompressedTerms = [ CompressedPartialClaim, CompressedAttestedClaim[], - IDelegationBaseNode['id'] | undefined, + IDelegationNode['id'] | undefined, CompressedQuoteAttesterSigned | undefined, ICType['hash'] | undefined, CompressedCType[] | undefined diff --git a/packages/utils/src/SDKErrors.ts b/packages/utils/src/SDKErrors.ts index 443bb46f0..082ee9f7a 100644 --- a/packages/utils/src/SDKErrors.ts +++ b/packages/utils/src/SDKErrors.ts @@ -43,6 +43,11 @@ export enum ErrorCode { ERROR_IDENTITY_NOT_PE_ENABLED = 10016, ERROR_WS_ADDRESS_NOT_SET = 10017, ERROR_DELEGATION_ID_MISSING = 10018, + ERROR_HIERARCHY_DETAILS_MISSING = 10019, + ERROR_DELEGATION_SIGNATURE_MISSING = 10020, + ERROR_DELEGATION_PARENT_MISSING = 10021, + ERROR_INVALID_ROOT_NODE = 10022, + ERROR_INVALID_DELEGATION_NODE = 10023, // Data type is wrong or malformed ERROR_ADDRESS_TYPE = 20001, @@ -59,7 +64,7 @@ export enum ErrorCode { ERROR_CLAIM_HASHTREE_MISMATCH = 20014, ERROR_PE_MISMATCH = 20015, ERROR_DID_IDENTIFIER_MISMATCH = 20016, - ERROR_ROOT_NODE_QUERY = 20017, + ERROR_HIERARCHY_QUERY = 20017, ERROR_INVALID_DID_PREFIX = 20018, ERROR_MESSAGE_BODY_MALFORMED = 20019, ERROR_NODE_QUERY = 20020, @@ -279,6 +284,41 @@ export const ERROR_DELEGATION_ID_MISSING: () => SDKError = () => { ) } +export const ERROR_HIERARCHY_DETAILS_MISSING: () => SDKError = () => { + return new SDKError( + ErrorCode.ERROR_HIERARCHY_DETAILS_MISSING, + 'Delegation hierarchy details missing' + ) +} + +export const ERROR_DELEGATION_SIGNATURE_MISSING: () => SDKError = () => { + return new SDKError( + ErrorCode.ERROR_DELEGATION_SIGNATURE_MISSING, + "Delegatee's signature missing" + ) +} + +export const ERROR_DELEGATION_PARENT_MISSING: () => SDKError = () => { + return new SDKError( + ErrorCode.ERROR_DELEGATION_PARENT_MISSING, + 'Delegation parentId missing' + ) +} + +export const ERROR_INVALID_ROOT_NODE: () => SDKError = () => { + return new SDKError( + ErrorCode.ERROR_INVALID_ROOT_NODE, + 'The given node is not a valid root node' + ) +} + +export const ERROR_INVALID_DELEGATION_NODE: () => SDKError = () => { + return new SDKError( + ErrorCode.ERROR_INVALID_DELEGATION_NODE, + 'The given node is not a valid delegation node' + ) +} + export const ERROR_CLAIM_CONTENTS_MALFORMED: () => SDKError = () => { return new SDKError( ErrorCode.ERROR_CLAIM_CONTENTS_MALFORMED, @@ -366,11 +406,11 @@ export const ERROR_PE_MISMATCH: () => SDKError = () => { 'Verifier requested public presentation, but privacy enhancement was forced.' ) } -export const ERROR_ROOT_NODE_QUERY: (rootId: string) => SDKError = ( +export const ERROR_HIERARCHY_QUERY: (rootId: string) => SDKError = ( rootId: string ) => { return new SDKError( - ErrorCode.ERROR_ROOT_NODE_QUERY, + ErrorCode.ERROR_HIERARCHY_QUERY, `Could not find root node with id ${rootId}` ) } diff --git a/yarn.lock b/yarn.lock index 61d62bd32..ad4ccfccb 100644 --- a/yarn.lock +++ b/yarn.lock @@ -815,7 +815,7 @@ __metadata: resolution: "@kiltprotocol/chain-helpers@workspace:packages/chain-helpers" dependencies: "@kiltprotocol/config": "workspace:*" - "@kiltprotocol/type-definitions": 0.1.6 + "@kiltprotocol/type-definitions": 0.1.10 "@kiltprotocol/types": "workspace:*" "@kiltprotocol/utils": "workspace:*" "@polkadot/api": ^4.13.1 @@ -913,10 +913,10 @@ __metadata: languageName: unknown linkType: soft -"@kiltprotocol/type-definitions@npm:0.1.6": - version: 0.1.6 - resolution: "@kiltprotocol/type-definitions@npm:0.1.6" - checksum: 685d3a63959d9482116293eb38993e84825615a0a994eb75315fbe28ba4a6fe3d0d4b5db3947942c6aeaa2d2c44a75b0f64fbb961b8df8707d4dd2b2c7939090 +"@kiltprotocol/type-definitions@npm:0.1.10": + version: 0.1.10 + resolution: "@kiltprotocol/type-definitions@npm:0.1.10" + checksum: 73a0a2720f2d7a7905043971699fa0a0daeb8e32ccbe2386ff385a2bdec773c1fd36cf33a422cfaf5069f78b0013e046d4edcd55099d6f86c715d2d585fafa7e languageName: node linkType: hard