Skip to content

Commit

Permalink
Call grpc verifier service on confirm_payments
Browse files Browse the repository at this point in the history
Payments are now actually verified when runnin the confirm_payments job.
  • Loading branch information
ryanberckmans committed Jul 13, 2024
1 parent b937cf3 commit e9d822f
Show file tree
Hide file tree
Showing 8 changed files with 183 additions and 145 deletions.
36 changes: 30 additions & 6 deletions pretix_eth/management/commands/confirm_payments.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,13 @@
from pretix.base.models import OrderPayment
from pretix.base.models.event import Event

# from pretix_eth.verifier.verify_payment import verify_payment
from threecities.v1 import transfer_verification_pb2

from pretix_eth.verifier.verify_payment import verify_payment

logger = logging.getLogger(__name__)


class Command(BaseCommand):
help = (
"Verify pending orders from on-chain payments. Performs a dry run by default."
Expand Down Expand Up @@ -45,18 +48,19 @@ def confirm_payments_for_event(self, event: Event, no_dry_run, log_verbosity=0):
state__in=(
OrderPayment.PAYMENT_STATE_CREATED,
OrderPayment.PAYMENT_STATE_PENDING,
# NB here we include canceled OrderPayments such that we attempt to detect and confirm payments for canceled OrderPayments in case the user canceled the OrderPayment after making a valid payment
OrderPayment.PAYMENT_STATE_CANCELED,
),
)
if log_verbosity > 0:
logger.info(
f" * Found {unconfirmed_order_payments.count()} "
f"unconfirmed order payments"
f"unconfirmed order payments, including OrderPayments that haven't paid yet, and also including canceled OrderPayments"
)

for order_payment in unconfirmed_order_payments:
try:
if log_verbosity > 0:
if log_verbosity > 0 and order_payment.state != OrderPayment.PAYMENT_STATE_CANCELED and order_payment.signed_messages.all().count() > 0:
logger.info(
f" * trying to confirm payment: {order_payment} "
f"(has {order_payment.signed_messages.all().count()} signed messages)"
Expand All @@ -67,9 +71,29 @@ def confirm_payments_for_event(self, event: Event, no_dry_run, log_verbosity=0):
full_id = order_payment.full_id
payment_verified = False
try:
# TODO real request/response types and check response type for verification success
# verify_payment()
payment_verified = False
transfer_verification_request = transfer_verification_pb2.TransferVerificationRequest(
trusted=transfer_verification_pb2.TransferVerificationRequest.TrustedData(
currency=order_payment.info_data.get('primary_currency'),
logical_asset_amount=str(order_payment.info_data.get('amount')),
# WARNING this list of tickers should be sourced from plugin config, but right now they are hardcoded into the 3cities interface link, so we also must hardcode them here, and the allowlists in both places must match
token_ticker_allowlist=["ETH", "WETH", "DAI"],
usd_per_eth=order_payment.info_data.get('usd_per_eth'),
receiver_address=signed_message.recipient_address, # WARNING recipient_address is only trusted field set from plugin config in SignedMessage. TODO consider converting recipient_address in SignedMessage to be untrusted and set this request value from a trusted receiver_address saved into info_data like all other trusted fields
),
untrusted_to_be_verified=transfer_verification_pb2.TransferVerificationRequest.UntrustedData(
chain_id=signed_message.chain_id,
transaction_hash=signed_message.transaction_hash,
sender_address=signed_message.sender_address,
caip222_style_signature=transfer_verification_pb2.TransferVerificationRequest.SignatureData(
message=signed_message.raw_message,
signature=signed_message.signature,
),
)
)

is_verified = verify_payment(transfer_verification_request)
if is_verified:
payment_verified = True
except Exception as e:
logger.error(f"Error verifying payment for order: {order_payment}")

Expand Down
2 changes: 1 addition & 1 deletion pretix_eth/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ class SignedMessage(models.Model):
# 1. start by building receiver addresses into 3cities base URL --> but verifier/model doesn't parse the receiver address from the base url, instead it uses the plugin's configured recipient_address (btw, verifier can return error if provided payment address is an ENS name to keep it simple and reliable... otherwise what if ENS address changes between payment and verification?) --> parsing receiver address out of base url and/or adding a receiver address url param creates extra work because it would temporarily deprecate the plugin's `recipient_address` here since it's no longer explicitly set. Better to minimize changes by keeping recipient_address as-is... the 3cities base url is then required to coincidentally match this recipient_address until the SDK is implemented in April
# 2. add chain-specific receiver addresses to CheckoutSettings and serializer
# 3. then, make a real SDK and deprecate `3cities base URL` in the plugin config such that receiver addresses are manually specified in plugin config and then clientside at runtime, added to a dynamically generated 3cities link using the SDK. --> NB just because we have a mature SDK doesn't mean that token/chain config needs to be specified in the payment plugin... I like the idea of the merchant trusting 3cities to pick appropriate chains/tokens in some cases.
recipient_address = models.CharField(max_length=42) # the receiving wallet address provided by pretix in the plugin config. recipient_address is passed securely from pretix to the 3cities frontend to collect the payment and then to the 3cities verifier for secure verification when confirming the payment. recipient_address is snapshotted for each order at the time the order is placed so that if the global recipient_address is updated in the plugin config, any older orders won't lose track of the old recipient address to which the customer may have already sent a payment
recipient_address = models.CharField(max_length=42) # the expected receiving wallet address for this transaction. WARNING must be set from pretix plugin config and then is trusted and secure. recipient_address is passed securely from pretix to the 3cities frontend to collect the payment and then to the 3cities verifier for secure verification when confirming the payment. recipient_address is snapshotted for each order at the time the order is placed so that if the global recipient_address is updated in the plugin config, any older orders won't lose track of the old recipient address to which the customer may have already sent a payment
transaction_hash = models.CharField(max_length=66, null=True, unique=True) # the transaction hash in which the customer sent payment for this order. transaction_hash is passed insecurely from the 3cities frontend and forwarded to the 3cities verifier for secure verification when confirming the payment. WARNING must be unique to prevent an attacker from "doulble spending" a transaction hash by claiming it as payment for multiple orders
chain_id = models.IntegerField() # the chain id on which transaction_hash exists (ie. chain_id is set if and only if transaction_hash has been set). Insecure and untrusted. Provided by user's browser for admin convenience.
receipt_url = models.TextField(null=True, default=None) # block explorer receipt URL for this transaction. Insecure and untrusted. Provided by user's browser for admin convenience.
Expand Down

Large diffs are not rendered by default.

90 changes: 49 additions & 41 deletions pretix_eth/verifier/verify_payment.py
Original file line number Diff line number Diff line change
@@ -1,55 +1,63 @@
import logging
import subprocess
import os
import sys
import grpc

from threecities.v1.verifier_pb2 import SayRequest
from threecities.v1.verifier_pb2_grpc import VerifierServiceStub

# get_grpc_ca_root_cert_file_path returns the file path of the
# additional CA certificate required to verify our self-signed
# certificate to run grpc with SSL. grpc requires SSL, even over
# localhost. Our strategy to satisfy this requirement is to follow the
# instructions here
# https://connectrpc.com/docs/node/getting-started/#use-the-grpc-protocol-instead-of-the-connect-protocol,
# which are roughly 1. one-time setup to create a self-signed
# certificate via `mkcert` and install it in the local CA root, 2. tell
# the python grpc client where to find the additional CA root
# certificate so that our self-signed certificate is verifiable. There
# are three ways to tell python where to find the addition CA root
# certificate: (i) run `mkcert -CAROOT` in a subshell, (ii) provide the
# env THREECITIES_GRPC_CA_ROOT_CERT="$(mkcert -CAROOT)/rootCA.pem", or
# (iii) set GRPC_DEFAULT_SSL_ROOTS_FILE_PATH="$(mkcert
# -CAROOT)/rootCA.pem"
# https://grpc.github.io/grpc/cpp/grpc__security__constants_8h.html#a48565da473b7c82fa2453798f620fd59
# --> we implement (i) and (ii) here.
from threecities.v1.transfer_verification_pb2_grpc import TransferVerificationServiceStub

logger = logging.getLogger(__name__)


def get_grpc_ca_root_cert_file_path():
# Try to execute the subshell command and get the path
try:
# Run the mkcert command and capture the output
completed_process = subprocess.run(
["mkcert", "-CAROOT"], capture_output=True, check=True, text=True)
ca_root_path = completed_process.stdout.strip()
grpc_ca_root_cert_file_path = os.path.join(ca_root_path, "rootCA.pem")
except subprocess.CalledProcessError:
# If the subshell command fails, fall back to the environment variable
grpc_ca_root_cert_file_path = os.getenv("THREECITIES_GRPC_CA_ROOT_CERT")
return os.path.join(ca_root_path, "rootCA.pem")
except subprocess.CalledProcessError as e:
logger.error(f"error obtaining CA root path: {e}")
return os.getenv("THREECITIES_GRPC_CA_ROOT_CERT") or None


# If neither method provides a path, exit with a fatal error
if not grpc_ca_root_cert_file_path:
sys.exit("Fatal Error: Could not determine gRPC CA root cert file path.")
grpc_stub = None

return grpc_ca_root_cert_file_path

grpc_ca_root_cert_file_path = get_grpc_ca_root_cert_file_path()
def ensure_grpc_initialized():
global grpc_stub
if grpc_stub is not None:
return

ca_cert_path = get_grpc_ca_root_cert_file_path()
if not ca_cert_path:
logger.error("grpc init failed: CA root cert file path not found")
return

try:
with open(ca_cert_path, 'rb') as f:
root_cert = f.read()
channel_credentials = grpc.ssl_channel_credentials(root_cert)
channel = grpc.secure_channel("127.0.0.1:8443", channel_credentials)
grpc_stub = TransferVerificationServiceStub(channel)
except Exception as e:
logger.error(f"failed to initialize grpc channel or stub: {e}")


with open(grpc_ca_root_cert_file_path, 'rb') as f:
root_cert = f.read()
channel_credentials = grpc.ssl_channel_credentials(root_cert)
channel = grpc.secure_channel("127.0.0.1:8443", channel_credentials) # TODO configurable grpc endpoint
stub = VerifierServiceStub(channel)
# verify_payment synchronously calls the remote 3cities grpc service to
# attempt to verify the passed
# threecities.v1.transfer_verification_pb2.TransferVerificationRequest.
# Returns True if and only if the payment was successfully verified.
def verify_payment(req):
is_verified = False
ensure_grpc_initialized()
if not grpc_stub:
logger.error("grpc stub unavailable, payment verification cannot proceed")
else:
try:
resp = grpc_stub.TransferVerification(req)
logger.info(f"{resp.description} {resp.error}")
if resp.is_verified:
is_verified = True
except grpc.RpcError as e:
logger.error(f"grpc call failed: {e}")

def verify_payment(): # TODO real request type and implementation
say_response = stub.Say(SayRequest(sentence="Hello there!"))
print("grpc server stub response: " + say_response.sentence)
return True # TODO real response type
return is_verified
36 changes: 36 additions & 0 deletions threecities/v1/transfer_verification_pb2.py

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

66 changes: 66 additions & 0 deletions threecities/v1/transfer_verification_pb2_grpc.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT!
"""Client and server classes corresponding to protobuf-defined services."""
import grpc

from threecities.v1 import transfer_verification_pb2 as threecities_dot_v1_dot_transfer__verification__pb2


class TransferVerificationServiceStub(object):
"""Missing associated documentation comment in .proto file."""

def __init__(self, channel):
"""Constructor.
Args:
channel: A grpc.Channel.
"""
self.TransferVerification = channel.unary_unary(
'/threecities.v1.TransferVerificationService/TransferVerification',
request_serializer=threecities_dot_v1_dot_transfer__verification__pb2.TransferVerificationRequest.SerializeToString,
response_deserializer=threecities_dot_v1_dot_transfer__verification__pb2.TransferVerificationResponse.FromString,
)


class TransferVerificationServiceServicer(object):
"""Missing associated documentation comment in .proto file."""

def TransferVerification(self, request, context):
"""Missing associated documentation comment in .proto file."""
context.set_code(grpc.StatusCode.UNIMPLEMENTED)
context.set_details('Method not implemented!')
raise NotImplementedError('Method not implemented!')


def add_TransferVerificationServiceServicer_to_server(servicer, server):
rpc_method_handlers = {
'TransferVerification': grpc.unary_unary_rpc_method_handler(
servicer.TransferVerification,
request_deserializer=threecities_dot_v1_dot_transfer__verification__pb2.TransferVerificationRequest.FromString,
response_serializer=threecities_dot_v1_dot_transfer__verification__pb2.TransferVerificationResponse.SerializeToString,
),
}
generic_handler = grpc.method_handlers_generic_handler(
'threecities.v1.TransferVerificationService', rpc_method_handlers)
server.add_generic_rpc_handlers((generic_handler,))


# This class is part of an EXPERIMENTAL API.
class TransferVerificationService(object):
"""Missing associated documentation comment in .proto file."""

@staticmethod
def TransferVerification(request,
target,
options=(),
channel_credentials=None,
call_credentials=None,
insecure=False,
compression=None,
wait_for_ready=None,
timeout=None,
metadata=None):
return grpc.experimental.unary_unary(request, target, '/threecities.v1.TransferVerificationService/TransferVerification',
threecities_dot_v1_dot_transfer__verification__pb2.TransferVerificationRequest.SerializeToString,
threecities_dot_v1_dot_transfer__verification__pb2.TransferVerificationResponse.FromString,
options, channel_credentials,
insecure, call_credentials, compression, wait_for_ready, timeout, metadata)
Loading

0 comments on commit e9d822f

Please sign in to comment.