Skip to content

Commit f91195f

Browse files
ryaniocbrzn
andauthored
client: fix eth_call, add remoteBlocks to engine (#1830)
* improve eth_call to use runCall format and return vm internal errors so they display in json rpc * add remoteBlocks, refactor out assembleBlock, return `latestValidHash: null` when SYNCING to follow spec * engine: add parent hash equals to block hash test case in new payload * improve return type of assembleBlock * pass array of blocks to setHead to save pending receipts Co-authored-by: cbrzn <cesarbrazon10@gmail.com>
1 parent 7a3549a commit f91195f

File tree

5 files changed

+212
-111
lines changed

5 files changed

+212
-111
lines changed

packages/client/lib/execution/vmexecution.ts

Lines changed: 12 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -109,17 +109,21 @@ export class VMExecution extends Execution {
109109
}
110110

111111
/**
112-
* Sets the chain to set the specified new head block.
112+
* Sets the chain to a new head block.
113113
* Should only be used after {@link VMExecution.runWithoutSetHead}
114+
* @param blocks Array of blocks to save pending receipts and set the last block as the head
114115
*/
115-
async setHead(block: Block): Promise<void> {
116-
await this.chain.putBlocks([block], true)
117-
const receipts = this.pendingReceipts?.get(block.hash().toString('hex'))
118-
if (receipts) {
119-
void this.receiptsManager?.saveReceipts(block, receipts)
120-
this.pendingReceipts?.delete(block.hash().toString('hex'))
116+
async setHead(blocks: Block[]): Promise<void> {
117+
await this.chain.putBlocks(blocks, true)
118+
for (const block of blocks) {
119+
const receipts = this.pendingReceipts?.get(block.hash().toString('hex'))
120+
if (receipts) {
121+
void this.receiptsManager?.saveReceipts(block, receipts)
122+
this.pendingReceipts?.delete(block.hash().toString('hex'))
123+
}
121124
}
122-
await this.chain.blockchain.setIteratorHead('vm', block.hash())
125+
const head = blocks[blocks.length - 1]
126+
await this.chain.blockchain.setIteratorHead('vm', head.hash())
123127
}
124128

125129
/**

packages/client/lib/rpc/modules/engine.ts

Lines changed: 114 additions & 79 deletions
Original file line numberDiff line numberDiff line change
@@ -166,6 +166,76 @@ const validateTerminalBlock = async (block: Block, chain: Chain): Promise<boolea
166166
return blockTd.gte(ttd) && parentBlockTd.lt(ttd)
167167
}
168168

169+
/**
170+
* Returns a block from a payload.
171+
* If errors, returns {@link PayloadStatusV1}
172+
*/
173+
const assembleBlock = async (
174+
payload: ExecutionPayloadV1,
175+
chain: Chain
176+
): Promise<{ block?: Block; error?: PayloadStatusV1 }> => {
177+
const {
178+
blockNumber: number,
179+
receiptsRoot: receiptTrie,
180+
prevRandao: mixHash,
181+
feeRecipient: coinbase,
182+
transactions,
183+
} = payload
184+
const { config } = chain
185+
const { chainCommon: common } = config
186+
187+
const txs = []
188+
for (const [index, serializedTx] of transactions.entries()) {
189+
try {
190+
const tx = TransactionFactory.fromSerializedData(toBuffer(serializedTx), { common })
191+
txs.push(tx)
192+
} catch (error) {
193+
const validationError = `Invalid tx at index ${index}: ${error}`
194+
config.logger.error(validationError)
195+
const latestValidHash = await validHash(toBuffer(payload.parentHash), chain)
196+
const response = { status: Status.INVALID, latestValidHash, validationError }
197+
return { error: response }
198+
}
199+
}
200+
201+
const transactionsTrie = await txsTrieRoot(txs)
202+
const header: HeaderData = {
203+
...payload,
204+
number,
205+
receiptTrie,
206+
transactionsTrie,
207+
mixHash,
208+
coinbase,
209+
}
210+
211+
let block: Block
212+
try {
213+
block = Block.fromBlockData(
214+
{ header, transactions: txs },
215+
{ common, hardforkByTD: chain.headers.td }
216+
)
217+
218+
// Verify blockHash matches payload
219+
if (!block.hash().equals(toBuffer(payload.blockHash))) {
220+
const validationError = `Invalid blockHash, expected: ${
221+
payload.blockHash
222+
}, received: ${bufferToHex(block.hash())}`
223+
config.logger.debug(validationError)
224+
const latestValidHash = await validHash(toBuffer(header.parentHash), chain)
225+
const response = { status: Status.INVALID_BLOCK_HASH, latestValidHash, validationError }
226+
return { error: response }
227+
}
228+
} catch (error) {
229+
const validationError = `Error verifying block during init: ${error}`
230+
config.logger.debug(validationError)
231+
const latestValidHash = await validHash(toBuffer(header.parentHash), chain)
232+
const response = { status: Status.INVALID, latestValidHash, validationError }
233+
return { error: response }
234+
}
235+
236+
return { block }
237+
}
238+
169239
/**
170240
* engine_* RPC module
171241
* @memberof module:rpc/modules
@@ -180,6 +250,7 @@ export class Engine {
180250
private vm: VM
181251
private txPool: TxPool
182252
private pendingBlock: PendingBlock
253+
private remoteBlocks: Map<String, Block>
183254
private connectionManager: CLConnectionManager
184255

185256
/**
@@ -200,6 +271,7 @@ export class Engine {
200271
this.txPool = (this.service.synchronizer as FullSynchronizer).txPool
201272
this.connectionManager = new CLConnectionManager({ config: this.chain.config })
202273
this.pendingBlock = new PendingBlock({ config: this.config, txPool: this.txPool })
274+
this.remoteBlocks = new Map()
203275

204276
this.newPayloadV1 = middleware(this.newPayloadV1.bind(this), 1, [
205277
[
@@ -277,17 +349,21 @@ export class Engine {
277349
* 3. validationError: String|null - validation error message
278350
*/
279351
async newPayloadV1(params: [ExecutionPayloadV1]): Promise<PayloadStatusV1> {
280-
const [payloadData] = params
281-
const {
282-
blockNumber: number,
283-
receiptsRoot: receiptTrie,
284-
prevRandao: mixHash,
285-
feeRecipient: coinbase,
286-
transactions,
287-
parentHash,
288-
blockHash,
289-
} = payloadData
290-
const { chainCommon: common } = this.config
352+
const [payload] = params
353+
const { parentHash, blockHash } = payload
354+
355+
const { block, error } = await assembleBlock(payload, this.chain)
356+
if (!block || error) {
357+
let response = error
358+
if (!response) {
359+
const validationError = `Error assembling block during init`
360+
this.config.logger.debug(validationError)
361+
const latestValidHash = await validHash(toBuffer(payload.parentHash), this.chain)
362+
response = { status: Status.INVALID, latestValidHash, validationError }
363+
}
364+
this.connectionManager.lastNewPayload({ payload, response })
365+
return response
366+
}
291367

292368
const blockExists = await validHash(toBuffer(blockHash), this.chain)
293369
if (blockExists) {
@@ -301,9 +377,9 @@ export class Engine {
301377
}
302378

303379
try {
304-
const block = await this.chain.getBlock(toBuffer(parentHash))
305-
if (!block._common.gteHardfork(Hardfork.Merge)) {
306-
const validTerminalBlock = await validateTerminalBlock(block, this.chain)
380+
const parent = await this.chain.getBlock(toBuffer(parentHash))
381+
if (!parent._common.gteHardfork(Hardfork.Merge)) {
382+
const validTerminalBlock = await validateTerminalBlock(parent, this.chain)
307383
if (!validTerminalBlock) {
308384
const response = {
309385
status: Status.INVALID_TERMINAL_BLOCK,
@@ -315,65 +391,15 @@ export class Engine {
315391
}
316392
}
317393
} catch (error: any) {
394+
// Stash the block for a potential forced forkchoice update to it later.
395+
this.remoteBlocks.set(block.hash().toString('hex'), block)
318396
// TODO if we can't find the parent and the block doesn't extend the canonical chain,
319397
// return ACCEPTED when optimistic sync is supported to store the block for later processing
320398
const response = { status: Status.SYNCING, validationError: null, latestValidHash: null }
321399
this.connectionManager.lastNewPayload({ payload: params[0], response })
322400
return response
323401
}
324402

325-
const txs = []
326-
for (const [index, serializedTx] of transactions.entries()) {
327-
try {
328-
const tx = TransactionFactory.fromSerializedData(toBuffer(serializedTx), { common })
329-
txs.push(tx)
330-
} catch (error) {
331-
const validationError = `Invalid tx at index ${index}: ${error}`
332-
this.config.logger.error(validationError)
333-
const latestValidHash = await validHash(toBuffer(payloadData.parentHash), this.chain)
334-
const response = { status: Status.INVALID, latestValidHash, validationError }
335-
this.connectionManager.lastNewPayload({ payload: params[0], response })
336-
return response
337-
}
338-
}
339-
340-
const transactionsTrie = await txsTrieRoot(txs)
341-
const header: HeaderData = {
342-
...payloadData,
343-
number,
344-
receiptTrie,
345-
transactionsTrie,
346-
mixHash,
347-
coinbase,
348-
}
349-
350-
let block: Block
351-
try {
352-
block = Block.fromBlockData(
353-
{ header, transactions: txs },
354-
{ common, hardforkByTD: this.chain.headers.td }
355-
)
356-
357-
// Verify blockHash matches payload
358-
if (!block.hash().equals(toBuffer(payloadData.blockHash))) {
359-
const validationError = `Invalid blockHash, expected: ${
360-
payloadData.blockHash
361-
}, received: ${bufferToHex(block.hash())}`
362-
this.config.logger.debug(validationError)
363-
const latestValidHash = await validHash(toBuffer(header.parentHash), this.chain)
364-
const response = { status: Status.INVALID_BLOCK_HASH, latestValidHash, validationError }
365-
this.connectionManager.lastNewPayload({ payload: params[0], response })
366-
return response
367-
}
368-
} catch (error) {
369-
const validationError = `Error verifying block during init: ${error}`
370-
this.config.logger.debug(validationError)
371-
const latestValidHash = await validHash(toBuffer(header.parentHash), this.chain)
372-
const response = { status: Status.INVALID, latestValidHash, validationError }
373-
this.connectionManager.lastNewPayload({ payload: params[0], response })
374-
return response
375-
}
376-
377403
const vmHead = this.chain.headers.latest!
378404
let blocks: Block[]
379405
try {
@@ -441,14 +467,20 @@ export class Engine {
441467
try {
442468
headBlock = await this.chain.getBlock(toBuffer(headBlockHash))
443469
} catch (error) {
444-
const latestValidHash = bufferToHex(this.chain.headers.latest!.hash())
445-
const payloadStatus = { status: Status.SYNCING, latestValidHash, validationError: null }
446-
const response = { payloadStatus, payloadId: null }
447-
this.connectionManager.lastForkchoiceUpdate({
448-
state: params[0],
449-
response,
450-
})
451-
return response
470+
headBlock = this.remoteBlocks.get(headBlockHash.slice(2)) as Block
471+
if (!headBlock) {
472+
const payloadStatus = {
473+
status: Status.SYNCING,
474+
latestValidHash: null,
475+
validationError: null,
476+
}
477+
const response = { payloadStatus, payloadId: null }
478+
this.connectionManager.lastForkchoiceUpdate({
479+
state: params[0],
480+
response,
481+
})
482+
return response
483+
}
452484
}
453485

454486
if (!headBlock._common.gteHardfork(Hardfork.Merge)) {
@@ -498,8 +530,11 @@ export class Engine {
498530
this.chain
499531
)
500532
} catch (error) {
501-
const latestValidHash = await validHash(headBlock.header.parentHash, this.chain)
502-
const payloadStatus = { status: Status.SYNCING, latestValidHash, validationError: null }
533+
const payloadStatus = {
534+
status: Status.SYNCING,
535+
latestValidHash: null,
536+
validationError: null,
537+
}
503538
const response = { payloadStatus, payloadId: null }
504539
this.connectionManager.lastForkchoiceUpdate({
505540
state: params[0],
@@ -510,7 +545,7 @@ export class Engine {
510545
}
511546

512547
const blocks = [...parentBlocks, headBlock]
513-
await this.execution.setHead(headBlock)
548+
await this.execution.setHead(blocks)
514549
this.txPool.removeNewBlockTxs(blocks)
515550

516551
const timeDiff = new Date().getTime() / 1000 - headBlock.header.timestamp.toNumber()
@@ -556,7 +591,7 @@ export class Engine {
556591
mixHash: prevRandao,
557592
coinbase: suggestedFeeRecipient,
558593
})
559-
const latestValidHash = await validHash(headBlock.header.parentHash, this.chain)
594+
const latestValidHash = await validHash(headBlock.hash(), this.chain)
560595
const payloadStatus = { status: Status.VALID, latestValidHash, validationError: null }
561596
const response = { payloadStatus, payloadId: bufferToHex(payloadId) }
562597
this.connectionManager.lastForkchoiceUpdate({
@@ -567,7 +602,7 @@ export class Engine {
567602
return response
568603
}
569604

570-
const latestValidHash = await validHash(headBlock.header.hash(), this.chain)
605+
const latestValidHash = await validHash(headBlock.hash(), this.chain)
571606
const payloadStatus = { status: Status.VALID, latestValidHash, validationError: null }
572607
const response = { payloadStatus, payloadId: null }
573608
this.connectionManager.lastForkchoiceUpdate({

packages/client/lib/rpc/modules/eth.ts

Lines changed: 33 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ import {
1818
rlp,
1919
toBuffer,
2020
setLengthLeft,
21+
toType,
22+
TypeOutput,
2123
} from 'ethereumjs-util'
2224
import { middleware, validators } from '../validation'
2325
import { INTERNAL_ERROR, INVALID_PARAMS, PARSE_ERROR } from '../error-code'
@@ -459,23 +461,25 @@ export class Eth {
459461
const vm = this._vm.copy()
460462
await vm.stateManager.setStateRoot(block.header.stateRoot)
461463

462-
if (!transaction.gas) {
463-
// If no gas limit is specified use the last block gas limit as an upper bound.
464-
const latest = await vm.blockchain.getLatestHeader()
465-
transaction.gas = latest.gasLimit as any
466-
}
464+
const { from, to, gas: gasLimit, gasPrice, value, data } = transaction
467465

468-
const txData = { ...transaction, gasLimit: transaction.gas }
469-
const tx = Transaction.fromTxData(txData, { common: vm._common, freeze: false })
470-
471-
// set from address
472-
const from = transaction.from ? Address.fromString(transaction.from) : Address.zero()
473-
tx.getSenderAddress = () => {
474-
return from
466+
try {
467+
const runCallOpts = {
468+
caller: from ? Address.fromString(from) : undefined,
469+
to: to ? Address.fromString(to) : undefined,
470+
gasLimit: toType(gasLimit, TypeOutput.BN),
471+
gasPrice: toType(gasPrice, TypeOutput.BN),
472+
value: toType(value, TypeOutput.BN),
473+
data: data ? toBuffer(data) : undefined,
474+
}
475+
const { execResult } = await vm.runCall(runCallOpts)
476+
return bufferToHex(execResult.returnValue)
477+
} catch (error: any) {
478+
throw {
479+
code: INTERNAL_ERROR,
480+
message: error.message.toString(),
481+
}
475482
}
476-
477-
const { execResult } = await vm.runTx({ tx, skipNonce: true })
478-
return bufferToHex(execResult.returnValue)
479483
}
480484

481485
/**
@@ -530,13 +534,20 @@ export class Eth {
530534
return from
531535
}
532536

533-
const { gasUsed } = await vm.runTx({
534-
tx,
535-
skipNonce: true,
536-
skipBalance: true,
537-
skipBlockGasLimitValidation: true,
538-
})
539-
return bnToHex(gasUsed)
537+
try {
538+
const { gasUsed } = await vm.runTx({
539+
tx,
540+
skipNonce: true,
541+
skipBalance: true,
542+
skipBlockGasLimitValidation: true,
543+
})
544+
return bnToHex(gasUsed)
545+
} catch (error: any) {
546+
throw {
547+
code: INTERNAL_ERROR,
548+
message: error.message.toString(),
549+
}
550+
}
540551
}
541552

542553
/**

0 commit comments

Comments
 (0)