diff --git a/e2e/dev.test.ts b/e2e/dev.test.ts index 265937fa..40887c0a 100644 --- a/e2e/dev.test.ts +++ b/e2e/dev.test.ts @@ -57,4 +57,10 @@ describe('dev rpc', () => { const newBlockNumber2 = (await api.rpc.chain.getHeader()).number.toNumber() expect(newBlockNumber2).toBe(blockNumber + 5) }) + + it('timeTravel', async () => { + const date = 'Jan 1, 2023' + const timestamp = await dev.timeTravel(date) + expect(timestamp).eq(Date.parse(date)) + }) }) diff --git a/e2e/helper.ts b/e2e/helper.ts index 1be2daa8..9a4b7aa3 100644 --- a/e2e/helper.ts +++ b/e2e/helper.ts @@ -51,8 +51,7 @@ export const setupAll = async ({ return { async setup() { - const setTimestamp = new SetTimestamp() - const inherents = new InherentProviders(setTimestamp, [new SetValidationData()]) + const inherents = new InherentProviders(new SetTimestamp(), [new SetValidationData()]) const chain = new Blockchain({ api, @@ -140,6 +139,9 @@ export const dev = { setStorages: (values: StorageValues, blockHash?: string) => { return ws.send('dev_setStorages', [values, blockHash]) }, + timeTravel: (date: string | number) => { + return ws.send('dev_timeTravel', [date]) + }, } function defer() { diff --git a/e2e/time-travel.test.ts b/e2e/time-travel.test.ts new file mode 100644 index 00000000..46843b4b --- /dev/null +++ b/e2e/time-travel.test.ts @@ -0,0 +1,31 @@ +import { chain, setupApi, ws } from './helper' +import { describe, expect, it } from 'vitest' +import { getCurrentTimestamp, getSlotDuration, timeTravel } from '../src/utils/time-travel' + +describe.each([ + { + chain: 'Polkadot', + endpoint: 'wss://rpc.polkadot.io', + blockHash: '0xb7fb7cfe79142652036e73f8044e0efbbbe7d3fb71cabc212efd5968c9041950', + }, + { + chain: 'Acala', + endpoint: 'wss://acala-rpc-1.aca-api.network', + blockHash: '0x1d9223c88161b512ebaac53c2c7df6dc6bd2731b12273b898f582af929cc5331', + }, +])('Can time-travel on $chain', async ({ endpoint, blockHash }) => { + setupApi({ endpoint, blockHash }) + + it.each(['Nov 30, 2022', 'Dec 22, 2022', 'Jan 1, 2024'])('%s', async (date) => { + const timestamp = Date.parse(date) + + await timeTravel(chain, timestamp) + + expect(await getCurrentTimestamp(chain)).eq(timestamp) + + // can build block successfully + await ws.send('dev_newBlock', []) + + expect(await getCurrentTimestamp(chain)).eq(timestamp + (await getSlotDuration(chain))) + }) +}) diff --git a/src/api.ts b/src/api.ts index 28a236af..e004a1a2 100644 --- a/src/api.ts +++ b/src/api.ts @@ -1,6 +1,7 @@ import { ExtDef } from '@polkadot/types/extrinsic/signedExtensions/types' import { HexString } from '@polkadot/util/types' import { ProviderInterface } from '@polkadot/rpc-provider/types' +import { WsProvider } from '@polkadot/rpc-provider' type ChainProperties = { ss58Format?: number @@ -28,35 +29,15 @@ type SignedBlock = { export class Api { #provider: ProviderInterface - #isReady: Promise - #chain: Promise - #chainProperties: Promise + #ready: Promise | undefined + #chain: Promise | undefined + #chainProperties: Promise | undefined readonly signedExtensions: ExtDef constructor(provider: ProviderInterface, signedExtensions?: ExtDef) { this.#provider = provider this.signedExtensions = signedExtensions || {} - this.#isReady = new Promise((resolve, reject) => { - if (this.#provider.isConnected) { - setTimeout(resolve, 500) - } else { - this.#provider.on('connected', () => { - setTimeout(resolve, 500) - }) - } - this.#provider.on('error', reject) - }) - - this.#provider.on('disconnected', () => { - // TODO: reconnect - console.warn('Api disconnected') - }) - - this.#chain = this.#isReady.then(() => this.getSystemChain()) - this.#chainProperties = this.#isReady.then(() => this.getSystemProperties()) - - this.#provider.connect() } async disconnect() { @@ -64,14 +45,28 @@ export class Api { } get isReady() { - return this.#isReady + if (this.#provider instanceof WsProvider) { + return this.#provider.isReady + } + + if (!this.#ready) { + this.#ready = this.#provider.connect() + } + + return this.#ready } get chain(): Promise { + if (!this.#chain) { + this.#chain = this.getSystemChain() + } return this.#chain } get chainProperties(): Promise { + if (!this.#chainProperties) { + this.#chainProperties = this.getSystemProperties() + } return this.#chainProperties } diff --git a/src/blockchain/block.ts b/src/blockchain/block.ts index 5c0f6915..d23091ad 100644 --- a/src/blockchain/block.ts +++ b/src/blockchain/block.ts @@ -50,6 +50,10 @@ export class Block { this.#registry = parentBlock?.registry } + get chain(): Blockchain { + return this.#chain + } + get header(): Header | Promise
{ if (!this.#header) { this.#header = Promise.all([this.registry, this.#chain.api.getHeader(this.hash)]).then( diff --git a/src/blockchain/inherent/index.ts b/src/blockchain/inherent/index.ts index 5627bb5e..fa9d420e 100644 --- a/src/blockchain/inherent/index.ts +++ b/src/blockchain/inherent/index.ts @@ -1,8 +1,7 @@ import { Block } from '../block' import { GenericExtrinsic } from '@polkadot/types' import { HexString } from '@polkadot/util/types' -import { compactHex } from '../../utils' -import { hexToU8a } from '@polkadot/util' +import { getCurrentTimestamp, getSlotDuration } from '../../utils/time-travel' export { SetValidationData } from './parachain/validation-data' @@ -15,13 +14,9 @@ export type InherentProvider = CreateInherents export class SetTimestamp implements InherentProvider { async createInherents(parent: Block): Promise { const meta = await parent.meta - const timestampRaw = (await parent.get(compactHex(meta.query.timestamp.now()))) || '0x' - const currentTimestamp = meta.registry.createType('u64', hexToU8a(timestampRaw)).toNumber() - const period = meta.consts.babe - ? (meta.consts.babe.expectedBlockTime.toJSON() as number) - : (meta.consts.timestamp.minimumPeriod.toJSON() as number) * 2 - const newTimestamp = currentTimestamp + period - return [new GenericExtrinsic(meta.registry, meta.tx.timestamp.set(newTimestamp)).toHex()] + const slotDuration = await getSlotDuration(parent.chain) + const currentTimestamp = await getCurrentTimestamp(parent.chain) + return [new GenericExtrinsic(meta.registry, meta.tx.timestamp.set(currentTimestamp + slotDuration)).toHex()] } } diff --git a/src/blockchain/txpool.ts b/src/blockchain/txpool.ts index 2c36d70d..30761b21 100644 --- a/src/blockchain/txpool.ts +++ b/src/blockchain/txpool.ts @@ -1,4 +1,4 @@ -import { Header, RawBabePreDigest, Slot } from '@polkadot/types/interfaces' +import { Header, RawBabePreDigest } from '@polkadot/types/interfaces' import { HexString } from '@polkadot/util/types' import { compactAddLength } from '@polkadot/util' import _ from 'lodash' @@ -9,6 +9,7 @@ import { InherentProvider } from './inherent' import { ResponseError } from '../rpc/shared' import { compactHex } from '../utils' import { defaultLogger, truncate, truncateStorageDiff } from '../logger' +import { getCurrentSlot } from '../utils/time-travel' const logger = defaultLogger.child({ name: 'txpool' }) @@ -25,35 +26,32 @@ const getConsensus = (header: Header) => { return { consensusEngine, slot, rest: header.digest.logs.slice(1) } } -const getNewSlot = (slot: RawBabePreDigest) => { - if (slot.isPrimary) { - const primary = slot.asPrimary.toJSON() +const getNewSlot = (digest: RawBabePreDigest, slotNumber: number) => { + if (digest.isPrimary) { return { primary: { - ...primary, - slotNumber: (primary.slotNumber as number) + 1, + ...digest.asPrimary.toJSON(), + slotNumber, }, } } - if (slot.isSecondaryPlain) { - const secondaryPlain = slot.asSecondaryPlain.toJSON() + if (digest.isSecondaryPlain) { return { secondaryPlain: { - ...secondaryPlain, - slotNumber: (secondaryPlain.slotNumber as number) + 1, + ...digest.asSecondaryPlain.toJSON(), + slotNumber, }, } } - if (slot.isSecondaryVRF) { - const secondaryVRF = slot.asSecondaryVRF.toJSON() + if (digest.isSecondaryVRF) { return { secondaryVRF: { - ...secondaryVRF, - slotNumber: (secondaryVRF.slotNumber as number) + 1, + ...digest.asSecondaryVRF.toJSON(), + slotNumber, }, } } - return slot.toJSON() + return digest.toJSON() } export class TxPool { @@ -110,13 +108,16 @@ export class TxPool { let newLogs = parentHeader.digest.logs as any const consensus = getConsensus(parentHeader) if (consensus?.consensusEngine.isAura) { - const slot = meta.registry.createType('Slot', consensus.slot).toNumber() - const newSlot = meta.registry.createType('Slot', slot + 1).toU8a() - newLogs = [{ PreRuntime: [consensus.consensusEngine, compactAddLength(newSlot)] }, ...consensus.rest] + const slot = await getCurrentSlot(this.#chain) + const newSlot = compactAddLength(meta.registry.createType('Slot', slot + 1).toU8a()) + newLogs = [{ PreRuntime: [consensus.consensusEngine, newSlot] }, ...consensus.rest] } else if (consensus?.consensusEngine.isBabe) { - const slot = meta.registry.createType('RawBabePreDigest', consensus.slot) - const newSlot = meta.registry.createType('RawBabePreDigest', getNewSlot(slot)).toU8a() - newLogs = [{ PreRuntime: [consensus.consensusEngine, compactAddLength(newSlot)] }, ...consensus.rest] + const slot = await getCurrentSlot(this.#chain) + const digest = meta.registry.createType('RawBabePreDigest', consensus.slot) + const newSlot = compactAddLength( + meta.registry.createType('RawBabePreDigest', getNewSlot(digest, slot + 1)).toU8a() + ) + newLogs = [{ PreRuntime: [consensus.consensusEngine, newSlot] }, ...consensus.rest] } const registry = await head.registry diff --git a/src/genesis-provider.ts b/src/genesis-provider.ts index 0431026a..a5cdd534 100644 --- a/src/genesis-provider.ts +++ b/src/genesis-provider.ts @@ -42,8 +42,6 @@ export class GenesisProvider implements ProviderInterface { }) this.#eventemitter.once('error', reject) }) - - this.connect() } static fromUrl = async (url: string) => { diff --git a/src/rpc/dev.ts b/src/rpc/dev.ts index d065f2bf..bd6c744b 100644 --- a/src/rpc/dev.ts +++ b/src/rpc/dev.ts @@ -1,6 +1,7 @@ import { Handlers, ResponseError } from './shared' import { StorageValues, setStorage } from '../utils/set-storage' import { defaultLogger } from '../logger' +import { timeTravel } from '../utils/time-travel' const logger = defaultLogger.child({ name: 'rpc-dev' }) @@ -35,6 +36,12 @@ const handlers: Handlers = { ) return hash }, + dev_timeTravel: async (context, [date]) => { + const timestamp = typeof date === 'string' ? Date.parse(date) : date + if (Number.isNaN(timestamp)) throw new ResponseError(1, 'Invalid date') + await timeTravel(context.chain, timestamp) + return timestamp + }, } export default handlers diff --git a/src/setup.ts b/src/setup.ts index 55ca667d..6f9ce204 100644 --- a/src/setup.ts +++ b/src/setup.ts @@ -1,9 +1,8 @@ import '@polkadot/types-codec' +import { DataSource } from 'typeorm' import { ProviderInterface } from '@polkadot/rpc-provider/types' import { WsProvider } from '@polkadot/api' -import { DataSource } from 'typeorm' - import { Api } from './api' import { Blockchain } from './blockchain' import { Config } from './schema' @@ -12,6 +11,7 @@ import { InherentProviders, SetTimestamp, SetValidationData } from './blockchain import { defaultLogger } from './logger' import { importStorage, overrideWasm } from './utils/import-storage' import { openDb } from './db' +import { timeTravel } from './utils/time-travel' export const setup = async (argv: Config) => { let provider: ProviderInterface @@ -45,11 +45,7 @@ export const setup = async (argv: Config) => { const header = await api.getHeader(blockHash) - // TODO: do we really need to set a custom timestamp? - // const timestamp = argv.timestamp - - const setTimestamp = new SetTimestamp() - const inherents = new InherentProviders(setTimestamp, [new SetValidationData()]) + const inherents = new InherentProviders(new SetTimestamp(), [new SetValidationData()]) const chain = new Blockchain({ api, @@ -62,10 +58,10 @@ export const setup = async (argv: Config) => { }, }) - const context = { chain, api, ws: provider } + if (argv.timestamp) await timeTravel(chain, argv.timestamp) await importStorage(chain, argv['import-storage']) await overrideWasm(chain, argv['wasm-override']) - return context + return { chain, api, ws: provider } } diff --git a/src/utils/time-travel.ts b/src/utils/time-travel.ts new file mode 100644 index 00000000..0772692f --- /dev/null +++ b/src/utils/time-travel.ts @@ -0,0 +1,65 @@ +import { BN, hexToU8a, u8aToHex } from '@polkadot/util' +import { HexString } from '@polkadot/util/types' +import { Slot } from '@polkadot/types/interfaces' + +import { Blockchain } from '../blockchain' +import { compactHex } from '.' +import { setStorage } from './set-storage' + +export const getCurrentSlot = async (chain: Blockchain) => { + const meta = await chain.head.meta + const slotRaw = meta.consts.babe + ? await chain.head.get(compactHex(meta.query.babe.currentSlot())) + : await chain.head.get(compactHex(meta.query.aura.currentSlot())) + if (!slotRaw) throw new Error('Cannot find current slot') + return meta.registry.createType('Slot', hexToU8a(slotRaw)).toNumber() +} + +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() +} + +export const getSlotDuration = async (chain: Blockchain) => { + const meta = await chain.head.meta + return meta.consts.babe + ? (meta.consts.babe.expectedBlockTime as any as BN).toNumber() + : (meta.consts.timestamp.minimumPeriod as any as BN).toNumber() * 2 +} + +export const timeTravel = async (chain: Blockchain, timestamp: number) => { + const meta = await chain.head.meta + + const slotDuration = await getSlotDuration(chain) + const newSlot = Math.floor(timestamp / slotDuration) + + // new timestamp + const storage: [HexString, HexString][] = [ + [compactHex(meta.query.timestamp.now()), u8aToHex(meta.registry.createType('u64', timestamp).toU8a())], + ] + + if (meta.consts.babe) { + // new slot + storage.push([ + compactHex(meta.query.babe.currentSlot()), + u8aToHex(meta.registry.createType('Slot', newSlot).toU8a()), + ]) + + // new epoch + const epochDuration = (meta.consts.babe.epochDuration as any as BN).toNumber() + const newEpoch = Math.floor(timestamp / epochDuration) + storage.push([ + compactHex(meta.query.babe.epochIndex()), + u8aToHex(meta.registry.createType('u64', newEpoch).toU8a()), + ]) + } else { + // new slot + storage.push([ + compactHex(meta.query.aura.currentSlot()), + u8aToHex(meta.registry.createType('Slot', newSlot).toU8a()), + ]) + } + + await setStorage(chain, storage) +}