--- a/tests/package.json +++ b/tests/package.json @@ -32,6 +32,7 @@ "fix": "eslint --ext .ts,.js src/ --fix", "setup": "ts-node ./src/util/globalSetup.ts", "setIdentities": "ts-node ./src/util/identitySetter.ts", + "checkRelayIdentities": "ts-node ./src/util/relayIdentitiesChecker.ts", "test": "yarn setup && mocha --timeout 9999999 -r ts-node/register './src/**/*.*test.ts'", "testParallelFull": "yarn testParallel && yarn testSequential", "testParallel": "yarn setup && mocha --parallel --timeout 9999999 -r ts-node/register './src/**/*.test.ts'", --- a/tests/src/util/identitySetter.ts +++ b/tests/src/util/identitySetter.ts @@ -1,6 +1,9 @@ // Copyright 2019-2022 Unique Network (Gibraltar) Ltd. // SPDX-License-Identifier: Apache-2.0 // +// Pulls identities and sub-identities from a chain and then uses sudo to force upload them into another. +// Only changed or previously non-existent data are inserted. +// // Usage: `yarn setIdentities [relay WS URL] [parachain WS URL] [sudo key]` // Example: `yarn setIdentities wss://polkadot-rpc.dwellir.com ws://localhost:9944 escape pattern miracle train sudden cart adapt embark wedding alien lamp mesh` @@ -12,15 +15,22 @@ const paraUrl = process.argv[3] ?? 'ws://localhost:9944'; const key = process.argv.length > 4 ? process.argv.slice(4).join(' ') : '//Alice'; -function extractAccountId(key: any): string { +export function extractAccountId(key: any): string { return (key as any).toHuman()[0]; } -function extractIdentity(key: any, value: any): [string, any] { - return [extractAccountId(key), (value as any).unwrap()]; +export function extractIdentityInfo(value: any): object { + const heart = (value as any).unwrap(); + const identity = heart.toJSON(); + identity.judgements = heart.judgements.toHuman(); + return identity; +} + +export function extractIdentity(key: any, value: any): [string, any] { + return [extractAccountId(key), extractIdentityInfo(value)]; } -async function getIdentities(helper: ChainHelperBase, noneCasePredicate?: (key: any, value: any) => void) { +export async function getIdentities(helper: ChainHelperBase, noneCasePredicate?: (key: any, value: any) => void) { const identities: [string, any][] = []; for(const [key, v] of await helper.getApi().query.identity.identityOf.entries()) { const value = v as any; @@ -33,41 +43,64 @@ return identities; } -function constructSubInfo(identityAccount: string, subQuery: any, supers: any[], ss58?: number): [string, any] { +// whether the existing chain data is more important than the coming +function isCurrentChainDataPriority(helper: ChainHelperBase, currentChainId: number | undefined, relayChainId: number) { + if (!currentChainId) return false; + // information has come from local chain, and is automatically superior + if (currentChainId == helper.chain.getChainProperties().ss58Format) return true; + // the lower the id, the more important it is (Polkadot has ss58 prefix = 0, Kusama has ss58 prefix = 2) + return currentChainId < relayChainId; +} + +// construct an object with all data necessary for insertion from storage query results +export function constructSubInfo(identityAccount: string, subQuery: any, supers: any[], ss58?: number): [string, any] { const deposit = subQuery.toJSON()[0]; const subIdentities = subQuery.toHuman()[1]; subIdentities.map((sub: string) => supers.find((sup: any) => sup[0] === sub)); - // supers.find((x: any) => x[0] === subIdentities[0])![1].toHuman(); + return [ encodeAddress(identityAccount, ss58), [ deposit, - subIdentities.map((sub: string) => [ - encodeAddress(sub, ss58), - supers.find((sup: any) => sup[0] === sub)![1].toJSON()[1], - ]), + subIdentities.map((sub: string): [string, object] | null => { + const sup = supers.find((sup: any) => sup[0] === sub); + if (!sup) { + console.error(`Error: Could not find info on super for \nsub-identity account\t${sub} of \nsuper account \t\t${identityAccount}, skipping.`); + return null; + } + return [encodeAddress(sub, ss58), sup[1].toJSON()[1]]; + }).filter((x: any) => x), ], ]; } -async function getSubs(helper: ChainHelperBase) { +export async function getSubs(helper: ChainHelperBase) { return (await helper.getApi().query.identity.subsOf.entries()).map(([key, value]) => [extractAccountId(key), value as any]); } -async function getSupers(helper: ChainHelperBase) { +export async function getSupers(helper: ChainHelperBase) { return (await helper.getApi().query.identity.superOf.entries()).map(([key, value]) => [extractAccountId(key), value as any]); } // The utility for pulling identity and sub-identity data const forceInsertIdentities = async (): Promise => { + let relaySS58Prefix = 0; const identitiesOnRelay: any[] = []; const subsOnRelay: any[] = []; const identitiesToRemove: string[] = []; await usingPlaygrounds(async helper => { try { + relaySS58Prefix = helper.chain.getChainProperties().ss58Format; // iterate over every identity for(const [key, value] of await getIdentities(helper, (key, _value) => identitiesToRemove.push((key as any).toHuman()[0]))) { // if any of the judgements resulted in a good confirmed outcome, keep this identity - if (value.toHuman().judgements.filter((x: any) => x[1] == 'Reasonable' || x[1] == 'KnownGood').length == 0) continue; + let knownGood = false, reasonable = false; + for (const [_id, judgement] of value.judgements) { + if (judgement == 'KnownGood') knownGood = true; + if (judgement == 'Reasonable') reasonable = true; + } + if (!(reasonable || knownGood)) continue; + // replace the registrator id with the relay chain's ss58 format + value.judgements = [[helper.chain.getChainProperties().ss58Format, knownGood ? 'KnownGood' : 'Reasonable']]; identitiesOnRelay.push([key, value]); } @@ -100,28 +133,29 @@ const ss58Format = helper.chain.getChainProperties().ss58Format; const paraIdentities = await getIdentities(helper); const identitiesToAdd: any[] = []; + const paraAccountsRegistrators: {[name: string]: number} = {}; // cross-reference every account for changes for (const [key, value] of identitiesOnRelay) { const encodedKey = encodeAddress(key, ss58Format); + // only update if the identity info does not exist or is changed const identity = paraIdentities.find(i => i[0] === encodedKey); if (identity) { - // only update if the identity info does not exist or is changed - if (JSON.stringify(value) === JSON.stringify(identity[1])) { + const registratorId = identity[1].judgements[0][0]; + paraAccountsRegistrators[encodedKey] = registratorId; + if (isCurrentChainDataPriority(helper, registratorId, value.judgements[0][0]) + || JSON.stringify(value.info) === JSON.stringify(identity[1].info)) { continue; } } + identitiesToAdd.push([key, value]); // exercise caution - in case we have an identity and the realy doesn't, it might mean one of two things: // 1) it was deleted on the relay; // 2) it is our own identity, we don't want to delete it. // identitiesToRemove.push((key as any).toHuman()[0]); } - - const paraSubs = await getSubs(helper); - const supersOfSubs = await getSupers(helper); - const subsToUpdate: any[] = []; if (identitiesToRemove.length != 0) await helper.getSudo().executeExtrinsic(superuser, 'api.tx.identity.forceRemoveIdentities', [identitiesToRemove]); @@ -132,15 +166,23 @@ + ` and found ${identitiesToRemove.length} identities for potential removal.` + ` Now there are ${(await helper.getApi().query.identity.identityOf.keys()).length}.`); + // fill sub-identities + const paraSubs = await getSubs(helper); + const supersOfSubs = await getSupers(helper); + const subsToUpdate: any[] = []; + for (const [key, value] of subsOnRelay) { const encodedKey = encodeAddress(key, ss58Format); const sub = paraSubs.find(i => i[0] === encodedKey); if (sub) { // only update if the sub-identity info does not exist or is changed - if (JSON.stringify(value) === JSON.stringify(constructSubInfo(sub[0], sub[1], supersOfSubs)[1])) { + if (isCurrentChainDataPriority(helper, paraAccountsRegistrators[encodedKey], relaySS58Prefix) + || JSON.stringify(value) === JSON.stringify(constructSubInfo(sub[0], sub[1], supersOfSubs)[1])) { continue; } - } + } else if (value[1].length == 0) + continue; + subsToUpdate.push([key, value]); } @@ -156,4 +198,5 @@ }, paraUrl); }; -forceInsertIdentities().catch(() => process.exit(1)); \ No newline at end of file +if (process.argv[1] === module.filename) + forceInsertIdentities().catch(() => process.exit(1)); --- /dev/null +++ b/tests/src/util/relayIdentitiesChecker.ts @@ -0,0 +1,114 @@ +// Copyright 2019-2022 Unique Network (Gibraltar) Ltd. +// SPDX-License-Identifier: Apache-2.0 +// +// Checks and reports the differences between identities and sub-identities on two chains. +// +// Usage: `yarn checkRelayIdentities [relay-1 WS URL] [relay-2 WS URL]` +// Example: `yarn checkRelayIdentities wss://polkadot-rpc.dwellir.com wss://kusama-rpc.dwellir.com` + +import {encodeAddress} from '@polkadot/keyring'; +import {usingPlaygrounds} from './index'; +import {getIdentities, getSubs, getSupers, constructSubInfo} from './identitySetter'; + +const relay1Url = process.argv[2] ?? 'ws://localhost:9844'; +const relay2Url = process.argv[3] ?? 'ws://localhost:9844'; + +async function pullIdentities(relayUrl: string): Promise<[any[], any[]]> { + const identities: any[] = []; + const subs: any[] = []; + + await usingPlaygrounds(async helper => { + try { + // iterate over every identity + for(const [key, value] of await getIdentities(helper)) { + // if any of the judgements resulted in a good confirmed outcome, keep this identity + if (value.toHuman().judgements.filter((x: any) => x[1] == 'Reasonable' || x[1] == 'KnownGood').length == 0) continue; + identities.push([key, value]); + } + + const supersOfSubs = await getSupers(helper); + + // iterate over every sub-identity + for(const [key, value] of await getSubs(helper)) { + // only get subs of the identities interesting to us + if (identities.find((x: any) => x[0] == key) == -1) continue; + subs.push(constructSubInfo(key, value, supersOfSubs)); + } + } catch (error) { + console.error(error); + throw Error(`Error during fetching identities on ${relayUrl}`); + } + }, relayUrl); + + return [identities, subs]; +} + +// The utility for pulling identity and sub-identity data +const checkRelayIdentities = async (): Promise => { + const [identitiesOnRelay1, subIdentitiesOnRelay1] = await pullIdentities(relay1Url); + const [identitiesOnRelay2, subIdentitiesOnRelay2] = await pullIdentities(relay2Url); + + console.log('identities counts:\t', identitiesOnRelay1.length, identitiesOnRelay2.length); + console.log('sub-identities counts:\t', subIdentitiesOnRelay1.length, subIdentitiesOnRelay2.length); + console.log(); + + try { + const matchingAddresses: string[] = []; + const inequalIdentities: {[name: string]: [any, any]} = {}; + + for (const [key1, value1] of identitiesOnRelay1) { + const address = encodeAddress(key1); + const identity2 = identitiesOnRelay2.find(([key2, _value2]) => address === encodeAddress(key2)); + if (!identity2) continue; + matchingAddresses.push(address); + + //const [[key2, value2]] = identitiesOnRelay2.splice(index2, 1); + const [_key2, value2] = identity2; + if (JSON.stringify(value1.info) === JSON.stringify(value2.info)) continue; + inequalIdentities[address] = [value1, value2]; + } + + /*for (const [v1, v2] of Object.values(inequalIdentities)) { + console.log(v1.toHuman().info); + console.log(); + console.log(v2.toHuman().info); + await new Promise(resolve => setTimeout(resolve, 4000)); + }*/ + + console.log(`Accounts with identities on both relays:\t${matchingAddresses.length}`); + console.log(`Sub-identities with conflicting information:\t${Object.entries(inequalIdentities).length}`); + console.log(); + + const inequalSubIdentities = []; + let matchesFound = 0; + for (const address of matchingAddresses) { + const sub1 = subIdentitiesOnRelay1.find(([key1, _value1]) => address === encodeAddress(key1)); + if (!sub1) continue; + const sub2 = subIdentitiesOnRelay2.find(([key2, _value2]) => address === encodeAddress(key2)); + if (!sub2) continue; + + const [value1, value2] = [sub1[1], sub2[1]]; + matchesFound++; + + if (JSON.stringify(value1[1]) === JSON.stringify(value2[1])) { + continue; + } + inequalSubIdentities.push([value1, value2]); + } + + /*for (const [v1, v2] of inequalSubIdentities) { + console.log(v1[1]); + console.log(); + console.log(v2[1]); + await new Promise(resolve => setTimeout(resolve, 300)); + }*/ + console.log(`Accounts with sub-identities on both relays:\t${matchesFound}`); + console.log(`Of them, those with conflicting sub-identities:\t${inequalSubIdentities.length}`); + } catch (error) { + console.error(error); + throw Error('Error during comparison'); + } +}; + +if (process.argv[1] === module.filename) + checkRelayIdentities().catch(() => process.exit(1));