Repository: clearmatics/ion Branch: master Commit: 89ec6142639a Files: 55 Total size: 297.1 KB Directory structure: gitextract_wxdcfq0s/ ├── .dockerignore ├── .github/ │ ├── ISSUE_TEMPLATE.md │ ├── PULL_REQUEST_TEMPLATE.md │ └── workflows/ │ ├── ci.yaml │ └── stale.yaml ├── .gitignore ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── Dockerfile ├── LICENSE ├── README.md ├── contracts/ │ ├── EventVerifier.sol │ ├── Ion.sol │ ├── IonCompatible.sol │ ├── Migrations.sol │ ├── functional/ │ │ ├── FabricFunction.sol │ │ ├── Function.sol │ │ ├── Trigger.sol │ │ └── TriggerEventVerifier.sol │ ├── libraries/ │ │ ├── ECVerify.sol │ │ ├── PatriciaTrie.sol │ │ ├── RLP.sol │ │ ├── SafeMath.sol │ │ └── SolidityUtils.sol │ ├── mock/ │ │ ├── MockIon.sol │ │ ├── MockStorage.sol │ │ └── MockValidation.sol │ ├── storage/ │ │ ├── BlockStore.sol │ │ ├── EthereumStore.sol │ │ └── FabricStore.sol │ ├── test/ │ │ └── PatriciaTrieTest.sol │ └── validation/ │ ├── Base.sol │ ├── Clique.sol │ └── IBFT.sol ├── docker_build/ │ ├── account/ │ │ ├── keystore/ │ │ │ └── UTC--2018-06-05T09-31-57.109288703Z--2be5ab0e43b6dc2908d5321cf318f35b80d0c10d │ │ └── password-2be5ab0e43b6dc2908d5321cf318f35b80d0c10d.txt │ ├── clique.json │ ├── launch_geth.sh │ └── password ├── docs/ │ ├── Ion-CLI.md │ └── Roadmap.md ├── migrations/ │ ├── 1_initial_migration.js │ └── 2_deploy_contracts.js ├── package.json ├── test/ │ ├── clique.js │ ├── helpers/ │ │ ├── encoder.js │ │ └── utils.js │ ├── ibft.js │ ├── integration-base_fabric.js │ ├── integration-clique_ethereum.js │ ├── ion.js │ ├── patricia_trie_test.js │ ├── storage-ethereum.js │ └── storage-fabric.js └── truffle.js ================================================ FILE CONTENTS ================================================ ================================================ FILE: .dockerignore ================================================ node_modules ================================================ FILE: .github/ISSUE_TEMPLATE.md ================================================ As a user I want So that ### Acceptance criteria - - - ### Related issues - - - ================================================ FILE: .github/PULL_REQUEST_TEMPLATE.md ================================================ ### Description: ### All Submissions: * [ ] Have you followed the guidelines in our [Contributing document](CONTRIBUTING.md)? * [ ] Have you checked to ensure there aren't other open [Pull Requests](../../../pulls) for the same update/change? * [ ] Have you signed your commits? * [ ] Does your submission pass tests? * [ ] Have you linted your code locally prior to submission? ================================================ FILE: .github/workflows/ci.yaml ================================================ on: ["push", "pull_request"] name: "Continuous Integration" jobs: build: runs-on: ubuntu-latest strategy: matrix: node-version: [10.x] steps: - uses: actions/checkout@v2 - name: Cache node modules uses: actions/cache@v1 with: path: ~/.npm key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} restore-keys: | ${{ runner.os }}-node- - name: Use Node.js ${{ matrix.node-version }} uses: actions/setup-node@v1 with: node-version: ${{ matrix.node-version }} - run: npm install - run: npm run testrpc & - run: npm test env: CI: true ================================================ FILE: .github/workflows/stale.yaml ================================================ name: "Close stale issues" on: schedule: - cron: "0 0 * * *" jobs: stale: runs-on: ubuntu-latest steps: - uses: actions/stale@v1 with: repo-token: ${{ secrets.GITHUB_TOKEN }} stale-issue-message: 'This issue is stale because it has been open 30 days with no activity. Remove stale label or comment or this will be closed in 5 days' days-before-stale: 30 days-before-close: 5 ================================================ FILE: .gitignore ================================================ /build /abi /dist /chaindata /node_modules /coverage /.idea coverage.json *.pyc docs/deps-files.dot docs/deps-modules.dot *.log *.pdf *.egg-info/ ================================================ FILE: CODE_OF_CONDUCT.md ================================================ # Contributor Covenant Code of Conduct ## Our Pledge In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation. ## Our Standards Examples of behavior that contributes to creating a positive environment include: * Using welcoming and inclusive language * Being respectful of differing viewpoints and experiences * Gracefully accepting constructive criticism * Focusing on what is best for the community * Showing empathy towards other community members Examples of unacceptable behavior by participants include: * The use of sexualized language or imagery and unwelcome sexual attention or advances * Trolling, insulting/derogatory comments, and personal or political attacks * Public or private harassment * Publishing others' private information, such as a physical or electronic address, without explicit permission * Other conduct which could reasonably be considered inappropriate in a professional setting ## Our Responsibilities Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior. Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful. ## Scope This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers. ## Enforcement Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at [opensource@clearmatics.com][email]. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately. Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership. ## Attribution This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version] [email]: mailto:opensource@clearmatics.com [homepage]: http://contributor-covenant.org [version]: http://contributor-covenant.org/version/1/4/ ================================================ FILE: CONTRIBUTING.md ================================================ # Contributing When contributing to this repository, please first discuss the change you wish to make via issue, email, or any other method with the owners of this repository before making a change. Please note we have a code of conduct, please follow it in all your interactions with the project. ## Pull Request Process 1. Ensure any install or build dependencies are removed before the end of the layer when doing a build. 2. Update the README.md with details of changes to the interface, this includes new environment variables, exposed ports, useful file locations and container parameters. 3. Increase the version numbers in any examples files and the README.md to the new version that this Pull Request would represent. The versioning scheme we use is [SemVer][semver]. 4. You may merge the Pull Request in once you have the sign-off of two other developers, or if you do not have permission to do that, you may request the second reviewer to merge it for you. ## Code of Conduct This project and everyone participating in it is governed by the [Code of Conduct][codeofconduct]. By participating, you are expected to uphold this code. Please report unacceptable behavior to [opensource@clearmatics.com][email]. [codeofconduct]: CODE_OF_CONDUCT.md [semver]: http://semver.org/ [email]: mailto:opensource@clearmatics.com ================================================ FILE: Dockerfile ================================================ FROM golang:1.8 RUN printf "deb http://archive.debian.org/debian/ jessie main\ndeb-src http://archive.debian.org/debian/ jessie main\ndeb http://security.debian.org jessie/updates main\ndeb-src http://security.debian.org jessie/updates main" > /etc/apt/sources.list RUN apt-get update && apt-get install -y \ vim \ curl \ sudo \ wget # Install a recent version of nodejs RUN curl -sL https://deb.nodesource.com/setup_10.x | sudo bash - && sudo apt-get install -y nodejs COPY . /go/src/github.com/clearmatics/ion # Install the current compatible solc version RUN wget https://github.com/ethereum/solidity/releases/download/v0.4.25/solc-static-linux -O solc RUN chmod +x ./solc RUN cp ./solc /go/src/github.com/clearmatics/ion ENV PATH $PATH:/go/src/github.com/clearmatics/ion WORKDIR /go/src/github.com/clearmatics/ion CMD ["/bin/bash"] ================================================ FILE: LICENSE ================================================ GNU LESSER GENERAL PUBLIC LICENSE Version 3, 29 June 2007 Copyright (C) 2007 Free Software Foundation, Inc. Everyone is permitted to copy and distribute verbatim copies of this license document, but changing it is not allowed. This version of the GNU Lesser General Public License incorporates the terms and conditions of version 3 of the GNU General Public License, supplemented by the additional permissions listed below. 0. Additional Definitions. As used herein, "this License" refers to version 3 of the GNU Lesser General Public License, and the "GNU GPL" refers to version 3 of the GNU General Public License. "The Library" refers to a covered work governed by this License, other than an Application or a Combined Work as defined below. An "Application" is any work that makes use of an interface provided by the Library, but which is not otherwise based on the Library. Defining a subclass of a class defined by the Library is deemed a mode of using an interface provided by the Library. A "Combined Work" is a work produced by combining or linking an Application with the Library. The particular version of the Library with which the Combined Work was made is also called the "Linked Version". The "Minimal Corresponding Source" for a Combined Work means the Corresponding Source for the Combined Work, excluding any source code for portions of the Combined Work that, considered in isolation, are based on the Application, and not on the Linked Version. The "Corresponding Application Code" for a Combined Work means the object code and/or source code for the Application, including any data and utility programs needed for reproducing the Combined Work from the Application, but excluding the System Libraries of the Combined Work. 1. Exception to Section 3 of the GNU GPL. You may convey a covered work under sections 3 and 4 of this License without being bound by section 3 of the GNU GPL. 2. Conveying Modified Versions. If you modify a copy of the Library, and, in your modifications, a facility refers to a function or data to be supplied by an Application that uses the facility (other than as an argument passed when the facility is invoked), then you may convey a copy of the modified version: a) under this License, provided that you make a good faith effort to ensure that, in the event an Application does not supply the function or data, the facility still operates, and performs whatever part of its purpose remains meaningful, or b) under the GNU GPL, with none of the additional permissions of this License applicable to that copy. 3. Object Code Incorporating Material from Library Header Files. The object code form of an Application may incorporate material from a header file that is part of the Library. You may convey such object code under terms of your choice, provided that, if the incorporated material is not limited to numerical parameters, data structure layouts and accessors, or small macros, inline functions and templates (ten or fewer lines in length), you do both of the following: a) Give prominent notice with each copy of the object code that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the object code with a copy of the GNU GPL and this license document. 4. Combined Works. You may convey a Combined Work under terms of your choice that, taken together, effectively do not restrict modification of the portions of the Library contained in the Combined Work and reverse engineering for debugging such modifications, if you also do each of the following: a) Give prominent notice with each copy of the Combined Work that the Library is used in it and that the Library and its use are covered by this License. b) Accompany the Combined Work with a copy of the GNU GPL and this license document. c) For a Combined Work that displays copyright notices during execution, include the copyright notice for the Library among these notices, as well as a reference directing the user to the copies of the GNU GPL and this license document. d) Do one of the following: 0) Convey the Minimal Corresponding Source under the terms of this License, and the Corresponding Application Code in a form suitable for, and under terms that permit, the user to recombine or relink the Application with a modified version of the Linked Version to produce a modified Combined Work, in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source. 1) Use a suitable shared library mechanism for linking with the Library. A suitable mechanism is one that (a) uses at run time a copy of the Library already present on the user's computer system, and (b) will operate properly with a modified version of the Library that is interface-compatible with the Linked Version. e) Provide Installation Information, but only if you would otherwise be required to provide such information under section 6 of the GNU GPL, and only to the extent that such information is necessary to install and execute a modified version of the Combined Work produced by recombining or relinking the Application with a modified version of the Linked Version. (If you use option 4d0, the Installation Information must accompany the Minimal Corresponding Source and Corresponding Application Code. If you use option 4d1, you must provide the Installation Information in the manner specified by section 6 of the GNU GPL for conveying Corresponding Source.) 5. Combined Libraries. You may place library facilities that are a work based on the Library side by side in a single library together with other library facilities that are not Applications and are not covered by this License, and convey such a combined library under terms of your choice, if you do both of the following: a) Accompany the combined library with a copy of the same work based on the Library, uncombined with any other library facilities, conveyed under the terms of this License. b) Give prominent notice with the combined library that part of it is a work based on the Library, and explaining where to find the accompanying uncombined form of the same work. 6. Revised Versions of the GNU Lesser General Public License. The Free Software Foundation may publish revised and/or new versions of the GNU Lesser General Public License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. Each version is given a distinguishing version number. If the Library as you received it specifies that a certain numbered version of the GNU Lesser General Public License "or any later version" applies to it, you have the option of following the terms and conditions either of that published version or of any later version published by the Free Software Foundation. If the Library as you received it does not specify a version number of the GNU Lesser General Public License, you may choose any version of the GNU Lesser General Public License ever published by the Free Software Foundation. If the Library as you received it specifies that a proxy can decide whether future versions of the GNU Lesser General Public License shall apply, that proxy's public statement of acceptance of any version is permanent authorization for you to choose that version for the Library. ================================================ FILE: README.md ================================================ # Ion Interoperability Framework ![Ion Version](https://img.shields.io/badge/ion-v2.2.0-brightgreen.svg) [![Build Status](https://travis-ci.org/clearmatics/ion.svg?branch=master)](https://travis-ci.org/clearmatics/ion) [![Solidity Version](https://img.shields.io/badge/solidity-v0.5.12-blue.svg)](https://solidity.readthedocs.io/en/v0.5.12/installing-solidity.html) [![LGPLv3](https://img.shields.io/badge/license-LGPL%20v3-brightgreen.svg)](./LICENSE) [![Gitter](https://img.shields.io/badge/%E2%8A%AA%20GITTER%20-JOIN%20CHAT%20%E2%86%92-orange.svg)](https://gitter.im/clearmatics/ion) The Ion Interoperability Framework is a library that provides an interface for the development of general cross-chain smart contracts. It is part of [Clearmatics'](http://clearmatics.com) http://autonity.io project. ## Introduction We strive towards a more interconnected fabric of systems, and to this end, methods for inter-system and cross-chain communications become paramount in facilitating this fluid ecosystem. Ion is a system and function-agnostic framework for building cross-interacting smart contracts between blockchains and/or systems. It does not restrict itself to certain methods of interoperation and is not opinionated on what specific functions it should be built for and as such is an open protocol. Atomic swaps and decentralised exchanges can be built on Ion and facilitate the free movement of value across different blockchains. These are just two of the possible use-cases that can be developed on top of the Ion framework. We envision Ion to evolve to become a library of tools that developers can use on any system to build cross-chain smart contracts to interoperate with any other system. ### Contents * [Getting Started](#getting-started) * [Interoperate with Rinkeby!](#interoperate-with-rinkeby) * [Ethereum to Rinkeby](#ethereum-to-rinkeby) * [Hyperledger Fabric to Rinkeby](#hyperledger-fabric-to-rinkeby) * [Develop on Ion](#develop-on-ion) * [Ethereum to Ethereum Interface](#ethereum-to-ethereum-interface) * [Hyperledger Fabric to Ethereum Interface](#hyperledger-fabric-to-ethereum-interface) * [Ion CLI](#ion-cli) * [Contribute!](#contribute) * [Ionosphere](#ionosphere) ## Getting Started Clone the repository and ensure that all the components work out of the box. ### With docker ``` docker build -t ion/dev . docker run -ti --name ion ion/dev # To run through the following test example you will need a separate terminal window docker exec -ti ion /bin/bash ``` ### Without docker The following minimum versions of `node` and `golang` are required. * [`nodejs`](https://nodejs.org/en/) v10.15.0 * [`golang`](https://golang.org/) 1.8 --- Run: ``` $ npm install ``` ``` $ npm run testrpc ``` ``` $ npm run test ``` to test the full stack of contracts including our example flow. The tests should pass as below: ``` ... ✓ Successful Add Block (546ms) ✓ Fail Add Block from unregistered chain ✓ Fail Add Block from non-ion (44ms) ✓ Fail Add Block with malformed data (70ms) ✓ Fail Add Same Block Twice (644ms) 74 passing (40s) ``` With that you've just interoperated your test RPC client with the Rinkeby testnet! Our repository includes some example contracts that show you how to build smart contracts that interoperate with another chain and what mechanism that looks like. We'll now use these example contracts to show you exactly how interoperation with Rinkeby looks like. ## Interoperate with Rinkeby! This is a quick tutorial using our example contracts included to be able to verify a state transition in a block and call a function that depends on it. We'll demonstrate that you can use the following instructions below to interoperate from the listed systems with Rinkeby. On the Rinkeby test network, we've already deployed a contract and executed a transaction there `Trigger.sol`. The example we will run you through will attempt to interact with that transaction by proving that it occurred on that chain and use the transaction data in a subsequent 'interactive' transaction on a local network. The transaction on Rinkeby has called the `fire()` function of the `Trigger.sol` contract, which emits an event containing the address of the caller. We will attempt to use the caller address by extracting it from the Rinkeby block using merkle proofs. ### Ethereum to Rinkeby We've already deployed some contracts to the Rinkeby test network for you to play around with! Ion: `0x3c70A876808ae953917ddf9d95f364614a59B941` Clique: `0x07a435c7b9df1F331505DdC05165473BEBeAFCdB` Ethereum Block Store: `0xe812064CCA52B42F6C1D5345Bc40fb0683eAfF15` We will deploy our own instance of the `Function.sol` contract and pass proofs to verify a transaction that we will depend on in order to execute a function in the contract. If the proofs verify correctly then the function should emit an event to indicate that it has been executed. Procedure: 1. We'll need the CLI here, if you are using the docker container build the CLI with `cd ion-cli/ && make build` else follow instructions to build the CLI [here](./ion-cli/). 2. `./ion-cli` Starts the CLI 3. `>>> connectToClient https://rinkeby.infura.io` Connect to the Rinkeby Testnet 4. `>>> addAccount me ./keystore/UTC--2018-11-14T13-34-31.599642840Z--b8844cf76df596e746f360957aa3af954ef51605` Add an account to be signing transactions with. We've included one that already has Rinkeby ETH for you :) Password to the keystore is `test`. If you arrived late to the party and there is no ETH left, tough luck, try creating your own account and requesting ETH from a faucet. Alternatively you can run this exact thread of commands on a `ganache-cli` instance but make sure you connect to the correct endpoint in step 2. 5. `>>> addContractInstance function /absolute/path/to/contracts/functional/Function.sol` Add your functional contract instance which compiles your contract. Must be passed an absolute path. 6. `>>> deployContract function me 1000000` Deploy your contract to Rinkeby! This will return an address that the contract is deployed at if successful. This contract has a constructor that requires two parameters to be supplied when prompted: * `_storeAddr`: `0xe812064CCA52B42F6C1D5345Bc40fb0683eAfF15` * `_verifierAddr`: `0xf973eB920fDB5897d79394F2e49430dCB9aA4ea1` 7. `>>> transactionMessage function verifyAndExecute me 0 1000000` Call the function. This requires you to supply the deployed contract instance address. Here you will need to supply the following data as an input to the function when prompted: * `_chainId`: `0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177` * `_blockHash`: `0x6e13edb9c701353743106de578730b3191d344a05c2e40cfd747bedc912f12cc` * `_contractEmittedAddress`: `0xA2e4a61a3D2ce626Ba9B3e927cfFDB0e4E0bd06d` * `_proof`: 0xf907430af86806843b9aca00830186a094a2e4a61a3d2ce626ba9b3e927cffdb0e4e0bd06d8084457094cc1ca082c3adf1cb22c7260686fd56ea8dc66fbb95717f2e02e4c3702e329fbd57cdada0552e7f3ef6e9f932e08acc7cc15ea6a92d7c11b8ac4a4c0765ce446b3415915ff90236f851a0fb63b3ee11a1ae3b9eb765d44ff793bea3f4dcc1b3851d3d499abeb7858675a180808080808080a07768d7f0c5cf3656a1b885a44f42aa9ac25e728a0ffd42064387f806a9d4c26b8080808080808080f9017180a01766dce2b77f929553bdb672e197ac6bd7a6e1af3bd6631a4761a0f9303de264a011691ee69c053c698656fd0d1d9109fdff8f54c555892e9a7595d9e4b13cdc81a034825307fc63fa81e00dfdb9567cbe216930cc54152008fec1c1a3c0cead50eba03b07413394305087c802b1b33ba2f4adee8ef781e9c44faa0024cfcd8bec8818a04b39fd7ee87b2ca8d0445c7c14e83e281b45df8ee0717e47ddd1586be6aa27aaa06eae234d88c3b3f1dd0696681dde9e3369bdf03b6f56d04ec57c97e65b8e4ae1a0ade94ca299f8a1effdc75566e92fd728c79e856dbaf1a1536e7cf650eb952a4da0c963e18570b2c6c64c47cb4b4ee0bc66b2d7c709585a096abdc22c82665e67b4a0b976edf4c20ccb46e24370ce40d056fa4b30b37f052095cf8e6826ea520376eca024b45b59e4246a74e0a532a172ae6eda1ab7f9f7575389ebcb49f0b3730bc8e2a07e55916b539f96fff5cb000dd364526fb29f46895ca69c6bebb62f200447cec88080808080f86d20b86af86806843b9aca00830186a094a2e4a61a3d2ce626ba9b3e927cffdb0e4e0bd06d8084457094cc1ca082c3adf1cb22c7260686fd56ea8dc66fbb95717f2e02e4c3702e329fbd57cdada0552e7f3ef6e9f932e08acc7cc15ea6a92d7c11b8ac4a4c0765ce446b3415915ff901640183122a96b9010000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020080000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af85894a2e4a61a3d2ce626ba9b3e927cffdb0e4e0bd06de1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000b8844cf76df596e746f360957aa3af954ef51605f90335f851a08e337e40227c1f29eb9be66dcce5738b0ebd7e3fb8f22f3bde4c2c562c47a3e980808080808080a02285cf183e1144e639fd557c61b8f7c9a3c407ea44d900819c24e0bdd043e8c28080808080808080f9017180a072146023fa33354e1aea072971ed85a27934706b44aa09cf1caf7f3fce3f53c4a02843057917ddf23376e770bcadde52ebbddea15eff5cdee7ed48e3b1705105c6a005d7e4bee60ad1bb393c7747e4770c0967abe35d0fe711ed44653665e1ba77d6a004eca1f6f4ca52e78956f277aba13ee78c408d065c12d15094eb9bbcc4d8fc19a09bd8a88adc645ca6cfef48ab9342e0c08e001a5b06c8a075f87795e7d4397152a01bb09aa9e8d5d2c86672a3f2ba2ebb57d82cc02a871097fa144ff6686be301b1a0fad6ed01fdfa90d8568187f3aa23cdb09a512a0438d38dd6fb665e0ffdb78f31a0fa1dd46146f1740adf146d8560ad67d5d99b881d459c6e194abdbc333ef43393a0768dae614a1f5dc5d2d5a5618065c001e808d28bea10b1ae7e8ab1f3aab8bf4aa08bf7adbbd243a63deffa64afd7e90aab7b58311ef7b4c4a824e9fb611786444ca0a93de51ab6adb387212faa7880788c217d2d56cf5baf60c72ca9ec11947afdbf8080808080f9016b20b90167f901640183122a96b9010000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000020080000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af85894a2e4a61a3d2ce626ba9b3e927cffdb0e4e0bd06de1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000b8844cf76df596e746f360957aa3af954ef51605 * `_expectedAddress`: `0xb8844cf76df596e746f360957aa3af954ef51605` 8. Success! The transaction should have been created and sent. Check on [Etherscan](https://rinkeby.etherscan.io/address/0xb8844cf76df596e746f360957aa3af954ef51605) (or any other method) whether the transaction was successful or not. It should have succeeded! What occurred above is a transaction `0xb5850f2aa95f504f77e93b5ea09c279f94120079980866f554d55ff1451cdd26` was mined to a block which emitted a `Triggered` event. The block containing this transaction was then submitted to Ion deployed to Rinkeby (the original transaction itself was also executed on Rinkeby) and validated against the PoA Clique of Rinkeby. These blocks belong to a uniquely identifable chain with ID of `0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177`. Deployment of your own functional smart contract references a specific verifier that is designed to make proofs for the `Triggered` event. Calling the `verifyAndExecute` function with a proof of the transaction in the specific block then makes checks against the submitted block data for the transaction and the event. Upon successful discovery, your function executes. #### Try out your own functions! Take a look at `Function.sol` and you'll find a very simple `execute()` function. Try adding your own logic or event emissions here and follow the same procedure above and you'll be able to execute your own arbitrary code with a dependence on a particular state transition. You can now also attempt to write your own functional smart contracts using the similar skeleton to the `Function.sol` contract. Note that all the data submitted as a proof to the function call is generated merkle patricia proofs for a particular transaction at `0xcd68852f99928ab11adbc72ec473ec6526dac3b1b976c852745c47900f6b8e30` that was also executed on the Rinkeby Testnet. This transaction emits a `Triggered` event which contains the address of the caller when emitted. The `Function.sol` contract simply consumes events of this type, verifies that the transaction occurred in a block, that the event parameters are as expected and then executes a function. ### Hyperledger Fabric to Rinkeby Once again we've already deployed some contracts to the Rinkeby test network for you to play around with! Ion: `0x3c70A876808ae953917ddf9d95f364614a59B941` Base Validation: `0x07d18C468C63Fca68198776D799316612840b1A0` Fabric Block Store: `0x7cc9155EB4a12783bE5aBa9dcaA698d695D19A7D` We will deploy our own instance of the `FabricFunction.sol` contract and retrieve data from a submitted fabric block to use in an Ethereum contract function call. The Fabric block submitted contains two key-value pairs currently, `A: 0` and `B: 3`. We'll show that we can retrieve the value of `B` and emit this in an event thus demonstrating usage of Fabric block state in an Ethereum transaction. Procedure: 1. We'll need the Ion CLI here. Build the CLI by following instructions found on the [repository](https://github.com/clearmatics/ion-cli) 2. `./ion-cli` Starts the CLI 3. `>>> connectToClient https://rinkeby.infura.io` Connect to the Rinkeby Testnet 4. `>>> addAccount me ./keystore/UTC--2018-11-14T13-34-31.599642840Z--b8844cf76df596e746f360957aa3af954ef51605` Add an account to be signing transactions with. We've included one that already has Rinkeby ETH for you :) Password to the keystore is `test`. If you arrived late to the party and there is no ETH left, tough luck, try creating your own account and requesting ETH from a faucet. Alternatively you can run this exact thread of commands on a `ganache-cli` instance but make sure you connect to the correct endpoint in step 2. 5. `>>> addContractInstance fabfunc /absolute/path/to/contracts/functional/FabricFunction.sol` Add your functional contract instance which compiles your contract. Must be passed an absolute path. 6. `>>> deployContract fabfunc me 4000000` Deploy your contract to Rinkeby! This will return an address that the contract is deployed at if successful. This contract has a constructor that requires a parameter to be supplied when prompted: * `_storeAddr`: `0x7cc9155EB4a12783bE5aBa9dcaA698d695D19A7D` 7. `>>> transactionMessage fabfunc retrieveAndExecute me 0 1000000` Call the function. This requires you to supply the deployed contract instance address. Here you will need to supply the following data as an input to the function when prompted: * `_chainId`: `0x05cd2d1264200118dd4878c3de1050d49a1c47fa67fecf42038fc0728e38cc7b` * `_channelId`: `orgchannel` * `_key`: `B` 8. Success! The transaction should have been created and sent. Check on [Etherscan](https://rinkeby.etherscan.io/address/0xb8844cf76df596e746f360957aa3af954ef51605) (or any other method) whether the transaction was successful or not. It should have succeeded! The above leverages Fabric block state that has been submitted to the Rinkeby chain from an external Fabric network with unique chain ID of `0x05cd2d1264200118dd4878c3de1050d49a1c47fa67fecf42038fc0728e38cc7b`. In this chain there exists a channel `orgchannel` with a block containing transactions that have mutated the ledger. The function executed queries the `FabricStore.sol` contract deployed to Rinkeby at `0x7cc9155EB4a12783bE5aBa9dcaA698d695D19A7D` and retrieves the value at key `B` in channel `orgchannel`. It then emits an event including the block number, transaction number and the value as parameters. #### Try out your own functions! Take a look at `FabricFunction.sol` and you'll find a very simple `execute()` function. This function currently simply takes data and emits it. However you can use this state in however you choose. Try replacing the `execute` function body with your own logic to perform a transaction using Fabric block state. It's important to note that chaincode, the smart contract equivalent in Fabric, writes values as raw strings which means the stored data is arbitrary. This means that your functional contracts should be knowledgeable of these data formats to be able to use them effectively in your smart contracts on Ethereum. ## Develop on Ion Develop your own cross-chain smart contracts using the Ion interface! ### Core Concept The core of Ion revolves around the concept of dependence on state. As such, to create functional cross-chain smart contracts, the idea is to be able to make the execution of a smart contract function be dependent on some state transition to have occurred. Thus we create smart contracts that perform verification of state transitions before allowing the execution of code and this can be done many ways depending on the systems that intend to interoperate. This results in a framework that should provide a simple interface to make proofs of state and as such is comprised of two core components: * State Validation * State Transition The two core layers of the Ion framework will need different implementations for each method/mechanism by which validation and verification can be achieved across two interoperating systems. The two layers, validation and state verification, are analogous to the functions of current systems, consensus and state transition respectively. Thus the Ion framework aims to provide interfaces to make interoperation between any ledger governed by any consensus mechanism with another through the development of such interfaces. #### State Validation In order for two systems or chains to interoperate there must be some notion of the passing of state between the systems. The passing of this state ideally must be trustless and thus the State Validation layer handles this. It's purpose is to provide a mechanism by which passed state is checked for validity and correctness. Since we draw dependence on the state of another system to trigger arbitrary code execution we must ensure that any state that is passed is indeed correct. #### State Transition Once state has been passed from one system to another it can be used as a dependency for code execution. This code execution should have conditions to be contingent on a certain piece of data from another system/chain. In practice this could be checking the balance of an account on another chain or asserting that some transaction has been fulfilled. The State Transition layer should provide a mechanism to allow checks to be made against the stored state from another system. These checks should involve discerning and/or confirming a certain piece of data or event in the state of another chain and using the successful verification to trigger the execution of code. ### Ethereum to Ethereum Interface With Ethereum, interoperation between chains is mainly a question of validation as they share the EVM. We currently have made an implementation of the Clique proof-of-authority consensus mechanism used by the Rinkeby Testnet for validation. We achieve state verifications via event consumption. Using the presence of an event in a transaction, we can verify if the expected computation was done and to only do something if the verification succeeds. To write a smart contract that depends on particular state transitions there are pre-requisites: * Event Verifier contracts For any event signature, a corresponding Event Verifier contract must be written which encodes the mechanism that extracts the relevant event to check expected parameters as part of the verification. [`TriggerEventVerifier.sol`](./contracts/functional/TriggerEventVerifier.sol) is a very simple event verifier: * Holds the event signature it decodes * Encodes a verification function that takes expected fields as input to check against that included in the event All Event Verifier contracts should perform the same way. The differences will simply be in the event signature and parameters checks of the event. [`Function.sol`](./contracts/functional/Function.sol) provides a very simple example of how an event-consuming contract is written. Changing the event to be consumed by referencing a different Event Verifier allows you to draw dependence on a different state transition. ### Hyperledger Fabric to Ethereum Interface Fabric state is written as key-value pairs. When Fabric blocks are submitted to an Ethereum chain we store a lot of data about the block. There are two types of information we store that can be used in Etheruem transactions: * Ledger State * Past transitions Since the only data stored in a Fabric block is a state transition from past value to new value for a given key from a specific chaincode, it is hard to access state directly and only storing this simply allows us to check the presence of an expected transaction in the past. As such we keep both a copy of the ledger and the entire list of state transitions. This allows us two different accessors of Fabric state: * Query the current value of a key * Verify that a transaction has happened at a given time/block With this we can build use-case smart contracts in Ethereum to be able to query state from a Fabric block for usage, or when passed with expected data, verify if a transaction in a specified block mutated the ledger in an expected way. The example [`FabricFunction.sol`](./contracts/functional/FabricFunction.sol) shows how you would query the storage contract to make use of current Fabric ledger state. ### Testing Test-driven development and unit-testing all individual components of your smart contracts are extremely important in developing cross-chain contracts. There are two main steps to testing: * Core functionality of smart contracts * Integration with Ion Interface Traditional tests that ensure that your smart contract is operating in the way that you intended is always required. However with the added use of the Ion interface, you'll need to write tests that make sure they both integrate well with the verification mechanisms and still behave in the expected way. Study the tests in the repository to discover how we've unit-tested the entire integrated stack of smart contracts. ## Ion CLI The Ion Command-Line Interface is a tool built in Golang to provide functions to facilitate the use of the Ion framework. We have a current work-in-progress for a [CLI for Hyperledger Fabric](https://github.com/Shirikatsu/fabric-examples/tree/format-block/fabric-cli) forked from another project. The Command-Line Interface reference can be found [here](./ion-cli) ## How Ion works Please see our [wiki](https://github.com/clearmatics/ion/wiki) for more detailed information about the design philosophy of Ion. ### Setting up an environment To start developing on Ion, you'll need access to at least two different networks. * [Set up a Hyperledger Fabric network](https://github.com/clearmatics/simpleshares) ### Deploy the Ion stack Use the [CLI](https://github.com/clearmatics/ion-cli) to deploy the Ion stack to your own chain. 1. Deploy `Ion.sol`. 2. Deploy storage and validation contracts. First use the CLI to connect to a client and connect an account to submit transaction from: ```bash $ ./ion_cli =============================================================== Ion Command Line Interface Use 'help' to list commands =============================================================== >>> connectToClient Connecting to client... Connected! =============================================================== >>> addAccount Please provide your key decryption password. Account added succesfully. ``` Deploy `Ion.sol`: ```bash >>> addContractInstance ion Compiling contract... Creating contract instance... Added! =============================================================== >>> deployContract ion Enter input data for parameter _id: Waiting for contract to be deployed Deployed contract at: 0x... =============================================================== ``` Deploy storage contract, in this example we deploy the Ethereum block store contract: ```bash >>> addContractInstance ethstore Compiling contract... Creating contract instance... Added! =============================================================== >>> deployContract ethstore Enter input data for parameter _ionAddr: Waiting for contract to be deployed Deployed contract at: 0x... =============================================================== ``` Deploy validation and register contract, in this example we deploy the Ethereum Clique validation contract: ```bash >>> addContractInstance clique Compiling contract... Creating contract instance... Added! =============================================================== >>> deployContract clique Enter input data for parameter _ionAddr: Waiting for contract to be deployed Deployed contract at: 0x... =============================================================== >>> transactionMessage clique register me 0 4000000 Marshalling ABI JSONify ABI Packing Args to ABI Retrieving public key Creating transaction Signing transaction SENDING TRANSACTION Waiting for transaction to be mined... Transaction hash: 0x... =============================================================== >>> transactionMessage clique RegisterChain 0 Enter input data for parameter _chainId: Enter input data for parameter _validators: Enter input data for parameter _genesisBlockHash: Enter input data for parameter _storeAddr: Marshalling ABI JSONify ABI Packing Args to ABI Retrieving public key Creating transaction Signing transaction SENDING TRANSACTION Waiting for transaction to be mined... Transaction hash: 0x... =============================================================== ``` Deployment of other validation/storage contracts would be performed similarly. # Contribute! We would love contributors to help evolve Ion into a universal framework for interoperability. Functional use-case smart contracts should not live in this repository. Please create use-cases in your own repositories and we'll include a link to them in our Ion-based contract catalogue. The repository is segmented into two main sections that require work: * Validation * Storage #### Validation Each system requires a mechanism to be able to prove the correctness/validity of any data it holds, and this mechanism must be encoded by a Validation contract. Thus each method by which data could be validated must have its own contract that describes it. For example, to validate blocks from a proof-of-authority chain, we must replicate the verification mechanism of that specific implementation. Validation contracts for the consensus mechanism of an interoperating chain would be required in order to interact with it. These should live in the `contracts/validation/` directory. #### Storage Each system holds its data in different formats, and subsequently proving that the data exists would be different. Thus a different storage contracts must be written that decode and store any arbitrary data formats for use on any other system. For example, proving a transaction exists in an Ethereum block is different from proving a UTXO in a Bitcoin block. Storage contracts for the data format and state verification mechanisms of an interoperating chain would be required in order to interact with it. These should live in the `contracts/storage/` directory. With the above, we aim to expand our system-specific implementations for verification of both data validity and state transitions to allow the easier development of smart contracts using these interfaces. # Ionosphere All Ion-extensions or applications built on Ion are part of the Ionosphere! If you have built a cross-chain smart contract use-case please add a reference to your project here. ## Applications * [Transact-Ion](https://github.com/Shirikatsu/Transact-Ion), an atomic swap contract built on Ion. * [Simple Shares](https://github.com/clearmatics/simpleshares), a DvP model between Fabric and Ethereum using Ion. * [web3j Ion](https://github.com/web3j/ion), a simple start application using Ion with web3j. ================================================ FILE: contracts/EventVerifier.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; import "./libraries/RLP.sol"; import "./libraries/SolidityUtils.sol"; /* EventVerifier This contract is the basic global EventVerifier interface that all specific event verifiers must inherit from. It supplies a function `retrieveLog` that will output the relevant log for a specified event signature from the provided receipts. Each specific verifier that inherits this contract must hold knowledge of the event signature it intends to consume which will be passed to the retrieval function for log separation. */ contract EventVerifier { /* retrieveLog param: _eventSignature (bytes32) Hash representing the event signature of the event type to be consumed param: _contractEmittedAddress (bytes20) Address of the contract expected to have emitted the event param: _rlpReceipt (bytes) RLP-encoded receipt containing the relevant logs returns: log (RLP.RLPItem[]) Decoded log object in the form [ contractAddress, topics, data ] This decodes an RLP-encoded receipt and trawls through the logs to find the event that matches the event signature required and checks if the event was emitted from the correct source. If no log could be found with the relevant signature or emitted from the expected source the execution fails with an assert. If a log is not found, an `assert(false)` consumes all the gas and fails the transaction in order to incentivise submission of proper data. */ function retrieveLog(bytes32 _eventSignature, bytes20 _contractEmittedAddress, bytes memory _rlpReceipt) internal pure returns (RLP.RLPItem[] memory) { /* Decode the receipt into it's consituents and grab the logs with it's known position in the receipt object and proceed to decode the logs also. */ RLP.RLPItem[] memory receipt = RLP.toList(RLP.toRLPItem(_rlpReceipt)); RLP.RLPItem[] memory logs = RLP.toList(receipt[3]); /* The receipts could contain multiple event logs if a single transaction emitted multiple events. We need to separate them and locate the relevant event by signature. */ for (uint i = 0; i < logs.length; i++) { RLP.RLPItem[] memory log = RLP.toList(logs[i]); RLP.RLPItem[] memory topics = RLP.toList(log[1]); bytes32 containedEventSignature = RLP.toBytes32(topics[0]); if (containedEventSignature == _eventSignature) { // If event signature is found, check the contract address it was emitted from bytes20 b20_emissionSource = SolUtils.BytesToBytes20(RLP.toData(log[0]), 0); assert( b20_emissionSource == _contractEmittedAddress); return log; } } assert( false ); } } ================================================ FILE: contracts/Ion.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; import "./libraries/ECVerify.sol"; import "./libraries/RLP.sol"; import "./libraries/PatriciaTrie.sol"; import "./libraries/SolidityUtils.sol"; import "./storage/BlockStore.sol"; contract Ion { bytes32 public chainId; mapping (address => bool) public m_registered_validation; address[] public validation_modules; /* * Constructor * param: id (bytes32) Unique id to identify this chain that the contract is being deployed to. * * Supplied with a unique id to identify this chain to others that may interoperate with it. * The deployer must assert that the id is indeed public and that it is not already being used * by another chain */ constructor(bytes32 _id) public { chainId = _id; } /* * onlyRegisteredValidation * param: _addr (address) Address of the Validation module being registered * * Modifier that checks if the provided chain id has been registered to this contract */ modifier onlyRegisteredValidation() { require( isContract(msg.sender), "Caller address is not a valid contract. Please inherit the BlockStore contract for proper usage." ); require( m_registered_validation[msg.sender], "Validation module is not registered"); _; } // Pseudo-modifier returns boolean, used with different 'require's to input custom revert messages function isContract(address _addr) internal view returns (bool) { uint size; assembly { size := extcodesize(_addr) } return (size > 0); } function registerValidationModule() public { require( isContract(msg.sender), "Caller address is not a valid contract. Please inherit the BlockStore contract for proper usage." ); require( !m_registered_validation[msg.sender], "Validation module has already been registered." ); m_registered_validation[msg.sender] = true; validation_modules.push(msg.sender); } function addChain(address _storageAddress, bytes32 _chainId) onlyRegisteredValidation public { BlockStore store = BlockStore(_storageAddress); store.addChain(_chainId); } /* * storeBlock * param: * */ function storeBlock(address _storageAddress, bytes32 _chainId, bytes memory _blockBlob) onlyRegisteredValidation public { require( isContract(_storageAddress), "Storage address provided is not contract."); BlockStore store = BlockStore(_storageAddress); store.addBlock(_chainId, _blockBlob); } } ================================================ FILE: contracts/IonCompatible.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; import "./Ion.sol"; contract IonCompatible { /* The Ion contract that proofs would be made to. Ensure that prior to verification attempts that the relevant blocks have been submitted to the Ion contract. */ Ion internal ion; constructor(address _ionAddr) public { ion = Ion(_ionAddr); } } ================================================ FILE: contracts/Migrations.sol ================================================ // Copyright (c) 2016-2017 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; contract Migrations { address public owner; uint256 public last_completed_migration; modifier restricted() { if (msg.sender == owner) _; } constructor() public { owner = msg.sender; } function setCompleted(uint256 completed) public restricted { last_completed_migration = completed; } function upgrade(address new_address) public restricted { Migrations upgraded = Migrations(new_address); upgraded.setCompleted(last_completed_migration); } } ================================================ FILE: contracts/functional/FabricFunction.sol ================================================ pragma solidity ^0.5.12; import "../storage/FabricStore.sol"; contract FabricFunction { FabricStore blockStore; constructor(address _storeAddr) public { blockStore = FabricStore(_storeAddr); } event State(uint blockNo, uint txNo, string mvalue); function execute(uint _blockNo, uint _txNo, string memory _value) internal { emit State(_blockNo, _txNo, _value); } function retrieveAndExecute(bytes32 _chainId, string memory _channelId, string memory _key) public { uint blockVersion; uint txVersion; string memory value; (blockVersion, txVersion, value) = blockStore.getState(_chainId, _channelId, _key); execute(blockVersion, txVersion, value); } } ================================================ FILE: contracts/functional/Function.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; import "../storage/EthereumStore.sol"; contract TriggerEventVerifier { function verify(bytes20 _contractEmittedAddress, bytes memory _rlpReceipt, bytes20 _expectedAddress) public returns (bool); } /* This function contract is the consumer of an event and performs some execution thereafter. In practice, this would be written by a contract designer that intends to consume specific events from another chain. As such all behaviour and dependence on such event must be defined here. Common custom behaviour may include: * Keeping an array of transaction hashes denoting the specific events from that transaction that have already been consumed to restrict multiple consumption or 'double spend' of events. * Extending the amount of expected event parameters above the stack limit. This might then require some other method of passing expected parameters to the contract possibly via RLP-encoding to compress all data to a single argument and decoding them within the `verifyAndExecute` function. * Including multiple event verifiers if a function requires proof of multiple state transitions from other chains. This would also bloat the local scope which is prone to 'stack too deep' issues which would require custom workarounds. */ contract Function { EthereumStore blockStore; /* The event verifier for the specific event being consumed. Each event would require a different event verifier to be deployed and each consumer would reference the relevant verifier to prove logs. */ TriggerEventVerifier verifier; /* Custom event that fires when execution is performed successfully. */ event Executed(); /* Constructor. Requires Ion contract address and all used event verifier contract addresses. In this case we only use one verifier. */ constructor(address _storeAddr, address _verifierAddr) public { blockStore = EthereumStore(_storeAddr); verifier = TriggerEventVerifier(_verifierAddr); } /* This is the function that is intended to be executed upon successful verification of proofs */ function execute() internal { emit Executed(); } /* verifyAndExecute Core parameters for verification param: _chainId (bytes32) Chain ID of the chain that the event being consumed was emitted on. This may require altering to (bytes) if proofs from multiple chains are needed. param: _blockHash (bytes32) Block hash of block with event to be consumed. This may require altering to (bytes) if proofs from multiple chains are needed. param: _contractEmittedAddress (bytes20) Contract address of the source of event emission. This may require altering to (bytes) if proofs from multiple chains are needed. param: _path (bytes) RLP-encoded transaction index of the relevant transaction that emitted the event being consumed. If multiple proofs are required, multiple paths supplied must be RLP-encoded and an extra (bool) parameter provided to denote multiple paths included. param: _tx (bytes) RLP-encoded transaction object provided by proof generation. param: _txNodes (bytes) RLP_encoded transaction nodes provided by proof generation. param: _receipt (bytes) RLP-encoded receipt object provided by proof generation. param: _receiptNodes (bytes) RLP-encoded receipt nodes provided by proof generation. Custom parameters for verification param: _expectedAddress (bytes20) The expected address value in the event parameter being consumed. This is the only public function apart from the constructor and is the only interface to this contract. This function wraps the verification and execution which only fires after a successful slew of verifications. As noted, stack restrictions will make it harder to implement multiple event consumption. Suggestions made here may not be the best way to achieve this but are possible methods. It may end up requiring separate functions for each event and persisting the consumption state of each event per tx hash and using that to allow or prevent verified execution. In our case, it is simple as we only consume a single event. */ function verifyAndExecute( bytes32 _chainId, bytes32 _blockHash, bytes20 _contractEmittedAddress, bytes memory _proof, bytes20 _expectedAddress ) public { bytes memory receipt = blockStore.CheckProofs(_chainId, _blockHash, _proof); require( verifier.verify(_contractEmittedAddress, receipt, _expectedAddress), "Event verification failed." ); execute(); } } ================================================ FILE: contracts/functional/Trigger.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; /* Trigger Example contract that emits an event to be consumed. Currently an instance deployed to: Rinkeby @: 0x61621bcf02914668f8404c1f860e92fc1893f74c Deployment Tx Hash: 0xc9500e84af2394e1d91b43e40c9c89f105636748f95ae05c11c73f2fd755795e Deployed Block Number: 2657325 `fire()` call Tx Hash 0xafc3ab60059ed38e71c7f6bea036822abe16b2c02fcf770a4f4b5fffcbfe6e7e `fire()` call Tx Hash 0xf8a9a2f7e894f243fd12e5379c1dca2e139817f440e0ced7a8db42ec8dcf30ff The current tests are running against generated proofs from Rinkeby for the above data and consumes the event emitted in the transaction executed. */ contract Trigger { event Triggered(address caller); function fire() public { emit Triggered(msg.sender); } } ================================================ FILE: contracts/functional/TriggerEventVerifier.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; import "../libraries/RLP.sol"; import "../libraries/SolidityUtils.sol"; import "../EventVerifier.sol"; /* TriggerEventVerifier Inherits from `EventVerifier` and verifies `Triggered` events. From the provided logs, we separate the data and define checks to assert certain information in the event and returns `true` if successful. Contracts similar to this that verify specific events should be designed only to verify the data inside the supplied events with similarly supplied expected outcomes. It is only meant to serve as a utility to perform defined checks against specific events. */ contract TriggerEventVerifier is EventVerifier { bytes32 eventSignature = keccak256("Triggered(address)"); function verify(bytes20 _contractEmittedAddress, bytes memory _rlpReceipt, bytes20 _expectedAddress) public view returns (bool) { // Retrieve specific log for given event signature RLP.RLPItem[] memory log = retrieveLog(eventSignature, _contractEmittedAddress, _rlpReceipt); // Split logs into constituents. Not all constituents are used here bytes memory contractEmittedEvent = RLP.toData(log[0]); RLP.RLPItem[] memory topics = RLP.toList(log[1]); bytes memory data = RLP.toData(log[2]); /* This section below is specific to this event verifier and checks the relevant data. In this event we only expect a single un-indexed address parameter which will be present in the data field. The data field pads it's contents if they are less than 32 bytes. Therefore we know that our address parameter exists in the 20 least significant bytes of the data field. We copy the last 20 bytes of our data field to a bytes20 variable to compare against the supplied expected parameter in the event from our function call. This acts as our conditional check that the event called is what the user expects. */ bytes20 b20_address = SolUtils.BytesToBytes20(data, data.length - 20); return b20_address == _expectedAddress; } } ================================================ FILE: contracts/libraries/ECVerify.sol ================================================ pragma solidity ^0.5.12; // // The new assembly support in Solidity makes writing helpers easy. // Many have complained how complex it is to use `ecrecover`, especially in conjunction // with the `eth_sign` RPC call. Here is a helper, which makes that a matter of a single call. // // Sample input parameters: // (with v=0) // "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad", // "0xaca7da997ad177f040240cdccf6905b71ab16b74434388c3a72f34fd25d6439346b2bac274ff29b48b3ea6e2d04c1336eaceafda3c53ab483fc3ff12fac3ebf200", // "0x0e5cb767cce09a7f3ca594df118aa519be5e2b5a" // // (with v=1) // "0x47173285a8d7341e5e972fc677286384f802f8ef42a5ec5f03bbfa254cb01fad", // "0xdebaaa0cddb321b2dcaaf846d39605de7b97e77ba6106587855b9106cb10421561a22d94fa8b8a687ff9c911c844d1c016d1a685a9166858f9c7c1bc85128aca01", // "0x8743523d96a1b2cbe0c6909653a56da18ed484af" // // (The hash is a hash of "hello world".) // // Written by Alex Beregszaszi (@axic), use it under the terms of the MIT license. // library ECVerify { // Duplicate Solidity's ecrecover, but catching the CALL return value function safer_ecrecover(bytes32 hash, uint8 v, bytes32 r, bytes32 s) view internal returns (address) { // We do our own memory management here. Solidity uses memory offset // 0x40 to store the current end of memory. We write past it (as // writes are memory extensions), but don't update the offset so // Solidity will reuse it. The memory used here is only needed for // this context. // FIXME: inline assembly can't access return values bool ret; address addr; assembly { let size := mload(0x40) mstore(size, hash) mstore(add(size, 32), v) mstore(add(size, 64), r) mstore(add(size, 96), s) // NOTE: we can reuse the request memory because we deal with // the return code ret := staticcall(3000, 1, size, 128, size, 32) addr := mload(size) } require( ret == true ); return addr; } function ecrecovery(bytes32 hash, bytes memory sig) internal view returns (address) { bytes32 r; bytes32 s; uint8 v; require (sig.length == 65); // The signature format is a compact form of: // {bytes32 r}{bytes32 s}{uint8 v} // Compact means, uint8 is not padded to 32 bytes. assembly { r := mload(add(sig, 32)) s := mload(add(sig, 64)) // Here we are loading the last 32 bytes. We exploit the fact that // 'mload' will pad with zeroes if we overread. // There is no 'mload8' to do this, but that would be nicer. v := byte(0, mload(add(sig, 96))) // Alternative solution: // 'byte' is not working due to the Solidity parser, so lets // use the second best option, 'and' // v := and(mload(add(sig, 65)), 255) } // albeit non-transactional signatures are not specified by the YP, one would expect it // to match the YP range of [27, 28] // // geth uses [0, 1] and some clients have followed. This might change, see: // https://github.com/ethereum/go-ethereum/issues/2053 if (v < 27) v += 27; require (v == 27 || v == 28); /* prefix might be needed for geth only * https://github.com/ethereum/go-ethereum/issues/3731 */ /* bytes memory prefix = "\x19Ethereum Signed Message:\n32"; hash = keccak256(prefix, hash); */ /* hash = sha3(prefix, hash); */ return safer_ecrecover(hash, v, r, s); /* return ecrecover(hash, v, r, s); */ } } ================================================ FILE: contracts/libraries/PatriciaTrie.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; import "./RLP.sol"; library PatriciaTrie { function verifyProof(bytes memory _value, bytes memory _parentNodes, bytes memory _path, bytes32 _root) internal pure returns (bool) { RLP.RLPItem memory nodes = RLP.toRLPItem(_parentNodes); RLP.RLPItem[] memory parentNodes = RLP.toList(nodes); bytes32 currentNodeKey = _root; uint traversedNibbles = 0; bytes memory path = toNibbleArray(_path, false); for (uint i = 0; i < parentNodes.length; i++) { if (currentNodeKey != keccak256(RLP.toBytes(parentNodes[i]))) { return false; } RLP.RLPItem[] memory currentNode = RLP.toList(parentNodes[i]); if (currentNode.length == 17) { // Branch Node (currentNodeKey, traversedNibbles) = processBranchNode(currentNode, traversedNibbles, path, _value); } else if (currentNode.length == 2) { // Extension/Leaf Node (currentNodeKey, traversedNibbles) = processExtensionLeafNode(currentNode, traversedNibbles, path, _value); } else { return false; } // Read comment block below for explanation of this if (currentNodeKey == 0x0) { return traversedNibbles == 1; } } return false; } /** Node Processing processBranchNodes returns (bytes32 currentNodeKey, uint traversedNibbles) processExtensionLeafNode returns (bytes32 currentNodeKey, uint traversedNibbles) Due to the dual nature of how a branch node may be processed where the next node in the path could be either referenced by hash or nested in the branch node is the total RLP-encoded node is less than 32 bytes (nested node), we required separation of logic due to "stack-too-deep" issues and opted for a messy returning of reused variables. These returned variables now hold two purposes: * currentNodeKey (bytes32): Holds value of the hash of the next node to be processed. If processing is finished this value is 0x0. * traversedNibbles (uint): Tracks how many nibbles have been traversed. If processing is finished this value will be 0 if verification failed, and 1 if verification succeeded. The dual-functionality of these variables is the crux of how I avoided stack issues which makes the code somewhat unreadable. If there is an improvement to this algorithm that can make it more readable please share. */ function processBranchNode(RLP.RLPItem[] memory _currentNode, uint _traversedNibbles, bytes memory _path, bytes memory _value) private pure returns (bytes32, uint) { // Return the value at the current node if we have reached the end of the path if (_traversedNibbles == _path.length) { return (0x0, checkNodeValue(_value, RLP.toBytes(_currentNode[16])) ? 1 : 0); } uint8 nextPathNibble = nibbleToUint8(_path[_traversedNibbles]); RLP.RLPItem memory nextNode = _currentNode[nextPathNibble]; _traversedNibbles += 1; bytes32 currentNodeKey; if (RLP.toBytes(nextNode).length < 32) { //Nested 'Node' (currentNodeKey, _traversedNibbles) = processNestedNode(nextNode, _traversedNibbles, _path, _value); } else { currentNodeKey = RLP.toBytes32(_currentNode[nextPathNibble]); } return (currentNodeKey, _traversedNibbles); } function processExtensionLeafNode( RLP.RLPItem[] memory _currentNode, uint _traversedNibbles, bytes memory _path, bytes memory _value ) private pure returns (bytes32, uint) { bytes memory nextPathNibbles = RLP.toData(_currentNode[0]); _traversedNibbles += toNibbleArray(nextPathNibbles, true).length; if (_traversedNibbles == _path.length) { return (0x0, checkNodeValue(_value, RLP.toData(_currentNode[1])) ? 1 : 0); } // Reached a leaf before end of the path. Proof false. if (toNibbleArray(nextPathNibbles, true).length == 0) { return (0x0, 0); } bytes memory nextNodeKey = RLP.toData(_currentNode[1]); bytes32 currentNodeKey = bytesToBytes32(nextNodeKey, 0); return (currentNodeKey, _traversedNibbles); } function processNestedNode(RLP.RLPItem memory _nextNode, uint _traversedNibbles, bytes memory _path, bytes memory _value) private pure returns (bytes32, uint) { RLP.RLPItem[] memory currentNode = RLP.toList(_nextNode); if (currentNode.length == 17) { // Branch Node return processBranchNode(currentNode, _traversedNibbles, _path, _value); } else if (currentNode.length == 2) { // Leaf Node return processExtensionLeafNode(currentNode, _traversedNibbles, _path, _value); } else { return (0x0, 0); } } function checkNodeValue(bytes memory _expected, bytes memory _nodeValue) private pure returns (bool) { return keccak256(_expected) == keccak256(_nodeValue); } function toNibbleArray(bytes memory b, bool hexPrefixed) private pure returns (bytes memory) { bytes memory nibbleArray = new bytes(255); uint8 nibblesFound = 0; for (uint i = 0; i < b.length; i++) { byte[2] memory nibbles = byteToNibbles(b[i]); if (hexPrefixed && i == 0) { if (nibbles[0] == byte(0x01) || nibbles[0] == byte(0x03)) { nibbleArray[nibblesFound] = nibbles[1]; nibblesFound += 1; } } else { nibbleArray[nibblesFound] = nibbles[0]; nibbleArray[nibblesFound + 1] = nibbles[1]; nibblesFound += 2; } } bytes memory finiteNibbleArray = new bytes(nibblesFound); for (uint j = 0; j < nibblesFound; j++) { finiteNibbleArray[j] = nibbleArray[j]; } return finiteNibbleArray; } function byteToNibbles(byte b) private pure returns (byte[2] memory) { byte firstNibble = rightShift(b, 4); byte secondNibble = b & byte(0x0f); return [firstNibble, secondNibble]; } function nibbleToUint8(byte nibble) private pure returns (uint8) { return uint8(nibble); } function leftShift(byte i, uint8 bits) private pure returns (byte) { return byte(uint8(i) * uint8(2) ** uint8(bits)); } function rightShift(byte i, uint8 bits) private pure returns (byte) { return byte(uint8(i) / uint8(2) ** uint8(bits)); } function bytesToBytes32(bytes memory b, uint offset) private pure returns (bytes32) { bytes32 out; for (uint i = 0; i < 32; i++) { out |= bytes32(b[offset + i] & 0xFF) >> (i * 8); } return out; } } ================================================ FILE: contracts/libraries/RLP.sol ================================================ pragma solidity ^0.5.12; /** * @title RLPReader * * RLPReader is used to read and parse RLP encoded data in memory. * * @author Andreas Olofsson (androlo1980@gmail.com) */ library RLP { uint constant DATA_SHORT_START = 0x80; uint constant DATA_LONG_START = 0xB8; uint constant LIST_SHORT_START = 0xC0; uint constant LIST_LONG_START = 0xF8; uint constant DATA_LONG_OFFSET = 0xB7; uint constant LIST_LONG_OFFSET = 0xF7; struct RLPItem { uint _unsafe_memPtr; // Pointer to the RLP-encoded bytes. uint _unsafe_length; // Number of bytes. This is the full length of the string. } struct Iterator { RLPItem _unsafe_item; // Item that's being iterated over. uint _unsafe_nextPtr; // Position of the next item in the list. } /* Iterator */ function next(Iterator memory self) internal pure returns (RLPItem memory subItem) { if(hasNext(self)) { uint ptr = self._unsafe_nextPtr; uint itemLength = _itemLength(ptr); subItem._unsafe_memPtr = ptr; subItem._unsafe_length = itemLength; self._unsafe_nextPtr = ptr + itemLength; } else revert(); } function next(Iterator memory self, bool strict) internal pure returns (RLPItem memory subItem) { subItem = next(self); if(strict && !_validate(subItem)) revert(); } function hasNext(Iterator memory self) internal pure returns (bool) { RLPItem memory item = self._unsafe_item; return self._unsafe_nextPtr < item._unsafe_memPtr + item._unsafe_length; } /* RLPItem */ /// @dev Creates an RLPItem from an array of RLP encoded bytes. /// @param self The RLP encoded bytes. /// @return An RLPItem function toRLPItem(bytes memory self) internal pure returns (RLPItem memory) { uint len = self.length; if (len == 0) { return RLPItem(0, 0); } uint memPtr; assembly { memPtr := add(self, 0x20) } return RLPItem(memPtr, len); } /// @dev Creates an RLPItem from an array of RLP encoded bytes. /// @param self The RLP encoded bytes. /// @param strict Will revert() if the data is not RLP encoded. /// @return An RLPItem function toRLPItem(bytes memory self, bool strict) internal pure returns (RLPItem memory) { RLPItem memory item = toRLPItem(self); if(strict) { uint len = self.length; if(_payloadOffset(item) > len) revert(); if(_itemLength(item._unsafe_memPtr) != len) revert(); if(!_validate(item)) revert(); } return item; } /// @dev Check if the RLP item is null. /// @param self The RLP item. /// @return 'true' if the item is null. function isNull(RLPItem memory self) internal pure returns (bool ret) { return self._unsafe_length == 0; } /// @dev Check if the RLP item is a list. /// @param self The RLP item. /// @return 'true' if the item is a list. function isList(RLPItem memory self) internal pure returns (bool ret) { if (self._unsafe_length == 0) return false; uint memPtr = self._unsafe_memPtr; assembly { ret := iszero(lt(byte(0, mload(memPtr)), 0xC0)) } } /// @dev Check if the RLP item is data. /// @param self The RLP item. /// @return 'true' if the item is data. function isData(RLPItem memory self) internal pure returns (bool ret) { if (self._unsafe_length == 0) return false; uint memPtr = self._unsafe_memPtr; assembly { ret := lt(byte(0, mload(memPtr)), 0xC0) } } /// @dev Check if the RLP item is empty (string or list). /// @param self The RLP item. /// @return 'true' if the item is null. function isEmpty(RLPItem memory self) internal pure returns (bool ret) { if(isNull(self)) return false; uint b0; uint memPtr = self._unsafe_memPtr; assembly { b0 := byte(0, mload(memPtr)) } return (b0 == DATA_SHORT_START || b0 == LIST_SHORT_START); } /// @dev Get the number of items in an RLP encoded list. /// @param self The RLP item. /// @return The number of items. function items(RLPItem memory self) internal pure returns (uint) { if (!isList(self)) return 0; uint b0; uint memPtr = self._unsafe_memPtr; assembly { b0 := byte(0, mload(memPtr)) } uint pos = memPtr + _payloadOffset(self); uint last = memPtr + self._unsafe_length - 1; uint itms; while(pos <= last) { pos += _itemLength(pos); itms++; } return itms; } /// @dev Create an iterator. /// @param self The RLP item. /// @return An 'Iterator' over the item. function iterator(RLPItem memory self) internal pure returns (Iterator memory it) { if (!isList(self)) revert(); uint ptr = self._unsafe_memPtr + _payloadOffset(self); it._unsafe_item = self; it._unsafe_nextPtr = ptr; } /// @dev Return the RLP encoded bytes. /// @param self The RLPItem. /// @return The bytes. function toBytes(RLPItem memory self) internal pure returns (bytes memory bts) { uint len = self._unsafe_length; bts = new bytes(len); if (len != 0) { _copyToBytes(self._unsafe_memPtr, bts, len); } } /// @dev Decode an RLPItem into bytes. This will not work if the /// RLPItem is a list. /// @param self The RLPItem. /// @return The decoded string. function toData(RLPItem memory self) internal pure returns (bytes memory bts) { if(!isData(self)) revert(); uint rStartPos; uint len; (rStartPos, len) = _decode(self); bts = new bytes(len); _copyToBytes(rStartPos, bts, len); } /// @dev Get the list of sub-items from an RLP encoded list. /// Warning: This is inefficient, as it requires that the list is read twice. /// @param self The RLP item. /// @return Array of RLPItems. function toList(RLPItem memory self) internal pure returns (RLPItem[] memory list) { if(!isList(self)) revert(); uint numItems = items(self); list = new RLPItem[](numItems); Iterator memory it = iterator(self); uint idx; while(hasNext(it)) { list[idx] = next(it); idx++; } } /// @dev Decode an RLPItem into an ascii string. This will not work if the /// RLPItem is a list. /// @param self The RLPItem. /// @return The decoded string. function toAscii(RLPItem memory self) internal pure returns (string memory str) { if(!isData(self)) revert(); uint rStartPos; uint len; (rStartPos, len) = _decode(self); bytes memory bts = new bytes(len); _copyToBytes(rStartPos, bts, len); str = string(bts); } /// @dev Decode an RLPItem into a uint. This will not work if the /// RLPItem is a list. /// @param self The RLPItem. /// @return The decoded string. function toUint(RLPItem memory self) internal pure returns (uint data) { if(!isData(self)) revert(); uint rStartPos; uint len; (rStartPos, len) = _decode(self); if (len > 32) revert(); else if (len == 0) return 0; assembly { data := div(mload(rStartPos), exp(256, sub(32, len))) } } /// @dev Decode an RLPItem into a boolean. This will not work if the /// RLPItem is a list. /// @param self The RLPItem. /// @return The decoded string. function toBool(RLPItem memory self) internal pure returns (bool data) { if(!isData(self)) revert(); uint rStartPos; uint len; (rStartPos, len) = _decode(self); if (len != 1) revert(); uint temp; assembly { temp := byte(0, mload(rStartPos)) } if (temp > 1) revert(); return temp == 1 ? true : false; } /// @dev Decode an RLPItem into a byte. This will not work if the /// RLPItem is a list. /// @param self The RLPItem. /// @return The decoded string. function toByte(RLPItem memory self) internal pure returns (byte data) { if(!isData(self)) revert(); uint rStartPos; uint len; (rStartPos, len) = _decode(self); if (len != 1) revert(); byte temp; assembly { temp := byte(0, mload(rStartPos)) } return temp; } /// @dev Decode an RLPItem into an int. This will not work if the /// RLPItem is a list. /// @param self The RLPItem. /// @return The decoded string. function toInt(RLPItem memory self) internal pure returns (int data) { return int(toUint(self)); } /// @dev Decode an RLPItem into a bytes32. This will not work if the /// RLPItem is a list. /// @param self The RLPItem. /// @return The decoded string. function toBytes32(RLPItem memory self) internal pure returns (bytes32 data) { return bytes32(toUint(self)); } /// @dev Decode an RLPItem into an address. This will not work if the /// RLPItem is a list. /// @param self The RLPItem. /// @return The decoded string. function toAddress(RLPItem memory self) internal pure returns (address data) { if(!isData(self)) revert(); uint rStartPos; uint len; (rStartPos, len) = _decode(self); if (len != 20) revert(); assembly { data := div(mload(rStartPos), exp(256, 12)) } } // Get the payload offset. function _payloadOffset(RLPItem memory self) private pure returns (uint) { if(self._unsafe_length == 0) return 0; uint b0; uint memPtr = self._unsafe_memPtr; assembly { b0 := byte(0, mload(memPtr)) } if(b0 < DATA_SHORT_START) return 0; if(b0 < DATA_LONG_START || (b0 >= LIST_SHORT_START && b0 < LIST_LONG_START)) return 1; if(b0 < LIST_SHORT_START) return b0 - DATA_LONG_OFFSET + 1; return b0 - LIST_LONG_OFFSET + 1; } // Get the full length of an RLP item. function _itemLength(uint memPtr) private pure returns (uint len) { uint b0; assembly { b0 := byte(0, mload(memPtr)) } if (b0 < DATA_SHORT_START) len = 1; else if (b0 < DATA_LONG_START) len = b0 - DATA_SHORT_START + 1; else if (b0 < LIST_SHORT_START) { assembly { let bLen := sub(b0, 0xB7) // bytes length (DATA_LONG_OFFSET) let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length len := add(1, add(bLen, dLen)) // total length } } else if (b0 < LIST_LONG_START) len = b0 - LIST_SHORT_START + 1; else { assembly { let bLen := sub(b0, 0xF7) // bytes length (LIST_LONG_OFFSET) let dLen := div(mload(add(memPtr, 1)), exp(256, sub(32, bLen))) // data length len := add(1, add(bLen, dLen)) // total length } } } // Get start position and length of the data. function _decode(RLPItem memory self) private pure returns (uint memPtr, uint len) { if(!isData(self)) revert(); uint b0; uint start = self._unsafe_memPtr; assembly { b0 := byte(0, mload(start)) } if (b0 < DATA_SHORT_START) { memPtr = start; len = 1; return (memPtr, len); } if (b0 < DATA_LONG_START) { len = self._unsafe_length - 1; memPtr = start + 1; } else { uint bLen; assembly { bLen := sub(b0, 0xB7) // DATA_LONG_OFFSET } len = self._unsafe_length - 1 - bLen; memPtr = start + bLen + 1; } return (memPtr, len); } // Assumes that enough memory has been allocated to store in target. function _copyToBytes(uint btsPtr, bytes memory tgt, uint btsLen) private pure { // Exploiting the fact that 'tgt' was the last thing to be allocated, // we can write entire words, and just overwrite any excess. assembly { { let words := div(add(btsLen, 31), 32) let rOffset := btsPtr let wOffset := add(tgt, 0x20) for { let i := 0 } // Start at arr + 0x20 lt(i, words) { i := add(i, 1) } { let offset := mul(i, 0x20) mstore(add(wOffset, offset), mload(add(rOffset, offset))) } mstore(add(tgt, add(0x20, mload(tgt))), 0) } } } // Check that an RLP item is valid. function _validate(RLPItem memory self) private pure returns (bool ret) { // Check that RLP is well-formed. uint b0; uint b1; uint memPtr = self._unsafe_memPtr; assembly { b0 := byte(0, mload(memPtr)) b1 := byte(1, mload(memPtr)) } if(b0 == DATA_SHORT_START + 1 && b1 < DATA_SHORT_START) return false; return true; } } ================================================ FILE: contracts/libraries/SafeMath.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; /** * Math operations with safety checks */ library SafeMath { function mul(uint a, uint b) internal pure returns (uint) { uint c = a * b; assert(a == 0 || c / a == b); return c; } function div(uint a, uint b) internal pure returns (uint) { // assert(b > 0); // Solidity automatically throws when dividing by 0 uint c = a / b; // assert(a == b * c + a % b); // There is no case in which this doesn't hold return c; } function sub(uint a, uint b) internal pure returns (uint) { assert(b <= a); return a - b; } function add(uint a, uint b) internal pure returns (uint) { uint c = a + b; assert(c >= a); return c; } function max64(uint64 a, uint64 b) internal pure returns (uint64) { return a >= b ? a : b; } function min64(uint64 a, uint64 b) internal pure returns (uint64) { return a < b ? a : b; } function max256(uint256 a, uint256 b) internal pure returns (uint256) { return a >= b ? a : b; } function min256(uint256 a, uint256 b) internal pure returns (uint256) { return a < b ? a : b; } } ================================================ FILE: contracts/libraries/SolidityUtils.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; // A library of funky data manipulation stuff library SolUtils { /* * @description copies 32 bytes from input into the output * @param output memory allocation for the data you need to extract * @param input array from which the data should be extracted * @param buf index which the data starts within the byte array needs to have 32 bytes appended */ function BytesToBytes32(bytes memory input, uint256 buf) internal pure returns (bytes32 output) { buf = buf + 32; assembly { output := mload(add(input, buf)) } } /* * @description copies 20 bytes from input into the output * @param output memory allocation for the data you need to extract * @param input array from which the data should be extracted * @param buf index which the data starts within the byte array needs to have 32 bytes appended */ function BytesToBytes20(bytes memory input, uint256 buf) internal pure returns (bytes20) { bytes20 output; for (uint i = 0; i < 20; i++) { output |= bytes20(input[buf + i] & 0xFF) >> (i * 8); } return output; } /* * @description copies 20 bytes from input into the output returning an address * @param output memory allocation for the data you need to extract * @param input array from which the data should be extracted * @param buf index which the data starts within the byte array needs to have 32 bytes appended */ function BytesToAddress(bytes memory input, uint256 buf) internal pure returns (address output) { buf = buf + 20; assembly { output := mload(add(input, buf)) } } /* * @description copies output.length bytes from the input into the output * @param output memory allocation for the data you need to extract * @param input array from which the data should be extracted * @param buf index which the data starts within the byte array */ function BytesToBytes(bytes memory output, bytes memory input, uint256 buf) view internal { uint256 outputLength = output.length; buf = buf + 32; // Append 32 as we need to point past the variable type definition assembly { let ret := staticcall(3000, 4, add(input, buf), outputLength, add(output, 32), outputLength) } } function UintToString(uint _i) internal pure returns (string memory _uintAsString) { if (_i == 0) { return "0"; } uint j = _i; uint len; while (j != 0) { len++; j /= 10; } bytes memory bstr = new bytes(len); uint k = len - 1; while (_i != 0) { bstr[k--] = byte(uint8(48 + _i % 10)); _i /= 10; } return string(bstr); } function BoolToString(bool _b) internal pure returns (string memory) { if (_b) return "true"; else return "false"; } } ================================================ FILE: contracts/mock/MockIon.sol ================================================ pragma solidity ^0.5.12; import "../Ion.sol"; import "../storage/BlockStore.sol"; contract MockIon is Ion { constructor(bytes32 _id) public Ion(_id) {} function registerValidationModule() public { require( isContract(msg.sender), "Caller address is not a valid contract. Please inherit the BlockStore contract for proper usage." ); require( !m_registered_validation[msg.sender], "Validation module has already been registered." ); m_registered_validation[msg.sender] = true; validation_modules.push(msg.sender); } function addChain(address _storageAddress, bytes32 _chainId) public { BlockStore store = BlockStore(_storageAddress); store.addChain(_chainId); } function storeBlock(address _storageAddress, bytes32 _chainId, bytes memory _blockBlob) public { BlockStore store = BlockStore(_storageAddress); store.addBlock(_chainId, _blockBlob); } } ================================================ FILE: contracts/mock/MockStorage.sol ================================================ pragma solidity ^0.5.12; import "../storage/BlockStore.sol"; /* Mock Block Store contract This mocking contract is used to simulate interactions and asserting certain return data from interaction via other contracts being tested. Use as a tool for testing ONLY. This is not an accurate representation of a block store contract and should not be used in any way as a representation of a block store contract. Please refer to BlockStore.sol and inherit functionality from that base contract and see EthereumStore.sol for more implementation details. */ contract MockStorage is BlockStore { constructor(address _ionAddr) BlockStore(_ionAddr) public {} event AddedBlock(); function addBlock(bytes32 _chainId, bytes memory _blockBlob) public { emit AddedBlock(); } } ================================================ FILE: contracts/mock/MockValidation.sol ================================================ pragma solidity ^0.5.12; import "../IonCompatible.sol"; contract MockValidation is IonCompatible { constructor (address _ionAddr) IonCompatible(_ionAddr) public {} function register() public returns (bool) { ion.registerValidationModule(); return true; } function SubmitBlock(address _storageAddress, bytes32 _chainId, bytes memory _blockBlob) public { ion.storeBlock(_storageAddress, _chainId, _blockBlob); } } ================================================ FILE: contracts/storage/BlockStore.sol ================================================ pragma solidity ^0.5.12; import "../IonCompatible.sol"; contract BlockStore is IonCompatible { bytes32[] public registeredChains; mapping (bytes32 => bool) public m_chains; modifier onlyIon() { require(msg.sender == address(ion), "Block does not exist for chain"); _; } /* * onlyRegisteredChains * param: _id (bytes32) Unique id of chain supplied to function * * Modifier that checks if the provided chain id has been registered to this contract */ modifier onlyRegisteredChains(bytes32 _chainId) { require(m_chains[_chainId], "Chain is not registered"); _; } /* * Constructor * param: id (bytes32) Unique id to identify this chain that the contract is being deployed to. * * Supplied with a unique id to identify this chain to others that may interoperate with it. * The deployer must assert that the id is indeed public and that it is not already being used * by another chain */ constructor(address _ionAddr) IonCompatible(_ionAddr) public {} /* * addChain * param: id (bytes32) Unique id of another chain to interoperate with * * Supplied with an id of another chain, checks if this id already exists in the known set of ids * and adds it to the list of known m_chains. * *Should be called by the validation registerChain() function */ function addChain(bytes32 _chainId) onlyIon public returns (bool) { require( _chainId != ion.chainId(), "Cannot add this chain id to chain register" ); require(!m_chains[_chainId], "Chain already exists" ); m_chains[_chainId] = true; registeredChains.push(_chainId); return true; } function addBlock(bytes32 _chainId, bytes memory _blockBlob) public; } ================================================ FILE: contracts/storage/EthereumStore.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; import "../libraries/ECVerify.sol"; import "../libraries/RLP.sol"; import "../libraries/PatriciaTrie.sol"; import "../libraries/SolidityUtils.sol"; import "./BlockStore.sol"; contract EthereumStore is BlockStore { using RLP for RLP.RLPItem; using RLP for RLP.Iterator; using RLP for bytes; /* * @description BlockHeader struct containing trie root hashes for tx verifications */ struct BlockHeader { bytes32 txRootHash; bytes32 receiptRootHash; } mapping (bytes32 => bool) public m_blockhashes; mapping (bytes32 => BlockHeader) public m_blockheaders; enum ProofType { TX, RECEIPT, ROOTS } event BlockAdded(bytes32 chainId, bytes32 blockHash); event VerifiedProof(bytes32 chainId, bytes32 blockHash, uint proofType); constructor(address _ionAddr) BlockStore(_ionAddr) public {} /* * onlyExistingBlocks * param: _id (bytes32) Unique id of chain supplied to function * param: _hash (bytes32) Block hash which needs validation * * Modifier that checks if the provided block hash has been verified by the validation contract */ modifier onlyExistingBlocks(bytes32 _hash) { require(m_blockhashes[_hash], "Block does not exist for chain"); _; } /* * @description when a block is submitted the header must be added to a mapping of blockhashes and m_chains to blockheaders * @param _chainId ID of the chain the block is from * @param _blockHash Block hash of the block being added * @param _blockBlob Bytes blob of the RLP-encoded block header being added */ function addBlock(bytes32 _chainId, bytes memory _blockBlob) public onlyIon onlyRegisteredChains(_chainId) { bytes32 blockHash = keccak256(_blockBlob); require(!m_blockhashes[blockHash], "Block already exists" ); RLP.RLPItem[] memory header = _blockBlob.toRLPItem().toList(); require(header.length == 15, "Block Header parameter mismatch"); m_blockhashes[blockHash] = true; m_blockheaders[blockHash].txRootHash = header[4].toBytes32(); m_blockheaders[blockHash].receiptRootHash = header[5].toBytes32(); emit BlockAdded(_chainId, blockHash); } function CheckProofs(bytes32 _chainId, bytes32 _blockHash, bytes memory _proof) public returns (bytes memory) { RLP.RLPItem[] memory proof = _proof.toRLPItem().toList(); require(proof.length == 5, "Malformed proof"); assert(CheckRootsProof(_chainId, _blockHash, proof[2].toBytes(), proof[4].toBytes())); assert(CheckTxProof(_chainId, _blockHash, proof[1].toBytes(), proof[2].toBytes(), proof[0].toBytes())); assert(CheckReceiptProof(_chainId, _blockHash, proof[3].toBytes(), proof[4].toBytes(), proof[0].toBytes())); return proof[3].toBytes(); } /* * CheckTxProof * param: _id (bytes32) Unique id of chain submitting block from * param: _blockHash (bytes32) Block hash of block being submitted * param: _value (bytes) RLP-encoded transaction object array with fields defined as: https://github.com/ethereumjs/ethereumjs-tx/blob/0358fad36f6ebc2b8bea441f0187f0ff0d4ef2db/index.js#L50 * param: _parentNodes (bytes) RLP-encoded array of all relevant nodes from root node to node to prove * param: _path (bytes) Byte array of the path to the node to be proved * * emits: VerifiedTxProof(chainId, blockHash, proofType) * chainId: (bytes32) hash of the chain verifying proof against * blockHash: (bytes32) hash of the block verifying proof against * proofType: (uint) enum of proof type * * All data associated with the proof must be constructed and provided to this function. Modifiers restrict execution * of this function to only allow if the chain the proof is for is registered to this contract and if the block that * the proof is for has been submitted. */ function CheckTxProof( bytes32 _chainId, bytes32 _blockHash, bytes memory _value, bytes memory _parentNodes, bytes memory _path ) onlyRegisteredChains(_chainId) onlyExistingBlocks(_blockHash) internal returns (bool) { verifyProof(_value, _parentNodes, _path, m_blockheaders[_blockHash].txRootHash); emit VerifiedProof(_chainId, _blockHash, uint(ProofType.TX)); return true; } /* * CheckReceiptProof * param: _id (bytes32) Unique id of chain submitting block from * param: _blockHash (bytes32) Block hash of block being submitted * param: _value (bytes) RLP-encoded receipt object array with fields defined as: https://github.com/ethereumjs/ethereumjs-tx/blob/0358fad36f6ebc2b8bea441f0187f0ff0d4ef2db/index.js#L50 * param: _parentNodes (bytes) RLP-encoded array of all relevant nodes from root node to node to prove * param: _path (bytes) Byte array of the path to the node to be proved * * emits: VerifiedTxProof(chainId, blockHash, proofType) * chainId: (bytes32) hash of the chain verifying proof against * blockHash: (bytes32) hash of the block verifying proof against * proofType: (uint) enum of proof type * * All data associated with the proof must be constructed and paddChainrovided to this function. Modifiers restrict execution * of this function to only allow if the chain the proof is for is registered to this contract and if the block that * the proof is for has been submitted. */ function CheckReceiptProof( bytes32 _chainId, bytes32 _blockHash, bytes memory _value, bytes memory _parentNodes, bytes memory _path ) onlyRegisteredChains(_chainId) onlyExistingBlocks(_blockHash) internal returns (bool) { verifyProof(_value, _parentNodes, _path, m_blockheaders[_blockHash].receiptRootHash); emit VerifiedProof(_chainId, _blockHash, uint(ProofType.RECEIPT)); return true; } /* * CheckRootsProof * param: _id (bytes32) Unique id of chain submitting block from * param: _blockHash (bytes32) Block hash of block being submitted * param: _txNodes (bytes) RLP-encoded relevant nodes of the Tx trie * param: _receiptNodes (bytes) RLP-encoded relevant nodes of the Receipt trie * * emits: VerifiedTxProof(chainId, blockHash, proofType) * chainId: (bytes32) hash of the chain verifying proof against * blockHash: (bytes32) hash of the block verifying proof against * proofType: (uint) enum of proof type * * All data associated with the proof must be constructed and provided to this function. Modifiers restrict execution * of this function to only allow if the chain the proof is for is registered to this contract and if the block that * the proof is for has been submitted. */ function CheckRootsProof( bytes32 _chainId, bytes32 _blockHash, bytes memory _txNodes, bytes memory _receiptNodes ) onlyRegisteredChains(_chainId) onlyExistingBlocks(_blockHash) internal returns (bool) { assert( m_blockheaders[_blockHash].txRootHash == getRootNodeHash(_txNodes) ); assert( m_blockheaders[_blockHash].receiptRootHash == getRootNodeHash(_receiptNodes) ); emit VerifiedProof(_chainId, _blockHash, uint(ProofType.ROOTS)); return true; } function verifyProof(bytes memory _value, bytes memory _parentNodes, bytes memory _path, bytes32 _hash) internal { assert( PatriciaTrie.verifyProof(_value, _parentNodes, _path, _hash) ); } /* ======================================================================================================================== Helper Functions ======================================================================================================================== */ /* * @description returns the root node of an RLP encoded Patricia Trie * @param _rlpNodes RLP encoded trie * @returns root hash */ function getRootNodeHash(bytes memory _rlpNodes) private pure returns (bytes32) { RLP.RLPItem[] memory nodeList = _rlpNodes.toRLPItem().toList(); bytes memory b_nodeRoot = RLP.toBytes(nodeList[0]); return keccak256(b_nodeRoot); } } ================================================ FILE: contracts/storage/FabricStore.sol ================================================ pragma solidity ^0.5.12; import "./BlockStore.sol"; import "../libraries/RLP.sol"; import "../libraries/SolidityUtils.sol"; contract FabricStore is BlockStore { using RLP for RLP.RLPItem; using RLP for RLP.Iterator; using RLP for bytes; struct Chain { bytes32 id; mapping (string => Channel) m_channels; } struct Channel { string id; mapping (string => bool) blocks; mapping (string => Block) m_blocks; mapping (string => Transaction) m_transactions; mapping (string => bool) m_transactions_exist; mapping (string => State) m_state; } struct Block { uint number; string hash; string prevHash; string dataHash; uint timestamp_s; uint timestamp_nanos; string[] transactions; } struct Transaction { string id; string blockHash; string[] namespaces; mapping (string => Namespace) m_nsrw; } struct Namespace { string namespace; ReadSet[] reads; WriteSet[] writes; } struct ReadSet { string key; RSVersion version; } struct RSVersion { uint blockNo; uint txNo; } struct WriteSet { string key; bool isDelete; string value; } struct State { string key; RSVersion version; string value; } mapping (bytes32 => Chain) public m_networks; constructor(address _ionAddr) BlockStore(_ionAddr) public {} event BlockAdded(bytes32 chainId, string channelId, string blockHash); function addChain(bytes32 _chainId) onlyIon public returns (bool) { require(super.addChain(_chainId), "Storage addChain parent call failed"); Chain storage chain = m_networks[_chainId]; chain.id = _chainId; return true; } // Function name is inaccurate for Fabric due to blocks being a sub-structure to a channel // Will need refactoring function addBlock(bytes32 _chainId, bytes memory _blockBlob) public onlyIon onlyRegisteredChains(_chainId) { RLP.RLPItem[] memory data = _blockBlob.toRLPItem().toList(); // Iterate all channel objects in the data structure for (uint i = 0; i < data.length; i++) { decodeChannelObject(_chainId, data[i].toBytes()); } } function decodeChannelObject(bytes32 _chainId, bytes memory _channelRLP) internal { RLP.RLPItem[] memory channelRLP = _channelRLP.toRLPItem().toList(); string memory channelId = channelRLP[0].toAscii(); Channel storage channel = m_networks[_chainId].m_channels[channelId]; // Currently adds the channel if it does not exist. This may need changing. if (keccak256(abi.encodePacked(channel.id)) == keccak256(abi.encodePacked(""))) { channel.id = channelId; } // RLP.RLPItem[] memory blocksRLP = channelRLP[1].toList(); // // // Iterate all blocks in the channel structure. Currently not used as we only focus on parsing single blocks // for (uint i = 0; i < blocksRLP.length; i++) { // Block memory block = decodeBlockObject(_chainId, channelId, channelRLP[1].toBytes()); // require(!channel.blocks[block.hash], "Block with identical hash already exists"); // channel.blocks[block.hash] = true; // channel.m_blocks[block.hash] = block; // // emit BlockAdded(_chainId, channelId, block.hash); // } Block memory blk = decodeBlockObject(_chainId, channelId, channelRLP[1].toBytes()); require(!channel.blocks[blk.hash], "Block with identical hash already exists"); mutateState(_chainId, channelId, blk); channel.blocks[blk.hash] = true; channel.m_blocks[blk.hash] = blk; emit BlockAdded(_chainId, channelId, blk.hash); } function decodeBlockObject(bytes32 _chainId, string memory _channelId, bytes memory _blockRLP) internal returns (Block memory) { RLP.RLPItem[] memory blockRLP = _blockRLP.toRLPItem().toList(); string memory blockHash = blockRLP[0].toAscii(); Block memory blk; blk.number = blockRLP[1].toUint(); blk.hash = blockHash; blk.prevHash = blockRLP[2].toAscii(); blk.dataHash = blockRLP[3].toAscii(); blk.timestamp_s = blockRLP[4].toUint(); blk.timestamp_nanos = blockRLP[5].toUint(); RLP.RLPItem[] memory txnsRLP = blockRLP[6].toList(); blk.transactions = new string[](txnsRLP.length); // Iterate all transactions in the block for (uint i = 0; i < txnsRLP.length; i++) { string memory txId = decodeTxObject(txnsRLP[i].toBytes(), _chainId, _channelId); require(!isTransactionExists(_chainId, _channelId, txId), "Transaction already exists"); blk.transactions[i] = txId; injectBlockHashToTx(_chainId, _channelId, txId, blockHash); flagTx(_chainId, _channelId, txId); } return blk; } function decodeTxObject(bytes memory _txRLP, bytes32 _chainId, string memory _channelId) internal returns (string memory) { RLP.RLPItem[] memory txRLP = _txRLP.toRLPItem().toList(); Transaction storage txn = m_networks[_chainId].m_channels[_channelId].m_transactions[txRLP[0].toAscii()]; txn.id = txRLP[0].toAscii(); RLP.RLPItem[] memory namespacesRLP = txRLP[1].toList(); // Iterate all namespace rwsets in the transaction for (uint i = 0; i < namespacesRLP.length; i++) { RLP.RLPItem[] memory nsrwRLP = namespacesRLP[i].toList(); Namespace storage namespace = txn.m_nsrw[nsrwRLP[0].toAscii()]; namespace.namespace = nsrwRLP[0].toAscii(); txn.namespaces.push(nsrwRLP[0].toAscii()); // Iterate all read sets in the namespace RLP.RLPItem[] memory readsetsRLP = nsrwRLP[1].toList(); for (uint j = 0; j < readsetsRLP.length; j++) { namespace.reads.push(decodeReadset(readsetsRLP[j].toBytes())); } // Iterate all write sets in the namespace RLP.RLPItem[] memory writesetsRLP = nsrwRLP[2].toList(); for (uint k = 0; k < writesetsRLP.length; k++) { namespace.writes.push(decodeWriteset(writesetsRLP[k].toBytes())); } } return txRLP[0].toAscii(); } function mutateState(bytes32 _chainId, string memory _channelId, Block memory _blk) internal { string[] memory txIds = _blk.transactions; // Iterate across all transactions for (uint i = 0; i < txIds.length; i++) { Transaction storage txn = m_networks[_chainId].m_channels[_channelId].m_transactions[txIds[i]]; // Iterate across all namespaces for (uint j = 0; j < txn.namespaces.length; j++) { string storage namespace = txn.namespaces[j]; // Iterate across all writesets and check readset version of each write key against stored version for (uint k = 0; k < txn.m_nsrw[namespace].writes.length; k++) { State storage state = m_networks[_chainId].m_channels[_channelId].m_state[txn.m_nsrw[namespace].writes[k].key]; if (keccak256(abi.encodePacked(state.key)) == keccak256(abi.encodePacked(txn.m_nsrw[namespace].writes[k].key))) { if (!isExpectedReadVersion(txn.m_nsrw[namespace], state.version, state.key)) continue; } state.key = txn.m_nsrw[namespace].writes[k].key; state.version = RSVersion(_blk.number, i); state.value = txn.m_nsrw[namespace].writes[k].value; } } } } function injectBlockHashToTx(bytes32 _chainId, string memory _channelId, string memory _txId, string memory _blockHash) internal { Transaction storage txn = m_networks[_chainId].m_channels[_channelId].m_transactions[_txId]; txn.blockHash = _blockHash; } function flagTx(bytes32 _chainId, string memory _channelId, string memory _txId) internal { m_networks[_chainId].m_channels[_channelId].m_transactions_exist[_txId] = true; } function decodeReadset(bytes memory _readsetRLP) internal pure returns (ReadSet memory) { RLP.RLPItem[] memory readsetRLP = _readsetRLP.toRLPItem().toList(); string memory key = readsetRLP[0].toAscii(); RLP.RLPItem[] memory rsv = readsetRLP[1].toList(); uint blockNo = rsv[0].toUint(); uint txNo = 0; if (rsv.length > 1) { txNo = rsv[1].toUint(); } RSVersion memory version = RSVersion(blockNo, txNo); return ReadSet(key, version); } function decodeWriteset(bytes memory _writesetRLP) internal pure returns (WriteSet memory){ RLP.RLPItem[] memory writesetRLP = _writesetRLP.toRLPItem().toList(); string memory key = writesetRLP[0].toAscii(); string memory value = writesetRLP[2].toAscii(); bool isDelete = false; string memory isDeleteStr = writesetRLP[1].toAscii(); if (keccak256(abi.encodePacked(isDeleteStr)) == keccak256(abi.encodePacked("true"))) { isDelete = true; } return WriteSet(key, isDelete, value); } function isExpectedReadVersion(Namespace memory _namespace, RSVersion memory _version, string memory _key) internal pure returns (bool) { ReadSet[] memory reads = _namespace.reads; for (uint i = 0; i < reads.length; i++) { ReadSet memory readset = reads[i]; if (keccak256(abi.encodePacked(readset.key)) == keccak256(abi.encodePacked(_key))) return isSameVersion(readset.version, _version); } return false; } function isSameVersion(RSVersion memory _v1, RSVersion memory _v2) internal pure returns (bool) { if (_v1.blockNo != _v2.blockNo) return false; if (_v1.txNo != _v2.txNo) return false; return true; } function getBlock(bytes32 _chainId, string memory _channelId, string memory _blockHash) public view returns (uint, string memory, string memory, string memory, uint, uint, string memory) { Block storage blk = m_networks[_chainId].m_channels[_channelId].m_blocks[_blockHash]; require(keccak256(abi.encodePacked(blk.hash)) != keccak256(abi.encodePacked("")), "Block does not exist."); string memory txs = blk.transactions[0]; for (uint i = 1; i < blk.transactions.length; i++) { txs = string(abi.encodePacked(txs, ",", blk.transactions[i])); } return (blk.number, blk.hash, blk.prevHash, blk.dataHash, blk.timestamp_s, blk.timestamp_nanos, txs); } function getTransaction(bytes32 _chainId, string memory _channelId, string memory _txId) public view returns (string memory, string memory) { Transaction storage txn = m_networks[_chainId].m_channels[_channelId].m_transactions[_txId]; require(isTransactionExists(_chainId, _channelId, _txId), "Transaction does not exist."); string memory ns = txn.namespaces[0]; for (uint i = 1; i < txn.namespaces.length; i++) { ns = string(abi.encodePacked(ns, ",", txn.namespaces[i])); } return (txn.blockHash, ns); } function isTransactionExists(bytes32 _chainId, string memory _channelId, string memory _txId) public view returns (bool) { return m_networks[_chainId].m_channels[_channelId].m_transactions_exist[_txId]; } function getNSRW(bytes32 _chainId, string memory _channelId, string memory _txId, string memory _namespace) public view returns (string memory, string memory) { Namespace storage ns = m_networks[_chainId].m_channels[_channelId].m_transactions[_txId].m_nsrw[_namespace]; require(keccak256(abi.encodePacked(ns.namespace)) != keccak256(abi.encodePacked("")), "Namespace does not exist."); string memory reads; for (uint i = 0; i < ns.reads.length; i++) { RSVersion storage version = ns.reads[i].version; reads = string(abi.encodePacked(reads, "{ key: ", ns.reads[i].key, ", version: { blockNo: ", SolUtils.UintToString(version.blockNo), ", txNo: ", SolUtils.UintToString(version.txNo), " } } ")); } string memory writes; for (uint j = 0; j < ns.writes.length; j++) { writes = string(abi.encodePacked(writes, "{ key: ", ns.writes[j].key, ", isDelete: ", SolUtils.BoolToString(ns.writes[j].isDelete), ", value: ", ns.writes[j].value, " } ")); } return (reads, writes); } function getState(bytes32 _chainId, string memory _channelId, string memory _key) public view returns (uint, uint, string memory) { State storage state = m_networks[_chainId].m_channels[_channelId].m_state[_key]; require(keccak256(abi.encodePacked(state.key)) != keccak256(abi.encodePacked("")), "Key unrecognised."); return (state.version.blockNo, state.version.txNo, state.value); } } ================================================ FILE: contracts/test/PatriciaTrieTest.sol ================================================ pragma solidity ^0.5.12; import "../libraries/PatriciaTrie.sol"; contract PatriciaTrieTest { event Result(bool result); function testVerify(bytes memory _value, bytes memory _parentNodes, bytes memory _path, bytes32 _root) public returns (bool) { bool result = PatriciaTrie.verifyProof(_value, _parentNodes, _path, _root); emit Result(result); return result; } } ================================================ FILE: contracts/validation/Base.sol ================================================ pragma solidity ^0.5.12; import "../IonCompatible.sol"; import "../storage/BlockStore.sol"; contract Base is IonCompatible { constructor (address _ionAddr) IonCompatible(_ionAddr) public {} function register() public returns (bool) { ion.registerValidationModule(); return true; } function RegisterChain(bytes32 _chainId, address _storeAddr) public { require( _chainId != ion.chainId(), "Cannot add this chain id to chain register" ); ion.addChain(_storeAddr, _chainId); } function SubmitBlock(bytes32 _chainId, bytes memory _rlpBlock, address _storageAddr) public { storeBlock(_chainId, _rlpBlock, _storageAddr); } function storeBlock( bytes32 _chainId, bytes memory _rlpBlock, address _storageAddr ) internal { // Add block to Ion ion.storeBlock(_storageAddr, _chainId, _rlpBlock); } } ================================================ FILE: contracts/validation/Clique.sol ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; import "../libraries/ECVerify.sol"; import "../libraries/RLP.sol"; import "../libraries/SolidityUtils.sol"; import "../IonCompatible.sol"; import "../storage/BlockStore.sol"; /* Smart contract for validation of blocks that use the Clique PoA consensus algorithm Blocks must be submitted sequentially due to the voting mechanism of Clique. */ contract Clique is IonCompatible { using RLP for RLP.RLPItem; using RLP for RLP.Iterator; using RLP for bytes; /* * @description persists the last submitted block of a chain being validated */ struct BlockHeader { uint256 blockNumber; bytes32 blockHash; bytes32 prevBlockHash; bytes32 txRootHash; bytes32 receiptRootHash; } struct Metadata { address[] validators; mapping (address => bool) m_validators; mapping (address => uint256) m_proposals; uint256 threshold; } event GenesisCreated(bytes32 chainId, bytes32 blockHash); event BlockSubmitted(bytes32 chainId, bytes32 blockHash); /* * onlyRegisteredChains * param: _id (bytes32) Unique id of chain supplied to function * * Modifier that checks if the provided chain id has been registered to this contract */ modifier onlyRegisteredChains(bytes32 _id) { require(chains[_id], "Chain is not registered"); _; } mapping (bytes32 => bool) public chains; mapping (bytes32 => mapping (bytes32 => bool)) public m_blockhashes; mapping (bytes32 => mapping (bytes32 => BlockHeader)) public m_blockheaders; mapping (bytes32 => mapping (bytes32 => Metadata)) public m_blockmetadata; mapping (bytes32 => bytes32[]) public heads; constructor (address _ionAddr) IonCompatible(_ionAddr) public {} /* ===================================================================================================================== Public Functions ===================================================================================================================== */ function register() public returns (bool) { ion.registerValidationModule(); return true; } /* * RegisterChain * param: _chainId (bytes32) Unique id of another chain to interoperate with * param: _validators (address[]) Array containing the validators at the genesis block * param: _genesisHash (bytes32) Hash of the genesis block for the chain being registered with Ion * param: _storeAddr (address) Address of block store contract to register chain to * * Registers knowledge of the id of another interoperable chain requiring the genesis block metadata. Allows * the initialising of genesis blocks and their validator sets for chains. Multiple may be submitted and built upon * and is not opinionated on how they are used. */ function RegisterChain(bytes32 _chainId, address[] memory _validators, bytes32 _genesisBlockHash, address _storeAddr) public { require( _chainId != ion.chainId(), "Cannot add this chain id to chain register" ); if (chains[_chainId]) { require( !m_blockhashes[_chainId][_genesisBlockHash], "Chain already exists with identical genesis" ); } else { chains[_chainId] = true; ion.addChain(_storeAddr, _chainId); } setGenesisBlock(_chainId, _validators, _genesisBlockHash); } /* * SubmitBlock * param: _chainId (bytes32) Unique id of chain submitting block from * param: _rlpBlockHeader (bytes) RLP-encoded byte array of the block header from other chain without the signature in extraData * param: _rlpSignedBlockHeader (bytes) RLP-encoded byte array of the block header from other chain with the signature in extraData * param: _storeAddr (address) Address of block store contract to store block to * * Submission of block headers from another chain. Signatures held in the extraData field of _rlpSignedBlockHeader is recovered * and if valid the block is persisted as BlockHeader structs defined above. */ function SubmitBlock(bytes32 _chainId, bytes memory _rlpUnsignedBlockHeader, bytes memory _rlpSignedBlockHeader, address _storageAddr) onlyRegisteredChains(_chainId) public { RLP.RLPItem[] memory header = _rlpUnsignedBlockHeader.toRLPItem().toList(); RLP.RLPItem[] memory signedHeader = _rlpSignedBlockHeader.toRLPItem().toList(); require( header.length == signedHeader.length, "Header properties length mismatch" ); // Check header and signedHeader contain the same data for (uint256 i=0; i= parentMetadata.threshold && !parentMetadata.m_validators[_candidate]) { newVoteCount = 0; for (uint i = 0; i < parentMetadata.validators.length; i++) { newValidators.push(parentMetadata.validators[i]); } newValidators.push(_candidate); } else if ( (parentMetadata.m_proposals[_candidate] + 1) >= parentMetadata.threshold && parentMetadata.m_validators[_candidate]) { newVoteCount = 0; for (uint j = 0; j < parentMetadata.validators.length; j++) { if (parentMetadata.validators[j] != _candidate) { newValidators.push(parentMetadata.validators[j]); } } } else { newVoteCount = parentMetadata.m_proposals[_candidate] + 1; for (uint k = 0; k < parentMetadata.validators.length; k++) { newValidators.push(parentMetadata.validators[k]); } } metadata.m_proposals[_candidate] = newVoteCount; newThreshold = (newValidators.length/2) + 1; for (uint vi = 0; vi < newValidators.length; vi++) { metadata.m_validators[newValidators[vi]] = true; if (newValidators[vi] != _candidate) { metadata.m_proposals[newValidators[vi]] = parentMetadata.m_proposals[newValidators[vi]]; } } } else { // If no vote, set current block metadata equal to parent block metadata.validators = parentMetadata.validators; metadata.threshold = parentMetadata.threshold; for (uint pi = 0; pi < parentMetadata.validators.length; pi++) { metadata.m_validators[parentMetadata.validators[pi]] = true; metadata.m_proposals[parentMetadata.validators[pi]] = parentMetadata.m_proposals[parentMetadata.validators[pi]]; } } } /* * storeBlock * param: _chainId (bytes32) Unique id of interoperating chain * param: _hash (address) Byte array of the extra data containing signature * param: _parentHash (bytes32) Current block hash being checked * param: _txRootHash (bytes32) Parent block hash of current block being checked * param: _receiptRootHash (bytes32) Parent block hash of current block being checked * param: _height (bytes32) Parent block hash of current block being checked * param: _rlpBlockHeader (bytes32) Parent block hash of current block being checked * param: _storageAddr (bytes32) Parent block hash of current block being checked * * Takes the submitted block to propagate to the storage contract. */ function storeBlock( bytes32 _chainId, bytes32 _hash, bytes32 _parentHash, bytes32 _txRootHash, bytes32 _receiptRootHash, uint256 _height, bytes memory _rlpBlockHeader, address _storageAddr ) internal { m_blockhashes[_chainId][_hash] = true; BlockHeader storage header = m_blockheaders[_chainId][_hash]; header.blockNumber = _height; header.blockHash = _hash; header.prevBlockHash = _parentHash; header.txRootHash = _txRootHash; header.receiptRootHash = _receiptRootHash; // Add block to Ion ion.storeBlock(_storageAddr, _chainId, _rlpBlockHeader); } /* * shiftHead * param: _chainId (bytes32) Unique id of chain * param: _childHash (bytes32) New block hash * param: _parentHash (bytes32) Previous block hash * * Updates set of current open chain heads per chain. Open chain heads are blocks that do not have a child that can * be built upon. */ function shiftHead(bytes32 _chainId, bytes32 _childHash, bytes32 _parentHash) public { int index = -1; bytes32[] storage chainHeads = heads[_chainId]; // Check if parent hash is an open head and replace with child for (uint i = 0; i < chainHeads.length; i++) { if (chainHeads[i] == _parentHash) { index = int(i); delete chainHeads[uint(index)]; chainHeads[uint(index)] = _childHash; return; } } // If parent is not an open head, child is, so append to heads chainHeads.push(_childHash); } function getValidators(bytes32 _chainId, bytes32 _blockHash) public view returns (address[] memory) { return m_blockmetadata[_chainId][_blockHash].validators; } function getProposal(bytes32 _chainId, bytes32 _blockHash, address _candidate) public view returns (uint256) { return m_blockmetadata[_chainId][_blockHash].m_proposals[_candidate]; } } ================================================ FILE: contracts/validation/IBFT.sol ================================================ // Copyright (c) 2016-2019 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ pragma solidity ^0.5.12; import "../libraries/ECVerify.sol"; import "../libraries/RLP.sol"; import "../libraries/SolidityUtils.sol"; import "../IonCompatible.sol"; import "../storage/BlockStore.sol"; /* Smart contract for validation of blocks that use the IBFT-Soma consensus algorithm Blocks must be submitted sequentially due to the voting mechanism of IBFT-Soma. */ contract IBFT is IonCompatible { using RLP for RLP.RLPItem; using RLP for RLP.Iterator; using RLP for bytes; /* * @description persists the last submitted block of a chain being validated */ struct BlockHeader { uint256 blockNumber; bytes32 blockHash; bytes32 prevBlockHash; address[] validators; uint256 threshold; } event GenesisCreated(bytes32 chainId, bytes32 blockHash); event BlockSubmitted(bytes32 chainId, bytes32 blockHash); /* * onlyRegisteredChains * param: _id (bytes32) Unique id of chain supplied to function * * Modifier that checks if the provided chain id has been registered to this contract */ modifier onlyRegisteredChains(bytes32 _id) { require(chains[_id], "Chain is not registered"); _; } mapping (bytes32 => bool) public chains; mapping (bytes32 => bytes32) public m_chainHeads; mapping (bytes32 => mapping (bytes32 => BlockHeader)) public m_blockheaders; constructor (address _ionAddr) IonCompatible(_ionAddr) public {} /* ===================================================================================================================== Public Functions ===================================================================================================================== */ function register() public returns (bool) { ion.registerValidationModule(); return true; } /* * RegisterChain * param: _chainId (bytes32) Unique id of another chain to interoperate with * param: _validators (address[]) Array containing the validators at the genesis block * param: _genesisHash (bytes32) Hash of the genesis block for the chain being registered with Ion * param: _storeAddr (address) Address of block store contract to register chain to * * Registers knowledge of the id of another interoperable chain requiring the genesis block metadata. Allows * the initialising of genesis blocks and their validator sets for chains. Multiple may be submitted and built upon * and is not opinionated on how they are used. */ function RegisterChain(bytes32 _chainId, address[] memory _validators, bytes32 _genesisBlockHash, address _storeAddr) public { require(_chainId != ion.chainId(), "Cannot add this chain id to chain register"); if (chains[_chainId]) { require(m_chainHeads[_chainId] == bytes32(0x0), "Chain already exists"); } else { chains[_chainId] = true; ion.addChain(_storeAddr, _chainId); } addGenesisBlock(_chainId, _validators, _genesisBlockHash); } /* * SubmitBlock * param: _chainId (bytes32) Unique id of chain submitting block from * param: _rlpUnsignedBlockHeader (bytes) RLP-encoded byte array of the block header from IBFT-Soma chain containing only validator set in IstanbulExtra field * param: _rlpSignedBlockHeader (bytes) RLP-encoded byte array of the block header from other chain including all proposal seal in the IstanbulExtra field * param: _commitSeals (bytes) RLP-encoded commitment seals that are typically contained in the last element of the IstanbulExtra field * param: _storeAddr (address) Address of block store contract to store block to * * Submission of block headers from another chain. */ function SubmitBlock(bytes32 _chainId, bytes memory _rlpUnsignedBlockHeader, bytes memory _rlpSignedBlockHeader, bytes memory _commitSeals, address _storageAddr) onlyRegisteredChains(_chainId) public { RLP.RLPItem[] memory header = _rlpSignedBlockHeader.toRLPItem().toList(); // Check the parent hash is the same as the previous block submitted bytes32 parentBlockHash = SolUtils.BytesToBytes32(header[0].toBytes(), 1); require(m_chainHeads[_chainId] == parentBlockHash, "Not child of previous block!"); // Verify that validator and sealers are correct require(checkSignature(_chainId, header[12].toData(), keccak256(_rlpUnsignedBlockHeader), parentBlockHash), "Signer is not validator"); require(checkSeals(_chainId, _commitSeals, _rlpSignedBlockHeader, parentBlockHash), "Sealer(s) not valid"); // Append new block to the struct addValidators(_chainId, header[12].toData(), keccak256(_rlpSignedBlockHeader)); storeBlock(_chainId, keccak256(_rlpSignedBlockHeader), parentBlockHash, header[8].toUint(), _rlpSignedBlockHeader, _storageAddr); emit BlockSubmitted(_chainId, keccak256(_rlpSignedBlockHeader)); } /* ===================================================================================================================== Internal Functions ===================================================================================================================== */ /* * addGenesisBlock * param: _chainId (bytes32) Unique id of another chain to interoperate with * param: _validators (address[]) Array containing the validators at the genesis block * param: _genesisHash (bytes32) Hash of the genesis block for the chain being registered with Ion * * Adds a genesis block with the validators and other metadata for this genesis block */ function addGenesisBlock(bytes32 _chainId, address[] memory _validators, bytes32 _genesisBlockHash) internal { BlockHeader storage header = m_blockheaders[_chainId][_genesisBlockHash]; header.blockNumber = 0; header.blockHash = _genesisBlockHash; header.validators = _validators; header.threshold = 2*(_validators.length/3) + 1; m_chainHeads[_chainId] = _genesisBlockHash; emit GenesisCreated(_chainId, _genesisBlockHash); } /* * checkSignature * param: _chainId (bytes32) Unique id of interoperating chain * param: _extraData (bytes) Byte array of the extra data containing signature * param: _hash (bytes32) Hash of the unsigned block header * param: _parentBlockHash (bytes32) Parent block hash of current block being checked * * Checks that the submitted block has actually been signed, recovers the signer and checks if they are validator in * parent block */ function checkSignature(bytes32 _chainId, bytes memory _extraData, bytes32 _hash, bytes32 _parentBlockHash) internal view returns (bool) { // Retrieve Istanbul Extra Data bytes memory istanbulExtra = new bytes(_extraData.length - 32); SolUtils.BytesToBytes(istanbulExtra, _extraData, 32); RLP.RLPItem[] memory signature = istanbulExtra.toRLPItem().toList(); bytes memory extraDataSig = new bytes(65); SolUtils.BytesToBytes(extraDataSig, signature[1].toBytes(), signature[1].toBytes().length-65); // Recover the signature address sigAddr = ECVerify.ecrecovery(keccak256(abi.encode(_hash)), extraDataSig); BlockHeader storage parentBlock = m_blockheaders[_chainId][_parentBlockHash]; // Check if signature is a validator that exists in previous block return isValidator(parentBlock.validators, sigAddr); } /* * checkSeals * param: _chainId (bytes32) Unique id of interoperating chain * param: _seals (bytes) RLP-encoded list of 65 byte seals * param: _rlpBlock (bytes) Byte array of RLP encoded unsigned block header * param: _parentBlockHash (bytes32) Parent block hash of current block being checked * * Checks that the submitted block has enough seals to be considered valid as per the IBFT Soma rules */ function checkSeals(bytes32 _chainId, bytes memory _seals, bytes memory _rlpBlock, bytes32 _parentBlockHash) internal view returns (bool) { bytes32 signedHash = keccak256(abi.encodePacked(keccak256(_rlpBlock), byte(0x02))); BlockHeader storage parentBlock = m_blockheaders[_chainId][_parentBlockHash]; uint256 validSeals = 0; // Check if signature is a validator that exists in previous block RLP.RLPItem[] memory seals = _seals.toRLPItem().toList(); for (uint i = 0; i < seals.length; i++) { // Recover the signature address sigAddr = ECVerify.ecrecovery(signedHash, seals[i].toData()); if (!isValidator(parentBlock.validators, sigAddr)) return false; validSeals++; } if (validSeals < parentBlock.threshold) return false; return true; } function isValidator(address[] memory _validators, address _validator) internal pure returns (bool) { for (uint i = 0; i < _validators.length; i++) { if (_validator == _validators[i]) return true; } return false; } /* * addValidators * param: _chainId (bytes32) Unique id of interoperating chain * param: _extraData (bytes) Byte array of the extra data containing signature * param: _blockHash (bytes32) Current block hash being checked * param: _parentBlockHash (bytes32) Parent block hash of current block being checked * * Updates the validators from the RLP encoded extradata */ function addValidators(bytes32 _chainId, bytes memory _extraData, bytes32 _blockHash) internal { BlockHeader storage newBlock = m_blockheaders[_chainId][_blockHash]; // Retrieve Istanbul Extra Data bytes memory rlpIstanbulExtra = new bytes(_extraData.length - 32); SolUtils.BytesToBytes(rlpIstanbulExtra, _extraData, 32); RLP.RLPItem[] memory istanbulExtra = rlpIstanbulExtra.toRLPItem().toList(); RLP.RLPItem[] memory decodedExtra = istanbulExtra[0].toBytes().toRLPItem().toList(); for (uint i = 0; i < decodedExtra.length; i++) { address validator = decodedExtra[i].toAddress(); newBlock.validators.push(validator); } newBlock.threshold = 2*(newBlock.validators.length/3) + 1; } /* * storeBlock * param: _chainId (bytes32) Unique id of interoperating chain * param: _hash (address) Byte array of the extra data containing signature * param: _parentHash (bytes32) Current block hash being checked * param: _height (bytes32) Parent block hash of current block being checked * param: _rlpBlockHeader (bytes32) Parent block hash of current block being checked * param: _storageAddr (bytes32) Parent block hash of current block being checked * * Takes the submitted block to propagate to the storage contract. */ function storeBlock( bytes32 _chainId, bytes32 _hash, bytes32 _parentHash, uint256 _height, bytes memory _rlpBlockHeader, address _storageAddr ) internal { m_chainHeads[_chainId] = _hash; BlockHeader storage header = m_blockheaders[_chainId][_hash]; header.blockNumber = _height; header.blockHash = _hash; header.prevBlockHash = _parentHash; delete m_blockheaders[_chainId][_parentHash]; // Add block to Ion ion.storeBlock(_storageAddr, _chainId, _rlpBlockHeader); } function getValidators(bytes32 _chainId) public view returns (address[] memory) { return m_blockheaders[_chainId][m_chainHeads[_chainId]].validators; } } ================================================ FILE: docker_build/account/keystore/UTC--2018-06-05T09-31-57.109288703Z--2be5ab0e43b6dc2908d5321cf318f35b80d0c10d ================================================ {"address":"2be5ab0e43b6dc2908d5321cf318f35b80d0c10d","crypto":{"cipher":"aes-128-ctr","ciphertext":"0b11aa865046778a1b16a9b8cb593df704e3fe09f153823d75442ad1aab66caa","cipherparams":{"iv":"4aa66b789ee2d98cf77272a72eeeaa50"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"b957fa7b7577240fd3791168bbe08903af4c8cc62c304f1df072dc2a59b1765e"},"mac":"197a06eb0449301d871400a6bdf6c136b6f7658ee41e3f2f7fd81ca11cd954a3"},"id":"a3cc1eae-3e36-4659-b759-6cf416216e72","version":3} ================================================ FILE: docker_build/account/password-2be5ab0e43b6dc2908d5321cf318f35b80d0c10d.txt ================================================ password1 ================================================ FILE: docker_build/clique.json ================================================ { "config": { "chainId": 1515, "homesteadBlock": 1, "eip150Block": 2, "eip150Hash": "0x0000000000000000000000000000000000000000000000000000000000000000", "eip155Block": 3, "eip158Block": 3, "byzantiumBlock": 4, "clique": { "period": 1, "epoch": 30000 } }, "nonce": "0x0", "timestamp": "0x5b165989", "extraData": "0x00000000000000000000000000000000000000000000000000000000000000002be5ab0e43b6dc2908d5321cf318f35b80d0c10d0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "gasLimit": "0xFFFFFFFFFFFF", "difficulty": "0x1", "mixHash": "0x0000000000000000000000000000000000000000000000000000000000000000", "coinbase": "0x0000000000000000000000000000000000000000", "alloc": { "2be5ab0e43b6dc2908d5321cf318f35b80d0c10d": { "balance": "0x200000000000000000000000000000000000000000000000000000000000000" } }, "number": "0x0", "gasUsed": "0x0", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" } ================================================ FILE: docker_build/launch_geth.sh ================================================ !#/bin/bash geth --datadir docker_build/account/ --syncmode 'full' --port 30311 --rpc --rpcaddr '0.0.0.0' --rpcport 8545 --networkid 1515 --gasprice '0' --targetgaslimit 0xFFFFFFFFFFFF --unlock '0x2be5ab0e43b6dc2908d5321cf318f35b80d0c10d' --password docker_build/account/password-2be5ab0e43b6dc2908d5321cf318f35b80d0c10d.txt --mine ================================================ FILE: docker_build/password ================================================ here_is_password ================================================ FILE: docs/Ion-CLI.md ================================================ # Ion Command Line Interface The Ion CLI is a tool which allows users to easily interact with the Ion project. Written in golang it allows rapid development of new commands and contracts by leveraging the [ishell](https://github.com/abiosoft/ishell) and [go-ethereum smart contract bindings](https://github.com/ethereum/go-ethereum/wiki/Native-DApps:-Go-bindings-to-Ethereum-contracts). ***Note:*** The Ion CLI is not a trusted part of the Ion infrastructure rather it is just a tool to facilitate users, who should verify it functionality prior to using any unknown code. ## Running Ion CLI In order to compile the Ion CLI run: ``` $ cd /path/to/validation/src $ make build $ make test ``` Given all tests pass, the Ion CLI can be run. Prior to running the user must ensure that the `setup.json` file has been modified to contain: * Address and port of foreign Clique chain rpc * Address and port of native chain rpc * User account on foreign Clique chain * User account on native chain * Address of the deployed validation contract on native Once this has been setup correctly the CLI can be launched as follows: ``` $ ./ion-cli -config [/path/to/setup.json] =============================================================== Ion Command Line Interface RPC Client [to]: Listening on: 127.0.0.1:8501 User Account: 0x2be5ab0e43b6dc2908d5321cf318f35b80d0c10d Ion Contract: 0xb9fd43a71c076f02d1dbbf473c389f0eacec559f RPC Client [from]: Listening on: 127.0.0.1:8545 User Account: 0x8671e5e08d74f338ee1c462340842346d797afd3 =============================================================== >>> ``` Running help displays the available commands: ``` >>> help Commands: clear clear the screen exit exit the program getBlock use: getBlock [integer] description: Returns block header specified getValidators use: getValidators description: Returns the whitelist of validators from validator contract help display help latestBlock use: latestBlock description: Returns number of latest block mined/sealed latestValidationBlock use: latestValidationBlock description: Returns hash of the last block submitted to the validation contract submitValidationBlock use: submitValidationBlock [integer] description: Returns the RLP block header, signed block prefix, extra data prefix and submits to validation contract ``` ### Tutorial ## Extending the Ion CLI In order to add your contract to the Ion CLI first a golang version of the solidity smart contract needs to be created, to do this we follow the instructions from [go-ethereum smart contract bindings](https://github.com/ethereum/go-ethereum/wiki/Native-DApps:-Go-bindings-to-Ethereum-contracts). We will add a contract called `Spoon.sol` to the CLI. This requires the generation of the `abi` and `bin` files. To do this run: ``` $ npm run genbin $ npm run genabi ``` Now the latest versions of the `abi` and `bin` files will be found in the `/path/to/ion/contracts/` directory. Next generate the `.go` version of the desired smart contract using the `abigen` to do so run: ``` $ abigen --bin=/path/to/Spoon.bin --abi /path/to/Spoon.abi --pkg contract --type Spoon --out Spoon.go ``` next place the output `Spoon.go` in the package specific directory for your golang code. The contract can then be interfaced with simply through importing the contract package. ### Golang Smart Contract Interface Given the exisiting Ion CLI framework any additional contracts should be placed in the `ion/ion-cli/contracts/` directory and appended to the contract package. To use an instance of the Spoon contract insert: ``` func InitSpoonContract(setup Setup, client *ethclient.Client) (Spoon *contract.Spoon) { // Initialise the contract address := common.HexToAddress(setup.Ion) Spoon, err := contract.NewSpoon(address, client) if err != nil { log.Fatal(err) } return } ``` ================================================ FILE: docs/Roadmap.md ================================================ # Ion Stage 2: Phase 1 Roadmap Ion Stage 2 separates the cross chain payment use case from the interoperability solution. Focus has moved away from pure atomic exchange of value across chains, towards a mechanism to prove state across chains. Given two blockchains A and B the state of B should be verifiable on A and vice-versa. To do this a smart-contract should be developed which effectively becomes a light client of the corresponding blockchain. This facilitates interoperability on a much larger scale than simply value transactions. However the cross chain payment still serves as an illustrative example of how the solution would work in a specific scenario, but it is not part of the core solution to be developed. The Ion Stage 2 relies on a smart contract that allows storing and verification of the state of another chain. The verification of blocks from a foreign blockchain on a native chain should leverage the underlying consensus of the chain that is to be passed. In Phase 1 we intend to tackle two different aspects: * Storing and proving of state from a foreign chain * Validation of passed foreign state We assume the context of the validator set is an existing known set of nodes that also engage in the consensus protocol of a foreign chain so as to ensure the validity of signed state. ## Nomenclature We define are a common set of terminology which will be used herein that have a specific meaning or context within the project. * Native Chain: Refers to the chain where active interaction is taking place. This is where state from the foreign chain will be sent to for persistence and be verified against. * Foreign Chain: Refers to the chain whose state is being persisted. Signed data about blocks from this chain will be passed to the native chain and stored. The above naming scheme applies in the context of describing interaction flow in a one-way setting where state is passed from one chain to another. In more complex flow where both chains are actively interacted with, this naming convention may be omitted to reduce confusion. * Proof: Refers to merkle proof-like mechanisms to assert the existence of an item or event in a block or root hash * Validation: Refers to the signing and verifying of signatures of a piece of data, usually the block hash/header of a foreign chain * State: Refers to the data captured and transferred between chains that facilitates the ability to prove a state transition of another chain. This will consist of another chain's block header and block hash. ## Targets State-proving is our fundamental goal but we frame our use-case around performing a cross-chain PvP payment. Any programmable contract can interface with the Relay contract and we outline an initial example through our use-case of how this would be achieved. In order to perform a cross-chain PvP payment we must: * Prove state of blockchain A on B * Verify the signatures of block signatories from a different blockchain * Settle a cross-chain transaction between two counterparties via the above mechanisms * Provide well-documented interfaces to allow users to easily interact with the project ### Assumptions Listed here are the assumptions with which the project is being developed: * Ethereum-based blockchain * IBFT Consensus Protocol or other immediate-finality algorithms * Permissioned network * Validator set is known and assumed as correct ## Project Planning Ion stage 2 will be developed using agile methodologies, with fortnightly sprints. Note that the sprint objective will remain dynamic and should change as the project continues. ## Project Planning Ion stage 2 will be developed using agile methodologies, with fortnightly sprints. Note that the sprint objective will remain dynamic and should change as the project continues. ### Sprint 6 - Minimal Viabable Ion Stage 2 Date: 16.07.2018 - 23.07.2018 Description: Various separated components should be integrated with required tooling and documentation. Goals: * Ability to submit and verify proofs against Ion Relay contract * Execute contract given state transition Achieved: ### Sprint 5 - Off-Chain Proof Generation Date: 02.07.2018 - 13.07.2018 Description: We aim to be able to generate proofs _off-chain_, preferably using the Ion CLI. This is the key part to being able to make claims against state _on-chain_. Goals: * Research indepth the complexities of creating continually executing smart-contracts * Update Ion specification * Generate off-chain proofs of state transition: Solidity, Golang, and Ion CLI Integration * Begin research into the outline of potential use cases i.e. PvP * Increase testing coverage of smart contracts and Ion CLI Achieved: ### Sprint 4 - User Flow Development Date: 25.06.2018 - 29.06.2018 Description: Given the original user stories the smart contract should now contain minimum functions necessary to interact with the project. This should naturally be an extension of the previous week to smooth out the integration and interaction flows of the stack. Goals: * Smart contract should now have protection for edge-cases * Addition of user permissioniong * Automation of block generation * Tutorial CLI and Validation contract * CLI Golang Achieved: * Automation of block generation * Tutorial CLI and Validation contract * CLI Golang Notes: * Sprints changed to fortnightly from this point onwards ### Sprint 3 - Validation of Passed State Date: 18.06.2018 - 22.06.2018 Description: The two separate problems of validation and proofs should be integrated and a minimum smart-contract that allows the immediate validation of a submitted block be developed. Goals: * Single contract which allows state proof and block validation to be performed simultaneously Achieved: ### Sprint 2 - Skeleton Implementation Date: 11.06.2018 - 15.06.2018 Description: It should be shown that it is indeed possible to prove the state of a foreign on a native chain and make assertions of that state. Separately it should be shown that the validators from foreign chain can be added to the native chain. Blocks submitted and validated on the foreign chain validated on the native chain using the signature of the foreign validator set. Goals: * Smart contract for state proof verification * Tests for state proofs * Smart contract for block validation * Tests for block validation Achieved: ### Sprint 1 - PoC Final Proposal Definition. Date: 04.06.2018 - 08.06.2018 Description: We aim to describe fully how the Phase 1 PoC would work, detailing in entirety the functionality of all smart-contracts to be developed. Goals: * Project specification. Achieved: * Specification was released ================================================ FILE: migrations/1_initial_migration.js ================================================ var Migrations = artifacts.require("./Migrations.sol"); module.exports = function(deployer) { deployer.deploy(Migrations); }; ================================================ FILE: migrations/2_deploy_contracts.js ================================================ const Ion = artifacts.require("Ion"); const Clique = artifacts.require("Clique"); const EthereumStore = artifacts.require("EthereumStore"); const EventFunction = artifacts.require("Function"); const EventVerifier = artifacts.require("TriggerEventVerifier"); module.exports = async (deployer) => { try { deployer.deploy(Ion, "0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177") .then(() => Ion.deployed) .then(() => deployer.deploy(EthereumStore, Ion.address)) .then(() => EthereumStore.deployed) .then(() => deployer.deploy(Clique, Ion.address)) .then(() => Clique.deployed) .then(() => deployer.deploy(EventVerifier)) .then(() => EventVerifier.deployed) .then(() => deployer.deploy(EventFunction, Ion.address, EventVerifier.address)) .then(() => EventFunction.deployed) } catch(err) { console.log('ERROR on deploy:',err); } }; ================================================ FILE: package.json ================================================ { "name": "ion", "version": "1.0.0", "description": "Inter Operability Network", "main": "truffle.js", "repository": "https://github.com/clearmatics/ion.git", "author": "opensource@clearmatics.com", "license": "LGPL-3.0+", "dependencies": { "axios": "^0.21.1", "bignumber.js": "^8.0.1", "concat-stream": "^1.5.2", "ethereumjs-block": "^2.0.0", "ethereumjs-tx": "^1.3.5", "lodash.template": "^4.5.0", "merkle-patricia-tree": "^2.3.1", "node-gyp": "^3.8.0", "rlp": "^2.0.0", "solc": "^0.5.12", "solhint": "^1.1.10", "truffle-assertions": "^0.9.2", "underscore": "^1.12.1", "web3-eth-accounts": "^1.0.0-beta.34", "yargs-parser": "^5.0.1" }, "devDependencies": { "chai": "^4.1.2", "chai-as-promised": "^7.1.1", "ganache-cli": "^6.7.0", "json-bigint-string": "^1.0.0", "lodash": "^4.17.21", "solidity-coverage": "^0.7.0", "truffle": "^5.3.6", "web3": "1.0.0-beta.33", "web3-eth-abi": "^1.3.5", "web3-utils": "1.0.0-beta.33" }, "scripts": { "testrpc": "ganache-cli --port 8545 --gasLimit 0xFFFFFFFFFFFFF --gasPrice 0 --defaultBalanceEther 99999999999 --networkId 1234", "clirpc": "ganache-cli --port 8545 --gasLimit 0xFFFFFFFFFFFFF --gasPrice 0 --defaultBalanceEther 99999999999 --networkId 1234", "compile": "truffle compile", "deploy": "truffle deploy", "test": "truffle test", "debug": "truffle debug", "coverage": "solidity-coverage", "lint": "solhint contracts/**/*.sol", "genbin": "solc --overwrite --bin ./contracts/*.sol -o abi", "genabi": "solc --overwrite --abi ./contracts/*.sol -o abi" } } ================================================ FILE: test/clique.js ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ /* Clique Validation contract test Tests here are standalone unit tests for clique module functionality. Other contracts have been mocked to simulate basic behaviour. Tests the clique scheme for block submission, validator signature verification and more. */ const eth_util = require('ethereumjs-util'); const utils = require('./helpers/utils.js'); const encoder = require('./helpers/encoder.js'); const Web3 = require('web3'); const Web3Utils = require('web3-utils'); const rlp = require('rlp'); const truffleAssert = require('truffle-assertions'); const sha3 = require('js-sha3').keccak_256 const Clique = artifacts.require("Clique"); const MockIon = artifacts.require("MockIon"); const MockStorage = artifacts.require("MockStorage"); const web3 = new Web3(); const rinkeby = new Web3(); web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545')); rinkeby.setProvider(new web3.providers.HttpProvider('https://rinkeby.infura.io/v3/430e7d9d2b104879aee73ced56f0b8ba')); require('chai') .use(require('chai-as-promised')) .should(); // Takes a header and private key returning the signed data // Needs extraData just to be sure of the final byte signHeader = (headerHash, privateKey, extraData) => { const sig = eth_util.ecsign(headerHash, privateKey) if (this._chainId > 0) { sig.v += this._chainId * 2 + 8 } const pubKey = eth_util.ecrecover(headerHash, sig.v, sig.r, sig.s); const addrBuf = eth_util.pubToAddress(pubKey); const newSigBytes = Buffer.concat([sig.r, sig.s]); let newSig; const bytes = utils.hexToBytes(extraData) const finalByte = bytes.splice(bytes.length-1) if (finalByte.toString('hex')=="0") { newSig = newSigBytes.toString('hex') + '00'; } if (finalByte.toString('hex')=="1") { newSig = newSigBytes.toString('hex') + '01'; } return newSig; } const DEPLOYEDCHAINID = "0xab830ae0774cb20180c8b463202659184033a9f30a21550b89a2b406c3ac8075" const TESTCHAINID = "0x22b55e8a4f7c03e1689da845dd463b09299cb3a574e64c68eafc4e99077a7254" const VALIDATORS_START = ["0x42eb768f2244c8811c63729a21a3569731535f06", "0x7ffc57839b00206d1ad20c69a1981b489f772031", "0xb279182d99e65703f0076e4812653aab85fca0f0"]; const VALIDATORS_FINISH = ["0x42eb768f2244c8811c63729a21a3569731535f06", "0x6635f83421bf059cd8111f180f0727128685bae4", "0x7ffc57839b00206d1ad20c69a1981b489f772031", "0xb279182d99e65703f0076e4812653aab85fca0f0"]; const GENESIS_HASH = "0xf32b505a5ad95dfa88c2bd6904a1ba81a92a1db547dc17f4d7c0f64cf2cddbb1"; const ADD_VALIDATORS_GENESIS_HASH = "0xf32b505a5ad95dfa88c2bd6904a1ba81a92a1db547dc17f4d7c0f64cf2cddbb1"; contract('Clique.js', (accounts) => { const joinHex = arr => '0x' + arr.map(el => el.slice(2)).join(''); const watchEvent = (eventObj) => new Promise((resolve,reject) => eventObj.watch((error,event) => error ? reject(error) : resolve(event))); // Fetch genesis from rinkeby let genesisBlock; let VALIDATORS; let GENESIS_HASH; let ion; let clique; let storage; beforeEach('setup contract for each test', async function () { ion = await MockIon.new(DEPLOYEDCHAINID); clique = await Clique.new(ion.address); storage = await MockStorage.new(ion.address); genesisBlock = await await rinkeby.eth.getBlock(0); VALIDATORS = encoder.extractValidators(genesisBlock.extraData); GENESIS_HASH = genesisBlock.hash; }) it('Deploy Contract', async () => { let chainId = await ion.chainId(); assert.equal(chainId, DEPLOYEDCHAINID); }) describe('Register Chain', () => { it('Successful Register Chain', async () => { // Successfully add id of another chain let tx = await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); console.log("\tGas used to register chain = " + tx.receipt.gasUsed.toString() + " gas"); let chainExists = await clique.chains(TESTCHAINID); assert(chainExists); // Fail adding id of this chain await clique.RegisterChain(storage.address, DEPLOYEDCHAINID, VALIDATORS, GENESIS_HASH).should.be.rejected; // Fail adding id of chain already initialised await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address).should.be.rejected; }) it('Check Validators', async () => { // Successfully add id of another chain await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); let registeredValidators = await clique.getValidators.call(TESTCHAINID, GENESIS_HASH); for (let i = 0; i < VALIDATORS.length; i++) { let validatorExists = registeredValidators.map(v => v.toLowerCase()).some(v => { return v == VALIDATORS[i] });; assert(validatorExists); } }) it('Check Genesis Hash', async () => { // Successfully add id of another chain await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); let header = await clique.m_blockheaders(TESTCHAINID, GENESIS_HASH); let blockHeight = header[0]; assert.equal(0, blockHeight); }) }) describe('Submit Block', () => { it('Authentic Submission Happy Path', async () => { await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); // Fetch block 1 from rinkeby const block = await rinkeby.eth.getBlock(1); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); // Submit block should succeed const validationReceipt = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); let event = validationReceipt.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("AddedBlock()") }); assert.ok(event, "Stored event not emitted"); const submittedEvent = validationReceipt.logs.find(l => { return l.event == 'BlockSubmitted' }); assert.equal(signedHeaderHash, submittedEvent.args.blockHash); let blockHashExists = await clique.m_blockhashes(TESTCHAINID, block.hash); assert(blockHashExists); let header = await clique.m_blockheaders(TESTCHAINID, block.hash); // Separate fetched header info parentHash = header[2]; // Assert that block was persisted correctly assert.equal(parentHash, block.parentHash); }) // Here the block header is signed off chain but by a a non-whitelisted validator it('Fail Submit Block unkown validator - SubmitBlock()', async () => { // Successfully add id of another chain await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); // Fetch block 1 from rinkeby const block = await rinkeby.eth.getBlock(1); // Alter txHashin the unsigned header concatenation const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeader = rlpHeaders.signed; const unsignedHeader = rlpHeaders.unsigned; // Remove last 65 Bytes of extraData const extraBytesShort = rlpHeaders.extraBytesShort; const extraDataSignature = rlpHeaders.extraDataSignature; const extraDataShort = rlpHeaders.extraDataShort; const signedHeaderHash = Web3Utils.sha3(signedHeader); const unsignedHeaderHash = Web3Utils.sha3(unsignedHeader); // Encode and sign the new header const encodedExtraData = '0x' + rlp.encode(extraDataShort).toString('hex'); const newSignedHeaderHash = eth_util.sha3(unsignedHeader); const privateKey = Buffer.from('4f35bad50b8b07fff875ec9d4dec6034b1cb0f7d283db4ce7df8fcfaa2030308', 'hex') let signature = await signHeader(newSignedHeaderHash, privateKey, block.extraData); // Append signature to the end of extraData const sigBytes = utils.hexToBytes(signature.toString('hex')); const newExtraDataBytes = extraBytesShort.concat(sigBytes); const newExtraData = '0x' + utils.bytesToHex(newExtraDataBytes); const newSignedHeader = [ block.parentHash, block.sha3Uncles, block.miner, block.stateRoot, block.transactionsRoot, block.receiptsRoot, block.logsBloom, Web3Utils.toBN(block.difficulty), Web3Utils.toBN(block.number), block.gasLimit, block.gasUsed, Web3Utils.toBN(block.timestamp), newExtraData, // Off-chain signed block block.mixHash, block.nonce ]; // Encode the offchain signed header const offchainSignedHeader = '0x' + rlp.encode(newSignedHeader).toString('hex'); const offchainHeaderHash = Web3Utils.sha3(offchainSignedHeader); await clique.SubmitBlock(TESTCHAINID, unsignedHeader, offchainSignedHeader, storage.address).should.be.rejected; }) it('Fail Submit Block from unknown chain - SubmitBlock()', async () => { await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); // Fetch block 1 from testrpc const block = await rinkeby.eth.getBlock(1); const rlpHeaders = encoder.encodeBlockHeader(block); // Submit block should fail await clique.SubmitBlock(TESTCHAINID.slice(0, -2) + "ff", rlpHeaders.unsigned, rlpHeaders.signed, storage.address).should.be.rejected; }) it('Fail Submit Block with wrong unsigned header - SubmitBlock()', async () => { await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); // Fetch block 1 from testrpc const block = await rinkeby.eth.getBlock(1); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); let unsignedHeader = rlpHeaders.rawunsigned; unsignedHeader[5] = unsignedHeader[5].slice(0, -2) + "fa"; const encodedUnsignedHeader = '0x' + rlp.encode(unsignedHeader).toString('hex'); const unsignedHeaderHash = Web3Utils.sha3(rlpHeaders.unsigned); // Submit block should fail await clique.SubmitBlock(TESTCHAINID, encodedUnsignedHeader, rlpHeaders.signed, storage.address).should.be.rejected; }) // This test checks that new validators get added into the validator list as blocks are submitted to the contract. // Rinkeby adds its first non-genesis validator at block 873987 with the votes occuring at blocks 873983 and 873986 // we will start following the chain from 873982 and then add blocks until the vote threshold, n/2 + 1, is passed. it('Add Validators Through Block Submission', async () => { await clique.RegisterChain(TESTCHAINID, VALIDATORS_START, ADD_VALIDATORS_GENESIS_HASH, storage.address); let registeredValidators = await clique.getValidators.call(TESTCHAINID, ADD_VALIDATORS_GENESIS_HASH); let voteThreshold = Math.floor((registeredValidators.length/2) + 1); assert.equal(voteThreshold, 2); let voteProposal = await clique.getProposal.call(TESTCHAINID, ADD_VALIDATORS_GENESIS_HASH, VALIDATORS_FINISH[1]); assert.equal(voteProposal, 0); // Fetch block 873982 from rinkeby let block = await rinkeby.eth.getBlock(873982); let rlpHeaders = encoder.encodeBlockHeader(block); // Submit block should succeed let validationReceipt = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); console.log("\tGas used to submit block 873982 = " + validationReceipt.receipt.gasUsed.toString() + " gas"); // Fetch block 873983 from rinkeby block = await rinkeby.eth.getBlock(873983); rlpHeaders = encoder.encodeBlockHeader(block); // Submit block should succeed validationReceipt = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); console.log("\tGas used to submit block 873983 = " + validationReceipt.receipt.gasUsed.toString() + " gas"); let submittedEvent = validationReceipt.logs.find(l => { return l.event == 'BlockSubmitted' }); let blockHash = submittedEvent.args.blockHash; // Check proposal is added voteProposal = await clique.getProposal.call(TESTCHAINID, blockHash, VALIDATORS_FINISH[1]); assert.equal(voteProposal, 1); // Fetch block 873984 from rinkeby block = await rinkeby.eth.getBlock(873984); rlpHeaders = encoder.encodeBlockHeader(block); // Submit block should succeed validationReceipt = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); console.log("\tGas used to submit block 873984 = " + validationReceipt.receipt.gasUsed.toString() + " gas"); // Fetch block 873985 from rinkeby block = await rinkeby.eth.getBlock(873985); rlpHeaders = encoder.encodeBlockHeader(block); // Submit block should succeed validationReceipt = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); console.log("\tGas used to submit block 873985 = " + validationReceipt.receipt.gasUsed.toString() + " gas"); // Fetch block 873986 from rinkeby block = await rinkeby.eth.getBlock(873986); rlpHeaders = encoder.encodeBlockHeader(block); // Submit block should succeed validationReceipt = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); console.log("\tGas used to submit block 873986 = " + validationReceipt.receipt.gasUsed.toString() + " gas"); submittedEvent = validationReceipt.logs.find(l => { return l.event == 'BlockSubmitted' }); blockHash = submittedEvent.args.blockHash; // Check proposal is added voteProposal = await clique.getProposal.call(TESTCHAINID, blockHash, VALIDATORS_FINISH[1]); assert.equal(voteProposal, 0); // Check all validators exist registeredValidators = await clique.getValidators.call(TESTCHAINID, blockHash); for (let i = 0; i < VALIDATORS_FINISH.length; i++) { let validatorExists = registeredValidators.map(v => v.toLowerCase()).some(v => { return v == VALIDATORS_FINISH[i] }); assert(validatorExists); } // Check that the vote threshold has increased with validator set size voteThreshold = Math.floor((registeredValidators.length/2) + 1); assert.equal(voteThreshold, 3); }) }) }); ================================================ FILE: test/helpers/encoder.js ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ const Web3Utils = require('web3-utils'); const rlp = require('rlp'); const utils = require('./utils.js'); const encoder = {}; // Encodes the block headers from clique returning the signed and unsigned instances encoder.encodeIbftHeader = (block) => { let istExtraData = block.extraData.slice(66); let rlpExtraData = rlp.decode('0x' + istExtraData); let sig = '0x' + rlpExtraData[1].toString('hex'); // Remove the committed seals committedSeals = rlpExtraData[2]; rlpExtraData[2] = []; let rlpEncodedExtraDataSeal = rlp.encode(rlpExtraData); // Remove last 65 Bytes of extraData let extraBytes = utils.hexToBytes(block.extraData); let extraBytesShort = extraBytes.splice(1, 32); let extraDataShort = '0x' + utils.bytesToHex(extraBytesShort) + rlpEncodedExtraDataSeal.toString('hex'); let header = [ block.parentHash, block.sha3Uncles, block.miner, block.stateRoot, block.transactionsRoot, block.receiptsRoot, block.logsBloom, Web3Utils.toBN(block.difficulty), Web3Utils.toBN(block.number), block.gasLimit, block.gasUsed, Web3Utils.toBN(block.timestamp), extraDataShort, block.mixHash, block.nonce ]; let encodedHeader = '0x' + rlp.encode(header).toString('hex'); let encodedBlockHeaderHash = Web3Utils.sha3(encodedHeader); // Create the rlp encoded extra data rlpExtraData[1] = new Buffer([]); rlpExtraData[2] = []; rlpEncodedExtraDataSeal = rlp.encode(rlpExtraData); // Remove last 65 Bytes of extraData extraBytes = utils.hexToBytes(block.extraData); extraBytesShort = extraBytes.splice(1, 32); extraDataShort = '0x' + utils.bytesToHex(extraBytesShort) + rlpEncodedExtraDataSeal.toString('hex'); header = [ block.parentHash, block.sha3Uncles, block.miner, block.stateRoot, block.transactionsRoot, block.receiptsRoot, block.logsBloom, Web3Utils.toBN(block.difficulty), Web3Utils.toBN(block.number), block.gasLimit, block.gasUsed, Web3Utils.toBN(block.timestamp), extraDataShort, block.mixHash, block.nonce ]; encodedUnsignedHeader = '0x' + rlp.encode(header).toString('hex'); encodedUnsignedHeaderHash = Web3Utils.sha3(encodedUnsignedHeader); encodedCommittedSeals = '0x' + rlp.encode(committedSeals).toString('hex'); return { unsigned: encodedUnsignedHeader, signed: encodedHeader, seal: encodedCommittedSeals }; } // Encodes the block headers from clique returning the signed and unsigned instances encoder.encodeBlockHeader = (block) => { const signedHeader = [ block.parentHash, block.sha3Uncles, block.miner, block.stateRoot, block.transactionsRoot, block.receiptsRoot, block.logsBloom, Web3Utils.toBN(block.difficulty), Web3Utils.toBN(block.number), block.gasLimit, block.gasUsed, Web3Utils.toBN(block.timestamp), block.extraData, block.mixHash, block.nonce ]; // Remove last 65 Bytes of extraData const extraBytes = utils.hexToBytes(block.extraData); const extraBytesShort = extraBytes.splice(1, extraBytes.length-66); const extraDataSignature = '0x' + utils.bytesToHex(extraBytes.splice(extraBytes.length-65)); const extraDataShort = '0x' + utils.bytesToHex(extraBytesShort); const unsignedHeader = [ block.parentHash, block.sha3Uncles, block.miner, block.stateRoot, block.transactionsRoot, block.receiptsRoot, block.logsBloom, Web3Utils.toBN(block.difficulty), Web3Utils.toBN(block.number), block.gasLimit, block.gasUsed, Web3Utils.toBN(block.timestamp), extraDataShort, // extraData minus the signature block.mixHash, block.nonce ]; const encodedSignedHeader = '0x' + rlp.encode(signedHeader).toString('hex'); const signedHeaderHash = Web3Utils.sha3(encodedSignedHeader); const encodedUnsignedHeader = '0x' + rlp.encode(unsignedHeader).toString('hex'); const unsignedHeaderHash = Web3Utils.sha3(encodedUnsignedHeader); return { unsigned: encodedUnsignedHeader, signed: encodedSignedHeader, rawunsigned: unsignedHeader, rawsigned: signedHeader, extraDataSignature: extraDataSignature, extraDataShort: extraDataShort, extraBytesShort: extraBytesShort }; } // Takes the extraData field from a clique genesis block and finds the validators encoder.extractValidators = (extraData) => { genesisExtraData = utils.hexToBytes(extraData) // Remove dressin, 32 bytes pre validators, 65 bytes post validators, and extra byte for 0x extraDataValidators = genesisExtraData.splice(33, genesisExtraData.length-32-65-1) // Check that the validators length is factor of 20 assert.equal(extraDataValidators.length%20, 0); numValidators = extraDataValidators.length / 20; let validators = []; // Append each new validator to the array for (i = 0; i < numValidators; ++i) { validator = extraDataValidators.splice(0, 20); validators.push('0x' + utils.bytesToHex(validator)); } return validators; } encoder.appendBlockHeaders = (signedHeaders, signedHeaderIndices, unsignedHeaders, unsignedHeaderIndices, rlpHeaders) => { // Start creating the long list of block headers signedHeaders.push(rlpHeaders.signed); unsignedHeaders.push(rlpHeaders.unsigned); // Need to append the cumulative length if (signedHeaderIndices.length==0) { signedHeaderIndices.push(utils.hexToBytes(rlpHeaders.signed).splice(1).length); unsignedHeaderIndices.push(utils.hexToBytes(rlpHeaders.unsigned).splice(1).length); } else { signedHeaderIndices.push(utils.hexToBytes(rlpHeaders.signed).splice(1).length + signedHeaderIndices[signedHeaderIndices.length - 1]); unsignedHeaderIndices.push(utils.hexToBytes(rlpHeaders.unsigned).splice(1).length + unsignedHeaderIndices[unsignedHeaderIndices.length - 1]); } } module.exports = encoder; ================================================ FILE: test/helpers/utils.js ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ const crypto = require('crypto') const Web3 = require('web3'); var web3; const utils = {}; // Format required for sending bytes through eth client: // - hex string representation // - prefixed with 0x utils.bufToStr = b => '0x' + b.toString('hex') utils.gasPrice = 100000000000 // truffle fixed gas price utils.joinHex = arr => '0x' + arr.map(el => el.slice(2)).join('') utils.hexToBytes = (hex) => { for (var bytes = [], c = 0; c < hex.length; c += 2) bytes.push(parseInt(hex.substr(c, 2), 16)); return bytes; } utils.bytesToHex = (bytes) => { for (var hex = [], i = 0; i < bytes.length; i++) { hex.push((bytes[i] >>> 4).toString(16)); hex.push((bytes[i] & 0xF).toString(16)); } return hex.join(""); } utils.sha256 = x => crypto .createHash('sha256') .update(x) .digest() utils.random32 = () => crypto.randomBytes(32) utils.randomHex = () => crypto.randomBytes(32).toString('hex'); utils.randomArr = () => { const result = [] const size =(Math.floor(Math.random() * 10) + 1); for(let i = size; 0 < i; i-- ) result.push(randomHex()) return result } utils.isSha256Hash = hashStr => /^0x[0-9a-f]{64}$/i.test(hashStr) const newSecretHashPair = () => { const secret = random32() const hash = sha256(secret) return { secret: bufToStr(secret), hash: bufToStr(hash), } } utils.sleep = ms => { return new Promise(resolve => setTimeout(resolve, ms)); } utils.txGas = txReceipt => txReceipt.receipt.gasUsed * gasPrice utils.txLoggedArgs = txReceipt => txReceipt.logs[0].args utils.txContractId = txReceipt => txLoggedArgs(txReceipt).contractId // Takes a header and private key returning the signed data // Needs extraData just to be sure of the final byte utils.signHeader = (headerHash, privateKey, extraData) => { const sig = eth_util.ecsign(headerHash, privateKey) if (this._chainId > 0) { sig.v += this._chainId * 2 + 8 } const pubKey = eth_util.ecrecover(headerHash, sig.v, sig.r, sig.s); const addrBuf = eth_util.pubToAddress(pubKey); const newSigBytes = Buffer.concat([sig.r, sig.s]); let newSig; const bytes = utils.hexToBytes(extraData) const finalByte = bytes.splice(bytes.length-1) if (finalByte.toString('hex')=="0") { newSig = newSigBytes.toString('hex') + '00'; } if (finalByte.toString('hex')=="1") { newSig = newSigBytes.toString('hex') + '01'; } return newSig; } utils.initWeb3 = (callback, provider) => { web3 = new Web3(); var host = process.env.STANDARD_CONTRACTS_RPC_HOST || "localhost"; if (provider == null) { web3.setProvider(new web3.providers.HttpProvider('http://' + host + ':8545')); } else { web3.setProvider(provider); } web3.eth.getAccounts(function (err, accs) { if (err) return callback(err); web3.eth.defaultAccount = accs[0]; callback(); }); } utils.deploy = (ABI, bytecode, callback) => { new web3.eth.Contract(ABI, {data: bytecode, gas: "0xFFFFFFFFFFFF"}, function (err, contract) { if (err) { callback(err); // callback fires twice, we only want the second call when the contract is deployed } else if (contract.address) { callback(null, contract); } }); } module.exports = utils; ================================================ FILE: test/ibft.js ================================================ // Copyright (c) 2016-2019 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ /* Clique Validation contract test Tests here are standalone unit tests for clique module functionality. Other contracts have been mocked to simulate basic behaviour. Tests the clique scheme for block submission, validator signature verification and more. */ const eth_util = require('ethereumjs-util'); const utils = require('./helpers/utils.js'); const encoder = require('./helpers/encoder.js'); const Web3 = require('web3'); const Web3Utils = require('web3-utils'); const rlp = require('rlp'); const sha3 = require('js-sha3').keccak_256 const Ibft = artifacts.require("IBFT"); const MockIon = artifacts.require("MockIon"); const MockStorage = artifacts.require("MockStorage"); const web3 = new Web3(); web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545')); require('chai') .use(require('chai-as-promised')) .should(); function pad(n, width, z) { z = z || '0'; n = n + ''; return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; } const DEPLOYEDCHAINID = "0xab830ae0774cb20180c8b463202659184033a9f30a21550b89a2b406c3ac8075" const TESTCHAINID = "0x22b55e8a4f7c03e1689da845dd463b09299cb3a574e64c68eafc4e99077a7254" const VALIDATORS_BEFORE = [ '0x4335d75841d8b85187cf651ed130774143927c79', '0x61d7d88dbc76259fcf1f26bc0b5763aebd67aead', '0x955425273ef777d6430d910f9a8b10adbe95fff6', '0xf00d3c728929e42000c8d92d1a7e6a666f12e6ed', '0xd42d697aa23f7b3e209259002b456c57af26edd6' ]; const VALIDATORS_AFTER = [ '0x4335d75841d8b85187cf651ed130774143927c79', '0x61d7d88dbc76259fcf1f26bc0b5763aebd67aead', '0x955425273ef777d6430d910f9a8b10adbe95fff6', '0xf00d3c728929e42000c8d92d1a7e6a666f12e6ed' ]; const GENESIS_HASH = "0x6893c6fe9270461992e748db2f30aa1359babbd74d0392eb4c3476ef942eb5ec"; const block = { difficulty: 1, extraData: "0xdc83010000886175746f6e69747988676f312e31302e34856c696e7578000000f90164f854944335d75841d8b85187cf651ed130774143927c799461d7d88dbc76259fcf1f26bc0b5763aebd67aead94955425273ef777d6430d910f9a8b10adbe95fff694f00d3c728929e42000c8d92d1a7e6a666f12e6edb8410c11022a97fcb2248a2d757a845b4804755702125f8b7ec6c06503ae0277ad996dc22f81431e8036b6cf9ef7d3c1ff1b65a255c9cb70dd2f4925951503a6fdbf01f8c9b8412d3849c86c8ba3ed9a79cdd71b1684364c4c4efb1f01e83ca8cf663f3c95f7ac64b711cd297527d42fb3111b8f78d5227182f38ccc442be5ac4dcb52efede89a01b84135de3661d0191247c7f835c8eb6d7939052c0da8ae234baf8bd208c00225e706112df9bad5bf773120ba4bbc55f6d18e478de43712c0cd3de7a3e2bfd65abb7c01b841735f482a051e6ad7fb76a815907e68d903b73eff4e472006e56fdeca8155cb575f4c1d3e98cf3a4b013331c1bd171d0d500243ac0e073a5fd382294c4fe996f000", gasLimit: 4877543, gasUsed: 0, hash: "0xed607d816f792bff503fc01bf8903b50aae5bbc6d00293350e38bba92cde40ab", logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", miner: "0x955425273ef777d6430d910f9a8b10adbe95fff6", mixHash: "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", nonce: "0x0000000000000000", number: 38, parentHash: "0x6893c6fe9270461992e748db2f30aa1359babbd74d0392eb4c3476ef942eb5ec", receiptsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 901, stateRoot: "0x4e64a3b5ab9c561f72836209e376d035a0aa23a1fc7251e5d21c3c8437fef58e", timestamp: 1549897775, totalDifficulty: 39, transactions: [], transactionsRoot: "0x56e81f171bcc55a6ff8345e692c0f86e5b48e01b996cadc001622fb5e363b421", uncles: [] }; const block_add = { difficulty: 1, extraData: "0xdc83010000886175746f6e69747988676f312e31302e34856c696e7578000000f90179f869944335d75841d8b85187cf651ed130774143927c799461d7d88dbc76259fcf1f26bc0b5763aebd67aead94955425273ef777d6430d910f9a8b10adbe95fff694f00d3c728929e42000c8d92d1a7e6a666f12e6ed94d42d697aa23f7b3e209259002b456c57af26edd6b841a01291465dfa2b138d48f0f819c31ae9e707a2ee2f3bb93d1341371ab315c9473a4b93b6ccb2b9b29462da66c1a95b27e9254cdf9fcac731e84c7183772f091200f8c9b841ce258c674a9b7ec8bacd5386313c976cbf3dd3f63dd704f93b5e71155c3ce11f124bcf430e1c285e0bce060172930a2c8c15054a14b5629b5dcec069c87e570400b841640736f30ef4ee4baf68448d87020366da4ce6ad2d3872027bbcba8cbbad58e01f2e4e057075dad411f958753615e4141bce861f2780e0499a485741154c707601b841490aa29598b1a7ee0830799bc781b47bfb22c884e2ed2aedd6e9c7ca648e1b547cb469e92e5f375bc1bc3abc191cb180abc93bf3cb67009c75d397a1ab4717d901", gasLimit: 4882305, gasUsed: 52254, hash: "0xd9944319153421ebe524ad3648fbb733f8d8b4aaa75bca8e406fc3b8c171e568", logsBloom: "0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", miner: "0xf00d3c728929e42000c8d92d1a7e6a666f12e6ed", mixHash: "0x63746963616c2062797a616e74696e65206661756c7420746f6c6572616e6365", nonce: "0x0000000000000000", number: 39, parentHash: "0xed607d816f792bff503fc01bf8903b50aae5bbc6d00293350e38bba92cde40ab", receiptsRoot: "0x5340517c0dcd60ef9d9735035fcd4a55607eff320684f48796ff57b0a28c8933", sha3Uncles: "0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347", size: 1066, stateRoot: "0x68ebd003e05d477e02be898089958e509ca2bff03fe4a9ca1bef2b24aefda03d", timestamp: 1549897776, totalDifficulty: 40, transactions: ["0x8c0faa1990b8b4e0ec8129cd8e2ccf5578be92ee9540361efad993b51179594c"], transactionsRoot: "0xd21dcc8688b5b3ab638474485516cda326615f0e8a9853e97589d198b01916b9", uncles: [] }; function hexToBytes(hex) { for (var bytes = [], c = 0; c < hex.length; c += 2) bytes.push(parseInt(hex.substr(c, 2), 16)); return bytes; } function bytesToHex(bytes) { for (var hex = [], i = 0; i < bytes.length; i++) { hex.push((bytes[i] >>> 4).toString(16)); hex.push((bytes[i] & 0xF).toString(16)); } return hex.join(""); } contract('Ibft.js', (accounts) => { const joinHex = arr => '0x' + arr.map(el => el.slice(2)).join(''); const watchEvent = (eventObj) => new Promise((resolve,reject) => eventObj.watch((error,event) => error ? reject(error) : resolve(event))); let ion; let ibft; let storage; beforeEach('setup contract for each test', async function () { ion = await MockIon.new(DEPLOYEDCHAINID); ibft = await Ibft.new(ion.address); storage = await MockStorage.new(ion.address); }) describe('Register Chain', () => { it('Successful Register Chain', async () => { // Successfully add id of another chain let tx = await ibft.RegisterChain(TESTCHAINID, VALIDATORS_BEFORE, GENESIS_HASH, storage.address); console.log("\tGas used to register chain = " + tx.receipt.gasUsed.toString() + " gas"); let chainExists = await ibft.chains(TESTCHAINID); assert(chainExists); let chainHead = await ibft.m_chainHeads(TESTCHAINID); assert.equal(chainHead, GENESIS_HASH); }) it('Fail Register Chain Twice', async () => { // Successfully add id of another chain let tx = await ibft.RegisterChain(TESTCHAINID, VALIDATORS_BEFORE, GENESIS_HASH, storage.address); console.log("\tGas used to register chain = " + tx.receipt.gasUsed.toString() + " gas"); let chainExists = await ibft.chains(TESTCHAINID); assert(chainExists); let chainHead = await ibft.m_chainHeads(TESTCHAINID); assert.equal(chainHead, GENESIS_HASH); // Fail adding id of this chain await ibft.RegisterChain(DEPLOYEDCHAINID, VALIDATORS_BEFORE, GENESIS_HASH, storage.address).should.be.rejected; // Fail adding id of chain already initialised await ibft.RegisterChain(TESTCHAINID, VALIDATORS_BEFORE, GENESIS_HASH, storage.address).should.be.rejected; }) it('Check Validators', async () => { // Successfully add id of another chain await ibft.RegisterChain(TESTCHAINID, VALIDATORS_BEFORE, GENESIS_HASH, storage.address); let registeredValidators = await ibft.getValidators.call(TESTCHAINID); for (let i = 0; i < VALIDATORS_BEFORE.length; i++) { let validatorExists = registeredValidators.map(v => v.toLowerCase()).some(v => { return v == VALIDATORS_BEFORE[i] });; assert(validatorExists); } }) it('Check Genesis Hash', async () => { // Successfully add id of another chain await ibft.RegisterChain(TESTCHAINID, VALIDATORS_BEFORE, GENESIS_HASH, storage.address); let chainHead = await ibft.m_chainHeads(TESTCHAINID); assert.equal(chainHead, GENESIS_HASH); }) }) describe('Submit Block', () => { it('Successful Submit block', async () => { await ibft.RegisterChain(TESTCHAINID, VALIDATORS_BEFORE, GENESIS_HASH, storage.address); let chainHead = await ibft.m_chainHeads(TESTCHAINID); assert.equal(chainHead, GENESIS_HASH); rlpHeader = encoder.encodeIbftHeader(block); // Submit block should succeed const validationReceipt = await ibft.SubmitBlock(TESTCHAINID, rlpHeader.unsigned, rlpHeader.signed, rlpHeader.seal, storage.address); console.log("\tGas used to submit block = " + validationReceipt.receipt.gasUsed.toString() + " gas"); let event = validationReceipt.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("AddedBlock()") }); assert.ok(event, "Stored event not emitted"); const submittedEvent = validationReceipt.logs.find(l => { return l.event == 'BlockSubmitted' }); assert.equal(Web3Utils.sha3(rlpHeader.signed), submittedEvent.args.blockHash); let addedBlockHash = await ibft.m_chainHeads.call(TESTCHAINID); assert.equal(addedBlockHash, block.hash); let header = await ibft.m_blockheaders(TESTCHAINID, block.hash); // Separate fetched header info parentHash = header[2]; // Assert that block was persisted correctly assert.equal(parentHash, block.parentHash); chainHead = await ibft.m_chainHeads(TESTCHAINID); assert.equal(chainHead, block.hash); }) it('Submit Sequential Blocks with Additional Validator', async () => { await ibft.RegisterChain(TESTCHAINID, VALIDATORS_BEFORE, GENESIS_HASH, storage.address); rlpHeader = encoder.encodeIbftHeader(block); // Submit block should succeed let validationReceipt = await ibft.SubmitBlock(TESTCHAINID, rlpHeader.unsigned, rlpHeader.signed, rlpHeader.seal, storage.address); console.log("\tGas used to submit block = " + validationReceipt.receipt.gasUsed.toString() + " gas"); let event = validationReceipt.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("AddedBlock()") }); assert.ok(event, "Stored event not emitted"); rlpHeader = encoder.encodeIbftHeader(block_add); validationReceipt = await ibft.SubmitBlock(TESTCHAINID, rlpHeader.unsigned, rlpHeader.signed, rlpHeader.seal, storage.address); event = validationReceipt.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("AddedBlock()") }); assert.ok(event, "Stored event not emitted"); const submittedEvent = validationReceipt.logs.find(l => { return l.event == 'BlockSubmitted' }); assert.equal(Web3Utils.sha3(rlpHeader.signed), submittedEvent.args.blockHash); let addedBlockHash = await ibft.m_chainHeads.call(TESTCHAINID); assert.equal(addedBlockHash, block_add.hash); let header = await ibft.m_blockheaders(TESTCHAINID, block_add.hash); // Separate fetched header info parentHash = header[2]; // Assert that block was persisted correctly assert.equal(parentHash, block_add.parentHash); // Check new validators let registeredValidators = await ibft.getValidators.call(TESTCHAINID); for (let i = 0; i < VALIDATORS_AFTER.length; i++) { let validatorExists = registeredValidators.map(v => v.toLowerCase()).some(v => { return v == VALIDATORS_AFTER[i] });; assert(validatorExists); } }) it('Fail Submit Block with Unknown Validator', async () => { await ibft.RegisterChain(TESTCHAINID, VALIDATORS_BEFORE, GENESIS_HASH, storage.address); block.extraData = "0xdc83010000886175746f6e69747988676f312e31302e34856c696e7578000000f90164f854941cb62855cd70774634c85c9acb7c3070ce692936946b2f468af3d0ba2f3a09712faea4d379c2e891a194a667ea98809a69724c6672018bd7db799cd3fefc94c2054df3acfdbe5b221866b25e09026734ca5572b841012edd2e5936deaf4c0ee17698dc0fda832bb51a81d929ae3156d73e5475123c19d162cf1e434637c16811d63d1d3b587906933d75e25cedf7bef59e8fa8375d01f8c9b841719c5bc521721e71ff7fafff09fdff4037e678a77a816b08d45b89d55f35edc94b5c51cc3eeba79d3de291c3c46fbf04faec4952e7d0836be9ad5d855f525c9301b841a7c9eed0337f92a5d4caf6f57b3b59ba10a14ea615c6264fc82fcf5b2e4b626f701fd3596cd1f8639b37a41cb4f3a7582bb530790441de73e6e3449284127b4d00b841210db6ef89906ef1c77538426d29b8440a1c987d508e396776e63515df2a345767c195dc540cfabdf86d696c73b4a24632445565d322d8e45fa2668ec5e6c0e000"; rlpHeader = encoder.encodeIbftHeader(block); // Submit block should not succeed await ibft.SubmitBlock(TESTCHAINID, rlpHeader.unsigned, rlpHeader.signed, rlpHeader.seal, storage.address).should.be.rejected; }) it('Fail Submit Block with Insufficient Seals', async () => { await ibft.RegisterChain(TESTCHAINID, VALIDATORS_BEFORE, GENESIS_HASH, storage.address); let badExtraData = "0xf90164f854944335d75841d8b85187cf651ed130774143927c799461d7d88dbc76259fcf1f26bc0b5763aebd67aead94955425273ef777d6430d910f9a8b10adbe95fff694f00d3c728929e42000c8d92d1a7e6a666f12e6edb8410c11022a97fcb2248a2d757a845b4804755702125f8b7ec6c06503ae0277ad996dc22f81431e8036b6cf9ef7d3c1ff1b65a255c9cb70dd2f4925951503a6fdbf01f8c9b8412d3849c86c8ba3ed9a79cdd71b1684364c4c4efb1f01e83ca8cf663f3c95f7ac64b711cd297527d42fb3111b8f78d5227182f38ccc442be5ac4dcb52efede89a01b84135de3661d0191247c7f835c8eb6d7939052c0da8ae234baf8bd208c00225e706112df9bad5bf773120ba4bbc55f6d18e478de43712c0cd3de7a3e2bfd65abb7c01b841735f482a051e6ad7fb76a815907e68d903b73eff4e472006e56fdeca8155cb575f4c1d3e98cf3a4b013331c1bd171d0d500243ac0e073a5fd382294c4fe996f000"; // Remove seal from extradata const decodedExtraData = rlp.decode(badExtraData); decodedExtraData[2].pop() // Reapply the rlp encoded istanbul extra minus single seal encodedExtraData = rlp.encode(decodedExtraData).toString('hex'); block.extraData = "0xdc83010000886175746f6e69747988676f312e31302e34856c696e7578000000" + encodedExtraData; rlpHeader = encoder.encodeIbftHeader(block); // Submit block should not succeed await ibft.SubmitBlock(TESTCHAINID, rlpHeader.unsigned, rlpHeader.signed, rlpHeader.seal, storage.address).should.be.rejected; }) }) }); ================================================ FILE: test/integration-base_fabric.js ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ const Web3Utils = require('web3-utils'); const utils = require('./helpers/utils.js'); const BN = require('bignumber.js') const encoder = require('./helpers/encoder.js') const rlp = require('rlp'); const async = require('async') const levelup = require('levelup'); const sha3 = require('js-sha3').keccak_256 const util = require('util'); // Connect to the Test RPC running const Web3 = require('web3'); const web3 = new Web3(); web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545')); const Ion = artifacts.require("Ion"); const FabricStore = artifacts.require("FabricStore"); const BaseValidation = artifacts.require("Base"); const FabricFunction = artifacts.require("FabricFunction"); require('chai') .use(require('chai-as-promised')) .should(); const DEPLOYEDCHAINID = "0xab830ae0774cb20180c8b463202659184033a9f30a21550b89a2b406c3ac8075" const TESTCHAINID = "0x22b55e8a4f7c03e1689da845dd463b09299cb3a574e64c68eafc4e99077a7254" const TESTDATA = [{ channelId: "orgchannel", blocks: [{ hash: "vBmkcC8xbLMAUK-wkLMYGDz9qFdu1n8SbsHp62Of_-o", number: 4, prevHash: "hnw1EQE3SXA_LCUsRWXAj5nZ_JjLPm6DGiRn-g7g4Pc", dataHash: "_xuKFW3Po3gNBXjXac11M39a-o-_92_PC6DWBUWnk4I", timestampS: 1548756504, timestampN: 121452526, transactions: [{ txId: "d4a03d5b71ac3fab92b90bae047c9c5e6ccf0b4396be6807c1724fc0139f999b", nsrw: [{ namespace: "ExampleCC", readsets: [{ key: "A", version: { blockNumber: 3, txNumber: 0 } }, { key: "B", version: { blockNumber: 3, txNumber: 0 } }], writesets: [{ key: "A", isDelete: "false", value: "0" }, { key: "B", isDelete: "false", value: "3" }] }, { namespace: "lscc", readsets: [{ key: "ExampleCC", version: { blockNumber: 3, txNumber: 0 } }], writesets: [] }] }] }] }] const formattedData = [[ TESTDATA[0].channelId, [ TESTDATA[0].blocks[0].hash, TESTDATA[0].blocks[0].number, TESTDATA[0].blocks[0].prevHash, TESTDATA[0].blocks[0].dataHash, TESTDATA[0].blocks[0].timestampS, TESTDATA[0].blocks[0].timestampN, [[ TESTDATA[0].blocks[0].transactions[0].txId, [[ TESTDATA[0].blocks[0].transactions[0].nsrw[0].namespace, [[ TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].key, [ TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].version.txNumber ] ], [ TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].key, [ TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].version.txNumber ] ]], [[ TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].isDelete, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].value ],[ TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].isDelete, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].value ]] ], [ TESTDATA[0].blocks[0].transactions[0].nsrw[1].namespace, [[ TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].key, [ TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].version.txNumber ] ]], [] ]] ]] ] ]]; contract('Base-Fabric Integration', (accounts) => { let ion; let validation; let storage; let rlpEncodedBlock_Gen = "0x" + rlp.encode(formattedData).toString('hex'); // Generated by CLI https://github.com/Shirikatsu/fabric-examples/blob/fc3ff7243f282a4edf21c6667e71ab02e759c3c5/fabric-cli/cmd/fabric-cli/printer/encoder.go#L40 let rlpEncodedBlock = "0xf90127f901248a6f72676368616e6e656cf90116ab76426d6b63433878624c4d41554b2d776b4c4d5947447a3971466475316e38536273487036324f665f2d6f04ab686e7731455145335358415f4c435573525758416a356e5a5f4a6a4c506d36444769526e2d673767345063ab5f78754b465733506f33674e42586a58616331314d3339612d6f2d5f39325f50433644574255576e6b3449845c50261884073d37eef885f883b84064346130336435623731616333666162393262393062616530343763396335653663636630623433393662653638303763313732346663303133396639393962f83fe8894578616d706c654343cac441c20380c442c20380d2c8418566616c736530c8428566616c736533d5846c736363cecd894578616d706c654343c20380c0"; beforeEach('setup contract for each test', async function () { ion = await Ion.new(DEPLOYEDCHAINID); validation = await BaseValidation.new(ion.address); storage = await FabricStore.new(ion.address); }) describe('Block Encode', () => { it('Correct Encoding', async () => { assert.equal(rlpEncodedBlock_Gen, rlpEncodedBlock); }) }) describe('Register Validation Module', () => { it('Successful Register', async () => { await validation.register(); }) it('Fail Register Twice', async () => { await validation.register(); await validation.register().should.be.rejected; }) }) describe('Register Chain', () => { it('Successful Register Chain', async () => { await validation.register(); let tx = await validation.RegisterChain(TESTCHAINID, storage.address); console.log("\tGas used to register chain = " + tx.receipt.gasUsed.toString() + " gas"); let chainRegistered = await storage.m_chains(TESTCHAINID); assert(chainRegistered); let chainId = await storage.m_networks.call(TESTCHAINID); assert.equal(TESTCHAINID, chainId); }) it('Fail Register Chain Twice', async () => { await validation.register(); let tx = await validation.RegisterChain(TESTCHAINID, storage.address); console.log("\tGas used to register chain = " + tx.receipt.gasUsed.toString() + " gas"); let chainRegistered = await storage.m_chains(TESTCHAINID); assert(chainRegistered); let chainId = await storage.m_networks.call(TESTCHAINID); assert.equal(TESTCHAINID, chainId); await validation.RegisterChain(TESTCHAINID, storage.address).should.be.rejected; }) it('Fail Register Chain without registering validation module', async () => { let tx = await validation.RegisterChain(TESTCHAINID, storage.address).should.be.rejected; }) }) describe('Add Block', () => { it('Successful Add Block', async () => { await validation.register(); let tx = await validation.RegisterChain(TESTCHAINID, storage.address); let receipt = await validation.SubmitBlock(TESTCHAINID, rlpEncodedBlock, storage.address); console.log("\tGas used to store fabric block: %d", receipt.receipt.gasUsed); let block = await storage.getBlock.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].hash); assert.equal(block[0], TESTDATA[0].blocks[0].number); assert.equal(block[1], TESTDATA[0].blocks[0].hash); assert.equal(block[2], TESTDATA[0].blocks[0].prevHash); assert.equal(block[3], TESTDATA[0].blocks[0].dataHash); assert.equal(block[4], TESTDATA[0].blocks[0].timestampS); assert.equal(block[5], TESTDATA[0].blocks[0].timestampN); assert.equal(block[6], TESTDATA[0].blocks[0].transactions[0].txId); tx = await storage.getTransaction.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].txId); assert.equal(tx[0], TESTDATA[0].blocks[0].hash); assert.equal(tx[1], TESTDATA[0].blocks[0].transactions[0].nsrw[0].namespace + "," + TESTDATA[0].blocks[0].transactions[0].nsrw[1].namespace); let txExists = await storage.isTransactionExists.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].txId); assert(txExists); let nsrw = await storage.getNSRW.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].txId, TESTDATA[0].blocks[0].transactions[0].nsrw[0].namespace); let expectedReadset = util.format("{ key: %s, version: { blockNo: %d, txNo: %d } } { key: %s, version: { blockNo: %d, txNo: %d } } ", TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].version.txNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].version.txNumber ) assert.equal(expectedReadset, nsrw[0]); let expectedWriteset = util.format("{ key: %s, isDelete: %s, value: %s } { key: %s, isDelete: %s, value: %s } ", TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].isDelete, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].value, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].isDelete, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].value ) assert.equal(expectedWriteset, nsrw[1]); nsrw = await storage.getNSRW.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].txId, TESTDATA[0].blocks[0].transactions[0].nsrw[1].namespace); expectedReadset = util.format("{ key: %s, version: { blockNo: %d, txNo: %d } } ", TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].key, TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].version.txNumber ) assert.equal(expectedReadset, nsrw[0]); expectedWriteset = ""; assert.equal(expectedWriteset, nsrw[1]); }) it('Fail Add Block from unregistered chain', async () => { await validation.register(); let receipt = await validation.SubmitBlock(TESTCHAINID, rlpEncodedBlock, storage.address).should.be.rejected; }) it('Fail Add Block from non-ion', async () => { await validation.register(); let tx = await validation.RegisterChain(TESTCHAINID, storage.address); await storage.addBlock(TESTCHAINID, "0x0", rlpEncodedBlock).should.be.rejected; }) it('Fail Add Block with malformed data', async () => { await validation.register(); let tx = await validation.RegisterChain(TESTCHAINID, storage.address); await validation.SubmitBlock(TESTCHAINID, "0xf86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288", storage.address).should.be.rejected; }) it('Fail Add Same Block Twice', async () => { await validation.register(); let tx = await validation.RegisterChain(TESTCHAINID, storage.address); await validation.SubmitBlock(TESTCHAINID, rlpEncodedBlock, storage.address); await validation.SubmitBlock(TESTCHAINID, rlpEncodedBlock, storage.address).should.be.rejected; }) }) describe('Chaincode usage Contract', () => { it('Deploy Function Contract', async () => { const functionContract = await FabricFunction.new(storage.address); }) it('Submit Block, retrieve state and execute', async () => { const functionContract = await FabricFunction.new(storage.address); await validation.register(); let tx = await validation.RegisterChain(TESTCHAINID, storage.address); let receipt = await validation.SubmitBlock(TESTCHAINID, rlpEncodedBlock, storage.address); tx = await functionContract.retrieveAndExecute(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].key); event = tx.logs.some(l => { return l.event == "State" }); assert.ok(event, "Executed event not emitted"); log = tx.logs.find(l => l.event == "State"); console.log("\tGas used to fetch data and execute the function = " + tx.receipt.gasUsed.toString() + " gas"); assert.equal(log.args.blockNo, TESTDATA[0].blocks[0].number); assert.equal(log.args.txNo, 0); assert.equal(log.args.mvalue, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].value); tx = await functionContract.retrieveAndExecute(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].key); event = tx.logs.some(l => { return l.event == "State" }); assert.ok(event, "Executed event not emitted"); log = tx.logs.find(l => l.event == "State"); console.log("\tGas used to fetch data and execute the function = " + tx.receipt.gasUsed.toString() + " gas"); assert.equal(log.args.blockNo, TESTDATA[0].blocks[0].number); assert.equal(log.args.txNo, 0); assert.equal(log.args.mvalue, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].value); }) it('Fail Function Execution', async () => { const functionContract = await FabricFunction.new(storage.address); await validation.register(); let tx = await validation.RegisterChain(TESTCHAINID, storage.address); let receipt = await validation.SubmitBlock(TESTCHAINID, rlpEncodedBlock, storage.address); // Fail with wrong chain ID await functionContract.retrieveAndExecute(DEPLOYEDCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].key).should.be.rejected; // Fail with wrong channel ID await functionContract.retrieveAndExecute(TESTCHAINID, "nochannel", TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].key).should.be.rejected; // Fail with wrong key await functionContract.retrieveAndExecute(TESTCHAINID, TESTDATA[0].channelId, "randomkey").should.be.rejected; }) }) }) ================================================ FILE: test/integration-clique_ethereum.js ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ const Web3Utils = require('web3-utils'); const utils = require('./helpers/utils.js'); const BN = require('bignumber.js') const encoder = require('./helpers/encoder.js') const rlp = require('rlp'); const async = require('async') const sha3 = require('js-sha3').keccak_256 // Connect to the Test RPC running const Web3 = require('web3'); const web3 = new Web3(); const rinkeby = new Web3(); web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545')); rinkeby.setProvider(new web3.providers.HttpProvider('https://rinkeby.infura.io/v3/430e7d9d2b104879aee73ced56f0b8ba')); const Ion = artifacts.require("Ion"); const Clique = artifacts.require("Clique"); const EthereumStore = artifacts.require("EthereumStore"); const TriggerEventVerifier = artifacts.require("TriggerEventVerifier"); const FunctionEvent = artifacts.require("Function"); require('chai') .use(require('chai-as-promised')) .should(); const DEPLOYEDCHAINID = "0xab830ae0774cb20180c8b463202659184033a9f30a21550b89a2b406c3ac8075" const TESTCHAINID = "0x22b55e8a4f7c03e1689da845dd463b09299cb3a574e64c68eafc4e99077a7254" /* TESTRPC TEST DATA */ const TESTBLOCK = { difficulty: 2, extraData: '0xd68301080d846765746886676f312e3130856c696e7578000000000000000000583a78dd245604e57368cb2688e42816ebc86eff73ee219dd96b8a56ea6392f75507e703203bc2cc624ce6820987cf9e8324dd1f9f67575502fe6060d723d0e100', gasLimit: 7509409, gasUsed: 2883490, hash: '0x694752333dd1bd0f806cc6ef1063162f4f330c88f9dcd9e61174fcf5e4927eb7', logsBloom: '0x22440000020000090000000000000000041000080000008000088000080000000200000400000800000000000000400000000000000000000010000008020102000000000000080000000008800000000000022000000004000000010000000000080000000620400440100010200400082000000000000080040010000100020020000000000000080080000001000000000100000400480000000002000000002000080018000008108000100000000000000000020000050010001004000000000102000040004000000000000000000000004400000000000000000000000208000000000400008200020000004022400000000004000200848000000000', miner: '0x0000000000000000000000000000000000000000', mixHash: '0x0000000000000000000000000000000000000000000000000000000000000000', nonce: '0x0000000000000000', number: 2657422, parentHash: '0x3471555ab9a99528f02f9cdd8f0017fe2f56e01116acc4fe7f78aee900442f35', receiptsRoot: '0x907121bec78b40e8256fac47867d955c560b321e93fc9f046f919ffb5e3823ff', sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', size: 4848, stateRoot: '0xf526f481ffb6c3c56956d596f2b23e1f7ff17c810ba59efb579d9334a1765444', timestamp: 1531931421, totalDifficulty: 5023706, transactions: [ '0x7adbc5ee3712552a1e85962c3ea3d82394cfed7960d60c12d60ebafe67445450', '0x6be870e6dfb11894b64371560ec39e563cef91642afd193bfa67874f3508a282', '0x5ba6422455cb7127958df15c453bfe60d92921b647879864b531fd6589e36af4', '0xa2597e6fe6882626e12055b1378025aa64a85a03dd23f5dc66034f2ef3746810', '0x7ffb940740050ae3604f99a4eef07c83de5d75076cae42cb1561c370cba3a0a3', '0x4d6326a6d4cf606c7e44a4ae6710acd3876363bcaabd1b1b59d29fff4da223c5', '0x10b3360ef00cd7c4faf826365fddbd33938292c98c55a4cdb37194a142626f63', '0x655290cb44be2e64d3b1825a86d5647579015c5cffb03ede7f67eb34cea6b97f', '0x6b5e025ea558f4872112a39539ce9a819bfbb795b04eefcc45e1cf5ea947614c', '0xefd68b516babcf8a4ca74a358cfca925d9d2d5177ef7b859f3d9183ff522efe8', '0xa056eeeeb098fd5adb283e12e77a239797c96860c21712963f183937613d3391', '0xa5d1adf694e3442975a13685a9c7d9013c05a4fdcea5bc827566a331b2fead2b', '0x95a47360f89c48f0b1a484cbeee8816b6a0e2fc321bdb9db48082bd7272b4ebc', '0x896d29a87393c6607844fa545d38eb96056d5310a6b4e056dc00adde67c24be2', '0xef3ce2ad9259920094f7fd5ad00453b35888662696ae9b85a393e55cde3ec28d', '0x2de8af9b4e84b3ac93adfce81964cc69bafd0a2dbcac3a5f7628ee9e56fd1c8a', '0x2790cdb3377f556e8f5bc8eaaf9c6c0d36d0f242c2e4226af2aac0203f43019b', '0x98ae65246249785bd1ac8157900f7e1a2c69d5c3b3ffc97d55b9eacab3e212f0', '0x7d4f090c58880761eaaab1399864d4a52631db8f0b21bfb7051f9a214ad07993', '0xafc3ab60059ed38e71c7f6bea036822abe16b2c02fcf770a4f4b5fffcbfe6e7e', '0x2af8f6c49d1123077f1efd13764cb2a50ff922fbaf49327efc44c6048c38c968', '0x6d5e1753dc91dae7d528ab9b02350e726e006a5591a5d315a34a46e2a951b3fb', '0xdc864827159c7fde6bbd1672ed9a90ce5d69f5d0c81761bf689775d19a90387e', '0x22fb4d90a7125988b2857c50709e544483f898cb1e8036477f9ddd94b177bf93', '0x999c2e2ba342bed4ccedea01d638db3bbd1abd6d10784c317843880841db6dec', '0x11355abb5fe745ed458b2a78e116f4a8c2fe046a131eafe08f30d23bd9d10394' ], transactionsRoot: '0x07f36c7ad26564fa65daebda75a23dfa95d660199092510743f6c8527dd72586', uncles: [] } const VALIDATORS_B2657422 = [ "0x42eb768f2244c8811c63729a21a3569731535f06", "0x6635f83421bf059cd8111f180f0727128685bae4", "0x7ffc57839b00206d1ad20c69a1981b489f772031", "0xb279182d99e65703f0076e4812653aab85fca0f0", "0xd6ae8250b8348c94847280928c79fb3b63ca453e", "0xda35dee8eddeaa556e4c26268463e26fb91ff74f", "0xfc18cbc391de84dbd87db83b20935d3e89f5dd91"] const signedHeader = [ TESTBLOCK.parentHash, TESTBLOCK.sha3Uncles, TESTBLOCK.miner, TESTBLOCK.stateRoot, TESTBLOCK.transactionsRoot, TESTBLOCK.receiptsRoot, TESTBLOCK.logsBloom, Web3Utils.toBN(TESTBLOCK.difficulty), Web3Utils.toBN(TESTBLOCK.number), TESTBLOCK.gasLimit, TESTBLOCK.gasUsed, Web3Utils.toBN(TESTBLOCK.timestamp), TESTBLOCK.extraData, TESTBLOCK.mixHash, TESTBLOCK.nonce ]; // Remove last 65 Bytes of extraData const extraBytes = utils.hexToBytes(TESTBLOCK.extraData); const extraBytesShort = extraBytes.splice(1, extraBytes.length-66); const extraDataSignature = '0x' + utils.bytesToHex(extraBytes.splice(extraBytes.length-65)); const extraDataShort = '0x' + utils.bytesToHex(extraBytesShort); const unsignedHeader = [ TESTBLOCK.parentHash, TESTBLOCK.sha3Uncles, TESTBLOCK.miner, TESTBLOCK.stateRoot, TESTBLOCK.transactionsRoot, TESTBLOCK.receiptsRoot, TESTBLOCK.logsBloom, Web3Utils.toBN(TESTBLOCK.difficulty), Web3Utils.toBN(TESTBLOCK.number), TESTBLOCK.gasLimit, TESTBLOCK.gasUsed, Web3Utils.toBN(TESTBLOCK.timestamp), extraDataShort, // extraData minus the signature TESTBLOCK.mixHash, TESTBLOCK.nonce ]; const TEST_SIGNED_HEADER = '0x' + rlp.encode(signedHeader).toString('hex'); const signedHeaderHash = Web3Utils.sha3(TEST_SIGNED_HEADER); const TEST_UNSIGNED_HEADER = '0x' + rlp.encode(unsignedHeader).toString('hex'); const unsignedHeaderHash = Web3Utils.sha3(TEST_UNSIGNED_HEADER); const TESTRLPENCODING = "0xf9025ca03471555ab9a99528f02f9cdd8f0017fe2f56e01116acc4fe7f78aee900442f35a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0f526f481ffb6c3c56956d596f2b23e1f7ff17c810ba59efb579d9334a1765444a007f36c7ad26564fa65daebda75a23dfa95d660199092510743f6c8527dd72586a0907121bec78b40e8256fac47867d955c560b321e93fc9f046f919ffb5e3823ffb90100224400000200000900000000000000000410000800000080000880000800000002000004000008000000000000004000000000000000000000100000080201020000000000000800000000088000000000000220000000040000000100000000000800000006204004401000102004000820000000000000800400100001000200200000000000000800800000010000000001000004004800000000020000000020000800180000081080001000000000000000000200000500100010040000000001020000400040000000000000000000000044000000000000000000000002080000000004000082000200000040224000000000040002008480000000000283288c8e837295a1832bffa2845b4f6b1db861d68301080d846765746886676f312e3130856c696e7578000000000000000000583a78dd245604e57368cb2688e42816ebc86eff73ee219dd96b8a56ea6392f75507e703203bc2cc624ce6820987cf9e8324dd1f9f67575502fe6060d723d0e100a00000000000000000000000000000000000000000000000000000000000000000880000000000000000" const TEST_PATH = "0x13" const TEST_TX_VALUE = "0xf86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288" const TEST_TX_NODES = "0xf90235f871a0804f9c841a6a1d3361d79980581c84e5b4d3e4c9bf33951346775542d0ee0728a0edadb5e660118ea4323654191131b62c81fc00203a15a21c925f9f50d0e4b3e4808080808080a03eda2d64b94c5ed45026a29c75c99677d44c561ea5efea30c1db6299871d5c2e8080808080808080f90151a0bc285699e68d2fe18e7af2cdf7e7e6456e91a3fd31e3c9935bc5bef92e94bf4ba06eb963b2c3a3b6c07a7221aa6f6f86f7cb8ddb45ab1ff1a9dc781f34da1f081fa0deea5b5566e7a5634d91c5fb56e25f4370e3531e2fd71ee17ed6c4ad0be2ced3a0b4e9d14555f162e811cfbcbff9b98a271a197b75271565f693912c2ff75e2131a03b0bc2d764fbefd76848ee2da7b211eb230ede08d8c54e6a868be9f5e42122c1a0b6dd488ad4fb82b0a98dff81ac6766d1dec26b29dc06174de1d315b0ab0bdf0ca066c20ff06dc33777f53eec32b0b9a8d99872bec24bb3998bb520ae6897c21d7ea02db2a399f611ba7993efb4768938a6f61b4add8959ce4c89f201f41e882ff375a02e31051a9f938b9b342b8070db3dd829f62da8d0c83a6dff91a4e3b4cb2adb9ea090e75708e7dbf856b75ed126a960085419fcde0e6a0129a92dffc0cb83ac089680808080808080f86c20b869f86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288" const TEST_RECEIPT_VALUE = "0xf901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613e" const TEST_RECEIPT_NODES = "0xf90335f871a012d378fe6800bc18f22e715a31971ef7e73ac5d1d85384f4b66ac32036ae43dea004d6e2678656a957ac776dbef512a04d266c1af3e2c5587fd233261a3d423213808080808080a05fac317a4d6d78181319fbc7e2cae4a9260f1a6afb5c6fea066e2308eed416818080808080808080f90151a03da235c6dd0fbdaf208c60cbdca0d609dee2ba107495aa7adaa658362616c8aaa09ebf378a9064aa4da0512c55c790a5e007ac79d2713e4533771cd2c95be47a4da0c06fed36ffe1f2ec164ba88f73b353960448d2decbb65355c5298a33555de742a0e057afe423ee17e5499c570a56880b0f5b5c1884b90ff9b9b5baa827f72fc816a093e06093cd2fdb67e0f87cfcc35ded2f445cc1309a0ff178e59f932aeadb6d73a0193e4e939fbc5d34a570bea3fff7c6d54adcb1c3ab7ef07510e7bd5fcef2d4b3a0a17a0c71c0118092367220f65b67f2ba2eb9068ff5270baeabe8184a01a37f14a03479a38e63123d497588ad5c31d781276ec8c11352dd3895c8add34f9a2b786ba042254728bb9ab94b58adeb75d2238da6f30382969c00c65e55d4cc4aa474c0a6a03c088484aa1c73b8fb291354f80e9557ab75a01c65d046c2471d19bd7f2543d880808080808080f9016b20b90167f901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613e" const TRIG_DEPLOYED_RINKEBY_ADDR = "0x61621bcf02914668f8404c1f860e92fc1893f74c"; const TRIG_FIRED_RINKEBY_TXHASH = "0xafc3ab60059ed38e71c7f6bea036822abe16b2c02fcf770a4f4b5fffcbfe6e7e" const TRIG_FIRED_RINKEBY_BLOCKNO = 2657422 const TRIG_CALLED_BY = "0x279884e133f9346f2fad9cc158222068221b613e"; const TRIG_GENESIS_HASH = TESTBLOCK.parentHash; contract('Clique-Ethereum Integration', async (accounts) => { let genesisBlock; let VALIDATORS; let GENESIS_HASH; let ion; let clique; let storage; before("Get genesis and validator from rinkeby once", async () => { genesisBlock = await rinkeby.eth.getBlock(0) VALIDATORS = encoder.extractValidators(genesisBlock.extraData); GENESIS_HASH = genesisBlock.hash; }) beforeEach('setup contract for each test', async function () { ion = await Ion.new(DEPLOYEDCHAINID); clique = await Clique.new(ion.address); storage = await EthereumStore.new(ion.address); }) describe('Register Clique Module', () => { it('Successful Register', async () => { await clique.register(); }) it('Fail Register Twice', async () => { await clique.register(); await clique.register().should.be.rejected; }) }) describe('Register Chain', () => { it('Successful Register Chain', async () => { await clique.register(); let tx = await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); console.log("\tGas used to register chain = " + tx.receipt.gasUsed.toString() + " gas"); let chain = await clique.chains(TESTCHAINID); assert(chain); }) it('Fail Register Twice', async () => { await clique.register(); let tx = await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); console.log("\tGas used to register chain = " + tx.receipt.gasUsed.toString() + " gas"); let chain = await clique.chains(TESTCHAINID); assert(chain); await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address).should.be.rejected; }) it('Fail Register Deployment Chain', async () => { await clique.register(); let tx = await clique.RegisterChain(storage.address, DEPLOYEDCHAINID, VALIDATORS, GENESIS_HASH).should.be.rejected; }) it('Fail Register Chain without registering clique module', async () => { let tx = await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address).should.be.rejected; }) }) describe('Add Block', () => { it('Successful Add Block', async () => { await clique.register(); await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); // Fetch block 1 from rinkeby const block = await rinkeby.eth.getBlock(1); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); let tx = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); let event = tx.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("BlockAdded(bytes32,bytes32)") }); assert.ok(event, "BlockAdded event not emitted"); let submittedEvent = tx.logs.find(l => { return l.event == 'BlockSubmitted' }); let blockHash = submittedEvent.args.blockHash; assert.equal(signedHeaderHash, blockHash); }) it('Successful Add Block from different genesis', async () => { await clique.register(); await clique.RegisterChain(TESTCHAINID, VALIDATORS_B2657422, TRIG_GENESIS_HASH, storage.address); // Fetch block 2657422 from rinkeby const block = await rinkeby.eth.getBlock(TESTBLOCK.number); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); let tx = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); let event = tx.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("BlockAdded(bytes32,bytes32)") }); assert.ok(event, "BlockAdded event not emitted"); let submittedEvent = tx.logs.find(l => { return l.event == 'BlockSubmitted' }); let blockHash = submittedEvent.args.blockHash; assert.equal(signedHeaderHash, blockHash); }) it('Fail Add Block with unregistered chain id', async () => { await clique.register(); // await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); // Fetch block 1 from rinkeby const block = await rinkeby.eth.getBlock(1); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address).should.be.rejected; }) it('Fail Add Block with non-sequential block', async () => { await clique.register(); await clique.RegisterChain(TESTCHAINID, VALIDATORS, GENESIS_HASH, storage.address); // Fetch block 2 from rinkeby instead of block 1 const block = await rinkeby.eth.getBlock(2); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address).should.be.rejected; }) it('Fail Add Block from non-clique', async () => { // Fetch block 1 from rinkeby const block = await rinkeby.eth.getBlock(1); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); // Fail direct adding of block to storage await storage.addBlock(TESTCHAINID, block.hash, rlpHeaders.unsigned).should.be.rejected; }) }) describe('Proof Decoding', () => { it('Successful Proof Decoding', async () => { compressedProof = generateProof(); CLIGeneratedProof = "0xf9074113f86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288f90235f871a0804f9c841a6a1d3361d79980581c84e5b4d3e4c9bf33951346775542d0ee0728a0edadb5e660118ea4323654191131b62c81fc00203a15a21c925f9f50d0e4b3e4808080808080a03eda2d64b94c5ed45026a29c75c99677d44c561ea5efea30c1db6299871d5c2e8080808080808080f90151a0bc285699e68d2fe18e7af2cdf7e7e6456e91a3fd31e3c9935bc5bef92e94bf4ba06eb963b2c3a3b6c07a7221aa6f6f86f7cb8ddb45ab1ff1a9dc781f34da1f081fa0deea5b5566e7a5634d91c5fb56e25f4370e3531e2fd71ee17ed6c4ad0be2ced3a0b4e9d14555f162e811cfbcbff9b98a271a197b75271565f693912c2ff75e2131a03b0bc2d764fbefd76848ee2da7b211eb230ede08d8c54e6a868be9f5e42122c1a0b6dd488ad4fb82b0a98dff81ac6766d1dec26b29dc06174de1d315b0ab0bdf0ca066c20ff06dc33777f53eec32b0b9a8d99872bec24bb3998bb520ae6897c21d7ea02db2a399f611ba7993efb4768938a6f61b4add8959ce4c89f201f41e882ff375a02e31051a9f938b9b342b8070db3dd829f62da8d0c83a6dff91a4e3b4cb2adb9ea090e75708e7dbf856b75ed126a960085419fcde0e6a0129a92dffc0cb83ac089680808080808080f86c20b869f86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288f901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613ef90335f871a012d378fe6800bc18f22e715a31971ef7e73ac5d1d85384f4b66ac32036ae43dea004d6e2678656a957ac776dbef512a04d266c1af3e2c5587fd233261a3d423213808080808080a05fac317a4d6d78181319fbc7e2cae4a9260f1a6afb5c6fea066e2308eed416818080808080808080f90151a03da235c6dd0fbdaf208c60cbdca0d609dee2ba107495aa7adaa658362616c8aaa09ebf378a9064aa4da0512c55c790a5e007ac79d2713e4533771cd2c95be47a4da0c06fed36ffe1f2ec164ba88f73b353960448d2decbb65355c5298a33555de742a0e057afe423ee17e5499c570a56880b0f5b5c1884b90ff9b9b5baa827f72fc816a093e06093cd2fdb67e0f87cfcc35ded2f445cc1309a0ff178e59f932aeadb6d73a0193e4e939fbc5d34a570bea3fff7c6d54adcb1c3ab7ef07510e7bd5fcef2d4b3a0a17a0c71c0118092367220f65b67f2ba2eb9068ff5270baeabe8184a01a37f14a03479a38e63123d497588ad5c31d781276ec8c11352dd3895c8add34f9a2b786ba042254728bb9ab94b58adeb75d2238da6f30382969c00c65e55d4cc4aa474c0a6a03c088484aa1c73b8fb291354f80e9557ab75a01c65d046c2471d19bd7f2543d880808080808080f9016b20b90167f901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613e" assert.equal("0x" + compressedProof.toString('hex'), CLIGeneratedProof); }) }) describe('Check All Proofs of Tx 0xafc3ab60059ed38e71c7f6bea036822abe16b2c02fcf770a4f4b5fffcbfe6e7e on Rinkeby', () => { it('Successful Check Proofs', async () => { await clique.register(); await clique.RegisterChain(TESTCHAINID, VALIDATORS_B2657422, TRIG_GENESIS_HASH, storage.address); // Fetch block 2657422 from rinkeby const block = await rinkeby.eth.getBlock(TESTBLOCK.number); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); let tx = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); let event = tx.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("BlockAdded(bytes32,bytes32)") }); assert.ok(event, "BlockAdded event not emitted"); let submittedEvent = tx.logs.find(l => l.event == 'BlockSubmitted' ); let blockHash = submittedEvent.args.blockHash; assert.equal(signedHeaderHash, blockHash); compressedProof = generateProof(); tx = await storage.CheckProofs(TESTCHAINID, TESTBLOCK.hash, "0x" + compressedProof.toString('hex')); console.log("\tGas used to submit check proofs = " + tx.receipt.gasUsed.toString() + " gas"); }) it('Fail Proofs with wrong proofs value', async () => { await clique.register(); await clique.RegisterChain(TESTCHAINID, VALIDATORS_B2657422, TRIG_GENESIS_HASH, storage.address); // Fetch block 2657422 from rinkeby const block = await rinkeby.eth.getBlock(TESTBLOCK.number); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); let tx = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); let event = tx.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("BlockAdded(bytes32,bytes32)") }); assert.ok(event, "BlockAdded event not emitted"); let submittedEvent = tx.logs.find(l => { return l.event == 'BlockSubmitted' }); let blockHash = submittedEvent.args.blockHash; assert.equal(signedHeaderHash, blockHash); compressedProof = generateCorruptedProof(); await storage.CheckProofs(TESTCHAINID, TESTBLOCK.hash, "0x" + compressedProof.toString('hex')).should.be.rejected; }) it('Fail Proofs with malformed proof', async () => { await clique.register(); await clique.RegisterChain(TESTCHAINID, VALIDATORS_B2657422, TRIG_GENESIS_HASH, storage.address); // Fetch block 2657422 from rinkeby const block = await rinkeby.eth.getBlock(TESTBLOCK.number); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); let tx = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); let event = tx.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("BlockAdded(bytes32,bytes32)") }); assert.ok(event, "BlockAdded event not emitted"); let submittedEvent = tx.logs.find(l => { return l.event == 'BlockSubmitted' }); let blockHash = submittedEvent.args.blockHash; assert.equal(signedHeaderHash, blockHash); compressedProof = generateMalformedProof(); await storage.CheckProofs(TESTCHAINID, TESTBLOCK.hash, "0x" + compressedProof.toString('hex')).should.be.rejected; }) }) describe('Event Consumption Contract', () => { it('Deploy Function Contract', async () => { const verifier = await TriggerEventVerifier.new(); const functionContract = await FunctionEvent.new(storage.address, verifier.address); }) it('Submit Block, verify tx and event, and execute', async () => { const verifier = await TriggerEventVerifier.new(); const functionContract = await FunctionEvent.new(storage.address, verifier.address); await clique.register(); await clique.RegisterChain(TESTCHAINID, VALIDATORS_B2657422, TRIG_GENESIS_HASH, storage.address); // Fetch block 2657422 from rinkeby const block = await rinkeby.eth.getBlock(TESTBLOCK.number); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); let tx = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); let event = tx.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("BlockAdded(bytes32,bytes32)") }); assert.ok(event, "BlockAdded event not emitted"); let submittedEvent = tx.logs.find(l => { return l.event == 'BlockSubmitted' }); let blockHash = submittedEvent.args.blockHash; assert.equal(signedHeaderHash, blockHash); compressedProof = generateProof(); tx = await functionContract.verifyAndExecute(TESTCHAINID, TESTBLOCK.hash, TRIG_DEPLOYED_RINKEBY_ADDR, "0x" + compressedProof.toString('hex'), TRIG_CALLED_BY); event = tx.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("Executed()") }); assert.ok(event, "Executed event not emitted"); console.log("\tGas used to verify all proofs against ion, verify logs against the verifier and execute the function = " + tx.receipt.gasUsed.toString() + " gas"); }) it('Fail Function Execution', async () => { const verifier = await TriggerEventVerifier.new(); const functionContract = await FunctionEvent.new(storage.address, verifier.address); await clique.register(); await clique.RegisterChain(TESTCHAINID, VALIDATORS_B2657422, TRIG_GENESIS_HASH, storage.address); // Fetch block 2657422 from rinkeby const block = await rinkeby.eth.getBlock(TESTBLOCK.number); const rlpHeaders = encoder.encodeBlockHeader(block); const signedHeaderHash = Web3Utils.sha3(rlpHeaders.signed); assert.equal(block.hash, signedHeaderHash); let tx = await clique.SubmitBlock(TESTCHAINID, rlpHeaders.unsigned, rlpHeaders.signed, storage.address); let event = tx.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("BlockAdded(bytes32,bytes32)") }); assert.ok(event, "BlockAdded event not emitted"); let submittedEvent = tx.logs.find(l => { return l.event == 'BlockSubmitted' }); let blockHash = submittedEvent.args.blockHash; assert.equal(signedHeaderHash, blockHash); compressedProof = generateProof(); // Fail with wrong chain ID await functionContract.verifyAndExecute(DEPLOYEDCHAINID, TESTBLOCK.hash, TRIG_DEPLOYED_RINKEBY_ADDR, "0x" + compressedProof.toString('hex'), TRIG_CALLED_BY).should.be.rejected; // Fail with wrong block hash await functionContract.verifyAndExecute(TESTCHAINID, TESTBLOCK.hash.substring(0, 30) + "ff", TRIG_DEPLOYED_RINKEBY_ADDR, "0x" + compressedProof.toString('hex'), TRIG_CALLED_BY).should.be.rejected; // Fail with wrong deployed contract address await functionContract.verifyAndExecute(TESTCHAINID, TESTBLOCK.hash, TRIG_CALLED_BY, "0x" + compressedProof.toString('hex'), TRIG_CALLED_BY).should.be.rejected; // Fail with malformed/corrupted proofs await functionContract.verifyAndExecute(TESTCHAINID, TESTBLOCK.hash, TRIG_DEPLOYED_RINKEBY_ADDR, "0x" + generateMalformedProof().toString('hex'), TRIG_CALLED_BY).should.be.rejected; await functionContract.verifyAndExecute(TESTCHAINID, TESTBLOCK.hash, TRIG_DEPLOYED_RINKEBY_ADDR, "0x" + generateCorruptedProof().toString('hex'), TRIG_CALLED_BY).should.be.rejected; // Fail with wrong expected event parameter await functionContract.verifyAndExecute(TESTCHAINID, TESTBLOCK.hash, TRIG_DEPLOYED_RINKEBY_ADDR, "0x" + compressedProof.toString('hex'), TRIG_DEPLOYED_RINKEBY_ADDR).should.be.rejected; }) }) // The below succeeds but manual registering with the same values fail // const DEPLOYEDCHAINID = "0xab830ae0774cb20180c8b463202659184033a9f30a21550b89a2b406c3ac8075" // // const testid = "0x6341fd3daf94b748c72ced5a5b26028f2474f5f00d824504e4fa37a75767e177"; // const testval = ["0x42eb768f2244c8811c63729a21a3569731535f06", "0x6635f83421bf059cd8111f180f0727128685bae4", "0x7ffc57839b00206d1ad20c69a1981b489f772031", "0xb279182d99e65703f0076e4812653aab85fca0f0", "0xd6ae8250b8348c94847280928c79fb3b63ca453e", "0xda35dee8eddeaa556e4c26268463e26fb91ff74f", "0xfc18cbc391de84dbd87db83b20935d3e89f5dd91"]; // const testgen = "0x3f09591cb976beecd52d7dbe5fb869bbfc76b9022883c0cfbd5813a6eeec973e"; // const teststore = "0xec7601E406A998898DE9A784a634f68E038E19D2" // // it('Rinkeby Register Chain', async () => { // // Successfully add id of another chain // // await clique.register(); // let tx = await clique.RegisterChain(testid, testval, testgen, storage.address); // console.log("\tGas used to register chain = " + tx.receipt.gasUsed.toString() + " gas"); // let chainExists = await clique.chains(testid); // // assert(chainExists); // // // Fail adding id of this chain // await clique.RegisterChain(DEPLOYEDCHAINID, testval, testgen, teststore).should.be.rejected; // // // Fail adding id of chain already initialised // await clique.RegisterChain(testid, testval, testgen, teststore).should.be.rejected; // }) }) async function verifyReceipts(eP, txHash) { await eP.getReceiptTrieRoot(txHash).then( (root) => { console.log("EP RECEIPT Root hash = 0x" + root.toString('hex')) }) var verified; await eP.getReceiptProof(txHash).then( (proof) => { verified = EP.receipt(proof.path, proof.value, proof.parentNodes, proof.header, proof.blockHash); }) return verified; } function generateTestReceiptRLPNodes() { let root = Buffer.from("f871a012d378fe6800bc18f22e715a31971ef7e73ac5d1d85384f4b66ac32036ae43dea004d6e2678656a957ac776dbef512a04d266c1af3e2c5587fd233261a3d423213808080808080a05fac317a4d6d78181319fbc7e2cae4a9260f1a6afb5c6fea066e2308eed416818080808080808080", 'hex'); second = Buffer.from("f90151a03da235c6dd0fbdaf208c60cbdca0d609dee2ba107495aa7adaa658362616c8aaa09ebf378a9064aa4da0512c55c790a5e007ac79d2713e4533771cd2c95be47a4da0c06fed36ffe1f2ec164ba88f73b353960448d2decbb65355c5298a33555de742a0e057afe423ee17e5499c570a56880b0f5b5c1884b90ff9b9b5baa827f72fc816a093e06093cd2fdb67e0f87cfcc35ded2f445cc1309a0ff178e59f932aeadb6d73a0193e4e939fbc5d34a570bea3fff7c6d54adcb1c3ab7ef07510e7bd5fcef2d4b3a0a17a0c71c0118092367220f65b67f2ba2eb9068ff5270baeabe8184a01a37f14a03479a38e63123d497588ad5c31d781276ec8c11352dd3895c8add34f9a2b786ba042254728bb9ab94b58adeb75d2238da6f30382969c00c65e55d4cc4aa474c0a6a03c088484aa1c73b8fb291354f80e9557ab75a01c65d046c2471d19bd7f2543d880808080808080", 'hex'); leaf = Buffer.from("f9016b20b90167f901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613e", 'hex'); decodedRoot = rlp.decode(root); decodedSecond = rlp.decode(second); decodedLeaf = rlp.decode(leaf); nodes = rlp.encode([decodedRoot, decodedSecond, decodedLeaf]); return nodes; } function generateProof() { decodedPath = rlp.decode(Buffer.from(TEST_PATH.slice(2), 'hex')); decodedTx = rlp.decode(Buffer.from(TEST_TX_VALUE.slice(2), 'hex')); decodedTxNodes = rlp.decode(Buffer.from(TEST_TX_NODES.slice(2), 'hex')); decodedReceipt = rlp.decode(Buffer.from(TEST_RECEIPT_VALUE.slice(2), 'hex')); decodedReceiptNodes = rlp.decode(Buffer.from(TEST_RECEIPT_NODES.slice(2), 'hex')); proof = rlp.encode([decodedPath, decodedTx, decodedTxNodes, decodedReceipt, decodedReceiptNodes]); return proof; } function generateMalformedProof() { decodedPath = rlp.decode(Buffer.from(TEST_PATH.slice(2), 'hex')); decodedTx = rlp.decode(Buffer.from(TEST_TX_VALUE.slice(2), 'hex')); decodedTxNodes = rlp.decode(Buffer.from(TEST_TX_NODES.slice(2), 'hex')); decodedReceipt = rlp.decode(Buffer.from(TEST_RECEIPT_VALUE.slice(2), 'hex')); // Exclude receipt nodes // decodedReceiptNodes = rlp.decode(Buffer.from(TEST_RECEIPT_NODES.slice(2), 'hex')); proof = rlp.encode([decodedPath, decodedTx, decodedTxNodes, decodedReceipt]); return proof; } function generateCorruptedProof() { decodedPath = rlp.decode(Buffer.from(TEST_PATH.slice(2), 'hex')); decodedTx = rlp.decode(Buffer.from(TEST_TX_VALUE.slice(2), 'hex')); decodedTxNodes = rlp.decode(Buffer.from(TEST_TX_NODES.slice(2), 'hex')); decodedReceipt = rlp.decode(Buffer.from(TEST_RECEIPT_VALUE.slice(2), 'hex')); decodedReceiptNodes = rlp.decode(Buffer.from(TEST_RECEIPT_NODES.slice(2), 'hex')); proof = rlp.encode([decodedPath, sha3(decodedTx), decodedTxNodes, decodedReceipt, decodedReceiptNodes]); return proof; } ================================================ FILE: test/ion.js ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ /* Ion Mediator contract test Tests here are standalone unit tests for Ion functionality. Other contracts have been mocked to simulate basic behaviour. Tests the central mediator for block passing and validation registering. */ const Web3Utils = require('web3-utils'); const utils = require('./helpers/utils.js'); const rlp = require('rlp'); const async = require('async') const sha3 = require('js-sha3').keccak_256 // Connect to the Test RPC running const Web3 = require('web3'); const web3 = new Web3(); web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545')); const Ion = artifacts.require("Ion"); const MockValidation = artifacts.require("MockValidation"); const MockStorage = artifacts.require("MockStorage"); require('chai') .use(require('chai-as-promised')) .should(); const DEPLOYEDCHAINID = "0xab830ae0774cb20180c8b463202659184033a9f30a21550b89a2b406c3ac8075" const TESTCHAINID = "0x22b55e8a4f7c03e1689da845dd463b09299cb3a574e64c68eafc4e99077a7254" /* TESTRPC TEST DATA */ const TESTBLOCK = { difficulty: 2, extraData: '0xd68301080d846765746886676f312e3130856c696e7578000000000000000000583a78dd245604e57368cb2688e42816ebc86eff73ee219dd96b8a56ea6392f75507e703203bc2cc624ce6820987cf9e8324dd1f9f67575502fe6060d723d0e100', gasLimit: 7509409, gasUsed: 2883490, hash: '0x694752333dd1bd0f806cc6ef1063162f4f330c88f9dcd9e61174fcf5e4927eb7', logsBloom: '0x22440000020000090000000000000000041000080000008000088000080000000200000400000800000000000000400000000000000000000010000008020102000000000000080000000008800000000000022000000004000000010000000000080000000620400440100010200400082000000000000080040010000100020020000000000000080080000001000000000100000400480000000002000000002000080018000008108000100000000000000000020000050010001004000000000102000040004000000000000000000000004400000000000000000000000208000000000400008200020000004022400000000004000200848000000000', miner: '0x0000000000000000000000000000000000000000', mixHash: '0x0000000000000000000000000000000000000000000000000000000000000000', nonce: '0x0000000000000000', number: 2657422, parentHash: '0x3471555ab9a99528f02f9cdd8f0017fe2f56e01116acc4fe7f78aee900442f35', receiptsRoot: '0x907121bec78b40e8256fac47867d955c560b321e93fc9f046f919ffb5e3823ff', sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', size: 4848, stateRoot: '0xf526f481ffb6c3c56956d596f2b23e1f7ff17c810ba59efb579d9334a1765444', timestamp: 1531931421, totalDifficulty: 5023706, transactions: [ '0x7adbc5ee3712552a1e85962c3ea3d82394cfed7960d60c12d60ebafe67445450', '0x6be870e6dfb11894b64371560ec39e563cef91642afd193bfa67874f3508a282', '0x5ba6422455cb7127958df15c453bfe60d92921b647879864b531fd6589e36af4', '0xa2597e6fe6882626e12055b1378025aa64a85a03dd23f5dc66034f2ef3746810', '0x7ffb940740050ae3604f99a4eef07c83de5d75076cae42cb1561c370cba3a0a3', '0x4d6326a6d4cf606c7e44a4ae6710acd3876363bcaabd1b1b59d29fff4da223c5', '0x10b3360ef00cd7c4faf826365fddbd33938292c98c55a4cdb37194a142626f63', '0x655290cb44be2e64d3b1825a86d5647579015c5cffb03ede7f67eb34cea6b97f', '0x6b5e025ea558f4872112a39539ce9a819bfbb795b04eefcc45e1cf5ea947614c', '0xefd68b516babcf8a4ca74a358cfca925d9d2d5177ef7b859f3d9183ff522efe8', '0xa056eeeeb098fd5adb283e12e77a239797c96860c21712963f183937613d3391', '0xa5d1adf694e3442975a13685a9c7d9013c05a4fdcea5bc827566a331b2fead2b', '0x95a47360f89c48f0b1a484cbeee8816b6a0e2fc321bdb9db48082bd7272b4ebc', '0x896d29a87393c6607844fa545d38eb96056d5310a6b4e056dc00adde67c24be2', '0xef3ce2ad9259920094f7fd5ad00453b35888662696ae9b85a393e55cde3ec28d', '0x2de8af9b4e84b3ac93adfce81964cc69bafd0a2dbcac3a5f7628ee9e56fd1c8a', '0x2790cdb3377f556e8f5bc8eaaf9c6c0d36d0f242c2e4226af2aac0203f43019b', '0x98ae65246249785bd1ac8157900f7e1a2c69d5c3b3ffc97d55b9eacab3e212f0', '0x7d4f090c58880761eaaab1399864d4a52631db8f0b21bfb7051f9a214ad07993', '0xafc3ab60059ed38e71c7f6bea036822abe16b2c02fcf770a4f4b5fffcbfe6e7e', '0x2af8f6c49d1123077f1efd13764cb2a50ff922fbaf49327efc44c6048c38c968', '0x6d5e1753dc91dae7d528ab9b02350e726e006a5591a5d315a34a46e2a951b3fb', '0xdc864827159c7fde6bbd1672ed9a90ce5d69f5d0c81761bf689775d19a90387e', '0x22fb4d90a7125988b2857c50709e544483f898cb1e8036477f9ddd94b177bf93', '0x999c2e2ba342bed4ccedea01d638db3bbd1abd6d10784c317843880841db6dec', '0x11355abb5fe745ed458b2a78e116f4a8c2fe046a131eafe08f30d23bd9d10394' ], transactionsRoot: '0x07f36c7ad26564fa65daebda75a23dfa95d660199092510743f6c8527dd72586', uncles: [] } const signedHeader = [ TESTBLOCK.parentHash, TESTBLOCK.sha3Uncles, TESTBLOCK.miner, TESTBLOCK.stateRoot, TESTBLOCK.transactionsRoot, TESTBLOCK.receiptsRoot, TESTBLOCK.logsBloom, Web3Utils.toBN(TESTBLOCK.difficulty), Web3Utils.toBN(TESTBLOCK.number), TESTBLOCK.gasLimit, TESTBLOCK.gasUsed, Web3Utils.toBN(TESTBLOCK.timestamp), TESTBLOCK.extraData, TESTBLOCK.mixHash, TESTBLOCK.nonce ]; const TEST_SIGNED_HEADER = '0x' + rlp.encode(signedHeader).toString('hex'); contract('Ion.js', (accounts) => { let ion; let validation; let storage; beforeEach('setup contract for each test', async function () { ion = await Ion.new(DEPLOYEDCHAINID); validation = await MockValidation.new(ion.address); storage = await MockStorage.new(ion.address); }) it('Deploy Ion', async () => { let chainId = await ion.chainId(); assert.equal(chainId, DEPLOYEDCHAINID); }) describe('Register Validation', () => { it('Successful registration', async () => { // Successfully add id of another chain let registered = await validation.register.call(); await validation.register(); assert(registered); }) it('Fail second registration', async () => { // Successfully add id of another chain let registered = await validation.register.call(); await validation.register(); assert(registered); // Fail second attempt to register validation await validation.register.call().should.be.rejected; }) it('Fail registration by non-contract', async () => { await ion.registerValidationModule().should.be.rejected; }) }) describe('Store Block', () => { it('Successful Store Block', async () => { await validation.register(); const tx = await validation.SubmitBlock(storage.address, TESTCHAINID, TEST_SIGNED_HEADER); let event = tx.receipt.rawLogs.some(l => { return l.topics[0] == '0x' + sha3("AddedBlock()") }); assert.ok(event, "Block not stored"); }) it('Fail Store Block by unregistered validation', async () => { await validation.SubmitBlock(storage.address, TESTCHAINID, TEST_SIGNED_HEADER).should.be.rejected; }) it('Fail Store Block by non-contract', async () => { await ion.storeBlock(storage.address, TESTCHAINID, TEST_SIGNED_HEADER).should.be.rejected; }) it('Fail Store Block with non contract storage address', async () => { await ion.storeBlock(accounts[0], TESTCHAINID, TEST_SIGNED_HEADER).should.be.rejected; }) }) }) ================================================ FILE: test/patricia_trie_test.js ================================================ const assert = require('assert'); const path = require('path'); const utils = require('./helpers/utils.js'); const async = require('async'); const PatriciaTrieTest = artifacts.require("PatriciaTrieTest"); contract('Patricia Trie', (accounts) => { describe('VerifyProof', async function () { it('should successfully verify all proofs', async function () { let patriciatrietest = await PatriciaTrieTest.new(); for( let i = 0; i < testData['success'].length; i++) { data = testData['success'][i]; let result = await patriciatrietest.testVerify.call(data.value, data.nodes, data.path, data.rootHash); assert.equal(result, true); } }); it('should fail verifying all proofs with incompatible data', async function () { let patriciatrietest = await PatriciaTrieTest.new(); for( let i = 0; i < testData['fail'].length; i++) { data = testData['fail'][i]; let result = await patriciatrietest.testVerify.call(data.value, data.nodes, data.path, data.rootHash); assert.equal(result, false); } }); }); }); const testData = { "success": [{ "rootHash": "0xda2e968e25198a0a41e4dcdc6fcb03b9d49274b3d44cb35d921e4ebe3fb5c54c", "path": "0x61", "value": "0x857465737431", "nodes": "0xf83bf839808080808080c8318685746573743180a0207947cf85c03bd3d9f9ff5119267616318dcef0e12de2f8ca02ff2cdc720a978080808080808080" }, { "rootHash": "0xda2e968e25198a0a41e4dcdc6fcb03b9d49274b3d44cb35d921e4ebe3fb5c54c", "path": "0x826162", "value": "0x74", "nodes": "0xf87ff839808080808080c8318685746573743180a0207947cf85c03bd3d9f9ff5119267616318dcef0e12de2f8ca02ff2cdc720a978080808080808080f8428080c58320616274cc842061626386857465737433a05d495bd9e35ab0dab60dec18b21acc860829508e7df1064fce1f0b8fa4c0e8b2808080808080808080808080" }, { "rootHash": "0xda2e968e25198a0a41e4dcdc6fcb03b9d49274b3d44cb35d921e4ebe3fb5c54c", "path": "0x83616263", "value": "0x857465737433", "nodes": "0xf87ff839808080808080c8318685746573743180a0207947cf85c03bd3d9f9ff5119267616318dcef0e12de2f8ca02ff2cdc720a978080808080808080f8428080c58320616274cc842061626386857465737433a05d495bd9e35ab0dab60dec18b21acc860829508e7df1064fce1f0b8fa4c0e8b2808080808080808080808080" }, { "rootHash": "0xda2e968e25198a0a41e4dcdc6fcb03b9d49274b3d44cb35d921e4ebe3fb5c54c", "path": "0x8461626564", "value": "0x857465737435", "nodes": "0xf8cbf839808080808080c8318685746573743180a0207947cf85c03bd3d9f9ff5119267616318dcef0e12de2f8ca02ff2cdc720a978080808080808080f8428080c58320616274cc842061626386857465737433a05d495bd9e35ab0dab60dec18b21acc860829508e7df1064fce1f0b8fa4c0e8b2808080808080808080808080e583161626a06b1a1127b4c489762c8259381ff9ecf51b7ef0c2879b89e72c993edc944f1ccce5808080ca8220648685746573743480ca822064868574657374358080808080808080808080" }, { "rootHash": "0xda2e968e25198a0a41e4dcdc6fcb03b9d49274b3d44cb35d921e4ebe3fb5c54c", "path": "0x8461626364", "value": "0x857465737434", "nodes": "0xf8cbf839808080808080c8318685746573743180a0207947cf85c03bd3d9f9ff5119267616318dcef0e12de2f8ca02ff2cdc720a978080808080808080f8428080c58320616274cc842061626386857465737433a05d495bd9e35ab0dab60dec18b21acc860829508e7df1064fce1f0b8fa4c0e8b2808080808080808080808080e583161626a06b1a1127b4c489762c8259381ff9ecf51b7ef0c2879b89e72c993edc944f1ccce5808080ca8220648685746573743480ca822064868574657374358080808080808080808080" }], "fail": [{ "rootHash": "0xda2e968e25198a0a41e4dcdc6fcb03b9d49274b3d44cb35d921e4ebe3fb5c54c", "path": "0x61", "value": "0x857465737432", "nodes": "0xf83bf839808080808080c8318685746573743180a0207947cf85c03bd3d9f9ff5119267616318dcef0e12de2f8ca02ff2cdc720a978080808080808080" }, { "rootHash": "0xda2e968e25198a0a41e4dcdc6fcb03b9d49274b3d44cb35d921e4ebe3fb5c54c", "path": "0x826163", "value": "0x75", "nodes": "0xf87ff839808080808080c8318685746573743180a0207947cf85c03bd3d9f9ff5119267616318dcef0e12de2f8ca02ff2cdc720a978080808080808080f8428080c58320616274cc842061626386857465737433a05d495bd9e35ab0dab60dec18b21acc860829508e7df1064fce1f0b8fa4c0e8b2808080808080808080808080" }, { "rootHash": "0xda2e968e25198a0a41e4dcdc6fcb03b9d49274b3d44cb35d921e4ebe3fb5c54c", "path": "0x83616263", "value": "0x857465737434", "nodes": "0xf87ff839808080808080c8318685746573743180a0207947cf85c03bd3d9f9ff5119267616318dcef0e12de2f8ca02ff2cdc720a978080808080808080f8428080c58320616274cc842061626386857465737433a05d495bd9e35ab0dab60dec18b21acc860829508e7df1064fce1f0b8fa4c0e8b2808080808080808080808080" }, { "rootHash": "0xda2e968e25198a0a41e4dcdc6fcb03b9d49274b3d44cb35d921e4ebe3fb5c54c", "path": "0x8461626564", "value": "0x857465737435", "nodes": "0xf8cbf839808080808080c8318685746573743180a0207947cf85c03bd3d9f9ff5119267616318dcef0e12de2f8ca02ff2cdc720a978080808080808080f8428080c58320616274cc842061626386857465737433a05d495bd9e35ab0dab60dec18b21acc860829508e7df1064fce1f0b8fa4c0e8b2808080808080808080808080e583161626a06b1a1127b4c489762c8259381ff9ecf51b7ef0c2879b89e72c993edc944f1ccce5808080ca8220648685746573743480ca822064868574657374358080808080808080808085" }, { "rootHash": "0xda2e968e25198a0a41e4dcdc6fcb03b9d49274b3d44cb35d921e4ebe3fb5c54c", "path": "0x8461626364", "value": "0x857465737435", "nodes": "0xf8cbf839808080808080c8318685746573743180a0207947cf85c03bd3d9f9ff5119267616318dcef0e12de2f8ca02ff2cdc720a978080808080808080f8428080c58320616274cc842061626386857465737433a05d495bd9e35ab0dab60dec18b21acc860829508e7df1064fce1f0b8fa4c0e8b2808080808080808080808080e583161626a06b1a1127b4c489762c8259381ff9ecf51b7ef0c2879b89e72c993edc944f1ccce5808080ca8220648685746573743480ca822064868574657374358080808080808080808080" }] } ================================================ FILE: test/storage-ethereum.js ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ /* Ethereum Storage contract test Tests here are standalone unit tests for Ion functionality. Other contracts have been mocked to simulate basic behaviour. Tests Ethereum block structure decoding and verification of state transitions. */ const Web3Utils = require('web3-utils'); const utils = require('./helpers/utils.js'); const BN = require('bignumber.js') const encoder = require('./helpers/encoder.js') const rlp = require('rlp'); const async = require('async') const sha3 = require('js-sha3').keccak_256 // Connect to the Test RPC running const Web3 = require('web3'); const web3 = new Web3(); web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545')); const MockIon = artifacts.require("MockIon"); const MockValidation = artifacts.require("MockValidation"); const EthereumStore = artifacts.require("EthereumStore"); require('chai') .use(require('chai-as-promised')) .should(); const DEPLOYEDCHAINID = "0xab830ae0774cb20180c8b463202659184033a9f30a21550b89a2b406c3ac8075" const TESTCHAINID = "0x22b55e8a4f7c03e1689da845dd463b09299cb3a574e64c68eafc4e99077a7254" /* TESTRPC TEST DATA */ const block = web3.eth.getBlock(1); const TESTBLOCK = { difficulty: 2, extraData: '0xd68301080d846765746886676f312e3130856c696e7578000000000000000000583a78dd245604e57368cb2688e42816ebc86eff73ee219dd96b8a56ea6392f75507e703203bc2cc624ce6820987cf9e8324dd1f9f67575502fe6060d723d0e100', gasLimit: 7509409, gasUsed: 2883490, hash: '0x694752333dd1bd0f806cc6ef1063162f4f330c88f9dcd9e61174fcf5e4927eb7', logsBloom: '0x22440000020000090000000000000000041000080000008000088000080000000200000400000800000000000000400000000000000000000010000008020102000000000000080000000008800000000000022000000004000000010000000000080000000620400440100010200400082000000000000080040010000100020020000000000000080080000001000000000100000400480000000002000000002000080018000008108000100000000000000000020000050010001004000000000102000040004000000000000000000000004400000000000000000000000208000000000400008200020000004022400000000004000200848000000000', miner: '0x0000000000000000000000000000000000000000', mixHash: '0x0000000000000000000000000000000000000000000000000000000000000000', nonce: '0x0000000000000000', number: 2657422, parentHash: '0x3471555ab9a99528f02f9cdd8f0017fe2f56e01116acc4fe7f78aee900442f35', receiptsRoot: '0x907121bec78b40e8256fac47867d955c560b321e93fc9f046f919ffb5e3823ff', sha3Uncles: '0x1dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347', size: 4848, stateRoot: '0xf526f481ffb6c3c56956d596f2b23e1f7ff17c810ba59efb579d9334a1765444', timestamp: 1531931421, totalDifficulty: 5023706, transactions: [ '0x7adbc5ee3712552a1e85962c3ea3d82394cfed7960d60c12d60ebafe67445450', '0x6be870e6dfb11894b64371560ec39e563cef91642afd193bfa67874f3508a282', '0x5ba6422455cb7127958df15c453bfe60d92921b647879864b531fd6589e36af4', '0xa2597e6fe6882626e12055b1378025aa64a85a03dd23f5dc66034f2ef3746810', '0x7ffb940740050ae3604f99a4eef07c83de5d75076cae42cb1561c370cba3a0a3', '0x4d6326a6d4cf606c7e44a4ae6710acd3876363bcaabd1b1b59d29fff4da223c5', '0x10b3360ef00cd7c4faf826365fddbd33938292c98c55a4cdb37194a142626f63', '0x655290cb44be2e64d3b1825a86d5647579015c5cffb03ede7f67eb34cea6b97f', '0x6b5e025ea558f4872112a39539ce9a819bfbb795b04eefcc45e1cf5ea947614c', '0xefd68b516babcf8a4ca74a358cfca925d9d2d5177ef7b859f3d9183ff522efe8', '0xa056eeeeb098fd5adb283e12e77a239797c96860c21712963f183937613d3391', '0xa5d1adf694e3442975a13685a9c7d9013c05a4fdcea5bc827566a331b2fead2b', '0x95a47360f89c48f0b1a484cbeee8816b6a0e2fc321bdb9db48082bd7272b4ebc', '0x896d29a87393c6607844fa545d38eb96056d5310a6b4e056dc00adde67c24be2', '0xef3ce2ad9259920094f7fd5ad00453b35888662696ae9b85a393e55cde3ec28d', '0x2de8af9b4e84b3ac93adfce81964cc69bafd0a2dbcac3a5f7628ee9e56fd1c8a', '0x2790cdb3377f556e8f5bc8eaaf9c6c0d36d0f242c2e4226af2aac0203f43019b', '0x98ae65246249785bd1ac8157900f7e1a2c69d5c3b3ffc97d55b9eacab3e212f0', '0x7d4f090c58880761eaaab1399864d4a52631db8f0b21bfb7051f9a214ad07993', '0xafc3ab60059ed38e71c7f6bea036822abe16b2c02fcf770a4f4b5fffcbfe6e7e', '0x2af8f6c49d1123077f1efd13764cb2a50ff922fbaf49327efc44c6048c38c968', '0x6d5e1753dc91dae7d528ab9b02350e726e006a5591a5d315a34a46e2a951b3fb', '0xdc864827159c7fde6bbd1672ed9a90ce5d69f5d0c81761bf689775d19a90387e', '0x22fb4d90a7125988b2857c50709e544483f898cb1e8036477f9ddd94b177bf93', '0x999c2e2ba342bed4ccedea01d638db3bbd1abd6d10784c317843880841db6dec', '0x11355abb5fe745ed458b2a78e116f4a8c2fe046a131eafe08f30d23bd9d10394' ], transactionsRoot: '0x07f36c7ad26564fa65daebda75a23dfa95d660199092510743f6c8527dd72586', uncles: [] } const VALIDATORS = ["0x42eb768f2244c8811c63729a21a3569731535f06", "0x6635f83421bf059cd8111f180f0727128685bae4", "0x7ffc57839b00206d1ad20c69a1981b489f772031", "0xb279182d99e65703f0076e4812653aab85fca0f0", "0xd6ae8250b8348c94847280928c79fb3b63ca453e", "0xda35dee8eddeaa556e4c26268463e26fb91ff74f", "0xfc18cbc391de84dbd87db83b20935d3e89f5dd91"] const signedHeader = [ TESTBLOCK.parentHash, TESTBLOCK.sha3Uncles, TESTBLOCK.miner, TESTBLOCK.stateRoot, TESTBLOCK.transactionsRoot, TESTBLOCK.receiptsRoot, TESTBLOCK.logsBloom, Web3Utils.toBN(TESTBLOCK.difficulty), Web3Utils.toBN(TESTBLOCK.number), TESTBLOCK.gasLimit, TESTBLOCK.gasUsed, Web3Utils.toBN(TESTBLOCK.timestamp), TESTBLOCK.extraData, TESTBLOCK.mixHash, TESTBLOCK.nonce ]; // Remove last 65 Bytes of extraData const extraBytes = utils.hexToBytes(TESTBLOCK.extraData); const extraBytesShort = extraBytes.splice(1, extraBytes.length-66); const extraDataSignature = '0x' + utils.bytesToHex(extraBytes.splice(extraBytes.length-65)); const extraDataShort = '0x' + utils.bytesToHex(extraBytesShort); const unsignedHeader = [ TESTBLOCK.parentHash, TESTBLOCK.sha3Uncles, TESTBLOCK.miner, TESTBLOCK.stateRoot, TESTBLOCK.transactionsRoot, TESTBLOCK.receiptsRoot, TESTBLOCK.logsBloom, Web3Utils.toBN(TESTBLOCK.difficulty), Web3Utils.toBN(TESTBLOCK.number), TESTBLOCK.gasLimit, TESTBLOCK.gasUsed, Web3Utils.toBN(TESTBLOCK.timestamp), extraDataShort, // extraData minus the signature TESTBLOCK.mixHash, TESTBLOCK.nonce ]; const TEST_SIGNED_HEADER = '0x' + rlp.encode(signedHeader).toString('hex'); const signedHeaderHash = Web3Utils.sha3(TEST_SIGNED_HEADER); const TEST_UNSIGNED_HEADER = '0x' + rlp.encode(unsignedHeader).toString('hex'); const unsignedHeaderHash = Web3Utils.sha3(TEST_UNSIGNED_HEADER); const TESTRLPENCODING = "0xf9025ca03471555ab9a99528f02f9cdd8f0017fe2f56e01116acc4fe7f78aee900442f35a01dcc4de8dec75d7aab85b567b6ccd41ad312451b948a7413f0a142fd40d49347940000000000000000000000000000000000000000a0f526f481ffb6c3c56956d596f2b23e1f7ff17c810ba59efb579d9334a1765444a007f36c7ad26564fa65daebda75a23dfa95d660199092510743f6c8527dd72586a0907121bec78b40e8256fac47867d955c560b321e93fc9f046f919ffb5e3823ffb90100224400000200000900000000000000000410000800000080000880000800000002000004000008000000000000004000000000000000000000100000080201020000000000000800000000088000000000000220000000040000000100000000000800000006204004401000102004000820000000000000800400100001000200200000000000000800800000010000000001000004004800000000020000000020000800180000081080001000000000000000000200000500100010040000000001020000400040000000000000000000000044000000000000000000000002080000000004000082000200000040224000000000040002008480000000000283288c8e837295a1832bffa2845b4f6b1db861d68301080d846765746886676f312e3130856c696e7578000000000000000000583a78dd245604e57368cb2688e42816ebc86eff73ee219dd96b8a56ea6392f75507e703203bc2cc624ce6820987cf9e8324dd1f9f67575502fe6060d723d0e100a00000000000000000000000000000000000000000000000000000000000000000880000000000000000" const TEST_PATH = "0x13" const TEST_TX_VALUE = "0xf86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288" const TEST_TX_NODES = "0xf90235f871a0804f9c841a6a1d3361d79980581c84e5b4d3e4c9bf33951346775542d0ee0728a0edadb5e660118ea4323654191131b62c81fc00203a15a21c925f9f50d0e4b3e4808080808080a03eda2d64b94c5ed45026a29c75c99677d44c561ea5efea30c1db6299871d5c2e8080808080808080f90151a0bc285699e68d2fe18e7af2cdf7e7e6456e91a3fd31e3c9935bc5bef92e94bf4ba06eb963b2c3a3b6c07a7221aa6f6f86f7cb8ddb45ab1ff1a9dc781f34da1f081fa0deea5b5566e7a5634d91c5fb56e25f4370e3531e2fd71ee17ed6c4ad0be2ced3a0b4e9d14555f162e811cfbcbff9b98a271a197b75271565f693912c2ff75e2131a03b0bc2d764fbefd76848ee2da7b211eb230ede08d8c54e6a868be9f5e42122c1a0b6dd488ad4fb82b0a98dff81ac6766d1dec26b29dc06174de1d315b0ab0bdf0ca066c20ff06dc33777f53eec32b0b9a8d99872bec24bb3998bb520ae6897c21d7ea02db2a399f611ba7993efb4768938a6f61b4add8959ce4c89f201f41e882ff375a02e31051a9f938b9b342b8070db3dd829f62da8d0c83a6dff91a4e3b4cb2adb9ea090e75708e7dbf856b75ed126a960085419fcde0e6a0129a92dffc0cb83ac089680808080808080f86c20b869f86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288" const TEST_RECEIPT_VALUE = "0xf901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613e" const TEST_RECEIPT_NODES = "0xf90335f871a012d378fe6800bc18f22e715a31971ef7e73ac5d1d85384f4b66ac32036ae43dea004d6e2678656a957ac776dbef512a04d266c1af3e2c5587fd233261a3d423213808080808080a05fac317a4d6d78181319fbc7e2cae4a9260f1a6afb5c6fea066e2308eed416818080808080808080f90151a03da235c6dd0fbdaf208c60cbdca0d609dee2ba107495aa7adaa658362616c8aaa09ebf378a9064aa4da0512c55c790a5e007ac79d2713e4533771cd2c95be47a4da0c06fed36ffe1f2ec164ba88f73b353960448d2decbb65355c5298a33555de742a0e057afe423ee17e5499c570a56880b0f5b5c1884b90ff9b9b5baa827f72fc816a093e06093cd2fdb67e0f87cfcc35ded2f445cc1309a0ff178e59f932aeadb6d73a0193e4e939fbc5d34a570bea3fff7c6d54adcb1c3ab7ef07510e7bd5fcef2d4b3a0a17a0c71c0118092367220f65b67f2ba2eb9068ff5270baeabe8184a01a37f14a03479a38e63123d497588ad5c31d781276ec8c11352dd3895c8add34f9a2b786ba042254728bb9ab94b58adeb75d2238da6f30382969c00c65e55d4cc4aa474c0a6a03c088484aa1c73b8fb291354f80e9557ab75a01c65d046c2471d19bd7f2543d880808080808080f9016b20b90167f901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613e" const TRIG_DEPLOYED_RINKEBY_ADDR = "0x61621bcf02914668f8404c1f860e92fc1893f74c"; const TRIG_FIRED_RINKEBY_TXHASH = "0xafc3ab60059ed38e71c7f6bea036822abe16b2c02fcf770a4f4b5fffcbfe6e7e" const TRIG_FIRED_RINKEBY_BLOCKNO = 2657422 const TRIG_CALLED_BY = "0x279884e133f9346f2fad9cc158222068221b613e"; const GENESIS_HASH = TESTBLOCK.parentHash; contract('EthereumStore.js', (accounts) => { let ion; let validation; let storage; beforeEach('setup contract for each test', async function () { ion = await MockIon.new(DEPLOYEDCHAINID); validation = await MockValidation.new(ion.address); storage = await EthereumStore.new(ion.address); }) describe('Register Chain', () => { it('Successful Register Chain', async () => { // Successfully add id of another chain await ion.addChain(storage.address, TESTCHAINID); let chainRegistered = storage.m_chains(TESTCHAINID); assert(chainRegistered); }) it('Fail Register Current Chain', async () => { // Fail adding deployment chain id await ion.addChain(storage.address, DEPLOYEDCHAINID).should.be.rejected; }) it('Fail Register Chain Twice', async () => { // Successfully add id of another chain await ion.addChain(storage.address, TESTCHAINID); let chainRegistered = storage.m_chains(TESTCHAINID); assert(chainRegistered); await ion.addChain(storage.address, TESTCHAINID).should.be.rejected; }) }) describe('Add Block', () => { it('Successful Add Block', async () => { // Successfully add id of another chain await ion.addChain(storage.address, TESTCHAINID); await ion.storeBlock(storage.address, TESTCHAINID, TESTRLPENCODING); }) it('Fail Add Block from unregistered chain', async () => { await ion.storeBlock(storage.address, TESTCHAINID, TESTRLPENCODING).should.be.rejected; }) it('Fail Add Block from non-ion', async () => { await ion.addChain(storage.address, TESTCHAINID); await storage.addBlock(TESTCHAINID, TESTBLOCK.hash, TESTRLPENCODING).should.be.rejected; }) it('Fail Add Block with malformed data', async () => { await ion.addChain(storage.address, TESTCHAINID); await ion.storeBlock(storage.address, TESTCHAINID, TEST_TX_VALUE).should.be.rejected; }) it('Fail Add Same Block Twice', async () => { await ion.addChain(storage.address, TESTCHAINID); await ion.storeBlock(storage.address, TESTCHAINID, TESTRLPENCODING); await ion.storeBlock(storage.address, TESTCHAINID, TESTRLPENCODING).should.be.rejected; }) }) describe('Proof Decoding', () => { it('Successful Proof Decoding', async () => { compressedProof = generateProof(); CLIGeneratedProof = "0xf9074113f86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288f90235f871a0804f9c841a6a1d3361d79980581c84e5b4d3e4c9bf33951346775542d0ee0728a0edadb5e660118ea4323654191131b62c81fc00203a15a21c925f9f50d0e4b3e4808080808080a03eda2d64b94c5ed45026a29c75c99677d44c561ea5efea30c1db6299871d5c2e8080808080808080f90151a0bc285699e68d2fe18e7af2cdf7e7e6456e91a3fd31e3c9935bc5bef92e94bf4ba06eb963b2c3a3b6c07a7221aa6f6f86f7cb8ddb45ab1ff1a9dc781f34da1f081fa0deea5b5566e7a5634d91c5fb56e25f4370e3531e2fd71ee17ed6c4ad0be2ced3a0b4e9d14555f162e811cfbcbff9b98a271a197b75271565f693912c2ff75e2131a03b0bc2d764fbefd76848ee2da7b211eb230ede08d8c54e6a868be9f5e42122c1a0b6dd488ad4fb82b0a98dff81ac6766d1dec26b29dc06174de1d315b0ab0bdf0ca066c20ff06dc33777f53eec32b0b9a8d99872bec24bb3998bb520ae6897c21d7ea02db2a399f611ba7993efb4768938a6f61b4add8959ce4c89f201f41e882ff375a02e31051a9f938b9b342b8070db3dd829f62da8d0c83a6dff91a4e3b4cb2adb9ea090e75708e7dbf856b75ed126a960085419fcde0e6a0129a92dffc0cb83ac089680808080808080f86c20b869f86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288f901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613ef90335f871a012d378fe6800bc18f22e715a31971ef7e73ac5d1d85384f4b66ac32036ae43dea004d6e2678656a957ac776dbef512a04d266c1af3e2c5587fd233261a3d423213808080808080a05fac317a4d6d78181319fbc7e2cae4a9260f1a6afb5c6fea066e2308eed416818080808080808080f90151a03da235c6dd0fbdaf208c60cbdca0d609dee2ba107495aa7adaa658362616c8aaa09ebf378a9064aa4da0512c55c790a5e007ac79d2713e4533771cd2c95be47a4da0c06fed36ffe1f2ec164ba88f73b353960448d2decbb65355c5298a33555de742a0e057afe423ee17e5499c570a56880b0f5b5c1884b90ff9b9b5baa827f72fc816a093e06093cd2fdb67e0f87cfcc35ded2f445cc1309a0ff178e59f932aeadb6d73a0193e4e939fbc5d34a570bea3fff7c6d54adcb1c3ab7ef07510e7bd5fcef2d4b3a0a17a0c71c0118092367220f65b67f2ba2eb9068ff5270baeabe8184a01a37f14a03479a38e63123d497588ad5c31d781276ec8c11352dd3895c8add34f9a2b786ba042254728bb9ab94b58adeb75d2238da6f30382969c00c65e55d4cc4aa474c0a6a03c088484aa1c73b8fb291354f80e9557ab75a01c65d046c2471d19bd7f2543d880808080808080f9016b20b90167f901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613e" assert.equal("0x" + compressedProof.toString('hex'), CLIGeneratedProof); }) }) describe('Check All Proofs', () => { it('Successful Check All Proofs', async () => { await ion.addChain(storage.address, TESTCHAINID); await ion.storeBlock(storage.address, TESTCHAINID, TESTRLPENCODING); compressedProof = generateProof(); let tx = await storage.CheckProofs(TESTCHAINID, TESTBLOCK.hash, "0x" + compressedProof.toString('hex')); console.log("\tGas used to submit check all proofs = " + tx.receipt.gasUsed.toString() + " gas"); }) it('Fail Proofs with wrong chain id', async () => { await ion.addChain(storage.address, TESTCHAINID); await ion.storeBlock(storage.address, TESTCHAINID, TESTRLPENCODING); compressedProof = generateProof(); CLIdecoded = (rlp.decode(Buffer.from("f9074113f86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288f90235f871a0804f9c841a6a1d3361d79980581c84e5b4d3e4c9bf33951346775542d0ee0728a0edadb5e660118ea4323654191131b62c81fc00203a15a21c925f9f50d0e4b3e4808080808080a03eda2d64b94c5ed45026a29c75c99677d44c561ea5efea30c1db6299871d5c2e8080808080808080f90151a0bc285699e68d2fe18e7af2cdf7e7e6456e91a3fd31e3c9935bc5bef92e94bf4ba06eb963b2c3a3b6c07a7221aa6f6f86f7cb8ddb45ab1ff1a9dc781f34da1f081fa0deea5b5566e7a5634d91c5fb56e25f4370e3531e2fd71ee17ed6c4ad0be2ced3a0b4e9d14555f162e811cfbcbff9b98a271a197b75271565f693912c2ff75e2131a03b0bc2d764fbefd76848ee2da7b211eb230ede08d8c54e6a868be9f5e42122c1a0b6dd488ad4fb82b0a98dff81ac6766d1dec26b29dc06174de1d315b0ab0bdf0ca066c20ff06dc33777f53eec32b0b9a8d99872bec24bb3998bb520ae6897c21d7ea02db2a399f611ba7993efb4768938a6f61b4add8959ce4c89f201f41e882ff375a02e31051a9f938b9b342b8070db3dd829f62da8d0c83a6dff91a4e3b4cb2adb9ea090e75708e7dbf856b75ed126a960085419fcde0e6a0129a92dffc0cb83ac089680808080808080f86c20b869f86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288f901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613ef90335f871a012d378fe6800bc18f22e715a31971ef7e73ac5d1d85384f4b66ac32036ae43dea004d6e2678656a957ac776dbef512a04d266c1af3e2c5587fd233261a3d423213808080808080a05fac317a4d6d78181319fbc7e2cae4a9260f1a6afb5c6fea066e2308eed416818080808080808080f90151a03da235c6dd0fbdaf208c60cbdca0d609dee2ba107495aa7adaa658362616c8aaa09ebf378a9064aa4da0512c55c790a5e007ac79d2713e4533771cd2c95be47a4da0c06fed36ffe1f2ec164ba88f73b353960448d2decbb65355c5298a33555de742a0e057afe423ee17e5499c570a56880b0f5b5c1884b90ff9b9b5baa827f72fc816a093e06093cd2fdb67e0f87cfcc35ded2f445cc1309a0ff178e59f932aeadb6d73a0193e4e939fbc5d34a570bea3fff7c6d54adcb1c3ab7ef07510e7bd5fcef2d4b3a0a17a0c71c0118092367220f65b67f2ba2eb9068ff5270baeabe8184a01a37f14a03479a38e63123d497588ad5c31d781276ec8c11352dd3895c8add34f9a2b786ba042254728bb9ab94b58adeb75d2238da6f30382969c00c65e55d4cc4aa474c0a6a03c088484aa1c73b8fb291354f80e9557ab75a01c65d046c2471d19bd7f2543d880808080808080f9016b20b90167f901640183252867b9010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000010000000000000000000000000400000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000f85af8589461621bcf02914668f8404c1f860e92fc1893f74ce1a027a9902e06885f7c187501d61990eae923b37634a8d6dda55a04dc7078395340a0000000000000000000000000279884e133f9346f2fad9cc158222068221b613e", 'hex'))); decoded = (rlp.decode(compressedProof)); // Fail with wrong chain ID await storage.CheckProofs(DEPLOYEDCHAINID, TESTBLOCK.hash, "0x" + compressedProof.toString('hex')).should.be.rejected; }) it('Fail Proofs with wrong block hash', async () => { await ion.addChain(storage.address, TESTCHAINID); await ion.storeBlock(storage.address, TESTCHAINID, TESTRLPENCODING); compressedProof = generateProof(); // Fail with wrong block hash await storage.CheckProofs(TESTCHAINID, TESTBLOCK.hash.substring(0, 30) + "ff", "0x" + compressedProof.toString('hex')).should.be.rejected; }) it('Fail Proofs with malformed proof', async () => { await ion.addChain(storage.address, TESTCHAINID); await ion.storeBlock(storage.address, TESTCHAINID, TESTRLPENCODING); compressedProof = generateMalformedProof(); // Fail with malformed proof await storage.CheckProofs(TESTCHAINID, TESTBLOCK.hash, "0x" + compressedProof.toString('hex')).should.be.rejected; }) it('Fail Proofs with corrupted proof', async () => { await ion.addChain(storage.address, TESTCHAINID); await ion.storeBlock(storage.address, TESTCHAINID, TESTRLPENCODING); compressedProof = generateCorruptedProof(); // Fail with corrupted proof await storage.CheckProofs(TESTCHAINID, TESTBLOCK.hash, "0x" + compressedProof.toString('hex')).should.be.rejected; }) }) }) function generateProof() { decodedPath = rlp.decode(Buffer.from(TEST_PATH.slice(2), 'hex')); decodedTx = rlp.decode(Buffer.from(TEST_TX_VALUE.slice(2), 'hex')); decodedTxNodes = rlp.decode(Buffer.from(TEST_TX_NODES.slice(2), 'hex')); decodedReceipt = rlp.decode(Buffer.from(TEST_RECEIPT_VALUE.slice(2), 'hex')); decodedReceiptNodes = rlp.decode(Buffer.from(TEST_RECEIPT_NODES.slice(2), 'hex')); proof = rlp.encode([decodedPath, decodedTx, decodedTxNodes, decodedReceipt, decodedReceiptNodes]); return proof; } function generateMalformedProof() { decodedPath = rlp.decode(Buffer.from(TEST_PATH.slice(2), 'hex')); decodedTx = rlp.decode(Buffer.from(TEST_TX_VALUE.slice(2), 'hex')); decodedTxNodes = rlp.decode(Buffer.from(TEST_TX_NODES.slice(2), 'hex')); decodedReceipt = rlp.decode(Buffer.from(TEST_RECEIPT_VALUE.slice(2), 'hex')); // Exclude receipt nodes // decodedReceiptNodes = rlp.decode(Buffer.from(TEST_RECEIPT_NODES.slice(2), 'hex')); proof = rlp.encode([decodedPath, decodedTx, decodedTxNodes, decodedReceipt]); return proof; } function generateCorruptedProof() { decodedPath = rlp.decode(Buffer.from(TEST_PATH.slice(2), 'hex')); decodedTx = rlp.decode(Buffer.from(TEST_TX_VALUE.slice(2), 'hex')); decodedTxNodes = rlp.decode(Buffer.from(TEST_TX_NODES.slice(2), 'hex')); decodedReceipt = rlp.decode(Buffer.from(TEST_RECEIPT_VALUE.slice(2), 'hex')); decodedReceiptNodes = rlp.decode(Buffer.from(TEST_RECEIPT_NODES.slice(2), 'hex')); proof = rlp.encode([decodedPath, sha3(decodedTx), decodedTxNodes, decodedReceipt, decodedReceiptNodes]); return proof; } ================================================ FILE: test/storage-fabric.js ================================================ // Copyright (c) 2016-2018 Clearmatics Technologies Ltd // SPDX-License-Identifier: LGPL-3.0+ /* Fabric Storage contract test Tests here are standalone unit tests for Ion functionality. Other contracts have been mocked to simulate basic behaviour. Tests Fabric block structure decoding and verification of state transitions. */ const rlp = require('rlp'); const async = require('async') const util = require('util'); // Connect to the Test RPC running const Web3 = require('web3'); const web3 = new Web3(); web3.setProvider(new web3.providers.HttpProvider('http://localhost:8545')); const MockIon = artifacts.require("MockIon"); const FabricStore = artifacts.require("FabricStore"); require('chai') .use(require('chai-as-promised')) .should(); const DEPLOYEDCHAINID = "0xab830ae0774cb20180c8b463202659184033a9f30a21550b89a2b406c3ac8075" const TESTCHAINID = "0x22b55e8a4f7c03e1689da845dd463b09299cb3a574e64c68eafc4e99077a7254" const TESTDATA = [{ channelId: "orgchannel", blocks: [{ hash: "vBmkcC8xbLMAUK-wkLMYGDz9qFdu1n8SbsHp62Of_-o", number: 4, prevHash: "hnw1EQE3SXA_LCUsRWXAj5nZ_JjLPm6DGiRn-g7g4Pc", dataHash: "_xuKFW3Po3gNBXjXac11M39a-o-_92_PC6DWBUWnk4I", timestampS: 1548756504, timestampN: 121452526, transactions: [{ txId: "d4a03d5b71ac3fab92b90bae047c9c5e6ccf0b4396be6807c1724fc0139f999b", nsrw: [{ namespace: "ExampleCC", readsets: [{ key: "A", version: { blockNumber: 3, txNumber: 0 } }, { key: "B", version: { blockNumber: 3, txNumber: 0 } }], writesets: [{ key: "A", isDelete: "false", value: "0" }, { key: "B", isDelete: "false", value: "3" }] }, { namespace: "lscc", readsets: [{ key: "ExampleCC", version: { blockNumber: 3, txNumber: 0 } }], writesets: [] }] }] }] }] const formattedData = [[ TESTDATA[0].channelId, [ TESTDATA[0].blocks[0].hash, TESTDATA[0].blocks[0].number, TESTDATA[0].blocks[0].prevHash, TESTDATA[0].blocks[0].dataHash, TESTDATA[0].blocks[0].timestampS, TESTDATA[0].blocks[0].timestampN, [[ TESTDATA[0].blocks[0].transactions[0].txId, [[ TESTDATA[0].blocks[0].transactions[0].nsrw[0].namespace, [[ TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].key, [ TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].version.txNumber ] ], [ TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].key, [ TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].version.txNumber ] ]], [[ TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].isDelete, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].value ],[ TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].isDelete, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].value ]] ], [ TESTDATA[0].blocks[0].transactions[0].nsrw[1].namespace, [[ TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].key, [ TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].version.txNumber ] ]], [] ]] ]] ] ]]; contract('FabricStore.sol', (accounts) => { let ion; let storage; let rlpEncodedBlock_Gen = "0x" + rlp.encode(formattedData).toString('hex'); // Generated by CLI https://github.com/Shirikatsu/fabric-examples/blob/fc3ff7243f282a4edf21c6667e71ab02e759c3c5/fabric-cli/cmd/fabric-cli/printer/encoder.go#L40 let rlpEncodedBlock = "0xf90127f901248a6f72676368616e6e656cf90116ab76426d6b63433878624c4d41554b2d776b4c4d5947447a3971466475316e38536273487036324f665f2d6f04ab686e7731455145335358415f4c435573525758416a356e5a5f4a6a4c506d36444769526e2d673767345063ab5f78754b465733506f33674e42586a58616331314d3339612d6f2d5f39325f50433644574255576e6b3449845c50261884073d37eef885f883b84064346130336435623731616333666162393262393062616530343763396335653663636630623433393662653638303763313732346663303133396639393962f83fe8894578616d706c654343cac441c20380c442c20380d2c8418566616c736530c8428566616c736533d5846c736363cecd894578616d706c654343c20380c0"; beforeEach('setup contract for each test', async function () { ion = await MockIon.new(DEPLOYEDCHAINID); storage = await FabricStore.new(ion.address); }) describe('Block Encode', () => { it('Correct Encoding', async () => { assert.equal(rlpEncodedBlock_Gen, rlpEncodedBlock); }) }) describe('Register Chain', () => { it('Successful Register Chain', async () => { // Successfully add id of another chain await ion.addChain(storage.address, TESTCHAINID); let chainRegistered = await storage.m_chains(TESTCHAINID); assert(chainRegistered); let chainId = await storage.m_networks.call(TESTCHAINID); assert.equal(TESTCHAINID, chainId); }) it('Fail Register Current Chain', async () => { // Fail adding deployment chain id await ion.addChain(storage.address, DEPLOYEDCHAINID).should.be.rejected; }) it('Fail Register Chain Twice', async () => { // Successfully add id of another chain await ion.addChain(storage.address, TESTCHAINID); let chainRegistered = storage.m_chains(TESTCHAINID); assert(chainRegistered); await ion.addChain(storage.address, TESTCHAINID).should.be.rejected; }) }) describe('Add Block', () => { it('Successful Add Block', async () => { // Successfully add id of another chain await ion.addChain(storage.address, TESTCHAINID); let receipt = await ion.storeBlock(storage.address, TESTCHAINID, rlpEncodedBlock); console.log("\tGas used to store fabric block: %d", receipt.receipt.gasUsed); let block = await storage.getBlock.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].hash); assert.equal(block[0], TESTDATA[0].blocks[0].number); assert.equal(block[1], TESTDATA[0].blocks[0].hash); assert.equal(block[2], TESTDATA[0].blocks[0].prevHash); assert.equal(block[3], TESTDATA[0].blocks[0].dataHash); assert.equal(block[4], TESTDATA[0].blocks[0].timestampS); assert.equal(block[5], TESTDATA[0].blocks[0].timestampN); assert.equal(block[6], TESTDATA[0].blocks[0].transactions[0].txId); let tx = await storage.getTransaction.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].txId); assert.equal(tx[0], TESTDATA[0].blocks[0].hash); assert.equal(tx[1], TESTDATA[0].blocks[0].transactions[0].nsrw[0].namespace + "," + TESTDATA[0].blocks[0].transactions[0].nsrw[1].namespace); let txExists = await storage.isTransactionExists.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].txId); assert(txExists); let nsrw = await storage.getNSRW.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].txId, TESTDATA[0].blocks[0].transactions[0].nsrw[0].namespace); let expectedReadset = util.format("{ key: %s, version: { blockNo: %d, txNo: %d } } { key: %s, version: { blockNo: %d, txNo: %d } } ", TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[0].version.txNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[0].readsets[1].version.txNumber ) assert.equal(expectedReadset, nsrw[0]); let expectedWriteset = util.format("{ key: %s, isDelete: %s, value: %s } { key: %s, isDelete: %s, value: %s } ", TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].isDelete, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].value, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].key, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].isDelete, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].value ) assert.equal(expectedWriteset, nsrw[1]); nsrw = await storage.getNSRW.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].txId, TESTDATA[0].blocks[0].transactions[0].nsrw[1].namespace); expectedReadset = util.format("{ key: %s, version: { blockNo: %d, txNo: %d } } ", TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].key, TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].version.blockNumber, TESTDATA[0].blocks[0].transactions[0].nsrw[1].readsets[0].version.txNumber ) assert.equal(expectedReadset, nsrw[0]); expectedWriteset = ""; assert.equal(expectedWriteset, nsrw[1]); let state = await storage.getState.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].key); assert.equal(state[0], TESTDATA[0].blocks[0].number); assert.equal(state[1], 0); assert.equal(state[2], TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[0].value); state = await storage.getState.call(TESTCHAINID, TESTDATA[0].channelId, TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].key); assert.equal(state[0], TESTDATA[0].blocks[0].number); assert.equal(state[1], 0); assert.equal(state[2], TESTDATA[0].blocks[0].transactions[0].nsrw[0].writesets[1].value); }) it('Fail Add Block from unregistered chain', async () => { await ion.storeBlock(storage.address, TESTCHAINID, rlpEncodedBlock).should.be.rejected; }) it('Fail Add Block from non-ion', async () => { await ion.addChain(storage.address, TESTCHAINID); await storage.addBlock(TESTCHAINID, rlpEncodedBlock).should.be.rejected; }) it('Fail Add Block with malformed data', async () => { // Successfully add id of another chain await ion.addChain(storage.address, TESTCHAINID); await ion.storeBlock(storage.address, TESTCHAINID, "0xf86707843b9aca008257c39461621bcf02914668f8404c1f860e92fc1893f74c8084457094cc1ba07e2ebe15f4ece2fd8ffc9a49d7e9e4e71a30534023ca6b24ab4000567709ad53a013a61e910eb7145aa93e865664c54846f26e09a74bd577eaf66b5dd00d334288").should.be.rejected; }) it('Fail Add Same Block Twice', async () => { // Successfully add id of another chain await ion.addChain(storage.address, TESTCHAINID); await ion.storeBlock(storage.address, TESTCHAINID, rlpEncodedBlock); await ion.storeBlock(storage.address, TESTCHAINID, rlpEncodedBlock).should.be.rejected; }) }) }) ================================================ FILE: truffle.js ================================================ module.exports = { networks: { development: { host: "localhost", port: 8545, gas: 0xFFFFFFFFFFFFF, network_id: "*" }, clique: { host: "localhost", port: 8501, network_id: "*" }, coverage: { host: "localhost", port: 8555, network_id: "*", // Match any network id gas: 0xFFFFFFF, gasprice: 0x1 }, }, mocha: { useColors: true, enableTimeouts: false }, solc: { optimizer: { enabled: true, runs: 200 } } };