From 182de1b9fcc9cc0879e952ab6d1bc62941582193 Mon Sep 17 00:00:00 2001 From: Ermal Kaleci Date: Wed, 15 Feb 2023 15:47:08 +0100 Subject: [PATCH] handle apply extrinsic error (#191) * handle apply extrinsic error * update event * fix response * update type * update logging * update test --- e2e/author.test.ts | 33 +++++++++++++++++++++++----- e2e/helper.ts | 2 +- e2e/mock-signature.test.ts | 2 +- src/blockchain/block-builder.ts | 39 +++++++++++++++++++++++++++------ src/blockchain/index.ts | 6 ++--- src/blockchain/txpool.ts | 9 +++++++- src/rpc/substrate/author.ts | 34 ++++++++++++++++++++++------ 7 files changed, 100 insertions(+), 25 deletions(-) diff --git a/e2e/author.test.ts b/e2e/author.test.ts index c9d90124..b2e58de5 100644 --- a/e2e/author.test.ts +++ b/e2e/author.test.ts @@ -1,13 +1,14 @@ +import { SubmittableResult } from '@polkadot/api' import { describe, expect, it } from 'vitest' -import { api, dev, env, expectJson, mockCallback, setupApi, testingPairs } from './helper' +import { api, defer, dev, env, expectJson, mockCallback, setupApi, testingPairs } from './helper' setupApi(env.mandala) describe('author rpc', () => { - it('works', async () => { - const { alice, bob } = testingPairs() + const { alice, bob } = testingPairs() + it('works', async () => { { const { callback, next } = mockCallback() await api.tx.balances.transfer(bob.address, 100).signAndSend(alice, callback) @@ -55,7 +56,6 @@ describe('author rpc', () => { }) it('reject invalid signature', async () => { - const { alice, bob } = testingPairs() const { nonce } = await api.query.system.account(alice.address) const tx = api.tx.balances.transfer(bob.address, 100) @@ -66,6 +66,29 @@ describe('author rpc', () => { blockHash: api.genesisHash, }) - await expect(tx.send()).rejects.toThrow('Extrinsic is invalid') + await expect(tx.send()).rejects.toThrow('1010: {"invalid":{"badProof":null}}') + }) + + it('failed apply extirinsic', async () => { + const finalized = defer() + const invalid = defer() + + const onStatusUpdate = (result: SubmittableResult) => { + if (result.status.isInvalid) { + invalid.resolve(result.status.toString()) + } + if (result.status.isFinalized) { + finalized.resolve(null) + } + } + + const { nonce } = await api.query.system.account(alice.address) + await api.tx.balances.transfer(bob.address, 100).signAndSend(alice, { nonce }, onStatusUpdate) + await api.tx.balances.transfer(bob.address, 200).signAndSend(alice, { nonce }, onStatusUpdate) + + await dev.newBlock() + + await finalized.promise + expect(await invalid.promise).toBe('Invalid') }) }) diff --git a/e2e/helper.ts b/e2e/helper.ts index 06c7eeb6..1d4955f7 100644 --- a/e2e/helper.ts +++ b/e2e/helper.ts @@ -167,7 +167,7 @@ export const dev = { }, } -function defer() { +export function defer() { const deferred = {} as { resolve: (value: any) => void; reject: (reason: any) => void; promise: Promise } deferred.promise = new Promise((resolve, reject) => { deferred.resolve = resolve diff --git a/e2e/mock-signature.test.ts b/e2e/mock-signature.test.ts index ff1243a8..7f85d13b 100644 --- a/e2e/mock-signature.test.ts +++ b/e2e/mock-signature.test.ts @@ -30,7 +30,7 @@ describe('mock signature', () => { blockHash: api.genesisHash, }) - await expect(tx.send()).rejects.toThrow('Extrinsic is invalid') + await expect(tx.send()).rejects.toThrow('1010: {"invalid":{"badProof":null}}') }) it('accept mock signature', async () => { diff --git a/src/blockchain/block-builder.ts b/src/blockchain/block-builder.ts index 5356398e..9ab1e421 100644 --- a/src/blockchain/block-builder.ts +++ b/src/blockchain/block-builder.ts @@ -1,8 +1,16 @@ -import { AccountInfo, Call, Header, RawBabePreDigest } from '@polkadot/types/interfaces' +import { + AccountInfo, + ApplyExtrinsicResult, + Call, + Header, + RawBabePreDigest, + TransactionValidityError, +} from '@polkadot/types/interfaces' import { Block, TaskCallResponse } from './block' import { GenericExtrinsic } from '@polkadot/types' import { HexString } from '@polkadot/util/types' import { StorageValueKind } from './storage-layer' +import { blake2AsHex } from '@polkadot/util-crypto' import { compactAddLength, hexToU8a, stringToHex } from '@polkadot/util' import { compactHex } from '../utils' import { defaultLogger, truncate } from '../logger' @@ -122,7 +130,8 @@ const initNewBlock = async (head: Block, header: Header, inherents: HexString[]) export const buildBlock = async ( head: Block, inherents: HexString[], - extrinsics: HexString[] + extrinsics: HexString[], + onApplyExtrinsicError: (extrinsic: HexString, error: TransactionValidityError) => void ): Promise<[Block, HexString[]]> => { const registry = await head.registry const header = await newHeader(head) @@ -134,17 +143,28 @@ export const buildBlock = async ( extrinsicsCount: extrinsics.length, tempHash: newBlock.hash, }, - `Building block #${newBlock.number.toLocaleString()}` + `Try building block #${newBlock.number.toLocaleString()}` ) const pendingExtrinsics: HexString[] = [] + const includedExtrinsic: HexString[] = [] // apply extrinsics for (const extrinsic of extrinsics) { try { - const { storageDiff } = await newBlock.call('BlockBuilder_apply_extrinsic', [extrinsic]) + const { result, storageDiff } = await newBlock.call('BlockBuilder_apply_extrinsic', [extrinsic]) + const outcome = registry.createType('ApplyExtrinsicResult', result) + if (outcome.isErr) { + if (outcome.asErr.isInvalid && outcome.asErr.asInvalid.isFuture) { + pendingExtrinsics.push(extrinsic) + } else { + onApplyExtrinsicError(extrinsic, outcome.asErr) + } + continue + } newBlock.pushStorageLayer().setAll(storageDiff) logger.trace(truncate(storageDiff), 'Applied extrinsic') + includedExtrinsic.push(extrinsic) } catch (e) { logger.info('Failed to apply extrinsic %o %s', e, e) pendingExtrinsics.push(extrinsic) @@ -161,7 +181,7 @@ export const buildBlock = async ( const blockData = registry.createType('Block', { header, - extrinsics, + extrinsics: includedExtrinsic, }) const storageDiff = await newBlock.storageDiff() @@ -171,13 +191,18 @@ export const buildBlock = async ( ) const finalBlock = new Block(head.chain, newBlock.number, blockData.hash.toHex(), head, { header, - extrinsics: [...inherents, ...extrinsics], + extrinsics: [...inherents, ...includedExtrinsic], storage: head.storage, storageDiff, }) logger.info( - { hash: finalBlock.hash, number: newBlock.number }, + { + hash: finalBlock.hash, + extrinsics: includedExtrinsic.map((x) => blake2AsHex(x, 256)), + pendingExtrinsics: pendingExtrinsics.length, + number: newBlock.number, + }, `Block built #${newBlock.number.toLocaleString()} hash ${finalBlock.hash}` ) diff --git a/src/blockchain/index.ts b/src/blockchain/index.ts index 25b89025..061fc006 100644 --- a/src/blockchain/index.ts +++ b/src/blockchain/index.ts @@ -79,8 +79,8 @@ export class Blockchain { return this.#head } - get pendingExtrinsics(): HexString[] { - return this.#txpool.pendingExtrinsics + get txPool() { + return this.#txpool } async getBlockAt(number?: number): Promise { @@ -158,7 +158,7 @@ export class Blockchain { this.#txpool.submitExtrinsic(extrinsic) return blake2AsHex(extrinsic, 256) } - throw new Error(`Extrinsic is invalid: ${validity.asErr.toString()}`) + throw validity.asErr } async newBlock(params?: BuildBlockParams): Promise { diff --git a/src/blockchain/txpool.ts b/src/blockchain/txpool.ts index b475a9c4..7fc25098 100644 --- a/src/blockchain/txpool.ts +++ b/src/blockchain/txpool.ts @@ -1,4 +1,5 @@ import { BehaviorSubject, firstValueFrom } from 'rxjs' +import { EventEmitter } from 'node:stream' import { HexString } from '@polkadot/util/types' import { skip, take } from 'rxjs/operators' import _ from 'lodash' @@ -8,6 +9,8 @@ import { Blockchain } from '.' import { InherentProvider } from './inherent' import { buildBlock } from './block-builder' +export const APPLY_EXTRINSIC_ERROR = 'TxPool::ApplyExtrinsicError' + export enum BuildBlockMode { Batch, // one block per batch, default Instant, // one block per tx @@ -37,6 +40,8 @@ export class TxPool { readonly #mode: BuildBlockMode readonly #inherentProvider: InherentProvider + readonly event = new EventEmitter() + #last: BehaviorSubject #lastBuildBlockPromise: Promise = Promise.resolve() @@ -87,7 +92,9 @@ export class TxPool { const head = this.#chain.head const extrinsics = this.#pool.splice(0) const inherents = await this.#inherentProvider.createInherents(head, params?.inherent) - const [newBlock, pendingExtrinsics] = await buildBlock(head, inherents, extrinsics) + const [newBlock, pendingExtrinsics] = await buildBlock(head, inherents, extrinsics, (extrinsic, error) => { + this.event.emit(APPLY_EXTRINSIC_ERROR, [extrinsic, error]) + }) this.#pool.push(...pendingExtrinsics) await this.#chain.setHead(newBlock) } diff --git a/src/rpc/substrate/author.ts b/src/rpc/substrate/author.ts index 88191b2d..c72b95c4 100644 --- a/src/rpc/substrate/author.ts +++ b/src/rpc/substrate/author.ts @@ -1,13 +1,16 @@ +import { APPLY_EXTRINSIC_ERROR } from '../../blockchain/txpool' import { Block } from '../../blockchain/block' import { Handlers, ResponseError } from '../../rpc/shared' +import { TransactionValidityError } from '@polkadot/types/interfaces' import { defaultLogger } from '../../logger' const logger = defaultLogger.child({ name: 'rpc-author' }) const handlers: Handlers = { author_submitExtrinsic: async (context, [extrinsic]) => { - return context.chain.submitExtrinsic(extrinsic).catch((error) => { - throw new ResponseError(1, error.toString()) + return context.chain.submitExtrinsic(extrinsic).catch((error: TransactionValidityError) => { + const code = error.isInvalid ? 1010 : 1011 + throw new ResponseError(code, error.toString()) }) }, author_submitAndWatchExtrinsic: async (context, [extrinsic], { subscribe, unsubscribe }) => { @@ -16,7 +19,24 @@ const handlers: Handlers = { const id = context.chain.headState.subscribeHead((block) => update(block)) const callback = subscribe('author_extrinsicUpdate', id, () => context.chain.headState.unsubscribeHead(id)) + const onExtrinsicFail = ([failedExtrinsic, error]: [string, TransactionValidityError]) => { + if (failedExtrinsic === extrinsic) { + callback(error.toJSON()) + done(id) + } + } + + context.chain.txPool.event.on(APPLY_EXTRINSIC_ERROR, onExtrinsicFail) + + const done = (id: string) => { + context.chain.txPool.event.removeListener(APPLY_EXTRINSIC_ERROR, onExtrinsicFail) + unsubscribe(id) + } + update = async (block) => { + const extrisnics = await block.extrinsics + if (!extrisnics.includes(extrinsic)) return + logger.debug({ block: block.hash }, 'author_extrinsicUpdate') // for now just assume tx is always included on next block callback({ @@ -25,7 +45,7 @@ const handlers: Handlers = { callback({ Finalized: block.hash, }) - unsubscribe(id) + done(id) } context.chain @@ -35,10 +55,10 @@ const handlers: Handlers = { Ready: null, }) }) - .catch((error) => { + .catch((error: TransactionValidityError) => { logger.error({ error }, 'ExtrinsicFailed') - callback({ Invalid: null }) - unsubscribe(id) + callback(error.toJSON()) + done(id) }) return id }, @@ -46,7 +66,7 @@ const handlers: Handlers = { unsubscribe(subid) }, author_pendingExtrinsics: async (context) => { - return context.chain.pendingExtrinsics + return context.chain.txPool.pendingExtrinsics }, }