diff --git a/acapy_agent/revocation/indy.py b/acapy_agent/revocation/indy.py index 42392b3f2f..4d1af5223a 100644 --- a/acapy_agent/revocation/indy.py +++ b/acapy_agent/revocation/indy.py @@ -7,6 +7,7 @@ from ..core.profile import Profile from ..ledger.base import BaseLedger +from ..ledger.error import LedgerError from ..ledger.multiple_ledger.ledger_requests_executor import ( GET_CRED_DEF, GET_REVOC_REG_DEF, @@ -260,6 +261,7 @@ async def get_or_create_active_registry( except StorageNotFoundError: pass + posted_registries = [] async with self._profile.session() as session: full_registries = await IssuerRevRegRecord.query_by_cred_def_id( session, cred_def_id, None, IssuerRevRegRecord.STATE_FULL, 1 @@ -280,19 +282,30 @@ async def get_or_create_active_registry( cred_def_id, max_cred_num=any_registry.max_cred_num, ) - # if there is a posted registry, activate oldest + # if there is a posted registry, complete its setup and activate it else: posted_registries = await IssuerRevRegRecord.query_by_cred_def_id( session, cred_def_id, IssuerRevRegRecord.STATE_POSTED, None, None ) - if posted_registries: - posted_registries = sorted( - posted_registries, key=lambda r: r.created_at - ) - await self._set_registry_status( - revoc_reg_id=posted_registries[0].revoc_reg_id, - state=IssuerRevRegRecord.STATE_ACTIVE, - ) + + if posted_registries: + # A registry is only left posted when its setup stalled after the + # definition reached the ledger: the tails file upload and/or the + # initial accumulator entry are still outstanding. Complete those + # steps instead of activating with a bare state change, which would + # break every credential later issued against the registry + # (unresolvable tailsLocation, missing initial entry). + rec = min(posted_registries, key=lambda r: r.created_at) + try: + await rec.upload_tails_file(self._profile) + # publishing the initial entry transitions posted -> active + await rec.send_entry(self._profile) + except (RevocationError, LedgerError): + LOGGER.exception( + "Setup of posted revocation registry %s could not be completed; " + "not activating (will retry on next issuance attempt)", + rec.revoc_reg_id, + ) return None async def get_ledger_registry(self, revoc_reg_id: str) -> RevocationRegistry: diff --git a/acapy_agent/revocation/tests/test_indy.py b/acapy_agent/revocation/tests/test_indy.py index 4cc3f6153a..7f8768bbce 100644 --- a/acapy_agent/revocation/tests/test_indy.py +++ b/acapy_agent/revocation/tests/test_indy.py @@ -318,11 +318,51 @@ async def test_get_or_create_active_registry_has_no_active_or_any_registry(self, ], ], ) + @mock.patch.object(IssuerRevRegRecord, "upload_tails_file", mock.CoroutineMock()) + @mock.patch.object(IssuerRevRegRecord, "send_entry", mock.CoroutineMock()) async def test_get_or_create_active_registry_has_no_active_with_posted(self, *_): result = await self.revoc.get_or_create_active_registry("cred_def_id") assert not result - assert ( - self.revoc._set_registry_status.call_args.kwargs["state"] - == IssuerRevRegRecord.STATE_ACTIVE - ) + # the stalled setup pipeline is completed rather than bare-activated: + # the tails file is uploaded and the initial entry published (which + # transitions the record posted -> active) + IssuerRevRegRecord.upload_tails_file.assert_awaited_once() + IssuerRevRegRecord.send_entry.assert_awaited_once() + self.revoc._set_registry_status.assert_not_called() + + @mock.patch( + "acapy_agent.revocation.indy.IndyRevocation.get_active_issuer_rev_reg_record", + mock.CoroutineMock(side_effect=StorageNotFoundError("No such record")), + ) + @mock.patch( + "acapy_agent.revocation.indy.IndyRevocation._set_registry_status", + mock.CoroutineMock(return_value=None), + ) + @mock.patch.object( + IssuerRevRegRecord, + "query_by_cred_def_id", + side_effect=[ + [IssuerRevRegRecord(max_cred_num=3)], + [ + IssuerRevRegRecord( + revoc_reg_id="test-rev-reg-id", + state=IssuerRevRegRecord.STATE_POSTED, + ) + ], + ], + ) + @mock.patch.object( + IssuerRevRegRecord, + "upload_tails_file", + mock.CoroutineMock(side_effect=RevocationError("tails server unavailable")), + ) + @mock.patch.object(IssuerRevRegRecord, "send_entry", mock.CoroutineMock()) + async def test_get_or_create_active_registry_posted_setup_incomplete(self, *_): + result = await self.revoc.get_or_create_active_registry("cred_def_id") + + assert not result + # the registry must stay posted: activating it without a tails file + # on the server would break credentials issued against it + IssuerRevRegRecord.send_entry.assert_not_called() + self.revoc._set_registry_status.assert_not_called()