diff --git a/.github/workflows/create-release.yaml b/.github/workflows/create-release.yaml index 97998a5..71fbbbc 100644 --- a/.github/workflows/create-release.yaml +++ b/.github/workflows/create-release.yaml @@ -17,9 +17,9 @@ jobs: python-version: [ 3.11 ] node-version: [ 18.16.0 ] - ganache-version: [ 7.8.0 ] + ganache-version: [ 7.9.1 ] - solc-version: [ v0.8.20 ] + solc-version: [ v0.8.21 ] env: SOLC_BIN: ${{ github.workspace }}/build/solc-static-linux diff --git a/.github/workflows/pubsub-solidity-unittesting.yaml b/.github/workflows/pubsub-solidity-unittesting.yaml index 80c3e12..af9321f 100644 --- a/.github/workflows/pubsub-solidity-unittesting.yaml +++ b/.github/workflows/pubsub-solidity-unittesting.yaml @@ -20,7 +20,7 @@ jobs: strategy: matrix: os: [ ubuntu-22.04 ] - solc-version: [ 0.8.20 ] + solc-version: [ 0.8.21 ] chain-fork: [ shanghai ] opt-runs: [ 200 ] diff --git a/Makefile b/Makefile index c3945f4..75a23aa 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,7 @@ MODULES := \ PubSub \ tests -SOLC_VERSION := v0.8.20 +SOLC_VERSION := v0.8.21 MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) CURRENT_DIR := $(dir $(MKFILE_PATH)) SOLC_BIN := $(CURRENT_DIR)/build/solc-static-linux diff --git a/tests/BasicActionGasCost.sol b/tests/BasicActionGasCost.sol new file mode 100644 index 0000000..67eaa93 --- /dev/null +++ b/tests/BasicActionGasCost.sol @@ -0,0 +1,63 @@ +// SPDX-License-Identifier: MIT +pragma solidity >=0.4.17 <0.9.0; + + +contract GasEvalSubscriber { + constructor() { + } + + function onNotify(bytes memory data) external { + // do nothing + } +} + + +contract BasicActionGasCost { + + event LogGasCost(uint8 idx, uint256 gasUsed); + + address m_subAddr = address(0); + bool m_someBool = false; + mapping(address => bool) m_someMap; + address m_someAddr = 0x0000000000000000000000000000000000000000; + + constructor() { + GasEvalSubscriber sub = new GasEvalSubscriber(); + m_subAddr = address(sub); + + m_someMap[m_someAddr] = true; + } + + function eval() external { + bytes memory data = new bytes(0); + + uint256 gasStart = 0; + uint256 gasUsed = 0; + bool someBool = !m_someBool; + address someAddr = msg.sender; + address someAddr2 = m_someAddr; + + gasStart = gasleft(); + GasEvalSubscriber(m_subAddr).onNotify(data); + gasUsed = gasStart - gasleft(); + emit LogGasCost(1, gasUsed); + + gasStart = gasleft(); + m_someBool = someBool; + gasUsed = gasStart - gasleft(); + emit LogGasCost(2, gasUsed); + + gasStart = gasleft(); + m_someMap[someAddr] = someBool; + gasUsed = gasStart - gasleft(); + emit LogGasCost(3, gasUsed); + + gasStart = gasleft(); + delete m_someMap[someAddr2]; + gasUsed = gasStart - gasleft(); + emit LogGasCost(4, gasUsed); + + m_someBool = false; + m_someMap[someAddr2] = true; + } +} diff --git a/tests/Makefile b/tests/Makefile index 315f3ec..9a6fbc3 100644 --- a/tests/Makefile +++ b/tests/Makefile @@ -1,7 +1,8 @@ MODULE_NAME := tests CONTRACTS := \ HelloWorldPublisher \ - HelloWorldSubscriber + HelloWorldSubscriber \ + BasicActionGasCost MKFILE_PATH := $(abspath $(lastword $(MAKEFILE_LIST))) CURRENT_DIR := $(dir $(MKFILE_PATH)) diff --git a/utils/GanacheBasicActionTest.py b/utils/GanacheBasicActionTest.py new file mode 100644 index 0000000..810c1e4 --- /dev/null +++ b/utils/GanacheBasicActionTest.py @@ -0,0 +1,172 @@ +#!/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 logging +import os +import signal +import subprocess +import sys +import time + +from typing import List +from web3 import Web3 + + +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') +PYHELPER_DIR = os.path.join(UTILS_DIR_PATH, 'PyEthHelper') +PROJECT_CONFIG_PATH = os.path.join(UTILS_DIR_PATH, 'project_conf.json') +CHECKSUM_KEYS_PATH = os.path.join(BUILD_DIR_PATH, 'ganache_keys_checksum.json') +GANACHE_KEYS_PATH = os.path.join(BUILD_DIR_PATH, 'ganache_keys.json') +GANACHE_PORT = 7545 +NUM_OF_ACCOUNTS = 100 +GANACHE_NET_ID = 1337 + + +sys.path.append(PYHELPER_DIR) +from PyEthHelper import EthContractHelper +from PyEthHelper import GanacheAccounts + + +def StartGanache() -> subprocess.Popen: + cmd = [ + 'ganache-cli', + '-p', str(GANACHE_PORT), + '-d', + '-a', str(NUM_OF_ACCOUNTS), + '--network-id', str(GANACHE_NET_ID), + '--chain.hardfork', 'shanghai', + '--wallet.accountKeysPath', str(GANACHE_KEYS_PATH), + ] + proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + return proc + + +def ReadEvalLogEvents(logs: List[dict]) -> None: + for log in logs: + if ( + (len(log['topics']) > 0) and + ( + log['topics'][0].hex() == + '0xe1ae46340cc3bb84afacee6678b86b538a6d5e4ce754adb3bfa9ce4e41d196ba' + ) + ): + hexData = log['data'].hex() + assert len(hexData) == 2 + (2 * (32 + 32)), 'Invalid log data length' + bData = bytes.fromhex(hexData[2:]) + idx = int.from_bytes(bData[:32], byteorder='big') + gasUsed = int.from_bytes(bData[32:], byteorder='big') + print(f'Evaluated action at index {idx} with gas used {gasUsed}') + + +def RunTests() -> dict: + # 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') + + # checksum keys + GanacheAccounts.ChecksumGanacheKeysFile( + CHECKSUM_KEYS_PATH, + GANACHE_KEYS_PATH + ) + + # setup account + privKey = EthContractHelper.SetupSendingAccount( + w3=w3, + account=0, + keyJson=CHECKSUM_KEYS_PATH + ) + + # deploy BasicActionGasCost contract + print('Deploying BasicActionGasCost contract...') + baContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='BasicActionGasCost', + release=None, # use locally built contract + address=None, # deploy new contract + ) + baReceipt = EthContractHelper.DeployContract( + w3=w3, + contract=baContract, + arguments=[ ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + baAddr = baReceipt.contractAddress + print('BasicActionGasCost contract deployed at {}'.format(baAddr)) + + # load deployed BasicActionGasCost contract + baContract = EthContractHelper.LoadContract( + w3=w3, + projConf=PROJECT_CONFIG_PATH, + contractName='BasicActionGasCost', + release=None, # use locally built contract + address=baAddr, # use deployed contract + ) + + evalTxReceipt = EthContractHelper.CallContractFunc( + w3=w3, + contract=baContract, + funcName='eval', + arguments=[ ], + privKey=privKey, + gas=None, # let web3 estimate + value=0, + confirmPrompt=False # don't prompt for confirmation + ) + ReadEvalLogEvents(evalTxReceipt.logs) + + +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(): + logging.basicConfig( + level=logging.DEBUG, + format='%(asctime)s %(levelname)s %(name)s %(message)s' + ) + + ganacheProc = StartGanache() + + try: + RunTests() + + finally: + # finish and exit + StopGanache(ganacheProc) + + +if __name__ == "__main__": + main() + diff --git a/utils/project_conf.json b/utils/project_conf.json index ff720d0..db4b4e6 100644 --- a/utils/project_conf.json +++ b/utils/project_conf.json @@ -3,7 +3,8 @@ "PubSubService" : "PubSub", "EventManager" : "PubSub", "HelloWorldPublisher" : "tests", - "HelloWorldSubscriber": "tests" + "HelloWorldSubscriber": "tests", + "BasicActionGasCost" : "tests" }, "releaseUrl": "https://github.com/lsd-ucsc/decent-pubsub-onchain/releases/download/{version}/{contract}", "buildDir" : "build"