Skip to content

Commit

Permalink
wip: containerization of services
Browse files Browse the repository at this point in the history
* contract is deployed in a separate service and address is written to
  a file that can be read by all parties
* servers are instantiated with a contract context (address, name, source
  code)
* Client reads the contract address from pubic data, and creates web3
  Contract object to interact with the on-chain contract.
* MPC servers serve a GET /inputmasks/{id} endpoint
* Client queries servers for input mask shares
* Makefile can be used to launch example into tmux panes for each
  service (ethereum blockchain, setup phase (contract deployment), MPC
  network, client)

next:
* config for public data including ethereum addresses of client and
  servers
* authorization check for clients when they query a share
* MPC server communication over network sockets
* preprocessing service
* cleanup

note: some of the above next steps may be done at at later stage
  • Loading branch information
sbellem committed Apr 3, 2020
1 parent 5485986 commit af3cc39
Show file tree
Hide file tree
Showing 17 changed files with 646 additions and 270 deletions.
48 changes: 48 additions & 0 deletions apps/masks/Makefile
Original file line number Diff line number Diff line change
@@ -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
136 changes: 119 additions & 17 deletions apps/masks/client.py
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -12,46 +16,51 @@

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
----------
sid: int
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)
Expand All @@ -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
Expand All @@ -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]}
)
Expand All @@ -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))
8 changes: 8 additions & 0 deletions apps/masks/config.py
Original file line number Diff line number Diff line change
@@ -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
)
32 changes: 27 additions & 5 deletions apps/masks/docker-compose.yml
Original file line number Diff line number Diff line change
@@ -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: ../..
Expand All @@ -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"]
15 changes: 15 additions & 0 deletions apps/masks/follow-logs-with-tmux.sh
Original file line number Diff line number Diff line change
@@ -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"
Loading

0 comments on commit af3cc39

Please sign in to comment.