async def _get_update(self, rr_builder: Callable, fro: int, to: int, delta: bool) -> (str, int):
        """
        Get rev reg delta/state json, and its timestamp on the distributed ledger,
        from cached rev reg delta/state frames list or distributed ledger,
        updating cache as necessary.

        Raise BadRevStateTime if caller asks for a delta/state in the future. Raise ClosedPool
        if an update requires the ledger but the node pool is closed.

        Issuer anchors cannot revoke retroactively.
        Hence, for any new request against asked-for interval (fro, to):
        * if the cache has a frame f on f.timestamp <= to <= f.to,
          > return its rev reg delta/state; e.g., starred frame below:

          Frames: --------[xxxxx]----[xx]-----[*********]-----[x]-----------[xx]---------> time
          Fro-to:                                ^----^

        * otherwise, if there is a maximum frame f with fro <= f.to and f.timestamp <= to
          > return its rev reg delta/state; e.g., starred frame below:

          Frames: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[*]-----------[xx]---------> time
          Fro-to:                  ^----------------------------------^

        * otherwise, if the cache has a frame f on f.timestamp < to,
          > check the distributed ledger for a delta to/state for the rev reg since e.timestamp;
            - if there is one, bake it through 'to' into a new delta/state, add new frame to cache and
              return rev reg delta/state; e.g., starred frame below:

              Frames: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]-----------[xx]---------> time
              Fro-to:                                                 ^------^
              Ledger: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]--!--------[xx]---------> time
              Update: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]--[*****]--[xx]---------> time

            - otherwise, update the 'to' time in the frame and return the rev reg delta/state;
              e.g., starred frame below:

              Frames: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]-----------[xx]---------> time
              Fro-to:                                                 ^------^
              Ledger: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]-----------[xx]---------> time
              Update: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[**********]--[xx]---------> time

        * otherwise, there is no cache frame f on f.timestamp < to:
          > create new frame and add it to cache; return rev reg delta/state; e.g., starred frame below:

          Frames: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[*]-----------[xx]-----> time
          Fro-to:   ^--^
          Ledger: -!------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]-----------[xx]---------> time
          Update: -[***]--[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]-----------[xx]---------> time

        On return of any previously existing rev reg delta/state frame, always update its query time beforehand.

        :param rr_builder: callback to build rev reg delta/state if need be (specify holder-prover anchor's
            _build_rr_delta_json() or verifier anchor's _build_rr_state_json() as needed)
        :param fro: least time (epoch seconds) of interest; lower-bounds 'to' on frame housing return data
        :param to: greatest time (epoch seconds) of interest; upper-bounds returned revocation delta/state timestamp
        :param delta: True to operate on rev reg deltas, False for states
        :return: rev reg delta/state json and ledger timestamp (epoch seconds)
        """

        LOGGER.debug(
            'RevoCacheEntry.get_update >>> rr_builder: %s, fro: %s, to: %s, delta: %s',
            rr_builder.__name__,
            fro,
            to,
            delta)

        if fro > to:
            (fro, to) = (to, fro)

        now = int(time())
        if to > now:
            LOGGER.debug(
                'RevoCacheEntry._get_update <!< Cannot query a rev reg %s in the future (%s > %s)',
                'delta' if delta else 'state',
                to,
                now)
            raise BadRevStateTime('Cannot query a rev reg {} in the future ({} > {})'.format(
                'delta' if delta else 'state',
                to,
                now))

        cache_frame = None
        rr_update_json = None
        rr_frames = self.rr_delta_frames if delta else self.rr_state_frames

        frames = [frame for frame in rr_frames if frame.timestamp <= to <= frame.to]
        if frames:
            cache_frame = max(frames, key=lambda f: f.timestamp)  # should be unique in any case
            # do not update frame.to, it's already past asked-for 'to'
        else:
            frames = [frame for frame in rr_frames if (fro <= frame.to and frame.timestamp <= to)]
            if frames:
                cache_frame = max(frames, key=lambda f: f.timestamp)
                # do not update frame.to - another update might occur, but we don't care; fro < frame.to, good enough
        if not frames:
            frames = [frame for frame in rr_frames if frame.timestamp < to]  # frame.to < to since not frames coming in
            if frames:
                latest_cached = max(frames, key=lambda frame: frame.timestamp)
                if delta:
                    (rr_update_json, timestamp) = await rr_builder(
                        self.rev_reg_def['id'],
                        to=to,
                        fro=latest_cached.timestamp,
                        fro_delta=latest_cached.rr_update)
                else:
                    (rr_update_json, timestamp) = await rr_builder(self.rev_reg_def['id'], to)
                if timestamp == latest_cached.timestamp:
                    latest_cached.to = to  # this timestamp now known good through more recent 'to'
                    cache_frame = latest_cached
            else:
                (rr_update_json, timestamp) = await rr_builder(self.rev_reg_def['id'], to)

        if cache_frame is None:
            cache_frame = RevRegUpdateFrame(to, timestamp, json.loads(rr_update_json))  # sets qtime to now
            rr_frames.append(cache_frame)
            self.cull(delta)
        else:
            cache_frame.qtime = int(time())

        rv = (json.dumps(cache_frame.rr_update), cache_frame.timestamp)
        LOGGER.debug('RevoCacheEntry._get_update <<< %s', rv)
        return rv
Get rev reg delta/state json, and its timestamp on the distributed ledger,
        from cached rev reg delta/state frames list or distributed ledger,
        updating cache as necessary.

        Raise BadRevStateTime if caller asks for a delta/state in the future. Raise ClosedPool
        if an update requires the ledger but the node pool is closed.

        Issuer anchors cannot revoke retroactively.
        Hence, for any new request against asked-for interval (fro, to):
        * if the cache has a frame f on f.timestamp <= to <= f.to,
          > return its rev reg delta/state; e.g., starred frame below:

          Frames: --------[xxxxx]----[xx]-----[*********]-----[x]-----------[xx]---------> time
          Fro-to:                                ^----^

        * otherwise, if there is a maximum frame f with fro <= f.to and f.timestamp <= to
          > return its rev reg delta/state; e.g., starred frame below:

          Frames: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[*]-----------[xx]---------> time
          Fro-to:                  ^----------------------------------^

        * otherwise, if the cache has a frame f on f.timestamp < to,
          > check the distributed ledger for a delta to/state for the rev reg since e.timestamp;
            - if there is one, bake it through 'to' into a new delta/state, add new frame to cache and
              return rev reg delta/state; e.g., starred frame below:

              Frames: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]-----------[xx]---------> time
              Fro-to:                                                 ^------^
              Ledger: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]--!--------[xx]---------> time
              Update: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]--[*****]--[xx]---------> time

            - otherwise, update the 'to' time in the frame and return the rev reg delta/state;
              e.g., starred frame below:

              Frames: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]-----------[xx]---------> time
              Fro-to:                                                 ^------^
              Ledger: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]-----------[xx]---------> time
              Update: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[**********]--[xx]---------> time

        * otherwise, there is no cache frame f on f.timestamp < to:
          > create new frame and add it to cache; return rev reg delta/state; e.g., starred frame below:

          Frames: --------[xxxxx]----[xx]-----[xxxxxxxxx]-----[*]-----------[xx]-----> time
          Fro-to:   ^--^
          Ledger: -!------[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]-----------[xx]---------> time
          Update: -[***]--[xxxxx]----[xx]-----[xxxxxxxxx]-----[x]-----------[xx]---------> time

        On return of any previously existing rev reg delta/state frame, always update its query time beforehand.

        :param rr_builder: callback to build rev reg delta/state if need be (specify holder-prover anchor's
            _build_rr_delta_json() or verifier anchor's _build_rr_state_json() as needed)
        :param fro: least time (epoch seconds) of interest; lower-bounds 'to' on frame housing return data
        :param to: greatest time (epoch seconds) of interest; upper-bounds returned revocation delta/state timestamp
        :param delta: True to operate on rev reg deltas, False for states
        :return: rev reg delta/state json and ledger timestamp (epoch seconds)