Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Auto connect from author to endoposer on startup #1461

Merged
merged 11 commits into from
Nov 8, 2021
37 changes: 33 additions & 4 deletions aries_cloudagent/config/argparse.py
Original file line number Diff line number Diff line change
Expand Up @@ -1535,15 +1535,25 @@ def add_arguments(self, parser: ArgumentParser):
"directly."
),
)
parser.add_argument(
"--endorser-invitation",
type=str,
metavar="<endorser-invitation>",
env_var="ACAPY_ENDORSER_INVITATION",
help=(
"For transaction Authors, specify the invitation used to "
"connect to the Endorser agent who will be endorsing transactions. "
"Note this is a multi-use invitation created by the Endorser agent."
),
)
parser.add_argument(
"--endorser-public-did",
type=str,
metavar="<endorser-public-did>",
env_var="ACAPY_ENDORSER_PUBLIC_DID",
help=(
"For transaction Authors, specify the the public DID of the Endorser "
"agent who will be endorsing transactions. Note this requires that "
"the connection be made using the Endorser's public DID."
"For transaction Authors, specify the public DID of the Endorser "
"agent who will be endorsing transactions."
),
)
parser.add_argument(
Expand All @@ -1552,7 +1562,7 @@ def add_arguments(self, parser: ArgumentParser):
metavar="<endorser-alias>",
env_var="ACAPY_ENDORSER_ALIAS",
help=(
"For transaction Authors, specify the the alias of the Endorser "
"For transaction Authors, specify the alias of the Endorser "
"connection that will be used to endorse transactions."
),
)
Expand Down Expand Up @@ -1623,6 +1633,25 @@ def get_settings(self, args: Namespace):
"Authors"
)

if args.endorser_invitation:
if settings["endorser.author"]:
if not settings.get("endorser.endorser_public_did"):
raise ArgsParseError(
"Parameter --endorser-public-did must be provided if "
"--endorser-invitation is set."
)
if not settings.get("endorser.endorser_alias"):
raise ArgsParseError(
"Parameter --endorser-alias must be provided if "
"--endorser-invitation is set."
)
settings["endorser.endorser_invitation"] = args.endorser_invitation
else:
raise ArgsParseError(
"Parameter --endorser-invitation should only be set for transaction "
"Authors"
)

if args.auto_request_endorsement:
if settings["endorser.author"]:
settings["endorser.auto_request"] = True
Expand Down
91 changes: 88 additions & 3 deletions aries_cloudagent/protocols/endorse_transaction/v1_0/routes.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
"""Endorse Transaction handling admin routes."""

import json
import logging

from aiohttp import web
from aiohttp_apispec import (
Expand All @@ -22,11 +23,20 @@
from ....messaging.models.base import BaseModelError
from ....messaging.models.openapi import OpenAPISchema
from ....messaging.valid import UUIDFour
from ....protocols.connections.v1_0.manager import ConnectionManager
from ....protocols.connections.v1_0.messages.connection_invitation import (
ConnectionInvitation,
)
from ....protocols.out_of_band.v1_0.manager import OutOfBandManager
from ....protocols.out_of_band.v1_0.messages.invitation import InvitationMessage
from ....storage.error import StorageError, StorageNotFoundError

from .manager import TransactionManager, TransactionManagerError
from .models.transaction_record import TransactionRecord, TransactionRecordSchema
from .transaction_jobs import TransactionJob
from .util import is_author_role, get_endorser_connection_id

LOGGER = logging.getLogger(__name__)


class TransactionListSchema(OpenAPISchema):
Expand Down Expand Up @@ -705,13 +715,88 @@ def register_events(event_bus: EventBus):

async def on_startup_event(profile: Profile, event: Event):
"""Handle any events we need to support."""
print(">>> TODO Received STARTUP event")
pass

# auto setup is only for authors
if not is_author_role(profile):
return

# see if we have an invitation to connect to the endorser
endorser_invitation = profile.settings.get_value("endorser.endorser_invitation")
if not endorser_invitation:
# no invitation, we can't connect automatically
return

# see if we need to initiate an endorser connection
endorser_alias = profile.settings.get_value("endorser.endorser_alias")
if not endorser_alias:
# no alias is specified for the endorser connection
# note that alias is required if invitation is specified
return

connection_id = await get_endorser_connection_id(profile)
if connection_id:
# there is already a connection
return

endorser_did = profile.settings.get_value("endorser.endorser_public_did")
if not endorser_did:
# no DID, we can connect but we can't properly setup the connection metadata
# note that DID is required if invitation is specified
return

try:
# OK, we are an author, we have no endorser connection but we have enough info
# to automatically initiate the connection
invite = InvitationMessage.from_url(endorser_invitation)
if invite:
oob_mgr = OutOfBandManager(profile)
conn_record = await oob_mgr.receive_invitation(
invitation=invite,
auto_accept=True,
alias=endorser_alias,
)
else:
invite = ConnectionInvitation.from_url(endorser_invitation)
if invite:
conn_mgr = ConnectionManager(profile)
conn_record = await conn_mgr.receive_invitation(
invitation=invite,
auto_accept=True,
alias=endorser_alias,
)
else:
raise Exception(
"Failed to establish endorser connection, invalid "
"invitation format."
)

# configure the connection role and info (don't need to wait for the connection)
transaction_mgr = TransactionManager(profile)
await transaction_mgr.set_transaction_my_job(
record=conn_record,
transaction_my_job=TransactionJob.TRANSACTION_AUTHOR.name,
)

async with profile.session() as session:
value = await conn_record.metadata_get(session, "endorser_info")
if value:
value["endorser_did"] = endorser_did
value["endorser_name"] = endorser_alias
else:
value = {"endorser_did": endorser_did, "endorser_name": endorser_alias}
await conn_record.metadata_set(session, key="endorser_info", value=value)

except Exception as e:
# log the error, but continue
LOGGER.exception(
"Error accepting endorser invitation/configuring endorser connection: %s",
str(e),
)


async def on_shutdown_event(profile: Profile, event: Event):
"""Handle any events we need to support."""
print(">>> TODO Received SHUTDOWN event")
# nothing to do for now ...
pass


Expand Down
34 changes: 22 additions & 12 deletions demo/runners/agent_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -596,6 +596,7 @@ def __init__(
self.multitenant = multitenant
self.mediation = mediation
self.use_did_exchange = use_did_exchange
print("Setting use_did_exchange:", self.use_did_exchange)
self.wallet_type = wallet_type
self.public_did = public_did
self.seed = seed
Expand Down Expand Up @@ -654,6 +655,25 @@ async def initialize(
await self.agent.register_did(cred_type=CRED_FORMAT_INDY)
log_msg("Created public DID")

# if we are endorsing, create the endorser agent first, then we can use the
# multi-use invitation to auto-connect the agent on startup
if create_endorser_agent:
self.endorser_agent = await start_endorser_agent(
self.start_port + 7,
self.genesis_txns,
use_did_exchange=self.use_did_exchange,
)
if not self.endorser_agent:
raise Exception("Endorser agent returns None :-(")

# set the endorser invite so the agent can auto-connect
self.agent.endorser_invite = (
self.endorser_agent.endorser_multi_invitation_url
)
self.agent.endorser_did = self.endorser_agent.endorser_public_did
else:
self.endorser_agent = None

with log_timer("Startup duration:"):
await self.agent.start_process()

Expand All @@ -677,23 +697,13 @@ async def initialize(
public_did=self.public_did,
webhook_port=None,
mediator_agent=self.mediator_agent,
endorser_agent=self.endorser_agent,
)
elif self.mediation:
# we need to pre-connect the agent to its mediator
if not await connect_wallet_to_mediator(self.agent, self.mediator_agent):
raise Exception("Mediation setup FAILED :-(")

if create_endorser_agent:
self.endorser_agent = await start_endorser_agent(
self.start_port + 7, self.genesis_txns
)
if not self.endorser_agent:
raise Exception("Endorser agent returns None :-(")
if not await connect_wallet_to_endorser(self.agent, self.endorser_agent):
raise Exception("Endorser setup FAILED :-(")
else:
self.endorser_agent = None

if self.public_did and self.cred_type == CRED_FORMAT_JSON_LD:
# create did of appropriate type
data = {"method": DID_METHOD_KEY, "options": {"key_type": KEY_TYPE_BLS}}
Expand Down Expand Up @@ -1150,7 +1160,7 @@ async def create_agent_with_args(args, ident: str = None):
multitenant=args.multitenant,
mediation=args.mediation,
cred_type=cred_type,
use_did_exchange=args.did_exchange if ("did_exchange" in args) else (aip == 20),
use_did_exchange=(aip == 20) if ("aip" in args) else args.did_exchange,
wallet_type=arg_file_dict.get("wallet-type") or args.wallet_type,
public_did=public_did,
seed="random" if public_did else None,
Expand Down
2 changes: 2 additions & 0 deletions demo/runners/faber.py
Original file line number Diff line number Diff line change
Expand Up @@ -468,12 +468,14 @@ async def main(args):
webhook_port=faber_agent.agent.get_new_webhook_port(),
public_did=True,
mediator_agent=faber_agent.mediator_agent,
endorser_agent=faber_agent.endorser_agent,
)
else:
created = await faber_agent.agent.register_or_switch_wallet(
target_wallet_name,
public_did=True,
mediator_agent=faber_agent.mediator_agent,
endorser_agent=faber_agent.endorser_agent,
cred_type=faber_agent.cred_type,
)
# create a schema and cred def for the new wallet
Expand Down