Skip to content

feat: add metadata hash checks #924

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 11 commits into from
Jan 13, 2025
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion packages/chain-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
"dependencies": {
"@kiltprotocol/config": "workspace:*",
"@kiltprotocol/types": "workspace:*",
"@kiltprotocol/utils": "workspace:*"
"@kiltprotocol/utils": "workspace:*",
"@polkadot-api/merkleize-metadata": "^1.0.0"
}
}
66 changes: 59 additions & 7 deletions packages/chain-helpers/src/blockchain/Blockchain.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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'

Expand Down Expand Up @@ -167,26 +173,69 @@ export async function submitSignedTx(

export const dispatchTx = submitSignedTx

const metadataHashes = new Map<string, HexString>()

// 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<HexString> {
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.
*
* @param tx An unsigned SubmittableExtrinsic.
* @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<SubmittableExtrinsic> {
const signOptions: Partial<SignerOptions> = 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 }

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]),
})
}
Expand All @@ -198,17 +247,20 @@ 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(
tx: SubmittableExtrinsic,
signer: KeyringPair | TransactionSigner,
{
tip,
checkMetadata,
...opts
}: Partial<SubscriptionPromise.Options> & Partial<{ tip: AnyNumber }> = {}
}: Partial<SubscriptionPromise.Options> &
Partial<{ tip: AnyNumber; checkMetadata: boolean }> = {}
): Promise<ISubmittableResult> {
const signedTx = await signTx(tx, signer, { tip })
const signedTx = await signTx(tx, signer, { tip, checkMetadata })
return submitSignedTx(signedTx, opts)
}

Expand Down
26 changes: 25 additions & 1 deletion tests/integration/Blockchain.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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()
})
66 changes: 57 additions & 9 deletions yarn.lock
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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"
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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

Expand Down Expand Up @@ -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

Expand Down
Loading