Skip to content
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

run block rpc #476

Merged
merged 7 commits into from
Oct 29, 2023
Merged
Show file tree
Hide file tree
Changes from 3 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
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ export const dryRunPreimage = async (argv: Config) => {
taskHandler(block),
)

if (result.Error) {
if ('Error' in result) {
throw new Error(result.Error)
}

Expand Down
242 changes: 237 additions & 5 deletions packages/chopsticks/src/plugins/run-block/index.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
import type yargs from 'yargs'

import { GenericExtrinsic } from '@polkadot/types'
import { Header } from '@polkadot/types/interfaces'
import { HexString } from '@polkadot/util/types'
import { defaultOptions, mockOptions } from '../../cli-options'
import { writeFileSync } from 'node:fs'
import { z } from 'zod'
import _ from 'lodash'
import type yargs from 'yargs'

import { Block, Context, decodeKeyValue, runTask, taskHandler } from '@acala-network/chopsticks-core'

import { Config } from '../../schema'
import { defaultLogger } from '../../logger'
import { defaultOptions, mockOptions } from '../../cli-options'
import { generateHtmlDiffPreviewFile } from '../../utils/generate-html-diff'
import { openHtml } from '../../utils/open-html'
import { runTask, taskHandler } from '@acala-network/chopsticks-core'
import { setupContext } from '../../context'

export const cli = (y: yargs.Argv) => {
Expand Down Expand Up @@ -58,7 +62,7 @@ export const cli = (y: yargs.Argv) => {
taskHandler(parent),
)

if (result.Error) {
if ('Error' in result) {
throw new Error(result.Error)
}

Expand All @@ -82,3 +86,231 @@ export const cli = (y: yargs.Argv) => {
},
)
}

const zHex = z.custom<HexString>((val: any) => /^0x\w+$/.test(val))
const zHash = z.string().length(66).and(zHex)

const schema = z.object({
includeRaw: z.boolean().optional(),
includeParsed: z.boolean().optional(),
includeBlockDetails: z.boolean().optional(),
head: zHash.optional(),
block: z.object({
header: z.any(),
extrinsics: z.array(zHex),
}),
})

type Params = z.infer<typeof schema>

export interface RunBlockParams {
/**
* Include raw storage diff. Default to true
*/
includeRaw: Params['includeRaw']
/**
* Include parsed storage diff in json format
*/
includeParsed: Params['includeParsed']
/**
* Include block details such as parsed extrinsics in json format
*/
includeBlockDetails: Params['includeBlockDetails']
/**
* Set chain head before running the extrinsics
*/
head: Params['head']
/**
* Block to run
*/
block: Params['block']
}

/**
* The phase of an execution.
* `number` means the phase is ApplyExtrinsic and the value is the extrinsic index.
*/
export type Phase = 'Initialization' | 'Finalization' | number // extrinsic index

export interface RunBlockResponse {
/**
* The storage diff of each phase.
*/
phases: {
/**
* The phase of the execution. See {@link Phase}.
*/
phase: Phase
/**
* Parsed storage diff. Only available when `includeParsed` is true.
*/
parsed?: Record<string, Record<string, any>>
/**
* Raw storage diff. Only available when `includeRaw` is true.
*/
raw?: [HexString, HexString | null][]
/**
* Runtime logs.
*/
logs?: string[]
}[]
/**
* Block details. Only available when `includeBlockDetails` is true.
*/
blockDetails?: {
/**
* Block timestamp in ms
*/
timestamp?: string
/**
* Parsed events in this block.
*/
events?: { phase: Phase; section: string; method: string; args: any[] }[]
/**
* Parsed extrinsics in this block.
*/
extrinsics: {
section: string
method: string
args: any[]
success: boolean
}[]
}
}

export const name = 'runBlock'

/**
* Run a set of extrinsics on top of a block and get the storage diff
* and optionally the parsed storage diff and block details.
* NOTE: The extrinsics should include inherents or tranasctions may have unexpected results.
*
* This function is a dev rpc handler. Use `dev_runBlock` as the method name when calling it.
*/
export const rpc = async ({ chain }: Context, [params]: [RunBlockParams]): Promise<RunBlockResponse> => {
const { includeRaw, includeParsed, includeBlockDetails, head, block } = schema.parse(params)

const includeRawStorage = includeRaw ?? true

if (head) {
const headBlock = await chain.getBlock(head)
if (!headBlock) {
throw Error(`Invalid block hash ${head}`)
}
await chain.setHead(headBlock)
}
xlc marked this conversation as resolved.
Show resolved Hide resolved

const registry = await chain.head.registry
const header = registry.createType<Header>('Header', block.header)

const wasm = await chain.head.wasm

const blockNumber = chain.head.number + 1
const hash: HexString = `0x${Math.round(Math.random() * 100000000)
.toString(16)
.padEnd(64, '0')}`

const newBlock = new Block(chain, blockNumber, hash, chain.head, {
header,
extrinsics: [],
storage: chain.head.storage,
})

const resp = {
phases: [],
} as RunBlockResponse

const run = async (fn: string, args: HexString[]) => {
const result = await runTask(
{
wasm,
calls: [[fn, args]],
mockSignatureHost: false,
allowUnresolvedImports: false,
runtimeLogLevel: 5,
},
taskHandler(newBlock),
)

if ('Error' in result) {
throw new Error(result.Error)
}

const resp = {} as any
const raw = result.Call.storageDiff

newBlock.pushStorageLayer().setAll(raw)

if (includeRawStorage) {
resp.raw = raw
}

if (includeParsed) {
const meta = await newBlock.meta
const parsed = {}
for (const [key, value] of raw) {
_.merge(parsed, decodeKeyValue(meta, newBlock, key, value, false))
}

// clear events because it can be stupidly large and redudant
if (parsed['system']?.['events']) {
delete parsed['system']['events']
}

resp.parsed = parsed
}

resp.logs = result.Call.runtimeLogs

return resp
}

const resInit = await run('Core_initialize_block', [header.toHex()])
resp.phases.push({ phase: 'Initialization', ...resInit })

for (const extrinsic of block.extrinsics) {
const res = await run('BlockBuilder_apply_extrinsic', [extrinsic])
resp.phases.push({ phase: resp.phases.length - 1, ...res })
}

const resFinalize = await run('BlockBuilder_finalize_block', [])
resp.phases.push({ phase: 'Finalization', ...resFinalize })

if (includeBlockDetails) {
const meta = await newBlock.meta
const registry = await newBlock.registry
const timestamp = await newBlock.read('u64', meta.query.timestamp.now)
const events = await newBlock.read('Vec<EventRecord>', meta.query.system.events)
const parsedEvents = events?.map((event) => ({
phase: event.phase.isApplyExtrinsic ? event.phase.asApplyExtrinsic.toNumber() : (event.phase.toString() as Phase),
section: event.event.section,
method: event.event.method,
args: event.event.data.map((arg) => arg.toJSON()),
}))
const extrinsics = block.extrinsics.map((extrinsic, idx) => {
const parsed = registry.createType<GenericExtrinsic>('GenericExtrinsic', extrinsic)
const resultEvent = events?.find(
({ event, phase }) =>
event.section === 'system' &&
(event.method === 'ExtrinsicSuccess' || event.method === 'ExtrinsicFailed') &&
phase.isApplyExtrinsic &&
phase.asApplyExtrinsic.eq(idx),
)

return {
section: parsed.method.section,
method: parsed.method.method,
args: parsed.method.args.map((arg) => arg.toJSON()),
success: resultEvent?.event.method === 'ExtrinsicSuccess',
}
})

resp.blockDetails = {
timestamp: timestamp?.toString(),
events: parsedEvents,
extrinsics,
}
}

return resp
}
14 changes: 13 additions & 1 deletion packages/core/src/blockchain/block.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { ChainProperties, Header } from '@polkadot/types/interfaces'
import { DecoratedMeta } from '@polkadot/types/metadata/decorate/types'
import { Metadata, TypeRegistry } from '@polkadot/types'
import { StorageEntry } from '@polkadot/types/primitive/types'
import { expandMetadata } from '@polkadot/types/metadata'
import { getSpecExtensions, getSpecHasher, getSpecTypes } from '@polkadot/types-known/util'
import { hexToU8a, objectSpread, stringToHex } from '@polkadot/util'
Expand Down Expand Up @@ -164,6 +165,17 @@ export class Block {
}
}

async read<T extends string>(type: T, query: StorageEntry, ...args: any[]) {
const key = compactHex(query(...args))
const value = await this.get(key)
if (!value) {
return undefined
}

const registry = await this.registry
return registry.createType(type, value)
xlc marked this conversation as resolved.
Show resolved Hide resolved
}

/**
* Get paged storage keys.
*/
Expand Down Expand Up @@ -309,7 +321,7 @@ export class Block {
},
taskHandler(this),
)
if (response.Call) {
if ('Call' in response) {
for (const log of response.Call.runtimeLogs) {
defaultLogger.info(`RuntimeLogs:\n${log}`)
}
Expand Down
10 changes: 8 additions & 2 deletions packages/core/src/utils/decoder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,13 @@ export const decodeKey = (
return {}
}

export const decodeKeyValue = (meta: DecoratedMeta, block: Block, key: HexString, value?: HexString | null) => {
export const decodeKeyValue = (
meta: DecoratedMeta,
block: Block,
key: HexString,
value?: HexString | null,
toHuman = true,
) => {
const { storage, decodedKey } = decodeKey(meta, block, key)

if (!storage || !decodedKey) {
Expand All @@ -60,7 +66,7 @@ export const decodeKeyValue = (meta: DecoratedMeta, block: Block, key: HexString
if (storage.section === 'substrate' && storage.method === 'code') {
return `:code blake2_256 ${blake2AsHex(value, 256)} (${hexToU8a(value).length} bytes)`
}
return meta.registry.createType(decodedKey.outputType, hexToU8a(value)).toHuman()
return meta.registry.createType(decodedKey.outputType, hexToU8a(value))[toHuman ? 'toHuman' : 'toJSON']()
}

switch (decodedKey.args.length) {
Expand Down
10 changes: 6 additions & 4 deletions packages/core/src/utils/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { HexString } from '@polkadot/util/types'
import { StorageKey } from '@polkadot/types'
import { compactStripLength, hexToU8a, u8aToHex } from '@polkadot/util'
import { compactStripLength, u8aToHex } from '@polkadot/util'
import { hexAddPrefix, hexStripPrefix } from '@polkadot/util/hex'

import { Blockchain } from '../blockchain'
Expand Down Expand Up @@ -46,9 +46,11 @@ export const compactHex = (value: Uint8Array): HexString => {

export const getParaId = async (chain: Blockchain) => {
const meta = await chain.head.meta
const raw = await chain.head.get(compactHex(meta.query.parachainInfo.parachainId()))
if (!raw) throw new Error('Cannot find parachain id')
return meta.registry.createType('u32', hexToU8a(raw))
const id = await chain.head.read('u32', meta.query.parachainInfo.parachainId)
if (!id) {
throw new Error('Cannot find parachain id')
}
return id
}

export const isUrl = (url: string) => {
Expand Down
4 changes: 2 additions & 2 deletions packages/core/src/utils/time-travel.ts
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,8 @@ export const getCurrentSlot = async (chain: Blockchain) => {

export const getCurrentTimestamp = async (chain: Blockchain) => {
const meta = await chain.head.meta
const currentTimestampRaw = (await chain.head.get(compactHex(meta.query.timestamp.now()))) || '0x'
return meta.registry.createType('u64', hexToU8a(currentTimestampRaw)).toNumber()
const timestamp = await chain.head.read('u64', meta.query.timestamp.now)
return timestamp?.toNumber() ?? 0
}

export const getSlotDuration = async (chain: Blockchain) => {
Expand Down
Loading
Loading