diff --git a/acapy_agent/anoncreds/revocation/revocation.py b/acapy_agent/anoncreds/revocation/revocation.py index 67b1a08704..f5a7247391 100644 --- a/acapy_agent/anoncreds/revocation/revocation.py +++ b/acapy_agent/anoncreds/revocation/revocation.py @@ -1531,7 +1531,13 @@ async def decommission_registry(self, cred_def_id: str) -> list: new_rev_reg_def_id = None if new_backup_reg and not isinstance(new_backup_reg, str): new_rev_reg_def_id = new_backup_reg.rev_reg_def_id - await self.store_revocation_registry_definition(new_backup_reg) + try: + await self.store_revocation_registry_definition(new_backup_reg) + except AnonCredsRevocationError: + LOGGER.debug( + "Registry %s already stored by event chain", + new_rev_reg_def_id, + ) elif isinstance(new_backup_reg, str): LOGGER.error("Failed to create new backup registry: %s", new_backup_reg) else: @@ -1544,10 +1550,12 @@ async def decommission_registry(self, cred_def_id: str) -> list: keep_ids.add(new_rev_reg_def_id) async with self.profile.transaction() as txn: - registries = await txn.handle.fetch_all( - CATEGORY_REV_REG_DEF, - {"cred_def_id": cred_def_id}, - for_update=True, + registries = list( + await txn.handle.fetch_all( + CATEGORY_REV_REG_DEF, + {"cred_def_id": cred_def_id}, + for_update=True, + ) ) recs = [ r for r in registries if r.tags.get("state") != RevRegDefState.STATE_WAIT diff --git a/acapy_agent/anoncreds/revocation/revocation_setup.py b/acapy_agent/anoncreds/revocation/revocation_setup.py index 295ef7a8eb..003ee89886 100644 --- a/acapy_agent/anoncreds/revocation/revocation_setup.py +++ b/acapy_agent/anoncreds/revocation/revocation_setup.py @@ -389,7 +389,6 @@ async def on_cred_def(self, profile: Profile, event: CredDefFinishedEvent) -> No ) if event.payload.options.get("wait_for_revocation_setup"): - # Wait for registry activation, if configured to do so await revoc.wait_for_active_revocation_registry(payload.cred_def_id) async def on_registry_create_requested( diff --git a/acapy_agent/anoncreds/revocation/tests/test_revocation.py b/acapy_agent/anoncreds/revocation/tests/test_revocation.py index 131f9b17d7..8ef1257f56 100644 --- a/acapy_agent/anoncreds/revocation/tests/test_revocation.py +++ b/acapy_agent/anoncreds/revocation/tests/test_revocation.py @@ -1046,6 +1046,87 @@ async def test_decommission_registry_new_backup_creation_fails(self, mock_handle assert len(result) == 2 assert result[0].tags["state"] == RevRegDefState.STATE_DECOMMISSIONED + @mock.patch.object(AskarAnonCredsProfileSession, "handle") + async def test_decommission_registry_duplicate_store_handled(self, mock_handle): + """When event chain already stored the registry, duplicate is handled gracefully.""" + mock_handle.fetch_all = mock.CoroutineMock( + side_effect=[ + [ + MockEntry( + name="backup-reg-reg", + tags={ + "cred_def_id": "test-rev-reg-def-id", + "state": RevRegDefState.STATE_FINISHED, + "active": "false", + }, + ), + ], + [ + MockEntry( + name="active-reg-reg", + tags={ + "cred_def_id": "test-rev-reg-def-id", + "state": RevRegDefState.STATE_FINISHED, + "active": "true", + }, + ), + MockEntry( + name="backup-reg-reg", + tags={ + "cred_def_id": "test-rev-reg-def-id", + "state": RevRegDefState.STATE_FINISHED, + "active": "false", + }, + ), + MockEntry( + name="new-rev-reg", + tags={ + "cred_def_id": "test-rev-reg-def-id", + "state": RevRegDefState.STATE_FINISHED, + "active": "false", + }, + ), + ], + ] + ) + self.revocation.get_or_create_active_registry = mock.CoroutineMock( + return_value=RevRegDefResult( + job_id="test-job-id", + revocation_registry_definition_state=RevRegDefState( + state=RevRegDefState.STATE_FINISHED, + revocation_registry_definition_id="active-reg-reg", + revocation_registry_definition=rev_reg_def, + ), + registration_metadata={}, + revocation_registry_definition_metadata={}, + ) + ) + self.revocation.create_and_register_revocation_registry_definition = ( + mock.CoroutineMock( + return_value=RevRegDefResult( + job_id="test-job-id", + revocation_registry_definition_state=RevRegDefState( + state=RevRegDefState.STATE_ACTION, + revocation_registry_definition_id="new-rev-reg", + revocation_registry_definition=rev_reg_def, + ), + registration_metadata={}, + revocation_registry_definition_metadata={}, + ) + ) + ) + self.revocation.store_revocation_registry_definition = mock.CoroutineMock( + side_effect=test_module.AnonCredsRevocationError("Duplicate entry") + ) + self.revocation.set_active_registry = mock.CoroutineMock(return_value=None) + mock_handle.replace = mock.CoroutineMock(return_value=None) + + result = await self.revocation.decommission_registry("test-rev-reg-def-id") + + assert isinstance(result, list) + self.revocation.store_revocation_registry_definition.assert_called_once() + self.revocation.set_active_registry.assert_called_once_with("backup-reg-reg") + @mock.patch.object(AskarAnonCredsProfileSession, "handle") async def test_get_backup_registry_id_raises_when_no_backup(self, mock_handle): """_get_backup_registry_id raises when no finished backup exists.""" diff --git a/acapy_agent/anoncreds/routes/revocation/registry/routes.py b/acapy_agent/anoncreds/routes/revocation/registry/routes.py index 6d58eb70e6..e9c1667ec9 100644 --- a/acapy_agent/anoncreds/routes/revocation/registry/routes.py +++ b/acapy_agent/anoncreds/routes/revocation/registry/routes.py @@ -277,6 +277,8 @@ async def get_active_rev_reg(request: web.BaseRequest): revocation = AnonCredsRevocation(profile) active_reg = await revocation.get_or_create_active_registry(cred_def_id) rev_reg = await _get_issuer_rev_reg_record(profile, active_reg.rev_reg_def_id) + except AnonCredsRevocationError as e: + raise web.HTTPNotFound(reason=str(e)) from e except AnonCredsIssuerError as e: raise web.HTTPInternalServerError(reason=str(e)) from e diff --git a/acapy_agent/anoncreds/routes/revocation/registry/tests/test_routes.py b/acapy_agent/anoncreds/routes/revocation/registry/tests/test_routes.py index e2920f900e..bc239f16a8 100644 --- a/acapy_agent/anoncreds/routes/revocation/registry/tests/test_routes.py +++ b/acapy_agent/anoncreds/routes/revocation/registry/tests/test_routes.py @@ -11,11 +11,12 @@ from .....issuer import AnonCredsIssuer from .....models.issuer_cred_rev_record import IssuerCredRevRecord from .....models.revocation import RevRegDef, RevRegDefState, RevRegDefValue -from .....revocation import AnonCredsRevocation +from .....revocation import AnonCredsRevocation, AnonCredsRevocationError from .....tests.mock_objects import MockRevocationRegistryDefinition from ....common.testing import BaseAnonCredsRouteTestCase from .. import routes as test_module from ..routes import ( + get_active_rev_reg, get_rev_reg_issued, get_rev_reg_issued_count, get_rev_regs, @@ -178,6 +179,16 @@ async def test_active_registry_wrong_profile_403(self): with self.assertRaises(web.HTTPForbidden): await set_active_registry(self.request) + @mock.patch.object( + AnonCredsRevocation, + "get_or_create_active_registry", + side_effect=AnonCredsRevocationError("No active registry"), + ) + async def test_get_active_rev_reg_not_found(self, mock_get): + self.request.match_info = {"cred_def_id": "test_cred_def_id"} + with self.assertRaises(web.HTTPNotFound): + await get_active_rev_reg(self.request) + async def test_get_rev_regs(self): self.request.query = { "cred_def_id": "test_cred_def_id", diff --git a/demo/runners/support/agent.py b/demo/runners/support/agent.py index 12938f508b..5a9917ec14 100644 --- a/demo/runners/support/agent.py +++ b/demo/runners/support/agent.py @@ -494,6 +494,29 @@ async def register_schema_and_creddef_anoncreds( "credential_definition_ids" ][0] log_msg("Cred def ID:", credential_definition_id) + + if support_revocation: + log_msg("Waiting for revocation registry to become active...") + poll_timeout = 60.0 + poll_interval = 2.0 + elapsed = 0.0 + while elapsed < poll_timeout: + try: + active_reg = await self.admin_GET( + f"/anoncreds/revocation/active-registry/{credential_definition_id}" + ) + if active_reg and active_reg.get("result"): + log_msg("Revocation registry is active.") + break + except Exception: + pass + await asyncio.sleep(poll_interval) + elapsed += poll_interval + else: + log_msg( + "WARNING: Revocation registry did not become active within timeout" + ) + return schema_id, credential_definition_id def get_agent_args(self):