Source code for autobahn.xbr._seller

###############################################################################
#
# The MIT License (MIT)
#
# Copyright (c) Crossbar.io Technologies GmbH
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
# in the Software without restriction, including without limitation the rights
# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
# copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in
# all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
# THE SOFTWARE.
#
###############################################################################

import asyncio
import binascii
import os
import uuid

from autobahn.wamp.types import RegisterOptions, CallDetails
from autobahn.wamp.exception import ApplicationError, TransportLost
from autobahn.wamp.protocol import ApplicationSession
from ._util import unpack_uint256, pack_uint256
from txaio import time_ns

import cbor2
import eth_keys
import nacl.secret
import nacl.utils
import nacl.public
import txaio

from ..util import hl, hlval
from ._eip712_channel_close import sign_eip712_channel_close, recover_eip712_channel_close


[docs]class KeySeries(object): """ Data encryption key series with automatic (time-based) key rotation and key offering (to the XBR market maker). """ def __init__(self, api_id, price, interval=None, count=None, on_rotate=None): """ :param api_id: ID of the API for which to generate keys. :type api_id: bytes :param price: Price per key in key series. :type price: int :param interval: Interval in seconds after which to auto-rotate key. :type interval: int :param count: Number of encryption operations after which to auto-rotate key. :type count: int :param on_rotate: Optional user callback fired after key was rotated. :type on_rotate: callable """ assert type(api_id) == bytes and len(api_id) == 16 assert type(price) == int and price >= 0 assert interval is None or (type(interval) == int and interval > 0) assert count is None or (type(count) == int and count > 0) assert (interval is None and count is not None) or (interval is not None and count is None) assert on_rotate is None or callable(on_rotate) self._api_id = api_id self._price = price self._interval = interval self._count = count self._count_current = 0 self._on_rotate = on_rotate self._id = None self._key = None self._box = None self._archive = {} @property def key_id(self): """ Get current XBR data encryption key ID (of the keys being rotated in a series). :return: Current key ID in key series (16 bytes). :rtype: bytes """ return self._id
[docs] async def encrypt(self, payload): """ Encrypt data with the current XBR data encryption key. :param payload: Application payload to encrypt. :type payload: object :return: The ciphertext for the encrypted application payload. :rtype: bytes """ data = cbor2.dumps(payload) if self._count is not None: self._count_current += 1 if self._count_current >= self._count: await self._rotate() self._count_current = 0 ciphertext = self._box.encrypt(data) return self._id, 'cbor', ciphertext
[docs] def encrypt_key(self, key_id, buyer_pubkey): """ Encrypt a (previously used) XBR data encryption key with a buyer public key. :param key_id: ID of the data encryption key to encrypt. :type key_id: bytes :param buyer_pubkey: Buyer WAMP public key (Ed25519) to asymmetrically encrypt the data encryption key (selected by ``key_id``) against. :type buyer_pubkey: bytes :return: The ciphertext for the encrypted data encryption key. :rtype: bytes """ assert type(key_id) == bytes and len(key_id) == 16 assert type(buyer_pubkey) == bytes and len(buyer_pubkey) == 32 key, _ = self._archive[key_id] sendkey_box = nacl.public.SealedBox(nacl.public.PublicKey(buyer_pubkey, encoder=nacl.encoding.RawEncoder)) encrypted_key = sendkey_box.encrypt(key, encoder=nacl.encoding.RawEncoder) return encrypted_key
[docs] def start(self): raise NotImplementedError()
[docs] def stop(self): raise NotImplementedError()
[docs] async def _rotate(self): # generate new ID for next key in key series self._id = os.urandom(16) # generate next data encryption key in key series self._key = nacl.utils.random(nacl.secret.SecretBox.KEY_SIZE) # create secretbox from new key self._box = nacl.secret.SecretBox(self._key) # add key to archive self._archive[self._id] = (self._key, self._box) self.log.debug( '{tx_type} key "{key_id}" rotated [api_id="{api_id}"]', tx_type=hl('XBR ROTATE', color='magenta'), key_id=hl(uuid.UUID(bytes=self._id)), api_id=hl(uuid.UUID(bytes=self._api_id))) # maybe fire user callback if self._on_rotate: await self._on_rotate(self)
[docs]class PayingChannel(object): def __init__(self, adr, seq, balance): assert type(adr) == bytes and len(adr) == 16 assert type(seq) == int and seq >= 0 assert type(balance) == int and balance >= 0 self._adr = adr self._seq = seq self._balance = balance
[docs]class SimpleSeller(object): log = None KeySeries = None STATE_NONE = 0 STATE_STARTING = 1 STATE_STARTED = 2 STATE_STOPPING = 3 STATE_STOPPED = 4 def __init__(self, market_maker_adr, seller_key, provider_id=None): """ :param market_maker_adr: Market maker public Ethereum address (20 bytes). :type market_maker_adr: bytes :param seller_key: Seller (delegate) private Ethereum key (32 bytes). :type seller_key: bytes :param provider_id: Optional explicit data provider ID. When not given, the seller delegate public WAMP key (Ed25519 in Hex) is used as the provider ID. This must be a valid WAMP URI part. :type provider_id: string """ assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'market_maker_adr must be bytes[20], but got "{}"'.format(market_maker_adr) assert type(seller_key) == bytes and len(seller_key) == 32, 'seller delegate must be bytes[32], but got "{}"'.format(seller_key) assert provider_id is None or type(provider_id) == str, 'provider_id must be None or string, but got "{}"'.format(provider_id) self.log = txaio.make_logger() # current seller state self._state = SimpleSeller.STATE_NONE # market maker address self._market_maker_adr = market_maker_adr self._xbrmm_config = None # seller raw ethereum private key (32 bytes) self._pkey_raw = seller_key # seller ethereum private key object self._pkey = eth_keys.keys.PrivateKey(seller_key) # seller ethereum private account from raw private key # FIXME # self._acct = Account.privateKeyToAccount(self._pkey) self._acct = None # seller ethereum account canonical address self._addr = self._pkey.public_key.to_canonical_address() # seller ethereum account canonical checksummed address # FIXME # self._caddr = web3.Web3.toChecksumAddress(self._addr) self._caddr = None # seller provider ID self._provider_id = provider_id or str(self._pkey.public_key) self._channels = {} # will be filled with on-chain payment channel contract, once started self._channel = None # channel current (off-chain) balance self._balance = 0 # channel sequence number self._seq = 0 self._keys = {} self._keys_map = {} # after start() is running, these will be set self._session = None self._session_regs = None @property def public_key(self): """ This seller delegate public Ethereum key. :return: Ethereum public key of this seller delegate. :rtype: bytes """ return self._pkey.public_key
[docs] def add(self, api_id, prefix, price, interval=None, count=None, categories=None): """ Add a new (rotating) private encryption key for encrypting data on the given API. :param api_id: API for which to create a new series of rotating encryption keys. :type api_id: bytes :param price: Price in XBR token per key. :type price: int :param interval: Interval (in seconds) after which to auto-rotate the encryption key. :type interval: int :param count: Number of encryption operations after which to auto-rotate the encryption key. :type count: int """ assert type(api_id) == bytes and len(api_id) == 16 and api_id not in self._keys assert type(price) == int and price >= 0 assert interval is None or (type(interval) == int and interval > 0) assert count is None or (type(count) == int and count > 0) assert (interval is None and count is not None) or (interval is not None and count is None) assert categories is None or (type(categories) == dict and (type(k) == str for k in categories.keys()) and (type(v) == str for v in categories.values())), 'invalid categories type (must be dict) or category key or value type (must both be string)' async def on_rotate(key_series): key_id = key_series.key_id self._keys_map[key_id] = key_series # FIXME: expose the knobs hard-coded in below .. # offer the key to the market maker (retry 5x in specific error cases) retries = 5 while retries: try: valid_from = time_ns() - 10 * 10 ** 9 delegate = self._addr # FIXME: sign the supplied offer information using self._pkey signature = os.urandom(65) provider_id = self._provider_id offer = await self._session.call('xbr.marketmaker.place_offer', key_id, api_id, prefix, valid_from, delegate, signature, privkey=None, price=pack_uint256(price) if price is not None else None, categories=categories, expires=None, copies=None, provider_id=provider_id) self.log.debug( '{tx_type} key "{key_id}" offered for {price} [api_id={api_id}, prefix="{prefix}", delegate="{delegate}"]', tx_type=hl('XBR OFFER ', color='magenta'), key_id=hl(uuid.UUID(bytes=key_id)), api_id=hl(uuid.UUID(bytes=api_id)), price=hl(str(int(price / 10 ** 18) if price is not None else 0) + ' XBR', color='magenta'), delegate=hl(binascii.b2a_hex(delegate).decode()), prefix=hl(prefix)) self.log.debug('offer={offer}', offer=offer) break except ApplicationError as e: if e.error == 'wamp.error.no_such_procedure': self.log.warn('xbr.marketmaker.offer: procedure unavailable!') else: self.log.failure() break except TransportLost: self.log.warn('TransportLost while calling xbr.marketmaker.offer!') break except: self.log.failure() retries -= 1 self.log.warn('Failed to place offer for key! Retrying {retries}/5 ..', retries=retries) await asyncio.sleep(1) key_series = self.KeySeries(api_id, price, interval=interval, count=count, on_rotate=on_rotate) self._keys[api_id] = key_series self.log.debug('Created new key series {key_series}', key_series=key_series) return key_series
[docs] async def start(self, session): """ Start rotating keys and placing key offers with the XBR market maker. :param session: WAMP session over which to communicate with the XBR market maker. :type session: :class:`autobahn.wamp.protocol.ApplicationSession` """ assert isinstance(session, ApplicationSession), 'session must be an ApplicationSession, was "{}"'.format(session) assert self._state in [SimpleSeller.STATE_NONE, SimpleSeller.STATE_STOPPED], 'seller already running' self._state = SimpleSeller.STATE_STARTING self._session = session self._session_regs = [] self.log.debug('Start selling from seller delegate address {address} (public key 0x{public_key}..)', address=hl(self._caddr), public_key=binascii.b2a_hex(self._pkey.public_key[:10]).decode()) # get the currently active (if any) paying channel for the delegate self._channel = await session.call('xbr.marketmaker.get_active_paying_channel', self._addr) if not self._channel: raise Exception('no active paying channel found') channel_oid = self._channel['channel_oid'] assert type(channel_oid) == bytes and len(channel_oid) == 16 self._channel_oid = uuid.UUID(bytes=channel_oid) procedure = 'xbr.provider.{}.sell'.format(self._provider_id) reg = await session.register(self.sell, procedure, options=RegisterOptions(details_arg='details')) self._session_regs.append(reg) self.log.debug('Registered procedure "{procedure}"', procedure=hl(reg.procedure)) procedure = 'xbr.provider.{}.close_channel'.format(self._provider_id) reg = await session.register(self.close_channel, procedure, options=RegisterOptions(details_arg='details')) self._session_regs.append(reg) self.log.debug('Registered procedure "{procedure}"', procedure=hl(reg.procedure)) for key_series in self._keys.values(): await key_series.start() self._xbrmm_config = await session.call('xbr.marketmaker.get_config') # get the current (off-chain) balance of the paying channel paying_balance = await session.call('xbr.marketmaker.get_paying_channel_balance', self._channel_oid.bytes) # FIXME if type(paying_balance['remaining']) == bytes: paying_balance['remaining'] = unpack_uint256(paying_balance['remaining']) if not paying_balance['remaining'] > 0: raise Exception('no off-chain balance remaining on paying channel') self._channels[channel_oid] = PayingChannel(channel_oid, paying_balance['seq'], paying_balance['remaining']) self._state = SimpleSeller.STATE_STARTED # FIXME self._balance = paying_balance['remaining'] if type(self._balance) == bytes: self._balance = unpack_uint256(self._balance) self._seq = paying_balance['seq'] self.log.info('Ok, seller delegate started [active paying channel {channel_oid} with remaining balance {remaining} at sequence {seq}]', channel_oid=hl(self._channel_oid), remaining=hlval(self._balance), seq=hlval(self._seq)) return paying_balance['remaining']
[docs] async def stop(self): """ Stop rotating/offering keys to the XBR market maker. """ assert self._state in [SimpleSeller.STATE_STARTED], 'seller not running' self._state = SimpleSeller.STATE_STOPPING dl = [] for key_series in self._keys.values(): d = key_series.stop() dl.append(d) if self._session_regs: if self._session and self._session.is_attached(): # voluntarily unregister interface for reg in self._session_regs: d = reg.unregister() dl.append(d) self._session_regs = None d = txaio.gather(dl) try: await d except: self.log.failure() finally: self._state = SimpleSeller.STATE_STOPPED self._session = None self.log.info('Ok, seller delegate stopped.')
[docs] async def balance(self): """ Return current (off-chain) balance of paying channel: * ``amount``: The initial amount with which the paying channel was opened. * ``remaining``: The remaining amount of XBR in the paying channel that can be earned. * ``inflight``: The amount of XBR allocated to sell transactions that are currently processed. :return: Current paying balance. :rtype: dict """ if self._state not in [SimpleSeller.STATE_STARTED]: raise RuntimeError('seller not running') if not self._session or not self._session.is_attached(): raise RuntimeError('market-maker session not attached') paying_balance = await self._session.call('xbr.marketmaker.get_paying_channel_balance', self._channel['channel_oid']) return paying_balance
[docs] async def wrap(self, api_id, uri, payload): """ Encrypt and wrap application payload for a given API and destined for a specific WAMP URI. :param api_id: API for which to encrypt and wrap the application payload for. :type api_id: bytes :param uri: WAMP URI the application payload is destined for (eg the procedure or topic URI). :type uri: str :param payload: Application payload to encrypt and wrap. :type payload: object :return: The encrypted and wrapped application payload: a tuple with ``(key_id, serializer, ciphertext)``. :rtype: tuple """ assert type(api_id) == bytes and len(api_id) == 16 and api_id in self._keys assert type(uri) == str assert payload is not None keyseries = self._keys[api_id] key_id, serializer, ciphertext = await keyseries.encrypt(payload) return key_id, serializer, ciphertext
[docs] def close_channel(self, market_maker_adr, channel_oid, channel_seq, channel_balance, channel_is_final, marketmaker_signature, details=None): """ Called by a XBR Market Maker to close a paying channel. """ assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'market_maker_adr must be bytes[20], but was {}'.format(type(market_maker_adr)) assert type(channel_oid) == bytes and len(channel_oid) == 16, 'channel_oid must be bytes[16], but was {}'.format(type(channel_oid)) assert type(channel_seq) == int, 'channel_seq must be int, but was {}'.format(type(channel_seq)) assert type(channel_balance) == bytes and len(channel_balance) == 32, 'channel_balance must be bytes[32], but was {}'.format(type(channel_balance)) assert type(channel_is_final) == bool, 'channel_is_final must be bool, but was {}'.format(type(channel_is_final)) assert type(marketmaker_signature) == bytes and len(marketmaker_signature) == (32 + 32 + 1), 'marketmaker_signature must be bytes[65], but was {}'.format(type(marketmaker_signature)) assert details is None or isinstance(details, CallDetails), 'details must be autobahn.wamp.types.CallDetails' # check that the delegate_adr fits what we expect for the market maker if market_maker_adr != self._market_maker_adr: raise ApplicationError('xbr.error.unexpected_delegate_adr', '{}.sell() - unexpected market maker (delegate) address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._market_maker_adr).decode(), binascii.b2a_hex(market_maker_adr).decode())) # FIXME: must be the currently active channel .. and we need to track all of these if channel_oid != self._channel['channel_oid']: self._session.leave() raise ApplicationError('xbr.error.unexpected_channel_oid', '{}.sell() - unexpected paying channel address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._channel['channel_oid']).decode(), binascii.b2a_hex(channel_oid).decode())) # channel sequence number: check we have consensus on off-chain channel state with peer (which is the market maker) if channel_seq != self._seq: raise ApplicationError('xbr.error.unexpected_channel_seq', '{}.sell() - unexpected channel (after tx) sequence number: expected {}, but got {}'.format(self.__class__.__name__, self._seq + 1, channel_seq)) # channel balance: check we have consensus on off-chain channel state with peer (which is the market maker) channel_balance = unpack_uint256(channel_balance) if channel_balance != self._balance: raise ApplicationError('xbr.error.unexpected_channel_balance', '{}.sell() - unexpected channel (after tx) balance: expected {}, but got {}'.format(self.__class__.__name__, self._balance, channel_balance)) # XBRSIG: check the signature (over all input data for the buying of the key) signer_address = recover_eip712_channel_close(channel_oid, channel_seq, channel_balance, channel_is_final, marketmaker_signature) if signer_address != market_maker_adr: self.log.warn('{klass}.sell()::XBRSIG[4/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}', klass=self.__class__.__name__, signer_address=hl(binascii.b2a_hex(signer_address).decode()), delegate_adr=hl(binascii.b2a_hex(market_maker_adr).decode())) raise ApplicationError('xbr.error.invalid_signature', '{}.sell()::XBRSIG[4/8] - EIP712 signature invalid or not signed by market maker'.format(self.__class__.__name__)) # XBRSIG: compute EIP712 typed data signature seller_signature = sign_eip712_channel_close(self._pkey_raw, channel_oid, channel_seq, channel_balance, channel_is_final) receipt = { 'delegate': self._addr, 'seq': channel_seq, 'balance': pack_uint256(channel_balance), 'is_final': channel_is_final, 'signature': seller_signature, } self.log.debug('{klass}.close_channel() - {tx_type} closing channel {channel_oid}, closing balance {channel_balance}, closing sequence {channel_seq} [caller={caller}, caller_authid="{caller_authid}"]', klass=self.__class__.__name__, tx_type=hl('XBR CLOSE ', color='magenta'), channel_balance=hl(str(int(channel_balance / 10 ** 18)) + ' XBR', color='magenta'), channel_seq=hl(channel_seq), channel_oid=hl(binascii.b2a_hex(channel_oid).decode()), caller=hl(details.caller), caller_authid=hl(details.caller_authid)) return receipt
[docs] def sell(self, market_maker_adr, buyer_pubkey, key_id, channel_oid, channel_seq, amount, balance, signature, details=None): """ Called by a XBR Market Maker to buy a data encyption key. The XBR Market Maker here is acting for (triggered by) the XBR buyer delegate. :param market_maker_adr: The market maker Ethereum address. The technical buyer is usually the XBR market maker (== the XBR delegate of the XBR market operator). :type market_maker_adr: bytes of length 20 :param buyer_pubkey: The buyer delegate Ed25519 public key. :type buyer_pubkey: bytes of length 32 :param key_id: The UUID of the data encryption key to buy. :type key_id: bytes of length 16 :param channel_oid: The on-chain channel contract address. :type channel_oid: bytes of length 16 :param channel_seq: Paying channel sequence off-chain transaction number. :type channel_seq: int :param amount: The amount paid by the XBR Buyer via the XBR Market Maker. :type amount: bytes :param balance: Balance remaining in the payment channel (from the market maker to the seller) after successfully buying the key. :type balance: bytes :param signature: Signature over the supplied buying information, using the Ethereum private key of the market maker (which is the delegate of the marker operator). :type signature: bytes of length 65 :param details: Caller details. The call will come from the XBR Market Maker. :type details: :class:`autobahn.wamp.types.CallDetails` :return: The data encryption key, itself encrypted to the public key of the original buyer. :rtype: bytes """ assert type(market_maker_adr) == bytes and len(market_maker_adr) == 20, 'delegate_adr must be bytes[20]' assert type(buyer_pubkey) == bytes and len(buyer_pubkey) == 32, 'buyer_pubkey must be bytes[32]' assert type(key_id) == bytes and len(key_id) == 16, 'key_id must be bytes[16]' assert type(channel_oid) == bytes and len(channel_oid) == 16, 'channel_oid must be bytes[16]' assert type(channel_seq) == int, 'channel_seq must be int' assert type(amount) == bytes and len(amount) == 32, 'amount_paid must be bytes[32], but was {}'.format(type(amount)) assert type(balance) == bytes and len(amount) == 32, 'post_balance must be bytes[32], but was {}'.format(type(balance)) assert type(signature) == bytes and len(signature) == (32 + 32 + 1), 'signature must be bytes[65]' assert details is None or isinstance(details, CallDetails), 'details must be autobahn.wamp.types.CallDetails' amount = unpack_uint256(amount) balance = unpack_uint256(balance) # check that the delegate_adr fits what we expect for the market maker if market_maker_adr != self._market_maker_adr: raise ApplicationError('xbr.error.unexpected_marketmaker_adr', '{}.sell() - unexpected market maker address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._market_maker_adr).decode(), binascii.b2a_hex(market_maker_adr).decode())) # get the key series given the key_id if key_id not in self._keys_map: raise ApplicationError('crossbar.error.no_such_object', '{}.sell() - no key with ID "{}"'.format(self.__class__.__name__, key_id)) key_series = self._keys_map[key_id] # FIXME: must be the currently active channel .. and we need to track all of these if channel_oid != self._channel['channel_oid']: self._session.leave() raise ApplicationError('xbr.error.unexpected_channel_oid', '{}.sell() - unexpected paying channel address: expected 0x{}, but got 0x{}'.format(self.__class__.__name__, binascii.b2a_hex(self._channel['channel_oid']).decode(), binascii.b2a_hex(channel_oid).decode())) # channel sequence number: check we have consensus on off-chain channel state with peer (which is the market maker) if channel_seq != self._seq + 1: raise ApplicationError('xbr.error.unexpected_channel_seq', '{}.sell() - unexpected channel (after tx) sequence number: expected {}, but got {}'.format(self.__class__.__name__, self._seq + 1, channel_seq)) # channel balance: check we have consensus on off-chain channel state with peer (which is the market maker) if balance != self._balance - amount: raise ApplicationError('xbr.error.unexpected_channel_balance', '{}.sell() - unexpected channel (after tx) balance: expected {}, but got {}'.format(self.__class__.__name__, self._balance - amount, balance)) # FIXME current_block_number = 1 verifying_chain_id = self._xbrmm_config['verifying_chain_id'] verifying_contract_adr = binascii.a2b_hex(self._xbrmm_config['verifying_contract_adr'][2:]) market_oid = self._channel['market_oid'] # XBRSIG[4/8]: check the signature (over all input data for the buying of the key) signer_address = recover_eip712_channel_close(verifying_chain_id, verifying_contract_adr, current_block_number, market_oid, channel_oid, channel_seq, balance, False, signature) if signer_address != market_maker_adr: self.log.warn('{klass}.sell()::XBRSIG[4/8] - EIP712 signature invalid: signer_address={signer_address}, delegate_adr={delegate_adr}', klass=self.__class__.__name__, signer_address=hl(binascii.b2a_hex(signer_address).decode()), delegate_adr=hl(binascii.b2a_hex(market_maker_adr).decode())) raise ApplicationError('xbr.error.invalid_signature', '{}.sell()::XBRSIG[4/8] - EIP712 signature invalid or not signed by market maker'.format(self.__class__.__name__)) # now actually update our local knowledge of the channel state # FIXME: what if code down below fails? self._seq += 1 self._balance -= amount # encrypt the data encryption key against the original buyer delegate Ed25519 public key sealed_key = key_series.encrypt_key(key_id, buyer_pubkey) assert type(sealed_key) == bytes and len(sealed_key) == 80, '{}.sell() - unexpected sealed key computed (expected bytes[80]): {}'.format(self.__class__.__name__, sealed_key) # XBRSIG[5/8]: compute EIP712 typed data signature seller_signature = sign_eip712_channel_close(self._pkey_raw, verifying_chain_id, verifying_contract_adr, current_block_number, market_oid, channel_oid, self._seq, self._balance, False) receipt = { # key ID that has been bought 'key_id': key_id, # seller delegate address that sold the key 'delegate': self._addr, # buyer delegate Ed25519 public key with which the bought key was sealed 'buyer_pubkey': buyer_pubkey, # finally return what the consumer (buyer) was actually interested in: # the data encryption key, sealed (public key Ed25519 encrypted) to the # public key of the buyer delegate 'sealed_key': sealed_key, # paying channel off-chain transaction sequence numbers 'channel_seq': self._seq, # amount paid for the key 'amount': amount, # paying channel amount remaining 'balance': self._balance, # seller (delegate) signature 'signature': seller_signature, } self.log.info('{klass}.sell() - {tx_type} key "{key_id}" sold for {amount_earned} - balance is {balance} [caller={caller}, caller_authid="{caller_authid}", buyer_pubkey="{buyer_pubkey}"]', klass=self.__class__.__name__, tx_type=hl('XBR SELL ', color='magenta'), key_id=hl(uuid.UUID(bytes=key_id)), amount_earned=hl(str(int(amount / 10 ** 18)) + ' XBR', color='magenta'), balance=hl(str(int(self._balance / 10 ** 18)) + ' XBR', color='magenta'), # paying_channel=hl(binascii.b2a_hex(paying_channel).decode()), caller=hl(details.caller), caller_authid=hl(details.caller_authid), buyer_pubkey=hl(binascii.b2a_hex(buyer_pubkey).decode())) return receipt