Skip to content

Commit

Permalink
handle apply extrinsic error (#191)
Browse files Browse the repository at this point in the history
* handle apply extrinsic error

* update event

* fix response

* update type

* update logging

* update test
  • Loading branch information
ermalkaleci authored Feb 15, 2023
1 parent bc9417d commit 182de1b
Show file tree
Hide file tree
Showing 7 changed files with 100 additions and 25 deletions.
33 changes: 28 additions & 5 deletions e2e/author.test.ts
Original file line number Diff line number Diff line change
@@ -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)
Expand Down Expand Up @@ -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)

Expand All @@ -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<void>()
const invalid = defer<string>()

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')
})
})
2 changes: 1 addition & 1 deletion e2e/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -167,7 +167,7 @@ export const dev = {
},
}

function defer<T>() {
export function defer<T>() {
const deferred = {} as { resolve: (value: any) => void; reject: (reason: any) => void; promise: Promise<T> }
deferred.promise = new Promise((resolve, reject) => {
deferred.resolve = resolve
Expand Down
2 changes: 1 addition & 1 deletion e2e/mock-signature.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down
39 changes: 32 additions & 7 deletions src/blockchain/block-builder.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -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)
Expand All @@ -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>('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)
Expand All @@ -161,7 +181,7 @@ export const buildBlock = async (

const blockData = registry.createType('Block', {
header,
extrinsics,
extrinsics: includedExtrinsic,
})

const storageDiff = await newBlock.storageDiff()
Expand All @@ -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}`
)

Expand Down
6 changes: 3 additions & 3 deletions src/blockchain/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Block | undefined> {
Expand Down Expand Up @@ -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<Block> {
Expand Down
9 changes: 8 additions & 1 deletion src/blockchain/txpool.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -37,6 +40,8 @@ export class TxPool {
readonly #mode: BuildBlockMode
readonly #inherentProvider: InherentProvider

readonly event = new EventEmitter()

#last: BehaviorSubject<Block>
#lastBuildBlockPromise: Promise<void> = Promise.resolve()

Expand Down Expand Up @@ -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)
}
Expand Down
34 changes: 27 additions & 7 deletions src/rpc/substrate/author.ts
Original file line number Diff line number Diff line change
@@ -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 }) => {
Expand All @@ -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({
Expand All @@ -25,7 +45,7 @@ const handlers: Handlers = {
callback({
Finalized: block.hash,
})
unsubscribe(id)
done(id)
}

context.chain
Expand All @@ -35,18 +55,18 @@ 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
},
author_unwatchExtrinsic: async (_context, [subid], { unsubscribe }) => {
unsubscribe(subid)
},
author_pendingExtrinsics: async (context) => {
return context.chain.pendingExtrinsics
return context.chain.txPool.pendingExtrinsics
},
}

Expand Down

0 comments on commit 182de1b

Please sign in to comment.