class Account(metaclass=API):
    ''' Accounts settle all sorts of stuff.
    
    TODO: move GolixCore into account. That way, everything to do with
    the private keys stays within the account, and is never directly
    accessed outside of it.
    '''
    _identity = immortal_property('__identity')
    _user_id = immortal_property('__user_id')
    
    _golcore = weak_property('__golcore')
    _ghidproxy = weak_property('__ghidproxy')
    _privateer = weak_property('__privateer')
    _oracle = weak_property('__oracle')
    _rolodex = weak_property('__rolodex')
    _dispatch = weak_property('__dispatch')
    _percore = weak_property('__percore')
    _librarian = weak_property('__librarian')
    _salmonator = weak_property('__salmonator')
    
    @public_api
    def __init__(self, user_id, root_secret, *args, hgxcore, **kwargs):
        ''' Gets everything ready for account bootstrapping.
        
        +   user_id explicitly passed with None means create a new
            Account.
        +   identity explicitly passed with None means load an existing
            account.
        +   user_id XOR identity must be passed.
        '''
        super().__init__(*args, **kwargs)
        
        if isinstance(user_id, Ghid):
            logger.info('Logging in with existing user ID: ' + str(user_id))
            self._identity = None
            self._user_id = user_id
        
        # This is used exclusively for testing.
        elif isinstance(user_id, FirstParty):
            logger.info('Creating a new account.')
            self._identity = user_id
            self._user_id = None
        
        else:
            raise TypeError('user_id must be an instance of Ghid or None.')
            
        self._golcore = hgxcore.golcore
        self._ghidproxy = hgxcore.ghidproxy
        self._privateer = hgxcore.privateer
        self._oracle = hgxcore.oracle
        self._rolodex = hgxcore.rolodex
        self._dispatch = hgxcore.dispatch
        self._percore = hgxcore.percore
        self._librarian = hgxcore.librarian
        self._salmonator = hgxcore.salmonator
        
        self._root_secret = root_secret
        
    @__init__.fixture
    def __init__(self, identity, *args, **kwargs):
        ''' Lulz just ignore errytang and skip calling super!
        '''
        self._identity = identity
        
        self.privateer_persistent = {}
        self.privateer_quarantine = {}
        
        self.rolodex_pending = {}
        self.rolodex_outstanding = SetMap()
        
        self.dispatch_tokens = set()
        self.dispatch_startup = {}
        self.dispatch_private = {}
        self.dispatch_incoming = set()
        self.dispatch_orphan_acks = SetMap()
        self.dispatch_orphan_naks = SetMap()
        
    @property
    def _fingerprint(self):
        ''' Get our fingerprint.
        '''
        return self._identity.ghid
        
    async def _inject_gao(self, gao):
        ''' Bypass the normal oracle get_object, new_object process and
        create the object directly.
        '''
        await self._salmonator.register(gao.ghid)
        await self._salmonator.attempt_pull(gao.ghid, quiet=True)
        gao._ctx = asyncio.Event()
        gao._ctx.set()
        self._oracle._lookup[gao.ghid] = gao
            
    async def bootstrap(self):
        ''' Used for account creation, to initialize the root node with
        its resource directory.
        '''
        # We need to pre-allocate the privateer stuff so we can bootstrap it
        # before pulling the root node when reloading.
        self.privateer_persistent = GAODict(
            ghid = None,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian
        )
        self.privateer_quarantine = GAODict(
            ghid = None,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian
        )
        
        # Privateer can be bootstrapped with or without pulling. Even though it
        # won't work *right* before pulling, it won't work AT ALL until it's
        # bootstrapped, so bootstrap first and pull later.
        logger.info('Bootstrapping privateer.')
        self._privateer.bootstrap(self)
        
        if self._user_id is not None:
            logger.info('Loading the root node.')
            root_node = GAO(
                ghid = self._user_id,
                dynamic = True,
                author = None,
                legroom = 7,
                state = b'you pass butter',
                golcore = self._golcore,
                ghidproxy = self._ghidproxy,
                privateer = self._privateer,
                percore = self._percore,
                librarian = self._librarian,
                master_secret = self._root_secret
            )
            # And now, remove the root secret from the parent namespace. This
            # will make the root_node GAO the only live reference to the root
            # secret from within the Account.
            del self._root_secret
            await root_node._pull()
                
            # TODO: convert all of this into a smartyparser (after rewriting
            # smartyparse, that is)
            
            # The last 64 bytes of the state is a SHA512 checksum for the whole
            # rest of the state
            password_validator = root_node.state[-64:]
            password_comparator = root_node.state[:-64]
            
            # This comparison is timing-insensitive: if attempting to brute
            # force a password, incorrect password attempts cannot extract any
            # information from the timing of the comparison.
            if sha512(password_comparator).digest() != password_validator:
                logger.critical('Incorrect password.')
                raise ValueError('Incorrect password.')
            
            # Identity
            identity_ghid = Ghid.from_bytes(
                root_node.state[0: 65])     # 65 bytes
            identity_master = Secret.from_bytes(
                root_node.state[65: 118])   # 53 bytes
            
            # Privateer
            privateer_persistent_ghid = Ghid.from_bytes(
                root_node.state[118: 183])
            privateer_persistent_master = Secret.from_bytes(
                root_node.state[183: 236])
            
            privateer_quarantine_ghid = Ghid.from_bytes(
                root_node.state[236: 301])
            privateer_quarantine_master = Secret.from_bytes(
                root_node.state[301: 354])
            
            # Rolodex stuff
            rolodex_pending_ghid = Ghid.from_bytes(
                root_node.state[354: 419])
            rolodex_pending_master = Secret.from_bytes(
                root_node.state[419: 472])
            
            rolodex_outstanding_ghid = Ghid.from_bytes(
                root_node.state[472: 537])
            rolodex_outstanding_master = Secret.from_bytes(
                root_node.state[537: 590])
            
            # Dispatch stuff
            dispatch_tokens_ghid = Ghid.from_bytes(
                root_node.state[590: 655])
            dispatch_tokens_master = Secret.from_bytes(
                root_node.state[655: 708])
            
            dispatch_startup_ghid = Ghid.from_bytes(
                root_node.state[708: 773])
            dispatch_startup_master = Secret.from_bytes(
                root_node.state[773: 826])
            
            dispatch_private_ghid = Ghid.from_bytes(
                root_node.state[826: 891])
            dispatch_private_master = Secret.from_bytes(
                root_node.state[891: 944])
            
            dispatch_incoming_ghid = Ghid.from_bytes(
                root_node.state[944: 1009])
            dispatch_incoming_master = Secret.from_bytes(
                root_node.state[1009: 1062])
            
            dispatch_orphan_acks_ghid = Ghid.from_bytes(
                root_node.state[1062: 1127])
            dispatch_orphan_acks_master = Secret.from_bytes(
                root_node.state[1127: 1180])
            
            dispatch_orphan_naks_ghid = Ghid.from_bytes(
                root_node.state[1180: 1245])
            dispatch_orphan_naks_master = Secret.from_bytes(
                root_node.state[1245: 1298])
        
        else:
            # We need an identity at to golcore before we can do anything
            logger.info('Bootstrapping golcore.')
            self._golcore.bootstrap(self)
            
            # Note that we first need to push our identity secondparty.
            packed = self._identity.second_party.packed
            gidc = _GidcLite.from_golix(GIDC.unpack(packed))
            logger.info('Publishing public keys.')
            # We haven't bootstrapped salmonator yet, so don't remote the keys
            await self._percore.direct_ingest(gidc, packed, remotable=False)
            
            # Now we need to declare the root note. It needs some kind of
            # useless, garbage data as well, lest it error out because it can't
            # encrypt nothingness.
            root_node = GAO(
                ghid = None,
                dynamic = True,
                author = None,
                legroom = 7,
                state = b'you pass butter',
                golcore = self._golcore,
                ghidproxy = self._ghidproxy,
                privateer = self._privateer,
                percore = self._percore,
                librarian = self._librarian,
                master_secret = self._root_secret
            )
            # And now, remove the root secret from the parent namespace. This
            # will make the root_node GAO the only live reference to the root
            # secret from within the Account.
            del self._root_secret
            
            identity_ghid = None
            identity_master = self._identity.new_secret()
            
            privateer_persistent_ghid = None
            privateer_persistent_master = self._identity.new_secret()
            
            privateer_quarantine_ghid = None
            privateer_quarantine_master = self._identity.new_secret()
            
            # Rolodex stuff
            rolodex_pending_ghid = None
            rolodex_pending_master = self._identity.new_secret()
            
            rolodex_outstanding_ghid = None
            rolodex_outstanding_master = self._identity.new_secret()
            
            # Dispatch stuff
            dispatch_tokens_ghid = None
            dispatch_tokens_master = self._identity.new_secret()
            
            dispatch_startup_ghid = None
            dispatch_startup_master = self._identity.new_secret()
            
            dispatch_private_ghid = None
            dispatch_private_master = self._identity.new_secret()
            
            dispatch_incoming_ghid = None
            dispatch_incoming_master = self._identity.new_secret()
            
            dispatch_orphan_acks_ghid = None
            dispatch_orphan_acks_master = self._identity.new_secret()
            
            dispatch_orphan_naks_ghid = None
            dispatch_orphan_naks_master = self._identity.new_secret()
        
        # Allocate the identity container
        #######################################################################
        # This stores the private keys
        identity_container = GAODict(
            ghid = identity_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = identity_master
        )
        
        # Jiggery-pokery for the privateer restoration
        #######################################################################
        # This stores persistent secrets
        self.privateer_persistent.ghid = privateer_persistent_ghid
        self.privateer_persistent._inject_msec(privateer_persistent_master)
        
        self.privateer_quarantine.ghid = privateer_quarantine_ghid
        self.privateer_quarantine._inject_msec(privateer_quarantine_master)
        
        # Save/load the identity container and bootstrap golcore, privateer
        #######################################################################
        
        # Load existing account
        if self._user_id is not None:
            logger.info('Loading identity.')
            await identity_container._pull()
            self._identity = FirstParty._from_serialized(
                identity_container.state
            )
            logger.info('Bootstrapping golcore.')
            self._golcore.bootstrap(self)
            logger.info('Bootstrapping salmonator.')
            await self._salmonator.bootstrap(self)
            
            logger.info('Loading persistent keystore.')
            await self.privateer_persistent._pull()

            logger.info('Loading quarantined keystore.')
            await self.privateer_quarantine._pull()
            
        # Save new account
        else:
            # First, bootstrap salmonator, so that we don't get errors as an
            # unknown author
            await self._salmonator.bootstrap(self)
            # Initializing is needed to prevent losing the first frame while
            # the secret ratchet initializes
            logger.info('Allocating the root node.')
            await root_node._push()
            # Don't forget to do this before creating the actual identity
            # frame, or we'll lose the first frame (and therefore, everything
            # we care about)
            logger.info('Allocating identity container.')
            await identity_container._push(force=True)
            logger.info('Saving identity.')
            identity_container.update(self._identity._serialize())
            await identity_container._push(force=True)
            
            # Because these use a master secret, they need to be initialized,
            # or the first frame will be unrecoverable.
            logger.info('Allocating persistent keystore.')
            await self.privateer_persistent._push(force=True)
            await self.privateer_persistent._push(force=True)
            
            logger.info('Allocating quarantined keystore.')
            await self.privateer_quarantine._push(force=True)
            await self.privateer_quarantine._push(force=True)
            
        # Establish the rest of the above at the various tracking agencies
        logger.info('Reticulating keystores.')
        await self._inject_gao(self.privateer_persistent)
        await self._inject_gao(self.privateer_quarantine)
        # We don't need to do this with the secondary manifest (unless we're
        # planning on adding things to it while already running, which would
        # imply an ad-hoc, on-the-fly upgrade process)
        
        # Rolodex gaos:
        self.rolodex_pending = GAODict(
            ghid = rolodex_pending_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = rolodex_pending_master
        )
        self.rolodex_outstanding = GAOSetMap(
            ghid = rolodex_outstanding_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = rolodex_outstanding_master
        )
        
        # Dispatch gaos:
        self.dispatch_tokens = GAOSet(
            ghid = dispatch_tokens_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_tokens_master
        )
        self.dispatch_startup = GAODict(
            ghid = dispatch_startup_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_startup_master
        )
        self.dispatch_private = GAODict(
            ghid = dispatch_private_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_private_master
        )
        self.dispatch_incoming = GAOSet(
            ghid = dispatch_incoming_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_incoming_master
        )
        self.dispatch_orphan_acks = GAOSetMap(
            ghid = dispatch_orphan_acks_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_orphan_acks_master
        )
        self.dispatch_orphan_naks = GAOSetMap(
            ghid = dispatch_orphan_naks_ghid,
            dynamic = True,
            author = None,
            legroom = 7,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            master_secret = dispatch_orphan_naks_master
        )
        
        # These need not have the actual objects pulled yet
        self._rolodex.bootstrap(self)
        self._dispatch.bootstrap(self)
        
        if self._user_id is not None:
            logger.info('Restoring sharing subsystem.')
            await self.rolodex_pending._pull()
            await self.rolodex_outstanding._pull()
            
            logger.info('Restoring object dispatch.')
            await self.dispatch_tokens._pull()
            await self.dispatch_startup._pull()
            await self.dispatch_private._pull()
            await self.dispatch_incoming._pull()
            await self.dispatch_orphan_acks._pull()
            await self.dispatch_orphan_naks._pull()
        
        else:
            logger.info('Building sharing subsystem.')
            await self.rolodex_pending._push(force=True)
            await self.rolodex_pending._push(force=True)
            await self.rolodex_outstanding._push(force=True)
            await self.rolodex_outstanding._push(force=True)
            
            logger.info('Building object dispatch.')
            await self.dispatch_tokens._push(force=True)
            await self.dispatch_tokens._push(force=True)
            await self.dispatch_startup._push(force=True)
            await self.dispatch_startup._push(force=True)
            await self.dispatch_private._push(force=True)
            await self.dispatch_private._push(force=True)
            await self.dispatch_incoming._push(force=True)
            await self.dispatch_incoming._push(force=True)
            await self.dispatch_orphan_acks._push(force=True)
            await self.dispatch_orphan_acks._push(force=True)
            await self.dispatch_orphan_naks._push(force=True)
            await self.dispatch_orphan_naks._push(force=True)
            
            self._user_id = root_node.ghid
        
            logger.info('Building root node...')
            # Generate secure-random-length, pseudorandom-content padding
            logger.info('    Generating noisy padding.')
            # Note that we don't actually need CSRNG for the padding, just the
            # padding length, since the whole thing is encrypted. We could just
            # as easily fill it with zeros, but by filling it with pseudorandom
            # noise, we can remove a recognizable pattern and therefore slighly
            # hinder brute force attacks against the password.
            # While we COULD use CSRNG despite all that, entropy is a limited
            # resource, and I'd rather conserve it as much as possible.
            padding_seed = int.from_bytes(os.urandom(2), byteorder='big')
            padding_min_size = 1024
            padding_clip_mask = 0b0001111111111111
            # Clip the seed to an upper range of 13 bits, of 8191, for a
            # maximum padding length of 8191 + 1024 = 9215 bytes
            padding_len = padding_min_size + (padding_seed & padding_clip_mask)
            padding_int = random.getrandbits(padding_len * 8)
            padding = padding_int.to_bytes(length=padding_len, byteorder='big')
            
            logger.info('    Serializing primary manifest.')
            root_node.state = (bytes(identity_container.ghid) +
                               bytes(identity_master) +
                               bytes(self.privateer_persistent.ghid) +
                               bytes(privateer_persistent_master) +
                               bytes(self.privateer_quarantine.ghid) +
                               bytes(privateer_quarantine_master) +
                               bytes(self.rolodex_pending.ghid) +
                               bytes(rolodex_pending_master) +
                               bytes(self.rolodex_outstanding.ghid) +
                               bytes(rolodex_outstanding_master) +
                               bytes(self.dispatch_tokens.ghid) +
                               bytes(dispatch_tokens_master) +
                               bytes(self.dispatch_startup.ghid) +
                               bytes(dispatch_startup_master) +
                               bytes(self.dispatch_private.ghid) +
                               bytes(dispatch_private_master) +
                               bytes(self.dispatch_incoming.ghid) +
                               bytes(dispatch_incoming_master) +
                               bytes(self.dispatch_orphan_acks.ghid) +
                               bytes(dispatch_orphan_acks_master) +
                               bytes(self.dispatch_orphan_naks.ghid) +
                               bytes(dispatch_orphan_naks_master) +
                               padding)
            
            # We'll use this upon future logins to verify password correctness
            logger.info('    Generating validator and comparator.')
            # Just append a SHA512 checksum to the whole thing
            password_validator = sha512(root_node.state).digest()
            root_node.state += password_validator
            
            logger.info('Saving root node.')
            await root_node._push()
        
        #######################################################################
        #######################################################################
        # ROOT NODE CREATION (PRIMARY BOOTSTRAP) COMPLETE!
        #######################################################################
        #######################################################################
        
        logger.info('Reticulating sharing subsystem.')
        await self._inject_gao(self.rolodex_pending)
        await self._inject_gao(self.rolodex_outstanding)
        
        logger.info('Reticulating object dispatch.')
        await self._inject_gao(self.dispatch_tokens)
        await self._inject_gao(self.dispatch_startup)
        await self._inject_gao(self.dispatch_private)
        await self._inject_gao(self.dispatch_incoming)
        await self._inject_gao(self.dispatch_orphan_acks)
        await self._inject_gao(self.dispatch_orphan_naks)
        
        logger.info('Account login successful.')
    
    @fixture_noop
    @public_api
    async def flush(self):
        ''' Push changes to any modified account components.
        '''
        tasks = {
            make_background_future(self.privateer_persistent.push()),
            make_background_future(self.privateer_quarantine.push()),
            make_background_future(self.rolodex_pending.push()),
            make_background_future(self.rolodex_outstanding.push()),
            make_background_future(self.dispatch_tokens.push()),
            make_background_future(self.dispatch_startup.push()),
            make_background_future(self.dispatch_private.push()),
            make_background_future(self.dispatch_incoming.push()),
            make_background_future(self.dispatch_orphan_acks.push()),
            make_background_future(self.dispatch_orphan_naks.push())
        }
        await asyncio.wait(fs=tasks, return_when=asyncio.ALL_COMPLETED) class GolixCore(metaclass=API):
    ''' Wrapper around Golix library that automates much of the state
    management, holds the Agent's identity, etc etc.
    '''
    _librarian = weak_property('__librarian')
    DEFAULT_LEGROOM = 7
    
    # Async stuff
    _executor = readonly_property('__executor')
    _loop = readonly_property('__loop')
    
    @public_api
    def __init__(self, executor, loop, *args, **kwargs):
        ''' Create a new agent. Persister should subclass _PersisterBase
        (eventually this requirement may be changed).
        
        persister isinstance _PersisterBase
        dispatcher isinstance DispatcherBase
        _identity isinstance golix.FirstParty
        '''
        super().__init__(*args, **kwargs)
        self._mutex_request = threading.Lock()
        self._mutex_container = threading.Lock()
        self._mutex_sbinding = threading.Lock()
        self._mutex_dbinding = threading.Lock()
        self._mutex_xbinding = threading.Lock()
        
        # Added during bootstrap
        self.__identity = None
        
        # Async-specific stuff
        setattr(self, '__executor', executor)
        setattr(self, '__loop', loop)
        
    @__init__.fixture
    def __init__(self, test_agent, *args, librarian=None, **kwargs):
        ''' Just, yknow, throw in the towel. Err, agent. Whatever.
        '''
        super(GolixCore.__fixture__, self).__init__(
            executor = None,
            loop = None,
            *args,
            **kwargs
        )
        self._identity = test_agent
        
        if librarian is not None:
            self._librarian = librarian
        
    def assemble(self, librarian):
        # Chicken, meet egg.
        self._librarian = librarian
        
    def bootstrap(self, account):
        # This must be done ASAGDFP. Must be absolute first thing to bootstrap.
        self._identity = account._identity
        
    @property
    def _identity(self):
        ''' Wrap our identity in such a way that, if it's not defined,
        we can fall back on the FirstParty class.
        '''
        if self.__identity is None:
            return FirstParty
        else:
            return self.__identity
            
    @_identity.setter
    def _identity(self, identity):
        ''' Set the identity.
        '''
        self.__identity = identity
        
    @property
    @public_api
    def whoami(self):
        ''' Return the Agent's Ghid.
        '''
        return self._identity.ghid
    
    @public_api
    async def unpack_request(self, request):
        ''' Just like it says on the label...
        Note that the request is PACKED, not unpacked.
        '''
        # Run the actual function in the executor
        return (await self._loop.run_in_executor(
            self._executor,
            self._unpack_request,
            request
        ))
        
    @unpack_request.fixture
    async def unpack_request(self, request):
        ''' Bypass executor for the fixture.
        '''
        return self._unpack_request(request)
        
    def _unpack_request(self, request):
        ''' Just like it says on the label...
        Note that the request is PACKED, not unpacked.
        '''
        with self._mutex_request:
            unpacked = self._identity.unpack_request(request)
        return unpacked
        
    @public_api
    async def open_request(self, unpacked):
        ''' Just like it says on the label...
        Note that the request is UNPACKED, not packed.
        '''
        try:
            requestor = SecondParty.from_packed(
                await self._librarian.retrieve(unpacked.author)
            )
            
        except KeyError as exc:
            raise UnknownParty(
                'Request author unknown: ' + str(unpacked.author)
            ) from exc
        
        # Run the actual function in the executor
        return (await self._loop.run_in_executor(
            self._executor,
            self._open_request,
            unpacked,
            requestor
        ))
        
    @open_request.fixture
    async def open_request(self, unpacked):
        ''' Also bypass executor here.
        '''
        requestor = SecondParty.from_packed(
            await self._librarian.retrieve(unpacked.author)
        )
        return self._open_request(unpacked, requestor)
        
    def _open_request(self, unpacked, requestor):
        ''' Just like it says on the label...
        Note that the request is UNPACKED, not packed.
        '''
        return self._identity.receive_request(requestor, unpacked)
    
    @public_api
    async def make_request(self, recipient, payload):
        # Just like it says on the label...
        try:
            recipient = SecondParty.from_packed(
                await self._librarian.retrieve(recipient)
            )
        except KeyError as exc:
            raise UnknownParty(
                'Request author unknown: ' + str(recipient)
            ) from exc
        
        # Run the actual function in the executor
        return (await self._loop.run_in_executor(
            self._executor,
            self._make_request,
            recipient,
            payload
        ))
        
    @make_request.fixture
    async def make_request(self, recipient, payload):
        ''' Bypass the goddamn executor. Ffs.
        '''
        recipient = SecondParty.from_packed(
            await self._librarian.retrieve(recipient)
        )
        return self._make_request(recipient, payload)
        
    def _make_request(self, recipient, payload):
        # Just like it says on the label...
        with self._mutex_request:
            return self._identity.make_request(
                recipient = recipient,
                request = payload,
            )
    
    @public_api
    async def open_container(self, container, secret):
        author = SecondParty.from_packed(
            await self._librarian.retrieve(container.author)
        )
        
        # Wrapper around golix.FirstParty.receive_container.
        # Run the actual function in the executor
        return (await self._loop.run_in_executor(
            self._executor,
            self._open_container,
            container,
            secret,
            author
        ))
        
    @open_container.fixture
    async def open_container(self, container, secret):
        ''' Bypass executor for fixture.
        '''
        author = SecondParty.from_packed(
            await self._librarian.retrieve(container.author)
        )
        
        return self._open_container(container, secret, author)
        
    def _open_container(self, container, secret, author):
        # Wrapper around golix.FirstParty.receive_container.
        with self._mutex_container:
            return self._identity.receive_container(
                author = author,
                secret = secret,
                container = container
            )
    
    @public_api
    async def make_container(self, data, secret):
        # Simple wrapper around golix.FirstParty.make_container
        # Run the actual function in the executor
        return (await self._loop.run_in_executor(
            self._executor,
            self._make_container,
            data,
            secret
        ))
        
    @make_container.fixture
    async def make_container(self, data, secret):
        ''' Bypass executor.
        '''
        return self._make_container(data, secret)
        
    def _make_container(self, data, secret):
        # Simple wrapper around golix.FirstParty.make_container
        with self._mutex_container:
            return self._identity.make_container(
                secret = secret,
                plaintext = data
            )

    @public_api
    async def make_binding_stat(self, target):
        # Note that this requires no open() method, as bindings are verified by
        # the local persister.
        # Run the actual function in the executor
        return (await self._loop.run_in_executor(
            self._executor,
            self._make_binding_stat,
            target
        ))
        
    @make_binding_stat.fixture
    async def make_binding_stat(self, target):
        ''' Skip the executor for the fixture.
        '''
        return self._make_binding_stat(target)

    def _make_binding_stat(self, target):
        # Note that this requires no open() method, as bindings are verified by
        # the local persister.
        with self._mutex_sbinding:
            return self._identity.make_bind_static(target)
    
    @public_api
    async def make_binding_dyn(self, target, ghid=None, history=None):
        ''' Make a new dynamic binding frame.
        If supplied, ghid is the dynamic address, and history is an
        ordered iterable of the previous frame ghids.
        '''
        # Either ghid AND history must be defined, XOR ghid AND history must be
        # undefined.
        if bool(ghid) ^ bool(history):
            raise ValueError('Mixed def of ghid/history while dyn binding.')
        
        if ghid:
            current_binding = await self._librarian.summarize(ghid)
            counter = current_binding.counter + 1
        else:
            counter = 0
            
        # Run the actual function in the executor
        return (await self._loop.run_in_executor(
            self._executor,
            self._make_binding_dyn,
            ghid,
            counter,
            target,
            history
        ))
        
    @make_binding_dyn.fixture
    async def make_binding_dyn(self, target, ghid=None, history=None):
        ''' Yep, just bypass the executor
        '''
        if ghid:
            current_binding = await self._librarian.summarize(ghid)
            counter = current_binding.counter + 1
        else:
            counter = 0
        
        return self._make_binding_dyn(ghid, counter, target, history)
        
    def _make_binding_dyn(self, ghid, counter, target, history):
        ''' Make a new dynamic binding frame.
        If supplied, ghid is the dynamic address, and history is an
        ordered iterable of the previous frame ghids.
        '''
        if history is not None:
            target_vector = [target, *history]
        else:
            target_vector = [target]
        
        with self._mutex_dbinding:
            return self._identity.make_bind_dynamic(
                counter = counter,
                target_vector = target_vector,
                ghid_dynamic = ghid
            )
    
    @public_api
    async def make_debinding(self, target):
        # Simple wrapper around golix.FirstParty.make_debind
        # Run the actual function in the executor
        return (await self._loop.run_in_executor(
            self._executor,
            self._make_debinding,
            target
        ))
        
    @make_debinding.fixture
    async def make_debinding(self, target):
        ''' Bypass executor for the fixture.
        '''
        return self._make_debinding(target)
        
    def _make_debinding(self, target):
        # Simple wrapper around golix.FirstParty.make_debind
        with self._mutex_xbinding:
            return self._identity.make_debind(target) class Oracle(metaclass=API):
    ''' Source for total internal truth and state tracking of objects.
    
    Maintains <ghid>: <obj> lookup. Used by dispatchers to track obj
    state. Might eventually be used by AgentBase. Just a quick way to
    store and retrieve any objects based on an associated ghid.
    '''
    # These are actually used by the oracle itself
    _salmonator = weak_property('__salmonator')
    
    # These are only here to pass along to GAOs
    _golcore = weak_property('__golcore')
    _ghidproxy = weak_property('__ghidproxy')
    _privateer = weak_property('__privateer')
    _percore = weak_property('__percore')
    _librarian = weak_property('__librarian')
    
    def __init__(self, *args, **kwargs):
        ''' Sets up internal tracking.
        '''
        super().__init__(*args, **kwargs)
        self._lookup = {}
        
    @fixture_api
    def RESET(self):
        ''' Simply re-call init.
        '''
        self._lookup.clear()
        
    def assemble(self, golcore, ghidproxy, privateer, percore, librarian,
                 salmonator):
        # Chicken, meet egg.
        self._golcore = golcore
        self._ghidproxy = ghidproxy
        self._privateer = privateer
        self._percore = percore
        self._librarian = librarian
        self._salmonator = salmonator
        
    @fixture_api
    def add_object(self, ghid, obj):
        ''' Add an object to the fixture.
        '''
        self._lookup[ghid] = obj
            
    @public_api
    async def get_object(self, gaoclass, ghid, *args, **kwargs):
        ''' Get an object.
        '''
        if ghid in self._lookup:
            # TODO: this is bad, because we're suppresing any potential argsig
            # problems. We should fix the abstraction so that it doesn't break
            # assumptions like that.
            obj = self._lookup[ghid]
            logger.debug(
                'GAO ' + str(ghid) + ' already exists in Oracle memory as: ' +
                str(type(obj))
            )
            
            if not isinstance(obj, gaoclass):
                raise TypeError(
                    'Object has already been resolved, and is not the '
                    'correct GAO class.'
                )
            else:
                logger.debug(
                    'GAO ' + str(ghid) + ' acceptable for ' + str(gaoclass)
                )
                
            await obj._ctx.wait()
            if ghid not in self._lookup:
                raise RuntimeError('Contentious delete while getting object.')
        
        else:
            logger.info(
                'GAO ' + str(ghid) +
                ' not currently in Oracle memory. Attempting load as: ' +
                str(gaoclass)
            )
            
            # First create the actual GAO. We do not need to have the ghid
            # downloaded to do this -- object creation is just making a Python
            # object locally.
            obj = gaoclass(
                ghid,
                None,   # dynamic
                None,   # author
                7,      # legroom (will be overwritten by pull)
                *args,
                golcore = self._golcore,
                ghidproxy = self._ghidproxy,
                privateer = self._privateer,
                percore = self._percore,
                librarian = self._librarian,
                **kwargs
            )
            
            # TODO: fix leaky abstraction
            obj._ctx = asyncio.Event()
            
            # Always do this first to make sure we have the most recent version
            # in all subsequent calls, without a race condition.
            self._lookup[ghid] = obj
            
            try:
                # Now immediately subscribe to the object upstream, so that there
                # is no race condition getting updates
                await self._salmonator.register(ghid)
                # Add deregister as a finalizer, but don't call it atexit. TODO:
                # fix leaky abstraction
                finalizer = weakref.finalize(obj, self._salmonator._deregister,
                                             ghid)
                finalizer.atexit = False
                # Explicitly pull the object from salmonator to ensure we have
                # the newest version, and that it is available locally in
                # librarian if also available anywhere else. Note that
                # salmonator handles modal switching for dynamic/static. It
                # will also (by default, with skip_refresh=False) pull in any
                # updates that have accumulated in the meantime.
                await self._salmonator.attempt_pull(ghid, quiet=True)
                
                # Now actually fetch the object. This may KeyError if the
                # ghid is still unknown.
                await obj._pull()
            
            # Got an exception? Revert the lookup and reraise
            except Exception:
                del self._lookup[ghid]
                raise
                
            # We have to release other waiters regardless
            finally:
                # Finally, let future callers unblock
                obj._ctx.set()
            
        return obj
        
    @get_object.fixture
    async def get_object(self, gaoclass, ghid, *args, **kwargs):
        ''' Do the easy thing and just pull it out of lookup.
        '''
        # We should also check argsig while we're at it
        signature = inspect.Signature.from_callable(gaoclass)
        signature.bind(
            ghid,
            None,   # dynamic
            None,   # author
            7,      # legroom (will be overwritten by pull)
            *args,
            golcore = self,     # It's not going to be used and may not exist
            ghidproxy = self,   # It's not going to be used and may not exist
            privateer = self,   # It's not going to be used and may not exist
            percore = self,     # It's not going to be used and may not exist
            librarian = self,   # It's not going to be used and may not exist
            **kwargs
        )
            
        return self._lookup[ghid]
        
    @public_api
    async def new_object(self, gaoclass, dynamic, legroom, *args, **kwargs):
        ''' Creates a new object and returns it. Passes all *kwargs to
        the declared gao_class. Requires a zeroth state, and calls push
        internally.
        '''
        obj = gaoclass(
            None,                   # ghid
            dynamic,
            self._golcore.whoami,   # author
            legroom,
            *args,
            golcore = self._golcore,
            ghidproxy = self._ghidproxy,
            privateer = self._privateer,
            percore = self._percore,
            librarian = self._librarian,
            **kwargs
        )
        await obj._push()
        
        # This is used strictly by oracle. TODO: fix leaky abstraction
        obj._ctx = asyncio.Event()
        obj._ctx.set()
        # Do this before registering with salmonator, in case the latter errors
        self._lookup[obj.ghid] = obj
        
        # Finally, register to receive any concurrent updates from other
        # simultaneous sessions, and then return the object
        await self._salmonator.register(obj.ghid)
        # Add deregister as a finalizer, but don't call it atexit. TODO: fix
        # leaky abstraction
        finalizer = weakref.finalize(obj, self._salmonator._deregister,
                                     obj.ghid)
        finalizer.atexit = False
        
        return obj
            
    @new_object.fixture
    async def new_object(self, gaoclass, dynamic, legroom, *args, **kwargs):
        ''' Relies upon add_object, but otherwise just pops something
        from the lookup.
        '''
        # We should also check argsig while we're at it
        signature = inspect.Signature.from_callable(gaoclass)
        signature.bind(
            None,               # ghid
            dynamic,
            None,               # author (normally assigned through whoami)
            legroom,
            *args,
            golcore = self,     # It's not going to be used and may not exist
            ghidproxy = self,   # It's not going to be used and may not exist
            privateer = self,   # It's not going to be used and may not exist
            percore = self,     # It's not going to be used and may not exist
            librarian = self,   # It's not going to be used and may not exist
            **kwargs
        )
        
        ghid, obj = self._lookup.popitem()
        self._lookup[ghid] = obj
        return obj
        
    def forget(self, ghid):
        ''' Removes the object from the cache. Next time an application
        wants it, it will need to be acquired from persisters.
        
        Indempotent; will not raise KeyError if called more than once.
        '''
        try:
            del self._lookup[ghid]
        except KeyError:
            logger.debug(str(ghid) + ' unknown to oracle.')
            
    def __contains__(self, ghid):
        ''' Checks for the ghid in cache (but does not check for global
        availability; that would require checking the persister for its
        existence and the privateer for access).
        '''
        return ghid in self._lookup class GhidProxier(metaclass=API):
    ''' Resolve the base container GHID from any associated ghid. Uses
    all weak references, so should not interfere with GCing objects.
    '''
    # Note that we can't really cache aliases, because their proxies will
    # not update when we change things unless the proxy is also removed
    # from the cache. Since the objects may (or may not) exist locally in
    # memory anyways, we should just take advantage of that, and allow our
    # inquisitor to more easily manage memory consumption as well.
    _librarian = weak_property('__librarian')
    
    @fixture_api
    def __init__(self, *args, **kwargs):
        ''' Add in a dict to store resolutions.
        '''
        super(GhidProxier.__fixture__, self).__init__(*args, **kwargs)
        self.lookup = {}
        
    def assemble(self, librarian):
        # Chicken, meet egg.
        self._librarian = librarian
        
    def __mklink(self, proxy, target):
        ''' Set, or update, a ghid proxy.
        
        Ghids must only ever have a single proxy. Calling chain on an 
        existing proxy will update the target.
        '''
        raise NotImplementedError('Explicit link creation has been removed.')
        
        if not isinstance(proxy, Ghid):
            raise TypeError('Proxy must be Ghid.')
            
        if not isinstance(target, Ghid):
            raise TypeError('Target must be ghid.')
        
        with self._modlock:
            self._refs[proxy] = target
    
    @public_api
    async def resolve(self, ghid):
        ''' Protect the entry point with a global lock, but don't leave
        the recursive bit alone.
        
        TODO: make this guarantee, through using the persister's
        librarian, that the resolved ghid IS, in fact, a container.
        
        TODO: consider adding a depth limit to resolution.
        '''
        if not isinstance(ghid, Ghid):
            raise TypeError('Can only resolve a ghid.')
            
        return (await self._resolve(ghid))
        
    async def _resolve(self, ghid):
        ''' Recursively resolves the container ghid for a proxy (or a
        container).
        '''
        try:
            obj = await self._librarian.summarize(ghid)
        
        # TODO: make this an error?
        except KeyError:
            logger.warning(''.join((
                'GAO ',
                str(ghid),
                ' address resolver failed to verify: missing at librarian.\n',
                traceback.format_exc()
            )))
            
            result = ghid
        
        else:
            if isinstance(obj, _GeocLite):
                result = ghid
                
            else:
                result = await self._resolve(obj.target)
                
        return result
        
    @resolve.fixture
    async def resolve(self, ghid):
        ''' Ehhhh, okay. So we're going to fixture this, mostly for
        privateer, in a way that just returns the ghid immediately.
        '''
        if ghid in self.lookup:
            return self.lookup[ghid]
        else:
            return ghid class GAOCore(metaclass=API):
    ''' Adds functions to a GAO.
    
    Notes:
    1.  GAO are only ever created through the oracle, via either
        oracle.new_object or oracle.get_object.
    2.  Note that callbacks are only used when defined in subclasses.
        IE, Dispatchable extends pull() to notify applications of the
        update (if applicable)
    3.  Updates for internal-use GAO should defer until a flush() call
    4.  Updates for dispatchables should immediately be pushed
    5.  Salmonator pushing should probably wait to return until all
        remotes have been contacted
    6.  Internal GAOs should probably have a delta-tracking mechanism
        for conflict resolution
    7.  Dispatchables should rely upon immediate pushing, and then raise
        immediately for any conflicts
    '''
    
    # Default a few things to prevent attributeerrors
    _legroom = None
    _target_history = tuple()
    
    # Make weak properties for the various thingajobbers
    _golcore = weak_property('__golcore')
    _ghidproxy = weak_property('__ghidproxy')
    _privateer = weak_property('__privateer')
    _percore = weak_property('__percore')
    _librarian = weak_property('__librarian')
    
    # Make readonly properties for dynamic, ghid, and author. Note that the
    # author field is technically deprecated and pending removal, but is
    # currently used to infer the binder for applications.
    dynamic = readonly_property('_dynamic')
    author = readonly_property('_author')
    # This is immutable, so that it may be set just after initting.
    ghid = immutable_property('_ghid')
    _master_secret = readonly_property('__msec')
    
    # Also, an un-deletable property for history (even if unused), etc
    isalive = immortal_property('_isalive')
    # Most recent TARGET ghids
    target_history = immortal_property('_target_history')
    _counter = immortal_property('__counter')
    
    @property
    def legroom(self):
        ''' Do the thing with the thing.
        '''
        return self._legroom
        
    @legroom.setter
    def legroom(self, value):
        ''' Cast value as int and then assign it both internally and to
        the two history deques.
        '''
        legroom = int(value)
        
        if legroom != self._legroom:
            self._legroom = legroom
            # We actually have to create new deques for this, because you
            # cannot resize existing ones.
            self.target_history = collections.deque(self.target_history,
                                                    maxlen=legroom)
            
    def _inject_msec(self, msec):
        ''' Forcibly update the master secret. ONLY call this with
        extreme foresight. Intended only for use when bootstrapping the
        privateer itself.
        '''
        setattr(self, '__msec', msec)
            
    @property
    def _local_secret(self):
        ''' This determines if the secret will be committed locally only
        (for master_secreted objects), or if it will be stored
        persistently for the account.
        '''
        return bool(self._master_secret)
    
    def __init__(self, ghid, dynamic, author, legroom, *args, golcore,
                 ghidproxy, privateer, percore, librarian, master_secret=None,
                 **kwargs):
        ''' Init should be used only to create a representation of an
        EXISTING (or about to be existing) object. If any fields are
        unknown, they must be explicitly passed None.
        
        If master_secret is None, ratcheting will be iterative.
        If master_secret is a Secret, ratcheting will use the primary
            bootstrap ratcheting mechanism.
        '''
        super().__init__(*args, **kwargs)
        
        # This is an init. It will be overwritten on either a push or a pull.
        setattr(self, '__counter', None)
        
        self._golcore = golcore
        self._ghidproxy = ghidproxy
        self._privateer = privateer
        self._percore = percore
        self._librarian = librarian
        
        # Dynamic and author be explicitly None; ghid should always be defined
        self.ghid = ghid
        self._dynamic = dynamic
        self._author = author
        
        # Note that this also initializes target_history
        self.legroom = legroom
        self.isalive = True
        
        # This determines our ratcheting behavior. Set it with setattr to
        # bypass both name mangling AND the readonly property.
        setattr(self, '__msec', master_secret)
        
        # We will only ever be created from within an event loop, so this is
        # fine to do without an explicit loop.
        self._update_lock = asyncio.Lock()
        
    @fixture_noop
    @public_api
    async def apply_delete(self, debinding):
        ''' Executes an external delete, caused by the debinding at the
        passed address. By default, just makes sure the debinding target
        is valid.
        
        Debinding is a _GdxxLite.
        
        Note that this gets used by Dispatchable to dispatch deletion to
        applications.
        '''
        if self.dynamic:
            if debinding.target != self.ghid:
                raise ValueError(
                    'Debinding target does not match GAO applying delete.'
                )
        else:
            debinding_target = \
                await self._librarian.summarize(debinding.target)
            if debinding_target.target != self.ghid:
                raise ValueError(
                    'Debinding target does not match GAO applying delete.'
                )
                
        self.isalive = False
            
    @public_api
    async def freeze(self):
        ''' Creates a static binding for the most current state of a
        dynamic binding. Returns the frozen ghid.
        '''
        if not self.dynamic:
            raise TypeError('Cannot freeze a static GAO.')
            
        container_ghid = await self._ghidproxy.resolve(self.ghid)
        binding = await self._golcore.make_binding_stat(container_ghid)
        await self._percore.direct_ingest(
            obj = _GobsLite.from_golix(binding),
            packed = binding.packed,
            remotable = True
        )
        
        return container_ghid
        
    @freeze.fixture
    async def freeze(self):
        ''' Just return a pseudorandom ghid.
        '''
        return Ghid.from_bytes(
            b'\x01' + bytes([random.randint(0, 255) for i in range(0, 64)])
        )
        
    @fixture_noop
    @public_api
    async def hold(self):
        ''' Make a static binding for the gao.
        Note that, since third parties can bind arbitrary objects, there
        isn't an easy way of checking for an existing static binding for
        this specific ghid.
        '''
        binding = await self._golcore.make_binding_stat(self.ghid)
        await self._percore.direct_ingest(
            obj = _GobsLite.from_golix(binding),
            packed = binding.packed,
            remotable = True
        )
        
    @fixture_noop
    @public_api
    async def delete(self):
        ''' Permanently removes the object. This method is only called
        locally; upstream deletes should call self.apply_delete.
        Incidentally, apply_delete will eventually get called through
        this. Which means (for the record) that the deleter will also
        get a notification of the delete (iff it was actually successful
        in removing the object).
        '''
        if self.dynamic:
            debinding = await self._golcore.make_debinding(self.ghid)
            await self._percore.direct_ingest(
                obj = _GdxxLite.from_golix(debinding),
                packed = debinding.packed,
                remotable = True
            )
            
        else:
            # Get frozenset of binding ghids
            bindings = await self._librarian.bind_status(self.ghid)
            
            for binding in bindings:
                obj = await self._librarian.summarize(binding)
                if isinstance(obj, _GobsLite):
                    if obj.author == self._golcore.whoami:
                        debinding = await self._golcore.make_debinding(
                            obj.ghid
                        )
                        await self._percore.direct_ingest(
                            obj = _GdxxLite.from_golix(debinding),
                            packed = debinding.packed,
                            remotable = True
                        )
            
    def _get_new_secret(self):
        ''' Gets a new secret for self.
        '''
        # If we're getting a new secret and we already have a ghid, then we
        # MUST be a dynamic object.
        if self.ghid:
            current_target = self.target_history[0]
            secret = self._privateer.ratchet_chain(
                self.ghid,
                current_target,
                self._master_secret
            )
        
        # This is a new object, and we therefore don't have a secret. Note that
        # the first secret for a bootstrapped chain will always be discarded,
        # because the salt process needs to start somewhere.
        else:
            secret = self._privateer.new_secret()
            
        return secret
    
    @public_api
    async def push(self):
        ''' Pushes the state upstream. Must be called explicitly. Unless
        push() is invoked, the state will not be synchronized.
        '''
        if not self.isalive:
            raise DeadObject('Cannot push a deleted object. Create a new one.')
        
        # We're alive. Don't do object creation here; do it in oracle. This is
        # strictly updating.
        elif self.dynamic:
            # We need to make sure we're not pushing and pulling at the same
            # time.
            async with self._update_lock:
                try:
                    await self._push()
                
                # TODO: narrow which exceptions can cause this.
                except Exception:
                    logger.error(''.join((
                        'GAO ',
                        str(self.ghid),
                        ' PUSH: failed to update object; forcibly restoring ',
                        'state w/ traceback:\n',
                        traceback.format_exc()
                    )))
                    # We had a problem, so we're going to forcibly restore the
                    # object to the last known good state. Use _pull to avoid
                    # the lock, as well as any extra baggage in "normal" pull
                    await self._pull()
                    raise
            
        else:
            raise TypeError('Static objects cannot be updated.')
            
    @push.fixture
    async def push(self):
        ''' For fixture, just pass to _push directly.
        '''
        await self._push()
    
    @public_api
    async def _push(self):
        ''' The actual "meat and bones" for pushing.
        '''
        secret = self._get_new_secret()
        packed = await self.pack_gao()
        container = await self._golcore.make_container(packed, secret)
        
        # Dynamic object
        if self.dynamic:
            binding = await self._golcore.make_binding_dyn(
                target = container.ghid,
                ghid = self.ghid,
                history = self.target_history
            )
            binding_obj = _GobdLite.from_golix(binding)
            counter = binding.counter
            reference_ghid = binding.ghid_dynamic
            
        # Static object.
        else:
            binding = await self._golcore.make_binding_stat(container.ghid)
            binding_obj = _GobsLite.from_golix(binding)
            counter = 0
            reference_ghid = container.ghid
        
        # Okay, up until now, we haven't changed any state. But now we need to
        # update state, which needs to be atomic-ish, so we're going to do some
        # funny business here.
        
        # If this gets cancelled, we don't need to worry about anything.
        await self._percore.direct_ingest(
            obj = binding_obj,
            packed = binding.packed,
            remotable = True
        )
        
        # But if we get here, we've changed state, so we need to protect stuff.
        try:
            # It we ingested the binding, we *must* also ingest the container
            # (if **at all** possible), or the binding will effectively be
            # unrecoverable
            container_ingester = asyncio.ensure_future(
                self._percore.direct_ingest(
                    obj = _GeocLite.from_golix(container),
                    packed = container.packed,
                    remotable = True
                )
            )
        
            await asyncio.shield(container_ingester)
        
        # That container ingestion is the only remaining async call within this
        # coro, so this is the only thing we need to defer cancellation on.
        # While we're at it, defer any other exceptions, too, though they will
        # likely break the object anyways.
        except Exception as exc:
            deferred_raise = exc
            
        else:
            deferred_raise = None
            
        # We created a container and binding frame, and then we successfully
        # uploaded them. Update our frame and target history accordingly and
        # commit the container secret.
        self._privateer.stage(container.ghid, secret)
        self._privateer.commit(container.ghid, localize=self._local_secret)
        # NOTE THE DISCREPANCY between the Golix dynamic binding version
        # of ghid and ours! This is recording the frame ghid.
        # I mean, for static stuff this isn't (strictly speaking) relevant,
        # but it also doesn't really hurt anything, sooo...
        self.target_history.appendleft(container.ghid)
        self._counter = counter
        
        # Do this last!
        # Update ghid if it was not defined (new object)
        self._conditional_init(
            reference_ghid,
            False,
            self._golcore.whoami
        )
        
        if deferred_raise is not None:
            raise deferred_raise
            
    @_push.fixture
    async def _push(self):
        ''' Do a conditional init if we haven't had stuff fully defined.
        '''
        self._conditional_init(
            ghid = Ghid.from_bytes(
                b'\x01' + bytes([random.randint(0, 255) for i in range(0, 64)])
            ),
            author = Ghid.from_bytes(
                b'\x01' + bytes([random.randint(0, 255) for i in range(0, 64)])
            ),
            dynamic = None
        )
    
    @public_api
    async def pull(self, notification):
        ''' Pulls state from upstream and applies it. Does not check for
        recency.
        '''
        # Is this a good idea? It is actually possible to un-delete an object,
        # if you re-bind it, soooo...
        # TODO: think darüber
        if not self.isalive:
            raise DeadObject('Cannot pull a deleted object. Create a new one.')
            
        elif self.dynamic:
            summary = await self._librarian.summarize(notification)
            # We've been deleted.
            if isinstance(summary, _GdxxLite):
                async with self._update_lock:
                    await self.apply_delete(summary)
                    logger.info(''.join((
                        'GAO at ',
                        str(self.ghid),
                        ' PULL COMPLETED; deleted.'
                    )))
                    
            # We're being updated.
            elif summary.ghid == self.ghid:
                async with self._update_lock:
                    await self._pull()
                    logger.info(''.join((
                        'GAO at ',
                        str(self.ghid),
                        ' PULL COMPLETED with updates.'
                    )))
            
            # The update did not match us. This is sufficient to verify
            # both that the update was a dynamic binding, and that there's
            # no upstream bug for us.
            else:
                raise ValueError(
                    'Update failed pull; mismatched binding ghid.'
                )
            
        else:
            raise TypeError('Static objects cannot be pulled.')
            
    @pull.fixture
    async def pull(self, notification):
        ''' For the fixture, just pass to _pull directly.
        '''
        await self._pull()
    
    @public_api
    async def _pull(self):
        ''' The actual "meat and bones" for pulling.
        
        This is what you'll extend in Dispatchable to update
        applications.
        '''
        # Make sure we have the most recent binding, regardless of what was
        # recently passed.
        binding = await self._librarian.summarize(self.ghid)
        
        # Okay, if this is calling _pull from oracle._get_object, we may be
        # going for either a dynamic binding or a static object (container).
        if isinstance(binding, _GobdLite):
            self._recover_target_secret(binding)
            
            target_vector = binding.target_vector
            dynamic = True
                    
        # Static object, so this is the actual container (and not the binding).
        elif isinstance(binding, _GeocLite):
            target_vector = [binding.ghid]
            dynamic = False
            
        # None of the above. TypeError.
        else:
            raise TypeError('Update failed pull; mismatched Golix primitive.')
        
        try:
            # Do this before the state update so it has access to self.ghid
            # If this was just pulled for the first time, init the dynamic and
            # author attributes.
            self._conditional_init(
                ghid = binding.ghid,    # For GOBS, actually the container ghid
                dynamic = dynamic,
                author = binding.author
            )
            
            state_update = asyncio.ensure_future(
                self._update_state(target_vector)
            )
            await asyncio.shield(state_update)
        
        # Ensure the state update cannot be cancelled partway through
        except asyncio.CancelledError:
            await state_update
            raise
            
    @_pull.fixture
    async def _pull(self):
        ''' Do a conditional init if we haven't had stuff fully defined.
        '''
        self._conditional_init(
            ghid = Ghid.from_bytes(
                b'\x01' + bytes([random.randint(0, 255) for i in range(0, 64)])
            ),
            author = Ghid.from_bytes(
                b'\x01' + bytes([random.randint(0, 255) for i in range(0, 64)])
            ),
            dynamic = bool(random.randint(0, 1))
        )
        
    async def _update_state(self, target_vector):
        ''' Does a failure-resistant, semi-atomic setate update.
        '''
        for target_ghid in target_vector:
            try:
                packed_state = await self._recover_container(target_ghid)
                
                # Finally, with all of the administrative stuff handled, unpack
                # the actual payload.
                await self.unpack_gao(packed_state)
                
            except asyncio.CancelledError:
                raise
                
            except Exception:
                logger.error('Exception updating GAO state. Attempting to ' +
                             'failback to stale frame w/ traceback:\n' +
                             ''.join(traceback.format_exc()))
            
            # On the first success, stop looking for a failback container.
            else:
                break
                
        # Hitting this means we failed to find a valid state. We need to raise.
        else:
            raise UnrecoverableState('No valid state found for ' +
                                     str(self.ghid))
        
    def _recover_target_secret(self, binding):
        ''' For the passed binding, ratchet the secret chain (if
        possible), staging all intermediate secrets in the process. If
        the ratchet is broken and the secret also missing, raise.
        '''
        # Also don't forget to update history. Do this first, because it
        # will create a canonical update, regardless of success in
        # unpacking.
        try:
            # Recover the new secret for the chain.
            logger.debug('Healing ratchet for ' + str(self.ghid) + ' frame ' +
                         str(binding.counter) + '...')
            self._privateer.heal_chain(
                proxy = self.ghid,
                target_vector = self._maximize_target_vector(binding),
                master_secret = self._master_secret
            )
            
        except RatchetError as exc:
            if binding.target not in self._privateer:
                raise UnknownSecret(str(binding.target)) from exc
        
        # Now, we need to be sure our local history doesn't forget about
        # our current-most-recent frame when making an update.
        finally:
            # Finally, update our target_history accordingly.
            self.target_history = collections.deque(
                binding.target_vector,
                # Make sure we have at least one historical target
                maxlen = max(2, len(binding.target_vector))
            )
            
    async def _recover_container(self, container_ghid):
        ''' From the container ghid, recover the packed state.
        '''
        secret = self._privateer.get(container_ghid)
        
        packed = await self._librarian.retrieve(container_ghid)
        
        try:
            # TODO: fix the leaky abstraction of jumping into the _identity
            unpacked = self._golcore._identity.unpack_container(packed)
            packed_state = await self._golcore.open_container(
                unpacked,
                secret
            )
        
        except SecurityError:
            self._privateer.abandon(container_ghid)
            raise
            
        else:
            self._privateer.commit(container_ghid, localize=self._local_secret)
        
        return packed_state
        
    def _maximize_target_vector(self, new_obj):
        ''' Construct the longest possible target vector, given the
        new_obj and our knowledge of the old target history.
        '''
        # We need to accommodate a possible counter is None
        old_counter = self._counter
        if old_counter is None:
            old_counter = 0
        
        offset = new_obj.counter - old_counter
        
        new_len = len(new_obj.target_vector)
        old_len = len(self.target_history)
        maximized = list(new_obj.target_vector)
        
        # If the new target vector is smaller than the offset, then the
        # old target vector is "buried" history (there are missing frames
        # between the newest old target and the oldest new target) and
        # therefore useless. Short-circuit so we don't accidentally try to
        # do something with a negative index and then IndexError.
        if new_len - offset > 0:
            for ii in range(new_len - offset, old_len):
                maximized.append(self.target_history[ii])
            
        return maximized
        
    def _advance_history(self, new_obj):
        ''' Updates our history to match the new one.
        
        DEPRECATED AND UNUSED.
        
        NOTE THAT THIS DEPENDS ON THE PERSISTER to enforce the history
        hash chain. This is only aligning the histories, and NOT looking
        at the current frames themselves.
        '''
        old_history = self.frame_history
        new_history = new_obj.history
        
        # We need to check to make sure this isn't pulling in the zeroth frame.
        if new_history:
            new_legroom = len(new_history)
            
            # Find how far offset the new_history is from the old_history.
            for offset in range(new_legroom):
                # Align the histories by checking the ii'th new element against
                # an offset ii'th old element.
                for ii in range(new_legroom - offset):
                    # The new element matches the offset old element, so don't
                    # change the offset.
                    if new_history[offset + ii] == old_history[ii]:
                        continue
                    
                    # It didn't match, so we need to up the offset by one.
                    # Short circuit the inner loop via break.
                    else:
                        break
                    
                # We found a match. Short-circuit parent loop.
                else:
                    break
        
            # We never found a match. Offset should be made into the length of
            # the entire thing. This depends on persistence system to enforce
            # the hash chain!
            else:
                offset += 1
        
        # This is pulling the zeroth frame. We have no history.
        else:
            # Make sure new_legroom is always at least 1. Otherwise, the zeroth
            # frame (which by definition has no history) will forever damn the
            # object from being pulled (until it gets updated at its origin
            # first).
            new_legroom = 1
            offset = 0
            
        # Calculate the offset as the difference between the two counters,
        # easy-peasy style.
        offset = new_obj.counter - self._counter
                    
        
        # Now, appendleft the new frame (and an empty target) until we get to
        # the most recent one -- but hold off on adding the new target so we
        # can preserve the maximum history length
        for ii in range(offset - 1, 0, -1):
            self.frame_history.appendleft(new_history[ii])
            self.target_history.appendleft(None)
            
    def _conditional_init(self, ghid, dynamic, author):
        ''' If dynamic had not been set, set both it and the author. If
        ghid has not been set, set it.
        '''
        if self._dynamic is None:
            self._dynamic = dynamic
            
        if self._author is None:
            self._author = author
            
        if self.ghid is None:
            self.ghid = ghid
        
    async def pack_gao(self):
        ''' Packs self into a bytes object. May be overwritten in subs
        to pack more complex objects. Should always be a staticmethod or
        classmethod.
        
        May be used to implement, for example, packing self into a
        DispatchableState, etc etc.
        '''
        pass
        
    async def unpack_gao(self, packed):
        ''' Unpacks state from a bytes object and applies state to self.
        May be overwritten in subs to unpack more complex objects.
        
        May be used to implement, for example, dicts performing a
        clear() operation before an update() instead of just reassigning
        the object.
        '''
        pass
        
    def __del__(self, *args, **kwargs):
        ''' Log the removal of the GAO.
        '''
        logger.info(
            'GAO ' + str(self.ghid) + ' passed out of memory and is being ' +
            'locally garbage collected by Python.'
        ) class Privateer(metaclass=API):
    ''' Lookup system to get secret from ghid. Loopsafe, but NOT
    threadsafe.
    '''
    _account = weak_property('__account')
    _golcore = weak_property('__golcore')
    
    @public_api
    def __init__(self, *args, **kwargs):
        ''' Temporarily invalidate (but init) the various secrets
        lookups.
        '''
        super().__init__(*args, **kwargs)
        
        # These must be bootstrapped.
        self._secrets_persistent = None
        self._secrets_quarantine = None
        self._secrets = None
        # These two are local only, but included to make the code more explicit
        self._secrets_staging = None
        self._secrets_local = None
        
    @__init__.fixture
    def __init__(self, identity, *args, **kwargs):
        ''' Directly inject a local identity, and then add a ghidproxy.
        '''
        super(Privateer.__fixture__, self).__init__(*args, **kwargs)
        
        self._identity = identity
        self._secrets_persistent = {}
        self._secrets_staging = {}
        self._secrets_local = {}
        self._secrets_quarantine = {}
        self._secrets = collections.ChainMap(
            self._secrets_persistent,
            self._secrets_local,
            self._secrets_staging,
            self._secrets_quarantine,
        )
        self._secrets_committed = collections.ChainMap(
            self._secrets_persistent,
            self._secrets_local
        )
        
    @fixture_api
    def RESET(self):
        # Clear errything internal.
        self._secrets_persistent.clear()
        self._secrets_staging.clear()
        self._secrets_local.clear()
        self._secrets_quarantine.clear()
            
    def __contains__(self, ghid):
        ''' Check if we think we know a secret for the ghid.
        '''
        return ghid in self._secrets
        
    def assemble(self, golcore):
        # Chicken, meet egg.
        self._golcore = golcore
    
    @fixture_noop
    @public_api
    def bootstrap(self, account):
        ''' Initializes the privateer into a distributed state.
        persistent is a GaoDict
        '''
        self._account = account
        
        # We very obviously need to be able to look up what secrets we have.
        # Lookups: <container ghid>: <container secret>
        self._secrets_persistent = account.privateer_persistent
        self._secrets_staging = {}
        self._secrets_local = {}
        self._secrets_deprecated = FiniteDict(maxlen=100)
        self._secrets_quarantine = account.privateer_quarantine
        self._secrets = collections.ChainMap(
            self._secrets_persistent,
            self._secrets_local,
            self._secrets_deprecated,
            self._secrets_staging,
            self._secrets_quarantine,
        )
        self._secrets_committed = collections.ChainMap(
            self._secrets_persistent,
            self._secrets_local
        )
        
        # Note that we just overwrote any chains that were created when we
        # initially loaded the three above resources. So, we may need to
        # re-create them. But, we can use a fake container address for two of
        # them, since they use a different secrets tracking mechanism.
        
    @public_api
    def new_secret(self):
        # Straight pass-through to the golix new_secret bit.
        return self._golcore._identity.new_secret()
        
    @new_secret.fixture
    def new_secret(self):
        # We have a direct identity for the fixture.
        return self._identity.new_secret()
        
    def get(self, ghid):
        ''' Get a secret for a ghid, regardless of status.
        
        Raises KeyError if secret is not present.
        '''
        try:
            return self._secrets[ghid]
        
        except KeyError:
            raise UnknownSecret(str(ghid)) from None
        
    def stage(self, ghid, secret):
        ''' Preliminarily set a secret for a ghid.
        
        If a secret is already staged for that ghid and the ghids are
        not equal, raises ConflictingSecrets.
        '''
        # Is there a reason we can't verify container-ness here?
        logger.debug(''.join((
            'GAO ',
            str(ghid),
            ' secret staging.'
        )))
        self._stage(ghid, secret, self._secrets_staging)
                
    def _stage(self, ghid, secret, lookup):
        ''' Raw staging, bypassing modlock. Only accessed directly
        during bootstrapping.
        '''
        if not isinstance(secret, Secret):
            raise TypeError('Invalid secret type: ' + type(secret).__name__)
        
        if ghid in self._secrets:
            if self._secrets[ghid] != secret:
                self._calc_and_log_diff(self._secrets[ghid], secret)
                raise ConflictingSecrets(
                    'Non-matching secret already known for ' + str(ghid)
                )
        else:
            lookup[ghid] = secret
            
    def quarantine(self, ghid, secret):
        ''' Store a secret temporarily, but distributedly, separate from
        the primary secret store. Used only for incoming secrets in
        shares that have not yet been opened.
        '''
        logger.debug(''.join((
            'GAO ',
            str(ghid),
            ' secret quarantining.'
        )))
        self._stage(ghid, secret, self._secrets_quarantine)
            
    def unstage(self, ghid):
        ''' Remove a staged secret, probably due to a SecurityError.
        Returns the secret.
        '''
        secret = self._secrets_quarantine.pop(ghid, None)
        secret = secret or self._secrets_staging.pop(ghid, None)
        
        if secret is None:
            raise UnknownSecret(
                'No currently staged secret for GHID ' + str(ghid)
            )
                
        return secret
        
    def commit(self, ghid, localize=False):
        ''' Store a secret "permanently". The secret must either:
        1. Be staged, XOR
        2. Be quarantined, XOR
        3. Be committed
        
        Other states will raise UnknownSecret (a subclass of KeyError).
        
        Note that this relies upon staging logic enforcing consistency
        of secrets! If that changes to allow the staging conflicting
        secrets, this will need to be modified.
        
        This is transactional and atomic; any errors (ex: ValueError
        above) will return its state to the previous.
        
        With localize=True, the secret is committed locally only. This
        is intended to be used in combination with a master secret; in
        that case, there's no need to store frame keys -- not to mention
        that it would cause an infinitely recursive chain of secret
        committing.
        
        '''
        # Catch anything with an unknown secret
        if ghid not in self._secrets:
            raise UnknownSecret(
                'Secret not currently staged for GHID ' + str(ghid)
            )
        
        # Note that we cannot stage or quarantine a secret that we already
        # have committed. If that secret matches what we already have, it
        # will indempotently and silently exit, without staging.
        elif ghid in self._secrets_committed:
            return
        
        secret = self._secrets_quarantine.pop(ghid, None)
        secret = secret or self._secrets_staging.pop(ghid, None)
            
        # If it's a bootstrap target, just locally commit it.
        if localize:
            logger.debug(''.join((
                'GAO ',
                str(ghid),
                ' secret committed locally.'
            )))
            self._secrets_local[ghid] = secret
            
        # Nope. Commit globally.
        else:
            logger.debug(''.join((
                'GAO ',
                str(ghid),
                ' secret committed globally.'
            )))
            self._secrets_persistent[ghid] = secret
    
    @classmethod
    def _calc_and_log_diff(cls, secret, other):
        ''' Calculate the difference between two secrets.
        '''
        try:
            cipher_match = (secret.cipher == other.cipher)
            
            if cipher_match:
                key_comp = cls._bitdiff(secret.key, other.key)
                seed_comp = cls._bitdiff(secret.seed, other.seed)
                logger.info('Keys are ' + str(key_comp) + '%% different.')
                logger.info('Seeds are ' + str(seed_comp) + '%% different.')
                
            else:
                logger.info('Secret ciphers do not match. Cannot compare.')
            
        except AttributeError:
            logger.error(
                'Attribute error while diffing secrets. Type mismatch? \n'
                '    ' + repr(type(secret)) + '\n'
                '    ' + repr(type(other))
            )
            
    @staticmethod
    def _bitdiff(this_bytes, other_bytes):
        ''' Calculates the percent of different bits between two byte
        strings.
        '''
        if len(this_bytes) == 0 or len(other_bytes) == 0:
            # By returning None, we can explicitly say we couldn't perform the
            # comparison, whilst also preventing math on a comparison.
            return None
        
        # Mask to extract each bit.
        masks = [
            0b00000001,
            0b00000010,
            0b00000100,
            0b00001000,
            0b00010000,
            0b00100000,
            0b01000000,
            0b10000000,
        ]
        
        # Counters for bits.
        diffbits = 0
        totalbits = 0
        
        # First iterate over each byte.
        for this_byte, other_byte in zip(this_bytes, other_bytes):
            
            # Now, using the masks, iterate over each bit.
            for mask in masks:
                # Extract the bit using bitwise AND.
                this_masked = mask & this_byte
                other_masked = mask & other_byte
                
                # Do a bool comparison of the bits, and add any != results to
                # diffbits. Note that 7 + False == 7 and 7 + True == 8
                diffbits += (this_masked != other_masked)
                totalbits += 1
                
        # Finally, calculate a percent difference
        doubdiff = diffbits / totalbits
        return int(doubdiff * 100)
        
    def abandon(self, ghid, quiet=True):
        ''' Remove a secret. If quiet=True, silence any KeyErrors.
        '''
        # Short circuit any tests if quiet is enabled
        if not quiet and ghid not in self._secrets:
            raise UnknownSecret('Secret not found for ' + str(ghid))
            
        self._secrets_persistent.pop(ghid, None)
        self._secrets_local.pop(ghid, None)
        self._secrets_quarantine.pop(ghid, None)
        self._secrets_staging.pop(ghid, None)
        self._secrets_deprecated.pop(ghid, None)
        
    @fixture_noop
    @public_api
    def deprecate(self, ghid, quiet=True):
        ''' Deprecate a secret. It will still be available locally, but
        will be removed from shared nonlocal state.
        '''
        # Short circuit any tests if quiet is enabled
        if not quiet and ghid not in self._secrets:
            raise UnknownSecret('Secret not found for ' + str(ghid))
        
        else:
            secret1 = self._secrets_persistent.pop(ghid, None)
            secret2 = self._secrets_local.pop(ghid, None)
            secret3 = self._secrets_quarantine.pop(ghid, None)
            secret4 = self._secrets_staging.pop(ghid, None)
            secret5 = self._secrets_deprecated.pop(ghid, None)
            
            secret = secret1 or secret2 or secret3 or secret4 or secret5
            
            self._secrets_deprecated[ghid] = secret
        
    def ratchet_chain(self, proxy, current_target, master_secret=None):
        ''' Gets a new secret for the proxy. Returns the secret, and
        flags the ratchet as in-progress.
        
        If master_secret is supplied, we will use the bootstrap ratchet.
        '''
        try:
            # Without a master_secret, perform a ratchet with the existing
            # secret for the current target.
            if master_secret is None:
                existing_secret = self.get(current_target)
                ratcheted = self._ratchet(
                    secret = existing_secret,
                    proxy = proxy,
                    salt_ghid = current_target
                )
            
            # With a master_secret, perform a ratchet with the master as a
            # static "seed" secret.
            else:
                existing_secret = None
                ratcheted = self._ratchet(
                    secret = master_secret,
                    proxy = proxy,
                    salt_ghid = current_target
                )
                
            return ratcheted
            
        except Exception:
            msec_str = (' with master secret of type ' +
                        type(master_secret).__name__) * bool(master_secret)
            xsec_str = (' with existing secret of type ' +
                        type(existing_secret).__name__) * (not master_secret)
            
            raise RatchetError('Failed ratchet for unknown reasons: ' +
                               str(proxy) + ' from ' + str(current_target) +
                               msec_str + xsec_str) from None
            
    def heal_chain(self, proxy, target_vector, master_secret=None):
        ''' Heals the ratchet for a binding using the gao. Call this any
        time an agent RECEIVES a new EXTERNAL ratcheted object. Stages
        the resulting secret for the most recent frame in binding, BUT
        DOES NOT RETURN (or commit) IT.
        
        Note that this is necessarily being called before the object is
        available at the oracle. It should, however, be available at
        both the ghidproxy and the librarian.
        
        NOTE that the binding is the LITEWEIGHT version from the
        librarian already, so its ghid is already the dynamic one.
        '''
        if len(target_vector) < 1:
            raise RatchetError('Target vector has no historical references.')
            
        # Master secret ratchets never break, and can alway recover from the
        # most recent frame. BUT, to increase failure tolerance, we are going
        # to stage any and all missing secrets, in case we need to load an
        # earlier container.
        if master_secret:
            aktueller_index = len(target_vector) - 1
            
        # Other ratchets, on the other hand, have to start somewhere.
        else:
            # Go from back to front, finding the most recent target vector that
            # exists locally.
            available = [target in self._secrets for target in target_vector]
            # If we didn't find ANY of them, then we're broken.
            if not any(available):
                raise RatchetError('Broken ratchet: ' + str(proxy))
        
            # Get the index for the most recent target that we already know a
            # secret for
            aktueller_index = available.index(True)
        
        # Go from the oldest to the newest, but start after the first one,
        # because we verified that above.
        # We can't use reversed(enumerate(target_vector)) without re-casting
        # target_vector into a dedicated sequence. We'd like to keep the
        # original iterator, so do it manually.
        for ii in range(aktueller_index - 1, -1, -1):
            target = target_vector[ii]
            # Skip the first one (deques don't support slicing)
            # Skip if we already have a secret
            if target in self._secrets:
                continue
            
            # Only do something if we don't know the secret
            else:
                secret = self.ratchet_chain(
                    proxy,
                    current_target = target_vector[ii + 1],
                    master_secret = master_secret
                )
                self.stage(target, secret)
        
    @staticmethod
    def _ratchet(secret, proxy, salt_ghid):
        ''' Ratchets a key using HKDF-SHA512, using the associated
        address as salt. For dynamic files, this should be the previous
        frame ghid (not the dynamic ghid).
        
        Note: this ratchet is bound to a particular dynamic address. The
        ratchet algorithm is:
        
        new_key = HKDF-SHA512(
            IKM = old_secret, (secret IV/nonce | secret key)
            salt = old_frame_ghid, (entire 65 bytes)
            info = dynamic_ghid, (entire 65 bytes)
            new_key_length = len(IV/nonce) + len(key),
            num_keys = 1,
        )
        '''
        cls = type(secret)
        cipher = secret.cipher
        version = secret.version
        len_seed = len(secret.seed)
        len_key = len(secret.key)
        source = bytes(secret.seed + secret.key)
        
        instance = hkdf.HKDF(
            algorithm = hashes.SHA512(),
            length = len_seed + len_key,
            salt = bytes(salt_ghid),
            info = bytes(proxy),
            backend = CRYPTO_BACKEND
        )
        ratcheted = instance.derive(source)
        
        return cls(
            cipher = cipher,
            version = version,
            key = ratcheted[:len_key],
            seed = ratcheted[len_key:]
        ) class Salmonator(loopa.TaskLooper, metaclass=API):
    ''' Responsible for disseminating Golix objects upstream and
    downstream. Handles all comms with them as well.
    '''
    _golcore = weak_property('__golcore')
    _percore = weak_property('__percore')
    _librarian = weak_property('__librarian')
    _remote_protocol = weak_property('__remote_protocol')
    
    @public_api
    def __init__(self, *args, **kwargs):
        ''' Yarp.
        '''
        super().__init__(*args, **kwargs)
        
        self._deferred = ListMap()
        self._clear_q = None
        
        self._upstream_remotes = set()
        self._downstream_remotes = set()
        
        # Lookup for <registered ghid>
        self._registered = set()
        
        self._bootstrapped = False
        
    @fixture_api
    def RESET(self):
        ''' Clear stuff out, yo.
        '''
        self._upstream_remotes.clear()
        self._downstream_remotes.clear()
        self._registered.clear()
        
    def assemble(self, golcore, percore, librarian, remote_protocol):
        self._golcore = golcore
        self._percore = percore
        self._librarian = librarian
        self._remote_protocol = remote_protocol
    
    @fixture_noop
    @public_api
    def add_upstream_remote(self, task_commander, connection_cls, *args,
                            **kwargs):
        ''' Adds an upstream remote persister.
        *args and **kwargs will be passed to the task_commander task.
        '''
        remote = ConnectionManager(
            connection_cls = connection_cls,
            msg_handler = self._remote_protocol,
            conn_init = self.restore_connection,
        )
        # We have to insert the remote before us at the commander, or shutdown
        # will kill the connections before we can clean them up.
        task_commander.register_task(remote, *args, before_task=self, **kwargs)
        self._upstream_remotes.add(remote)
        
    def add_downstream_remote(self, persister):
        ''' Adds a downstream persister.
        
        PersistenceCore will not attempt to keep a consistent state with
        downstream persisters. Instead, it will simply push updates to
        local objects downstream. It will not, however, look to them for
        updates.
        
        Therefore, to create synchronization **between** multiple
        upstream remotes, also add them as downstream remotes.
        '''
        raise NotImplementedError()
        self._downstream_remotes.add(persister)
        
    async def loop_init(self, *args, **kwargs):
        ''' On top of the usual stuff, set up our queues.
        '''
        await super().loop_init(*args, **kwargs)
        # This "must" be unbounded, because we don't have any control over
        # finalization of objects.
        self._clear_q = asyncio.Queue()
        
    async def loop_stop(self, *args, **kwargs):
        ''' On top of the usual stuff, clear our queues.
        '''
        # Would be good to do, but not currently working
        # for remote in self._persisters:
        #     await self._stop_persister(remote)
        
        await super().loop_stop(*args, **kwargs)
        self._clear_q = None
        
        disconnections = set()
        for remote in self._upstream_remotes:
            if remote.has_connection:
                disconnections.add(
                    make_background_future(remote.disconnect())
                )
        
        # Need to make sure it's not empty
        if disconnections:
            await asyncio.wait(
                fs = disconnections,
                return_when = asyncio.ALL_COMPLETED
            )
        
    async def loop_run(self, *args, **kwargs):
        ''' Wait for object finalizers, and then immediately unsub the
        remotes when the objects are removed from memory.
        '''
        to_clear = await self._clear_q.get()
        await self.deregister(to_clear)
        
    @fixture_noop
    @public_api
    async def notify(self, ghid):
        ''' Notify the salmonator that an upstream subscription
        notification was just successfully completed.
        '''
        # If that was a dynamic object, we also need to pull its target.
        obj = await self._librarian.summarize(ghid)
        if isinstance(obj, _GobdLite):
            if not (await self._librarian.contains(obj.target)):
                await self.pull(obj.target)
    
    @fixture_noop
    @public_api
    async def register(self, ghid):
        ''' Tells the Salmonator to listen upstream for any updates.
        Consumers must independently register an object finalizer for
        salmonator._deregister (not to be called atexit) to remove the
        subscription when the object leaves local memory. TODO: fix that
        leaky abstraction.
        '''
        obj = await self._librarian.summarize(ghid)
        
        if isinstance(obj, _GobdLite):
            logger.info(
                'GAO ' + str(obj.ghid) + ' upstream registration starting.'
            )
            self._registered.add(obj.ghid)
    
            subscriptions = set()
            for remote in self._upstream_remotes:
                if remote.has_connection:
                    subscriptions.add(
                        make_background_future(remote.subscribe(obj.ghid))
                    )
                
            # Need to make sure it's not empty
            if subscriptions:
                await asyncio.wait(
                    fs = subscriptions,
                    return_when = asyncio.ALL_COMPLETED
                )
        
        else:
            logger.debug(
                'GAO ' + str(obj.ghid) + ' cannot be registered upstream: ' +
                'invalid object type: ' + str(type(obj))
            )
    
    @fixture_noop
    @public_api
    def _deregister(self, ghid):
        ''' Finalizer for GAO objects that executes async deregister()
        from within the event loop. Must be called from within our own
        thread, which it should be (finalizers are called from object
        thread, and all gao must be created within event loop's thread).
        '''
        logger.debug('_deregister finalizer called for ' + str(ghid))
        if self._clear_q is not None:
            # This needs to be a function, not a coro, so use nowait.
            self._clear_q.put_nowait(ghid)
    
    @fixture_noop
    @public_api
    async def deregister(self, ghid):
        ''' Tells the salmonator to stop listening for upstream object
        updates.
        '''
        logger.debug('Deregistering updates for ' + str(ghid))
        # This should maybe use remove instead of discard?
        self._registered.discard(ghid)
    
        unsubscriptions = set()
        for remote in self._upstream_remotes:
            unsubscriptions.add(
                make_background_future(remote.unsubscribe(ghid))
            )
        
        # Need to make sure it's not empty
        if unsubscriptions:
            await asyncio.wait(
                fs = unsubscriptions,
                return_when = asyncio.ALL_COMPLETED
            )
    
    @fixture_noop
    @public_api
    async def push(self, ghid):
        ''' Push a single ghid to all remotes.
        
        TODO: smartly defer any pushes to unavailable remotes using
        "if remote.has_connection()"
        '''
        data = await self._librarian.retrieve(ghid)
        
        tasks = set()
        for remote in self._upstream_remotes:
            if remote.has_connection:
                tasks.add(
                    make_background_future(remote.publish(data))
                )
            else:
                self._deferred.append(remote, data)
            
        # Need to make sure it's not empty
        if tasks:
            await asyncio.wait(
                fs = tasks,
                return_when = asyncio.ALL_COMPLETED
            )
    
    @fixture_noop
    @public_api
    async def pull(self, ghid):
        ''' Gets a ghid from upstream. Returns on the first result. Note
        that this is not meant to be called on a dynamic address, as a
        subs update from a slower remote would always be overridden by
        the faster one.
        '''
        pull_complete = None
        tasks_available = set()
        task_to_remote = {}
        for remote in self._upstream_remotes:
            if remote.has_connection:
                task = asyncio.ensure_future(
                    self._attempt_pull_single(ghid, remote)
                )
                tasks_available.add(task)
                task_to_remote[task] = remote
            
        # Wait until the first successful task completion
        # Note that this also shields us against having no tasks
        while tasks_available and not pull_complete:
            finished, pending = await asyncio.wait(
                fs = tasks_available,
                return_when = asyncio.FIRST_COMPLETED
            )
        
            # Despite FIRST_COMPLETED, asyncio may return more than one task
            for task in finished:
                # The task finished, so discard it from the available and reap
                # any exception
                tasks_available.discard(task)
                exc = task.exception()
                
                # If there's been an exception, continue waiting for the rest.
                # Downgrade this to info, since things may be missing from
                # persisters all the time. Let the parent log if, for example,
                # it's missing everywhere.
                if exc is not None:
                    remote = task_to_remote[task]
                    logger.info(
                        'Error while pulling from remote at ' +
                        remote._conn_desc + ':\n' +
                        ''.join(traceback.format_tb(exc.__traceback__)) +
                        repr(exc)
                    )
                    pull_complete = None
                
                # Completed successfully, but it could be a 404 (or other
                # error), which would present as result() = False.
                # Instead of letting the while loop handle this, since more
                # than one task can complete simultaneously, make sure we don't
                # already have a finalized result before blindly assigning the
                # task's result.
                elif not pull_complete:
                    pull_complete = task.result()
                    
                # Multiple tasks completed at once. An earlier one was
                # successful. We need to grab the result to suppress asyncio
                # complaints.
                else:
                    task.result()
                
        # No dice. Either finished is None (no remotes), None (no successful
        # pulls), or False (exactly one remote had the object, but it was
        # unloadable). Raise.
        if not pull_complete:
            raise UnavailableUpstream(
                'Object was unavailable or unacceptable at all '
                'currently-registered remotes.'
            )
            
        # Log success.
        else:
            logger.debug(
                'Successful remote pull for {!s}. Handling...'.format(ghid)
            )
        
        # We may still have some pending tasks. Cancel them. Note that we have
        # not yielded control to the event loop, so there is no race.
        for task in pending:
            logger.debug('Cancelling pending pulls.')
            task.cancel()
            
        # Now handle the result.
        await self._handle_successful_pull(pull_complete)
            
    async def _handle_successful_pull(self, maybe_obj):
        ''' Dispatches the object in the successful pull.
        
        maybe_obj can be either True (if the object already existed
        locally), or the object itself (if the object was new).
        '''
        # If we got a gobd, make sure its target is in the librarian
        if isinstance(maybe_obj, _GobdLite):
            if not (await self._librarian.contains(maybe_obj.target)):
                # Catch unavailableupstream and log a warning.
                # TODO: add logic to retry a few times and then discard
                try:
                    await self.pull(maybe_obj.target)
                    
                except UnavailableUpstream:
                    logger.warning(
                        'Received a subscription notification for ' +
                        str(maybe_obj.ghid) + ', but the sub\'s target was '
                        'missing both locally and upstream.'
                    )
        
        logger.debug('Successful pull handled.')
    
    @fixture_noop
    @public_api
    async def attempt_pull(self, ghid, quiet=False):
        ''' Grabs the ghid from remotes, if available, and puts it into
        the ingestion pipeline.
        '''
        # TODO: check locally, run _inspect, check if mutable before blindly
        # pulling.
        try:
            await self.pull(ghid)
            
        except UnavailableUpstream:
            if not quiet:
                raise
            # Suppress errors if we were called quietly.
            else:
                logger.info(
                    'Object was unavailable or unacceptable upstream, but '
                    'pull was called quietly: ' + str(ghid)
                )
        
    async def _attempt_pull_single(self, ghid, remote):
        ''' Attempt to fetch a single object from a single remote. If
        successful, put it into the ingestion pipeline.
        '''
        # This may error, but any errors here will be caught by the parent.
        data = await remote.get(ghid)
        
        # Call as remotable=False to avoid infinite loops.
        ingested = await self._percore.ingest(data, remotable=False)
        
        # Note that ingest can either return None, if we already have
        # the object, or the object itself, if it's new.
        if ingested:
            return ingested
        else:
            return True
            
    async def bootstrap(self, account):
        ''' Bootstrapping publishes our identity upstream.
        
        ONLY do this if we already have a connection available there, or
        we will hang.
        '''
        for remote in self._upstream_remotes:
            if remote.has_connection:
                await remote.publish(account._identity.second_party.packed)
                await remote.subscribe(account._identity.ghid)
            
        self._bootstrapped = True
    
    @fixture_noop
    @public_api
    async def restore_connection(self, remote, connection):
        ''' Start or re-start a connection.
        '''
        # We need to subscribe to our identity if we're restoring a terminated
        # connection. If we haven't bootstrapped though, that will be handled
        # there.
        if self._bootstrapped:
            await remote.publish(self._golcore._identity.second_party.packed)
            await self._remote_protocol.subscribe(
                connection,
                self._golcore.whoami
            )
        
        # For every every active (salmonator-registered) GAO's ghid...
        tasks = set()
        for registrant in self._registered:
            # Record that we need to perform...
            tasks.add(
                # ...as a background future (which handles its own errors)...
                make_background_future(
                    # ...a subscription call with our connection at protocol
                    self._remote_protocol.subscribe(connection, registrant)
                )
            )
        
        # We need to make sure there's at least one task.
        if tasks:
            # And now just wait for all of that to complete.
            await asyncio.wait(
                fs = tasks,
                return_when = asyncio.ALL_COMPLETED
            )
        
        # Now, we need to destructively iterate over our deferreds until the
        # remote list is exhausted. We have to do this in order, sequentially,
        # and serially, because eg. containers require bindings, etc.
        to_push = collections.deque(self._deferred.pop_key(remote))
        try:
            while to_push:
                data = to_push.popleft()
                await self._remote_protocol.publish(connection, data)
        
        # Restore the last to_push, put it back into the deferred list, and
        # then re-raise
        except Exception:
            to_push.appendleft(data)
            self._deferred.extend(remote, to_push)
            raise class PersistenceCore(metaclass=API):
    ''' Provides the core functions for storing Golix objects. Required
    for the hypergolix service to start.
    
    Can coordinate with both "upstream" and "downstream" persisters.
    Other persisters should pass through the "ingestive tract". Local
    objects can be published directly through calling the ingest_<type>
    methods.
    
    TODO: add librarian validation, so that attempting to update an
    object we already have an identical copy to silently exits.
    '''
    _doorman = weak_property('__doorman')
    _enforcer = weak_property('__enforcer')
    _lawyer = weak_property('__lawyer')
    _bookie = weak_property('__bookie')
    _postman = weak_property('__postman')
    _undertaker = weak_property('__undertaker')
    _librarian = weak_property('__librarian')
    _salmonator = weak_property('__salmonator')
    
    # This is a messy way of getting the suffix for validation and stuff but
    # it's getting the job done.
    _ATTR_LOOKUP = {
        _GidcLite: 'gidc',
        _GeocLite: 'geoc',
        _GobsLite: 'gobs',
        _GobdLite: 'gobd',
        _GdxxLite: 'gdxx',
        _GarqLite: 'garq'
    }
    
    @public_api
    def __init__(self, loop=None, *args, **kwargs):
        ''' Create a KeyedAsyncioLock for ingestion.
        '''
        self._ingestion_mutex = KeyedAsyncioLock(loop)
        
    @__init__.fixture
    def __init__(self, librarian=None, *args, **kwargs):
        ''' Right now, well, this is basically a noop. Anticipating
        substantial changes!
        '''
        super(PersistenceCore.__fixture__, self).__init__(
            *args,
            **kwargs
        )
        
        if librarian is not None:
            self._librarian = librarian
        
    def assemble(self, doorman, enforcer, lawyer, bookie, librarian, postman,
                 undertaker, salmonator):
        self._doorman = doorman
        self._enforcer = enforcer
        self._lawyer = lawyer
        self._bookie = bookie
        self._postman = postman
        self._undertaker = undertaker
        self._librarian = librarian
        self._salmonator = salmonator
    
    @public_api
    async def direct_ingest(self, obj, packed, remotable, skip_conn=None):
        ''' Standard ingestion flow for stuff. To be called from ingest
        above, or directly (for objects created "in-house").
        '''
        # Check for a redundant object, which will immediately short-circuit.
        if isinstance(obj, _GobdLite):
            check_ghid = obj.frame_ghid
            log_frame = True
            counter = obj.counter
            target = obj.target
        else:
            check_ghid = obj.ghid
            log_frame = False
            counter = 0
            target = None
        
        async with self._ingestion_mutex(check_ghid):
            if (await self._librarian.contains(check_ghid)):
                logger.debug(
                    str(check_ghid) + ' not ingested: already exists.'
                )
                return False
            
            else:
                logger.info(
                    'Ingesting ' + str(obj) +
                    (
                        ' frame ' + str(counter) + ': ' + str(check_ghid) +
                        ', target: ' + str(target)
                    ) * log_frame + '...'
                )
                # Calculate "gidc", etc
                suffix = self._ATTR_LOOKUP[type(obj)]
                validation_method = 'validate_' + suffix
                
                # Validate the object... (will raise for invalid)
                # ########################
                # Enforce target selection
                await getattr(self._enforcer, validation_method)(obj)
                # Now make sure authorship requirements are satisfied
                await getattr(self._lawyer, validation_method)(obj)
                # Finally make sure persistence rules are followed
                await getattr(self._bookie, validation_method)(obj)
                
                # Ingest the object
                # ########################
                # Alert the undertaker for any necessary GC of targets, etc. Do
                # that before storing at the librarian, so that the undertaker
                # has access to the old state.
                await getattr(self._undertaker, 'alert_' + suffix)(obj,
                                                                   skip_conn)
                # Finally, add it to the librarian.
                await self._librarian.store(obj, packed)
                
                if remotable:
                    await self._salmonator.push(obj.ghid)
            
                return True
    
    @direct_ingest.fixture
    async def direct_ingest(self, obj, packed, remotable, skip_conn=None):
        ''' Fixture direct ingest to just store at librarian directly
        with no verification (if librarian exists), otherwise simply
        return None.
        '''
        try:
            librarian = self._librarian
        except AttributeError:
            return None
        else:
            await librarian.store(obj, packed)
    
    @public_api
    async def attempt_load(self, packed, quiet=True):
        ''' Attempt to load a packed golix object. If quiet=False, it
        will raise if there is no loader.
        
        TODO: move into doorman.
        '''
        # This is kinda silly, but instead of spewing off a million different
        # threads that will immediately die, let's pre-select the loader
        magic = bytes(packed[:4])
        loaders = {
            b'GIDC': self._doorman.load_gidc,
            b'GEOC': self._doorman.load_geoc,
            b'GOBS': self._doorman.load_gobs,
            b'GOBD': self._doorman.load_gobd,
            b'GDXX': self._doorman.load_gdxx,
            b'GARQ': self._doorman.load_garq
        }
        
        try:
            loader = loaders[magic]
            
        # If no successful loader was found, return None, and allow the parent
        # to raise.
        except KeyError as exc:
            if quiet:
                return None
            else:
                raise MalformedGolixPrimitive('No loader found for magic: ' +
                                              str(magic)) from exc
        
        obj = await loader(packed)
            
        return obj
        
    @attempt_load.fixture
    async def attempt_load(self, packed):
        ''' Create an ad-hoc doorman fixture.
        '''
        # Note that, because of the weak ref, we need to actually hold this
        # here to prevent the doorman from being gc'd. So, just create a
        # disposable one.
        doorman = Doorman.__fixture__()
        self._doorman = doorman
        result = \
            await super(PersistenceCore.__fixture__, self).attempt_load(packed)
        del doorman
        return result
    
    @fixture_noop
    @public_api
    async def ingest(self, packed, remotable=True, skip_conn=None):
        ''' Called on an untrusted and unknown object. May be bypassed
        by locally-created, trusted objects (by calling the individual
        ingest methods directly). Parses, validates, and stores the
        object, and returns True; or, raises an error.
        '''
        # This may return None, but that will be caught by the KeyError below.
        obj = await self.attempt_load(packed)
        if obj is None:
            raise MalformedGolixPrimitive(
                'Packed bytes do not appear to be a Golix primitive.'
            )
        
        ingested = await self.direct_ingest(obj, packed, remotable)
        
        # Note that individual ingest methods are only called directly for
        # locally-built objects, which do not need a mail run.
        # If the object is identical to what we already have, the ingester
        # will return None.
        if ingested:
            await self._postman.schedule(obj, skip_conn=skip_conn)
            
        else:
            logger.debug(str(obj.ghid) + ' postal scheduling aborted on ' +
                         'unchanged object.')
        
        # Note: this is not the place for salmonator pushing! Locally
        # created/updated objects call the individual ingest methods
        # directly, so they have to be the ones that actually deal with it.
        
        return ingested class LibrarianCore(metaclass=API):
    ''' Base class for caching systems common to non-volatile librarians
    such as DiskLibrarian, S3Librarian, etc.
    
    TODO: make ghid vs frame ghid usage more consistent across things.
    
    NOTE: ideally, everything stateful should be in the librarian. In
    other words, you shouldn't use a bookie to track binding status and
    stuff; that should all be handled within librarian. That would make
    the librarian the single source of truth. Unfortunately, there's not
    really a good way of doing that right now ops-wise; you'd need some
    kind of relational filesystem.
    
    TODO: instead, should create a dedicated "cache" system. It can be
    responsible for restoration of stuff, atomic state updates, etc.
    '''
    _enforcer = weak_property('__enforcer')
    _lawyer = weak_property('__lawyer')
    _percore = weak_property('__percore')
    
    @public_api
    def __init__(self, *args, memory_cache=10000, **kwargs):
        super().__init__(*args, **kwargs)
        
        # Lookup for ghid -> hypergolix description
        # This may be GC'd by the python process.
        self._catalog = FiniteDict(maxlen=memory_cache)
        
    @__init__.fixture
    def __init__(self, *args, **kwargs):
        ''' Construct an in-memory-only version of librarian.
        '''
        super(LibrarianCore.__fixture__, self).__init__(*args, **kwargs)
        
        # Lookup <ghid>: <raw data>
        self._shelf = {}
        
        # Lookup for dynamic ghid -> frame ghid
        self._dyn_resolver = {}
        
        # Lookup <bound ghid>: set(<binding obj>)
        self._bound_by_ghid = SetMap()
        
        # Lookup <debound ghid>: set(<debinding ghid>)
        self._debound_by_ghid = SetMap()
        
        # Lookup <recipient>: set(<request ghid>)
        self._requests_for_recipient = SetMap()
        
    @fixture_api
    def RESET(self):
        ''' Reset all of the librarian.
        '''
        self._catalog.clear()
        self._shelf.clear()
        self._dyn_resolver.clear()
        self._bound_by_ghid.clear_all()
        self._debound_by_ghid.clear_all()
        self._requests_for_recipient.clear_all()
        
    def assemble(self, enforcer, lawyer, percore):
        ''' Assign stuff hereto avoid circuitous problems.
        '''
        # We need to be able to check the legality of debindings whose targets
        # are uploaded after the debinding itself is.
        self._enforcer = enforcer
        self._lawyer = lawyer
        # And we need to be able to load things back up from the cache
        self._percore = percore
    
    @public_api
    async def is_bound(self, obj):
        ''' Check to see if the object has been bound.
        '''
        bindings = await self.bind_status(obj.ghid)
        return bool(bindings)
    
    @public_api
    async def is_debound(self, obj):
        # We need to validate any unvalidated bindings. For now, just always
        # validate debindings when an object is being checked.
        debindings = await self.debind_status(obj.ghid)
        invalidated = 0
        for debinding_ghid in debindings:
            # Get the existing debinding object
            debinding = await self.summarize(debinding_ghid)
            
            # Validate existing binding against newly-known target
            try:
                await self._enforcer.validate_gdxx(debinding, target_obj=obj)
                await self._lawyer.validate_gdxx(debinding, target_obj=obj)
                
            # Validation failed. Remove illegal debinding.
            except (InvalidTarget, InconsistentAuthor):
                logger.error(''.join((
                    'Removed invalid existing debinding.\n',
                    '    Debinding author:     ', str(debinding.author), '\n',
                    '    Valid object author:  ', str(obj.author), '\n',
                    '    Debinding target:     ', str(debinding.target), '\n',
                    '    Target type:          ', str(type(obj))
                )))
                # There's no need to do anything with the undertaker, because
                # debindings don't get subscribed to, and we (the librarian)
                # are the sole source of truth.
                await self.abandon(debinding)
                
                # Record that this was invalidated
                invalidated += 1
                
            # If we were differentiating between validated debindings and
            # provisional (unvalidated) debindings, this would be the place to
            # commit the provisional to the validated.
        
        # Instead of making another call to debind_status, we can calculate it
        # directly. NOTE! There is a race condition between the initial
        # validity check on upload, and the commit call.
        
        return bool(len(debindings) - invalidated)
        
    @is_debound.fixture
    async def is_debound(self, obj):
        ''' Create ad-hoc enforcers and lawyers when fixturing self.
        '''
        enforcer = Enforcer.__fixture__(librarian=self)
        lawyer = LawyerCore.__fixture__(librarian=self)
        self._enforcer = enforcer
        self._lawyer = lawyer
        result = await super(LibrarianCore.__fixture__, self).is_debound(obj)
        del enforcer
        del lawyer
        return result
    
    @public_api
    async def store(self, obj, data):
        ''' Starts tracking an object.
        obj is a hypergolix representation object.
        raw is bytes-like.
        '''
        if isinstance(obj, _GobdLite):
            reference_ghid = obj.frame_ghid
            # If we have an existing frame, get ITS frame ghid, so we can clear
            # it if the storage succeeds.
            try:
                old_ghid = await self.resolve_frame(obj.ghid)
            except KeyError:
                old_ghid = None
            
        else:
            reference_ghid = obj.ghid
            old_ghid = None
            
        await self.add_to_cache(obj, data)
        self._catalog[reference_ghid] = obj
        
        # If successful (which is any time we get to here), we also need to get
        # rid of any old dynamic frames and pop them from the catalog.
        if old_ghid is not None:
            await self.remove_from_cache(old_ghid)
            # We need the None regardless of bugs, in case the old frame is
            # "stale" enough to have been released from memory
            self._catalog.pop(old_ghid, None)
    
    @public_api
    async def retrieve(self, ghid):
        ''' Returns the raw data associated with the ghid, checking only
        locally.
        '''
        try:
            ghid = await self.resolve_frame(ghid)
        except KeyError:
            pass
            
        return (await self.get_from_cache(ghid))
    
    @public_api
    async def summarize(self, ghid):
        ''' Returns a lightweight Hypergolix description of the object.
        Checks only locally.
        '''
        try:
            ghid = await self.resolve_frame(ghid)
        except KeyError:
            pass
        
        try:
            obj = self._catalog[ghid]
        
        except KeyError:
            logger.debug('Attempting lazy-load for ' + str(ghid))
            # This will raise DoesNotExist if missing.
            data = await self.get_from_cache(ghid)
            # This does NOT ingest the data into the persistence system!
            obj = await self._percore.attempt_load(data, quiet=False)
            self._catalog[ghid] = obj
            
        return obj
        
    @public_api
    async def abandon(self, obj):
        ''' Forces erasure of an object without notifying anyone else.
        Idempotent. Should never raise KeyError.
        '''
        if isinstance(obj, _GobdLite):
            ghid = obj.frame_ghid
        else:
            ghid = obj.ghid
        
        await self.remove_from_cache(ghid)
        
        # Delete it from the catalog (if it exists there)
        self._catalog.pop(ghid, None)
    
    # Subclasses MAY define this, but are not required to do so.
    @fixture_api
    async def restore(self):
        ''' For LibrarianCore, do nothing. This is just here as an
        endpoint for restore calls. If subclasses want/need to support
        restoration, they should override this.
        '''
    
    # Subclasses MUST define this to work!
    # @abc.abstractmethod
    @fixture_api
    async def contains(self, ghid):
        ''' Checks the ghidcache for the ghid.
        '''
        try:
            ghid = await self.resolve_frame(ghid)
        except KeyError:
            pass
        
        # Catalog may only be accurate locally. Shelf is accurate globally.
        return ghid in self._shelf
    
    # Subclasses MUST define this to work!
    # @abc.abstractmethod
    @fixture_api
    async def resolve_frame(self, ghid):
        ''' Get the current frame ghid from the dynamic ghid.
        '''
        if not isinstance(ghid, Ghid):
            raise TypeError('Ghid must be a Ghid.')
            
        if ghid in self._dyn_resolver:
            return self._dyn_resolver[ghid]
        else:
            raise KeyError(str(ghid) + ' not known as dynamic ghid.')
    
    # Subclasses MUST define this to work!
    # @abc.abstractmethod
    @fixture_api
    async def recipient_status(self, ghid):
        ''' Return a frozenset of ghids assigned to the passed ghid as
        a recipient.
        '''
        return self._requests_for_recipient.get_any(ghid)
    
    # Subclasses MUST define this to work!
    # @abc.abstractmethod
    @fixture_api
    async def bind_status(self, ghid):
        ''' Return a frozenset of ghids binding the passed ghid.
        '''
        return self._bound_by_ghid.get_any(ghid)
    
    # Subclasses MUST define this to work!
    # @abc.abstractmethod
    @fixture_api
    async def debind_status(self, ghid):
        ''' Return either a ghid, or None.
        '''
        # Note that any particular object can have exactly zero or one VALID
        # debinds, but that a malicious actor could find a race condition and
        # debind something FOR SOMEONE ELSE before the bookie knows about the
        # original object authorship.
        return self._debound_by_ghid.get_any(ghid)
            
    # Subclasses MUST define this to work!
    # @abc.abstractmethod
    @fixture_api
    async def add_to_cache(self, obj, data):
        ''' Adds the passed raw data to the cache.
        '''
        if isinstance(obj, _GobsLite):
            reference_ghid = obj.ghid
            self._bound_by_ghid.add(obj.target, obj.ghid)
            
        elif isinstance(obj, _GobdLite):
            reference_ghid = obj.frame_ghid
            try:
                existing = await self.summarize(obj.ghid)
            except KeyError:
                pass
            else:
                self._bound_by_ghid.remove(existing.target, obj.ghid)
                
            # Now we have a clean slate and need to update things accordingly.
            self._bound_by_ghid.add(obj.target, obj.ghid)
            self._dyn_resolver[obj.ghid] = obj.frame_ghid
                
        elif isinstance(obj, _GdxxLite):
            reference_ghid = obj.ghid
            self._debound_by_ghid.add(obj.target, obj.ghid)
            
        elif isinstance(obj, _GarqLite):
            reference_ghid = obj.ghid
            # NOTE: this is only necessary for a persistence server
            self._requests_for_recipient.add(obj.recipient, obj.ghid)
            
        else:
            reference_ghid = obj.ghid
            
        self._shelf[reference_ghid] = data
        
    # Subclasses MUST define this to work!
    # @abc.abstractmethod
    @fixture_api
    async def remove_from_cache(self, ghid):
        ''' Removes the data associated with the passed ghid from the
        cache.
        '''
        obj = await self.summarize(ghid)
        
        if isinstance(obj, _GobsLite):
            reference_ghid = obj.ghid
            self._bound_by_ghid.discard(obj.target, obj.ghid)
            
        elif isinstance(obj, _GobdLite):
            reference_ghid = obj.frame_ghid
            self._bound_by_ghid.discard(obj.target, obj.ghid)
            
            if self._dyn_resolver.get(obj.ghid, None) == obj.frame_ghid:
                self._dyn_resolver.pop(obj.ghid, None)
            
        elif isinstance(obj, _GdxxLite):
            reference_ghid = obj.ghid
            self._debound_by_ghid.discard(obj.target, obj.ghid)
            
        elif isinstance(obj, _GarqLite):
            reference_ghid = obj.ghid
            self._requests_for_recipient.discard(obj.recipient, obj.ghid)
            
        else:
            reference_ghid = obj.ghid
        
        try:
            self._shelf.pop(reference_ghid)
        except KeyError as exc:
            # Protect the full GHID from accidental exposure
            raise DoesNotExist(str(ghid)) from None
        
    # Subclasses MUST define this to work!
    # @abc.abstractmethod
    @fixture_api
    async def get_from_cache(self, ghid):
        ''' Returns the raw data associated with the ghid.
        '''
        try:
            return self._shelf[ghid]
        except KeyError as exc:
            # Protect the full GHID from accidental exposure
            raise DoesNotExist(str(ghid)) from None class _GidcLite(_BaseLite):
    ''' Lightweight description of a GIDC.
    '''
    __slots__ = [
        'identity'
    ]
    
    def __init__(self, ghid, identity):
        self.ghid = ghid
        self.identity = identity
        
    @classmethod
    def from_golix(cls, golix_obj):
        ''' Convert the golix object to a lightweight representation.
        '''
        identity = SecondParty.from_identity(golix_obj)
        return cls(
            ghid = golix_obj.ghid,
            identity = identity,
        ) class _GeocLite(_BaseLite):
    ''' Lightweight description of a GEOC.
    '''
    __slots__ = [
        'author',
    ]
    
    def __init__(self, ghid, author):
        self.ghid = ghid
        self.author = author
        
    def __eq__(self, other):
        try:
            return (
                super().__eq__(other) and
                self.author == other.author
            )
        # This will not catch a super() TyperError, so we want to be able to
        # compare anything with a ghid. In reality, any situation where the
        # authors don't match but the ghids do is almost certainly a bug; but,
        # compare it anyways just in case.
        except AttributeError as exc:
            return False
        
    @classmethod
    def from_golix(cls, golix_obj):
        ''' Convert the golix object to a lightweight representation.
        '''
        return cls(
            ghid = golix_obj.ghid,
            author = golix_obj.author,
        ) class _GobdLite(_BaseLite):
    ''' Lightweight description of a GOBD.
    '''
    __slots__ = [
        'author',
        'counter',
        'target_vector',
        'frame_ghid'
    ]
    
    def __init__(self, ghid, author, counter, target_vector, frame_ghid):
        self.ghid = ghid
        self.author = author
        self.counter = counter
        self.target_vector = target_vector
        self.frame_ghid = frame_ghid
        
    def __eq__(self, other):
        try:
            return (
                super().__eq__(other) and
                self.author == other.author and
                self.target == other.target and
                self.frame_ghid == other.frame_ghid
                # Skip history, because it could potentially vary
                # self.history == other.history
            )
            
        # This will not catch a super() TyperError, so we want to be able to
        # compare anything with a ghid. In reality, any situation where the
        # authors don't match but the ghids do is almost certainly a bug; but,
        # compare it anyways just in case.
        except AttributeError as exc:
            return False
        
    @classmethod
    def from_golix(cls, golix_obj):
        ''' Convert the golix object to a lightweight representation.
        '''
        return cls(
            ghid = golix_obj.ghid_dynamic,
            author = golix_obj.binder,
            counter = golix_obj.counter,
            target_vector = golix_obj.target_vector,
            frame_ghid = golix_obj.ghid
        )
        
    @property
    def target(self):
        ''' Access the current target.
        '''
        return self.target_vector[0]
import unittest
import concurrent.futures
from loopa import NoopLoop
from loopa.utils import await_coroutine_threadsafe
from hypergolix.accounting import Account
from hypergolix.core import GolixCore
from hypergolix.core import Oracle
from hypergolix.core import GhidProxier
from hypergolix.gao import GAOCore
from hypergolix.privateer import Privateer
from hypergolix.remotes import Salmonator
from hypergolix.persistence import PersistenceCore
from hypergolix.librarian import LibrarianCore
from hypergolix.persistence import _GidcLite
from hypergolix.persistence import _GeocLite
from hypergolix.persistence import _GobdLite
from golix import Ghid
from _fixtures.identities import TEST_AGENT1
from _fixtures.identities import TEST_AGENT2
from _fixtures.ghidutils import make_random_ghid
        from _fixtures.remote_exchanges import handshake2_1
        from _fixtures.remote_exchanges import secret2_1
        from _fixtures.remote_exchanges import cont2_1
        from _fixtures.remote_exchanges import cont1_1
        from _fixtures.remote_exchanges import cont1_2
        from _fixtures.remote_exchanges import dyn1_1a
        from _fixtures.remote_exchanges import dyn1_1b
        from _fixtures.remote_exchanges import cont2_1
        from _fixtures.remote_exchanges import cont2_2
        from _fixtures.remote_exchanges import dyn2_1a
        from _fixtures.remote_exchanges import dyn2_1b
    from hypergolix import logutils
        
        # Test round #2...
        geoc2_1 = _GeocLite(cont2_1.ghid, cont2_1.author)
        geoc2_2 = _GeocLite(cont2_2.ghid, cont2_2.author)
        gobd2_a = _GobdLite.from_golix(dyn2_1a)
        gobd2_b = _GobdLite.from_golix(dyn2_1b)
        
        # Hold on to your butts!
        await_coroutine_threadsafe(
            coro = self.librarian.store(geoc2_1, cont2_1.packed),
            loop = self.nooploop._loop
        )
        await_coroutine_threadsafe(
            coro = self.librarian.store(geoc2_2, cont2_2.packed),
            loop = self.nooploop._loop
        )
        
        await_coroutine_threadsafe(
            coro = self.librarian.store(gobd2_a, dyn2_1a.packed),
            loop = self.nooploop._loop
        )
        self.assertEqual(
            await_coroutine_threadsafe(
                coro = self.ghidproxy.resolve(dyn2_1a.ghid_dynamic),
                loop = self.nooploop._loop
            ),
            cont2_1.ghid
        )
        
        await_coroutine_threadsafe(
            coro = self.librarian.store(gobd2_b, dyn2_1b.packed),
            loop = self.nooploop._loop
        )
        # Intentionally reuse old dynamic ghid, even though it's the same
        self.assertEqual(
            await_coroutine_threadsafe(
                coro = self.ghidproxy.resolve(dyn2_1a.ghid_dynamic),
                loop = self.nooploop._loop
            ),
            cont2_2.ghid
        )

        
class OracleTest(unittest.TestCase):
    
    @classmethod
    def setUpClass(cls):
        cls.nooploop = NoopLoop(
            debug = True,
            threaded = True
        )
        cls.nooploop.start()
        
    @classmethod
    def tearDownClass(cls):
        # Kill the running loop.
        cls.nooploop.stop_threadsafe_nowait()
    
    def setUp(self):
        # This is what we're testing!
        self.oracle = Oracle()
        
        # These are directly required by the oracle
        self.golcore = GolixCore.__fixture__(TEST_AGENT1)
        self.ghidproxy = GhidProxier()
        self.privateer = Privateer.__fixture__(TEST_AGENT1)
        self.percore = PersistenceCore.__fixture__()
        self.librarian = LibrarianCore.__fixture__()
        self.salmonator = Salmonator.__fixture__()
        
        # This is obviously necessary to test.
        self.oracle.assemble(self.golcore, self.ghidproxy, self.privateer,
                             self.percore, self.librarian, self.salmonator)
    
    def test_load(self):
        ''' Test getting an existing object.
        '''
        # Test actual object getting
        ghid = make_random_ghid()
        obj = await_coroutine_threadsafe(
            coro = self.oracle.get_object(
                gaoclass = GAOCore.__fixture__,
                ghid = ghid
            ),
            loop = self.nooploop._loop
        )
        self.assertEqual(obj.ghid, ghid)
        
        # Now make sure that running it a second time returns the cached (and
        # identical) object
        obj2 = await_coroutine_threadsafe(
            coro = self.oracle.get_object(
                gaoclass = GAOCore.__fixture__,
                ghid = ghid
            ),
            loop = self.nooploop._loop
        )
        self.assertTrue(obj is obj2)
    
    def test_new(self):
        ''' Test creating new objects.
        '''
        # Test actual object creation
        obj = await_coroutine_threadsafe(
            coro = self.oracle.new_object(
                gaoclass = GAOCore.__fixture__,
                dynamic = True,
                legroom = 7
            ),
            loop = self.nooploop._loop
        )
        # Make sure that oracle calls _push() to create a ghid, etc
        self.assertTrue(obj.ghid)
        self.assertTrue(obj.dynamic)
        self.assertTrue(obj.legroom == 7)
        
        # Now make sure that running get after that returns the cached (and
        # identical) object
        obj2 = await_coroutine_threadsafe(
            coro = self.oracle.get_object(

                gaoclass = GAOCore.__fixture__,