diff --git a/.github/workflows/create-release.yaml b/.github/workflows/create-release.yaml index 6390796..b349b9a 100644 --- a/.github/workflows/create-release.yaml +++ b/.github/workflows/create-release.yaml @@ -11,9 +11,19 @@ jobs: strategy: matrix: os: [ ubuntu-22.04 ] + + python-version: [ 3.11 ] node-version: [ 18.16.0 ] + solc-version: [ 0.8.19 ] + ganache-version: [ 7.8.0 ] + + numpy-version: [ 1.24.3 ] + matplotlib-version: [ 3.7.1 ] + web3-version: [ 6.2.0 ] + opt-flags: [ "--optimize --optimize-runs 200" ] + name: A job to create a release steps: - name: Checkout @@ -24,9 +34,21 @@ jobs: with: node-version: ${{ matrix.node-version }} - - name: Installing solc compiler + - name: Installing Python ${{ matrix.python-version }} + uses: actions/setup-python@v4 + with: + python-version: ${{ matrix.python-version }} + + - name: Installing NPM packages run: | npm install -g solc@${{ matrix.solc-version }} + npm install -g ganache@${{ matrix.ganache-version }} + + - name: Installing Python packages + run: | + python3 -m pip install web3==${{ matrix.web3-version }} + python3 -m pip install numpy==${{ matrix.numpy-version }} + python3 -m pip install matplotlib==${{ matrix.matplotlib-version }} - name: Compiling contracts for PubSub/EventManager.sol run: | @@ -67,6 +89,32 @@ jobs: sha256sum ./build/HelloWorldSubscriber.bin >> ./build/checksums.txt sha256sum ./build/HelloWorldSubscriber.abi >> ./build/checksums.txt + - name: Prepare binaries for gas cost evaluation + run: | + mkdir -p ./build/PubSub + cp ./build/EventManager.bin ./build/PubSub/EventManager.bin + cp ./build/EventManager.abi ./build/PubSub/EventManager.abi + cp ./build/PubSubService.bin ./build/PubSub/PubSubService.bin + cp ./build/PubSubService.abi ./build/PubSub/PubSubService.abi + mkdir -p ./build/tests + cp ./build/HelloWorldPublisher.bin ./build/tests/HelloWorldPublisher.bin + cp ./build/HelloWorldPublisher.abi ./build/tests/HelloWorldPublisher.abi + cp ./build/HelloWorldSubscriber.bin ./build/tests/HelloWorldSubscriber.bin + cp ./build/HelloWorldSubscriber.abi ./build/tests/HelloWorldSubscriber.abi + + - name: Run publish gas cost evaluation + run: | + python3 ./tests/MultiSubsGasCostEval.py + + - name: Run subscribe gas cost evaluation + run: | + python3 ./tests/MultiPubsGasCostEval.py + + - name: Convert figures into inlined SVG + run: | + python3 ./utils/SvgToInlineMd.py --input ./build/publish_gas_cost.svg --output ./build/publish_gas_cost.md --title "Gas Cost of Publishing Events" + python3 ./utils/SvgToInlineMd.py --input ./build/subscribe_gas_cost.svg --output ./build/subscribe_gas_cost.md --title "Gas cost of Subscribing to Publishers" + - name: Generate release note run: | echo "# Release note" >> ./build/release_note.md @@ -87,6 +135,12 @@ jobs: cat ./build/checksums.txt >> ./build/release_note.md echo "\`\`\`" >> ./build/release_note.md echo "" >> ./build/release_note.md + echo "## Gas Cost Evaluations" >> ./build/release_note.md + echo "### Gas Cost of Publishing Events" >> ./build/release_note.md + cat ./build/publish_gas_cost.md >> ./build/release_note.md + echo "### Gas cost of Subscribing to Publishers" >> ./build/release_note.md + cat ./build/subscribe_gas_cost.md >> ./build/release_note.md + echo "" >> ./build/release_note.md - name: Release uses: softprops/action-gh-release@v1 diff --git a/tests/MultiPubsGasCostEval.py b/tests/MultiPubsGasCostEval.py new file mode 100644 index 0000000..a2d8bee --- /dev/null +++ b/tests/MultiPubsGasCostEval.py @@ -0,0 +1,291 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +### +# Copyright (c) 2023 Roy Shadmon, Haofan Zheng +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +### + + +import os +import signal +import subprocess +import sys +import time + +import matplotlib.pyplot as plt +import numpy as np + +from web3 import Web3 +from typing import List, Tuple + + +BASE_DIR_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BUILD_DIR_PATH = os.path.join(BASE_DIR_PATH, 'build') +UTILS_DIR_PATH = os.path.join(BASE_DIR_PATH, 'utils') +PROJECT_CONFIG_PATH = os.path.join(UTILS_DIR_PATH, 'project_conf.json') +CHECKSUM_KEYS_PATH = os.path.join(UTILS_DIR_PATH, 'ganache_keys_checksum.json') +GANACHE_KEYS_PATH = os.path.join(UTILS_DIR_PATH, 'ganache_keys.json') +GANACHE_PORT = 7545 + +sys.path.append(UTILS_DIR_PATH) +import EthContractHelper + + +def StartGanache() -> subprocess.Popen: + cmd = [ + 'ganache-cli', + '-p', str(GANACHE_PORT), + '-d', + '-a', '20', + '--network-id', '1337', + '--wallet.accountKeysPath', GANACHE_KEYS_PATH, + ] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + return proc + + +def RunTests() -> List[Tuple[int, int]]: + maxNumPublishers = 20 + + # connect to ganache + ganacheUrl = 'http://localhost:{}'.format(GANACHE_PORT) + w3 = Web3(Web3.HTTPProvider(ganacheUrl)) + while not w3.is_connected(): + print('Attempting to connect to ganache...') + time.sleep(1) + print('Connected to ganache') + + # setup account + privKey = EthContractHelper.SetupSendingAccount( + w3=w3, + account=0, # use account 0 + keyJson=CHECKSUM_KEYS_PATH + ) + + + subscribeCost = [] + + + for numPublishers in range(1, maxNumPublishers + 1): + print() + print(f'Running test with {numPublishers} subscribers') + print() + + # deploy PubSub contract + print('Deploying PubSub contract...') + pubSubContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='PubSubService', + release=None, # use locally built contract + address=None, # deploy new contract + ) + pubSubReceipt = EthContractHelper.DeployContract( + w3=w3, + contract=pubSubContract, + arguments=[ ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + pubSubAddr = pubSubReceipt.contractAddress + print('PubSub contract deployed at {}'.format(pubSubAddr)) + + # load deployed PubSub contract + pubSubContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='PubSubService', + release=None, # use locally built contract + address=pubSubAddr, # use deployed contract + ) + + publishers = [] + for pubIndex in range(0, numPublishers): + # deploy Publisher contract + print('Deploying publisher contract...') + publisherContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='HelloWorldPublisher', + release=None, # use locally built contract + address=None, # deploy new contract + ) + publisherReceipt = EthContractHelper.DeployContract( + w3=w3, + contract=publisherContract, + arguments=[ ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + publisherAddr = publisherReceipt.contractAddress + print('Publisher contract deployed at {}'.format(publisherAddr)) + + # load deployed Publisher contract + publisherContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='HelloWorldPublisher', + release=None, # use locally built contract + address=publisherAddr, # use deployed contract + ) + + # register publisher + print('Registering publisher...') + EthContractHelper.CallContractFunc( + w3=w3, + contract=publisherContract, + funcName='register', + arguments=[ pubSubAddr ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + + publishers.append(publisherContract) + + costs = [] + for publisherContract in publishers: + publisherAddr = publisherContract.address + + # deploy Subscriber contract + print('Deploying subscriber contract...') + subscriberContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='HelloWorldSubscriber', + release=None, # use locally built contract + address=None, # deploy new contract + ) + subscriberReceipt = EthContractHelper.DeployContract( + w3=w3, + contract=subscriberContract, + arguments=[ pubSubAddr ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + subscriberAddr = subscriberReceipt.contractAddress + + # load deployed Subscriber contract + subscriberContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='HelloWorldSubscriber', + release=None, # use locally built contract + address=subscriberAddr, # use deployed contract + ) + + # subscribe + print('Subscribing...') + subTxReceipt = EthContractHelper.CallContractFunc( + w3=w3, + contract=subscriberContract, + funcName='subscribe', + arguments=[ publisherAddr ], + privKey=privKey, + gas=None, # let web3 estimate + value=10000000000000000, # 0.01 ether + confirmPrompt=False # don't prompt for confirmation + ) + print('Subscriber@{} subscribed to publisher@{}'.format( + subscriberAddr, + publisherAddr + )) + + costs.append(subTxReceipt.gasUsed) + print('Gas used: {}'.format(subTxReceipt.gasUsed)) + + # record gas used + subscribeCost.append(( + numPublishers, + sum(costs) / len(costs), # average gas cost + )) + + return subscribeCost + + +def DrawGraph( + dest: os.PathLike, + data: List[Tuple[int, int]], + scaleBy: int, + title: str, + xlabel: str = 'Number of subscribers', + ylabel: str = 'Gas cost', +) -> None: + + scale = np.power(10, scaleBy) + axes = plt.subplot() + axes.plot( + np.arange(1, len(data) + 1), + np.array([ cost for _, cost in data ]) / scale, + ) + + # set y-axis limits + dataAvg = sum([ cost for _, cost in data ]) + dataAvg = dataAvg / len(data) + ymax = (dataAvg + (dataAvg * 0.0001)) / scale + ymin = (dataAvg - (dataAvg * 0.0001)) / scale + axes.set_ylim([ymin, ymax]) + + # avoid scientific notation + current_values = axes.get_yticks() + axes.set_yticklabels( + ['{:.04f}'.format(x) for x in current_values] + ) + + plt.xticks(np.arange(1, len(data) + 1)) + plt.title(title) + plt.xlabel(xlabel) + plt.ylabel(ylabel + f' (1e{scaleBy})') + plt.savefig(dest) + + +def StopGanache(ganacheProc: subprocess.Popen) -> None: + print('Shutting down ganache (it may take ~15 seconds)...') + waitEnd = time.time() + 20 + ganacheProc.terminate() + while ganacheProc.poll() is None: + try: + if time.time() > waitEnd: + print('Force to shut down ganache') + ganacheProc.kill() + else: + print('Still waiting for ganache to shut down...') + ganacheProc.send_signal(signal.SIGINT) + ganacheProc.wait(timeout=2) + except subprocess.TimeoutExpired: + continue + print('Ganache has been shut down') + + +def main(): + ganacheProc = StartGanache() + + try: + subscribeCost = RunTests() + + print('Subscribe gas cost results:') + for cost in subscribeCost: + print('{:03} publishers: {:010.2f} gas'.format(cost[0], cost[1])) + + DrawGraph( + dest=os.path.join(BUILD_DIR_PATH, 'subscribe_gas_cost.svg'), + data=subscribeCost, + scaleBy=5, + title='Gas cost of subscribing', + ) + finally: + # finish and exit + StopGanache(ganacheProc) + +if __name__ == "__main__": + main() diff --git a/tests/MultiSubsGasCostEval.py b/tests/MultiSubsGasCostEval.py new file mode 100644 index 0000000..210f249 --- /dev/null +++ b/tests/MultiSubsGasCostEval.py @@ -0,0 +1,336 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +### +# Copyright (c) 2023 Roy Shadmon, Haofan Zheng +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +### + + +import os +import random +import signal +import subprocess +import sys +import time + +import matplotlib.pyplot as plt +import numpy as np + +from web3 import Web3 +from typing import List, Tuple + + +BASE_DIR_PATH = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) +BUILD_DIR_PATH = os.path.join(BASE_DIR_PATH, 'build') +UTILS_DIR_PATH = os.path.join(BASE_DIR_PATH, 'utils') +PROJECT_CONFIG_PATH = os.path.join(UTILS_DIR_PATH, 'project_conf.json') +CHECKSUM_KEYS_PATH = os.path.join(UTILS_DIR_PATH, 'ganache_keys_checksum.json') +GANACHE_KEYS_PATH = os.path.join(UTILS_DIR_PATH, 'ganache_keys.json') +GANACHE_PORT = 7545 + +sys.path.append(UTILS_DIR_PATH) +import EthContractHelper + + +def StartGanache() -> subprocess.Popen: + cmd = [ + 'ganache-cli', + '-p', str(GANACHE_PORT), + '-d', + '-a', '20', + '--network-id', '1337', + '--wallet.accountKeysPath', GANACHE_KEYS_PATH, + ] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + return proc + + +def RunTests() -> List[Tuple[int, int]]: + maxNumSubscribers = 20 + + # connect to ganache + ganacheUrl = 'http://localhost:{}'.format(GANACHE_PORT) + w3 = Web3(Web3.HTTPProvider(ganacheUrl)) + while not w3.is_connected(): + print('Attempting to connect to ganache...') + time.sleep(1) + print('Connected to ganache') + + # setup account + privKey = EthContractHelper.SetupSendingAccount( + w3=w3, + account=0, # use account 0 + keyJson=CHECKSUM_KEYS_PATH + ) + + + publishCost = [] + + + for numSubscribers in range(1, maxNumSubscribers + 1): + print() + print(f'Running test with {numSubscribers} subscribers') + print() + + # deploy PubSub contract + print('Deploying PubSub contract...') + pubSubContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='PubSubService', + release=None, # use locally built contract + address=None, # deploy new contract + ) + pubSubReceipt = EthContractHelper.DeployContract( + w3=w3, + contract=pubSubContract, + arguments=[ ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + pubSubAddr = pubSubReceipt.contractAddress + print('PubSub contract deployed at {}'.format(pubSubAddr)) + + # load deployed PubSub contract + pubSubContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='PubSubService', + release=None, # use locally built contract + address=pubSubAddr, # use deployed contract + ) + + # deploy Publisher contract + print('Deploying publisher contract...') + publisherContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='HelloWorldPublisher', + release=None, # use locally built contract + address=None, # deploy new contract + ) + publisherReceipt = EthContractHelper.DeployContract( + w3=w3, + contract=publisherContract, + arguments=[ ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + publisherAddr = publisherReceipt.contractAddress + print('Publisher contract deployed at {}'.format(publisherAddr)) + + # load deployed Publisher contract + publisherContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='HelloWorldPublisher', + release=None, # use locally built contract + address=publisherAddr, # use deployed contract + ) + + # register publisher + print('Registering publisher...') + EthContractHelper.CallContractFunc( + w3=w3, + contract=publisherContract, + funcName='register', + arguments=[ pubSubAddr ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + + subscribers = [] + for subsIndex in range(0, numSubscribers): + # deploy Subscriber contract + print('Deploying subscriber contract...') + subscriberContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='HelloWorldSubscriber', + release=None, # use locally built contract + address=None, # deploy new contract + ) + subscriberReceipt = EthContractHelper.DeployContract( + w3=w3, + contract=subscriberContract, + arguments=[ pubSubAddr ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + subscriberAddr = subscriberReceipt.contractAddress + + # load deployed Subscriber contract + subscriberContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='HelloWorldSubscriber', + release=None, # use locally built contract + address=subscriberAddr, # use deployed contract + ) + + # subscribe + print('Subscribing...') + EthContractHelper.CallContractFunc( + w3=w3, + contract=subscriberContract, + funcName='subscribe', + arguments=[ publisherAddr ], + privKey=privKey, + gas=None, # let web3 estimate + value=10000000000000000, # 0.01 ether + confirmPrompt=False # don't prompt for confirmation + ) + + subscribers.append(subscriberContract) + + # generate a random message to be published + expectedMsg = random.randbytes(32).hex() + + # set message to be published + print('Setting message to be published...') + EthContractHelper.CallContractFunc( + w3=w3, + contract=publisherContract, + funcName='setSendData', + arguments=[ expectedMsg ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + print('Message set to "{}"'.format(expectedMsg)) + + # estimate the gas limit for publishing + publishEstGas = ( + 100000 + # est gas cost before publishing + 202000 + # gas cost for publishing + 100000 # est gas cost after publishing + ) + publishEstGas *= numSubscribers + + # publish + print('Publishing...') + pubTxReceipt = EthContractHelper.CallContractFunc( + w3=w3, + contract=publisherContract, + funcName='publish', + arguments=[ ], + privKey=privKey, + gas=publishEstGas, + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + + # ensure every subscriber received the message + recvCount = {} + for subscriberContract in subscribers: + msg = EthContractHelper.CallContractFunc( + w3=w3, + contract=subscriberContract, + funcName='m_recvData', + arguments=[ ], + privKey=None, + gas=None, + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + print('Message received: "{}"'.format(msg)) + if msg != expectedMsg: + raise RuntimeError( + 'Message received does not match the expected message ' + '"{} != {}"'.format( + msg, + expectedMsg, + ) + ) + recvCount[subscriberContract.address] = subscriberContract.address + + if len(recvCount) != numSubscribers: + raise RuntimeError( + 'Not all subscribers received the message' + ) + for addr in recvCount.keys(): + print('Subscriber {} received the message'.format(addr)) + + # record gas used + publishCost.append(( + numSubscribers, + pubTxReceipt.gasUsed, + )) + + return publishCost + + +def DrawGraph( + dest: os.PathLike, + data: List[Tuple[int, int]], + scaleBy: int, + title: str, + xlabel: str = 'Number of subscribers', + ylabel: str = 'Gas cost', +) -> None: + + scale = np.power(10, scaleBy) + plt.plot( + np.arange(1, len(data) + 1), + np.array([ cost for _, cost in data ]) / scale, + ) + plt.xticks(np.arange(1, len(data) + 1)) + plt.title(title) + plt.xlabel(xlabel) + plt.ylabel(ylabel + f' (1e{scaleBy})') + plt.savefig(dest) + + +def StopGanache(ganacheProc: subprocess.Popen) -> None: + print('Shutting down ganache (it may take ~15 seconds)...') + waitEnd = time.time() + 20 + ganacheProc.terminate() + while ganacheProc.poll() is None: + try: + if time.time() > waitEnd: + print('Force to shut down ganache') + ganacheProc.kill() + else: + print('Still waiting for ganache to shut down...') + ganacheProc.send_signal(signal.SIGINT) + ganacheProc.wait(timeout=2) + except subprocess.TimeoutExpired: + continue + print('Ganache has been shut down') + + +def main(): + ganacheProc = StartGanache() + + try: + publishCost = RunTests() + + print('Publish gas cost results:') + for cost in publishCost: + print('{:03} subscribers: {:010} gas'.format(cost[0], cost[1])) + + DrawGraph( + dest=os.path.join(BUILD_DIR_PATH, 'publish_gas_cost.svg'), + data=publishCost, + scaleBy=6, + title='Gas cost of publishing', + ) + finally: + # finish and exit + StopGanache(ganacheProc) + + +if __name__ == "__main__": + main() diff --git a/utils/EthContractHelper.py b/utils/EthContractHelper.py index 5ee87ac..4d50fa3 100644 --- a/utils/EthContractHelper.py +++ b/utils/EthContractHelper.py @@ -133,6 +133,18 @@ def _DetermineGas( return gas +def _DetermineMaxPriorFee( + w3: Web3, +) -> int: + baseGasFee = w3.eth.gas_price + # priority fee is 2% of base fee + maxPriorFee = int(baseGasFee * 2) // 100 + # ensure it's higher than w3.eth.max_priority_fee + maxPriorFee = max(maxPriorFee, int(w3.eth.max_priority_fee)) + + return maxPriorFee + + def _FillMessage( w3: Web3, gas: int, @@ -148,7 +160,7 @@ def _FillMessage( } if privKey is not None: msg['maxFeePerGas'] = int(w3.eth.gas_price * 2) - msg['maxPriorityFeePerGas'] = w3.eth.max_priority_fee + msg['maxPriorityFeePerGas'] = _DetermineMaxPriorFee(w3) return msg @@ -252,6 +264,13 @@ def _DoTransaction( receiptJson = json.dumps(json.loads(Web3.to_json(receipt)), indent=4) logger.info('Transaction receipt: {}'.format(receiptJson)) + logger.info('Balance after transaction: {} Ether'.format( + w3.from_wei( + w3.eth.get_balance(w3.eth.default_account), + 'ether' + ) + )) + return receipt @@ -394,6 +413,14 @@ def SetupSendingAccount( w3.eth.default_account = w3.eth.accounts[0] privKey = None + if privKey is not None: + # ensure that the private key matches the address + privAcc = w3.eth.account.from_key(privKey) + if privAcc.address != w3.eth.default_account: + raise ValueError( + 'The private key does not match the address' + ) + logger.info( 'The address of the account to be used: {}'.format( w3.eth.default_account diff --git a/utils/SvgToInlineMd.py b/utils/SvgToInlineMd.py new file mode 100644 index 0000000..74a3cfc --- /dev/null +++ b/utils/SvgToInlineMd.py @@ -0,0 +1,57 @@ +#!/usr/bin/env python3 +# -*- coding:utf-8 -*- +### +# Copyright (c) 2023 Haofan Zheng +# Use of this source code is governed by an MIT-style +# license that can be found in the LICENSE file or at +# https://opensource.org/licenses/MIT. +### + + +import argparse +import base64 + + +def _Convert( + svg: str, +) -> str: + + return base64.b64encode(svg.encode('utf-8')).decode('utf-8') + + +def main(): + argParser = argparse.ArgumentParser() + argParser.add_argument( + '--input', type=str, required=True, + help='input file path' + ) + argParser.add_argument( + '--output', type=str, required=True, + help='output file path' + ) + argParser.add_argument( + '--title', type=str, required=False, default=None, + help='title of the inlining figure' + ) + args = argParser.parse_args() + + with open(args.input, 'r') as f: + svg = f.read() + + converted = _Convert(svg) + + if args.title is not None: + converted = '![{title}](data:image/svg+xml;base64,{encode} "{title}")'.format( + title=args.title, + encode=converted, + ) + # converted = ''.format( + # #title=args.title, + # encode=converted, + # ) + + with open(args.output, 'w') as f: + f.write(converted) + +if __name__ == '__main__': + main() diff --git a/web3-py-impl/Publisher.sol b/web3-py-impl/Publisher.sol deleted file mode 100644 index 775fd41..0000000 --- a/web3-py-impl/Publisher.sol +++ /dev/null @@ -1,97 +0,0 @@ -// SPDX-License-Identifier: MIT -pragma solidity ^0.8.17; - - - -interface InterfacePubSubService { - function register() external returns(InterfaceEventManager); -} - - -interface InterfaceEventManager { - function addSubscriber(address publisher_addr, uint subscribeContractAddr) external payable; - function notify(bytes memory data) external; -} - -contract Publisher { - enum Action { ADD_TO_BLACKLIST, DELETE_FROM_BLACKLIST } // Types of actions - - struct BlackList { - address[] memberList; // keep list of blacklist addresses - mapping(address => bool) members; // easy lookup into in memberList (publisher => bool) - } - - bool public registeredToPubSub = false; - address public PubSubAddress; - address public eventManagerAddress; - InterfacePubSubService pubService; - - BlackList officialBL; // getting weird errors, so the key is eventManagerAddress - - - // mapping(address => eventManager) eventManagers; // Keep track of eventManagers (eventManagerAddr => eventManager) - // address[] eventManagersList; // List of eventManagers - - function registerToPubSubService(address pubSubAddr) payable external returns(address) { - - require(registeredToPubSub == false, "Publisher already registered to PubSubService"); // Assumes publisher can only register to a single pubsub service - pubService = InterfacePubSubService(pubSubAddr); - eventManagerAddress = address(pubService.register()); // TODO: Specify what kind of event types. e.g., - registeredToPubSub = true; - PubSubAddress = pubSubAddr; - return eventManagerAddress; - } - - - function addToBlackList(address user) public { - require(registeredToPubSub == true, "Publisher not registered yet"); - require(officialBL.members[user] == false, "User is already in blacklist"); - officialBL.memberList.push(user); - officialBL.members[user] = true; - InterfaceEventManager(eventManagerAddress).notify(encodeAction(Action.ADD_TO_BLACKLIST, user)); - } - - function viewBlackList() public view returns(address[] memory) { - return officialBL.memberList; - } - - - function removeFromBlackList(address member) public { - require(registeredToPubSub == true, "Publisher not registered yet"); - require(officialBL.members[member] == true, "User is not on blacklist"); - remove(officialBL.memberList, member); - officialBL.members[member] = false; - InterfaceEventManager(eventManagerAddress).notify(encodeAction(Action.DELETE_FROM_BLACKLIST, member)); - } - - - // helper function to remove element from array - function remove(address[] storage publisherList, address publisher) private { - require(publisherList.length > 0, "Received empty publisher list"); - for (uint i = 0; i < publisherList.length; i++){ - if (publisherList[i] == publisher) { - publisherList[i] = publisherList[publisherList.length-1]; // remove element by overwriting previous index and removing the last index - publisherList.pop(); - } - } - officialBL.members[publisher] = false; - } - - function getContractBalance() external view returns (uint256) { - return address(this).balance; - } - - function encodeAction(Action action, address user) internal pure returns (bytes memory) { - bytes memory data = abi.encode(action, user); - return data; - } - - - // Receive function - called when ether is sent directly to the contract address - receive() external payable { - // payable(owen_wallet).transfer(msg.value); - } - - // // Fallback function - called when no other function matches the function signature - // fallback() external payable {} -} diff --git a/web3-py-impl/deploy_contracts.py b/web3-py-impl/deploy_contracts.py deleted file mode 100644 index 191ada0..0000000 --- a/web3-py-impl/deploy_contracts.py +++ /dev/null @@ -1,186 +0,0 @@ -# import sys -# import time -# import pprint -import os -# import subprocess -# from subprocess import PIPE, Popen -# import glob -import json - -import web3.logs -# -# import solcx -from web3.providers import HTTPProvider -from web3 import Web3 -import matplotlib.pyplot as plt -import numpy as np - -import importlib.util -file_path = '/Users/royshadmon/Web3-Ethereum-Interface/src/web3_eth.py' - -# Specify the module name -module_name = 'Web3_Eth' - -# Load the module from the file path -spec = importlib.util.spec_from_file_location(module_name, file_path) -module = importlib.util.module_from_spec(spec) -spec.loader.exec_module(module) - - -def load_contract_abi_bin(root_directory, contract_name, solidity_version="0.8.17"): - with open( - os.path.join(root_directory, f"contracts/compiled_contracts/{solidity_version}/{contract_name}/{contract_name}.abi")) as f: - - abi = f.read() - with open( - os.path.join(root_directory, f"contracts/compiled_contracts/{solidity_version}/{contract_name}/{contract_name}.bin")) as f: - - bin = f.read() - return abi, bin - - -def compile_contract(web3_interface, root_dir, contract_name, output_dir=None, solidity_version="0.8.17"): - contract_path = os.path.join(root_dir, f"contracts/{contract_name}") - contract_name = contract_name.split('.')[0] - if not output_dir: - output_dir = os.path.join(root_dir, f"contracts/compiled_contracts/{solidity_version}/{contract_name}") - - id, interface = web3_interface.compile_contract(contract_path, output_dir, solidity_version) - return id, interface - -# self.connection.eth.contract(address=contract_address, abi=abi_) -def get_contract_instance(web3_interface, contract_addr, abi): - - contract_instance = web3_interface.w3.eth.contract(address=web3_interface.toCheckSumAddress(contract_addr), abi=abi) - return contract_instance - - -def deploy_contract(web3_interface, contract_name, account_addr, private_key, solidity_version="0.8.17"): - abi, bin = load_contract_abi_bin(contract_name, solidity_version) - if web3_interface.verify_acct_addr_matches_p_key(account_addr, private_key): - contract_address = web3_interface.deploy_contract(abi, bin, private_key, account_addr) - else: - print(f"Account address does not match private key") - exit(-1) - return contract_address - -def submit_transaction(web3_interface, transaction, private_key): - signed_tx = web3_interface.signTransaction(transaction, private_key=private_key) - tx_hash = web3_interface.sendRawTransaction(signed_tx) - tx_receipt = web3_interface.waitForTransactionReceipt(tx_hash) - return tx_receipt - - -def main(): - # Get the directory where the current Python script is located - script_directory = os.path.dirname(os.path.abspath(__file__)) - # Get the root directory of the project (assuming the root directory is the parent directory of the script directory) - root_directory = os.path.dirname(script_directory) - ganache_url = "http://127.0.0.1:8545" - web3_interface = module.Web3_Eth(ganache_url) - - # acct_address = "0x90F8bf6A479f320ead074411a4B0e7944Ea8c9C1" - # user_private_key = "0x4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d" - - # Get Private Keys - with open(os.path.join(root_directory, "eth_accounts/keys.json")) as f: - private_keys = f.read() - private_keys = json.loads(private_keys)['private_keys'] - - private_keys_list = list(private_keys.values()) - - user_private_key = private_keys_list[0] - acct_address = web3_interface.get_eth_user(user_private_key).address - - - print(web3_interface.w3.is_connected()) - - # # Compile Contracts - publisher_id, publisher_interface = compile_contract(web3_interface, root_directory, "Publisher.sol") - id, interface = compile_contract(web3_interface, root_directory, "PubSubService.sol") - subscriber_id, subscriber_interface = compile_contract(web3_interface, root_directory, "Subscriber.sol") - - notify_add_cost = [] - notify_remove_cost = [] - - for i in range(1, len(private_keys_list)+1): - publisher_addr, publisher_contract = web3_interface.deploy_contract(publisher_interface, user_private_key) - print(f"PUBLISHER ADDRESS {web3_interface.toCheckSumAddress(publisher_addr)}") - - pubsub_addr, pubsub_contract = web3_interface.deploy_contract(interface, user_private_key) - - # Publisher register to PubSubService - transaction = publisher_contract.functions.registerToPubSubService(pubsub_addr).build_transaction( - { - 'from': acct_address, - 'chainId': 1337, # Ganache chain id - 'nonce': web3_interface.w3.eth.get_transaction_count(acct_address), - # 'value': 0, - } - ) - tx_receipt = submit_transaction(web3_interface, transaction, private_key=user_private_key) - - # Get event manager contract address - em_addr = publisher_contract.events.EM_CREATED().process_receipt(tx_receipt)[0].args.em_addr - print(f"EVENT MANAGER {em_addr}") - - # Deploy Subscribers - for p_key in private_keys_list[:i]: - user = web3_interface.get_eth_user(p_key) - subscriber_addr, subscriber_contract = web3_interface.deploy_contract(subscriber_interface, p_key, publisher_addr, pubsub_addr, 100000010101, value=100000010101) - print(f'SUBSCRIBER_ADDR {subscriber_addr}') - - - # Add to blacklist - - - user = web3_interface.get_eth_user(p_key) - print(f'ADDING {web3_interface.toCheckSumAddress(user.address)}') - transaction = publisher_contract.functions.addToBlackList(web3_interface.toCheckSumAddress(user.address)).build_transaction({ - 'from': web3_interface.toCheckSumAddress(user.address), - 'chainId': 1337, # Ganache chain id - 'nonce': web3_interface.getTransactionCount(user), - }) - tx_receipt = submit_transaction(web3_interface, transaction, p_key) - print(f"TX_RECEIPT") - print(f"=======================") - notify_add_cost.append(publisher_contract.events.GAS_COST().process_receipt(tx_receipt, errors=web3.logs.DISCARD)[0].args.gas) # Discarding warnings due to "MismatchedABI(The event signature did not match the provided ABI)". Reason from https://github.com/oceanprotocol/ocean.py/issues/348 - - - # Remove single element from blacklist - - print(f'REMOVING {web3_interface.toCheckSumAddress(user.address)}') - - - transaction = publisher_contract.functions.removeFromBlackList(web3_interface.toCheckSumAddress(user.address)).build_transaction({ - 'from': web3_interface.toCheckSumAddress(user.address), - 'chainId': 1337, # Ganache chain id - 'nonce': web3_interface.getTransactionCount(user), - }) - tx_receipt = submit_transaction(web3_interface, transaction, p_key) - print(f"TX_RECEIPT") - print(f"=======================") - notify_remove_cost.append(publisher_contract.events.GAS_COST().process_receipt(tx_receipt, errors=web3.logs.DISCARD)[0].args.gas) - - - scale_by = 6 - scale = np.power(10, scale_by) - plt.plot(np.arange(1, len(notify_add_cost) + 1), np.array(notify_add_cost)/scale) - plt.xticks(np.arange(1,len(notify_add_cost)+1, 1)) - plt.title(f"Gas cost adding to blacklist") - plt.xlabel("Number of subscribing contracts") - plt.ylabel(f"Gas cost (1e{scale_by})") - plt.show() - print(notify_add_cost) - - plt.plot(np.arange(1, len(notify_remove_cost) + 1), np.array(notify_remove_cost)/scale) - plt.xticks(np.arange(1, len(notify_remove_cost) + 1, 1)) - plt.title(f"Gas cost removing from blacklist starting with {len(notify_remove_cost)} items") - plt.xlabel("Number of subscribing contracts") - plt.ylabel(f"Gas cost (1e{scale_by})") - plt.show() - exit(-1) - - -if __name__ == "__main__": - main() \ No newline at end of file