URI: 
       tblockchain.py - electrum - Electrum Bitcoin wallet
  HTML git clone https://git.parazyd.org/electrum
   DIR Log
   DIR Files
   DIR Refs
   DIR Submodules
       ---
       tblockchain.py (27576B)
       ---
            1 # Electrum - lightweight Bitcoin client
            2 # Copyright (C) 2012 thomasv@ecdsa.org
            3 #
            4 # Permission is hereby granted, free of charge, to any person
            5 # obtaining a copy of this software and associated documentation files
            6 # (the "Software"), to deal in the Software without restriction,
            7 # including without limitation the rights to use, copy, modify, merge,
            8 # publish, distribute, sublicense, and/or sell copies of the Software,
            9 # and to permit persons to whom the Software is furnished to do so,
           10 # subject to the following conditions:
           11 #
           12 # The above copyright notice and this permission notice shall be
           13 # included in all copies or substantial portions of the Software.
           14 #
           15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
           16 # EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
           17 # MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
           18 # NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
           19 # BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
           20 # ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
           21 # CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
           22 # SOFTWARE.
           23 import os
           24 import threading
           25 import time
           26 from typing import Optional, Dict, Mapping, Sequence
           27 
           28 from . import util
           29 from .bitcoin import hash_encode, int_to_hex, rev_hex
           30 from .crypto import sha256d
           31 from . import constants
           32 from .util import bfh, bh2u
           33 from .simple_config import SimpleConfig
           34 from .logging import get_logger, Logger
           35 
           36 
           37 _logger = get_logger(__name__)
           38 
           39 HEADER_SIZE = 80  # bytes
           40 MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000
           41 
           42 
           43 class MissingHeader(Exception):
           44     pass
           45 
           46 class InvalidHeader(Exception):
           47     pass
           48 
           49 def serialize_header(header_dict: dict) -> str:
           50     s = int_to_hex(header_dict['version'], 4) \
           51         + rev_hex(header_dict['prev_block_hash']) \
           52         + rev_hex(header_dict['merkle_root']) \
           53         + int_to_hex(int(header_dict['timestamp']), 4) \
           54         + int_to_hex(int(header_dict['bits']), 4) \
           55         + int_to_hex(int(header_dict['nonce']), 4)
           56     return s
           57 
           58 def deserialize_header(s: bytes, height: int) -> dict:
           59     if not s:
           60         raise InvalidHeader('Invalid header: {}'.format(s))
           61     if len(s) != HEADER_SIZE:
           62         raise InvalidHeader('Invalid header length: {}'.format(len(s)))
           63     hex_to_int = lambda s: int.from_bytes(s, byteorder='little')
           64     h = {}
           65     h['version'] = hex_to_int(s[0:4])
           66     h['prev_block_hash'] = hash_encode(s[4:36])
           67     h['merkle_root'] = hash_encode(s[36:68])
           68     h['timestamp'] = hex_to_int(s[68:72])
           69     h['bits'] = hex_to_int(s[72:76])
           70     h['nonce'] = hex_to_int(s[76:80])
           71     h['block_height'] = height
           72     return h
           73 
           74 def hash_header(header: dict) -> str:
           75     if header is None:
           76         return '0' * 64
           77     if header.get('prev_block_hash') is None:
           78         header['prev_block_hash'] = '00'*32
           79     return hash_raw_header(serialize_header(header))
           80 
           81 
           82 def hash_raw_header(header: str) -> str:
           83     return hash_encode(sha256d(bfh(header)))
           84 
           85 
           86 # key: blockhash hex at forkpoint
           87 # the chain at some key is the best chain that includes the given hash
           88 blockchains = {}  # type: Dict[str, Blockchain]
           89 blockchains_lock = threading.RLock()  # lock order: take this last; so after Blockchain.lock
           90 
           91 
           92 def read_blockchains(config: 'SimpleConfig'):
           93     best_chain = Blockchain(config=config,
           94                             forkpoint=0,
           95                             parent=None,
           96                             forkpoint_hash=constants.net.GENESIS,
           97                             prev_hash=None)
           98     blockchains[constants.net.GENESIS] = best_chain
           99     # consistency checks
          100     if best_chain.height() > constants.net.max_checkpoint():
          101         header_after_cp = best_chain.read_header(constants.net.max_checkpoint()+1)
          102         if not header_after_cp or not best_chain.can_connect(header_after_cp, check_height=False):
          103             _logger.info("[blockchain] deleting best chain. cannot connect header after last cp to last cp.")
          104             os.unlink(best_chain.path())
          105             best_chain.update_size()
          106     # forks
          107     fdir = os.path.join(util.get_headers_dir(config), 'forks')
          108     util.make_dir(fdir)
          109     # files are named as: fork2_{forkpoint}_{prev_hash}_{first_hash}
          110     l = filter(lambda x: x.startswith('fork2_') and '.' not in x, os.listdir(fdir))
          111     l = sorted(l, key=lambda x: int(x.split('_')[1]))  # sort by forkpoint
          112 
          113     def delete_chain(filename, reason):
          114         _logger.info(f"[blockchain] deleting chain {filename}: {reason}")
          115         os.unlink(os.path.join(fdir, filename))
          116 
          117     def instantiate_chain(filename):
          118         __, forkpoint, prev_hash, first_hash = filename.split('_')
          119         forkpoint = int(forkpoint)
          120         prev_hash = (64-len(prev_hash)) * "0" + prev_hash  # left-pad with zeroes
          121         first_hash = (64-len(first_hash)) * "0" + first_hash
          122         # forks below the max checkpoint are not allowed
          123         if forkpoint <= constants.net.max_checkpoint():
          124             delete_chain(filename, "deleting fork below max checkpoint")
          125             return
          126         # find parent (sorting by forkpoint guarantees it's already instantiated)
          127         for parent in blockchains.values():
          128             if parent.check_hash(forkpoint - 1, prev_hash):
          129                 break
          130         else:
          131             delete_chain(filename, "cannot find parent for chain")
          132             return
          133         b = Blockchain(config=config,
          134                        forkpoint=forkpoint,
          135                        parent=parent,
          136                        forkpoint_hash=first_hash,
          137                        prev_hash=prev_hash)
          138         # consistency checks
          139         h = b.read_header(b.forkpoint)
          140         if first_hash != hash_header(h):
          141             delete_chain(filename, "incorrect first hash for chain")
          142             return
          143         if not b.parent.can_connect(h, check_height=False):
          144             delete_chain(filename, "cannot connect chain to parent")
          145             return
          146         chain_id = b.get_id()
          147         assert first_hash == chain_id, (first_hash, chain_id)
          148         blockchains[chain_id] = b
          149 
          150     for filename in l:
          151         instantiate_chain(filename)
          152 
          153 
          154 def get_best_chain() -> 'Blockchain':
          155     return blockchains[constants.net.GENESIS]
          156 
          157 # block hash -> chain work; up to and including that block
          158 _CHAINWORK_CACHE = {
          159     "0000000000000000000000000000000000000000000000000000000000000000": 0,  # virtual block at height -1
          160 }  # type: Dict[str, int]
          161 
          162 
          163 def init_headers_file_for_best_chain():
          164     b = get_best_chain()
          165     filename = b.path()
          166     length = HEADER_SIZE * len(constants.net.CHECKPOINTS) * 2016
          167     if not os.path.exists(filename) or os.path.getsize(filename) < length:
          168         with open(filename, 'wb') as f:
          169             if length > 0:
          170                 f.seek(length - 1)
          171                 f.write(b'\x00')
          172         util.ensure_sparse_file(filename)
          173     with b.lock:
          174         b.update_size()
          175 
          176 
          177 class Blockchain(Logger):
          178     """
          179     Manages blockchain headers and their verification
          180     """
          181 
          182     def __init__(self, config: SimpleConfig, forkpoint: int, parent: Optional['Blockchain'],
          183                  forkpoint_hash: str, prev_hash: Optional[str]):
          184         assert isinstance(forkpoint_hash, str) and len(forkpoint_hash) == 64, forkpoint_hash
          185         assert (prev_hash is None) or (isinstance(prev_hash, str) and len(prev_hash) == 64), prev_hash
          186         # assert (parent is None) == (forkpoint == 0)
          187         if 0 < forkpoint <= constants.net.max_checkpoint():
          188             raise Exception(f"cannot fork below max checkpoint. forkpoint: {forkpoint}")
          189         Logger.__init__(self)
          190         self.config = config
          191         self.forkpoint = forkpoint  # height of first header
          192         self.parent = parent
          193         self._forkpoint_hash = forkpoint_hash  # blockhash at forkpoint. "first hash"
          194         self._prev_hash = prev_hash  # blockhash immediately before forkpoint
          195         self.lock = threading.RLock()
          196         self.update_size()
          197 
          198     def with_lock(func):
          199         def func_wrapper(self, *args, **kwargs):
          200             with self.lock:
          201                 return func(self, *args, **kwargs)
          202         return func_wrapper
          203 
          204     @property
          205     def checkpoints(self):
          206         return constants.net.CHECKPOINTS
          207 
          208     def get_max_child(self) -> Optional[int]:
          209         children = self.get_direct_children()
          210         return max([x.forkpoint for x in children]) if children else None
          211 
          212     def get_max_forkpoint(self) -> int:
          213         """Returns the max height where there is a fork
          214         related to this chain.
          215         """
          216         mc = self.get_max_child()
          217         return mc if mc is not None else self.forkpoint
          218 
          219     def get_direct_children(self) -> Sequence['Blockchain']:
          220         with blockchains_lock:
          221             return list(filter(lambda y: y.parent==self, blockchains.values()))
          222 
          223     def get_parent_heights(self) -> Mapping['Blockchain', int]:
          224         """Returns map: (parent chain -> height of last common block)"""
          225         with self.lock, blockchains_lock:
          226             result = {self: self.height()}
          227             chain = self
          228             while True:
          229                 parent = chain.parent
          230                 if parent is None: break
          231                 result[parent] = chain.forkpoint - 1
          232                 chain = parent
          233             return result
          234 
          235     def get_height_of_last_common_block_with_chain(self, other_chain: 'Blockchain') -> int:
          236         last_common_block_height = 0
          237         our_parents = self.get_parent_heights()
          238         their_parents = other_chain.get_parent_heights()
          239         for chain in our_parents:
          240             if chain in their_parents:
          241                 h = min(our_parents[chain], their_parents[chain])
          242                 last_common_block_height = max(last_common_block_height, h)
          243         return last_common_block_height
          244 
          245     @with_lock
          246     def get_branch_size(self) -> int:
          247         return self.height() - self.get_max_forkpoint() + 1
          248 
          249     def get_name(self) -> str:
          250         return self.get_hash(self.get_max_forkpoint()).lstrip('0')[0:10]
          251 
          252     def check_header(self, header: dict) -> bool:
          253         header_hash = hash_header(header)
          254         height = header.get('block_height')
          255         return self.check_hash(height, header_hash)
          256 
          257     def check_hash(self, height: int, header_hash: str) -> bool:
          258         """Returns whether the hash of the block at given height
          259         is the given hash.
          260         """
          261         assert isinstance(header_hash, str) and len(header_hash) == 64, header_hash  # hex
          262         try:
          263             return header_hash == self.get_hash(height)
          264         except Exception:
          265             return False
          266 
          267     def fork(parent, header: dict) -> 'Blockchain':
          268         if not parent.can_connect(header, check_height=False):
          269             raise Exception("forking header does not connect to parent chain")
          270         forkpoint = header.get('block_height')
          271         self = Blockchain(config=parent.config,
          272                           forkpoint=forkpoint,
          273                           parent=parent,
          274                           forkpoint_hash=hash_header(header),
          275                           prev_hash=parent.get_hash(forkpoint-1))
          276         self.assert_headers_file_available(parent.path())
          277         open(self.path(), 'w+').close()
          278         self.save_header(header)
          279         # put into global dict. note that in some cases
          280         # save_header might have already put it there but that's OK
          281         chain_id = self.get_id()
          282         with blockchains_lock:
          283             blockchains[chain_id] = self
          284         return self
          285 
          286     @with_lock
          287     def height(self) -> int:
          288         return self.forkpoint + self.size() - 1
          289 
          290     @with_lock
          291     def size(self) -> int:
          292         return self._size
          293 
          294     @with_lock
          295     def update_size(self) -> None:
          296         p = self.path()
          297         self._size = os.path.getsize(p)//HEADER_SIZE if os.path.exists(p) else 0
          298 
          299     @classmethod
          300     def verify_header(cls, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None:
          301         _hash = hash_header(header)
          302         if expected_header_hash and expected_header_hash != _hash:
          303             raise Exception("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash))
          304         if prev_hash != header.get('prev_block_hash'):
          305             raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
          306         if constants.net.TESTNET:
          307             return
          308         bits = cls.target_to_bits(target)
          309         if bits != header.get('bits'):
          310             raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits')))
          311         block_hash_as_num = int.from_bytes(bfh(_hash), byteorder='big')
          312         if block_hash_as_num > target:
          313             raise Exception(f"insufficient proof of work: {block_hash_as_num} vs target {target}")
          314 
          315     def verify_chunk(self, index: int, data: bytes) -> None:
          316         num = len(data) // HEADER_SIZE
          317         start_height = index * 2016
          318         prev_hash = self.get_hash(start_height - 1)
          319         target = self.get_target(index-1)
          320         for i in range(num):
          321             height = start_height + i
          322             try:
          323                 expected_header_hash = self.get_hash(height)
          324             except MissingHeader:
          325                 expected_header_hash = None
          326             raw_header = data[i*HEADER_SIZE : (i+1)*HEADER_SIZE]
          327             header = deserialize_header(raw_header, index*2016 + i)
          328             self.verify_header(header, prev_hash, target, expected_header_hash)
          329             prev_hash = hash_header(header)
          330 
          331     @with_lock
          332     def path(self):
          333         d = util.get_headers_dir(self.config)
          334         if self.parent is None:
          335             filename = 'blockchain_headers'
          336         else:
          337             assert self.forkpoint > 0, self.forkpoint
          338             prev_hash = self._prev_hash.lstrip('0')
          339             first_hash = self._forkpoint_hash.lstrip('0')
          340             basename = f'fork2_{self.forkpoint}_{prev_hash}_{first_hash}'
          341             filename = os.path.join('forks', basename)
          342         return os.path.join(d, filename)
          343 
          344     @with_lock
          345     def save_chunk(self, index: int, chunk: bytes):
          346         assert index >= 0, index
          347         chunk_within_checkpoint_region = index < len(self.checkpoints)
          348         # chunks in checkpoint region are the responsibility of the 'main chain'
          349         if chunk_within_checkpoint_region and self.parent is not None:
          350             main_chain = get_best_chain()
          351             main_chain.save_chunk(index, chunk)
          352             return
          353 
          354         delta_height = (index * 2016 - self.forkpoint)
          355         delta_bytes = delta_height * HEADER_SIZE
          356         # if this chunk contains our forkpoint, only save the part after forkpoint
          357         # (the part before is the responsibility of the parent)
          358         if delta_bytes < 0:
          359             chunk = chunk[-delta_bytes:]
          360             delta_bytes = 0
          361         truncate = not chunk_within_checkpoint_region
          362         self.write(chunk, delta_bytes, truncate)
          363         self.swap_with_parent()
          364 
          365     def swap_with_parent(self) -> None:
          366         with self.lock, blockchains_lock:
          367             # do the swap; possibly multiple ones
          368             cnt = 0
          369             while True:
          370                 old_parent = self.parent
          371                 if not self._swap_with_parent():
          372                     break
          373                 # make sure we are making progress
          374                 cnt += 1
          375                 if cnt > len(blockchains):
          376                     raise Exception(f'swapping fork with parent too many times: {cnt}')
          377                 # we might have become the parent of some of our former siblings
          378                 for old_sibling in old_parent.get_direct_children():
          379                     if self.check_hash(old_sibling.forkpoint - 1, old_sibling._prev_hash):
          380                         old_sibling.parent = self
          381 
          382     def _swap_with_parent(self) -> bool:
          383         """Check if this chain became stronger than its parent, and swap
          384         the underlying files if so. The Blockchain instances will keep
          385         'containing' the same headers, but their ids change and so
          386         they will be stored in different files."""
          387         if self.parent is None:
          388             return False
          389         if self.parent.get_chainwork() >= self.get_chainwork():
          390             return False
          391         self.logger.info(f"swapping {self.forkpoint} {self.parent.forkpoint}")
          392         parent_branch_size = self.parent.height() - self.forkpoint + 1
          393         forkpoint = self.forkpoint  # type: Optional[int]
          394         parent = self.parent  # type: Optional[Blockchain]
          395         child_old_id = self.get_id()
          396         parent_old_id = parent.get_id()
          397         # swap files
          398         # child takes parent's name
          399         # parent's new name will be something new (not child's old name)
          400         self.assert_headers_file_available(self.path())
          401         child_old_name = self.path()
          402         with open(self.path(), 'rb') as f:
          403             my_data = f.read()
          404         self.assert_headers_file_available(parent.path())
          405         assert forkpoint > parent.forkpoint, (f"forkpoint of parent chain ({parent.forkpoint}) "
          406                                               f"should be at lower height than children's ({forkpoint})")
          407         with open(parent.path(), 'rb') as f:
          408             f.seek((forkpoint - parent.forkpoint)*HEADER_SIZE)
          409             parent_data = f.read(parent_branch_size*HEADER_SIZE)
          410         self.write(parent_data, 0)
          411         parent.write(my_data, (forkpoint - parent.forkpoint)*HEADER_SIZE)
          412         # swap parameters
          413         self.parent, parent.parent = parent.parent, self  # type: Optional[Blockchain], Optional[Blockchain]
          414         self.forkpoint, parent.forkpoint = parent.forkpoint, self.forkpoint
          415         self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(bh2u(parent_data[:HEADER_SIZE]))
          416         self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash
          417         # parent's new name
          418         os.replace(child_old_name, parent.path())
          419         self.update_size()
          420         parent.update_size()
          421         # update pointers
          422         blockchains.pop(child_old_id, None)
          423         blockchains.pop(parent_old_id, None)
          424         blockchains[self.get_id()] = self
          425         blockchains[parent.get_id()] = parent
          426         return True
          427 
          428     def get_id(self) -> str:
          429         return self._forkpoint_hash
          430 
          431     def assert_headers_file_available(self, path):
          432         if os.path.exists(path):
          433             return
          434         elif not os.path.exists(util.get_headers_dir(self.config)):
          435             raise FileNotFoundError('Electrum headers_dir does not exist. Was it deleted while running?')
          436         else:
          437             raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path))
          438 
          439     @with_lock
          440     def write(self, data: bytes, offset: int, truncate: bool=True) -> None:
          441         filename = self.path()
          442         self.assert_headers_file_available(filename)
          443         with open(filename, 'rb+') as f:
          444             if truncate and offset != self._size * HEADER_SIZE:
          445                 f.seek(offset)
          446                 f.truncate()
          447             f.seek(offset)
          448             f.write(data)
          449             f.flush()
          450             os.fsync(f.fileno())
          451         self.update_size()
          452 
          453     @with_lock
          454     def save_header(self, header: dict) -> None:
          455         delta = header.get('block_height') - self.forkpoint
          456         data = bfh(serialize_header(header))
          457         # headers are only _appended_ to the end:
          458         assert delta == self.size(), (delta, self.size())
          459         assert len(data) == HEADER_SIZE
          460         self.write(data, delta*HEADER_SIZE)
          461         self.swap_with_parent()
          462 
          463     @with_lock
          464     def read_header(self, height: int) -> Optional[dict]:
          465         if height < 0:
          466             return
          467         if height < self.forkpoint:
          468             return self.parent.read_header(height)
          469         if height > self.height():
          470             return
          471         delta = height - self.forkpoint
          472         name = self.path()
          473         self.assert_headers_file_available(name)
          474         with open(name, 'rb') as f:
          475             f.seek(delta * HEADER_SIZE)
          476             h = f.read(HEADER_SIZE)
          477             if len(h) < HEADER_SIZE:
          478                 raise Exception('Expected to read a full header. This was only {} bytes'.format(len(h)))
          479         if h == bytes([0])*HEADER_SIZE:
          480             return None
          481         return deserialize_header(h, height)
          482 
          483     def header_at_tip(self) -> Optional[dict]:
          484         """Return latest header."""
          485         height = self.height()
          486         return self.read_header(height)
          487 
          488     def is_tip_stale(self) -> bool:
          489         STALE_DELAY = 8 * 60 * 60  # in seconds
          490         header = self.header_at_tip()
          491         if not header:
          492             return True
          493         # note: We check the timestamp only in the latest header.
          494         #       The Bitcoin consensus has a lot of leeway here:
          495         #       - needs to be greater than the median of the timestamps of the past 11 blocks, and
          496         #       - up to at most 2 hours into the future compared to local clock
          497         #       so there is ~2 hours of leeway in either direction
          498         if header['timestamp'] + STALE_DELAY < time.time():
          499             return True
          500         return False
          501 
          502     def get_hash(self, height: int) -> str:
          503         def is_height_checkpoint():
          504             within_cp_range = height <= constants.net.max_checkpoint()
          505             at_chunk_boundary = (height+1) % 2016 == 0
          506             return within_cp_range and at_chunk_boundary
          507 
          508         if height == -1:
          509             return '0000000000000000000000000000000000000000000000000000000000000000'
          510         elif height == 0:
          511             return constants.net.GENESIS
          512         elif is_height_checkpoint():
          513             index = height // 2016
          514             h, t = self.checkpoints[index]
          515             return h
          516         else:
          517             header = self.read_header(height)
          518             if header is None:
          519                 raise MissingHeader(height)
          520             return hash_header(header)
          521 
          522     def get_target(self, index: int) -> int:
          523         # compute target from chunk x, used in chunk x+1
          524         if constants.net.TESTNET:
          525             return 0
          526         if index == -1:
          527             return MAX_TARGET
          528         if index < len(self.checkpoints):
          529             h, t = self.checkpoints[index]
          530             return t
          531         # new target
          532         first = self.read_header(index * 2016)
          533         last = self.read_header(index * 2016 + 2015)
          534         if not first or not last:
          535             raise MissingHeader()
          536         bits = last.get('bits')
          537         target = self.bits_to_target(bits)
          538         nActualTimespan = last.get('timestamp') - first.get('timestamp')
          539         nTargetTimespan = 14 * 24 * 60 * 60
          540         nActualTimespan = max(nActualTimespan, nTargetTimespan // 4)
          541         nActualTimespan = min(nActualTimespan, nTargetTimespan * 4)
          542         new_target = min(MAX_TARGET, (target * nActualTimespan) // nTargetTimespan)
          543         # not any target can be represented in 32 bits:
          544         new_target = self.bits_to_target(self.target_to_bits(new_target))
          545         return new_target
          546 
          547     @classmethod
          548     def bits_to_target(cls, bits: int) -> int:
          549         bitsN = (bits >> 24) & 0xff
          550         if not (0x03 <= bitsN <= 0x1d):
          551             raise Exception("First part of bits should be in [0x03, 0x1d]")
          552         bitsBase = bits & 0xffffff
          553         if not (0x8000 <= bitsBase <= 0x7fffff):
          554             raise Exception("Second part of bits should be in [0x8000, 0x7fffff]")
          555         return bitsBase << (8 * (bitsN-3))
          556 
          557     @classmethod
          558     def target_to_bits(cls, target: int) -> int:
          559         c = ("%064x" % target)[2:]
          560         while c[:2] == '00' and len(c) > 6:
          561             c = c[2:]
          562         bitsN, bitsBase = len(c) // 2, int.from_bytes(bfh(c[:6]), byteorder='big')
          563         if bitsBase >= 0x800000:
          564             bitsN += 1
          565             bitsBase >>= 8
          566         return bitsN << 24 | bitsBase
          567 
          568     def chainwork_of_header_at_height(self, height: int) -> int:
          569         """work done by single header at given height"""
          570         chunk_idx = height // 2016 - 1
          571         target = self.get_target(chunk_idx)
          572         work = ((2 ** 256 - target - 1) // (target + 1)) + 1
          573         return work
          574 
          575     @with_lock
          576     def get_chainwork(self, height=None) -> int:
          577         if height is None:
          578             height = max(0, self.height())
          579         if constants.net.TESTNET:
          580             # On testnet/regtest, difficulty works somewhat different.
          581             # It's out of scope to properly implement that.
          582             return height
          583         last_retarget = height // 2016 * 2016 - 1
          584         cached_height = last_retarget
          585         while _CHAINWORK_CACHE.get(self.get_hash(cached_height)) is None:
          586             if cached_height <= -1:
          587                 break
          588             cached_height -= 2016
          589         assert cached_height >= -1, cached_height
          590         running_total = _CHAINWORK_CACHE[self.get_hash(cached_height)]
          591         while cached_height < last_retarget:
          592             cached_height += 2016
          593             work_in_single_header = self.chainwork_of_header_at_height(cached_height)
          594             work_in_chunk = 2016 * work_in_single_header
          595             running_total += work_in_chunk
          596             _CHAINWORK_CACHE[self.get_hash(cached_height)] = running_total
          597         cached_height += 2016
          598         work_in_single_header = self.chainwork_of_header_at_height(cached_height)
          599         work_in_last_partial_chunk = (height % 2016 + 1) * work_in_single_header
          600         return running_total + work_in_last_partial_chunk
          601 
          602     def can_connect(self, header: dict, check_height: bool=True) -> bool:
          603         if header is None:
          604             return False
          605         height = header['block_height']
          606         if check_height and self.height() != height - 1:
          607             return False
          608         if height == 0:
          609             return hash_header(header) == constants.net.GENESIS
          610         try:
          611             prev_hash = self.get_hash(height - 1)
          612         except:
          613             return False
          614         if prev_hash != header.get('prev_block_hash'):
          615             return False
          616         try:
          617             target = self.get_target(height // 2016 - 1)
          618         except MissingHeader:
          619             return False
          620         try:
          621             self.verify_header(header, prev_hash, target)
          622         except BaseException as e:
          623             return False
          624         return True
          625 
          626     def connect_chunk(self, idx: int, hexdata: str) -> bool:
          627         assert idx >= 0, idx
          628         try:
          629             data = bfh(hexdata)
          630             self.verify_chunk(idx, data)
          631             self.save_chunk(idx, data)
          632             return True
          633         except BaseException as e:
          634             self.logger.info(f'verify_chunk idx {idx} failed: {repr(e)}')
          635             return False
          636 
          637     def get_checkpoints(self):
          638         # for each chunk, store the hash of the last block and the target after the chunk
          639         cp = []
          640         n = self.height() // 2016
          641         for index in range(n):
          642             h = self.get_hash((index+1) * 2016 -1)
          643             target = self.get_target(index)
          644             cp.append((h, target))
          645         return cp
          646 
          647 
          648 def check_header(header: dict) -> Optional[Blockchain]:
          649     """Returns any Blockchain that contains header, or None."""
          650     if type(header) is not dict:
          651         return None
          652     with blockchains_lock: chains = list(blockchains.values())
          653     for b in chains:
          654         if b.check_header(header):
          655             return b
          656     return None
          657 
          658 
          659 def can_connect(header: dict) -> Optional[Blockchain]:
          660     """Returns the Blockchain that has a tip that directly links up
          661     with header, or None.
          662     """
          663     with blockchains_lock: chains = list(blockchains.values())
          664     for b in chains:
          665         if b.can_connect(header):
          666             return b
          667     return None
          668 
          669 
          670 def get_chains_that_contain_header(height: int, header_hash: str) -> Sequence[Blockchain]:
          671     """Returns a list of Blockchains that contain header, best chain first."""
          672     with blockchains_lock: chains = list(blockchains.values())
          673     chains = [chain for chain in chains
          674               if chain.check_hash(height=height, header_hash=header_hash)]
          675     chains = sorted(chains, key=lambda x: x.get_chainwork(), reverse=True)
          676     return chains