From f8175657d3f790ebd0e3de59e7a0d387b904f0d0 Mon Sep 17 00:00:00 2001 From: Andrew Whitehead Date: Wed, 15 Jun 2022 16:47:28 -0700 Subject: [PATCH 1/5] initial updates to revocation creation procedure Signed-off-by: Andrew Whitehead --- aries_cloudagent/indy/sdk/tests/test_util.py | 23 +- aries_cloudagent/indy/util.py | 16 +- aries_cloudagent/ledger/base.py | 4 +- aries_cloudagent/ledger/indy.py | 4 +- aries_cloudagent/ledger/indy_vdr.py | 4 +- .../credential_definitions/routes.py | 11 +- .../tests/test_routes.py | 1 - .../endorse_transaction/v1_0/manager.py | 59 ++-- .../issue_credential/v1_0/manager.py | 186 ++++-------- .../v2_0/formats/indy/handler.py | 158 +++------- aries_cloudagent/revocation/indy.py | 89 +++++- .../models/issuer_rev_reg_record.py | 64 +++- .../tests/test_issuer_rev_reg_record.py | 2 +- aries_cloudagent/revocation/routes.py | 275 ++++++++---------- aries_cloudagent/revocation/util.py | 63 ++-- 15 files changed, 411 insertions(+), 548 deletions(-) diff --git a/aries_cloudagent/indy/sdk/tests/test_util.py b/aries_cloudagent/indy/sdk/tests/test_util.py index cb6a55ea50..be3f5ee36b 100644 --- a/aries_cloudagent/indy/sdk/tests/test_util.py +++ b/aries_cloudagent/indy/sdk/tests/test_util.py @@ -1,20 +1,15 @@ import pytest -from os import makedirs -from os.path import join -from pathlib import Path from shutil import rmtree import indy.blob_storage from asynctest import mock as async_mock, TestCase as AsyncTestCase -from ...util import indy_client_dir, generate_pr_nonce, tails_path +from ...util import indy_client_dir, generate_pr_nonce from ..util import create_tails_reader, create_tails_writer -from .. import util as test_module - @pytest.mark.indy class TestIndyUtils(AsyncTestCase): @@ -49,19 +44,3 @@ async def test_tails_writer(self): async def test_nonce(self): assert await generate_pr_nonce() - - async def test_tails_path(self): - tails_dir = indy_client_dir("tails", create=False) - rmtree(tails_dir, ignore_errors=True) - - tails_local_path = tails_path("rev-reg-id") - assert tails_local_path is None - - tails_rr_dir = indy_client_dir(join("tails", "rev-reg-id"), create=True) - tails_local_path = tails_path("rev-reg-id") - assert tails_local_path is None - - with open(join(tails_rr_dir, "tails-hash"), "w") as f: - f.write("content") - tails_local_path = tails_path("rev-reg-id") - assert tails_local_path diff --git a/aries_cloudagent/indy/util.py b/aries_cloudagent/indy/util.py index bb44f446b0..2c9a126c46 100644 --- a/aries_cloudagent/indy/util.py +++ b/aries_cloudagent/indy/util.py @@ -1,6 +1,6 @@ """Utilities for dealing with Indy conventions.""" -from os import getenv, listdir, makedirs, urandom +from os import getenv, makedirs, urandom from os.path import isdir, join from pathlib import Path from platform import system @@ -37,17 +37,3 @@ def indy_client_dir(subpath: str = None, create: bool = False) -> str: makedirs(target_dir, exist_ok=True) return target_dir - - -def tails_path(rev_reg_id: str) -> str: - """Return path to indy tails file for input rev reg id.""" - - tails_dir = indy_client_dir(join("tails", rev_reg_id), create=False) - if not isdir(tails_dir): - return None - - content = listdir(tails_dir) - if len(content) != 1: - return None - - return join(tails_dir, content[0]) diff --git a/aries_cloudagent/ledger/base.py b/aries_cloudagent/ledger/base.py index 451f0ffc41..bcbb8c54e1 100644 --- a/aries_cloudagent/ledger/base.py +++ b/aries_cloudagent/ledger/base.py @@ -352,7 +352,7 @@ async def send_revoc_reg_def( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry definition to the ledger.""" @abstractmethod @@ -364,7 +364,7 @@ async def send_revoc_reg_entry( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry entry to the ledger.""" async def create_and_send_credential_definition( diff --git a/aries_cloudagent/ledger/indy.py b/aries_cloudagent/ledger/indy.py index 36aaef00c7..3ac2b19363 100644 --- a/aries_cloudagent/ledger/indy.py +++ b/aries_cloudagent/ledger/indy.py @@ -1131,7 +1131,7 @@ async def send_revoc_reg_def( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry definition to the ledger.""" # NOTE - issuer DID could be extracted from the revoc_reg_def ID if issuer_did: @@ -1167,7 +1167,7 @@ async def send_revoc_reg_entry( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry entry to the ledger.""" if issuer_did: async with self.profile.session() as session: diff --git a/aries_cloudagent/ledger/indy_vdr.py b/aries_cloudagent/ledger/indy_vdr.py index cbc350d748..4dc98e75ba 100644 --- a/aries_cloudagent/ledger/indy_vdr.py +++ b/aries_cloudagent/ledger/indy_vdr.py @@ -1068,7 +1068,7 @@ async def send_revoc_reg_def( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry definition to the ledger.""" # NOTE - issuer DID could be extracted from the revoc_reg_def ID async with self.profile.session() as session: @@ -1105,7 +1105,7 @@ async def send_revoc_reg_entry( issuer_did: str = None, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Publish a revocation registry entry to the ledger.""" async with self.profile.session() as session: wallet = session.inject(BaseWallet) diff --git a/aries_cloudagent/messaging/credential_definitions/routes.py b/aries_cloudagent/messaging/credential_definitions/routes.py index 82ffa09fab..5724b8a979 100644 --- a/aries_cloudagent/messaging/credential_definitions/routes.py +++ b/aries_cloudagent/messaging/credential_definitions/routes.py @@ -41,7 +41,7 @@ get_endorser_connection_id, ) -from ...revocation.util import notify_revocation_reg_event +from ...revocation.indy import IndyRevocation from ...storage.base import BaseStorage, StorageRecord from ...storage.error import StorageError @@ -514,16 +514,15 @@ async def on_cred_def_event(profile: Profile, event: Event): if support_revocation and novel and auto_create_rev_reg: # this kicks off the revocation registry creation process, which is 3 steps: # 1 - create revocation registry (ledger transaction may require endorsement) - # 2 - create revocation entry (ledger transaction may require endorsement) - # 3 - upload tails file + # 2 - upload tails file + # 3 - create revocation entry (ledger transaction may require endorsement) # For a cred def we also automatically create a second "pending" revocation # registry, so when the first one fills up we can continue to issue credentials # without a delay - await notify_revocation_reg_event( - profile, + revoc = IndyRevocation(profile) + await revoc.init_issuer_registry( cred_def_id, rev_reg_size, - auto_create_rev_reg=auto_create_rev_reg, create_pending_rev_reg=create_pending_rev_reg, endorser_connection_id=endorser_connection_id, ) diff --git a/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py b/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py index 88164b437a..24b5a41bda 100644 --- a/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py +++ b/aries_cloudagent/messaging/credential_definitions/tests/test_routes.py @@ -11,7 +11,6 @@ from ....multitenant.base import BaseMultitenantManager from ....multitenant.manager import MultitenantManager from ....storage.base import BaseStorage -from ....tails.base import BaseTailsServer from .. import routes as test_module from ....connections.models.conn_record import ConnRecord diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py index 4c8b7d0d21..da95091be4 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py @@ -16,7 +16,7 @@ from ....messaging.schemas.util import notify_schema_event from ....revocation.util import ( notify_revocation_entry_event, - notify_revocation_tails_file_event, + notify_revocation_reg_endorsed_event, ) from ....storage.error import StorageError, StorageNotFoundError from ....transport.inbound.receipt import MessageReceipt @@ -255,13 +255,12 @@ async def create_endorse_response( endorser_did = endorser_did_info.did endorser_verkey = endorser_did_info.verkey - async with self._profile.session() as session: - ledger = session.context.inject_or(BaseLedger) - if not ledger: - reason = "No ledger available" - if not session.context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" - raise LedgerError(reason=reason) + ledger = self._profile.context.inject_or(BaseLedger) + if not ledger: + reason = "No ledger available" + if not self._profile.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise LedgerError(reason=reason) async with ledger: endorsed_msg = await shield( @@ -371,23 +370,20 @@ async def complete_transaction(self, transaction: TransactionRecord): """ ledger_transaction = transaction.messages_attach[0]["data"]["json"] - async with self._profile.session() as session: - ledger = self._profile.inject(BaseLedger) - if not ledger: - reason = "No ledger available" - if not session.context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" - raise TransactionManagerError(reason) + ledger = self._profile.inject(BaseLedger) + if not ledger: + reason = "No ledger available" + if not self._profile.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise TransactionManagerError(reason) - async with ledger: - try: - ledger_response_json = await shield( - ledger.txn_submit( - ledger_transaction, sign=False, taa_accept=False - ) - ) - except (IndyIssuerError, LedgerError) as err: - raise TransactionManagerError(err.roll_up) from err + async with ledger: + try: + ledger_response_json = await shield( + ledger.txn_submit(ledger_transaction, sign=False, taa_accept=False) + ) + except (IndyIssuerError, LedgerError) as err: + raise TransactionManagerError(err.roll_up) from err ledger_response = json.loads(ledger_response_json) @@ -734,13 +730,12 @@ async def endorsed_txn_post_processing( would be stored in wallet. """ - async with self._profile.session() as session: - ledger = self._profile.inject(BaseLedger) - if not ledger: - reason = "No ledger available" - if not session.context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" - raise TransactionManagerError(reason) + ledger = self._profile.inject(BaseLedger) + if not ledger: + reason = "No ledger available" + if not self._profile.context.settings.get_value("wallet.type"): + reason += ": missing wallet-type?" + raise TransactionManagerError(reason) # setup meta_data to pass to future events, if necessary meta_data = transaction.meta_data @@ -802,7 +797,7 @@ async def endorsed_txn_post_processing( # If "auto_processing" is enabled, also upload tails file for this registry if auto_create_rev_reg: - await notify_revocation_tails_file_event( + await notify_revocation_reg_endorsed_event( self._profile, rev_reg_id, meta_data ) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py index 7aba66c330..88ceb3396d 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py @@ -24,8 +24,6 @@ from ....multitenant.base import BaseMultitenantManager from ....revocation.indy import IndyRevocation from ....revocation.models.revocation_registry import RevocationRegistry -from ....revocation.models.issuer_rev_reg_record import IssuerRevRegRecord -from ....revocation.util import notify_revocation_reg_event from ....storage.base import BaseStorage from ....storage.error import StorageError, StorageNotFoundError from ....connections.models.conn_record import ConnRecord @@ -593,13 +591,25 @@ async def issue_credential( ) credential_ser = cred_ex_record._credential.ser - elif cred_ex_record.state == V10CredentialExchange.STATE_REQUEST_RECEIVED: - rev_reg = None - rev_reg_id = None - cred_rev_id = None + elif cred_ex_record.state != V10CredentialExchange.STATE_REQUEST_RECEIVED: + raise CredentialManagerError( + f"Credential exchange {cred_ex_record.credential_exchange_id} " + f"in {cred_ex_record.state} state " + f"(must be {V10CredentialExchange.STATE_REQUEST_RECEIVED})" + ) + + else: cred_offer_ser = cred_ex_record._credential_offer.ser cred_req_ser = cred_ex_record._credential_request.ser + cred_values = ( + cred_ex_record.credential_proposal_dict.credential_proposal.attr_dict( + decode=False + ) + ) schema_id = cred_ex_record.schema_id + cred_def_id = cred_ex_record.credential_definition_id + + issuer = self.profile.inject(IndyIssuer) multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) if multitenant_mgr: ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) @@ -616,127 +626,59 @@ async def issue_credential( credential_definition = await ledger.get_credential_definition( cred_ex_record.credential_definition_id ) + revocable = credential_definition["value"].get("revocation") - tails_path = None - if credential_definition["value"].get("revocation"): - revoc = IndyRevocation(self._profile) - try: - active_rev_reg_rec = await revoc.get_active_issuer_rev_reg_record( - cred_ex_record.credential_definition_id - ) - rev_reg = await active_rev_reg_rec.get_registry() - rev_reg_id = rev_reg.registry_id - tails_path = rev_reg.tails_local_path - await rev_reg.get_or_fetch_local_tails_path() - - except StorageNotFoundError: - async with self._profile.session() as session: - posted_rev_reg_recs = ( - await IssuerRevRegRecord.query_by_cred_def_id( - session, - cred_ex_record.credential_definition_id, - state=IssuerRevRegRecord.STATE_POSTED, - ) - ) - if not posted_rev_reg_recs: - # Send next 2 rev regs, publish tails files in background - async with self._profile.session() as session: - old_rev_reg_recs = sorted( - await IssuerRevRegRecord.query_by_cred_def_id( - session, - cred_ex_record.credential_definition_id, - ) - ) # prefer to reuse prior rev reg size - cred_def_id = cred_ex_record.credential_definition_id - rev_reg_size = ( - old_rev_reg_recs[0].max_cred_num - if old_rev_reg_recs - else None - ) - for _ in range(2): - await notify_revocation_reg_event( - self.profile, - cred_def_id, - rev_reg_size, - auto_create_rev_reg=True, - ) - - if retries > 0: - LOGGER.info( - "Waiting 2s on posted rev reg for cred def %s, retrying", - cred_ex_record.credential_definition_id, - ) - await asyncio.sleep(2) - return await self.issue_credential( - cred_ex_record=cred_ex_record, - comment=comment, - retries=retries - 1, - ) - - raise CredentialManagerError( - f"Cred def id {cred_ex_record.credential_definition_id} " - "has no active revocation registry" - ) from None - del revoc - - credential_values = ( - cred_ex_record.credential_proposal_dict.credential_proposal.attr_dict( - decode=False - ) - ) - issuer = self._profile.inject(IndyIssuer) - try: - (credential_json, cred_rev_id) = await issuer.create_credential( - schema, - cred_offer_ser, - cred_req_ser, - credential_values, - cred_ex_record.credential_exchange_id, - rev_reg_id, - tails_path, - ) - credential_ser = json.loads(credential_json) - - # If the rev reg is now full - if rev_reg and rev_reg.max_creds == int(cred_rev_id): - async with self._profile.session() as session: - await active_rev_reg_rec.set_state( - session, - IssuerRevRegRecord.STATE_FULL, - ) - - # Send next 1 rev reg, publish tails file in background - cred_def_id = cred_ex_record.credential_definition_id - rev_reg_size = active_rev_reg_rec.max_cred_num - await notify_revocation_reg_event( - self.profile, + for attempt in range(max(retries, 1)): + if attempt > 0: + LOGGER.info( + "Waiting 2s before retrying credential issuance " + "for cred def '%s'", cred_def_id, - rev_reg_size, - auto_create_rev_reg=True, ) + await asyncio.sleep(2) - except IndyIssuerRevocationRegistryFullError: - # unlucky: duelling instance issued last cred near same time as us - async with self._profile.session() as session: - await active_rev_reg_rec.set_state( - session, - IssuerRevRegRecord.STATE_FULL, + if revocable: + revoc = IndyRevocation(self.profile) + registry_info = await revoc.get_or_create_active_registry( + cred_def_id ) + if not registry_info: + continue + del revoc + issuer_rev_reg, rev_reg = registry_info + rev_reg_id = issuer_rev_reg.revoc_reg_id + tails_path = rev_reg.tails_local_path + else: + rev_reg_id = None + tails_path = None - if retries > 0: - # use next rev reg; at worst, lucky instance is putting one up - LOGGER.info( - "Waiting 1s and retrying: revocation registry %s is full", - active_rev_reg_rec.revoc_reg_id, - ) - await asyncio.sleep(1) - return await self.issue_credential( - cred_ex_record=cred_ex_record, - comment=comment, - retries=retries - 1, + try: + (credential_json, cred_rev_id) = await issuer.create_credential( + schema, + cred_offer_ser, + cred_req_ser, + cred_values, + cred_ex_record.credential_exchange_id, + rev_reg_id, + tails_path, ) + except IndyIssuerRevocationRegistryFullError: + # unlucky, another instance filled the registry first + continue + + if rev_reg and rev_reg.max_creds <= int(cred_rev_id): + revoc = IndyRevocation(self.profile) + await revoc.handle_full_registry(rev_reg_id) + del revoc + + credential_ser = json.loads(credential_json) + break - raise + if not credential_ser: + raise CredentialManagerError( + f"Cred def id {cred_ex_record.credential_definition_id} " + "has no active revocation registry" + ) from None async with self._profile.transaction() as txn: cred_ex_record = await V10CredentialExchange.retrieve_by_id( @@ -754,12 +696,6 @@ async def issue_credential( cred_ex_record.revocation_id = cred_rev_id await cred_ex_record.save(txn, reason="issue credential") await txn.commit() - else: - raise CredentialManagerError( - f"Credential exchange {cred_ex_record.credential_exchange_id} " - f"in {cred_ex_record.state} state " - f"(must be {V10CredentialExchange.STATE_REQUEST_RECEIVED})" - ) credential_message = CredentialIssue( comment=comment, diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py index 3561687065..c6f4c62cdf 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py @@ -25,13 +25,9 @@ ) from ......messaging.decorators.attach_decorator import AttachDecorator from ......multitenant.base import BaseMultitenantManager -from ......revocation.models.issuer_rev_reg_record import IssuerRevRegRecord from ......revocation.models.revocation_registry import RevocationRegistry from ......revocation.indy import IndyRevocation -from ......revocation.util import notify_revocation_reg_event from ......storage.base import BaseStorage -from ......storage.error import StorageNotFoundError - from ...message_types import ( ATTACHMENT_FORMAT, @@ -328,18 +324,17 @@ async def issue_credential( self, cred_ex_record: V20CredExRecord, retries: int = 5 ) -> CredFormatAttachment: """Issue indy credential.""" - await self._check_uniqueness(cred_ex_record.cred_ex_id) - cred_offer = cred_ex_record.cred_offer.attachment(IndyCredFormatHandler.format) cred_request = cred_ex_record.cred_request.attachment( IndyCredFormatHandler.format ) - + cred_values = cred_ex_record.cred_offer.credential_preview.attr_dict( + decode=False + ) schema_id = cred_offer["schema_id"] cred_def_id = cred_offer["cred_def_id"] - rev_reg_id = None - rev_reg = None + issuer = self.profile.inject(IndyIssuer) multitenant_mgr = self.profile.inject_or(BaseMultitenantManager) if multitenant_mgr: ledger_exec_inst = IndyLedgerRequestsExecutor(self.profile) @@ -354,124 +349,63 @@ async def issue_credential( async with ledger: schema = await ledger.get_schema(schema_id) cred_def = await ledger.get_credential_definition(cred_def_id) + revocable = cred_def["value"].get("revocation") - tails_path = None - if cred_def["value"].get("revocation"): - revoc = IndyRevocation(self.profile) - try: - active_rev_reg_rec = await revoc.get_active_issuer_rev_reg_record( - cred_def_id + for attempt in range(max(retries, 1)): + if attempt > 0: + LOGGER.info( + "Waiting 2s before retrying credential issuance for cred def '%s'", + cred_def_id, ) - rev_reg = await active_rev_reg_rec.get_registry() - rev_reg_id = active_rev_reg_rec.revoc_reg_id - + await asyncio.sleep(2) + + await self._check_uniqueness(cred_ex_record.cred_ex_id) + + if revocable: + revoc = IndyRevocation(self.profile) + registry_info = await revoc.get_or_create_active_registry(cred_def_id) + if not registry_info: + continue + del revoc + issuer_rev_reg, rev_reg = registry_info + rev_reg_id = issuer_rev_reg.revoc_reg_id tails_path = rev_reg.tails_local_path - await rev_reg.get_or_fetch_local_tails_path() - - except StorageNotFoundError: - async with self.profile.session() as session: - posted_rev_reg_recs = await IssuerRevRegRecord.query_by_cred_def_id( - session, - cred_def_id, - state=IssuerRevRegRecord.STATE_POSTED, - ) - if not posted_rev_reg_recs: - # Send next 2 rev regs, publish tails files in background - async with self.profile.session() as session: - old_rev_reg_recs = sorted( - await IssuerRevRegRecord.query_by_cred_def_id( - session, - cred_def_id, - ) - ) # prefer to reuse prior rev reg size - rev_reg_size = ( - old_rev_reg_recs[0].max_cred_num if old_rev_reg_recs else None - ) - for _ in range(2): - await notify_revocation_reg_event( - self.profile, - cred_def_id, - rev_reg_size, - auto_create_rev_reg=True, - ) - - if retries > 0: - LOGGER.info( - ("Waiting 2s on posted rev reg " "for cred def %s, retrying"), - cred_def_id, - ) - await asyncio.sleep(2) - return await self.issue_credential( - cred_ex_record, - retries - 1, - ) + else: + rev_reg_id = None + tails_path = None - raise V20CredFormatError( - f"Cred def id {cred_def_id} " "has no active revocation registry" + try: + (cred_json, cred_rev_id) = await issuer.create_credential( + schema, + cred_offer, + cred_request, + cred_values, + cred_ex_record.cred_ex_id, + rev_reg_id, + tails_path, ) - del revoc - - cred_values = cred_ex_record.cred_offer.credential_preview.attr_dict( - decode=False - ) - issuer = self.profile.inject(IndyIssuer) - try: - (cred_json, cred_rev_id,) = await issuer.create_credential( - schema, - cred_offer, - cred_request, - cred_values, - cred_ex_record.cred_ex_id, - rev_reg_id, - tails_path, - ) + except IndyIssuerRevocationRegistryFullError: + # unlucky, another instance filled the registry first + continue detail_record = V20CredExRecordIndy( cred_ex_id=cred_ex_record.cred_ex_id, rev_reg_id=rev_reg_id, cred_rev_id=cred_rev_id, ) - - # If the rev reg is now full - if rev_reg and rev_reg.max_creds == int(cred_rev_id): - async with self.profile.session() as session: - await active_rev_reg_rec.set_state( - session, - IssuerRevRegRecord.STATE_FULL, - ) - - # Send next 1 rev reg, publish tails file in background - rev_reg_size = active_rev_reg_rec.max_cred_num - await notify_revocation_reg_event( - self.profile, cred_def_id, rev_reg_size, auto_create_rev_reg=True - ) - - async with self.profile.session() as session: + async with self._profile.session() as session: await detail_record.save(session, reason="v2.0 issue credential") - except IndyIssuerRevocationRegistryFullError: - # unlucky: duelling instance issued last cred near same time as us - async with self.profile.session() as session: - await active_rev_reg_rec.set_state( - session, - IssuerRevRegRecord.STATE_FULL, - ) - - if retries > 0: - # use next rev reg; at worst, lucky instance is putting one up - LOGGER.info( - "Waiting 1s and retrying: revocation registry %s is full", - active_rev_reg_rec.revoc_reg_id, - ) - await asyncio.sleep(1) - return await self.issue_credential( - cred_ex_record, - retries - 1, - ) + if rev_reg and rev_reg.max_creds <= int(cred_rev_id): + revoc = IndyRevocation(self.profile) + await revoc.handle_full_registry(rev_reg_id) + del revoc - raise + return self.get_format_data(CRED_20_ISSUE, json.loads(cred_json)) - return self.get_format_data(CRED_20_ISSUE, json.loads(cred_json)) + raise V20CredFormatError( + f"Cred def '{cred_def_id}' has no active revocation registry" + ) async def receive_credential( self, cred_ex_record: V20CredExRecord, cred_issue_message: V20CredIssue diff --git a/aries_cloudagent/revocation/indy.py b/aries_cloudagent/revocation/indy.py index 7d09ece62f..9e4562e88c 100644 --- a/aries_cloudagent/revocation/indy.py +++ b/aries_cloudagent/revocation/indy.py @@ -1,6 +1,6 @@ """Indy revocation registry management.""" -from typing import Sequence +from typing import Optional, Sequence, Tuple from ..core.profile import Profile from ..ledger.base import BaseLedger @@ -10,11 +10,20 @@ IndyLedgerRequestsExecutor, ) from ..multitenant.base import BaseMultitenantManager +from ..protocols.endorse_transaction.v1_0.util import ( + get_endorser_connection_id, + is_author_role, +) from ..storage.base import StorageNotFoundError -from .error import RevocationNotSupportedError, RevocationRegistryBadSizeError +from .error import ( + RevocationError, + RevocationNotSupportedError, + RevocationRegistryBadSizeError, +) from .models.issuer_rev_reg_record import IssuerRevRegRecord from .models.revocation_registry import RevocationRegistry +from .util import notify_revocation_reg_init_event class IndyRevocation: @@ -32,6 +41,8 @@ async def init_issuer_registry( max_cred_num: int = None, revoc_def_type: str = None, tag: str = None, + create_pending_rev_reg: bool = False, + endorser_connection_id: str = None, ) -> "IssuerRevRegRecord": """Create a new revocation registry record for a credential definition.""" multitenant_mgr = self._profile.inject_or(BaseMultitenantManager) @@ -69,11 +80,44 @@ async def init_issuer_registry( ) async with self._profile.session() as session: await record.save(session, reason="Init revocation registry") + + if endorser_connection_id is None and is_author_role(self._profile): + endorser_connection_id = await get_endorser_connection_id(self._profile) + if not endorser_connection_id: + raise RevocationError(reason="Endorser connection not found") + + await notify_revocation_reg_init_event( + self._profile, + record.record_id, + create_pending_rev_reg=create_pending_rev_reg, + endorser_connection_id=endorser_connection_id, + ) + return record + async def handle_full_registry(self, revoc_reg_id: str): + """Update the registry status and start the next registry generation.""" + async with self._profile.transaction() as txn: + registry = await IssuerRevRegRecord.retrieve_by_revoc_reg_id( + txn, revoc_reg_id, for_update=True + ) + if registry.state == IssuerRevRegRecord.STATE_FULL: + return + await registry.set_state( + txn, + IssuerRevRegRecord.STATE_FULL, + ) + await txn.commit() + + await self.init_issuer_registry( + registry.cred_def_id, + registry.max_cred_num, + registry.revoc_def_type, + ) + async def get_active_issuer_rev_reg_record( self, cred_def_id: str - ) -> "IssuerRevRegRecord": + ) -> IssuerRevRegRecord: """Return current active registry for issuing a given credential definition. Args: @@ -91,9 +135,7 @@ async def get_active_issuer_rev_reg_record( f"No active issuer revocation record found for cred def id {cred_def_id}" ) - async def get_issuer_rev_reg_record( - self, revoc_reg_id: str - ) -> "IssuerRevRegRecord": + async def get_issuer_rev_reg_record(self, revoc_reg_id: str) -> IssuerRevRegRecord: """Return a revocation registry record by identifier. Args: @@ -104,7 +146,7 @@ async def get_issuer_rev_reg_record( session, revoc_reg_id ) - async def list_issuer_registries(self) -> Sequence["IssuerRevRegRecord"]: + async def list_issuer_registries(self) -> Sequence[IssuerRevRegRecord]: """List the issuer's current revocation registries.""" async with self._profile.session() as session: return await IssuerRevRegRecord.query(session) @@ -129,7 +171,36 @@ async def get_issuer_rev_reg_delta( return rev_reg_delta - async def get_ledger_registry(self, revoc_reg_id: str) -> "RevocationRegistry": + async def get_or_create_active_registry( + self, cred_def_id: str, max_cred_num: int = None + ) -> Optional[Tuple[IssuerRevRegRecord, RevocationRegistry]]: + """Fetch the active revocation registry. + + If there is no active registry then creation of a new registry will be + triggered and the caller should retry after a delay. + """ + try: + active_rev_reg_rec = await self.get_active_issuer_rev_reg_record( + cred_def_id + ) + rev_reg = active_rev_reg_rec.get_registry() + await rev_reg.get_or_fetch_local_tails_path() + return active_rev_reg_rec, rev_reg + except StorageNotFoundError: + pass + + async with self._profile.session() as session: + rev_reg_recs = await IssuerRevRegRecord.query_by_cred_def_id( + session, cred_def_id, {"$neq": IssuerRevRegRecord.STATE_FULL} + ) + if not rev_reg_recs: + await self.init_issuer_registry( + cred_def_id, + max_cred_num=max_cred_num, + ) + return None + + async def get_ledger_registry(self, revoc_reg_id: str) -> RevocationRegistry: """Get a revocation registry from the ledger, fetching as necessary.""" if revoc_reg_id in IndyRevocation.REV_REG_CACHE: return IndyRevocation.REV_REG_CACHE[revoc_reg_id] @@ -143,7 +214,7 @@ async def get_ledger_registry(self, revoc_reg_id: str) -> "RevocationRegistry": IndyRevocation.REV_REG_CACHE[revoc_reg_id] = rev_reg return rev_reg - async def get_ledger_for_registry(self, revoc_reg_id: str) -> "BaseLedger": + async def get_ledger_for_registry(self, revoc_reg_id: str) -> BaseLedger: """Get the ledger for the given registry.""" multitenant_mgr = self._profile.inject_or(BaseMultitenantManager) if multitenant_mgr: diff --git a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py index fbbaac9e43..d95a5f938b 100644 --- a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py @@ -5,6 +5,7 @@ import uuid from functools import total_ordering from os.path import join +from pathlib import Path from shutil import move from typing import Any, Mapping, Sequence, Union from urllib.parse import urlparse @@ -29,7 +30,10 @@ INDY_REV_REG_ID, UUIDFour, ) +from ...tails.base import BaseTailsServer + from ..error import RevocationError + from .revocation_registry import RevocationRegistry DEFAULT_REGISTRY_SIZE = 1000 @@ -62,7 +66,8 @@ class Meta: STATE_INIT = "init" STATE_GENERATED = "generated" - STATE_POSTED = "posted" # definition published: ephemeral, should last milliseconds + STATE_POSTED = "posted" # definition published + STATE_UPLOADED = "uploaded" # tails file uploaded STATE_ACTIVE = "active" # initial entry published, possibly subsequent entries STATE_FULL = "full" # includes corrupt @@ -227,7 +232,7 @@ async def send_def( profile: Profile, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Send the revocation registry definition to the ledger.""" if not (self.revoc_reg_def and self.issuer_did): raise RevocationError(f"Revocation registry {self.revoc_reg_id} undefined") @@ -261,7 +266,7 @@ async def send_entry( profile: Profile, write_ledger: bool = True, endorser_did: str = None, - ): + ) -> dict: """Send a registry entry to the ledger.""" if not ( self.revoc_reg_id @@ -274,7 +279,7 @@ async def send_entry( self._check_url(self.tails_public_uri) if self.state not in ( - IssuerRevRegRecord.STATE_POSTED, + IssuerRevRegRecord.STATE_UPLOADED, IssuerRevRegRecord.STATE_ACTIVE, IssuerRevRegRecord.STATE_FULL, # can still publish revocation deltas ): @@ -294,7 +299,7 @@ async def send_entry( write_ledger=write_ledger, endorser_did=endorser_did, ) - if self.state == IssuerRevRegRecord.STATE_POSTED: + if self.state == IssuerRevRegRecord.STATE_UPLOADED: self.state = IssuerRevRegRecord.STATE_ACTIVE # initial entry activates async with profile.session() as session: await self.save( @@ -303,6 +308,36 @@ async def send_entry( return rev_entry_res + @property + def has_local_tails_file(self) -> bool: + """Check if a local copy of the tails file is available.""" + return bool(self.tails_local_path) and Path(self.tails_local_path).is_file() + + async def upload_tails_file(self, profile: Profile): + """Upload the local tails file to the tails server.""" + tails_server = profile.inject_or(BaseTailsServer) + if not tails_server: + raise RevocationError("Tails server not configured") + if not self.has_local_tails_file: + raise RevocationError("Local tails file not found") + + (upload_success, reason) = await tails_server.upload_tails_file( + profile.context, + self.revoc_reg_id, + self.tails_local_path, + interval=0.8, + backoff=-0.5, + max_attempts=5, # heuristic: respect HTTP timeout + ) + if not upload_success: + raise RevocationError( + f"Tails file for rev reg {self.revoc_reg_id} failed to upload: {reason}" + ) + + self.state = IssuerRevRegRecord.STATE_UPLOADED + async with profile.session() as session: + await self.save(session, reason="Uploaded tails file") + async def mark_pending(self, session: ProfileSession, cred_rev_id: str) -> None: """Mark a credential revocation id as revoked pending publication to ledger. @@ -334,7 +369,7 @@ async def clear_pending( self.pending_pub.clear() await self.save(session, reason="Cleared pending revocations") - async def get_registry(self) -> RevocationRegistry: + def get_registry(self) -> RevocationRegistry: """Create a `RevocationRegistry` instance from this record.""" return RevocationRegistry( self.revoc_reg_id, @@ -359,10 +394,12 @@ async def query_by_cred_def_id( cred_def_id: The credential definition ID to filter by state: A state value to filter by """ - tag_filter = { - **{"cred_def_id": cred_def_id for _ in [""] if cred_def_id}, - **{"state": state for _ in [""] if state}, - } + tag_filter = dict( + filter( + lambda f: f[1] is not None, + (("cred_def_id", cred_def_id), ("state", state)), + ) + ) return await cls.query(session, tag_filter) @classmethod @@ -383,16 +420,19 @@ async def query_by_pending( @classmethod async def retrieve_by_revoc_reg_id( - cls, session: ProfileSession, revoc_reg_id: str + cls, session: ProfileSession, revoc_reg_id: str, for_update: bool = False ) -> "IssuerRevRegRecord": """Retrieve a revocation registry record by revocation registry ID. Args: session: The profile session to use revoc_reg_id: The revocation registry ID + for_update: Retrieve for update """ tag_filter = {"revoc_reg_id": revoc_reg_id} - return await cls.retrieve_by_tag_filter(session, tag_filter) + return await cls.retrieve_by_tag_filter( + session, tag_filter, for_update=for_update + ) async def set_state(self, session: ProfileSession, state: str = None): """Change the registry state (default full).""" diff --git a/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py index c023caa68b..34048f0423 100644 --- a/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py @@ -128,7 +128,7 @@ async def test_generate_registry_etc(self): assert rec.state == IssuerRevRegRecord.STATE_ACTIVE self.ledger.send_revoc_reg_entry.assert_called_once() - rev_reg = await rec.get_registry() + rev_reg = rec.get_registry() assert type(rev_reg) == RevocationRegistry async with self.profile.session() as session: diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index f22ac1c0fd..b54547d63f 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -21,7 +21,6 @@ from ..core.event_bus import Event, EventBus from ..core.profile import Profile from ..indy.issuer import IndyIssuerError -from ..indy.util import tails_path from ..ledger.base import BaseLedger from ..ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager from ..ledger.error import LedgerError @@ -51,7 +50,6 @@ ) from ..storage.base import BaseStorage from ..storage.error import StorageError, StorageNotFoundError -from ..tails.base import BaseTailsServer from .error import RevocationError, RevocationNotSupportedError from .indy import IndyRevocation @@ -64,12 +62,10 @@ from .recover import generate_ledger_rrrecovery_txn from .util import ( REVOCATION_EVENT_PREFIX, - REVOCATION_REG_EVENT, + REVOCATION_REG_INIT_EVENT, + REVOCATION_REG_ENDORSED_EVENT, REVOCATION_ENTRY_EVENT, - REVOCATION_TAILS_EVENT, - notify_revocation_reg_event, notify_revocation_entry_event, - notify_revocation_tails_file_event, ) @@ -958,24 +954,19 @@ async def upload_tails_file(request: web.BaseRequest): context: AdminRequestContext = request["context"] rev_reg_id = request.match_info["rev_reg_id"] + try: + revoc = IndyRevocation(context.profile) + rev_reg = await revoc.get_issuer_rev_reg_record(rev_reg_id) + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err - tails_server = context.inject_or(BaseTailsServer) - if not tails_server: - raise web.HTTPForbidden(reason="No tails server configured") - - loc_tails_path = tails_path(rev_reg_id) - if not loc_tails_path: + if not rev_reg.has_local_tails_file: raise web.HTTPNotFound(reason=f"No local tails file for rev reg {rev_reg_id}") - (upload_success, reason) = await tails_server.upload_tails_file( - context, - rev_reg_id, - loc_tails_path, - interval=0.8, - backoff=-0.5, - max_attempts=16, - ) - if not upload_success: - raise web.HTTPInternalServerError(reason=reason) + + try: + await rev_reg.upload_tails_file(context.profile) + except RevocationError as e: + raise web.HTTPInternalServerError(reason=str(e)) return web.json_response({}) @@ -1175,7 +1166,6 @@ async def send_rev_reg_entry(request: web.BaseRequest): except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - except RevocationError as err: raise web.HTTPBadRequest(reason=err.roll_up) from err @@ -1287,162 +1277,145 @@ async def set_rev_reg_state(request: web.BaseRequest): def register_events(event_bus: EventBus): """Subscribe to any events we need to support.""" event_bus.subscribe( - re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_REG_EVENT}.*"), - on_revocation_registry_event, + re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_REG_INIT_EVENT}.*"), + on_revocation_registry_init_event, ) event_bus.subscribe( - re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_ENTRY_EVENT}.*"), - on_revocation_entry_event, + re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_REG_ENDORSED_EVENT}.*"), + on_revocation_registry_endorsed_event, ) event_bus.subscribe( - re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_TAILS_EVENT}.*"), - on_revocation_tails_file_event, + re.compile(f"^{REVOCATION_EVENT_PREFIX}{REVOCATION_ENTRY_EVENT}.*"), + on_revocation_entry_event, ) -async def on_revocation_registry_event(profile: Profile, event: Event): - """Handle revocation registry event.""" - if "endorser" in event.payload: +async def on_revocation_registry_init_event(profile: Profile, event: Event): + """Handle revocation registry initiation event.""" + meta_data = event.payload + if "endorser" in meta_data: # TODO error handling - for now just let exceptions get raised async with profile.session() as session: connection = await ConnRecord.retrieve_by_id( - session, event.payload["endorser"]["connection_id"] + session, meta_data["endorser"]["connection_id"] ) endorser_info = await connection.metadata_get(session, "endorser_info") endorser_did = endorser_info["endorser_did"] write_ledger = False - create_transaction_for_endorser = True else: endorser_did = None write_ledger = True - create_transaction_for_endorser = False - cred_def_id = event.payload["context"]["cred_def_id"] - rev_reg_size = event.payload["context"]["rev_reg_size"] - try: - tails_base_url = profile.settings.get("tails_server_base_url") - if not tails_base_url: - raise RevocationError("tails_server_base_url not configured") - - # Create registry - revoc = IndyRevocation(profile) - registry_record = await revoc.init_issuer_registry( - cred_def_id, - max_cred_num=rev_reg_size, - ) - - await shield(registry_record.generate_registry(profile)) + tails_base_url = profile.settings.get("tails_server_base_url") + if not tails_base_url: + raise RevocationError("tails_server_base_url not configured") - await registry_record.set_tails_file_public_uri( + # Generate the registry and upload the tails file + async def generate(rr_record: IssuerRevRegRecord) -> dict: + await rr_record.generate_registry(profile) + await rr_record.set_tails_file_public_uri( profile, f"{tails_base_url}/{registry_record.revoc_reg_id}", ) - rev_reg_resp = await registry_record.send_def( + rev_reg_resp = await rr_record.send_def( profile, write_ledger=write_ledger, endorser_did=endorser_did, ) - except RevocationError: - raise + if write_ledger: + # Upload the tails file + await rr_record.upload_tails_file(profile) - if not create_transaction_for_endorser: - meta_data = event.payload - rev_reg_id = registry_record.revoc_reg_id - meta_data["context"]["rev_reg_id"] = rev_reg_id - auto_create_rev_reg = meta_data["processing"].get("auto_create_rev_reg", False) - - # Notify event - if auto_create_rev_reg: - await notify_revocation_entry_event(profile, rev_reg_id, meta_data) - - else: - transaction_manager = TransactionManager(profile) - try: - revo_transaction = await transaction_manager.create_record( - messages_attach=rev_reg_resp["result"], - connection_id=connection.connection_id, - meta_data=event.payload, - ) - except StorageError as err: - raise TransactionManagerError(reason=err.roll_up) from err - - # if auto-request, send the request to the endorser - if profile.settings.get_value("endorser.auto_request"): + # Post the initial revocation entry + await notify_revocation_entry_event(profile, record_id, meta_data) + else: + transaction_manager = TransactionManager(profile) try: - ( - revo_transaction, - revo_transaction_request, - ) = await transaction_manager.create_request( - transaction=revo_transaction, - # TODO see if we need to parameterize these params - # expires_time=expires_time, - # endorser_write_txn=endorser_write_txn, + revo_transaction = await transaction_manager.create_record( + messages_attach=rev_reg_resp["result"], + connection_id=connection.connection_id, + meta_data=event.payload, ) - except (StorageError, TransactionManagerError) as err: + except StorageError as err: raise TransactionManagerError(reason=err.roll_up) from err - responder = profile.inject_or(BaseResponder) - if responder: - await responder.send( - revo_transaction_request, - connection_id=connection.connection_id, - ) - else: - LOGGER.warning( - "Configuration has no BaseResponder: cannot update " - "revocation on cred def %s", - cred_def_id, - ) + # if auto-request, send the request to the endorser + if profile.settings.get_value("endorser.auto_request"): + try: + ( + revo_transaction, + revo_transaction_request, + ) = await transaction_manager.create_request( + transaction=revo_transaction, + # TODO see if we need to parameterize these params + # expires_time=expires_time, + # endorser_write_txn=endorser_write_txn, + ) + except (StorageError, TransactionManagerError) as err: + raise TransactionManagerError(reason=err.roll_up) from err + + responder = profile.inject_or(BaseResponder) + if responder: + await responder.send( + revo_transaction_request, + connection_id=connection.connection_id, + ) + else: + LOGGER.warning( + "Configuration has no BaseResponder: cannot update " + "revocation on registry ID: %s", + record_id, + ) + + record_id = meta_data["context"]["issuer_rev_id"] + async with profile.session() as session: + registry_record = await IssuerRevRegRecord.retrieve_by_id(session, record_id) + await shield(generate(registry_record)) + + create_pending_rev_reg = meta_data["processing"].get( + "create_pending_rev_reg", False + ) + if write_ledger and create_pending_rev_reg: + revoc = IndyRevocation(profile) + await revoc.init_issuer_registry( + registry_record.cred_def_id, + registry_record.max_cred_num, + registry_record.revoc_def_type, + ) async def on_revocation_entry_event(profile: Profile, event: Event): """Handle revocation entry event.""" - if "endorser" in event.payload: + meta_data = event.payload + if "endorser" in meta_data: # TODO error handling - for now just let exceptions get raised async with profile.session() as session: connection = await ConnRecord.retrieve_by_id( - session, event.payload["endorser"]["connection_id"] + session, meta_data["endorser"]["connection_id"] ) endorser_info = await connection.metadata_get(session, "endorser_info") endorser_did = endorser_info["endorser_did"] write_ledger = False - create_transaction_for_endorser = True else: endorser_did = None write_ledger = True - create_transaction_for_endorser = False - rev_reg_id = event.payload["context"]["rev_reg_id"] - try: - tails_base_url = profile.settings.get("tails_server_base_url") - if not tails_base_url: - raise RevocationError("tails_server_base_url not configured") - - revoc = IndyRevocation(profile) - registry_record = await revoc.get_issuer_rev_reg_record(rev_reg_id) - rev_entry_resp = await registry_record.send_entry( - profile, - write_ledger=write_ledger, - endorser_did=endorser_did, - ) - except RevocationError: - raise - - if not create_transaction_for_endorser: - meta_data = event.payload - auto_create_rev_reg = meta_data["processing"].get("auto_create_rev_reg", False) - - # Notify event - if auto_create_rev_reg: - await notify_revocation_tails_file_event(profile, rev_reg_id, meta_data) + record_id = meta_data["context"]["issuer_rev_id"] + async with profile.session() as session: + registry_record = await IssuerRevRegRecord.retrieve_by_id(session, record_id) + rev_entry_resp = await registry_record.send_entry( + profile, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) - else: + if not write_ledger: transaction_manager = TransactionManager(profile) try: revo_transaction = await transaction_manager.create_record( messages_attach=rev_entry_resp["result"], connection_id=connection.connection_id, - meta_data=event.payload, + meta_data=meta_data, ) except StorageError as err: raise RevocationError(err.roll_up) from err @@ -1472,57 +1445,37 @@ async def on_revocation_entry_event(profile: Profile, event: Event): LOGGER.warning( "Configuration has no BaseResponder: cannot update " "revocation on cred def %s", - event.payload["endorser"]["cred_def_id"], + meta_data["endorser"]["cred_def_id"], ) -async def on_revocation_tails_file_event(profile: Profile, event: Event): +async def on_revocation_registry_endorsed_event(profile: Profile, event: Event): """Handle revocation tails file event.""" - tails_base_url = profile.settings.get("tails_server_base_url") - if not tails_base_url: - raise RevocationError("tails_server_base_url not configured") - - tails_server = profile.inject(BaseTailsServer) - revoc_reg_id = event.payload["context"]["rev_reg_id"] - tails_local_path = tails_path(revoc_reg_id) - (upload_success, reason) = await tails_server.upload_tails_file( - profile.context, - revoc_reg_id, - tails_local_path, - interval=0.8, - backoff=-0.5, - max_attempts=5, # heuristic: respect HTTP timeout - ) - if not upload_success: - raise RevocationError( - f"Tails file for rev reg {revoc_reg_id} failed to upload: {reason}" - ) + meta_data = event.payload + rev_reg_id = meta_data["context"]["rev_reg_id"] + revoc = IndyRevocation(profile) + registry_record = await revoc.get_issuer_rev_reg_record(rev_reg_id) + # NOTE: if there are multiple pods, then the one processing this + # event may not be the one that generated the tails file. + await registry_record.upload_tails_file(profile) # create a "pending" registry if one is requested # (this is done automatically when creating a credential definition, so that when a # revocation registry fills up, we ca continue to issue credentials without a # delay) - create_pending_rev_reg = event.payload["processing"].get( + create_pending_rev_reg = meta_data["processing"].get( "create_pending_rev_reg", False ) if create_pending_rev_reg: - meta_data = event.payload - del meta_data["context"]["rev_reg_id"] - del meta_data["processing"]["create_pending_rev_reg"] - cred_def_id = meta_data["context"]["cred_def_id"] - rev_reg_size = meta_data["context"].get("rev_reg_size", None) - auto_create_rev_reg = meta_data["processing"].get("auto_create_rev_reg", False) endorser_connection_id = ( meta_data["endorser"].get("connection_id", None) if "endorser" in meta_data else None ) - - await notify_revocation_reg_event( - profile, - cred_def_id, - rev_reg_size, - auto_create_rev_reg=auto_create_rev_reg, + await revoc.init_issuer_registry( + registry_record.cred_def_id, + registry_record.max_cred_num, + registry_record.revoc_def_type, endorser_connection_id=endorser_connection_id, ) diff --git a/aries_cloudagent/revocation/util.py b/aries_cloudagent/revocation/util.py index 50dd08a50c..f81e26dc4a 100644 --- a/aries_cloudagent/revocation/util.py +++ b/aries_cloudagent/revocation/util.py @@ -4,79 +4,50 @@ from typing import Sequence from ..core.profile import Profile -from ..protocols.endorse_transaction.v1_0.util import ( - get_endorser_connection_id, - is_author_role, -) REVOCATION_EVENT_PREFIX = "acapy::REVOCATION::" EVENT_LISTENER_PATTERN = re.compile(f"^{REVOCATION_EVENT_PREFIX}(.*)?$") -REVOCATION_REG_EVENT = "REGISTRY" +REVOCATION_REG_INIT_EVENT = "REGISTRY_INIT" +REVOCATION_REG_ENDORSED_EVENT = "REGISTRY_ENDORSED" REVOCATION_ENTRY_EVENT = "ENTRY" -REVOCATION_TAILS_EVENT = "TAILS" REVOCATION_PUBLISHED_EVENT = "published" REVOCATION_CLEAR_PENDING_EVENT = "clear-pending" -async def notify_revocation_reg_event( +async def notify_revocation_reg_init_event( profile: Profile, - cred_def_id: str, - rev_reg_size: int, - auto_create_rev_reg: bool = False, + issuer_rev_id: str, create_pending_rev_reg: bool = False, endorser_connection_id: str = None, ): - """Send notification for a revocation registry event.""" + """Send notification for a revocation registry init event.""" meta_data = { "context": { - "cred_def_id": cred_def_id, - "support_revocation": True, - "rev_reg_size": rev_reg_size, - }, - "processing": { - "auto_create_rev_reg": auto_create_rev_reg, + "issuer_rev_id": issuer_rev_id, }, + "processing": {"create_pending_rev_reg": create_pending_rev_reg}, } - if ( - (not endorser_connection_id) - and is_author_role(profile) - and "endorser" not in meta_data - ): - endorser_connection_id = await get_endorser_connection_id(profile) - if not endorser_connection_id: - raise Exception(reason="No endorser connection found") - if create_pending_rev_reg: - meta_data["processing"]["create_pending_rev_reg"] = create_pending_rev_reg if endorser_connection_id: meta_data["endorser"] = {"connection_id": endorser_connection_id} - event_id = REVOCATION_EVENT_PREFIX + REVOCATION_REG_EVENT + "::" + cred_def_id - await profile.notify( - event_id, - meta_data, - ) + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_REG_INIT_EVENT}::{issuer_rev_id}" + await profile.notify(topic, meta_data) async def notify_revocation_entry_event( - profile: Profile, rev_reg_id: str, meta_data: dict + profile: Profile, issuer_rev_id: str, meta_data: dict ): - """Send notification for a revocation registry event.""" - event_id = REVOCATION_EVENT_PREFIX + REVOCATION_ENTRY_EVENT + "::" + rev_reg_id - await profile.notify( - event_id, - meta_data, - ) + """Send notification for a revocation registry entry event.""" + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_ENTRY_EVENT}::{issuer_rev_id}" + await profile.notify(topic, meta_data) -async def notify_revocation_tails_file_event( +async def notify_revocation_reg_endorsed_event( profile: Profile, rev_reg_id: str, meta_data: dict ): - """Send notification for a revocation tails file event.""" - event_id = REVOCATION_EVENT_PREFIX + REVOCATION_TAILS_EVENT + "::" + rev_reg_id - await profile.notify( - event_id, - meta_data, - ) + """Send notification for a revocation registry endorsement event.""" + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_REG_ENDORSED_EVENT}::{rev_reg_id}" + await profile.notify(topic, meta_data) async def notify_revocation_published_event( From 86624306a1c6741070d654a2ccb99fe237513409 Mon Sep 17 00:00:00 2001 From: Andrew Whitehead Date: Wed, 15 Jun 2022 20:18:23 -0700 Subject: [PATCH 2/5] fixing unit tests Signed-off-by: Andrew Whitehead --- .../issue_credential/v1_0/manager.py | 2 +- .../v1_0/tests/test_manager.py | 182 +++++----------- .../v2_0/formats/indy/handler.py | 6 +- .../v2_0/formats/indy/tests/test_handler.py | 195 ++++-------------- .../tests/test_issuer_rev_reg_record.py | 7 +- .../revocation/tests/test_routes.py | 75 +++---- 6 files changed, 133 insertions(+), 334 deletions(-) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py index 88ceb3396d..2a1c37dd38 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py @@ -666,7 +666,7 @@ async def issue_credential( # unlucky, another instance filled the registry first continue - if rev_reg and rev_reg.max_creds <= int(cred_rev_id): + if revocable and rev_reg.max_creds <= int(cred_rev_id): revoc = IndyRevocation(self.profile) await revoc.handle_full_registry(rev_reg_id) del revoc diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py index e889aae662..ba0e65f924 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py @@ -5,6 +5,7 @@ from time import time from asynctest import mock as async_mock, TestCase as AsyncTestCase +from more_itertools import side_effect from .....core.in_memory import InMemoryProfile from .....cache.base import BaseCache @@ -880,7 +881,7 @@ async def test_receive_request_no_cred_ex_with_offer_found(self): for_update=True, ) - async def test_issue_credential(self): + async def test_issue_credential_revocable(self): connection_id = "test_conn_id" comment = "comment" cred_values = {"attr": "value"} @@ -918,18 +919,18 @@ async def test_issue_credential(self): with async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc, async_mock.patch.object( - asyncio, "ensure_future", autospec=True - ) as asyncio_mock, async_mock.patch.object( V10CredentialExchange, "save", autospec=True ) as save_ex: - revoc.return_value.get_active_issuer_rev_reg_record = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - registry_id=REV_REG_ID, - tails_local_path="dummy-path", - get_or_fetch_local_tails_path=async_mock.CoroutineMock(), - ) + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + return_value=( + async_mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + ), + async_mock.MagicMock( # rev_reg + registry_id=REV_REG_ID, + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=async_mock.CoroutineMock(), + max_creds=10, ), ) ) @@ -1078,33 +1079,28 @@ async def test_issue_credential_fills_rr(self): with async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc, async_mock.patch.object( - asyncio, "ensure_future", autospec=True - ) as asyncio_mock, async_mock.patch.object( V10CredentialExchange, "save", autospec=True ) as save_ex: revoc.return_value = async_mock.MagicMock( - get_active_issuer_rev_reg_record=( + get_or_create_active_registry=( async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - registry_id=REV_REG_ID, - tails_local_path="dummy-path", - max_creds=1000, - get_or_fetch_local_tails_path=( - async_mock.CoroutineMock() - ), - ) + return_value=( + async_mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + set_state=async_mock.CoroutineMock(), + ), + async_mock.MagicMock( # rev_reg + registry_id=REV_REG_ID, + tails_local_path="dummy-path", + max_creds=1000, + get_or_fetch_local_tails_path=( + async_mock.CoroutineMock() + ), ), - set_state=async_mock.CoroutineMock(), ) ) ), - init_issuer_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) - ), + handle_full_registry=async_mock.CoroutineMock(), ) (ret_exchange, ret_cred_issue) = await self.manager.issue_credential( stored_exchange, comment=comment, retries=0 @@ -1122,6 +1118,8 @@ async def test_issue_credential_fills_rr(self): "dummy-path", ) + revoc.return_value.handle_full_registry.assert_awaited_once_with(REV_REG_ID) + assert ret_exchange._credential.ser == cred assert ret_cred_issue.indy_credential() == cred assert ret_exchange.state == V10CredentialExchange.STATE_ISSUED @@ -1191,28 +1189,28 @@ async def test_issue_credential_no_active_rr_no_retries(self): ), ) with async_mock.patch.object( - test_module, "IssuerRevRegRecord", autospec=True - ) as issuer_rr_rec, async_mock.patch.object( test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( - V10CredentialExchange, "save", autospec=True - ) as save_ex: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock(side_effect=test_module.StorageNotFoundError()) - ) - revoc.return_value.init_issuer_registry = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) - ) - issuer_rr_rec.query_by_cred_def_id = async_mock.CoroutineMock( - return_value=[] + ) as revoc: + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + side_effect=[ + None, + ( + async_mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + set_state=async_mock.CoroutineMock(), + ), + async_mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(async_mock.CoroutineMock()), + ), + ), + ] ) - with self.assertRaises(CredentialManagerError) as x_cred_mgr: + with self.assertRaises(CredentialManagerError) as context: await self.manager.issue_credential( stored_exchange, comment=comment, retries=0 ) - assert "has no active revocation registry" in x_cred_mgr.message + assert "has no active revocation registry" in context.message async def test_issue_credential_no_active_rr_retry(self): connection_id = "test_conn_id" @@ -1256,99 +1254,17 @@ async def test_issue_credential_no_active_rr_retry(self): ) ), ) - with async_mock.patch.object( - test_module, "IssuerRevRegRecord", autospec=True - ) as issuer_rr_rec, async_mock.patch.object( - test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( - V10CredentialExchange, "save", autospec=True - ) as save_ex: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock(side_effect=test_module.StorageNotFoundError()) - ) - issuer_rr_rec.query_by_cred_def_id = async_mock.CoroutineMock( - side_effect=[ - [], # posted_rev_reg_recs - [async_mock.MagicMock(max_cred_num=1000)], # old_rev_reg_recs - ] - * 2 - ) - revoc.return_value.init_issuer_registry = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) - ) - with self.assertRaises(CredentialManagerError) as x_cred_mgr: - await self.manager.issue_credential( - stored_exchange, comment=comment, retries=1 - ) - assert "has no active revocation registry" in x_cred_mgr.message - - async def test_issue_credential_rr_full(self): - connection_id = "test_conn_id" - comment = "comment" - cred_values = {"attr": "value"} - thread_id = "thread-id" - - stored_exchange = V10CredentialExchange( - credential_exchange_id="dummy-cxid", - connection_id=connection_id, - credential_definition_id=CRED_DEF_ID, - credential_offer=INDY_OFFER, - credential_request=INDY_CRED_REQ, - credential_proposal_dict=CredentialProposal( - credential_proposal=CredentialPreview.deserialize( - {"attributes": [{"name": "attr", "value": "value"}]} - ), - cred_def_id=CRED_DEF_ID, - schema_id=SCHEMA_ID, - ).serialize(), - initiator=V10CredentialExchange.INITIATOR_SELF, - role=V10CredentialExchange.ROLE_ISSUER, - state=V10CredentialExchange.STATE_REQUEST_RECEIVED, - thread_id=thread_id, - new_with_id=True, - ) - await stored_exchange.save(self.session) - - issuer = async_mock.MagicMock() - cred = {"indy": "credential"} - issuer.create_credential = async_mock.CoroutineMock( - side_effect=test_module.IndyIssuerRevocationRegistryFullError("Nope") - ) - self.context.injector.bind_instance(IndyIssuer, issuer) - self.context.injector.bind_instance( - IndyLedgerRequestsExecutor, - async_mock.MagicMock( - get_ledger_for_identifier=async_mock.CoroutineMock( - return_value=("test_ledger_id", self.ledger) - ) - ), - ) with async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec - revoc_reg_id=REV_REG_ID, - set_state=async_mock.CoroutineMock(), - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - get_or_fetch_local_tails_path=( - async_mock.CoroutineMock() - ), - ) - ), - ) - ) + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + return_value=None ) - - with self.assertRaises(test_module.IndyIssuerRevocationRegistryFullError): + with self.assertRaises(CredentialManagerError) as context: await self.manager.issue_credential( stored_exchange, comment=comment, retries=1 ) + assert "has no active revocation registry" in context.message async def test_receive_credential(self): connection_id = "test_conn_id" diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py index c6f4c62cdf..fbd2882b7d 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py @@ -324,6 +324,8 @@ async def issue_credential( self, cred_ex_record: V20CredExRecord, retries: int = 5 ) -> CredFormatAttachment: """Issue indy credential.""" + await self._check_uniqueness(cred_ex_record.cred_ex_id) + cred_offer = cred_ex_record.cred_offer.attachment(IndyCredFormatHandler.format) cred_request = cred_ex_record.cred_request.attachment( IndyCredFormatHandler.format @@ -359,8 +361,6 @@ async def issue_credential( ) await asyncio.sleep(2) - await self._check_uniqueness(cred_ex_record.cred_ex_id) - if revocable: revoc = IndyRevocation(self.profile) registry_info = await revoc.get_or_create_active_registry(cred_def_id) @@ -396,7 +396,7 @@ async def issue_credential( async with self._profile.session() as session: await detail_record.save(session, reason="v2.0 issue credential") - if rev_reg and rev_reg.max_creds <= int(cred_rev_id): + if revocable and rev_reg.max_creds <= int(cred_rev_id): revoc = IndyRevocation(self.profile) await revoc.handle_full_registry(rev_reg_id) del revoc diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py index c759e37e17..e97fd6d457 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py @@ -5,6 +5,7 @@ from asynctest import TestCase as AsyncTestCase from asynctest import mock as async_mock from marshmallow import ValidationError +from more_itertools import side_effect from .. import handler as test_module @@ -678,7 +679,7 @@ async def test_receive_request_no_offer(self): in str(context.exception) ) - async def test_issue_credential(self): + async def test_issue_credential_revocable(self): attr_values = { "legalName": "value", "jurisdictionId": "value", @@ -729,22 +730,17 @@ async def test_issue_credential(self): with async_mock.patch.object( test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( - asyncio, "ensure_future", autospec=True - ) as asyncio_mock: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec + ) as revoc: + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + return_value=( + async_mock.MagicMock( # active_rev_reg_rec revoc_reg_id=REV_REG_ID, - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - get_or_fetch_local_tails_path=( - async_mock.CoroutineMock() - ), - ) - ), - ) + ), + async_mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(async_mock.CoroutineMock()), + max_creds=10, + ), ) ) @@ -870,109 +866,6 @@ async def test_issue_credential_not_unique_x(self): assert "indy detail record already exists" in str(context.exception) - async def test_issue_credential_fills_revocation_registry(self): - attr_values = { - "legalName": "value", - "jurisdictionId": "value", - "incorporationDate": "value", - } - cred_rev_id = "1000" - - cred_preview = V20CredPreview( - attributes=[ - V20CredAttrSpec(name=k, value=v) for (k, v) in attr_values.items() - ] - ) - cred_offer = V20CredOffer( - credential_preview=cred_preview, - formats=[ - V20CredFormat( - attach_id="0", - format_=ATTACHMENT_FORMAT[CRED_20_OFFER][ - V20CredFormat.Format.INDY.api - ], - ) - ], - offers_attach=[AttachDecorator.data_base64(INDY_OFFER, ident="0")], - ) - cred_request = V20CredRequest( - formats=[ - V20CredFormat( - attach_id="0", - format_=ATTACHMENT_FORMAT[CRED_20_REQUEST][ - V20CredFormat.Format.INDY.api - ], - ) - ], - requests_attach=[AttachDecorator.data_base64(INDY_CRED_REQ, ident="0")], - ) - - cred_ex_record = V20CredExRecord( - cred_ex_id="dummy-cxid", - cred_offer=cred_offer.serialize(), - cred_request=cred_request.serialize(), - initiator=V20CredExRecord.INITIATOR_SELF, - role=V20CredExRecord.ROLE_ISSUER, - state=V20CredExRecord.STATE_REQUEST_RECEIVED, - ) - - self.issuer.create_credential = async_mock.CoroutineMock( - return_value=(json.dumps(INDY_CRED), cred_rev_id) - ) - - with async_mock.patch.object( - test_module, "IndyRevocation", autospec=True - ) as revoc, async_mock.patch.object( - asyncio, "ensure_future", autospec=True - ) as asyncio_mock: - revoc.return_value = async_mock.MagicMock( - get_active_issuer_rev_reg_record=( - async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec - revoc_reg_id=REV_REG_ID, - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - max_creds=1000, - get_or_fetch_local_tails_path=( - async_mock.CoroutineMock() - ), - ) - ), - set_state=async_mock.CoroutineMock(), - ) - ) - ), - init_issuer_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) - ), - ) - - (cred_format, attachment) = await self.handler.issue_credential( - cred_ex_record, retries=0 - ) - - self.issuer.create_credential.assert_called_once_with( - SCHEMA, - INDY_OFFER, - INDY_CRED_REQ, - attr_values, - cred_ex_record.cred_ex_id, - REV_REG_ID, - "dummy-path", - ) - - # assert identifier match - assert cred_format.attach_id == self.handler.format.api == attachment.ident - - # assert content of attachment is proposal data - assert attachment.content == INDY_CRED - - # assert data is encoded as base64 - assert attachment.data.base64 - async def test_issue_credential_no_active_rr_no_retries(self): attr_values = { "legalName": "value", @@ -1024,22 +917,11 @@ async def test_issue_credential_no_active_rr_no_retries(self): ) with async_mock.patch.object( - test_module, "IssuerRevRegRecord", autospec=True - ) as issuer_rr_rec, async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock(side_effect=StorageNotFoundError()) - ) - revoc.return_value.init_issuer_registry = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) - ) - issuer_rr_rec.query_by_cred_def_id = async_mock.CoroutineMock( - return_value=[] + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + return_value=() ) - with self.assertRaises(V20CredFormatError) as context: await self.handler.issue_credential(cred_ex_record, retries=0) assert "has no active revocation registry" in str(context.exception) @@ -1095,24 +977,22 @@ async def test_issue_credential_no_active_rr_retry(self): ) with async_mock.patch.object( - test_module, "IssuerRevRegRecord", autospec=True - ) as issuer_rr_rec, async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock(side_effect=StorageNotFoundError()) - ) - issuer_rr_rec.query_by_cred_def_id = async_mock.CoroutineMock( + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( side_effect=[ - [], # posted_rev_reg_recs - [async_mock.MagicMock(max_cred_num=1000)], # old_rev_reg_recs + None, + ( + async_mock.MagicMock( # active_rev_reg_rec + revoc_reg_id=REV_REG_ID, + set_state=async_mock.CoroutineMock(), + ), + async_mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(async_mock.CoroutineMock()), + ), + ), ] - * 2 - ) - revoc.return_value.init_issuer_registry = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # pending_rev_reg_rec - stage_pending_registry=async_mock.CoroutineMock() - ) ) with self.assertRaises(V20CredFormatError) as context: @@ -1171,25 +1051,22 @@ async def test_issue_credential_rr_full(self): with async_mock.patch.object( test_module, "IndyRevocation", autospec=True ) as revoc: - revoc.return_value.get_active_issuer_rev_reg_record = ( - async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # active_rev_reg_rec + revoc.return_value.get_or_create_active_registry = async_mock.CoroutineMock( + return_value=( + async_mock.MagicMock( # active_rev_reg_rec revoc_reg_id=REV_REG_ID, set_state=async_mock.CoroutineMock(), - get_registry=async_mock.CoroutineMock( - return_value=async_mock.MagicMock( # rev_reg - tails_local_path="dummy-path", - get_or_fetch_local_tails_path=( - async_mock.CoroutineMock() - ), - ) - ), - ) + ), + async_mock.MagicMock( # rev_reg + tails_local_path="dummy-path", + get_or_fetch_local_tails_path=(async_mock.CoroutineMock()), + ), ) ) - with self.assertRaises(test_module.IndyIssuerRevocationRegistryFullError): + with self.assertRaises(V20CredFormatError) as context: await self.handler.issue_credential(cred_ex_record, retries=1) + assert "has no active revocation registry" in str(context.exception) async def test_receive_credential(self): cred_ex_record = async_mock.MagicMock() diff --git a/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py index 34048f0423..f0347939e4 100644 --- a/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py @@ -65,7 +65,7 @@ async def setUp(self): TailsServer = async_mock.MagicMock(BaseTailsServer, autospec=True) self.tails_server = TailsServer() self.tails_server.upload_tails_file = async_mock.CoroutineMock( - return_value=(False, "Internal Server Error") + return_value=(True, None) ) self.profile.context.injector.bind_instance(BaseTailsServer, self.tails_server) @@ -124,6 +124,11 @@ async def test_generate_registry_etc(self): assert rec.state == IssuerRevRegRecord.STATE_POSTED self.ledger.send_revoc_reg_def.assert_called_once() + with async_mock.patch.object(test_module.Path, "is_file", lambda _: True): + await rec.upload_tails_file(self.profile) + assert rec.state == IssuerRevRegRecord.STATE_UPLOADED + self.tails_server.upload_tails_file.assert_called_once() + await rec.send_entry(self.profile) assert rec.state == IssuerRevRegRecord.STATE_ACTIVE self.ledger.send_revoc_reg_entry.assert_called_once() diff --git a/aries_cloudagent/revocation/tests/test_routes.py b/aries_cloudagent/revocation/tests/test_routes.py index f004d0c90b..ace6f54e9d 100644 --- a/aries_cloudagent/revocation/tests/test_routes.py +++ b/aries_cloudagent/revocation/tests/test_routes.py @@ -3,25 +3,18 @@ from asynctest import mock as async_mock from aries_cloudagent.core.in_memory import InMemoryProfile +from aries_cloudagent.revocation.error import RevocationError -from ...admin.request_context import AdminRequestContext from ...storage.in_memory import InMemoryStorage -from ...tails.base import BaseTailsServer from .. import routes as test_module class TestRevocationRoutes(AsyncTestCase): def setUp(self): - TailsServer = async_mock.MagicMock(BaseTailsServer, autospec=True) - self.tails_server = TailsServer() - self.tails_server.upload_tails_file = async_mock.CoroutineMock( - return_value=(True, None) - ) self.profile = InMemoryProfile.test_profile() self.context = self.profile.context setattr(self.context, "profile", self.profile) - self.context.injector.bind_instance(BaseTailsServer, self.tails_server) self.request_dict = { "context": self.context, "outbound_message_router": async_mock.CoroutineMock(), @@ -539,34 +532,32 @@ async def test_get_tails_file_not_found(self): result = await test_module.get_tails_file(self.request) mock_file_response.assert_not_called() - async def test_upload_tails_file(self): + async def test_upload_tails_file_basic(self): REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( self.test_did, self.test_did ) self.request.match_info = {"rev_reg_id": REV_REG_ID} with async_mock.patch.object( - test_module, "tails_path", async_mock.MagicMock() - ) as mock_tails_path, async_mock.patch.object( + test_module, "IndyRevocation", autospec=True + ) as mock_indy_revoc, async_mock.patch.object( test_module.web, "json_response", async_mock.Mock() ) as mock_json_response: - mock_tails_path.return_value = f"/tmp/tails/{REV_REG_ID}" - + mock_upload = async_mock.CoroutineMock() + mock_indy_revoc.return_value = async_mock.MagicMock( + get_issuer_rev_reg_record=async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + tails_local_path=f"/tmp/tails/{REV_REG_ID}", + has_local_tails_file=True, + upload_tails_file=mock_upload, + ) + ) + ) result = await test_module.upload_tails_file(self.request) + mock_upload.assert_awaited_once() mock_json_response.assert_called_once_with({}) assert result is mock_json_response.return_value - async def test_upload_tails_file_no_tails_server(self): - REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( - self.test_did, self.test_did - ) - self.request.match_info = {"rev_reg_id": REV_REG_ID} - - self.context.injector.clear_binding(BaseTailsServer) - - with self.assertRaises(test_module.web.HTTPForbidden): - await test_module.upload_tails_file(self.request) - async def test_upload_tails_file_no_local_tails_file(self): REV_REG_ID = "{}:4:{}:3:CL:1234:default:CL_ACCUM:default".format( self.test_did, self.test_did @@ -574,9 +565,16 @@ async def test_upload_tails_file_no_local_tails_file(self): self.request.match_info = {"rev_reg_id": REV_REG_ID} with async_mock.patch.object( - test_module, "tails_path", async_mock.MagicMock() - ) as mock_tails_path: - mock_tails_path.return_value = None + test_module, "IndyRevocation", autospec=True + ) as mock_indy_revoc: + mock_indy_revoc.return_value = async_mock.MagicMock( + get_issuer_rev_reg_record=async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + tails_local_path=f"/tmp/tails/{REV_REG_ID}", + has_local_tails_file=False, + ) + ) + ) with self.assertRaises(test_module.web.HTTPNotFound): await test_module.upload_tails_file(self.request) @@ -587,17 +585,20 @@ async def test_upload_tails_file_fail(self): ) self.request.match_info = {"rev_reg_id": REV_REG_ID} - TailsServer = async_mock.MagicMock(BaseTailsServer, autospec=True) - self.tails_server = TailsServer() - self.tails_server.upload_tails_file = async_mock.CoroutineMock( - return_value=(False, "Internal Server Error") - ) - self.context.injector.clear_binding(BaseTailsServer) - self.context.injector.bind_instance(BaseTailsServer, self.tails_server) - with async_mock.patch.object( - test_module, "tails_path", async_mock.MagicMock() - ) as mock_tails_path: + test_module, "IndyRevocation", autospec=True + ) as mock_indy_revoc: + mock_upload = async_mock.CoroutineMock(side_effect=RevocationError("test")) + mock_indy_revoc.return_value = async_mock.MagicMock( + get_issuer_rev_reg_record=async_mock.CoroutineMock( + return_value=async_mock.MagicMock( + tails_local_path=f"/tmp/tails/{REV_REG_ID}", + has_local_tails_file=True, + upload_tails_file=mock_upload, + ) + ) + ) + with self.assertRaises(test_module.web.HTTPInternalServerError): await test_module.upload_tails_file(self.request) From 252a9b3fe048a237266874d16b833c3c9e22e50c Mon Sep 17 00:00:00 2001 From: Andrew Whitehead Date: Fri, 17 Jun 2022 17:10:06 -0700 Subject: [PATCH 3/5] detect when send_entry has been called Signed-off-by: Andrew Whitehead --- demo/features/steps/0586-sign-transaction.py | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/demo/features/steps/0586-sign-transaction.py b/demo/features/steps/0586-sign-transaction.py index 5f3868f830..04b6249598 100644 --- a/demo/features/steps/0586-sign-transaction.py +++ b/demo/features/steps/0586-sign-transaction.py @@ -366,8 +366,20 @@ def step_impl(context, agent_name): def step_impl(context, agent_name): agent = context.active_agents[agent_name] - # TODO not sure what to check here, let's just do a short pause - async_sleep(2.0) + # a registry is promoted to active when its initial entry is sent + i = 5 + while i > 0: + async_sleep(1.0) + reg_info = agent_container_GET( + agent["agent"], + f"/revocation/registry/{context.rev_reg_id}", + ) + state = reg_info["result"]["state"] + if state == "active": + return + i = i - 1 + + assert False @when( From 5cd0a4e4921baf3abdaa17ea6a90fdd70c214a59 Mon Sep 17 00:00:00 2001 From: Andrew Whitehead Date: Fri, 17 Jun 2022 17:10:56 -0700 Subject: [PATCH 4/5] revert uploaded state for IssuerRevRegRecord; fix interaction with transaction endorsement Signed-off-by: Andrew Whitehead --- .../endorse_transaction/v1_0/manager.py | 22 +++------- aries_cloudagent/revocation/indy.py | 14 ++++--- .../models/issuer_rev_reg_record.py | 14 +++---- .../tests/test_issuer_rev_reg_record.py | 7 +++- aries_cloudagent/revocation/routes.py | 42 +++++++++++-------- aries_cloudagent/revocation/util.py | 11 ++++- aries_cloudagent/tails/indy_tails_server.py | 24 ++++++----- aries_cloudagent/tails/tests/test_indy.py | 12 ++++-- 8 files changed, 80 insertions(+), 66 deletions(-) diff --git a/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py b/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py index da95091be4..8ce93c45a8 100644 --- a/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py +++ b/aries_cloudagent/protocols/endorse_transaction/v1_0/manager.py @@ -15,8 +15,8 @@ from ....messaging.credential_definitions.util import notify_cred_def_event from ....messaging.schemas.util import notify_schema_event from ....revocation.util import ( - notify_revocation_entry_event, notify_revocation_reg_endorsed_event, + notify_revocation_entry_endorsed_event, ) from ....storage.error import StorageError, StorageNotFoundError from ....transport.inbound.receipt import MessageReceipt @@ -777,30 +777,18 @@ async def endorsed_txn_post_processing( # revocation registry transaction rev_reg_id = ledger_response["result"]["txnMetadata"]["txnId"] meta_data["context"]["rev_reg_id"] = rev_reg_id - auto_create_rev_reg = meta_data["processing"].get( - "auto_create_rev_reg", False + await notify_revocation_reg_endorsed_event( + self._profile, rev_reg_id, meta_data ) - # If "auto_processing" is enabled, also create the revocation entry record - if auto_create_rev_reg: - await notify_revocation_entry_event( - self._profile, rev_reg_id, meta_data - ) - elif ledger_response["result"]["txn"]["type"] == "114": # revocation entry transaction rev_reg_id = ledger_response["result"]["txn"]["data"]["revocRegDefId"] meta_data["context"]["rev_reg_id"] = rev_reg_id - auto_create_rev_reg = meta_data["processing"].get( - "auto_create_rev_reg", False + await notify_revocation_entry_endorsed_event( + self._profile, rev_reg_id, meta_data ) - # If "auto_processing" is enabled, also upload tails file for this registry - if auto_create_rev_reg: - await notify_revocation_reg_endorsed_event( - self._profile, rev_reg_id, meta_data - ) - elif ledger_response["result"]["txn"]["type"] == "1": # write DID to ledger did = ledger_response["result"]["txn"]["data"]["dest"] diff --git a/aries_cloudagent/revocation/indy.py b/aries_cloudagent/revocation/indy.py index 9e4562e88c..e010fe4534 100644 --- a/aries_cloudagent/revocation/indy.py +++ b/aries_cloudagent/revocation/indy.py @@ -43,6 +43,7 @@ async def init_issuer_registry( tag: str = None, create_pending_rev_reg: bool = False, endorser_connection_id: str = None, + notify: bool = True, ) -> "IssuerRevRegRecord": """Create a new revocation registry record for a credential definition.""" multitenant_mgr = self._profile.inject_or(BaseMultitenantManager) @@ -86,12 +87,13 @@ async def init_issuer_registry( if not endorser_connection_id: raise RevocationError(reason="Endorser connection not found") - await notify_revocation_reg_init_event( - self._profile, - record.record_id, - create_pending_rev_reg=create_pending_rev_reg, - endorser_connection_id=endorser_connection_id, - ) + if notify: + await notify_revocation_reg_init_event( + self._profile, + record.record_id, + create_pending_rev_reg=create_pending_rev_reg, + endorser_connection_id=endorser_connection_id, + ) return record diff --git a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py index d95a5f938b..98451dcb3d 100644 --- a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py @@ -67,7 +67,6 @@ class Meta: STATE_INIT = "init" STATE_GENERATED = "generated" STATE_POSTED = "posted" # definition published - STATE_UPLOADED = "uploaded" # tails file uploaded STATE_ACTIVE = "active" # initial entry published, possibly subsequent entries STATE_FULL = "full" # includes corrupt @@ -279,7 +278,7 @@ async def send_entry( self._check_url(self.tails_public_uri) if self.state not in ( - IssuerRevRegRecord.STATE_UPLOADED, + IssuerRevRegRecord.STATE_POSTED, IssuerRevRegRecord.STATE_ACTIVE, IssuerRevRegRecord.STATE_FULL, # can still publish revocation deltas ): @@ -299,7 +298,7 @@ async def send_entry( write_ledger=write_ledger, endorser_did=endorser_did, ) - if self.state == IssuerRevRegRecord.STATE_UPLOADED: + if self.state == IssuerRevRegRecord.STATE_POSTED: self.state = IssuerRevRegRecord.STATE_ACTIVE # initial entry activates async with profile.session() as session: await self.save( @@ -321,7 +320,7 @@ async def upload_tails_file(self, profile: Profile): if not self.has_local_tails_file: raise RevocationError("Local tails file not found") - (upload_success, reason) = await tails_server.upload_tails_file( + (upload_success, result) = await tails_server.upload_tails_file( profile.context, self.revoc_reg_id, self.tails_local_path, @@ -331,12 +330,9 @@ async def upload_tails_file(self, profile: Profile): ) if not upload_success: raise RevocationError( - f"Tails file for rev reg {self.revoc_reg_id} failed to upload: {reason}" + f"Tails file for rev reg {self.revoc_reg_id} failed to upload: {result}" ) - - self.state = IssuerRevRegRecord.STATE_UPLOADED - async with profile.session() as session: - await self.save(session, reason="Uploaded tails file") + await self.set_tails_file_public_uri(profile, result) async def mark_pending(self, session: ProfileSession, cred_rev_id: str) -> None: """Mark a credential revocation id as revoked pending publication to ledger. diff --git a/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py index f0347939e4..8154884aeb 100644 --- a/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/tests/test_issuer_rev_reg_record.py @@ -65,7 +65,7 @@ async def setUp(self): TailsServer = async_mock.MagicMock(BaseTailsServer, autospec=True) self.tails_server = TailsServer() self.tails_server.upload_tails_file = async_mock.CoroutineMock( - return_value=(True, None) + return_value=(True, "http://1.2.3.4:8088/rev-reg-id") ) self.profile.context.injector.bind_instance(BaseTailsServer, self.tails_server) @@ -126,7 +126,10 @@ async def test_generate_registry_etc(self): with async_mock.patch.object(test_module.Path, "is_file", lambda _: True): await rec.upload_tails_file(self.profile) - assert rec.state == IssuerRevRegRecord.STATE_UPLOADED + assert ( + rec.tails_public_uri + and rec.revoc_reg_def.value.tails_location == rec.tails_public_uri + ) self.tails_server.upload_tails_file.assert_called_once() await rec.send_entry(self.profile) diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index b54547d63f..637332d55a 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -550,6 +550,7 @@ async def create_rev_reg(request: web.BaseRequest): issuer_rev_reg_rec = await revoc.init_issuer_registry( credential_definition_id, max_cred_num=max_cred_num, + notify=False, ) except RevocationNotSupportedError as e: raise web.HTTPBadRequest(reason=e.message) from e @@ -1128,17 +1129,16 @@ async def send_rev_reg_entry(request: web.BaseRequest): raise web.HTTPBadRequest(reason="No endorser connection found") if not write_ledger: - try: - async with profile.session() as session: + async with profile.session() as session: + try: connection_record = await ConnRecord.retrieve_by_id( session, connection_id ) - except StorageNotFoundError as err: - raise web.HTTPNotFound(reason=err.roll_up) from err - except BaseModelError as err: - raise web.HTTPBadRequest(reason=err.roll_up) from err + except StorageNotFoundError as err: + raise web.HTTPNotFound(reason=err.roll_up) from err + except BaseModelError as err: + raise web.HTTPBadRequest(reason=err.roll_up) from err - async with profile.session() as session: endorser_info = await connection_record.metadata_get( session, "endorser_info" ) @@ -1295,14 +1295,16 @@ async def on_revocation_registry_init_event(profile: Profile, event: Event): meta_data = event.payload if "endorser" in meta_data: # TODO error handling - for now just let exceptions get raised + endorser_connection_id = meta_data["endorser"]["connection_id"] async with profile.session() as session: connection = await ConnRecord.retrieve_by_id( - session, meta_data["endorser"]["connection_id"] + session, endorser_connection_id ) endorser_info = await connection.metadata_get(session, "endorser_info") endorser_did = endorser_info["endorser_did"] write_ledger = False else: + endorser_connection_id = None endorser_did = None write_ledger = True @@ -1313,10 +1315,8 @@ async def on_revocation_registry_init_event(profile: Profile, event: Event): # Generate the registry and upload the tails file async def generate(rr_record: IssuerRevRegRecord) -> dict: await rr_record.generate_registry(profile) - await rr_record.set_tails_file_public_uri( - profile, - f"{tails_base_url}/{registry_record.revoc_reg_id}", - ) + public_uri = tails_base_url.rstrip("/") + f"/{registry_record.revoc_reg_id}" + await rr_record.set_tails_file_public_uri(profile, public_uri) rev_reg_resp = await rr_record.send_def( profile, write_ledger=write_ledger, @@ -1381,6 +1381,7 @@ async def generate(rr_record: IssuerRevRegRecord) -> dict: registry_record.cred_def_id, registry_record.max_cred_num, registry_record.revoc_def_type, + endorser_connection_id=endorser_connection_id, ) @@ -1450,18 +1451,25 @@ async def on_revocation_entry_event(profile: Profile, event: Event): async def on_revocation_registry_endorsed_event(profile: Profile, event: Event): - """Handle revocation tails file event.""" + """Handle revocation registry endorsement event.""" meta_data = event.payload rev_reg_id = meta_data["context"]["rev_reg_id"] revoc = IndyRevocation(profile) registry_record = await revoc.get_issuer_rev_reg_record(rev_reg_id) - # NOTE: if there are multiple pods, then the one processing this - # event may not be the one that generated the tails file. - await registry_record.upload_tails_file(profile) + + if profile.settings.get_value("endorser.auto_request"): + # NOTE: if there are multiple pods, then the one processing this + # event may not be the one that generated the tails file. + await registry_record.upload_tails_file(profile) + + # Post the initial revocation entry + await notify_revocation_entry_event( + profile, registry_record.record_id, meta_data + ) # create a "pending" registry if one is requested # (this is done automatically when creating a credential definition, so that when a - # revocation registry fills up, we ca continue to issue credentials without a + # revocation registry fills up, we can continue to issue credentials without a # delay) create_pending_rev_reg = meta_data["processing"].get( "create_pending_rev_reg", False diff --git a/aries_cloudagent/revocation/util.py b/aries_cloudagent/revocation/util.py index f81e26dc4a..df40a17630 100644 --- a/aries_cloudagent/revocation/util.py +++ b/aries_cloudagent/revocation/util.py @@ -10,7 +10,8 @@ EVENT_LISTENER_PATTERN = re.compile(f"^{REVOCATION_EVENT_PREFIX}(.*)?$") REVOCATION_REG_INIT_EVENT = "REGISTRY_INIT" REVOCATION_REG_ENDORSED_EVENT = "REGISTRY_ENDORSED" -REVOCATION_ENTRY_EVENT = "ENTRY" +REVOCATION_ENTRY_ENDORSED_EVENT = "ENTRY_ENDORSED" +REVOCATION_ENTRY_EVENT = "SEND_ENTRY" REVOCATION_PUBLISHED_EVENT = "published" REVOCATION_CLEAR_PENDING_EVENT = "clear-pending" @@ -50,6 +51,14 @@ async def notify_revocation_reg_endorsed_event( await profile.notify(topic, meta_data) +async def notify_revocation_entry_endorsed_event( + profile: Profile, rev_reg_id: str, meta_data: dict +): + """Send notification for a revocation registry entry endorsement event.""" + topic = f"{REVOCATION_EVENT_PREFIX}{REVOCATION_ENTRY_ENDORSED_EVENT}::{rev_reg_id}" + await profile.notify(topic, meta_data) + + async def notify_revocation_published_event( profile: Profile, rev_reg_id: str, diff --git a/aries_cloudagent/tails/indy_tails_server.py b/aries_cloudagent/tails/indy_tails_server.py index 9f07970a42..0c5ebb6ab4 100644 --- a/aries_cloudagent/tails/indy_tails_server.py +++ b/aries_cloudagent/tails/indy_tails_server.py @@ -1,8 +1,9 @@ """Indy tails server interface class.""" -from typing import Tuple import logging +from typing import Tuple + from ..config.injection_context import InjectionContext from ..ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager from ..utils.http import put_file, PutError @@ -58,17 +59,18 @@ async def upload_tails_file( "tails_server_upload_url setting is not set" ) + upload_url = tails_server_upload_url.rstrip("/") + f"/{rev_reg_id}" + try: - return ( - True, - await put_file( - f"{tails_server_upload_url}/{rev_reg_id}", - {"tails": tails_file_path}, - {"genesis": genesis_transactions}, - interval=interval, - backoff=backoff, - max_attempts=max_attempts, - ), + await put_file( + upload_url, + {"tails": tails_file_path}, + {"genesis": genesis_transactions}, + interval=interval, + backoff=backoff, + max_attempts=max_attempts, ) except PutError as x_put: return (False, x_put.message) + + return True, upload_url diff --git a/aries_cloudagent/tails/tests/test_indy.py b/aries_cloudagent/tails/tests/test_indy.py index ca9d1d6729..65d026a59e 100644 --- a/aries_cloudagent/tails/tests/test_indy.py +++ b/aries_cloudagent/tails/tests/test_indy.py @@ -38,7 +38,9 @@ async def test_upload(self): "/tmp/dummy/path", ) assert ok - assert text == "tails-hash" + assert ( + text == context.settings["tails_server_upload_url"] + "/" + REV_REG_ID + ) async def test_upload_indy_sdk(self): profile = InMemoryProfile.test_profile() @@ -68,7 +70,9 @@ async def test_upload_indy_sdk(self): "/tmp/dummy/path", ) assert ok - assert text == "tails-hash" + assert ( + text == profile.settings["tails_server_upload_url"] + "/" + REV_REG_ID + ) async def test_upload_indy_vdr(self): profile = InMemoryProfile.test_profile() @@ -98,7 +102,9 @@ async def test_upload_indy_vdr(self): "/tmp/dummy/path", ) assert ok - assert text == "tails-hash" + assert ( + text == profile.settings["tails_server_upload_url"] + "/" + REV_REG_ID + ) async def test_upload_x(self): context = InjectionContext( From 8bfa60bd2082a23bd7140a072dffd270519df2c1 Mon Sep 17 00:00:00 2001 From: Andrew Whitehead Date: Tue, 21 Jun 2022 11:37:00 -0700 Subject: [PATCH 5/5] move IssuerCredRevRecord creation to issue-credential manager Signed-off-by: Andrew Whitehead --- aries_cloudagent/indy/credx/issuer.py | 18 -- .../indy/credx/tests/test_cred_issuance.py | 2 - aries_cloudagent/indy/issuer.py | 2 - aries_cloudagent/indy/sdk/issuer.py | 19 -- .../indy/sdk/tests/test_issuer.py | 234 +++++++----------- .../issue_credential/v1_0/manager.py | 24 +- .../v1_0/tests/test_manager.py | 3 - .../v2_0/formats/indy/handler.py | 50 ++-- .../v2_0/formats/indy/tests/test_handler.py | 2 - aries_cloudagent/revocation/manager.py | 53 ++-- .../models/issuer_cred_rev_record.py | 10 + .../revocation/tests/test_manager.py | 42 +++- aries_cloudagent/storage/in_memory.py | 3 +- 13 files changed, 228 insertions(+), 234 deletions(-) diff --git a/aries_cloudagent/indy/credx/issuer.py b/aries_cloudagent/indy/credx/issuer.py index cc996ae3f6..b4a4524948 100644 --- a/aries_cloudagent/indy/credx/issuer.py +++ b/aries_cloudagent/indy/credx/issuer.py @@ -28,8 +28,6 @@ DEFAULT_CRED_DEF_TAG, DEFAULT_SIGNATURE_TYPE, ) -from ...revocation.models.issuer_cred_rev_record import IssuerCredRevRecord - LOGGER = logging.getLogger(__name__) @@ -225,7 +223,6 @@ async def create_credential( credential_offer: dict, credential_request: dict, credential_values: dict, - cred_ex_id: str, revoc_reg_id: str = None, tails_file_path: str = None, ) -> Tuple[str, str]: @@ -237,7 +234,6 @@ async def create_credential( credential_offer: Credential Offer to create credential for credential_request: Credential request to create credential for credential_values: Values to go in credential - cred_ex_id: credential exchange identifier to use in issuer cred rev rec revoc_reg_id: ID of the revocation registry tails_file_path: The location of the tails file @@ -324,20 +320,6 @@ async def create_credential( await txn.handle.replace( CATEGORY_REV_REG_INFO, revoc_reg_id, value_json=rev_info ) - - issuer_cr_rec = IssuerCredRevRecord( - state=IssuerCredRevRecord.STATE_ISSUED, - cred_ex_id=cred_ex_id, - rev_reg_id=revoc_reg_id, - cred_rev_id=str(rev_reg_index), - ) - await issuer_cr_rec.save( - txn, - reason=( - "Created issuer cred rev record for " - f"rev reg id {revoc_reg_id}, {rev_reg_index}" - ), - ) await txn.commit() except AskarError as err: raise IndyIssuerError( diff --git a/aries_cloudagent/indy/credx/tests/test_cred_issuance.py b/aries_cloudagent/indy/credx/tests/test_cred_issuance.py index 0c23eda904..027c16b94d 100644 --- a/aries_cloudagent/indy/credx/tests/test_cred_issuance.py +++ b/aries_cloudagent/indy/credx/tests/test_cred_issuance.py @@ -137,7 +137,6 @@ async def test_issue_store_non_rev(self): cred_offer, cred_req, {"name": "NAME", "moniker": "MONIKER"}, - cred_ex_id="cred_ex_id", revoc_reg_id=None, tails_file_path=None, ) @@ -255,7 +254,6 @@ async def test_issue_store_rev(self): cred_offer, cred_req, {"name": "NAME", "moniker": "MONIKER"}, - cred_ex_id="cred_ex_id", revoc_reg_id=reg_id, tails_file_path=tails_path, ) diff --git a/aries_cloudagent/indy/issuer.py b/aries_cloudagent/indy/issuer.py index 3503626f72..d889746d41 100644 --- a/aries_cloudagent/indy/issuer.py +++ b/aries_cloudagent/indy/issuer.py @@ -122,7 +122,6 @@ async def create_credential( credential_offer: dict, credential_request: dict, credential_values: dict, - cred_ex_id: str, revoc_reg_id: str = None, tails_file_path: str = None, ) -> Tuple[str, str]: @@ -134,7 +133,6 @@ async def create_credential( credential_offer: Credential Offer to create credential for credential_request: Credential request to create credential for credential_values: Values to go in credential - cred_ex_id: credential exchange identifier to use in issuer cred rev rec revoc_reg_id: ID of the revocation registry tails_file_path: The location of the tails file diff --git a/aries_cloudagent/indy/sdk/issuer.py b/aries_cloudagent/indy/sdk/issuer.py index 72f569f2ee..3d84473c93 100644 --- a/aries_cloudagent/indy/sdk/issuer.py +++ b/aries_cloudagent/indy/sdk/issuer.py @@ -10,7 +10,6 @@ from ...indy.sdk.profile import IndySdkProfile from ...messaging.util import encode -from ...revocation.models.issuer_cred_rev_record import IssuerCredRevRecord from ...storage.error import StorageError from ..issuer import ( @@ -162,7 +161,6 @@ async def create_credential( credential_offer: dict, credential_request: dict, credential_values: dict, - cred_ex_id: str, rev_reg_id: str = None, tails_file_path: str = None, ) -> Tuple[str, str]: @@ -174,7 +172,6 @@ async def create_credential( credential_offer: Credential Offer to create credential for credential_request: Credential request to create credential for credential_values: Values to go in credential - cred_ex_id: credential exchange identifier to use in issuer cred rev rec rev_reg_id: ID of the revocation registry tails_file_path: Path to the local tails file @@ -219,22 +216,6 @@ async def create_credential( rev_reg_id, tails_reader_handle, ) - - if cred_rev_id: - issuer_cr_rec = IssuerCredRevRecord( - state=IssuerCredRevRecord.STATE_ISSUED, - cred_ex_id=cred_ex_id, - rev_reg_id=rev_reg_id, - cred_rev_id=cred_rev_id, - ) - async with self.profile.session() as session: - await issuer_cr_rec.save( - session, - reason=( - "Created issuer cred rev record for " - f"rev reg id {rev_reg_id}, {cred_rev_id}" - ), - ) except AnoncredsRevocationRegistryFullError: LOGGER.warning( "Revocation registry %s is full: cannot create credential", diff --git a/aries_cloudagent/indy/sdk/tests/test_issuer.py b/aries_cloudagent/indy/sdk/tests/test_issuer.py index a1b76509a0..3b007e788e 100644 --- a/aries_cloudagent/indy/sdk/tests/test_issuer.py +++ b/aries_cloudagent/indy/sdk/tests/test_issuer.py @@ -167,58 +167,46 @@ async def test_create_revoke_credentials( for cr_id in test_cred_rev_ids ] - with async_mock.patch.object( - test_module, "IssuerCredRevRecord", async_mock.MagicMock() - ) as mock_issuer_cr_rec: - mock_issuer_cr_rec.return_value.save = async_mock.CoroutineMock() - mock_issuer_cr_rec.retrieve_by_ids = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( - set_state=async_mock.CoroutineMock(), - ) - ) - - with self.assertRaises(test_module.IndyIssuerError): # missing attribute - cred_json, revoc_id = await self.issuer.create_credential( - test_schema, - test_offer, - test_request, - {}, - "dummy-cxid", - ) - - (cred_json, cred_rev_id) = await self.issuer.create_credential( # main line + with self.assertRaises(test_module.IndyIssuerError): # missing attribute + cred_json, revoc_id = await self.issuer.create_credential( test_schema, test_offer, test_request, - test_values, - "dummy-cxid", - REV_REG_ID, - "/tmp/tails/path/dummy", - ) - mock_indy_create_credential.assert_called_once() - ( - call_wallet, - call_offer, - call_request, - call_values, - call_etc1, - call_etc2, - ) = mock_indy_create_credential.call_args[0] - assert call_wallet is self.wallet.handle - assert json.loads(call_offer) == test_offer - assert json.loads(call_request) == test_request - values = json.loads(call_values) - assert "attr1" in values - - mock_indy_revoke_credential.return_value = json.dumps(TEST_RR_DELTA) - mock_indy_merge_rr_deltas.return_value = json.dumps(TEST_RR_DELTA) - (result, failed) = await self.issuer.revoke_credentials( - REV_REG_ID, tails_file_path="dummy", cred_rev_ids=test_cred_rev_ids + {}, ) - assert json.loads(result) == TEST_RR_DELTA - assert not failed - assert mock_indy_revoke_credential.call_count == 2 - mock_indy_merge_rr_deltas.assert_called_once() + + (cred_json, cred_rev_id) = await self.issuer.create_credential( # main line + test_schema, + test_offer, + test_request, + test_values, + REV_REG_ID, + "/tmp/tails/path/dummy", + ) + mock_indy_create_credential.assert_called_once() + ( + call_wallet, + call_offer, + call_request, + call_values, + call_etc1, + call_etc2, + ) = mock_indy_create_credential.call_args[0] + assert call_wallet is self.wallet.handle + assert json.loads(call_offer) == test_offer + assert json.loads(call_request) == test_request + values = json.loads(call_values) + assert "attr1" in values + + mock_indy_revoke_credential.return_value = json.dumps(TEST_RR_DELTA) + mock_indy_merge_rr_deltas.return_value = json.dumps(TEST_RR_DELTA) + (result, failed) = await self.issuer.revoke_credentials( + REV_REG_ID, tails_file_path="dummy", cred_rev_ids=test_cred_rev_ids + ) + assert json.loads(result) == TEST_RR_DELTA + assert not failed + assert mock_indy_revoke_credential.call_count == 2 + mock_indy_merge_rr_deltas.assert_called_once() @async_mock.patch("indy.anoncreds.issuer_create_credential") @async_mock.patch.object(test_module, "create_tails_reader", autospec=True) @@ -267,73 +255,53 @@ async def test_create_revoke_credentials_x( test_offer, test_request, {}, - "dummy-cxid", ) - with async_mock.patch.object( - test_module, "IssuerCredRevRecord", async_mock.MagicMock() - ) as mock_issuer_cr_rec: - mock_issuer_cr_rec.return_value.save = async_mock.CoroutineMock( - side_effect=test_module.StorageError( - "could not store" # not fatal; maximize coverage - ) - ) - mock_issuer_cr_rec.retrieve_by_ids = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( - set_state=async_mock.CoroutineMock( - side_effect=test_module.StorageError( - "could not store" # not fatal; maximize coverage - ) - ), - ) - ) - - (cred_json, cred_rev_id) = await self.issuer.create_credential( # main line - test_schema, - test_offer, - test_request, - test_values, - "dummy-cxid", - REV_REG_ID, - "/tmp/tails/path/dummy", - ) - mock_indy_create_credential.assert_called_once() - ( - call_wallet, - call_offer, - call_request, - call_values, - call_etc1, - call_etc2, - ) = mock_indy_create_credential.call_args[0] - assert call_wallet is self.wallet.handle - assert json.loads(call_offer) == test_offer - assert json.loads(call_request) == test_request - values = json.loads(call_values) - assert "attr1" in values - - def mock_revoke(_h, _t, _r, cred_rev_id): - if cred_rev_id == "42": - return json.dumps(TEST_RR_DELTA) - if cred_rev_id == "54": - raise IndyError( - error_code=ErrorCode.AnoncredsInvalidUserRevocId, - error_details={"message": "already revoked"}, - ) + (cred_json, cred_rev_id) = await self.issuer.create_credential( # main line + test_schema, + test_offer, + test_request, + test_values, + REV_REG_ID, + "/tmp/tails/path/dummy", + ) + mock_indy_create_credential.assert_called_once() + ( + call_wallet, + call_offer, + call_request, + call_values, + call_etc1, + call_etc2, + ) = mock_indy_create_credential.call_args[0] + assert call_wallet is self.wallet.handle + assert json.loads(call_offer) == test_offer + assert json.loads(call_request) == test_request + values = json.loads(call_values) + assert "attr1" in values + + def mock_revoke(_h, _t, _r, cred_rev_id): + if cred_rev_id == "42": + return json.dumps(TEST_RR_DELTA) + if cred_rev_id == "54": raise IndyError( - error_code=ErrorCode.UnknownCryptoTypeError, - error_details={"message": "truly an outlier"}, + error_code=ErrorCode.AnoncredsInvalidUserRevocId, + error_details={"message": "already revoked"}, ) - - mock_indy_revoke_credential.side_effect = mock_revoke - mock_indy_merge_rr_deltas.return_value = json.dumps(TEST_RR_DELTA) - (result, failed) = await self.issuer.revoke_credentials( - REV_REG_ID, tails_file_path="dummy", cred_rev_ids=test_cred_rev_ids + raise IndyError( + error_code=ErrorCode.UnknownCryptoTypeError, + error_details={"message": "truly an outlier"}, ) - assert json.loads(result) == TEST_RR_DELTA - assert failed == ["54", "103"] - assert mock_indy_revoke_credential.call_count == 3 - mock_indy_merge_rr_deltas.assert_not_called() + + mock_indy_revoke_credential.side_effect = mock_revoke + mock_indy_merge_rr_deltas.return_value = json.dumps(TEST_RR_DELTA) + (result, failed) = await self.issuer.revoke_credentials( + REV_REG_ID, tails_file_path="dummy", cred_rev_ids=test_cred_rev_ids + ) + assert json.loads(result) == TEST_RR_DELTA + assert failed == ["54", "103"] + assert mock_indy_revoke_credential.call_count == 3 + mock_indy_merge_rr_deltas.assert_not_called() @async_mock.patch("indy.anoncreds.issuer_create_credential") @async_mock.patch.object(test_module, "create_tails_reader", autospec=True) @@ -358,25 +326,14 @@ async def test_create_credential_rr_full( error_code=ErrorCode.AnoncredsRevocationRegistryFullError ) - with async_mock.patch.object( - test_module, "IssuerCredRevRecord", async_mock.MagicMock() - ) as mock_issuer_cr_rec: - mock_issuer_cr_rec.return_value.save = async_mock.CoroutineMock() - mock_issuer_cr_rec.retrieve_by_ids = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( - set_state=async_mock.CoroutineMock(), - ) + with self.assertRaises(IndyIssuerRevocationRegistryFullError): + await self.issuer.create_credential( + test_schema, + test_offer, + test_request, + test_values, ) - with self.assertRaises(IndyIssuerRevocationRegistryFullError): - await self.issuer.create_credential( - test_schema, - test_offer, - test_request, - test_values, - "dummy-cxid", - ) - @async_mock.patch("indy.anoncreds.issuer_create_credential") @async_mock.patch.object(test_module, "create_tails_reader", autospec=True) async def test_create_credential_x_indy( @@ -401,25 +358,14 @@ async def test_create_credential_x_indy( error_code=ErrorCode.WalletInvalidHandle ) - with async_mock.patch.object( - test_module, "IssuerCredRevRecord", async_mock.MagicMock() - ) as mock_issuer_cr_rec: - mock_issuer_cr_rec.return_value.save = async_mock.CoroutineMock() - mock_issuer_cr_rec.retrieve_by_ids = async_mock.CoroutineMock( - return_value=async_mock.MagicMock( - set_state=async_mock.CoroutineMock(), - ) + with self.assertRaises(test_module.IndyIssuerError): + await self.issuer.create_credential( + test_schema, + test_offer, + test_request, + test_values, ) - with self.assertRaises(test_module.IndyIssuerError): - await self.issuer.create_credential( - test_schema, - test_offer, - test_request, - test_values, - "dummy-cxid", - ) - @async_mock.patch("indy.anoncreds.issuer_create_and_store_revoc_reg") @async_mock.patch.object(test_module, "create_tails_writer", autospec=True) async def test_create_and_store_revocation_registry( diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py index 2a1c37dd38..90d1a15217 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/manager.py @@ -7,6 +7,7 @@ from typing import Mapping, Optional, Tuple from ....cache.base import BaseCache +from ....connections.models.conn_record import ConnRecord from ....core.error import BaseError from ....core.profile import Profile from ....indy.holder import IndyHolder, IndyHolderError @@ -23,10 +24,10 @@ from ....messaging.responder import BaseResponder from ....multitenant.base import BaseMultitenantManager from ....revocation.indy import IndyRevocation +from ....revocation.models.issuer_cred_rev_record import IssuerCredRevRecord from ....revocation.models.revocation_registry import RevocationRegistry from ....storage.base import BaseStorage from ....storage.error import StorageError, StorageNotFoundError -from ....connections.models.conn_record import ConnRecord from ...out_of_band.v1_0.models.oob_record import OobRecord from .messages.credential_ack import CredentialAck @@ -638,7 +639,7 @@ async def issue_credential( await asyncio.sleep(2) if revocable: - revoc = IndyRevocation(self.profile) + revoc = IndyRevocation(self._profile) registry_info = await revoc.get_or_create_active_registry( cred_def_id ) @@ -658,7 +659,6 @@ async def issue_credential( cred_offer_ser, cred_req_ser, cred_values, - cred_ex_record.credential_exchange_id, rev_reg_id, tails_path, ) @@ -667,7 +667,7 @@ async def issue_credential( continue if revocable and rev_reg.max_creds <= int(cred_rev_id): - revoc = IndyRevocation(self.profile) + revoc = IndyRevocation(self._profile) await revoc.handle_full_registry(rev_reg_id) del revoc @@ -681,6 +681,22 @@ async def issue_credential( ) from None async with self._profile.transaction() as txn: + if revocable and cred_rev_id: + issuer_cr_rec = IssuerCredRevRecord( + state=IssuerCredRevRecord.STATE_ISSUED, + cred_ex_id=cred_ex_record.credential_exchange_id, + cred_ex_version=IssuerCredRevRecord.VERSION_1, + rev_reg_id=rev_reg_id, + cred_rev_id=cred_rev_id, + ) + await issuer_cr_rec.save( + txn, + reason=( + "Created issuer cred rev record for " + f"rev reg id {rev_reg_id}, index {cred_rev_id}" + ), + ) + cred_ex_record = await V10CredentialExchange.retrieve_by_id( txn, cred_ex_record.credential_exchange_id, for_update=True ) diff --git a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py index ba0e65f924..94ec08f0f6 100644 --- a/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py +++ b/aries_cloudagent/protocols/issue_credential/v1_0/tests/test_manager.py @@ -945,7 +945,6 @@ async def test_issue_credential_revocable(self): INDY_OFFER, INDY_CRED_REQ, cred_values, - stored_exchange.credential_exchange_id, REV_REG_ID, "dummy-path", ) @@ -1031,7 +1030,6 @@ async def test_issue_credential_non_revocable(self): INDY_OFFER, INDY_CRED_REQ, cred_values, - stored_exchange.credential_exchange_id, None, None, ) @@ -1113,7 +1111,6 @@ async def test_issue_credential_fills_rr(self): INDY_OFFER, INDY_CRED_REQ, cred_values, - stored_exchange.credential_exchange_id, REV_REG_ID, "dummy-path", ) diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py index fbd2882b7d..7ef901260f 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/handler.py @@ -25,8 +25,9 @@ ) from ......messaging.decorators.attach_decorator import AttachDecorator from ......multitenant.base import BaseMultitenantManager -from ......revocation.models.revocation_registry import RevocationRegistry from ......revocation.indy import IndyRevocation +from ......revocation.models.issuer_cred_rev_record import IssuerCredRevRecord +from ......revocation.models.revocation_registry import RevocationRegistry from ......storage.base import BaseStorage from ...message_types import ( @@ -352,6 +353,7 @@ async def issue_credential( schema = await ledger.get_schema(schema_id) cred_def = await ledger.get_credential_definition(cred_def_id) revocable = cred_def["value"].get("revocation") + result = None for attempt in range(max(retries, 1)): if attempt > 0: @@ -380,7 +382,6 @@ async def issue_credential( cred_offer, cred_request, cred_values, - cred_ex_record.cred_ex_id, rev_reg_id, tails_path, ) @@ -388,24 +389,45 @@ async def issue_credential( # unlucky, another instance filled the registry first continue - detail_record = V20CredExRecordIndy( - cred_ex_id=cred_ex_record.cred_ex_id, - rev_reg_id=rev_reg_id, - cred_rev_id=cred_rev_id, - ) - async with self._profile.session() as session: - await detail_record.save(session, reason="v2.0 issue credential") - if revocable and rev_reg.max_creds <= int(cred_rev_id): revoc = IndyRevocation(self.profile) await revoc.handle_full_registry(rev_reg_id) del revoc - return self.get_format_data(CRED_20_ISSUE, json.loads(cred_json)) + result = self.get_format_data(CRED_20_ISSUE, json.loads(cred_json)) + break - raise V20CredFormatError( - f"Cred def '{cred_def_id}' has no active revocation registry" - ) + if not result: + raise V20CredFormatError( + f"Cred def '{cred_def_id}' has no active revocation registry" + ) + + async with self._profile.transaction() as txn: + detail_record = V20CredExRecordIndy( + cred_ex_id=cred_ex_record.cred_ex_id, + rev_reg_id=rev_reg_id, + cred_rev_id=cred_rev_id, + ) + await detail_record.save(txn, reason="v2.0 issue credential") + + if revocable and cred_rev_id: + issuer_cr_rec = IssuerCredRevRecord( + state=IssuerCredRevRecord.STATE_ISSUED, + cred_ex_id=cred_ex_record.cred_ex_id, + cred_ex_version=IssuerCredRevRecord.VERSION_2, + rev_reg_id=rev_reg_id, + cred_rev_id=cred_rev_id, + ) + await issuer_cr_rec.save( + txn, + reason=( + "Created issuer cred rev record for " + f"rev reg id {rev_reg_id}, index {cred_rev_id}" + ), + ) + await txn.commit() + + return result async def receive_credential( self, cred_ex_record: V20CredExRecord, cred_issue_message: V20CredIssue diff --git a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py index e97fd6d457..06028fa00e 100644 --- a/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py +++ b/aries_cloudagent/protocols/issue_credential/v2_0/formats/indy/tests/test_handler.py @@ -753,7 +753,6 @@ async def test_issue_credential_revocable(self): INDY_OFFER, INDY_CRED_REQ, attr_values, - cred_ex_record.cred_ex_id, REV_REG_ID, "dummy-path", ) @@ -837,7 +836,6 @@ async def test_issue_credential_non_revocable(self): INDY_OFFER, INDY_CRED_REQ, attr_values, - cred_ex_record.cred_ex_id, None, None, ) diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index 4057d7a799..10db676f70 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -297,6 +297,7 @@ async def set_cred_revoked_state( txn, rev_reg_id, cred_rev_id, for_update=True ) cred_ex_id = rev_rec.cred_ex_id + cred_ex_version = rev_rec.cred_ex_version rev_rec.state = IssuerCredRevRecord.STATE_REVOKED await rev_rec.save(txn, reason="revoke credential") await txn.commit() @@ -304,25 +305,33 @@ async def set_cred_revoked_state( continue async with self._profile.transaction() as txn: - try: - cred_ex_record = await V10CredentialExchange.retrieve_by_id( - txn, cred_ex_id, for_update=True - ) - cred_ex_record.state = ( - V10CredentialExchange.STATE_CREDENTIAL_REVOKED - ) - await cred_ex_record.save(txn, reason="revoke credential") - await txn.commit() - continue # skip 2.0 record check - except StorageNotFoundError: - pass - - try: - cred_ex_record = await V20CredExRecord.retrieve_by_id( - txn, cred_ex_id, for_update=True - ) - cred_ex_record.state = V20CredExRecord.STATE_CREDENTIAL_REVOKED - await cred_ex_record.save(txn, reason="revoke credential") - await txn.commit() - except StorageNotFoundError: - pass + if ( + not cred_ex_version + or cred_ex_version == IssuerCredRevRecord.VERSION_1 + ): + try: + cred_ex_record = await V10CredentialExchange.retrieve_by_id( + txn, cred_ex_id, for_update=True + ) + cred_ex_record.state = ( + V10CredentialExchange.STATE_CREDENTIAL_REVOKED + ) + await cred_ex_record.save(txn, reason="revoke credential") + await txn.commit() + continue # skip 2.0 record check + except StorageNotFoundError: + pass + + if ( + not cred_ex_version + or cred_ex_version == IssuerCredRevRecord.VERSION_2 + ): + try: + cred_ex_record = await V20CredExRecord.retrieve_by_id( + txn, cred_ex_id, for_update=True + ) + cred_ex_record.state = V20CredExRecord.STATE_CREDENTIAL_REVOKED + await cred_ex_record.save(txn, reason="revoke credential") + await txn.commit() + except StorageNotFoundError: + pass diff --git a/aries_cloudagent/revocation/models/issuer_cred_rev_record.py b/aries_cloudagent/revocation/models/issuer_cred_rev_record.py index 5ab8d3ba09..10ba69ef53 100644 --- a/aries_cloudagent/revocation/models/issuer_cred_rev_record.py +++ b/aries_cloudagent/revocation/models/issuer_cred_rev_record.py @@ -27,6 +27,7 @@ class Meta: RECORD_TOPIC = "issuer_cred_rev" TAG_NAMES = { "cred_ex_id", + "cred_ex_version", "cred_def_id", "rev_reg_id", "cred_rev_id", @@ -36,6 +37,9 @@ class Meta: STATE_ISSUED = "issued" STATE_REVOKED = "revoked" + VERSION_1 = "1" + VERSION_2 = "2" + def __init__( self, *, @@ -45,6 +49,7 @@ def __init__( rev_reg_id: str = None, cred_rev_id: str = None, cred_def_id: str = None, # Marshmallow formalism: leave None + cred_ex_version: str = None, **kwargs, ): """Initialize a new IssuerCredRevRecord.""" @@ -53,6 +58,7 @@ def __init__( self.rev_reg_id = rev_reg_id self.cred_rev_id = cred_rev_id self.cred_def_id = ":".join(rev_reg_id.split(":")[-7:-2]) + self.cred_ex_version = cred_ex_version @property def record_id(self) -> str: @@ -158,3 +164,7 @@ class Meta: description="Credential revocation identifier", **INDY_CRED_REV_ID, ) + cred_ex_version = fields.Str( + required=False, + description="Credential exchange version", + ) diff --git a/aries_cloudagent/revocation/tests/test_manager.py b/aries_cloudagent/revocation/tests/test_manager.py index 113c4850d1..ec026b9afd 100644 --- a/aries_cloudagent/revocation/tests/test_manager.py +++ b/aries_cloudagent/revocation/tests/test_manager.py @@ -2,7 +2,6 @@ from asynctest import mock as async_mock from asynctest import TestCase as AsyncTestCase -from more_itertools import side_effect from aries_cloudagent.revocation.models.issuer_cred_rev_record import ( IssuerCredRevRecord, @@ -13,6 +12,8 @@ from ...protocols.issue_credential.v1_0.models.credential_exchange import ( V10CredentialExchange, ) +from ...protocols.issue_credential.v2_0.models.cred_ex_record import V20CredExRecord + from ..manager import RevocationManager, RevocationManagerError @@ -427,7 +428,7 @@ async def test_retrieve_records(self): assert ret_ex.connection_id == str(index) assert ret_ex.thread_id == str(1000 + index) - async def test_set_revoked_state(self): + async def test_set_revoked_state_v1(self): CRED_REV_ID = "1" async with self.profile.session() as session: @@ -466,3 +467,40 @@ async def test_set_revoked_state(self): session, crev_record.record_id ) assert check_crev_record.state == IssuerCredRevRecord.STATE_REVOKED + + async def test_set_revoked_state_v2(self): + CRED_REV_ID = "1" + + async with self.profile.session() as session: + exchange_record = V20CredExRecord( + connection_id="mark-revoked-cid", + thread_id="mark-revoked-tid", + initiator=V20CredExRecord.INITIATOR_SELF, + role=V20CredExRecord.ROLE_ISSUER, + state=V20CredExRecord.STATE_ISSUED, + ) + await exchange_record.save(session) + + crev_record = IssuerCredRevRecord( + cred_ex_id=exchange_record.cred_ex_id, + cred_def_id=CRED_DEF_ID, + rev_reg_id=REV_REG_ID, + cred_rev_id=CRED_REV_ID, + state=IssuerCredRevRecord.STATE_ISSUED, + ) + await crev_record.save(session) + + await self.manager.set_cred_revoked_state(REV_REG_ID, [CRED_REV_ID]) + + async with self.profile.session() as session: + check_exchange_record = await V20CredExRecord.retrieve_by_id( + session, exchange_record.cred_ex_id + ) + assert ( + check_exchange_record.state == V20CredExRecord.STATE_CREDENTIAL_REVOKED + ) + + check_crev_record = await IssuerCredRevRecord.retrieve_by_id( + session, crev_record.record_id + ) + assert check_crev_record.state == IssuerCredRevRecord.STATE_REVOKED diff --git a/aries_cloudagent/storage/in_memory.py b/aries_cloudagent/storage/in_memory.py index c94a9d6f93..296858f1d2 100644 --- a/aries_cloudagent/storage/in_memory.py +++ b/aries_cloudagent/storage/in_memory.py @@ -70,8 +70,7 @@ async def get_record( row = self.profile.records.get(record_id) if row and row.type == record_type: return row - if not row: - raise StorageNotFoundError("Record not found: {}".format(record_id)) + raise StorageNotFoundError("Record not found: {}".format(record_id)) async def update_record(self, record: StorageRecord, value: str, tags: Mapping): """