Custom Precompiles
Precompiled contracts are special EVM contracts with native implementations at fixed addresses. Tevm lets you create your own precompiles in JavaScript to add custom functionality, improve performance, or implement new features directly in the EVM.
What Are Precompiles?
Precompile Types
- Ethereum Native ๐งฉ - Standard precompiles include hash functions, curve operations, and more at reserved addresses
- Custom JavaScript โก - Tevm lets you add your own JavaScript functions as precompiles
- Performance ๐ - Native code is much faster than EVM bytecode for complex operations
- Cross-Chain ๐ - Many L2s use precompiles to implement chain-specific functionality
Quick Start
Basic Example
import { createTevmNode, definePrecompile } from 'tevm'
import { createAddress } from 'tevm/address'
import { createContract } from 'tevm/contract'
import { parseAbi } from 'tevm/utils'
import { createImpersonatedTx } from 'tevm/tx'
import { EvmError, EvmErrorMessage } from 'tevm/evm'
// Create a basic precompile that doubles each byte
const customPrecompile = definePrecompile({
contract: createContract({
abi: parseAbi(['function double(bytes) returns (bytes)']),
address: '0x0000000000000000000000000000000000000123'
}),
call: async ({ data }) => {
const input = Array.from(data)
return {
returnValue: new Uint8Array(input.map(byte => Number(byte) * 2)),
executionGasUsed: 200n,
}
},
})
// Create node with the precompile
const node = createTevmNode({
customPrecompiles: [customPrecompile.precompile()],
})
// Create a transaction to call the precompile
const tx = createImpersonatedTx({
impersonatedAddress: createAddress('0x1234567890123456789012345678901234567890'),
to: customPrecompile.contract.address,
data: '0x00',
gasLimit: 21000n,
})
// Execute the transaction
const vm = await node.getVm()
const result = await vm.runTx({ tx })
Complex Example
import { createTevmNode, definePrecompile } from 'tevm'
import { createAddress } from 'tevm/address'
import { createContract } from 'tevm/contract'
import { parseAbi, hexToBytes } from 'tevm/utils'
import { createImpersonatedTx } from 'tevm/tx'
import { EvmError, EvmErrorMessage } from 'tevm/evm'
import { keccak256 } from 'tevm/crypto'
// Create a cryptographic hash precompile
const hashPrecompile = definePrecompile({
contract: createContract({
abi: parseAbi(['function hash(bytes) returns (bytes32)']),
address: '0x0000000000000000000000000000000000000321'
}),
call: async ({ data, gasLimit }) => {
// Calculate gas cost based on input size
const gasPerByte = 10n
const gasUsed = BigInt(data.length) * gasPerByte + 100n // Base cost + per-byte cost
// Perform the hash operation
const hash = keccak256(data)
return { returnValue: hash, executionGasUsed: gasUsed }
},
})
// Create a node with multiple precompiles
const node = createTevmNode({
customPrecompiles: [customPrecompile.precompile(), hashPrecompile.precompile()],
})
Precompile Interface
Define the Contract Interface
Every precompile needs an ABI and address:
const contract = createContract({
abi: parseAbi(['function myFunction(uint256) returns (uint256)']),
address: '0x0000000000000000000000000000000000000123'
})
Implement the Call Handler
The call handler receives the input data and gas limit:
const call = async ({ data, gasLimit }: PrecompileInput): Promise<PrecompileOutput> => {
// Process input data
// ...
return {
returnValue: new Uint8Array([/* result data */]),
executionGasUsed: 1000n,
// Optional: exceptionError for when the operation fails
}
}
Create and Register the Precompile
Combine both parts and register with a Tevm Node:
const myPrecompile = definePrecompile({ contract, call })
// Create a node with the precompile
const node = createTevmNode({
customPrecompiles: [
myPrecompile.precompile()
]
})
Example Implementations
State Access Example
const statePrecompile = definePrecompile({
contract: createContract({
abi: parseAbi(['function store(bytes32,bytes32)']),
address: '0x0000000000000000000000000000000000000124'
}),
call: async ({ data, gasLimit }) => {
// Extract key and value from input data
const key = data.slice(0, 32)
const value = data.slice(32)
// Get VM and state manager
const vm = await node.getVm()
// Store the value at the specified key
await vm.stateManager.putContractStorage(
createAddress(statePrecompile.contract.address),
key,
value
)
return { returnValue: new Uint8Array(), executionGasUsed: 200n }
},
})
Gas Calculation Example
const gasPrecompile = definePrecompile({
contract: createContract({
abi: parseAbi(['function processWithGas(bytes)']),
address: '0x0000000000000000000000000000000000000125'
}),
call: async ({ data, gasLimit }) => {
// Charge 100 gas per byte
const gasUsed = BigInt(data.length * 100)
// Check if we have enough gas
if (gasUsed > gasLimit) {
return {
returnValue: new Uint8Array(),
exceptionError: new EvmError(EvmErrorMessage.OUT_OF_GAS),
executionGasUsed: gasLimit,
}
}
return { returnValue: new Uint8Array(), executionGasUsed: gasUsed }
},
})
Error Handling Example
const errorPrecompile = definePrecompile({
contract: createContract({
abi: parseAbi(['function process(bytes)']),
address: '0x0000000000000000000000000000000000000126'
}),
call: async ({ data, gasLimit }) => {
try {
// Validate input
if (data.length === 0) {
return {
returnValue: new Uint8Array(),
exceptionError: new EvmError('Custom error: Empty input not allowed'),
executionGasUsed: 200n,
}
}
// Process data
return { returnValue: processData(data), executionGasUsed: 200n }
} catch (error) {
// Handle unexpected errors
return {
returnValue: new Uint8Array(),
exceptionError: new EvmError(`Precompile error: ${error.message}`),
executionGasUsed: gasLimit,
}
}
},
})
Multiple Precompiles Example
// First precompile
const precompileA = definePrecompile({
contract: createContract({
abi: parseAbi(['function processA() returns (bytes)']),
address: '0x0000000000000000000000000000000000000127'
}),
call: async () => ({
returnValue: new Uint8Array([1]),
executionGasUsed: 200n,
}),
})
// Second precompile
const precompileB = definePrecompile({
contract: createContract({
abi: parseAbi(['function processB() returns (bytes)']),
address: '0x0000000000000000000000000000000000000128'
}),
call: async () => ({
returnValue: new Uint8Array([2]),
executionGasUsed: 200n,
}),
})
// Register both precompiles
const node = createTevmNode({
customPrecompiles: [precompileA.precompile(), precompileB.precompile()],
})
Use Cases
Common Use Cases
- ๐ Cryptographic Operations - Implement efficient cryptographic operations like encryption, hashing, or signature verification
- ๐ฎ Oracle Functionality - Simulate oracles or external data sources during local testing
- ๐งฎ Complex Math - Perform complex mathematical calculations that would be gas-intensive in Solidity
- ๐ Cross-Chain Bridges - Simulate cross-chain verification logic for testing bridge implementations
- ๐ Custom Data Structures - Implement efficient data structure operations (trees, graphs, etc.)
- ๐งช Testing Helpers - Create special testing functions like time manipulation or state snapshots
Best Practices
Gas Calculation
const precompile = definePrecompile({
contract: createContract({
abi: parseAbi(['function process(bytes)']),
address: createAddress('0x0000000000000000000000000000000000000123')
}),
call: async ({ data, gasLimit }) => {
// Calculate gas based on input size and operations
const baseGas = 100n; // Base cost
const dataGas = BigInt(data.length * 10); // Per-byte cost
const totalGas = baseGas + dataGas; // Total cost
// Check gas limit
if (totalGas > gasLimit) {
return {
returnValue: new Uint8Array(),
exceptionError: new EvmError(EvmErrorMessage.OUT_OF_GAS),
executionGasUsed: gasLimit,
}
}
// Process data
return { returnValue: processData(data), executionGasUsed: totalGas }
},
})
Error Handling
const precompile = definePrecompile({
contract: createContract({
abi: parseAbi(['function process(bytes32,uint256)']),
address: createAddress('0x0000000000000000000000000000000000000123')
}),
call: async ({ data, gasLimit }) => {
try {
// Validate input format
if (data.length < 36) {
return {
returnValue: new Uint8Array(),
exceptionError: new EvmError('Invalid input: insufficient data'),
executionGasUsed: 100n,
}
}
// Additional validation and processing
// ...
return { returnValue: result, executionGasUsed: gasUsed }
} catch (error) {
// Log error for debugging (will not be visible to the transaction caller)
console.error('Precompile execution error:', error);
// Return appropriate error to the EVM
return {
returnValue: new Uint8Array(),
exceptionError: new EvmError(error.message || 'Unknown precompile error'),
executionGasUsed: Math.min(100n, gasLimit), // Charge some minimum gas
}
}
},
})
State Management
const statePrecompile = definePrecompile({
contract: createContract({
abi: parseAbi(['function getData(bytes32) returns (bytes32)', 'function setData(bytes32,bytes32)']),
address: createAddress('0x0000000000000000000000000000000000000124')
}),
call: async ({ data, gasLimit }) => {
const vm = await node.getVm();
const stateManager = vm.stateManager;
const address = createAddress(statePrecompile.contract.address);
// Parse function selector
const selector = data.slice(0, 4);
const isGetData = selector[0] === 0x9b & selector[1] === 0x18 & selector[2] === 0x30 & selector[3] === 0x4c;
if (isGetData) {
// Read from state
const key = data.slice(4, 36);
const value = await stateManager.getContractStorage(address, key);
return { returnValue: value, executionGasUsed: 200n };
} else {
// Write to state
const key = data.slice(4, 36);
const value = data.slice(36, 68);
await stateManager.putContractStorage(address, key, value);
return { returnValue: new Uint8Array(), executionGasUsed: 500n };
}
},
})
Performance
const precompile = definePrecompile({
// Contract definition...
call: async ({ data, gasLimit }) => {
// For expensive operations, consider caching results
const cacheKey = data.toString();
if (resultsCache.has(cacheKey)) {
return resultsCache.get(cacheKey);
}
// Perform computation
const result = performExpensiveOperation(data);
// Cache the result
resultsCache.set(cacheKey, { returnValue: result, executionGasUsed: gasUsed });
return { returnValue: result, executionGasUsed: gasUsed };
},
})
Related Resources
- Contract Reference - Working with smart contracts in Tevm
- State Management - Access and manipulate blockchain state
- JSON-RPC Support - Expose precompiles via JSON-RPC
- EVM Precompiles Reference - Standard Ethereum precompiled contracts