Skip to content

Commit 6c1ff19

Browse files
committed
fix: complete stalled setup when activating a posted revocation registry
When no active registry exists, get_or_create_active_registry promoted the oldest posted registry with a bare wallet state change. A registry is only left in the posted state when its setup pipeline stalled between publishing the definition and publishing the initial entry (e.g. the tails file upload to the tails server failed), so the promoted registry had an unresolvable tailsLocation (holders get 404 and cannot build non-revocation proofs) and possibly no initial accumulator entry on the ledger. Every credential subsequently issued against it was silently unverifiable. Resume the stalled pipeline instead: re-upload the tails file (put_file already treats HTTP 409 'already exists' as success, so this is idempotent against the reference tails server) and publish the initial entry, which itself performs the posted -> active transition. On failure the registry stays posted and promotion is retried on the next issuance attempt instead of activating a broken registry. Signed-off-by: kukgini <kukgini@gmail.com>
1 parent c92d715 commit 6c1ff19

2 files changed

Lines changed: 66 additions & 13 deletions

File tree

acapy_agent/revocation/indy.py

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
from ..core.profile import Profile
99
from ..ledger.base import BaseLedger
10+
from ..ledger.error import LedgerError
1011
from ..ledger.multiple_ledger.ledger_requests_executor import (
1112
GET_CRED_DEF,
1213
GET_REVOC_REG_DEF,
@@ -260,6 +261,7 @@ async def get_or_create_active_registry(
260261
except StorageNotFoundError:
261262
pass
262263

264+
posted_registries = []
263265
async with self._profile.session() as session:
264266
full_registries = await IssuerRevRegRecord.query_by_cred_def_id(
265267
session, cred_def_id, None, IssuerRevRegRecord.STATE_FULL, 1
@@ -280,19 +282,30 @@ async def get_or_create_active_registry(
280282
cred_def_id,
281283
max_cred_num=any_registry.max_cred_num,
282284
)
283-
# if there is a posted registry, activate oldest
285+
# if there is a posted registry, complete its setup and activate it
284286
else:
285287
posted_registries = await IssuerRevRegRecord.query_by_cred_def_id(
286288
session, cred_def_id, IssuerRevRegRecord.STATE_POSTED, None, None
287289
)
288-
if posted_registries:
289-
posted_registries = sorted(
290-
posted_registries, key=lambda r: r.created_at
291-
)
292-
await self._set_registry_status(
293-
revoc_reg_id=posted_registries[0].revoc_reg_id,
294-
state=IssuerRevRegRecord.STATE_ACTIVE,
295-
)
290+
291+
if posted_registries:
292+
# A registry is only left posted when its setup stalled after the
293+
# definition reached the ledger: the tails file upload and/or the
294+
# initial accumulator entry are still outstanding. Complete those
295+
# steps instead of activating with a bare state change, which would
296+
# break every credential later issued against the registry
297+
# (unresolvable tailsLocation, missing initial entry).
298+
rec = min(posted_registries, key=lambda r: r.created_at)
299+
try:
300+
await rec.upload_tails_file(self._profile)
301+
# publishing the initial entry transitions posted -> active
302+
await rec.send_entry(self._profile)
303+
except (RevocationError, LedgerError):
304+
LOGGER.exception(
305+
"Setup of posted revocation registry %s could not be completed; "
306+
"not activating (will retry on next issuance attempt)",
307+
rec.revoc_reg_id,
308+
)
296309
return None
297310

298311
async def get_ledger_registry(self, revoc_reg_id: str) -> RevocationRegistry:

acapy_agent/revocation/tests/test_indy.py

Lines changed: 44 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -318,11 +318,51 @@ async def test_get_or_create_active_registry_has_no_active_or_any_registry(self,
318318
],
319319
],
320320
)
321+
@mock.patch.object(IssuerRevRegRecord, "upload_tails_file", mock.CoroutineMock())
322+
@mock.patch.object(IssuerRevRegRecord, "send_entry", mock.CoroutineMock())
321323
async def test_get_or_create_active_registry_has_no_active_with_posted(self, *_):
322324
result = await self.revoc.get_or_create_active_registry("cred_def_id")
323325

324326
assert not result
325-
assert (
326-
self.revoc._set_registry_status.call_args.kwargs["state"]
327-
== IssuerRevRegRecord.STATE_ACTIVE
328-
)
327+
# the stalled setup pipeline is completed rather than bare-activated:
328+
# the tails file is uploaded and the initial entry published (which
329+
# transitions the record posted -> active)
330+
IssuerRevRegRecord.upload_tails_file.assert_awaited_once()
331+
IssuerRevRegRecord.send_entry.assert_awaited_once()
332+
self.revoc._set_registry_status.assert_not_called()
333+
334+
@mock.patch(
335+
"acapy_agent.revocation.indy.IndyRevocation.get_active_issuer_rev_reg_record",
336+
mock.CoroutineMock(side_effect=StorageNotFoundError("No such record")),
337+
)
338+
@mock.patch(
339+
"acapy_agent.revocation.indy.IndyRevocation._set_registry_status",
340+
mock.CoroutineMock(return_value=None),
341+
)
342+
@mock.patch.object(
343+
IssuerRevRegRecord,
344+
"query_by_cred_def_id",
345+
side_effect=[
346+
[IssuerRevRegRecord(max_cred_num=3)],
347+
[
348+
IssuerRevRegRecord(
349+
revoc_reg_id="test-rev-reg-id",
350+
state=IssuerRevRegRecord.STATE_POSTED,
351+
)
352+
],
353+
],
354+
)
355+
@mock.patch.object(
356+
IssuerRevRegRecord,
357+
"upload_tails_file",
358+
mock.CoroutineMock(side_effect=RevocationError("tails server unavailable")),
359+
)
360+
@mock.patch.object(IssuerRevRegRecord, "send_entry", mock.CoroutineMock())
361+
async def test_get_or_create_active_registry_posted_setup_incomplete(self, *_):
362+
result = await self.revoc.get_or_create_active_registry("cred_def_id")
363+
364+
assert not result
365+
# the registry must stay posted: activating it without a tails file
366+
# on the server would break credentials issued against it
367+
IssuerRevRegRecord.send_entry.assert_not_called()
368+
self.revoc._set_registry_status.assert_not_called()

0 commit comments

Comments
 (0)