Skip to content

Commit 6eb7def

Browse files
ntn-x2rflechtner
andauthored
feat: add metadata hash checks (#924)
* Add support for metadata hash check * chore: update lockfile * Use cached metadata * Address more comments * Factor out metadata root calculation * Use local cache * Add integration tests * Empty * Update comment * Update metadata fetching * chore: update comment typo Co-authored-by: Raphael Flechtner <[email protected]> --------- Co-authored-by: Raphael Flechtner <[email protected]> Co-authored-by: Raphael Flechtner <[email protected]>
1 parent feeaa7a commit 6eb7def

File tree

4 files changed

+143
-18
lines changed

4 files changed

+143
-18
lines changed

packages/chain-helpers/package.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,6 +57,7 @@
5757
"dependencies": {
5858
"@kiltprotocol/config": "workspace:*",
5959
"@kiltprotocol/types": "workspace:*",
60-
"@kiltprotocol/utils": "workspace:*"
60+
"@kiltprotocol/utils": "workspace:*",
61+
"@polkadot-api/merkleize-metadata": "^1.0.0"
6162
}
6263
}

packages/chain-helpers/src/blockchain/Blockchain.ts

Lines changed: 59 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,14 +7,20 @@
77

88
import { ApiPromise, SubmittableResult } from '@polkadot/api'
99
import type { TxWithEvent } from '@polkadot/api-derive/types'
10+
import type { SignerOptions } from '@polkadot/api-base/types'
1011
import type { Vec } from '@polkadot/types'
1112
import type { Call, Extrinsic } from '@polkadot/types/interfaces'
1213
import type { AnyNumber, IMethod } from '@polkadot/types/types'
13-
import type { BN } from '@polkadot/util'
14+
import { u8aToHex, type BN } from '@polkadot/util'
15+
import {
16+
type ExtraInfo,
17+
merkleizeMetadata,
18+
} from '@polkadot-api/merkleize-metadata'
1419

1520
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- doing this instead of import '@kiltprotocol/augment-api' to avoid creating an import at runtime
1621
import type * as _ from '@kiltprotocol/augment-api'
1722
import type {
23+
HexString,
1824
ISubmittableResult,
1925
KeyringPair,
2026
SubmittableExtrinsic,
@@ -23,7 +29,7 @@ import type {
2329
} from '@kiltprotocol/types'
2430
import { ConfigService } from '@kiltprotocol/config'
2531
import { SDKErrors, Signers } from '@kiltprotocol/utils'
26-
32+
import { blake2AsHex } from '@polkadot/util-crypto'
2733
import { ErrorHandler } from '../errorhandling/index.js'
2834
import { makeSubscriptionPromise } from './SubscriptionPromise.js'
2935

@@ -167,26 +173,69 @@ export async function submitSignedTx(
167173

168174
export const dispatchTx = submitSignedTx
169175

176+
const metadataHashes = new Map<string, HexString>()
177+
178+
// 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.
179+
async function getMetadataHash(api: ApiPromise): Promise<HexString> {
180+
const { specName, specVersion } = api.runtimeVersion
181+
const genesisHash = await api.genesisHash
182+
const cacheKey = blake2AsHex(
183+
Uint8Array.from([
184+
...specName.toU8a(),
185+
...specVersion.toU8a(),
186+
...genesisHash.toU8a(),
187+
])
188+
)
189+
if (metadataHashes.has(cacheKey)) {
190+
return metadataHashes.get(cacheKey) as HexString
191+
}
192+
const merkleInfo: ExtraInfo = {
193+
base58Prefix: api.consts.system.ss58Prefix.toNumber(),
194+
decimals: api.registry.chainDecimals[0],
195+
specName: specName.toString(),
196+
specVersion: specVersion.toNumber(),
197+
tokenSymbol: api.registry.chainTokens[0],
198+
}
199+
const metadata = await api.call.metadata.metadataAtVersion(15)
200+
const merkleizedMetadata = merkleizeMetadata(metadata.toHex(), merkleInfo)
201+
const metadataHash = u8aToHex(merkleizedMetadata.digest())
202+
metadataHashes.set(cacheKey, metadataHash)
203+
return metadataHash
204+
}
205+
170206
/**
171207
* Signs a SubmittableExtrinsic.
172208
*
173209
* @param tx An unsigned SubmittableExtrinsic.
174210
* @param signer The {@link KeyringPair} used to sign the tx.
175211
* @param opts Additional options.
176212
* @param opts.tip Optional amount of Femto-KILT to tip the validator.
213+
* @param opts.checkMetadata Boolean flag indicated whether to verify the metadata hash upon tx submission.
177214
* @returns A signed {@link SubmittableExtrinsic}.
178215
*/
179216
export async function signTx(
180217
tx: SubmittableExtrinsic,
181218
signer: KeyringPair | TransactionSigner,
182-
{ tip }: { tip?: AnyNumber } = {}
219+
{ tip, checkMetadata }: { tip?: AnyNumber; checkMetadata?: boolean } = {}
183220
): Promise<SubmittableExtrinsic> {
221+
const signOptions: Partial<SignerOptions> = checkMetadata
222+
? {
223+
tip,
224+
// Required as described in https://github.com/polkadot-js/api/blob/109d3b2201ea51f27180e34dfd883ec71d402f6b/packages/api-base/src/types/submittable.ts#L79.
225+
metadataHash: await getMetadataHash(ConfigService.get('api')),
226+
// Used by external signers to to know there's additional data to be included in the payload (see link above).
227+
withSignedTransaction: true,
228+
// 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.
229+
mode: 1,
230+
}
231+
: { tip }
232+
184233
if ('address' in signer) {
185-
return tx.signAsync(signer, { tip })
234+
return tx.signAsync(signer, signOptions)
186235
}
187236

188237
return tx.signAsync(signer.id, {
189-
tip,
238+
...signOptions,
190239
signer: Signers.getPolkadotSigner([signer]),
191240
})
192241
}
@@ -198,17 +247,20 @@ export async function signTx(
198247
* @param signer The {@link KeyringPair} used to sign the tx.
199248
* @param opts Partial optional criteria for resolving/rejecting the promise.
200249
* @param opts.tip Optional amount of Femto-KILT to tip the validator.
250+
* @param opts.checkMetadata Boolean flag indicated whether to verify the metadata hash upon tx submission.
201251
* @returns Promise result of executing the extrinsic, of type ISubmittableResult.
202252
*/
203253
export async function signAndSubmitTx(
204254
tx: SubmittableExtrinsic,
205255
signer: KeyringPair | TransactionSigner,
206256
{
207257
tip,
258+
checkMetadata,
208259
...opts
209-
}: Partial<SubscriptionPromise.Options> & Partial<{ tip: AnyNumber }> = {}
260+
}: Partial<SubscriptionPromise.Options> &
261+
Partial<{ tip: AnyNumber; checkMetadata: boolean }> = {}
210262
): Promise<ISubmittableResult> {
211-
const signedTx = await signTx(tx, signer, { tip })
263+
const signedTx = await signTx(tx, signer, { tip, checkMetadata })
212264
return submitSignedTx(signedTx, opts)
213265
}
214266

tests/integration/Blockchain.spec.ts

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ import {
1616
import type { KeyringPair } from '@kiltprotocol/types'
1717

1818
import { makeSigningKeyTool } from '../testUtils/index.js'
19-
import { devCharlie, devFaucet, initializeApi, submitTx } from './utils.js'
19+
import {
20+
devAlice,
21+
devCharlie,
22+
devFaucet,
23+
initializeApi,
24+
submitTx,
25+
} from './utils.js'
2026

2127
let api: ApiPromise
2228
beforeAll(async () => {
@@ -153,6 +159,24 @@ describe('Chain returns specific errors, that we check for', () => {
153159
}, 40000)
154160
})
155161

162+
describe('The added `SignedExtension`s are valid', () => {
163+
it(`'CheckMetadataHash' works`, async () => {
164+
const systemRemarkTx = api.tx.system.remark('Test remark')
165+
const submitPromise = Blockchain.signAndSubmitTx(systemRemarkTx, devAlice, {
166+
checkMetadata: true,
167+
})
168+
await expect(submitPromise).resolves.not.toThrow()
169+
})
170+
171+
it(`No 'CheckMetadataHash' works`, async () => {
172+
const systemRemarkTx = api.tx.system.remark('Test remark')
173+
const submitPromise = Blockchain.signAndSubmitTx(systemRemarkTx, devAlice, {
174+
checkMetadata: false,
175+
})
176+
await expect(submitPromise).resolves.not.toThrow()
177+
})
178+
})
179+
156180
afterAll(async () => {
157181
if (typeof api !== 'undefined') await disconnect()
158182
})

yarn.lock

Lines changed: 57 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2069,6 +2069,7 @@ __metadata:
20692069
"@kiltprotocol/config": "workspace:*"
20702070
"@kiltprotocol/types": "workspace:*"
20712071
"@kiltprotocol/utils": "workspace:*"
2072+
"@polkadot-api/merkleize-metadata": "npm:^1.0.0"
20722073
rimraf: "npm:^3.0.2"
20732074
typescript: "npm:^4.8.3"
20742075
peerDependencies:
@@ -2350,13 +2351,20 @@ __metadata:
23502351
languageName: node
23512352
linkType: hard
23522353

2353-
"@noble/hashes@npm:1.4.0, @noble/hashes@npm:^1.3.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.3":
2354+
"@noble/hashes@npm:1.4.0":
23542355
version: 1.4.0
23552356
resolution: "@noble/hashes@npm:1.4.0"
23562357
checksum: 10c0/8c3f005ee72e7b8f9cff756dfae1241485187254e3f743873e22073d63906863df5d4f13d441b7530ea614b7a093f0d889309f28b59850f33b66cb26a779a4a5
23572358
languageName: node
23582359
linkType: hard
23592360

2361+
"@noble/hashes@npm:^1.3.0, @noble/hashes@npm:^1.3.1, @noble/hashes@npm:^1.3.3, @noble/hashes@npm:^1.6.1":
2362+
version: 1.7.0
2363+
resolution: "@noble/hashes@npm:1.7.0"
2364+
checksum: 10c0/1ef0c985ebdb5a1bd921ea6d959c90ba826af3ae05b40b459a703e2a5e9b259f190c6e92d6220fb3800e2385521e4159e238415ad3f6b79c52f91dd615e491dc
2365+
languageName: node
2366+
linkType: hard
2367+
23602368
"@nodelib/fs.scandir@npm:2.1.5":
23612369
version: 2.1.5
23622370
resolution: "@nodelib/fs.scandir@npm:2.1.5"
@@ -2430,6 +2438,17 @@ __metadata:
24302438
languageName: node
24312439
linkType: hard
24322440

2441+
"@polkadot-api/merkleize-metadata@npm:^1.0.0":
2442+
version: 1.1.12
2443+
resolution: "@polkadot-api/merkleize-metadata@npm:1.1.12"
2444+
dependencies:
2445+
"@polkadot-api/metadata-builders": "npm:0.10.0"
2446+
"@polkadot-api/substrate-bindings": "npm:0.11.0"
2447+
"@polkadot-api/utils": "npm:0.1.2"
2448+
checksum: 10c0/d3fac5be41878dd8c318dd76c9f09e3a6e769f8791386bc4c5a5c27c272efa0c4d3cf839034dfc2b28324c0a35b9bf5fd9df2e33392cff7760f8f5b40f41fe20
2449+
languageName: node
2450+
linkType: hard
2451+
24332452
"@polkadot-api/metadata-builders@npm:0.0.1":
24342453
version: 0.0.1
24352454
resolution: "@polkadot-api/metadata-builders@npm:0.0.1"
@@ -2440,6 +2459,16 @@ __metadata:
24402459
languageName: node
24412460
linkType: hard
24422461

2462+
"@polkadot-api/metadata-builders@npm:0.10.0":
2463+
version: 0.10.0
2464+
resolution: "@polkadot-api/metadata-builders@npm:0.10.0"
2465+
dependencies:
2466+
"@polkadot-api/substrate-bindings": "npm:0.11.0"
2467+
"@polkadot-api/utils": "npm:0.1.2"
2468+
checksum: 10c0/0fb49a6cd4e2b66e3c3983f66e427b5763da0b67d5c4847c190e6e546f67bc4908d456b2afe80ce85316736d3aa408d779f309b292957648820aca44e6578719
2469+
languageName: node
2470+
linkType: hard
2471+
24432472
"@polkadot-api/observable-client@npm:0.1.0":
24442473
version: 0.1.0
24452474
resolution: "@polkadot-api/observable-client@npm:0.1.0"
@@ -2466,6 +2495,18 @@ __metadata:
24662495
languageName: node
24672496
linkType: hard
24682497

2498+
"@polkadot-api/substrate-bindings@npm:0.11.0":
2499+
version: 0.11.0
2500+
resolution: "@polkadot-api/substrate-bindings@npm:0.11.0"
2501+
dependencies:
2502+
"@noble/hashes": "npm:^1.6.1"
2503+
"@polkadot-api/utils": "npm:0.1.2"
2504+
"@scure/base": "npm:^1.2.1"
2505+
scale-ts: "npm:^1.6.1"
2506+
checksum: 10c0/8e0ea627a036b2bfd34adba06bb535d5ec473b118c53c2de88e48f245907decebbebd701b27f62d351509c6d28c88630160c1a4110ef5a61b0ca53088e94864f
2507+
languageName: node
2508+
linkType: hard
2509+
24692510
"@polkadot-api/substrate-client@npm:0.0.1":
24702511
version: 0.0.1
24712512
resolution: "@polkadot-api/substrate-client@npm:0.0.1"
@@ -2480,6 +2521,13 @@ __metadata:
24802521
languageName: node
24812522
linkType: hard
24822523

2524+
"@polkadot-api/utils@npm:0.1.2":
2525+
version: 0.1.2
2526+
resolution: "@polkadot-api/utils@npm:0.1.2"
2527+
checksum: 10c0/530270141ab7a8d114aff68adabbc643a7b7f5abcfb974a5dac5044e1f5a459881f427e357a7eadfecf55847da5e48828be6dbcf502dd22e097c87546762a036
2528+
languageName: node
2529+
linkType: hard
2530+
24832531
"@polkadot/api-augment@npm:12.2.1":
24842532
version: 12.2.1
24852533
resolution: "@polkadot/api-augment@npm:12.2.1"
@@ -2919,10 +2967,10 @@ __metadata:
29192967
languageName: node
29202968
linkType: hard
29212969

2922-
"@scure/base@npm:^1.1.0, @scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5":
2923-
version: 1.1.7
2924-
resolution: "@scure/base@npm:1.1.7"
2925-
checksum: 10c0/2d06aaf39e6de4b9640eb40d2e5419176ebfe911597856dcbf3bc6209277ddb83f4b4b02cb1fd1208f819654268ec083da68111d3530bbde07bae913e2fc2e5d
2970+
"@scure/base@npm:^1.1.0, @scure/base@npm:^1.1.1, @scure/base@npm:^1.1.5, @scure/base@npm:^1.2.1":
2971+
version: 1.2.1
2972+
resolution: "@scure/base@npm:1.2.1"
2973+
checksum: 10c0/e61068854370855b89c50c28fa4092ea6780f1e0db64ea94075ab574ebcc964f719a3120dc708db324991f4b3e652d92ebda03fce2bf6a4900ceeacf9c0ff933
29262974
languageName: node
29272975
linkType: hard
29282976

@@ -8820,10 +8868,10 @@ __metadata:
88208868
languageName: node
88218869
linkType: hard
88228870

8823-
"scale-ts@npm:^1.6.0":
8824-
version: 1.6.0
8825-
resolution: "scale-ts@npm:1.6.0"
8826-
checksum: 10c0/ce4ea3559c6b6bdf2a62454aac83cc3151ae93d1a507ddb8e95e83ce1190085aed61c46901bd42d41d8f8ba58279d7e37057c68c2b674c2d39b8cf5d169e90dd
8871+
"scale-ts@npm:^1.6.0, scale-ts@npm:^1.6.1":
8872+
version: 1.6.1
8873+
resolution: "scale-ts@npm:1.6.1"
8874+
checksum: 10c0/bbcf476029095152189c5bd210922b43342e8bfb712bf56237de172d55b528e090419e80da67c627a8f706a228237346b82de527755d7f197bb4d822c6383dfd
88278875
languageName: node
88288876
linkType: hard
88298877

0 commit comments

Comments
 (0)