Read all consensus hashes into memory.  They're write-once read-many,
        so no need to worry about cache-coherency.

def get_ops_hashes(cls, impl, working_dir, start_block_height=None, end_block_height=None):
        """
        Read all consensus hashes into memory.  They're write-once read-many,
        so no need to worry about cache-coherency.
        """
        if (start_block_height is None and end_block_height is not None) or (start_block_height is not None and end_block_height is None):
            raise ValueError("Need either both or neither start/end block height")

        con = cls.db_open(impl, working_dir)
        range_query = ' WHERE ops_hash IS NOT NULL'
        range_args = ()

        if start_block_height and end_block_height:
            range_query += ' AND block_id >= ? AND block_id < ?'
            range_args += (start_block_height,end_block_height)

        query = 'SELECT block_id,ops_hash FROM snapshots' + range_query + ';'
        args = range_args

        rows = cls.db_query_execute(con, query, range_args, verbose=False)
        ret = {}
        block_min = None
        block_max = None

        for r in rows:
            ret[r['block_id']] = r['ops_hash']

            if block_min is None or block_min > r['block_id']:
                block_min = r['block_id']

            if block_max is None or block_max < r['block_id']:
                block_max = r['block_id']

        con.close()
        
        # sanity check
        if block_min is not None and block_max is not None:
            for i in range(block_min,block_max+1):
                oh = ret.get(i, None)

                assert oh is not None, 'Missing ops hash for {}'.format(i)
                assert isinstance(ret[i], (str,unicode)), 'ops hash for {} is type {}'.format(i, type(ret[i]))

        return ret