Skip to content

Commit

Permalink
Prysm support
Browse files Browse the repository at this point in the history
* Add prysm mock endpoints
* Add separate github action steps for each beacon clients
* Update Readme

Related [lido-dao#198](lidofinance/core#198)
  • Loading branch information
onionglass committed Dec 7, 2020
1 parent ec8f359 commit 230897b
Show file tree
Hide file tree
Showing 8 changed files with 238 additions and 264 deletions.
7 changes: 5 additions & 2 deletions .github/workflows/linters.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,5 +29,8 @@ jobs:
- name: Lint
run: ./check_code.sh
continue-on-error: true
- name: Test with pytest
run: ./run_tests.sh
- name: Test with pytest and Lightouse beacon
run: ./run_tests.sh 1

- name: Test with pytest and Prysm beacon
run: ./run_tests.sh 2
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,10 +87,10 @@ To run tests you need all test dependencies installed
pip install -U -r requirements-test.txt
```

To run tests just type
To run tests

```python
pytest
```bash
./run_tests.sh
```

## Helpers
Expand Down
5 changes: 1 addition & 4 deletions app/beacon.py
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,7 @@ def get_beacon(provider, slots_per_epoch):
return Lighthouse(provider, slots_per_epoch)
version = requests.get(urljoin(provider, 'eth/v1alpha1/node/version')).text
if 'Prysm' in version:
logging.error(f'Not supporting Prysm beacon node')
exit(1)
# TODO: fix me
# return Prysm(provider, slots_per_epoch)
return Prysm(provider, slots_per_epoch)
raise ValueError('Unknown beacon')


Expand Down
76 changes: 29 additions & 47 deletions app/oracle.py
Original file line number Diff line number Diff line change
Expand Up @@ -99,8 +99,10 @@

# Get Beacon specs from contract
beacon_spec = oracle.functions.beaconSpec().call()
epochs_per_frame = beacon_spec[0]
slots_per_epoch = beacon_spec[1]
seconds_per_slot = beacon_spec[2]
genesis_time = beacon_spec[3]

beacon = get_beacon(beacon_provider, slots_per_epoch) # >>lighthouse<< / prism implementation of ETH 2.0

Expand All @@ -118,6 +120,8 @@
logging.info(f'Registry contract address: {registry_address} (auto-discovered)')
logging.info(f'Seconds per slot: {seconds_per_slot} (auto-discovered)')
logging.info(f'Slots per epoch: {slots_per_epoch} (auto-discovered)')
logging.info(f'Epochs per frame: {epochs_per_frame} (auto-discovered)')
logging.info(f'Genesis time: {genesis_time} (auto-discovered)')


def build_report_beacon_tx(reportable_epoch, sum_balance, validators_on_beacon): # hash tx
Expand All @@ -127,33 +131,28 @@ def build_report_beacon_tx(reportable_epoch, sum_balance, validators_on_beacon):


def sign_and_send_tx(tx):
logging.info('Prepearing to send a tx...')

if not run_as_daemon:
time.sleep(5) # To be able to Ctrl + C

logging.info('Preparing TX... CTRL-C to abort')
time.sleep(3) # To be able to Ctrl + C
tx['nonce'] = w3.eth.getTransactionCount(
account.address
) # Get correct transaction nonce for sender from the node
signed = w3.eth.account.signTransaction(tx, account.privateKey)

logging.info(f'TX hash: {signed.hash.hex()} ... CTRL-C to abort')
time.sleep(3)
logging.info(f'Sending TX... CTRL-C to abort')
time.sleep(3)
tx_hash = w3.eth.sendRawTransaction(signed.rawTransaction)
logging.info('Transaction in progress...')

logging.info('TX has been sent. Waiting for receipt...')
tx_receipt = w3.eth.waitForTransactionReceipt(tx_hash)

str_tx_hash = '0x' + binascii.hexlify(tx_receipt.transactionHash).decode()
logging.info(f'Transaction hash: {str_tx_hash}')

if tx_receipt.status == 1:
logging.info('Transaction successful')
logging.info('TX successful')
else:
logging.warning('Transaction reverted')
logging.warning('TX reverted')
logging.warning(tx_receipt)


def prompt(prompt_message, prompt_end):
logging.warning(prompt_message)
print(prompt_message, end='')
while True:
choice = input().lower()
if choice == 'y':
Expand All @@ -167,50 +166,33 @@ def prompt(prompt_message, prompt_end):

logging.info('Starting the main loop')
while True:

# Get the frame and validators keys from ETH1 side
# Get the the epoch that is both finalized and reportable
current_frame = oracle.functions.getCurrentFrame().call()
reportable_epoch = current_frame[0]
logging.info(f'Reportable epoch: {reportable_epoch}')
potentially_reportable_epoch = current_frame[0]
logging.info(f'Potentially reportable epoch: {potentially_reportable_epoch} (from ETH1 contract)')
finalized_epoch_beacon = beacon.get_finalized_epoch()
logging.info(f'Last finalized epoch: {finalized_epoch_beacon} (from Beacon)')
reportable_epoch = min(finalized_epoch_beacon, potentially_reportable_epoch) // epochs_per_frame * epochs_per_frame
reportable_slot = reportable_epoch * slots_per_epoch
logging.info(f'Reportable state: epoch:{reportable_epoch} slot:{reportable_slot}')

validators_keys = get_validators_keys(registry)
logging.info(f'Total validator keys in registry: {len(validators_keys)}')

# Wait for the epoch finalization on the beacon chain
while True:

finalized_epoch = beacon.get_finalized_epoch() # take into account only finalized epoch
reportable_slot = reportable_epoch * slots_per_epoch # it's possible that in epoch there are less slots

if reportable_epoch > finalized_epoch:
# The reportable epoch received from the contract
# is not finalized on the beacon chain so we are waiting
logging.info(
f'Reportable epoch: {reportable_epoch} is not finalized on Beacon. Finalized: {finalized_epoch}. Wait {await_time_in_sec} s'
)
time.sleep(await_time_in_sec)
continue
else:
logging.info(f'Reportable epoch ({reportable_epoch}) is finalized on beacon chain.')
break

# At this point the slot is finalized on the beacon
# so we are able to retrieve validators set and balances
sum_balance, validators_on_beacon = beacon.get_balances(reportable_slot, validators_keys)
logging.info(f'ReportBeacon transaction arguments:')
logging.info(f'Reportable epoch: {reportable_epoch} Slot: {reportable_slot}')
logging.info(f'Sum balance in wei: {sum_balance}')
logging.info(f'Lido validators on Beacon chain: {validators_on_beacon}')

logging.info(f'Total balance on Beacon: {sum_balance} wei')
logging.info(f'Lido validators on Beacon: {validators_on_beacon}')
logging.info(f'Tx call data: oracle.reportBeacon({reportable_epoch}, {sum_balance}, {validators_on_beacon})')
if not dry_run:
try:
tx = build_report_beacon_tx(reportable_epoch, sum_balance, validators_on_beacon)
# Create the tx and execute it locally to check validity
w3.eth.call(tx)
logging.info('Calling tx locally is succeeded. Sending it to the network')
logging.info('Calling tx locally is succeeded.')
if run_as_daemon:
sign_and_send_tx(tx)
else:
logging.info(f'Tx data: {tx.__repr__()}')
print(f'Tx data: {tx.__repr__()}')
if prompt('Should we sent this TX? [y/n]: ', ''):
sign_and_send_tx(tx)
except Exception as exc:
Expand All @@ -232,5 +214,5 @@ def prompt(prompt_message, prompt_end):
logging.info('We are in single-iteration mode, so exiting. Set DAEMON=1 env to run in the loop.')
break

logging.info(f'We are in DAEMON mode. Sleep {await_time_in_sec} s.')
logging.info(f'We are in DAEMON mode. Sleep {await_time_in_sec} s and continue')
time.sleep(await_time_in_sec)
121 changes: 121 additions & 0 deletions helpers/eth_nodes_mock.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,121 @@
import json
from time import sleep
from aiohttp import web

routes = web.RouteTableDef()

with open('tests/responses.json', 'r') as file:
responses = json.loads(file.read())

lighthouse_responses = responses['lighthouse']
prysm_responses = responses['prysm']


@routes.get('/mock/set/{beacon}')
async def set_beacon(request):
beacon = int(request.match_info['beacon'])
if beacon == 1:
request.app['ligthouse'] = True
request.app['prysm'] = not request.app['ligthouse']
print('mock set to Lighthouse')
return web.json_response('mock set to Lighthouse')
else:
request.app['prysm'] = True
request.app['ligthouse'] = not request.app['prysm']
print('mock set to Prysm')
return web.json_response('mock set to Prysm')


@routes.get('/eth/v1/node/version')
async def beacon_ver(request):
if request.app['ligthouse']:
return web.json_response(lighthouse_responses['version'])
return web.json_response('404: Not Found')


@routes.get('/eth/v1/beacon/states/head/finality_checkpoints')
async def beacon_cp(request):
if request.app['ligthouse']:
return web.json_response(lighthouse_responses['finalized_epoch'])
return web.json_response('404: Not Found')


@routes.get('/eth/v1alpha1/node/version')
async def prysm_ver(request):
if request.app['prysm']:
return web.json_response(prysm_responses['version'])
return web.json_response('404: Not Found')


@routes.get('/eth/v1alpha1/beacon/chainhead')
async def beacon_prysm__cp(request):
if request.app['prysm']:
return web.json_response(prysm_responses['head'])
return web.json_response('404: Not Found')


@routes.get('/eth/v1alpha1/validators/balances')
async def validators_prysm(request):
if request.app['prysm']:
return web.json_response(prysm_responses['validators'])
return web.json_response('404: Not Found')


@routes.post('/')
async def eth1(request):
req = await request.json()
resp = {}
if 'jsonrpc' not in req:
raise Exception("It's not a JSON PRC request")
resp['jsonrpc'] = req['jsonrpc']
if 'id' not in req:
raise Exception("It's not a JSON PRC request")
resp['id'] = req['id']
if 'method' not in req:
raise Exception("It's not a JSON PRC request")
if 'params' not in req:
raise Exception("Params are absent")
print(f"Received ETH1 request: {req}")
if req['method'] == 'eth_chainId':
resp["result"] = "0x5"
print(f"Response: {resp}")
if req['method'] == 'eth_gasPrice':
resp["result"] = '0x3b9aca00'
if req['method'] == 'eth_getTransactionCount':
resp["result"] = '0x2'
if req['method'] == 'eth_sendRawTransaction':
resp["result"] = '0x2'
if req['method'] == 'eth_getTransactionReceipt':
resp['result'] = {"blockHash": "0xa3a679373fa4f98bb4bd638042f2550ecff5171194a1a9d132a6d7237b50fe0d", "blockNumber": "0x1079", "contractAddress": None, "cumulativeGasUsed": "0x18d3c", "from": "0x656e544deab532e9f5b8b8079b3809aa1757fb0d", "gasUsed": "0x18d3c", "logs": [
], "logsBloom": "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "status": "0x1", "to": "0xcd3db5ca818a645359e09543cc0e5b7bb9593229", "transactionHash": "0x4624ea5e5f8512a994abf68a5999bc921bd47cafec48920f58306b5c3afefda3", "transactionIndex": "0x0"}
if req['method'] == 'eth_call':
if req['params'][0]['data'] == '0x833b1fce':
resp["result"] = "0x000000000000000000000000cd3db5ca818a645359e09543cc0e5b7bb9593229"
elif req['params'][0]['data'] == '0x27a099d8':
resp["result"] = "0x0000000000000000000000007faf80e96530e5cd13a1f35701fcc6b334b2fd75"
elif req['params'][0]['data'] == '0x5aeff123':
resp["result"] = "0x000000000000000000000000000000000000000000000000000000000000001400000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000005fcbcdd0"
elif req['params'][0]['data'] == '0x72f79b13':
resp["result"] = "0x0000000000000000000000000000000000000000000000000000000000000474000000000000000000000000000000000000000000000000000000005fcbf170000000000000000000000000000000000000000000000000000000005fcbf20f"
elif req['params'][0]['data'] == '0xa70c70e4':
resp["result"] = "0x0000000000000000000000000000000000000000000000000000000000000000"
elif req['params'][0]['data'] == '0xdb9887ea0000000000000000000000000000000000000000000000000000000000000000':
resp["result"] = "0x0000000000000000000000000000000000000000000000000000000000000000"
elif req['params'][0]['data'] == '0xb449402a00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000':
resp["result"] = "0x000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c0000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000308e7ebb0d21a59d2197c0d42fecb115fade630873995db96830174efbc5f2ab26fa6d1e5d2725738e2870c311e852e89d000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000060a25beab0a9f2077f97e4b3244362b3b71f533287d76fd5c74d862130f4951a6af5aff74e15298074ba05946e8526bf3b116658f001890ecfe440ac576e84dede95ff80c478695606eb7e315c25731c14b0c9330cd49108b5df5e833d1f24db21"
elif 'gas' in req['params'][0].keys():
resp["result"] = "0x"
else:
print("Unknown request {req}")
print(f"Response: {resp}")

return (web.json_response(resp))


def main(argv):
app = web.Application()
app['ligthouse'] = True
app['prysm'] = not app['ligthouse']
app.add_routes(routes)
web.run_app(app)
return app
47 changes: 43 additions & 4 deletions run_tests.sh
Original file line number Diff line number Diff line change
@@ -1,7 +1,46 @@
#!/usr/bin/env bash

patch app/oracle.py < tests/patch
export ETH1_NODE=http://127.0.0.1:8080
export BEACON_NODE=http://127.0.0.1:8080
export POOL_CONTRACT=0xdead00000000000000000000000000000000beef
export PYTHONPATH=app/
pytest
patch -R app/oracle.py < tests/patch
export MEMBER_PRIV_KEY=0xdeadbeef000000000000000000000000000000000000000000000000deadbeef

if [[ $# == 0 ]]
then
echo "Performing all tests"
LIGHTHOUSE=1
PRYSM=1
fi

if [[ "$1" == 1 ]]
then
LIGHTHOUSE=1
fi

if [[ "$1" == 2 ]]
then
PRYSM=1
fi

echo "Run ETH1/ETH2 mock webservice"
python -m aiohttp.web -H localhost -P 8080 helpers.eth_nodes_mock:main &> /dev/null &
sleep 1
ETH_MOCK_PID=$!

if [[ -v LIGHTHOUSE ]]
then
echo "Switch mock to Lighthouse"
curl http://127.0.0.1:8080/mock/set/1
echo "Run python tests"
pytest
fi

if [[ -v PRYSM ]]
then
echo "Switch mock to Prysm"
curl http://127.0.0.1:8080/mock/set/2
pytest
fi

echo "Stop ETH1/ETH2 mock webservice and exit"
kill ${ETH_MOCK_PID}
Loading

0 comments on commit 230897b

Please sign in to comment.