diff --git a/integrations/evm/.gitignore b/integrations/evm/.gitignore new file mode 100644 index 00000000..7210993e --- /dev/null +++ b/integrations/evm/.gitignore @@ -0,0 +1,190 @@ +# Created by https://www.toptal.com/developers/gitignore/api/node + +# Edit at https://www.toptal.com/developers/gitignore?templates=node + +### Node + +# Logs + +logs +_.log +npm-debug.log_ +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) + +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# Runtime data + +pids +_.pid +_.seed +\*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover + +lib-cov + +# Coverage directory used by tools like istanbul + +coverage +\*.lcov + +# nyc test coverage + +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) + +.grunt + +# Bower dependency directory (https://bower.io/) + +bower_components + +# node-waf configuration + +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) + +build/Release + +# Dependency directories + +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) + +web_modules/ + +# TypeScript cache + +\*.tsbuildinfo + +# Optional npm cache directory + +.npm + +# Optional eslint cache + +.eslintcache + +# Optional stylelint cache + +.stylelintcache + +# Microbundle cache + +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history + +.node_repl_history + +# Output of 'npm pack' + +\*.tgz + +# Yarn Integrity file + +.yarn-integrity + +# dotenv environment variable files + +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) + +.cache +.parcel-cache + +# Next.js build output + +.next +out + +# Nuxt.js build / generate output + +.nuxt +dist + +# Gatsby files + +.cache/ + +# Comment in the public line in if your project uses Gatsby and not Next.js + +# https://nextjs.org/blog/next-9-1#public-directory-support + +# public + +# vuepress build output + +.vuepress/dist + +# vuepress v2.x temp and cache directory + +.temp + +# Docusaurus cache and generated files + +.docusaurus + +# Serverless directories + +.serverless/ + +# FuseBox cache + +.fusebox/ + +# DynamoDB Local files + +.dynamodb/ + +# TernJS port file + +.tern-port + +# Stores VSCode versions used for testing VSCode extensions + +.vscode-test + +# yarn v2 + +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.\* +.DS_Store/ + +### Node Patch + +# Serverless Webpack directories + +.webpack/ + +# Optional stylelint cache + +# SvelteKit build / generate output + +.svelte-kit +.idea +/common/types/client/ +/common/types/lcd/ + +# End of https://www.toptal.com/developers/gitignore/api/node \ No newline at end of file diff --git a/integrations/evm/LICENSE b/integrations/evm/LICENSE new file mode 100644 index 00000000..bf519b1c --- /dev/null +++ b/integrations/evm/LICENSE @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + +TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + +1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + +2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + +3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + +4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + +5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + +6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + +7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + +8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + +9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + +END OF TERMS AND CONDITIONS + +APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + +Copyright 2022 BCP Innovations UG (haftungsbeschränkt) + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. \ No newline at end of file diff --git a/integrations/evm/README.md b/integrations/evm/README.md new file mode 100644 index 00000000..5ab6c965 --- /dev/null +++ b/integrations/evm/README.md @@ -0,0 +1 @@ +# @kyvejs/evm \ No newline at end of file diff --git a/integrations/evm/package.json b/integrations/evm/package.json new file mode 100644 index 00000000..08aa27c6 --- /dev/null +++ b/integrations/evm/package.json @@ -0,0 +1,34 @@ +{ + "name": "@kyvejs/evm", + "version": "1.1.0-beta.1", + "license": "MIT", + "scripts": { + "build": "rimraf dist && tsc", + "build:binaries": "yarn build && rimraf out && pkg --no-bytecode --public-packages '*' --compress GZip --output out/kyve package.json && node ../../common/protocol/dist/src/scripts/checksum.js", + "start": "node ./dist/src/index.js", + "format": "prettier --write ." + }, + "bin": "./dist/src/index.js", + "pkg": { + "scripts": "./dist/src/index.js", + "targets": [ + "latest-linux-x64", + "latest-linux-arm64", + "latest-macos-x64" + ], + "outputPath": "out" + }, + "prettier": { + "singleQuote": true + }, + "dependencies": { + "@kyvejs/protocol": "1.1.7", + "ethers": "^5.6.5" + }, + "devDependencies": { + "pkg": "^5.8.0", + "prettier": "^2.7.1", + "rimraf": "^3.0.2", + "typescript": "^4.7.4" + } +} \ No newline at end of file diff --git a/integrations/evm/src/index.ts b/integrations/evm/src/index.ts new file mode 100644 index 00000000..447aff0d --- /dev/null +++ b/integrations/evm/src/index.ts @@ -0,0 +1,6 @@ +import { Validator } from '@kyvejs/protocol'; +import EVM from './runtime'; + +const runtime = new EVM(); + +new Validator(runtime).bootstrap(); \ No newline at end of file diff --git a/integrations/evm/src/runtime.ts b/integrations/evm/src/runtime.ts new file mode 100644 index 00000000..bcec5581 --- /dev/null +++ b/integrations/evm/src/runtime.ts @@ -0,0 +1,156 @@ +import { DataItem, IRuntime, Validator, VOTE } from "@kyvejs/protocol"; +import { name, version } from "../package.json"; +import { providers, utils } from "ethers"; +import { TransactionReceipt } from "@ethersproject/abstract-provider"; +import { createHashesFromBundle, generateMerkleRoot } from "../utils/merkle"; + +// EVM config +interface IConfig { + rpc: string; + finality: number; + includedData: { + blockWithTransactions: boolean, + blockReceipts: boolean, + transactionReceipts: boolean, + } +} + +export default class EVM implements IRuntime { + public name = name; + public version = version; + public config!: IConfig; + + async validateSetConfig(rawConfig: string): Promise { + const config: IConfig = JSON.parse(rawConfig); + + if (!config.rpc) { + throw new Error(`Config does not have property "rpc" defined`); + } + + if (process.env.KYVEJS_EVM_RPC) { + config.rpc = process.env.KYVEJS_EVM_RPC; + } + + if (!config.finality) { + throw new Error(`Config does not have finality defined`); + } + + if (!config.includedData?.blockWithTransactions && !config.includedData?.blockReceipts && !config.includedData?.transactionReceipts) { + throw new Error(`Config require included data`); + } + + if (!config.includedData?.blockReceipts && !config.includedData?.transactionReceipts) { + throw new Error(`Config can not include block receipts and transaction receipts at the same time`); + } + + this.config = config; + } + + async getDataItem(_: Validator, key: string): Promise { + let receipts: TransactionReceipt[] = []; + + const provider = new providers.StaticJsonRpcProvider({ + url: this.config.rpc, + }); + + const currentHeight = await provider.getBlockNumber(); + + const hexKey = utils.hexValue(+key); + + const block = await provider.getBlockWithTransactions(hexKey); + + // only validate if current height is already 'finalized' + if (block.number >= currentHeight - this.config.finality) { + throw new Error( + `Finality not reached yet; waiting for next block` + ) + } + // delete confirmations from transactions to keep data deterministic + block.transactions.forEach( + (tx: Partial) => { + delete tx.confirmations + } + ); + + if (this.config.includedData.blockReceipts) { + const receiptRequestData = { + method: 'eth_getBlockReceipts', + params: [hexKey], + id: 1, + jsonrpc: '2.0', + }; + + // retrieve all transaction receipts for the key + receipts = await provider.send(receiptRequestData.method, receiptRequestData.params) + } else if (this.config.includedData.transactionReceipts) { + for (const tx of block.transactions) { + // retrieve transaction receipt + const receiptRequestData = { + method: 'eth_getTransactionReceipt', + params: [tx.hash], + id: 1, + jsonrpc: '2.0', + }; + + const txReceipt = await provider.send(receiptRequestData.method, receiptRequestData.params); + receipts.push(txReceipt) + } + } + + let value: any = {} + + if (this.config.includedData.blockWithTransactions) { + value.block = block + } + + if (this.config.includedData.transactionReceipts) { + value.receipts = receipts + } else if (this.config.includedData.blockReceipts) { + value.receipts = receipts + } + + return { + key, + value: value, + }; + } + + + async prevalidateDataItem(_: Validator, item: DataItem): Promise { + // check if item value is not null + return !!item.value; + } + + async transformDataItem(_: Validator, item: DataItem): Promise { + // do not transform data item + return item; + } + + async validateDataItem( + _: Validator, + proposedDataItem: DataItem, + validationDataItem: DataItem + ): Promise { + // apply equal comparison + if ( + JSON.stringify(proposedDataItem) === JSON.stringify(validationDataItem) + ) { + return VOTE.VOTE_TYPE_VALID; + } + + return VOTE.VOTE_TYPE_INVALID; + } + + async summarizeDataBundle(_: Validator, bundle: DataItem[]): Promise { + const hashes = createHashesFromBundle(bundle); + const merkleRoot = generateMerkleRoot(hashes); + + return JSON.stringify({ + "merkle_root": merkleRoot + }) + } + + async nextKey(_: Validator, key: string): Promise { + return (parseInt(key) + 1).toString(); + } +} \ No newline at end of file diff --git a/integrations/evm/tsconfig.json b/integrations/evm/tsconfig.json new file mode 100644 index 00000000..d2db7767 --- /dev/null +++ b/integrations/evm/tsconfig.json @@ -0,0 +1,11 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "es2019", + "strict": true, + "outDir": "dist", + "esModuleInterop": true, + "resolveJsonModule": true + }, + "files": ["src/index.ts"] +} \ No newline at end of file diff --git a/integrations/evm/utils/merkle.ts b/integrations/evm/utils/merkle.ts new file mode 100644 index 00000000..de500199 --- /dev/null +++ b/integrations/evm/utils/merkle.ts @@ -0,0 +1,38 @@ +import * as crypto from '@cosmjs/crypto'; + +export function createHashesFromBundle(bundle: any[]): Uint8Array[] { + return bundle.map(dataItem => dataItemToSha256(dataItem)) +} + +function dataItemToSha256(data: any): Uint8Array { + // Encode the serialized object to UTF-8 + const encoded_obj: Uint8Array = Buffer.from(JSON.stringify(data), 'utf-8'); + // Calculate the SHA-256 hash + return crypto.sha256(encoded_obj) +} + +export function generateMerkleRoot(hashes: Uint8Array[]): string { + if (!hashes || hashes.length == 0) { + return ''; + } + + // Ensure number of hashes (leafs) are even by copying the + // last hash (the very right leaf) if the amount is odd + if (hashes.length % 2 !== 0) { + hashes.push(hashes[hashes.length - 1]); + } + + const combinedHashes: Uint8Array[] = []; + for(let i = 0; i < hashes.length; i += 2) { + const hashesConcatenated = new Uint8Array([ ...hashes[i], ...hashes[i+1]]) + const hash = crypto.sha256(hashesConcatenated); + combinedHashes.push(hash); + } + + // If the combinedHashes length is 1, it means that we have the merkle root already, + // and we can return the hex representation + if (combinedHashes.length === 1) { + return Buffer.from(combinedHashes[0]).toString('hex'); + } + return generateMerkleRoot(combinedHashes); +} \ No newline at end of file diff --git a/integrations/evm/utils/utils.ts b/integrations/evm/utils/utils.ts new file mode 100644 index 00000000..9c471210 --- /dev/null +++ b/integrations/evm/utils/utils.ts @@ -0,0 +1,46 @@ +export function shuffle(array: string[]): string[] { + for (let i = array.length - 1; i > 0; i--) { + const j = Math.floor(Math.random() * (i + 1)); + [array[i], array[j]] = [array[j], array[i]]; + } + return array; +} + +// Function to chunk array into groups +export function chunkArray(arr: any, chunkSize: any) { + const result = []; + for (let i = 0; i < arr.length; i += chunkSize) { + result.push(arr.slice(i, i + chunkSize)); + } + return result; +} + +export function removeLeadingZero(inputStr: string): string { + if (inputStr[2] === "0") { + return inputStr.slice(0, 2) + inputStr.slice(3); + } else { + return inputStr; + } +} + +export function removeOutputProperties(obj: any, removeKeys: string[]) { + if (!obj || typeof obj !== 'object') { + return obj; + } + + if (Array.isArray(obj)) { + for (let i = 0; i < obj.length; i++) { + obj[i] = removeOutputProperties(obj[i], removeKeys); + } + } else { + for (const key in obj) { + if (removeKeys.some(removeKey => key.includes(removeKey))) { + delete obj[key]; + } else if (typeof obj[key] === 'object' && obj[key] !== null) { + obj[key] = removeOutputProperties(obj[key], removeKeys); + } + } + } + + return obj; +} \ No newline at end of file