diff --git a/packages/core/src/blockchain/head-state.ts b/packages/core/src/blockchain/head-state.ts index 815f4c4c..fbfa232b 100644 --- a/packages/core/src/blockchain/head-state.ts +++ b/packages/core/src/blockchain/head-state.ts @@ -45,7 +45,7 @@ export class HeadState { delete this.#storageListeners[id] } - async subscrubeRuntimeVersion(cb: (block: Block) => void) { + async subscribeRuntimeVersion(cb: (block: Block) => void) { const id = randomId() const codeKey = stringToHex(':code') this.#storageListeners[id] = [[codeKey], cb] diff --git a/packages/core/src/rpc/substrate/grandpa.ts b/packages/core/src/rpc/substrate/grandpa.ts new file mode 100644 index 00000000..4cca7be0 --- /dev/null +++ b/packages/core/src/rpc/substrate/grandpa.ts @@ -0,0 +1,37 @@ +import { u8aToHex } from '@polkadot/util' + +import { Block } from '../../blockchain/block.js' +import { Handler } from '../shared.js' + +export const grandpa_subscribeJustifications: Handler = async (context, _params, { subscribe }) => { + let update = (_block: Block) => {} + + const id = context.chain.headState.subscribeHead((block) => update(block)) + const callback = subscribe('grandpa_justifications', id, () => context.chain.headState.unsubscribeHead(id)) + + update = async (block: Block) => { + const meta = await block.meta + const justification = meta.registry.createType('GrandpaJustification', { + round: 1, + commit: { + targetHash: block.hash, + targetNumber: block.number, + precommits: [], + }, + votesAncestries: [], + }) + callback(u8aToHex(justification.toU8a())) + } + + setTimeout(() => update(context.chain.head), 50) + + return id +} + +export const grandpa_unsubscribeJustifications: Handler<[string], void> = async ( + _context, + [subid], + { unsubscribe }, +) => { + unsubscribe(subid) +} diff --git a/packages/core/src/rpc/substrate/index.ts b/packages/core/src/rpc/substrate/index.ts index 4db882e6..d4c2b7e4 100644 --- a/packages/core/src/rpc/substrate/index.ts +++ b/packages/core/src/rpc/substrate/index.ts @@ -1,11 +1,13 @@ import * as AuthorRPC from './author.js' import * as ChainRPC from './chain.js' +import * as GrandpaRPC from './grandpa.js' import * as PaymentRPC from './payment.js' import * as StateRPC from './state.js' import * as SystemRPC from './system.js' export { AuthorRPC } export { ChainRPC } +export { GrandpaRPC } export { PaymentRPC } export { StateRPC } export { SystemRPC } @@ -13,6 +15,7 @@ export { SystemRPC } const handlers = { ...AuthorRPC, ...ChainRPC, + ...GrandpaRPC, ...PaymentRPC, ...StateRPC, ...SystemRPC, diff --git a/packages/core/src/rpc/substrate/state.ts b/packages/core/src/rpc/substrate/state.ts index 2f1ce230..8c380210 100644 --- a/packages/core/src/rpc/substrate/state.ts +++ b/packages/core/src/rpc/substrate/state.ts @@ -1,8 +1,9 @@ -import { Block } from '../../blockchain/block.js' import { HexString } from '@polkadot/util/types' +import _ from 'lodash' +import { Block } from '../../blockchain/block.js' import { Handler, ResponseError } from '../shared.js' -import { RuntimeVersion } from '../../wasm-executor/index.js' +import { RuntimeVersion, createProof } from '../../wasm-executor/index.js' import { defaultLogger } from '../../logger.js' import { isPrefixedChildKey, prefixedChildKey, stripChildPrefix } from '../../utils/index.js' @@ -56,6 +57,33 @@ export const state_getKeysPaged: Handler<[string, number, string, HexString], st return block?.getKeysPaged({ prefix, pageSize, startKey }) } +/** + * Get proof of the runtime storage value. + * NOTE: The retuned proof trie_root_hash does not match the block trie_root_hash. + * + * @param context + * @param params - [`keys`, `blockhash`] + * + * @return storage proof + */ +export const state_getReadProof: Handler< + [HexString[], HexString], + { at: HexString; proof: HexString[] } | null +> = async (context, [keys, hash]) => { + const block = await context.chain.getBlock(hash) + if (!block) { + return null + } + const entries = await Promise.all( + _.sortedUniq(keys).map(async (key) => [key, await block.get(key)] as [HexString, HexString | null]), + ) + const { nodes: proof } = await createProof([], entries) + return { + at: block.hash, + proof, + } +} + /** * @param context * @param params - [`keys`, `blockhash`] @@ -110,7 +138,7 @@ export const state_call: Handler<[HexString, HexString, HexString], HexString> = */ export const state_subscribeRuntimeVersion: Handler<[], string> = async (context, _params, { subscribe }) => { let update = (_block: Block) => {} - const id = await context.chain.headState.subscrubeRuntimeVersion((block) => update(block)) + const id = await context.chain.headState.subscribeRuntimeVersion((block) => update(block)) const callback = subscribe('state_runtimeVersion', id) update = async (block) => callback(await block.runtimeVersion) setTimeout(() => { diff --git a/packages/e2e/src/__snapshots__/grandpa.test.ts.snap b/packages/e2e/src/__snapshots__/grandpa.test.ts.snap new file mode 100644 index 00000000..d0339830 --- /dev/null +++ b/packages/e2e/src/__snapshots__/grandpa.test.ts.snap @@ -0,0 +1,21 @@ +// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html + +exports[`grandpa rpc > subscribeJustifications 1`] = ` +[ + [ + "0x0100000000000000b012d04c56b65cfa1f47cb1f884d920f95d0097b1ed42f5da18d5e2a436c2f3edcd2dc000000", + ], +] +`; + +exports[`grandpa rpc > subscribeJustifications 2`] = `"0x7ba5ba52f5b88fa5bcab5c1a0f7e158d845d93e1c70e9a5b42a7c3a9af681ee7"`; + +exports[`grandpa rpc > subscribeJustifications 3`] = ` +[ + [ + "0x01000000000000007ba5ba52f5b88fa5bcab5c1a0f7e158d845d93e1c70e9a5b42a7c3a9af681ee7ddd2dc000000", + ], +] +`; + +exports[`grandpa rpc > subscribeJustifications 4`] = `"0xaf0b8a9bb7abb768f0cac30c0bd31750eb4b8541c6012f65938e16415c3a024f"`; diff --git a/packages/e2e/src/__snapshots__/state.test.ts.snap b/packages/e2e/src/__snapshots__/state.test.ts.snap index f6ad187d..004d02a5 100644 --- a/packages/e2e/src/__snapshots__/state.test.ts.snap +++ b/packages/e2e/src/__snapshots__/state.test.ts.snap @@ -1,5 +1,14 @@ // Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html +exports[`state rpc > getReadProof 1`] = ` +{ + "at": "0x0df086f32a9c3399f7fa158d3d77a1790830bd309134c5853718141c969299c7", + "proof": [ + "0xa0f0c365c3cf59d671eb72da0e7a4113c41002505f0e7b9012096b41c4eb3aaf947f6ea429080000685f0f1f0515f462cdcf84e0f1d6045dfcbb20c6ad70bd88010000", + ], +} +`; + exports[`state rpc > getXXX 1`] = ` { "apis": [ diff --git a/packages/e2e/src/grandpa.test.ts b/packages/e2e/src/grandpa.test.ts new file mode 100644 index 00000000..0ec7804b --- /dev/null +++ b/packages/e2e/src/grandpa.test.ts @@ -0,0 +1,36 @@ +import { describe, expect, it } from 'vitest' + +import { api, delay, dev, mockCallback, setupApi } from './helper.js' + +setupApi({ + endpoint: ['wss://rpc.ibp.network/polkadot'], + blockHash: '0xb012d04c56b65cfa1f47cb1f884d920f95d0097b1ed42f5da18d5e2a436c2f3e', +}) + +describe('grandpa rpc', () => { + it('subscribeJustifications', async () => { + const { callback, next } = mockCallback() + const unsub = await api.rpc.grandpa.subscribeJustifications(callback) + + await next() + expect(callback.mock.calls).toMatchSnapshot() + + callback.mockClear() + + expect(await dev.newBlock()).toMatchSnapshot() + + await next() + + expect(callback.mock.calls).toMatchSnapshot() + + callback.mockClear() + + unsub() + + expect(await dev.newBlock()).toMatchSnapshot() + + await delay(100) + + expect(callback).not.toHaveBeenCalled() + }) +}) diff --git a/packages/e2e/src/state.test.ts b/packages/e2e/src/state.test.ts index e266cbc2..38b9e9fd 100644 --- a/packages/e2e/src/state.test.ts +++ b/packages/e2e/src/state.test.ts @@ -12,5 +12,17 @@ describe('state rpc', () => { expect(await api.rpc.state.getMetadata(genesisHash)).to.not.be.eq(await api.rpc.state.getMetadata()) }) + it('getReadProof', async () => { + expect( + await api.rpc.state.getReadProof( + [ + '0xf0c365c3cf59d671eb72da0e7a4113c44e7b9012096b41c4eb3aaf947f6ea429', + '0xf0c365c3cf59d671eb72da0e7a4113c49f1f0515f462cdcf84e0f1d6045dfcbb', + ], + env.acala.blockHash, + ), + ).toMatchSnapshot() + }) + it.todo('subscribeRuntimeVersion') })