diff --git a/aries_cloudagent/anoncreds/default/legacy_indy/recover.py b/aries_cloudagent/anoncreds/default/legacy_indy/recover.py new file mode 100644 index 0000000000..1933e69763 --- /dev/null +++ b/aries_cloudagent/anoncreds/default/legacy_indy/recover.py @@ -0,0 +1,126 @@ +"""Recover a revocation registry.""" + +import hashlib +import logging +import time + +import aiohttp +import base58 +import indy_vdr +from anoncreds import ( + RevocationRegistry, + RevocationRegistryDefinition, +) + +from ...models.anoncreds_revocation import RevList + +LOGGER = logging.getLogger(__name__) + + +""" +This module calculates a new ledger accumulator, based on the revocation status +on the ledger vs revocations recorded in the wallet. +The calculated transaction can be written to the ledger to get the ledger back +in sync with the wallet. +This function can be used if there were previous revocation errors (i.e. the +credential revocation was successfully written to the wallet but the ledger write +failed.) +""" + + +class RevocRecoveryException(Exception): + """Raise exception generating the recovery transaction.""" + + +async def _check_tails_hash_for_inconsistency(tails_location: str, tails_hash: str): + async with aiohttp.ClientSession() as session: + LOGGER.debug("Tails URL: %s", tails_location) + tails_data_http_response = await session.get(tails_location) + tails_data = await tails_data_http_response.read() + remote_tails_hash = base58.b58encode( + hashlib.sha256(tails_data).digest() + ).decode("utf-8") + if remote_tails_hash != tails_hash: + raise RevocRecoveryException( + f"Tails hash mismatch {remote_tails_hash} {tails_hash}" + ) + else: + LOGGER.debug(f"Checked tails hash: {tails_hash}") + + +async def fetch_txns(genesis_txns: str, registry_id: str, issuer_id: str) -> tuple[ + dict, + set[int], +]: + """Fetch tails file and revocation registry information.""" + + LOGGER.debug(f"Fetch revocation registry def {registry_id} from ledger") + revoc_reg_delta_request = indy_vdr.ledger.build_get_revoc_reg_def_request( + None, registry_id + ) + + pool = await indy_vdr.open_pool(transactions=genesis_txns) + result = await pool.submit_request(revoc_reg_delta_request) + if not result["data"]: + raise RevocRecoveryException(f"Registry definition not found for {registry_id}") + + # Load the anoncreds revocation registry definition + rev_reg_def_raw = result["data"] + rev_reg_def_raw["ver"] = "1.0" + rev_reg_def_raw["issuerId"] = issuer_id + revoc_reg_def = RevocationRegistryDefinition.load(rev_reg_def_raw) + + await _check_tails_hash_for_inconsistency( + revoc_reg_def.tails_location, revoc_reg_def.tails_hash + ) + + LOGGER.debug(f"Fetch revocation registry delta {registry_id} from ledger") + to_timestamp = int(time.time()) + revoc_reg_delta_request = indy_vdr.ledger.build_get_revoc_reg_delta_request( + None, registry_id, None, to_timestamp + ) + result = await pool.submit_request(revoc_reg_delta_request) + if not result["data"]: + raise RevocRecoveryException("Error fetching delta from ledger") + + registry_from_ledger = result["data"]["value"]["accum_to"] + registry_from_ledger["ver"] = "1.0" + revoked = set(result["data"]["value"]["revoked"]) + LOGGER.debug("Ledger revoked indexes: %s", revoked) + + return registry_from_ledger, revoked + + +async def generate_ledger_rrrecovery_txn(genesis_txns: str, rev_list: RevList): + """Generate a new ledger accum entry, using the wallet value if revocations ahead of ledger.""" # noqa: E501 + + registry_from_ledger, prev_revoked = await fetch_txns( + genesis_txns, rev_list.rev_reg_def_id, rev_list.issuer_id + ) + + set_revoked = { + index for index, value in enumerate(rev_list.revocation_list) if value == 1 + } + mismatch = prev_revoked - set_revoked + if mismatch: + LOGGER.warning( + "Credential index(es) revoked on the ledger, but not in wallet: %s", + mismatch, + ) + + updates = set_revoked - prev_revoked + if not updates: + LOGGER.debug("No updates to perform") + return {} + else: + LOGGER.debug("New revoked indexes: %s", updates) + + # Prepare the transaction to write to the ledger + registry = RevocationRegistry.load(registry_from_ledger) + registry = registry.to_dict() + registry["ver"] = "1.0" + registry["value"]["prevAccum"] = registry_from_ledger["value"]["accum"] + registry["value"]["accum"] = rev_list.current_accumulator + registry["value"]["issued"] = [] + registry["value"]["revoked"] = list(updates) + return registry diff --git a/aries_cloudagent/anoncreds/default/legacy_indy/registry.py b/aries_cloudagent/anoncreds/default/legacy_indy/registry.py index 582a7ec0c6..21f199454e 100644 --- a/aries_cloudagent/anoncreds/default/legacy_indy/registry.py +++ b/aries_cloudagent/anoncreds/default/legacy_indy/registry.py @@ -1,11 +1,16 @@ """Legacy Indy Registry.""" -import json +import asyncio import logging import re from asyncio import shield from typing import List, Optional, Pattern, Sequence, Tuple +from anoncreds import ( + CredentialDefinition, + RevocationRegistryDefinition, + RevocationRegistryDefinitionPrivate, +) from base58 import alphabet from uuid_utils import uuid4 @@ -13,14 +18,18 @@ from ....cache.base import BaseCache from ....config.injection_context import InjectionContext from ....core.event_bus import EventBus -from ....core.profile import Profile +from ....core.profile import Profile, ProfileSession from ....ledger.base import BaseLedger from ....ledger.error import ( LedgerError, LedgerObjectAlreadyExistsError, LedgerTransactionError, ) -from ....ledger.merkel_validation.constants import GET_SCHEMA +from ....ledger.merkel_validation.constants import ( + GET_REVOC_REG_DELTA, + GET_REVOC_REG_ENTRY, + GET_SCHEMA, +) from ....ledger.multiple_ledger.ledger_requests_executor import ( GET_CRED_DEF, IndyLedgerRequestsExecutor, @@ -33,7 +42,6 @@ ) from ....protocols.endorse_transaction.v1_0.util import is_author_role from ....revocation_anoncreds.models.issuer_cred_rev_record import IssuerCredRevRecord -from ....revocation_anoncreds.recover import generate_ledger_rrrecovery_txn from ....storage.error import StorageError from ....utils import sentinel from ....wallet.did_info import DIDInfo @@ -47,7 +55,7 @@ BaseAnonCredsResolver, ) from ...events import RevListFinishedEvent -from ...issuer import AnonCredsIssuer, AnonCredsIssuerError +from ...issuer import CATEGORY_CRED_DEF, AnonCredsIssuer, AnonCredsIssuerError from ...models.anoncreds_cred_def import ( CredDef, CredDefResult, @@ -72,12 +80,29 @@ SchemaResult, SchemaState, ) +from ...revocation import ( + CATEGORY_REV_LIST, + CATEGORY_REV_REG_DEF, + CATEGORY_REV_REG_DEF_PRIVATE, +) +from .recover import generate_ledger_rrrecovery_txn LOGGER = logging.getLogger(__name__) +# Defaults DEFAULT_CRED_DEF_TAG = "default" DEFAULT_SIGNATURE_TYPE = "CL" +# Common error messages +NO_LEDGER_AVAILABLE_MSG = "No ledger available" +MISSING_WALLET_TYPE_MSG = ": missing wallet-type?" +TRANSACTION_MANAGER_FAILED_MSG = "Transaction manager failed to create request: " +FAILED_TO_STORE_TRANSACTION_RECORD = "Failed to store transaction record" + +# Common settings +ENDORSER_AUTO = "endorser.auto_request" +WALLET_TYPE = "wallet.type" + class LegacyIndyRegistry(BaseAnonCredsResolver, BaseAnonCredsRegistrar): """LegacyIndyRegistry.""" @@ -163,9 +188,9 @@ async def get_schema(self, profile: Profile, schema_id: str) -> GetSchemaResult: ) if not ledger: - reason = "No ledger available" - if not profile.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" + reason = NO_LEDGER_AVAILABLE_MSG + if not profile.settings.get_value(WALLET_TYPE): + reason += MISSING_WALLET_TYPE_MSG raise AnonCredsResolutionError(reason) async with ledger: @@ -207,7 +232,7 @@ async def register_schema( # Assume endorser role on the network, no option for 3rd-party endorser ledger = profile.inject_or(BaseLedger) if not ledger: - raise AnonCredsRegistrationError("No ledger available") + raise AnonCredsRegistrationError(NO_LEDGER_AVAILABLE_MSG) # Translate schema into format expected by Indy LOGGER.debug("Registering schema: %s", schema_id) @@ -276,9 +301,9 @@ async def register_schema( meta_data=meta_data, ) except StorageError: - raise AnonCredsRegistrationError("Failed to store transaction record") + raise AnonCredsRegistrationError(FAILED_TO_STORE_TRANSACTION_RECORD) - if profile.settings.get("endorser.auto_request"): + if profile.settings.get(ENDORSER_AUTO): try: ( transaction, @@ -286,7 +311,7 @@ async def register_schema( ) = await transaction_manager.create_request(transaction=transaction) except (StorageError, TransactionManagerError) as err: raise AnonCredsRegistrationError( - "Transaction manager failed to create request: " + err.roll_up + TRANSACTION_MANAGER_FAILED_MSG + err.roll_up ) from err responder = profile.inject(BaseResponder) @@ -324,9 +349,9 @@ async def get_credential_definition( txn_record_type=GET_CRED_DEF, ) if not ledger: - reason = "No ledger available" - if not profile.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" + reason = NO_LEDGER_AVAILABLE_MSG + if not profile.settings.get_value(WALLET_TYPE): + reason += MISSING_WALLET_TYPE_MSG raise AnonCredsResolutionError(reason) async with ledger: @@ -367,7 +392,7 @@ async def register_credential_definition( ledger = profile.inject_or(BaseLedger) if not ledger: - raise AnonCredsRegistrationError("No ledger available") + raise AnonCredsRegistrationError(NO_LEDGER_AVAILABLE_MSG) # Check if in wallet but not on ledger issuer = AnonCredsIssuer(profile) @@ -463,9 +488,9 @@ async def register_credential_definition( meta_data=meta_data, ) except StorageError: - raise AnonCredsRegistrationError("Failed to store transaction record") + raise AnonCredsRegistrationError(FAILED_TO_STORE_TRANSACTION_RECORD) - if profile.settings.get("endorser.auto_request"): + if profile.settings.get(ENDORSER_AUTO): try: ( transaction, @@ -473,7 +498,7 @@ async def register_credential_definition( ) = await transaction_manager.create_request(transaction=transaction) except (StorageError, TransactionManagerError) as err: raise AnonCredsRegistrationError( - "Transaction manager failed to create request: " + err.roll_up + TRANSACTION_MANAGER_FAILED_MSG + err.roll_up ) from err responder = profile.inject(BaseResponder) @@ -511,9 +536,9 @@ async def get_revocation_registry_definition( txn_record_type=GET_CRED_DEF, ) if not ledger: - reason = "No ledger available" - if not profile.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" + reason = NO_LEDGER_AVAILABLE_MSG + if not profile.settings.get_value(WALLET_TYPE): + reason += MISSING_WALLET_TYPE_MSG raise AnonCredsResolutionError(reason) async with ledger: @@ -555,7 +580,7 @@ async def register_revocation_registry_definition( ledger = profile.inject(BaseLedger) if not ledger: - raise AnonCredsRegistrationError("No ledger available") + raise AnonCredsRegistrationError(NO_LEDGER_AVAILABLE_MSG) # Translate anoncreds object to indy object indy_rev_reg_def = { @@ -631,9 +656,9 @@ async def register_revocation_registry_definition( meta_data=meta_data, ) except StorageError: - raise AnonCredsRegistrationError("Failed to store transaction record") + raise AnonCredsRegistrationError(FAILED_TO_STORE_TRANSACTION_RECORD) - if profile.settings.get("endorser.auto_request"): + if profile.settings.get(ENDORSER_AUTO): try: ( transaction, @@ -641,7 +666,7 @@ async def register_revocation_registry_definition( ) = await transaction_manager.create_request(transaction=transaction) except (StorageError, TransactionManagerError) as err: raise AnonCredsRegistrationError( - "Transaction manager failed to create request: " + err.roll_up + TRANSACTION_MANAGER_FAILED_MSG + err.roll_up ) from err responder = profile.inject(BaseResponder) @@ -705,9 +730,9 @@ async def _get_ledger(self, profile: Profile, rev_reg_def_id: str): txn_record_type=GET_CRED_DEF, ) if not ledger: - reason = "No ledger available" - if not profile.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" + reason = NO_LEDGER_AVAILABLE_MSG + if not profile.settings.get_value(WALLET_TYPE): + reason += MISSING_WALLET_TYPE_MSG raise AnonCredsResolutionError(reason) return ledger_id, ledger @@ -776,8 +801,15 @@ async def _revoc_reg_entry_with_fix( endorser_did: str = None, ) -> dict: """Send a revocation registry entry to the ledger with fixes if needed.""" - # TODO Handle multitenancy and multi-ledger (like in get cred def) - ledger = profile.inject(BaseLedger) + multitenant_mgr = profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) + else: + ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) + _, ledger = await ledger_exec_inst.get_ledger_for_identifier( + rev_list.rev_reg_def_id, + txn_record_type=GET_REVOC_REG_ENTRY, + ) try: async with ledger: @@ -817,7 +849,6 @@ async def _revoc_reg_entry_with_fix( "Ledger update failed due to TAA Issue" ) from err else: - # not sure what happened, raise an error LOGGER.exception("Ledger update failed due to unknown issue") raise AnonCredsRegistrationError( "Ledger update failed due to unknown issue" @@ -892,9 +923,9 @@ async def register_revocation_list( meta_data=meta_data, ) except StorageError: - raise AnonCredsRegistrationError("Failed to store transaction record") + raise AnonCredsRegistrationError(FAILED_TO_STORE_TRANSACTION_RECORD) - if profile.settings.get("endorser.auto_request"): + if profile.settings.get(ENDORSER_AUTO): try: ( transaction, @@ -902,7 +933,7 @@ async def register_revocation_list( ) = await transaction_manager.create_request(transaction=transaction) except (StorageError, TransactionManagerError) as err: raise AnonCredsRegistrationError( - "Transaction manager failed to create request: " + err.roll_up + TRANSACTION_MANAGER_FAILED_MSG + err.roll_up ) from err responder = profile.inject(BaseResponder) @@ -1006,9 +1037,9 @@ async def update_revocation_list( meta_data=meta_data, ) except StorageError: - raise AnonCredsRegistrationError("Failed to store transaction record") + raise AnonCredsRegistrationError(FAILED_TO_STORE_TRANSACTION_RECORD) - if profile.settings.get("endorser.auto_request"): + if profile.settings.get(ENDORSER_AUTO): try: ( transaction, @@ -1016,7 +1047,7 @@ async def update_revocation_list( ) = await transaction_manager.create_request(transaction=transaction) except (StorageError, TransactionManagerError) as err: raise AnonCredsRegistrationError( - "Transaction manager failed to create request: " + err.roll_up + TRANSACTION_MANAGER_FAILED_MSG + err.roll_up ) from err responder = profile.inject(BaseResponder) @@ -1047,64 +1078,56 @@ async def fix_ledger_entry( endorser_did: str = None, ) -> Tuple[dict, dict, dict]: """Fix the ledger entry to match wallet-recorded credentials.""" - # get rev reg delta (revocations published to ledger) - ledger = profile.inject(BaseLedger) + + def _wallet_accumalator_matches_ledger_list( + rev_list: RevList, rev_reg_delta: dict + ) -> bool: + return ( + rev_reg_delta.get("value") + and rev_list.current_accumulator == rev_reg_delta["value"]["accum"] + ) + + applied_txn = {} + recovery_txn = {} + + LOGGER.debug("Fixing ledger entry for revocation list...") + + multitenant_mgr = profile.inject_or(BaseMultitenantManager) + if multitenant_mgr: + ledger_exec_inst = IndyLedgerRequestsExecutor(profile) + else: + ledger_exec_inst = profile.inject(IndyLedgerRequestsExecutor) + _, ledger = await ledger_exec_inst.get_ledger_for_identifier( + rev_list.rev_reg_def_id, + txn_record_type=GET_REVOC_REG_DELTA, + ) + async with ledger: (rev_reg_delta, _) = await ledger.get_revoc_reg_delta( rev_list.rev_reg_def_id ) - # get rev reg records from wallet (revocations and list) - recs = [] - rec_count = 0 - accum_count = 0 - recovery_txn = {} - applied_txn = {} async with profile.session() as session: - recs = await IssuerCredRevRecord.query_by_ids( - session, rev_reg_id=rev_list.rev_reg_def_id - ) - revoked_ids = [] - for rec in recs: - if rec.state == IssuerCredRevRecord.STATE_REVOKED: - revoked_ids.append(int(rec.cred_rev_id)) - if int(rec.cred_rev_id) not in rev_reg_delta["value"]["revoked"]: - # await rec.set_state(session, IssuerCredRevRecord.STATE_ISSUED) - rec_count += 1 + LOGGER.debug(f"revocation_list = {rev_list.revocation_list}") + LOGGER.debug(f"rev_reg_delta = {rev_reg_delta.get('value')}") - LOGGER.debug(">>> fixed entry recs count = %s", rec_count) - LOGGER.debug( - ">>> rev_list.revocation_list: %s", - rev_list.revocation_list, - ) - LOGGER.debug( - '>>> rev_reg_delta.get("value"): %s', rev_reg_delta.get("value") + rev_list = await self._sync_wallet_rev_list_with_issuer_cred_rev_records( + session, rev_list ) - # if we had any revocation discrepancies, check the accumulator value - if rec_count > 0: - if (rev_list.current_accumulator and rev_reg_delta.get("value")) and ( - rev_list.current_accumulator != rev_reg_delta["value"]["accum"] - ): - # self.revoc_reg_entry = rev_reg_delta["value"] - # await self.save(session) - accum_count += 1 - - calculated_txn = await generate_ledger_rrrecovery_txn( - genesis_transactions, - rev_list.rev_reg_def_id, - revoked_ids, + if not _wallet_accumalator_matches_ledger_list(rev_list, rev_reg_delta): + + recovery_txn = await generate_ledger_rrrecovery_txn( + genesis_transactions, rev_list ) - recovery_txn = json.loads(calculated_txn.to_json()) - LOGGER.debug(">>> apply_ledger_update = %s", apply_ledger_update) - if apply_ledger_update: + if apply_ledger_update and recovery_txn: ledger = session.inject_or(BaseLedger) if not ledger: - reason = "No ledger available" - if not session.context.settings.get_value("wallet.type"): - reason += ": missing wallet-type?" + reason = NO_LEDGER_AVAILABLE_MSG + if not session.context.settings.get_value(WALLET_TYPE): + reason += MISSING_WALLET_TYPE_MSG raise LedgerError(reason=reason) async with ledger: @@ -1119,6 +1142,72 @@ async def fix_ledger_entry( return (rev_reg_delta, recovery_txn, applied_txn) + async def _sync_wallet_rev_list_with_issuer_cred_rev_records( + self, session: ProfileSession, rev_list: RevList + ) -> RevList: + """Sync the wallet revocation list with the issuer cred rev records.""" + + async def _revoked_issuer_cred_rev_record_ids() -> List[int]: + cred_rev_records = await IssuerCredRevRecord.query_by_ids( + session, rev_reg_id=rev_list.rev_reg_def_id + ) + return [ + int(rec.cred_rev_id) + for rec in cred_rev_records + if rec.state == "revoked" + ] + + def _revocation_list_to_array_of_indexes( + revocation_list: List[int], + ) -> List[int]: + return [index for index, value in enumerate(revocation_list) if value == 1] + + revoked = await _revoked_issuer_cred_rev_record_ids() + if revoked == _revocation_list_to_array_of_indexes(rev_list.revocation_list): + return rev_list + + # The revocation list is out of sync with the issuer cred rev records + # Recreate the revocation list with the issuer cred rev records + revoc_reg_def_entry = await session.handle.fetch( + CATEGORY_REV_REG_DEF, rev_list.rev_reg_def_id + ) + cred_def_entry = await session.handle.fetch( + CATEGORY_CRED_DEF, + RevRegDef.deserialize(revoc_reg_def_entry.value_json).cred_def_id, + ) + revoc_reg_def_private_entry = await session.handle.fetch( + CATEGORY_REV_REG_DEF_PRIVATE, rev_list.rev_reg_def_id + ) + updated_list = await asyncio.get_event_loop().run_in_executor( + None, + lambda: rev_list.to_native().update( + cred_def=CredentialDefinition.load(cred_def_entry.value_json), + rev_reg_def=RevocationRegistryDefinition.load( + revoc_reg_def_entry.value_json + ), + rev_reg_def_private=RevocationRegistryDefinitionPrivate.load( + revoc_reg_def_private_entry.raw_value + ), + issued=None, + revoked=revoked, + timestamp=None, + ), + ) + rev_list_entry_update = await session.handle.fetch( + CATEGORY_REV_LIST, rev_list.rev_reg_def_id, for_update=True + ) + tags = rev_list_entry_update.tags + rev_list_entry_update = rev_list_entry_update.value_json + rev_list_entry_update["rev_list"] = updated_list.to_dict() + + await session.handle.replace( + CATEGORY_REV_LIST, + rev_list.rev_reg_def_id, + value_json=rev_list_entry_update, + tags=tags, + ) + return RevList.deserialize(updated_list.to_json()) + async def txn_submit( self, ledger: BaseLedger, diff --git a/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_recover.py b/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_recover.py new file mode 100644 index 0000000000..2973c1824b --- /dev/null +++ b/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_recover.py @@ -0,0 +1,190 @@ +"""Test Recover.""" + +import hashlib +from unittest import IsolatedAsyncioTestCase + +import aiohttp +import base58 +import indy_vdr +import pytest +from anoncreds import RevocationRegistryDefinition + +from aries_cloudagent.tests import mock + +from ....models.anoncreds_revocation import RevList, RevRegDef, RevRegDefValue +from ..recover import ( + RevocRecoveryException, + _check_tails_hash_for_inconsistency, + fetch_txns, + generate_ledger_rrrecovery_txn, +) + +GENESIS = '{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"172.17.0.2","client_port":9702,"node_ip":"172.17.0.2","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"}\n{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"172.17.0.2","client_port":9704,"node_ip":"172.17.0.2","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"}\n{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node3","blskey":"3WFpdbg7C5cnLYZwFZevJqhubkFALBfCBBok15GdrKMUhUjGsk3jV6QKj6MZgEubF7oqCafxNdkm7eswgA4sdKTRc82tLGzZBd6vNqU8dupzup6uYUf32KTHTPQbuUM8Yk4QFXjEf2Usu2TJcNkdgpyeUSX42u5LqdDDpNSWUK5deC5","blskey_pop":"QwDeb2CkNSx6r8QC8vGQK3GRv7Yndn84TGNijX8YXHPiagXajyfTjoR87rXUu4G4QLk2cF8NNyqWiYMus1623dELWwx57rLCFqGh7N4ZRbGDRP4fnVcaKg1BcUxQ866Ven4gw8y4N56S5HzxXNBZtLYmhGHvDtk6PFkFwCvxYrNYjh","client_ip":"172.17.0.2","client_port":9706,"node_ip":"172.17.0.2","node_port":9705,"services":["VALIDATOR"]},"dest":"DKVxG2fXXTU8yT5N7hGEbXB3dfdAnYv1JczDUHpmDxya"},"metadata":{"from":"4cU41vWW82ArfxJxHkzXPG"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"7e9f355dffa78ed24668f0e0e369fd8c224076571c51e2ea8be5f26479edebe4"},"ver":"1"}\n{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"172.17.0.2","client_port":9708,"node_ip":"172.17.0.2","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}' + + +rev_reg_def = RevRegDef( + tag="tag", + cred_def_id="CsQY9MGeD3CQP4EyuVFo5m:3:CL:14951:MYCO_Biomarker", + value=RevRegDefValue( + max_cred_num=100, + public_keys={ + "accum_key": {"z": "1 0BB...386"}, + }, + tails_hash="58NNWYnVxVFzAfUztwGSNBL4551XNq6nXk56pCiKJxxt", + tails_location="http://tails-server.com", + ), + issuer_id="CsQY9MGeD3CQP4EyuVFo5m", + type="CL_ACCUM", +) + + +@pytest.mark.anoncreds +class TestLegacyIndyRecover(IsolatedAsyncioTestCase): + + @mock.patch.object( + indy_vdr, + "open_pool", + mock.CoroutineMock( + return_value=mock.MagicMock( + submit_request=mock.CoroutineMock(return_value={"data": {}}) + ) + ), + ) + async def test_fetch_txns_empty_data_from_ledger(self, *_): + with self.assertRaises(RevocRecoveryException): + await fetch_txns( + GENESIS, + "4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + "CsQY9MGeD3CQP4EyuVFo5m", + ) + + @mock.patch.object( + RevocationRegistryDefinition, + "load", + return_value=rev_reg_def.value, + ) + @mock.patch.object( + indy_vdr, + "open_pool", + mock.CoroutineMock( + return_value=mock.MagicMock( + submit_request=mock.CoroutineMock( + return_value={ + "data": { + "ver": "1.0", + "value": { + "accum_to": {}, + "revoked": [1, 0, 1, 0], + }, + } + } + ) + ) + ), + ) + @mock.patch( + "aries_cloudagent.anoncreds.default.legacy_indy.recover._check_tails_hash_for_inconsistency" + ) + async def test_fetch_txns(self, *_): + result = await fetch_txns( + GENESIS, + "4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + "CsQY9MGeD3CQP4EyuVFo5m", + ) + assert isinstance(result, tuple) + + @mock.patch( + "aries_cloudagent.anoncreds.default.legacy_indy.recover.fetch_txns", + mock.CoroutineMock( + return_value=( + { + "ver": "1.0", + "value": { + "accum": "2 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C" + }, + }, + {0, 1, 2}, + ) + ), + ) + async def test_generate_ledger_rrrecovery_txn(self): + + # Has updates + result = await generate_ledger_rrrecovery_txn( + GENESIS, + RevList( + issuer_id="CsQY9MGeD3CQP4EyuVFo5m", + current_accumulator="21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + revocation_list=[1, 1, 1, 1], + timestamp=1669640864487, + rev_reg_def_id="4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + ), + ) + assert result != {} + # Doesn't have updates + result = await generate_ledger_rrrecovery_txn( + GENESIS, + RevList( + issuer_id="CsQY9MGeD3CQP4EyuVFo5m", + current_accumulator="21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + revocation_list=[1, 1, 1, 0], + timestamp=1669640864487, + rev_reg_def_id="4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + ), + ) + assert result == {} + + # Logs waring when ledger has revoked indexes not in wallet + with mock.patch( + "aries_cloudagent.anoncreds.default.legacy_indy.recover.LOGGER" + ) as mock_logger: + result = await generate_ledger_rrrecovery_txn( + GENESIS, + RevList( + issuer_id="CsQY9MGeD3CQP4EyuVFo5m", + current_accumulator="21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + revocation_list=[1, 0, 0, 0], + timestamp=1669640864487, + rev_reg_def_id="4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + ), + ) + assert mock_logger.warning.called + assert result == {} + + @mock.patch.object( + aiohttp, + "ClientSession", + mock.MagicMock( + return_value=mock.MagicMock( + get=mock.CoroutineMock( + return_value=mock.MagicMock( + read=mock.CoroutineMock(return_value=b"some data") + ) + ) + ) + ), + ) + @mock.patch.object( + base58, + "b58encode", + side_effect=[ + b"58NNWYnVxVFzAfUztwGSNBL4551XNq6nXk56pCiKJxxt", + b"58NNWYnVxVFzAfUztwGSNBL4551XNq6nXk56pCiKJxxz", + ], + ) + @mock.patch.object( + hashlib, + "sha256", + return_value=mock.MagicMock(digest=mock.MagicMock()), + ) + async def test_check_tails_hash_for_inconsistency(self, *_): + # Matches + await _check_tails_hash_for_inconsistency( + "http://tails-server.com", "58NNWYnVxVFzAfUztwGSNBL4551XNq6nXk56pCiKJxxt" + ) + # Mismatch + with self.assertRaises(RevocRecoveryException): + await _check_tails_hash_for_inconsistency( + "http://tails-server.com", + "58NNWYnVxVFzAfUztwGSNBL4551XNq6nXk56pCiKJxxt", + ) diff --git a/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py b/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py index 830a0bb722..4e761fca2d 100644 --- a/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py +++ b/aries_cloudagent/anoncreds/default/legacy_indy/tests/test_registry.py @@ -5,7 +5,12 @@ from unittest import IsolatedAsyncioTestCase import pytest -from anoncreds import Schema +from anoncreds import ( + CredentialDefinition, + RevocationRegistryDefinition, + RevocationRegistryDefinitionPrivate, + Schema, +) from base58 import alphabet from .....anoncreds.base import ( @@ -18,9 +23,13 @@ ) from .....askar.profile_anon import AskarAnoncredsProfile from .....connections.models.conn_record import ConnRecord -from .....core.in_memory.profile import InMemoryProfile +from .....core.event_bus import EventBus +from .....core.in_memory.profile import InMemoryProfile, InMemoryProfileSession from .....ledger.base import BaseLedger from .....ledger.error import LedgerObjectAlreadyExistsError +from .....ledger.multiple_ledger.ledger_requests_executor import ( + IndyLedgerRequestsExecutor, +) from .....messaging.responder import BaseResponder from .....protocols.endorse_transaction.v1_0.manager import ( TransactionManager, @@ -28,6 +37,7 @@ from .....protocols.endorse_transaction.v1_0.models.transaction_record import ( TransactionRecord, ) +from .....revocation_anoncreds.models.issuer_cred_rev_record import IssuerCredRevRecord from .....tests import mock from ....issuer import AnonCredsIssuer from ....models.anoncreds_cred_def import ( @@ -41,6 +51,7 @@ RevListResult, RevRegDef, RevRegDefResult, + RevRegDefState, RevRegDefValue, ) from .. import registry as test_module @@ -81,6 +92,55 @@ ) +class MockTxn: + def to_json(self): + return json.dumps(self.__dict__) + + +class MockRevRegDefEntry: + def __init__(self, name="name"): + self.name = name + + tags = { + "state": RevRegDefState.STATE_ACTION, + } + value = "mock_value" + value_json = { + "value": { + "maxCredNum": 100, + "publicKeys": {"accumKey": {"z": "1 0BB...386"}}, + "tailsHash": "string", + "tailsLocation": "string", + }, + "credDefId": "CsQY9MGeD3CQP4EyuVFo5m:3:CL:14951:MYCO_Biomarker", + "issuerId": "CsQY9MGeD3CQP4EyuVFo5m", + "revocDefType": "CL_ACCUM", + "tag": "string", + } + + +class MockCredDefEntry: + value_json = {} + + +class MockRevListEntry: + tags = {} + value = "mock_value" + value_json = { + "issuerId": "CsQY9MGeD3CQP4EyuVFo5m", + "revRegDefId": "4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + "revocationList": [0, 1, 0, 0], + "currentAccumulator": "21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + "timestamp": 1669640864487, + } + + def to_json(self): + return self.value_json + + def to_dict(self): + return self.value_json + + @pytest.mark.anoncreds class TestLegacyIndyRegistry(IsolatedAsyncioTestCase): async def asyncSetUp(self): @@ -738,11 +798,29 @@ async def test_txn_submit(self): result = await self.registry.txn_submit(ledger, "test_txn") assert result == "transaction_id" - async def test_register_revocation_list_no_endorsement(self): - self.profile.context.injector.bind_instance( - BaseLedger, - mock.MagicMock(send_revoc_reg_entry=mock.CoroutineMock(return_value=1)), + @mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + return_value=( + "id", + mock.MagicMock( + send_revoc_reg_entry=mock.CoroutineMock(return_value="transaction_id") + ), + ), + ) + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_register_revocation_list_no_endorsement( + self, mock_handle, mock_send_revoc_reg_entry + ): + self.profile.inject_or = mock.MagicMock() + mock_handle.fetch = mock.CoroutineMock( + side_effect=[ + mock.CoroutineMock(return_value=None), + mock.CoroutineMock(return_value=None), + mock.CoroutineMock(return_value=None), + ] ) + result = await self.registry.register_revocation_list( self.profile, RevRegDef( @@ -770,9 +848,7 @@ async def test_register_revocation_list_no_endorsement(self): ) assert isinstance(result, RevListResult) - assert self.profile.context.injector.get_provider( - BaseLedger - )._instance.send_revoc_reg_entry.called + assert mock_send_revoc_reg_entry.called @mock.patch.object( ConnRecord, @@ -786,17 +862,28 @@ async def test_register_revocation_list_no_endorsement(self): "create_record", return_value=TransactionRecord(), ) - async def test_register_revocation_list_with_author_role( - self, mock_create_record, mock_endorsement_conn - ): - self.profile.context.injector.bind_instance( - BaseLedger, + @mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + return_value=( + "id", mock.MagicMock( send_revoc_reg_entry=mock.CoroutineMock( - return_value=("id", {"signed_txn": "txn"}) + return_value=( + "rev_reg_def_id", + { + "signed_txn": "txn", + }, + ) ) ), - ) + ), + ) + async def test_register_revocation_list_with_author_role( + self, mock_send_revoc_reg_entry, mock_create_record, _ + ): + + self.profile.inject_or = mock.MagicMock() self.profile.settings.set_value("endorser.author", True) result = await self.registry.register_revocation_list( @@ -828,10 +915,8 @@ async def test_register_revocation_list_with_author_role( ) assert isinstance(result, RevListResult) - assert self.profile.context.injector.get_provider( - BaseLedger - )._instance.send_revoc_reg_entry.called assert mock_create_record.called + assert mock_send_revoc_reg_entry.called @mock.patch.object( ConnRecord, @@ -845,17 +930,27 @@ async def test_register_revocation_list_with_author_role( "create_record", return_value=TransactionRecord(), ) - async def test_register_revocation_list_with_create_transaction_option( - self, mock_create_record, mock_endorsement_conn - ): - self.profile.context.injector.bind_instance( - BaseLedger, + @mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + return_value=( + "id", mock.MagicMock( send_revoc_reg_entry=mock.CoroutineMock( - return_value=("id", {"signed_txn": "txn"}) + return_value=( + "rev_reg_def_id", + { + "signed_txn": "txn", + }, + ) ) ), - ) + ), + ) + async def test_register_revocation_list_with_create_transaction_option( + self, mock_send_revoc_reg_entry, mock_create_record, _ + ): + self.profile.inject_or = mock.MagicMock() result = await self.registry.register_revocation_list( self.profile, @@ -887,10 +982,8 @@ async def test_register_revocation_list_with_create_transaction_option( ) assert isinstance(result, RevListResult) - assert self.profile.context.injector.get_provider( - BaseLedger - )._instance.send_revoc_reg_entry.called assert mock_create_record.called + assert mock_send_revoc_reg_entry.called @mock.patch.object( ConnRecord, @@ -909,17 +1002,27 @@ async def test_register_revocation_list_with_create_transaction_option( "create_request", return_value=(TransactionRecord(), "transaction_request"), ) - async def test_register_revocation_list_with_create_transaction_option_and_auto_request( - self, mock_create_request, mock_create_record, mock_endorsement_conn - ): - self.profile.context.injector.bind_instance( - BaseLedger, + @mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + return_value=( + "id", mock.MagicMock( send_revoc_reg_entry=mock.CoroutineMock( - return_value=("id", {"signed_txn": "txn"}) + return_value=( + "rev_reg_def_id", + { + "signed_txn": "txn", + }, + ) ) ), - ) + ), + ) + async def test_register_revocation_list_with_create_transaction_option_and_auto_request( + self, mock_send_revoc_reg_entry, mock_create_request, mock_create_record, _ + ): + self.profile.inject_or = mock.MagicMock() self.profile.context.injector.bind_instance( BaseResponder, mock.MagicMock(send=mock.CoroutineMock(return_value=None)), @@ -956,11 +1059,162 @@ async def test_register_revocation_list_with_create_transaction_option_and_auto_ ) assert isinstance(result, RevListResult) - assert self.profile.context.injector.get_provider( - BaseLedger - )._instance.send_revoc_reg_entry.called assert mock_create_record.called assert mock_create_request.called + assert mock_send_revoc_reg_entry.called assert self.profile.context.injector.get_provider( BaseResponder )._instance.send.called + + @mock.patch.object( + IndyLedgerRequestsExecutor, + "get_ledger_for_identifier", + return_value=( + "id", + mock.MagicMock( + get_revoc_reg_delta=mock.CoroutineMock( + return_value=( + { + "value": { + "accum": "21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + } + }, + 123, + ) + ) + ), + ), + ) + @mock.patch.object( + test_module.LegacyIndyRegistry, + "_sync_wallet_rev_list_with_issuer_cred_rev_records", + mock.CoroutineMock( + return_value=RevList( + issuer_id="CsQY9MGeD3CQP4EyuVFo5m", + current_accumulator="2 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + revocation_list=[1, 0, 1, 0], + timestamp=1669640864487, + rev_reg_def_id="4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + ) + ), + ) + @mock.patch( + "aries_cloudagent.anoncreds.default.legacy_indy.registry.generate_ledger_rrrecovery_txn", + mock.CoroutineMock(return_value=MockTxn()), + ) + async def test_fix_ledger_entry(self, *_): + + self.profile.context.injector.bind_instance( + BaseLedger, + mock.MagicMock(send_revoc_reg_entry=mock.CoroutineMock(return_value={})), + ) + + self.profile.context.injector.bind_instance( + EventBus, + {}, + ) + + async with self.profile.transaction() as txn: + issuer_cr_rec = IssuerCredRevRecord( + state=IssuerCredRevRecord.STATE_ISSUED, + cred_ex_id="cred_ex_id", + cred_ex_version=IssuerCredRevRecord.VERSION_1, + rev_reg_id="4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + cred_rev_id="cred_rev_id", + ) + await issuer_cr_rec.save( + txn, + reason=("Testing"), + ) + + self.profile.inject_or = mock.MagicMock() + result = await self.registry.fix_ledger_entry( + self.profile, + RevList( + issuer_id="CsQY9MGeD3CQP4EyuVFo5m", + current_accumulator="21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + revocation_list=[0, 1, 1, 0], + timestamp=1669640864487, + rev_reg_def_id="4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + ), + True, + '{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node1","blskey":"4N8aUNHSgjQVgkpm8nhNEfDf6txHznoYREg9kirmJrkivgL4oSEimFF6nsQ6M41QvhM2Z33nves5vfSn9n1UwNFJBYtWVnHYMATn76vLuL3zU88KyeAYcHfsih3He6UHcXDxcaecHVz6jhCYz1P2UZn2bDVruL5wXpehgBfBaLKm3Ba","blskey_pop":"RahHYiCvoNCtPTrVtP7nMC5eTYrsUA8WjXbdhNc8debh1agE9bGiJxWBXYNFbnJXoXhWFMvyqhqhRoq737YQemH5ik9oL7R4NTTCz2LEZhkgLJzB3QRQqJyBNyv7acbdHrAT8nQ9UkLbaVL9NBpnWXBTw4LEMePaSHEw66RzPNdAX1","client_ip":"172.17.0.2","client_port":9702,"node_ip":"172.17.0.2","node_port":9701,"services":["VALIDATOR"]},"dest":"Gw6pDLhcBcoQesN72qfotTgFa7cbuqZpkX3Xo6pLhPhv"},"metadata":{"from":"Th7MpTaRZVRYnPiabds81Y"},"type":"0"},"txnMetadata":{"seqNo":1,"txnId":"fea82e10e894419fe2bea7d96296a6d46f50f93f9eeda954ec461b2ed2950b62"},"ver":"1"}\n{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node2","blskey":"37rAPpXVoxzKhz7d9gkUe52XuXryuLXoM6P6LbWDB7LSbG62Lsb33sfG7zqS8TK1MXwuCHj1FKNzVpsnafmqLG1vXN88rt38mNFs9TENzm4QHdBzsvCuoBnPH7rpYYDo9DZNJePaDvRvqJKByCabubJz3XXKbEeshzpz4Ma5QYpJqjk","blskey_pop":"Qr658mWZ2YC8JXGXwMDQTzuZCWF7NK9EwxphGmcBvCh6ybUuLxbG65nsX4JvD4SPNtkJ2w9ug1yLTj6fgmuDg41TgECXjLCij3RMsV8CwewBVgVN67wsA45DFWvqvLtu4rjNnE9JbdFTc1Z4WCPA3Xan44K1HoHAq9EVeaRYs8zoF5","client_ip":"172.17.0.2","client_port":9704,"node_ip":"172.17.0.2","node_port":9703,"services":["VALIDATOR"]},"dest":"8ECVSk179mjsjKRLWiQtssMLgp6EPhWXtaYyStWPSGAb"},"metadata":{"from":"EbP4aYNeTHL6q385GuVpRV"},"type":"0"},"txnMetadata":{"seqNo":2,"txnId":"1ac8aece2a18ced660fef8694b61aac3af08ba875ce3026a160acbc3a3af35fc"},"ver":"1"}\n{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node3","blskey":"3WFpdbg7C5cnLYZwFZevJqhubkFALBfCBBok15GdrKMUhUjGsk3jV6QKj6MZgEubF7oqCafxNdkm7eswgA4sdKTRc82tLGzZBd6vNqU8dupzup6uYUf32KTHTPQbuUM8Yk4QFXjEf2Usu2TJcNkdgpyeUSX42u5LqdDDpNSWUK5deC5","blskey_pop":"QwDeb2CkNSx6r8QC8vGQK3GRv7Yndn84TGNijX8YXHPiagXajyfTjoR87rXUu4G4QLk2cF8NNyqWiYMus1623dELWwx57rLCFqGh7N4ZRbGDRP4fnVcaKg1BcUxQ866Ven4gw8y4N56S5HzxXNBZtLYmhGHvDtk6PFkFwCvxYrNYjh","client_ip":"172.17.0.2","client_port":9706,"node_ip":"172.17.0.2","node_port":9705,"services":["VALIDATOR"]},"dest":"DKVxG2fXXTU8yT5N7hGEbXB3dfdAnYv1JczDUHpmDxya"},"metadata":{"from":"4cU41vWW82ArfxJxHkzXPG"},"type":"0"},"txnMetadata":{"seqNo":3,"txnId":"7e9f355dffa78ed24668f0e0e369fd8c224076571c51e2ea8be5f26479edebe4"},"ver":"1"}\n{"reqSignature":{},"txn":{"data":{"data":{"alias":"Node4","blskey":"2zN3bHM1m4rLz54MJHYSwvqzPchYp8jkHswveCLAEJVcX6Mm1wHQD1SkPYMzUDTZvWvhuE6VNAkK3KxVeEmsanSmvjVkReDeBEMxeDaayjcZjFGPydyey1qxBHmTvAnBKoPydvuTAqx5f7YNNRAdeLmUi99gERUU7TD8KfAa6MpQ9bw","blskey_pop":"RPLagxaR5xdimFzwmzYnz4ZhWtYQEj8iR5ZU53T2gitPCyCHQneUn2Huc4oeLd2B2HzkGnjAff4hWTJT6C7qHYB1Mv2wU5iHHGFWkhnTX9WsEAbunJCV2qcaXScKj4tTfvdDKfLiVuU2av6hbsMztirRze7LvYBkRHV3tGwyCptsrP","client_ip":"172.17.0.2","client_port":9708,"node_ip":"172.17.0.2","node_port":9707,"services":["VALIDATOR"]},"dest":"4PS3EDQ3dW1tci1Bp6543CfuuebjFrg36kLAUcskGfaA"},"metadata":{"from":"TWwCRQRZ2ZHMJFn9TzLp7W"},"type":"0"},"txnMetadata":{"seqNo":4,"txnId":"aa5e817d7cc626170eca175822029339a444eb0ee8f0bd20d3b0b76e566fb008"},"ver":"1"}', + True, + "endorser_did", + ) + + assert isinstance(result, tuple) + + @mock.patch.object(CredentialDefinition, "load") + @mock.patch.object(RevocationRegistryDefinition, "load") + @mock.patch.object(RevocationRegistryDefinitionPrivate, "load") + @mock.patch.object( + IssuerCredRevRecord, + "query_by_ids", + return_value=[ + IssuerCredRevRecord( + state=IssuerCredRevRecord.STATE_REVOKED, + cred_ex_id="cred_ex_id", + rev_reg_id="4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + cred_rev_id="1", + ), + IssuerCredRevRecord( + state=IssuerCredRevRecord.STATE_REVOKED, + cred_ex_id="cred_ex_id", + rev_reg_id="4xE68b6S5VRFrKMMG1U95M:5:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + cred_rev_id="2", + ), + ], + ) + @mock.patch.object( + RevList, + "to_native", + return_value=mock.MagicMock( + update=mock.MagicMock(return_value=MockRevListEntry()) + ), + ) + @mock.patch.object(InMemoryProfileSession, "handle") + async def test_sync_wallet_rev_list_with_issuer_cred_rev_records( + self, mock_handle, *_ + ): + async with self.profile.session() as session: + # Matching revocations and rev_list + mock_handle.fetch = mock.CoroutineMock( + side_effect=[ + MockRevRegDefEntry(), + MockCredDefEntry(), + mock.CoroutineMock(return_value=None), + ] + ) + result = await self.registry._sync_wallet_rev_list_with_issuer_cred_rev_records( + session, + RevList( + issuer_id="CsQY9MGeD3CQP4EyuVFo5m", + current_accumulator="21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + revocation_list=[0, 1, 1, 0], + timestamp=1669640864487, + rev_reg_def_id="4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + ), + ) + assert isinstance(result, RevList) + # Non-matching revocations and rev_list + mock_handle.fetch = mock.CoroutineMock( + side_effect=[ + MockRevRegDefEntry(), + MockCredDefEntry(), + mock.CoroutineMock(return_value=None), + MockRevListEntry(), + ] + ) + mock_handle.replace = mock.CoroutineMock(return_value=None) + result = await self.registry._sync_wallet_rev_list_with_issuer_cred_rev_records( + session, + RevList( + issuer_id="CsQY9MGeD3CQP4EyuVFo5m", + current_accumulator="21 124C594B6B20E41B681E92B2C43FD165EA9E68BC3C9D63A82C8893124983CAE94 21 124C5341937827427B0A3A32113BD5E64FB7AB39BD3E5ABDD7970874501CA4897 6 5438CB6F442E2F807812FD9DC0C39AFF4A86B1E6766DBB5359E86A4D70401B0F 4 39D1CA5C4716FFC4FE0853C4FF7F081DFD8DF8D2C2CA79705211680AC77BF3A1 6 70504A5493F89C97C225B68310811A41AD9CD889301F238E93C95AD085E84191 4 39582252194D756D5D86D0EED02BF1B95CE12AED2FA5CD3C53260747D891993C", + revocation_list=[0, 1, 0, 0], + timestamp=1669640864487, + rev_reg_def_id="4xE68b6S5VRFrKMMG1U95M:4:4xE68b6S5VRFrKMMG1U95M:3:CL:59232:default:CL_ACCUM:4ae1cc6c-f6bd-486c-8057-88f2ce74e960", + ), + ) + assert isinstance(result, RevList) diff --git a/aries_cloudagent/anoncreds/revocation.py b/aries_cloudagent/anoncreds/revocation.py index 84a03ef866..a5d49e0dbb 100644 --- a/aries_cloudagent/anoncreds/revocation.py +++ b/aries_cloudagent/anoncreds/revocation.py @@ -577,7 +577,7 @@ async def update_revocation_list( self.profile, rev_reg_def, prev, curr, revoked, options ) - # # TODO Handle `failed` state + # TODO Handle `failed` state try: async with self.profile.session() as session: rev_list_entry_upd = await session.handle.fetch( @@ -1284,7 +1284,7 @@ async def revoke_pending_credentials( for rev_id in cred_revoc_ids: if rev_id < 1 or rev_id > max_cred_num: LOGGER.error( - "Skipping requested credential revocation" + "Skipping requested credential revocation " "on rev reg id %s, cred rev id=%s not in range", revoc_reg_id, rev_id, @@ -1292,7 +1292,7 @@ async def revoke_pending_credentials( failed_crids.add(rev_id) elif rev_id >= rev_info["next_index"]: LOGGER.warning( - "Skipping requested credential revocation" + "Skipping requested credential revocation " "on rev reg id %s, cred rev id=%s not yet issued", revoc_reg_id, rev_id, @@ -1300,7 +1300,7 @@ async def revoke_pending_credentials( failed_crids.add(rev_id) elif rev_list.revocation_list[rev_id] == 1: LOGGER.warning( - "Skipping requested credential revocation" + "Skipping requested credential revocation " "on rev reg id %s, cred rev id=%s already revoked", revoc_reg_id, rev_id, diff --git a/aries_cloudagent/revocation_anoncreds/routes.py b/aries_cloudagent/revocation_anoncreds/routes.py index e6cf3ec7e7..327627db96 100644 --- a/aries_cloudagent/revocation_anoncreds/routes.py +++ b/aries_cloudagent/revocation_anoncreds/routes.py @@ -883,6 +883,7 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): apply_ledger_update = json.loads(request.query.get("apply_ledger_update", "false")) genesis_transactions = None + recovery_txn = {} try: revocation = AnonCredsRevocation(profile) rev_reg_def = await revocation.get_created_revocation_registry_definition( @@ -944,8 +945,8 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): return web.json_response( { "rev_reg_delta": rev_reg_delta, - "accum_calculated": recovery_txn, - "accum_fixed": applied_txn, + "recovery_txn": recovery_txn, + "applied_txn": applied_txn, } )