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
23 changes: 21 additions & 2 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 the invitation used to "
ianco marked this conversation as resolved.
Show resolved Hide resolved
"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."
"agent who will be endorsing transactions."
),
)
parser.add_argument(
Expand Down Expand Up @@ -1605,6 +1615,15 @@ def get_settings(self, args: Namespace):
elif args.endorser_protocol_role == ENDORSER_ENDORSER:
settings["endorser.endorser"] = True

if args.endorser_invitation:
ianco marked this conversation as resolved.
Show resolved Hide resolved
if settings["endorser.author"]:
settings["endorser.endorser_invitation"] = args.endorser_invitation
else:
raise ArgsParseError(
"Parameter --endorser-public-did should only be set for transaction "
"Authors"
)

if args.endorser_public_did:
if settings["endorser.author"]:
settings["endorser.endorser_public_did"] = args.endorser_public_did
Expand Down
72 changes: 69 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,18 @@
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 ....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 +713,71 @@ 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
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:
# TBD possibly bail at this point, we can't configure the connection
# for now just continue
pass

try:
# OK, we are an author, we have no endorser connection but we have enough info
# to automatically initiate the connection
conn_mgr = ConnectionManager(profile)
conn_record = await conn_mgr.receive_invitation(
invitation=ConnectionInvitation.from_url(endorser_invitation),
auto_accept=True,
alias=endorser_alias,
)

# 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
29 changes: 18 additions & 11 deletions demo/runners/agent_container.py
Original file line number Diff line number Diff line change
Expand Up @@ -654,6 +654,23 @@ 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
)
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 +694,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
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
61 changes: 53 additions & 8 deletions demo/runners/support/agent.py
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,8 @@ def __init__(
self.postgres = DEFAULT_POSTGRES if postgres is None else postgres
self.tails_server_base_url = tails_server_base_url
self.endorser_role = endorser_role
self.endorser_did = None # set this later
self.endorser_invite = None # set this later
self.extra_args = extra_args
self.trace_enabled = TRACE_ENABLED
self.trace_target = TRACE_TARGET
Expand Down Expand Up @@ -398,6 +400,19 @@ def get_agent_args(self):
("--endorser-alias", "endorser"),
]
)
if self.endorser_did:
result.extend(
[
("--endorser-public-did", self.endorser_did),
]
)
if self.endorser_invite:
result.extend(
(
"--endorser-invitation",
self.endorser_invite,
)
)
elif self.endorser_role == "endorser":
result.extend(
[
Expand Down Expand Up @@ -474,6 +489,7 @@ async def register_or_switch_wallet(
webhook_port: int = None,
mediator_agent=None,
cred_type: str = CRED_FORMAT_INDY,
endorser_agent=None,
):
if webhook_port is not None:
await self.listen_webhooks(webhook_port)
Expand Down Expand Up @@ -553,6 +569,11 @@ async def register_or_switch_wallet(
log_msg("Mediation setup FAILED :-(")
raise Exception("Mediation setup FAILED :-(")

# if endorser, endorse the wallet ledger operations
if endorser_agent:
if not await connect_wallet_to_endorser(self, endorser_agent):
raise Exception("Endorser setup FAILED :-(")

self.log(f"Created NEW wallet {target_wallet_name}")
return True

Expand Down Expand Up @@ -1262,11 +1283,26 @@ def connection_ready(self):
return self._connection_ready.done() and self._connection_ready.result()

async def handle_connections(self, message):
# author responds to a multi-use invitation
if message["state"] == "request":
self.endorser_connection_id = message["connection_id"]
self._connection_ready = asyncio.Future()

# finish off the connection
if message["connection_id"] == self.endorser_connection_id:
if message["state"] == "active" and not self._connection_ready.done():
self.log("Endorser Connected")
self._connection_ready.set_result(True)

# setup endorser meta-data on our connection
log_msg("Setup endorser agent meta-data ...")
await self.admin_POST(
"/transactions/"
+ self.endorser_connection_id
+ "/set-endorser-role",
params={"transaction_my_job": "TRANSACTION_ENDORSER"},
)

async def handle_basicmessages(self, message):
self.log("Received message:", message["content"])

Expand All @@ -1286,6 +1322,22 @@ async def start_endorser_agent(start_port, genesis):
log_msg("Endorser Endpoint URL is at:", endorser_agent.endpoint)
log_msg("Endorser webhooks listening on:", start_port + 2)

# get a reusable invitation to connect to this endorser
log_msg("Generate endorser multi-use invite ...")
endorser_agent.endorser_connection_id = None
endorser_agent.endorser_public_did = None
endorser_connection = await endorser_agent.admin_POST(
"/connections/create-invitation?alias=EndorserMultiuse&auto_accept=true&multi_use=true"
)
ianco marked this conversation as resolved.
Show resolved Hide resolved
endorser_agent.endorser_multi_connection = endorser_connection
endorser_agent.endorser_connection_id = endorser_connection["connection_id"]
endorser_agent.endorser_multi_invitation = endorser_connection["invitation"]
endorser_agent.endorser_multi_invitation_url = endorser_connection["invitation_url"]

endorser_agent_public_did = await endorser_agent.admin_GET("/wallet/did/public")
endorser_did = endorser_agent_public_did["result"]["did"]
endorser_agent.endorser_public_did = endorser_did

return endorser_agent


Expand All @@ -1312,19 +1364,12 @@ async def connect_wallet_to_endorser(agent, endorser_agent):
log_msg("Connected agent to endorser:", agent.ident, endorser_agent.ident)

# setup endorser meta-data on our connection
log_msg("Setup endorser agent meta-data ...")
await endorser_agent.admin_POST(
"/transactions/" + endorser_agent.endorser_connection_id + "/set-endorser-role",
params={"transaction_my_job": "TRANSACTION_ENDORSER"},
)
await asyncio.sleep(1.0)
log_msg("Setup author agent meta-data ...")
await agent.admin_POST(
f"/transactions/{agent.endorser_connection_id }/set-endorser-role",
params={"transaction_my_job": "TRANSACTION_AUTHOR"},
)
endorser_agent_public_did = await endorser_agent.admin_GET("/wallet/did/public")
endorser_did = endorser_agent_public_did["result"]["did"]
endorser_did = endorser_agent.endorser_public_did
await agent.admin_POST(
f"/transactions/{agent.endorser_connection_id}/set-endorser-info",
params={"endorser_did": endorser_did, "endorser_name": "endorser"},
Expand Down