Skip to content
Draft
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
25 changes: 25 additions & 0 deletions packages/vm/src/worker/signatureWorker.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { ecrecover } from '@ethereumjs/util'
import { parentPort } from 'worker_threads'

Check warning on line 2 in packages/vm/src/worker/signatureWorker.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorker.ts#L1-L2

Added lines #L1 - L2 were not covered by tests

interface SignatureTask {
msgHash: Uint8Array
v: bigint
r: Uint8Array
s: Uint8Array
chainId?: bigint
}

interface SignatureResult {
publicKey: Uint8Array
}

function processSignature(task: SignatureTask): SignatureResult {
const publicKey = ecrecover(task.msgHash, task.v, task.r, task.s, task.chainId)
return { publicKey }
}

Check warning on line 19 in packages/vm/src/worker/signatureWorker.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorker.ts#L16-L19

Added lines #L16 - L19 were not covered by tests

// Listen for messages from the main thread
parentPort?.on('message', (tasks: SignatureTask[]) => {
const results = tasks.map(processSignature)
parentPort?.postMessage(results)
})

Check warning on line 25 in packages/vm/src/worker/signatureWorker.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorker.ts#L22-L25

Added lines #L22 - L25 were not covered by tests
86 changes: 86 additions & 0 deletions packages/vm/src/worker/signatureWorkerPool.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
import { Worker } from 'worker_threads'

Check warning on line 1 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L1

Added line #L1 was not covered by tests

interface SignatureTask {
msgHash: Uint8Array
v: bigint
r: Uint8Array
s: Uint8Array
chainId?: bigint
}

interface SignatureResult {
publicKey: Uint8Array
}

export class SignatureWorkerPool {
private workers: Worker[] = []
private results: Map<number, SignatureResult> = new Map()
private nextTaskId = 0
private pendingResults = 0

Check warning on line 19 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L15-L19

Added lines #L15 - L19 were not covered by tests

constructor(numWorkers: number = 4) {
const workerCode = `

Check warning on line 22 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L21-L22

Added lines #L21 - L22 were not covered by tests
import { parentPort } from 'worker_threads'
import { ecrecover } from '@ethereumjs/util'

parentPort.on('message', (data) => {
const { tasks, taskId } = data
const results = tasks.map(task => {
const publicKey = ecrecover(task.msgHash, task.v, task.r, task.s, task.chainId)
return { publicKey }
})
parentPort.postMessage({ results, taskId })
})
`
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ugh, there is likely no way around this "string-ification of code"? 😬 Or will there be a more "solid" way to do this later on? Anyhow - even if not - guess the price would be worth it. If the snippets are not getting too extensive.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The pain point here is how worker threads work. The two ways to invoke worker threads are to pass the stringified code directly or invoke a separate file (e.g. worker.js). This issue shows how hard this once you integrate typescript into the equation. The worker thread environment doesn't inherit from the top level application so tsx doesn't run (or at least not easily). Nor do any of the tricks for having node run typescript directly. I spent a couple of hours thinking this through and my options are to either compile the code to Javascript (thus meaning we have js source files in our src directory (which is kinda weird) or I have to go the route suggested in the tsx issue and add this extra tsx/cli step to the thread loading step. Happy to go either way at this point.


// Initialize workers
for (let i = 0; i < numWorkers; i++) {
const worker = new Worker(workerCode, { eval: true })

Check warning on line 38 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L37-L38

Added lines #L37 - L38 were not covered by tests

worker.on('message', (data: { results: SignatureResult[]; taskId: number }) => {
const { results, taskId } = data
results.forEach((result, index) => {
this.results.set(taskId + index, result)
})
this.pendingResults--
})

Check warning on line 46 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L40-L46

Added lines #L40 - L46 were not covered by tests

this.workers.push(worker)
}
}

Check warning on line 50 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L48-L50

Added lines #L48 - L50 were not covered by tests

async processBatch(tasks: SignatureTask[]): Promise<Map<number, SignatureResult>> {

Check warning on line 52 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L52

Added line #L52 was not covered by tests
// Clear previous results
this.results.clear()
this.nextTaskId = 0
this.pendingResults = 0

Check warning on line 56 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L54-L56

Added lines #L54 - L56 were not covered by tests

// Calculate batch size per worker
const batchSize = Math.ceil(tasks.length / this.workers.length)

Check warning on line 59 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L59

Added line #L59 was not covered by tests

// Process tasks in parallel
for (let i = 0; i < this.workers.length; i++) {
const start = i * batchSize
const end = Math.min(start + batchSize, tasks.length)
if (start >= tasks.length) break

Check warning on line 65 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L62-L65

Added lines #L62 - L65 were not covered by tests

const batch = tasks.slice(start, end)
this.pendingResults++
this.workers[i].postMessage({ tasks: batch, taskId: this.nextTaskId })
this.nextTaskId += batch.length
}

Check warning on line 71 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L67-L71

Added lines #L67 - L71 were not covered by tests

// Wait for all results
while (this.pendingResults > 0) {
await new Promise((resolve) => setTimeout(resolve, 10))
}

Check warning on line 76 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L74-L76

Added lines #L74 - L76 were not covered by tests

return this.results
}

Check warning on line 79 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L78-L79

Added lines #L78 - L79 were not covered by tests

terminate() {
for (const worker of this.workers) {
worker.terminate()
}
}
}

Check warning on line 86 in packages/vm/src/worker/signatureWorkerPool.ts

View check run for this annotation

Codecov / codecov/patch

packages/vm/src/worker/signatureWorkerPool.ts#L81-L86

Added lines #L81 - L86 were not covered by tests
112 changes: 112 additions & 0 deletions packages/vm/test/benchmark/signatureVerification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
import { LegacyTx } from '@ethereumjs/tx'
import {
bigIntToUnpaddedBytes,
bytesToHex,

Check failure on line 4 in packages/vm/test/benchmark/signatureVerification.ts

View workflow job for this annotation

GitHub Actions / lint / lint

'bytesToHex' is defined but never used. Allowed unused vars must match /^_/u
ecrecover,
equalsBytes,
randomBytes,
} from '@ethereumjs/util'
import { SignatureWorkerPool } from '../../src/worker/signatureWorkerPool.ts'

async function runBenchmark() {
// Create 10 test transactions
const transactions = Array.from({ length: 2000 }, (_, i) => {
return new LegacyTx({
nonce: i,
gasPrice: 1000000000n,
gasLimit: 21000n,
to: '0x0000000000000000000000000000000000000000',
value: 1000000000000000000n,
data: '0x',
})
})

// Sign all transactions
const signedTxs = transactions.map((tx) => tx.sign(randomBytes(32)))

// Precompute all hashes and signature values
const msgHashes = signedTxs.map((tx) => tx.getMessageToVerifySignature())
const vs = signedTxs.map((tx) => tx.v!)
const rs = signedTxs.map((tx) => bigIntToUnpaddedBytes(tx.r!))
const ss = signedTxs.map((tx) => bigIntToUnpaddedBytes(tx.s!))
const chainIds = signedTxs.map((tx) => tx.common.chainId())

console.log('Running benchmark...\n')

Check warning on line 34 in packages/vm/test/benchmark/signatureVerification.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Unexpected console statement

// Sequential verification
console.log('Sequential verification:')

Check warning on line 37 in packages/vm/test/benchmark/signatureVerification.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Unexpected console statement
const startSeq = performance.now()

// Store sequential results
const sequentialResults = new Map()
for (let i = 0; i < signedTxs.length; i++) {
const publicKey = ecrecover(msgHashes[i], vs[i], rs[i], ss[i], 1n)
sequentialResults.set(i, publicKey)
}

const endSeq = performance.now()
console.log(`Time taken: ${endSeq - startSeq}ms\n`)

Check warning on line 48 in packages/vm/test/benchmark/signatureVerification.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Unexpected console statement

// Parallel verification using worker pool
console.log('Parallel verification using worker pool:')

Check warning on line 51 in packages/vm/test/benchmark/signatureVerification.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Unexpected console statement
const startPar = performance.now()

const signatureTasks = msgHashes.map((msgHash, i) => {
return {
msgHash,
v: vs[i],
r: rs[i],
s: ss[i],
chainId: chainIds[i],
}
})

const workerPool = new SignatureWorkerPool(4) // Use 4 workers
const parallelResults = await workerPool.processBatch(signatureTasks)
await workerPool.terminate()

const endPar = performance.now()
console.log(`Time taken: ${endPar - startPar}ms\n`)

Check warning on line 69 in packages/vm/test/benchmark/signatureVerification.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Unexpected console statement

// Print speedup
const speedup = (endSeq - startSeq) / (endPar - startPar)
console.log(`Speedup: ${speedup.toFixed(2)}x`)

Check warning on line 73 in packages/vm/test/benchmark/signatureVerification.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Unexpected console statement

// Print result counts
console.log(

Check warning on line 76 in packages/vm/test/benchmark/signatureVerification.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Unexpected console statement
`\nResults count - Sequential: ${sequentialResults.size}, Parallel: ${parallelResults.size}`,
)

// Verify results match by comparing the stored sequential results with parallel results
console.log('\nVerifying results...')

Check warning on line 81 in packages/vm/test/benchmark/signatureVerification.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Unexpected console statement
let allMatch = true
let missingResults = 0
let mismatchedResults = 0

for (let i = 0; i < signedTxs.length; i++) {
const parallelResult = parallelResults.get(i)
const sequentialResult = sequentialResults.get(i)

if (!parallelResult) {
console.log(`Missing parallel result at index ${i}`)

Check warning on line 91 in packages/vm/test/benchmark/signatureVerification.ts

View workflow job for this annotation

GitHub Actions / lint / lint

Unexpected console statement
missingResults++
allMatch = false
continue
}

if (!equalsBytes(parallelResult.publicKey, sequentialResult)) {
console.log(`Mismatch at index ${i}`)
console.log(`Sequential: ${sequentialResult}`)
console.log(`Parallel: ${parallelResult.publicKey}`)
mismatchedResults++
allMatch = false
}
}

console.log(`\nVerification Summary:`)
console.log(`- All results match: ${allMatch}`)
console.log(`- Missing results: ${missingResults}`)
console.log(`- Mismatched results: ${mismatchedResults}`)
}

runBenchmark().catch(console.error)
Loading