Skip to content

Commit 874f4db

Browse files
authored
Update accumulator value in wallet on repair (openwallet-foundation#3299)
* Update wallet reg entry on repair Signed-off-by: Jamie Hale <[email protected]> * Refactoring Signed-off-by: Jamie Hale <[email protected]> * await prepare notification function Signed-off-by: Jamie Hale <[email protected]> * refactor / fix test Signed-off-by: Jamie Hale <[email protected]> * Add a couple unit tests Signed-off-by: Jamie Hale <[email protected]> --------- Signed-off-by: Jamie Hale <[email protected]>
1 parent 92eb0a5 commit 874f4db

File tree

6 files changed

+322
-210
lines changed

6 files changed

+322
-210
lines changed

acapy_agent/revocation/manager.py

Lines changed: 104 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import json
44
import logging
5-
from typing import Mapping, Optional, Sequence, Text, Tuple
5+
from typing import Mapping, NamedTuple, Optional, Sequence, Text, Tuple
66

77
from ..connections.models.conn_record import ConnRecord
88
from ..core.error import BaseError
@@ -26,6 +26,17 @@ class RevocationManagerError(BaseError):
2626
"""Revocation manager error."""
2727

2828

29+
class RevocationNotificationInfo(NamedTuple):
30+
"""Revocation notification information."""
31+
32+
rev_reg_id: str
33+
cred_rev_id: str
34+
thread_id: Optional[str]
35+
connection_id: Optional[str]
36+
comment: Optional[str]
37+
notify_version: Optional[str]
38+
39+
2940
class RevocationManager:
3041
"""Class for managing revocation operations."""
3142

@@ -107,6 +118,46 @@ async def revoke_credential_by_cred_ex_id(
107118
write_ledger=write_ledger,
108119
)
109120

121+
async def _prepare_revocation_notification(
122+
self,
123+
revoc_notif_info: RevocationNotificationInfo,
124+
):
125+
"""Saves the revocation notification record, and thread_id if not provided."""
126+
thread_id = (
127+
revoc_notif_info.thread_id
128+
or f"indy::{revoc_notif_info.rev_reg_id}::{revoc_notif_info.cred_rev_id}"
129+
)
130+
rev_notify_rec = RevNotificationRecord(
131+
rev_reg_id=revoc_notif_info.rev_reg_id,
132+
cred_rev_id=revoc_notif_info.cred_rev_id,
133+
thread_id=thread_id,
134+
connection_id=revoc_notif_info.connection_id,
135+
comment=revoc_notif_info.comment,
136+
version=revoc_notif_info.notify_version,
137+
)
138+
async with self._profile.session() as session:
139+
await rev_notify_rec.save(session, reason="New revocation notification")
140+
141+
async def _get_endorsement_txn_for_revocation(
142+
self, endorser_conn_id: str, issuer_rr_upd: IssuerRevRegRecord
143+
):
144+
async with self._profile.session() as session:
145+
try:
146+
connection_record = await ConnRecord.retrieve_by_id(
147+
session, endorser_conn_id
148+
)
149+
except StorageNotFoundError:
150+
raise RevocationManagerError(
151+
"No endorser connection record found " f"for id: {endorser_conn_id}"
152+
)
153+
endorser_info = await connection_record.metadata_get(session, "endorser_info")
154+
endorser_did = endorser_info["endorser_did"]
155+
return await issuer_rr_upd.send_entry(
156+
self._profile,
157+
write_ledger=False,
158+
endorser_did=endorser_did,
159+
)
160+
110161
async def revoke_credential(
111162
self,
112163
rev_reg_id: str,
@@ -150,81 +201,72 @@ async def revoke_credential(
150201
write_ledger is True, otherwise None.
151202
"""
152203
issuer = self._profile.inject(IndyIssuer)
153-
154204
revoc = IndyRevocation(self._profile)
205+
155206
issuer_rr_rec = await revoc.get_issuer_rev_reg_record(rev_reg_id)
156207
if not issuer_rr_rec:
157208
raise RevocationManagerError(
158209
f"No revocation registry record found for id: {rev_reg_id}"
159210
)
160211

161212
if notify:
162-
thread_id = thread_id or f"indy::{rev_reg_id}::{cred_rev_id}"
163-
rev_notify_rec = RevNotificationRecord(
164-
rev_reg_id=rev_reg_id,
165-
cred_rev_id=cred_rev_id,
166-
thread_id=thread_id,
167-
connection_id=connection_id,
168-
comment=comment,
169-
version=notify_version,
170-
)
171-
async with self._profile.session() as session:
172-
await rev_notify_rec.save(session, reason="New revocation notification")
173-
174-
if publish:
175-
rev_reg = await revoc.get_ledger_registry(rev_reg_id)
176-
await rev_reg.get_or_fetch_local_tails_path()
177-
# pick up pending revocations on input revocation registry
178-
crids = (issuer_rr_rec.pending_pub or []) + [cred_rev_id]
179-
(delta_json, _) = await issuer.revoke_credentials(
180-
issuer_rr_rec.cred_def_id,
181-
issuer_rr_rec.revoc_reg_id,
182-
issuer_rr_rec.tails_local_path,
183-
crids,
213+
await self._prepare_revocation_notification(
214+
RevocationNotificationInfo(
215+
rev_reg_id=rev_reg_id,
216+
cred_rev_id=cred_rev_id,
217+
thread_id=thread_id,
218+
connection_id=connection_id,
219+
comment=comment,
220+
notify_version=notify_version,
221+
),
184222
)
185-
async with self._profile.transaction() as txn:
186-
issuer_rr_upd = await IssuerRevRegRecord.retrieve_by_id(
187-
txn, issuer_rr_rec.record_id, for_update=True
188-
)
189-
if delta_json:
190-
issuer_rr_upd.revoc_reg_entry = json.loads(delta_json)
191-
await issuer_rr_upd.clear_pending(txn, crids)
192-
await txn.commit()
193-
await self.set_cred_revoked_state(rev_reg_id, crids)
194-
if delta_json:
195-
if write_ledger:
196-
rev_entry_resp = await issuer_rr_upd.send_entry(self._profile)
197-
await notify_revocation_published_event(
198-
self._profile, rev_reg_id, [cred_rev_id]
199-
)
200-
return rev_entry_resp
201-
else:
202-
async with self._profile.session() as session:
203-
try:
204-
connection_record = await ConnRecord.retrieve_by_id(
205-
session, endorser_conn_id
206-
)
207-
except StorageNotFoundError:
208-
raise RevocationManagerError(
209-
"No endorser connection record found "
210-
f"for id: {endorser_conn_id}"
211-
)
212-
endorser_info = await connection_record.metadata_get(
213-
session, "endorser_info"
214-
)
215-
endorser_did = endorser_info["endorser_did"]
216-
rev_entry_resp = await issuer_rr_upd.send_entry(
217-
self._profile,
218-
write_ledger=write_ledger,
219-
endorser_did=endorser_did,
220-
)
221-
return rev_entry_resp
222-
else:
223+
224+
if not publish:
225+
# If not publishing, just mark the revocation as pending.
223226
async with self._profile.transaction() as txn:
224227
await issuer_rr_rec.mark_pending(txn, cred_rev_id)
225228
await txn.commit()
226229
return None
227230

231+
rev_reg = await revoc.get_ledger_registry(rev_reg_id)
232+
await rev_reg.get_or_fetch_local_tails_path()
233+
# pick up pending revocations on input revocation registry
234+
crids = (issuer_rr_rec.pending_pub or []) + [cred_rev_id]
235+
(delta_json, _) = await issuer.revoke_credentials(
236+
issuer_rr_rec.cred_def_id,
237+
issuer_rr_rec.revoc_reg_id,
238+
issuer_rr_rec.tails_local_path,
239+
crids,
240+
)
241+
242+
# Update the revocation registry record with the new delta
243+
# and clear pending revocations
244+
async with self._profile.transaction() as txn:
245+
issuer_rr_upd = await IssuerRevRegRecord.retrieve_by_id(
246+
txn, issuer_rr_rec.record_id, for_update=True
247+
)
248+
if delta_json:
249+
issuer_rr_upd.revoc_reg_entry = json.loads(delta_json)
250+
await issuer_rr_upd.clear_pending(txn, crids)
251+
await txn.commit()
252+
253+
await self.set_cred_revoked_state(rev_reg_id, crids)
254+
255+
# Revocation list needs to be updated on ledger
256+
if delta_json:
257+
# Can write to ledger directly
258+
if write_ledger:
259+
rev_entry_resp = await issuer_rr_upd.send_entry(self._profile)
260+
await notify_revocation_published_event(
261+
self._profile, rev_reg_id, [cred_rev_id]
262+
)
263+
return rev_entry_resp
264+
# Author --> Need endorsed transaction for revocation
265+
else:
266+
return await self._get_endorsement_txn_for_revocation(
267+
endorser_conn_id, issuer_rr_upd
268+
)
269+
228270
async def update_rev_reg_revoked_state(
229271
self,
230272
apply_ledger_update: bool,

acapy_agent/revocation/models/issuer_rev_reg_record.py

Lines changed: 84 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,11 @@
1414
from uuid_utils import uuid4
1515

1616
from ...core.profile import Profile, ProfileSession
17-
from ...indy.credx.issuer import CATEGORY_CRED_DEF, CATEGORY_REV_REG_DEF_PRIVATE
17+
from ...indy.credx.issuer import (
18+
CATEGORY_CRED_DEF,
19+
CATEGORY_REV_REG,
20+
CATEGORY_REV_REG_DEF_PRIVATE,
21+
)
1822
from ...indy.issuer import IndyIssuer, IndyIssuerError
1923
from ...indy.models.revocation import (
2024
IndyRevRegDef,
@@ -358,91 +362,108 @@ async def send_entry(
358362

359363
return rev_entry_res
360364

365+
def _get_revoked_discrepancies(
366+
self, recs: Sequence[IssuerCredRevRecord], rev_reg_delta: dict
367+
) -> Tuple[list, int]:
368+
revoked_ids = []
369+
rec_count = 0
370+
for rec in recs:
371+
if rec.state == IssuerCredRevRecord.STATE_REVOKED:
372+
revoked_ids.append(int(rec.cred_rev_id))
373+
if int(rec.cred_rev_id) not in rev_reg_delta["value"]["revoked"]:
374+
rec_count += 1
375+
376+
return revoked_ids, rec_count
377+
361378
async def fix_ledger_entry(
362379
self,
363380
profile: Profile,
364381
apply_ledger_update: bool,
365382
genesis_transactions: str,
366383
) -> Tuple[dict, dict, dict]:
367384
"""Fix the ledger entry to match wallet-recorded credentials."""
385+
recovery_txn = {}
386+
applied_txn = {}
387+
368388
# get rev reg delta (revocations published to ledger)
369389
ledger = profile.inject(BaseLedger)
370390
async with ledger:
371391
(rev_reg_delta, _) = await ledger.get_revoc_reg_delta(self.revoc_reg_id)
372392

373393
# get rev reg records from wallet (revocations and status)
374-
recs = []
375-
rec_count = 0
376-
accum_count = 0
377-
recovery_txn = {}
378-
applied_txn = {}
379394
async with profile.session() as session:
380395
recs = await IssuerCredRevRecord.query_by_ids(
381396
session, rev_reg_id=self.revoc_reg_id
382397
)
383398

384-
revoked_ids = []
385-
for rec in recs:
386-
if rec.state == IssuerCredRevRecord.STATE_REVOKED:
387-
revoked_ids.append(int(rec.cred_rev_id))
388-
if int(rec.cred_rev_id) not in rev_reg_delta["value"]["revoked"]:
389-
# await rec.set_state(session, IssuerCredRevRecord.STATE_ISSUED)
390-
rec_count += 1
399+
revoked_ids, rec_count = self._get_revoked_discrepancies(recs, rev_reg_delta)
400+
401+
LOGGER.debug(f"Fixed entry recs count = {rec_count}")
402+
LOGGER.debug(f"Rev reg entry value: {self.revoc_reg_entry.value}")
403+
LOGGER.debug(f'Rev reg delta: {rev_reg_delta.get("value")}')
404+
405+
# No update required if no discrepancies
406+
if rec_count == 0:
407+
return (rev_reg_delta, {}, {})
408+
409+
# We have revocation discrepancies, generate the recovery txn
410+
async with profile.session() as session:
411+
# We need the cred_def and rev_reg_def_private to generate the recovery txn
412+
issuer_rev_reg_record = await IssuerRevRegRecord.retrieve_by_revoc_reg_id(
413+
session, self.revoc_reg_id
414+
)
415+
cred_def_id = issuer_rev_reg_record.cred_def_id
416+
cred_def = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id)
417+
rev_reg_def_private = await session.handle.fetch(
418+
CATEGORY_REV_REG_DEF_PRIVATE, self.revoc_reg_id
419+
)
391420

392-
LOGGER.debug(">>> fixed entry recs count = %s", rec_count)
393-
LOGGER.debug(
394-
">>> rev_reg_record.revoc_reg_entry.value: %s",
395-
self.revoc_reg_entry.value,
421+
credx_module = importlib.import_module("indy_credx")
422+
cred_defn = credx_module.CredentialDefinition.load(cred_def.value_json)
423+
rev_reg_defn_private = credx_module.RevocationRegistryDefinitionPrivate.load(
424+
rev_reg_def_private.value_json
396425
)
397-
LOGGER.debug('>>> rev_reg_delta.get("value"): %s', rev_reg_delta.get("value"))
398-
399-
# if we had any revocation discrepancies, check the accumulator value
400-
if rec_count > 0:
401-
if (self.revoc_reg_entry.value and rev_reg_delta.get("value")) and not (
402-
self.revoc_reg_entry.value.accum == rev_reg_delta["value"]["accum"]
403-
):
404-
# self.revoc_reg_entry = rev_reg_delta["value"]
405-
# await self.save(session)
406-
accum_count += 1
426+
calculated_txn = await generate_ledger_rrrecovery_txn(
427+
genesis_transactions,
428+
self.revoc_reg_id,
429+
revoked_ids,
430+
cred_defn,
431+
rev_reg_defn_private,
432+
)
433+
recovery_txn = json.loads(calculated_txn.to_json())
434+
435+
LOGGER.debug(f"Applying ledger update: {apply_ledger_update}")
436+
if apply_ledger_update:
407437
async with profile.session() as session:
408-
issuer_rev_reg_record = await IssuerRevRegRecord.retrieve_by_revoc_reg_id(
409-
session, self.revoc_reg_id
438+
ledger = session.inject_or(BaseLedger)
439+
if not ledger:
440+
reason = "No ledger available"
441+
if not session.context.settings.get_value("wallet.type"):
442+
reason += ": missing wallet-type?"
443+
raise LedgerError(reason=reason)
444+
445+
async with ledger:
446+
ledger_response = await ledger.send_revoc_reg_entry(
447+
self.revoc_reg_id, "CL_ACCUM", recovery_txn
448+
)
449+
450+
applied_txn = ledger_response["result"]
451+
452+
# Update the local wallets rev reg entry with the new accumulator value
453+
async with profile.session() as session:
454+
rev_reg = await session.handle.fetch(
455+
CATEGORY_REV_REG, self.revoc_reg_id, for_update=True
410456
)
411-
cred_def_id = issuer_rev_reg_record.cred_def_id
412-
_cred_def = await session.handle.fetch(CATEGORY_CRED_DEF, cred_def_id)
413-
_rev_reg_def_private = await session.handle.fetch(
414-
CATEGORY_REV_REG_DEF_PRIVATE, self.revoc_reg_id
457+
new_value_json = rev_reg.value_json
458+
new_value_json["value"]["accum"] = applied_txn["txn"]["data"]["value"][
459+
"accum"
460+
]
461+
await session.handle.replace(
462+
CATEGORY_REV_REG,
463+
rev_reg.name,
464+
json.dumps(new_value_json),
465+
rev_reg.tags,
415466
)
416-
credx_module = importlib.import_module("indy_credx")
417-
cred_defn = credx_module.CredentialDefinition.load(_cred_def.value_json)
418-
rev_reg_defn_private = credx_module.RevocationRegistryDefinitionPrivate.load(
419-
_rev_reg_def_private.value_json
420-
)
421-
calculated_txn = await generate_ledger_rrrecovery_txn(
422-
genesis_transactions,
423-
self.revoc_reg_id,
424-
revoked_ids,
425-
cred_defn,
426-
rev_reg_defn_private,
427-
)
428-
recovery_txn = json.loads(calculated_txn.to_json())
429-
430-
LOGGER.debug(">>> apply_ledger_update = %s", apply_ledger_update)
431-
if apply_ledger_update:
432-
async with profile.session() as session:
433-
ledger = session.inject_or(BaseLedger)
434-
if not ledger:
435-
reason = "No ledger available"
436-
if not session.context.settings.get_value("wallet.type"):
437-
reason += ": missing wallet-type?"
438-
raise LedgerError(reason=reason)
439-
440-
async with ledger:
441-
ledger_response = await ledger.send_revoc_reg_entry(
442-
self.revoc_reg_id, "CL_ACCUM", recovery_txn
443-
)
444-
445-
applied_txn = ledger_response["result"]
446467

447468
return (rev_reg_delta, recovery_txn, applied_txn)
448469

0 commit comments

Comments
 (0)