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..ec787d0e0 100644 --- a/packages/chain-helpers/src/blockchain/Blockchain.ts +++ b/packages/chain-helpers/src/blockchain/Blockchain.ts @@ -7,14 +7,20 @@ 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' +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' import type { + HexString, ISubmittableResult, KeyringPair, SubmittableExtrinsic, @@ -23,7 +29,7 @@ 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' @@ -167,6 +173,36 @@ export async function submitSignedTx( export const dispatchTx = submitSignedTx +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 { specName, specVersion } = api.runtimeVersion + const genesisHash = await api.genesisHash + const cacheKey = blake2AsHex( + Uint8Array.from([ + ...specName.toU8a(), + ...specVersion.toU8a(), + ...genesisHash.toU8a(), + ]) + ) + if (metadataHashes.has(cacheKey)) { + return metadataHashes.get(cacheKey) as HexString + } + 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 metadata = await api.call.metadata.metadataAtVersion(15) + const merkleizedMetadata = merkleizeMetadata(metadata.toHex(), merkleInfo) + const metadataHash = u8aToHex(merkleizedMetadata.digest()) + metadataHashes.set(cacheKey, metadataHash) + return metadataHash +} + /** * Signs a SubmittableExtrinsic. * @@ -174,19 +210,32 @@ 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: 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 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 } + 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 +247,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 +255,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) } 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() }) 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