472 lines
15 KiB
Python
472 lines
15 KiB
Python
![]() |
###############################################################################
|
||
|
#
|
||
|
# 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 os
|
||
|
from binascii import b2a_hex
|
||
|
from typing import Optional, Dict, Any, List
|
||
|
|
||
|
import web3
|
||
|
from web3.contract import Contract
|
||
|
from ens import ENS
|
||
|
|
||
|
from twisted.internet.defer import Deferred, inlineCallbacks
|
||
|
from twisted.internet.threads import deferToThread
|
||
|
|
||
|
from autobahn.wamp.interfaces import ICryptosignKey, IEthereumKey
|
||
|
from autobahn.wamp.message import identify_realm_name_category
|
||
|
from autobahn.xbr import make_w3, EIP712AuthorityCertificate
|
||
|
|
||
|
|
||
|
class Seeder(object):
|
||
|
"""
|
||
|
|
||
|
"""
|
||
|
__slots__ = (
|
||
|
'_frealm',
|
||
|
'_operator',
|
||
|
'_label',
|
||
|
'_country',
|
||
|
'_legal',
|
||
|
'_endpoint',
|
||
|
'_bandwidth_requested',
|
||
|
'_bandwidth_offered',
|
||
|
)
|
||
|
|
||
|
def __init__(self,
|
||
|
frealm: 'FederatedRealm',
|
||
|
operator: Optional[str] = None,
|
||
|
label: Optional[str] = None,
|
||
|
country: Optional[str] = None,
|
||
|
legal: Optional[str] = None,
|
||
|
endpoint: Optional[str] = None,
|
||
|
bandwidth_requested: Optional[int] = None,
|
||
|
bandwidth_offered: Optional[int] = None,
|
||
|
):
|
||
|
"""
|
||
|
|
||
|
:param frealm:
|
||
|
:param operator:
|
||
|
:param label:
|
||
|
:param country:
|
||
|
:param legal:
|
||
|
:param endpoint:
|
||
|
:param bandwidth_requested:
|
||
|
:param bandwidth_offered:
|
||
|
"""
|
||
|
self._frealm: FederatedRealm = frealm
|
||
|
self._operator: Optional[str] = operator
|
||
|
self._label: Optional[str] = label
|
||
|
self._country: Optional[str] = country
|
||
|
self._legal: Optional[str] = legal
|
||
|
self._endpoint: Optional[str] = endpoint
|
||
|
self._bandwidth_requested: Optional[str] = bandwidth_requested
|
||
|
self._bandwidth_offered: Optional[str] = bandwidth_offered
|
||
|
|
||
|
@staticmethod
|
||
|
def _create_eip712_connect(chain_id: int,
|
||
|
verifying_contract: bytes,
|
||
|
channel_binding: str,
|
||
|
channel_id: bytes,
|
||
|
block_no: int,
|
||
|
challenge: bytes,
|
||
|
pubkey: bytes,
|
||
|
realm: bytes,
|
||
|
delegate: bytes,
|
||
|
seeder: bytes,
|
||
|
bandwidth: int):
|
||
|
channel_binding = channel_binding or ''
|
||
|
channel_id = channel_id or b''
|
||
|
assert chain_id
|
||
|
assert verifying_contract
|
||
|
assert channel_binding is not None
|
||
|
assert channel_id is not None
|
||
|
assert block_no
|
||
|
assert challenge
|
||
|
assert pubkey
|
||
|
assert realm
|
||
|
assert delegate
|
||
|
assert bandwidth
|
||
|
|
||
|
data = {
|
||
|
'types': {
|
||
|
'EIP712Domain': [
|
||
|
{
|
||
|
'name': 'name',
|
||
|
'type': 'string'
|
||
|
},
|
||
|
{
|
||
|
'name': 'version',
|
||
|
'type': 'string'
|
||
|
},
|
||
|
],
|
||
|
'EIP712SeederConnect': [
|
||
|
{
|
||
|
'name': 'chainId',
|
||
|
'type': 'uint256'
|
||
|
},
|
||
|
{
|
||
|
'name': 'verifyingContract',
|
||
|
'type': 'address'
|
||
|
},
|
||
|
{
|
||
|
'name': 'channel_binding',
|
||
|
'type': 'string'
|
||
|
},
|
||
|
{
|
||
|
'name': 'channel_id',
|
||
|
'type': 'bytes32'
|
||
|
},
|
||
|
{
|
||
|
'name': 'block_no',
|
||
|
'type': 'uint256'
|
||
|
},
|
||
|
{
|
||
|
'name': 'challenge',
|
||
|
'type': 'bytes32'
|
||
|
},
|
||
|
{
|
||
|
'name': 'pubkey',
|
||
|
'type': 'bytes32'
|
||
|
},
|
||
|
{
|
||
|
'name': 'realm',
|
||
|
'type': 'address'
|
||
|
},
|
||
|
{
|
||
|
'name': 'delegate',
|
||
|
'type': 'address'
|
||
|
},
|
||
|
{
|
||
|
'name': 'seeder',
|
||
|
'type': 'address'
|
||
|
},
|
||
|
{
|
||
|
'name': 'bandwidth',
|
||
|
'type': 'uint32'
|
||
|
},
|
||
|
]
|
||
|
},
|
||
|
'primaryType': 'EIP712SeederConnect',
|
||
|
'domain': {
|
||
|
'name': 'XBR',
|
||
|
'version': '1',
|
||
|
},
|
||
|
'message': {
|
||
|
'chainId': chain_id,
|
||
|
'verifyingContract': verifying_contract,
|
||
|
'channel_binding': channel_binding,
|
||
|
'channel_id': channel_id,
|
||
|
'block_no': block_no,
|
||
|
'challenge': challenge,
|
||
|
'pubkey': pubkey,
|
||
|
'realm': realm,
|
||
|
'delegate': delegate,
|
||
|
'seeder': seeder,
|
||
|
'bandwidth': bandwidth,
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return data
|
||
|
|
||
|
@inlineCallbacks
|
||
|
def create_authextra(self,
|
||
|
client_key: ICryptosignKey,
|
||
|
delegate_key: IEthereumKey,
|
||
|
bandwidth_requested: int,
|
||
|
channel_id: Optional[bytes] = None,
|
||
|
channel_binding: Optional[str] = None) -> Deferred:
|
||
|
"""
|
||
|
|
||
|
:param client_key:
|
||
|
:param delegate_key:
|
||
|
:param bandwidth_requested:
|
||
|
:param channel_id:
|
||
|
:param channel_binding:
|
||
|
:return:
|
||
|
"""
|
||
|
chain_id = 1
|
||
|
# FIXME
|
||
|
verifying_contract = b'\x01' * 20
|
||
|
block_no = 1
|
||
|
challenge = os.urandom(32)
|
||
|
eip712_data = Seeder._create_eip712_connect(chain_id=chain_id,
|
||
|
verifying_contract=verifying_contract,
|
||
|
channel_binding=channel_binding,
|
||
|
channel_id=channel_id,
|
||
|
block_no=block_no,
|
||
|
challenge=challenge,
|
||
|
pubkey=client_key.public_key(binary=True),
|
||
|
# FIXME
|
||
|
# realm=self._frealm.address(binary=True),
|
||
|
realm=b'\x02' * 20,
|
||
|
delegate=delegate_key.address(binary=False),
|
||
|
# FIXME
|
||
|
# seeder=self._operator,
|
||
|
seeder=b'\x03' * 20,
|
||
|
bandwidth=bandwidth_requested)
|
||
|
signature = yield delegate_key.sign_typed_data(eip712_data)
|
||
|
authextra = {
|
||
|
# string
|
||
|
'pubkey': client_key.public_key(binary=False),
|
||
|
|
||
|
# string
|
||
|
'challenge': challenge,
|
||
|
|
||
|
# string
|
||
|
'channel_binding': channel_binding,
|
||
|
|
||
|
# string
|
||
|
'channel_id': channel_id,
|
||
|
|
||
|
# address
|
||
|
# FIXME
|
||
|
'realm': '7f' * 20,
|
||
|
|
||
|
# int
|
||
|
'chain_id': chain_id,
|
||
|
|
||
|
# int
|
||
|
'block_no': block_no,
|
||
|
|
||
|
# address
|
||
|
'delegate': delegate_key.address(binary=False),
|
||
|
|
||
|
# address
|
||
|
'seeder': self._operator,
|
||
|
|
||
|
# int: requested bandwidth in kbps
|
||
|
'bandwidth': bandwidth_requested,
|
||
|
|
||
|
# string: Eth signature by delegate_key over EIP712 typed data as above
|
||
|
'signature': b2a_hex(signature).decode(),
|
||
|
}
|
||
|
return authextra
|
||
|
|
||
|
@property
|
||
|
def frealm(self) -> 'FederatedRealm':
|
||
|
"""
|
||
|
|
||
|
:return:
|
||
|
"""
|
||
|
return self._frealm
|
||
|
|
||
|
@property
|
||
|
def operator(self) -> Optional[str]:
|
||
|
"""
|
||
|
Operator address, e.g. ``"0xe59C7418403CF1D973485B36660728a5f4A8fF9c"``.
|
||
|
|
||
|
:return: The Ethereum address of the endpoint operator.
|
||
|
"""
|
||
|
return self._operator
|
||
|
|
||
|
@property
|
||
|
def label(self) -> Optional[str]:
|
||
|
"""
|
||
|
Operator endpoint label.
|
||
|
|
||
|
:return: A human readable label for the operator or specific operator endpoint.
|
||
|
"""
|
||
|
return self._label
|
||
|
|
||
|
@property
|
||
|
def country(self) -> Optional[str]:
|
||
|
"""
|
||
|
Operator country (ISO 3166-1 alpha-2), e.g. ``"US"``.
|
||
|
|
||
|
:return: ISO 3166-1 alpha-2 country code.
|
||
|
"""
|
||
|
return self._country
|
||
|
|
||
|
@property
|
||
|
def legal(self) -> Optional[str]:
|
||
|
"""
|
||
|
|
||
|
:return:
|
||
|
"""
|
||
|
return self._legal
|
||
|
|
||
|
@property
|
||
|
def endpoint(self) -> Optional[str]:
|
||
|
"""
|
||
|
Public WAMP endpoint of seeder. Secure WebSocket URL resolving to a public IPv4
|
||
|
or IPv6 listening url accepting incoming WAMP-WebSocket connections,
|
||
|
e.g. ``wss://service1.example.com/ws``.
|
||
|
|
||
|
:return: The endpoint URL.
|
||
|
"""
|
||
|
return self._endpoint
|
||
|
|
||
|
@property
|
||
|
def bandwidth_requested(self) -> Optional[int]:
|
||
|
"""
|
||
|
|
||
|
:return:
|
||
|
"""
|
||
|
return self._bandwidth_requested
|
||
|
|
||
|
@property
|
||
|
def bandwidth_offered(self) -> Optional[int]:
|
||
|
"""
|
||
|
|
||
|
:return:
|
||
|
"""
|
||
|
return self._bandwidth_offered
|
||
|
|
||
|
|
||
|
class FederatedRealm(object):
|
||
|
"""
|
||
|
A federated realm is a WAMP application realm with a trust anchor rooted in Ethereum, and
|
||
|
which can be shared between multiple parties.
|
||
|
|
||
|
A federated realm is globally identified on an Ethereum chain (e.g. on Mainnet or Rinkeby)
|
||
|
by an Ethereum address associated to a federated realm owner by an on-chain record stored
|
||
|
in the WAMP Network contract. The federated realm address thus only needs to exist as an
|
||
|
identifier of the federated realm-owner record.
|
||
|
"""
|
||
|
__slots__ = (
|
||
|
'_name_or_address',
|
||
|
'_gateway_config',
|
||
|
'_status',
|
||
|
'_name_category',
|
||
|
'_w3',
|
||
|
'_ens',
|
||
|
'_address',
|
||
|
'_contract',
|
||
|
|
||
|
'_seeders',
|
||
|
'_root_ca',
|
||
|
'_catalog',
|
||
|
'_meta',
|
||
|
)
|
||
|
# FIXME
|
||
|
CONTRACT_ADDRESS = web3.Web3.toChecksumAddress('0xF7acf1C4CB4a9550B8969576573C2688B48988C2')
|
||
|
CONTRACT_ABI: str = ''
|
||
|
|
||
|
def __init__(self, name_or_address: str, gateway_config: Optional[Dict[str, Any]] = None):
|
||
|
"""
|
||
|
Instantiate a federated realm from a federated realm ENS name (which is resolved to an Ethereum
|
||
|
address) or Ethereum address.
|
||
|
|
||
|
:param name_or_address: Ethereum ENS name or address.
|
||
|
:param gateway_config: If provided, use this Ethereum gateway. If not provided,
|
||
|
connect via Infura to Ethereum Mainnet, which requires an environment variable
|
||
|
``WEB3_INFURA_PROJECT_ID`` with your Infura project ID.
|
||
|
"""
|
||
|
self._name_or_address = name_or_address
|
||
|
self._gateway_config = gateway_config
|
||
|
|
||
|
# status, will change to 'RUNNING' after initialize() has completed
|
||
|
self._status = 'STOPPED'
|
||
|
|
||
|
self._name_category: Optional[str] = identify_realm_name_category(self._name_or_address)
|
||
|
if self._name_category not in ['eth', 'ens', 'reverse_ens']:
|
||
|
raise ValueError('name_or_address "{}" not an Ethereum address or ENS name'.format(self._name_or_address))
|
||
|
|
||
|
# will be filled once initialize()'ed
|
||
|
self._w3 = None
|
||
|
self._ens = None
|
||
|
|
||
|
# address identifying the federated realm
|
||
|
self._address: Optional[str] = None
|
||
|
|
||
|
# will be initialized with a FederatedRealm contract instance
|
||
|
self._contract: Optional[Contract] = None
|
||
|
|
||
|
# cache of federated realm seeders, filled once in status running
|
||
|
self._seeders: List[Seeder] = []
|
||
|
|
||
|
self._root_ca = None
|
||
|
|
||
|
@property
|
||
|
def status(self) -> str:
|
||
|
return self._status
|
||
|
|
||
|
@property
|
||
|
def name_or_address(self) -> str:
|
||
|
return self._name_or_address
|
||
|
|
||
|
@property
|
||
|
def gateway_config(self) -> Optional[Dict[str, Any]]:
|
||
|
return self._gateway_config
|
||
|
|
||
|
@property
|
||
|
def name_category(self) -> Optional[str]:
|
||
|
return self._name_category
|
||
|
|
||
|
@property
|
||
|
def address(self):
|
||
|
return self._address
|
||
|
|
||
|
def root_ca(self) -> EIP712AuthorityCertificate:
|
||
|
assert self._status == 'RUNNING'
|
||
|
return self._root_ca
|
||
|
|
||
|
@property
|
||
|
def seeders(self) -> List[Seeder]:
|
||
|
return self._seeders
|
||
|
|
||
|
def initialize(self):
|
||
|
"""
|
||
|
|
||
|
:return:
|
||
|
"""
|
||
|
if self._status != 'STOPPED':
|
||
|
raise RuntimeError('cannot start in status "{}"'.format(self._status))
|
||
|
d = deferToThread(self._initialize_background)
|
||
|
return d
|
||
|
|
||
|
def _initialize_background(self):
|
||
|
self._status = 'STARTING'
|
||
|
|
||
|
if self._gateway_config:
|
||
|
self._w3 = make_w3(self._gateway_config)
|
||
|
else:
|
||
|
raise RuntimeError('cannot auto-configure ethereum connection (was removed from web3.py in v6)')
|
||
|
# https://github.com/ethereum/web3.py/issues/1416
|
||
|
# https://github.com/ethereum/web3.py/pull/2706
|
||
|
# from web3.auto.infura import w3
|
||
|
# self._w3 = w3
|
||
|
self._ens = ENS.from_web3(self._w3)
|
||
|
|
||
|
if self._name_category in ['ens', 'reverse_ens']:
|
||
|
if self._name_category == 'reverse_ens':
|
||
|
name = ''.join(reversed(self._name_or_address.split('.')))
|
||
|
else:
|
||
|
name = self._name_or_address
|
||
|
self._address = self._ens.address(name)
|
||
|
elif self._name_category == 'eth':
|
||
|
self._address = self._w3.toChecksumAddress(self._name_or_address)
|
||
|
else:
|
||
|
assert False, 'should not arrive here'
|
||
|
|
||
|
# https://web3py.readthedocs.io/en/stable/contracts.html#web3.contract.Contract
|
||
|
# https://web3py.readthedocs.io/en/stable/web3.eth.html#web3.eth.Eth.contract
|
||
|
# self._contract = self._w3.eth.contract(address=self.CONTRACT_ADDRESS, abi=self.CONTRACT_ABI)
|
||
|
|
||
|
# FIXME: get IPFS hash, download file, unzip seeders
|
||
|
|
||
|
self._status = 'RUNNING'
|