Skip to content

Commit

Permalink
add option to time-travel to a specific timestamp (#114)
Browse files Browse the repository at this point in the history
* add option to time-travel to a specific timestamp
  • Loading branch information
ermalkaleci authored Dec 22, 2022
1 parent 38dc60a commit 3910af5
Show file tree
Hide file tree
Showing 11 changed files with 167 additions and 67 deletions.
6 changes: 6 additions & 0 deletions e2e/dev.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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))
})
})
6 changes: 4 additions & 2 deletions e2e/helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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<number>('dev_timeTravel', [date])
},
}

function defer<T>() {
Expand Down
31 changes: 31 additions & 0 deletions e2e/time-travel.test.ts
Original file line number Diff line number Diff line change
@@ -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)))
})
})
43 changes: 19 additions & 24 deletions src/api.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -28,50 +29,44 @@ type SignedBlock = {

export class Api {
#provider: ProviderInterface
#isReady: Promise<void>
#chain: Promise<string>
#chainProperties: Promise<ChainProperties>
#ready: Promise<void> | undefined
#chain: Promise<string> | undefined
#chainProperties: Promise<ChainProperties> | 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() {
return this.#provider.disconnect()
}

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<string> {
if (!this.#chain) {
this.#chain = this.getSystemChain()
}
return this.#chain
}

get chainProperties(): Promise<ChainProperties> {
if (!this.#chainProperties) {
this.#chainProperties = this.getSystemProperties()
}
return this.#chainProperties
}

Expand Down
4 changes: 4 additions & 0 deletions src/blockchain/block.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,10 @@ export class Block {
this.#registry = parentBlock?.registry
}

get chain(): Blockchain {
return this.#chain
}

get header(): Header | Promise<Header> {
if (!this.#header) {
this.#header = Promise.all([this.registry, this.#chain.api.getHeader(this.hash)]).then(
Expand Down
13 changes: 4 additions & 9 deletions src/blockchain/inherent/index.ts
Original file line number Diff line number Diff line change
@@ -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'

Expand All @@ -15,13 +14,9 @@ export type InherentProvider = CreateInherents
export class SetTimestamp implements InherentProvider {
async createInherents(parent: Block): Promise<HexString[]> {
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()]
}
}

Expand Down
43 changes: 22 additions & 21 deletions src/blockchain/txpool.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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' })

Expand All @@ -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 {
Expand Down Expand Up @@ -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>('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>('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>('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
Expand Down
2 changes: 0 additions & 2 deletions src/genesis-provider.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,8 +42,6 @@ export class GenesisProvider implements ProviderInterface {
})
this.#eventemitter.once('error', reject)
})

this.connect()
}

static fromUrl = async (url: string) => {
Expand Down
7 changes: 7 additions & 0 deletions src/rpc/dev.ts
Original file line number Diff line number Diff line change
@@ -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' })

Expand Down Expand Up @@ -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
14 changes: 5 additions & 9 deletions src/setup.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -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
Expand Down Expand Up @@ -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,
Expand All @@ -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 }
}
Loading

0 comments on commit 3910af5

Please sign in to comment.