├── .editorconfig ├── .github └── workflows │ ├── pushpull.yml │ └── weekly.yml ├── .gitignore ├── .projectile ├── .tool-versions ├── LICENSE ├── Makefile ├── README.md ├── cocol.http ├── config.yml ├── docs ├── Cocol.html ├── Cocol │ ├── App.html │ ├── App │ │ ├── Api.html │ │ └── Miner.html │ ├── Command.html │ └── Command │ │ ├── Command_Main_command_of_clim_library.html │ │ └── Command_Main_command_of_clim_library │ │ ├── Options_Main_command_of_clim_library.html │ │ ├── Options_Main_command_of_clim_library │ │ ├── Option_address.html │ │ ├── Option_help.html │ │ ├── Option_host.html │ │ ├── Option_master.html │ │ ├── Option_max_connections.html │ │ ├── Option_miner.html │ │ ├── Option_port.html │ │ └── Option_version.html │ │ └── RunProc.html ├── Event.html ├── Event │ ├── EventType.html │ ├── NewBlockEvent.html │ ├── PeerConnectionEvent.html │ ├── Repo.html │ └── TransactionEvent.html ├── Ledger.html ├── Ledger │ ├── Action.html │ ├── Action │ │ ├── Abstract.html │ │ ├── Signature.html │ │ ├── Stake.html │ │ ├── Transaction.html │ │ └── TxnHash.html │ ├── Block.html │ ├── Block │ │ ├── Abstract.html │ │ ├── BlockHash.html │ │ ├── BlockHashSeed.html │ │ ├── Coinbase.html │ │ ├── Pos.html │ │ └── Pow.html │ ├── Inventory.html │ ├── Mempool.html │ ├── Mempool │ │ └── TxnHash.html │ ├── Pos.html │ ├── Pow.html │ ├── Repo.html │ ├── Repo │ │ ├── Block.html │ │ └── BlockHash.html │ ├── Sync.html │ └── Util.html ├── Messenger.html ├── Messenger │ ├── Action.html │ ├── Action │ │ ├── Base.html │ │ ├── GetBlock.html │ │ ├── GetPeers.html │ │ ├── Handshake.html │ │ ├── Inventory.html │ │ └── PropagateTransaction.html │ ├── Repo.html │ ├── Struct.html │ └── Struct │ │ └── Peer.html ├── Node.html ├── Node │ └── Settings.html ├── css │ └── style.css ├── empty ├── index.html ├── index.json ├── js │ └── doc.js └── search-index.js ├── explorer ├── elliptic.min.js ├── explorer.js ├── index.html └── vivagraph.min.js ├── img ├── demo.gif ├── demo2.gif └── header.png ├── script ├── live.cr └── start.sh ├── shard.lock ├── shard.yml ├── spec ├── block_spec.cr ├── ledger_repo_spec.cr ├── ledger_spec.cr ├── ledger_util_spec.cr ├── mempool_spec.cr ├── spec_helper.cr └── transaction_spec.cr └── src ├── cocol.cr ├── cocol ├── logger.cr ├── node │ ├── event.cr │ ├── event │ │ ├── api.cr │ │ └── repo.cr │ ├── ledger.cr │ ├── ledger │ │ ├── action.cr │ │ ├── api.cr │ │ ├── block.cr │ │ ├── mempool.cr │ │ ├── repo.cr │ │ └── util.cr │ ├── messenger.cr │ ├── messenger │ │ ├── api.cr │ │ ├── repo.cr │ │ └── struct │ │ │ └── peer.cr │ └── settings.cr └── version.cr └── deps.cr /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*.cr] 4 | charset = utf-8 5 | end_of_line = lf 6 | insert_final_newline = true 7 | indent_style = space 8 | indent_size = 2 9 | trim_trailing_whitespace = true 10 | -------------------------------------------------------------------------------- /.github/workflows/pushpull.yml: -------------------------------------------------------------------------------- 1 | name: pushpull 2 | 3 | on: 4 | push: 5 | branches: 6 | - master 7 | pull_request: 8 | branches: 9 | - master 10 | 11 | jobs: 12 | build: 13 | 14 | runs-on: ubuntu-latest 15 | 16 | container: 17 | image: crystallang/crystal 18 | 19 | steps: 20 | - uses: actions/checkout@v1 21 | - name: Install dependencies 22 | run: shards install 23 | - name: Run tests 24 | run: crystal spec --error-trace -t -Dpreview_mt 25 | - name: Check formatting 26 | run: crystal tool format --check 27 | -------------------------------------------------------------------------------- /.github/workflows/weekly.yml: -------------------------------------------------------------------------------- 1 | name: weekly 2 | on: 3 | push: 4 | branches: 5 | - master 6 | schedule: 7 | - cron: 0 1 * * 0 8 | 9 | jobs: 10 | build: 11 | runs-on: ubuntu-latest 12 | container: 13 | image: crystallang/crystal 14 | steps: 15 | - uses: actions/checkout@v1 16 | - name: Install Dependencies 17 | run: shards install 18 | - name: Check formatting 19 | run: crystal tool format --check 20 | - name: Run tests 21 | run: crystal spec --error-trace -t -Dpreview_mt 22 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /doc/ 2 | /lib/ 3 | /playground/ 4 | /bin/ 5 | /.shards/ 6 | *.dwarf 7 | 8 | TODOs.org 9 | -------------------------------------------------------------------------------- /.projectile: -------------------------------------------------------------------------------- 1 | -/docs 2 | -/lib 3 | -/bin 4 | -/img 5 | -------------------------------------------------------------------------------- /.tool-versions: -------------------------------------------------------------------------------- 1 | crystal 0.31.1 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Mozilla Public License Version 2.0 2 | ================================== 3 | 4 | 1. Definitions 5 | -------------- 6 | 7 | 1.1. "Contributor" 8 | means each individual or legal entity that creates, contributes to 9 | the creation of, or owns Covered Software. 10 | 11 | 1.2. "Contributor Version" 12 | means the combination of the Contributions of others (if any) used 13 | by a Contributor and that particular Contributor's Contribution. 14 | 15 | 1.3. "Contribution" 16 | means Covered Software of a particular Contributor. 17 | 18 | 1.4. "Covered Software" 19 | means Source Code Form to which the initial Contributor has attached 20 | the notice in Exhibit A, the Executable Form of such Source Code 21 | Form, and Modifications of such Source Code Form, in each case 22 | including portions thereof. 23 | 24 | 1.5. "Incompatible With Secondary Licenses" 25 | means 26 | 27 | (a) that the initial Contributor has attached the notice described 28 | in Exhibit B to the Covered Software; or 29 | 30 | (b) that the Covered Software was made available under the terms of 31 | version 1.1 or earlier of the License, but not also under the 32 | terms of a Secondary License. 33 | 34 | 1.6. "Executable Form" 35 | means any form of the work other than Source Code Form. 36 | 37 | 1.7. "Larger Work" 38 | means a work that combines Covered Software with other material, in 39 | a separate file or files, that is not Covered Software. 40 | 41 | 1.8. "License" 42 | means this document. 43 | 44 | 1.9. "Licensable" 45 | means having the right to grant, to the maximum extent possible, 46 | whether at the time of the initial grant or subsequently, any and 47 | all of the rights conveyed by this License. 48 | 49 | 1.10. "Modifications" 50 | means any of the following: 51 | 52 | (a) any file in Source Code Form that results from an addition to, 53 | deletion from, or modification of the contents of Covered 54 | Software; or 55 | 56 | (b) any new file in Source Code Form that contains any Covered 57 | Software. 58 | 59 | 1.11. "Patent Claims" of a Contributor 60 | means any patent claim(s), including without limitation, method, 61 | process, and apparatus claims, in any patent Licensable by such 62 | Contributor that would be infringed, but for the grant of the 63 | License, by the making, using, selling, offering for sale, having 64 | made, import, or transfer of either its Contributions or its 65 | Contributor Version. 66 | 67 | 1.12. "Secondary License" 68 | means either the GNU General Public License, Version 2.0, the GNU 69 | Lesser General Public License, Version 2.1, the GNU Affero General 70 | Public License, Version 3.0, or any later versions of those 71 | licenses. 72 | 73 | 1.13. "Source Code Form" 74 | means the form of the work preferred for making modifications. 75 | 76 | 1.14. "You" (or "Your") 77 | means an individual or a legal entity exercising rights under this 78 | License. For legal entities, "You" includes any entity that 79 | controls, is controlled by, or is under common control with You. For 80 | purposes of this definition, "control" means (a) the power, direct 81 | or indirect, to cause the direction or management of such entity, 82 | whether by contract or otherwise, or (b) ownership of more than 83 | fifty percent (50%) of the outstanding shares or beneficial 84 | ownership of such entity. 85 | 86 | 2. License Grants and Conditions 87 | -------------------------------- 88 | 89 | 2.1. Grants 90 | 91 | Each Contributor hereby grants You a world-wide, royalty-free, 92 | non-exclusive license: 93 | 94 | (a) under intellectual property rights (other than patent or trademark) 95 | Licensable by such Contributor to use, reproduce, make available, 96 | modify, display, perform, distribute, and otherwise exploit its 97 | Contributions, either on an unmodified basis, with Modifications, or 98 | as part of a Larger Work; and 99 | 100 | (b) under Patent Claims of such Contributor to make, use, sell, offer 101 | for sale, have made, import, and otherwise transfer either its 102 | Contributions or its Contributor Version. 103 | 104 | 2.2. Effective Date 105 | 106 | The licenses granted in Section 2.1 with respect to any Contribution 107 | become effective for each Contribution on the date the Contributor first 108 | distributes such Contribution. 109 | 110 | 2.3. Limitations on Grant Scope 111 | 112 | The licenses granted in this Section 2 are the only rights granted under 113 | this License. No additional rights or licenses will be implied from the 114 | distribution or licensing of Covered Software under this License. 115 | Notwithstanding Section 2.1(b) above, no patent license is granted by a 116 | Contributor: 117 | 118 | (a) for any code that a Contributor has removed from Covered Software; 119 | or 120 | 121 | (b) for infringements caused by: (i) Your and any other third party's 122 | modifications of Covered Software, or (ii) the combination of its 123 | Contributions with other software (except as part of its Contributor 124 | Version); or 125 | 126 | (c) under Patent Claims infringed by Covered Software in the absence of 127 | its Contributions. 128 | 129 | This License does not grant any rights in the trademarks, service marks, 130 | or logos of any Contributor (except as may be necessary to comply with 131 | the notice requirements in Section 3.4). 132 | 133 | 2.4. Subsequent Licenses 134 | 135 | No Contributor makes additional grants as a result of Your choice to 136 | distribute the Covered Software under a subsequent version of this 137 | License (see Section 10.2) or under the terms of a Secondary License (if 138 | permitted under the terms of Section 3.3). 139 | 140 | 2.5. Representation 141 | 142 | Each Contributor represents that the Contributor believes its 143 | Contributions are its original creation(s) or it has sufficient rights 144 | to grant the rights to its Contributions conveyed by this License. 145 | 146 | 2.6. Fair Use 147 | 148 | This License is not intended to limit any rights You have under 149 | applicable copyright doctrines of fair use, fair dealing, or other 150 | equivalents. 151 | 152 | 2.7. Conditions 153 | 154 | Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted 155 | in Section 2.1. 156 | 157 | 3. Responsibilities 158 | ------------------- 159 | 160 | 3.1. Distribution of Source Form 161 | 162 | All distribution of Covered Software in Source Code Form, including any 163 | Modifications that You create or to which You contribute, must be under 164 | the terms of this License. You must inform recipients that the Source 165 | Code Form of the Covered Software is governed by the terms of this 166 | License, and how they can obtain a copy of this License. You may not 167 | attempt to alter or restrict the recipients' rights in the Source Code 168 | Form. 169 | 170 | 3.2. Distribution of Executable Form 171 | 172 | If You distribute Covered Software in Executable Form then: 173 | 174 | (a) such Covered Software must also be made available in Source Code 175 | Form, as described in Section 3.1, and You must inform recipients of 176 | the Executable Form how they can obtain a copy of such Source Code 177 | Form by reasonable means in a timely manner, at a charge no more 178 | than the cost of distribution to the recipient; and 179 | 180 | (b) You may distribute such Executable Form under the terms of this 181 | License, or sublicense it under different terms, provided that the 182 | license for the Executable Form does not attempt to limit or alter 183 | the recipients' rights in the Source Code Form under this License. 184 | 185 | 3.3. Distribution of a Larger Work 186 | 187 | You may create and distribute a Larger Work under terms of Your choice, 188 | provided that You also comply with the requirements of this License for 189 | the Covered Software. If the Larger Work is a combination of Covered 190 | Software with a work governed by one or more Secondary Licenses, and the 191 | Covered Software is not Incompatible With Secondary Licenses, this 192 | License permits You to additionally distribute such Covered Software 193 | under the terms of such Secondary License(s), so that the recipient of 194 | the Larger Work may, at their option, further distribute the Covered 195 | Software under the terms of either this License or such Secondary 196 | License(s). 197 | 198 | 3.4. Notices 199 | 200 | You may not remove or alter the substance of any license notices 201 | (including copyright notices, patent notices, disclaimers of warranty, 202 | or limitations of liability) contained within the Source Code Form of 203 | the Covered Software, except that You may alter any license notices to 204 | the extent required to remedy known factual inaccuracies. 205 | 206 | 3.5. Application of Additional Terms 207 | 208 | You may choose to offer, and to charge a fee for, warranty, support, 209 | indemnity or liability obligations to one or more recipients of Covered 210 | Software. However, You may do so only on Your own behalf, and not on 211 | behalf of any Contributor. You must make it absolutely clear that any 212 | such warranty, support, indemnity, or liability obligation is offered by 213 | You alone, and You hereby agree to indemnify every Contributor for any 214 | liability incurred by such Contributor as a result of warranty, support, 215 | indemnity or liability terms You offer. You may include additional 216 | disclaimers of warranty and limitations of liability specific to any 217 | jurisdiction. 218 | 219 | 4. Inability to Comply Due to Statute or Regulation 220 | --------------------------------------------------- 221 | 222 | If it is impossible for You to comply with any of the terms of this 223 | License with respect to some or all of the Covered Software due to 224 | statute, judicial order, or regulation then You must: (a) comply with 225 | the terms of this License to the maximum extent possible; and (b) 226 | describe the limitations and the code they affect. Such description must 227 | be placed in a text file included with all distributions of the Covered 228 | Software under this License. Except to the extent prohibited by statute 229 | or regulation, such description must be sufficiently detailed for a 230 | recipient of ordinary skill to be able to understand it. 231 | 232 | 5. Termination 233 | -------------- 234 | 235 | 5.1. The rights granted under this License will terminate automatically 236 | if You fail to comply with any of its terms. However, if You become 237 | compliant, then the rights granted under this License from a particular 238 | Contributor are reinstated (a) provisionally, unless and until such 239 | Contributor explicitly and finally terminates Your grants, and (b) on an 240 | ongoing basis, if such Contributor fails to notify You of the 241 | non-compliance by some reasonable means prior to 60 days after You have 242 | come back into compliance. Moreover, Your grants from a particular 243 | Contributor are reinstated on an ongoing basis if such Contributor 244 | notifies You of the non-compliance by some reasonable means, this is the 245 | first time You have received notice of non-compliance with this License 246 | from such Contributor, and You become compliant prior to 30 days after 247 | Your receipt of the notice. 248 | 249 | 5.2. If You initiate litigation against any entity by asserting a patent 250 | infringement claim (excluding declaratory judgment actions, 251 | counter-claims, and cross-claims) alleging that a Contributor Version 252 | directly or indirectly infringes any patent, then the rights granted to 253 | You by any and all Contributors for the Covered Software under Section 254 | 2.1 of this License shall terminate. 255 | 256 | 5.3. In the event of termination under Sections 5.1 or 5.2 above, all 257 | end user license agreements (excluding distributors and resellers) which 258 | have been validly granted by You or Your distributors under this License 259 | prior to termination shall survive termination. 260 | 261 | ************************************************************************ 262 | * * 263 | * 6. Disclaimer of Warranty * 264 | * ------------------------- * 265 | * * 266 | * Covered Software is provided under this License on an "as is" * 267 | * basis, without warranty of any kind, either expressed, implied, or * 268 | * statutory, including, without limitation, warranties that the * 269 | * Covered Software is free of defects, merchantable, fit for a * 270 | * particular purpose or non-infringing. The entire risk as to the * 271 | * quality and performance of the Covered Software is with You. * 272 | * Should any Covered Software prove defective in any respect, You * 273 | * (not any Contributor) assume the cost of any necessary servicing, * 274 | * repair, or correction. This disclaimer of warranty constitutes an * 275 | * essential part of this License. No use of any Covered Software is * 276 | * authorized under this License except under this disclaimer. * 277 | * * 278 | ************************************************************************ 279 | 280 | ************************************************************************ 281 | * * 282 | * 7. Limitation of Liability * 283 | * -------------------------- * 284 | * * 285 | * Under no circumstances and under no legal theory, whether tort * 286 | * (including negligence), contract, or otherwise, shall any * 287 | * Contributor, or anyone who distributes Covered Software as * 288 | * permitted above, be liable to You for any direct, indirect, * 289 | * special, incidental, or consequential damages of any character * 290 | * including, without limitation, damages for lost profits, loss of * 291 | * goodwill, work stoppage, computer failure or malfunction, or any * 292 | * and all other commercial damages or losses, even if such party * 293 | * shall have been informed of the possibility of such damages. This * 294 | * limitation of liability shall not apply to liability for death or * 295 | * personal injury resulting from such party's negligence to the * 296 | * extent applicable law prohibits such limitation. Some * 297 | * jurisdictions do not allow the exclusion or limitation of * 298 | * incidental or consequential damages, so this exclusion and * 299 | * limitation may not apply to You. * 300 | * * 301 | ************************************************************************ 302 | 303 | 8. Litigation 304 | ------------- 305 | 306 | Any litigation relating to this License may be brought only in the 307 | courts of a jurisdiction where the defendant maintains its principal 308 | place of business and such litigation shall be governed by laws of that 309 | jurisdiction, without reference to its conflict-of-law provisions. 310 | Nothing in this Section shall prevent a party's ability to bring 311 | cross-claims or counter-claims. 312 | 313 | 9. Miscellaneous 314 | ---------------- 315 | 316 | This License represents the complete agreement concerning the subject 317 | matter hereof. If any provision of this License is held to be 318 | unenforceable, such provision shall be reformed only to the extent 319 | necessary to make it enforceable. Any law or regulation which provides 320 | that the language of a contract shall be construed against the drafter 321 | shall not be used to construe this License against a Contributor. 322 | 323 | 10. Versions of the License 324 | --------------------------- 325 | 326 | 10.1. New Versions 327 | 328 | Mozilla Foundation is the license steward. Except as provided in Section 329 | 10.3, no one other than the license steward has the right to modify or 330 | publish new versions of this License. Each version will be given a 331 | distinguishing version number. 332 | 333 | 10.2. Effect of New Versions 334 | 335 | You may distribute the Covered Software under the terms of the version 336 | of the License under which You originally received the Covered Software, 337 | or under the terms of any subsequent version published by the license 338 | steward. 339 | 340 | 10.3. Modified Versions 341 | 342 | If you create software not governed by this License, and you want to 343 | create a new license for such software, you may create and use a 344 | modified version of this License if you rename the license and remove 345 | any references to the name of the license steward (except to note that 346 | such modified license differs from this License). 347 | 348 | 10.4. Distributing Source Code Form that is Incompatible With Secondary 349 | Licenses 350 | 351 | If You choose to distribute Source Code Form that is Incompatible With 352 | Secondary Licenses under the terms of this version of the License, the 353 | notice described in Exhibit B of this License must be attached. 354 | 355 | Exhibit A - Source Code Form License Notice 356 | ------------------------------------------- 357 | 358 | This Source Code Form is subject to the terms of the Mozilla Public 359 | License, v. 2.0. If a copy of the MPL was not distributed with this 360 | file, You can obtain one at http://mozilla.org/MPL/2.0/. 361 | 362 | If it is not possible or desirable to put the notice in a particular 363 | file, then You may include the notice in a location (such as a LICENSE 364 | file in a relevant directory) where a recipient would be likely to look 365 | for such a notice. 366 | 367 | You may add additional accurate notices of copyright ownership. 368 | 369 | Exhibit B - "Incompatible With Secondary Licenses" Notice 370 | --------------------------------------------------------- 371 | 372 | This Source Code Form is "Incompatible With Secondary Licenses", as 373 | defined by the Mozilla Public License, v. 2.0. 374 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | .DEFAULT_GOAL := development 2 | 3 | development: 4 | crystal build -Dpreview_mt --progress --error-trace -o bin/cocol ./src/cocol.cr 5 | 6 | quick: 7 | crystal build src/cocol -Dpreview_mt --progress --error-trace 8 | 9 | prepare: format ameba test 10 | 11 | format: 12 | crystal tool format 13 | 14 | ameba: 15 | ameba 16 | 17 | test: 18 | crystal spec --error-trace -t -Dpreview_mt $(spec) 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # COCOL! 2 | 3 | [![Build Status](https://img.shields.io/github/workflow/status/cocol-project/cocol/weekly)](https://github.com/cocol-project/cocol/actions) ![Stability Experimental](https://img.shields.io/badge/Stability-Experimental-orange.svg?style=flat-square) ![Crystal 0.27.2](https://img.shields.io/badge/Crystal-0.32.1-blue.svg?style=flat-square&link=https://crystal-lang.org&link=https://crystal-lang.org/api/0.32.1/) ![License MPL-2.0](https://img.shields.io/badge/License-MPL--2.0-green.svg?style=flat-square) 4 | 5 | 6 | --- 7 | 8 | ![https://github.com/cocol-project/cocol/blob/master/img/demo2.gif](https://github.com/cocol-project/cocol/blob/master/img/demo2.gif) 9 | 10 | --- 11 | 12 | ## About 🌐 13 | The Cocol Project has the goal to lower the entry barrier for developers interested in building blockchains and dApps. 14 | There is still a long way to go and your help is needed. 15 | 16 | ## Installation 🏹 17 | Cocol is written in [Crystal](https://crystal-lang.org/), so make sure to follow the [installation instructions](https://crystal-lang.org/reference/installation/) first. 18 | 19 | After setting up Crystal you can clone the Cocol repository and install the dependencies: 20 | ```shell 21 | > git clone https://github.com/cocol-project/cocol.git 22 | > cd cocol 23 | > shards install 24 | ``` 25 | 26 | ## Usage ⚔ 27 | Make your changes to the code-base and than build Cocol 28 | ```shell 29 | > make 30 | ``` 31 | The binary `./cocol` offers the following CLI options 32 | 33 | ```text 34 | Options: 35 | 36 | -p --port The port your Cocol node is going to run on 37 | -m --master Making this node a master (there can only be one) 38 | --max-connections Setting the max-connections for peers. 39 | --miner Making this node a miner 40 | --update Triggering an update on launch (will catch up with the current height) 41 | 42 | ``` 43 | 44 | There is also a script that starts multiple nodes and the master for you 45 | 46 | ```shell 47 | > ./script/start.sh 66 5 48 | ``` 49 | First option is the amount of nodes and the second amount of miner. 50 | It will start the master node with the port `3000` and every other node with 51 | `3000 + n`, while all miner have port `4000 + n` 52 | 53 | 54 | Now go ahead and open the explorer in a browser: 55 | ```shell 56 | > open ./explorer/index.html 57 | ``` 58 | 59 | You should see 66 nodes and a miner (red border) 60 | 61 | Each one of the nodes has a REST API on the corresponding port (e.g. `3001`) 62 | 63 | Start creating live fake transactions and watch the network come alive 64 | ```shell 65 | > crystal script/live.cr 66 | 67 | ``` 68 | 69 | ## Development 👩‍💻👨‍💻 70 | 71 | Cocol is in a very early stage. **Expect changes, bugs and messy code.** 72 | Test coverage sucks atm. 73 | 74 | ## Contributing ️👷‍♀️👷‍♂ 75 | 76 | 1. Fork it ( https://github.com/cocol-project/cocol/fork ) 77 | 2. Create your feature branch (git checkout -b my-new-feature) 78 | 3. Commit your changes (git commit -am 'Add some feature') 79 | 4. Push to the branch (git push origin my-new-feature) 80 | 5. Create a new Pull Request 81 | 82 | ## Contributors 83 | 84 | - github: [cserb](https://github.com/cserb) | twitter: [@cerbivore](http://twitter.com/cerbivore) | Cristian Șerb - creator, maintainer 85 | -------------------------------------------------------------------------------- /cocol.http: -------------------------------------------------------------------------------- 1 | # All connected peers 2 | GET http://localhost:3001/peers 3 | 4 | # Create genesis block 5 | GET http://localhost:3000/genesis 6 | 7 | # Mine block and include pending transactions 8 | GET http://localhost:3027/internal/mine 9 | 10 | # All mined blocks 11 | GET http://localhost:3100/ledger 12 | 13 | # All mined blocks 14 | GET http://localhost:3100/candidates 15 | 16 | # All mined blocks 17 | GET http://localhost:3100/orphans 18 | 19 | # All mined blocks 20 | GET http://localhost:3100/blocks 21 | 22 | # All candidate votes 23 | GET http://localhost:3002/blocks 24 | 25 | # All pending transactions 26 | GET http://localhost:3100/transactions 27 | 28 | # Create a new transaction 29 | POST http://localhost:3001/transactions 30 | Content-Type: application/json 31 | { 32 | "from": "06303644-4b41-46d6-a094-1393ff6e2521", 33 | "to": "06303644-4b41-46d6-a094-1393ff6e2521", 34 | "timestamp": 1449970561, 35 | "amount": 87, 36 | "hash": "a2e8a2181f2783594c8c30faeef2ff2a531e3abe00238d01b0fc2cd75408f791" 37 | } 38 | 39 | # Connect lonely nodes :( 40 | POST http://localhost:3101/internal/handshake/3034 41 | Content-Type: application/json 42 | {} -------------------------------------------------------------------------------- /config.yml: -------------------------------------------------------------------------------- 1 | master: 2 | host: master.cocol.io 3 | port: 3000 4 | 5 | node: 6 | max_connections: 3 7 | -------------------------------------------------------------------------------- /docs/Cocol/App.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | Cocol::App - github.com/cocol-project/cocol 19 | 22 | 23 | 24 | 25 | 467 | 468 | 469 |
470 |

471 | 472 | module Cocol::App 473 | 474 |

475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 |

494 | 495 | 498 | 499 | Defined in: 500 |

501 | 502 | 503 | 504 | cocol.cr 505 | 506 | 507 |
508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 |
523 | 524 |
525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 |
535 | 536 | 537 | 538 | -------------------------------------------------------------------------------- /docs/Event/EventType.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | Event::EventType - github.com/cocol-project/cocol 19 | 22 | 23 | 24 | 25 | 467 | 468 | 469 |
470 |

471 | 472 | alias Event::EventType 473 | 474 |

475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 |

483 | 484 | 487 | 488 | Alias Definition 489 |

490 | String 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 |

504 | 505 | 508 | 509 | Defined in: 510 |

511 | 512 | 513 | 514 | cocol/node/event.cr 515 | 516 | 517 |
518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 |
533 | 534 |
535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 |
545 | 546 | 547 | 548 | -------------------------------------------------------------------------------- /docs/Event/NewBlockEvent.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | Event::NewBlockEvent - github.com/cocol-project/cocol 19 | 22 | 23 | 24 | 25 | 467 | 468 | 469 |
470 |

471 | 472 | alias Event::NewBlockEvent 473 | 474 |

475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 |

483 | 484 | 487 | 488 | Alias Definition 489 |

490 | NamedTuple(**T) 491 | 492 | 493 | 494 | 495 | 496 | 497 | 498 | 499 | 500 | 501 | 502 | 503 |

504 | 505 | 508 | 509 | Defined in: 510 |

511 | 512 | 513 | 514 | cocol/node/event.cr 515 | 516 | 517 |
518 | 519 | 520 | 521 | 522 | 523 | 524 | 525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 |
533 | 534 |
535 | 536 | 537 | 538 | 539 | 540 | 541 | 542 | 543 | 544 |
545 | 546 | 547 | 548 | -------------------------------------------------------------------------------- /docs/Messenger/Action.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | Messenger::Action - github.com/cocol-project/cocol 19 | 22 | 23 | 24 | 25 | 467 | 468 | 469 |
470 |

471 | 472 | module Messenger::Action 473 | 474 |

475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 |

494 | 495 | 498 | 499 | Defined in: 500 |

501 | 502 | 503 | 504 | cocol/node/messenger.cr 505 | 506 | 507 |
508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 |
523 | 524 |
525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 |
535 | 536 | 537 | 538 | -------------------------------------------------------------------------------- /docs/Messenger/Struct.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 16 | 17 | 18 | Messenger::Struct - github.com/cocol-project/cocol 19 | 22 | 23 | 24 | 25 | 467 | 468 | 469 |
470 |

471 | 472 | module Messenger::Struct 473 | 474 |

475 | 476 | 477 | 478 | 479 | 480 | 481 | 482 | 483 | 484 | 485 | 486 | 487 | 488 | 489 | 490 | 491 | 492 | 493 |

494 | 495 | 498 | 499 | Defined in: 500 |

501 | 502 | 503 | 504 | cocol/node/messenger/struct/peer.cr 505 | 506 | 507 |
508 | 509 | 510 | 511 | 512 | 513 | 514 | 515 | 516 | 517 | 518 | 519 | 520 | 521 | 522 |
523 | 524 |
525 | 526 | 527 | 528 | 529 | 530 | 531 | 532 | 533 | 534 |
535 | 536 | 537 | 538 | -------------------------------------------------------------------------------- /docs/css/style.css: -------------------------------------------------------------------------------- 1 | html, body { 2 | background: #FFFFFF; 3 | position: relative; 4 | margin: 0; 5 | padding: 0; 6 | width: 100%; 7 | height: 100%; 8 | overflow: hidden; 9 | } 10 | 11 | body { 12 | font-family: "Avenir", "Tahoma", "Lucida Sans", "Lucida Grande", Verdana, Arial, sans-serif; 13 | color: #333; 14 | line-height: 1.5; 15 | } 16 | 17 | a { 18 | color: #263F6C; 19 | } 20 | 21 | a:visited { 22 | color: #112750; 23 | } 24 | 25 | h1, h2, h3, h4, h5, h6 { 26 | margin: 35px 0 25px; 27 | color: #444444; 28 | } 29 | 30 | h1.type-name { 31 | color: #47266E; 32 | margin: 20px 0 30px; 33 | background-color: #F8F8F8; 34 | padding: 10px 12px; 35 | border: 1px solid #EBEBEB; 36 | border-radius: 2px; 37 | } 38 | 39 | h2 { 40 | border-bottom: 1px solid #E6E6E6; 41 | padding-bottom: 5px; 42 | } 43 | 44 | body { 45 | display: flex; 46 | } 47 | 48 | .sidebar, .main-content { 49 | overflow: auto; 50 | } 51 | 52 | .sidebar { 53 | width: 30em; 54 | color: #F8F4FD; 55 | background-color: #2E1052; 56 | padding: 0 0 30px; 57 | box-shadow: inset -3px 0 4px rgba(0,0,0,.35); 58 | line-height: 1.2; 59 | } 60 | 61 | .sidebar .search-box { 62 | padding: 8px 9px; 63 | } 64 | 65 | .sidebar input { 66 | display: block; 67 | box-sizing: border-box; 68 | margin: 0; 69 | padding: 5px; 70 | font: inherit; 71 | font-family: inherit; 72 | line-height: 1.2; 73 | width: 100%; 74 | border: 0; 75 | outline: 0; 76 | border-radius: 2px; 77 | box-shadow: 0px 3px 5px rgba(0,0,0,.25); 78 | transition: box-shadow .12s; 79 | } 80 | 81 | .sidebar input:focus { 82 | box-shadow: 0px 5px 6px rgba(0,0,0,.5); 83 | } 84 | 85 | .sidebar input::-webkit-input-placeholder { /* Chrome/Opera/Safari */ 86 | color: #C8C8C8; 87 | font-size: 14px; 88 | text-indent: 2px; 89 | } 90 | 91 | .sidebar input::-moz-placeholder { /* Firefox 19+ */ 92 | color: #C8C8C8; 93 | font-size: 14px; 94 | text-indent: 2px; 95 | } 96 | 97 | .sidebar input:-ms-input-placeholder { /* IE 10+ */ 98 | color: #C8C8C8; 99 | font-size: 14px; 100 | text-indent: 2px; 101 | } 102 | 103 | .sidebar input:-moz-placeholder { /* Firefox 18- */ 104 | color: #C8C8C8; 105 | font-size: 14px; 106 | text-indent: 2px; 107 | } 108 | 109 | .sidebar ul { 110 | margin: 0; 111 | padding: 0; 112 | list-style: none outside; 113 | } 114 | 115 | .sidebar li { 116 | display: block; 117 | position: relative; 118 | } 119 | 120 | .types-list li.hide { 121 | display: none; 122 | } 123 | 124 | .sidebar a { 125 | text-decoration: none; 126 | color: inherit; 127 | transition: color .14s; 128 | } 129 | .types-list a { 130 | display: block; 131 | padding: 5px 15px 5px 30px; 132 | } 133 | 134 | .types-list { 135 | display: block; 136 | } 137 | 138 | .sidebar a:focus { 139 | outline: 1px solid #D1B7F1; 140 | } 141 | 142 | .types-list a { 143 | padding: 5px 15px 5px 30px; 144 | } 145 | 146 | .sidebar .current > a, 147 | .sidebar a:hover { 148 | color: #866BA6; 149 | } 150 | 151 | .repository-links { 152 | padding: 5px 15px 5px 30px; 153 | } 154 | 155 | .types-list li ul { 156 | overflow: hidden; 157 | height: 0; 158 | max-height: 0; 159 | transition: 1s ease-in-out; 160 | } 161 | 162 | .types-list li.parent { 163 | padding-left: 30px; 164 | } 165 | 166 | .types-list li.parent::before { 167 | box-sizing: border-box; 168 | content: "▼"; 169 | display: block; 170 | width: 30px; 171 | height: 30px; 172 | position: absolute; 173 | top: 0; 174 | left: 0; 175 | text-align: center; 176 | color: white; 177 | font-size: 8px; 178 | line-height: 30px; 179 | transform: rotateZ(-90deg); 180 | cursor: pointer; 181 | transition: .2s linear; 182 | } 183 | 184 | 185 | .types-list li.parent > a { 186 | padding-left: 0; 187 | } 188 | 189 | .types-list li.parent.open::before { 190 | transform: rotateZ(0); 191 | } 192 | 193 | .types-list li.open > ul { 194 | height: auto; 195 | max-height: 1000em; 196 | } 197 | 198 | .main-content { 199 | padding: 0 30px 30px 30px; 200 | width: 100%; 201 | } 202 | 203 | .kind { 204 | font-size: 60%; 205 | color: #866BA6; 206 | } 207 | 208 | .superclass-hierarchy { 209 | margin: -15px 0 30px 0; 210 | padding: 0; 211 | list-style: none outside; 212 | font-size: 80%; 213 | } 214 | 215 | .superclass-hierarchy .superclass { 216 | display: inline-block; 217 | margin: 0 7px 0 0; 218 | padding: 0; 219 | } 220 | 221 | .superclass-hierarchy .superclass + .superclass::before { 222 | content: "<"; 223 | margin-right: 7px; 224 | } 225 | 226 | .other-types-list li { 227 | display: inline-block; 228 | } 229 | 230 | .other-types-list, 231 | .list-summary { 232 | margin: 0 0 30px 0; 233 | padding: 0; 234 | list-style: none outside; 235 | } 236 | 237 | .entry-const { 238 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 239 | } 240 | 241 | .entry-const code { 242 | white-space: pre-wrap; 243 | } 244 | 245 | .entry-summary { 246 | padding-bottom: 4px; 247 | } 248 | 249 | .superclass-hierarchy .superclass a, 250 | .other-type a, 251 | .entry-summary .signature { 252 | padding: 4px 8px; 253 | margin-bottom: 4px; 254 | display: inline-block; 255 | background-color: #f8f8f8; 256 | color: #47266E; 257 | border: 1px solid #f0f0f0; 258 | text-decoration: none; 259 | border-radius: 3px; 260 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 261 | transition: background .15s, border-color .15s; 262 | } 263 | 264 | .superclass-hierarchy .superclass a:hover, 265 | .other-type a:hover, 266 | .entry-summary .signature:hover { 267 | background: #D5CAE3; 268 | border-color: #624288; 269 | } 270 | 271 | .entry-summary .summary { 272 | padding-left: 32px; 273 | } 274 | 275 | .entry-summary .summary p { 276 | margin: 12px 0 16px; 277 | } 278 | 279 | .entry-summary a { 280 | text-decoration: none; 281 | } 282 | 283 | .entry-detail { 284 | padding: 30px 0; 285 | } 286 | 287 | .entry-detail .signature { 288 | position: relative; 289 | padding: 5px 15px; 290 | margin-bottom: 10px; 291 | display: block; 292 | border-radius: 5px; 293 | background-color: #f8f8f8; 294 | color: #47266E; 295 | border: 1px solid #f0f0f0; 296 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 297 | transition: .2s ease-in-out; 298 | } 299 | 300 | .entry-detail:target .signature { 301 | background-color: #D5CAE3; 302 | border: 1px solid #624288; 303 | } 304 | 305 | .entry-detail .signature .method-permalink { 306 | position: absolute; 307 | top: 0; 308 | left: -35px; 309 | padding: 5px 15px; 310 | text-decoration: none; 311 | font-weight: bold; 312 | color: #624288; 313 | opacity: .4; 314 | transition: opacity .2s; 315 | } 316 | 317 | .entry-detail .signature .method-permalink:hover { 318 | opacity: 1; 319 | } 320 | 321 | .entry-detail:target .signature .method-permalink { 322 | opacity: 1; 323 | } 324 | 325 | .methods-inherited { 326 | padding-right: 10%; 327 | line-height: 1.5em; 328 | } 329 | 330 | .methods-inherited h3 { 331 | margin-bottom: 4px; 332 | } 333 | 334 | .methods-inherited a { 335 | display: inline-block; 336 | text-decoration: none; 337 | color: #47266E; 338 | } 339 | 340 | .methods-inherited a:hover { 341 | text-decoration: underline; 342 | color: #6C518B; 343 | } 344 | 345 | .methods-inherited .tooltip>span { 346 | background: #D5CAE3; 347 | padding: 4px 8px; 348 | border-radius: 3px; 349 | margin: -4px -8px; 350 | } 351 | 352 | .methods-inherited .tooltip * { 353 | color: #47266E; 354 | } 355 | 356 | pre { 357 | padding: 10px 20px; 358 | margin-top: 4px; 359 | border-radius: 3px; 360 | line-height: 1.45; 361 | overflow: auto; 362 | color: #333; 363 | background: #fdfdfd; 364 | font-size: 14px; 365 | border: 1px solid #eee; 366 | } 367 | 368 | code { 369 | font-family: Menlo, Monaco, Consolas, 'Courier New', Courier, monospace; 370 | } 371 | 372 | :not(pre) > code { 373 | background-color: rgba(40,35,30,0.05); 374 | padding: 0.2em 0.4em; 375 | font-size: 85%; 376 | border-radius: 3px; 377 | } 378 | 379 | span.flag { 380 | padding: 2px 4px 1px; 381 | border-radius: 3px; 382 | margin-right: 3px; 383 | font-size: 11px; 384 | border: 1px solid transparent; 385 | } 386 | 387 | span.flag.orange { 388 | background-color: #EE8737; 389 | color: #FCEBDD; 390 | border-color: #EB7317; 391 | } 392 | 393 | span.flag.yellow { 394 | background-color: #E4B91C; 395 | color: #FCF8E8; 396 | border-color: #B69115; 397 | } 398 | 399 | span.flag.green { 400 | background-color: #469C14; 401 | color: #E2F9D3; 402 | border-color: #34700E; 403 | } 404 | 405 | span.flag.red { 406 | background-color: #BF1919; 407 | color: #F9ECEC; 408 | border-color: #822C2C; 409 | } 410 | 411 | span.flag.purple { 412 | background-color: #2E1052; 413 | color: #ECE1F9; 414 | border-color: #1F0B37; 415 | } 416 | 417 | .tooltip>span { 418 | position: absolute; 419 | opacity: 0; 420 | display: none; 421 | pointer-events: none; 422 | } 423 | 424 | .tooltip:hover>span { 425 | display: inline-block; 426 | opacity: 1; 427 | } 428 | 429 | .c { 430 | color: #969896; 431 | } 432 | 433 | .n { 434 | color: #0086b3; 435 | } 436 | 437 | .t { 438 | color: #0086b3; 439 | } 440 | 441 | .s { 442 | color: #183691; 443 | } 444 | 445 | .i { 446 | color: #7f5030; 447 | } 448 | 449 | .k { 450 | color: #a71d5d; 451 | } 452 | 453 | .o { 454 | color: #a71d5d; 455 | } 456 | 457 | .m { 458 | color: #795da3; 459 | } 460 | 461 | .hidden { 462 | display: none; 463 | } 464 | .search-results { 465 | font-size: 90%; 466 | line-height: 1.3; 467 | } 468 | 469 | .search-results mark { 470 | color: inherit; 471 | background: transparent; 472 | font-weight: bold; 473 | } 474 | .search-result { 475 | padding: 5px 8px 5px 5px; 476 | cursor: pointer; 477 | border-left: 5px solid transparent; 478 | transform: translateX(-3px); 479 | transition: all .2s, background-color 0s, border .02s; 480 | min-height: 3.2em; 481 | } 482 | .search-result.current { 483 | border-left-color: #ddd; 484 | background-color: rgba(200,200,200,0.4); 485 | transform: translateX(0); 486 | transition: all .2s, background-color .5s, border 0s; 487 | } 488 | .search-result.current:hover, 489 | .search-result.current:focus { 490 | border-left-color: #866BA6; 491 | } 492 | .search-result:not(.current):nth-child(2n) { 493 | background-color: rgba(255,255,255,.06); 494 | } 495 | .search-result__title { 496 | font-size: 105%; 497 | word-break: break-all; 498 | line-height: 1.1; 499 | padding: 3px 0; 500 | } 501 | .search-result__title strong { 502 | font-weight: normal; 503 | } 504 | .search-results .search-result__title > a { 505 | padding: 0; 506 | display: block; 507 | } 508 | .search-result__title > a > .args { 509 | color: #dddddd; 510 | font-weight: 300; 511 | transition: inherit; 512 | font-size: 88%; 513 | line-height: 1.2; 514 | letter-spacing: -.02em; 515 | } 516 | .search-result__title > a > .args * { 517 | color: inherit; 518 | } 519 | 520 | .search-result a, 521 | .search-result a:hover { 522 | color: inherit; 523 | } 524 | .search-result:not(.current):hover .search-result__title > a, 525 | .search-result:not(.current):focus .search-result__title > a, 526 | .search-result__title > a:focus { 527 | color: #866BA6; 528 | } 529 | .search-result:not(.current):hover .args, 530 | .search-result:not(.current):focus .args { 531 | color: #6a5a7d; 532 | } 533 | 534 | .search-result__type { 535 | color: #e8e8e8; 536 | font-weight: 300; 537 | } 538 | .search-result__doc { 539 | color: #bbbbbb; 540 | font-size: 90%; 541 | } 542 | .search-result__doc p { 543 | margin: 0; 544 | text-overflow: ellipsis; 545 | display: -webkit-box; 546 | -webkit-box-orient: vertical; 547 | -webkit-line-clamp: 2; 548 | overflow: hidden; 549 | line-height: 1.2em; 550 | max-height: 2.4em; 551 | } 552 | 553 | .js-modal-visible .modal-background { 554 | display: flex; 555 | } 556 | .main-content { 557 | position: relative; 558 | } 559 | .modal-background { 560 | position: absolute; 561 | display: none; 562 | height: 100%; 563 | width: 100%; 564 | background: rgba(120,120,120,.4); 565 | z-index: 100; 566 | align-items: center; 567 | justify-content: center; 568 | } 569 | .usage-modal { 570 | max-width: 90%; 571 | background: #fff; 572 | border: 2px solid #ccc; 573 | border-radius: 9px; 574 | padding: 5px 15px 20px; 575 | min-width: 50%; 576 | color: #555; 577 | position: relative; 578 | transform: scale(.5); 579 | transition: transform 200ms; 580 | } 581 | .js-modal-visible .usage-modal { 582 | transform: scale(1); 583 | } 584 | .usage-modal > .close-button { 585 | position: absolute; 586 | right: 15px; 587 | top: 8px; 588 | color: #aaa; 589 | font-size: 27px; 590 | cursor: pointer; 591 | } 592 | .usage-modal > .close-button:hover { 593 | text-shadow: 2px 2px 2px #ccc; 594 | color: #999; 595 | } 596 | .modal-title { 597 | margin: 0; 598 | text-align: center; 599 | font-weight: normal; 600 | color: #666; 601 | border-bottom: 2px solid #ddd; 602 | padding: 10px; 603 | } 604 | .usage-list { 605 | padding: 0; 606 | margin: 13px; 607 | } 608 | .usage-list > li { 609 | padding: 5px 2px; 610 | overflow: auto; 611 | padding-left: 100px; 612 | min-width: 12em; 613 | } 614 | .usage-modal kbd { 615 | background: #eee; 616 | border: 1px solid #ccc; 617 | border-bottom-width: 2px; 618 | border-radius: 3px; 619 | padding: 3px 8px; 620 | font-family: monospace; 621 | margin-right: 2px; 622 | display: inline-block; 623 | } 624 | .usage-key { 625 | float: left; 626 | clear: left; 627 | margin-left: -100px; 628 | margin-right: 12px; 629 | } 630 | .doc-inherited { 631 | font-weight: bold; 632 | } 633 | 634 | .anchor { 635 | float: left; 636 | padding-right: 4px; 637 | margin-left: -20px; 638 | } 639 | 640 | .main-content .anchor .octicon-link { 641 | width: 16px; 642 | height: 16px; 643 | } 644 | 645 | .main-content .anchor:focus { 646 | outline: none 647 | } 648 | 649 | .main-content h1:hover .anchor, 650 | .main-content h2:hover .anchor, 651 | .main-content h3:hover .anchor, 652 | .main-content h4:hover .anchor, 653 | .main-content h5:hover .anchor, 654 | .main-content h6:hover .anchor { 655 | text-decoration: none 656 | } 657 | 658 | .main-content h1 .octicon-link, 659 | .main-content h2 .octicon-link, 660 | .main-content h3 .octicon-link, 661 | .main-content h4 .octicon-link, 662 | .main-content h5 .octicon-link, 663 | .main-content h6 .octicon-link { 664 | visibility: hidden 665 | } 666 | 667 | .main-content h1:hover .anchor .octicon-link, 668 | .main-content h2:hover .anchor .octicon-link, 669 | .main-content h3:hover .anchor .octicon-link, 670 | .main-content h4:hover .anchor .octicon-link, 671 | .main-content h5:hover .anchor .octicon-link, 672 | .main-content h6:hover .anchor .octicon-link { 673 | visibility: visible 674 | } 675 | -------------------------------------------------------------------------------- /docs/empty: -------------------------------------------------------------------------------- 1 | empty 2 | -------------------------------------------------------------------------------- /explorer/explorer.js: -------------------------------------------------------------------------------- 1 | var nodes = [ 2 | { 3 | port: '3000', 4 | peers: [] 5 | } 6 | ]; 7 | 8 | var transactions = {}; 9 | 10 | var blocks = { 11 | // Genesis block 12 | 0: [ 13 | { 14 | hash: '00f82f15d9fee292860b2a37183d769efd3b617451c04017f700238fd472e8bb', 15 | previous_hash: 'Olivia' 16 | } 17 | ] 18 | }; 19 | 20 | var stats = new Vue({ 21 | el: '#stats-list', 22 | data: {transactions: transactions} 23 | }); 24 | 25 | var chain = new Vue({ 26 | el: '#blocks', 27 | data: {blocks: blocks} 28 | }); 29 | 30 | var graph = Viva.Graph.graph(); 31 | var graphics = Viva.Graph.View.svgGraphics(); 32 | var nodeSize = 24; 33 | 34 | 35 | var loopNodes = function () { 36 | for (i=0; i < nodes.length; ++i) { 37 | (function () { 38 | var currentIndex = i; 39 | if (!nodes[i].socket) { 40 | connectToNode(currentIndex); 41 | } 42 | }()); 43 | } 44 | }; 45 | 46 | var connectToNode = function (i) { 47 | socket = new WebSocket('ws://localhost:'+nodes[i].port+'/events'); 48 | socket.addEventListener('message', function (event) { 49 | console.log('===== now event ======'); 50 | var eventData = JSON.parse(event.data); 51 | processEvent(eventData, i); 52 | }); 53 | socket.addEventListener('close', function (event) { 54 | console.log('===== now closing ======'); 55 | url = new URL(event.target.url); 56 | removeNode(url.port); 57 | }); 58 | nodes[i].socket = socket; 59 | }; 60 | 61 | var processEvent = function (event, i) { 62 | console.log('> process event'); 63 | if (event.event === 'onTxn') { 64 | console.log('> + new txn'); 65 | transactions[event.hash] = event.amount; 66 | stats.transactions = transactions; 67 | stats.$forceUpdate(); 68 | } else if(event.event === 'onConnection') { 69 | console.log('> + new peer'); 70 | checkConnections([event.peer_port], event.node_port); 71 | } else if(event.event === 'onNewBlock') { 72 | console.log('> + new block'); 73 | if (!blocks[event.height]) { 74 | blocks[event.height] = []; 75 | } 76 | blocks[event.height].push({hash: event.hash, previous_hash: event.previous_hash}); 77 | chain.blocks = blocks; 78 | chain.$forceUpdate(); 79 | } else { 80 | // this we only do because we don't know the master node ident 81 | console.log('> + new node'); 82 | nodes[i].height = event.height; 83 | nodes[i].hash = event.hash; 84 | nodes[i].peers = event.peers; 85 | nodes[i].port = event.port; 86 | nodes[i].txn = event.txn; 87 | nodes[i].miner = event.miner; 88 | 89 | if (nodes[i].port !== '3000' && nodes[i].port !== 3000) { 90 | graph.addNode(nodes[i].port, { 91 | height: nodes[i].height, 92 | hash: nodes[i].hash || '-', 93 | txn: nodes[i].txn || 0, 94 | miner: nodes[i].miner 95 | }); 96 | } 97 | 98 | if (event.event === 'onInitialUpdate') { 99 | checkConnections(event.peers, nodes[i].port); 100 | } 101 | } 102 | }; 103 | 104 | var checkConnections = function (peers, parentApiPort) { 105 | for (i=0; i < peers.length; ++i) { 106 | (function () { 107 | var peer = nodes.find(node => node.port === peers[i]); 108 | if (!peer) { 109 | nodes.push({port: peers[i]}); 110 | stats.nodes = nodes; 111 | connectToNode(nodes.length - 1); 112 | } 113 | // also create link 114 | if (parentApiPort !== '3000' && parentApiPort !== 3000) { 115 | if (graph.getLink(peers[i], parentApiPort) === null && graph.getLink(parentApiPort, peers[i]) === null) { 116 | graph.addLink(peers[i], parentApiPort); 117 | } 118 | } 119 | }()); 120 | } 121 | }; 122 | 123 | var removeNode = function (port) { 124 | graph.removeNode(port); 125 | nodes = nodes.filter(node => parseInt(port) !== node.port); 126 | stats.nodes = nodes; 127 | }; 128 | 129 | loopNodes(); 130 | 131 | graphics.node(function(node) { 132 | // This time it's a group of elements: http://www.w3.org/TR/SVG/struct.html#Groups 133 | var ui = Viva.Graph.svg('g'); 134 | // Create SVG text element with user id as content 135 | var stringToColour = function(str) { 136 | var hash = 0; 137 | for (var i = 0; i < str.length; i++) { 138 | hash = str.charCodeAt(i) + ((hash << 5) - hash); 139 | } 140 | var colour = '#'; 141 | for (var i = 0; i < 3; i++) { 142 | var value = (hash >> (i * 8)) & 0xFF; 143 | colour += ('00' + value.toString(16)).substr(-2); 144 | } 145 | return colour; 146 | }; 147 | var height = node.data ? node.data.height : '0'; 148 | var hash = node.data ? node.data.hash : '-'; 149 | 150 | var svgText = Viva.Graph.svg('text').attr('y', '-4px').text(node.id+' / '+height+' / '+hash.substr(-6)); 151 | var svgCircle = Viva.Graph.svg('circle') 152 | .attr('r', '7px') 153 | .attr('cy', '12') 154 | .attr('cx', '12') 155 | .attr('fill', stringToColour(hash)); 156 | 157 | if ( node.data && node.data.miner === true ) { 158 | svgCircle = svgCircle.attr('stroke', '#ff6859').attr('stroke-width', 3); 159 | } 160 | 161 | ui.append(svgText); 162 | ui.append(svgCircle); 163 | 164 | return ui; 165 | }).placeNode(function(nodeUI, pos) { 166 | // 'g' element doesn't have convenient (x,y) attributes, instead 167 | // we have to deal with transforms: http://www.w3.org/TR/SVG/coords.html#SVGGlobalTransformAttribute 168 | nodeUI.attr('transform', 169 | 'translate(' + 170 | (pos.x - nodeSize/2) + ',' + (pos.y - nodeSize/2) + 171 | ')'); 172 | }); 173 | 174 | graphics.link(function(link){ 175 | return Viva.Graph.svg('line') 176 | .attr('stroke', 'rgba(0,0,0,0.1)') 177 | .attr('stroke-width', 1); 178 | }); 179 | var layout = Viva.Graph.Layout.forceDirected(graph, { 180 | springLength: 300 181 | }); 182 | var renderer = Viva.Graph.View.renderer(graph, { 183 | layout: layout, 184 | graphics : graphics, 185 | container: document.getElementById('graph') 186 | }); 187 | 188 | renderer.run(); 189 | 190 | // ============================================================================ 191 | -------------------------------------------------------------------------------- /explorer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Cocol Explorer 6 | 7 | 8 | 9 | 10 | 11 | 73 | 74 | 75 |
76 |
77 |
Transactions
78 | 83 |
84 |
85 |
86 | 87 |
88 |
89 |
97 |
98 | 99 | 100 | 101 | 102 | -------------------------------------------------------------------------------- /img/demo.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocol-project/cocol/f136699fb190cb24205b26a05e1e9da0bc507fdc/img/demo.gif -------------------------------------------------------------------------------- /img/demo2.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocol-project/cocol/f136699fb190cb24205b26a05e1e9da0bc507fdc/img/demo2.gif -------------------------------------------------------------------------------- /img/header.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/cocol-project/cocol/f136699fb190cb24205b26a05e1e9da0bc507fdc/img/header.png -------------------------------------------------------------------------------- /script/live.cr: -------------------------------------------------------------------------------- 1 | require "http/client" 2 | require "json" 3 | require "digest/sha1" 4 | 5 | puts "GO" 6 | (1..1000).each do |_i| 7 | timestamp = Time.utc.to_unix 8 | from = Random::Secure.urlsafe_base64 9 | to = Random::Secure.urlsafe_base64 10 | hash = Digest::SHA1.hexdigest("#{timestamp}#{from}#{to}") 11 | nodes = %w(3002 3003 3004 3005 3006 3007 4001 4002 4003) 12 | url = "http://localhost:#{nodes.sample(1)[0]}/transactions" 13 | 14 | begin 15 | HTTP::Client.post( 16 | url, 17 | headers: HTTP::Headers{ 18 | "Content-Type" => "application/json", 19 | "X-Node-Id" => "E2E-Node", 20 | }, 21 | body: { 22 | from: from, 23 | to: to, 24 | amount: Random.rand(100), 25 | timestamp: timestamp, 26 | hash: hash, 27 | }.to_json 28 | ) 29 | rescue 30 | puts "TIMEOUT: #{url}" 31 | end 32 | 33 | puts "txn-#{timestamp}-#{hash}" 34 | sleep Random.new.rand(0.1..3.2) 35 | end 36 | -------------------------------------------------------------------------------- /script/start.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | (( startPort=3001 )) 4 | (( nodes=$(($1 - 1)) )) 5 | (( miner=$2 )) 6 | 7 | #./bin/cocol -p 3000 -m --max-connections 300 &> /dev/null & 8 | ./bin/cocol -p 3000 -m --max-connections 300 &> /tmp/cocol.log 2>&1 & 9 | sleep 1 10 | 11 | for ((i=1;i<=nodes;i++)); 12 | do 13 | # ../cocol -p $(($startPort + $i)) -a $(($startApiPort + $i)) > /dev/null 2>&1 & 14 | #./bin/cocol -p $((startPort + i)) --max-connections 5 1>> /tmp/cocol.log & 15 | ./bin/cocol -p $((startPort + i)) --max-connections 5 > /tmp/cocol.log 2>&1 & 16 | echo $i 17 | sleep 0.2 18 | done 19 | 20 | for ((i=1;i<=miner;i++)); 21 | do 22 | # ../cocol -p $(($startPort + $i)) -a $(($startApiPort + $i)) > /dev/null 2>&1 & 23 | #./bin/cocol -p $((4000 + i)) --max-connections 5 --miner 1>> /tmp/cocol.log & 24 | ./bin/cocol -p $((4000 + i)) --max-connections 5 --miner > /tmp/cocol.log 2>&1 & 25 | echo $i 26 | sleep 0.2 27 | done 28 | 29 | trap ctrl_c INT 30 | function ctrl_c() { 31 | killall cocol 32 | } 33 | 34 | sleep 1d 35 | -------------------------------------------------------------------------------- /shard.lock: -------------------------------------------------------------------------------- 1 | version: 1.0 2 | shards: 3 | ccl-pos: 4 | github: cocol-project/ccl-pos 5 | commit: 23812ffb3550e76912e8e981606f67b6570779ab 6 | 7 | ccl-pow: 8 | github: cocol-project/ccl-pow 9 | commit: cee2b7994be4f780b8fe505d50e0587f39173596 10 | 11 | clim: 12 | github: at-grandpa/clim 13 | version: 0.10.0 14 | 15 | exception_page: 16 | github: crystal-loot/exception_page 17 | version: 0.1.4 18 | 19 | kemal: 20 | github: kemalcr/kemal 21 | version: 0.26.1 22 | 23 | kilt: 24 | github: jeromegn/kilt 25 | version: 0.4.0 26 | 27 | minitest: 28 | github: ysbaddaden/minitest.cr 29 | version: 0.5.1 30 | 31 | popcorn: 32 | github: icyleaf/popcorn 33 | version: 0.2.2 34 | 35 | probfin: 36 | github: cocol-project/probfin 37 | version: 0.2.1 38 | 39 | radix: 40 | github: luislavena/radix 41 | version: 0.3.9 42 | 43 | secp256k1: 44 | github: q9f/secp256k1.cr 45 | version: 0.3.2 46 | 47 | sha3: 48 | github: OscarBarrett/crystal-sha3 49 | version: 0.3.2 50 | 51 | totem: 52 | github: icyleaf/totem 53 | version: 0.6.1 54 | 55 | -------------------------------------------------------------------------------- /shard.yml: -------------------------------------------------------------------------------- 1 | name: cocol 2 | version: v0.4.1 3 | license: MPL-2.0 4 | crystal: 0.34.0 5 | authors: 6 | - Cristian Șerb 7 | 8 | targets: 9 | cocol: 10 | main: src/cocol.cr 11 | 12 | dependencies: 13 | clim: 14 | github: at-grandpa/clim 15 | version: 0.10.0 16 | kemal: 17 | github: kemalcr/kemal 18 | version: ~> 0.26.0 19 | ccl-pow: 20 | github: cocol-project/ccl-pow 21 | branch: master 22 | probfin: 23 | github: cocol-project/probfin 24 | version: 0.2.1 25 | ccl-pos: 26 | github: cocol-project/ccl-pos 27 | branch: master 28 | secp256k1: 29 | github: q9f/secp256k1.cr 30 | version: "~> 0.2" 31 | totem: 32 | github: icyleaf/totem 33 | 34 | development_dependencies: 35 | minitest: 36 | github: ysbaddaden/minitest.cr 37 | version: ~> 0.5.0 38 | -------------------------------------------------------------------------------- /spec/block_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe "Ledger::Block" do 4 | let(:height) { 0_u64 } 5 | let(:previous_hash) { "Olivia" } 6 | 7 | describe "Pow" do 8 | describe "Generic block" do 9 | let(:block) do 10 | Ledger::Block::Pow.new( 11 | height: height, 12 | transactions: Array(Ledger::Action::Transaction).new, 13 | previous_hash: previous_hash, 14 | nbits: Ledger::Block::Pow::MIN_NBITS, 15 | coinbase: Ledger::Block::Coinbase.new("3000") 16 | ) 17 | end 18 | 19 | it "should have autogenerated values" do 20 | block.hash.wont_be_nil 21 | block.nonce.wont_be_nil 22 | block.timestamp.wont_be_nil 23 | block.height.must_equal(height) 24 | block.previous_hash.must_equal(previous_hash) 25 | end 26 | end 27 | end 28 | 29 | describe "Pos" do 30 | describe "Generic block" do 31 | let(:nodes) { %w(4001 4002) } 32 | let(:stakes) do 33 | [ 34 | Ledger::Action::Stake.new( 35 | staker: nodes[0], 36 | amount: 50_u64 37 | ), 38 | Ledger::Action::Stake.new( 39 | staker: nodes[1], 40 | amount: 50_u64 41 | ), 42 | ] 43 | end 44 | 45 | let(:block) do 46 | Ledger::Block::Pos.new( 47 | height: height, 48 | previous_hash: previous_hash, 49 | stakes: stakes, 50 | transactions: Array(Ledger::Action::Transaction).new, 51 | coinbase: Ledger::Block::Coinbase.new("3000") 52 | ) 53 | end 54 | 55 | it "contains all genesis stakes" do 56 | block.hash.wont_be_nil 57 | block.stakes.must_equal(stakes) 58 | block.height.must_equal(height) 59 | block.previous_hash.must_equal(previous_hash) 60 | block.timestamp.wont_be_nil 61 | end 62 | end 63 | end 64 | end 65 | -------------------------------------------------------------------------------- /spec/ledger_repo_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe "Ledger::Repo" do 4 | let(:coinbase) { Ledger::Block::Coinbase.new("3000") } 5 | let(:block_a) do 6 | Ledger::Block::Pos.new( 7 | height: 0_u64, 8 | previous_hash: "Olivia", 9 | transactions: Array(Ledger::Action::Transaction).new, 10 | stakes: Array(Ledger::Action::Stake).new, 11 | coinbase: coinbase 12 | ) 13 | end 14 | 15 | let(:block_b) do 16 | Ledger::Block::Pos.new( 17 | height: 1_u64, 18 | previous_hash: block_a.hash, 19 | transactions: Array(Ledger::Action::Transaction).new, 20 | stakes: Array(Ledger::Action::Stake).new, 21 | coinbase: coinbase 22 | ) 23 | end 24 | 25 | describe "saving blocks" do 26 | before do 27 | clear_ledger_repo 28 | end 29 | 30 | it "saves a new block" do 31 | saved = Ledger::Repo.save(block: block_a) 32 | 33 | # saved.should be_true 34 | saved.must_equal(true) 35 | Ledger::Repo.blocks[block_a.hash]?.must_equal(block_a) 36 | end 37 | 38 | it "rejects saving if it's a known block" do 39 | Ledger::Repo.save(block: block_a) 40 | saved = Ledger::Repo.save(block: block_a) 41 | 42 | saved.must_equal(false) 43 | end 44 | end 45 | 46 | describe "finality" do 47 | before do 48 | clear_ledger_repo 49 | Ledger::Repo.save(block: block_a) 50 | end 51 | 52 | it "finalizes block" do 53 | Ledger::Repo.save(block: block_b) 54 | Ledger::Repo.finalize block: block_a.hash 55 | Ledger::Repo.finalize block: block_b.hash 56 | 57 | Ledger::Repo.ledger_last.must_equal(block_b) 58 | 59 | Ledger::Repo.block_at_height[0_u64].must_equal(block_a.hash) 60 | Ledger::Repo.block_at_height[1_u64].must_equal(block_b.hash) 61 | end 62 | end 63 | end 64 | -------------------------------------------------------------------------------- /spec/ledger_spec.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | require "./spec_helper" 6 | 7 | describe Ledger::Pow do 8 | let(:txn) do 9 | Ledger::Action::Transaction.new( 10 | from: "me", 11 | to: "you", 12 | amount: 1_u64 13 | ) 14 | end 15 | let(:transactions) { [txn] of Ledger::Action::Transaction } 16 | let(:block) { Ledger::Pow.mine(transactions) } 17 | 18 | let(:invalid_block) do 19 | Ledger::Block::Pow.new( 20 | hash: "somethinginvalid", 21 | timestamp: Time.utc.to_unix, 22 | height: 12_u64, 23 | nonce: 123_u64, 24 | nbits: Ledger::Block::Pow::MIN_NBITS, 25 | previous_hash: "somethinginvalidagain", 26 | transactions: Array(Ledger::Action::Transaction).new, 27 | coinbase: Ledger::Block::Coinbase.new("Me") 28 | ) 29 | end 30 | 31 | before do 32 | Node.settings.miner_address = "bogus" 33 | end 34 | 35 | describe "Genesis" do 36 | it "created the genesis block" do 37 | Ledger::Pow.genesis 38 | Ledger::Repo.blocks[ 39 | Ledger::Repo.block_at_height[0_u64], 40 | ].previous_hash.must_equal("a78a4203908a94d91b1a8f6aa65f4d1176d68ba67ced10a32cfb661f40c58b88") 41 | end 42 | end 43 | 44 | describe "Mine" do 45 | it "mines a block" do 46 | block.transactions.must_equal(transactions) 47 | block.nonce.wont_be_nil 48 | block.nbits.wont_be_nil 49 | block.timestamp.wont_be_nil 50 | end 51 | end 52 | 53 | describe "Validation" do 54 | it "validates a block" do 55 | Ledger::Pow.valid?(block: block).must_equal(true) 56 | end 57 | 58 | it "rejects invalid block" do 59 | Ledger::Pow.valid?(block: invalid_block).must_equal(false) 60 | end 61 | end 62 | end 63 | 64 | describe Ledger::Pos do 65 | let(:block) do 66 | Ledger::Block::Pos.new( 67 | height: 12_u64, 68 | transactions: Array(Ledger::Action::Transaction).new, 69 | stakes: Array(Ledger::Action::Stake).new, 70 | previous_hash: "bogus", 71 | coinbase: Ledger::Block::Coinbase.new("bogus") 72 | ) 73 | end 74 | 75 | let(:invalid_block) do 76 | Ledger::Block::Pos.new( 77 | hash: "bogus", 78 | timestamp: Time.utc.to_unix, 79 | height: 12_u64, 80 | transactions: Array(Ledger::Action::Transaction).new, 81 | stakes: Array(Ledger::Action::Stake).new, 82 | previous_hash: "bogus", 83 | coinbase: Ledger::Block::Coinbase.new("bogus") 84 | ) 85 | end 86 | 87 | it "validates block hash" do 88 | Ledger::Pos.valid?(block: block).must_equal(true) 89 | end 90 | 91 | it "rejects invalid block hash" do 92 | Ledger::Pos.valid?(block: invalid_block).must_equal(false) 93 | end 94 | end 95 | -------------------------------------------------------------------------------- /spec/ledger_util_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe "Ledger::Util" do 4 | describe "Balance" do 5 | let(:nobody) { "nobody" } 6 | let(:myself) { "myself" } 7 | let(:coinbase) { Ledger::Block::Coinbase.new("3000") } 8 | 9 | let(:b0) do 10 | Ledger::Block::Pos.new( 11 | height: 0_u64, 12 | previous_hash: Ledger::GENESIS_CREATOR, 13 | stakes: Array(Ledger::Action::Stake).new, 14 | transactions: [ 15 | Ledger::Action::Transaction.new( 16 | from: nobody, 17 | to: myself, 18 | amount: 100_u64 19 | ), 20 | ], 21 | coinbase: coinbase 22 | ) 23 | end 24 | let(:b1) do 25 | Ledger::Block::Pos.new( 26 | height: 0_u64, 27 | previous_hash: b0.hash, 28 | stakes: Array(Ledger::Action::Stake).new, 29 | transactions: [ 30 | Ledger::Action::Transaction.new( 31 | from: myself, 32 | to: nobody, 33 | amount: 32_u64 34 | ), 35 | ], 36 | coinbase: coinbase 37 | ) 38 | end 39 | let(:b2) do 40 | Ledger::Block::Pos.new( 41 | height: 0_u64, 42 | previous_hash: b1.hash, 43 | stakes: Array(Ledger::Action::Stake).new, 44 | transactions: [ 45 | Ledger::Action::Transaction.new( 46 | from: myself, 47 | to: nobody, 48 | amount: 67_u64 49 | ), 50 | ], 51 | coinbase: coinbase 52 | ) 53 | end 54 | 55 | let(:txn_over_limit) do 56 | Ledger::Action::Transaction.new( 57 | from: myself, 58 | to: nobody, 59 | amount: 12412512532_u64 60 | ) 61 | end 62 | let(:txn_in_limit) do 63 | Ledger::Action::Transaction.new( 64 | from: myself, 65 | to: nobody, 66 | amount: 1_u64 67 | ) 68 | end 69 | 70 | before do 71 | Ledger::Repo.blocks[b0.hash] = b0 72 | Ledger::Repo.blocks[b1.hash] = b1 73 | Ledger::Repo.blocks[b2.hash] = b2 74 | end 75 | 76 | it "should calculate the account balance" do 77 | balance = Ledger::Util.balance for: myself, until: b2.hash 78 | 79 | balance.must_equal(1_u64) 80 | end 81 | 82 | it "should calculate 0 for unknown address" do 83 | balance = Ledger::Util.balance for: "unknown", until: b2.hash 84 | 85 | balance.must_equal(0_u64) 86 | end 87 | 88 | # it "should return false for over-limit transaction" do 89 | # is_valid = Ledger::Util.valid? transaction: txn_over_limit, at: b2.hash 90 | 91 | # is_valid.must_equal(false) 92 | # end 93 | 94 | # it "should return true for in-limit transaction" do 95 | # is_valid = Ledger::Util.valid? transaction: txn_in_limit, at: b2.hash 96 | 97 | # is_valid.must_equal(true) 98 | # end 99 | end 100 | end 101 | -------------------------------------------------------------------------------- /spec/mempool_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe "Ledger::Mempool" do 4 | let(:txn) do 5 | Ledger::Action::Transaction.new("VW", "Merkel", 3_000_u64) 6 | end 7 | 8 | describe "Managing pending transactions" do 9 | before do 10 | Ledger::Mempool.pending.clear 11 | end 12 | 13 | it "should return false when trying to remove unknown txn" do 14 | Ledger::Mempool.remove(txn.hash).must_equal(false) 15 | end 16 | 17 | it "should return true when trying to add unknown txn" do 18 | Ledger::Mempool.add(txn).must_equal(true) 19 | end 20 | 21 | it "should return false when trying to add known txn" do 22 | Ledger::Mempool.add(txn) 23 | Ledger::Mempool.add(txn).must_equal(false) 24 | end 25 | 26 | it "should return true when trying to remove known txn" do 27 | Ledger::Mempool.add(txn) 28 | Ledger::Mempool.remove(txn.hash).must_equal(true) 29 | end 30 | end 31 | end 32 | -------------------------------------------------------------------------------- /spec/spec_helper.cr: -------------------------------------------------------------------------------- 1 | require "../src/deps" 2 | require "minitest/autorun" 3 | 4 | def clear_ledger_repo 5 | Ledger::Repo.blocks.clear 6 | Ledger::Repo.block_at_height.clear 7 | Ledger::Repo.ledger.clear 8 | end 9 | -------------------------------------------------------------------------------- /spec/transaction_spec.cr: -------------------------------------------------------------------------------- 1 | require "./spec_helper" 2 | 3 | describe "Ledger::Action::Transaction" do 4 | let(:hash) { "c9b0f2181f2783594c8c30f79ef8ff6231494ab50013ed0bb0fc2cd75408f791" } 5 | let(:from) { "Me" } 6 | let(:to) { "You" } 7 | let(:amount) { 100_u64 } 8 | let(:timestamp) { 1449970561 } 9 | let(:signature) { Ledger::Action::Signature.new( 10 | v: "034ce3a20d210dc22b79a0944a9b9ef29f3aa50730cc27be7ec4adc601cbcb7372", 11 | r: "70189975654209893549254487705918808402318817289942319050850973295297592779325", 12 | s: "56643761485509572745767837990785422271240332652515523415468197818355438975767" 13 | ) } 14 | 15 | describe "JSON Serializable" do 16 | let(:txn_json) do 17 | Ledger::Action::Transaction.from_json({ 18 | from: from, 19 | to: to, 20 | amount: amount, 21 | timestamp: timestamp, 22 | hash: hash, 23 | sig: signature, 24 | }.to_json) 25 | end 26 | 27 | it "should initialize successfully" do 28 | txn_json.from.must_equal(from) 29 | txn_json.to.must_equal(to) 30 | txn_json.amount.must_equal(amount) 31 | txn_json.timestamp.must_equal(timestamp) 32 | txn_json.hash.must_equal(hash) 33 | end 34 | end 35 | 36 | describe "Construct" do 37 | let(:txn) do 38 | Ledger::Action::Transaction.new( 39 | from: from, 40 | to: to, 41 | amount: amount 42 | ) 43 | end 44 | 45 | it "should generate timestamp" do 46 | txn.timestamp.wont_be_nil 47 | end 48 | 49 | it "should generate hash" do 50 | txn.hash.wont_be_nil 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/cocol.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | require "./deps" 6 | 7 | module Cocol 8 | module App 9 | module Api 10 | extend self 11 | 12 | def run(port : UInt32) 13 | before_all do |env| 14 | env.response.content_type = "application/json" 15 | env.response.headers["Allow"] = "HEAD,GET,PUT,POST,DELETE,OPTIONS" 16 | env.response.headers["Access-Control-Allow-Origin"] = "*" 17 | env.response.headers["Access-Control-Allow-Headers"] = 18 | "X-Requested-With, X-HTTP-Method-Override, Content-Type, Cache-Control, Accept" 19 | end 20 | 21 | logging false 22 | Kemal.run(port: port.to_i32, args: nil) 23 | end 24 | end 25 | 26 | module Miner 27 | extend self 28 | 29 | # TODO: refactor me https://gph.is/1cO1G6K 30 | def run 31 | chan = Channel(Nil).new 32 | pending_transactions = Ledger::Mempool.pending.values 33 | 34 | spawn do 35 | Ledger::Pow.mine(pending_transactions) 36 | chan.send(nil) 37 | end 38 | 39 | chan.receive 40 | Ledger::Mempool.remove(pending_transactions) 41 | Miner.run 42 | end 43 | end 44 | end 45 | 46 | class Command < Clim 47 | main do 48 | desc "Cocol Client - minimal blockchain testbed" 49 | usage "cocol [options] [arguments] ..." 50 | version "Version 0.3.0" 51 | option "-h HOST", "--host=HOST", type: String, desc: "Change host if running a public node (default localhost)" 52 | option "-p NUMBER", "--port=NUMBER", type: UInt32, desc: "Change port (default 3001)" 53 | option "-n NAME", "--name=NAME", type: String, desc: "Provide a unique name for your node" 54 | option "-x NUMBER", "--max-connections=NUMBER", type: UInt16, desc: "Change max-connections (default 5)" 55 | option "-M", "--miner", type: Bool, desc: "Start as miner (default false)" 56 | option "-a", "--address=ADDRESS", type: String, desc: "Required for the block reward if started as miner" 57 | option "-m", "--master", type: Bool, desc: "Start as master (default false)" 58 | run do |opts, _args| 59 | if opts.miner && !opts.address 60 | puts "You need to provide an address for the block reward `--address`" 61 | return 62 | end 63 | Node.settings.host = opts.host.as(String) if opts.host 64 | Node.settings.port = opts.port.as(UInt32) if opts.port 65 | Node.settings.name = opts.name.as(String) if opts.name 66 | Node.settings.max_connections = opts.max_connections.as(UInt16) if opts.max_connections 67 | Node.settings.miner = opts.miner if opts.miner 68 | Node.settings.miner_address = opts.address.as(String) if opts.address 69 | Node.settings.master = opts.master if opts.master 70 | 71 | Cocol.logger.info { Node.settings.inspect } 72 | 73 | spawn { Cocol::App::Api.run port: Node.settings.port } 74 | spawn { Cocol::App::Miner.run } if Node.settings.miner 75 | Ledger::Pow.genesis 76 | Messenger.establish_network_position if !Node.settings.master 77 | spawn { Ledger::Sync.call } if !Node.settings.master 78 | 79 | sleep 80 | end 81 | end 82 | end 83 | end 84 | 85 | Cocol::Command.start(ARGV) 86 | -------------------------------------------------------------------------------- /src/cocol/logger.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | require "log" 6 | 7 | module Cocol 8 | extend self 9 | 10 | def logger 11 | Log.builder.bind "*", :debug, Log::IOBackend.new 12 | 13 | @@logger ||= Log.for( 14 | "#{Node.settings.ident}@#{Node.settings.host}:#{Node.settings.port}" 15 | ) 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/cocol/node/event.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | require "./event/**" 6 | require "./messenger" 7 | require "./ledger" 8 | 9 | module Event 10 | extend self 11 | 12 | alias EventType = String 13 | alias TransactionEvent = NamedTuple 14 | alias PeerConnectionEvent = NamedTuple 15 | alias NewBlockEvent = NamedTuple 16 | 17 | def add_socket(socket : HTTP::WebSocket) 18 | Event::Repo.websockets << socket 19 | end 20 | 21 | def del_socket(socket : HTTP::WebSocket) 22 | Event::Repo.websockets.delete(socket) 23 | end 24 | 25 | def broadcast(payload : String) 26 | Event::Repo.websockets.each do |socket| 27 | socket.send payload 28 | end 29 | end 30 | 31 | def update(event : String) : NamedTuple 32 | update = { 33 | event: event, 34 | peers: Messenger::Repo.peers.map { |peer| {port: peer.port, host: peer.host, ident: peer.ident} }, 35 | port: Node.settings.port, 36 | miner: Node.settings.miner, 37 | host: Node.settings.host, 38 | ident: Node.settings.ident, 39 | name: Node.settings.name, 40 | master: Node.settings.master, 41 | } 42 | 43 | latest_block = Ledger::Repo.blocks[Ledger::Util.probfin_tip_hash] 44 | update = update.merge({ 45 | height: latest_block.height, 46 | hash: latest_block.hash, 47 | }) 48 | 49 | update 50 | end 51 | 52 | def transaction( 53 | event : EventType, 54 | transaction : Ledger::Action::Transaction 55 | ) : TransactionEvent 56 | { 57 | event: event, 58 | hash: transaction.hash, 59 | amount: transaction.amount, 60 | } 61 | end 62 | 63 | def peer(peer : Messenger::Struct::Peer) : PeerConnectionEvent 64 | { 65 | event: "onConnection", 66 | node: {ident: Node.settings.ident, master: Node.settings.master}, 67 | peer: {ident: peer.ident, host: peer.host, port: peer.port}, 68 | } 69 | end 70 | 71 | def block( 72 | block : Ledger::Block::Abstract 73 | ) : NewBlockEvent 74 | { 75 | event: "onNewBlock", 76 | hash: block.hash, 77 | previous_hash: block.previous_hash, 78 | height: block.height, 79 | } 80 | end 81 | end 82 | -------------------------------------------------------------------------------- /src/cocol/node/event/api.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | ws "/events" do |socket| 6 | Event.add_socket socket 7 | spawn Event.broadcast(Event.update("onInitialUpdate").to_json) 8 | 9 | socket.on_close do 10 | Event.del_socket socket 11 | end 12 | end 13 | -------------------------------------------------------------------------------- /src/cocol/node/event/repo.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | module Event 6 | module Repo 7 | extend self 8 | 9 | def websockets 10 | @@websockets ||= Array(HTTP::WebSocket).new 11 | end 12 | end 13 | end 14 | -------------------------------------------------------------------------------- /src/cocol/node/ledger.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | require "./ledger/util" 6 | require "./ledger/block" 7 | require "./ledger/action" 8 | require "./ledger/mempool" 9 | require "./ledger/repo" 10 | require "./ledger/api" 11 | require "./event" 12 | require "./messenger" 13 | 14 | module Ledger 15 | GENESIS_CREATOR = "Olivia" 16 | 17 | module Inventory 18 | extend self 19 | include Ledger::Block 20 | 21 | def call(best_hash : BlockHash) : Array(BlockHash) 22 | peer_best_block = Ledger::Repo.blocks[best_hash]? 23 | return Array(BlockHash).new if peer_best_block.nil? 24 | 25 | Ledger::Repo.blocks.map do |k, v| 26 | k if v.height > peer_best_block.height 27 | end.compact 28 | end 29 | end 30 | 31 | module Sync 32 | extend self 33 | include Ledger::Block 34 | 35 | def call 36 | Cocol.logger.info { "--- START SYNC" } 37 | loop do 38 | sleep 0.5 39 | next if Messenger::Repo.peers.size == 0 40 | 41 | peer = Messenger::Repo.peers.first 42 | best_hash = Ledger::Util.probfin_tip_hash 43 | peer_inventory = Messenger::Action::Inventory.call( 44 | peer: peer, 45 | best_hash: best_hash 46 | ) 47 | break if peer_inventory.size == 0 48 | my_inventory = Ledger::Inventory.call(best_hash) 49 | inventory = peer_inventory - my_inventory 50 | 51 | inventory.each do |bh| 52 | next if Ledger::Repo.blocks[bh]? 53 | block = Messenger::Action::GetBlock.call(block_hash: bh, peer: peer) 54 | next if !Ledger::Pow.valid?(block: block) 55 | 56 | Ledger::Pow.submit(block, broadcast: false) 57 | end 58 | end 59 | Cocol.logger.info { "--- SYNC FINISHED" } 60 | end 61 | end 62 | 63 | module Pow 64 | extend self 65 | include Ledger::Block 66 | include Ledger::Action 67 | 68 | RETARGET_TIMESPAN = 60_f64 # In seconds (I think *g*) 69 | 70 | def genesis : Nil 71 | Ledger::Repo.ledger.clear 72 | Ledger::Repo.blocks.clear 73 | Ledger::Repo.block_at_height.clear 74 | 75 | genesis = Ledger::Block::Pow.new( 76 | hash: "000003816de5a8feb130232390a28ac3566d483bde194ae6b503fec96993dffc", 77 | timestamp: 1582643058_i64, 78 | height: 0_u64, 79 | nonce: 2058209_u64, 80 | nbits: Ledger::Block::Pow::MIN_NBITS, 81 | previous_hash: "a78a4203908a94d91b1a8f6aa65f4d1176d68ba67ced10a32cfb661f40c58b88", 82 | transactions: Array(Ledger::Action::Transaction).new, 83 | coinbase: Block::Coinbase.new(miner: "Olivia", reward: 1_u64) 84 | ) 85 | 86 | Ledger::Repo.blocks[genesis.hash] = genesis 87 | Ledger::Repo.finalize(block: genesis.hash) 88 | ProbFin.push(block: genesis.hash, parent: genesis.previous_hash) 89 | end 90 | 91 | def mine(transactions : Array(Ledger::Action::Transaction)) : Ledger::Block::Pow 92 | tip_hash = Ledger::Util.probfin_tip_hash 93 | height = Ledger::Repo.blocks[tip_hash].height + 1 94 | 95 | if height % 20 == 0 # retargeting 96 | difficulty = CCL::Pow::Utils.retarget( 97 | **timespan_from_tip(hash: tip_hash), 98 | wanted_timespan: RETARGET_TIMESPAN, 99 | current_target: CCL::Pow::Utils.calculate_target( 100 | from: Ledger::Repo.blocks[tip_hash].as(Block::Pow).nbits 101 | ), 102 | min_target: Ledger::Block::Pow.min_target 103 | ) 104 | Cocol.logger.info { "New target: #{difficulty}" } 105 | else # last blocks difficulty 106 | difficulty = Ledger::Repo.blocks[tip_hash].as(Block::Pow).nbits 107 | end 108 | 109 | new_block = Ledger::Block::Pow.new( 110 | height: height, 111 | transactions: transactions, 112 | previous_hash: tip_hash, 113 | nbits: difficulty, 114 | coinbase: Block::Coinbase.new(miner: Node.settings.miner_address.as(String)) 115 | ) 116 | 117 | Cocol.logger.info { "--- MINED Height: #{new_block.height} NBits: #{new_block.nbits} Block: #{new_block.hash}" } 118 | submit(new_block) 119 | 120 | new_block 121 | end 122 | 123 | def valid?(block : Ledger::Block::Abstract) : Bool 124 | sha = OpenSSL::Digest.new("SHA256") 125 | sha.update("#{block.nonce}#{block.hash_seed}") 126 | 127 | block.hash == sha.hexdigest 128 | end 129 | 130 | def submit(block : Ledger::Block::Pow, broadcast = true) 131 | return if !Ledger::Repo.save block 132 | 133 | Cocol.logger.info { "Height: #{block.height} NBits: #{block.nbits} Block: #{block.hash}" } 134 | 135 | Ledger::Mempool.remove(block.transactions) 136 | ProbFin.push(block: block.hash, parent: block.previous_hash) 137 | result = ProbFin.finalize( 138 | from: Ledger::Repo.ledger.last 139 | ) 140 | if result.class == BlockHash 141 | Ledger::Repo.finalize(result.as(BlockHash)) 142 | end 143 | 144 | if broadcast 145 | spawn { Messenger::Action::Base.broadcast to: "/blocks/pow", body: block.to_json } 146 | 147 | spawn Event.broadcast(Event.update("onInitialUpdate").to_json) 148 | spawn Event.broadcast(Event.block(block).to_json) 149 | end 150 | end 151 | 152 | private def timespan_from_tip(hash : String) : NamedTuple 153 | last_block = Ledger::Repo.blocks[hash].as(Block::Pow) 154 | way_back_block = first_block(hash, 19) 155 | 156 | { 157 | start_time: way_back_block.timestamp.to_f64, 158 | end_time: last_block.timestamp.to_f64, 159 | } 160 | end 161 | 162 | private def first_block(hash : String, count : UInt8) 163 | return Ledger::Repo.blocks[hash].as(Block::Pow) if count <= 0_u8 164 | first_block(Ledger::Repo.blocks[hash].previous_hash, count - 1_u8) 165 | end 166 | 167 | private def genesis_transactions : Array(Ledger::Action::Transaction) 168 | txns = [] of Ledger::Action::Transaction 169 | 170 | txns << Ledger::Action::Transaction.new( 171 | from: "Olivia", 172 | to: "0x904F4094aeaadce2d46512BaeEc920977e90e7c9", 173 | amount: 100000_u64 174 | ) 175 | end 176 | end 177 | 178 | module Pos 179 | extend self 180 | include Ledger::Block 181 | 182 | def genesis : Nil 183 | Ledger::Repo.ledger.clear 184 | Ledger::Repo.blocks.clear 185 | Ledger::Repo.block_at_height.clear 186 | 187 | genesis = Ledger::Block::Pos.new( 188 | hash: "00000f33293fc3092f436fec6480ba8460589087f2118c1c2d4a60f35372f297", 189 | timestamp: 1449966000_i64, 190 | height: 0_u64, 191 | transactions: Array(Ledger::Action::Transaction).new, 192 | stakes: Array(Ledger::Action::Stake).new, 193 | previous_hash: Ledger::GENESIS_CREATOR, 194 | coinbase: Coinbase.new("3000") 195 | ) 196 | # Cocol::Pos::ValidatorPool.add(id: "4001", timestamp: Time.utc.to_unix) 197 | # Cocol::Pos::ValidatorPool.add(id: "4002", timestamp: Time.utc.to_unix + 1) 198 | # Cocol::Pos::ValidatorPool.add(id: "4003", timestamp: Time.utc.to_unix + 2) 199 | # Cocol::Pos::ValidatorPool.add(id: "4004", timestamp: Time.utc.to_unix + 3) 200 | 201 | Ledger::Repo.blocks[genesis.hash] = genesis 202 | Ledger::Repo.finalize(block: genesis.hash) 203 | ProbFin.push(block: genesis.hash, parent: genesis.previous_hash) 204 | 205 | new_block_if_leader 206 | end 207 | 208 | def mine( 209 | transactions : Array(Ledger::Action::Transaction), 210 | stakes : Array(Ledger::Action::Stake) 211 | ) : Nil 212 | tip_hash = Ledger::Util.probfin_tip_hash 213 | height = Ledger::Repo.blocks[tip_hash].height + 1 214 | 215 | stakes << Ledger::Action::Stake.new( 216 | staker: Node.settings.miner_address.as(String), 217 | amount: 33_u64, 218 | ) 219 | 220 | new_block = Ledger::Block::Pos.new( 221 | height: height, 222 | transactions: transactions, 223 | stakes: stakes, 224 | previous_hash: tip_hash, 225 | coinbase: Coinbase.new(Node.settings.miner_address.as(String)), 226 | ) 227 | 228 | if Ledger::Repo.save(block: new_block) 229 | Cocol.logger.info { "Height: #{new_block.height} Mined: #{new_block.hash[-7..-1]}" } 230 | spawn Event.broadcast(Event.block(new_block).to_json) 231 | on_save new_block 232 | end 233 | end 234 | 235 | def valid?(block : Ledger::Block::Pos) : Bool 236 | block.hash == block.calc_hash 237 | end 238 | 239 | def validate(block : Ledger::Block::Pos) : Nil 240 | if Ledger::Repo.save(block: block) 241 | Cocol.logger.debug { "BLOCK Height: #{block.height} | Saved: #{block.hash[-7..-1]}" } 242 | on_save block 243 | end 244 | end 245 | 246 | def on_save(block) 247 | Ledger::Mempool.remove block.transactions 248 | ProbFin.push(block: block.hash, parent: block.previous_hash) 249 | spawn { Messenger::Action::Base.broadcast to: "/blocks/pos", body: block.to_json } 250 | spawn Event.broadcast(Event.update("onInitialUpdate").to_json) 251 | remove_validator block.coinbase.miner 252 | add_stakers block.stakes 253 | new_block_if_leader 254 | end 255 | 256 | def add_stakers(stakes : Array(Ledger::Action::Stake)) : Nil 257 | stakes.each do |s| 258 | Cocol.logger.debug { "VALIDATOR ADDED: #{s.staker}" } 259 | CCL::Pos::ValidatorPool.add(id: s.staker, timestamp: s.timestamp) 260 | end 261 | end 262 | 263 | def remove_validator(id : String) 264 | Cocol.logger.debug { "VALIDATOR REMOVED: #{id}" } 265 | CCL::Pos::ValidatorPool.remove id 266 | Cocol.logger.debug { "VALIDATORS: #{CCL::Pos::ValidatorPool.validators}" } 267 | end 268 | 269 | def new_block_if_leader 270 | if Node.settings.miner 271 | my_turn = CCL::Pos.naive_leader?( 272 | seed: Ledger::Util.probfin_tip_hash, 273 | validator_id: Node.settings.miner_address.as(String) 274 | ) 275 | Cocol.logger.debug { "MY_TURN: #{my_turn}" } 276 | spawn block_creation_loop if my_turn 277 | end 278 | end 279 | 280 | def block_creation_loop 281 | Cocol.logger.info { "Creation loop triggered" } 282 | threshold = 2 283 | loop do 284 | sleep 0.333 285 | if (pending_transactions = Ledger::Mempool.pending.values).size >= threshold 286 | mining_transactions = pending_transactions 287 | Ledger::Mempool.remove(mining_transactions) 288 | Ledger::Pos.mine(mining_transactions, Array(Ledger::Action::Stake).new) 289 | 290 | break 291 | end 292 | end 293 | end 294 | end 295 | 296 | # def workflow_fetch_ledger(client : HTTP::Client) : Void 297 | # response = client.get "/ledger" 298 | # ledger = Array(String).from_json(response.body) 299 | 300 | # # compare with my ledger 301 | 302 | # Ledger::Repo.ledger.concat(ledger) 303 | # end 304 | 305 | # def update_ledger : Void 306 | # # pick first peer 307 | # if peer = Messenger::Repo.peers.first? 308 | # client = HTTP::Client.new(peer.ip_addr, peer.handshake.port) 309 | 310 | # response = client.get "/blocks" 311 | # json_ledger = JSON.parse(response.body) 312 | 313 | # json_ledger.as_a.each do |json_block| 314 | # block = Ledger::Action::Block.from_json(json_block.to_json) 315 | # Ledger::Repo.blocks[block.hash] = block 316 | # Ledger::Repo.establish(block.hash, block.height) 317 | # end 318 | # Event.broadcast(Event.update("onInitialUpdate").to_json) 319 | # else 320 | # sleep 1 321 | # update_ledger 322 | # end 323 | # end 324 | end 325 | -------------------------------------------------------------------------------- /src/cocol/node/ledger/action.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | module Ledger::Action 6 | alias TxnHash = String 7 | 8 | struct Signature 9 | include JSON::Serializable 10 | 11 | getter v : String 12 | getter r : String 13 | getter s : String 14 | 15 | def initialize(@v, @r, @s) 16 | end 17 | end 18 | 19 | abstract struct Abstract 20 | include JSON::Serializable 21 | 22 | getter hash : TxnHash 23 | getter amount : UInt64 24 | getter timestamp : Int64 25 | @[JSON::Field(emit_null: true)] 26 | property sig : Signature? 27 | 28 | private def calc_hash(*seed) : TxnHash 29 | sha = OpenSSL::Digest.new("SHA256") 30 | sha.update(seed.join("")) 31 | sha.hexdigest 32 | end 33 | end 34 | 35 | struct Transaction < Abstract 36 | getter from : String 37 | getter to : String 38 | 39 | def initialize(@from, @to, @amount) 40 | @timestamp = Time.utc.to_unix 41 | @hash = calc_hash(from, to, amount, timestamp) 42 | end 43 | end 44 | 45 | struct Stake < Abstract 46 | getter staker : String 47 | 48 | def initialize(@staker, @amount) 49 | @timestamp = Time.utc.to_unix 50 | @hash = calc_hash(staker, amount, timestamp) 51 | end 52 | end 53 | end 54 | -------------------------------------------------------------------------------- /src/cocol/node/ledger/api.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | # --- Transactions 6 | post "/transactions" do |env| 7 | begin 8 | new_txn = Ledger::Action::Transaction.from_json( 9 | env.request.body.not_nil!) 10 | rescue 11 | halt( 12 | env, 13 | status_code: 400, 14 | response: "Bad Request" 15 | ) 16 | end 17 | halt( 18 | env, 19 | status_code: 422, 20 | response: "Invalid Transaction" 21 | ) if !Ledger::Util.valid?(transaction: new_txn) 22 | 23 | if Ledger::Mempool.add(new_txn) 24 | spawn { Messenger::Action::PropagateTransaction.call(new_txn) } 25 | 26 | if Node.settings.miner 27 | spawn Event.broadcast(Event.transaction("onTxn", new_txn).to_json) 28 | end 29 | end 30 | 31 | new_txn.hash 32 | end 33 | 34 | get "/transactions/:hash" do |env| 35 | Ledger::Mempool.pending[env.params.url["hash"]].to_json 36 | end 37 | 38 | get "/transactions" do 39 | Ledger::Mempool.pending.to_json 40 | end 41 | 42 | # --- Blocks 43 | post "/blocks/pow" do |env| 44 | begin 45 | new_block = Ledger::Block::Pow.from_json( 46 | env.request.body.not_nil!) 47 | rescue 48 | halt( 49 | env, 50 | status_code: 400, 51 | response: "Bad Request" 52 | ) 53 | end 54 | halt( 55 | env, 56 | status_code: 422, 57 | response: "Invalid Block" 58 | ) if !Ledger::Pow.valid?(block: new_block) 59 | 60 | Ledger::Pow.submit(new_block) 61 | end 62 | 63 | post "/blocks/pos" do |env| 64 | begin 65 | new_block = Ledger::Block::Pos.from_json( 66 | env.request.body.not_nil!) 67 | rescue 68 | halt( 69 | env, 70 | status_code: 400, 71 | response: "Bad Request" 72 | ) 73 | end 74 | halt( 75 | env, 76 | status_code: 422, 77 | response: "Invalid Block" 78 | ) if !Ledger::Pos.valid?(block: new_block) 79 | 80 | Ledger::Pos.validate new_block 81 | end 82 | 83 | get "/inventory/:best_hash" do |env| 84 | Ledger::Inventory.call(env.params.url["best_hash"]).to_json 85 | end 86 | 87 | get "/blocks/:hash" do |env| 88 | block = Ledger::Repo.blocks[env.params.url["hash"]]? 89 | halt( 90 | env, 91 | status_code: 404, 92 | response: "Not found" 93 | ) if block.nil? 94 | 95 | block.to_json 96 | end 97 | 98 | # --- Ledger 99 | get "/blocks/finalized" do 100 | Ledger::Repo.ledger.to_json 101 | end 102 | 103 | get "/blocks/pending" do 104 | { 105 | dag: ProbFin::Chain.dag.map { |k, _v| k }, 106 | orphans: ProbFin::Chain.orphans.map { |k, _v| k }, 107 | }.to_json 108 | end 109 | -------------------------------------------------------------------------------- /src/cocol/node/ledger/block.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | require "./action" 6 | 7 | module Ledger::Block 8 | alias BlockHash = String 9 | alias BlockHashSeed = String 10 | 11 | struct Coinbase 12 | include JSON::Serializable 13 | 14 | getter miner : String 15 | getter reward : UInt64 16 | 17 | def initialize(@miner) 18 | @reward = 50_u64 19 | end 20 | 21 | def initialize(@miner, @reward) 22 | end 23 | end 24 | 25 | abstract struct Abstract 26 | include JSON::Serializable 27 | include Ledger::Action 28 | 29 | getter hash : String 30 | getter timestamp : Int64 31 | getter height : UInt64 32 | getter previous_hash : String 33 | getter coinbase : Coinbase 34 | getter transactions : Array(Transaction) 35 | 36 | abstract def hash_seed : BlockHashSeed 37 | abstract def calc_hash 38 | end 39 | 40 | struct Pos < Abstract 41 | getter stakes : Array(Stake) 42 | 43 | def initialize(@hash, 44 | @timestamp, 45 | @height, 46 | @previous_hash, 47 | @transactions, 48 | @stakes, 49 | @coinbase) 50 | end 51 | 52 | def initialize(@height, 53 | @transactions, 54 | @stakes, 55 | @previous_hash, 56 | @coinbase) 57 | @timestamp = Time.utc.to_unix 58 | @hash = calc_hash 59 | end 60 | 61 | def hash_seed : BlockHashSeed 62 | transactions = @transactions.map { |txn| txn.hash }.join("") 63 | "#{@height}#{@timestamp}#{transactions}#{@previous_hash}#{@coinbase}" 64 | end 65 | 66 | def calc_hash 67 | sha256 = OpenSSL::Digest.new("SHA256") 68 | sha256.update(hash_seed) 69 | sha256.hexdigest 70 | end 71 | end 72 | 73 | struct Pow < Abstract 74 | MIN_NBITS = "20000100" 75 | getter nonce : UInt64 76 | getter nbits : String 77 | 78 | def initialize(@hash, 79 | @timestamp, 80 | @height, 81 | @nonce, 82 | @nbits, 83 | @previous_hash, 84 | @transactions, 85 | @coinbase) 86 | end 87 | 88 | def initialize(@height, 89 | @transactions, 90 | @previous_hash, 91 | @nbits, 92 | @coinbase) 93 | @timestamp = Time.utc.to_unix 94 | 95 | pow = calc_hash() 96 | @hash = pow.hash 97 | @nonce = pow.nonce 98 | end 99 | 100 | def hash_seed : BlockHashSeed 101 | transactions = @transactions.map { |txn| txn.hash }.join("") 102 | "#{@height}#{@timestamp}#{transactions}#{@previous_hash}#{@coinbase}" 103 | end 104 | 105 | def self.min_target 106 | CCL::Pow::Utils.calculate_target(MIN_NBITS) 107 | end 108 | 109 | def calc_hash 110 | CCL::Pow.mine(difficulty: @nbits, for: hash_seed()) 111 | end 112 | end 113 | end 114 | -------------------------------------------------------------------------------- /src/cocol/node/ledger/mempool.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | require "./action" 6 | 7 | module Ledger 8 | module Mempool 9 | extend self 10 | 11 | include Ledger::Action 12 | 13 | alias TxnHash = String 14 | 15 | def pending : Hash(TxnHash, Transaction) 16 | @@pending ||= Hash(TxnHash, Transaction).new 17 | end 18 | 19 | def remove(arg) : Bool 20 | case arg 21 | when TxnHash 22 | return false if !pending[arg]? 23 | 24 | pending.delete(arg) 25 | true 26 | when Array 27 | arg.each { |txn| self.remove(txn.hash) } 28 | true 29 | else 30 | false 31 | end 32 | end 33 | 34 | def add(txn : Transaction) : Bool 35 | return false if pending[txn.hash]? 36 | 37 | pending[txn.hash] = txn 38 | true 39 | end 40 | end 41 | end 42 | -------------------------------------------------------------------------------- /src/cocol/node/ledger/repo.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | require "./block" 6 | 7 | module Ledger 8 | module Repo 9 | extend self 10 | 11 | alias BlockHash = String 12 | alias Block = (Ledger::Block::Pow | Ledger::Block::Pos) 13 | 14 | def blocks 15 | @@blocks ||= Hash(BlockHash, Block).new 16 | end 17 | 18 | def ledger 19 | @@ledger ||= Array(BlockHash).new 20 | end 21 | 22 | def block_at_height 23 | @@block_at_height ||= Hash(UInt64, BlockHash).new 24 | end 25 | 26 | # === 27 | 28 | def ledger_last : Block 29 | blocks[ledger.last] 30 | end 31 | 32 | def save(block : Block) : Bool 33 | return false if self.blocks[block.hash]? 34 | 35 | self.blocks[block.hash] = block 36 | 37 | true 38 | end 39 | 40 | def finalize(block hash : BlockHash) : Bool 41 | finalized_block = self.blocks[hash] 42 | return false if self.block_at_height[finalized_block.height]? 43 | 44 | self.ledger << hash 45 | self.block_at_height[finalized_block.height] = hash 46 | 47 | true 48 | end 49 | end 50 | end 51 | -------------------------------------------------------------------------------- /src/cocol/node/ledger/util.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | module Ledger 6 | module Util 7 | extend self 8 | 9 | def valid?(transaction : Ledger::Action::Transaction) : Bool 10 | tip = probfin_tip_hash 11 | valid? transaction: transaction, at: tip 12 | end 13 | 14 | def valid?( 15 | transaction : Ledger::Action::Transaction, 16 | at block : String 17 | ) : Bool 18 | # validate signature 19 | return false if !signature_valid?(transaction) 20 | 21 | # 0 amount transactions are invalid 22 | return false if transaction.amount == 0 23 | 24 | # validate against account balance 25 | allowed_amount = balance for: transaction.from, until: block 26 | allowed_amount >= transaction.amount 27 | end 28 | 29 | def probfin_tip_hash : String 30 | current = ProbFin::Chain.dag[Ledger::Repo.ledger.last] 31 | DAG.tip_of_longest_branch(from: current).vertex.name 32 | end 33 | 34 | def balance(for address : String) : Int64 35 | tip = probfin_tip_hash 36 | 37 | balance for: address, until: tip 38 | end 39 | 40 | def balance(for address : String, until block_hash : String) : Int64 41 | balance(for: address, 42 | until: block_hash, 43 | result: 0_i64) 44 | end 45 | 46 | def signature_valid?(txn : Ledger::Action::Transaction) : Bool 47 | txn_sig = txn.sig.as(Ledger::Action::Signature) 48 | r = BigInt.new(txn_sig.r) 49 | s = BigInt.new(txn_sig.s) 50 | sig = Secp256k1::ECDSASignature.new(r: r, s: s) 51 | pubdc = Secp256k1::Util.decode_compressed_public_key(txn_sig.v) 52 | Secp256k1::Signature.verify(txn.hash, sig, pubdc) 53 | end 54 | 55 | protected def balance( 56 | for address : String, 57 | until block_hash : String, 58 | result : Int64 59 | ) : Int64 60 | block = Ledger::Repo.blocks[block_hash] 61 | result = balance(for: address, 62 | until: block.previous_hash, 63 | result: result) if block.previous_hash != Ledger::GENESIS_CREATOR 64 | 65 | block.transactions.each do |txn| 66 | result = result + txn.amount if txn.to == address 67 | result = result - txn.amount if txn.from == address 68 | end 69 | 70 | result 71 | end 72 | end 73 | end 74 | -------------------------------------------------------------------------------- /src/cocol/node/messenger.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | require "./messenger/**" 6 | 7 | module Messenger 8 | extend self 9 | 10 | module Action 11 | module Base 12 | extend self 13 | 14 | def broadcast(to endpoint : String, body : String) : Void 15 | Messenger::Repo.peers.each do |peer| 16 | uri = peer_uri(from: peer) 17 | uri.path = endpoint 18 | post(to: uri, body: body) 19 | end 20 | end 21 | 22 | def post( 23 | to uri : URI, 24 | body : String 25 | ) : HTTP::Client::Response | Nil 26 | client = HTTP::Client.new(uri) 27 | begin 28 | response = client.post( 29 | path: uri.path, 30 | headers: HTTP::Headers{ 31 | "Content-Type" => "application/json", 32 | "X-Node-Id" => Node.settings.ident.to_s, 33 | }, 34 | body: body 35 | ) 36 | client.close 37 | response 38 | rescue 39 | Cocol.logger.warn { "Peer is not responding at POST-#{uri}" } 40 | nil 41 | end 42 | end 43 | 44 | def get(from uri : URI) : HTTP::Client::Response | Nil 45 | client = HTTP::Client.new(uri) 46 | begin 47 | response = client.get( 48 | path: uri.path, 49 | headers: HTTP::Headers{ 50 | "Content-Type" => "application/json", 51 | "X-Node-Id" => Node.settings.ident.to_s, 52 | } 53 | ) 54 | client.close 55 | response 56 | rescue 57 | Cocol.logger.warn { "Peer is not responding at GET-#{uri}" } 58 | nil 59 | end 60 | end 61 | 62 | def peer_uri(from peer : Messenger::Struct::Peer) : URI 63 | URI.new( 64 | scheme: "http", 65 | host: peer.host, 66 | port: peer.port.to_i32 67 | ) 68 | end 69 | end 70 | 71 | module PropagateTransaction 72 | include Base 73 | extend self 74 | 75 | PATH = "/transactions" 76 | 77 | def call(payload : Ledger::Action::Transaction) 78 | broadcast to: PATH, body: payload.to_json 79 | end 80 | end 81 | 82 | module Handshake 83 | include Base 84 | extend self 85 | 86 | PATH = "/peers" 87 | 88 | def call(peer : Messenger::Struct::Peer) : HTTP::Client::Response | Nil 89 | payload = Messenger::Struct::Peer.new(**Node.settings.peer_info) 90 | uri = peer_uri(from: peer) 91 | uri.path = PATH 92 | post(to: uri, body: payload.to_json) 93 | end 94 | end 95 | 96 | module GetPeers 97 | include Base 98 | extend self 99 | 100 | PATH = "/peers" 101 | 102 | def call(peer : Messenger::Struct::Peer) : Array(Messenger::Struct::Peer) 103 | uri = peer_uri(from: peer) 104 | uri.path = PATH 105 | response = get(from: uri) 106 | # TODO: fixme, this is bad https://gph.is/2drwobJ 107 | return Array(Messenger::Struct::Peer).new if response.nil? 108 | Array(Messenger::Struct::Peer).from_json(response.body) 109 | end 110 | end 111 | 112 | module GetBlock 113 | include Base 114 | extend self 115 | 116 | PATH = "/blocks/:hash" 117 | 118 | def call(block_hash : String, peer : Messenger::Struct::Peer) : Ledger::Block::Pow 119 | uri = peer_uri(from: peer) 120 | uri.path = PATH.gsub(":hash", block_hash) 121 | response = get(from: uri) 122 | Ledger::Block::Pow.from_json(response.as(HTTP::Client::Response).body) 123 | end 124 | end 125 | 126 | module Inventory 127 | extend self 128 | include Base 129 | include Ledger::Block 130 | 131 | PATH = "/inventory/:best_hash" 132 | 133 | def call(peer : Messenger::Struct::Peer, best_hash : BlockHash) : Array(BlockHash) 134 | uri = peer_uri(from: peer) 135 | uri.path = PATH.gsub(":best_hash", best_hash) 136 | response = get(from: uri) 137 | 138 | Array(BlockHash).from_json(response.as(HTTP::Client::Response).body) 139 | end 140 | end 141 | end 142 | 143 | def establish_network_position 144 | config = Totem.from_file("./config.yml") 145 | master = config.mapping(Messenger::Struct::Peer, "master") 146 | 147 | # make yourself known 148 | Messenger::Action::Handshake.call peer: master 149 | 150 | # now get all peers master knows about 151 | peers = Messenger::Action::GetPeers.call(peer: master) 152 | # remove yourself 153 | peers = peers.reject! { |p| p.ident == Node.settings.ident } 154 | Messenger::Repo.known_peers.concat(peers) 155 | 156 | # now establish connection to peers if free slots are available 157 | peers.sample(Messenger.free_slots).each do |peer| 158 | sleep 1 159 | next if Node.settings.ident == peer.ident 160 | next if (handshake = Messenger::Action::Handshake.call(peer)).nil? 161 | next if handshake.status_code != 200 162 | 163 | Messenger::Repo.peers.add(peer) 164 | end 165 | end 166 | 167 | def free_slots : UInt16 168 | free = Node.settings.max_connections - Messenger::Repo.peers.size 169 | return 0_u16 if free < 0 170 | free.to_u16 171 | end 172 | end 173 | -------------------------------------------------------------------------------- /src/cocol/node/messenger/api.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | get "/peers" do |_env| 6 | Messenger::Repo.peers.to_json 7 | end 8 | 9 | post "/peers" do |env| 10 | halt( 11 | env, 12 | status_code: 429, 13 | response: "Too Many Requests" 14 | ) if Messenger.free_slots <= 0 15 | 16 | begin 17 | new_peer = Messenger::Struct::Peer.from_json( 18 | env.request.body.not_nil!) 19 | rescue 20 | halt( 21 | env, 22 | status_code: 400, 23 | response: "Bad Request" 24 | ) 25 | end 26 | 27 | spawn Event.broadcast(Event.peer(new_peer).to_json) 28 | Messenger::Repo.peers << new_peer 29 | 30 | new_peer.ident.to_s 31 | end 32 | 33 | get "/known-peers" do |_env| 34 | Messenger::Repo.known_peers.to_json 35 | end 36 | 37 | post "/internal/handshake/:host/:port" do |env| 38 | target_host = env.params.url["host"] 39 | target_port = env.params.url["port"].to_u32 40 | 41 | peer = Messenger::Struct::Peer.new(port: target_port, host: target_host) 42 | Messenger::Action::Handshake.call(peer) 43 | end 44 | -------------------------------------------------------------------------------- /src/cocol/node/messenger/repo.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | module Messenger 6 | module Repo 7 | extend self 8 | 9 | def peers 10 | @@peers ||= Set(Messenger::Struct::Peer).new 11 | end 12 | 13 | def known_peers 14 | @@known_peers ||= Set(Messenger::Struct::Peer).new 15 | end 16 | end 17 | end 18 | -------------------------------------------------------------------------------- /src/cocol/node/messenger/struct/peer.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | module Messenger::Struct 6 | struct Peer 7 | include JSON::Serializable 8 | 9 | property ident : String? 10 | property host : String 11 | property port : UInt32 12 | 13 | def initialize(@port, @host) 14 | end 15 | 16 | def initialize(@ident, @port, @host) 17 | end 18 | end 19 | end 20 | -------------------------------------------------------------------------------- /src/cocol/node/settings.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | module Node 6 | class Settings 7 | getter ident : String 8 | property host : String = "localhost" 9 | property port : UInt32 = 3001_u32 10 | property name : String = "Unknown" 11 | property max_connections : UInt16 = 5_u16 12 | property miner : Bool = false 13 | property miner_address : String? 14 | property master : Bool = false 15 | 16 | def initialize 17 | @ident = Random::Secure.urlsafe_base64(10) 18 | end 19 | 20 | def peer_info 21 | {host: @host, port: @port, ident: @ident} 22 | end 23 | end 24 | 25 | def self.settings 26 | @@settings ||= Settings.new 27 | end 28 | end 29 | -------------------------------------------------------------------------------- /src/cocol/version.cr: -------------------------------------------------------------------------------- 1 | module Cocol 2 | VERSION = "0.2.1" 3 | end 4 | -------------------------------------------------------------------------------- /src/deps.cr: -------------------------------------------------------------------------------- 1 | # This Source Code Form is subject to the terms of the Mozilla Public License, 2 | # v. 2.0. If a copy of the MPL was not distributed with this file, You can 3 | # obtain one at # http://mozilla.org/MPL/2.0/ 4 | 5 | require "./cocol/logger" 6 | 7 | require "json" 8 | require "uuid" 9 | require "uuid/json" 10 | require "http/web_socket" 11 | require "http/client" 12 | 13 | require "kemal" 14 | require "clim" 15 | require "ccl-pow" 16 | require "probfin" 17 | require "ccl-pos" 18 | require "secp256k1" 19 | require "totem" 20 | 21 | require "./cocol/node/settings" 22 | require "./cocol/node/ledger" 23 | require "./cocol/node/messenger" 24 | require "./cocol/node/event" 25 | --------------------------------------------------------------------------------