From 6d7961e7fa5f8bc8fbefefe1d58dd7a4a0643190 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 7 Jan 2025 11:55:03 +0100 Subject: [PATCH 01/11] Add support for metadata hash check --- packages/chain-helpers/package.json | 3 +- .../src/blockchain/Blockchain.ts | 45 ++++++++++++++++--- 2 files changed, 41 insertions(+), 7 deletions(-) diff --git a/packages/chain-helpers/package.json b/packages/chain-helpers/package.json index 8ce2f6f0d..638ed2809 100644 --- a/packages/chain-helpers/package.json +++ b/packages/chain-helpers/package.json @@ -57,6 +57,7 @@ "dependencies": { "@kiltprotocol/config": "workspace:*", "@kiltprotocol/types": "workspace:*", - "@kiltprotocol/utils": "workspace:*" + "@kiltprotocol/utils": "workspace:*", + "@polkadot-api/merkleize-metadata": "^1.0.0" } } diff --git a/packages/chain-helpers/src/blockchain/Blockchain.ts b/packages/chain-helpers/src/blockchain/Blockchain.ts index ea7f053a0..118ed2fe0 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -7,10 +7,11 @@ import { ApiPromise, SubmittableResult } from '@polkadot/api' import type { TxWithEvent } from '@polkadot/api-derive/types' +import type { SignerOptions } from '@polkadot/api-base/types' import type { Vec } from '@polkadot/types' import type { Call, Extrinsic } from '@polkadot/types/interfaces' import type { AnyNumber, IMethod } from '@polkadot/types/types' -import type { BN } from '@polkadot/util' +import { u8aToHex, type BN } from '@polkadot/util' // eslint-disable-next-line @typescript-eslint/no-unused-vars -- doing this instead of import '@kiltprotocol/augment-api' to avoid creating an import at runtime import type * as _ from '@kiltprotocol/augment-api' @@ -24,6 +25,7 @@ import type { import { ConfigService } from '@kiltprotocol/config' import { SDKErrors, Signers } from '@kiltprotocol/utils' +import { merkleizeMetadata } from '@polkadot-api/merkleize-metadata' import { ErrorHandler } from '../errorhandling/index.js' import { makeSubscriptionPromise } from './SubscriptionPromise.js' @@ -174,19 +176,47 @@ export const dispatchTx = submitSignedTx * @param signer The {@link KeyringPair} used to sign the tx. * @param opts Additional options. * @param opts.tip Optional amount of Femto-KILT to tip the validator. + * @param opts.checkMetadata Boolean flag indicated whether to verify the metadata hash upon tx submission. * @returns A signed {@link SubmittableExtrinsic}. */ export async function signTx( tx: SubmittableExtrinsic, signer: KeyringPair | TransactionSigner, - { tip }: { tip?: AnyNumber } = {} + { tip, checkMetadata }: { tip?: AnyNumber; checkMetadata?: boolean } = {} ): Promise { + const signOptions = await (async (): Promise> => { + if (!checkMetadata) { + return { + tip, + } + } + // If `checkMetadata` is enabled, include that when signing the tx. + const api = ConfigService.get('api') + const metadata = await api.call.metadata.metadataAtVersion(15) + const { specName, specVersion } = api.runtimeVersion + const merkleInfo = { + base58Prefix: api.consts.system.ss58Prefix.toNumber(), + decimals: api.registry.chainDecimals[0], + specName: specName.toString(), + specVersion: specVersion.toNumber(), + tokenSymbol: api.registry.chainTokens[0], + } + const merkleizedMetadata = merkleizeMetadata(metadata.toHex(), merkleInfo) + const metadataHash = u8aToHex(merkleizedMetadata.digest()) + return { + tip, + mode: 1, + withSignedTransaction: true, + metadataHash, + } + })() + if ('address' in signer) { - return tx.signAsync(signer, { tip }) + return tx.signAsync(signer, signOptions) } return tx.signAsync(signer.id, { - tip, + ...signOptions, signer: Signers.getPolkadotSigner([signer]), }) } @@ -198,6 +228,7 @@ export async function signTx( * @param signer The {@link KeyringPair} used to sign the tx. * @param opts Partial optional criteria for resolving/rejecting the promise. * @param opts.tip Optional amount of Femto-KILT to tip the validator. + * @param opts.checkMetadata Boolean flag indicated whether to verify the metadata hash upon tx submission. * @returns Promise result of executing the extrinsic, of type ISubmittableResult. */ export async function signAndSubmitTx( @@ -205,10 +236,12 @@ export async function signAndSubmitTx( signer: KeyringPair | TransactionSigner, { tip, + checkMetadata, ...opts - }: Partial & Partial<{ tip: AnyNumber }> = {} + }: Partial & + Partial<{ tip: AnyNumber; checkMetadata: boolean }> = {} ): Promise { - const signedTx = await signTx(tx, signer, { tip }) + const signedTx = await signTx(tx, signer, { tip, checkMetadata }) return submitSignedTx(signedTx, opts) } From 2ac55df5dc23719d783b9039cf72f0637c44f34d Mon Sep 17 00:00:00 2001 From: Raphael Flechtner Date: Tue, 7 Jan 2025 12:04:15 +0100 Subject: [PATCH 02/11] chore: update lockfile --- yarn.lock | 66 +++++++++++++++++++++++++++++++++++++++++++++++-------- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/yarn.lock b/yarn.lock index 1e27a04c2..69376e4b2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2069,6 +2069,7 @@ __metadata: "@kiltprotocol/config": "workspace:*" "@kiltprotocol/types": "workspace:*" "@kiltprotocol/utils": "workspace:*" + "@polkadot-api/merkleize-metadata": "npm:^1.0.0" rimraf: "npm:^3.0.2" typescript: "npm:^4.8.3" peerDependencies: @@ -2350,13 +2351,20 @@ __metadata: languageName: node linkType: hard -"@noble/hashes@npm:1.4.0, @noble/hashes@npm:^1.3.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.3": +"@noble/hashes@npm:1.4.0": version: 1.4.0 resolution: "@noble/hashes@npm:1.4.0" checksum: 10c0/8c3f005ee72e7b8f9cff756dfae1241485187254e3f743873e22073d63906863df5d4f13d441b7530ea614b7a093f0d889309f28b59850f33b66cb26a779a4a5 languageName: node linkType: hard +"@noble/hashes@npm:^1.3.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.3, @noble/hashes@npm:^1.6.1": + version: 1.7.0 + resolution: "@noble/hashes@npm:1.7.0" + checksum: 10c0/1ef0c985ebdb5a1bd921ea6d959c90ba826af3ae05b40b459a703e2a5e9b259f190c6e92d6220fb3800e2385521e4159e238415ad3f6b79c52f91dd615e491dc + languageName: node + linkType: hard + "@nodelib/fs.scandir@npm:2.1.5": version: 2.1.5 resolution: "@nodelib/fs.scandir@npm:2.1.5" @@ -2430,6 +2438,17 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/merkleize-metadata@npm:^1.0.0": + version: 1.1.12 + resolution: "@polkadot-api/merkleize-metadata@npm:1.1.12" + dependencies: + "@polkadot-api/metadata-builders": "npm:0.10.0" + "@polkadot-api/substrate-bindings": "npm:0.11.0" + "@polkadot-api/utils": "npm:0.1.2" + checksum: 10c0/d3fac5be41878dd8c318dd76c9f09e3a6e769f8791386bc4c5a5c27c272efa0c4d3cf839034dfc2b28324c0a35b9bf5fd9df2e33392cff7760f8f5b40f41fe20 + languageName: node + linkType: hard + "@polkadot-api/metadata-builders@npm:0.0.1": version: 0.0.1 resolution: "@polkadot-api/metadata-builders@npm:0.0.1" @@ -2440,6 +2459,16 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/metadata-builders@npm:0.10.0": + version: 0.10.0 + resolution: "@polkadot-api/metadata-builders@npm:0.10.0" + dependencies: + "@polkadot-api/substrate-bindings": "npm:0.11.0" + "@polkadot-api/utils": "npm:0.1.2" + checksum: 10c0/0fb49a6cd4e2b66e3c3983f66e427b5763da0b67d5c4847c190e6e546f67bc4908d456b2afe80ce85316736d3aa408d779f309b292957648820aca44e6578719 + languageName: node + linkType: hard + "@polkadot-api/observable-client@npm:0.1.0": version: 0.1.0 resolution: "@polkadot-api/observable-client@npm:0.1.0" @@ -2466,6 +2495,18 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/substrate-bindings@npm:0.11.0": + version: 0.11.0 + resolution: "@polkadot-api/substrate-bindings@npm:0.11.0" + dependencies: + "@noble/hashes": "npm:^1.6.1" + "@polkadot-api/utils": "npm:0.1.2" + "@scure/base": "npm:^1.2.1" + scale-ts: "npm:^1.6.1" + checksum: 10c0/8e0ea627a036b2bfd34adba06bb535d5ec473b118c53c2de88e48f245907decebbebd701b27f62d351509c6d28c88630160c1a4110ef5a61b0ca53088e94864f + languageName: node + linkType: hard + "@polkadot-api/substrate-client@npm:0.0.1": version: 0.0.1 resolution: "@polkadot-api/substrate-client@npm:0.0.1" @@ -2480,6 +2521,13 @@ __metadata: languageName: node linkType: hard +"@polkadot-api/utils@npm:0.1.2": + version: 0.1.2 + resolution: "@polkadot-api/utils@npm:0.1.2" + checksum: 10c0/530270141ab7a8d114aff68adabbc643a7b7f5abcfb974a5dac5044e1f5a459881f427e357a7eadfecf55847da5e48828be6dbcf502dd22e097c87546762a036 + languageName: node + linkType: hard + "@polkadot/api-augment@npm:12.2.1": version: 12.2.1 resolution: "@polkadot/api-augment@npm:12.2.1" @@ -2919,10 +2967,10 @@ __metadata: languageName: node linkType: hard -"@scure/base@npm:^1.1.0, @scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5": - version: 1.1.7 - resolution: "@scure/base@npm:1.1.7" - checksum: 10c0/2d06aaf39e6de4b9640eb40d2e5419176ebfe911597856dcbf3bc6209277ddb83f4b4b02cb1fd1208f819654268ec083da68111d3530bbde07bae913e2fc2e5d +"@scure/base@npm:^1.1.0, @scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5, @scure/base@npm:^1.2.1": + version: 1.2.1 + resolution: "@scure/base@npm:1.2.1" + checksum: 10c0/e61068854370855b89c50c28fa4092ea6780f1e0db64ea94075ab574ebcc964f719a3120dc708db324991f4b3e652d92ebda03fce2bf6a4900ceeacf9c0ff933 languageName: node linkType: hard @@ -8820,10 +8868,10 @@ __metadata: languageName: node linkType: hard -"scale-ts@npm:^1.6.0": - version: 1.6.0 - resolution: "scale-ts@npm:1.6.0" - checksum: 10c0/ce4ea3559c6b6bdf2a62454aac83cc3151ae93d1a507ddb8e95e83ce1190085aed61c46901bd42d41d8f8ba58279d7e37057c68c2b674c2d39b8cf5d169e90dd +"scale-ts@npm:^1.6.0, scale-ts@npm:^1.6.1": + version: 1.6.1 + resolution: "scale-ts@npm:1.6.1" + checksum: 10c0/bbcf476029095152189c5bd210922b43342e8bfb712bf56237de172d55b528e090419e80da67c627a8f706a228237346b82de527755d7f197bb4d822c6383dfd languageName: node linkType: hard From 7b9e1e4294504ba65287bf6db174849069d3a998 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 7 Jan 2025 14:11:12 +0100 Subject: [PATCH 03/11] Use cached metadata --- packages/chain-helpers/src/blockchain/Blockchain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chain-helpers/src/blockchain/Blockchain.ts b/packages/chain-helpers/src/blockchain/Blockchain.ts index 118ed2fe0..dc99f2a75 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -192,7 +192,7 @@ export async function signTx( } // If `checkMetadata` is enabled, include that when signing the tx. const api = ConfigService.get('api') - const metadata = await api.call.metadata.metadataAtVersion(15) + const metadata = api.runtimeMetadata.asV15 const { specName, specVersion } = api.runtimeVersion const merkleInfo = { base58Prefix: api.consts.system.ss58Prefix.toNumber(), From 09c5876416ec955aaa0afa56427670de65798042 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 7 Jan 2025 14:38:45 +0100 Subject: [PATCH 04/11] Address more comments --- .../chain-helpers/src/blockchain/Blockchain.ts | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/packages/chain-helpers/src/blockchain/Blockchain.ts b/packages/chain-helpers/src/blockchain/Blockchain.ts index dc99f2a75..b21b4b520 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -11,7 +11,11 @@ import type { SignerOptions } from '@polkadot/api-base/types' import type { Vec } from '@polkadot/types' import type { Call, Extrinsic } from '@polkadot/types/interfaces' import type { AnyNumber, IMethod } from '@polkadot/types/types' -import { u8aToHex, type BN } from '@polkadot/util' +import { type BN } from '@polkadot/util' +import { + type ExtraInfo, + merkleizeMetadata, +} from '@polkadot-api/merkleize-metadata' // eslint-disable-next-line @typescript-eslint/no-unused-vars -- doing this instead of import '@kiltprotocol/augment-api' to avoid creating an import at runtime import type * as _ from '@kiltprotocol/augment-api' @@ -24,8 +28,6 @@ import type { } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' import { SDKErrors, Signers } from '@kiltprotocol/utils' - -import { merkleizeMetadata } from '@polkadot-api/merkleize-metadata' import { ErrorHandler } from '../errorhandling/index.js' import { makeSubscriptionPromise } from './SubscriptionPromise.js' @@ -194,7 +196,7 @@ export async function signTx( const api = ConfigService.get('api') const metadata = api.runtimeMetadata.asV15 const { specName, specVersion } = api.runtimeVersion - const merkleInfo = { + const merkleInfo: ExtraInfo = { base58Prefix: api.consts.system.ss58Prefix.toNumber(), decimals: api.registry.chainDecimals[0], specName: specName.toString(), @@ -202,11 +204,9 @@ export async function signTx( tokenSymbol: api.registry.chainTokens[0], } const merkleizedMetadata = merkleizeMetadata(metadata.toHex(), merkleInfo) - const metadataHash = u8aToHex(merkleizedMetadata.digest()) + const metadataHash = merkleizedMetadata.digest() return { tip, - mode: 1, - withSignedTransaction: true, metadataHash, } })() @@ -217,6 +217,9 @@ export async function signTx( return tx.signAsync(signer.id, { ...signOptions, + // Required as described in https://github.com/polkadot-js/api/blob/109d3b2201ea51f27180e34dfd883ec71d402f6b/packages/api-base/src/types/submittable.ts#L79. + withSignedTransaction: true, + mode: 1, signer: Signers.getPolkadotSigner([signer]), }) } From dfd17af1f20716866adb67d07eb1d937a01f44e8 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Tue, 7 Jan 2025 14:53:29 +0100 Subject: [PATCH 05/11] Factor out metadata root calculation --- .../src/blockchain/Blockchain.ts | 56 +++++++++++-------- 1 file changed, 32 insertions(+), 24 deletions(-) diff --git a/packages/chain-helpers/src/blockchain/Blockchain.ts b/packages/chain-helpers/src/blockchain/Blockchain.ts index b21b4b520..46e1da040 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -30,6 +30,7 @@ import { ConfigService } from '@kiltprotocol/config' import { SDKErrors, Signers } from '@kiltprotocol/utils' import { ErrorHandler } from '../errorhandling/index.js' import { makeSubscriptionPromise } from './SubscriptionPromise.js' +import { blake2AsHex } from '@polkadot/util-crypto' const log = ConfigService.LoggingFactory.getLogger('Blockchain') @@ -171,6 +172,34 @@ export async function submitSignedTx( export const dispatchTx = submitSignedTx +// Returns the Merkle root of the metadata as stored in the `ConfigService` cache. If not present, it computes it, stores it in the cache for future retrievals, and returns it. +async function getMetadataHash(api: ApiPromise): Promise { + const metadata = api.runtimeMetadata.asV15 + const { specName, specVersion } = api.runtimeVersion + const genesisHash = await api.genesisHash + const cacheKey = blake2AsHex( + Uint8Array.from([ + ...specName.toU8a(), + ...specVersion.toU8a(), + ...genesisHash.toU8a(), + ]) + ) + if (ConfigService.isSet(cacheKey)) { + return ConfigService.get(cacheKey) + } + const merkleInfo: ExtraInfo = { + base58Prefix: api.consts.system.ss58Prefix.toNumber(), + decimals: api.registry.chainDecimals[0], + specName: specName.toString(), + specVersion: specVersion.toNumber(), + tokenSymbol: api.registry.chainTokens[0], + } + const merkleizedMetadata = merkleizeMetadata(metadata.toHex(), merkleInfo) + const metadataHash = merkleizedMetadata.digest() + ConfigService.set({ cacheKey: metadataHash }) + return metadataHash +} + /** * Signs a SubmittableExtrinsic. * @@ -186,30 +215,9 @@ export async function signTx( signer: KeyringPair | TransactionSigner, { tip, checkMetadata }: { tip?: AnyNumber; checkMetadata?: boolean } = {} ): Promise { - const signOptions = await (async (): Promise> => { - if (!checkMetadata) { - return { - tip, - } - } - // If `checkMetadata` is enabled, include that when signing the tx. - const api = ConfigService.get('api') - const metadata = api.runtimeMetadata.asV15 - const { specName, specVersion } = api.runtimeVersion - const merkleInfo: ExtraInfo = { - base58Prefix: api.consts.system.ss58Prefix.toNumber(), - decimals: api.registry.chainDecimals[0], - specName: specName.toString(), - specVersion: specVersion.toNumber(), - tokenSymbol: api.registry.chainTokens[0], - } - const merkleizedMetadata = merkleizeMetadata(metadata.toHex(), merkleInfo) - const metadataHash = merkleizedMetadata.digest() - return { - tip, - metadataHash, - } - })() + const signOptions: Partial = checkMetadata + ? { tip, metadataHash: await getMetadataHash(ConfigService.get('api')) } + : { tip } if ('address' in signer) { return tx.signAsync(signer, signOptions) From 22c3af8d938ed0f7c3acb1ec5f551731ccf5774b Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 8 Jan 2025 10:42:38 +0100 Subject: [PATCH 06/11] Use local cache --- packages/chain-helpers/src/blockchain/Blockchain.ts | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/packages/chain-helpers/src/blockchain/Blockchain.ts b/packages/chain-helpers/src/blockchain/Blockchain.ts index 46e1da040..b219cd78c 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -28,9 +28,9 @@ import type { } from '@kiltprotocol/types' import { ConfigService } from '@kiltprotocol/config' import { SDKErrors, Signers } from '@kiltprotocol/utils' +import { blake2AsHex } from '@polkadot/util-crypto' import { ErrorHandler } from '../errorhandling/index.js' import { makeSubscriptionPromise } from './SubscriptionPromise.js' -import { blake2AsHex } from '@polkadot/util-crypto' const log = ConfigService.LoggingFactory.getLogger('Blockchain') @@ -172,6 +172,8 @@ export async function submitSignedTx( export const dispatchTx = submitSignedTx +const metadataHashes = new Map() + // Returns the Merkle root of the metadata as stored in the `ConfigService` cache. If not present, it computes it, stores it in the cache for future retrievals, and returns it. async function getMetadataHash(api: ApiPromise): Promise { const metadata = api.runtimeMetadata.asV15 @@ -184,8 +186,8 @@ async function getMetadataHash(api: ApiPromise): Promise { ...genesisHash.toU8a(), ]) ) - if (ConfigService.isSet(cacheKey)) { - return ConfigService.get(cacheKey) + if (metadataHashes.has(cacheKey)) { + return metadataHashes.get(cacheKey) as Uint8Array } const merkleInfo: ExtraInfo = { base58Prefix: api.consts.system.ss58Prefix.toNumber(), @@ -196,7 +198,7 @@ async function getMetadataHash(api: ApiPromise): Promise { } const merkleizedMetadata = merkleizeMetadata(metadata.toHex(), merkleInfo) const metadataHash = merkleizedMetadata.digest() - ConfigService.set({ cacheKey: metadataHash }) + metadataHashes.set(cacheKey, metadataHash) return metadataHash } From 49626342d675ec1bcf2a71cfb567c022c0587522 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Wed, 8 Jan 2025 15:04:26 +0100 Subject: [PATCH 07/11] Add integration tests --- .../src/blockchain/Blockchain.ts | 28 ++++++++++++------- tests/integration/Blockchain.spec.ts | 26 ++++++++++++++++- 2 files changed, 43 insertions(+), 11 deletions(-) diff --git a/packages/chain-helpers/src/blockchain/Blockchain.ts b/packages/chain-helpers/src/blockchain/Blockchain.ts index b219cd78c..0213ac3ca 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -11,7 +11,7 @@ import type { SignerOptions } from '@polkadot/api-base/types' import type { Vec } from '@polkadot/types' import type { Call, Extrinsic } from '@polkadot/types/interfaces' import type { AnyNumber, IMethod } from '@polkadot/types/types' -import { type BN } from '@polkadot/util' +import { u8aToHex, type BN } from '@polkadot/util' import { type ExtraInfo, merkleizeMetadata, @@ -20,6 +20,7 @@ import { // eslint-disable-next-line @typescript-eslint/no-unused-vars -- doing this instead of import '@kiltprotocol/augment-api' to avoid creating an import at runtime import type * as _ from '@kiltprotocol/augment-api' import type { + HexString, ISubmittableResult, KeyringPair, SubmittableExtrinsic, @@ -172,11 +173,13 @@ export async function submitSignedTx( export const dispatchTx = submitSignedTx -const metadataHashes = new Map() +const metadataHashes = new Map() // Returns the Merkle root of the metadata as stored in the `ConfigService` cache. If not present, it computes it, stores it in the cache for future retrievals, and returns it. -async function getMetadataHash(api: ApiPromise): Promise { - const metadata = api.runtimeMetadata.asV15 +async function getMetadataHash(api: ApiPromise): Promise { + const metadata = await api.call.metadata.metadataAtVersion(15) + // TODO: Find out why using this metadata here fails to decode when calculating the Merkle root. + // const metadata = api.runtimeMetadata.asV15 const { specName, specVersion } = api.runtimeVersion const genesisHash = await api.genesisHash const cacheKey = blake2AsHex( @@ -187,7 +190,7 @@ async function getMetadataHash(api: ApiPromise): Promise { ]) ) if (metadataHashes.has(cacheKey)) { - return metadataHashes.get(cacheKey) as Uint8Array + return metadataHashes.get(cacheKey) as HexString } const merkleInfo: ExtraInfo = { base58Prefix: api.consts.system.ss58Prefix.toNumber(), @@ -197,7 +200,7 @@ async function getMetadataHash(api: ApiPromise): Promise { tokenSymbol: api.registry.chainTokens[0], } const merkleizedMetadata = merkleizeMetadata(metadata.toHex(), merkleInfo) - const metadataHash = merkleizedMetadata.digest() + const metadataHash = u8aToHex(merkleizedMetadata.digest()) metadataHashes.set(cacheKey, metadataHash) return metadataHash } @@ -218,7 +221,15 @@ export async function signTx( { tip, checkMetadata }: { tip?: AnyNumber; checkMetadata?: boolean } = {} ): Promise { const signOptions: Partial = checkMetadata - ? { tip, metadataHash: await getMetadataHash(ConfigService.get('api')) } + ? { + tip, + // Required as described in https://github.com/polkadot-js/api/blob/109d3b2201ea51f27180e34dfd883ec71d402f6b/packages/api-base/src/types/submittable.ts#L79. + metadataHash: await getMetadataHash(ConfigService.get('api')), + // Used by external signers to to know there's additional data to be included in the payload (see link above). + withSignedTransaction: true, + // Forces the tx to fail of the metadata does not match (added for backward compatibility). See https://paritytech.github.io/polkadot-sdk/master/frame_metadata_hash_extension/struct.CheckMetadataHash.html. + mode: 1, + } : { tip } if ('address' in signer) { @@ -227,9 +238,6 @@ export async function signTx( return tx.signAsync(signer.id, { ...signOptions, - // Required as described in https://github.com/polkadot-js/api/blob/109d3b2201ea51f27180e34dfd883ec71d402f6b/packages/api-base/src/types/submittable.ts#L79. - withSignedTransaction: true, - mode: 1, signer: Signers.getPolkadotSigner([signer]), }) } diff --git a/tests/integration/Blockchain.spec.ts b/tests/integration/Blockchain.spec.ts index 8452ecc95..8df10c26f 100644 --- a/tests/integration/Blockchain.spec.ts +++ b/tests/integration/Blockchain.spec.ts @@ -16,7 +16,13 @@ import { import type { KeyringPair } from '@kiltprotocol/types' import { makeSigningKeyTool } from '../testUtils/index.js' -import { devCharlie, devFaucet, initializeApi, submitTx } from './utils.js' +import { + devAlice, + devCharlie, + devFaucet, + initializeApi, + submitTx, +} from './utils.js' let api: ApiPromise beforeAll(async () => { @@ -153,6 +159,24 @@ describe('Chain returns specific errors, that we check for', () => { }, 40000) }) +describe('The added `SignedExtension`s are valid', () => { + it(`'CheckMetadataHash' works`, async () => { + const systemRemarkTx = api.tx.system.remark('Test remark') + const submitPromise = Blockchain.signAndSubmitTx(systemRemarkTx, devAlice, { + checkMetadata: true, + }) + await expect(submitPromise).resolves.not.toThrow() + }) + + it(`No 'CheckMetadataHash' works`, async () => { + const systemRemarkTx = api.tx.system.remark('Test remark') + const submitPromise = Blockchain.signAndSubmitTx(systemRemarkTx, devAlice, { + checkMetadata: false, + }) + await expect(submitPromise).resolves.not.toThrow() + }) +}) + afterAll(async () => { if (typeof api !== 'undefined') await disconnect() }) From af1c1286cbfc1df3844d8bf1c931f7b89764d371 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 13 Jan 2025 08:47:23 +0100 Subject: [PATCH 08/11] Empty From f5174bd3a7958e778d330a6b7828eaedccd16d64 Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 13 Jan 2025 10:44:16 +0100 Subject: [PATCH 09/11] Update comment --- .../src/blockchain/Blockchain.ts | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/chain-helpers/src/blockchain/Blockchain.ts b/packages/chain-helpers/src/blockchain/Blockchain.ts index 0213ac3ca..f45024b23 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -175,11 +175,11 @@ export const dispatchTx = submitSignedTx const metadataHashes = new Map() -// Returns the Merkle root of the metadata as stored in the `ConfigService` cache. If not present, it computes it, stores it in the cache for future retrievals, and returns it. +// Returns the Merkle root of the metadata as stored in the local `metadataHashes` cache. If not present, it computes it, stores it in the cache for future retrievals, and returns it. async function getMetadataHash(api: ApiPromise): Promise { - const metadata = await api.call.metadata.metadataAtVersion(15) + // const metadata = await api.call.metadata.metadataAtVersion(15) // TODO: Find out why using this metadata here fails to decode when calculating the Merkle root. - // const metadata = api.runtimeMetadata.asV15 + const metadata = api.runtimeMetadata.asV15 const { specName, specVersion } = api.runtimeVersion const genesisHash = await api.genesisHash const cacheKey = blake2AsHex( @@ -222,14 +222,14 @@ export async function signTx( ): Promise { const signOptions: Partial = checkMetadata ? { - tip, - // Required as described in https://github.com/polkadot-js/api/blob/109d3b2201ea51f27180e34dfd883ec71d402f6b/packages/api-base/src/types/submittable.ts#L79. - metadataHash: await getMetadataHash(ConfigService.get('api')), - // Used by external signers to to know there's additional data to be included in the payload (see link above). - withSignedTransaction: true, - // Forces the tx to fail of the metadata does not match (added for backward compatibility). See https://paritytech.github.io/polkadot-sdk/master/frame_metadata_hash_extension/struct.CheckMetadataHash.html. - mode: 1, - } + tip, + // Required as described in https://github.com/polkadot-js/api/blob/109d3b2201ea51f27180e34dfd883ec71d402f6b/packages/api-base/src/types/submittable.ts#L79. + metadataHash: await getMetadataHash(ConfigService.get('api')), + // Used by external signers to to know there's additional data to be included in the payload (see link above). + withSignedTransaction: true, + // Forces the tx to fail of the metadata does not match (added for backward compatibility). See https://paritytech.github.io/polkadot-sdk/master/frame_metadata_hash_extension/struct.CheckMetadataHash.html. + mode: 1, + } : { tip } if ('address' in signer) { From 4b05d7251ee8fc96bc36296cca0b92d20f75c95e Mon Sep 17 00:00:00 2001 From: Antonio Antonino Date: Mon, 13 Jan 2025 10:50:26 +0100 Subject: [PATCH 10/11] Update metadata fetching --- packages/chain-helpers/src/blockchain/Blockchain.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/packages/chain-helpers/src/blockchain/Blockchain.ts b/packages/chain-helpers/src/blockchain/Blockchain.ts index f45024b23..47149e27c 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -177,9 +177,6 @@ const metadataHashes = new Map() // Returns the Merkle root of the metadata as stored in the local `metadataHashes` cache. If not present, it computes it, stores it in the cache for future retrievals, and returns it. async function getMetadataHash(api: ApiPromise): Promise { - // const metadata = await api.call.metadata.metadataAtVersion(15) - // TODO: Find out why using this metadata here fails to decode when calculating the Merkle root. - const metadata = api.runtimeMetadata.asV15 const { specName, specVersion } = api.runtimeVersion const genesisHash = await api.genesisHash const cacheKey = blake2AsHex( @@ -199,6 +196,7 @@ async function getMetadataHash(api: ApiPromise): Promise { specVersion: specVersion.toNumber(), tokenSymbol: api.registry.chainTokens[0], } + const metadata = await api.call.metadata.metadataAtVersion(15) const merkleizedMetadata = merkleizeMetadata(metadata.toHex(), merkleInfo) const metadataHash = u8aToHex(merkleizedMetadata.digest()) metadataHashes.set(cacheKey, metadataHash) From ed3ae728ebace6eee78dcb483229b5a595070761 Mon Sep 17 00:00:00 2001 From: Antonio Date: Mon, 13 Jan 2025 13:57:42 +0100 Subject: [PATCH 11/11] chore: update comment typo Co-authored-by: Raphael Flechtner <39338561+rflechtner@users.noreply.github.com> --- packages/chain-helpers/src/blockchain/Blockchain.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/chain-helpers/src/blockchain/Blockchain.ts b/packages/chain-helpers/src/blockchain/Blockchain.ts index 47149e27c..ec787d0e0 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -225,7 +225,7 @@ export async function signTx( metadataHash: await getMetadataHash(ConfigService.get('api')), // Used by external signers to to know there's additional data to be included in the payload (see link above). withSignedTransaction: true, - // Forces the tx to fail of the metadata does not match (added for backward compatibility). See https://paritytech.github.io/polkadot-sdk/master/frame_metadata_hash_extension/struct.CheckMetadataHash.html. + // Forces the tx to fail if the metadata does not match (added for backward compatibility). See https://paritytech.github.io/polkadot-sdk/master/frame_metadata_hash_extension/struct.CheckMetadataHash.html. mode: 1, } : { tip }