123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726 |
- ###############################################################################
- #
- # The MIT License (MIT)
- #
- # Copyright (c) typedef int 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
-
-
- 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
-
- 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
-
- 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
-
- def start(self):
- raise NotImplementedError()
-
- def stop(self):
- raise NotImplementedError()
-
- 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)
-
-
- 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
-
-
- 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
-
- 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
-
- 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']
-
- 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.')
-
- 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
-
- 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
-
- 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
-
- 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
|