diff --git a/aries_cloudagent/anoncreds/revocation.py b/aries_cloudagent/anoncreds/revocation.py index b45d09232a..1994256753 100644 --- a/aries_cloudagent/anoncreds/revocation.py +++ b/aries_cloudagent/anoncreds/revocation.py @@ -3,6 +3,7 @@ import asyncio import hashlib import http +import json import logging import os from pathlib import Path @@ -21,6 +22,7 @@ import base58 from requests import RequestException, Session + from ..askar.profile import AskarProfile, AskarProfileSession from ..core.error import BaseError from ..core.profile import Profile @@ -47,6 +49,9 @@ CATEGORY_REV_REG_DEF = "revocation_reg_def" CATEGORY_REV_REG_DEF_PRIVATE = "revocation_reg_def_private" CATEGORY_REV_REG_ISSUER = "revocation_reg_def_issuer" +STATE_REVOCATION_POSTED = "posted" +STATE_REVOCATION_PENDING = "pending" +REV_REG_DEF_STATE_ACTIVE = "active" class AnonCredsRevocationError(BaseError): @@ -87,7 +92,13 @@ def profile(self) -> AskarProfile: # Revocation artifact management async def _finish_registration( - self, txn: AskarProfileSession, category: str, job_id: str, registered_id: str + self, + txn: AskarProfileSession, + category: str, + job_id: str, + registered_id: str, + *, + state: Optional[str] = None, ): entry = await txn.handle.fetch( category, @@ -99,8 +110,12 @@ async def _finish_registration( f"{category} with job id {job_id} could not be found" ) - tags = entry.tags - tags["state"] = STATE_FINISHED + if state: + tags = entry.tags + tags["state"] = state + else: + tags = entry.tags + await txn.handle.insert( category, registered_id, @@ -178,25 +193,33 @@ async def create_and_register_revocation_registry_definition( self.profile, rev_reg_def, options ) - rev_reg_def_id = result.rev_reg_def_id - rev_reg_def_json = rev_reg_def.to_json() + ident = result.rev_reg_def_id or result.job_id + if not ident: + raise AnonCredsRevocationError( + "Revocation registry definition id or job id not found" + ) + + # TODO Handle `failed` state try: async with self.profile.transaction() as txn: await txn.handle.insert( - CATEGORY_REV_REG_INFO, - rev_reg_def_id, - value_json={"curr_id": 0, "used_ids": []}, + CATEGORY_REV_REG_DEF, + ident, + rev_reg_def.to_json(), + tags={ + "cred_def_id": cred_def_id, + "state": result.revocation_registry_definition_state.state, + }, ) await txn.handle.insert( - CATEGORY_REV_REG_DEF, - rev_reg_def_id, - rev_reg_def_json, - tags={"cred_def_id": cred_def_id}, + CATEGORY_REV_REG_INFO, + ident, + value_json={"curr_id": 0, "used_ids": []}, ) await txn.handle.insert( CATEGORY_REV_REG_DEF_PRIVATE, - rev_reg_def_id, + ident, rev_reg_def_private.to_json_buffer(), ) await txn.commit() @@ -213,7 +236,7 @@ async def finish_revocation_registry_definition( """Mark a rev reg def as finished.""" async with self.profile.transaction() as txn: await self._finish_registration( - txn, CATEGORY_REV_REG_DEF, job_id, rev_reg_def_id + txn, CATEGORY_REV_REG_DEF, job_id, rev_reg_def_id, state=STATE_FINISHED ) await self._finish_registration( txn, @@ -267,6 +290,61 @@ async def get_created_revocation_registry_definition( return None + async def set_active_registry(self, cred_def_id: str, rev_reg_def_id: str): + """Mark a registry as active.""" + async with self.profile.transaction() as txn: + entry = await txn.handle.fetch( + CATEGORY_REV_REG_DEF, + rev_reg_def_id, + for_update=True, + ) + if not entry: + raise AnonCredsRevocationError( + f"{CATEGORY_REV_REG_DEF} with id " + f"{rev_reg_def_id} could not be found" + ) + + if entry.tags["active"] == json.dumps(True): + # NOTE If there are other registries set as active, we're not + # clearing them if the one we want to be active is already + # active. This probably isn't an issue. + return + + old_active_entries = await txn.handle.fetch_all( + CATEGORY_REV_REG_DEF, + { + "active": json.dumps(True), + "cred_def_id": cred_def_id, + }, + for_update=True, + ) + + if len(old_active_entries) > 1: + LOGGER.error( + "More than one registry was set as active for " + f"cred def {cred_def_id}; clearing active tag from all records" + ) + + for old_entry in old_active_entries: + tags = old_entry.tags + tags["active"] = json.dumps(False) + await txn.handle.replace( + CATEGORY_REV_REG_DEF, + old_entry.name, + old_entry.value, + tags, + ) + + tags = entry.tags + tags["active"] = json.dumps(True) + await txn.handle.insert( + CATEGORY_REV_REG_DEF, + rev_reg_def_id, + value=entry.value, + tags=tags, + ) + await txn.commit() + async def create_and_register_revocation_list( self, rev_reg_def_id: str, options: Optional[dict] = None ): @@ -301,12 +379,19 @@ async def create_and_register_revocation_list( self.profile, rev_reg_def, RevList.from_native(rev_list), options ) + # TODO Handle `failed` state + + posted = result.revocation_list_state.revocation_list try: async with self.profile.session() as session: await session.handle.insert( CATEGORY_REV_LIST, rev_reg_def_id, - result.revocation_list_state.revocation_list.to_json(), + value_json={ + "posted": posted.serialize(), + "pending": None, + }, + tags={"state": result.revocation_list_state.state}, ) except AskarError as err: raise AnonCredsRevocationError( @@ -315,6 +400,90 @@ async def create_and_register_revocation_list( return result + async def finish_revocation_list(self, rev_reg_def_id: str): + """Mark a revocation list as finished.""" + async with self.profile.transaction() as txn: + entry = await txn.handle.fetch( + CATEGORY_REV_LIST, + rev_reg_def_id, + for_update=True, + ) + if not entry: + raise AnonCredsRevocationError( + f"revocation list with id {rev_reg_def_id} could not be found" + ) + + await txn.handle.replace( + CATEGORY_REV_LIST, + rev_reg_def_id, + value=entry.value, + tags={"state": STATE_FINISHED}, + ) + await txn.commit() + + async def mark_pending_revocations(self, rev_reg_def_id: str, *crids: int): + async with self.profile.transaction() as txn: + entry = await txn.handle.fetch( + CATEGORY_REV_REG_DEF, + rev_reg_def_id, + ) + if not entry: + raise AnonCredsRevocationError( + "Revocation registry definition not found for id {rev_reg_def_id}" + ) + rev_reg_def = RevocationRegistryDefinition.load(entry.value) + + entry = await txn.handle.fetch( + CATEGORY_REV_LIST, + rev_reg_def_id, + for_update=True, + ) + + if not entry: + raise AnonCredsRevocationError( + "Revocation list with id {rev_reg_def_id} not found" + ) + + posted = RevocationStatusList.load(entry.value_json["posted"]) + if entry.value_json["pending"]: + pending = RevocationStatusList.load(entry.value_json["pending"]) + else: + pending = posted + + pending = pending.update( + timestamp=None, issued=None, revoked=crids, rev_reg_def=rev_reg_def + ) + + await txn.handle.replace( + CATEGORY_REV_LIST, + rev_reg_def_id, + value_json={ + "posted": posted.to_dict(), + "pending": pending.to_dict(), + }, + ) + await txn.commit() + + async def clear_pending_revocations(self, rev_reg_def_id: str): + async with self.profile.transaction() as txn: + entry = await txn.handle.fetch( + CATEGORY_REV_LIST, + rev_reg_def_id, + for_update=True, + ) + + if not entry: + raise AnonCredsRevocationError( + "Revocation list with id {rev_reg_def_id} not found" + ) + + await txn.handle.replace( + CATEGORY_REV_LIST, + rev_reg_def_id, + value_json={"posted": entry.value_json["posted"], "pending": None}, + ) + await txn.commit() + async def retrieve_tails(self, rev_reg_def: RevRegDef) -> str: """Retrieve tails file from server.""" LOGGER.info( @@ -433,14 +602,15 @@ async def get_or_create_active_registry(self, cred_def_id: str) -> RevRegDefResu CATEGORY_REV_REG_DEF, { "cred_def_id": cred_def_id, + "active": json.dumps(True), }, + limit=1, ) if not rev_reg_defs: + # TODO Create a registry if none available raise AnonCredsRevocationError("No active registry") - # TODO Sort and return most recent - # TODO Create a registry if none available entry = rev_reg_defs[0] rev_reg_def = RevRegDef.deserialize(entry.value_json) diff --git a/aries_cloudagent/indy/credx/issuer.py b/aries_cloudagent/indy/credx/issuer.py index b4a4524948..27aa853695 100644 --- a/aries_cloudagent/indy/credx/issuer.py +++ b/aries_cloudagent/indy/credx/issuer.py @@ -603,7 +603,9 @@ async def create_and_store_revocation_registry( value_json={"curr_id": 0, "used_ids": []}, ) await txn.handle.insert( - CATEGORY_REV_REG_DEF, rev_reg_def_id, rev_reg_def_json + CATEGORY_REV_REG_DEF, + rev_reg_def_id, + rev_reg_def_json, ) await txn.handle.insert( CATEGORY_REV_REG_DEF_PRIVATE,