|
1 | 1 | """Classes to manage credential revocation."""
|
2 | 2 |
|
| 3 | +import asyncio |
3 | 4 | import json
|
4 | 5 | import logging
|
5 | 6 | from typing import Mapping, NamedTuple, Optional, Sequence, Text, Tuple
|
6 | 7 |
|
| 8 | +from ..cache.base import BaseCache |
7 | 9 | from ..connections.models.conn_record import ConnRecord
|
8 | 10 | from ..core.error import BaseError
|
9 |
| -from ..core.profile import Profile |
| 11 | +from ..core.profile import Profile, ProfileSession |
| 12 | +from ..indy.credx.issuer import CATEGORY_REV_REG |
10 | 13 | from ..indy.issuer import IndyIssuer
|
| 14 | +from ..ledger.base import BaseLedger |
| 15 | +from ..messaging.responder import BaseResponder |
| 16 | +from ..protocols.endorse_transaction.v1_0.manager import ( |
| 17 | + TransactionManager, |
| 18 | + TransactionManagerError, |
| 19 | +) |
| 20 | +from ..protocols.endorse_transaction.v1_0.util import ( |
| 21 | + get_endorser_connection_id, |
| 22 | +) |
11 | 23 | from ..protocols.issue_credential.v1_0.models.credential_exchange import (
|
12 | 24 | V10CredentialExchange,
|
13 | 25 | )
|
14 | 26 | from ..protocols.issue_credential.v2_0.models.cred_ex_record import V20CredExRecord
|
15 | 27 | from ..protocols.revocation_notification.v1_0.models.rev_notification_record import (
|
16 | 28 | RevNotificationRecord,
|
17 | 29 | )
|
18 |
| -from ..storage.error import StorageNotFoundError |
| 30 | +from ..storage.error import StorageError, StorageNotFoundError |
19 | 31 | from .indy import IndyRevocation
|
20 | 32 | from .models.issuer_cred_rev_record import IssuerCredRevRecord
|
21 | 33 | from .models.issuer_rev_reg_record import IssuerRevRegRecord
|
22 | 34 | from .util import notify_pending_cleared_event, notify_revocation_published_event
|
23 | 35 |
|
| 36 | +LOGGER = logging.getLogger(__name__) |
| 37 | + |
24 | 38 |
|
25 | 39 | class RevocationManagerError(BaseError):
|
26 | 40 | """Revocation manager error."""
|
@@ -498,3 +512,140 @@ async def set_cred_revoked_state(
|
498 | 512 | await txn.commit()
|
499 | 513 | except StorageNotFoundError:
|
500 | 514 | pass
|
| 515 | + |
| 516 | + async def _get_endorser_info(self) -> Tuple[Optional[str], Optional[ConnRecord]]: |
| 517 | + connection_id = await get_endorser_connection_id(self._profile) |
| 518 | + |
| 519 | + endorser_did = None |
| 520 | + async with self._profile.session() as session: |
| 521 | + connection_record = await ConnRecord.retrieve_by_id(session, connection_id) |
| 522 | + endorser_info = await connection_record.metadata_get(session, "endorser_info") |
| 523 | + endorser_did = endorser_info.get("endorser_did") |
| 524 | + |
| 525 | + return endorser_did, connection_record |
| 526 | + |
| 527 | + async def fix_and_publish_from_invalid_accum_err(self, err_msg: str): |
| 528 | + """Fix and publish revocation registry entries from invalid accumulator error.""" |
| 529 | + cache = self._profile.inject_or(BaseCache) |
| 530 | + |
| 531 | + async def check_retry(accum): |
| 532 | + """Used to manage retries for fixing revocation registry entries.""" |
| 533 | + retry_value = await cache.get(accum) |
| 534 | + if not retry_value: |
| 535 | + await cache.set(accum, 5) |
| 536 | + else: |
| 537 | + if retry_value > 0: |
| 538 | + await cache.set(accum, retry_value - 1) |
| 539 | + else: |
| 540 | + LOGGER.error( |
| 541 | + f"Revocation registry entry transaction failed for {accum}" |
| 542 | + ) |
| 543 | + |
| 544 | + def get_genesis_transactions(): |
| 545 | + """Get the genesis transactions needed for fixing broken accum.""" |
| 546 | + genesis_transactions = self._profile.context.settings.get( |
| 547 | + "ledger.genesis_transactions" |
| 548 | + ) |
| 549 | + if not genesis_transactions: |
| 550 | + write_ledger = self._profile.context.injector.inject(BaseLedger) |
| 551 | + pool = write_ledger.pool |
| 552 | + genesis_transactions = pool.genesis_txns |
| 553 | + return genesis_transactions |
| 554 | + |
| 555 | + async def sync_accumulator(session: ProfileSession): |
| 556 | + """Sync the local accumulator with the ledger and create recovery txn.""" |
| 557 | + rev_reg_record = await IssuerRevRegRecord.retrieve_by_id( |
| 558 | + session, rev_reg_entry.name |
| 559 | + ) |
| 560 | + |
| 561 | + # Fix and get the recovery transaction |
| 562 | + ( |
| 563 | + rev_reg_delta, |
| 564 | + recovery_txn, |
| 565 | + applied_txn, |
| 566 | + ) = await rev_reg_record.fix_ledger_entry( |
| 567 | + self._profile, False, genesis_transactions |
| 568 | + ) |
| 569 | + |
| 570 | + # Update locally assuming ledger write will succeed |
| 571 | + rev_reg = await session.handle.fetch( |
| 572 | + CATEGORY_REV_REG, |
| 573 | + rev_reg_entry.value_json["revoc_reg_id"], |
| 574 | + for_update=True, |
| 575 | + ) |
| 576 | + new_value_json = rev_reg.value_json |
| 577 | + new_value_json["value"]["accum"] = recovery_txn["value"]["accum"] |
| 578 | + await session.handle.replace( |
| 579 | + CATEGORY_REV_REG, |
| 580 | + rev_reg.name, |
| 581 | + json.dumps(new_value_json), |
| 582 | + rev_reg.tags, |
| 583 | + ) |
| 584 | + |
| 585 | + return rev_reg_record, recovery_txn |
| 586 | + |
| 587 | + async def create_and_send_endorser_txn(): |
| 588 | + """Create and send the endorser transaction again.""" |
| 589 | + async with ledger: |
| 590 | + # Create the revocation registry entry |
| 591 | + rev_entry_res = await ledger.send_revoc_reg_entry( |
| 592 | + rev_reg_entry.value_json["revoc_reg_id"], |
| 593 | + "CL_ACCUM", |
| 594 | + recovery_txn, |
| 595 | + rev_reg_record.issuer_did, |
| 596 | + write_ledger=False, |
| 597 | + endorser_did=endorser_did, |
| 598 | + ) |
| 599 | + |
| 600 | + # Send the transaction to the endorser again with recovery txn |
| 601 | + transaction_manager = TransactionManager(self._profile) |
| 602 | + try: |
| 603 | + revo_transaction = await transaction_manager.create_record( |
| 604 | + messages_attach=rev_entry_res["result"], |
| 605 | + connection_id=connection.connection_id, |
| 606 | + ) |
| 607 | + ( |
| 608 | + revo_transaction, |
| 609 | + revo_transaction_request, |
| 610 | + ) = await transaction_manager.create_request(transaction=revo_transaction) |
| 611 | + except (StorageError, TransactionManagerError) as err: |
| 612 | + raise RevocationManagerError(err.roll_up) from err |
| 613 | + |
| 614 | + responder = self._profile.inject_or(BaseResponder) |
| 615 | + if not responder: |
| 616 | + raise RevocationManagerError( |
| 617 | + "No responder found. Unable to send transaction request" |
| 618 | + ) |
| 619 | + await responder.send( |
| 620 | + revo_transaction_request, |
| 621 | + connection_id=connection.connection_id, |
| 622 | + ) |
| 623 | + |
| 624 | + async with self._profile.session() as session: |
| 625 | + rev_reg_records = await session.handle.fetch_all( |
| 626 | + IssuerRevRegRecord.RECORD_TYPE |
| 627 | + ) |
| 628 | + # Cycle through all rev_rev_def records to find the offending accumulator |
| 629 | + for rev_reg_entry in rev_reg_records: |
| 630 | + ledger = session.inject_or(BaseLedger) |
| 631 | + # Get the value from the ledger |
| 632 | + async with ledger: |
| 633 | + (accum_response, _) = await ledger.get_revoc_reg_delta( |
| 634 | + rev_reg_entry.value_json["revoc_reg_id"] |
| 635 | + ) |
| 636 | + accum = accum_response.get("value", {}).get("accum") |
| 637 | + |
| 638 | + # If the accum from the ledger matches the error message, fix it |
| 639 | + if accum and accum in err_msg: |
| 640 | + await check_retry(accum) |
| 641 | + |
| 642 | + # Get the genesis transactions needed for fix |
| 643 | + genesis_transactions = get_genesis_transactions() |
| 644 | + |
| 645 | + # We know this needs endorsement |
| 646 | + endorser_did, connection = await self._get_endorser_info() |
| 647 | + rev_reg_record, recovery_txn = await sync_accumulator(session=session) |
| 648 | + await create_and_send_endorser_txn() |
| 649 | + |
| 650 | + # Some time in between re-tries |
| 651 | + await asyncio.sleep(1) |
0 commit comments