From 0fcc72172316b168a392c8e32ad1a9f3e01cd5d7 Mon Sep 17 00:00:00 2001 From: Atlas Date: Mon, 21 Oct 2024 00:43:41 +0200 Subject: [PATCH] feat: init code --- .github/workflows/test.yml | 45 ++++++++++++ .gitignore | 16 +++++ .gitmodules | 3 + README.md | 62 +++++++++++++++++ foundry.toml | 6 ++ lib/forge-std | 1 + package-lock.json | 35 ++++++++++ package.json | 6 ++ programs/main.py | 27 +++++++ programs/ovm/__init__.py | 11 +++ programs/ovm/bridge.py | 16 +++++ programs/ovm/bridge_pb2.py | 45 ++++++++++++ programs/ovm/bridge_pb2_grpc.py | 120 ++++++++++++++++++++++++++++++++ programs/requirements.txt | 12 ++++ src/Pi.sol | 107 ++++++++++++++++++++++++++++ test/Pi.t.sol | 25 +++++++ 16 files changed, 537 insertions(+) create mode 100644 .github/workflows/test.yml create mode 100644 .gitignore create mode 100644 .gitmodules create mode 100644 README.md create mode 100644 foundry.toml create mode 160000 lib/forge-std create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 programs/main.py create mode 100644 programs/ovm/__init__.py create mode 100644 programs/ovm/bridge.py create mode 100644 programs/ovm/bridge_pb2.py create mode 100644 programs/ovm/bridge_pb2_grpc.py create mode 100644 programs/requirements.txt create mode 100644 src/Pi.sol create mode 100644 test/Pi.t.sol diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml new file mode 100644 index 0000000..762a296 --- /dev/null +++ b/.github/workflows/test.yml @@ -0,0 +1,45 @@ +name: CI + +on: + push: + pull_request: + workflow_dispatch: + +env: + FOUNDRY_PROFILE: ci + +jobs: + check: + strategy: + fail-fast: true + + name: Foundry project + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + submodules: recursive + + - name: Install Foundry + uses: foundry-rs/foundry-toolchain@v1 + with: + version: nightly + + - name: Show Forge version + run: | + forge --version + + - name: Run Forge fmt + run: | + forge fmt --check + id: fmt + + - name: Run Forge build + run: | + forge build --sizes + id: build + + - name: Run Forge tests + run: | + forge test -vvv + id: test diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..36a800a --- /dev/null +++ b/.gitignore @@ -0,0 +1,16 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env + +node_modules/ \ No newline at end of file diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..888d42d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "lib/forge-std"] + path = lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/README.md b/README.md new file mode 100644 index 0000000..7fb8469 --- /dev/null +++ b/README.md @@ -0,0 +1,62 @@ +## Cal-Pi-On-Chain + +This is a demo repo using OVM contracts lib to calculate Pi onchain. + +`./prgrams` contains the python code for computing task. +`./src` is the main entry for the contract code. + +## Documentation + +https://book.getfoundry.sh/ + +## Usage + +### Build + +```shell +$ forge build +``` + +### Test + +```shell +$ forge test +``` + +### Format + +```shell +$ forge fmt +``` + +### Gas Snapshots + +```shell +$ forge snapshot +``` + +### Anvil + +```shell +$ anvil +``` + +### Deploy + +```shell +$ forge script script/Counter.s.sol:CounterScript --rpc-url --private-key +``` + +### Cast + +```shell +$ cast +``` + +### Help + +```shell +$ forge --help +$ anvil --help +$ cast --help +``` diff --git a/foundry.toml b/foundry.toml new file mode 100644 index 0000000..96be5b2 --- /dev/null +++ b/foundry.toml @@ -0,0 +1,6 @@ +[profile.default] +src = "src" +out = "out" +libs = ["node_modules","lib"] + +# See more config options https://github.com/foundry-rs/foundry/blob/master/crates/config/README.md#all-options diff --git a/lib/forge-std b/lib/forge-std new file mode 160000 index 0000000..8f24d6b --- /dev/null +++ b/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 8f24d6b04c92975e0795b5868aa0d783251cdeaa diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..dcddd18 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,35 @@ +{ + "name": "cal-pi-onchain", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "dependencies": { + "@openzeppelin/contracts": "^5.0.2", + "@webisopen/ovm-contracts": "^1.0.0" + } + }, + "node_modules/@openzeppelin/contracts": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.2.tgz", + "integrity": "sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==" + }, + "node_modules/@webisopen/ovm-contracts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@webisopen/ovm-contracts/-/ovm-contracts-1.0.0.tgz", + "integrity": "sha512-InMDuldzY3xwOXFOY5X1rOIxBQXGsMMw91ci0Ax/rZcGSC6XVHaqIZNiYj2F6ZScRS/81jTw6T6uVQCw40Kg/g==" + } + }, + "dependencies": { + "@openzeppelin/contracts": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/@openzeppelin/contracts/-/contracts-5.0.2.tgz", + "integrity": "sha512-ytPc6eLGcHHnapAZ9S+5qsdomhjo6QBHTDRRBFfTxXIpsicMhVPouPgmUPebZZZGX7vt9USA+Z+0M0dSVtSUEA==" + }, + "@webisopen/ovm-contracts": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@webisopen/ovm-contracts/-/ovm-contracts-1.0.0.tgz", + "integrity": "sha512-InMDuldzY3xwOXFOY5X1rOIxBQXGsMMw91ci0Ax/rZcGSC6XVHaqIZNiYj2F6ZScRS/81jTw6T6uVQCw40Kg/g==" + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..553e364 --- /dev/null +++ b/package.json @@ -0,0 +1,6 @@ +{ + "dependencies": { + "@webisopen/ovm-contracts": "^1.0.0", + "@openzeppelin/contracts": "^5.0.2" + } +} diff --git a/programs/main.py b/programs/main.py new file mode 100644 index 0000000..a396739 --- /dev/null +++ b/programs/main.py @@ -0,0 +1,27 @@ +import math +import sys +from typing import Union +import eth_abi +import ovm + +bridge = ovm.Bridge() + + +def cal_pi(n: int) -> Union[bool, str]: + if n > 15: + raise ValueError("n must be less than 15") + + return True, format(math.pi, f".{n}f") + + +def main(): + if len(sys.argv) != 2: + raise ValueError("missing messages argument") + + (input,) = eth_abi.decode(["int"], bytes.fromhex(sys.argv[1])) + + bridge.submit(["bool", "string"], [True, cal_pi(input)]) + + +if __name__ == "__main__": + main() diff --git a/programs/ovm/__init__.py b/programs/ovm/__init__.py new file mode 100644 index 0000000..78e2173 --- /dev/null +++ b/programs/ovm/__init__.py @@ -0,0 +1,11 @@ +import os +import sys + + +sys.path.append(os.path.dirname(os.path.abspath(__file__))) + +from . import bridge_pb2 +from . import bridge_pb2_grpc +from .bridge import Bridge + +__all__ = ["Bridge", "bridge_pb2", "bridge_pb2_grpc"] diff --git a/programs/ovm/bridge.py b/programs/ovm/bridge.py new file mode 100644 index 0000000..b7f1fe5 --- /dev/null +++ b/programs/ovm/bridge.py @@ -0,0 +1,16 @@ +from typing import Any, Iterable +import eth_abi +from eth_typing import TypeStr +import grpc +from . import bridge_pb2 +from . import bridge_pb2_grpc + +class Bridge: + def __init__(self): + self.bridge_client = bridge_pb2_grpc.BridgeStub(grpc.insecure_channel("unix:///var/run/ovm.sock")) + + def load(self): + return self.bridge_client.Load(bridge_pb2.LoadRequest()) + + def submit(self, abi: Iterable[TypeStr], values: Iterable[Any]): + return self.bridge_client.Submit(bridge_pb2.SubmitRequest(output=eth_abi.encode(abi, values).hex())) diff --git a/programs/ovm/bridge_pb2.py b/programs/ovm/bridge_pb2.py new file mode 100644 index 0000000..3894414 --- /dev/null +++ b/programs/ovm/bridge_pb2.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- +# Generated by the protocol buffer compiler. DO NOT EDIT! +# NO CHECKED-IN PROTOBUF GENCODE +# source: api/v1/ovm/bridge.proto +# Protobuf Python Version: 5.27.3 +"""Generated protocol buffer code.""" +from google.protobuf import descriptor as _descriptor +from google.protobuf import descriptor_pool as _descriptor_pool +from google.protobuf import runtime_version as _runtime_version +from google.protobuf import symbol_database as _symbol_database +from google.protobuf.internal import builder as _builder +_runtime_version.ValidateProtobufRuntimeVersion( + _runtime_version.Domain.PUBLIC, + 5, + 27, + 3, + '', + 'api/v1/ovm/bridge.proto' +) +# @@protoc_insertion_point(imports) + +_sym_db = _symbol_database.Default() + + + + +DESCRIPTOR = _descriptor_pool.Default().AddSerializedFile(b'\n\x17\x61pi/v1/ovm/bridge.proto\x12\x03ovm\"\r\n\x0bLoadRequest\"$\n\x0cLoadResponse\x12\x14\n\x05input\x18\x02 \x01(\tR\x05input\"\'\n\rSubmitRequest\x12\x16\n\x06output\x18\x01 \x01(\tR\x06output\"\x10\n\x0eSubmitResponse2l\n\x06\x42ridge\x12-\n\x04Load\x12\x10.ovm.LoadRequest\x1a\x11.ovm.LoadResponse\"\x00\x12\x33\n\x06Submit\x12\x12.ovm.SubmitRequest\x1a\x13.ovm.SubmitResponse\"\x00\x42Q\n\x07\x63om.ovmB\x0b\x42ridgeProtoP\x01Z\rgo/api/v1/ovm\xa2\x02\x03OXX\xaa\x02\x03Ovm\xca\x02\x03Ovm\xe2\x02\x0fOvm\\GPBMetadata\xea\x02\x03Ovmb\x06proto3') + +_globals = globals() +_builder.BuildMessageAndEnumDescriptors(DESCRIPTOR, _globals) +_builder.BuildTopDescriptorsAndMessages(DESCRIPTOR, 'api.v1.ovm.bridge_pb2', _globals) +if not _descriptor._USE_C_DESCRIPTORS: + _globals['DESCRIPTOR']._loaded_options = None + _globals['DESCRIPTOR']._serialized_options = b'\n\007com.ovmB\013BridgeProtoP\001Z\rgo/api/v1/ovm\242\002\003OXX\252\002\003Ovm\312\002\003Ovm\342\002\017Ovm\\GPBMetadata\352\002\003Ovm' + _globals['_LOADREQUEST']._serialized_start=32 + _globals['_LOADREQUEST']._serialized_end=45 + _globals['_LOADRESPONSE']._serialized_start=47 + _globals['_LOADRESPONSE']._serialized_end=83 + _globals['_SUBMITREQUEST']._serialized_start=85 + _globals['_SUBMITREQUEST']._serialized_end=124 + _globals['_SUBMITRESPONSE']._serialized_start=126 + _globals['_SUBMITRESPONSE']._serialized_end=142 + _globals['_BRIDGE']._serialized_start=144 + _globals['_BRIDGE']._serialized_end=252 +# @@protoc_insertion_point(module_scope) diff --git a/programs/ovm/bridge_pb2_grpc.py b/programs/ovm/bridge_pb2_grpc.py new file mode 100644 index 0000000..90f213e --- /dev/null +++ b/programs/ovm/bridge_pb2_grpc.py @@ -0,0 +1,120 @@ +# Generated by the gRPC Python protocol compiler plugin. DO NOT EDIT! +"""Client and server classes corresponding to protobuf-defined services.""" +import grpc + +import bridge_pb2 as api_dot_v1_dot_ovm_dot_bridge__pb2 + + +class BridgeStub(object): + """Missing associated documentation comment in .proto file.""" + + def __init__(self, channel): + """Constructor. + + Args: + channel: A grpc.Channel. + """ + self.Load = channel.unary_unary( + '/ovm.Bridge/Load', + request_serializer=api_dot_v1_dot_ovm_dot_bridge__pb2.LoadRequest.SerializeToString, + response_deserializer=api_dot_v1_dot_ovm_dot_bridge__pb2.LoadResponse.FromString, + _registered_method=True) + self.Submit = channel.unary_unary( + '/ovm.Bridge/Submit', + request_serializer=api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitRequest.SerializeToString, + response_deserializer=api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitResponse.FromString, + _registered_method=True) + + +class BridgeServicer(object): + """Missing associated documentation comment in .proto file.""" + + def Load(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + def Submit(self, request, context): + """Missing associated documentation comment in .proto file.""" + context.set_code(grpc.StatusCode.UNIMPLEMENTED) + context.set_details('Method not implemented!') + raise NotImplementedError('Method not implemented!') + + +def add_BridgeServicer_to_server(servicer, server): + rpc_method_handlers = { + 'Load': grpc.unary_unary_rpc_method_handler( + servicer.Load, + request_deserializer=api_dot_v1_dot_ovm_dot_bridge__pb2.LoadRequest.FromString, + response_serializer=api_dot_v1_dot_ovm_dot_bridge__pb2.LoadResponse.SerializeToString, + ), + 'Submit': grpc.unary_unary_rpc_method_handler( + servicer.Submit, + request_deserializer=api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitRequest.FromString, + response_serializer=api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitResponse.SerializeToString, + ), + } + generic_handler = grpc.method_handlers_generic_handler( + 'ovm.Bridge', rpc_method_handlers) + server.add_generic_rpc_handlers((generic_handler,)) + server.add_registered_method_handlers('ovm.Bridge', rpc_method_handlers) + + + # This class is part of an EXPERIMENTAL API. +class Bridge(object): + """Missing associated documentation comment in .proto file.""" + + @staticmethod + def Load(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/ovm.Bridge/Load', + api_dot_v1_dot_ovm_dot_bridge__pb2.LoadRequest.SerializeToString, + api_dot_v1_dot_ovm_dot_bridge__pb2.LoadResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) + + @staticmethod + def Submit(request, + target, + options=(), + channel_credentials=None, + call_credentials=None, + insecure=False, + compression=None, + wait_for_ready=None, + timeout=None, + metadata=None): + return grpc.experimental.unary_unary( + request, + target, + '/ovm.Bridge/Submit', + api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitRequest.SerializeToString, + api_dot_v1_dot_ovm_dot_bridge__pb2.SubmitResponse.FromString, + options, + channel_credentials, + insecure, + call_credentials, + compression, + wait_for_ready, + timeout, + metadata, + _registered_method=True) diff --git a/programs/requirements.txt b/programs/requirements.txt new file mode 100644 index 0000000..56cbe21 --- /dev/null +++ b/programs/requirements.txt @@ -0,0 +1,12 @@ +cytoolz==1.0.0 +eth-hash==0.7.0 +eth-typing==5.0.0 +eth-utils==5.0.0 +eth_abi==5.1.0 +grpcio==1.66.2 +hexbytes==1.2.1 +parsimonious==0.10.0 +protobuf==5.28.2 +regex==2024.9.11 +toolz==1.0.0 +typing_extensions==4.12.2 diff --git a/src/Pi.sol b/src/Pi.sol new file mode 100644 index 0000000..eabaa5f --- /dev/null +++ b/src/Pi.sol @@ -0,0 +1,107 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.13; +import {OVMClient} from "@webisopen/ovm-contracts/src/OVMClient.sol"; +import {ExecMode, Requirement, Specification} from "@webisopen/ovm-contracts/src/libraries/DataTypes.sol"; + +event ResponseParsed(bytes32 requestId, bool success, string strPI); + +contract Pi is OVMClient { + bool public constant REQ_DETERMINISTIC = true; + + mapping(bytes32 requestId => string _strPI) internal _responseData; + + /** + * @dev Constructor function for the PI contract. + * @param OVMTaskAddress The address of the OVMTask contract. + * @param admin The address of the admin. + */ + constructor( + address OVMTaskAddress, + address admin + ) OVMClient(OVMTaskAddress, admin) { + // set specification + Specification memory spec; + spec.name = "kallypi"; + spec.version = "1.0.0"; + spec.description = "Calculate PI"; + spec.environment = "python:3.7"; + spec.repository = "https://github.com/kallydev/kallypi"; + spec + .repoTag = "0xb6a6502fa480fd1fb5bf95c1fb1366bcbc335a08356c2a97daf6bc44e9cc0253"; + spec.license = "WTFPL"; + spec.entrypoint = "src/main.py"; + spec.requirement = Requirement({ + ram: "256mb", + disk: "5mb", + timeout: 600, + cpu: 1, + gpu: false + }); + spec + .apiABIs = '[{"request":{"type":"function","name":"getResponse","inputs":[{"name":"requestId","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"},"getResponse":{"type":"function","name":"getResponse","inputs":[{"name":"requestId","type":"bytes32","internalType":"bytes32"}],"outputs":[{"name":"","type":"string","internalType":"string"}],"stateMutability":"view"}}]'; + spec.royalty = 5; + spec.execMode = ExecMode.JIT; + + _updateSpecification(spec); + } + + /** + * @dev Sends a request to calculate the value of PI with a specified number of digits. + * @param numDigits The number of digits to calculate for PI. + * @return requestId The ID of the request returned by the OVMTasks contract. + */ + function sendRequest( + uint256 numDigits + ) external payable returns (bytes32 requestId) { + // encode the data + bytes memory data = abi.encode(numDigits); + requestId = _sendRequest( + msg.sender, + msg.value, + REQ_DETERMINISTIC, + data + ); + } + + /** + * @dev Sets the response data for a specific request. This function is called by the OVMTasks + * contract. + * @param requestId The ID of the request. + * @param data The response data to be set. + */ + function setResponse( + bytes32 requestId, + bytes calldata data + ) external override recordResponse(requestId) onlyOVMTask { + // parse and save the data fulfilled by the OVMTasks contract + (bool success, string memory strPI) = _parseData(data); + if (success) { + _responseData[requestId] = strPI; + } + + emit ResponseParsed(requestId, success, strPI); + } + + /** + * @dev Retrieves the response associated with the given request ID. + * @param requestId The ID of the request. + * @return The response data as a string in our pi calculation case. + */ + function getResponse( + bytes32 requestId + ) external view returns (string memory) { + return _responseData[requestId]; + } + + /** + * @dev Parses the given data and returns a boolean value and a string. + * @param data The input data to be parsed. + * @return A tuple containing a boolean value indicating the success of the task execution + * and a string representing the parsed data. + */ + function _parseData( + bytes calldata data + ) internal pure returns (bool, string memory) { + return abi.decode(data, (bool, string)); + } +} diff --git a/test/Pi.t.sol b/test/Pi.t.sol new file mode 100644 index 0000000..da176fd --- /dev/null +++ b/test/Pi.t.sol @@ -0,0 +1,25 @@ +pragma solidity 0.8.24; + +import {Test} from "forge-std/Test.sol"; +import {Pi, ResponseParsed} from "../src/Pi.sol"; + +contract PiTest is Test { + address public constant alice = address(0x1111); + address public constant mockTask = address(0x1234abcd); + Pi public pi; + + function setUp() public { + pi = new Pi(mockTask, alice); + } + + function testSetResponse() public { + bytes memory mockData = abi.encode(true, "3.14159"); + vm.prank(mockTask); + vm.expectEmit(); + emit ResponseParsed("0x1234", true, "3.14159"); + pi.setResponse("0x1234", mockData); + + string memory strPI = pi.getResponse("0x1234"); + vm.assertEq(strPI, "3.14159"); + } +}