docs: 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