Skip to content

Mock Engine API req/resp in tests - take 2 #2639

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

Closed
wants to merge 3 commits into from
Closed
Show file tree
Hide file tree
Changes from all 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
6 changes: 3 additions & 3 deletions setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -510,8 +510,8 @@ def sundry_functions(cls) -> str:
ExecutionState = Any


def get_pow_block(hash: Bytes32) -> PowBlock:
return PowBlock(block_hash=hash, parent_hash=Bytes32(), total_difficulty=uint256(0), difficulty=uint256(0))
def get_pow_block(block_hash: Bytes32) -> PowBlock:
return PowBlock(block_hash=block_hash, parent_hash=Bytes32(), total_difficulty=uint256(0), difficulty=uint256(0))


def get_execution_state(execution_state_root: Bytes32) -> ExecutionState:
Expand All @@ -537,7 +537,7 @@ def prepare_payload(self: ExecutionEngine,
parent_hash: Hash32,
timestamp: uint64,
random: Bytes32,
feeRecipient: ExecutionAddress) -> PayloadId:
fee_recipient: ExecutionAddress) -> PayloadId:
raise NotImplementedError("no default block production")

def get_payload(self: ExecutionEngine, payload_id: PayloadId) -> ExecutionPayload:
Expand Down
221 changes: 221 additions & 0 deletions tests/core/pyspec/eth2spec/test/helpers/engine_apis.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,221 @@
from enum import Enum
from eth_utils import encode_hex

from eth2spec.test.exceptions import BlockNotFoundException
from eth2spec.test.helpers.execution_payload import (
build_execution_payload,
)
from eth2spec.test.helpers.fork_choice import (
get_pow_block_file_name,
)


class StatusCode(Enum):
VALID = "VALID"
INVALID = "INVALID"
SYNCING = "SYNCING"


def to_json_rpc_request(method, params):
return {
'method': method,
'params': params,
}


def to_json_rpc_response(result, error=False):
return {
'error': error,
'result': result,
}


def execution_payload_to_json(execution_payload):
return {
"parentHash": encode_hex(execution_payload.parent_hash),
"coinbase": encode_hex(execution_payload.coinbase),
"stateRoot": encode_hex(execution_payload.state_root),
"receiptRoot": encode_hex(execution_payload.receipt_root),
"logsBloom": encode_hex(execution_payload.logs_bloom),
"random": encode_hex(execution_payload.random),
"blockNumber": int(execution_payload.block_number),
"gasLimit": int(execution_payload.gas_limit),
"gasUsed": int(execution_payload.gas_used),
"timestamp": int(execution_payload.timestamp),
"extraData": encode_hex(execution_payload.extra_data),
"baseFeePerGas": int.from_bytes(execution_payload.base_fee_per_gas, byteorder='little'),
"blockHash": encode_hex(execution_payload.block_hash),
"transactions": [encode_hex(tx) for tx in execution_payload.transactions],
}


def run_prepare_execution_payload_with_mock_engine_prepare_payload(
spec,
state,
pow_chain,
parent_hash,
timestamp,
random,
fee_recipient,
payload_id,
test_steps,
dump_rpc=False):
class TestEngine(spec.NoopExecutionEngine):
def prepare_payload(self,
parent_hash,
timestamp,
random,
fee_recipient):
if dump_rpc:
req = to_json_rpc_request(
method='engine_preparePayload',
params={
"parentHash": encode_hex(parent_hash),
"timestamp": int(timestamp),
"random": encode_hex(random),
"feeRecipient": encode_hex(fee_recipient),
}
)
resp = to_json_rpc_response(
result={
"payloadId": int(payload_id)
},
)
test_steps.append({
'_block_production': {
'prepare_execution_payload': {
'engine_api': {
'request': req,
'response': resp
}
}
}
})
return payload_id

return spec.prepare_execution_payload(
state=state,
pow_chain=pow_chain,
fee_recipient=fee_recipient,
execution_engine=TestEngine(),
)


def run_get_execution_payload_with_mock_engine_get_payload(
spec,
state,
payload_id,
parent_hash,
fee_recipient,
test_steps,
dump_rpc=False):
timestamp = spec.compute_timestamp_at_slot(state, state.slot + 1)
random = spec.get_randao_mix(state, spec.get_current_epoch(state))

class TestEngine(spec.NoopExecutionEngine):
def get_payload(self, payload_id):
execution_payload = build_execution_payload(
spec,
state,
parent_hash=parent_hash,
timestamp=timestamp,
random=random,
coinbase=fee_recipient,
)
if dump_rpc:
req = to_json_rpc_request(
method='engine_getPayload',
params={
"payloadId": int(payload_id)
},
)
resp = to_json_rpc_response(
result={
"executionPayload": execution_payload_to_json(execution_payload),
},
)
test_steps.append({
'_block_production': {
'get_execution_payload': {
'engine_api': {
'request': req,
'response': resp
}
}
}
})
return execution_payload

return spec.get_execution_payload(payload_id, TestEngine())


def with_pow_blocks_and_execute_payload(
spec,
pow_chain,
status,
func,
test_steps
):
def get_pow_block(block_hash):
for block in pow_chain: # type: ignore
if block.block_hash == block_hash:
test_steps.append({
'_to_next_on_block': {
'get_pow_block': {
'input': {
'block_hash': encode_hex(block_hash)
},
'output': {
'result': get_pow_block_file_name(block),
},
}
}
})
return block
raise BlockNotFoundException()

class TestEngine(spec.NoopExecutionEngine):
def execute_payload(self, execution_payload):
req = to_json_rpc_request(
method='engine_executePayload',
params=execution_payload_to_json(execution_payload),
)
resp = to_json_rpc_response(
result={
"status": status.value
},
)
test_steps.append({
'_to_next_on_block': {
'process_execution_payload': {
'engine_api': {
'request': req,
'response': resp
}
}
}
})
return status == StatusCode.VALID

# Spec stub replacement
get_pow_block_backup = spec.get_pow_block
spec.get_pow_block = get_pow_block

execute_engine_backup = spec.EXECUTION_ENGINE
spec.EXECUTION_ENGINE = TestEngine()

class AtomicBoolean():
value = False
is_called = AtomicBoolean()

def wrap(flag: AtomicBoolean):
yield from func()
flag.value = True

try:
yield from wrap(is_called)
finally:
# Revert replacement
spec.get_pow_block = get_pow_block_backup
spec.EXECUTION_ENGINE = execute_engine_backup
assert is_called.value
83 changes: 63 additions & 20 deletions tests/core/pyspec/eth2spec/test/helpers/execution_payload.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,75 @@
def build_empty_execution_payload(spec, state, randao_mix=None):
def build_empty_execution_payload(spec, state):
"""
Assuming a pre-state of the same slot, build a valid ExecutionPayload without any transactions.
"""
latest = state.latest_execution_payload_header
timestamp = spec.compute_timestamp_at_slot(state, state.slot)
empty_txs = spec.List[spec.Transaction, spec.MAX_TRANSACTIONS_PER_PAYLOAD]()
return build_execution_payload(spec, state)


if randao_mix is None:
randao_mix = spec.get_randao_mix(state, spec.get_current_epoch(state))
def build_execution_payload(spec,
state,
*,
parent_hash=None,
coinbase=None,
state_root=None,
receipt_root=None,
logs_bloom=None,
block_number=None,
random=None,
gas_limit=None,
gas_used=0,
timestamp=None,
extra_data=None,
base_fee_per_gas=None,
block_hash=None,
transactions=None):
latest = state.latest_execution_payload_header
# By default, assuming a pre-state of the same slot, build a valid ExecutionPayload without any transactions.
if parent_hash is None:
parent_hash = latest.block_hash
if coinbase is None:
coinbase = spec.ExecutionAddress()
if state_root is None:
state_root = latest.state_root
if receipt_root is None:
receipt_root = b"no receipts here" + b"\x00" * 16 # TODO: root of empty MPT may be better.
if logs_bloom is None:
logs_bloom = spec.ByteVector[spec.BYTES_PER_LOGS_BLOOM]() # TODO: zeroed logs bloom for empty logs ok?
if block_number is None:
block_number = latest.block_number + 1
if random is None:
random = spec.get_randao_mix(state, spec.get_current_epoch(state))
if gas_limit is None:
gas_limit = latest.gas_limit # retain same limit
if timestamp is None:
timestamp = spec.compute_timestamp_at_slot(state, state.slot)
if extra_data is None:
extra_data = spec.ByteList[spec.MAX_EXTRA_DATA_BYTES]()
if base_fee_per_gas is None:
base_fee_per_gas = latest.base_fee_per_gas # retain same base_fee
if transactions is None:
transactions = spec.List[spec.Transaction, spec.MAX_TRANSACTIONS_PER_PAYLOAD]()

payload = spec.ExecutionPayload(
parent_hash=latest.block_hash,
coinbase=spec.ExecutionAddress(),
state_root=latest.state_root, # no changes to the state
receipt_root=b"no receipts here" + b"\x00" * 16, # TODO: root of empty MPT may be better.
logs_bloom=spec.ByteVector[spec.BYTES_PER_LOGS_BLOOM](), # TODO: zeroed logs bloom for empty logs ok?
block_number=latest.block_number + 1,
random=randao_mix,
gas_limit=latest.gas_limit, # retain same limit
gas_used=0, # empty block, 0 gas
parent_hash=parent_hash,
coinbase=coinbase,
state_root=state_root,
receipt_root=receipt_root,
logs_bloom=logs_bloom,
block_number=block_number,
random=random,
gas_limit=gas_limit,
gas_used=gas_used,
timestamp=timestamp,
extra_data=spec.ByteList[spec.MAX_EXTRA_DATA_BYTES](),
base_fee_per_gas=latest.base_fee_per_gas, # retain same base_fee
block_hash=spec.Hash32(),
transactions=empty_txs,
extra_data=extra_data,
base_fee_per_gas=base_fee_per_gas,
transactions=transactions,
)

# TODO: real RLP + block hash logic would be nice, requires RLP and keccak256 dependency however.
payload.block_hash = spec.Hash32(spec.hash(payload.hash_tree_root() + b"FAKE RLP HASH"))
payload.block_hash = (
block_hash if block_hash is not None
else spec.Hash32(spec.hash(parent_hash + b"FAKE RLP HASH"))
)

return payload

Expand Down
Loading