Skip to content

Commit

Permalink
Merge branch 'main' into feature/manual-keylist-update
Browse files Browse the repository at this point in the history
  • Loading branch information
dbluhm authored Jul 21, 2022
2 parents da8f509 + cb584c0 commit 9b870be
Show file tree
Hide file tree
Showing 7 changed files with 410 additions and 554 deletions.
5 changes: 4 additions & 1 deletion actions/run-integration-tests/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
2 changes: 1 addition & 1 deletion aries_cloudagent/indy/credx/issuer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
248 changes: 245 additions & 3 deletions aries_cloudagent/ledger/base.py
Original file line number Diff line number Diff line change
@@ -1,18 +1,24 @@
"""Ledger base class."""

import json
import logging
import re

from abc import ABC, abstractmethod, ABCMeta
from enum import Enum
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."""
Expand Down Expand Up @@ -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."""
Expand Down Expand Up @@ -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,
Expand All @@ -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."""
Expand All @@ -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,
Expand All @@ -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:
Expand Down
Loading

0 comments on commit 9b870be

Please sign in to comment.