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

feat: transaction simulation endpoint #294

Open
ncitron opened this issue Oct 24, 2023 · 3 comments
Open

feat: transaction simulation endpoint #294

ncitron opened this issue Oct 24, 2023 · 3 comments
Labels
execution Execution-related work. feat New feature or request

Comments

@ncitron
Copy link
Collaborator

ncitron commented Oct 24, 2023

Supporting transaction simulation would be an awesome usecase for Helios. We need to think about what exactly this endpoint would look like.

The simplest case would be to have the same interface as eth_call but returning a receipt for the simulated transaction. Two additional features we could add is showing a state diff from the transaction, or even to support allowing arbitrary rpc calls on the post transaction state.

@ncitron ncitron added feat New feature or request execution Execution-related work. labels Oct 24, 2023
@shazow
Copy link

shazow commented Oct 24, 2023

Related brainstorming thread in case it's useful later: https://warpcast.com/ncitron.eth/0x4238de4f

@TateGunning
Copy link

Nice work! Many variables to consider.

Perhaps more complex cases involve greater decentralisation contingent or security detail depending, or applied analytics.

Mutual respect and disclosure of raw info or tasteful privacy.

Idea-flow possibilities: origination, consent, scope model promoting user awareness best practice and or training module, mental status, biographics, attempt versus equal benefit, time mechanisms, early observatory clicks, too often repetitive feature, response to request, response to robot request or vice versa false attempt, close, total failure, failure to respond after responding respectfully to request, respond with reputation risks, clear definables or bona fide glossary, login attempts, aggregates, opt out, opt less in, blindness radar, actual response to actual request, timidability versus publishability, royalty if any, etc.

@roninjin10
Copy link

roninjin10 commented Oct 22, 2024

Integrating Tevm into helios will create an absurdly powerful version of this.

tevm_call params:

/**
 * Properties shared across call-like params.
 * This type is used as the base for various call-like parameter types:
 * - [CallParams](https://tevm.sh/reference/tevm/actions/type-aliases/callparams-1/)
 * - [ContractParams](https://tevm.sh/reference/tevm/actions/type-aliases/contractparams-1/)
 * - [DeployParams](https://tevm.sh/reference/tevm/actions/type-aliases/deployparams-1/)
 * - [ScriptParams](https://tevm.sh/reference/tevm/actions/type-aliases/scriptparams-1/)
 *
 * @extends BaseParams
 * @example
 * ```typescript
 * import { BaseCallParams } from 'tevm'
 *
 * const params: BaseCallParams = {
 *   createTrace: true,
 *   createAccessList: true,
 *   createTransaction: 'on-success',
 *   blockTag: 'latest',
 *   skipBalance: true,
 *   gas: 1000000n,
 *   gasPrice: 1n,
 *   maxFeePerGas: 1n,
 *   maxPriorityFeePerGas: 1n,
 *   gasRefund: 0n,
 *   from: '0x123...',
 *   origin: '0x123...',
 *   caller: '0x123...',
 *   value: 0n,
 *   depth: 0,
 *   to: '0x123...',
 * }
 * ```
 */
export type BaseCallParams<TThrowOnFail extends boolean = boolean> = BaseParams<TThrowOnFail> & {
	/**
	 * Whether to return a complete trace with the call.
	 * Defaults to `false`.
	 * @example
	 * ```ts
	 * import { createMemoryClient } from 'tevm'
	 *
	 * const client = createMemoryClient()
	 *
	 * const { trace } = await client.call({ address: '0x1234', data: '0x1234', createTrace: true })
	 *
	 * trace.structLogs.forEach(console.log)
	 * ```
	 */
	readonly createTrace?: boolean
	/**
	 * Whether to return an access list mapping of addresses to storage keys.
	 * Defaults to `false`.
	 * @example
	 * ```ts
	 * import { createMemoryClient } from 'tevm'
	 *
	 * const client = createMemoryClient()
	 *
	 * const { accessList } = await client.tevmCall({ to: '0x1234...', data: '0x1234', createAccessList: true })
	 * console.log(accessList) // { "0x...": Set(["0x..."]) }
	 * ```
	 */
	readonly createAccessList?: boolean
	/**
	 * Whether or not to update the state or run the call in a dry-run. Defaults to `never`.
	 * - `on-success`: Only update the state if the call is successful.
	 * - `always`: Always include the transaction even if it reverts.
	 * - `never`: Never include the transaction.
	 * - `true`: Alias for `on-success`.
	 * - `false`: Alias for `never`.
	 *
	 * @example
	 * ```typescript
	 * const { txHash } = await client.call({ address: '0x1234', data: '0x1234', createTransaction: 'on-success' })
	 * await client.mine()
	 * const receipt = await client.getTransactionReceipt({ hash: txHash })
	 * ```
	 */
	readonly createTransaction?: 'on-success' | 'always' | 'never' | boolean
	/**
	 * The block number or block tag to execute the call at. Defaults to `latest`.
	 * - `bigint`: The block number to execute the call at.
	 * - `Hex`: The block hash to execute the call at.
	 * - `BlockTag`: The named block tag to execute the call at.
	 *
	 * Notable block tags:
	 * - 'latest': The canonical head.
	 * - 'pending': A block that is optimistically built with transactions in the txpool that have not yet been mined.
	 * - 'forked': If forking, the 'forked' block will be the block the chain was forked at.
	 */
	readonly blockTag?: BlockParam
	/**
	 * Whether to skip the balance check. Defaults to `false`, except for scripts where it is set to `true`.
	 */
	readonly skipBalance?: boolean
	/**
	 * The gas limit for the call.
	 * Defaults to the block gas limit as specified by the common configuration or the fork URL.
	 */
	readonly gas?: bigint
	/**
	 * The gas price for the call.
	 * Note: This option is currently ignored when creating transactions because only EIP-1559 transactions are supported. This will be fixed in a future release.
	 */
	readonly gasPrice?: bigint
	/**
	 * The maximum fee per gas for EIP-1559 transactions.
	 */
	readonly maxFeePerGas?: bigint
	/**
	 * The maximum priority fee per gas for EIP-1559 transactions.
	 */
	readonly maxPriorityFeePerGas?: bigint
	/**
	 * The refund counter. Defaults to `0`.
	 */
	readonly gasRefund?: bigint
	/**
	 * The from address for the call. Defaults to the zero address for reads and the first account for writes.
	 * It is also possible to set the `origin` and `caller` addresses separately using those options. Otherwise, both are set to the `from` address.
	 */
	readonly from?: Address
	/**
	 * The address where the call originated from. Defaults to the zero address.
	 * If the `from` address is set, it defaults to the `from` address; otherwise, it defaults to the zero address.
	 */
	readonly origin?: Address
	/**
	 * The address that ran this code (`msg.sender`). Defaults to the zero address.
	 * If the `from` address is set, it defaults to the `from` address; otherwise, it defaults to the zero address.
	 */
	readonly caller?: Address
	/**
	 * The value in ether that is being sent to the `to` address. Defaults to `0`.
	 */
	readonly value?: bigint
	/**
	 * The depth of the EVM call. Useful for simulating an internal call. Defaults to `0`.
	 */
	readonly depth?: number
	/**
	 * Addresses to selfdestruct. Defaults to an empty set.
	 */
	readonly selfdestruct?: Set<Address>
	/**
	 * The address of the account executing this code (`address(this)`). Defaults to the zero address.
	 * This is not set for create transactions but is required for most transactions.
	 */
	readonly to?: Address
	/**
	 * Versioned hashes for each blob in a blob transaction for EIP-4844 transactions.
	 */
	readonly blobVersionedHashes?: Hex[]
	/**
	 * The state override set is an optional address-to-state mapping where each entry specifies some state to be ephemerally overridden prior to executing the call. Each address maps to an object containing:
	 * This option cannot be used when `createTransaction` is set to `true`.
	 *
	 * @example
	 * ```ts
	 * const stateOverride = {
	 *   "0xd9c9cd5f6779558b6e0ed4e6acf6b1947e7fa1f3": {
	 *     balance: "0xde0b6b3a7640000"
	 *   },
	 *   "0xebe8efa441b9302a0d7eaecc277c09d20d684540": {
	 *     code: "0x...",
	 *     state: {
	 *       "0x...": "0x..."
	 *     }
	 *   }
	 * }
	 * const res = await client.call({ address: '0x1234', data: '0x1234', stateOverrideSet: stateOverride })
	 * ```
	 */
	readonly stateOverrideSet?: StateOverrideSet
	/**
	 * The fields of this optional object customize the block as part of which the call is simulated.
	 * The object contains fields such as block number, hash, parent hash, nonce, etc.
	 * This option cannot be used when `createTransaction` is set to `true`.
	 * Setting the block number to a past block will not run in the context of that block's state. To do that, fork that block number first.
	 *
	 * @example
	 * ```ts
	 * const blockOverride = {
	 *   number: "0x1b4",
	 *   hash: "0x...",
	 *   parentHash: "0x...",
	 *   nonce: "0x0000000000000042",
	 * }
	 * const res = await client.call({ address: '0x1234', data: '0x1234', blockOverrideSet: blockOverride })
	 * ```
	 */
	readonly blockOverrideSet?: BlockOverrideSet
}
import type { BaseCallParams } from '../BaseCall/BaseCallParams.js'
import type { Hex } from '../common/index.js'

/**
 * TEVM parameters to execute a call on the VM.
 * `Call` is the lowest level method to interact with the VM, and other methods such as `contract` and `script` use `call` under the hood.
 *
 * @example
 * ```typescript
 * import { createClient } from 'viem'
 * import { createTevmTransport, tevmCall } from 'tevm'
 * import { optimism } from 'tevm/common'
 *
 * const client = createClient({
 *   transport: createTevmTransport({}),
 *   chain: optimism,
 * })
 *
 * const callParams = {
 *   data: '0x...',
 *   bytecode: '0x...',
 *   gasLimit: 420n,
 * }
 *
 * await tevmCall(client, callParams)
 * ```
 *
 * @see [BaseCallParams](https://tevm.sh/reference/tevm/actions/type-aliases/basecallparams-1/)
 * @see [tevmCall](https://tevm.sh/reference/tevm/memory-client/functions/tevmCall/)
 */
export type CallParams<TThrowOnFail extends boolean = boolean> = BaseCallParams<TThrowOnFail> & {
	/**
	 * An optional CREATE2 salt.
	 *
	 * @example
	 * ```typescript
	 * import { createClient } from 'viem'
	 * import { createTevmTransport, tevmCall } from 'tevm'
	 * import { optimism } from 'tevm/common'
	 *
	 * const client = createClient({
	 *   transport: createTevmTransport({}),
	 *   chain: optimism,
	 * })
	 *
	 * const callParams = {
	 *   data: '0x...',
	 *   bytecode: '0x...',
	 *   gasLimit: 420n,
	 *   salt: '0x1234...',
	 * }
	 *
	 * await tevmCall(client, callParams)
	 * ```
	 *
	 * @see [CREATE2](https://eips.ethereum.org/EIPS/eip-1014)
	 */
	readonly salt?: Hex
	/**
	 * The input data for the call.
	 */
	readonly data?: Hex
	/**
	 * The encoded code to deploy with for a deployless call. Code is encoded with constructor arguments, unlike `deployedBytecode`.
	 *
	 * @example
	 * ```typescript
	 * import { createClient } from 'viem'
	 * import { createTevmTransport, tevmCall, encodeDeployData } from 'tevm'
	 * import { optimism } from 'tevm/common'
	 *
	 * const client = createClient({
	 *   transport: createTevmTransport({}),
	 *   chain: optimism,
	 * })
	 *
	 * const callParams = {
	 *   createTransaction: true,
	 *   data: encodeDeployData({
	 *     bytecode: '0x...',
	 *     data: '0x...',
	 *     abi: [{...}],
	 *     args: [1, 2, 3],
	 *   })
	 * }
	 *
	 * await tevmCall(client, callParams)
	 * ```
	 * Code is also automatically created if using TEVM contracts via the `script` method.
	 *
	 * @example
	 * ```typescript
	 * import { createClient } from 'viem'
	 * import { createTevmTransport, tevmContract } from 'tevm'
	 * import { optimism } from 'tevm/common'
	 * import { SimpleContract } from 'tevm/contracts'
	 *
	 * const client = createClient({
	 *   transport: createTevmTransport({}),
	 *   chain: optimism,
	 * })
	 *
	 * const script = SimpleContract.script({ constructorArgs: [420n] })
	 *
	 * await tevmContract(client, script.read.get()) // 420n
	 * ```
	 */
	readonly code?: Hex
	/**
	 * The code to put into the state before executing the call. If you wish to call the constructor, use `code` instead.
	 *
	 * @example
	 * ```typescript
	 * import { createClient } from 'viem'
	 * import { createTevmTransport, tevmCall } from 'tevm'
	 * import { optimism } from 'tevm/common'
	 *
	 * const client = createClient({
	 *   transport: createTevmTransport({}),
	 *   chain: optimism,
	 * })
	 *
	 * const callParams = {
	 *   data: '0x...',
	 *   deployedBytecode: '0x...',
	 * }
	 *
	 * await tevmCall(client, callParams)
	 * ```
	 */
	readonly deployedBytecode?: Hex
}

And it returns the following

export type CallResult<ErrorType = TevmCallError> = {
	/**
	 * The call trace if tracing is enabled on call.
	 *
	 * @example
	 * ```typescript
	 * const trace = result.trace
	 * trace.structLogs.forEach(console.log)
	 * ```
	 */
	trace?: DebugTraceCallResult
	/**
	 * The access list if enabled on call.
	 * Mapping of addresses to storage slots.
	 *
	 * @example
	 * ```typescript
	 * const accessList = result.accessList
	 * console.log(accessList) // { "0x...": Set(["0x..."]) }
	 * ```
	 */
	accessList?: Record<Address, Set<Hex>>
	/**
	 * Preimages mapping of the touched accounts from the transaction (see `reportPreimages` option).
	 */
	preimages?: Record<Hex, Hex>
	/**
	 * The returned transaction hash if the call was included in the chain.
	 * Will not be defined if the call was not included in the chain.
	 * Whether a call is included in the chain depends on the `createTransaction` option and the result of the call.
	 *
	 * @example
	 * ```typescript
	 * const txHash = result.txHash
	 * if (txHash) {
	 *   console.log(`Transaction included in the chain with hash: ${txHash}`)
	 * }
	 * ```
	 */
	txHash?: Hex
	/**
	 * Amount of gas left after execution.
	 */
	gas?: bigint
	/**
	 * Amount of gas the code used to run within the EVM.
	 * This only includes gas spent on the EVM execution itself and doesn't account for gas spent on other factors such as data storage.
	 */
	executionGasUsed: bigint
	/**
	 * Array of logs that the contract emitted.
	 *
	 * @example
	 * ```typescript
	 * const logs = result.logs
	 * logs?.forEach(log => console.log(log))
	 * ```
	 */
	logs?: Log[]
	/**
	 * The gas refund counter as a uint256.
	 */
	gasRefund?: bigint
	/**
	 * Amount of blob gas consumed by the transaction.
	 */
	blobGasUsed?: bigint
	/**
	 * Address of created account during the transaction, if any.
	 */
	createdAddress?: Address
	/**
	 * A set of accounts to selfdestruct.
	 */
	selfdestruct?: Set<Address>
	/**
	 * Map of addresses which were created (used in EIP 6780).
	 * Note the addresses are not actually created until the transaction is mined.
	 */
	createdAddresses?: Set<Address>
	/**
	 * Encoded return value from the contract as a hex string.
	 *
	 * @example
	 * ```typescript
	 * const rawData = result.rawData
	 * console.log(`Raw data returned: ${rawData}`)
	 * ```
	 */
	rawData: Hex
	/**
	 * Description of the exception, if any occurred.
	 */
	errors?: ErrorType[]
	/**
	 * Priority fee set by the transaction.
	 */
	priorityFee?: bigint
	/**
	 * The base fee of the transaction.
	 */
	baseFee?: bigint
	/**
	 * L1 fee that should be paid for the transaction.
	 * Only included when an OP-Stack common is provided.
	 *
	 * @see [OP-Stack docs](https://docs.optimism.io/stack/transactions/fees)
	 */
	l1Fee?: bigint
	/**
	 * Amount of L1 gas used to publish the transaction.
	 * Only included when an OP-Stack common is provided.
	 *
	 * @see [OP-Stack docs](https://docs.optimism.io/stack/transactions/fees)
	 */
	l1GasUsed?: bigint
	/**
	 * Current blob base fee known by the L2 chain.
	 *
	 * @see [OP-Stack docs](https://docs.optimism.io/stack/transactions/fees)
	 */
	l1BlobFee?: bigint
	/**
	 * Latest known L1 base fee known by the L2 chain.
	 * Only included when an OP-Stack common is provided.
	 *
	 * @see [OP-Stack docs](https://docs.optimism.io/stack/transactions/fees)
	 */
	l1BaseFee?: bigint
	/**
	 * The amount of gas used in this transaction, which is paid for.
	 * This contains the gas units that have been used on execution, plus the upfront cost,
	 * which consists of calldata cost, intrinsic cost, and optionally the access list costs.
	 * This is analogous to what `eth_estimateGas` would return. Does not include L1 fees.
	 */
	totalGasSpent?: bigint
	/**
	 * The amount of ether used by this transaction. Does not include L1 fees.
	 */
	amountSpent?: bigint
	/**
	 * The value that accrues to the miner by this transaction.
	 */
	minerValue?: bigint
}

Furthermore you can actually write javascript to arbitrarily plug into the EVM events. You can even modify the evm in the middle of the call if you want

import { DefensiveNullCheckError } from '@tevm/errors'
import { bytesToHex, invariant, numberToHex } from '@tevm/utils'
/**
 * @internal
 * Prepares a trace to be listened to. If laizlyRun is true, it will return an object with the trace and not run the evm internally
 * @param {import('@tevm/vm').Vm} vm
 * @param {import('@tevm/node').TevmNode['logger']} logger
 * @param {import('@tevm/evm').EvmRunCallOpts} params
 * @param {boolean} [lazilyRun]
 * @returns {Promise<import('@tevm/evm').EvmResult & {trace: import('../debug/DebugResult.js').DebugTraceCallResult}>}
 * @throws {never}
 */
export const runCallWithTrace = async (vm, logger, params, lazilyRun = false) => {
	/**
	 * As the evm runs we will be updating this trace object
	 * and then returning it
	 */
	const trace = {
		gas: 0n,
		/**
		 * @type {import('@tevm/utils').Hex}
		 */
		returnValue: '0x0',
		failed: false,
		/**
		 * @type {Array<import('../debug/DebugResult.js').DebugTraceCallResult['structLogs'][number]>}
		 */
		structLogs: [],
	}

	/**
	 * On every step push a struct log
	 */
	vm.evm.events?.on('step', async (step, next) => {
		logger.debug(step, 'runCallWithTrace: new evm step')
		trace.structLogs.push({
			pc: step.pc,
			op: step.opcode.name,
			gasCost: BigInt(step.opcode.fee) + (step.opcode.dynamicFee ?? 0n),
			gas: step.gasLeft,
			depth: step.depth,
			stack: step.stack.map((code) => numberToHex(code)),
		})
		next?.()
	})

	/**
	 * After any internal call push error if any
	 */
	vm.evm.events?.on('afterMessage', (data, next) => {
		logger.debug(data.execResult, 'runCallWithTrace: new message result')
		if (data.execResult.exceptionError !== undefined && trace.structLogs.length > 0) {
			// Mark last opcode trace as error if exception occurs
			const nextLog = trace.structLogs[trace.structLogs.length - 1]
			invariant(nextLog, new DefensiveNullCheckError('No structLogs to mark as error'))
			// TODO fix this type
			Object.assign(nextLog, {
				error: data.execResult.exceptionError,
			})
		}
		next?.()
	})

	if (lazilyRun) {
		// TODO internally used function is not typesafe here
		return /** @type any*/ ({ trace })
	}

	const runCallResult = await vm.evm.runCall(params)

	logger.debug(runCallResult, 'runCallWithTrace: evm run call complete')

	trace.gas = runCallResult.execResult.executionGasUsed
	trace.failed = runCallResult.execResult.exceptionError !== undefined
	trace.returnValue = bytesToHex(runCallResult.execResult.returnValue)

	return {
		...runCallResult,
		trace,
	}
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
execution Execution-related work. feat New feature or request
Projects
None yet
Development

No branches or pull requests

4 participants