Пример #1
0
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))
Пример #2
0
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}')
Пример #3
0
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))
Пример #4
0
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)
Пример #5
0
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()
Пример #6
0
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
Пример #7
0
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))
Пример #8
0
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))
Пример #9
0
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
Пример #10
0
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
Пример #11
0
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))
Пример #12
0
async def connect() -> None:
    await sio.call('auth', config.get()['API_KEY'])
    logger.info(f'connected and authed')
Пример #13
0
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