Given consensus rules, a target block height, and a path to an existing chainstate db, replay the chain state's virtual transactions
    through the consensus rules into a given directory (working_dir).

    Optionally check that the snapshots in @expected_snapshots match up as we verify.
    @expected_snapshots maps str(block_height) to str(consensus hash)

    Return the consensus hash calculated at the target block height
    Return None on verification failure (i.e. we got a different consensus hash than one for the same block in expected_snapshots)

def state_engine_replay(consensus_impl, existing_working_dir, new_state_engine, target_block_height, start_block=None, initial_snapshots={}, expected_snapshots={}):
    """
    Given consensus rules, a target block height, and a path to an existing chainstate db, replay the chain state's virtual transactions
    through the consensus rules into a given directory (working_dir).

    Optionally check that the snapshots in @expected_snapshots match up as we verify.
    @expected_snapshots maps str(block_height) to str(consensus hash)

    Return the consensus hash calculated at the target block height
    Return None on verification failure (i.e. we got a different consensus hash than one for the same block in expected_snapshots)
    """
    
    assert hasattr(consensus_impl, 'get_opcodes')
    assert hasattr(consensus_impl, 'get_magic_bytes')
    assert hasattr(consensus_impl, 'get_opfields')
    assert hasattr(consensus_impl, 'get_first_block_id')

    consensus_opcodes = consensus_impl.get_opcodes()
    consensus_magic_bytes = consensus_impl.get_magic_bytes()
    consensus_opfields = consensus_impl.get_opfields()

    existing_state_engine = StateEngine(consensus_impl, existing_working_dir)
    
    # set up existing state engine 
    rc = existing_state_engine.db_setup()
    if not rc:
        # do not touch the existing db
        raise Exception("Existing state in {} is unusable or corrupt".format(os.path.dirname(existing_working_dir)))

    if start_block is None:
        # maybe we're resuming?
        start_block = new_state_engine.get_lastblock(new_state_engine.impl, new_state_engine.working_dir)
        if start_block is None:
            # starting from scratch
            start_block = consensus_impl.get_first_block_id()

    log.debug("Rebuilding database from {} to {}".format(start_block, target_block_height))

    consensus_hashes = {}
    for block_height in range(start_block, target_block_height+1):
        
        # recover virtualchain transactions from the existing db and feed them into the new db
        consensus_hash = state_engine_replay_block(existing_state_engine, new_state_engine, block_height, expected_snapshots=expected_snapshots)

        log.debug("VERIFY CONSENSUS({}): {}".format(block_height, consensus_hash))
        consensus_hashes[block_height] = consensus_hash

        if block_height in expected_snapshots:
            if expected_snapshots[block_height] != consensus_hash:
                log.error("DATABASE IS NOT CONSISTENT AT {}: {} != {}".format(block_height, expected_snapshots[block_height], consensus_hash))
                return None

    # final consensus hash
    return consensus_hashes[target_block_height]