# Electrum - lightweight Bitcoin client
# Copyright (C) 2012 thomasv@ecdsa.org
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import os
import threading
from typing import Optional, Dict, Mapping, Sequence

from . import util
from .bitcoin import hash_encode, int_to_hex, rev_hex
from .crypto import sha256d
from . import constants
from .util import bfh, bh2u
from .simple_config import SimpleConfig


HEADER_SIZE = 80  # bytes, 160 chars (TEST)
MAX_TARGET = 0x00000000FFFF0000000000000000000000000000000000000000000000000000


class MissingHeader(Exception):
    pass

class InvalidHeader(Exception):
    pass

#Calvin: Accepts a dictionary of header values and appends all the values to a hexadecimal string representing the block header
 #Calvin: The bits value is the difficulty of the block
    #Aviv: Headers are propogated in a serialized format (80 bytes) and found in the block in a serialized format
    #Aviv: So this method is likely used to recontrcut headers that have already been validated.
def serialize_header(header_dict: dict) -> str:
    s = int_to_hex(header_dict['version'], 4) \
        + rev_hex(header_dict['prev_block_hash']) \
        + rev_hex(header_dict['merkle_root']) \
        + int_to_hex(int(header_dict['timestamp']), 4) \
        + int_to_hex(int(header_dict['bits']), 4) \
        + int_to_hex(int(header_dict['nonce']), 4)
    return s

#Calvin: Accepts a (presumably hexadecimal) string representing the block header and converts it to a dictionary
def deserialize_header(s: bytes, height: int) -> dict:
    # Calvin: If s is null
    if not s:
        raise InvalidHeader('Invalid header: {}'.format(s))
    # Calvin: Validates that the header is 80 bytes
    if len(s) != HEADER_SIZE:
        raise InvalidHeader('Invalid header length: {}'.format(len(s)))
    hex_to_int = lambda s: int.from_bytes(s, byteorder='little')
    h = {}                                                  #aviv:
    h['version'] = hex_to_int(s[0:4])                       # Version allows miners to signal which soft-fork rules they support
    h['prev_block_hash'] = hash_encode(s[4:36])             # MUST be the Hash(last_header)
    h['merkle_root'] = hash_encode(s[36:68])                # A commitment to all Transactions inside of this block
    h['timestamp'] = hex_to_int(s[68:72])                   # A UNIX timestamp
    h['bits'] = hex_to_int(s[72:76])                        # The current difficulty for mining --> Target Hash
    h['nonce'] = hex_to_int(s[76:80])                       # Number with no meaning, for miners to change the hash
    h['block_height'] = height                              # NOT PART OF THE HEADER
    return h


def hash_header(header: dict) -> str:
    # Calvin: Should only hit these two cases if receiving very first block
    # Aviv: Header is a dictionary with keys ('version','merkle_root', 'height', etc.) and values
    #       It represents the latest valid header that you have.
    if header is None:
        return '0' * 64 #TODO-Calvin: why would it return 64 0's?
    if header.get('prev_block_hash') is None:
        header['prev_block_hash'] = '00'*32 #TODO-Calvin: why would it return 64 0's?
    return hash_raw_header(serialize_header(header))

#Calvin: Converts Header String (Presumably Hex) to bytes, then double hashes, reverses the order and returns the hex value (hash_encode)
def hash_raw_header(header: str) -> str:
    return hash_encode(sha256d(bfh(header)))


# key: blockhash hex at forkpoint
# the chain at some key is the best chain that includes the given hash
blockchains = {}  # type: Dict[str, Blockchain]
blockchains_lock = threading.RLock()


# Calvin: This is executed when the wallet first connects to the network, effectively builds the current blockchain locally
def read_blockchains(config: 'SimpleConfig'):
    best_chain = Blockchain(config=config,
                            forkpoint=0,
                            parent=None,
                            forkpoint_hash=constants.net.GENESIS,
                            prev_hash=None)
    blockchains[constants.net.GENESIS] = best_chain
    # consistency checks
    # Calvin: Seems like this is validation that we would need
    # Aviv: It looks like the checkpoint is 1 before the new epoch (2016*) and checks if the next block is valid,
    #       If it is not valid, then it deletes that checkpoint. 
    # Todo-Calvin: Isn't the height 0 at this point? Need to find if there is some kind of header file that exists during this initialization...
    if best_chain.height() > constants.net.max_checkpoint():
        header_after_cp = best_chain.read_header(constants.net.max_checkpoint()+1)
        if not header_after_cp or not best_chain.can_connect(header_after_cp, check_height=False):
            util.print_error("[blockchain] deleting best chain. cannot connect header after last cp to last cp.")
            os.unlink(best_chain.path())
            best_chain.update_size()
    # forks
    # Calvin: makes a folder called 'forks' wherever config.path is defined
    fdir = os.path.join(util.get_headers_dir(config), 'forks')
    util.make_dir(fdir)
    # files are named as: fork2_{forkpoint}_{prev_hash}_{first_hash}
    # Calvin: returns the list of files that follows the format above and no '.' and sorts the list
    l = filter(lambda x: x.startswith('fork2_') and '.' not in x, os.listdir(fdir))
    l = sorted(l, key=lambda x: int(x.split('_')[1]))  # sort by forkpoint


    # Calvin: unlink will delete the file
    def delete_chain(filename, reason):
        util.print_error(f"[blockchain] deleting chain {filename}: {reason}")
        os.unlink(os.path.join(fdir, filename))


    # Calvin: Performs header validation and then appends the block to the blockchain if validation passes
    def instantiate_chain(filename):
        # files are named as: fork2_{forkpoint}_{prev_hash}_{first_hash}
        __, forkpoint, prev_hash, first_hash = filename.split('_')
        forkpoint = int(forkpoint)
        # Calvin: The hash value used is 64 characters
        prev_hash = (64-len(prev_hash)) * "0" + prev_hash  # left-pad with zeroes
        first_hash = (64-len(first_hash)) * "0" + first_hash
        # forks below the max checkpoint are not allowed
        # Calvin: Checkpoints are when the network agree on the best chain, therefore forks should not exist before the latest checkpoint
        if forkpoint <= constants.net.max_checkpoint():
            delete_chain(filename, "deleting fork below max checkpoint")
            return
        # find parent (sorting by forkpoint guarantees it's already instantiated TODO-Calvin: why?)
        # Todo-Calvin: Where does the sorting occur?
        for parent in blockchains.values():
            if parent.check_hash(forkpoint - 1, prev_hash):  # Calvin: Seems like the forkpoint is always the beginning of a new block
                break
        else:
            delete_chain(filename, "cannot find parent for chain")
            return
        b = Blockchain(config=config,
                       forkpoint=forkpoint,
                       parent=parent,
                       forkpoint_hash=first_hash,
                       prev_hash=prev_hash)
        # consistency checks
        # Todo-Calvin: Why is this necessary if the Blockchain constructor does what its told (should only fail if a bug?)
        # Calvin: Answer is that it checks that the file was created properly
        h = b.read_header(b.forkpoint) # Calvin: gets the dictionary value of the header at the forkpoint
        if first_hash != hash_header(h):  # Calvin: Checks that the forkpoint header is the beginning of the block
            delete_chain(filename, "incorrect first hash for chain")
            return
        if not b.parent.can_connect(h, check_height=False):
            delete_chain(filename, "cannot connect chain to parent")
            return
        chain_id = b.get_id()
        # assert <condition>,<error message>
        assert first_hash == chain_id, (first_hash, chain_id)
        blockchains[chain_id] = b

    for filename in l:
        instantiate_chain(filename)


def get_best_chain() -> 'Blockchain':
    return blockchains[constants.net.GENESIS]

# Calvin: what is the chainwork cache used for? It is the total number of hashes that are expected to have been necessary to produce the current chain, in hexadecimal.
# block hash -> chain work; up to and including that block
# Aviv: The lowest difficulty for the chain is 1, which has a chainwork of 0100010001 in hex. Every additional block at that difficulty adds
#       That ammount to the chainwork. So chainwork of a block is trivial Chainwork = difficulty * 0100010001
#       The reason why chainwork is important is that the real way to find the best blockchain is to find the chain that has the most
#       Cumulative proof-of-work (which is the chainwork_cache) and not the longest chain.
_CHAINWORK_CACHE = {
    "0000000000000000000000000000000000000000000000000000000000000000": 0,  # virtual block at height -1
}  # type: Dict[str, int]


class Blockchain(util.PrintError):
    """
    Manages blockchain headers and their verification

    Calvin Notes:

    - Forkpoint seems to the beginning of the block
    - Height is treated as an index
    """

    def __init__(self, config: SimpleConfig, forkpoint: int, parent: Optional['Blockchain'],
                 forkpoint_hash: str, prev_hash: Optional[str]):
        assert isinstance(forkpoint_hash, str) and len(forkpoint_hash) == 64, forkpoint_hash # Calvin: forkpoint_hash validation
        assert (prev_hash is None) or (isinstance(prev_hash, str) and len(prev_hash) == 64), prev_hash # Calvin: prev_hash validation
        # assert (parent is None) == (forkpoint == 0)
        if 0 < forkpoint <= constants.net.max_checkpoint():
            raise Exception(f"cannot fork below max checkpoint. forkpoint: {forkpoint}")
        self.config = config
        self.forkpoint = forkpoint  # height of first header
        self.parent = parent
        self._forkpoint_hash = forkpoint_hash  # blockhash at forkpoint. "first hash"
        self._prev_hash = prev_hash  # blockhash immediately before forkpoint
        self.lock = threading.RLock()
        self.update_size()

    def with_lock(func):
        def func_wrapper(self, *args, **kwargs):
            with self.lock:
                return func(self, *args, **kwargs)
        return func_wrapper

    # Calvin: Checkpoints are defined in a json file called checkpoints.json? Todo: Where does it get an updated version?
    # Aviv: The devs update the checkpoints file every so often and the new realse of electrum to the app store has more up-to-date checkpoints.
    #       Users who update their wallets will likely not care, as they have all of the latest headers anyway.
    #       This only benefits new users who create a new wallet.
    @property
    def checkpoints(self):
        return constants.net.CHECKPOINTS

    # Calvin: Get the max forkpoint (header) of all the children, return the max or None
    def get_max_child(self) -> Optional[int]:
        children = self.get_direct_children()
        return max([x.forkpoint for x in children]) if children else None

    # Calvin: Return the max child forkpoint if not null or else return the current chain's forkpoint
    def get_max_forkpoint(self) -> int:
        """Returns the max height where there is a fork
        related to this chain.
        """
        mc = self.get_max_child()
        return mc if mc is not None else self.forkpoint

    # Calvin: gets a list of all immediate children of the current chain
    def get_direct_children(self) -> Sequence['Blockchain']:
        with blockchains_lock:
            return list(filter(lambda y: y.parent==self, blockchains.values()))

    # Calvin: Returns a map of all the blocks and their height
    def get_parent_heights(self) -> Mapping['Blockchain', int]:
        """Returns map: (parent chain -> height of last common block)"""
        with blockchains_lock:
            result = {self: self.height()}
            chain = self
            while True:
                parent = chain.parent
                if parent is None: break
                result[parent] = chain.forkpoint - 1
                chain = parent
            return result

    # Calvin: Merges two height maps by taking their minimum values for each key and finds the maximum height value in the union of the maps
    def get_height_of_last_common_block_with_chain(self, other_chain: 'Blockchain') -> int:
        last_common_block_height = 0
        our_parents = self.get_parent_heights()
        their_parents = other_chain.get_parent_heights()
        for chain in our_parents:
            if chain in their_parents:
                h = min(our_parents[chain], their_parents[chain])
                last_common_block_height = max(last_common_block_height, h)
        return last_common_block_height

    # Calvin: get the current height compared to the max forkpoint
    @with_lock
    def get_branch_size(self) -> int:
        return self.height() - self.get_max_forkpoint() + 1

    # Calvin: Get forkpoint header, hash it, strip leading 0s, return substring
    def get_name(self) -> str:
        return self.get_hash(self.get_max_forkpoint()).lstrip('0')[0:10]

    # Calvin: Verifies that the header hash matches the hash of the block
    # Aviv: Slight correction - it is just checking that the hash of header-n is the hash stored.
    def check_header(self, header: dict) -> bool:
        header_hash = hash_header(header)
        height = header.get('block_height')
        return self.check_hash(height, header_hash)

    def check_hash(self, height: int, header_hash: str) -> bool:
        """Returns whether the hash of the block at given height
        is the given hash.
        """
        assert isinstance(header_hash, str) and len(header_hash) == 64, header_hash  # hex
        try:
            return header_hash == self.get_hash(height)
        except Exception:
            return False

    # Calvin: Adds a fork to the blockchain
    def fork(parent, header: dict) -> 'Blockchain':
        if not parent.can_connect(header, check_height=False):
            raise Exception("forking header does not connect to parent chain")
        forkpoint = header.get('block_height')
        self = Blockchain(config=parent.config,
                          forkpoint=forkpoint,
                          parent=parent,
                          forkpoint_hash=hash_header(header),
                          prev_hash=parent.get_hash(forkpoint-1))
        open(self.path(), 'w+').close() # Calvin: Creating the block file
        self.save_header(header)
        # put into global dict. note that in some cases
        # save_header might have already put it there but that's OK
        chain_id = self.get_id()
        with blockchains_lock:
            blockchains[chain_id] = self
        return self

    # Calvin: Returns the height of the current block
    @with_lock
    def height(self) -> int:
        return self.forkpoint + self.size() - 1

    # Calvin: Returns the size of the current block
    @with_lock
    def size(self) -> int:
        return self._size

    # Calvin: This will get the current size of the blockchain by dividing the file size by the HEADER_SIZE
    @with_lock
    def update_size(self) -> None:
        p = self.path()
        self._size = os.path.getsize(p)//HEADER_SIZE if os.path.exists(p) else 0

    # Calvin: This verifies the value of the header provided against the expected header
    # Calvin: What is the target?
    @classmethod
    def verify_header(cls, header: dict, prev_hash: str, target: int, expected_header_hash: str=None) -> None:
        _hash = hash_header(header)
        if expected_header_hash and expected_header_hash != _hash:
            raise Exception("hash mismatches with expected: {} vs {}".format(expected_header_hash, _hash))
        if prev_hash != header.get('prev_block_hash'):
            raise Exception("prev hash mismatch: %s vs %s" % (prev_hash, header.get('prev_block_hash')))
        if constants.net.TESTNET:
            return
        bits = cls.target_to_bits(target)
        if bits != header.get('bits'):
            raise Exception("bits mismatch: %s vs %s" % (bits, header.get('bits')))
        block_hash_as_num = int.from_bytes(bfh(_hash), byteorder='big')
        if block_hash_as_num > target:
            raise Exception(f"insufficient proof of work: {block_hash_as_num} vs target {target}")

    # Todo-Calvin: Chuck = Chuck of data?
    # Calvin: Verify that all of the data in this block have valid header values
    def verify_chunk(self, index: int, data: bytes) -> None:
        # Todo-Calvin: What does this num represent? what is data?
        num = len(data) // HEADER_SIZE
        start_height = index * 2016
        prev_hash = self.get_hash(start_height - 1)
        # Calvin: Get the target of the previous block
        target = self.get_target(index-1)
        # Calvin: Verify that all of the data in this block meet the target difficulty
        for i in range(num):
            height = start_height + i
            try:
                expected_header_hash = self.get_hash(height)
            except MissingHeader:
                expected_header_hash = None
            # Calvin: I think this gets a subset of the bytes (gets 80 bytes)
            raw_header = data[i*HEADER_SIZE : (i+1)*HEADER_SIZE]
            header = deserialize_header(raw_header, index*2016 + i)
            self.verify_header(header, prev_hash, target, expected_header_hash)
            prev_hash = hash_header(header)

    # Calvin: This defines the path for the file to be saved
    @with_lock
    def path(self):
        d = util.get_headers_dir(self.config)
        if self.parent is None:
            filename = 'blockchain_headers'
        else:
            assert self.forkpoint > 0, self.forkpoint
            prev_hash = self._prev_hash.lstrip('0')
            first_hash = self._forkpoint_hash.lstrip('0')
            basename = f'fork2_{self.forkpoint}_{prev_hash}_{first_hash}'
            filename = os.path.join('forks', basename)
        return os.path.join(d, filename)

    # Calvin: Saves a chunk of headers
    @with_lock
    def save_chunk(self, index: int, chunk: bytes):
        assert index >= 0, index
        # Todo-Calvin: When would the chunk have to be within the checkpoints length?
        chunk_within_checkpoint_region = index < len(self.checkpoints)
        # chunks in checkpoint region are the responsibility of the 'main chain'
        # Todo-Calvin: The main chain
        if chunk_within_checkpoint_region and self.parent is not None:
            main_chain = get_best_chain()
            main_chain.save_chunk(index, chunk)
            return

        # Calvin: Why is it doing 2016 at a time? This must be saving all the data up to a checkpoint
        delta_height = (index * 2016 - self.forkpoint)
        delta_bytes = delta_height * HEADER_SIZE
        # if this chunk contains our forkpoint, only save the part after forkpoint
        # (the part before is the responsibility of the parent)
        if delta_bytes < 0:
            chunk = chunk[-delta_bytes:] # index starts from end, backwards
            delta_bytes = 0
        truncate = not chunk_within_checkpoint_region
        self.write(chunk, delta_bytes, truncate)
        self.swap_with_parent()

    # Calvin: Called everytime we save a single or chunk of headers to this blockchain
    def swap_with_parent(self) -> None:
        with self.lock, blockchains_lock: # Calvin: Does the internal stuff only after acquiring a lock
            # do the swap; possibly multiple ones # Todo-Calvin: Why multiple swaps?
            cnt = 0
            while True:
                old_parent = self.parent
                if not self._swap_with_parent():
                    break
                # make sure we are making progress
                cnt += 1
                if cnt > len(blockchains): # Calvin: Stops an infinite loop of swapping
                    raise Exception(f'swapping fork with parent too many times: {cnt}')
                # we might have become the parent of some of our former siblings
                # Calvin: Since we have replaced the parent, we need to update the reference to the Blockchain object for all of our siblings
                for old_sibling in old_parent.get_direct_children():
                    if self.check_hash(old_sibling.forkpoint - 1, old_sibling._prev_hash): # Calvin: Update the sibling if they point to the same hash
                        old_sibling.parent = self

    # Calvin: IMPORTANT! These keeps the main_chain/best_chain up to date
    def _swap_with_parent(self) -> bool:
        """Check if this chain became stronger than its parent, and swap
        the underlying files if so. The Blockchain instances will keep
        'containing' the same headers, but their ids change and so
        they will be stored in different files."""
        if self.parent is None:
            return False
        if self.parent.get_chainwork() >= self.get_chainwork():
            return False
        self.print_error("swap", self.forkpoint, self.parent.forkpoint) #Calvin: We should see in the logs when a swap happens
        parent_branch_size = self.parent.height() - self.forkpoint + 1
        forkpoint = self.forkpoint  # type: Optional[int]
        parent = self.parent  # type: Optional[Blockchain]
        child_old_id = self.get_id()
        parent_old_id = parent.get_id()
        # swap files
        # child takes parent's name
        # parent's new name will be something new (not child's old name) Calvin: This makes sense, otherwise the hash would be invalid
        self.assert_headers_file_available(self.path())
        child_old_name = self.path()
        with open(self.path(), 'rb') as f:
            my_data = f.read()
        self.assert_headers_file_available(parent.path())
        assert forkpoint > parent.forkpoint, (f"forkpoint of parent chain ({parent.forkpoint}) "
                                              f"should be at lower height than children's ({forkpoint})")
        with open(parent.path(), 'rb') as f:
            # Calvin: forkpoint - parent.forkpoint is technically the height of this blockchain, why not use height method?
            # Calvin: Answer: There is a main_chain, this uses the blockchain with the greatest chainwork as the main_chain
            f.seek((forkpoint - parent.forkpoint)*HEADER_SIZE) # Calvin: This excludes the forkpoint_hash, why? Technically the forkpoints have the same first header! Saves a few bytes of writing.
            parent_data = f.read(parent_branch_size*HEADER_SIZE)
        self.write(parent_data, 0) # Calvin: writes the parents block data into this (current child)
        parent.write(my_data, (forkpoint - parent.forkpoint)*HEADER_SIZE) # Calvin: writes the child's block data into parents file
        # swap parameters # Calvin: Swaps the childs parents to be the parent's parent and the parent's parent is now the previous child
        self.parent, parent.parent = parent.parent, self  # type: Optional[Blockchain], Optional[Blockchain]
        self.forkpoint, parent.forkpoint = parent.forkpoint, self.forkpoint
        self._forkpoint_hash, parent._forkpoint_hash = parent._forkpoint_hash, hash_raw_header(bh2u(parent_data[:HEADER_SIZE])) # Swaps the forkpoint_hash values
        self._prev_hash, parent._prev_hash = parent._prev_hash, self._prev_hash
        # parent's new name
        os.replace(child_old_name, parent.path())
        self.update_size()
        parent.update_size()
        # update pointers
        blockchains.pop(child_old_id, None)
        blockchains.pop(parent_old_id, None)
        blockchains[self.get_id()] = self
        blockchains[parent.get_id()] = parent
        return True

    # Calvin: Uses the forkpoint_has as the id of this chunk, used for the blockchain cache
    def get_id(self) -> str:
        return self._forkpoint_hash

    # Calvin: Checks that the headers directory exists
    def assert_headers_file_available(self, path):
        if os.path.exists(path):
            return
        elif not os.path.exists(util.get_headers_dir(self.config)):
            raise FileNotFoundError('Electrum headers_dir does not exist. Was it deleted while running?')
        else:
            raise FileNotFoundError('Cannot find headers file but headers_dir is there. Should be at {}'.format(path))

    # Calvin: Writes the given bytes at the offset provided into the header file of this block
    @with_lock
    def write(self, data: bytes, offset: int, truncate: bool=True) -> None:
        filename = self.path()
        self.assert_headers_file_available(filename)
        with open(filename, 'rb+') as f:
            if truncate and offset != self._size * HEADER_SIZE:
                f.seek(offset)
                f.truncate()
            f.seek(offset)
            f.write(data)
            f.flush()
            os.fsync(f.fileno())
        self.update_size()

    # Calvin: Saves the header dictionary as bytes into the header file.
    @with_lock
    def save_header(self, header: dict) -> None:
        delta = header.get('block_height') - self.forkpoint # Todo-Calvin: Is the forkpoint treated as the beginning of every block?
        data = bfh(serialize_header(header))
        # headers are only _appended_ to the end:
        assert delta == self.size(), (delta, self.size())
        assert len(data) == HEADER_SIZE
        self.write(data, delta*HEADER_SIZE) # Calvin: Writes the header into the blockchain
        self.swap_with_parent()

    # Calvin: Reads the header from file and returns the dictionary value
    @with_lock
    def read_header(self, height: int) -> Optional[dict]:
        if height < 0:
            return
        if height < self.forkpoint: #Calvin: Recursively looks for the header
            return self.parent.read_header(height)
        if height > self.height():
            return
        delta = height - self.forkpoint
        name = self.path()
        self.assert_headers_file_available(name)
        # Calvin: opens the current block's header file in read bytes mode
        with open(name, 'rb') as f:
            f.seek(delta * HEADER_SIZE) # Calvin: find the forkpoint's header
            h = f.read(HEADER_SIZE) # Calvin: read the forkpoint's header
            if len(h) < HEADER_SIZE: # Calvin: Validate that the forkpoint is the correct length (when would this be false, eof?)
                raise Exception('Expected to read a full header. This was only {} bytes'.format(len(h)))
        if h == bytes([0])*HEADER_SIZE: # Calvin: If header is all 0's?
            return None
        return deserialize_header(h, height) # Calvin: Convert the header to a dictionary

    def header_at_tip(self) -> Optional[dict]:
        """Return latest header."""
        height = self.height()
        return self.read_header(height)

    # Calvin: Returns the hash of the header at the given block index.
    def get_hash(self, height: int) -> str:
        # Calvin: Current max block size is 2016. Height is synonymous with block size.
        def is_height_checkpoint():
            within_cp_range = height <= constants.net.max_checkpoint()
            at_chunk_boundary = (height+1) % 2016 == 0
            return within_cp_range and at_chunk_boundary

        if height == -1:
            return '0000000000000000000000000000000000000000000000000000000000000000' # Todo-Calvin: Why return all 0's?
        elif height == 0:
            return constants.net.GENESIS
        elif is_height_checkpoint():
            index = height // 2016
            h, t = self.checkpoints[index]
            return h
        else:
            header = self.read_header(height)
            if header is None:
                raise MissingHeader(height)
            return hash_header(header)


    # Calvin: Gets the target of the header at the given index
    # Calvin: Bits represents the target difficulty of this block
    def get_target(self, index: int) -> int:
        # compute target from chunk x, used in chunk x+1
        if constants.net.TESTNET:
            return 0
        if index == -1:
            return MAX_TARGET
        if index < len(self.checkpoints):
            h, t = self.checkpoints[index]
            return t
        # new target
        first = self.read_header(index * 2016)
        last = self.read_header(index * 2016 + 2015)
        if not first or not last:
            raise MissingHeader()
        bits = last.get('bits')
        target = self.bits_to_target(bits)
        nActualTimespan = last.get('timestamp') - first.get('timestamp')
        nTargetTimespan = 14 * 24 * 60 * 60
        # Todo-Calvin: What is the significance of 4?
        nActualTimespan = max(nActualTimespan, nTargetTimespan // 4)
        nActualTimespan = min(nActualTimespan, nTargetTimespan * 4)
        new_target = min(MAX_TARGET, (target * nActualTimespan) // nTargetTimespan)
        # not any target can be represented in 32 bits:
        new_target = self.bits_to_target(self.target_to_bits(new_target))
        return new_target

    # Calvin: Converts the bits (from the header) to the target difficulty.
    @classmethod
    def bits_to_target(cls, bits: int) -> int:
        bitsN = (bits >> 24) & 0xff
        if not (0x03 <= bitsN <= 0x1d):
            raise Exception("First part of bits should be in [0x03, 0x1d]")
        bitsBase = bits & 0xffffff
        if not (0x8000 <= bitsBase <= 0x7fffff):
            raise Exception("Second part of bits should be in [0x8000, 0x7fffff]")
        return bitsBase << (8 * (bitsN-3))

    # Calvin: Converts the target difficulty to bits.
    @classmethod
    def target_to_bits(cls, target: int) -> int:
        c = ("%064x" % target)[2:]
        while c[:2] == '00' and len(c) > 6:
            c = c[2:]
        bitsN, bitsBase = len(c) // 2, int.from_bytes(bfh(c[:6]), byteorder='big')
        if bitsBase >= 0x800000:
            bitsN += 1
            bitsBase >>= 8
        return bitsN << 24 | bitsBase

    # Todo-Calvin: Why is this calculating instead of using the chainworks_cache?
    def chainwork_of_header_at_height(self, height: int) -> int:
        """work done by single header at given height"""
        chunk_idx = height // 2016 - 1
        target = self.get_target(chunk_idx)
        # Todo-Calvin: What are these values from?
        work = ((2 ** 256 - target - 1) // (target + 1)) + 1
        return work

    # Todo-Calvin: Why does it need to store the chainwork in a cache? Don't we just need the highest? Must have something to do with the forks
    # Calvin: IMPORTANT logic for maintaining the main_chain
    @with_lock
    def get_chainwork(self, height=None) -> int:
        if height is None:
            height = max(0, self.height())
        if constants.net.TESTNET:
            # On testnet/regtest, difficulty works somewhat different.
            # It's out of scope to properly implement that.
            return height
        # Calvin: // means floor division
        # Calvin: last_retarget is the Integer value of the height of this block
        last_retarget = height // 2016 * 2016 - 1
        cached_height = last_retarget
        while _CHAINWORK_CACHE.get(self.get_hash(cached_height)) is None:
            if cached_height <= -1: # Calvin: if height of the current block is 0 (is empty)
                break
            cached_height -= 2016 # Todo-Calvin: Why subtract 2016?
        assert cached_height >= -1, cached_height
        running_total = _CHAINWORK_CACHE[self.get_hash(cached_height)]
        while cached_height < last_retarget:
            cached_height += 2016
            work_in_single_header = self.chainwork_of_header_at_height(cached_height)
            work_in_chunk = 2016 * work_in_single_header
            running_total += work_in_chunk
            _CHAINWORK_CACHE[self.get_hash(cached_height)] = running_total
        cached_height += 2016
        work_in_single_header = self.chainwork_of_header_at_height(cached_height)
        work_in_last_partial_chunk = (height % 2016 + 1) * work_in_single_header
        return running_total + work_in_last_partial_chunk

    # Calvin: Validation logic when connecting a new block to the chain
    # Calvin: Checks that the header provided is continuation of the current chain (has valid prev_hash) and has the expected target
    def can_connect(self, header: dict, check_height: bool=True) -> bool:
        if header is None:
            return False
        height = header['block_height']
        if check_height and self.height() != height - 1:
            #self.print_error("cannot connect at height", height)
            return False
        if height == 0:
            return hash_header(header) == constants.net.GENESIS
        try:
            prev_hash = self.get_hash(height - 1)
        except:
            return False
        if prev_hash != header.get('prev_block_hash'):
            return False
        try:
            target = self.get_target(height // 2016 - 1) # Todo-Calvin: why not use the previous hash target? Because this is used by fork too, might not point to previous hash...
        except MissingHeader:
            return False
        try:
            self.verify_header(header, prev_hash, target)
        except BaseException as e:
            return False
        return True

    def connect_chunk(self, idx: int, hexdata: str) -> bool:
        assert idx >= 0, idx
        try:
            data = bfh(hexdata)
            self.verify_chunk(idx, data)
            #self.print_error("validated chunk %d" % idx)
            self.save_chunk(idx, data)
            return True
        except BaseException as e:
            self.print_error(f'verify_chunk idx {idx} failed: {repr(e)}')
            return False

    def get_checkpoints(self):
        # for each chunk, store the hash of the last block and the target after the chunk
        cp = []
        n = self.height() // 2016
        for index in range(n):
            h = self.get_hash((index+1) * 2016 -1)
            target = self.get_target(index)
            cp.append((h, target))
        return cp


#Calvin: Verifies that the list of blockchain headers are all valid
#Calvin: Returns a reference to the Blockchain object that contains this header
def check_header(header: dict) -> Optional[Blockchain]:
    if type(header) is not dict:
        return None
    with blockchains_lock: chains = list(blockchains.values())
    for b in chains:
        if b.check_header(header):
            return b
    return None

#Calvin: Verifies that the list of blockchain can all be connected with each other
def can_connect(header: dict) -> Optional[Blockchain]:
    with blockchains_lock: chains = list(blockchains.values())
    for b in chains:
        if b.can_connect(header):
            return b
    return None
