@@ -536,6 +536,7 @@ async def update_revocation_list(
536536 curr : RevList ,
537537 revoked : Sequence [int ],
538538 options : Optional [dict ] = None ,
539+ unrevoke : bool = False ,
539540 ):
540541 """Publish and update to a revocation list."""
541542 options = options or {}
@@ -576,7 +577,7 @@ async def update_revocation_list(
576577
577578 anoncreds_registry = self .profile .inject (AnonCredsRegistry )
578579 result = await anoncreds_registry .update_revocation_list (
579- self .profile , rev_reg_def , prev , curr , revoked , options
580+ self .profile , rev_reg_def , prev , curr , revoked , options , unrevoke
580581 )
581582
582583 # # TODO Handle `failed` state
@@ -1303,6 +1304,202 @@ async def revoke_pending_credentials(
13031304 failed = [str (rev_id ) for rev_id in sorted (failed_crids )],
13041305 )
13051306
1307+ async def unrevoke_pending_credentials (
1308+ self ,
1309+ revoc_reg_id : str ,
1310+ * ,
1311+ additional_crids : Optional [Sequence [int ]] = None ,
1312+ limit_crids : Optional [Sequence [int ]] = None ,
1313+ ) -> RevokeResult :
1314+ """UnRevoke a set of credentials in a revocation registry.
1315+
1316+ Args:
1317+ revoc_reg_id: ID of the revocation registry
1318+ additional_crids: sequences of additional credential indexes to revoke
1319+ limit_crids: a sequence of credential indexes to limit revocation to
1320+ If None, all pending revocations will be published.
1321+ If given, the intersection of pending and limit crids will be published.
1322+
1323+ Returns:
1324+ Tuple with the update revocation list, list of cred rev ids not revoked
1325+
1326+ """
1327+ updated_list = None
1328+ failed_crids = set ()
1329+ max_attempt = 5
1330+ attempt = 0
1331+
1332+ while True :
1333+ attempt += 1
1334+ if attempt >= max_attempt :
1335+ raise AnonCredsRevocationError (
1336+ "Repeated conflict attempting to update registry"
1337+ )
1338+ try :
1339+ async with self .profile .session () as session :
1340+ rev_reg_def_entry = await session .handle .fetch (
1341+ CATEGORY_REV_REG_DEF , revoc_reg_id
1342+ )
1343+ rev_list_entry = await session .handle .fetch (
1344+ CATEGORY_REV_LIST , revoc_reg_id
1345+ )
1346+ rev_reg_def_private_entry = await session .handle .fetch (
1347+ CATEGORY_REV_REG_DEF_PRIVATE , revoc_reg_id
1348+ )
1349+ except AskarError as err :
1350+ raise AnonCredsRevocationError (
1351+ "Error retrieving revocation registry"
1352+ ) from err
1353+
1354+ if (
1355+ not rev_reg_def_entry
1356+ or not rev_list_entry
1357+ or not rev_reg_def_private_entry
1358+ ):
1359+ raise AnonCredsRevocationError (
1360+ (
1361+ "Missing required revocation registry data: "
1362+ "revocation registry definition"
1363+ if not rev_reg_def_entry
1364+ else ""
1365+ ),
1366+ "revocation list" if not rev_list_entry else "" ,
1367+ (
1368+ "revocation registry private definition"
1369+ if not rev_reg_def_private_entry
1370+ else ""
1371+ ),
1372+ )
1373+
1374+ try :
1375+ async with self .profile .session () as session :
1376+ cred_def_entry = await session .handle .fetch (
1377+ CATEGORY_CRED_DEF , rev_reg_def_entry .value_json ["credDefId" ]
1378+ )
1379+ except AskarError as err :
1380+ raise AnonCredsRevocationError (
1381+ f"Error retrieving cred def { rev_reg_def_entry .value_json ['credDefId' ]} " # noqa: E501
1382+ ) from err
1383+
1384+ try :
1385+ # TODO This is a little rough; stored tails location will have public uri
1386+ # but library needs local tails location
1387+ rev_reg_def = RevRegDef .deserialize (rev_reg_def_entry .value_json )
1388+ rev_reg_def .value .tails_location = self .get_local_tails_path (
1389+ rev_reg_def
1390+ )
1391+ cred_def = CredDef .deserialize (cred_def_entry .value_json )
1392+ rev_reg_def_private = RevocationRegistryDefinitionPrivate .load (
1393+ rev_reg_def_private_entry .value_json
1394+ )
1395+ except AnoncredsError as err :
1396+ raise AnonCredsRevocationError (
1397+ "Error loading revocation registry definition"
1398+ ) from err
1399+
1400+ rev_crids = set ()
1401+ failed_crids = set ()
1402+ max_cred_num = rev_reg_def .value .max_cred_num
1403+ rev_info = rev_list_entry .value_json
1404+ cred_revoc_ids = (rev_info ["pending" ] or []) + (additional_crids or [])
1405+ rev_list = RevList .deserialize (rev_info ["rev_list" ])
1406+
1407+ for rev_id in cred_revoc_ids :
1408+ if rev_id < 1 or rev_id > max_cred_num :
1409+ LOGGER .error (
1410+ "Skipping requested credential revocation"
1411+ "on rev reg id %s, cred rev id=%s not in range" ,
1412+ revoc_reg_id ,
1413+ rev_id ,
1414+ )
1415+ failed_crids .add (rev_id )
1416+ elif rev_id >= rev_info ["next_index" ]:
1417+ LOGGER .warning (
1418+ "Skipping requested credential revocation"
1419+ "on rev reg id %s, cred rev id=%s not yet issued" ,
1420+ revoc_reg_id ,
1421+ rev_id ,
1422+ )
1423+ failed_crids .add (rev_id )
1424+ elif rev_list .revocation_list [rev_id ] == 0 :
1425+ LOGGER .warning (
1426+ "Skipping requested credential unrevocation"
1427+ "on rev reg id %s, cred rev id=%s is not revoked" ,
1428+ revoc_reg_id ,
1429+ rev_id ,
1430+ )
1431+ failed_crids .add (rev_id )
1432+ else :
1433+ rev_crids .add (rev_id )
1434+
1435+ if not rev_crids :
1436+ break
1437+
1438+ if limit_crids is None :
1439+ skipped_crids = set ()
1440+ else :
1441+ skipped_crids = rev_crids - set (limit_crids )
1442+ rev_crids = rev_crids - skipped_crids
1443+
1444+ try :
1445+ updated_list = await asyncio .get_event_loop ().run_in_executor (
1446+ None ,
1447+ lambda : rev_list .to_native ().update (
1448+ cred_def = cred_def .to_native (),
1449+ rev_reg_def = rev_reg_def .to_native (),
1450+ rev_reg_def_private = rev_reg_def_private ,
1451+ issued = list (rev_crids ),
1452+ revoked = None ,
1453+ timestamp = int (time .time ()),
1454+ ),
1455+ )
1456+ except AnoncredsError as err :
1457+ raise AnonCredsRevocationError (
1458+ "Error updating revocation registry"
1459+ ) from err
1460+
1461+ try :
1462+ async with self .profile .transaction () as txn :
1463+ rev_info_upd = await txn .handle .fetch (
1464+ CATEGORY_REV_LIST , revoc_reg_id , for_update = True
1465+ )
1466+ if not rev_info_upd :
1467+ LOGGER .warning (
1468+ "Revocation registry missing, skipping update: {}" ,
1469+ revoc_reg_id ,
1470+ )
1471+ updated_list = None
1472+ break
1473+ tags = rev_info_upd .tags
1474+ rev_info_upd = rev_info_upd .value_json
1475+ if rev_info_upd != rev_info :
1476+ # handle concurrent update to the registry by retrying
1477+ continue
1478+ rev_info_upd ["rev_list" ] = updated_list .to_dict ()
1479+ rev_info_upd ["pending" ] = (
1480+ list (skipped_crids ) if skipped_crids else None
1481+ )
1482+ tags ["pending" ] = json .dumps (True if skipped_crids else False )
1483+ await txn .handle .replace (
1484+ CATEGORY_REV_LIST ,
1485+ revoc_reg_id ,
1486+ value_json = rev_info_upd ,
1487+ tags = tags ,
1488+ )
1489+ await txn .commit ()
1490+ except AskarError as err :
1491+ raise AnonCredsRevocationError (
1492+ "Error saving revocation registry"
1493+ ) from err
1494+ break
1495+
1496+ return RevokeResult (
1497+ prev = rev_list ,
1498+ curr = RevList .from_native (updated_list ) if updated_list else None ,
1499+ revoked = list (rev_crids ),
1500+ failed = [str (rev_id ) for rev_id in sorted (failed_crids )],
1501+ )
1502+
13061503 async def mark_pending_revocations (self , rev_reg_def_id : str , * crids : int ):
13071504 """Cred rev ids stored to publish later."""
13081505 async with self .profile .transaction () as txn :
0 commit comments