Ejemplo n.º 1
0
import threading

from evm.db.chain import BaseChainDB
from evm.chains.ropsten import ROPSTEN_NETWORK_ID
from evm.chains.mainnet import MAINNET_VM_CONFIGURATION
from evm.p2p import ecies
from evm.p2p.lightchain import LightChain
from evm.db.backends.level import LevelDB

parser = argparse.ArgumentParser()
parser.add_argument('-db', type=str, required=True)
args = parser.parse_args()

DemoLightChain = LightChain.configure(
    name='Demo LightChain',
    privkey=ecies.generate_privkey(),
    vm_configuration=MAINNET_VM_CONFIGURATION,
    network_id=ROPSTEN_NETWORK_ID,
)

chain = DemoLightChain(BaseChainDB(LevelDB(args.db)))
loop = asyncio.get_event_loop()
t = threading.Thread(target=loop.run_until_complete, args=(chain.run(), ))
t.setDaemon(True)
t.start()


def wait_for_result(coroutine):
    future = asyncio.run_coroutine_threadsafe(coroutine, loop)
    return future.result()
Ejemplo n.º 2
0
def _test():
    import argparse
    import signal
    from evm.chains.mainnet import (MAINNET_GENESIS_HEADER,
                                    MAINNET_VM_CONFIGURATION,
                                    MAINNET_NETWORK_ID)
    from evm.chains.ropsten import ROPSTEN_GENESIS_HEADER, ROPSTEN_NETWORK_ID
    from evm.db.backends.level import LevelDB
    from evm.exceptions import CanonicalHeadNotFound
    from evm.p2p import ecies
    from evm.p2p.integration_test_helpers import LocalGethPeerPool

    logging.basicConfig(level=logging.INFO,
                        format='%(levelname)s: %(message)s')
    logging.getLogger("evm.p2p.lightchain.LightChain").setLevel(logging.DEBUG)

    parser = argparse.ArgumentParser()
    parser.add_argument('-db', type=str, required=True)
    parser.add_argument('-mainnet', action="store_true")
    parser.add_argument('-local-geth', action="store_true")
    args = parser.parse_args()

    GENESIS_HEADER = ROPSTEN_GENESIS_HEADER
    NETWORK_ID = ROPSTEN_NETWORK_ID
    if args.mainnet:
        GENESIS_HEADER = MAINNET_GENESIS_HEADER
        NETWORK_ID = MAINNET_NETWORK_ID
    DemoLightChain = LightChain.configure(
        'DemoLightChain',
        vm_configuration=MAINNET_VM_CONFIGURATION,
        network_id=NETWORK_ID,
    )

    chaindb = BaseChainDB(LevelDB(args.db))
    if args.local_geth:
        peer_pool = LocalGethPeerPool(LESPeer, chaindb, NETWORK_ID,
                                      ecies.generate_privkey())
    else:
        peer_pool = PeerPool(LESPeer, chaindb, NETWORK_ID,
                             ecies.generate_privkey())
    try:
        chaindb.get_canonical_head()
    except CanonicalHeadNotFound:
        # We're starting with a fresh DB.
        chain = DemoLightChain.from_genesis_header(chaindb, GENESIS_HEADER,
                                                   peer_pool)
    else:
        # We're reusing an existing db.
        chain = DemoLightChain(chaindb, peer_pool)

    asyncio.ensure_future(peer_pool.run())
    loop = asyncio.get_event_loop()

    async def run():
        # chain.run() will run in a loop until stop() (registered as SIGINT/SIGTERM handler) is
        # called, at which point it returns and we cleanly stop the pool and chain.
        await chain.run()
        await peer_pool.stop()
        await chain.stop()

    def stop():
        chain._should_stop.set()

    for sig in [signal.SIGINT, signal.SIGTERM]:
        loop.add_signal_handler(sig, stop)

    loop.run_until_complete(run())
    loop.close()
async def test_lightchain_integration(request, event_loop):
    """Test LightChain against a local geth instance.

    This test assumes a geth/ropsten instance is listening on 127.0.0.1:30303 and serving light
    clients. In order to achieve that, simply run it with the following command line:

        $ geth -nodekeyhex 45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8 \
               -testnet -lightserv 90
    """
    # TODO: Implement a pytest fixture that runs geth as above, so that we don't need to run it
    # manually.
    if not pytest.config.getoption("--integration"):
        pytest.skip("Not asked to run integration tests")

    chaindb = ChainDB(MemoryDB())
    chaindb.persist_header_to_db(ROPSTEN_GENESIS_HEADER)
    peer_pool = LocalGethPeerPool(LESPeer, chaindb, ROPSTEN_NETWORK_ID,
                                  ecies.generate_privkey())
    chain = IntegrationTestLightChain(chaindb, peer_pool)
    asyncio.ensure_future(peer_pool.run())
    asyncio.ensure_future(chain.run())
    await asyncio.sleep(
        0)  # Yield control to give the LightChain a chance to start

    def finalizer():
        event_loop.run_until_complete(peer_pool.stop())
        event_loop.run_until_complete(chain.stop())

    request.addfinalizer(finalizer)

    n = 11

    # Wait for the chain to sync a few headers.
    async def wait_for_header_sync(block_number):
        while chaindb.get_canonical_head().block_number < block_number:
            await asyncio.sleep(0.1)

    await asyncio.wait_for(wait_for_header_sync(n), 2)

    # https://ropsten.etherscan.io/block/11
    b = await chain.get_canonical_block_by_number(n)
    assert isinstance(b, FrontierBlock)
    assert b.number == 11
    assert encode_hex(b.hash) == (
        '0xda882aeff30f59eda9da2b3ace3023366ab9d4219b5a83cdd589347baae8678e')
    assert len(b.transactions) == 15
    assert isinstance(b.transactions[0], b.transaction_class)

    receipts = await chain.get_receipts(b.hash)
    assert len(receipts) == 15
    assert encode_hex(keccak(rlp.encode(receipts[0]))) == (
        '0xf709ed2c57efc18a1675e8c740f3294c9e2cb36ba7bb3b89d3ab4c8fef9d8860')

    assert len(chain.peer_pool.peers) == 1
    head_info = chain.peer_pool.peers[0].head_info
    head = await chain.get_block_by_hash(head_info.block_hash)
    assert head.number == head_info.block_number

    # In order to answer queries for contract code, geth needs the state trie entry for the block
    # we specify in the query, but because of fast sync we can only assume it has that for recent
    # blocks, so we use the current head to lookup the code for the contract below.
    # https://ropsten.etherscan.io/address/0x95a48dca999c89e4e284930d9b9af973a7481287
    contract_addr = decode_hex('95a48dca999c89e4e284930d9b9af973a7481287')
    contract_code = await chain.get_contract_code(head.hash,
                                                  keccak(contract_addr))
    assert encode_hex(keccak(contract_code)) == (
        '0x1e0b2ad970b365a217c40bcf3582cbb4fcc1642d7a5dd7a82ae1e278e010123e')

    account = await chain.get_account(head.hash, contract_addr)
    assert account.code_hash == keccak(contract_code)
    assert account.balance == 0
Ejemplo n.º 4
0
Archivo: peer.py Proyecto: ycdk/py-evm
    logging.basicConfig(level=logging.DEBUG,
                        format='%(levelname)s: %(message)s')

    # The default remoteid can be used if you pass nodekeyhex as above to geth.
    nodekey = keys.PrivateKey(
        decode_hex(
            "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
    )
    remoteid = nodekey.public_key.to_hex()
    parser = argparse.ArgumentParser()
    parser.add_argument('-remoteid', type=str, default=remoteid)
    args = parser.parse_args()

    remote = Node(keys.PublicKey(decode_hex(args.remoteid)),
                  Address('127.0.0.1', 30303, 30303))
    chaindb = BaseChainDB(MemoryDB())
    chaindb.persist_header_to_db(ROPSTEN_GENESIS_HEADER)
    network_id = RopstenChain.network_id
    loop = asyncio.get_event_loop()
    try:
        peer = loop.run_until_complete(
            asyncio.wait_for(
                handshake(remote, ecies.generate_privkey(), LESPeer, chaindb,
                          network_id), HANDSHAKE_TIMEOUT))
        loop.run_until_complete(peer.start())
    except KeyboardInterrupt:
        pass

    loop.run_until_complete(peer.stop())
    loop.close()
Ejemplo n.º 5
0
def _test():
    """
    Create a Peer instance connected to a local geth instance and log messages exchanged with it.

    Use the following command line to run geth:

        ./build/bin/geth -vmodule p2p=4,p2p/discv5=0,eth/*=0 \
          -nodekeyhex 45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8 \
          -testnet -lightserv 90
    """
    import argparse
    from evm.chains.ropsten import RopstenChain, ROPSTEN_GENESIS_HEADER
    from evm.db.backends.memory import MemoryDB
    logging.basicConfig(level=logging.DEBUG,
                        format='%(levelname)s: %(message)s')

    # The default remoteid can be used if you pass nodekeyhex as above to geth.
    nodekey = keys.PrivateKey(
        decode_hex(
            "45a915e4d060149eb4365960e6a7a45f334393093061116b197e3240065ff2d8")
    )
    remoteid = nodekey.public_key.to_hex()
    parser = argparse.ArgumentParser()
    parser.add_argument('-remoteid', type=str, default=remoteid)
    parser.add_argument('-light',
                        action='store_true',
                        help="Connect as a light node")
    args = parser.parse_args()

    peer_class = ETHPeer  # type: ignore
    if args.light:
        peer_class = LESPeer  # type: ignore
    remote = Node(keys.PublicKey(decode_hex(args.remoteid)),
                  Address('127.0.0.1', 30303, 30303))
    chaindb = BaseChainDB(MemoryDB())
    chaindb.persist_header_to_db(ROPSTEN_GENESIS_HEADER)
    network_id = RopstenChain.network_id
    loop = asyncio.get_event_loop()
    try:
        peer = loop.run_until_complete(
            asyncio.wait_for(
                handshake(remote, ecies.generate_privkey(), peer_class,
                          chaindb, network_id), HANDSHAKE_TIMEOUT))
        asyncio.ensure_future(peer.do_sub_proto_handshake())

        async def request_stuff():
            # Request some stuff from ropsten's block 2440319
            # (https://ropsten.etherscan.io/block/2440319), just as a basic test.
            nonlocal peer
            block_hash = decode_hex(
                '0x59af08ab31822c992bb3dad92ddb68d820aa4c69e9560f07081fa53f1009b152'
            )
            if peer_class == ETHPeer:
                peer = cast(ETHPeer, peer)
                peer.sub_proto.send_get_block_headers(block_hash, 1)
                peer.sub_proto.send_get_block_bodies([block_hash])
                peer.sub_proto.send_get_receipts([block_hash])
            else:
                peer = cast(LESPeer, peer)
                request_id = 1
                peer.sub_proto.send_get_block_headers(block_hash, 1,
                                                      request_id)
                peer.sub_proto.send_get_block_bodies([block_hash],
                                                     request_id + 1)
                peer.sub_proto.send_get_receipts(block_hash, request_id + 2)

        asyncio.ensure_future(request_stuff())
        loop.run_until_complete(peer.run())
    except KeyboardInterrupt:
        pass

    loop.run_until_complete(peer.stop())
    loop.close()
Ejemplo n.º 6
0
async def get_directly_linked_peers(chaindb1=None, chaindb2=None):
    """Create two LESPeers with their readers/writers connected directly.

    The first peer's reader will write directly to the second's writer, and vice-versa.
    """
    if chaindb1 is None:
        chaindb1 = BaseChainDB(MemoryDB())
        chaindb1.persist_header_to_db(MAINNET_GENESIS_HEADER)
    if chaindb2 is None:
        chaindb2 = BaseChainDB(MemoryDB())
        chaindb2.persist_header_to_db(MAINNET_GENESIS_HEADER)
    peer1_private_key = ecies.generate_privkey()
    peer2_private_key = ecies.generate_privkey()
    peer1_remote = kademlia.Node(
        peer2_private_key.public_key, kademlia.Address('0.0.0.0', 0, 0))
    peer2_remote = kademlia.Node(
        peer1_private_key.public_key, kademlia.Address('0.0.0.0', 0, 0))
    initiator = auth.HandshakeInitiator(peer1_remote, peer1_private_key)
    peer2_reader = asyncio.StreamReader()
    peer1_reader = asyncio.StreamReader()
    # Link the peer1's writer to the peer2's reader, and the peer2's writer to the
    # peer1's reader.
    peer2_writer = type(
        "mock-streamwriter",
        (object,),
        {"write": peer1_reader.feed_data,
         "close": lambda: None}
    )
    peer1_writer = type(
        "mock-streamwriter",
        (object,),
        {"write": peer2_reader.feed_data,
         "close": lambda: None}
    )

    peer1, peer2 = None, None
    handshake_finished = asyncio.Event()

    async def do_handshake():
        nonlocal peer1, peer2
        aes_secret, mac_secret, egress_mac, ingress_mac = await auth._handshake(
            initiator, peer1_reader, peer1_writer)

        # Need to copy those before we pass them on to the Peer constructor because they're
        # mutable. Also, the 2nd peer's ingress/egress MACs are reversed from the first peer's.
        peer2_ingress = egress_mac.copy()
        peer2_egress = ingress_mac.copy()

        peer1 = LESPeerServing(
            remote=peer1_remote, privkey=peer1_private_key, reader=peer1_reader,
            writer=peer1_writer, aes_secret=aes_secret, mac_secret=mac_secret,
            egress_mac=egress_mac, ingress_mac=ingress_mac, chaindb=chaindb1,
            network_id=1)

        peer2 = LESPeerServing(
            remote=peer2_remote, privkey=peer2_private_key, reader=peer2_reader,
            writer=peer2_writer, aes_secret=aes_secret, mac_secret=mac_secret,
            egress_mac=peer2_egress, ingress_mac=peer2_ingress, chaindb=chaindb2,
            network_id=1)

        handshake_finished.set()

    asyncio.ensure_future(do_handshake())

    responder = auth.HandshakeResponder(peer2_remote, peer2_private_key)
    auth_msg = await peer2_reader.read(constants.ENCRYPTED_AUTH_MSG_LEN)
    peer1_ephemeral_pubkey, peer1_nonce = responder.decode_authentication(auth_msg)

    peer2_nonce = keccak(os.urandom(constants.HASH_LEN))
    auth_ack_msg = responder.create_auth_ack_message(peer2_nonce)
    auth_ack_ciphertext = responder.encrypt_auth_ack_message(auth_ack_msg)
    peer2_writer.write(auth_ack_ciphertext)

    await handshake_finished.wait()

    # Perform the base protocol (P2P) handshake.
    peer1.base_protocol.send_handshake()
    peer2.base_protocol.send_handshake()
    msg1 = await peer1.read_msg()
    peer1.process_msg(msg1)
    msg2 = await peer2.read_msg()
    peer2.process_msg(msg2)

    # Now send the handshake msg for each enabled sub-protocol.
    for proto in peer1.enabled_sub_protocols:
        proto.send_handshake(peer1.head_info)
    for proto in peer2.enabled_sub_protocols:
        proto.send_handshake(peer2.head_info)

    return peer1, peer2
Ejemplo n.º 7
0
Archivo: auth.py Proyecto: ycdk/py-evm
 def __init__(self, remote: kademlia.Node, privkey: datatypes.PrivateKey) -> None:
     self.remote = remote
     self.privkey = privkey
     self.ephemeral_privkey = ecies.generate_privkey()
Ejemplo n.º 8
0
async def _get_directly_linked_peers_without_handshake(peer1_class=LESPeer,
                                                       peer1_chaindb=None,
                                                       peer2_class=LESPeer,
                                                       peer2_chaindb=None):
    """See get_directly_linked_peers().

    Neither the P2P handshake nor the sub-protocol handshake will be performed here.
    """
    if peer1_chaindb is None:
        peer1_chaindb = get_fresh_mainnet_chaindb()
    if peer2_chaindb is None:
        peer2_chaindb = get_fresh_mainnet_chaindb()
    peer1_private_key = ecies.generate_privkey()
    peer2_private_key = ecies.generate_privkey()
    peer1_remote = kademlia.Node(peer2_private_key.public_key,
                                 kademlia.Address('0.0.0.0', 0, 0))
    peer2_remote = kademlia.Node(peer1_private_key.public_key,
                                 kademlia.Address('0.0.0.0', 0, 0))
    initiator = auth.HandshakeInitiator(peer1_remote, peer1_private_key)
    peer2_reader = asyncio.StreamReader()
    peer1_reader = asyncio.StreamReader()
    # Link the peer1's writer to the peer2's reader, and the peer2's writer to the
    # peer1's reader.
    peer2_writer = type("mock-streamwriter", (object, ), {
        "write": peer1_reader.feed_data,
        "close": lambda: None
    })
    peer1_writer = type("mock-streamwriter", (object, ), {
        "write": peer2_reader.feed_data,
        "close": lambda: None
    })

    peer1, peer2 = None, None
    handshake_finished = asyncio.Event()

    async def do_handshake():
        nonlocal peer1, peer2
        aes_secret, mac_secret, egress_mac, ingress_mac = await auth._handshake(
            initiator, peer1_reader, peer1_writer)

        # Need to copy those before we pass them on to the Peer constructor because they're
        # mutable. Also, the 2nd peer's ingress/egress MACs are reversed from the first peer's.
        peer2_ingress = egress_mac.copy()
        peer2_egress = ingress_mac.copy()

        peer1 = peer1_class(remote=peer1_remote,
                            privkey=peer1_private_key,
                            reader=peer1_reader,
                            writer=peer1_writer,
                            aes_secret=aes_secret,
                            mac_secret=mac_secret,
                            egress_mac=egress_mac,
                            ingress_mac=ingress_mac,
                            chaindb=peer1_chaindb,
                            network_id=1)

        peer2 = peer2_class(remote=peer2_remote,
                            privkey=peer2_private_key,
                            reader=peer2_reader,
                            writer=peer2_writer,
                            aes_secret=aes_secret,
                            mac_secret=mac_secret,
                            egress_mac=peer2_egress,
                            ingress_mac=peer2_ingress,
                            chaindb=peer2_chaindb,
                            network_id=1)

        handshake_finished.set()

    asyncio.ensure_future(do_handshake())

    responder = auth.HandshakeResponder(peer2_remote, peer2_private_key)
    auth_msg = await peer2_reader.read(constants.ENCRYPTED_AUTH_MSG_LEN)

    # Can't assert return values, but checking that the decoder doesn't raise
    # any exceptions at least.
    _, _ = responder.decode_authentication(auth_msg)

    peer2_nonce = keccak(os.urandom(constants.HASH_LEN))
    auth_ack_msg = responder.create_auth_ack_message(peer2_nonce)
    auth_ack_ciphertext = responder.encrypt_auth_ack_message(auth_ack_msg)
    peer2_writer.write(auth_ack_ciphertext)

    await handshake_finished.wait()

    return peer1, peer2
Ejemplo n.º 9
0
                        help="Connect as a light node")
    args = parser.parse_args()

    peer_class = ETHPeer  # type: ignore
    if args.light:
        peer_class = LESPeer  # type: ignore
    remote = Node(keys.PublicKey(decode_hex(args.remoteid)),
                  Address('127.0.0.1', 30303, 30303))
    chaindb = BaseChainDB(MemoryDB())
    chaindb.persist_header_to_db(ROPSTEN_GENESIS_HEADER)
    network_id = RopstenChain.network_id
    loop = asyncio.get_event_loop()
    try:
        peer = loop.run_until_complete(
            asyncio.wait_for(
                handshake(remote, ecies.generate_privkey(), peer_class,
                          chaindb, network_id), HANDSHAKE_TIMEOUT))
        # Request some stuff from ropsten's block 2440319
        # (https://ropsten.etherscan.io/block/2440319), just as a basic test.
        block_hash = decode_hex(
            '0x59af08ab31822c992bb3dad92ddb68d820aa4c69e9560f07081fa53f1009b152'
        )
        if peer_class == ETHPeer:
            peer = cast(ETHPeer, peer)
            peer.eth_proto.send_get_block_headers(block_hash, 1)
            peer.eth_proto.send_get_block_bodies([block_hash])
            peer.eth_proto.send_get_receipts([block_hash])
        else:
            peer = cast(LESPeer, peer)
            request_id = 1
            peer.les_proto.send_get_block_headers(block_hash, 1, request_id)
Ejemplo n.º 10
0
class LightChain(Chain):
    logger = logging.getLogger("evm.p2p.lightchain.LightChain")
    # FIXME: Should be passed in by callers
    privkey = ecies.generate_privkey()
    max_consecutive_timeouts = 5
    peer_pool_class = PeerPool

    def __init__(self, chaindb: BaseChainDB) -> None:
        super(LightChain, self).__init__(chaindb)
        self.peer_pool = self.peer_pool_class(chaindb, self.network_id,
                                              self.privkey, self.msg_handler)
        self._announcement_queue = asyncio.Queue(
        )  # type: asyncio.Queue[Tuple[LESPeer, les.HeadInfo]]  # noqa: E501
        self._last_processed_announcements = {
        }  # type: Dict[LESPeer, les.HeadInfo]
        self._latest_head_info = {}  # type: Dict[LESPeer, les.HeadInfo]
        self._should_stop = asyncio.Event()
        self._finished = asyncio.Event()

    def msg_handler(self, peer: BasePeer, cmd: protocol.Command,
                    announcement: protocol._DecodedMsgType) -> None:
        """The callback passed to BasePeer, called for every incoming message."""
        peer = cast(LESPeer, peer)
        if isinstance(cmd, (les.Announce, les.Status)):
            head_info = cmd.as_head_info(announcement)
            self._latest_head_info[peer] = head_info
            self._announcement_queue.put_nowait((peer, head_info))

    async def drop_peer(self, peer: LESPeer) -> None:
        self._last_processed_announcements.pop(peer, None)
        self._latest_head_info.pop(peer, None)
        await peer.stop_and_wait_until_finished()

    async def wait_for_announcement(self) -> Tuple[LESPeer, les.HeadInfo]:
        """Wait for a new announcement from any of our connected peers.

        Returns a tuple containing the LESPeer on which the announcement was received and the
        announcement info.

        Raises StopRequested when LightChain.stop() has been called.
        """
        should_stop = False

        async def wait_for_stop_event():
            nonlocal should_stop
            await self._should_stop.wait()
            should_stop = True

        # Wait for either a new announcement or the _should_stop event.
        done, pending = await asyncio.wait(
            [self._announcement_queue.get(),
             wait_for_stop_event()],
            return_when=asyncio.FIRST_COMPLETED)
        # The async call above returns as soon as one of our 2 coroutines complete, so we know
        # for sure we'll have one task in the <done> and one task in the <pending> set.
        pending.pop().cancel()
        if should_stop:
            raise StopRequested()
        return done.pop().result()

    async def run(self) -> None:
        """Run the LightChain, ensuring headers are in sync with connected peers.

        Run our PeerPool to ensure we are always connected to some peers and then loop forever,
        waiting for announcements from connected peers and fetching new headers.

        If .stop() is called, we'll disconnect from all peers and return.
        """
        self.logger.info("Running LightChain...")
        asyncio.ensure_future(self.peer_pool.run())
        while True:
            try:
                peer, head_info = await self.wait_for_announcement()
            except StopRequested:
                break

            try:
                await self.process_announcement(peer, head_info)
                self._last_processed_announcements[peer] = head_info
            except LESAnnouncementProcessingError as e:
                self.logger.warn(repr(e))
                await self.drop_peer(peer)
            except Exception as e:
                self.logger.error(
                    "Unexpected error when processing announcement: %s",
                    repr(e))
                await self.drop_peer(peer)

        self._finished.set()

    async def fetch_headers(self, start_block: int,
                            peer: LESPeer) -> List[BlockHeader]:
        for i in range(self.max_consecutive_timeouts):
            try:
                return await peer.fetch_headers_starting_at(start_block)
            except asyncio.TimeoutError:
                self.logger.info(
                    "Timeout when fetching headers from %s (attempt %d of %d)",
                    peer, i + 1, self.max_consecutive_timeouts)
                # TODO: Figure out what's a good value to use here.
                await asyncio.sleep(0.5)
        raise TooManyTimeouts()

    async def get_sync_start_block(self, peer: LESPeer,
                                   head_info: les.HeadInfo) -> int:
        chain_head = self.chaindb.get_canonical_head()
        last_peer_announcement = self._last_processed_announcements.get(peer)
        if chain_head.block_number == GENESIS_BLOCK_NUMBER:
            start_block = GENESIS_BLOCK_NUMBER
        elif last_peer_announcement is None:
            # It's the first time we hear from this peer, need to figure out which headers to
            # get from it.  We can't simply fetch headers starting from our current head
            # number because there may have been a chain reorg, so we fetch some headers prior
            # to our head from the peer, and insert any missing ones in our DB, essentially
            # making our canonical chain identical to the peer's up to
            # chain_head.block_number.
            oldest_ancestor_to_consider = max(
                0, chain_head.block_number - peer.max_headers_fetch + 1)
            try:
                headers = await self.fetch_headers(oldest_ancestor_to_consider,
                                                   peer)
            except EmptyGetBlockHeadersReply:
                raise LESAnnouncementProcessingError(
                    "No common ancestors found between us and %s", peer)
            except TooManyTimeouts:
                raise LESAnnouncementProcessingError(
                    "Too many timeouts when fetching headers from %s", peer)
            for header in headers:
                self.chaindb.persist_header_to_db(header)
            start_block = chain_head.block_number
        else:
            start_block = last_peer_announcement.block_number - head_info.reorg_depth
        return start_block

    # TODO: Distribute requests among our peers, ensuring the selected peer has the info we want
    # and respecting the flow control rules.
    async def process_announcement(self, peer: LESPeer,
                                   head_info: les.HeadInfo) -> None:
        if self.chaindb.header_exists(head_info.block_hash):
            self.logger.debug(
                "Skipping processing of %s from %s as head has already been fetched",
                head_info, peer)
            return

        start_block = await self.get_sync_start_block(peer, head_info)
        while start_block < head_info.block_number:
            try:
                # We should use "start_block + 1" here, but we always re-fetch the last synced
                # block to work around https://github.com/ethereum/go-ethereum/issues/15447
                batch = await self.fetch_headers(start_block, peer)
            except TooManyTimeouts:
                raise LESAnnouncementProcessingError(
                    "Too many timeouts when fetching headers from %s", peer)
            for header in batch:
                self.chaindb.persist_header_to_db(header)
                start_block = header.block_number
            self.logger.info("synced headers up to #%s", start_block)

    async def stop(self):
        self.logger.info("Stopping LightChain...")
        self._should_stop.set()
        await self.peer_pool.stop()
        await self._finished.wait()

    async def get_canonical_block_by_number(self,
                                            block_number: int) -> BaseBlock:
        try:
            block_hash = self.chaindb.lookup_block_hash(block_number)
        except KeyError:
            raise BlockNotFound(
                "No block with number {} found on local chain".format(
                    block_number))
        return await self.get_block_by_hash(block_hash)

    @alru_cache(maxsize=1024)
    async def get_block_by_hash(self, block_hash: bytes) -> BaseBlock:
        peer = await self.peer_pool.get_best_peer()
        self.logger.debug("Fetching block %s from %s", encode_hex(block_hash),
                          peer)
        request_id = gen_request_id()
        peer.les_proto.send_get_block_bodies([block_hash], request_id)
        reply = await peer.wait_for_reply(request_id)
        if len(reply['bodies']) == 0:
            raise BlockNotFound(
                "No block with hash {} found".format(block_hash))
        body = reply['bodies'][0]
        # This will raise a BlockNotFound if we don't have the header in our DB, which is correct
        # because it means our peer doesn't know about it.
        header = self.chaindb.get_block_header_by_hash(block_hash)
        block_class = self.get_vm_class_for_block_number(
            header.block_number).get_block_class()
        return block_class(
            header=header,
            transactions=body.transactions,
            uncles=body.uncles,
            chaindb=self.chaindb,
        )