From c480cae4824fc7a46822920bd64e9e2bc2c6afd0 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Mon, 8 Aug 2022 09:26:41 -0700 Subject: [PATCH 1/5] Update integration tests to replicate LSBC issue Signed-off-by: Ian Costanzo --- demo/features/steps/0453-issue-credential.py | 9 +- .../taa-txn-author-acceptance.feature | 106 ++++++++++++++++++ 2 files changed, 114 insertions(+), 1 deletion(-) diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index e59f668b15..dffb380c0a 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -155,9 +155,16 @@ def step_impl(context, holder): # get the required revocation info from the last credential exchange cred_exchange = context.cred_exchange + print("cred_exchange:", json.dumps(cred_exchange)) + + cred_ex_id = ( + cred_exchange["cred_ex_id"] + if "cred_ex_id" in cred_exchange + else cred_exchange["cred_ex_record"]["cred_ex_id"] + ) cred_exchange = agent_container_GET( - agent["agent"], "/issue-credential-2.0/records/" + cred_exchange["cred_ex_id"] + agent["agent"], "/issue-credential-2.0/records/" + cred_ex_id ) context.cred_exchange = cred_exchange print("rev_reg_id:", cred_exchange["indy"]["rev_reg_id"]) diff --git a/demo/features/taa-txn-author-acceptance.feature b/demo/features/taa-txn-author-acceptance.feature index bcaaa3dc32..e6c99a44fe 100644 --- a/demo/features/taa-txn-author-acceptance.feature +++ b/demo/features/taa-txn-author-acceptance.feature @@ -90,3 +90,109 @@ Feature: TAA Transaction Author Agreement related tests Examples: | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + Then "Faber" posts a revocation correction to the ledger + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.1-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + Then "Faber" posts a revocation correction to the ledger + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.2-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA + And "Bob" has an issued credential from "" + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation + Then "Faber" posts a revocation correction to the ledger + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | From 89c436decd8bd2d3bfaca239a62b0bc4ac4f6a75 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Mon, 8 Aug 2022 15:06:26 -0700 Subject: [PATCH 2/5] Refactor ledger-correcting code Signed-off-by: Ian Costanzo --- aries_cloudagent/revocation/manager.py | 113 ++++++++++++++++++++++++- aries_cloudagent/revocation/routes.py | 110 ++++++++---------------- 2 files changed, 147 insertions(+), 76 deletions(-) diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index 10db676f70..f5402337cf 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -10,10 +10,13 @@ from ..core.error import BaseError from ..core.profile import Profile from ..indy.issuer import IndyIssuer +from ..ledger.base import BaseLedger +from ..ledger.error import LedgerError, LedgerTransactionError from ..storage.error import StorageNotFoundError from .indy import IndyRevocation from .models.issuer_cred_rev_record import IssuerCredRevRecord from .models.issuer_rev_reg_record import IssuerRevRegRecord +from .recover import generate_ledger_rrrecovery_txn from .util import notify_pending_cleared_event, notify_revocation_published_event from ..protocols.issue_credential.v1_0.models.credential_exchange import ( V10CredentialExchange, @@ -147,7 +150,25 @@ async def revoke_credential( await txn.commit() await self.set_cred_revoked_state(rev_reg_id, crids) if delta_json: - await issuer_rr_upd.send_entry(self._profile) + try: + await issuer_rr_upd.send_entry(self._profile) + except LedgerTransactionError as err: + if "InvalidClientTaaAcceptanceError" in err.roll_up: + # ... if the ledger write fails (with "InvalidClientRequest") + # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: + # Ledger rejected transaction request: client request invalid: + # InvalidClientRequest(...) + # TODO in this scenario we try to post a correction + raise err + elif "InvalidClientRequest" in err.roll_up: + # if no write access (with "InvalidClientTaaAcceptanceError") + # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: + # Ledger rejected transaction request: client request invalid: + # InvalidClientTaaAcceptanceError(...) + raise err + else: + # not sure what happened, raise an error + raise err await notify_revocation_published_event( self._profile, rev_reg_id, [cred_rev_id] ) @@ -157,6 +178,96 @@ async def revoke_credential( await issuer_rr_rec.mark_pending(txn, cred_rev_id) await txn.commit() + async def update_rev_reg_revoked_state( + self, + rev_reg_id: str, + apply_ledger_update: bool, + rev_reg_record: IssuerRevRegRecord, + genesis_transactions: dict, + ): + """ + Request handler to fix ledger entry of credentials revoked against registry. + + Args: + rev_reg_id: revocation registry id + apply_ledger_update: whether to apply an update to the ledger + + Returns: + Number of credentials posted to ledger + + """ + # get rev reg delta (revocations published to ledger) + revoc = IndyRevocation(self._profile) + rev_reg_delta = await revoc.get_issuer_rev_reg_delta(rev_reg_id) + + # get rev reg records from wallet (revocations and status) + recs = [] + rec_count = 0 + accum_count = 0 + recovery_txn = {} + applied_txn = {} + async with self._profile.session() as session: + # rev_reg_record = await IssuerRevRegRecord.retrieve_by_revoc_reg_id( + # session, rev_reg_id + # ) + recs = await IssuerCredRevRecord.query_by_ids( + session, rev_reg_id=rev_reg_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 + + self._logger.debug(">>> fixed entry recs count = %s", rec_count) + self._logger.debug( + ">>> rev_reg_record.revoc_reg_entry.value: %s", + rev_reg_record.revoc_reg_entry.value, + ) + self._logger.debug( + '>>> rev_reg_delta.get("value"): %s', rev_reg_delta.get("value") + ) + + # if we had any revocation discrepencies, check the accumulator value + if rec_count > 0: + if ( + rev_reg_record.revoc_reg_entry.value and rev_reg_delta.get("value") + ) and not ( + rev_reg_record.revoc_reg_entry.value.accum + == rev_reg_delta["value"]["accum"] + ): + # rev_reg_record.revoc_reg_entry = rev_reg_delta["value"] + # await rev_reg_record.save(session) + accum_count += 1 + + calculated_txn = await generate_ledger_rrrecovery_txn( + genesis_transactions, + rev_reg_id, + revoked_ids, + ) + recovery_txn = json.loads(calculated_txn.to_json()) + + self._logger.debug(">>> apply_ledger_update = %s", apply_ledger_update) + if apply_ledger_update: + 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?" + raise LedgerError(reason=reason) + + async with ledger: + ledger_response = await ledger.send_revoc_reg_entry( + rev_reg_id, "CL_ACCUM", recovery_txn + ) + + applied_txn = ledger_response["result"] + + return (rev_reg_delta, recovery_txn, applied_txn) + async def publish_pending_revocations( self, rrid2crid: Mapping[Text, Sequence[Text]] = None, diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index 637332d55a..bfca31c560 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -59,7 +59,6 @@ IssuerCredRevRecordSchema, ) from .models.issuer_rev_reg_record import IssuerRevRegRecord, IssuerRevRegRecordSchema -from .recover import generate_ledger_rrrecovery_txn from .util import ( REVOCATION_EVENT_PREFIX, REVOCATION_REG_INIT_EVENT, @@ -728,13 +727,13 @@ async def get_rev_reg_indy_recs(request: web.BaseRequest): @response_schema(RevRegWalletUpdatedResultSchema(), 200, description="") async def update_rev_reg_revoked_state(request: web.BaseRequest): """ - Request handler to get number of credentials issued against revocation registry. + Request handler to fix ledger entry of credentials revoked against registry. Args: request: aiohttp request object Returns: - Number of credentials updated in wallet + Number of credentials posted to ledger """ context: AdminRequestContext = request["context"] @@ -745,16 +744,8 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): LOGGER.debug(">>> apply_ledger_update_json = %s", apply_ledger_update_json) apply_ledger_update = json.loads(request.query.get("apply_ledger_update", "false")) - # get rev reg delta (revocations published to ledger) - revoc = IndyRevocation(context.profile) - rev_reg_delta = await revoc.get_issuer_rev_reg_delta(rev_reg_id) - - # get rev reg records from wallet (revocations and status) - recs = [] - rec_count = 0 - accum_count = 0 - recovery_txn = {} - applied_txn = {} + rev_reg_record = None + genesis_transactions = None async with context.profile.session() as session: try: rev_reg_record = await IssuerRevRegRecord.retrieve_by_revoc_reg_id( @@ -762,72 +753,41 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): ) except StorageNotFoundError as err: raise web.HTTPNotFound(reason=err.roll_up) from err - recs = await IssuerCredRevRecord.query_by_ids(session, rev_reg_id=rev_reg_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(">>> fixed entry recs count = %s", rec_count) - LOGGER.debug( - ">>> rev_reg_record.revoc_reg_entry.value: %s", - rev_reg_record.revoc_reg_entry.value, - ) - LOGGER.debug('>>> rev_reg_delta.get("value"): %s', rev_reg_delta.get("value")) - - # if we had any revocation discrepencies, check the accumulator value - if rec_count > 0: - if ( - rev_reg_record.revoc_reg_entry.value and rev_reg_delta.get("value") - ) and not ( - rev_reg_record.revoc_reg_entry.value.accum - == rev_reg_delta["value"]["accum"] - ): - # rev_reg_record.revoc_reg_entry = rev_reg_delta["value"] - # await rev_reg_record.save(session) - accum_count += 1 - - genesis_transactions = context.settings.get("ledger.genesis_transactions") - if not genesis_transactions: - ledger_manager = context.injector.inject(BaseMultipleLedgerManager) - write_ledgers = await ledger_manager.get_write_ledger() - LOGGER.debug(f"write_ledgers = {write_ledgers}") - pool = write_ledgers[1].pool - LOGGER.debug(f"write_ledger pool = {pool}") - - genesis_transactions = pool.genesis_txns - - if not genesis_transactions: - raise web.HTTPInternalServerError( - reason="no genesis_transactions for writable ledger" - ) + genesis_transactions = context.settings.get("ledger.genesis_transactions") + if not genesis_transactions: + ledger_manager = context.injector.inject(BaseMultipleLedgerManager) + write_ledgers = await ledger_manager.get_write_ledger() + LOGGER.debug(f"write_ledgers = {write_ledgers}") + pool = write_ledgers[1].pool + LOGGER.debug(f"write_ledger pool = {pool}") + + genesis_transactions = pool.genesis_txns - calculated_txn = await generate_ledger_rrrecovery_txn( - genesis_transactions, - rev_reg_id, - revoked_ids, + if not genesis_transactions: + raise web.HTTPInternalServerError( + reason="no genesis_transactions for writable ledger" ) - recovery_txn = json.loads(calculated_txn.to_json()) - - LOGGER.debug(">>> apply_ledger_update = %s", apply_ledger_update) - if apply_ledger_update: - 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?" - raise web.HTTPInternalServerError(reason=reason) - - async with ledger: - ledger_response = await ledger.send_revoc_reg_entry( - rev_reg_id, "CL_ACCUM", recovery_txn - ) - applied_txn = ledger_response["result"] + if apply_ledger_update: + 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?" + raise web.HTTPInternalServerError(reason=reason) + + rev_manager = RevocationManager(context.profile) + try: + ( + rev_reg_delta, + recovery_txn, + applied_txn, + ) = await rev_manager.update_rev_reg_revoked_state( + rev_reg_id, apply_ledger_update, rev_reg_record, genesis_transactions + ) + except Exception as err: + raise web.HTTPBadRequest(reason=err.roll_up) return web.json_response( { From a655913b7728e3fd7295adba3fff3747199daa94 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 9 Aug 2022 15:03:32 -0700 Subject: [PATCH 3/5] Refactor ledger correction code and insert into revocation error handling Signed-off-by: Ian Costanzo --- aries_cloudagent/revocation/manager.py | 39 ++++++++++-- demo/features/steps/0453-issue-credential.py | 16 ++++- .../taa-txn-author-acceptance.feature | 60 +++++++++++++++++-- 3 files changed, 104 insertions(+), 11 deletions(-) diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index f5402337cf..0bb64a1b36 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -12,6 +12,7 @@ from ..indy.issuer import IndyIssuer from ..ledger.base import BaseLedger from ..ledger.error import LedgerError, LedgerTransactionError +from ..ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager from ..storage.error import StorageNotFoundError from .indy import IndyRevocation from .models.issuer_cred_rev_record import IssuerCredRevRecord @@ -153,21 +154,49 @@ async def revoke_credential( try: await issuer_rr_upd.send_entry(self._profile) except LedgerTransactionError as err: - if "InvalidClientTaaAcceptanceError" in err.roll_up: + if "InvalidClientRequest" in err.roll_up: # ... if the ledger write fails (with "InvalidClientRequest") # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: # Ledger rejected transaction request: client request invalid: # InvalidClientRequest(...) - # TODO in this scenario we try to post a correction - raise err - elif "InvalidClientRequest" in err.roll_up: + # In this scenario we try to post a correction + self._logger.warn("Retry ledger update/fix due to error") + self._logger.warn(err) + + async with self._profile.session() as session: + genesis_transactions = session.context.settings.get( + "ledger.genesis_transactions" + ) + if not genesis_transactions: + ledger_manager = session.context.injector.inject( + BaseMultipleLedgerManager + ) + write_ledgers = await ledger_manager.get_write_ledger() + self._logger.debug(f"write_ledgers = {write_ledgers}") + pool = write_ledgers[1].pool + self._logger.debug(f"write_ledger pool = {pool}") + + genesis_transactions = pool.genesis_txns + + (_, _, _) = await self.update_rev_reg_revoked_state( + rev_reg_id, + True, + issuer_rr_upd, + genesis_transactions, + ) + self._logger.warn("Ledger update/fix applied") + elif "InvalidClientTaaAcceptanceError" in err.roll_up: # if no write access (with "InvalidClientTaaAcceptanceError") # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: # Ledger rejected transaction request: client request invalid: # InvalidClientTaaAcceptanceError(...) + self._logger.error("Ledger update failed due to TAA issue") + self._logger.error(err) raise err else: # not sure what happened, raise an error + self._logger.error("Ledger update failed due to unknown issue") + self._logger.error(err) raise err await notify_revocation_published_event( self._profile, rev_reg_id, [cred_rev_id] @@ -184,7 +213,7 @@ async def update_rev_reg_revoked_state( apply_ledger_update: bool, rev_reg_record: IssuerRevRegRecord, genesis_transactions: dict, - ): + ) -> (dict, dict, dict): """ Request handler to fix ledger entry of credentials revoked against registry. diff --git a/demo/features/steps/0453-issue-credential.py b/demo/features/steps/0453-issue-credential.py index dffb380c0a..b5fac909fc 100644 --- a/demo/features/steps/0453-issue-credential.py +++ b/demo/features/steps/0453-issue-credential.py @@ -83,8 +83,14 @@ def step_impl(context, holder): # get the required revocation info from the last credential exchange cred_exchange = context.cred_exchange + cred_ex_id = ( + cred_exchange["cred_ex_id"] + if "cred_ex_id" in cred_exchange + else cred_exchange["cred_ex_record"]["cred_ex_id"] + ) + cred_exchange = agent_container_GET( - agent["agent"], "/issue-credential-2.0/records/" + cred_exchange["cred_ex_id"] + agent["agent"], "/issue-credential-2.0/records/" + cred_ex_id ) context.cred_exchange = cred_exchange print("rev_reg_id:", cred_exchange["indy"]["rev_reg_id"]) @@ -141,6 +147,13 @@ def step_impl(context, holder): + cred_exchange["indy"]["rev_reg_id"] + "/issued/indy_recs", ) + print("ledger_revoked_creds:", ledger_revoked_creds) + print( + "assert", + cred_exchange["indy"]["cred_rev_id"], + "in", + ledger_revoked_creds["rev_reg_delta"]["value"]["revoked"], + ) assert ( int(cred_exchange["indy"]["cred_rev_id"]) in ledger_revoked_creds["rev_reg_delta"]["value"]["revoked"] @@ -224,6 +237,7 @@ def step_impl(context, holder): + cred_exchange["indy"]["rev_reg_id"] + "/issued/indy_recs", ) + print("ledger_revoked_creds:", ledger_revoked_creds) assert ( int(cred_exchange["indy"]["cred_rev_id"]) not in ledger_revoked_creds["rev_reg_delta"]["value"]["revoked"] diff --git a/demo/features/taa-txn-author-acceptance.feature b/demo/features/taa-txn-author-acceptance.feature index e6c99a44fe..55d5790227 100644 --- a/demo/features/taa-txn-author-acceptance.feature +++ b/demo/features/taa-txn-author-acceptance.feature @@ -92,7 +92,7 @@ Feature: TAA Transaction Author Agreement related tests | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @T004-TAA @taa_required - Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger authomatically with the next revoked credential Given we have "2" agents | name | role | capabilities | | Faber | verifier | | @@ -111,10 +111,35 @@ Feature: TAA Transaction Author Agreement related tests And "Faber" fails to publish the credential revocation And "Faber" accepts the TAA And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.0-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger manually before revoking more credentials + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" And "Faber" attempts to revoke the credential And "Faber" fails to publish the credential revocation And "Faber" attempts to revoke the credential And "Faber" fails to publish the credential revocation + And "Faber" accepts the TAA Then "Faber" posts a revocation correction to the ledger And "Faber" successfully revoked the credential And "Bob" has an issued credential from "" @@ -126,7 +151,7 @@ Feature: TAA Transaction Author Agreement related tests | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @T004.1-TAA @taa_required - Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger by manually applying a correction Given we have "2" agents | name | role | capabilities | | Faber | verifier | | @@ -160,7 +185,7 @@ Feature: TAA Transaction Author Agreement related tests | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | @T004.2-TAA @taa_required - Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger automatically with the next revocation Given we have "2" agents | name | role | capabilities | | Faber | verifier | | @@ -183,12 +208,37 @@ Feature: TAA Transaction Author Agreement related tests And "Faber" fails to publish the credential revocation And "Faber" accepts the TAA And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + + Examples: + | issuer | Acme_capabilities | Bob_capabilities | Schema_name | Credential_data | Proof_request | + | Faber | --taa-accept --revocation --public-did | | driverslicense_v2 | Data_DL_MaxValues | DL_age_over_19_v2 | + + @T004.5-TAA @taa_required + Scenario Outline: Fail to publish revoked credential using a ledger with TAA required, and fix the ledger authomatically by revoking the last credential + Given we have "2" agents + | name | role | capabilities | + | Faber | verifier | | + | Bob | prover | | + And "Faber" connects to a ledger that requires acceptance of the TAA + And "Faber" accepts the TAA + And "Faber" and "Bob" have an existing connection + And "Bob" has an issued credential from "" + And "Faber" revokes the credential + And "Faber" successfully revoked the credential + When "Faber" rejects the TAA + And "Bob" has an issued credential from "" And "Faber" attempts to revoke the credential And "Faber" fails to publish the credential revocation And "Faber" attempts to revoke the credential And "Faber" fails to publish the credential revocation - Then "Faber" posts a revocation correction to the ledger - And "Faber" successfully revoked the credential + And "Faber" accepts the TAA + And "Faber" attempts to revoke the credential + And "Faber" fails to publish the credential revocation And "Bob" has an issued credential from "" And "Faber" revokes the credential And "Faber" successfully revoked the credential From e14ee18a79e5e429e1cc2c5cca8a86eb648d52da Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Wed, 10 Aug 2022 13:53:14 -0700 Subject: [PATCH 4/5] Refactoring and handle the publish separately use case Signed-off-by: Ian Costanzo --- aries_cloudagent/revocation/manager.py | 129 +----------------- .../models/issuer_rev_reg_record.py | 123 +++++++++++++++-- aries_cloudagent/revocation/routes.py | 12 +- 3 files changed, 131 insertions(+), 133 deletions(-) diff --git a/aries_cloudagent/revocation/manager.py b/aries_cloudagent/revocation/manager.py index 0bb64a1b36..592a879c7a 100644 --- a/aries_cloudagent/revocation/manager.py +++ b/aries_cloudagent/revocation/manager.py @@ -10,14 +10,10 @@ from ..core.error import BaseError from ..core.profile import Profile from ..indy.issuer import IndyIssuer -from ..ledger.base import BaseLedger -from ..ledger.error import LedgerError, LedgerTransactionError -from ..ledger.multiple_ledger.base_manager import BaseMultipleLedgerManager from ..storage.error import StorageNotFoundError from .indy import IndyRevocation from .models.issuer_cred_rev_record import IssuerCredRevRecord from .models.issuer_rev_reg_record import IssuerRevRegRecord -from .recover import generate_ledger_rrrecovery_txn from .util import notify_pending_cleared_event, notify_revocation_published_event from ..protocols.issue_credential.v1_0.models.credential_exchange import ( V10CredentialExchange, @@ -151,53 +147,7 @@ async def revoke_credential( await txn.commit() await self.set_cred_revoked_state(rev_reg_id, crids) if delta_json: - try: - await issuer_rr_upd.send_entry(self._profile) - except LedgerTransactionError as err: - if "InvalidClientRequest" in err.roll_up: - # ... if the ledger write fails (with "InvalidClientRequest") - # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: - # Ledger rejected transaction request: client request invalid: - # InvalidClientRequest(...) - # In this scenario we try to post a correction - self._logger.warn("Retry ledger update/fix due to error") - self._logger.warn(err) - - async with self._profile.session() as session: - genesis_transactions = session.context.settings.get( - "ledger.genesis_transactions" - ) - if not genesis_transactions: - ledger_manager = session.context.injector.inject( - BaseMultipleLedgerManager - ) - write_ledgers = await ledger_manager.get_write_ledger() - self._logger.debug(f"write_ledgers = {write_ledgers}") - pool = write_ledgers[1].pool - self._logger.debug(f"write_ledger pool = {pool}") - - genesis_transactions = pool.genesis_txns - - (_, _, _) = await self.update_rev_reg_revoked_state( - rev_reg_id, - True, - issuer_rr_upd, - genesis_transactions, - ) - self._logger.warn("Ledger update/fix applied") - elif "InvalidClientTaaAcceptanceError" in err.roll_up: - # if no write access (with "InvalidClientTaaAcceptanceError") - # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: - # Ledger rejected transaction request: client request invalid: - # InvalidClientTaaAcceptanceError(...) - self._logger.error("Ledger update failed due to TAA issue") - self._logger.error(err) - raise err - else: - # not sure what happened, raise an error - self._logger.error("Ledger update failed due to unknown issue") - self._logger.error(err) - raise err + await issuer_rr_upd.send_entry(self._profile) await notify_revocation_published_event( self._profile, rev_reg_id, [cred_rev_id] ) @@ -209,7 +159,6 @@ async def revoke_credential( async def update_rev_reg_revoked_state( self, - rev_reg_id: str, apply_ledger_update: bool, rev_reg_record: IssuerRevRegRecord, genesis_transactions: dict, @@ -225,77 +174,11 @@ async def update_rev_reg_revoked_state( Number of credentials posted to ledger """ - # get rev reg delta (revocations published to ledger) - revoc = IndyRevocation(self._profile) - rev_reg_delta = await revoc.get_issuer_rev_reg_delta(rev_reg_id) - - # get rev reg records from wallet (revocations and status) - recs = [] - rec_count = 0 - accum_count = 0 - recovery_txn = {} - applied_txn = {} - async with self._profile.session() as session: - # rev_reg_record = await IssuerRevRegRecord.retrieve_by_revoc_reg_id( - # session, rev_reg_id - # ) - recs = await IssuerCredRevRecord.query_by_ids( - session, rev_reg_id=rev_reg_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 - - self._logger.debug(">>> fixed entry recs count = %s", rec_count) - self._logger.debug( - ">>> rev_reg_record.revoc_reg_entry.value: %s", - rev_reg_record.revoc_reg_entry.value, - ) - self._logger.debug( - '>>> rev_reg_delta.get("value"): %s', rev_reg_delta.get("value") - ) - - # if we had any revocation discrepencies, check the accumulator value - if rec_count > 0: - if ( - rev_reg_record.revoc_reg_entry.value and rev_reg_delta.get("value") - ) and not ( - rev_reg_record.revoc_reg_entry.value.accum - == rev_reg_delta["value"]["accum"] - ): - # rev_reg_record.revoc_reg_entry = rev_reg_delta["value"] - # await rev_reg_record.save(session) - accum_count += 1 - - calculated_txn = await generate_ledger_rrrecovery_txn( - genesis_transactions, - rev_reg_id, - revoked_ids, - ) - recovery_txn = json.loads(calculated_txn.to_json()) - - self._logger.debug(">>> apply_ledger_update = %s", apply_ledger_update) - if apply_ledger_update: - 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?" - raise LedgerError(reason=reason) - - async with ledger: - ledger_response = await ledger.send_revoc_reg_entry( - rev_reg_id, "CL_ACCUM", recovery_txn - ) - - applied_txn = ledger_response["result"] - - return (rev_reg_delta, recovery_txn, applied_txn) + return await rev_reg_record.fix_ledger_entry( + self._profile, + apply_ledger_update, + genesis_transactions, + ) async def publish_pending_revocations( self, diff --git a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py index 98451dcb3d..b9c12d8065 100644 --- a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py @@ -22,6 +22,7 @@ ) from ...indy.util import indy_client_dir from ...ledger.base import BaseLedger +from ...ledger.error import LedgerError, LedgerTransactionError from ...messaging.models.base_record import BaseRecord, BaseRecordSchema from ...messaging.valid import ( BASE58_SHA256_HASH, @@ -33,7 +34,9 @@ from ...tails.base import BaseTailsServer from ..error import RevocationError +from ..recover import generate_ledger_rrrecovery_txn +from .issuer_cred_rev_record import IssuerCredRevRecord from .revocation_registry import RevocationRegistry DEFAULT_REGISTRY_SIZE = 1000 @@ -290,14 +293,44 @@ async def send_entry( ledger = profile.inject(BaseLedger) async with ledger: - rev_entry_res = await ledger.send_revoc_reg_entry( - self.revoc_reg_id, - self.revoc_def_type, - self._revoc_reg_entry.ser, - self.issuer_did, - write_ledger=write_ledger, - endorser_did=endorser_did, - ) + try: + rev_entry_res = await ledger.send_revoc_reg_entry( + self.revoc_reg_id, + self.revoc_def_type, + self._revoc_reg_entry.ser, + self.issuer_did, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) + except LedgerTransactionError as err: + if "InvalidClientRequest" in err.roll_up: + # ... if the ledger write fails (with "InvalidClientRequest") + # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: + # Ledger rejected transaction request: client request invalid: + # InvalidClientRequest(...) + # In this scenario we try to post a correction + LOGGER.warn("Retry ledger update/fix due to error") + LOGGER.warn(err) + (_, _, res) = await self.fix_ledger_entry( + profile, + True, + ledger.pool.genesis_txns, + ) + rev_entry_res = {"result": res} + LOGGER.warn("Ledger update/fix applied") + elif "InvalidClientTaaAcceptanceError" in err.roll_up: + # if no write access (with "InvalidClientTaaAcceptanceError") + # e.g. aries_cloudagent.ledger.error.LedgerTransactionError: + # Ledger rejected transaction request: client request invalid: + # InvalidClientTaaAcceptanceError(...) + LOGGER.error("Ledger update failed due to TAA issue") + LOGGER.error(err) + raise err + else: + # not sure what happened, raise an error + LOGGER.error("Ledger update failed due to unknown issue") + LOGGER.error(err) + raise err if self.state == IssuerRevRegRecord.STATE_POSTED: self.state = IssuerRevRegRecord.STATE_ACTIVE # initial entry activates async with profile.session() as session: @@ -307,6 +340,80 @@ async def send_entry( return rev_entry_res + async def fix_ledger_entry( + self, + profile: Profile, + apply_ledger_update: bool, + genesis_transactions: str, + ) -> (dict, dict, dict): + """Fix the ledger entry to match wallet-recorded credentials.""" + # get rev reg delta (revocations published to ledger) + ledger = profile.inject(BaseLedger) + async with ledger: + (rev_reg_delta, _) = await ledger.get_revoc_reg_delta(self.revoc_reg_id) + + # get rev reg records from wallet (revocations and status) + 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=self.revoc_reg_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(">>> fixed entry recs count = %s", rec_count) + LOGGER.debug( + ">>> rev_reg_record.revoc_reg_entry.value: %s", + self.revoc_reg_entry.value, + ) + LOGGER.debug( + '>>> rev_reg_delta.get("value"): %s', rev_reg_delta.get("value") + ) + + # if we had any revocation discrepencies, check the accumulator value + if rec_count > 0: + if (self.revoc_reg_entry.value and rev_reg_delta.get("value")) and not ( + self.revoc_reg_entry.value.accum == 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, + self.revoc_reg_id, + revoked_ids, + ) + recovery_txn = json.loads(calculated_txn.to_json()) + + LOGGER.debug(">>> apply_ledger_update = %s", apply_ledger_update) + if apply_ledger_update: + 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?" + raise LedgerError(reason=reason) + + async with ledger: + ledger_response = await ledger.send_revoc_reg_entry( + self.revoc_reg_id, "CL_ACCUM", recovery_txn + ) + + applied_txn = ledger_response["result"] + + return (rev_reg_delta, recovery_txn, applied_txn) + @property def has_local_tails_file(self) -> bool: """Check if a local copy of the tails file is available.""" diff --git a/aries_cloudagent/revocation/routes.py b/aries_cloudagent/revocation/routes.py index bfca31c560..e768a00049 100644 --- a/aries_cloudagent/revocation/routes.py +++ b/aries_cloudagent/revocation/routes.py @@ -784,10 +784,18 @@ async def update_rev_reg_revoked_state(request: web.BaseRequest): recovery_txn, applied_txn, ) = await rev_manager.update_rev_reg_revoked_state( - rev_reg_id, apply_ledger_update, rev_reg_record, genesis_transactions + apply_ledger_update, rev_reg_record, genesis_transactions ) - except Exception as err: + except ( + RevocationManagerError, + RevocationError, + StorageError, + IndyIssuerError, + LedgerError, + ) as err: raise web.HTTPBadRequest(reason=err.roll_up) + except Exception as err: + raise web.HTTPBadRequest(reason=str(err)) return web.json_response( { From 910aa205db8f25a6ecfad395139dbf37ba253585 Mon Sep 17 00:00:00 2001 From: Ian Costanzo Date: Tue, 16 Aug 2022 11:53:17 -0700 Subject: [PATCH 5/5] type fix Signed-off-by: Ian Costanzo --- aries_cloudagent/revocation/models/issuer_rev_reg_record.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py index b9c12d8065..2e9d7b7e42 100644 --- a/aries_cloudagent/revocation/models/issuer_rev_reg_record.py +++ b/aries_cloudagent/revocation/models/issuer_rev_reg_record.py @@ -7,7 +7,7 @@ from os.path import join from pathlib import Path from shutil import move -from typing import Any, Mapping, Sequence, Union +from typing import Any, Mapping, Sequence, Union, Tuple from urllib.parse import urlparse from marshmallow import fields, validate @@ -345,7 +345,7 @@ async def fix_ledger_entry( profile: Profile, apply_ledger_update: bool, genesis_transactions: str, - ) -> (dict, dict, dict): + ) -> 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)