diff --git a/apps/masks/Makefile b/apps/masks/Makefile new file mode 100644 index 00000000..3b96c17c --- /dev/null +++ b/apps/masks/Makefile @@ -0,0 +1,48 @@ +.PHONY: clean clean-test clean-pyc clean-build docs help + +.DEFAULT_GOAL := help + +define PRINT_HELP_PYSCRIPT +import re, sys + +for line in sys.stdin: + match = re.match(r'^([a-zA-Z_-]+):.*?## (.*)$$', line) + if match: + target, help = match.groups() + print("%-20s %s" % (target, help)) +endef + +export PRINT_HELP_PYSCRIPT + + + +help: + @python -c "$$PRINT_HELP_PYSCRIPT" < $(MAKEFILE_LIST) + +clean: clean-pyc + +clean-pyc: ## remove Python file artifacts + docker-compose run --no-deps --rm mpcnet find . -name '*.pyc' -exec rm -f {} + + docker-compose run --no-deps --rm mpcnet find . -name '*.pyo' -exec rm -f {} + + docker-compose run --no-deps --rm mpcnet find . -name '*~' -exec rm -f {} + + docker-compose run --no-deps --rm mpcnet find . -name '__pycache__' -exec rm -fr {} + + +down: ## stop and remove containers, networks, images, and volumes + docker-compose down + +run: down ## run the example + docker-compose up -d blockchain + docker-compose up setup + docker-compose up -d client + sh follow-logs-with-tmux.sh + +run-without-tmux: down ## run the example + docker-compose up -d blockchain + docker-compose up setup + docker-compose up -d client + docker-compose logs --follow blockchain mpcnet client + +setup: down + docker-compose up -d blockchain + docker-compose up setup + docker-compose down blockchain diff --git a/apps/masks/client.py b/apps/masks/client.py index 56118f5d..34a34418 100644 --- a/apps/masks/client.py +++ b/apps/masks/client.py @@ -1,9 +1,13 @@ import asyncio import logging +from collections import namedtuple + + +from aiohttp import ClientSession from web3.contract import ConciseContract -from apps.utils import wait_for_receipt +from apps.utils import fetch_contract, wait_for_receipt from honeybadgermpc.elliptic_curve import Subgroup from honeybadgermpc.field import GF @@ -12,11 +16,13 @@ field = GF(Subgroup.BLS12_381) +Server = namedtuple("Server", ("host", "port")) + class Client: """An MPC client that sends "masked" messages to an Ethereum contract.""" - def __init__(self, sid, myid, send, recv, w3, contract, req_mask): + def __init__(self, sid, myid, w3, req_mask, *, contract_context, mpc_network): """ Parameters ---------- @@ -24,34 +30,37 @@ def __init__(self, sid, myid, send, recv, w3, contract, req_mask): Session id. myid: int Client id. - send: - Function used to send messages. Not used? - recv: - Function used to receive messages. Not used? w3: Connection instance to an Ethereum node. - contract: - Contract instance on the Ethereum blockchain. req_mask: Function used to request an input mask from a server. + contract_context: dict + Contract attributes needed to interact with the contract + using web3. Should contain the address, name and source code + file path. + mpc_network : dict + Dictionary of MPC servers where the key is the server id, and the + value is a dictionary of server attributes necessary to interact with + the server. The expected server attributes are: host and port. """ self.sid = sid self.myid = myid - self.contract = contract + self._contract_context = contract_context + self.contract = fetch_contract(w3, **contract_context) self.w3 = w3 self.req_mask = req_mask - self._task = asyncio.ensure_future(self._run()) + self.mpc_network = {i: Server(**attrs) for i, attrs in mpc_network.items()} + self._task = asyncio.create_task(self._run()) self._task.add_done_callback(print_exception_callback) async def _run(self): contract_concise = ConciseContract(self.contract) - await asyncio.sleep(60) # give the servers a head start # Client sends several batches of messages then quits # for epoch in range(1000): for epoch in range(3): logging.info(f"[Client] Starting Epoch {epoch}") receipts = [] - m = f"Hello Shard! (Epoch: {epoch})" + m = f"Hello! (Epoch: {epoch})" task = asyncio.ensure_future(self.send_message(m)) task.add_done_callback(print_exception_callback) receipts.append(task) @@ -62,17 +71,47 @@ async def _run(self): break await asyncio.sleep(5) + async def _request_mask_share(self, server, mask_idx): + logging.info( + f"query server {server.host}:{server.port} " + f"for its share of input mask with id {mask_idx}" + ) + url = f"http://{server.host}:{server.port}/inputmasks/{mask_idx}" + async with ClientSession() as session: + async with session.get(url) as resp: + json_response = await resp.json() + return json_response["inputmask"] + + def _request_mask_shares(self, mpc_network, mask_idx): + shares = [] + for server in mpc_network.values(): + share = self._request_mask_share(server, mask_idx) + shares.append(share) + return shares + + def _req_masks(self, server_ids, mask_idx): + shares = [] + for server_id in server_ids: + share = self.req_mask(server_id, mask_idx) + shares.append(share) + return shares + async def _get_inputmask(self, idx): # Private reconstruct contract_concise = ConciseContract(self.contract) n = contract_concise.n() poly = polynomials_over(field) eval_point = EvalPoint(field, n, use_omega_powers=False) - shares = [] - for i in range(n): - share = self.req_mask(i, idx) - shares.append(share) + # shares = self._req_masks(range(n), idx) + shares = self._request_mask_shares(self.mpc_network, idx) shares = await asyncio.gather(*shares) + logging.info( + f"{len(shares)} of input mask shares have" + "been received from the MPC servers" + ) + logging.info( + "privately reconstruct the input mask from the received shares ..." + ) shares = [(eval_point(i), share) for i, share in enumerate(shares)] mask = poly.interpolate_at(shares, 0) return mask @@ -81,18 +120,20 @@ async def join(self): await self._task async def send_message(self, m): + logging.info("sending message ...") # Submit a message to be unmasked contract_concise = ConciseContract(self.contract) # Step 1. Wait until there is input available, and enough triples while True: inputmasks_available = contract_concise.inputmasks_available() - # logging.infof'inputmasks_available: {inputmasks_available}') + logging.info(f"inputmasks_available: {inputmasks_available}") if inputmasks_available >= 1: break await asyncio.sleep(5) # Step 2. Reserve the input mask + logging.info("trying to reserve an input mask ...") tx_hash = self.contract.functions.reserve_inputmask().transact( {"from": self.w3.eth.accounts[0]} ) @@ -102,16 +143,77 @@ async def send_message(self, m): inputmask_idx = rich_logs[0]["args"]["inputmask_idx"] else: raise ValueError + logging.info(f"input mask (id: {inputmask_idx}) reserved") + logging.info(f"tx receipt hash is: {tx_receipt['transactionHash'].hex()}") # Step 3. Fetch the input mask from the servers + logging.info("query the MPC servers for their share of the input mask ...") inputmask = await self._get_inputmask(inputmask_idx) + logging.info("input mask has been privately reconstructed") message = int.from_bytes(m.encode(), "big") + logging.info("masking the message ...") masked_message = message + inputmask masked_message_bytes = self.w3.toBytes(hexstr=hex(masked_message.value)) masked_message_bytes = masked_message_bytes.rjust(32, b"\x00") # Step 4. Publish the masked input + logging.info("publish the masked message to the public contract ...") tx_hash = self.contract.functions.submit_message( inputmask_idx, masked_message_bytes ).transact({"from": self.w3.eth.accounts[0]}) tx_receipt = await wait_for_receipt(self.w3, tx_hash) + logging.info( + f"masked message has been published to the " + f"public contract at address {self.contract.address}" + ) + logging.info(f"tx receipt hash is: {tx_receipt['transactionHash'].hex()}") + + +def create_client(w3, *, contract_context): + # TODO put in a toml config file, that could perhaps be auto-generated + server_host = "mpcnet" + mpc_network = { + 0: {"host": server_host, "port": 8080}, + 1: {"host": server_host, "port": 8081}, + 2: {"host": server_host, "port": 8082}, + 3: {"host": server_host, "port": 8083}, + } + client = Client( + "sid", + "client", + w3, + None, + contract_context=contract_context, + mpc_network=mpc_network, + ) + return client + + +async def main(w3, *, contract_context): + client = create_client(w3, contract_context=contract_context) + await client.join() + + +if __name__ == "__main__": + from pathlib import Path + from web3 import HTTPProvider, Web3 + from apps.masks.config import CONTRACT_ADDRESS_FILEPATH + from apps.utils import get_contract_address + + # Launch a client + contract_name = "MpcCoordinator" + contract_filename = "contract.sol" + contract_filepath = Path(__file__).resolve().parent.joinpath(contract_filename) + contract_address = get_contract_address(CONTRACT_ADDRESS_FILEPATH) + contract_context = { + "address": contract_address, + "filepath": contract_filepath, + "name": contract_name, + } + + eth_rpc_hostname = "blockchain" + eth_rpc_port = 8545 + n, t = 4, 1 + w3_endpoint_uri = f"http://{eth_rpc_hostname}:{eth_rpc_port}" + w3 = Web3(HTTPProvider(w3_endpoint_uri)) + asyncio.run(main(w3, contract_context=contract_context)) diff --git a/apps/masks/config.py b/apps/masks/config.py new file mode 100644 index 00000000..cce25c00 --- /dev/null +++ b/apps/masks/config.py @@ -0,0 +1,8 @@ +from pathlib import Path + +PARENT_DIR = Path(__file__).resolve().parent +PUBLIC_DATA_DIR = "public-data" +CONTRACT_ADDRESS_FILENAME = "contract_address" +CONTRACT_ADDRESS_FILEPATH = PARENT_DIR.joinpath( + PUBLIC_DATA_DIR, CONTRACT_ADDRESS_FILENAME +) diff --git a/apps/masks/docker-compose.yml b/apps/masks/docker-compose.yml index de0b3301..0f847178 100644 --- a/apps/masks/docker-compose.yml +++ b/apps/masks/docker-compose.yml @@ -1,11 +1,11 @@ version: '3.7' services: - ganache: - container_name: ganache + blockchain: + container_name: blockchain image: trufflesuite/ganache-cli command: --accounts 50 --blockTime 1 > acctKeys.json 2>&1 - simulation: + setup: image: honeybadgermpc-local build: context: ../.. @@ -14,5 +14,27 @@ services: - ../../apps:/usr/src/HoneyBadgerMPC/apps - ../../honeybadgermpc:/usr/src/honeybadgermpc/honeybadgermpc depends_on: - - ganache - command: python apps/masks/simulation.py + - blockchain + command: ["./apps/wait-for-it.sh", "blockchain:8545", "--", "python", "apps/masks/setup_phase.py"] + mpcnet: + image: honeybadgermpc-local + build: + context: ../.. + dockerfile: Dockerfile + volumes: + - ../../apps:/usr/src/HoneyBadgerMPC/apps + - ../../honeybadgermpc:/usr/src/honeybadgermpc/honeybadgermpc + depends_on: + - setup + command: ["./apps/wait-for-it.sh", "blockchain:8545", "--", "python", "apps/masks/mpcnet.py"] + client: + image: honeybadgermpc-local + build: + context: ../.. + dockerfile: Dockerfile + volumes: + - ../../apps:/usr/src/HoneyBadgerMPC/apps + - ../../honeybadgermpc:/usr/src/honeybadgermpc/honeybadgermpc + depends_on: + - mpcnet + command: ["./apps/wait-for-it.sh", "mpcnet:8083", "--", "python", "apps/masks/client.py"] diff --git a/apps/masks/follow-logs-with-tmux.sh b/apps/masks/follow-logs-with-tmux.sh new file mode 100755 index 00000000..1d56a6c3 --- /dev/null +++ b/apps/masks/follow-logs-with-tmux.sh @@ -0,0 +1,15 @@ +#!/bin/bash + +if [ -z $TMUX ]; then + echo "tmux is not active, will start new session" + TMUX_CMD="new-session" +else + echo "tmux is active, will launch into new window" + TMUX_CMD="new-window" +fi + +tmux $TMUX_CMD "docker-compose logs -f blockchain; sh" \; \ + splitw -h -p 50 "docker-compose logs -f setup; sh" \; \ + splitw -v -p 50 "docker-compose logs -f mpcnet; sh" \; \ + selectp -t 0 \; \ + splitw -v -p 50 "docker-compose logs -f client; sh" diff --git a/apps/masks/main.py b/apps/masks/main.py deleted file mode 100644 index 0367467f..00000000 --- a/apps/masks/main.py +++ /dev/null @@ -1,107 +0,0 @@ -"""Volume Matching Auction : buy and sell orders are matched only on volume while price is determined by reference to some external market.""" - -import asyncio -import logging -import subprocess -from contextlib import contextmanager -from pathlib import Path - -from web3 import HTTPProvider, Web3 -from web3.contract import ConciseContract - -from apps.masks.client import Client -from apps.masks.server import Server -from apps.utils import create_and_deploy_contract - -from honeybadgermpc.preprocessing import PreProcessedElements -from honeybadgermpc.router import SimpleRouter - - -async def main_loop(w3, *, contract_address, abi): - pp_elements = PreProcessedElements() - # deletes sharedata/ if present - pp_elements.clear_preprocessing() - - # Contract instance in concise mode - contract = w3.eth.contract(address=contract_address, abi=abi) - contract_concise = ConciseContract(contract) - - # Call read only methods to check - n = contract_concise.n() - - # Step 2: Create the servers - router = SimpleRouter(n) - sends, recvs = router.sends, router.recvs - servers = [Server("sid", i, sends[i], recvs[i], w3, contract) for i in range(n)] - - # Step 3. Create the client - # TODO communicate with server instead of fetching from list of servers - async def req_mask(i, idx): - # client requests input mask {idx} from server {i} - return servers[i]._inputmasks[idx] - - client = Client("sid", "client", None, None, w3, contract, req_mask) - - # Step 4. Wait for conclusion - for i, server in enumerate(servers): - await server.join() - await client.join() - - -@contextmanager -def run_and_terminate_process(*args, **kwargs): - try: - p = subprocess.Popen(*args, **kwargs) - yield p - finally: - logging.info(f"Killing ganache-cli {p.pid}") - p.terminate() # send sigterm, or ... - p.kill() # send sigkill - p.wait() - logging.info("done") - - -def run_eth(*, contract_name, contract_filepath, n=4, t=1): - w3 = Web3(HTTPProvider()) # Connect to localhost:8545 - deployer = w3.eth.accounts[49] - mpc_addrs = w3.eth.accounts[:n] - contract_address, abi = create_and_deploy_contract( - w3, - deployer=deployer, - contract_name=contract_name, - contract_filepath=contract_filepath, - args=(mpc_addrs, t), - ) - - asyncio.set_event_loop(asyncio.new_event_loop()) - loop = asyncio.get_event_loop() - - try: - logging.info("entering loop") - loop.run_until_complete( - asyncio.gather(main_loop(w3, contract_address=contract_address, abi=abi)) - ) - finally: - logging.info("closing") - loop.close() - - -def main(contract_name=None, contract_filepath=None, n=4, t=1): - import time - - cmd = "ganache-cli -p 8545 --accounts 50 --blockTime 1 > acctKeys.json 2>&1" - logging.info(f"Running {cmd}") - with run_and_terminate_process(cmd, shell=True): - time.sleep(5) - run_eth( - contract_name=contract_name, contract_filepath=contract_filepath, n=n, t=t - ) - - -if __name__ == "__main__": - # Launch an ethereum test chain - contract_name = "MpcCoordinator" - contract_filename = "contract.sol" - contract_filepath = Path(__file__).resolve().parent.joinpath(contract_filename) - n, t = 4, 1 - main(contract_name=contract_name, contract_filepath=contract_filepath, n=4, t=1) diff --git a/apps/masks/mpcnet.py b/apps/masks/mpcnet.py new file mode 100644 index 00000000..217926bf --- /dev/null +++ b/apps/masks/mpcnet.py @@ -0,0 +1,60 @@ +import asyncio +from pathlib import Path + +from web3 import HTTPProvider, Web3 + +from apps.masks.config import CONTRACT_ADDRESS_FILEPATH +from apps.masks.server import Server +from apps.utils import get_contract_address + +from honeybadgermpc.preprocessing import PreProcessedElements +from honeybadgermpc.router import SimpleRouter + +MPCNET_HOST = "mpcnet" + + +def create_servers(w3, *, n, contract_context): + pp_elements = PreProcessedElements() + pp_elements.clear_preprocessing() # deletes sharedata/ if present + + router = SimpleRouter(n) + sends, recvs = router.sends, router.recvs + return [ + Server( + "sid", + i, + sends[i], + recvs[i], + w3, + contract_context=contract_context, + http_host=MPCNET_HOST, + http_port=8080 + i, + ) + for i in range(n) + ] + + +async def main(w3, *, n, contract_context): + servers = create_servers(w3, n=n, contract_context=contract_context) + for server in servers: + await server.join() + + +if __name__ == "__main__": + # Launch MPC network + contract_name = "MpcCoordinator" + contract_filename = "contract.sol" + contract_filepath = Path(__file__).resolve().parent.joinpath(contract_filename) + contract_address = get_contract_address(CONTRACT_ADDRESS_FILEPATH) + contract_context = { + "address": contract_address, + "filepath": contract_filepath, + "name": contract_name, + } + + eth_rpc_hostname = "blockchain" + eth_rpc_port = 8545 + n, t = 4, 1 + w3_endpoint_uri = f"http://{eth_rpc_hostname}:{eth_rpc_port}" + w3 = Web3(HTTPProvider(w3_endpoint_uri)) + asyncio.run(main(w3, n=n, contract_context=contract_context)) diff --git a/apps/masks/public-data/.gitignore b/apps/masks/public-data/.gitignore new file mode 100644 index 00000000..2ebf87a4 --- /dev/null +++ b/apps/masks/public-data/.gitignore @@ -0,0 +1 @@ +contract_address diff --git a/apps/masks/public-data/README.md b/apps/masks/public-data/README.md new file mode 100644 index 00000000..6abfe4eb --- /dev/null +++ b/apps/masks/public-data/README.md @@ -0,0 +1,14 @@ +# Public Data Directory +This directory, `public-data`, serves the purpose of storing data that +does not require any privacy protection, and is meant to be accessible +by all participants involved in an MPC computation, including the +MPC servers, clients, coordinator, and any other entity that may play a +role in the MPC computation from the point of view of making its +execution a reality. + +An example of such data is the address of the MPC coordinator contract, +which is needed by clients and MPC servers. + +**NOTE**: This data is not meant to be tracked by a version control +system (git) as it may differ from one MPC protocol execution to +another. diff --git a/apps/masks/screenshot-tmux.png b/apps/masks/screenshot-tmux.png new file mode 100644 index 00000000..1c42cece Binary files /dev/null and b/apps/masks/screenshot-tmux.png differ diff --git a/apps/masks/server.py b/apps/masks/server.py index 8206ce3f..bc0ab63d 100644 --- a/apps/masks/server.py +++ b/apps/masks/server.py @@ -2,9 +2,11 @@ import logging import time +from aiohttp import web + from web3.contract import ConciseContract -from apps.utils import wait_for_receipt +from apps.utils import fetch_contract, wait_for_receipt from honeybadgermpc.elliptic_curve import Subgroup from honeybadgermpc.field import GF @@ -22,7 +24,18 @@ class Server: """MPC server class to ...""" - def __init__(self, sid, myid, send, recv, w3, contract): + def __init__( + self, + sid, + myid, + send, + recv, + w3, + *, + contract_context, + http_host="0.0.0.0", + http_port=8080, + ): """ Parameters ---------- @@ -36,15 +49,20 @@ def __init__(self, sid, myid, send, recv, w3, contract): Function used to receive messages. w3: Connection instance to an Ethereum node. - contract: - Contract instance on the Ethereum blockchain. + contract_context: dict + Contract attributes needed to interact with the contract + using web3. Should contain the address, name and source code + file path. """ self.sid = sid self.myid = myid - self.contract = contract + self._contract_context = contract_context + self.contract = fetch_contract(w3, **contract_context) self.w3 = w3 self._init_tasks() self._subscribe_task, subscribe = subscribe_recv(recv) + self._http_host = http_host + self._http_port = http_port def _get_send_recv(tag): return wrap_send(tag, send), subscribe(tag) @@ -61,6 +79,8 @@ def _init_tasks(self): self._task3.add_done_callback(print_exception_callback) self._task4 = asyncio.ensure_future(self._mpc_initiate_loop()) self._task4.add_done_callback(print_exception_callback) + # self._http_server = asyncio.create_task(self._client_request_loop()) + # self._http_server.add_done_callback(print_exception_callback) async def join(self): await self._task1 @@ -68,6 +88,8 @@ async def join(self): await self._task3 await self._task4 await self._subscribe_task + # await self._http_server + await self._client_request_loop() ####################### # Step 1. Offline Phase @@ -130,11 +152,40 @@ async def _offline_inputmasks_loop(self): # Increment the preprocessing round and continue preproc_round += 1 + ################################## + # Web server for input mask shares + ################################## + async def _client_request_loop(self): - # Task 2. Handling client input - # TODO: if a client requests a share, - # check if it is authorized and if so send it along - pass + """ Task 2. Handling client input + + .. todo:: if a client requests a share, check if it is + authorized and if so send it along + + """ + routes = web.RouteTableDef() + + @routes.get("/inputmasks/{idx}") + async def _handler(request): + idx = int(request.match_info.get("idx")) + inputmask = self._inputmasks[idx] + data = { + "inputmask": inputmask, + "server_id": self.myid, + "server_port": self._http_port, + } + return web.json_response(data) + + app = web.Application() + app.add_routes(routes) + runner = web.AppRunner(app) + await runner.setup() + site = web.TCPSite(runner, host=self._http_host, port=self._http_port) + await site.start() + print(f"======= Serving on http://{self._http_host}:{self._http_port}/ ======") + # pause here for very long time by serving HTTP requests and + # waiting for keyboard interruption + await asyncio.sleep(100 * 3600) async def _mpc_loop(self): # Task 3. Participating in MPC epochs diff --git a/apps/masks/setup_phase.py b/apps/masks/setup_phase.py new file mode 100644 index 00000000..811bbcb2 --- /dev/null +++ b/apps/masks/setup_phase.py @@ -0,0 +1,59 @@ +import logging +from pathlib import Path + +from web3 import HTTPProvider, Web3 + +from apps.masks.config import CONTRACT_ADDRESS_FILEPATH +from apps.utils import create_and_deploy_contract + +PARENT_DIR = Path(__file__).resolve().parent + +logger = logging.getLogger(__name__) +logger.setLevel(logging.INFO) + + +def deploy_contract( + *, contract_name, contract_filepath, n=4, t=1, eth_rpc_hostname, eth_rpc_port +): + w3_endpoint_uri = f"http://{eth_rpc_hostname}:{eth_rpc_port}" + w3 = Web3(HTTPProvider(w3_endpoint_uri)) + deployer = w3.eth.accounts[49] + mpc_addrs = w3.eth.accounts[:n] + contract_address, abi = create_and_deploy_contract( + w3, + deployer=deployer, + contract_name=contract_name, + contract_filepath=contract_filepath, + args=(mpc_addrs, t), + ) + return contract_address + + +if __name__ == "__main__": + # TODO figure out why logging does not show up in the output + # NOTE appears to be a configuration issue with respect to the + # level as `.warning()` works. + logger.info(f"Deploying contract ...") + print(f"Deploying contract ...") + contract_name = "MpcCoordinator" + contract_filename = "contract.sol" + contract_filepath = PARENT_DIR.joinpath(contract_filename) + eth_rpc_hostname = "blockchain" + eth_rpc_port = 8545 + n, t = 4, 1 + contract_address = deploy_contract( + contract_name=contract_name, + contract_filepath=contract_filepath, + t=1, + eth_rpc_hostname=eth_rpc_hostname, + eth_rpc_port=eth_rpc_port, + ) + logger.info(f"Contract deployed at address: {contract_address}") + print(f"Contract deployed at address: {contract_address}") + with open(CONTRACT_ADDRESS_FILEPATH, "w") as f: + f.write(contract_address) + logger.info(f"Wrote contract address to file: {CONTRACT_ADDRESS_FILEPATH}") + print(f"Wrote contract address to file: {CONTRACT_ADDRESS_FILEPATH}") + import time + + time.sleep(10) diff --git a/apps/masks/simulation.py b/apps/masks/simulation.py deleted file mode 100644 index e43b4739..00000000 --- a/apps/masks/simulation.py +++ /dev/null @@ -1,129 +0,0 @@ -"""Volume Matching Auction : buy and sell orders are matched only on volume while price is determined by reference to some external market.""" - -import asyncio -import logging -import subprocess -from contextlib import contextmanager -from pathlib import Path - -from web3 import HTTPProvider, Web3 -from web3.contract import ConciseContract - -from apps.masks.client import Client -from apps.masks.server import Server -from apps.utils import create_and_deploy_contract - -from honeybadgermpc.preprocessing import PreProcessedElements -from honeybadgermpc.router import SimpleRouter - - -async def main_loop(w3, *, contract_address, abi): - pp_elements = PreProcessedElements() - # deletes sharedata/ if present - pp_elements.clear_preprocessing() - - # Contract instance in concise mode - contract = w3.eth.contract(address=contract_address, abi=abi) - contract_concise = ConciseContract(contract) - - # Call read only methods to check - n = contract_concise.n() - - # Step 2: Create the servers - router = SimpleRouter(n) - sends, recvs = router.sends, router.recvs - servers = [Server("sid", i, sends[i], recvs[i], w3, contract) for i in range(n)] - - # Step 3. Create the client - # TODO communicate with server instead of fetching from list of servers - async def req_mask(i, idx): - # client requests input mask {idx} from server {i} - return servers[i]._inputmasks[idx] - - client = Client("sid", "client", None, None, w3, contract, req_mask) - - # Step 4. Wait for conclusion - for i, server in enumerate(servers): - await server.join() - await client.join() - - -@contextmanager -def run_and_terminate_process(*args, **kwargs): - try: - p = subprocess.Popen(*args, **kwargs) - yield p - finally: - logging.info(f"Killing ganache-cli {p.pid}") - p.terminate() # send sigterm, or ... - p.kill() # send sigkill - p.wait() - logging.info("done") - - -def run_eth( - *, contract_name, contract_filepath, n=4, t=1, eth_rpc_hostname, eth_rpc_port, -): - w3_endpoint_uri = f"http://{eth_rpc_hostname}:{eth_rpc_port}" - w3 = Web3(HTTPProvider(w3_endpoint_uri)) # Connect to localhost:8545 - deployer = w3.eth.accounts[49] - mpc_addrs = w3.eth.accounts[:n] - contract_address, abi = create_and_deploy_contract( - w3, - deployer=deployer, - contract_name=contract_name, - contract_filepath=contract_filepath, - args=(mpc_addrs, t), - ) - - asyncio.set_event_loop(asyncio.new_event_loop()) - loop = asyncio.get_event_loop() - - try: - logging.info("entering loop") - loop.run_until_complete( - asyncio.gather(main_loop(w3, contract_address=contract_address, abi=abi)) - ) - finally: - logging.info("closing") - loop.close() - - -def main( - contract_name=None, - contract_filepath=None, - n=4, - t=1, - eth_rpc_hostname="localhost", - eth_rpc_port=8545, -): - import time - - # cmd = "ganache-cli -p 8545 --accounts 50 --blockTime 1 > acctKeys.json 2>&1" - # logging.info(f"Running {cmd}") - # with run_and_terminate_process(cmd, shell=True): - time.sleep(5) - run_eth( - contract_name=contract_name, - contract_filepath=contract_filepath, - n=n, - t=t, - eth_rpc_hostname=eth_rpc_hostname, - eth_rpc_port=eth_rpc_port, - ) - - -if __name__ == "__main__": - # Launch an ethereum test chain - contract_name = "MpcCoordinator" - contract_filename = "contract.sol" - contract_filepath = Path(__file__).resolve().parent.joinpath(contract_filename) - n, t = 4, 1 - main( - contract_name=contract_name, - contract_filepath=contract_filepath, - n=4, - t=1, - eth_rpc_hostname="ganache", - eth_rpc_port=8545, - ) diff --git a/apps/utils.py b/apps/utils.py index d740bc62..263f733d 100644 --- a/apps/utils.py +++ b/apps/utils.py @@ -29,6 +29,24 @@ def compile_contract_source(filepath): return compile_source(source) +def get_contract_interface(*, contract_name, contract_filepath): + compiled_sol = compile_contract_source(contract_filepath) + try: + contract_interface = compiled_sol[f":{contract_name}"] + except KeyError: + logging.error(f"Contract {contract_name} not found") + raise + + return contract_interface + + +def get_contract_abi(*, contract_name, contract_filepath): + ci = get_contract_interface( + contract_name=contract_name, contract_filepath=contract_filepath + ) + return ci["abi"] + + def deploy_contract(w3, *, abi, bytecode, deployer, args=(), kwargs=None): """Deploy the contract. @@ -122,3 +140,33 @@ def create_and_deploy_contract( kwargs=kwargs, ) return contract_address, abi + + +def get_contract_address(filepath): + with open(filepath, "r") as f: + line = f.readline() + contract_address = line.strip() + return contract_address + + +def fetch_contract(w3, *, address, name, filepath): + """Fetch a contract using the given web3 connection, and contract + attributes. + + Parameters + ---------- + address : str + Ethereum address of the contract. + name : str + Name of the contract. + filepath : str + File path to the source code of the contract. + + Returns + ------- + web3.contract.Contract + The ``web3`` ``Contract`` object. + """ + abi = get_contract_abi(contract_name=name, contract_filepath=filepath) + contract = w3.eth.contract(address=address, abi=abi) + return contract diff --git a/apps/wait-for-it.sh b/apps/wait-for-it.sh new file mode 100755 index 00000000..c5773a44 --- /dev/null +++ b/apps/wait-for-it.sh @@ -0,0 +1,184 @@ +#!/usr/bin/env bash +# Use this script to test if a given TCP host/port are available + +# Source: https://github.com/vishnubob/wait-for-it/blob/c096cface5fbd9f2d6b037391dfecae6fde1362e/wait-for-it.sh + +WAITFORIT_cmdname=${0##*/} + +echoerr() { if [[ $WAITFORIT_QUIET -ne 1 ]]; then echo "$@" 1>&2; fi } + +usage() +{ + cat << USAGE >&2 +Usage: + $WAITFORIT_cmdname host:port [-s] [-t timeout] [-- command args] + -h HOST | --host=HOST Host or IP under test + -p PORT | --port=PORT TCP port under test + Alternatively, you specify the host and port as host:port + -s | --strict Only execute subcommand if the test succeeds + -q | --quiet Don't output any status messages + -t TIMEOUT | --timeout=TIMEOUT + Timeout in seconds, zero for no timeout + -- COMMAND ARGS Execute command with args after the test finishes +USAGE + exit 1 +} + +wait_for() +{ + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + echoerr "$WAITFORIT_cmdname: waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + else + echoerr "$WAITFORIT_cmdname: waiting for $WAITFORIT_HOST:$WAITFORIT_PORT without a timeout" + fi + WAITFORIT_start_ts=$(date +%s) + while : + do + if [[ $WAITFORIT_ISBUSY -eq 1 ]]; then + nc -z $WAITFORIT_HOST $WAITFORIT_PORT + WAITFORIT_result=$? + else + (echo > /dev/tcp/$WAITFORIT_HOST/$WAITFORIT_PORT) >/dev/null 2>&1 + WAITFORIT_result=$? + fi + if [[ $WAITFORIT_result -eq 0 ]]; then + WAITFORIT_end_ts=$(date +%s) + echoerr "$WAITFORIT_cmdname: $WAITFORIT_HOST:$WAITFORIT_PORT is available after $((WAITFORIT_end_ts - WAITFORIT_start_ts)) seconds" + break + fi + sleep 1 + done + return $WAITFORIT_result +} + +wait_for_wrapper() +{ + # In order to support SIGINT during timeout: http://unix.stackexchange.com/a/57692 + if [[ $WAITFORIT_QUIET -eq 1 ]]; then + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --quiet --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + else + timeout $WAITFORIT_BUSYTIMEFLAG $WAITFORIT_TIMEOUT $0 --child --host=$WAITFORIT_HOST --port=$WAITFORIT_PORT --timeout=$WAITFORIT_TIMEOUT & + fi + WAITFORIT_PID=$! + trap "kill -INT -$WAITFORIT_PID" INT + wait $WAITFORIT_PID + WAITFORIT_RESULT=$? + if [[ $WAITFORIT_RESULT -ne 0 ]]; then + echoerr "$WAITFORIT_cmdname: timeout occurred after waiting $WAITFORIT_TIMEOUT seconds for $WAITFORIT_HOST:$WAITFORIT_PORT" + fi + return $WAITFORIT_RESULT +} + +# process arguments +while [[ $# -gt 0 ]] +do + case "$1" in + *:* ) + WAITFORIT_hostport=(${1//:/ }) + WAITFORIT_HOST=${WAITFORIT_hostport[0]} + WAITFORIT_PORT=${WAITFORIT_hostport[1]} + shift 1 + ;; + --child) + WAITFORIT_CHILD=1 + shift 1 + ;; + -q | --quiet) + WAITFORIT_QUIET=1 + shift 1 + ;; + -s | --strict) + WAITFORIT_STRICT=1 + shift 1 + ;; + -h) + WAITFORIT_HOST="$2" + if [[ $WAITFORIT_HOST == "" ]]; then break; fi + shift 2 + ;; + --host=*) + WAITFORIT_HOST="${1#*=}" + shift 1 + ;; + -p) + WAITFORIT_PORT="$2" + if [[ $WAITFORIT_PORT == "" ]]; then break; fi + shift 2 + ;; + --port=*) + WAITFORIT_PORT="${1#*=}" + shift 1 + ;; + -t) + WAITFORIT_TIMEOUT="$2" + if [[ $WAITFORIT_TIMEOUT == "" ]]; then break; fi + shift 2 + ;; + --timeout=*) + WAITFORIT_TIMEOUT="${1#*=}" + shift 1 + ;; + --) + shift + WAITFORIT_CLI=("$@") + break + ;; + --help) + usage + ;; + *) + echoerr "Unknown argument: $1" + usage + ;; + esac +done + +if [[ "$WAITFORIT_HOST" == "" || "$WAITFORIT_PORT" == "" ]]; then + echoerr "Error: you need to provide a host and port to test." + usage +fi + +WAITFORIT_TIMEOUT=${WAITFORIT_TIMEOUT:-15} +WAITFORIT_STRICT=${WAITFORIT_STRICT:-0} +WAITFORIT_CHILD=${WAITFORIT_CHILD:-0} +WAITFORIT_QUIET=${WAITFORIT_QUIET:-0} + +# Check to see if timeout is from busybox? +WAITFORIT_TIMEOUT_PATH=$(type -p timeout) +WAITFORIT_TIMEOUT_PATH=$(realpath $WAITFORIT_TIMEOUT_PATH 2>/dev/null || readlink -f $WAITFORIT_TIMEOUT_PATH) + +WAITFORIT_BUSYTIMEFLAG="" +if [[ $WAITFORIT_TIMEOUT_PATH =~ "busybox" ]]; then + WAITFORIT_ISBUSY=1 + # Check if busybox timeout uses -t flag + # (recent Alpine versions don't support -t anymore) + if timeout &>/dev/stdout | grep -q -e '-t '; then + WAITFORIT_BUSYTIMEFLAG="-t" + fi +else + WAITFORIT_ISBUSY=0 +fi + +if [[ $WAITFORIT_CHILD -gt 0 ]]; then + wait_for + WAITFORIT_RESULT=$? + exit $WAITFORIT_RESULT +else + if [[ $WAITFORIT_TIMEOUT -gt 0 ]]; then + wait_for_wrapper + WAITFORIT_RESULT=$? + else + wait_for + WAITFORIT_RESULT=$? + fi +fi + +if [[ $WAITFORIT_CLI != "" ]]; then + if [[ $WAITFORIT_RESULT -ne 0 && $WAITFORIT_STRICT -eq 1 ]]; then + echoerr "$WAITFORIT_cmdname: strict mode, refusing to execute subprocess" + exit $WAITFORIT_RESULT + fi + exec "${WAITFORIT_CLI[@]}" +else + exit $WAITFORIT_RESULT +fi diff --git a/honeybadgermpc/__init__.py b/honeybadgermpc/__init__.py index 08e09058..d24c13bd 100644 --- a/honeybadgermpc/__init__.py +++ b/honeybadgermpc/__init__.py @@ -14,4 +14,4 @@ os.makedirs(ROOT_DIR / "benchmark-logs", exist_ok=True) logging_config = yaml.safe_load(f.read()) logging.config.dictConfig(logging_config) - logging.getLogger("asyncio").setLevel(logging.WARNING) + logging.getLogger("asyncio").setLevel(logging.INFO) diff --git a/setup.py b/setup.py index c2212a1e..6d18387a 100644 --- a/setup.py +++ b/setup.py @@ -13,7 +13,7 @@ REQUIRES_PYTHON = ">=3.7.0" VERSION = None -REQUIRED = ["gmpy2", "zfec", "pycrypto", "cffi", "psutil", "pyzmq"] +REQUIRED = ["aiohttp", "gmpy2", "zfec", "pycrypto", "cffi", "psutil", "pyzmq"] TESTS_REQUIRES = [ "black", @@ -44,7 +44,7 @@ "doc8", ] -ETH_REQUIRES = ["web3", "ethereum"] +ETH_REQUIRES = ["bitcoin", "web3", "ethereum"] AWS_REQUIRES = ["boto3", "paramiko"]