From 0830d18fa4171ea4ac3bc6b0f7ae50ac27984cb7 Mon Sep 17 00:00:00 2001 From: Andrew Whitehead Date: Wed, 20 Jul 2022 11:39:39 -0700 Subject: [PATCH] move create_and_send_schema, create_and_send_credential_definition implementation to base class, fixes indentation error in indy_vdr method; cleanups Signed-off-by: Andrew Whitehead --- actions/run-integration-tests/action.yml | 5 +- aries_cloudagent/indy/credx/issuer.py | 2 +- aries_cloudagent/ledger/base.py | 248 +++++++++- aries_cloudagent/ledger/indy.py | 432 +++++------------- aries_cloudagent/ledger/indy_vdr.py | 269 ++--------- aries_cloudagent/ledger/tests/test_indy.py | 3 +- .../ledger/tests/test_indy_vdr.py | 5 +- 7 files changed, 410 insertions(+), 554 deletions(-) diff --git a/actions/run-integration-tests/action.yml b/actions/run-integration-tests/action.yml index 474f348ac2..61784bb6ba 100644 --- a/actions/run-integration-tests/action.yml +++ b/actions/run-integration-tests/action.yml @@ -20,9 +20,12 @@ runs: - name: run-integration-tests-acapy # to run with external ledger and tails server run as follows (and remove the ledger and tails actions from the workflow): # run: LEDGER_URL=http://test.bcovrin.vonx.io PUBLIC_TAILS_URL=https://tails.vonx.io ./run_bdd ${{ inputs.TEST_SCOPE }} - run: LEDGER_URL=${{inputs.IN_LEDGER_URL}} PUBLIC_TAILS_URL=${{inputs.IN_PUBLIC_TAILS_URL}} ./run_bdd ${{ inputs.TEST_SCOPE }} + run: ./run_bdd ${{ inputs.TEST_SCOPE }} shell: bash env: + LEDGER_URL: ${{ inputs.IN_LEDGER_URL }} + PUBLIC_TAILS_URL: ${{ inputs.IN_PUBLIC_TAILS_URL }} + LOG_LEVEL: warning NO_TTY: "1" working-directory: acapy/demo branding: diff --git a/aries_cloudagent/indy/credx/issuer.py b/aries_cloudagent/indy/credx/issuer.py index b0108fedbc..cc996ae3f6 100644 --- a/aries_cloudagent/indy/credx/issuer.py +++ b/aries_cloudagent/indy/credx/issuer.py @@ -109,7 +109,7 @@ async def credential_definition_in_wallet( async with self._profile.session() as session: return ( await session.handle.fetch( - CATEGORY_CRED_DEF_KEY_PROOF, credential_definition_id + CATEGORY_CRED_DEF_PRIVATE, credential_definition_id ) ) is not None except AskarError as err: diff --git a/aries_cloudagent/ledger/base.py b/aries_cloudagent/ledger/base.py index b916def364..451f0ffc41 100644 --- a/aries_cloudagent/ledger/base.py +++ b/aries_cloudagent/ledger/base.py @@ -1,5 +1,7 @@ """Ledger base class.""" +import json +import logging import re from abc import ABC, abstractmethod, ABCMeta @@ -7,12 +9,16 @@ from hashlib import sha256 from typing import Sequence, Tuple, Union -from ..indy.issuer import IndyIssuer +from ..indy.issuer import DEFAULT_CRED_DEF_TAG, IndyIssuer, IndyIssuerError from ..utils import sentinel from ..wallet.did_info import DIDInfo +from .error import BadLedgerRequestError, LedgerError, LedgerTransactionError + from .endpoint_type import EndpointType +LOGGER = logging.getLogger(__name__) + class BaseLedger(ABC, metaclass=ABCMeta): """Base class for ledger.""" @@ -133,6 +139,10 @@ def did_to_nym(self, did: str) -> str: if did: return re.sub(r"^did:\w+:", "", did) + @abstractmethod + async def get_wallet_public_did(self) -> DIDInfo: + """Fetch the public DID from the wallet.""" + @abstractmethod async def get_txn_author_agreement(self, reload: bool = False): """Get the current transaction author agreement, fetching it if necessary.""" @@ -171,12 +181,60 @@ async def txn_submit( self, request_json: str, sign: bool, - taa_accept: bool, + taa_accept: bool = None, sign_did: DIDInfo = sentinel, + write_ledger: bool = True, ) -> str: """Write the provided (signed and possibly endorsed) transaction to the ledger.""" @abstractmethod + async def fetch_schema_by_id(self, schema_id: str) -> dict: + """ + Get schema from ledger. + + Args: + schema_id: The schema id (or stringified sequence number) to retrieve + + Returns: + Indy schema dict + + """ + + @abstractmethod + async def fetch_schema_by_seq_no(self, seq_no: int) -> dict: + """ + Fetch a schema by its sequence number. + + Args: + seq_no: schema ledger sequence number + + Returns: + Indy schema dict + + """ + + async def check_existing_schema( + self, + public_did: str, + schema_name: str, + schema_version: str, + attribute_names: Sequence[str], + ) -> Tuple[str, dict]: + """Check if a schema has already been published.""" + fetch_schema_id = f"{public_did}:2:{schema_name}:{schema_version}" + schema = await self.fetch_schema_by_id(fetch_schema_id) + if schema: + fetched_attrs = schema["attrNames"].copy() + fetched_attrs.sort() + cmp_attrs = list(attribute_names) + cmp_attrs.sort() + if fetched_attrs != cmp_attrs: + raise LedgerTransactionError( + "Schema already exists on ledger, but attributes do not match: " + + f"{schema_name}:{schema_version} {fetched_attrs} != {cmp_attrs}" + ) + return fetch_schema_id, schema + async def create_and_send_schema( self, issuer: IndyIssuer, @@ -197,6 +255,92 @@ async def create_and_send_schema( """ + public_info = await self.get_wallet_public_did() + if not public_info: + raise BadLedgerRequestError("Cannot publish schema without a public DID") + + schema_info = await self.check_existing_schema( + public_info.did, schema_name, schema_version, attribute_names + ) + if schema_info: + LOGGER.warning("Schema already exists on ledger. Returning details.") + schema_id, schema_def = schema_info + else: + if self.read_only: + raise LedgerError( + "Error cannot write schema when ledger is in read only mode" + ) + + try: + schema_id, schema_json = await issuer.create_schema( + public_info.did, + schema_name, + schema_version, + attribute_names, + ) + except IndyIssuerError as err: + raise LedgerError(err.message) from err + schema_def = json.loads(schema_json) + + schema_req = await self._create_schema_request( + public_info, + schema_json, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) + + try: + resp = await self.txn_submit( + schema_req, + sign=True, + sign_did=public_info, + write_ledger=write_ledger, + ) + + if not write_ledger: + return schema_id, {"signed_txn": resp} + + try: + # parse sequence number out of response + seq_no = json.loads(resp)["result"]["txnMetadata"]["seqNo"] + schema_def["seqNo"] = seq_no + except KeyError as err: + raise LedgerError( + "Failed to parse schema sequence number from ledger response" + ) from err + except LedgerTransactionError as e: + # Identify possible duplicate schema errors on indy-node < 1.9 and > 1.9 + if ( + "can have one and only one SCHEMA with name" in e.message + or "UnauthorizedClientRequest" in e.message + ): + # handle potential race condition if multiple agents are publishing + # the same schema simultaneously + schema_info = await self.check_existing_schema( + public_info.did, schema_name, schema_version, attribute_names + ) + if schema_info: + LOGGER.warning( + "Schema already exists on ledger. Returning details." + " Error: %s", + e, + ) + schema_id, schema_def = schema_info + else: + raise + + return schema_id, schema_def + + @abstractmethod + async def _create_schema_request( + self, + public_info: DIDInfo, + schema_json: str, + write_ledger: bool = True, + endorser_did: str = None, + ): + """Create the ledger request for publishing a schema.""" + @abstractmethod async def get_revoc_reg_def(self, revoc_reg_id: str) -> dict: """Look up a revocation registry definition by ID.""" @@ -223,7 +367,6 @@ async def send_revoc_reg_entry( ): """Publish a revocation registry entry to the ledger.""" - @abstractmethod async def create_and_send_credential_definition( self, issuer: IndyIssuer, @@ -248,6 +391,105 @@ async def create_and_send_credential_definition( Tuple with cred def id, cred def structure, and whether it's novel """ + public_info = await self.get_wallet_public_did() + if not public_info: + raise BadLedgerRequestError( + "Cannot publish credential definition without a public DID" + ) + + schema = await self.get_schema(schema_id) + if not schema: + raise LedgerError(f"Ledger {self.pool_name} has no schema {schema_id}") + + novel = False + + # check if cred def is on ledger already + for test_tag in [tag] if tag else ["tag", DEFAULT_CRED_DEF_TAG]: + credential_definition_id = issuer.make_credential_definition_id( + public_info.did, schema, signature_type, test_tag + ) + ledger_cred_def = await self.fetch_credential_definition( + credential_definition_id + ) + if ledger_cred_def: + LOGGER.warning( + "Credential definition %s already exists on ledger %s", + credential_definition_id, + self.pool_name, + ) + + try: + if not await issuer.credential_definition_in_wallet( + credential_definition_id + ): + raise LedgerError( + f"Credential definition {credential_definition_id} is on " + f"ledger {self.pool_name} but not in wallet " + f"{self.profile.name}" + ) + except IndyIssuerError as err: + raise LedgerError(err.message) from err + + credential_definition_json = json.dumps(ledger_cred_def) + break + else: # no such cred def on ledger + try: + if await issuer.credential_definition_in_wallet( + credential_definition_id + ): + raise LedgerError( + f"Credential definition {credential_definition_id} is in " + f"wallet {self.profile.name} but not on ledger " + f"{self.pool.name}" + ) + except IndyIssuerError as err: + raise LedgerError(err.message) from err + + # Cred def is neither on ledger nor in wallet: create and send it + novel = True + try: + ( + credential_definition_id, + credential_definition_json, + ) = await issuer.create_and_store_credential_definition( + public_info.did, + schema, + signature_type, + tag, + support_revocation, + ) + except IndyIssuerError as err: + raise LedgerError(err.message) from err + + if self.read_only: + raise LedgerError( + "Error cannot write cred def when ledger is in read only mode" + ) + + cred_def_req = await self._create_credential_definition_request( + public_info, + credential_definition_json, + write_ledger=write_ledger, + endorser_did=endorser_did, + ) + + resp = await self.txn_submit( + cred_def_req, True, sign_did=public_info, write_ledger=write_ledger + ) + if not write_ledger: + return (credential_definition_id, {"signed_txn": resp}, novel) + + return (credential_definition_id, json.loads(credential_definition_json), novel) + + @abstractmethod + async def _create_credential_definition_request( + self, + public_info: DIDInfo, + credential_definition_json: str, + write_ledger: bool = True, + endorser_did: str = None, + ): + """Create the ledger request for publishing a credential definition.""" @abstractmethod async def get_credential_definition(self, credential_definition_id: str) -> dict: diff --git a/aries_cloudagent/ledger/indy.py b/aries_cloudagent/ledger/indy.py index 96ac9e0e3a..36aaef00c7 100644 --- a/aries_cloudagent/ledger/indy.py +++ b/aries_cloudagent/ledger/indy.py @@ -8,7 +8,7 @@ from io import StringIO from os import path from time import time -from typing import Sequence, Tuple, Optional +from typing import TYPE_CHECKING, Tuple, Optional import indy.ledger import indy.pool @@ -16,8 +16,6 @@ from ..cache.base import BaseCache from ..config.base import BaseInjector, BaseProvider, BaseSettings -from ..core.profile import Profile -from ..indy.issuer import DEFAULT_CRED_DEF_TAG, IndyIssuer, IndyIssuerError from ..indy.sdk.error import IndyErrorHandler from ..storage.base import StorageRecord from ..storage.indy import IndySdkStorage @@ -38,6 +36,9 @@ ) from .util import TAA_ACCEPTED_RECORD_TYPE +if TYPE_CHECKING: + from ..indy.sdk.profile import IndySdkProfile + LOGGER = logging.getLogger(__name__) GENESIS_TRANSACTION_FILE = "indy_genesis_transactions.txt" @@ -207,7 +208,7 @@ async def close(self): """Close the pool ledger.""" if self.opened: exc = None - for attempt in range(3): + for _attempt in range(3): try: await indy.pool.close_pool_ledger(self.handle) except IndyError as err: @@ -266,14 +267,14 @@ class IndySdkLedger(BaseLedger): def __init__( self, pool: IndySdkLedgerPool, - profile: Profile, + profile: "IndySdkProfile", ): """ Initialize an IndySdkLedger instance. Args: pool: The pool instance handling the raw ledger connection - wallet: The IndySdkWallet instance + profile: The IndySdkProfile instance """ self.pool = pool self.profile = profile @@ -331,11 +332,9 @@ async def _endorse( raise BadLedgerRequestError( "Cannot endorse transaction without a public DID" ) - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - endorsed_request_json = await indy.ledger.multi_sign_request( - wallet.opened.handle, public_info.did, request_json - ) + endorsed_request_json = await indy.ledger.multi_sign_request( + self.profile.wallet.handle, public_info.did, request_json + ) return endorsed_request_json async def _submit( @@ -354,6 +353,7 @@ async def _submit( sign: whether or not to sign the request taa_accept: whether to apply TAA acceptance to the (signed, write) request sign_did: override the signing DID + write_ledger: skip the request submission """ @@ -387,20 +387,18 @@ async def _submit( acceptance["time"], ) ) - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - if write_ledger: - submit_op = indy.ledger.sign_and_submit_request( - self.pool.handle, - wallet.opened.handle, - sign_did.did, - request_json, - ) - else: - # multi-sign, since we expect this to get endorsed later - submit_op = indy.ledger.multi_sign_request( - wallet.opened.handle, sign_did.did, request_json - ) + if write_ledger: + submit_op = indy.ledger.sign_and_submit_request( + self.pool.handle, + self.profile.wallet.handle, + sign_did.did, + request_json, + ) + else: + # multi-sign, since we expect this to get endorsed later + submit_op = indy.ledger.multi_sign_request( + self.profile.wallet.handle, sign_did.did, request_json + ) else: submit_op = indy.ledger.submit_request(self.pool.handle, request_json) @@ -443,125 +441,36 @@ async def txn_submit( sign: bool = None, taa_accept: bool = None, sign_did: DIDInfo = sentinel, + write_ledger: bool = True, ) -> str: """Submit a signed (and endorsed) transaction to the ledger.""" return await self._submit( - request_json, sign=sign, taa_accept=taa_accept, sign_did=sign_did + request_json, + sign=sign, + taa_accept=taa_accept, + sign_did=sign_did, + write_ledger=write_ledger, ) - async def create_and_send_schema( + async def _create_schema_request( self, - issuer: IndyIssuer, - schema_name: str, - schema_version: str, - attribute_names: Sequence[str], + public_info: DIDInfo, + schema_json: str, write_ledger: bool = True, endorser_did: str = None, - ) -> Tuple[str, dict]: - """ - Send schema to ledger. - - Args: - issuer: The issuer instance creating the schema - schema_name: The schema name - schema_version: The schema version - attribute_names: A list of schema attributes - - """ - - public_info = await self.get_wallet_public_did() - if not public_info: - raise BadLedgerRequestError("Cannot publish schema without a public DID") - - schema_info = await self.check_existing_schema( - public_info.did, schema_name, schema_version, attribute_names - ) - if schema_info: - LOGGER.warning("Schema already exists on ledger. Returning details.") - schema_id, schema_def = schema_info - else: - if self.pool.read_only: - raise LedgerError( - "Error cannot write schema when ledger is in read only mode" - ) - - try: - schema_id, schema_json = await issuer.create_schema( - public_info.did, - schema_name, - schema_version, - attribute_names, - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - schema_def = json.loads(schema_json) - - with IndyErrorHandler("Exception building schema request", LedgerError): - request_json = await indy.ledger.build_schema_request( - public_info.did, schema_json - ) - - try: - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - resp = await self._submit( - request_json, True, sign_did=public_info, write_ledger=write_ledger - ) - if not write_ledger: - return schema_id, {"signed_txn": resp} - try: - # parse sequence number out of response - seq_no = json.loads(resp)["result"]["txnMetadata"]["seqNo"] - schema_def["seqNo"] = seq_no - except KeyError as err: - raise LedgerError( - "Failed to parse schema sequence number from ledger response" - ) from err - except LedgerTransactionError as e: - # Identify possible duplicate schema errors on indy-node < 1.9 and > 1.9 - if "can have one and only one SCHEMA with name" in getattr( - e, "message", "" - ) or "UnauthorizedClientRequest" in getattr(e, "message", ""): - # handle potential race condition if multiple agents are publishing - # the same schema simultaneously - schema_info = await self.check_existing_schema( - public_info.did, schema_name, schema_version, attribute_names - ) - if schema_info: - LOGGER.warning( - "Schema already exists on ledger. Returning details." - " Error: %s", - e, - ) - schema_id, schema_def = schema_info - else: - raise + ): + """Create the ledger request for publishing a schema.""" + with IndyErrorHandler("Exception building schema request", LedgerError): + request_json = await indy.ledger.build_schema_request( + public_info.did, schema_json + ) - return schema_id, schema_def + if endorser_did and not write_ledger: + request_json = await indy.ledger.append_request_endorser( + request_json, endorser_did + ) - async def check_existing_schema( - self, - public_did: str, - schema_name: str, - schema_version: str, - attribute_names: Sequence[str], - ) -> Tuple[str, dict]: - """Check if a schema has already been published.""" - fetch_schema_id = f"{public_did}:2:{schema_name}:{schema_version}" - schema = await self.fetch_schema_by_id(fetch_schema_id) - if schema: - fetched_attrs = schema["attrNames"].copy() - fetched_attrs.sort() - cmp_attrs = list(attribute_names) - cmp_attrs.sort() - if fetched_attrs != cmp_attrs: - raise LedgerTransactionError( - "Schema already exists on ledger, but attributes do not match: " - + f"{schema_name}:{schema_version} {fetched_attrs} != {cmp_attrs}" - ) - return fetch_schema_id, schema + return request_json async def get_schema(self, schema_id: str) -> dict: """ @@ -622,7 +531,7 @@ async def fetch_schema_by_id(self, schema_id: str) -> dict: return parsed_response - async def fetch_schema_by_seq_no(self, seq_no: int): + async def fetch_schema_by_seq_no(self, seq_no: int) -> dict: """ Fetch a schema by its sequence number. @@ -654,123 +563,25 @@ async def fetch_schema_by_seq_no(self, seq_no: int): f"Could not get schema from ledger for seq no {seq_no}" ) - async def create_and_send_credential_definition( + async def _create_credential_definition_request( self, - issuer: IndyIssuer, - schema_id: str, - signature_type: str = None, - tag: str = None, - support_revocation: bool = False, + public_info: DIDInfo, + credential_definition_json: str, write_ledger: bool = True, endorser_did: str = None, - ) -> Tuple[str, dict, bool]: - """ - Send credential definition to ledger and store relevant key matter in wallet. - - Args: - issuer: The issuer instance to use for credential definition creation - schema_id: The schema id of the schema to create cred def for - signature_type: The signature type to use on the credential definition - tag: Optional tag to distinguish multiple credential definitions - support_revocation: Optional flag to enable revocation for this cred def - - Returns: - Tuple with cred def id, cred def structure, and whether it's novel - - """ - public_info = await self.get_wallet_public_did() - if not public_info: - raise BadLedgerRequestError( - "Cannot publish credential definition without a public DID" - ) - - schema = await self.get_schema(schema_id) - if not schema: - raise LedgerError(f"Ledger {self.pool.name} has no schema {schema_id}") - - novel = False - - # check if cred def is on ledger already - for test_tag in [tag] if tag else ["tag", DEFAULT_CRED_DEF_TAG]: - credential_definition_id = issuer.make_credential_definition_id( - public_info.did, schema, signature_type, test_tag - ) - ledger_cred_def = await self.fetch_credential_definition( - credential_definition_id + ): + """Create the ledger request for publishing a credential definition.""" + with IndyErrorHandler("Exception building cred def request", LedgerError): + request_json = await indy.ledger.build_cred_def_request( + public_info.did, credential_definition_json ) - if ledger_cred_def: - LOGGER.warning( - "Credential definition %s already exists on ledger %s", - credential_definition_id, - self.pool.name, - ) - - try: - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - if not await issuer.credential_definition_in_wallet( - credential_definition_id - ): - raise LedgerError( - f"Credential definition {credential_definition_id} is on " - f"ledger {self.pool.name} but not in wallet " - f"{wallet.opened.name}" - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - credential_definition_json = json.dumps(ledger_cred_def) - break - else: # no such cred def on ledger - try: - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - if await issuer.credential_definition_in_wallet( - credential_definition_id - ): - raise LedgerError( - f"Credential definition {credential_definition_id} is in " - f"wallet {wallet.opened.name} but not on ledger " - f"{self.pool.name}" - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - - # Cred def is neither on ledger nor in wallet: create and send it - novel = True - try: - ( - credential_definition_id, - credential_definition_json, - ) = await issuer.create_and_store_credential_definition( - public_info.did, - schema, - signature_type, - tag, - support_revocation, - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - - if self.pool.read_only: - raise LedgerError( - "Error cannot write cred def when ledger is in read only mode" - ) - with IndyErrorHandler("Exception building cred def request", LedgerError): - request_json = await indy.ledger.build_cred_def_request( - public_info.did, credential_definition_json - ) - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - resp = await self._submit( - request_json, True, sign_did=public_info, write_ledger=write_ledger + if endorser_did and not write_ledger: + request_json = await indy.ledger.append_request_endorser( + request_json, endorser_did ) - if not write_ledger: - return (credential_definition_id, {"signed_txn": resp}, novel) - return (credential_definition_id, json.loads(credential_definition_json), novel) + return request_json async def get_credential_definition(self, credential_definition_id: str) -> dict: """ @@ -1006,27 +817,26 @@ async def register_nym( ) public_info = await self.get_wallet_public_did() + if not public_info: + raise WalletNotFoundError( + f"Cannot register NYM to ledger: wallet {self.profile.name} " + "has no public DID" + ) + with IndyErrorHandler("Exception building nym request", LedgerError): + request_json = await indy.ledger.build_nym_request( + public_info.did, did, verkey, alias, role + ) + if endorser_did and not write_ledger: + request_json = await indy.ledger.append_request_endorser( + request_json, endorser_did + ) + resp = await self._submit( + request_json, sign=True, sign_did=public_info, write_ledger=write_ledger + ) # let ledger raise on insufficient privilege + if not write_ledger: + return True, {"signed_txn": resp} async with self.profile.session() as session: wallet = session.inject(BaseWallet) - if not public_info: - raise WalletNotFoundError( - f"Cannot register NYM to ledger: wallet {wallet.opened.name} " - "has no public DID" - ) - - with IndyErrorHandler("Exception building nym request", LedgerError): - request_json = await indy.ledger.build_nym_request( - public_info.did, did, verkey, alias, role - ) - if endorser_did and not write_ledger: - request_json = await indy.ledger.append_request_endorser( - request_json, endorser_did - ) - resp = await self._submit( - request_json, sign=True, sign_did=public_info, write_ledger=write_ledger - ) # let ledger raise on insufficient privilege - if not write_ledger: - return True, {"signed_txn": resp} try: did_info = await wallet.get_local_did(did) except WalletNotFoundError: @@ -1034,7 +844,7 @@ async def register_nym( else: metadata = {**did_info.metadata, **DIDPosture.POSTED.metadata} await wallet.replace_local_did_metadata(did, metadata) - return True, None + return True, None async def get_nym_role(self, did: str) -> Role: """ @@ -1093,37 +903,37 @@ async def rotate_public_did_keypair(self, next_seed: str = None) -> None: wallet = session.inject(BaseWallet) verkey = await wallet.rotate_did_keypair_start(public_did, next_seed) - # submit to ledger (retain role and alias) - nym = self.did_to_nym(public_did) - with IndyErrorHandler("Exception building nym request", LedgerError): - request_json = await indy.ledger.build_get_nym_request(public_did, nym) + # submit to ledger (retain role and alias) + nym = self.did_to_nym(public_did) + with IndyErrorHandler("Exception building nym request", LedgerError): + request_json = await indy.ledger.build_get_nym_request(public_did, nym) - response_json = await self._submit(request_json) - data = json.loads((json.loads(response_json))["result"]["data"]) - if not data: - raise BadLedgerRequestError( - f"Ledger has no public DID for wallet {wallet.opened.name}" - ) - seq_no = data["seqNo"] + response_json = await self._submit(request_json) + data = json.loads((json.loads(response_json))["result"]["data"]) + if not data: + raise BadLedgerRequestError( + f"Ledger has no public DID for wallet {self.profile.name}" + ) + seq_no = data["seqNo"] - with IndyErrorHandler("Exception building get-txn request", LedgerError): - txn_req_json = await indy.ledger.build_get_txn_request( - None, None, seq_no - ) + with IndyErrorHandler("Exception building get-txn request", LedgerError): + txn_req_json = await indy.ledger.build_get_txn_request(None, None, seq_no) - txn_resp_json = await self._submit(txn_req_json) - txn_resp = json.loads(txn_resp_json) - txn_resp_data = txn_resp["result"]["data"] - if not txn_resp_data: - raise BadLedgerRequestError( - f"Bad or missing ledger NYM transaction for DID {public_did}" - ) - txn_data_data = txn_resp_data["txn"]["data"] - role_token = Role.get(txn_data_data.get("role")).token() - alias = txn_data_data.get("alias") - await self.register_nym(public_did, verkey, role_token, alias) + txn_resp_json = await self._submit(txn_req_json) + txn_resp = json.loads(txn_resp_json) + txn_resp_data = txn_resp["result"]["data"] + if not txn_resp_data: + raise BadLedgerRequestError( + f"Bad or missing ledger NYM transaction for DID {public_did}" + ) + txn_data_data = txn_resp_data["txn"]["data"] + role_token = Role.get(txn_data_data.get("role")).token() + alias = txn_data_data.get("alias") + await self.register_nym(public_did, verkey, role_token, alias) - # update wallet + # update wallet + async with self.profile.session() as session: + wallet = session.inject(BaseWallet) await wallet.rotate_did_keypair_apply(public_did) async def get_txn_author_agreement(self, reload: bool = False) -> dict: @@ -1162,9 +972,7 @@ async def fetch_txn_author_agreement(self) -> dict: async def get_indy_storage(self) -> IndySdkStorage: """Get an IndySdkStorage instance for the current wallet.""" - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - return IndySdkStorage(wallet.opened) + return IndySdkStorage(self.profile.wallet) def taa_rough_timestamp(self) -> int: """Get a timestamp accurate to the day. @@ -1193,33 +1001,27 @@ async def accept_txn_author_agreement( ) storage = await self.get_indy_storage() await storage.add_record(record) - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) - if self.pool.cache: - cache_key = ( - TAA_ACCEPTED_RECORD_TYPE - + "::" - + wallet.opened.name - + "::" - + self.pool.name - + "::" - ) - await self.pool.cache.set( - cache_key, acceptance, self.pool.cache_duration - ) - - async def get_latest_txn_author_acceptance(self) -> dict: - """Look up the latest TAA acceptance.""" - async with self.profile.session() as session: - wallet = session.inject(BaseWallet) + if self.pool.cache: cache_key = ( TAA_ACCEPTED_RECORD_TYPE + "::" - + wallet.opened.name + + self.profile.name + "::" + self.pool.name + "::" ) + await self.pool.cache.set(cache_key, acceptance, self.pool.cache_duration) + + async def get_latest_txn_author_acceptance(self) -> dict: + """Look up the latest TAA acceptance.""" + cache_key = ( + TAA_ACCEPTED_RECORD_TYPE + + "::" + + self.profile.name + + "::" + + self.pool.name + + "::" + ) acceptance = self.pool.cache and await self.pool.cache.get(cache_key) if not acceptance: storage = await self.get_indy_storage() diff --git a/aries_cloudagent/ledger/indy_vdr.py b/aries_cloudagent/ledger/indy_vdr.py index 3043f16cc0..cbc350d748 100644 --- a/aries_cloudagent/ledger/indy_vdr.py +++ b/aries_cloudagent/ledger/indy_vdr.py @@ -12,13 +12,12 @@ from io import StringIO from pathlib import Path from time import time -from typing import Sequence, Tuple, Union, Optional +from typing import Tuple, Union, Optional from indy_vdr import ledger, open_pool, Pool, Request, VdrError from ..cache.base import BaseCache from ..core.profile import Profile -from ..indy.issuer import IndyIssuer, IndyIssuerError, DEFAULT_CRED_DEF_TAG from ..storage.base import BaseStorage, StorageRecord from ..utils import sentinel from ..utils.env import storage_path @@ -367,124 +366,23 @@ async def _submit( return request_result - async def create_and_send_schema( + async def _create_schema_request( self, - issuer: IndyIssuer, - schema_name: str, - schema_version: str, - attribute_names: Sequence[str], + public_info: DIDInfo, + schema_json: str, write_ledger: bool = True, endorser_did: str = None, - ) -> Tuple[str, dict]: - """ - Send schema to ledger. - - Args: - issuer: The issuer instance creating the schema - schema_name: The schema name - schema_version: The schema version - attribute_names: A list of schema attributes - - """ - - public_info = await self.get_wallet_public_did() - if not public_info: - raise BadLedgerRequestError("Cannot publish schema without a public DID") - - schema_info = await self.check_existing_schema( - public_info.did, schema_name, schema_version, attribute_names - ) - if schema_info: - LOGGER.warning("Schema already exists on ledger. Returning details.") - schema_id, schema_def = schema_info - else: - if self.read_only: - raise LedgerError( - "Error cannot write schema when ledger is in read only mode" - ) - - try: - schema_id, schema_json = await issuer.create_schema( - public_info.did, - schema_name, - schema_version, - attribute_names, - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - schema_def = json.loads(schema_json) - - try: - schema_req = ledger.build_schema_request(public_info.did, schema_json) - except VdrError as err: - raise LedgerError("Exception when building schema request") from err - - if endorser_did and not write_ledger: - schema_req.set_endorser(endorser_did) - - try: - resp = await self._submit( - schema_req, - sign=True, - sign_did=public_info, - write_ledger=write_ledger, - ) - - if not write_ledger: - return schema_id, {"signed_txn": resp} - - try: - # parse sequence number out of response - seq_no = resp["txnMetadata"]["seqNo"] - schema_def["seqNo"] = seq_no - except KeyError as err: - raise LedgerError( - "Failed to parse schema sequence number from ledger response" - ) from err - except LedgerTransactionError as e: - # Identify possible duplicate schema errors on indy-node < 1.9 and > 1.9 - if ( - "can have one and only one SCHEMA with name" in e.message - or "UnauthorizedClientRequest" in e.message - ): - # handle potential race condition if multiple agents are publishing - # the same schema simultaneously - schema_info = await self.check_existing_schema( - public_info.did, schema_name, schema_version, attribute_names - ) - if schema_info: - LOGGER.warning( - "Schema already exists on ledger. Returning details." - " Error: %s", - e, - ) - schema_id, schema_def = schema_info - else: - raise + ): + """Create the ledger request for publishing a schema.""" + try: + schema_req = ledger.build_schema_request(public_info.did, schema_json) + except VdrError as err: + raise LedgerError("Exception when building schema request") from err - return schema_id, schema_def + if endorser_did and not write_ledger: + schema_req.set_endorser(endorser_did) - async def check_existing_schema( - self, - public_did: str, - schema_name: str, - schema_version: str, - attribute_names: Sequence[str], - ) -> Tuple[str, dict]: - """Check if a schema has already been published.""" - fetch_schema_id = f"{public_did}:2:{schema_name}:{schema_version}" - schema = await self.fetch_schema_by_id(fetch_schema_id) - if schema: - fetched_attrs = schema["attrNames"].copy() - fetched_attrs.sort() - cmp_attrs = list(attribute_names) - cmp_attrs.sort() - if fetched_attrs != cmp_attrs: - raise LedgerTransactionError( - "Schema already exists on ledger, but attributes do not match: " - + f"{schema_name}:{schema_version} {fetched_attrs} != {cmp_attrs}" - ) - return fetch_schema_id, schema + return schema_req async def get_schema(self, schema_id: str) -> dict: """ @@ -551,7 +449,7 @@ async def fetch_schema_by_id(self, schema_id: str) -> dict: return schema_data - async def fetch_schema_by_seq_no(self, seq_no: int): + async def fetch_schema_by_seq_no(self, seq_no: int) -> dict: """ Fetch a schema by its sequence number. @@ -585,122 +483,25 @@ async def fetch_schema_by_seq_no(self, seq_no: int): f"Could not get schema from ledger for seq no {seq_no}" ) - async def create_and_send_credential_definition( + async def _create_credential_definition_request( self, - issuer: IndyIssuer, - schema_id: str, - signature_type: str = None, - tag: str = None, - support_revocation: bool = False, + public_info: DIDInfo, + credential_definition_json: str, write_ledger: bool = True, endorser_did: str = None, - ) -> Tuple[str, dict, bool]: - """ - Send credential definition to ledger and store relevant key matter in wallet. - - Args: - issuer: The issuer instance to use for credential definition creation - schema_id: The schema id of the schema to create cred def for - signature_type: The signature type to use on the credential definition - tag: Optional tag to distinguish multiple credential definitions - support_revocation: Optional flag to enable revocation for this cred def - - Returns: - Tuple with cred def id, cred def structure, and whether it's novel - - """ - - public_info = await self.get_wallet_public_did() - if not public_info: - raise BadLedgerRequestError( - "Cannot publish credential definition without a public DID" - ) - - schema = await self.get_schema(schema_id) - if not schema: - raise LedgerError(f"Ledger {self.pool_name} has no schema {schema_id}") - - novel = False - - # check if cred def is on ledger already - for test_tag in [tag] if tag else ["tag", DEFAULT_CRED_DEF_TAG]: - credential_definition_id = issuer.make_credential_definition_id( - public_info.did, schema, signature_type, test_tag - ) - ledger_cred_def = await self.fetch_credential_definition( - credential_definition_id + ): + """Create the ledger request for publishing a credential definition.""" + try: + cred_def_req = ledger.build_cred_def_request( + public_info.did, credential_definition_json ) - if ledger_cred_def: - LOGGER.warning( - "Credential definition %s already exists on ledger %s", - credential_definition_id, - self.pool_name, - ) - - try: - if not await issuer.credential_definition_in_wallet( - credential_definition_id - ): - raise LedgerError( - f"Credential definition {credential_definition_id} is on " - f"ledger {self.pool_name} but not in wallet " - f"{self.profile.name}" - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - - credential_definition_json = json.dumps(ledger_cred_def) - break - else: # no such cred def on ledger - try: - if await issuer.credential_definition_in_wallet( - credential_definition_id - ): - raise LedgerError( - f"Credential definition {credential_definition_id} is in " - f"wallet {self.profile.name} but not on ledger {self.pool_name}" - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - - # Cred def is neither on ledger nor in wallet: create and send it - novel = True - try: - ( - credential_definition_id, - credential_definition_json, - ) = await issuer.create_and_store_credential_definition( - public_info.did, - schema, - signature_type, - tag, - support_revocation, - ) - except IndyIssuerError as err: - raise LedgerError(err.message) from err - - if self.read_only: - raise LedgerError( - "Error cannot write cred def when ledger is in read only mode" - ) - - try: - cred_def_req = ledger.build_cred_def_request( - public_info.did, credential_definition_json - ) - except VdrError as err: - raise LedgerError("Exception when building cred def request") from err - - if endorser_did and not write_ledger: - cred_def_req.set_endorser(endorser_did) + except VdrError as err: + raise LedgerError("Exception when building cred def request") from err - resp = await self._submit( - cred_def_req, True, sign_did=public_info, write_ledger=write_ledger - ) - if not write_ledger: - return (credential_definition_id, {"signed_txn": resp}, novel) + if endorser_did and not write_ledger: + cred_def_req.set_endorser(endorser_did) - return (credential_definition_id, json.loads(credential_definition_json), novel) + return cred_def_req async def get_credential_definition(self, credential_definition_id: str) -> dict: """ @@ -1368,13 +1169,19 @@ async def txn_submit( self, request_json: str, sign: bool, - taa_accept: bool, + taa_accept: bool = None, sign_did: DIDInfo = sentinel, + write_ledger: bool = True, ) -> str: """Write the provided (signed and possibly endorsed) transaction to the ledger.""" resp = await self._submit( - request_json, sign=sign, taa_accept=taa_accept, sign_did=sign_did + request_json, + sign=sign, + taa_accept=taa_accept, + sign_did=sign_did, + write_ledger=write_ledger, ) - # match the format returned by indy sdk - sdk_resp = {"op": "REPLY", "result": resp} - return json.dumps(sdk_resp) + if write_ledger: + # match the format returned by indy sdk + resp = {"op": "REPLY", "result": resp} + return json.dumps(resp) diff --git a/aries_cloudagent/ledger/tests/test_indy.py b/aries_cloudagent/ledger/tests/test_indy.py index effa67cac2..796ea1e887 100644 --- a/aries_cloudagent/ledger/tests/test_indy.py +++ b/aries_cloudagent/ledger/tests/test_indy.py @@ -593,8 +593,9 @@ async def test_send_schema( mock_submit.assert_called_once_with( mock_build_schema_req.return_value, - True, + sign=True, sign_did=mock_wallet_get_public_did.return_value, + taa_accept=None, write_ledger=True, ) diff --git a/aries_cloudagent/ledger/tests/test_indy_vdr.py b/aries_cloudagent/ledger/tests/test_indy_vdr.py index bf8375f1d6..f6ef98962a 100644 --- a/aries_cloudagent/ledger/tests/test_indy_vdr.py +++ b/aries_cloudagent/ledger/tests/test_indy_vdr.py @@ -237,8 +237,9 @@ async def test_send_schema( endorser_did=test_did.did, ) assert schema_id == issuer.create_schema.return_value[0] - assert signed_txn["signed_txn"].get("endorser") == test_did.did - assert signed_txn["signed_txn"].get("signature") + txn = json.loads(signed_txn["signed_txn"]) + assert txn.get("endorser") == test_did.did + assert txn.get("signature") @pytest.mark.asyncio async def test_send_schema_no_public_did(