async def _update_best_digest(new_best: RelayHeader) -> None: '''Send an ethereum transaction that marks a new best known chain tip''' nonce = next(shared.NONCE) will_succeed = False while not will_succeed: current_best_digest = await contract.get_best_block() current_best = cast( RelayHeader, await bcoin_rpc.get_header_by_hash(current_best_digest)) delta = new_best['height'] - current_best['height'] + 1 # find the latest block in current's history that is an ancestor of new is_ancestor = False ancestor = current_best originalAncestor = ancestor counter = 0 while True: is_ancestor = await contract.is_ancestor(ancestor['hash_le'], new_best['hash_le']) if is_ancestor: counter = 0 break ancestor = cast( RelayHeader, await bcoin_rpc.get_header_by_hash(ancestor['prevhash'])) counter = counter + 1 if counter > 200: ancestor = originalAncestor counter = 0 ancestor_le = ancestor['hash_le'] tx = shared.make_call_tx( contract=config.get()['CONTRACT'], abi=relay_ABI, method='markNewHeaviest', args=[ancestor_le, current_best["raw"], new_best["raw"], delta], nonce=nonce) try: result = await shared.CONNECTION.preflight_tx( tx, sender=config.get()['ETH_ADDRESS']) except RuntimeError: await asyncio.sleep(10) continue will_succeed = bool(int(result, 16)) if not will_succeed: await asyncio.sleep(10) logger.info(f'\nmarking new best\n' f'LCA is {ancestor["hash"].hex()}\n' f'previous best was {utils.format_header(current_best)}\n' f'new best is {utils.format_header(new_best)}\n') asyncio.create_task(shared.sign_and_broadcast(tx))
async def init() -> None: '''Set up a connection to the interwebs''' global CONNECTION c = config.get() network = c['NETWORK'] project_id = c['PROJECT_ID'] uri = c['ETHER_URL'] force_https = project_id != '' logger.info(f'contract is {c["CONTRACT"]}') CONNECTION = ethrpc.get_client(network=network, infura_key=project_id, uri=uri, logger=logger.getChild('ethrpc'), force_https=force_https) await CONNECTION.open() if c['PRIVKEY'] is None and c['GETH_UNLOCK'] is None: logger.warn('No ethereum privkey found in env config. Txns will error') else: global NONCE address = cast(str, c['ETH_ADDRESS']) n = await CONNECTION.get_nonce(address) NONCE = _nonce(n) logger.info(f'nonce is {n}')
async def _add_diff_change(headers: List[RelayHeader]) -> None: print('Diff change') nonce = next(shared.NONCE) logger.info(f'\ndiff change {len(headers)} new headers,\n' f'first is {utils.format_header(headers[0])}\n' f'last is {utils.format_header(headers[-1])}\n') epoch_start = headers[0]['height'] - 2016 old_start_or_none, old_end_or_none = await asyncio.gather( bcoin_rpc.get_header_by_height(epoch_start), bcoin_rpc.get_header_by_height(epoch_start + 2015), ) # we know these casts won't fail old_start = cast(RelayHeader, old_start_or_none) old_end = cast(RelayHeader, old_end_or_none) logger.debug(f'old start is {old_start["hash_le"].hex()}') logger.debug(f'old end is {old_end["hash_le"].hex()}') headers_hex = ''.join(h['raw'].hex() for h in headers) tx = shared.make_call_tx( contract=config.get()['CONTRACT'], abi=relay_ABI, method='addHeadersWithRetarget', args=[old_start["raw"], old_end["raw"], headers_hex], nonce=nonce) asyncio.create_task(shared.sign_and_broadcast(tx))
async def find_height(digest_le: bytes) -> int: data = calldata.call( "findHeight", [digest_le], relay_ABI) res = await shared.CONNECTION._RPC( method='eth_call', params=[ { 'from': config.get()['ETH_ADDRESS'], 'to': config.get()['CONTRACT'], 'data': f'0x{data.hex()}' }, 'latest' # block height parameter ] ) logger.debug(f'findHeight for {digest_le.hex()} is {res}') return int(res, 16)
async def _GET(route: str, session: S = SESSION) -> Tuple[int, Any]: '''Dispatch a GET request''' URL = config.get()['BCOIN_URL'] logger.debug('get request {route}') full_route = f'{URL}/{route}' resp = await session.get(full_route) return resp.status, await resp.json()
async def get_best_block() -> str: ''' Get the contract's marked best known digest. Counterintuitively, the contract may know of a better digest that hasn't been marked yet ''' f = abi.find('getBestKnownDigest', relay_ABI)[0] selector = calldata.make_selector(f) res = await shared.CONNECTION._RPC( method='eth_call', params=[ { 'from': config.get()['ETH_ADDRESS'], 'to': config.get()['CONTRACT'], 'data': f'0x{selector.hex()}' }, 'latest' # block height parameter ] ) digest = bytes.fromhex(res[2:])[::-1].hex() # block-explorer format return digest
async def is_ancestor( ancestor: bytes, descendant: bytes, limit: int = 240) -> bool: '''Determine if ancestor precedes descendant''' data = calldata.call( "isAncestor", [ancestor, descendant, limit], relay_ABI) res = await shared.CONNECTION._RPC( method='eth_call', params=[ { 'from': config.get()['ETH_ADDRESS'], 'to': config.get()['CONTRACT'], 'data': f'0x{data.hex()}' }, 'latest' # block height parameter ] ) # returned as 0x-prepended hex string representing 32 bytes return bool(int(res, 16))
async def _add_headers(headers: List[RelayHeader]) -> None: logger.info(f'\nsending {len(headers)} new headers\n' f'first is {utils.format_header(headers[0])}\n' f'last is {utils.format_header(headers[-1])}\n') nonce = next(shared.NONCE) anchor_or_none = await bcoin_rpc.get_header_by_hash( headers[0]['prevhash'].hex()) anchor = cast(RelayHeader, anchor_or_none) headers_hex = ''.join(h['raw'].hex() for h in headers) tx = shared.make_call_tx(contract=config.get()['CONTRACT'], abi=relay_ABI, method='addHeaders', args=[anchor["raw"], headers_hex], nonce=nonce) asyncio.create_task(shared.sign_and_broadcast(tx))
async def _POST(route: str = '', payload: Dict[str, Any] = {}, session: S = SESSION) -> Tuple[int, Any]: '''Dispatch a POST request''' URL = config.get()['BCOIN_URL'] logger.debug(f'sending bcoin post request {payload["method"]}') resp = await session.post(f'{URL}/{route}', json=payload) status = resp.status resp_json = await unwrap_json(resp) result = None if resp_json is not None: result = resp_json['result'] if 'result' in resp_json else resp_json if status != 200: r = await resp.read() logger.error(f'Unexpected status {status} body {r!r}') return resp.status, result
def make_call_tx(contract: str, abi: List[Dict[str, Any]], method: str, args: List[Any], nonce: int, value: int = 0, gas: int = DEFAULT_GAS, gas_price: int = DEFAULT_GAS_PRICE) -> UnsignedEthTx: ''' Sends tokens to a recipient Args: contract (str): address of contract being called abi (dict): contract ABI method (str): the name of the method to call args (list): the arguments to the method call nonce (int): the account nonce for the txn value (int): ether in wei gas_price (int): the price of gas in wei or gwei Returns: (UnsignedEthTx): the unsigned tx object ''' logger.debug(f'making tx call {method} on {contract} ' f'with value {value} and {len(args)} args') gas_price = _adjust_gas_price(gas_price) chainId = config.get()['CHAIN_ID'] data = calldata.call(method, args, abi) txn = UnsignedEthTx(to=contract, value=value, gas=gas, gasPrice=gas_price, nonce=nonce, data=data, chainId=chainId) return txn
async def sign_and_broadcast(tx: UnsignedEthTx, ignore_result: bool = False) -> None: '''Sign an ethereum transaction and broadcast it to the network''' c = config.get() privkey = c['PRIVKEY'] address = c['ETH_ADDRESS'] unlock_code = c['GETH_UNLOCK'] if privkey is None and unlock_code is None: raise RuntimeError('Attempted to sign tx without access to key') if privkey is None: logger.debug('signing with ether node') await CONNECTION._RPC('personal_unlockAccount', [address, unlock_code]) tx_id = await CONNECTION.send_transaction(cast(str, address), tx) else: logger.debug('signing with local key') signed = tx.sign(cast(bytes, privkey)) serialized = signed.serialize_hex() tx_id = await CONNECTION.broadcast(serialized) logger.info(f'dispatched transaction {tx_id}') if not ignore_result: asyncio.ensure_future(_track_tx_result(tx_id))
async def connect() -> None: await sio.call('auth', config.get()['API_KEY']) logger.info(f'connected and authed')
async def get_connection() -> socketio.AsyncClient: logger.info('opening bsock ws session') if not sio.connected: await sio.connect(config.get()['BCOIN_WS_URL'], transports='websocket') return sio