From 06f85553d425d1a0c3adeed21468d71d4b545a45 Mon Sep 17 00:00:00 2001 From: iotex-dev Date: Thu, 19 Apr 2018 16:02:22 -0700 Subject: [PATCH] initial release of testnet preview --- .circleci/config.yml | 26 + .gitignore | 32 + .pre-commit-config.yaml | 9 + CONTRIBUTING.md | 28 + Dockerfile | 12 + LICENSE.md | 201 +++++ Makefile | 55 ++ README.md | 129 ++++ blockchain/block.go | 218 ++++++ blockchain/block_test.go | 103 +++ blockchain/blockchain.go | 346 +++++++++ blockchain/blockchain_test.go | 283 +++++++ blockchain/iblockchain.go | 53 ++ blockchain/transaction.go | 271 +++++++ blockchain/transaction_test.go | 104 +++ blockchain/utxo.go | 233 ++++++ blockchain/utxo_test.go | 154 ++++ blockdb/blockdb.go | 248 +++++++ blocksync/blocksync.go | 337 +++++++++ blocksync/slidingwindow.go | 130 ++++ blocksync/slidingwindow_test.go | 71 ++ common/definition.go | 12 + common/idispatcher.go | 27 + common/node.go | 39 + common/routine/recurringtask.go | 49 ++ common/routine/recurringtask_test.go | 35 + common/routine/timeouttask.go | 56 ++ common/routine/timeouttask_test.go | 47 ++ common/service/service.go | 71 ++ common/utils/counter.go | 91 +++ common/utils/counter_test.go | 50 ++ config.yaml | 54 ++ config/config.go | 248 +++++++ config/config_test.go | 150 ++++ consensus/consensus.go | 100 +++ consensus/fsm/fsm.go | 342 +++++++++ consensus/fsm/fsm_test.go | 236 ++++++ consensus/scheme/noop.go | 37 + consensus/scheme/rdpos/evtconv.go | 63 ++ consensus/scheme/rdpos/fsm_create.go | 36 + consensus/scheme/rdpos/fsm_create_test.go | 138 ++++ .../scheme/rdpos/handler_accept_prevote.go | 26 + .../scheme/rdpos/handler_accept_propose.go | 27 + consensus/scheme/rdpos/handler_accept_vote.go | 26 + .../scheme/rdpos/handler_init_propose.go | 34 + .../scheme/rdpos/handler_init_propose_test.go | 33 + consensus/scheme/rdpos/handler_start.go | 28 + consensus/scheme/rdpos/proposer_rotation.go | 39 + .../scheme/rdpos/proposer_rotation_test.go | 49 ++ consensus/scheme/rdpos/rdpos.go | 162 ++++ consensus/scheme/rdpos/rdpos_test.go | 378 ++++++++++ consensus/scheme/rdpos/rule_commit.go | 54 ++ consensus/scheme/rdpos/rule_is_proposer.go | 18 + consensus/scheme/rdpos/rule_not_proposer.go | 20 + consensus/scheme/rdpos/rule_prevote.go | 52 ++ consensus/scheme/rdpos/rule_propose.go | 47 ++ consensus/scheme/rdpos/rule_propose_test.go | 35 + consensus/scheme/rdpos/rule_vote.go | 57 ++ consensus/scheme/rdpos/state_machine.png | Bin 0 -> 43877 bytes consensus/scheme/scheme.go | 32 + consensus/scheme/standalone.go | 73 ++ crypto/merkle.go | 99 +++ crypto/merkle_test.go | 69 ++ crypto/sig.go | 34 + crypto/sig_test.go | 27 + delegate/delegate.go | 58 ++ delegate/delegate_test.go | 44 ++ dispatcher/dispatcher.go | 299 ++++++++ dispatcher/dispatcher_test.go | 153 ++++ e2etests/config_local_delegate.yaml | 48 ++ e2etests/config_local_fullnode.yaml | 49 ++ e2etests/config_local_rdpos.yaml | 57 ++ e2etests/local_rdpos_test.go | 63 ++ e2etests/local_test.go | 267 +++++++ e2etests/net_test.go | 75 ++ e2etests/util.go | 69 ++ glide.lock | 95 +++ glide.yaml | 31 + iotxaddress/bech32/bech32.go | 265 +++++++ iotxaddress/bech32/bech32_test.go | 70 ++ iotxaddress/iotxaddress.go | 159 ++++ iotxaddress/iotxaddress_test.go | 56 ++ misc/scripts/mockgen.sh | 42 ++ misc/scripts/stringer.sh | 5 + network/gossip.go | 124 ++++ network/healthchecker.go | 39 + network/overlay.go | 148 ++++ network/overlay_test.go | 401 ++++++++++ network/peer.go | 114 +++ network/peermaintainer.go | 111 +++ network/peermanager.go | 123 +++ network/pinger.go | 37 + network/proto/rpc.pb.go | 428 +++++++++++ network/proto/rpc.proto | 58 ++ network/rpcserver.go | 188 +++++ network/rpcserver_test.go | 248 +++++++ network/utils.go | 90 +++ proto/blockchain.pb.go | 553 ++++++++++++++ proto/blockchain.proto | 104 +++ proto/extra_interface.go | 46 ++ proto/rpc.pb.go | 240 ++++++ proto/rpc.proto | 35 + proto/utils.go | 81 ++ proto/utxo.pb.go | 118 +++ proto/utxo.proto | 26 + rpcservice/rpcservice.go | 112 +++ rpcservice/rpcservice_test.go | 150 ++++ server/itx/itxserver.go | 78 ++ server/main.go | 43 ++ server/run/run.go | 74 ++ test/assets/ssl/127.0.0.1.crt | 25 + test/assets/ssl/127.0.0.1.csr | 16 + test/assets/ssl/127.0.0.1.key | 27 + test/assets/ssl/README.md | 1 + test/assets/ssl/iotex.io.crl | 16 + test/assets/ssl/iotex.io.crt | 29 + test/assets/ssl/iotex.io.key | 51 ++ test/mock/mock_blockchain/mock_blockchain.go | 242 ++++++ test/mock/mock_blocksync/mock_blocksync.go | 108 +++ test/mock/mock_delegate/mock_delegate.go | 59 ++ test/mock/mock_dispatcher/mock_dispatcher.go | 79 ++ test/mock/mock_rdpos/mock_rdpos.go | 71 ++ test/mock/mock_txpool/mock_txpool.go | 203 +++++ test/testaddress/testaddress.go | 64 ++ tools/cli/cli.go | 94 +++ tools/cli/get_balance.go | 28 + tools/cli/print_chain.go | 33 + tools/cli/send.go | 33 + tools/run_tnet.sh | 8 + tools/start_node.sh | 7 + tools/txinjector/txinjector.go | 103 +++ txpool/txpool.go | 701 ++++++++++++++++++ txpool/txpool_test.go | 188 +++++ txvm/ast.go | 35 + txvm/ast_test.go | 40 + txvm/error.go | 44 ++ txvm/error_test.go | 30 + txvm/opcode.go | 278 +++++++ txvm/opcode_test.go | 77 ++ txvm/scriptbuilder.go | 79 ++ txvm/scripts.go | 77 ++ txvm/scripts_test.go | 59 ++ txvm/vm.go | 36 + txvm/vm_test.go | 33 + 144 files changed, 14991 insertions(+) create mode 100644 .circleci/config.yml create mode 100644 .gitignore create mode 100644 .pre-commit-config.yaml create mode 100644 CONTRIBUTING.md create mode 100644 Dockerfile create mode 100644 LICENSE.md create mode 100644 Makefile create mode 100644 README.md create mode 100644 blockchain/block.go create mode 100644 blockchain/block_test.go create mode 100644 blockchain/blockchain.go create mode 100644 blockchain/blockchain_test.go create mode 100644 blockchain/iblockchain.go create mode 100644 blockchain/transaction.go create mode 100644 blockchain/transaction_test.go create mode 100644 blockchain/utxo.go create mode 100644 blockchain/utxo_test.go create mode 100644 blockdb/blockdb.go create mode 100644 blocksync/blocksync.go create mode 100644 blocksync/slidingwindow.go create mode 100644 blocksync/slidingwindow_test.go create mode 100644 common/definition.go create mode 100644 common/idispatcher.go create mode 100644 common/node.go create mode 100644 common/routine/recurringtask.go create mode 100644 common/routine/recurringtask_test.go create mode 100644 common/routine/timeouttask.go create mode 100644 common/routine/timeouttask_test.go create mode 100644 common/service/service.go create mode 100644 common/utils/counter.go create mode 100644 common/utils/counter_test.go create mode 100644 config.yaml create mode 100644 config/config.go create mode 100644 config/config_test.go create mode 100644 consensus/consensus.go create mode 100644 consensus/fsm/fsm.go create mode 100644 consensus/fsm/fsm_test.go create mode 100644 consensus/scheme/noop.go create mode 100644 consensus/scheme/rdpos/evtconv.go create mode 100644 consensus/scheme/rdpos/fsm_create.go create mode 100644 consensus/scheme/rdpos/fsm_create_test.go create mode 100644 consensus/scheme/rdpos/handler_accept_prevote.go create mode 100644 consensus/scheme/rdpos/handler_accept_propose.go create mode 100644 consensus/scheme/rdpos/handler_accept_vote.go create mode 100644 consensus/scheme/rdpos/handler_init_propose.go create mode 100644 consensus/scheme/rdpos/handler_init_propose_test.go create mode 100644 consensus/scheme/rdpos/handler_start.go create mode 100644 consensus/scheme/rdpos/proposer_rotation.go create mode 100644 consensus/scheme/rdpos/proposer_rotation_test.go create mode 100644 consensus/scheme/rdpos/rdpos.go create mode 100644 consensus/scheme/rdpos/rdpos_test.go create mode 100644 consensus/scheme/rdpos/rule_commit.go create mode 100644 consensus/scheme/rdpos/rule_is_proposer.go create mode 100644 consensus/scheme/rdpos/rule_not_proposer.go create mode 100644 consensus/scheme/rdpos/rule_prevote.go create mode 100644 consensus/scheme/rdpos/rule_propose.go create mode 100644 consensus/scheme/rdpos/rule_propose_test.go create mode 100644 consensus/scheme/rdpos/rule_vote.go create mode 100644 consensus/scheme/rdpos/state_machine.png create mode 100644 consensus/scheme/scheme.go create mode 100644 consensus/scheme/standalone.go create mode 100644 crypto/merkle.go create mode 100644 crypto/merkle_test.go create mode 100644 crypto/sig.go create mode 100644 crypto/sig_test.go create mode 100644 delegate/delegate.go create mode 100644 delegate/delegate_test.go create mode 100644 dispatcher/dispatcher.go create mode 100644 dispatcher/dispatcher_test.go create mode 100644 e2etests/config_local_delegate.yaml create mode 100644 e2etests/config_local_fullnode.yaml create mode 100644 e2etests/config_local_rdpos.yaml create mode 100644 e2etests/local_rdpos_test.go create mode 100644 e2etests/local_test.go create mode 100644 e2etests/net_test.go create mode 100644 e2etests/util.go create mode 100644 glide.lock create mode 100644 glide.yaml create mode 100644 iotxaddress/bech32/bech32.go create mode 100644 iotxaddress/bech32/bech32_test.go create mode 100644 iotxaddress/iotxaddress.go create mode 100644 iotxaddress/iotxaddress_test.go create mode 100755 misc/scripts/mockgen.sh create mode 100644 misc/scripts/stringer.sh create mode 100644 network/gossip.go create mode 100644 network/healthchecker.go create mode 100644 network/overlay.go create mode 100644 network/overlay_test.go create mode 100644 network/peer.go create mode 100644 network/peermaintainer.go create mode 100644 network/peermanager.go create mode 100644 network/pinger.go create mode 100644 network/proto/rpc.pb.go create mode 100644 network/proto/rpc.proto create mode 100644 network/rpcserver.go create mode 100644 network/rpcserver_test.go create mode 100644 network/utils.go create mode 100644 proto/blockchain.pb.go create mode 100644 proto/blockchain.proto create mode 100644 proto/extra_interface.go create mode 100644 proto/rpc.pb.go create mode 100644 proto/rpc.proto create mode 100644 proto/utils.go create mode 100644 proto/utxo.pb.go create mode 100644 proto/utxo.proto create mode 100644 rpcservice/rpcservice.go create mode 100644 rpcservice/rpcservice_test.go create mode 100644 server/itx/itxserver.go create mode 100644 server/main.go create mode 100644 server/run/run.go create mode 100644 test/assets/ssl/127.0.0.1.crt create mode 100644 test/assets/ssl/127.0.0.1.csr create mode 100644 test/assets/ssl/127.0.0.1.key create mode 100644 test/assets/ssl/README.md create mode 100644 test/assets/ssl/iotex.io.crl create mode 100644 test/assets/ssl/iotex.io.crt create mode 100644 test/assets/ssl/iotex.io.key create mode 100644 test/mock/mock_blockchain/mock_blockchain.go create mode 100644 test/mock/mock_blocksync/mock_blocksync.go create mode 100644 test/mock/mock_delegate/mock_delegate.go create mode 100644 test/mock/mock_dispatcher/mock_dispatcher.go create mode 100644 test/mock/mock_rdpos/mock_rdpos.go create mode 100644 test/mock/mock_txpool/mock_txpool.go create mode 100644 test/testaddress/testaddress.go create mode 100644 tools/cli/cli.go create mode 100644 tools/cli/get_balance.go create mode 100644 tools/cli/print_chain.go create mode 100644 tools/cli/send.go create mode 100755 tools/run_tnet.sh create mode 100755 tools/start_node.sh create mode 100644 tools/txinjector/txinjector.go create mode 100644 txpool/txpool.go create mode 100644 txpool/txpool_test.go create mode 100755 txvm/ast.go create mode 100755 txvm/ast_test.go create mode 100755 txvm/error.go create mode 100755 txvm/error_test.go create mode 100755 txvm/opcode.go create mode 100755 txvm/opcode_test.go create mode 100755 txvm/scriptbuilder.go create mode 100644 txvm/scripts.go create mode 100644 txvm/scripts_test.go create mode 100755 txvm/vm.go create mode 100755 txvm/vm_test.go diff --git a/.circleci/config.yml b/.circleci/config.yml new file mode 100644 index 0000000000..00ae5d54be --- /dev/null +++ b/.circleci/config.yml @@ -0,0 +1,26 @@ +# Golang CircleCI 2.0 configuration file +# +# Check https://circleci.com/docs/2.0/language-go/ for more details +version: 2 +jobs: + build: + docker: + # specify the version + - image: circleci/golang:1.9 + + # Specify service dependencies here if necessary + # CircleCI maintains a library of pre-built images + # documented at https://circleci.com/docs/2.0/circleci-images/ + # - image: circleci/postgres:9.4 + + #### TEMPLATE_NOTE: go expects specific checkout path representing url + #### expecting it in the form of + #### /go/src/github.com/circleci/go-tool + #### /go/src/bitbucket.org/circleci/go-tool + working_directory: /go/src/github.com/iotexproject/iotex-core + steps: + - checkout + + # specify any bash command here prefixed with `run: ` + - run: go get -v -t -d ./... + - run: go test -short -v ./... diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..bf515f4738 --- /dev/null +++ b/.gitignore @@ -0,0 +1,32 @@ +.idea +*.iml +*.db + +*.DS_Store +.AppleDouble +.LSOverride + + +# Binaries for programs and plugins +*.exe +*.dll +*.so +*.dylib + +# Test binary, build with `go test -c` +*.test + +# Output of the go coverage tool, specifically when used with LiteIDE +*.out + +# Project-local glide cache, RE: https://github.com/Masterminds/glide/issues/736 +.glide/ + +# Golang dependencies +vendor/* + +# wallet date file +*.dat + +# binary +bin/* diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000000..7ac5de301b --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,9 @@ +exclude: '^vendor/' +fail_fast: true +repos: +- repo: https://github.com/troian/pre-commit-golang + sha: HEAD + hooks: + - id: go-fmt + - id: go-build + - id: go-lint diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000000..aeca69f27e --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,28 @@ +Contributing to IoTeX Core +============================ + +The IoTeX Core project operates an open contributor model where anyone is welcome to contribute towards development in the form of peer review, testing and patches. This document explains the practical process and guidelines for contributing. + +Contributor Workflow +-------------------- + +The codebase is maintained using the "contributor workflow" where everyone +without exception contributes patch proposals using "pull requests". This +facilitates social contribution, easy testing and peer review. + +To contribute a patch, the workflow is as follows: + + 1. Fork repository + 1. Create topic branch + 1. Commit patches + +In general commits should be atomic and diffs should be easy to read, and the [coding conventions](https://github.com/golang/go/wiki/CodeReviewComments) +must be adhered to. + + +Copyright +--------- + +By contributing to this repository, you agree to license your work under the +Apache 2.0 license. Any work contributed where you are not the original +author must contain its license header with the original author(s) and source. \ No newline at end of file diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000000..be824374a3 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,12 @@ +FROM golang + +RUN \ + apt-get install -y --no-install-recommends make && \ + curl https://glide.sh/get | sh && \ + mkdir -p $GOPATH/src/github.com/iotexproject/ && \ + (cd $GOPATH/src/github.com/iotexproject/ && git clone https://github.com/iotexproject/iotex-core.git) && \ + (cd $GOPATH/src/github.com/iotexproject/iotex-core && glide i&& make build) + +ENTRYPOINT ["/go/src/github.com/iotexproject/iotex-core/tools/start_node.sh"] + +EXPOSE 40000-40005 diff --git a/LICENSE.md b/LICENSE.md new file mode 100644 index 0000000000..f49a4e16e6 --- /dev/null +++ b/LICENSE.md @@ -0,0 +1,201 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION + + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. + + END OF TERMS AND CONDITIONS + + APPENDIX: How to apply the Apache License to your work. + + To apply the Apache License to your work, attach the following + boilerplate notice, with the fields enclosed by brackets "[]" + replaced with your own identifying information. (Don't include + the brackets!) The text should be enclosed in the appropriate + comment syntax for the file format. We also recommend that a + file or class name and description of purpose be included on the + same "printed page" as the copyright notice for easier + identification within third-party archives. + + Copyright [yyyy] [name of copyright owner] + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. \ No newline at end of file diff --git a/Makefile b/Makefile new file mode 100644 index 0000000000..a3040c302c --- /dev/null +++ b/Makefile @@ -0,0 +1,55 @@ +######################################################################################################################## +# Copyright (c) 2018 IoTeX +# This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +# warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +# permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +# License 2.0 that can be found in the LICENSE file. +######################################################################################################################## + +# Go parameters +GOCMD=go +GOLINT=golint +GOBUILD=$(GOCMD) build +GOCLEAN=$(GOCMD) clean +GOTEST=$(GOCMD) test +GOGET=$(GOCMD) get +BUILD_TARGET_SERVER=server +BUILD_TARGET_TXINJ=txinjector + +all: build test +.PHONY: build +build: + $(GOBUILD) -o ./bin/$(BUILD_TARGET_SERVER) -v ./$(BUILD_TARGET_SERVER) + $(GOBUILD) -o ./bin/$(BUILD_TARGET_TXINJ) -v ./tools/txinjector + +.PHONY: fmt +fmt: + $(GOCMD) fmt ./... + +.PHONY: lint +lint: + go list ./... | grep -v /vendor/ | xargs $(GOLINT) + +.PHONY: test +test: fmt + rm -f ./e2etests/db.test + $(GOTEST) -short ./... + +.PHONY: mockgen +mockgen: + @./misc/scripts/mockgen.sh + +.PHONY: stringer +stringer: + sh ./misc/scripts/stringer.sh + +.PHONY: clean +clean: + $(GOCLEAN) + rm -f ./bin/$(BUILD_TARGET_SERVER) + rm -f ./bin/$(BUILD_TARGET_TXINJ) + +.PHONY: run +run: + $(GOBUILD) -o ./bin/$(BUILD_TARGET_SERVER) -v ./$(BUILD_TARGET_SERVER) + ./bin/$(BUILD_TARGET_SERVER) -stderrthreshold=WARNING -log_dir=./log diff --git a/README.md b/README.md new file mode 100644 index 0000000000..402c1b2945 --- /dev/null +++ b/README.md @@ -0,0 +1,129 @@ +[![Go version](https://img.shields.io/badge/go-1.9.2-blue.svg)](https://github.com/moovweb/gvm) +[![CircleCI](https://circleci.com/gh/iotexproject/iotex-core/tree/master.svg?style=svg)](https://circleci.com/gh/iotexproject/iotex-core/tree/master) + +# iotex-core +Welcome to the official Go implementation of IoTeX protocol! IoTeX is building the next generation of the decentralized +network for IoT powered by scalability- and privacy-centric blockchains. Please refer to +IoTeX [whitepaper](https://iotex.io/white-paper) for details. + +Currently, This repo is of alpha-quality with limited features supported and it is subjected to rapid change. Please +contact us if you intend to run it in production. + +## System Components and Flowchart +![systemflowchart](https://user-images.githubusercontent.com/15241597/38832065-3e57ca3a-4176-11e8-9bff-110387cf2378.png) + +## Feature List +### Testnet Preview (codename: StoneVan) +1. TBC (Transactions, Block & Chain) +* Bech32-encoded address +* Serialization and deserialize of messages on the wire +* Merkle tree +* Transactions, blocks and chain +* Transaction pool +* Fast and reliable blockchain storage and query using BoltDB +* Block sync from network peers +* Basic framework for script and VM +2. Network +* Efficient gossip protocol over TLS +* Broadcast & unicast semantics +* Seeding through network config +* Rate-limit requests per connection +* Peer discovery +* Large-scale simulation and load test +3. Consensus +* Framework for plugable consensus +* Standalone and NOOP schemes +* Basic implementation of R-DPoS scheme +4. Clients +* Initial RPC support +* Tools for injecting transactions/blocks +5. Testing \& Integration +* Unit test coverage > 50% +* Integration tests +* Staging development to 50 nodes (for internal use only) + +### Testnet Alpha \& Beta +* libsect283k1 and integration +* Random beacon and full R-DPoS with voting support +* Lightweight stealth address +* Cross Chain Communication (CCC) +* Fast block sync and checkpointing +* Script and VM +* Full explorer and wallet supporting Hierarchical Deterministic (HD) addresses +* SPV clients +* Seeding through IPFS and version negotiation +* Pluggable transportation framework w/ UDP + TCP support +* Peer metrics +* Unit test coverage > 70% +* e2e demo among 500-1000 peers +* Enhancement of existing features +* And much more ... + + +## Dev Tips +## Minimum requirements + +| Components | Version | Description | +|----------|-------------|-------------| +|[Golang](https://golang.org) | >= 1.9.2| The Go Programming Language | +|[Glide](https://github.com/Masterminds/glide) | >= 0.13.0 | Glide is a dependency management tool for Go | + +### Setup Dev Environment +``` +mkdir -p ~/go/src/github.com/iotexproject +cd ~/go/src/github.com/iotexproject +git clone git@github.com:iotexproject/iotex-core.git +cd iotex-core +``` + +```glide install``` + +```make fmt; make build``` + +#### Setup Precommit Hook + +Install git hook tools from [precommit hook](https://pre-commit.com/) first and then + +```pre-commit install``` + +### Run Unit Tests +```make test``` + +### Run +```make run``` +You will see log message output like: +``` +W0416 12:52:15.652394 1576 blocksync.go:220] ====== receive tip block 116 +W0416 12:52:15.653014 1576 blocksync.go:276] ------ [127.0.0.1:4689] receive block 116 in 616.939µs +W0416 12:52:15.653967 1576 blocksync.go:293] ------ commit block 116 time = 923.364µs + +W0416 12:52:16.648667 1576 blocksync.go:276] ------ [127.0.0.1:4689] receive block 117 in 994.656929ms +W0416 12:52:16.649745 1576 blocksync.go:293] ------ commit block 117 time = 1.029462ms + +W0416 12:52:17.648360 1576 blocksync.go:276] ------ [127.0.0.1:4689] receive block 118 in 998.560518ms +W0416 12:52:17.649604 1576 blocksync.go:293] ------ commit block 118 time = 1.186833ms + +W0416 12:52:18.648309 1576 blocksync.go:276] ------ [127.0.0.1:4689] receive block 119 in 998.658293ms +W0416 12:52:18.649432 1576 blocksync.go:293] ------ commit block 119 time = 1.075772ms + +W0416 12:52:19.653393 1576 blocksync.go:276] ------ [127.0.0.1:4689] receive block 120 in 1.003920977s +W0416 12:52:19.654698 1576 blocksync.go:293] ------ commit block 120 time = 1.256924ms + +W0416 12:52:20.649165 1576 blocksync.go:276] ------ [127.0.0.1:4689] receive block 121 in 994.424355ms +W0416 12:52:20.650254 1576 blocksync.go:293] ------ commit block 121 time = 1.0282ms + +W0416 12:52:21.653420 1576 blocksync.go:276] ------ [127.0.0.1:4689] receive block 122 in 1.00312559s +W0416 12:52:21.654650 1576 blocksync.go:293] ------ commit block 122 time = 1.182673ms +``` + +# Deploy w/ Docker Image +```docker build . -t stonevan``` + +```docker run stonevan``` + +## Contribution +We are glad to have contributors out of the core team; contributions, including (but not limited to) style/bug fixes, implementation of features, proposals of schemes/algorithms, and thorough documentation, are +welcomed. Please refer to our [contribution guideline](https://github.com/iotexproject/iotex-core/blob/master/CONTRIBUTING.md) for more information. + +## License +This project is licensed under the [Apache License 2.0](https://github.com/iotexproject/iotex-core/blob/master/LICENSE.md). diff --git a/blockchain/block.go b/blockchain/block.go new file mode 100644 index 0000000000..96813d52b8 --- /dev/null +++ b/blockchain/block.go @@ -0,0 +1,218 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blockchain + +import ( + "bytes" + "errors" + "time" + + "github.com/golang/protobuf/proto" + "golang.org/x/crypto/blake2b" + + cm "github.com/iotexproject/iotex-core/common" + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/proto" +) + +const ( + // Version of blockchain protocol + Version = 1 +) + +// BlockHeader defines the struct of block header +// make sure the variable type and order of this struct is same as "BlockHeaderPb" in blockchain.pb.go +type BlockHeader struct { + version uint32 // version + chainID uint32 // this chain's ID + height uint32 // block height + timestamp uint64 // timestamp + prevBlockHash cp.Hash32B // hash of previous block + merkleRoot cp.Hash32B // merkle root of all trn + trnxNumber uint32 // number of transaction in this block + trnxDataSize uint32 // size (in bytes) of transaction data in this block +} + +// Block defines the struct of block +// make sure the variable type and order of this struct is same as "type Block" in blockchain.pb.go +type Block struct { + Header *BlockHeader + Tranxs []*Tx +} + +// NewBlock returns a new block +func NewBlock(chainID uint32, height uint32, prevBlockHash cp.Hash32B, transactions []*Tx) *Block { + block := &Block{ + Header: &BlockHeader{Version, chainID, height, uint64(time.Now().Unix()), prevBlockHash, cp.ZeroHash32B, uint32(len(transactions)), 0}, + Tranxs: transactions, + } + + block.Header.merkleRoot = block.MerkleRoot() + for _, tx := range transactions { + // add up trnx size + block.Header.trnxDataSize += tx.TotalSize() + } + return block +} + +// Height returns the height of this block +func (b *Block) Height() uint32 { + return b.Header.height +} + +// TranxsSize returns the size of transactions in this block +func (b *Block) TranxsSize() uint32 { + return b.Header.trnxDataSize +} + +// PrevHash returns the hash of prev block +func (b *Block) PrevHash() cp.Hash32B { + return b.Header.prevBlockHash +} + +// ByteStream returns a byte stream of the block +// used to calculate the block hash +func (b *Block) ByteStream() []byte { + stream := make([]byte, 4) + cm.MachineEndian.PutUint32(stream, b.Header.version) + + temp := make([]byte, 4) + cm.MachineEndian.PutUint32(temp, b.Header.chainID) + stream = append(stream, temp...) + cm.MachineEndian.PutUint32(temp, b.Header.height) + stream = append(stream, temp...) + + time := make([]byte, 8) + cm.MachineEndian.PutUint64(time, b.Header.timestamp) + stream = append(stream, time...) + + stream = append(stream, b.Header.prevBlockHash[:]...) + stream = append(stream, b.Header.merkleRoot[:]...) + + cm.MachineEndian.PutUint32(temp, b.Header.trnxNumber) + stream = append(stream, temp...) + cm.MachineEndian.PutUint32(temp, b.Header.trnxDataSize) + stream = append(stream, temp...) + + // write all trnx + for _, tx := range b.Tranxs { + stream = append(stream, tx.ByteStream()...) + } + + return stream +} + +// ConvertToBlockHeaderPb converts BlockHeader to BlockHeaderPb +func (b *Block) ConvertToBlockHeaderPb() *iproto.BlockHeaderPb { + pbHeader := iproto.BlockHeaderPb{} + + pbHeader.Version = b.Header.version + pbHeader.ChainID = b.Header.chainID + pbHeader.Height = b.Header.height + pbHeader.Timestamp = b.Header.timestamp + pbHeader.PrevBlockHash = b.Header.prevBlockHash[:] + pbHeader.MerkleRoot = b.Header.merkleRoot[:] + pbHeader.TrnxNumber = b.Header.trnxNumber + pbHeader.TrnxDataSize = b.Header.trnxDataSize + + return &pbHeader +} + +// ConvertToBlockPb converts Block to BlockPb +func (b *Block) ConvertToBlockPb() *iproto.BlockPb { + tx := make([]*iproto.TxPb, len(b.Tranxs)) + for i, in := range b.Tranxs { + tx[i] = in.ConvertToTxPb() + } + + return &iproto.BlockPb{b.ConvertToBlockHeaderPb(), tx} +} + +// Serialize returns the serialized byte stream of the block +func (b *Block) Serialize() ([]byte, error) { + return proto.Marshal(b.ConvertToBlockPb()) +} + +// ConvertFromBlockHeaderPb converts BlockHeaderPb to BlockHeader +func (b *Block) ConvertFromBlockHeaderPb(pbBlock *iproto.BlockPb) { + b.Header = nil + b.Header = new(BlockHeader) + + b.Header.version = pbBlock.GetHeader().GetVersion() + b.Header.chainID = pbBlock.GetHeader().GetChainID() + b.Header.height = pbBlock.GetHeader().GetHeight() + b.Header.timestamp = pbBlock.GetHeader().GetTimestamp() + copy(b.Header.prevBlockHash[:], pbBlock.GetHeader().GetPrevBlockHash()) + copy(b.Header.merkleRoot[:], pbBlock.GetHeader().GetMerkleRoot()) + b.Header.trnxNumber = pbBlock.GetHeader().GetTrnxNumber() + b.Header.trnxDataSize = pbBlock.GetHeader().GetTrnxDataSize() +} + +// ConvertFromBlockPb converts BlockPb to Block +func (b *Block) ConvertFromBlockPb(pbBlock *iproto.BlockPb) { + b.ConvertFromBlockHeaderPb(pbBlock) + + b.Tranxs = nil + b.Tranxs = make([]*Tx, len(pbBlock.Transactions)) + for i, tx := range pbBlock.Transactions { + b.Tranxs[i] = &Tx{} + b.Tranxs[i].ConvertFromTxPb(tx) + } +} + +// Deserialize parse the byte stream into Block +func (b *Block) Deserialize(buf []byte) error { + pbBlock := iproto.BlockPb{} + if err := proto.Unmarshal(buf, &pbBlock); err != nil { + return err + } + + b.ConvertFromBlockPb(&pbBlock) + + // verify merkle root can match after deserialize + merkle := b.MerkleRoot() + if bytes.Compare(b.Header.merkleRoot[:], merkle[:]) != 0 { + return errors.New("Failed to match merkle root after deserialize") + } + + return nil +} + +// MerkleRoot returns the Merkle root of this block. +func (b *Block) MerkleRoot() cp.Hash32B { + // create hash list of all trnx + var txHash []cp.Hash32B + for _, tx := range b.Tranxs { + txHash = append(txHash, tx.Hash()) + } + + return cp.NewMerkleTree(txHash).HashTree() +} + +// HashBlock return the hash of this block (actually hash of block header) +func (b *Block) HashBlock() cp.Hash32B { + stream := make([]byte, 4) + cm.MachineEndian.PutUint32(stream, b.Header.version) + tmp4B := make([]byte, 4) + cm.MachineEndian.PutUint32(tmp4B, b.Header.chainID) + stream = append(stream, tmp4B...) + cm.MachineEndian.PutUint32(tmp4B, b.Header.height) + stream = append(stream, tmp4B...) + tmp8B := make([]byte, 8) + cm.MachineEndian.PutUint64(tmp8B, b.Header.timestamp) + stream = append(stream, tmp8B...) + stream = append(stream, b.Header.prevBlockHash[:]...) + stream = append(stream, b.Header.merkleRoot[:]...) + cm.MachineEndian.PutUint32(tmp4B, b.Header.trnxNumber) + stream = append(stream, tmp4B...) + cm.MachineEndian.PutUint32(tmp4B, b.Header.trnxDataSize) + stream = append(stream, tmp4B...) + + hash := blake2b.Sum256(stream) + hash = blake2b.Sum256(hash[:]) + return hash +} diff --git a/blockchain/block_test.go b/blockchain/block_test.go new file mode 100644 index 0000000000..3162d8aed5 --- /dev/null +++ b/blockchain/block_test.go @@ -0,0 +1,103 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blockchain + +import ( + "crypto/sha256" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/blake2b" + + cp "github.com/iotexproject/iotex-core/crypto" + ta "github.com/iotexproject/iotex-core/test/testaddress" +) + +func TestBasicHash(t *testing.T) { + assert := assert.New(t) + + // basic hash test + input := []byte("hello") + hash := sha256.Sum256(input) + hash = sha256.Sum256(hash[:]) + hello, _ := hex.DecodeString("9595c9df90075148eb06860365df33584b75bff782a510c6cd4883a419833d50") + assert.Equal(hello, hash[:]) + t.Logf("sha256(sha256(\"hello\") = %x", hash) + + hash = blake2b.Sum256(input) + hash = blake2b.Sum256(hash[:]) + hello, _ = hex.DecodeString("901c60ffffd77f743729f8fea0233c0b00223428b5192c2015f853562b45ce59") + assert.Equal(hello, hash[:]) + t.Logf("blake2b(blake2b(\"hello\") = %x", hash) +} + +func TestMerkle(t *testing.T) { + assert := assert.New(t) + + amount := uint64(50 << 22) + // create testing transactions + cbtx0 := NewCoinbaseTx(ta.Addrinfo["miner"].Address, amount, GenesisCoinbaseData) + cbtx1 := NewCoinbaseTx(ta.Addrinfo["alfa"].Address, amount, GenesisCoinbaseData) + cbtx2 := NewCoinbaseTx(ta.Addrinfo["bravo"].Address, amount, GenesisCoinbaseData) + cbtx3 := NewCoinbaseTx(ta.Addrinfo["charlie"].Address, amount, GenesisCoinbaseData) + cbtx4 := NewCoinbaseTx(ta.Addrinfo["echo"].Address, amount, GenesisCoinbaseData) + + // verify tx hash + hash0, _ := hex.DecodeString("90e0967d54b5f6f898c95404d0818f3f7a332ee6d5d7439666dd1e724771cb5e") + actual := cbtx0.Hash() + assert.Equal(hash0, actual[:]) + + hash1, _ := hex.DecodeString("7959228bfdb316949973c08d8bb7bea2a21227a7b4ed85c35d247bf3d6b15a11") + actual = cbtx1.Hash() + assert.Equal(hash1, actual[:]) + + hash2, _ := hex.DecodeString("b57c9d659f99c21c38ab3323996ad69cbb5f417fbce496be25d35b3ebecefca6") + actual = cbtx2.Hash() + assert.Equal(hash2, actual[:]) + + hash3, _ := hex.DecodeString("e5f326657b615336301ccaed5c5f6e2952547d7bdf7ee198458fd7e9e2e2b3f7") + actual = cbtx3.Hash() + assert.Equal(hash3, actual[:]) + + hash4, _ := hex.DecodeString("f937d7a934e25230ec3c701b18ce108f322519e70544745ac3f848739f4efa80") + actual = cbtx4.Hash() + assert.Equal(hash4, actual[:]) + + // manually compute merkle root + cat := append(hash0, hash1...) + hash01 := blake2b.Sum256(cat) + t.Logf("hash01 = %x", hash01) + + cat = append(hash2, hash3...) + hash23 := blake2b.Sum256(cat) + t.Logf("hash23 = %x", hash23) + + cat = append(hash4, hash4...) + hash45 := blake2b.Sum256(cat) + t.Logf("hash45 = %x", hash45) + + cat = append(hash01[:], hash23[:]...) + hash03 := blake2b.Sum256(cat) + t.Logf("hash03 = %x", hash03) + + cat = append(hash45[:], hash45[:]...) + hash47 := blake2b.Sum256(cat) + t.Logf("hash47 = %x", hash47) + + cat = append(hash03[:], hash47[:]...) + hash07 := blake2b.Sum256(cat) + t.Logf("hash07 = %x", hash07) + + // create block using above 5 tx and verify merkle + block := NewBlock(0, 0, cp.ZeroHash32B, []*Tx{cbtx0, cbtx1, cbtx2, cbtx3, cbtx4}) + hash := block.MerkleRoot() + assert.Equal(hash07[:], hash[:]) + t.Log("Merkle root match pass\n") + + // serialize +} diff --git a/blockchain/blockchain.go b/blockchain/blockchain.go new file mode 100644 index 0000000000..6718ae9aad --- /dev/null +++ b/blockchain/blockchain.go @@ -0,0 +1,346 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blockchain + +import ( + "math" + "os" + + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-core/blockdb" + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/config" + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/iotxaddress" + "github.com/iotexproject/iotex-core/proto" + "github.com/iotexproject/iotex-core/txvm" +) + +const ( + // GenesisCoinbaseData is the text in genesis block + GenesisCoinbaseData = "The Times 03/Jan/2009 Chancellor on brink of second bailout for banks" +) + +var ( + // ErrInvalidBlock is the error returned when the block is not vliad + ErrInvalidBlock = errors.New("failed to validate the block") +) + +// Blockchain implements the IBlockchain interface +type Blockchain struct { + blockDb *blockdb.BlockDB + config *config.Config + chainID uint32 + height uint32 + tip cp.Hash32B + Utk *UtxoTracker // tracks the current UTXO pool +} + +// NewBlockchain creates a new blockchain instance +func NewBlockchain(db *blockdb.BlockDB, cfg *config.Config) *Blockchain { + chain := &Blockchain{ + blockDb: db, + config: cfg, + Utk: NewUtxoTracker()} + return chain +} + +// Init initializes the blockchain +func (bc *Blockchain) Init() error { + tip, height, err := bc.blockDb.Init() + if err != nil { + return err + } + + copy(bc.tip[:], tip) + bc.height = height + + // build UTXO pool + // Genesis block has height 0 + for i := uint32(0); i <= bc.height; i++ { + blk, err := bc.GetBlockByHeight(i) + if err != nil { + return err + } + bc.Utk.UpdateUtxoPool(blk) + } + return nil +} + +// Close closes the Db connection +func (bc *Blockchain) Close() error { + return bc.blockDb.Close() +} + +// commitBlock commits Block to Db +func (bc *Blockchain) commitBlock(blk *Block) (err error) { + // post-commit actions + defer func() { + // update tip hash and height + if r := recover(); r != nil { + return + } + + // update tip hash/height + bc.tip = blk.HashBlock() + bc.height = blk.Header.height + + // update UTXO pool + bc.Utk.UpdateUtxoPool(blk) + }() + + // serialize the block + serialized, err := blk.Serialize() + if err != nil { + panic(err) + } + + hash := blk.HashBlock() + if err = bc.blockDb.CheckInBlock(serialized, hash[:], blk.Header.height); err != nil { + panic(err) + } + return +} + +// GetHeightByHash returns block's height by hash +func (bc *Blockchain) GetHeightByHash(hash cp.Hash32B) (uint32, error) { + return bc.blockDb.GetBlockHeight(hash[:]) +} + +// GetHashByHeight returns block's hash by height +func (bc *Blockchain) GetHashByHeight(height uint32) (cp.Hash32B, error) { + hash := cp.ZeroHash32B + dbHash, err := bc.blockDb.GetBlockHash(height) + copy(hash[:], dbHash) + return hash, err +} + +// GetBlockByHeight returns block from the blockchain hash by height +func (bc *Blockchain) GetBlockByHeight(height uint32) (*Block, error) { + hash, err := bc.GetHashByHeight(height) + if err != nil { + return nil, err + } + return bc.GetBlockByHash(hash) +} + +// GetBlockByHash returns block from the blockchain hash by hash +func (bc *Blockchain) GetBlockByHash(hash cp.Hash32B) (*Block, error) { + serialized, err := bc.blockDb.CheckOutBlock(hash[:]) + if err != nil { + return nil, err + } + + // deserialize the block + blk := Block{} + if err := blk.Deserialize(serialized); err != nil { + return nil, err + } + return &blk, nil +} + +// TipHash returns tip block's hash +func (bc *Blockchain) TipHash() cp.Hash32B { + return bc.tip +} + +// TipHeight returns tip block's height +func (bc *Blockchain) TipHeight() uint32 { + return bc.height +} + +// Reset reset for next block +func (bc *Blockchain) Reset() { + bc.Utk.Reset() +} + +// ValidateBlock validates a new block before adding it to the blockchain +func (bc *Blockchain) ValidateBlock(blk *Block) error { + if blk == nil { + return errors.Wrap(ErrInvalidBlock, "Block is nil") + } + // verify new block has correctly linked to current tip + if blk.Header.prevBlockHash != bc.tip { + return errors.Wrapf(ErrInvalidBlock, "Wrong prev hash %x, expecting %x", blk.Header.prevBlockHash, bc.tip) + } + + // verify new block has height incremented by 1 + if blk.Header.height != 0 && blk.Header.height != bc.height+1 { + return errors.Wrapf(ErrInvalidBlock, "Wrong block height %d, expecting %d", blk.Header.height, bc.height+1) + } + + // validate all Tx conforms to blockchain protocol + + // validate UXTO contained in this Tx + return bc.Utk.ValidateUtxo(blk) +} + +// MintNewBlock creates a new block with given transactions. +// Note: the coinbase transaction will be added to the given transactions +// when minting a new block. +func (bc *Blockchain) MintNewBlock(txs []*Tx, toaddr, data string) *Block { + txs = append(txs, NewCoinbaseTx(toaddr, bc.config.Chain.BlockReward, data)) + return NewBlock(bc.chainID, bc.height+1, bc.tip, txs) +} + +// AddBlockCommit adds a new block into blockchain +func (bc *Blockchain) AddBlockCommit(blk *Block) error { + if err := bc.ValidateBlock(blk); err != nil { + return err + } + + // commit block into blockchain DB + return bc.commitBlock(blk) +} + +// AddBlockSync adds a past block into blockchain +// used by block syncer when the chain in out-of-sync +func (bc *Blockchain) AddBlockSync(blk *Block) error { + // directly commit block into blockchain DB + return bc.commitBlock(blk) +} + +// StoreBlock persists the blocks in the range to file on disk +func (bc *Blockchain) StoreBlock(start, end uint32) error { + return bc.blockDb.StoreBlockToFile(start, end) +} + +// ReadBlock read the block from file on disk +func (bc *Blockchain) ReadBlock(height uint32) *Block { + file, err := os.Open(blockdb.BlockData) + defer file.Close() + if err != nil { + glog.Error(err) + return nil + } + + // read block index + indexSize := make([]byte, 4) + file.Read(indexSize) + size := cm.MachineEndian.Uint32(indexSize) + indexBytes := make([]byte, size) + if n, err := file.Read(indexBytes); err != nil || n != int(size) { + glog.Error(err) + return nil + } + blkIndex := iproto.BlockIndex{} + if proto.Unmarshal(indexBytes, &blkIndex) != nil { + glog.Error(err) + return nil + } + + // read the specific block + index := height - blkIndex.Start + file.Seek(int64(4+size+blkIndex.Offset[index]), 0) + size = blkIndex.Offset[index+1] - blkIndex.Offset[index] + blkBytes := make([]byte, size) + if n, err := file.Read(blkBytes); err != nil || n != int(size) { + glog.Error(err) + return nil + } + blk := Block{} + if blk.Deserialize(blkBytes) != nil { + glog.Error(err) + return nil + } + return &blk +} + +// CreateBlockchain creates a new blockchain and DB instance +func CreateBlockchain(address string, cfg *config.Config) *Blockchain { + db, dbFileExist := blockdb.NewBlockDB(cfg) + if db == nil { + glog.Error("cannot find db") + return nil + } + chain := NewBlockchain(db, cfg) + + if dbFileExist { + glog.Info("Blockchain already exists.") + + if err := chain.Init(); err != nil { + glog.Fatalf("Failed to create Blockchain, error = %v", err) + return nil + } + return chain + } + + // create genesis block + cbtx := NewCoinbaseTx(address, cfg.Chain.TotalSupply, GenesisCoinbaseData) + genesis := NewBlock(chain.chainID, 0, cp.ZeroHash32B, []*Tx{cbtx}) + genesis.Header.timestamp = 0 + + // Genesis block has height 0 + if genesis.Header.height != 0 { + glog.Errorf("Genesis block has height = %d, expecting 0", genesis.Height()) + return nil + } + + // add Genesis block as very first block + if err := chain.AddBlockCommit(genesis); err != nil { + glog.Error(err) + return nil + } + return chain +} + +// BalanceOf returns the balance of an address +func (bc *Blockchain) BalanceOf(address string) uint64 { + _, balance := bc.Utk.UtxoEntries(address, math.MaxUint64) + return balance +} + +// UtxoPool returns the UTXO pool of current blockchain +func (bc *Blockchain) UtxoPool() map[cp.Hash32B][]*TxOutput { + return bc.Utk.utxoPool +} + +// createTx creates a transaction paying 'amount' from 'from' to 'to' +func (bc *Blockchain) createTx(from iotxaddress.Address, amount uint64, to []*Payee, isRaw bool) *Tx { + utxo, change := bc.Utk.UtxoEntries(from.Address, amount) + if utxo == nil { + glog.Errorf("Fail to get UTXO for %v", from.Address) + return nil + } + + in := []*TxInput{} + for _, out := range utxo { + unlock := []byte(out.TxOutputPb.String()) + if !isRaw { + var err error + unlock, err = txvm.SignatureScript([]byte(out.TxOutputPb.String()), from.PublicKey, from.PrivateKey) + if err != nil { + return nil + } + } + + in = append(in, bc.Utk.CreateTxInputUtxo(out.txHash, out.outIndex, unlock)) + } + + out := []*TxOutput{} + for _, payee := range to { + out = append(out, bc.Utk.CreateTxOutputUtxo(payee.Address, payee.Amount)) + } + if change > 0 { + out = append(out, bc.Utk.CreateTxOutputUtxo(from.Address, change)) + } + + return NewTx(1, in, out, 0) +} + +// CreateTransaction creates a signed transaction paying 'amount' from 'from' to 'to' +func (bc *Blockchain) CreateTransaction(from iotxaddress.Address, amount uint64, to []*Payee) *Tx { + return bc.createTx(from, amount, to, false) +} + +// CreateRawTransaction creates a unsigned transaction paying 'amount' from 'from' to 'to' +func (bc *Blockchain) CreateRawTransaction(from iotxaddress.Address, amount uint64, to []*Payee) *Tx { + return bc.createTx(from, amount, to, true) +} diff --git a/blockchain/blockchain_test.go b/blockchain/blockchain_test.go new file mode 100644 index 0000000000..c82775b9df --- /dev/null +++ b/blockchain/blockchain_test.go @@ -0,0 +1,283 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blockchain + +import ( + "errors" + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/iotexproject/iotex-core/config" + cp "github.com/iotexproject/iotex-core/crypto" + ta "github.com/iotexproject/iotex-core/test/testaddress" +) + +const ( + testingConfigPath = "../config.yaml" + testDBPath = "db.test" +) + +func addTestingBlocks(bc *Blockchain) error { + // Add block 1 + // test --> A, B, C, D, E, F + payee := []*Payee{} + payee = append(payee, &Payee{ta.Addrinfo["alfa"].Address, 20}) + payee = append(payee, &Payee{ta.Addrinfo["bravo"].Address, 30}) + payee = append(payee, &Payee{ta.Addrinfo["charlie"].Address, 50}) + payee = append(payee, &Payee{ta.Addrinfo["delta"].Address, 70}) + payee = append(payee, &Payee{ta.Addrinfo["echo"].Address, 110}) + payee = append(payee, &Payee{ta.Addrinfo["foxtrot"].Address, 50 << 20}) + tx := bc.CreateTransaction(ta.Addrinfo["miner"], 280+(50<<20), payee) + if tx == nil { + return errors.New("empty tx for block 1") + } + blk := bc.MintNewBlock([]*Tx{tx}, ta.Addrinfo["miner"].Address, "") + if err := bc.AddBlockCommit(blk); err != nil { + return err + } + bc.Reset() + + // Add block 2 + // Charlie --> A, B, D, E, test + payee = nil + payee = append(payee, &Payee{ta.Addrinfo["alfa"].Address, 1}) + payee = append(payee, &Payee{ta.Addrinfo["bravo"].Address, 1}) + payee = append(payee, &Payee{ta.Addrinfo["charlie"].Address, 1}) + payee = append(payee, &Payee{ta.Addrinfo["delta"].Address, 1}) + payee = append(payee, &Payee{ta.Addrinfo["miner"].Address, 1}) + tx = bc.CreateTransaction(ta.Addrinfo["charlie"], 5, payee) + blk = bc.MintNewBlock([]*Tx{tx}, ta.Addrinfo["miner"].Address, "") + if err := bc.AddBlockCommit(blk); err != nil { + return err + } + bc.Reset() + + // Add block 3 + // Delta --> B, E, F, test + payee = payee[1:] + payee[1] = &Payee{ta.Addrinfo["echo"].Address, 1} + payee[2] = &Payee{ta.Addrinfo["foxtrot"].Address, 1} + tx = bc.CreateTransaction(ta.Addrinfo["delta"], 4, payee) + blk = bc.MintNewBlock([]*Tx{tx}, ta.Addrinfo["miner"].Address, "") + if err := bc.AddBlockCommit(blk); err != nil { + return err + } + bc.Reset() + + // Add block 4 + // Delta --> A, B, C, D, F, test + payee = nil + payee = append(payee, &Payee{ta.Addrinfo["alfa"].Address, 2}) + payee = append(payee, &Payee{ta.Addrinfo["bravo"].Address, 2}) + payee = append(payee, &Payee{ta.Addrinfo["charlie"].Address, 2}) + payee = append(payee, &Payee{ta.Addrinfo["delta"].Address, 2}) + payee = append(payee, &Payee{ta.Addrinfo["foxtrot"].Address, 2}) + payee = append(payee, &Payee{ta.Addrinfo["miner"].Address, 2}) + tx = bc.CreateTransaction(ta.Addrinfo["echo"], 12, payee) + blk = bc.MintNewBlock([]*Tx{tx}, ta.Addrinfo["miner"].Address, "") + if err := bc.AddBlockCommit(blk); err != nil { + return err + } + bc.Reset() + + return nil +} + +func TestCreateBlockchain(t *testing.T) { + defer os.Remove(testDBPath) + assert := assert.New(t) + + config, err := config.LoadConfigWithPathWithoutValidation(testingConfigPath) + assert.Nil(err) + config.Chain.ChainDBPath = testDBPath + // Disable block reward to make bookkeeping easier + config.Chain.BlockReward = 0 + + // create chain + bc := CreateBlockchain(ta.Addrinfo["miner"].Address, config) + assert.NotNil(bc) + assert.Equal(0, int(bc.height)) + fmt.Printf("Create blockchain pass, height = %d\n", bc.height) + defer bc.Close() + + // verify Genesis block + genesis, _ := bc.GetBlockByHeight(0) + assert.NotNil(genesis) + // serialize + data, err := genesis.Serialize() + assert.Nil(err) + + stream := genesis.ByteStream() + assert.Equal(uint32(len(stream)), genesis.TranxsSize()+92) + fmt.Printf("Block size match pass\n") + fmt.Printf("Marshaling Block pass\n") + + // deserialize + deserialize := Block{} + err = deserialize.Deserialize(data) + assert.Nil(err) + fmt.Printf("Unmarshaling Block pass\n") + + hash := genesis.HashBlock() + assert.Equal(hash, deserialize.HashBlock()) + fmt.Printf("Serialize/Deserialize Block hash = %x match\n", hash) + + hash = genesis.MerkleRoot() + assert.Equal(hash, deserialize.MerkleRoot()) + fmt.Printf("Serialize/Deserialize Block merkle = %x match\n", hash) + + // add 4 sample blocks + assert.Nil(addTestingBlocks(bc)) + assert.Equal(4, int(bc.height)) +} + +func TestLoadBlockchainfromDB(t *testing.T) { + defer os.Remove(testDBPath) + assert := assert.New(t) + + config, err := config.LoadConfigWithPathWithoutValidation(testingConfigPath) + assert.Nil(err) + config.Chain.ChainDBPath = testDBPath + // Disable block reward to make bookkeeping easier + config.Chain.BlockReward = 0 + + // Create a blockchain from scratch + bc := CreateBlockchain(ta.Addrinfo["miner"].Address, config) + assert.NotNil(bc) + fmt.Printf("Open blockchain pass, height = %d\n", bc.height) + assert.Nil(addTestingBlocks(bc)) + bc.Close() + + // Load a blockchain from DB + bc = CreateBlockchain(ta.Addrinfo["miner"].Address, config) + defer bc.Close() + assert.NotNil(bc) + + // check hash<-->height mapping + hash, err := bc.GetHashByHeight(0) + assert.Nil(err) + height, err := bc.GetHeightByHash(hash) + assert.Nil(err) + assert.Equal(uint32(0), height) + blk, err := bc.GetBlockByHash(hash) + assert.Nil(err) + assert.Equal(hash, blk.HashBlock()) + fmt.Printf("Genesis hash = %x\n", hash) + + hash1, err := bc.GetHashByHeight(1) + assert.Nil(err) + height, err = bc.GetHeightByHash(hash1) + assert.Nil(err) + assert.Equal(uint32(1), height) + blk, err = bc.GetBlockByHash(hash1) + assert.Nil(err) + assert.Equal(hash1, blk.HashBlock()) + fmt.Printf("block 1 hash = %x\n", hash1) + + hash2, err := bc.GetHashByHeight(2) + assert.Nil(err) + height, err = bc.GetHeightByHash(hash2) + assert.Nil(err) + assert.Equal(uint32(2), height) + blk, err = bc.GetBlockByHash(hash2) + assert.Nil(err) + assert.Equal(hash2, blk.HashBlock()) + fmt.Printf("block 2 hash = %x\n", hash2) + + hash3, err := bc.GetHashByHeight(3) + assert.Nil(err) + height, err = bc.GetHeightByHash(hash3) + assert.Nil(err) + assert.Equal(uint32(3), height) + blk, err = bc.GetBlockByHash(hash3) + assert.Nil(err) + assert.Equal(hash3, blk.HashBlock()) + fmt.Printf("block 3 hash = %x\n", hash3) + + hash4, err := bc.GetHashByHeight(4) + assert.Nil(err) + height, err = bc.GetHeightByHash(hash4) + assert.Nil(err) + assert.Equal(uint32(4), height) + blk, err = bc.GetBlockByHash(hash4) + assert.Nil(err) + assert.Equal(hash4, blk.HashBlock()) + fmt.Printf("block 4 hash = %x\n", hash4) + + empblk, err := bc.GetBlockByHash(cp.ZeroHash32B) + assert.Nil(empblk) + + blk, err = bc.GetBlockByHeight(60000) + assert.Nil(blk) + + // add wrong blocks + h := bc.TipHeight() + hash = bc.TipHash() + blk, err = bc.GetBlockByHeight(h) + assert.Nil(err) + assert.Equal(hash, blk.HashBlock()) + fmt.Printf("Current tip = %d hash = %x\n", h, hash) + + // add block with wrong height + blk = NewBlock(0, h+2, hash, []*Tx{NewCoinbaseTx(ta.Addrinfo["bravo"].Address, 50, GenesisCoinbaseData)}) + err = bc.ValidateBlock(blk) + assert.NotNil(err) + fmt.Printf("Cannot validate block %d: %v\n", blk.Height(), err) + + // add block with zero prev hash + blk = NewBlock(0, h+1, cp.ZeroHash32B, []*Tx{NewCoinbaseTx(ta.Addrinfo["bravo"].Address, 50, GenesisCoinbaseData)}) + err = bc.ValidateBlock(blk) + assert.NotNil(err) + fmt.Printf("Cannot validate block %d: %v\n", blk.Height(), err) + + // cannot add existing block again + blk, err = bc.GetBlockByHeight(3) + assert.NotNil(blk) + err = bc.commitBlock(blk) + assert.NotNil(err) + fmt.Printf("Cannot add block 3 again: %v\n", err) + + // read/write blocks from/to storage + err = bc.StoreBlock(1, 4) + assert.Nil(err) + blk = bc.ReadBlock(1) + assert.Equal(hash1, blk.HashBlock()) + fmt.Printf("Read block 1 hash match\n") + blk = bc.ReadBlock(2) + assert.Equal(hash2, blk.HashBlock()) + fmt.Printf("Read block 2 hash match\n") + blk = bc.ReadBlock(3) + assert.Equal(hash3, blk.HashBlock()) + fmt.Printf("Read block 3 hash match\n") + blk = bc.ReadBlock(4) + assert.Equal(hash4, blk.HashBlock()) + fmt.Printf("Read block 4 hash match\n") +} + +func TestEmptyBlockOnlyHasCoinbaseTx(t *testing.T) { + defer os.Remove(testDBPath) + + config, err := config.LoadConfigWithPathWithoutValidation(testingConfigPath) + assert.Nil(t, err) + config.Chain.ChainDBPath = testDBPath + config.Chain.BlockReward = 7777 + + bc := CreateBlockchain(ta.Addrinfo["miner"].Address, config) + defer bc.Close() + assert.NotNil(t, bc) + + blk := bc.MintNewBlock([]*Tx{}, ta.Addrinfo["miner"].Address, "") + assert.Equal(t, uint32(1), blk.Height()) + assert.Equal(t, 1, len(blk.Tranxs)) + assert.True(t, blk.Tranxs[0].IsCoinbase()) + assert.Equal(t, uint32(1), blk.Tranxs[0].NumTxIn) + assert.Equal(t, uint32(1), blk.Tranxs[0].NumTxOut) + assert.Equal(t, uint64(7777), blk.Tranxs[0].TxOut[0].Value) +} diff --git a/blockchain/iblockchain.go b/blockchain/iblockchain.go new file mode 100644 index 0000000000..f9baeec5cc --- /dev/null +++ b/blockchain/iblockchain.go @@ -0,0 +1,53 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blockchain + +import ( + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/iotxaddress" +) + +// IBlockchain defines the interface of blockchain +type IBlockchain interface { + // Init initializes the blockchain + Init() error + // Close closes the Db connection + Close() error + // GetHeightByHash returns block's height by hash + GetHeightByHash(hash cp.Hash32B) (uint32, error) + // GetHashByHeight returns block's hash by height + GetHashByHeight(height uint32) (cp.Hash32B, error) + // GetBlockByHeight returns block from the blockchain hash by height + GetBlockByHeight(height uint32) (*Block, error) + // GetBlockByHash returns block from the blockchain hash by hash + GetBlockByHash(hash cp.Hash32B) (*Block, error) + // TipHash returns tip block's hash + TipHash() cp.Hash32B + // TipHeight returns tip block's height + TipHeight() uint32 + // Reset reset for next block + Reset() + // ValidateBlock validates a new block before adding it to the blockchain + ValidateBlock(blk *Block) error + // MintNewBlock creates a new block with given transactions. + // Note: the coinbase transaction will be added to the given transactions + // when minting a new block. + MintNewBlock([]*Tx, string, string) *Block + // AddBlockCommit adds a new block into blockchain + AddBlockCommit(blk *Block) error + // AddBlockSync adds a past block into blockchain + // used by block syncer when the chain in out-of-sync + AddBlockSync(blk *Block) error + // BalanceOf returns the balance of a given address + BalanceOf(string) uint64 + // UtxoPool returns the UTXO pool of current blockchain + UtxoPool() map[cp.Hash32B][]*TxOutput + // CreateTransaction creates a signed transaction paying 'amount' from 'from' to 'to' + CreateTransaction(from iotxaddress.Address, amount uint64, to []*Payee) *Tx + // CreateRawTransaction creates a signed transaction paying 'amount' from 'from' to 'to' + CreateRawTransaction(from iotxaddress.Address, amount uint64, to []*Payee) *Tx +} diff --git a/blockchain/transaction.go b/blockchain/transaction.go new file mode 100644 index 0000000000..67952c0a62 --- /dev/null +++ b/blockchain/transaction.go @@ -0,0 +1,271 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blockchain + +import ( + "bytes" + "crypto/rand" + "fmt" + + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + "golang.org/x/crypto/blake2b" + + cm "github.com/iotexproject/iotex-core/common" + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/proto" + "github.com/iotexproject/iotex-core/txvm" +) + +const ( + // TxOutputPb fields + + // ValueSizeInBytes defines the size of value in byte units + ValueSizeInBytes = 8 + // LockScriptSizeInBytes defines the size of lock script in byte units + LockScriptSizeInBytes = 4 + + // TxPb fields + + // VersionSizeInBytes defines the size of version in byte units + VersionSizeInBytes = 4 + //NumTxInSizeInBytes defines the size of number of transaction inputs in byte units + NumTxInSizeInBytes = 4 + //NumTxOutSizeInBytes defines the size of number of transaction outputs in byte units + NumTxOutSizeInBytes = 4 + //LockTimeSizeInBytes defines the size of lock time in byte units + LockTimeSizeInBytes = 4 +) + +// TxInput defines the transaction input protocol buffer +type TxInput = iproto.TxInputPb + +// TxOutput defines the transaction output protocol buffer +type TxOutput struct { + *iproto.TxOutputPb // embedded + + // below fields only used internally, not part of serialize/deserialize + outIndex int32 // outIndex is needed when spending UTXO +} + +// Tx defines the struct of transaction +// make sure the variable type and order of this struct is same as "type Tx" in blockchain.pb.go +type Tx struct { + Version uint32 + NumTxIn uint32 // number of transaction input + TxIn []*TxInput + NumTxOut uint32 // number of transaction output + TxOut []*TxOutput + LockTime uint32 // UTXO to be locked until this time +} + +// NewTxInput returns a TxInput instance +func NewTxInput(hash cp.Hash32B, index int32, unlock []byte, seq uint32) *TxInput { + return &TxInput{ + hash[:], + index, + uint32(len(unlock)), + unlock, + seq} +} + +// NewTxOutput returns a TxOutput instance +func NewTxOutput(amount uint64, index int32) *TxOutput { + return &TxOutput{ + &iproto.TxOutputPb{amount, 0, nil}, + index} +} + +// NewTx returns a Tx instance +func NewTx(version uint32, in []*TxInput, out []*TxOutput, lockTime uint32) *Tx { + return &Tx{ + version, + uint32(len(in)), + in, uint32(len(out)), + out, + lockTime} +} + +// Payee defines the struct of payee +type Payee struct { + Address string + Amount uint64 +} + +// NewPayee returns a Payee instance +func NewPayee(address string, amount uint64) *Payee { + return &Payee{address, amount} +} + +// NewCoinbaseTx creates the coinbase transaction - a special type of transaction that does not require previously outputs. +func NewCoinbaseTx(toaddr string, amount uint64, data string) *Tx { + if data == "" { + randData := make([]byte, 20) + _, err := rand.Read(randData) + if err != nil { + glog.Fatal(err) + } + + data = fmt.Sprintf("%x", randData) + } + + txin := NewTxInput(cp.ZeroHash32B, -1, []byte(data), 0xffffffff) + txout := CreateTxOutput(toaddr, amount) + return NewTx(1, []*TxInput{txin}, []*TxOutput{txout}, 0) +} + +// IsCoinbase checks if it is a coinbase transaction by checking if Vin is empty +func (tx *Tx) IsCoinbase() bool { + return len(tx.TxIn) == 1 && len(tx.TxOut) == 1 && tx.TxIn[0].OutIndex == -1 && tx.TxIn[0].Sequence == 0xffffffff && + bytes.Compare(tx.TxIn[0].TxHash[:], cp.ZeroHash32B[:]) == 0 +} + +// TotalSize returns the total size of this transaction +func (tx *Tx) TotalSize() uint32 { + size := uint32(VersionSizeInBytes + NumTxInSizeInBytes + NumTxOutSizeInBytes + LockTimeSizeInBytes) + + // add trnx input size + for _, in := range tx.TxIn { + size += in.TotalSize() + } + + // add trnx output size + for _, out := range tx.TxOut { + size += out.TotalSize() + } + return size +} + +// ByteStream returns a raw byte stream of trnx data +func (tx *Tx) ByteStream() []byte { + stream := make([]byte, 4) + cm.MachineEndian.PutUint32(stream, tx.Version) + + temp := make([]byte, 4) + cm.MachineEndian.PutUint32(temp, tx.NumTxIn) + stream = append(stream, temp...) + + // write all trnx input + for _, txIn := range tx.TxIn { + stream = append(stream, txIn.ByteStream()...) + } + + cm.MachineEndian.PutUint32(temp, tx.NumTxOut) + stream = append(stream, temp...) + + // write all trnx output + for _, txOut := range tx.TxOut { + stream = append(stream, txOut.ByteStream()...) + } + cm.MachineEndian.PutUint32(temp, tx.LockTime) + stream = append(stream, temp...) + + return stream +} + +// ConvertToTxPb creates a protobuf's Tx using type Tx +func (tx *Tx) ConvertToTxPb() *iproto.TxPb { + pbOut := make([]*iproto.TxOutputPb, len(tx.TxOut)) + for i, out := range tx.TxOut { + pbOut[i] = out.TxOutputPb + } + + return &iproto.TxPb{ + tx.Version, + tx.NumTxIn, + tx.TxIn, + tx.NumTxOut, + pbOut, + tx.LockTime} +} + +// Serialize returns a serialized byte stream for the Tx +func (tx *Tx) Serialize() ([]byte, error) { + return proto.Marshal(tx.ConvertToTxPb()) +} + +// ConvertFromTxPb converts a protobuf's Tx back to type Tx +func (tx *Tx) ConvertFromTxPb(pbTx *iproto.TxPb) { + // set trnx fields + tx.Version = pbTx.GetVersion() + tx.NumTxIn = pbTx.GetNumTxIn() + tx.NumTxOut = pbTx.GetNumTxOut() + tx.LockTime = pbTx.GetLockTime() + + tx.TxIn = nil + tx.TxIn = pbTx.TxIn + + tx.TxOut = nil + tx.TxOut = make([]*TxOutput, len(pbTx.TxOut)) + for i, out := range pbTx.TxOut { + tx.TxOut[i] = &TxOutput{out, int32(i)} + } +} + +// Deserialize parse the byte stream into the Tx +func (tx *Tx) Deserialize(buf []byte) error { + pbTx := iproto.TxPb{} + if err := proto.Unmarshal(buf, &pbTx); err != nil { + panic(err) + } + + tx.ConvertFromTxPb(&pbTx) + return nil +} + +// Hash returns the hash of the Tx +func (tx *Tx) Hash() cp.Hash32B { + hash := blake2b.Sum256(tx.ByteStream()) + return blake2b.Sum256(hash[:]) +} + +// +// below are transaction output functions +// + +// CreateTxOutput creates a new transaction output +func CreateTxOutput(toaddr string, value uint64) *TxOutput { + out := NewTxOutput(value, 0) + + locks, err := txvm.PayToAddrScript(toaddr) + if err != nil { + glog.Error(err) + return nil + } + out.LockScript = locks + out.LockScriptSize = uint32(len(out.LockScript)) + + return out +} + +// IsLockedWithKey checks if the UTXO in output is locked with script +func (out *TxOutput) IsLockedWithKey(lockScript []byte) bool { + if len(out.LockScript) < 23 { + glog.Error("LockScript too short") + return false + } + // TODO: avoid hard-coded extraction of public key hash + return bytes.Compare(out.LockScript[3:23], lockScript) == 0 +} + +// TotalSize returns the total size of transaction output +func (out *TxOutput) TotalSize() uint32 { + return ValueSizeInBytes + LockScriptSizeInBytes + uint32(out.LockScriptSize) +} + +// ByteStream returns a raw byte stream of transaction output +func (out *TxOutput) ByteStream() []byte { + stream := make([]byte, 8) + cm.MachineEndian.PutUint64(stream, out.Value) + + temp := make([]byte, 4) + cm.MachineEndian.PutUint32(temp, out.LockScriptSize) + stream = append(stream, temp...) + stream = append(stream, out.LockScript...) + + return stream +} diff --git a/blockchain/transaction_test.go b/blockchain/transaction_test.go new file mode 100644 index 0000000000..13af8b1017 --- /dev/null +++ b/blockchain/transaction_test.go @@ -0,0 +1,104 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blockchain + +import ( + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/blake2b" + + "github.com/iotexproject/iotex-core/iotxaddress" + ta "github.com/iotexproject/iotex-core/test/testaddress" +) + +func TestTransaction(t *testing.T) { + assert := assert.New(t) + + // create some transactions + cbtx := NewCoinbaseTx(ta.Addrinfo["miner"].Address, 50<<22, GenesisCoinbaseData) + hash := cbtx.Hash() + + if assert.True(cbtx.IsCoinbase()) { + /* + t.Logf("version = %x, numTxIn = %x, numTxOut = %x", cbtx.version, cbtx.numTxIn, cbtx.numTxOut ) + t.Logf("Vin.hash = %x", cbtx.vin[0].txHash ) + t.Logf("Vin.outIndex = %x, Vin.length = %d", cbtx.vin[0].outIndex, cbtx.vin[0].unlockScriptSize ) + t.Logf("Vin.script = %x", cbtx.vin[0].unlockScript ) + t.Logf("Vin.sequence = %x", cbtx.vin[0].sequence ) + t.Logf("numTxOut = %x", cbtx.numTxOut ) + t.Logf("Vout.value = %x, Vout.length = %d", cbtx.vout[0].Value, cbtx.vout[0].lockScriptSize) + t.Logf("Vout.script = %x", cbtx.vout[0].PubKeyHash) + */ + t.Log("Create coinbase pass") + } + + // verify byte stream size + assert.Equal(uint32(113), cbtx.TxIn[0].TotalSize()) + t.Log("TxIn size match pass") + + assert.Equal(uint32(37), cbtx.TxOut[0].TotalSize()) + t.Log("TxOut size match pass") + + assert.Equal(uint32(166), cbtx.TotalSize()) + t.Log("Tx size match pass") + + // verify coinbase transaction hash value + expected, _ := hex.DecodeString("90e0967d54b5f6f898c95404d0818f3f7a332ee6d5d7439666dd1e724771cb5e") + assert.Equal(expected, hash[:]) + t.Logf("Coinbase hash = %x match", hash) + + // serialize + data, err := cbtx.Serialize() + assert.Nil(err) + /* + t.Logf("Serialized = %x", data) + t.Logf("Version = %d, NumIn = %d, NumOut = %d, LockTime = %d", + cbtx.Version, cbtx.NumTxIn, cbtx.NumTxOut, cbtx.LockTime) + t.Logf("Vin.hash = %x", cbtx.TxIn[0].TxHash) + t.Logf("Vin.outIndex = %d, Vin.scriptSize = %d, Vin.sequence = %x", + cbtx.TxIn[0].OutIndex, cbtx.TxIn[0].UnlockScriptSize, cbtx.TxIn[0].Sequence) + t.Logf("Vin.script = %s", cbtx.TxIn[0].UnlockScript) + t.Logf("Vout.value = %d, Vin.scriptSize = %d", cbtx.TxOut[0].Value, cbtx.TxOut[0].LockScriptSize) + */ + t.Log("Marshaling Tx pass") + + // deserialize + newTx := Tx{} + err = newTx.Deserialize(data) + assert.Nil(err) + /* + t.Logf("Version = %d, NumIn = %d, NumOut = %d, LockTime = %d", + newTx.Version, newTx.NumTxIn, newTx.NumTxOut, newTx.LockTime) + t.Logf("Vin.hash = %x", newTx.TxIn[0].TxHash) + t.Logf("Vin.outIndex = %d, Vin.scriptSize = %d, Vin.sequence = %x", + newTx.TxIn[0].OutIndex, newTx.TxIn[0].UnlockScriptSize, newTx.TxIn[0].Sequence) + t.Logf("Vin.script = %s", newTx.TxIn[0].UnlockScript) + t.Logf("Vout.value = %d, Vin.scriptSize = %d", newTx.TxOut[0].Value, newTx.TxOut[0].LockScriptSize) + */ + t.Log("Unmarshaling Tx pass") + + // compare hash + newHash := blake2b.Sum256(newTx.ByteStream()) + newHash = blake2b.Sum256(newHash[:]) + + assert.Equal(hash[:], newHash[:]) + t.Logf("Serialize/Deserialize Tx hash = %x match\n", hash) + + // a/(b+c) + b/(a+c) + c/(a+b) = 4 + //var a float64 = 154476802108746166441951315019919837485664325669565431700026634898253202035277999 + //var b float64 = 36875131794129999827197811565225474825492979968971970996283137471637224634055579 + //var c float64 = 4373612677928697257861252602371390152816537558161613618621437993378423467772036 +} + +func TestIsLockedWithKey(t *testing.T) { + addr := ta.Addrinfo["miner"].Address + cbtx := NewCoinbaseTx(addr, 100, GenesisCoinbaseData) + assert.False(t, cbtx.TxOut[0].IsLockedWithKey([]byte("tooshort"))) + assert.True(t, cbtx.TxOut[0].IsLockedWithKey(iotxaddress.GetPubkeyHash(addr))) +} diff --git a/blockchain/utxo.go b/blockchain/utxo.go new file mode 100644 index 0000000000..ca82c3cd8f --- /dev/null +++ b/blockchain/utxo.go @@ -0,0 +1,233 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blockchain + +import ( + "fmt" + + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/iotxaddress" + "github.com/iotexproject/iotex-core/proto" + "github.com/iotexproject/iotex-core/txvm" +) + +// UtxoEntry contain TxOutput + hash and index +type UtxoEntry struct { + *iproto.TxOutputPb // embedded + + // below fields only used internally, not part of serialize/deserialize + txHash cp.Hash32B + outIndex int32 // outIndex is needed when spending UTXO +} + +// UtxoTracker tracks the active UTXO pool +type UtxoTracker struct { + currOutIndex int32 // newly created output index + utxoPool map[cp.Hash32B][]*TxOutput +} + +// NewUtxoTracker returns a UTXO tracker instance +func NewUtxoTracker() *UtxoTracker { + return &UtxoTracker{0, map[cp.Hash32B][]*TxOutput{}} +} + +// UtxoEntries returns list of UTXO entries containing >= requested amount, and +// return (nil, addr's total balance) if cannot reach reqamount +func (tk *UtxoTracker) UtxoEntries(address string, reqamount uint64) ([]*UtxoEntry, uint64) { + list := []*UtxoEntry{} + balance := uint64(0) + key := iotxaddress.GetPubkeyHash(address) + hasEnoughFund := false + +found: + for hash, txOut := range tk.utxoPool { + for _, out := range txOut { + if out.IsLockedWithKey(key) { + utxo := UtxoEntry{out.TxOutputPb, hash, out.outIndex} + list = append(list, &utxo) + balance += out.Value + + if reqamount <= balance { + balance -= reqamount + hasEnoughFund = true + break found + } + } + } + } + + if hasEnoughFund { + return list, balance + } + return nil, balance +} + +// CreateTxInputUtxo returns a UTXO transaction input +func (tk *UtxoTracker) CreateTxInputUtxo(hash cp.Hash32B, index int32, unlockScript []byte) *TxInput { + return NewTxInput(hash, index, unlockScript, 0) +} + +// CreateTxOutputUtxo creates transaction to spend UTXO +func (tk *UtxoTracker) CreateTxOutputUtxo(address string, amount uint64) *TxOutput { + out := NewTxOutput(amount, tk.currOutIndex) + locks, err := txvm.PayToAddrScript(address) + if err != nil { + return nil + } + out.LockScript = locks + + out.LockScriptSize = uint32(len(out.LockScript)) + // increment the index for new output + tk.currOutIndex++ + + return out +} + +// ValidateTxInputUtxo validates the UTXO in transaction input +// return amount of UTXO if pass, 0 otherwise +func (tk *UtxoTracker) ValidateTxInputUtxo(txIn *TxInput) uint64 { + hash := cp.ZeroHash32B + copy(hash[:], txIn.TxHash) + unspent, exist := tk.utxoPool[hash] + + // if hash does not exist in UTXO pool, it is spoof/fraudulent spending + if !exist { + fmt.Errorf("UTXO %x does not exist", hash) + return 0 + } + + // check transaction input, including unlock script can pass authentication + + for _, utxo := range unspent { + if utxo.outIndex == txIn.OutIndex && txIn.UnlockSuccess(utxo.LockScript) { + return utxo.Value + } + } + + return 0 +} + +// ValidateUtxo validates all UTXO in the block +func (tk *UtxoTracker) ValidateUtxo(blk *Block) error { + // iterate thru all transactions of this block + for _, tx := range blk.Tranxs { + txHash := tx.Hash() + + // coinbase has 1 output which becomes UTXO + if tx.IsCoinbase() { + continue + } + + credit := uint64(0) + for _, txIn := range tx.TxIn { + // verify UTXO before they can be spent + amount := tk.ValidateTxInputUtxo(txIn) + if amount == 0 { + return fmt.Errorf("Cannot validate UTXO %x", txIn.TxHash) + } + + // sum up all UTXO + credit += uint64(amount) + } + + debit := uint64(0) + for _, txOut := range tx.TxOut { + debit += uint64(txOut.Value) + } + + // make sure we have enough fund to spend + if credit < debit { + return fmt.Errorf("Tx %x does not have enough UTXO to spend", txHash) + } + } + + return nil +} + +// Reset reset the out index +func (tk *UtxoTracker) Reset() { + // reset output index + tk.currOutIndex = 0 +} + +// UpdateUtxoPool updates the UTXO pool according to transactions in the block +func (tk *UtxoTracker) UpdateUtxoPool(blk *Block) error { + // iterate thru all transactions of this block + for _, tx := range blk.Tranxs { + txHash := tx.Hash() + + // coinbase has 1 output which becomes UTXO + if tx.IsCoinbase() { + tk.utxoPool[txHash] = []*TxOutput{tx.TxOut[0]} + continue + } + + // add new TxOutput into pool + utxo := []*TxOutput{} + for _, txOut := range tx.TxOut { + utxo = append(utxo, txOut) + } + tk.utxoPool[txHash] = utxo + + // remove TxInput from pool + for _, txIn := range tx.TxIn { + hash := cp.ZeroHash32B + copy(hash[:], txIn.TxHash) + unspent, _ := tk.utxoPool[hash] + + if len(unspent) == 1 { + // this is the only UTXO so remove this entry + delete(tk.utxoPool, hash) + } else { + // remove this UTXO from the entry + newUnspent := []*TxOutput{} + for _, entry := range unspent { + if entry.outIndex != txIn.OutIndex { + newUnspent = append(newUnspent, entry) + } + } + tk.utxoPool[hash] = newUnspent + } + } + } + + return nil +} + +// ConvertToUtxoPb creates a protobuf's UTXO +func (tx *Tx) ConvertToUtxoPb() *iproto.UtxoMapPb { + return nil +} + +// Serialize returns a serialized byte stream for the Tx +func (tk *UtxoTracker) Serialize() ([]byte, error) { + return nil, nil +} + +// Deserialize parse the byte stream into the Tx +func (tk *UtxoTracker) Deserialize(buf []byte) error { + return nil +} + +// GetPool returns the UTXO pool +func (tk *UtxoTracker) GetPool() map[cp.Hash32B][]*TxOutput { + return tk.utxoPool +} + +// AddTx is called by TxPool to add a transaction +func (tk *UtxoTracker) AddTx(tx *Tx, height uint32) { + hash := tx.Hash() + outputs, exists := tk.utxoPool[hash] + if !exists { + outputs = []*TxOutput{} + } + for _, out := range tx.TxOut { + // check script lock + outputs = append(outputs, out) + } + tk.utxoPool[hash] = outputs +} diff --git a/blockchain/utxo_test.go b/blockchain/utxo_test.go new file mode 100644 index 0000000000..72a0bd7aae --- /dev/null +++ b/blockchain/utxo_test.go @@ -0,0 +1,154 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blockchain + +import ( + "fmt" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/iotexproject/iotex-core/config" + ta "github.com/iotexproject/iotex-core/test/testaddress" +) + +func TestUTXO(t *testing.T) { + defer os.Remove(testDBPath) + + // create chain + totalSupply := uint64(100000000) + bc := CreateBlockchain(ta.Addrinfo["miner"].Address, + &config.Config{Chain: config.Chain{ChainDBPath: testDBPath, TotalSupply: totalSupply}}) + assert.NotNil(t, bc) + fmt.Println("Create blockchain pass") + + defer bc.Close() + + assert.Equal(t, 0, int(bc.height)) + assert.Nil(t, addTestingBlocks(bc)) + + // check all UTXO + total := bc.BalanceOf(ta.Addrinfo["alfa"].Address) + fmt.Printf("Alfa balance = %d\n", total) + + beta := bc.BalanceOf(ta.Addrinfo["bravo"].Address) + fmt.Printf("Bravo balance = %d\n", beta) + total += beta + + beta = bc.BalanceOf(ta.Addrinfo["charlie"].Address) + fmt.Printf("Charlie balance = %d\n", beta) + total += beta + + beta = bc.BalanceOf(ta.Addrinfo["delta"].Address) + fmt.Printf("Delta balance = %d\n", beta) + total += beta + + beta = bc.BalanceOf(ta.Addrinfo["echo"].Address) + fmt.Printf("Echo balance = %d\n", beta) + total += beta + + beta = bc.BalanceOf(ta.Addrinfo["foxtrot"].Address) + fmt.Printf("Foxtrot balance = %d\n", beta) + total += beta + + beta = bc.BalanceOf(ta.Addrinfo["miner"].Address) + fmt.Printf("test balance = %d\n", beta) + utxo, _ := bc.Utk.UtxoEntries(ta.Addrinfo["miner"].Address, beta) + assert.NotNil(t, utxo) + total += beta + + assert.Equal(t, totalSupply, total) + fmt.Println("Total balance match") + + /* + pool := bc.UtxoEntries() + fmt.Print("UTXO pool") + for hash, out := range pool { + fmt.Printf("Hash = %x", hash) + for _, entry := range out { + fmt.Printf("Value = %d, Index = %d, script = %x", entry.Value, entry.outIndex, entry.LockScript) + } + } + */ + + /* + sum := float64(6) + count := 0 + solution := make(map[float64]bool) + + for y := float64(1); y < 10000; y++ { + for z := y; z < 10000; z++ { + ys := y*y + zs := z*z + yz := y*z + delta := math.Pow(ys - sum*yz, 2) - 4*yz*zs + + if _, frac := math.Modf(math.Sqrt(delta)); frac < 0.000000000001 { + x1 := (sum*yz - ys + math.Sqrt(delta)) / (2*z) + x2 := (sum*yz - ys - math.Sqrt(delta)) / (2*z) + + if _, frac := math.Modf(x1); x1 > 0 && frac < 0.000000000000001 { + if VerifySol(x1, y, z, &solution) { + count++ + fmt.Printf("Found root = %f %f %f", x1, y, z) + } + } + + if _, frac := math.Modf(x2); x2 > 0 && frac < 0.000000000000001 { + if VerifySol(x2, y, z, &solution) { + count++ + fmt.Printf("Found root = %f %f %f", x2, y, z) + } + } + } else { + delta := math.Pow(zs - sum*yz, 2) - 4*yz*ys + + if _, frac := math.Modf(math.Sqrt(delta)); frac < 0.000000000001 { + x1 := (sum*yz - zs + math.Sqrt(delta)) / (2*y) + x2 := (sum*yz - zs - math.Sqrt(delta)) / (2*y) + + if _, frac := math.Modf(x1); x1 > 0 && frac < 0.000000000000001 { + if VerifySol(x1, y, z, &solution) { + count++ + fmt.Printf("Found root = %f %f %f", x1, z, y) + } + } + + if _, frac := math.Modf(x2); x2 > 0 && frac < 0.000000000000001 { + if VerifySol(x2, y, z, &solution) { + count++ + fmt.Printf("Found root = %f %f %f", x2, z, y) + } + } + } + } + } + } + fmt.Printf("Has %d solutions\n", count) + */ +} + +func VerifySol(x, y, z float64, sol *map[float64]bool) bool { + + invar1 := y*z + x*y + x*z + invar2 := x * y * z + + if _, exist := (*sol)[invar1+invar2]; !exist { + (*sol)[invar1+invar2] = true + + // set multiple of (x,y,z) + for k := float64(2); k*z <= 10000; k++ { + invar := k * k * (invar1 + k*invar2) + (*sol)[invar] = true + } + + return true + } + + return false +} diff --git a/blockdb/blockdb.go b/blockdb/blockdb.go new file mode 100644 index 0000000000..f452c95deb --- /dev/null +++ b/blockdb/blockdb.go @@ -0,0 +1,248 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blockdb + +import ( + "io/ioutil" + "os" + + "github.com/boltdb/bolt" + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/config" + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/proto" +) + +// Directory of block data +const ( + BlockData = "../block.dat" +) + +var ( + tipHash = []byte("tip.hash") + tipHeight = []byte("tip.height") + utxoHeight = []byte("utxo.height") + + // bucket to store serialized block + blocksBucket = []byte("blocks") + + // bucket to store block height <-> hash + hashHeightBucket = []byte("hash<->height") +) + +var ( + // ErrNotExist indicates certain item does not exist in Blockchain database + ErrNotExist = errors.New("not exist in DB") + // ErrAlreadyExist indicates certain item already exists in Blockchain database + ErrAlreadyExist = errors.New("already exist in DB") +) + +// BlockDB defines the DB interface to read/store/persist blocks +type BlockDB struct { + *bolt.DB +} + +// NewBlockDB returns a new BlockDB instance +func NewBlockDB(cfg *config.Config) (*BlockDB, bool) { + exist := fileExists(cfg.Chain.ChainDBPath) + + // create/open database file + db, err := bolt.Open(cfg.Chain.ChainDBPath, 0600, nil) + if err != nil { + glog.Fatalf("Failed to open Blockchain Db, error = %v", err) + return nil, exist + } + + if !exist { + // create buckets + if err := db.Update(func(tx *bolt.Tx) error { + b, err := tx.CreateBucket(blocksBucket) + if err != nil { + return errors.Wrap(err, "Creating bucket for blocks") + } + + // set init value for tip hash and height + if err := b.Put(tipHash, cp.ZeroHash32B[:]); err != nil { + return errors.Wrap(err, "Writing init value for tipHash") + } + + zero := []byte{0, 0, 0, 0} + if err := b.Put(tipHeight, zero); err != nil { + return errors.Wrap(err, "Writing init value for tipHeight") + } + + if b, err = tx.CreateBucket(hashHeightBucket); err != nil { + return errors.Wrap(err, "Creating bucket for hash <-> height mapping") + } + return nil + }); err != nil { + glog.Fatal(err) + return nil, exist + } + } + return &BlockDB{db}, exist +} + +// Init initializes the BlockDB instance +func (db *BlockDB) Init() (hash []byte, height uint32, err error) { + // verify all buckets are properly created + // so from this point on later calls don't need to sanity check again + err = db.View(func(tx *bolt.Tx) error { + b := tx.Bucket(hashHeightBucket) + if b == nil { + return errors.Wrap(bolt.ErrBucketNotFound, "Bucket for hash <-> height mapping") + } + + b = tx.Bucket(blocksBucket) + if b == nil { + return errors.Wrap(bolt.ErrBucketNotFound, "Bucket for blocks") + } + + // get tip hash and height + if hash = b.Get(tipHash); hash == nil { + return errors.Wrap(ErrNotExist, "Blockchain tip") + } + + h := b.Get(tipHeight) + if h == nil { + return errors.Wrap(ErrNotExist, "Blockchain height") + } + height = cm.MachineEndian.Uint32(h) + return nil + }) + return +} + +// GetBlockHash returns the block hash by height +func (db *BlockDB) GetBlockHash(height uint32) (hash []byte, err error) { + err = db.View(func(tx *bolt.Tx) error { + b := tx.Bucket(hashHeightBucket) + // get block hash at passed-in height + dbHeight := []byte{0, 0, 0, 0} + cm.MachineEndian.PutUint32(dbHeight, height) + if hash = b.Get(dbHeight); hash == nil { + return errors.Wrapf(ErrNotExist, "Block with height = %d", height) + } + return nil + }) + return +} + +// GetBlockHeight returns the block height by hash +func (db *BlockDB) GetBlockHeight(hash []byte) (height uint32, err error) { + err = db.View(func(tx *bolt.Tx) error { + b := tx.Bucket(hashHeightBucket) + // get the height corresponding to passed in hash + var dbHeight []byte + if dbHeight = b.Get(hash); dbHeight == nil { + return errors.Wrapf(ErrNotExist, "Block with hash = %x", hash) + } + height = cm.MachineEndian.Uint32(dbHeight) + return nil + }) + return +} + +// CheckOutBlock checks a block out of DB +func (db *BlockDB) CheckOutBlock(hash []byte) (blk []byte, err error) { + err = db.View(func(tx *bolt.Tx) error { + b := tx.Bucket(blocksBucket) + + // get the block with passed-in hash + if blk = b.Get(hash); blk == nil { + return errors.Wrapf(ErrNotExist, "Block with hash = %x", hash) + } + return nil + }) + return +} + +// CheckInBlock checks a block into DB +func (db *BlockDB) CheckInBlock(blk []byte, hash []byte, h uint32) error { + return db.Update(func(tx *bolt.Tx) error { + // new block hash should not collide with any existing blocks + b := tx.Bucket(blocksBucket) + if collide := b.Get(hash); collide != nil { + return errors.Wrapf(ErrAlreadyExist, "New block hash %x", hash) + } + + // prepare tip height + height := []byte{0, 0, 0, 0} + cm.MachineEndian.PutUint32(height, h) + + // update tip hash/height + if err := b.Put(tipHash, hash); err != nil { + return errors.Wrapf(err, "Writing tipHash = %x", hash) + } + + if err := b.Put(tipHeight, height); err != nil { + return errors.Wrapf(err, "Writing tipHeight = %v", height) + } + + // commit the block data into Db + if err := b.Put(hash, blk); err != nil { + return errors.Wrapf(err, "Writing block = %x", hash) + } + + // update hash <-> height mapping + b = tx.Bucket(hashHeightBucket) + if err := b.Put(hash, height); err != nil { + return errors.Wrapf(err, "Updating hash <-> height mapping height = %v", height) + } + + if err := b.Put(height, hash); err != nil { + return errors.Wrapf(err, "Updating hash <-> height mapping hash = %x", hash) + } + return nil + }) +} + +// StoreBlockToFile writes block raw data into file +func (db *BlockDB) StoreBlockToFile(start, end uint32) error { + data := []byte{} + offset := []uint32{} + seek := uint32(0) + for height := start; height <= end; height++ { + hash, err := db.GetBlockHash(height) + if err != nil { + return err + } + bytes, err := db.CheckOutBlock(hash[:]) + if err != nil { + return err + } + offset = append(offset, seek) + seek += uint32(len(bytes)) + data = append(data, bytes...) + } + offset = append(offset, seek) + + // create block index + index, err := proto.Marshal(&iproto.BlockIndex{start, end, offset}) + if err != nil { + return err + } + + // first 4-byte of file is the size of block index + size := []byte{0, 0, 0, 0} + cm.MachineEndian.PutUint32(size, uint32(len(index))) + file := append(size, index...) + file = append(file, data...) + return ioutil.WriteFile(BlockData, file, 0600) +} + +// fileExists checks if a file already exists +func fileExists(path string) bool { + if _, err := os.Stat(path); os.IsNotExist(err) { + return false + } + return true +} diff --git a/blocksync/blocksync.go b/blocksync/blocksync.go new file mode 100644 index 0000000000..2d889ab1c2 --- /dev/null +++ b/blocksync/blocksync.go @@ -0,0 +1,337 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blocksync + +import ( + "fmt" + "sync" + "syscall" + "time" + + "github.com/golang/glog" + + bc "github.com/iotexproject/iotex-core/blockchain" + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/common/routine" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/delegate" + "github.com/iotexproject/iotex-core/network" + pb "github.com/iotexproject/iotex-core/proto" + "github.com/iotexproject/iotex-core/txpool" +) + +const ( + // Idle indicates an idle state + Idle = iota + // Init indicates the state when first block is received + Init = Idle + 1 + // Active indicates the state after first block has been processed + Active = Init + 1 +) + +// BlockSync defines the interface of blocksyncer +type BlockSync interface { + Start() error + Stop() error + P2P() *network.Overlay + ProcessSyncRequest(sender string, sync *pb.BlockSync) error + ProcessBlock(blk *bc.Block) error + ProcessBlockSync(blk *bc.Block) error +} + +// blockSyncer implements BlockSync interface +type blockSyncer struct { + mu sync.RWMutex + ackBlockCommit bool // acknowledges latest committed block + ackBlockSync bool // acknowledges old block from sync request + ackSyncReq bool // acknowledges incoming Sync request + state int + syncHeight uint32 // height of block at last sync request + dropHeight uint32 // height of most recent block being dropped + currRcvdHeight uint32 // height of most recent incoming block + lastRcvdHeight uint32 // height of last incoming block + rcvdBlocks map[uint32]*bc.Block // buffer of received blocks + actionTime time.Time + sw *SlidingWindow + bc *bc.Blockchain + tp txpool.TxPool + p2p *network.Overlay + task *routine.RecurringTask + fnd string + dp delegate.Pool +} + +// SyncTaskInterval returns the recurring sync task interval, or 0 if this config should not need to run sync task +func SyncTaskInterval(cfg *config.Config) time.Duration { + interval := time.Duration(0) + + if cfg.IsLightweight() { + return interval + } + + switch cfg.Consensus.Scheme { + case "RDPOS": + interval = cfg.Consensus.RDPoS.ProposerRotation.Interval + default: + interval = cfg.Consensus.BlockCreationInterval + } + + if cfg.IsFullnode() { + // fullnode has less stringent requirement of staying in sync so can check less frequently + interval <<= 2 + } + return interval +} + +// NewBlockSyncer returns a new block syncer instance +func NewBlockSyncer(cfg *config.Config, chain *bc.Blockchain, tp txpool.TxPool, p2p *network.Overlay, dp delegate.Pool) BlockSync { + sync := &blockSyncer{ + state: Idle, + rcvdBlocks: map[uint32]*bc.Block{}, + sw: NewSlidingWindow(), + bc: chain, + tp: tp, + p2p: p2p, + dp: dp} + + sync.ackBlockCommit = cfg.IsDelegate() || cfg.IsFullnode() + sync.ackBlockSync = cfg.IsDelegate() || cfg.IsFullnode() + sync.ackSyncReq = cfg.IsDelegate() || cfg.IsFullnode() + + if interval := SyncTaskInterval(cfg); interval != 0 { + sync.task = routine.NewRecurringTask(sync, interval) + } + + delegates, err := dp.AllDelegates() + if err != nil || len(delegates) == 0 { + if err != nil { + glog.Fatal(err) + } else { + glog.Fatal("No delegates found") + } + syscall.Exit(syscall.SYS_EXIT) + } + + switch cfg.NodeType { + case config.DelegateType: + // pick a delegate that is not myself + if dlg := dp.AnotherDelegate(p2p.PRC.Addr); dlg != nil { + sync.fnd = dlg.String() + } + case config.FullNodeType: + // pick any valid delegate + if dlg := dp.AnotherDelegate(""); dlg != nil { + sync.fnd = dlg.String() + } + default: + glog.Fatal("Unexpected node type ", cfg.NodeType) + return nil + } + return sync +} + +// P2P returns the network overlay object +func (bs *blockSyncer) P2P() *network.Overlay { + return bs.p2p +} + +// Start starts a block syncer +func (bs *blockSyncer) Start() error { + glog.Info("Starting block syncer") + if bs.task != nil { + bs.task.Init() + bs.task.Start() + } + return nil +} + +// Stop stops a block syncer +func (bs *blockSyncer) Stop() error { + glog.Infof("Stopping block syncer") + if bs.task != nil { + bs.task.Stop() + } + return nil +} + +// Do checks the sliding window and send more sync request if needed +func (bs *blockSyncer) Do() { + if bs.state == Idle { + // simple exit if we haven't received any blocks + return + } + + bs.mu.RLock() + defer bs.mu.RUnlock() + if bs.state == Init { + bs.processFirstBlock() + bs.state = Active + return + } + + // This handles the case where a sync takes long time. By the time the window is closing, enough new + // blocks are being dropped, so we check the window range and issue a new sync request + if bs.state == Active && bs.sw.State != Open && bs.syncHeight < bs.dropHeight { + bs.p2p.Tell(cm.NewTCPNode(bs.fnd), &pb.BlockSync{bs.syncHeight + 1, bs.dropHeight}) + glog.Warningf("++++++ [%s] Send start = %d end = %d to %s", bs.p2p.PRC.Addr, bs.syncHeight+1, bs.dropHeight, bs.fnd) + if bs.dropHeight-bs.syncHeight > WindowSize { + // trigger ProcessBlock() to drop incoming blocks, preventing too many blocks piling up in the buffer + bs.sw.Update(bs.dropHeight) + glog.Warningf("++++++ reopen window to [%d %d]", bs.syncHeight+1, bs.dropHeight) + } + bs.syncHeight = bs.dropHeight + return + } + + // health check if blocks keep coming in + if bs.lastRcvdHeight == bs.currRcvdHeight { + bs.state = Idle + glog.Warning(">>>>>> No longer receiving blocks. Last received block ", bs.lastRcvdHeight) + } + bs.lastRcvdHeight = bs.currRcvdHeight +} + +// ProcessSyncRequest processes a block sync request +func (bs *blockSyncer) ProcessSyncRequest(sender string, sync *pb.BlockSync) error { + if !bs.ackSyncReq { + // node is not meant to handle sync request, simply exit + return nil + } + + for i := sync.Start; i <= sync.End; i++ { + blk, err := bs.bc.GetBlockByHeight(i) + if err != nil { + return err + } + // TODO: send back multiple blocks in one shot + bs.p2p.Tell(cm.NewTCPNode(sender), &pb.BlockContainer{blk.ConvertToBlockPb()}) + //time.Sleep(time.Millisecond << 8) + } + return nil +} + +// processFirstBlock processes an incoming latest committed block +func (bs *blockSyncer) processFirstBlock() error { + if bs.syncHeight = bs.bc.TipHeight(); bs.currRcvdHeight > bs.syncHeight+1 { + glog.Warningf("++++++ [%s] Send first start = %d end = %d to %s", bs.p2p.PRC.Addr, bs.syncHeight+1, bs.currRcvdHeight, bs.fnd) + bs.p2p.Tell(cm.NewTCPNode(bs.fnd), &pb.BlockSync{bs.syncHeight + 1, bs.currRcvdHeight}) + } + if err := bs.sw.SetRange(bs.syncHeight, bs.currRcvdHeight); err != nil { + return err + } + bs.syncHeight = bs.currRcvdHeight + bs.actionTime = time.Now() + return nil +} + +// ProcessBlock processes an incoming latest committed block +func (bs *blockSyncer) ProcessBlock(blk *bc.Block) error { + if !bs.ackBlockCommit { + // node is not meant to handle latest committed block, simply exit + return nil + } + + bs.mu.Lock() + if bs.currRcvdHeight = blk.Height(); bs.currRcvdHeight <= bs.bc.TipHeight() { + err := fmt.Errorf("****** [%s] Received block height %d <= Blockchain tip height %d", bs.p2p.PRC.Addr, bs.currRcvdHeight, bs.bc.TipHeight()) + bs.mu.Unlock() + return err + } + + if bs.state == Idle && bs.currRcvdHeight == bs.bc.TipHeight()+1 { + // This is the special case where the first incoming block happens to be the next block following current + // Blockchain tip, so we call ProcessFirstBlock() and let it proceed to commit right away + // Otherwise waiting DO() to add it will cause thread context switch and incur extra latency, which usually + // leads to a duplicate Consensus round on the same block height, increasing chance of Consensus failure + if err := bs.processFirstBlock(); err != nil { + bs.mu.Unlock() + return err + } + bs.state = Active + glog.Warningf("====== receive tip block %d", bs.currRcvdHeight) + } + + if bs.state == Idle || bs.state == Init { + // indicate first valid block being received, DO() to handle sync request if needed + bs.state = Init + bs.mu.Unlock() + return nil + } + + if bs.state == Active && bs.sw.State == Open { + // when window is open we are still WIP to sync old blocks, so simply drop incoming blocks + bs.dropHeight = bs.currRcvdHeight + glog.Warningf("****** [%s] drop block %d", bs.p2p.PRC.Addr, bs.currRcvdHeight) + bs.mu.Unlock() + return nil + } + bs.mu.Unlock() + + // check-in incoming block to the buffer + if err := bs.checkBlockIntoBuffer(blk); err != nil { + glog.Warning(err) + return nil + } + + // commit all blocks in buffer that can be added to Blockchain + return bs.commitBlocksInBuffer() +} + +// ProcessBlockSync processes an incoming old block +func (bs *blockSyncer) ProcessBlockSync(blk *bc.Block) error { + if !bs.ackBlockSync { + // node is not meant to handle sync block, simply exit + return nil + } + + if blk.Height() <= bs.bc.TipHeight() { + glog.Warningf("****** [%s] Received block height %d <= Blockchain tip height %d", bs.p2p.PRC.Addr, blk.Height(), bs.bc.TipHeight()) + return nil + } + + // check-in incoming block to the buffer + bs.checkBlockIntoBuffer(blk) + + // commit all blocks in buffer that can be added to Blockchain + return bs.commitBlocksInBuffer() +} + +// checkBlockIntoBuffer adds a received blocks into the buffer +func (bs *blockSyncer) checkBlockIntoBuffer(blk *bc.Block) error { + height := blk.Height() + if bs.rcvdBlocks[height] != nil { + return fmt.Errorf("|||||| [%s] discard existing block %d", bs.p2p.PRC.Addr, height) + } + bs.rcvdBlocks[height] = blk + + glog.Warningf("------ [%s] receive block %d in %v", bs.p2p.PRC.Addr, height, time.Since(bs.actionTime)) + bs.actionTime = time.Now() + return nil +} + +// commitBlocksInBuffer commits all blocks in the buffer that can be added to Blockchain +func (bs *blockSyncer) commitBlocksInBuffer() error { + next := bs.bc.TipHeight() + 1 + for blk := bs.rcvdBlocks[next]; blk != nil; { + if err := bs.bc.AddBlockCommit(blk); err != nil { + return err + } + delete(bs.rcvdBlocks, next) + + // remove transactions in this block from TxPool + bs.tp.RemoveTxInBlock(blk) + + glog.Warningf("------ commit block %d time = %v\n\n", next, time.Since(bs.actionTime)) + bs.actionTime = time.Now() + + // update sliding window + bs.sw.Update(next) + next = bs.bc.TipHeight() + 1 + blk = bs.rcvdBlocks[next] + } + return nil +} diff --git a/blocksync/slidingwindow.go b/blocksync/slidingwindow.go new file mode 100644 index 0000000000..069c8d749a --- /dev/null +++ b/blocksync/slidingwindow.go @@ -0,0 +1,130 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package blocksync + +import ( + "sync" + "time" + + "github.com/golang/glog" + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-core/blockchain" +) + +const ( + // Open indicates an open state + Open = iota + // Closing indicates a closing state + Closing = Open + 1 + // Closed indicates a closed state + Closed = Closing + 1 +) + +// WindowSize defines the size of window +var WindowSize uint32 = 8 + +var ( + // ErrInvalidRange indicates invalid range. + ErrInvalidRange = errors.New("invalid open/close values") +) + +// +// A sliding window is a range [close, open] where close denotes the maximal value +// that exists, while open denotes the minimal value that does not exist yet +// +// It is used to track blocks received from p2p network during a block sync event +// For instance a sliding window of [100, 400] means blocks 101~400 are missing and +// need to be downloaded from peers. The sliding window manages the progress of such +// sync event, and exposes interface for BlockSyncer to use + +// SlidingWindow implements a sliding window +type SlidingWindow struct { + mu sync.RWMutex + start time.Time + end time.Time + State int + prevState int + close uint32 + open uint32 + orphan []int // orphaned item that has not been received yet + elapse []time.Duration // time (in millisecond) it takes to complete each sync task + bc *blockchain.Blockchain +} + +// NewSlidingWindow returns a SlidingWindow instance +func NewSlidingWindow() *SlidingWindow { + return &SlidingWindow{State: Open, prevState: Open} +} + +// SetRange set the initial range for sliding window +func (sw *SlidingWindow) SetRange(left uint32, right uint32) error { + sw.mu.Lock() + defer sw.mu.Unlock() + + if right <= left { + return ErrInvalidRange + } + sw.close = left + sw.open = right + sw.updateState() + // record the time the sync task starts + sw.start = time.Now() + return nil +} + +// Next returns the next close value of the sliding window +func (sw *SlidingWindow) Next() uint32 { + sw.mu.Lock() + defer sw.mu.Unlock() + return sw.close + 1 +} + +// updateState updates the sliding window state +func (sw *SlidingWindow) updateState() { + sw.prevState = sw.State + gap := sw.open - sw.close + switch { + case gap == 1: + sw.State = Closed + case gap < WindowSize: + sw.State = Closing + default: + sw.State = Open + } + glog.Infof("window = [%d %d], state = %d | %d", sw.close, sw.open, sw.prevState, sw.State) +} + +// Update updates the window [close, open] +func (sw *SlidingWindow) Update(value uint32) { + sw.mu.Lock() + switch { + case value > sw.open: + sw.open = value + case value == sw.close+1: + sw.close++ + if sw.close == sw.open { + sw.open++ + } + } + sw.updateState() + sw.mu.Unlock() +} + +// TurnClose returns true if State transitions Open --> Closing/Closed +func (sw *SlidingWindow) TurnClose() bool { + sw.mu.Lock() + defer sw.mu.Unlock() + return sw.prevState == Open && sw.State == Closing +} + +// TurnOpen returns true if State transitions Closing/Closed --> Open +func (sw *SlidingWindow) TurnOpen() bool { + sw.mu.Lock() + defer sw.mu.Unlock() + return sw.prevState != Open && sw.State == Open +} diff --git a/blocksync/slidingwindow_test.go b/blocksync/slidingwindow_test.go new file mode 100644 index 0000000000..0ecf592ebc --- /dev/null +++ b/blocksync/slidingwindow_test.go @@ -0,0 +1,71 @@ +package blocksync + +import ( + "github.com/stretchr/testify/assert" + "testing" +) + +func TestSlidingWindow(t *testing.T) { + assert := assert.New(t) + + sw := NewSlidingWindow() + assert.True(sw.State == Open) + + // close must be < open + err := sw.SetRange(12, 5) + assert.NotNil(err) + err = sw.SetRange(12, 12) + assert.NotNil(err) + close := uint32(1) + open := uint32(12) + err = sw.SetRange(close, open) + assert.Nil(err) + + // update with any value in [close, open] and != close+1 should have no effect + sw.Update(close + 2) + assert.Equal(close, sw.close) + assert.Equal(open, sw.open) + sw.Update(close) + assert.Equal(close, sw.close) + assert.Equal(open, sw.open) + sw.Update(open) + assert.Equal(close, sw.close) + assert.Equal(open, sw.open) + + // open - WindowSize + 1 is where the windows turns to closing + closing := open - WindowSize + 1 + for i := sw.close + 1; i <= closing; i++ { + sw.Update(i) + assert.Equal(i, sw.close) + } + assert.True(sw.State == Closing) + + // update with value < close should have no effect + sw.Update(sw.close - 1) + assert.Equal(closing, sw.close) + assert.Equal(open, sw.open) + assert.True(sw.State == Closing) + + // update with value > open should open the window more + open = sw.open + 1 + sw.Update(open) + assert.Equal(closing, sw.close) + assert.Equal(open, sw.open) + assert.True(sw.State == Open) + + // close the window + for i := closing + 1; i < sw.open; i++ { + sw.Update(i) + assert.Equal(i, sw.close) + } + assert.True(sw.State == Closed) + + // after window closes, each close increments both the open and close value + for i := 0; i < 5; i++ { + close = sw.close + 1 + sw.Update(close) + assert.Equal(close, sw.close) + assert.Equal(close+1, sw.open) + assert.True(sw.State == Closed) + } +} diff --git a/common/definition.go b/common/definition.go new file mode 100644 index 0000000000..ccaa309cf1 --- /dev/null +++ b/common/definition.go @@ -0,0 +1,12 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package common + +import "encoding/binary" + +// MachineEndian is the endianess of the machine +var MachineEndian = binary.LittleEndian diff --git a/common/idispatcher.go b/common/idispatcher.go new file mode 100644 index 0000000000..58942964d2 --- /dev/null +++ b/common/idispatcher.go @@ -0,0 +1,27 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package common + +import ( + "net" + + "github.com/golang/protobuf/proto" +) + +// Dispatcher is used by peers, handles incoming block and header notifications and relays announcements of new blocks. +type Dispatcher interface { + // Start starts a dispatcher + Start() error + // Stop stops a dispatcher + Stop() error + // HandleBroadcast handles the incoming broadcast message. The transportation layer semantics is at least once. + // That said, the handler is likely to receive duplicate messages. + HandleBroadcast(proto.Message, chan bool) + // HandleTell handles the incoming tell message. The transportation layer semantics is exact once. The sender is + // given for the sake of replying the message + HandleTell(net.Addr, proto.Message, chan bool) +} diff --git a/common/node.go b/common/node.go new file mode 100644 index 0000000000..fbd5d433f4 --- /dev/null +++ b/common/node.go @@ -0,0 +1,39 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package common + +// Node is the root struct to embed the newtork identifier +type Node struct { + NetworkType string + Addr string +} + +// Network returns the transportation layer type (tcp by default) +func (n *Node) Network() string { + if n.NetworkType == "" { + return "tcp" + } + return n.NetworkType +} + +// String returns the network address (127.0.0.1:0 by default) +func (n *Node) String() string { + if n.Addr == "" { + return "127.0.0.1:0" + } + return n.Addr +} + +// NewTCPNode creates an instance of Peer with tcp transportation +func NewTCPNode(addr string) *Node { + return NewNode("tcp", addr) +} + +// NewNode creates an instance of Peer +func NewNode(n string, addr string) *Node { + return &Node{NetworkType: n, Addr: addr} +} diff --git a/common/routine/recurringtask.go b/common/routine/recurringtask.go new file mode 100644 index 0000000000..963544aef0 --- /dev/null +++ b/common/routine/recurringtask.go @@ -0,0 +1,49 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package routine + +import ( + "time" + + "github.com/iotexproject/iotex-core/common/service" +) + +// IRecurringTaskHandler is the interface to implement the recurring task business logic +type IRecurringTaskHandler interface { + // Do is called on constant interval + Do() +} + +// RecurringTask represents a recurring task +type RecurringTask struct { + service.AbstractService + H IRecurringTaskHandler + Interval time.Duration + ticker *time.Ticker +} + +// NewRecurringTask creates an instance of RecurringTask +func NewRecurringTask(h IRecurringTaskHandler, i time.Duration) *RecurringTask { + return &RecurringTask{H: h, Interval: i} +} + +// Start starts the timer +func (t *RecurringTask) Start() error { + t.ticker = time.NewTicker(t.Interval) + go func() { + for range t.ticker.C { + t.H.Do() + } + }() + return nil +} + +// Stop stops the timer +func (t *RecurringTask) Stop() error { + t.ticker.Stop() + return nil +} diff --git a/common/routine/recurringtask_test.go b/common/routine/recurringtask_test.go new file mode 100644 index 0000000000..7f6fe80469 --- /dev/null +++ b/common/routine/recurringtask_test.go @@ -0,0 +1,35 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package routine + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +type MockHandler struct { + Count uint +} + +func (h *MockHandler) Do() { + h.Count++ +} + +func TestRecurringTask(t *testing.T) { + h := &MockHandler{Count: 0} + task := NewRecurringTask(h, 100*time.Millisecond) + task.Init() + task.Start() + defer func() { + task.Stop() + }() + + time.Sleep(600 * time.Millisecond) + assert.True(t, h.Count >= 5) +} diff --git a/common/routine/timeouttask.go b/common/routine/timeouttask.go new file mode 100644 index 0000000000..799980d1b3 --- /dev/null +++ b/common/routine/timeouttask.go @@ -0,0 +1,56 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package routine + +import ( + "time" + + "github.com/iotexproject/iotex-core/common/service" +) + +// ITimeoutTaskHandler is the interface to implement the timeout task business logic +type ITimeoutTaskHandler interface { + // Do is called on constant interval + Do() +} + +// TimeoutTask represents a timeout task +type TimeoutTask struct { + service.AbstractService + H ITimeoutTaskHandler + Duration time.Duration + ch chan interface{} +} + +// NewTimeoutTask creates an instance of TimeoutTask +func NewTimeoutTask(h ITimeoutTaskHandler, d time.Duration) *TimeoutTask { + return &TimeoutTask{ + H: h, + Duration: d, + ch: make(chan interface{}, 1), + } +} + +// Start starts the timeout +func (t *TimeoutTask) Start() error { + go func() { + select { + case <-t.ch: + return + case <-time.After(t.Duration): + t.H.Do() + } + }() + + return nil +} + +// Stop stops the timeout +func (t *TimeoutTask) Stop() error { + t.ch <- struct{}{} + return nil +} diff --git a/common/routine/timeouttask_test.go b/common/routine/timeouttask_test.go new file mode 100644 index 0000000000..58392fed89 --- /dev/null +++ b/common/routine/timeouttask_test.go @@ -0,0 +1,47 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package routine_test + +import ( + "testing" + "time" + + "github.com/iotexproject/iotex-core/common/routine" + "github.com/stretchr/testify/assert" +) + +type MockHandler struct { + Done bool +} + +func (h *MockHandler) Do() { + h.Done = true +} + +func TestTimeoutTaskTimeout(t *testing.T) { + h := &MockHandler{} + task := routine.NewTimeoutTask(h, 100*time.Millisecond) + task.Init() + task.Start() + defer func() { + task.Stop() + }() + + time.Sleep(600 * time.Millisecond) + assert.True(t, h.Done, "Do executed") +} + +func TestTimeoutTaskStop(t *testing.T) { + h := &MockHandler{} + task := routine.NewTimeoutTask(h, 100*time.Millisecond) + task.Init() + task.Start() + task.Stop() + + time.Sleep(600 * time.Millisecond) + assert.False(t, h.Done, "Do not executed because stopped") +} diff --git a/common/service/service.go b/common/service/service.go new file mode 100644 index 0000000000..d1c53cc103 --- /dev/null +++ b/common/service/service.go @@ -0,0 +1,71 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package service + +// Service defines the lifecycle of a service +type Service interface { + // Init initiates the service + Init() error + // Start starts the service + Start() error + // Stop stops the service + Stop() error +} + +// AbstractService implements an empty service +type AbstractService struct { +} + +// Init does nothing +func (AbstractService) Init() error { + return nil +} + +// Start does nothing +func (AbstractService) Start() error { + return nil +} + +// Stop does nothing +func (AbstractService) Stop() error { + return nil +} + +// CompositeService is a service that contains multiple child services +type CompositeService struct { + AbstractService + Services []Service +} + +// Init initiates all child services +func (s *CompositeService) Init() error { + for _, ss := range s.Services { + ss.Init() + } + return nil +} + +// Start starts all child services +func (s *CompositeService) Start() error { + for _, ss := range s.Services { + ss.Start() + } + return nil +} + +// Stop stops all child services +func (s *CompositeService) Stop() error { + for _, ss := range s.Services { + ss.Stop() + } + return nil +} + +// AddService adds another service as the child service of this one +func (s *CompositeService) AddService(ss Service) { + s.Services = append(s.Services, ss) +} diff --git a/common/utils/counter.go b/common/utils/counter.go new file mode 100644 index 0000000000..5d01fe91de --- /dev/null +++ b/common/utils/counter.go @@ -0,0 +1,91 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package utils + +import ( + "sync" + "time" +) + +// SlidingWindowCounter is used to count the number of events happened in the last X duration (in terms of a sliding +// window). Interval defines how big the time window is and SlotGranularity defines how fine grained the counter is. +type SlidingWindowCounter struct { + Interval time.Duration + SlotGranularity time.Duration + window []uint64 + count uint64 + headIdx int + lastUpdateTime time.Time + locker sync.Mutex +} + +// NewSlidingWindowCounter creates an instance of SlidingWindowCounter +func NewSlidingWindowCounter(i time.Duration, sg time.Duration) *SlidingWindowCounter { + c := &SlidingWindowCounter{Interval: i, SlotGranularity: sg} + c.window = make([]uint64, i/sg) + c.count = 0 + c.headIdx = 0 + c.lastUpdateTime = time.Now() + return c +} + +// NewSlidingWindowCounterWithSecondSlot creates an instance of SlidingWindowCounter with the second level slot +func NewSlidingWindowCounterWithSecondSlot(i time.Duration) *SlidingWindowCounter { + return NewSlidingWindowCounter(i, time.Second) +} + +// Increment increase the counter by 1. It's a blocking operation. +func (c *SlidingWindowCounter) Increment() { + c.locker.Lock() + defer c.locker.Unlock() + + c.refresh() + c.window[c.headIdx]++ + c.count++ +} + +// Count reads the current gauge. It's a blocking operation. +func (c *SlidingWindowCounter) Count() uint64 { + c.locker.Lock() + defer c.locker.Unlock() + + c.refresh() + return c.count +} + +func (c *SlidingWindowCounter) refresh() { + now := time.Now() + duration := int(now.Sub(c.lastUpdateTime) / c.SlotGranularity) + if duration >= len(c.window) { + for i := 0; i < len(c.window); i++ { + if i == 0 { + c.window[i] = 1 + } else { + c.window[i] = 0 + } + } + c.headIdx = 0 + c.count = 0 + + } else { + for i := 0; i < duration; i++ { + c.headIdx++ + if c.headIdx >= len(c.window) { + c.headIdx = 0 + } + c.count -= c.window[c.headIdx] + c.window[c.headIdx] = 0 + } + } + // Only change the lastUpdateTime when duration is greater than 0. That said, lastUpdateTime is updated only when + // the delta is greater than the slog granularity. This is to prevent keep updating the lastUpdateTime if incoming + // messages is so frequent that now - lastUpdateTime is always smaller than slog granularity, eventually always + // increasing the counter in the same slot. + if duration > 0 { + c.lastUpdateTime = now + } +} diff --git a/common/utils/counter_test.go b/common/utils/counter_test.go new file mode 100644 index 0000000000..becc9eed85 --- /dev/null +++ b/common/utils/counter_test.go @@ -0,0 +1,50 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package utils + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestSlidingWindowCounter1(t *testing.T) { + c := NewSlidingWindowCounter(time.Second, 100*time.Millisecond) + for i := 0; i < 10; i++ { + c.Increment() + } + assert.Equal(t, uint64(10), c.Count()) + assert.Equal(t, uint64(10), c.Count()) + time.Sleep(200 * time.Millisecond) + assert.Equal(t, uint64(10), c.Count()) + time.Sleep(1000 * time.Millisecond) + assert.Equal(t, uint64(0), c.Count()) +} + +func TestSlidingWindowCounter2(t *testing.T) { + c := NewSlidingWindowCounter(time.Second, 100*time.Millisecond) + for i := 0; i < 15; i++ { + time.Sleep(110 * time.Millisecond) + c.Increment() + } + assert.Equal(t, uint64(10), c.Count()) + for c.Count() > 0 { + time.Sleep(100 * time.Millisecond) + } +} + +func TestSlidingWindowCounter3(t *testing.T) { + c := NewSlidingWindowCounter(time.Second, 100*time.Millisecond) + for i := 0; i < 150; i++ { + time.Sleep(10 * time.Millisecond) + c.Increment() + } + for _, slot := range c.window { + assert.True(t, slot > 0) + } +} diff --git a/config.yaml b/config.yaml new file mode 100644 index 0000000000..1b98d206f6 --- /dev/null +++ b/config.yaml @@ -0,0 +1,54 @@ +# go-yaml expects the YAML field corresponding to a struct field to be lowercase. So if your struct field is +# UpdateInterval, the corresponding field in YAML is updateinterval. + + +nodetype: "full_node" # should be one of "delegate", "full_node", and "lightweight" + +network: + addr: "127.0.0.1:10000" + msglogscleaninginterval: 2s + msglogretention: 10s + healthcheckinterval: 1s + silentinterval: 5s + peermaintainerinterval: 1s + allowmulticonnsperip: false + numpeerslowerbound: 5 + numpeersupperbound: 5 + pinginterval: 1s + ratelimitenabled: false + ratelimitpersec: 5 + ratelimitwindowsize: 60s + bootstrapnodes: [] + tlsenabled: false + cacrtpath: "" + peercrtpath: "" + peerkeypath: "" + maxmsgsize: 10485760 + peerdiscovery: true + +chain: + chaindbpath: "./chain.db" + totalsupply: 10000000000 + blockreward: 5 + mineraddr: "io1qyqsyqcy6nm58gjd2wr035wz5eyd5uq47zyqpng3gxe7nh" + +consensus: + scheme: "NOOP" + rdpos: + proposerrotation: + enabled: true + interval: 3s + unmatchedeventttl: 3s + acceptpropose: + ttl: 1s + acceptprevote: + ttl: 1s + acceptvote: + ttl: 1s + blockcreationinterval: 1s + +delegate: + addrs: [] + +rpc: + port: ":42124" diff --git a/config/config.go b/config/config.go new file mode 100644 index 0000000000..cb2973d701 --- /dev/null +++ b/config/config.go @@ -0,0 +1,248 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package config + +import ( + "fmt" + "io/ioutil" + "time" + + "github.com/golang/glog" + "google.golang.org/grpc/keepalive" + "gopkg.in/yaml.v2" + + "github.com/iotexproject/iotex-core/iotxaddress" +) + +const ( + // DefaultConfigPath is the default config path + DefaultConfigPath = "./config.yaml" +) + +const ( + // DelegateType represents the delegate node type + DelegateType = "delegate" + // FullNodeType represents the full node type + FullNodeType = "full_node" + // LightweightType represents the lightweight type + LightweightType = "lightweight" +) + +// Network is the config struct for network package +type Network struct { + Addr string + MsgLogsCleaningInterval time.Duration + MsgLogRetention time.Duration + HealthCheckInterval time.Duration + SilentInterval time.Duration + PeerMaintainerInterval time.Duration + AllowMultiConnsPerIP bool + NumPeersLowerBound uint + NumPeersUpperBound uint + PingInterval time.Duration + RateLimitEnabled bool + RateLimitPerSec uint64 + RateLimitWindowSize time.Duration + BootstrapNodes []string + TLSEnabled bool + CACrtPath string + PeerCrtPath string + PeerKeyPath string + KLClientParams keepalive.ClientParameters + KLServerParams keepalive.ServerParameters + KLPolicy keepalive.EnforcementPolicy + MaxMsgSize int + PeerDiscovery bool + TopologyPath string +} + +// Chain is the config struct for blockchain package +type Chain struct { + ChainDBPath string + TotalSupply uint64 + BlockReward uint64 + + // MinerAddr is an address where the block rewards will be sent to. + MinerAddr string +} + +// Consensus is the config struct for consensus package +type Consensus struct { + // There are three schemes that are supported: + // RDPOS -- Randomized Delegated Proof of Stake + // STANDALONE -- The node creates a block periodically regardless of others (if there is any) + // NOOP -- The node does not create only block + Scheme string + RDPoS RDPoS + BlockCreationInterval time.Duration +} + +// RDPoS is the config struct for RDPoS consensus package +type RDPoS struct { + ProposerRotation ProposerRotation + UnmatchedEventTTL time.Duration + AcceptPropose AcceptPropose + AcceptPrevote AcceptPrevote + AcceptVote AcceptVote +} + +// ProposerRotation is the RDPoS ProposerRotation config +type ProposerRotation struct { + // Interval determines how long to propose another round of RDPoS. + Interval time.Duration + // Enabled flags whether we periodically rotate the proposer and trigger a new round of RDPoS + Enabled bool +} + +// AcceptPropose is the RDPoS AcceptPropose config +type AcceptPropose struct { + // TTL is the time the state machine will wait for the AcceptPropose state. + // Once timeout, it will move to the next state. + TTL time.Duration +} + +// AcceptPrevote is the RDPoS AcceptPrevote config +type AcceptPrevote struct { + // TTL is the time the state machine will wait for the AcceptPrevote state. + // Once timeout, it will move to the next state. + TTL time.Duration +} + +// AcceptVote is the RDPoS AcceptVote config +type AcceptVote struct { + // TTL is the time the state machine will wait for the AcceptVote state. + // Once timeout, it will move to the next state. + TTL time.Duration +} + +// Delegate is the delegate config +type Delegate struct { + Addrs []string +} + +// RPC is the chain service config +type RPC struct { + Port string +} + +// Config is the root config struct, each package's config should be put as its sub struct +type Config struct { + NodeType string + Network Network + Chain Chain + Consensus Consensus + Delegate Delegate + RPC RPC +} + +// IsDelegate returns true if the node type is Delegate +func (cfg *Config) IsDelegate() bool { + return cfg.NodeType == DelegateType +} + +// IsFullnode returns true if the node type is Fullnode +func (cfg *Config) IsFullnode() bool { + return cfg.NodeType == FullNodeType +} + +// IsLightweight returns true if the node type is Lightweight +func (cfg *Config) IsLightweight() bool { + return cfg.NodeType == LightweightType +} + +// LoadConfig loads the config instance from the default config path +func LoadConfig() (*Config, error) { + return LoadConfigWithPath(DefaultConfigPath) +} + +// LoadConfigWithPath loads the config instance and validates fields +func LoadConfigWithPath(path string) (*Config, error) { + return loadConfigWithPathInternal(path, true) +} + +// LoadConfigWithPathWithoutValidation loads the config instance but doesn't validate fields +func LoadConfigWithPathWithoutValidation(path string) (*Config, error) { + return loadConfigWithPathInternal(path, false) +} + +// loadConfigWithPathInternal loads the config instance. If validation is true, the function will check if the fields +// are valid or not. +func loadConfigWithPathInternal(path string, validate bool) (*Config, error) { + configBytes, err := ioutil.ReadFile(path) + if err != nil { + glog.Fatalf("Error when reading the config file: %v\n", err) + return nil, err + } + + config := Config{} + err = yaml.Unmarshal(configBytes, &config) + if err != nil { + glog.Fatalf("Error when decoding the config file: %v\n", err) + return nil, err + } + + if validate { + if err = validateConfig(&config); err != nil { + glog.Fatalf("Error when validating config: %v\n", err) + return nil, err + } + } + return &config, nil +} + +// validateConfig validates the given config +func validateConfig(cfg *Config) error { + // Validate miner's address + if len(cfg.Chain.MinerAddr) > 0 && !iotxaddress.ValidateAddress(cfg.Chain.MinerAddr) { + return fmt.Errorf("invalid miner's address") + } + + // Validate node type + switch cfg.NodeType { + case DelegateType: + break + case FullNodeType: + if cfg.Consensus.Scheme != "NOOP" { + return fmt.Errorf("consensus scheme of fullnode should be NOOP") + } + case LightweightType: + if cfg.Consensus.Scheme != "NOOP" { + return fmt.Errorf("consensus scheme of lightweight node should be NOOP") + } + default: + return fmt.Errorf("unknown node type %s", cfg.NodeType) + } + + if !cfg.Network.PeerDiscovery && cfg.Network.TopologyPath == "" { + return fmt.Errorf("either peer discover should be enabled or a topology should be given") + } + return nil +} + +// Topology is the neighbor list for each node. This is used for generating the P2P network in a given topology. Note +// that the list contains the outgoing connections. +type Topology struct { + NeighborList map[string][]string +} + +// LoadTopology loads the topology struct from the given yaml file +func LoadTopology(path string) (*Topology, error) { + topologyBytes, err := ioutil.ReadFile(path) + if err != nil { + glog.Fatalf("Error when reading the topology file: %v\n", err) + return nil, err + } + + topology := Topology{} + err = yaml.Unmarshal(topologyBytes, &topology) + if err != nil { + glog.Fatalf("Error when decoding the topology file: %v\n", err) + return nil, err + } + + return &topology, nil +} diff --git a/config/config_test.go b/config/config_test.go new file mode 100644 index 0000000000..ca47c5b308 --- /dev/null +++ b/config/config_test.go @@ -0,0 +1,150 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package config + +import ( + "io/ioutil" + "math/rand" + "os" + "strconv" + "testing" + "time" + + "github.com/stretchr/testify/assert" + "google.golang.org/grpc/keepalive" + "gopkg.in/yaml.v2" +) + +func TestLoadTestConfig(t *testing.T) { + config1 := LoadTestConfig() + configStr, err := yaml.Marshal(config1) + assert.Nil(t, err) + path := "/tmp/config_" + strconv.Itoa(rand.Int()) + ".yaml" + ioutil.WriteFile(path, configStr, 0666) + + defer func() { + if os.Remove(path) != nil { + assert.Fail(t, "Error when deleting the test file") + } + }() + + config2, err := LoadConfigWithPath(path) + assert.Nil(t, err) + assert.NotNil(t, config2) + assert.Equal(t, config1, config2) +} + +func TestLoadProdConfig(t *testing.T) { + config, err := LoadConfigWithPath("../config.yaml") + assert.Nil(t, err) + assert.NotNil(t, config) + assert.NotEmpty(t, config.Chain.ChainDBPath) + assert.NotEmpty(t, config.Chain.MinerAddr) +} + +func TestValidateConfig(t *testing.T) { + cfg := LoadTestConfig() + cfg.Chain.MinerAddr = "invalid_address" + err := validateConfig(cfg) + assert.NotNil(t, err) + assert.Equal(t, "invalid miner's address", err.Error()) + + cfg = LoadTestConfig() + cfg.Chain.MinerAddr = "" + cfg.NodeType = "invalid_type" + err = validateConfig(cfg) + assert.NotNil(t, err) + assert.Equal(t, "unknown node type invalid_type", err.Error()) + + cfg = LoadTestConfig() + cfg.Network.PeerDiscovery = false + err = validateConfig(cfg) + assert.NotNil(t, err) + assert.Equal(t, "either peer discover should be enabled or a topology should be given", err.Error()) + + cfg = LoadTestConfig() + cfg.NodeType = FullNodeType + cfg.Consensus.Scheme = "RDPOS" + err = validateConfig(cfg) + assert.NotNil(t, err) + assert.Equal(t, "consensus scheme of fullnode should be NOOP", err.Error()) + + cfg.NodeType = LightweightType + err = validateConfig(cfg) + assert.NotNil(t, err) + assert.Equal(t, "consensus scheme of lightweight node should be NOOP", err.Error()) +} + +func LoadTestConfig() *Config { + return &Config{ + NodeType: FullNodeType, + Network: Network{ + MsgLogsCleaningInterval: 2 * time.Second, + MsgLogRetention: 10 * time.Second, + HealthCheckInterval: time.Second, + SilentInterval: 5 * time.Second, + PeerMaintainerInterval: time.Second, + NumPeersLowerBound: 5, + NumPeersUpperBound: 5, + AllowMultiConnsPerIP: false, + PingInterval: time.Second, + RateLimitEnabled: true, + RateLimitPerSec: 5, + RateLimitWindowSize: 60 * time.Second, + BootstrapNodes: []string{}, + TLSEnabled: false, + CACrtPath: "", + PeerCrtPath: "", + PeerKeyPath: "", + KLClientParams: keepalive.ClientParameters{Time: 60 * time.Second}, + KLServerParams: keepalive.ServerParameters{Time: 60 * time.Second}, + KLPolicy: keepalive.EnforcementPolicy{MinTime: 30 * time.Second}, + MaxMsgSize: 1024 * 1024 * 10, + PeerDiscovery: true, + TopologyPath: "", + }, + Chain: Chain{ + ChainDBPath: "./a/fake/path", + }, + Consensus: Consensus{ + Scheme: "NOOP", + }, + Delegate: Delegate{ + Addrs: []string{"127.0.0.1:10001"}, + }, + } +} + +func TestLoadTestTopology(t *testing.T) { + topology1 := LoadTestTopology() + topologyStr, err := yaml.Marshal(topology1) + assert.Nil(t, err) + path := "/tmp/topology_" + strconv.Itoa(rand.Int()) + ".yaml" + ioutil.WriteFile(path, topologyStr, 0666) + + defer func() { + if os.Remove(path) != nil { + assert.Fail(t, "Error when deleting the test file") + } + }() + + topology2, err := LoadTopology(path) + assert.Nil(t, err) + assert.NotNil(t, topology2) + assert.Equal(t, topology1, topology2) +} + +func LoadTestTopology() *Topology { + return &Topology{ + NeighborList: map[string][]string{ + "127.0.0.1:10001": {"127.0.0.1:10002", "127.0.0.1:10003", "127.0.0.1:10004"}, + "127.0.0.1:10002": {"127.0.0.1:10001", "127.0.0.1:10003", "127.0.0.1:10004"}, + "127.0.0.1:10003": {"127.0.0.1:10001", "127.0.0.1:10002", "127.0.0.1:10004"}, + "127.0.0.1:10004": {"127.0.0.1:10001", "127.0.0.1:10002", "127.0.0.1:10003"}, + }, + } +} diff --git a/consensus/consensus.go b/consensus/consensus.go new file mode 100644 index 0000000000..c6bf6ba6e9 --- /dev/null +++ b/consensus/consensus.go @@ -0,0 +1,100 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package consensus + +import ( + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/blocksync" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/consensus/scheme" + "github.com/iotexproject/iotex-core/consensus/scheme/rdpos" + "github.com/iotexproject/iotex-core/delegate" + "github.com/iotexproject/iotex-core/txpool" +) + +// Consensus is the interface for handling consensus view change. +type Consensus interface { + Start() error + Stop() error + HandleViewChange(proto.Message, chan bool) error + HandleBlockPropose(proto.Message, chan bool) error +} + +type consensus struct { + cfg *config.Consensus + scheme scheme.Scheme +} + +// NewConsensus creates a consensus struct. +func NewConsensus(cfg *config.Config, bc blockchain.IBlockchain, tp txpool.TxPool, bs blocksync.BlockSync, dlg delegate.Pool) Consensus { + if bc == nil || bs == nil { + glog.Fatal("Try to attach to chain or bs == nil") + return nil + } + + cs := &consensus{cfg: &cfg.Consensus} + mintBlockCB := func() (*blockchain.Block, error) { + blk := bc.MintNewBlock(tp.Txs(), cfg.Chain.MinerAddr, "") + glog.Infof("created a new block at height %v with %v txs", blk.Height(), len(blk.Tranxs)) + return blk, nil + } + + tellBlockCB := func(msg proto.Message) error { + return bs.P2P().Broadcast(msg) + } + + commitBlockCB := func(blk *blockchain.Block) error { + return bs.ProcessBlock(blk) + } + + broadcastBlockCB := func(blk *blockchain.Block) error { + if blkPb := blk.ConvertToBlockPb(); blkPb != nil { + return bs.P2P().Broadcast(blkPb) + } + return nil + } + + switch cfg.Consensus.Scheme { + case "RDPOS": + cs.scheme = rdpos.NewRDPoS(cfg.Consensus.RDPoS, mintBlockCB, tellBlockCB, commitBlockCB, broadcastBlockCB, bc, bs.P2P().Self(), dlg) + case "NOOP": + cs.scheme = scheme.NewNoop() + case "STANDALONE": + cs.scheme = scheme.NewStandalone(mintBlockCB, commitBlockCB, broadcastBlockCB, bc, cfg.Consensus.BlockCreationInterval) + default: + glog.Fatal("unexpected consensus scheme", cfg.Consensus.Scheme) + } + + return cs +} + +func (c *consensus) Start() error { + glog.Infof("Starting consensus scheme %v", c.cfg.Scheme) + + c.scheme.Start() + return nil +} + +func (c *consensus) Stop() error { + glog.Infof("Stopping consensus scheme %v", c.cfg.Scheme) + + c.scheme.Stop() + return nil +} + +// HandleViewChange dispatches the call to different schemes +func (c *consensus) HandleViewChange(m proto.Message, done chan bool) error { + return c.scheme.Handle(m) +} + +// HandleBlockPropose handles a proposed block +func (c *consensus) HandleBlockPropose(m proto.Message, done chan bool) error { + return nil +} diff --git a/consensus/fsm/fsm.go b/consensus/fsm/fsm.go new file mode 100644 index 0000000000..a7e27d50d7 --- /dev/null +++ b/consensus/fsm/fsm.go @@ -0,0 +1,342 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package fsm + +import ( + "log" + "net" + "sync" + "time" + + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/common/routine" + cp "github.com/iotexproject/iotex-core/crypto" +) + +var ( + // ErrMachineNotInitialized is the error thatthe state machine is not initialized. + ErrMachineNotInitialized = errors.New("state machine is not initialized") + // ErrTransitionNotPermitted is the error that the state transition is not permitted. + ErrTransitionNotPermitted = errors.New("state transition is not permitted") + // ErrStateUndefined is the error that the state is undefined + ErrStateUndefined = errors.New("state is undefined") + // ErrStateHandlerNotMatched is the error that the current event is not matched to state handler + ErrStateHandlerNotMatched = errors.New("event is not matched to state handler") + // ErrNoTransitionApplied is the error that no state transition is applied + ErrNoTransitionApplied = errors.New("no state transition is applied") +) + +// State is the machine state name. +type State string + +// TransitionRuleMap is a set of map of transition destination states to rules. +type TransitionRuleMap map[State]Rule + +// Copy clones the TransitionRuleMap. +func (trs TransitionRuleMap) Copy() TransitionRuleMap { + clone := make(TransitionRuleMap) + + for rule, value := range trs { + clone[rule] = value + } + + return clone +} + +// Event is holding request event info across the handler and the rule. +type Event struct { + Err error + + State State + StateTimedOut bool + + Block *blockchain.Block + BlockHash *cp.Hash32B + + SenderAddr net.Addr + + ExpireAt *time.Time +} + +// Rule condition is evaluated when state handler is called. +type Rule interface { + Condition(event *Event) bool +} + +// Handler handles events for the state +// The difference between state handler and rule is that +// 1. rule dose not handle state event, it's just a transition from one state to another. +// 2. rule must succeed. even it has side-effects, no matter they fail or not, the transition is deterministic to +// the destination state. +type Handler interface { + Handle(event *Event) + + // TimeoutDuration returns the timeout of the state handler. + // If it returns a duration, after the duration and the state is still not + // If it returns nil, then no timeout task is set. + TimeoutDuration() *time.Duration +} + +// NilTimeout is the base handler struct that has no timeout +type NilTimeout struct{} + +// TimeoutDuration returns the duration for timeout +func (h *NilTimeout) TimeoutDuration() *time.Duration { + return nil +} + +// Machine is the state machine. +type Machine struct { + state State + initialState State + mu sync.RWMutex + toTask *routine.TimeoutTask + + transitions map[State]TransitionRuleMap + handlers map[State]Handler +} + +// NewMachine returns a state machine instance +func NewMachine() Machine { + sm := Machine{} + + sm.handlers = make(map[State]Handler) + sm.transitions = make(map[State]TransitionRuleMap) + + return sm +} + +// SetInitialState sets the initial state which not only handles request for itself +// but also accepts state types for all direct neighbor states +func (m *Machine) SetInitialState(state State, handler Handler) { + m.AddState(state, handler) + err := m.transitionAndSetupTimeout(state, &Event{State: "EMPTY"}) + if err != nil { + log.Fatal("failed to SetInitialState: cannot transit to initial state") + } + + m.mu.Lock() + defer m.mu.Unlock() + m.initialState = state +} + +// CurrentState returns the machine's current state. It returns "" when not initialized. +func (m *Machine) CurrentState() State { + m.mu.RLock() + defer m.mu.RUnlock() + + return m.state +} + +// transitionRules returns the allowed states to transit to and corresponding rules. +func (m *Machine) transitionRules(state State) (TransitionRuleMap, error) { + if m.transitions == nil { + return nil, errors.Wrap(ErrMachineNotInitialized, "the machine has not been fully initialized") + } + + if _, ok := m.transitions[state]; !ok { + return nil, errors.Wrapf(ErrStateUndefined, "state %s has not been registered", state) + } + + return m.transitions[state].Copy(), nil +} + +// AddTransition is a function for adding a valid state transition to the machine. +func (m *Machine) AddTransition(src State, dest State, rule Rule) error { + m.mu.Lock() + defer m.mu.Unlock() + + if m.transitions == nil { + m.transitions = make(map[State]TransitionRuleMap) + } + + if m.transitions[src] == nil { + m.transitions[src] = make(TransitionRuleMap) + } + m.transitions[src][dest] = rule + + if m.transitions[dest] == nil { + m.transitions[dest] = make(TransitionRuleMap) + } + + return nil +} + +// AddState add the state and the handler. +func (m *Machine) AddState(state State, handler Handler) { + m.mu.Lock() + defer m.mu.Unlock() + + m.handlers[state] = handler + if m.transitions == nil { + m.transitions = make(map[State]TransitionRuleMap) + } + if _, ok := m.transitions[state]; !ok { + m.transitions[state] = make(TransitionRuleMap) + } +} + +func (m *Machine) isExpectedState(event *Event) bool { + if event.State == m.state { + return true + } + + return m.tryMoveToStartingState(event) +} + +func (m *Machine) tryMoveToStartingState(event *Event) bool { + if m.state == m.initialState { + trm, err := m.transitionRules(m.initialState) + if err != nil { + return false + } + for dest, rule := range trm { + if dest != event.State { + continue + } + + // now the event is targeting a starting state. + if !rule.Condition(event) { + return false + } + + err := m.transitionAndSetupTimeout(dest, event) + if err != nil { + return false + } + h := m.handlers[m.initialState] + h.Handle(event) + return true + } + } + return false +} + +func (m *Machine) autoTransition(ctx *Event) error { + trm, err := m.transitionRules(ctx.State) + if err != nil { + return err + } + + // try each outgoing rule + for dest, rule := range trm { + matched := rule.Condition(ctx) + if matched { + err = m.transitionAndSetupTimeout(dest, ctx) + return err + } + } + + return errors.Wrap(ErrNoTransitionApplied, "cannot make auto transition") +} + +// HandleTransition tries to move to the next state by the request ctx. +func (m *Machine) HandleTransition(ctx *Event) error { + m.mu.Lock() + defer m.mu.Unlock() + + // not current state or (current state is initial but incoming state is not for neighbors) + if !m.isExpectedState(ctx) { + return errors.Wrap(ErrStateHandlerNotMatched, "the event dose not match the current state") + } + + handler, ok := m.handlers[ctx.State] + if !ok { + return errors.Wrapf(ErrStateUndefined, "the machine has no handler for state '%+v'", ctx.State) + } + + // state handler handles the input ctx + handler.Handle(ctx) + + // auto transition to next state + err := m.autoTransition(ctx) + // auto transition error + if err != nil { + return err + } + + return nil +} + +// transitionTo makes a transition to the destination state. +func (m *Machine) transitionTo(dest State) error { + if m.transitions == nil { + return errors.Wrap(ErrMachineNotInitialized, "the machine has no states added") + } + + if m.state == "" { + if _, ok := m.transitions[dest]; !ok { + return errors.Wrap(ErrStateUndefined, "the initial state has not been defined within the machine") + } + + m.state = dest + return nil + } + + if _, ok := m.transitions[m.state][dest]; !ok { + return errors.Wrapf(ErrTransitionNotPermitted, "transition from state %s dest %s is not permitted", m.state, dest) + } + + if _, ok := m.transitions[dest]; !ok { + return errors.Wrapf(ErrStateUndefined, "state %s has not been registered", dest) + } + + m.state = dest + + return nil +} + +func (m *Machine) transitionAndSetupTimeout(dest State, ctx *Event) error { + curState := m.state + err := m.transitionTo(dest) + if err != nil { + return err + } + + if dest != curState { + m.tryStopTimeout() + m.tryStartTimeout(dest) + } + return nil +} + +type task struct { + do func() +} + +func (t *task) Do() { + t.do() +} + +func (m *Machine) tryStartTimeout(state State) { + handler := m.handlers[state] + if handler.TimeoutDuration() == nil { + return + } + m.toTask = routine.NewTimeoutTask( + &task{ + do: func() { + ctx := &Event{ + StateTimedOut: true, + State: state, + } + m.autoTransition(ctx) + }, + }, + *handler.TimeoutDuration(), + ) + m.toTask.Start() +} + +func (m *Machine) tryStopTimeout() { + if m.toTask != nil { + m.toTask.Stop() + m.toTask = nil + } +} diff --git a/consensus/fsm/fsm_test.go b/consensus/fsm/fsm_test.go new file mode 100644 index 0000000000..103a82da09 --- /dev/null +++ b/consensus/fsm/fsm_test.go @@ -0,0 +1,236 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package fsm + +import ( + "testing" + "time" + + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" +) + +type RuleNoError struct{ NilTimeout } + +func (r RuleNoError) Condition(ctx *Event) bool { + return ctx.Err == nil +} + +type RuleHasError struct{ NilTimeout } + +func (r RuleHasError) Condition(ctx *Event) bool { + return ctx.Err != nil +} + +type EmptyHandler struct { + NilTimeout + hasError bool + handled bool +} + +func (h *EmptyHandler) Handle(ctx *Event) { + if h.hasError { + ctx.Err = errors.New("error") + } + h.handled = true +} + +// TestStateTransition123 tests the transition from 1 -> 2 -> 3 -> 1 +// | +// +-err-> 1 +func TestStateTransition123(t *testing.T) { + sm := NewMachine() + handle2 := &EmptyHandler{} + sm.AddTransition("1", "2", RuleNoError{}) + sm.AddTransition("2", "3", RuleNoError{}) + sm.AddTransition("2", "1", RuleHasError{}) + sm.AddTransition("3", "1", RuleNoError{}) + sm.AddState("1", &EmptyHandler{}) + sm.AddState("2", handle2) + sm.AddState("3", &EmptyHandler{}) + + err := sm.transitionTo(State("1")) // initial state + assert.NoError(t, err) + assert.Equal(t, "1", string(sm.CurrentState())) + + err = sm.HandleTransition(&Event{State: "1"}) + assert.NoError(t, err) + assert.Equal(t, "2", string(sm.CurrentState())) + + err = sm.HandleTransition(&Event{State: "2"}) + assert.NoError(t, err) + assert.Equal(t, "3", string(sm.CurrentState())) + + err = sm.HandleTransition(&Event{State: "3"}) + assert.NoError(t, err) + assert.Equal(t, "1", string(sm.CurrentState())) + + err = sm.HandleTransition(&Event{State: "1"}) + assert.NoError(t, err) + assert.Equal(t, "2", string(sm.CurrentState())) + + handle2.hasError = true + err = sm.HandleTransition(&Event{State: "2"}) + assert.NoError(t, err) + assert.Equal(t, "1", string(sm.CurrentState())) +} + +// 1 -> 2 -> 3 so 1 cannot move to 3 directly +func TestCannotSkipTransition(t *testing.T) { + sm := NewMachine() + sm.SetInitialState("1", &EmptyHandler{}) + sm.AddState("2", &EmptyHandler{}) + sm.AddState("3", &EmptyHandler{}) + sm.AddTransition("1", "2", RuleNoError{}) + sm.AddTransition("2", "3", RuleNoError{}) + + err := sm.HandleTransition(&Event{State: "3"}) + assert.Equal(t, ErrStateHandlerNotMatched, errors.Cause(err)) +} + +// initial -> 1 -> 3 +// | +// +----> 2 +// when in initial state, the state machine accepts 1 and 2 still. +func TestInitialStateAcceptsStatesForNeighbors(t *testing.T) { + sm := NewMachine() + start := &EmptyHandler{} + sm.SetInitialState("initial", start) + sm.AddState("1", &EmptyHandler{}) + sm.AddState("2", &EmptyHandler{}) + sm.AddState("3", &EmptyHandler{}) + sm.AddTransition("initial", "1", RuleNoError{}) + sm.AddTransition("initial", "2", RuleNoError{}) + sm.AddTransition("1", "3", RuleNoError{}) + + // cannot handle 3 directly from initial + assert.Equal(t, State("initial"), sm.CurrentState()) + err := sm.HandleTransition(&Event{State: "3"}) + assert.Error(t, err) + + // can handle 1 directly from initial + assert.Equal(t, State("initial"), sm.CurrentState()) + err = sm.HandleTransition(&Event{State: "1"}) + assert.True(t, start.handled, "start handler called") + assert.Equal(t, State("3"), sm.CurrentState()) + assert.NoError(t, err) +} + +// initial -NoError-> 1(hasError) -NoError-> 2 +// when in initial state, the state machine accepts 1. if 1 set error, stays in 1. +func TestInitialStateAcceptsNeighborAndStaysIfError(t *testing.T) { + sm := NewMachine() + sm.SetInitialState("initial", &EmptyHandler{}) + sm.AddState("1", &EmptyHandler{hasError: true}) + sm.AddState("2", &EmptyHandler{}) + sm.AddTransition("initial", "1", RuleNoError{}) + sm.AddTransition("1", "2", RuleNoError{}) + + // can handle 1 directly from initial, but stays in 1 because there is error + assert.Equal(t, State("initial"), sm.CurrentState()) + err := sm.HandleTransition(&Event{State: "1"}) + assert.Equal(t, State("1"), sm.CurrentState()) + assert.Equal(t, ErrNoTransitionApplied, errors.Cause(err)) +} + +// +-HasError-+ +// | | +// \|/ | +// initial -NoError-> 1(hasError) --NoError-> 2 +// when in initial state, the state machine accepts 1. if 1 set error, stays in 1. +func TestInitialStateAcceptsNeighborCircle(t *testing.T) { + sm := NewMachine() + sm.SetInitialState("initial", &EmptyHandler{}) + sm.AddState("1", &EmptyHandler{hasError: true}) + sm.AddState("2", &EmptyHandler{}) + sm.AddTransition("initial", "1", RuleNoError{}) + sm.AddTransition("1", "2", RuleNoError{}) + sm.AddTransition("1", "1", RuleHasError{}) + + // can handle 1 directly from initial, but stays in 1 because there is error + assert.Equal(t, State("initial"), sm.CurrentState()) + err := sm.HandleTransition(&Event{State: "1"}) + assert.Equal(t, State("1"), sm.CurrentState()) + assert.NoError(t, err) +} + +// TestStateTransitionToUnknownState +func TestStateTransitionToUnknownState(t *testing.T) { + sm := NewMachine() + err := sm.transitionTo(State("blah")) + assert.Equal(t, ErrStateUndefined, errors.Cause(err)) +} + +type TimeoutHandler struct { + stopped bool +} + +func (h *TimeoutHandler) TimeoutDuration() *time.Duration { + d := time.Duration(100 * time.Millisecond) + return &d +} + +func (h *TimeoutHandler) Handle(ctx *Event) { + ctx.Err = errors.New("error") +} + +type RuleTimeout struct{} + +func (r *RuleTimeout) Condition(ctx *Event) bool { + return ctx.StateTimedOut +} + +// initial -> 1 -> 2 +// when state 1 timeout, the state will move to 2 automatically +func TestTimeoutTriggersTransition(t *testing.T) { + sm := NewMachine() + sm.SetInitialState("initial", &EmptyHandler{}) + sm.AddState("1", &TimeoutHandler{}) + sm.AddState("2", &EmptyHandler{hasError: true}) + sm.AddTransition("initial", "1", RuleNoError{}) + sm.AddTransition("1", "2", &RuleTimeout{}) + + err := sm.HandleTransition(&Event{State: "1"}) + assert.Equal(t, ErrNoTransitionApplied, errors.Cause(err)) + assert.Equal(t, State("1"), sm.CurrentState()) + + time.Sleep(400 * time.Millisecond) + assert.Equal(t, State("2"), sm.CurrentState()) +} + +// initial -> 1 -> 2 +// when state 1 -> 2 happens, the state 1's timeout is stopped. +func TestTransitionStopsTimeout(t *testing.T) { + sm := NewMachine() + sm.SetInitialState("initial", &EmptyHandler{}) + sm.AddState("1", &TimeoutHandler{}) + sm.AddState("2", &EmptyHandler{}) + sm.AddTransition("initial", "1", RuleNoError{}) + sm.AddTransition("1", "2", &RuleHasError{}) + + err := sm.HandleTransition(&Event{State: "1"}) + assert.NoError(t, err) + assert.Equal(t, State("2"), sm.CurrentState()) +} + +// initial -> 1 -> 2 +// when state 1 timeout, the state will move to 2 automatically +func TestTimeoutStopsIfTransitionApplies(t *testing.T) { + sm := NewMachine() + sm.SetInitialState("initial", &EmptyHandler{}) + sm.AddState("1", &TimeoutHandler{}) + sm.AddState("2", &EmptyHandler{hasError: true}) + sm.AddTransition("initial", "1", RuleNoError{}) + sm.AddTransition("1", "2", &RuleTimeout{}) + + err := sm.HandleTransition(&Event{State: "1"}) + assert.Equal(t, ErrNoTransitionApplied, errors.Cause(err)) + assert.Equal(t, State("1"), sm.CurrentState()) + + time.Sleep(400 * time.Millisecond) + assert.Equal(t, State("2"), sm.CurrentState()) +} diff --git a/consensus/scheme/noop.go b/consensus/scheme/noop.go new file mode 100644 index 0000000000..edae3478fb --- /dev/null +++ b/consensus/scheme/noop.go @@ -0,0 +1,37 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package scheme + +import ( + "github.com/golang/glog" + "github.com/golang/protobuf/proto" +) + +// Noop is the consensus scheme that does NOT create blocks +type Noop struct { +} + +// NewNoop creates a Noop struct +func NewNoop() Scheme { + return &Noop{} +} + +// Start does nothing here +func (n *Noop) Start() error { + return nil +} + +// Stop does nothing here +func (n *Noop) Stop() error { + return nil +} + +// Handle handles incoming requests +func (n *Noop) Handle(message proto.Message) error { + glog.Warning("Noop scheme does not handle incoming requests") + return nil +} diff --git a/consensus/scheme/rdpos/evtconv.go b/consensus/scheme/rdpos/evtconv.go new file mode 100644 index 0000000000..caa5380e50 --- /dev/null +++ b/consensus/scheme/rdpos/evtconv.go @@ -0,0 +1,63 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-core/blockchain" + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/consensus/fsm" + cp "github.com/iotexproject/iotex-core/crypto" + pb "github.com/iotexproject/iotex-core/proto" +) + +func eventFromProto(m proto.Message) (*fsm.Event, error) { + event := &fsm.Event{} + vc, ok := (m).(*pb.ViewChangeMsg) + if !ok { + return event, errors.Wrapf(ErrInvalidViewChangeMsg, "message content is %+v", m.String()) + } + + event.State = fsm.State(vc.GetVctype().String()) + + event.SenderAddr = cm.NewTCPNode(vc.GetSenderAddr()) + + blkPb := vc.GetBlock() + if blkPb != nil { + blk := &blockchain.Block{} + blk.ConvertFromBlockPb(blkPb) + event.Block = blk + } + + if blkHashPb := vc.GetBlockHash(); blkHashPb != nil { + event.BlockHash = &cp.Hash32B{} + copy(event.BlockHash[:], blkHashPb) + } + return event, nil +} + +func protoFromEvent(event *fsm.Event) proto.Message { + msg := &pb.ViewChangeMsg{} + switch event.State { + case stateAcceptVote: + msg.Vctype = pb.ViewChangeMsg_VOTE + case stateAcceptPrevote: + msg.Vctype = pb.ViewChangeMsg_PREVOTE + case stateAcceptPropose: + msg.Vctype = pb.ViewChangeMsg_PROPOSE + } + if event.Block != nil { + msg.Block = event.Block.ConvertToBlockPb() + } + if event.BlockHash != nil { + msg.BlockHash = event.BlockHash[:] + } + msg.SenderAddr = event.SenderAddr.String() + return msg +} diff --git a/consensus/scheme/rdpos/fsm_create.go b/consensus/scheme/rdpos/fsm_create.go new file mode 100644 index 0000000000..846bce9303 --- /dev/null +++ b/consensus/scheme/rdpos/fsm_create.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import "github.com/iotexproject/iotex-core/consensus/fsm" + +const ( + stateStart fsm.State = "START" + stateInitPropose fsm.State = "INIT_PROPOSE" + stateAcceptPropose fsm.State = "PROPOSE" + stateAcceptPrevote fsm.State = "PREVOTE" + stateAcceptVote fsm.State = "VOTE" +) + +func fsmCreate(r *RDPoS) fsm.Machine { + sm := fsm.NewMachine() + + sm.SetInitialState(stateStart, &start{RDPoS: r}) + sm.AddState(stateInitPropose, &initPropose{RDPoS: r}) + sm.AddState(stateAcceptPropose, &acceptPropose{RDPoS: r}) + sm.AddState(stateAcceptPrevote, &acceptPrevote{RDPoS: r}) + sm.AddState(stateAcceptVote, &acceptVote{RDPoS: r}) + + sm.AddTransition(stateStart, stateInitPropose, &ruleIsProposer{RDPoS: r}) + sm.AddTransition(stateStart, stateAcceptPropose, &ruleNotProposer{RDPoS: r}) + sm.AddTransition(stateInitPropose, stateAcceptPrevote, &rulePropose{RDPoS: r}) + sm.AddTransition(stateAcceptPropose, stateAcceptPrevote, &rulePrevote{RDPoS: r}) + sm.AddTransition(stateAcceptPrevote, stateAcceptVote, &ruleVote{RDPoS: r}) + sm.AddTransition(stateAcceptVote, stateStart, &ruleCommit{RDPoS: r}) + + return sm +} diff --git a/consensus/scheme/rdpos/fsm_create_test.go b/consensus/scheme/rdpos/fsm_create_test.go new file mode 100644 index 0000000000..072d7dc5fe --- /dev/null +++ b/consensus/scheme/rdpos/fsm_create_test.go @@ -0,0 +1,138 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "net" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + + . "github.com/iotexproject/iotex-core/blockchain" + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/consensus/fsm" + cp "github.com/iotexproject/iotex-core/crypto" + ta "github.com/iotexproject/iotex-core/test/testaddress" +) + +func TestAcceptPrevoteAndProceedToEnd(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // arrange 2 consensus nodes + delegates := []net.Addr{cm.NewTCPNode("192.168.0.1:10001"), cm.NewTCPNode("192.168.0.2:10002")} + m := func(mcks mocks) { + mcks.dp.EXPECT().AllDelegates().Return(delegates, nil).AnyTimes() + mcks.dNet.EXPECT().Broadcast(gomock.Any()).AnyTimes() + mcks.bc.EXPECT().ValidateBlock(gomock.Any()).AnyTimes() + mcks.bc.EXPECT().AddBlockCommit(gomock.Any()).Times(1) + } + cs := createTestRDPoS(ctrl, delegates[0], delegates, m, false) + cs.Start() + defer cs.Stop() + + // arrange proposal request + cbtx := NewCoinbaseTx(ta.Addrinfo["miner"].Address, 888, GenesisCoinbaseData) + genesis := NewBlock(0, 0, cp.ZeroHash32B, []*Tx{cbtx}) + blkHash := genesis.HashBlock() + + // Accept PROPOSE and then prevote + event := &fsm.Event{ + State: stateAcceptPropose, + SenderAddr: delegates[1], + Block: genesis, + } + err := cs.fsm.HandleTransition(event) + assert.NoError(t, err, "accept PROPOSE no error") + assert.Equal(t, fsm.State("PREVOTE"), cs.fsm.CurrentState(), "current state PREVOTE") + assert.Equal(t, genesis, cs.roundCtx.block, "roundCtx.block set") + assert.Equal(t, &blkHash, cs.roundCtx.blockHash, "roundCtx.blockHash set") + + // Accept PREVOTE and then vote + event = &fsm.Event{ + State: stateAcceptPrevote, + SenderAddr: delegates[1], + BlockHash: &blkHash, + } + err = cs.fsm.HandleTransition(event) + assert.NoError(t, err, "accept PREVOTE no error") + assert.Equal(t, fsm.State("VOTE"), cs.fsm.CurrentState(), "current state VOTE") + assert.Equal( + t, + map[net.Addr]*cp.Hash32B{ + delegates[0]: &blkHash, + delegates[1]: &blkHash, + }, + cs.roundCtx.prevotes, + "roundCtx.prevote set", + ) + + // Accept VOTE and then commit + event = &fsm.Event{ + State: stateAcceptVote, + SenderAddr: delegates[1], + BlockHash: &blkHash, + } + err = cs.fsm.HandleTransition(event) + assert.NoError(t, err, "accept VOTE no error") + assert.Equal(t, fsm.State("START"), cs.fsm.CurrentState(), "current state START") + assert.Equal( + t, + map[net.Addr]*cp.Hash32B{ + delegates[0]: &blkHash, + delegates[1]: &blkHash, + }, + cs.roundCtx.votes, + "roundCtx.votes set", + ) +} + +func TestAcceptPrevoteAndTimeoutToEnd(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // arrange 2 consensus nodes + delegates := []net.Addr{ + cm.NewTCPNode("192.168.0.1:10001"), + cm.NewTCPNode("192.168.0.2:10002"), + } + m := func(mcks mocks) { + mcks.dp.EXPECT().AllDelegates().Return(delegates, nil).AnyTimes() + mcks.dNet.EXPECT().Broadcast(gomock.Any()).AnyTimes() + mcks.bc.EXPECT().ValidateBlock(gomock.Any()).Return(errors.New("error")) + mcks.bc.EXPECT().AddBlockCommit(gomock.Any()).Times(0) + } + cs := createTestRDPoS(ctrl, delegates[0], delegates, m, false) + cs.Start() + defer cs.Stop() + + // arrange proposal request + cbtx := NewCoinbaseTx(ta.Addrinfo["miner"].Address, 888, GenesisCoinbaseData) + genesis := NewBlock(0, 0, cp.ZeroHash32B, []*Tx{cbtx}) + + // Accept PROPOSE and then prevote + event := &fsm.Event{ + State: stateAcceptPropose, + SenderAddr: delegates[1], + Block: genesis, + } + err := cs.fsm.HandleTransition(event) + assert.NoError(t, err, "accept PROPOSE no error") + assert.Equal(t, fsm.State("PREVOTE"), cs.fsm.CurrentState(), "current state PREVOTE") + assert.Nil(t, cs.roundCtx.block, "roundCtx.block nil") + assert.Nil(t, cs.roundCtx.blockHash, "roundCtx.blockHash nil") + + time.Sleep(2 * time.Second) + assert.Equal(t, fsm.State("START"), cs.fsm.CurrentState(), "current state timeout back to START") +} diff --git a/consensus/scheme/rdpos/handler_accept_prevote.go b/consensus/scheme/rdpos/handler_accept_prevote.go new file mode 100644 index 0000000000..c82611e340 --- /dev/null +++ b/consensus/scheme/rdpos/handler_accept_prevote.go @@ -0,0 +1,26 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "time" + + "github.com/iotexproject/iotex-core/consensus/fsm" +) + +// acceptPrevote waits for 2k prevote messages for the same block from others or timeout. +type acceptPrevote struct { + *RDPoS +} + +func (h *acceptPrevote) TimeoutDuration() *time.Duration { + return &h.cfg.AcceptPrevote.TTL +} + +func (h *acceptPrevote) Handle(event *fsm.Event) { + h.roundCtx.prevotes[event.SenderAddr] = event.BlockHash +} diff --git a/consensus/scheme/rdpos/handler_accept_propose.go b/consensus/scheme/rdpos/handler_accept_propose.go new file mode 100644 index 0000000000..972f2a6ca1 --- /dev/null +++ b/consensus/scheme/rdpos/handler_accept_propose.go @@ -0,0 +1,27 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "time" + + "github.com/iotexproject/iotex-core/consensus/fsm" +) + +// acceptPropose waits for the proposed block and validate or timeout. +type acceptPropose struct { + *RDPoS +} + +func (h *acceptPropose) TimeoutDuration() *time.Duration { + return &h.cfg.AcceptPropose.TTL +} + +func (h *acceptPropose) Handle(event *fsm.Event) { + h.roundCtx.prevotes[event.SenderAddr] = event.BlockHash + event.Err = h.bc.ValidateBlock(event.Block) +} diff --git a/consensus/scheme/rdpos/handler_accept_vote.go b/consensus/scheme/rdpos/handler_accept_vote.go new file mode 100644 index 0000000000..cc8172167b --- /dev/null +++ b/consensus/scheme/rdpos/handler_accept_vote.go @@ -0,0 +1,26 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "time" + + "github.com/iotexproject/iotex-core/consensus/fsm" +) + +// acceptVote waits for 2k vote messages from others or timeout. +type acceptVote struct { + *RDPoS +} + +func (h *acceptVote) TimeoutDuration() *time.Duration { + return &h.cfg.AcceptVote.TTL +} + +func (h *acceptVote) Handle(event *fsm.Event) { + h.roundCtx.votes[event.SenderAddr] = event.BlockHash +} diff --git a/consensus/scheme/rdpos/handler_init_propose.go b/consensus/scheme/rdpos/handler_init_propose.go new file mode 100644 index 0000000000..3e8a34f3e4 --- /dev/null +++ b/consensus/scheme/rdpos/handler_init_propose.go @@ -0,0 +1,34 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "time" + + "github.com/iotexproject/iotex-core/consensus/fsm" +) + +// initPropose proposes a new block and send it out. +type initPropose struct { + fsm.NilTimeout + *RDPoS +} + +// TimeoutDuration returns the duration for timeout +func (h *initPropose) TimeoutDuration() *time.Duration { + return &h.cfg.AcceptPropose.TTL +} + +func (h *initPropose) Handle(event *fsm.Event) { + blk, err := h.propCb() + if err != nil { + event.Err = err + return + } + + h.roundCtx.block = blk +} diff --git a/consensus/scheme/rdpos/handler_init_propose_test.go b/consensus/scheme/rdpos/handler_init_propose_test.go new file mode 100644 index 0000000000..b1f72f515a --- /dev/null +++ b/consensus/scheme/rdpos/handler_init_propose_test.go @@ -0,0 +1,33 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/consensus/fsm" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestInitProposeInjectError(t *testing.T) { + t.Parallel() + + err := errors.New("error") + h := initPropose{ + RDPoS: &RDPoS{ + propCb: func() (*blockchain.Block, error) { + return nil, err + }, + }, + } + + evt := &fsm.Event{} + h.Handle(evt) + + assert.Equal(t, evt.Err, err) +} diff --git a/consensus/scheme/rdpos/handler_start.go b/consensus/scheme/rdpos/handler_start.go new file mode 100644 index 0000000000..8805611a9d --- /dev/null +++ b/consensus/scheme/rdpos/handler_start.go @@ -0,0 +1,28 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "net" + + "github.com/iotexproject/iotex-core/consensus/fsm" + cp "github.com/iotexproject/iotex-core/crypto" +) + +// start is the initial and idle state of all consensus states. It initiates +// the round context. +type start struct { + fsm.NilTimeout + *RDPoS +} + +func (h *start) Handle(event *fsm.Event) { + h.roundCtx = &roundCtx{ + prevotes: make(map[net.Addr]*cp.Hash32B), + votes: make(map[net.Addr]*cp.Hash32B), + } +} diff --git a/consensus/scheme/rdpos/proposer_rotation.go b/consensus/scheme/rdpos/proposer_rotation.go new file mode 100644 index 0000000000..0d3e5dbb46 --- /dev/null +++ b/consensus/scheme/rdpos/proposer_rotation.go @@ -0,0 +1,39 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "github.com/golang/glog" + + "github.com/iotexproject/iotex-core/common/routine" + "github.com/iotexproject/iotex-core/consensus/fsm" +) + +// proposerRotation is supposed to rotate the proposer per round of PBFT. +// However, use the first delegate as the proposer for now. +// We can propose based on the block height in the future. +type proposerRotation struct { + *RDPoS +} + +func (s *proposerRotation) Do() { + proposer := s.delegates[0] + s.proposer = proposer.String() == s.self.String() + if !s.proposer || s.fsm.CurrentState() != stateStart { + return + } + + glog.Warningf("[%s] propose a new block height %v", s.self, s.bc.TipHeight()+1) + s.fsm.HandleTransition(&fsm.Event{ + State: stateInitPropose, + }) +} + +// NewProposerRotation creates a recurring task of proposer rotation. +func NewProposerRotation(r *RDPoS) *routine.RecurringTask { + return routine.NewRecurringTask(&proposerRotation{r}, r.cfg.ProposerRotation.Interval) +} diff --git a/consensus/scheme/rdpos/proposer_rotation_test.go b/consensus/scheme/rdpos/proposer_rotation_test.go new file mode 100644 index 0000000000..b969a0f37c --- /dev/null +++ b/consensus/scheme/rdpos/proposer_rotation_test.go @@ -0,0 +1,49 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "net" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + bc "github.com/iotexproject/iotex-core/blockchain" + cm "github.com/iotexproject/iotex-core/common" + cp "github.com/iotexproject/iotex-core/crypto" + ta "github.com/iotexproject/iotex-core/test/testaddress" +) + +func TestProposerRotation(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // arrange 2 consensus nodes + delegates := []net.Addr{ + cm.NewTCPNode("192.168.0.1:10000"), + cm.NewTCPNode("192.168.0.1:10001"), + } + m := func(mcks mocks) { + mcks.dp.EXPECT().AllDelegates().Return(delegates, nil).AnyTimes() + mcks.dNet.EXPECT().Broadcast(gomock.Any()).AnyTimes() + mcks.bc.EXPECT().TipHeight().AnyTimes() + cbtx := bc.NewCoinbaseTx(ta.Addrinfo["miner"].Address, 888, bc.GenesisCoinbaseData) + genesis := bc.NewBlock(0, 0, cp.ZeroHash32B, []*bc.Tx{cbtx}) + mcks.bc.EXPECT().MintNewBlock(gomock.Any(), gomock.Any(), gomock.Any()).Return(genesis).AnyTimes() + } + cs := createTestRDPoS(ctrl, delegates[0], delegates, m, true) + cs.Start() + defer cs.Stop() + + time.Sleep(2 * time.Second) + + assert.NotNil(t, cs.roundCtx) +} diff --git a/consensus/scheme/rdpos/rdpos.go b/consensus/scheme/rdpos/rdpos.go new file mode 100644 index 0000000000..bd41f68b59 --- /dev/null +++ b/consensus/scheme/rdpos/rdpos.go @@ -0,0 +1,162 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "net" + "sync" + "syscall" + "time" + + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/common/routine" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/consensus/fsm" + "github.com/iotexproject/iotex-core/consensus/scheme" + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/delegate" + pb "github.com/iotexproject/iotex-core/proto" +) + +var ( + // ErrInvalidViewChangeMsg is the error that ViewChangeMsg is invalid + ErrInvalidViewChangeMsg = errors.New("ViewChangeMsg is invalid") +) + +// roundCtx keeps the context data for the current round and block. +type roundCtx struct { + block *blockchain.Block + blockHash *cp.Hash32B + prevotes map[net.Addr]*cp.Hash32B + votes map[net.Addr]*cp.Hash32B +} + +// DNet is the delegate networks interface. +type DNet interface { + Tell(node net.Addr, msg proto.Message) error + Self() net.Addr + Broadcast(msg proto.Message) error +} + +// RDPoS is the RDPoS consensus scheme +type RDPoS struct { + bc blockchain.IBlockchain + propCb scheme.CreateBlockCB + voteCb scheme.TellPeerCB + consCb scheme.ConsensusDoneCB + pubCb scheme.BroadcastCB + fsm fsm.Machine + roundCtx *roundCtx + self net.Addr + proposer bool // am i the proposer for this round or not + delegates []net.Addr + majNum int + wg sync.WaitGroup + quit chan struct{} + eventChan chan *fsm.Event + cfg config.RDPoS + pr *routine.RecurringTask +} + +// NewRDPoS creates a RDPoS struct +func NewRDPoS(cfg config.RDPoS, prop scheme.CreateBlockCB, vote scheme.TellPeerCB, cons scheme.ConsensusDoneCB, pub scheme.BroadcastCB, bc blockchain.IBlockchain, myaddr net.Addr, dlg delegate.Pool) *RDPoS { + delegates, err := dlg.AllDelegates() + if err != nil { + glog.Fatal(err.Error()) + syscall.Exit(syscall.SYS_EXIT) + } + sc := &RDPoS{ + propCb: prop, + voteCb: vote, + consCb: cons, + pubCb: pub, + bc: bc, + self: myaddr, + delegates: delegates, + majNum: len(delegates)*2/3 + 1, + quit: make(chan struct{}), + eventChan: make(chan *fsm.Event, 100), + cfg: cfg, + } + sc.pr = NewProposerRotation(sc) + sc.fsm = fsmCreate(sc) + return sc +} + +// Start initialize the RDPoS and start to consume requests from request channel. +func (n *RDPoS) Start() error { + glog.Info("Starting RDPoS") + n.wg.Add(1) + go n.consume() + if n.cfg.ProposerRotation.Enabled { + n.pr.Start() + } + return nil +} + +// Stop stops the RDPoS and stop consuming requests from request channel. +func (n *RDPoS) Stop() error { + glog.Infof("RDPoS is shutting down") + close(n.quit) + n.wg.Wait() + return nil +} + +// Handle handles incoming messages and publish to the channel. +func (n *RDPoS) Handle(m proto.Message) error { + glog.Info("RDPoS scheme handles incoming requests") + + event, err := eventFromProto(m) + if err != nil { + return err + } + + n.eventChan <- event + return nil +} + +func (n *RDPoS) consume() { +loop: + for { + select { + case r := <-n.eventChan: + err := n.fsm.HandleTransition(r) + if err == nil { + break + } + + fErr := errors.Cause(err) + switch fErr { + case fsm.ErrStateHandlerNotMatched: + if r.ExpireAt == nil { + expireAt := time.Now().Add(n.cfg.UnmatchedEventTTL) + r.ExpireAt = &expireAt + n.eventChan <- r + } else if time.Now().Before(*r.ExpireAt) { + n.eventChan <- r + } + case fsm.ErrNoTransitionApplied: + default: + glog.Errorf("%s failed to fsm.HandleTransition: %s", n.self, err) + } + case <-n.quit: + break loop + } + } + + n.wg.Done() + glog.Info("consume done") +} + +func (n *RDPoS) tellDelegates(msg *pb.ViewChangeMsg) { + msg.SenderAddr = n.self.String() + n.voteCb(msg) +} diff --git a/consensus/scheme/rdpos/rdpos_test.go b/consensus/scheme/rdpos/rdpos_test.go new file mode 100644 index 0000000000..26fedd88bd --- /dev/null +++ b/consensus/scheme/rdpos/rdpos_test.go @@ -0,0 +1,378 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "net" + "testing" + "time" + + "github.com/golang/glog" + "github.com/golang/mock/gomock" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + + "github.com/iotexproject/iotex-core/blockchain" + . "github.com/iotexproject/iotex-core/blockchain" + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/consensus/fsm" + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/proto" + "github.com/iotexproject/iotex-core/test/mock/mock_blockchain" + "github.com/iotexproject/iotex-core/test/mock/mock_delegate" + . "github.com/iotexproject/iotex-core/test/mock/mock_rdpos" + ta "github.com/iotexproject/iotex-core/test/testaddress" + "github.com/iotexproject/iotex-core/txpool" +) + +type mocks struct { + dNet *MockDNet + bc *mock_blockchain.MockIBlockchain + dp *mock_delegate.MockPool +} + +type mockFn func(mcks mocks) + +func createTestRDPoS(ctrl *gomock.Controller, self net.Addr, delegates []net.Addr, mockFn mockFn, enableProposerRotation bool) *RDPoS { + bc := mock_blockchain.NewMockIBlockchain(ctrl) + + tp := txpool.New(bc) + createblockCB := func() (*blockchain.Block, error) { + blk := bc.MintNewBlock(tp.Txs(), "", "") + glog.Infof("created a new block at height %v with %v txs", blk.Height(), len(blk.Tranxs)) + return blk, nil + } + commitBlockCB := func(blk *blockchain.Block) error { + return bc.AddBlockCommit(blk) + } + broadcastBlockCB := func(blk *blockchain.Block) error { + return nil + } + dp := mock_delegate.NewMockPool(ctrl) + dp.EXPECT().AllDelegates().Return(delegates, nil).AnyTimes() + dNet := NewMockDNet(ctrl) + dNet.EXPECT().Self().Return(self) + tellblockCB := func(msg proto.Message) error { + return dNet.Broadcast(msg) + } + csCfg := config.RDPoS{ + UnmatchedEventTTL: 300 * time.Millisecond, + AcceptPropose: config.AcceptPropose{ + TTL: 300 * time.Millisecond, + }, + AcceptPrevote: config.AcceptPrevote{ + TTL: 300 * time.Millisecond, + }, + AcceptVote: config.AcceptVote{ + TTL: 300 * time.Millisecond, + }, + } + if enableProposerRotation { + csCfg.ProposerRotation.Enabled = enableProposerRotation + csCfg.ProposerRotation.Interval = time.Second + } + mockFn(mocks{ + dNet: dNet, + bc: bc, + dp: dp, + }) + return NewRDPoS(csCfg, createblockCB, tellblockCB, commitBlockCB, broadcastBlockCB, bc, dNet.Self(), dp) +} + +type testCs struct { + cs *RDPoS + mocks mocks +} + +// 1 faulty node and 3 trusty nodes +func TestByzantineFault(t *testing.T) { + t.Parallel() + tests := []struct { + desc string + proposerNode int + }{ + { + desc: "faulty node(0) is a proposer", + proposerNode: 0, + }, + { + desc: "faulty node(0) is a validator", + proposerNode: 3, + }, + } + for _, tt := range tests { + t.Run(tt.desc, func(t *testing.T) { + testByzantineFault(t, tt.proposerNode) + }) + } +} + +// 1 faulty node and 3 trusty nodes +func testByzantineFault(t *testing.T, proposerNode int) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // arrange proposal request + cbtx := NewCoinbaseTx(ta.Addrinfo["miner"].Address, 888, GenesisCoinbaseData) + genesis := NewBlock(0, 0, cp.ZeroHash32B, []*Tx{cbtx}) + blkHash := genesis.HashBlock() + + // arrange 4 consensus nodes + tcss := make(map[net.Addr]testCs) + delegates := []net.Addr{ + cm.NewTCPNode("192.168.0.0:10000"), + cm.NewTCPNode("192.168.0.1:10001"), + cm.NewTCPNode("192.168.0.2:10002"), + cm.NewTCPNode("192.168.0.3:10003"), + } + for _, d := range delegates { + cur := d // watch out for the callback + + tcs := testCs{} + m := func(mcks mocks) { + mcks.dp.EXPECT().AllDelegates().Return(delegates, nil).AnyTimes() + mcks.dNet.EXPECT().Self().Return(cur).AnyTimes() + mcks.bc.EXPECT().MintNewBlock(gomock.Any(), gomock.Any(), gomock.Any()).Return(genesis).AnyTimes() + mcks.bc.EXPECT().ValidateBlock(gomock.Any()).Do(func(blk *Block) error { + if blk == nil { + return errors.New("invalid block") + } + return nil + }).AnyTimes() + + // ===================== + // expect AddBlockCommit + // ===================== + mcks.bc.EXPECT().AddBlockCommit(gomock.Any()).AnyTimes().Do(func(blk *Block) error { + if proposerNode == 0 { + // commit nil when proposer is faulty + assert.Nil(t, blk, "final block committed") + } else { + // commit block when proposer is trusty + assert.Equal(t, *genesis, *blk, "final block committed") + } + t.Log(cur, "AddBlockCommit: ", blk) + return nil + }) + tcs.mocks = mcks + } + tcs.cs = createTestRDPoS(ctrl, cur, delegates, m, false) + tcs.cs.Start() + defer tcs.cs.Stop() + tcss[cur] = tcs + } + + // arrange network call + for i, d := range delegates { + sender := d // watch out for the callback + if i == 0 { + // faulty node + tcss[sender].mocks.dNet.EXPECT().Broadcast(gomock.Any()).Do(func(msg proto.Message) error { + for ti, target := range delegates { + event, _ := eventFromProto(msg) + // I (node 0) send authentic msg to 1 but send reversed msg to 2 and 3 + if ti == 2 || ti == 3 { + if event.Block != nil { + event.Block = nil + } else { + event.Block = genesis + } + if event.BlockHash != nil { + event.BlockHash = nil + } else { + event.BlockHash = &blkHash + } + } + faultyMsg := protoFromEvent(event) + tcss[target].cs.Handle(faultyMsg) + } + return nil + }).AnyTimes() + } else { + // trusty nodes + tcss[sender].mocks.dNet.EXPECT().Broadcast(gomock.Any()).Do(func(msg proto.Message) error { + for _, target := range delegates { + tcss[target].cs.Handle(msg) + } + return nil + }).AnyTimes() + } + } + + // act + // trigger proposerNode as proposer + tcss[delegates[proposerNode]].cs.fsm.HandleTransition(&fsm.Event{ + State: stateInitPropose, + }) + + // assert + time.Sleep(time.Second) + for _, tcs := range tcss { + assert.Equal(t, fsm.State("START"), tcs.cs.fsm.CurrentState(), "back to START in the end") + } + voteStats(t, tcss) +} + +func voteStats(t *testing.T, tcss map[net.Addr]testCs) { + // prevote + for _, tcs := range tcss { + for i, v := range tcs.cs.roundCtx.prevotes { + t.Log(tcs.cs.self, "collect prevotes [", i, "]", v != nil) + } + } + + // votes + for _, tcs := range tcss { + for i, v := range tcs.cs.roundCtx.votes { + t.Log(tcs.cs.self, "collect votes [", i, "]", v != nil) + } + } +} + +func TestRDPoSFourTrustyNodes(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // arrange proposal request + cbtx := NewCoinbaseTx(ta.Addrinfo["miner"].Address, 888, GenesisCoinbaseData) + genesis := NewBlock(0, 0, cp.ZeroHash32B, []*Tx{cbtx}) + + // arrange 4 consensus nodes + tcss := make(map[net.Addr]testCs) + delegates := []net.Addr{ + cm.NewTCPNode("192.168.0.0:10000"), + cm.NewTCPNode("192.168.0.1:10001"), + cm.NewTCPNode("192.168.0.2:10002"), + cm.NewTCPNode("192.168.0.3:10003"), + } + for _, d := range delegates { + cur := d // watch out for the callback + + tcs := testCs{} + m := func(mcks mocks) { + mcks.dp.EXPECT().AllDelegates().Return(delegates, nil).AnyTimes() + mcks.dNet.EXPECT().Self().Return(cur).AnyTimes() + mcks.bc.EXPECT().MintNewBlock(gomock.Any(), gomock.Any(), gomock.Any()).Return(genesis).AnyTimes() + mcks.bc.EXPECT().ValidateBlock(gomock.Any()).AnyTimes() + + // ===================== + // expect AddBlockCommit + // ===================== + mcks.bc.EXPECT().AddBlockCommit(gomock.Any()).AnyTimes().Do(func(blk *Block) error { + t.Log(cur, "AddBlockCommit: ", blk) + assert.Equal(t, *genesis, *blk, "final block committed") + return nil + }) + tcs.mocks = mcks + } + tcs.cs = createTestRDPoS(ctrl, cur, delegates, m, false) + tcs.cs.Start() + defer tcs.cs.Stop() + tcss[cur] = tcs + } + + // arrange network call + for _, d := range delegates { + sender := d // watch out for the callback + // trusty nodes + tcss[sender].mocks.dNet.EXPECT().Broadcast(gomock.Any()).Do(func(msg proto.Message) error { + for _, target := range delegates { + tcss[target].cs.Handle(msg) + } + return nil + }).AnyTimes() + } + + // act + // trigger 2 as proposer + tcss[delegates[2]].cs.fsm.HandleTransition(&fsm.Event{ + State: stateInitPropose, + }) + + // assert + time.Sleep(time.Second) + for _, tcs := range tcss { + assert.Equal(t, fsm.State("START"), tcs.cs.fsm.CurrentState(), "back to START in the end") + } +} + +// Delegate0 receives PROPOSE from Delegate1 and hence move to PREVOTE state and timeout to other states and finally to start +func TestRDPoSConsumePROPOSE(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // arrange 2 consensus nodes + delegates := []net.Addr{ + cm.NewTCPNode("192.168.0.1:10001"), + cm.NewTCPNode("192.168.0.2:10002"), + } + m := func(mcks mocks) { + mcks.dp.EXPECT().AllDelegates().Return(delegates, nil).AnyTimes() + mcks.dNet.EXPECT().Broadcast(gomock.Any()).AnyTimes() + mcks.bc.EXPECT().ValidateBlock(gomock.Any()).AnyTimes() + mcks.bc.EXPECT().AddBlockCommit(gomock.Any()).Times(0) + } + cs := createTestRDPoS(ctrl, delegates[0], delegates, m, false) + cs.Start() + defer cs.Stop() + + // arrange proposal request + cbtx := NewCoinbaseTx(ta.Addrinfo["miner"].Address, 888, GenesisCoinbaseData) + genesis := NewBlock(0, 0, cp.ZeroHash32B, []*Tx{cbtx}) + proposal := &iproto.ViewChangeMsg{ + Vctype: iproto.ViewChangeMsg_PROPOSE, + Block: genesis.ConvertToBlockPb(), + SenderAddr: delegates[1].String(), + } + + // act + cs.Handle(proposal) + + // assert + time.Sleep(time.Second) + assert.Equal(t, fsm.State("START"), cs.fsm.CurrentState(), "Back to start because of timeout") + assert.Equal(t, genesis, cs.roundCtx.block) +} + +// Delegate0 receives unmatched VOTE from Delegate1 and stays in START +func TestRDPoSConsumeErrorStateHandlerNotMatched(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + // arrange 2 consensus nodes + delegates := []net.Addr{ + cm.NewTCPNode("192.168.0.1:10001"), + cm.NewTCPNode("192.168.0.2:10002"), + } + m := func(mcks mocks) {} + cs := createTestRDPoS(ctrl, delegates[0], delegates, m, false) + + cs.Start() + defer cs.Stop() + + // arrange unmatched VOTE proposal request + proposal := &iproto.ViewChangeMsg{ + Vctype: iproto.ViewChangeMsg_VOTE, + Block: nil, + SenderAddr: delegates[1].String(), + } + + // act + cs.Handle(proposal) + + // assert + time.Sleep(time.Second) + assert.Equal(t, fsm.State("START"), cs.fsm.CurrentState()) +} diff --git a/consensus/scheme/rdpos/rule_commit.go b/consensus/scheme/rdpos/rule_commit.go new file mode 100644 index 0000000000..9789697899 --- /dev/null +++ b/consensus/scheme/rdpos/rule_commit.go @@ -0,0 +1,54 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "github.com/golang/glog" + "github.com/iotexproject/iotex-core/consensus/fsm" +) + +// ruleCommit commits the block based on 2k + 1 vote messages (K + 1 yes or no),, +// or commits an empty block if timeout or error occurred. +type ruleCommit struct { + *RDPoS +} + +func (r ruleCommit) Condition(event *fsm.Event) bool { + if !event.StateTimedOut && event.Err == nil && !r.reachedMaj() { + return false + } + + // no consensus reached + if event.StateTimedOut || event.Err != nil || !r.reachedMaj() { + glog.Warningf("|||||| node %s no consensus agreed: state time out %+v, event error %+v, r.reachedMaj() %+v", r.self.String(), event.StateTimedOut, event.Err, r.reachedMaj()) + return true + } + + // consensus reached + // TODO: Can roundCtx.block be nil as well? nil may also be a valid consensus result + if r.roundCtx.block != nil { + r.consCb(r.roundCtx.block) + + // only proposer needs to broadcast the consensus block + if r.proposer { + glog.Warningf("|||||| node %s, brodcast block", r.self.String()) + r.pubCb(r.roundCtx.block) + } + } + return true +} + +func (r ruleCommit) reachedMaj() bool { + agreed := 0 + for _, blkHash := range r.roundCtx.votes { + if blkHash == nil && r.roundCtx.blockHash == nil || + (blkHash != nil && r.roundCtx.blockHash != nil && *r.roundCtx.blockHash == *blkHash) { + agreed++ + } + } + return agreed >= r.majNum +} diff --git a/consensus/scheme/rdpos/rule_is_proposer.go b/consensus/scheme/rdpos/rule_is_proposer.go new file mode 100644 index 0000000000..dfc674af12 --- /dev/null +++ b/consensus/scheme/rdpos/rule_is_proposer.go @@ -0,0 +1,18 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import "github.com/iotexproject/iotex-core/consensus/fsm" + +// ruleIsProposer checks if the event is init propose. +type ruleIsProposer struct { + *RDPoS +} + +func (r ruleIsProposer) Condition(event *fsm.Event) bool { + return event.State == stateInitPropose +} diff --git a/consensus/scheme/rdpos/rule_not_proposer.go b/consensus/scheme/rdpos/rule_not_proposer.go new file mode 100644 index 0000000000..4b9cef648f --- /dev/null +++ b/consensus/scheme/rdpos/rule_not_proposer.go @@ -0,0 +1,20 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "github.com/iotexproject/iotex-core/consensus/fsm" +) + +// ruleNotProposer checks if the event is not init propose. +type ruleNotProposer struct { + *RDPoS +} + +func (r ruleNotProposer) Condition(event *fsm.Event) bool { + return event.State != stateInitPropose +} diff --git a/consensus/scheme/rdpos/rule_prevote.go b/consensus/scheme/rdpos/rule_prevote.go new file mode 100644 index 0000000000..e077d311db --- /dev/null +++ b/consensus/scheme/rdpos/rule_prevote.go @@ -0,0 +1,52 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "github.com/iotexproject/iotex-core/consensus/fsm" + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/proto" +) + +// rulePrevote sends out prevote messages. +type rulePrevote struct { + *RDPoS +} + +func (r rulePrevote) Condition(event *fsm.Event) bool { + if event.StateTimedOut || event.Err != nil { + r.tellDelegates( + &iproto.ViewChangeMsg{ + Vctype: iproto.ViewChangeMsg_PREVOTE, + BlockHash: nil, + }, + ) + return true + } + + // set self prevoted + r.roundCtx.block = event.Block + var blkHash *cp.Hash32B + if event.Block != nil { + bkh := event.Block.HashBlock() + blkHash = &bkh + } else { + blkHash = nil + } + + r.roundCtx.prevotes[r.self] = blkHash + r.roundCtx.blockHash = blkHash + + msg := &iproto.ViewChangeMsg{ + Vctype: iproto.ViewChangeMsg_PREVOTE, + } + if blkHash != nil { + msg.BlockHash = blkHash[:] + } + r.tellDelegates(msg) + return true +} diff --git a/consensus/scheme/rdpos/rule_propose.go b/consensus/scheme/rdpos/rule_propose.go new file mode 100644 index 0000000000..4621cdf001 --- /dev/null +++ b/consensus/scheme/rdpos/rule_propose.go @@ -0,0 +1,47 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "github.com/iotexproject/iotex-core/consensus/fsm" + "github.com/iotexproject/iotex-core/proto" +) + +// rulePropose sends out propose messages. +type rulePropose struct { + *RDPoS +} + +func (r rulePropose) Condition(event *fsm.Event) bool { + if event.StateTimedOut || event.Err != nil || r.roundCtx.block == nil { + event.Block = nil + r.tellDelegates( + &iproto.ViewChangeMsg{ + Vctype: iproto.ViewChangeMsg_PROPOSE, + Block: nil, + }, + ) + return true + } + + bkh := r.roundCtx.block.HashBlock() + blkHash := &bkh + msg := &iproto.ViewChangeMsg{ + Vctype: iproto.ViewChangeMsg_PROPOSE, + Block: r.roundCtx.block.ConvertToBlockPb(), + BlockHash: blkHash[:], + } + + // tell other validators of the block + r.tellDelegates(msg) + + // set self prevoted + r.roundCtx.prevotes[r.self] = blkHash + r.roundCtx.blockHash = blkHash + + return true +} diff --git a/consensus/scheme/rdpos/rule_propose_test.go b/consensus/scheme/rdpos/rule_propose_test.go new file mode 100644 index 0000000000..db1234b846 --- /dev/null +++ b/consensus/scheme/rdpos/rule_propose_test.go @@ -0,0 +1,35 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "github.com/golang/protobuf/proto" + "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/consensus/fsm" + "github.com/iotexproject/iotex-core/proto" + "github.com/pkg/errors" + "github.com/stretchr/testify/assert" + "testing" +) + +func TestRuleProposeErrorVoteNil(t *testing.T) { + t.Parallel() + + h := rulePropose{ + RDPoS: &RDPoS{ + self: common.NewTCPNode(""), + voteCb: func(msg proto.Message) error { + vc, ok := (msg).(*iproto.ViewChangeMsg) + assert.True(t, ok) + assert.Nil(t, vc.Block) + assert.Equal(t, vc.Vctype, iproto.ViewChangeMsg_PROPOSE) + return nil + }, + }, + } + assert.True(t, h.Condition(&fsm.Event{Err: errors.New("err")})) +} diff --git a/consensus/scheme/rdpos/rule_vote.go b/consensus/scheme/rdpos/rule_vote.go new file mode 100644 index 0000000000..21489a7373 --- /dev/null +++ b/consensus/scheme/rdpos/rule_vote.go @@ -0,0 +1,57 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rdpos + +import ( + "github.com/iotexproject/iotex-core/consensus/fsm" + "github.com/iotexproject/iotex-core/proto" +) + +// ruleVote prevotes based on 2k + 1 prevote messages, or vote for an empty block if +// timeout or error occurred. +type ruleVote struct { + *RDPoS +} + +func (r ruleVote) Condition(event *fsm.Event) bool { + if !event.StateTimedOut && event.Err == nil && !r.reachedMaj() { + return false + } + + if event.StateTimedOut || event.Err != nil || !r.reachedMaj() { + r.tellDelegates( + &iproto.ViewChangeMsg{ + Vctype: iproto.ViewChangeMsg_VOTE, + BlockHash: nil, + }, + ) + return true + } + + // set self voted + r.roundCtx.votes[r.self] = r.roundCtx.blockHash + + msg := &iproto.ViewChangeMsg{ + Vctype: iproto.ViewChangeMsg_VOTE, + } + if r.roundCtx.blockHash != nil { + msg.BlockHash = r.roundCtx.blockHash[:] + } + r.tellDelegates(msg) + return true +} + +func (r ruleVote) reachedMaj() bool { + agreed := 0 + for _, blkHash := range r.roundCtx.prevotes { + if blkHash == nil && r.roundCtx.blockHash == nil || + (blkHash != nil && r.roundCtx.blockHash != nil && *r.roundCtx.blockHash == *blkHash) { + agreed++ + } + } + return agreed >= r.majNum +} diff --git a/consensus/scheme/rdpos/state_machine.png b/consensus/scheme/rdpos/state_machine.png new file mode 100644 index 0000000000000000000000000000000000000000..66901d64b6b3b2306b5248e6f0b642b89042b6e5 GIT binary patch literal 43877 zcmeFZbx@tn(E4NAP|PMl(;eoga`qF;BS$E4p0I; zIy~^_xv7Y}2nbXag?4X%0JJF$rIh7CAU7Hi$R_{eelo_(cv)`$m%4%&%$rKe!h!A_+bm~3p%NYm@5*o z?=)r-=VWO7+vvVKs=VT|rIzBtk-LqwKP6>o@0GUPTy;iG08$%aMnwQ^(1xxsASK8H z9~ldvR`Co92Li#=D4!T^M-4y%fjU3>!hps#JvKS;M-QPJ(8ET_|M!7>u%dkvr5@Tq z*UW=w8}PYYTc*^Pzn^WiP>%RxMK48XM$E3G`JDJMVhIYHi+OP=d$WMr(W=4MAlCdl z5l!DaUtk0jzV8*SzTx!*12Sx;$i;7@c*541^b5%h;TO{Xo2Lu&8S>RRRe`~#9#ZMZ z{Jt=szaj~<8xA{kWWKAJ<_NZ%Xv>kxR>LN@6^h>OLXt9;v&iG&WvIJwy#11VD!_dn zo)^p!fV6^T37JfXw(h`cef8~s- zhzW(;;Qy=W!K7hdvfl%$Ca!OgAPMz~Oh?ya3l8J%+({hBY-1`TElL*}>Zmun0@J8I z0e>MBAjE$Xf0-IO7yI7lxOj&wX5drlJ?=Qm_%%vv!bM~M%M1I+*)<`4#<-&E2%@|C zAOm319ytGusqlW!3cVdhRBA4-&ohf{`21@tw?R2wN4!Ayk+x;Y+_4T0_U2!{kvNl^ zBN8@Q`J1vPjw;5vtc)xQCOz%JJzLK}?eI_QTNGZjFUvr9;yKcX5~W|tK7d*?D2b|A zfh7U~UB7&?hSwbVeoZ22)tpN`T=^=?hNDra|26h5pW5_?l|uP4dkjqO^Y@Ex!#Tum z_|6951SAT|DmF5Nk4%}`5m(qyB9a0b1;`OEuu<2aX^?yPz!T0Cee)MMOC(54Xv7YL zfuhl$sZU_c8Zac;kr$Maw{-=t^y?LY6)0WX7C>-UhzoyTY zAG^`j&z~!YOy$1c4~b@GNh zCjSo`82zXv^77!5^Y`S7#? zpngXoDTaT{--m5=BC?iviG2u{8YK996#g27o$+uVr!J;q6!N3~8SIrlc_)$&>}c0a z(`*jdPhoK9AVE-osZi96%sVe%$RGI{Pzx&Y4d|(OFU$(MqE~`4R~S5)#l8>_XB{{J|9;Q1*p4*B36A9Z%*Ty z0(~W6XCnE?wiE1w1%fU{$(2+wae&BeyLOnL*siY4jqK5gHml=I2Rt0Gsse-4-}o?1 zMGDlgj^UzwV4zAQ4NWhX-PZw-%h6P#`4)N)+(bZ5aJ|4EEg!9EQ3z;Z=p;}Q+^TZV zH%b6qXWD;PMs6{IV1>2d-?%dn1p7-@F-{$;cGs^S=?D%mXUG4EfJ513`fX(zTA_W><8YOx?#3Pqw9j{>ZHty`BkeaW6X3Q>Fb)4*UAS8UaZ2MZRTgQc zL+3QCYs>l^?oC-%dZj&i(>kU5G+%dWNzGP@< z?evSoY_5S{^clhPCq1uGfi>2cguV{fzBx*cFy=a2N@GBXE_=QD@aEDmjRGa=TXY^< zOzI@xGDL|APr|E~;8J4LTFr&PBlIS0?^ORxF4_m%zaM!3HV`V#tf z?A1)fr8XoyvR1*bHVHkon_AmJrmi4BkUcr)4j@Gm@iDT)?Q>UxA_Ldnz=XG;xaOHo&H&{kweb`6{VmEdc}C+V8t(1YTso zu)V3kt18ebL(zwO3rg??$`d2sR9I+1stvVEJFdiH-r=8DoIk7E-HG_qC-oRr!U(#IfID{OMu^QRTKse~EDB!cYi$~>W? zOGQ5RnF(5}A7((kt-MDnO?L-%Bc$`B?6Mx3zdMJeKAhgg18(A9$Xpm*keTQtB4X30 zP}#R?*%MX_;>GXhWIHTf+uwp?QR=kEL-fXIAjhU#r^mmpetqfw zAK^TdAx0IS-sOw_QN7G9SS=f_^{lO-4F~>AC2PxaN7>+aHx!T$iD_${8lGqK-&2*UL*KMDr=-L^A z+pN8|>@tR%$&j>55l0M*YhlsSLB1@{JWR_4XH_AKAESq)!>B0!3hE; z8n6-IDa}pGOCH^$j*~piy9#E_K);@x(rmbu!RQ}vmZj5?3F|X&cC3n^SroWm&w!!Y zi+GEp**^(X&;DpG?I!9R^n!i^M}V+s0%B0jtyT4Sd6(10_E@5p_C^A*-=+GtfpmBZ_c~zUI455*P{I(cRy|IyU883^dYcQ1h zb0l!mNC8pxHVI=3T!g{Iqy}s7jl_T|+Tn?Wc)J!J_I6gtOwZsO(VzOdrf3xZpldAk zU~bAB+-6YN-%{d^n^dd%lMW_w_{qq64Ma-G{6C2bS$aI*##!w zH8#S@Q{1=S6Ar>h&%(+nhF z#&7ETt^G4nfJ+cX_J$6iF9^q`G%di6N3h>-U`KA0rMUt$TCt|?(_$|=ah^~J_U{}4 z5Utd}oDSgDxS^wMcy=Mnf(oGEenNrPOSFuU?} zkmnv>Xv6FK(2V;UiJ*Ab$rD=I!B2O6G>Ru~u7E%tPp3LX z=i<-|zYl=p=}PL=Cr+cdzG_DU@G<{{0qxhnvgtlSmrSOYvgKR*VkuRU0N@o<>yBp^ z69Bda6hLwUWE#aB-HvAf^}q2){&{SHQl2*mz?J#r0kPyV*3f_k1%S8`p54Qjs7Htc z73~GF<&#!SZ$mX$H3)s4#0P;zZ6m7~VP1giKu3NxIVlC|$aMt`0Sg);?pJ6Q912nhoeL4xu?&kKOljg#=r8u+8{u<(hO zx0b6MtuMNBQiU}Et@%hghI3veUMzPmEd+4zy3KIgyISp!%SW8)d{93a?A+3OgVu`9 zc{b6`TwPk|PBE*hRL_cS_4fLri z5=$VGQH(*w&bd*V-Ii$xFAvx_TlCq^YL?t~y_v#RxCW;hnE7b$=cbp??vQAIGo5v= zdZwclb0L~h>7M3qSW3JucTotpr#4@qNM2$1G;bH{S?!K4_54Xy{7u3@Oblzp4HYHBEgtI9T1<=iT=S)&RR*=zRJwmVp;{f|McF6i0(Qe_RgxmJxy5e5 z!ZkloiF7R`JbcG4quYD9UD;D~kg@;y}Dj#M(W>prho;sx+phDWw*1 z$@%3ic$Chssp^8gCM_m4_$7i8xu@pk+hmzWzLBH`6^Rm5J^bA*w!|Lfi47C{>IL1x zfcV|~ey_Og6m3pvBY9!jUN?2Bk-|@^JK5m*GOC+4^UIQH03eGB)(TrB0Y*-y#e z)}`y~CEOE3GsXzsPhLM;N9u+j_hHknGjk1(IFXsMorj-U3=)>bWy3N|HqDAzH*n{z6^!U9S`&%lH zGX3hIdM0rH?T8I~O4jK2Zmv(Q34=BHL1nX52lp{Uy;LqyzmkY&i^{l)T;Ar;`m6rc z$yWxKt1zN4lbCLmFK?@_g3GYRmT={3K1K%)Cvn*v8+mFh<@b~AJJm>Cw=_0|nN}g| zBC~*R{6h?ql`t~BxC~4@sX0D|zyxL-{ZsSNht~#POFXOAh;i1NABakILb- z+H&G0jH($if;=vIkA5^KMo&`0m@<$gGVtaKTX9lWj;*B%XLmx01wB(@Q5`Fo^zath z+-tfQ1iZFUYpZr+$x$qNZ&9f@R#2j3sY@tRyL$$0$X6Dc>nDzkLTYv7Ya_q z6?xRWR}R-D@u!x zxnwn3X>-5W`aIN*O6$AiqQ?*C_{eSg$NDs0gH5g_N`K{at00IVv`Z)BkrMrogBlD z2vb>pbuMhc&7f!5)l0N1j*=p1@-7bs-6gCLckZ96G(5Q6dmbAhu0| zGc`=pDC#cewsIlFaSMWtiftB8a%iBs3PEFRD(>;bZCYMtoMGu$rK1&vXI8!id)`wH z%@zd;uvc!3R2ZTw1Fp@~=To{8li^B=g1lGaEd>YMF;;iYBzJRW(F`7{rbyDsYjBMj z9Aep69jw(LoHj#yazX@at(LbbE4ORj4L6Na%$tM_u?PqaIk1VCx9Q)&bSee<4Xdwe zf#0;vL@KkT0}#d~-!4*T>OjR7nvz7VF{l=rqS(96$F&#a+}@Pyxa6t#&OYOcMhz-L zNkZ%c?enSyrH^MZQ547%fExr5lQ74;=hRk^Q)@ebU(T3QAMe_a(6GWZ9zf*op?5n& zko^F&H=J*5h*Z&M0+Zdm2%P$Q>;&FP{S;3tvfZvbmC|3Pw{5sx`sN&^&KaXM|9nvb zAMUpk#Ku!y7^3{-9y$rjMHb+#a?#U9>zP(MtH+k|DoB>+LXF^Z_M`U2j=!l}rrnKvpz=@?l>bc9}FM^<%In5bB@Fzxa+uTEH`b(Eqrg z_FeE=TC3&9GFy#TMx(#RXr}#?13XlTJZOt5Lu6ci+@C?T?t@TXRmIj^*&(?Kb679- zTEpwCq@C~Mt^8Enq12f(5$AOLYUwQvZ)y!PZl{NdxUrPS^S8@}B@5FYn72?%ho7g| zxqdW!gV*f<`pZnt_Y}@y)XUrfz3-lNZ~vG#P2m(H2dZU6e&SUja1P=lDLmul-8YH6 z>a0-zu(VujA=@(kh}q52IEJY1e|cEW)%NSh_}xsmPK&h9R(roIdc|gz2{bdwA}>*I zi0s`O)cjADd7ZUnLqV&Sy*oj`F6e2mPj+*d|DLKlqabVEO@x0p?Jmp2GDR$gFES=U zNB@#%pyG_t)I2opu38|6*6?#AmsJtS;1v%eaS32U9g3014{7Rn zRokA#- zZJyqpqW_BKKVfL^y|H6ESK#-fV`(Q1HQ?yfl+a!-~c&nN&QKq@|f|A2S!eVmR%W zFrQO3oGR=WD{s*u-VB!w|cTRzQk3kfv2%%*0w@h&2X~+IogOG2{bV zAqG-d&#^ON;h;^N07(=|FfDwy!_%!BR~Vvz7_j@}3DgIc7M@7UiS~-QW$N$&*le<} zv)r9XhZw$T(8^PY^uKtnP>>v9dRT{t*8@Bxh=(HGK~>jQu11gypr$FI+sK4UOqkii zc5Ss`9>d4J!3j{#y{5NOH_-FbVy`RTQKCXiLj`;bwOIOH#!JgXCq1_g7CnCtTOdl@ zaFo;8=htAZ_i*U%pA(@s`dR~=qqJhzxPK3@ziE9hwh0I&5iRb)1EhEgm_Wua4UR7pBsRDK zmy>xEoF@EP9DW;AlKgsT^O5dzGBB(}kChm;}^gsASPGl=Wr{DCku=<6sg zosvI73uTWl`a^XP5OYS#ZiHjh7@9ME6~8d6^o#}_)+lXyIW z$$>{89&%p}t{6V9jn)e_tpe%`~jW``FVYPz|h_B?s!3ATJ$jFg#padph%Q>FCyZza9=ZiH34;=1D zl(zj}tsyWXDXtRngTWObI+wyotMavhqsT@AXiQJX4$ZJ4Y;0b@Ubz8>0Oc!yuTO9= z{6Z`xxpz{){qQyX_<}Go%{8_>aLvhu0~#{VJ%aNAk#Rw$mXy3e%>T~49k^e5kntG8 z4`lgcO+CeKE&v`|$_=r17(l{99?%3SdUGw(@rezzXt@lj%9o_ZpZ$0AR<00~)z<=_hsoYfm|&Ob|f62vE=ju#_i~z`ZR= z5j2hh=w0sK8K5`@P;5j7$g=^BvZn^N5tPHfC4&2DzMCSzT@B1_R}5qHDcc_|Q}!LQ0+Pr+6h#fg z1Uq;>%N&;Y5vZOMLcj(TVzrt*eB_`}gs+5obH^0~oZTY_Ac0L6j)wauF+pA0=1U|# zKTq)hbBVKdN5&^x1c6B(M&BO<_7vPcl_FlSbm^GPrCH+(bu4}8_6zGIs zD##mlLq*uApPdLp=IZqV?Awz4;X`3u!3WSUIW;f~HH_#M8&L3P#!P~@{e-CapmRF$ z0oSnwlaMabG$a5A;#BdJ{Zw)&%Y`U1`Y%xB&};DtSF!iVB%f^nku4nB6g1CjxU+Mx^#JlL_JCl+^%(BIsFhsE9$`GAvCypx?Q6d)m&*v zJ*0PbA=eCy1=2c!yNU**jh%r67$&k}9C6VmiUPvlN6}6=XoxcP(>U;M0_4hgD8WkR zxskLy_P&`5 zIm^n+tDuD4cdM%nVL|^eZR0EC6;lUe)uOalKYisitJvQavHzV1qPbm->_NE_iT3A_ z-1#IM@gI$>cauYLR*oLV{mwpI6j6KAV_M5$^;OLjt1Nc7dw0I?f4Gyit9gG6Kf#Cf zj~33EU}P-aM;~beI`$YLv9f16sVfT@6sUh zS2+_6m68y9DsaBhsA!#+_`zOgup!Beq1|3Bal&B4*`E{4PnO6NrD0#y4aj!cs=($$NMR?5x;Gj122H+jFs3Qw4=ark2B#u}NXuEIQ^eQWFHdw`vD4$_<< z(@8}aXgXCA(j7)}P~si)0>zJ2!DeYVw}j5P3$O-JAEXQ>rrEwv3CtDeR!Z8>yWk)= z{%f#LoKc_OM#uQ+(-7FEI_-XfXsUp>gjP%f*Gx8B&r#+i3`JG8X^xWY_lNxtRS84s zvT7WR4Bg%Ve55C&`C;D7Thj}dmo*HJ2eQcer_$=ncG_;L_rq3M)PjgC@jqL|15FiT^rra2c#{^o-v*;=iqZJ4 zgb<0}?lVNezP|f7S?I;FgLY6D7UR}vZR3>x(gbbkhPRqcfLKzYM46j{`173+ky$}r zRc5O5EYW6+M^`;ecF|Ac`_vdjBn4f zC^5)#xZAM4lDH19#!NS)jV3ACP12M9M{mBcJrC2cF~#oKyU-*iwNbA9__Nuo((%RW z;8_l)gyO8>X|08?k&MU@Aqh<-&4aba@FX)m04K#(W^}8aD8Dxa-X}C1q$vMs+q%hC zQDd^8lgvRJRqt{1CESth=1{LUGRcq6KB?TyiSTM3ToF@ayZBQG`NBl?-o{=B;|CJF z(4Nt&f6m97m(QR?t1XLsaZN^I5LH*(>Fkl7jq|#hxKUG66><7+U1(%q6U(K8;_jf6 zkA*rQ7JGd|H=f8;bv&z8!lt3kVw|d-Up@>x`37B5Z&~M&x@S-YI215;zOn-G*p>8e|P`!F3-To5A5b z3J$v15lQ#R*zwVe@o<6rZ&`A(`M$BUlGTD+hwUQvPkF30U1KXu8uCQ*EF?m&a|tReuYFPoWlf5yQ2$1Al5>XStYU44KM zenhS=Jr}_OEmMQbZ7=GPS=lNWK4WLN+bz_FV$_*GY*i6VFhPr&Bz5Ka(;76vpC4et zkm5a7va+eX;c<-*(b>HAI=*o;K8?DhzZM|o_WxNih#G|JHDyjLm-ph&WMiFDds!IY z(pQ_;f4R1|=g3&-GJL-S@2uZ-e{0aG} zxr6TaYRQ@%e z6ien0Aria94RcHH6D3HWY*#5_ABXpg=4A|J^yLKKa%$hZ*cpV);jDAfD%CQ2S1h!h ztxppOf$rW_USM8L9{FTcJbNl-{;5LN#;uQq#F-zom1U;ZXL09nVk+kB zaQS0d)k<4fI!LjY?e!o0vw<&f#4up|14_kcJ}vCmIWWh1PY#6kB}XWO)qt3R=PPH4 zZDMYmLT)T4h4NX>pbOmppD7MgoY zwM0hlu0JpO8996w{}IE%AxrJ!vNGi2)oNl{h9=iR(bAhj=ZTRy9G&(!t@VxaoPVRd z^==?;&~d$1f?R<2s;L$~B?gI7u&))V)mJ2~&weapq9ayQ_9U}U`&v-@|T{~YiEYg($u zFZiwv*>fVVW>?hY;)|>u0yQ9x+SP6f&Xj83-k=epxlkIvmv*5fGiDo>Q6|ritZC)3 zc7?O=YPM>1~Ke49-~GX&@&+CR1kM#3iV!3f`9 zZWPfJfn5C?#ZABk4A{aO-f~$-t7B&Xx2mUDdBOn%Wn1Y?YL6hv#SbuL`S+*Q}kdu3X;;r&2Et6XnXzgrz?=YHvB(|Wl6RH>C}b~)-I zpZbTH=04!7T|6cU%DErOx8JUezW+q_>!vy2whjA(h_#}%<3019xw&O4B%q=T35UF9 z|L)L06haB^fJ~)7U0s0Zm~$kcwo7_yj;o9*ZzZ}dwePW#KGu{HjGdv^x;Or`2qzA- zP`lNOZ5E2YoLWEF5$$`;kh(nWYK;eFLWF=_g2YEs+<>5b;Hu-5$&V7X*$`C>P@s9Q zc`6TeHKVf|23n05yU~>3cfj@N{BA`F(rZHzF9z`%Go3NV6)iQ@Rs-G{{FnQkr0{N3 zfFg>17xbTtsw`}yr`tXKif7ksg4n&1>pTMJGi&ck^%6+21vQ>=5)FlLVrR_jIhvYV zlInP(ebY#E`0Ug}PhCFJOV7cC^uu*B9e1MInKY1A#L8d<&s8E?C#V4Hj|h{|Y*1Ow zg<@>yvWPrMQ3s}7I91Uf4?6I@Na+B@H|tS=-<>G4x)rM(VHQ0Wl9dVb1iV}{0pl#L zCgI(@(T$F}${Uv%3Hz7m%7aAn(=q7Y& z(eVf4-|h;>44JTA#*d8pM}F3ItzyTr92ijAlW|h~BMiBSJtkGIplQP_ENrvF#W#Gf zUdmkdunSQ!@qtYW_2sf__9G_pI3%W4@T&1CzC5;^sNR;{KFDdYnNMbia>Vi~H}jsV zs3eWnM#dIi9`i<(2+(6)@)Tn5T$?jB`H|~pm~^2!sp;b!L6w&*L{3Jn%=7nKcKmJ<|I*qa-njg0aFVuugml~( z(mAiJ`PUW0+U=?9S%1JDqs-Krj=DUq3=Za1`2>cL-0w}H`wqVo#~aVW!eH<~tht~y z${NE7#~v%Ots?Qgo|ZrLnGBavJ+VjHqEXI{?(5&(=23f;`AZbZFEbmxy=9oRX=wQMx;oe43ue=V63ZA1f3BCmq>3`d z&Nj>!YDRvRkg;Pm%~*2K@j9udZgzR^ze(Xmkw_?D_Glj|kMM5BM|GjYT0UE)zw%ei zTq%mPeJ`Eplvy_)D1IGRJc>i{y5XcdAM6p@Qg~ED5KwS=vJ@S5Y)A?A-i|VxYn~8c z4;Wvs4rgOl#g|CrPfu*T|IDew&TG%CMVjXDpmd&(SNvE}U4H+QafIl6t~8QG1pnZ6 z+cKHuxAW*OT(X#K`Ll?TU3;}ARpDiYx%hyP<8Q07O$l2g$<=iXe3 ztB4xkqpkg>_b~P~Vk>FfGtw!ko(RdFENT&N$dv@j#Q4-XBjPLJ*gR9~i0ev48;aG7 zr{{E+aQtEAqaf&aZ)V=_`+UZvR_pTA4yFYWZsYD#>Rf%Q3cBpRzepF&^vq zmr>jF4<~$zmFS)$T_v)PRZLg)^n`3Xo)4!2YTfF_EKcPu|R;t+2;6p?_L;VYNy^X@5%k80UYZWf; zvo_oLon|j%W+$6nqAxRD^F@v+m-WM{3+7AB(c$HcF8Q1)E#LHrRG_gQpYu}|nwAgL ze$`7D<$SELjZppkRTR3I6buJloY0#ohH*Chh{H>73#5pDn_WDE2gTV+dmCHqi}U*n zQn%r#mEl>+#Je->uuQ{|+yIY zCx`Us^Km{b>5~&pX6i_?O^wq|6YudV<7ysB{3wu27B9kiFiS~ecaZ8~kP5;q`WbgC zhP=L3PZZ`crW-`#VPdYf>PnSa725GdWkDpx?v2bSL4dM%jnjuV&qrJ`llGD*AHTc$ zIL^z__@1<_JB7@md#jwbS=l&Kv*5)R`R#D$@zf+|!I0F(=xDg&ym*dVDJfL4Z?l7woy*MxS;0d~^pDvPesIE5^TOnbTinQv1Ci2)* z@pO7*Sa>|o&XN<<8o$u)NDt1Kkp97mFR}AzMQa0uExsH3a&coX z#dQdbb=qb1?hH?l95ss9g!y&+;AIZs5JNxu_2?4LT&kx&8H%@hUsYIk7uEJ+@UyX8 zEkp2y(e5wekmE`*qY^s?*Zm93RbN=|(@J(MZE*zQ;4am^JoG3zRKXjIpcyDpdjHMo z%a}#5Cbg2KxNX)cfeYU=5)ri}bQaV6waX&}*O)k3Gy6lze1WFW_lqlSvx?1PBW^eW zv^l#n-i+YlE>V?G8eTU1^bWlM>A~MS{X zSe!LXbw1FnvGU6^fa|!zNkr=SQntTY(9TB^W$TC14N^)rbL8Ri_$6m(Seo?xkQ694 zx*dO>(rfT)3U5tD@Y)pr>R85xK7Ye2Ws3z_v5HF!JLC7+Zf)^V<@Rfi?8l;@`<2{X zf&kjE_UlS9$QLhc$7+tDty70V8;aBvliU?!ZIX|x+}zU(sk-a6S9}aWH{m>ql=Fk5H7!IS#q`C zz1f1zzgfIr$kBmGZoG_#=3ay`h6QCh=Az=pH-0k{rO(9wmYkZ%a>18D<<|CAPXFNr zU*PTc*vZQr_UUF0fdq_z-LiY$GR?Ww4}-+fvYJuJCq=rw@!}iKeW0xGw!*c?)l>0sAnx`^a~vXDeoWpW3a;;X)zDEx(DCOwyTnWHZXMLBC5PfTSZFT*I2i8fzk zGs6(S`2hJMmZM$Ng=^P>UOZ3mV(PFgFi7IGi!UR+TSWx2Hk>(Q9<76zW6e1tk}-7Q zIcM#fSCV^zopDB>V@1+^&RXw?ZuSPu-@Md6R?g+1dq=)U-3GbdAgfy1Gv5Sa%X>MP z5a{Wef9sG_GDdp+y`*e{+^Nn??gx(0HTmd+-#uA(4ZbS1jmj#XgN>K79joz+t)6nO zs<&X{jtsdn{(kW!_3yCCo1k*8*l)uGVuIGU_>lpU_su0`;vbg_ZrnBTW&Lw!IG^(l z&X7bqX}UM# ztGgd)H|X1%^>Q$a@9P1-W~Oqd1Iss#@B4Nr)!6$~v~0|Ga~OBs7PocJSSl3MyAWV! zbe#XL)vT8KxOegz$u<1Xv+C^fHSCOq_%}Mj=4i4|f}dsNx8jg5S{B94YVo5lKebTK zP$b*i5DpTHXe}@__{^F>YNEee5$ZhV8cte7e5)_dNSAI?2f)@4f-aS{k){!CuXHP|M&`_`1F zM6kM$(h{++dc{$35b3HUf9M<`w?=3NVd5P_YH6&0Hw)Kgg++CT)_(I#@@k2)NYtUh zXBm~NJ+V(H|#HebHW=2xMrTOtG+ zr7sz$Cv0i`oR%ZEAGeJ2ro9<)U#t`~Zak-G)=b@A=?Kr8NJ#hq3B3{dP|d%H{H4N6 zVzq|7c&)(Zh+Q%Ws=afKSu09m*S_@AaudAsg*)q=Hm>WT>%i#ytU#kPc*!4Kk&5=q zL6xt-)*_|Omo3RgYK*bHUGWSn<(Z=VZgnx}vlM{v+`0H8+D;W`f%1H^6+z!is=$eyWnaTN?qAA$I(N^@2e~ws1Ehm;RAvmiv+(#&$wN3k9EI|8u>`1+OBq` zVVa&#aohhU+a9<1`P5QHQFY6GyScM(**v35g!v1BQqBnaOeW~IhR%>#pI;h!pyX;J ze%zUCnYW_Cvu=I+ODcL62DR@Bq9x8DtuDCq>jl45)oTSE9mjQ>0_o#;v0Ddah%jk) ze8k&-1m>31v<5$m4*6uyM!(+L6mN$=!$&KvAedN5z}e;oqlU)BlwA)iiY{X2ikD2Q z(Ir-JQC8St{mn(PD+e4z-Up!LKF&6!#7C|pP@%j*&= z9I5HWI4Wt~uPh4f5$?%nP)BpaVn2smsq;MynM4j$H?Oa-)^3vN(CgIf@`BM&Hr^2B zEdvD=*F9et&t~S`YC6~FLXPwWMw|BF%Cf50HJ-C>6m`i9uFLxjJ@2;XZM^3e^DM7| zE78zV=jZD}{oeArQY&)WmAbX4jnJ#B?<~BK{1&pO%vTB{`PW0nIvqvpMmtHg;H2I7 z(eoeZ+z(3FI=2<_|NN29N)WfqW;N$wkt`uDpM^%c03W#A{kgSr2x`aRzQb^yO&6C{Q2gw$ zxx1b1g-u=oV@PTqe!y-}Vjyf1h_YT(fPvyIerprCKfPnq2i)B?B@|Ggj_WA``#+6QD@)$~ zd+h(uW2oKdJtLx!FF*_w`Y{723?t*_$oKwf0;D=`pvJF&WEUVj!vs(~7MV&kBV<=o zi&`M`OWz@5TkYRW-G=JQXwoA}F8=>Br2+q2>k|GK#0wBX6)}7knSrzGD5T-6?oXdy z1kP&QG3GuhZ0H9p!UG>cF#KOsbs+8<&{ZE%RF|eGOKw3{G$5TcRv8a0$qZj-ao{#) z9h*vv?hRA<$R&Y-q0jl=Kb+h%_^3c>iACypg~ABdXvkQfs8MxH#1Dc&w#r|X<6k+} zSOJwKBythf7w_n}K=*!vk`-MXlwf^46@!txC!`*Gp<2w!1DlmkH7(X*wM>>Q>Z#IB$RIH` zdxqG@G$~^rcO2CVkjzU0EAcZv7f6xN5u;3k?S=>_s`1rP2u+>?fwN}jIO1Xi08GqJ zm}om`_nzvY1V`Fn=MO-g zB8=TRhuo3bC)K~q#S8dW<=*clM+uyd;-nrRNryr2sJX4n2HwSQ0L=-jgDb${3PAD! z9)Oe$cc2G2Z0Nmy{+@fe0zj>2m=OD!=4~Sx4CNNMX>9zK12kxj;^Fs*1JtFVEfgZH zU~l5RXnLFn(5-Jg4N72SW_bhu?BOwZxe#d;a|09Lk_12{G-F`G*@yuk!UPbVEJV0f z=E7;GBXaPr!vPZdlXH}GVE}A$1Lbr!UDmVy$#Wac0Bj2Y%MC2R=g7B>bfkhJ>p<;T zD@IG&3t&EiFEIaa30oyciHhWN9~bz{tP@~))}+G#@NfEu=?%cJO&>FDpM=1MD1a^% zD6r!;e7W$%3_8G!HNebjV}u)^2kmEo>iGdZcs(_cRAPpw#;zoQarnmVT)NVYEdZPr z;3i7ntq;(58VR5_WMKNMe-@TDzv~A-1yF$~h+v}bAIlLBUU0LihRa!t)R~ACk$9-aiL0YE&u#HKYK%5&%DgTrPrxSO6y| zA_UYV0{;$Vbm8?ss35`EROWedBj3}xE0{f@Yr8R!&5{VwfO=!>XF7huSrLy00Q@W2 zHEbwOvX1jwYx6~*d#2>ih>HHrm5C$b%aGlUFc#2;Epg zul1=?)vRpCX_DK1Q%{~u8)@tsUNBf5aEABA_PhcE-?yE^^9D$sRhCwg@N}Es*53NeEXiyp9v=71|DmU7=v@gd$RE@h)u8l|6rg+7xA`pKX=_n;H zTI;bntv_ZGY3{*!n85P)hE>zHre#1^>OFs&r4!uuj36O zENm?io(*8vVTPkcO|nzo*yWac^*1J_ZZ*e+9knyda|}?nF5ZE#2(*4ahr>|D#;@&< zOWx9?7&xyZ3~pLD$gZ5>Hw$9Xv~L3z2Y*@Z;wCK??%xHp_2_+Z4v3~H6;7}g9JZ)iYN7<$jv=Dq_%V0w&0Kn#NA^5SgC)Tm z(bzFrIrB4m;DF^(>WFN%mE@MOGTrbB5Y8vlmwqeaAsYISqRhAXi}`qTOc|13VgJxM zwXX6-+bJ+U7RMpLmyhGyY!I?L?gUbpv<@RphBk;+=RKmu}p zBpGSBaT%aGXYnUicg~Vs&8LhHKhpVU#|(9n@>i!Rb%|Lctglg))MJ0sSEWPLnBOk% zkvy;1ZWVBV&i=3rVb`mBJNGfI*{vvW8Ie|+<6$<1xt276i={%`W51TvYRot9rn#2X zeEr~LbFL4Gg29xgKen^Db&!L zA;*IVb6bO7YML{tVF}y^+e=79%cXm>?7`jj9ZRD@>PGMX9M)0LGwCclum>fX_YEN} ztkTM@is=3dx3)O*pORm$c9MU8Nv4g*WUVrsMdPmiv0W!czMSk>v#tC_8@(&9+U>Q& z+lDHdvb8GPu1`u%5w=zYJ@Zj`ruZ0%@7^_WOxuq8)7)GE*~?)hX?FLD)>%^B>P(FF zCGGXJ2E85^0YV$_Yxo0JSJ+OIcX)fF9fg=+Zv7>lbdg1x|J^tZ@5d9f95owMMHuM+ zVDGJ>>S(%kVG@D|cemhfi{K#y4ek!XZQ)KJf;$BF;Ife54q*}8g1fBX?jFM5EBpK2 zy)X9o&lqQnJDJ(W;x0xSYnlG)t0pc_7snKOTd*_{66*xj6B9rH(6XnH* zSl4#_;q8&5o<3d}Z<-OWWP`b2$IJ8FoH@GLv(Q^ozqTjI$5I!glN-GeOFH@%vU?Qu z^%aXioXIzLqOJK^PO1-wIqoP>xAz+GU+R(ZuCJ><$@QYd4Bpi|@$q!8a$4~i1ksU5 z2lmdx7KB8#l8rjRInI1sJ}N*7RbE08`zV%-N;=^9#)BpTp@N)Y7W1)`hb#Nn+muDl6jv@BLA< z(Rmx~Up>(v;6dx=LaF$)BADsrRJv#ASVavFlOL)g?-xZ|bW{5UR#qxP%-Oc$A+#1- z{5IO#g`WJX!#vJouVdm5htz^&fhQE%?EDsh8_jL>oL|zU^|JR*X=TAr1wzi0l`@$- zx=MsuZm5qWLaPAJIi6xvTxRoMJ!g1U&7mNE&8nkEiGhd~@5P3^a+fxJ^U@pfq1_+0W_@}u9Dz|YsOQ)+ko?^U z&5y`XVyR+E!k!g1D;O}^C8?Dl;$4WLS881lTPzK>rBAFJG!2ON{ZvnTv^-$1SUpY? zTRoe0-;|=ic)f#|uD}@Dd0A)_X9WJ0dYy_gj`8ShJD@El@B$CQIwZqy6C7@Z%_h^F zax|jykbbhknKd%K^nqrAf-4W3kdYBHfud#>A9hMO>QE#Q)zz^6d81%?Z`e}*UQ)1i z!C^4zbk|nU!{k%Lx~~aRSV(3+qJhD2eX?yn=~iB5f|n(EdzwE9$V;r{C`&jCqV4m_2!PGg z7OFT6l+O65dqz@n<8l6^!@q^O^7j@U;P24ES?b_oSL#pYRH!D?0etD0`bJ6KtfKAh z*{HE6v{-zl@A&sQw}!n2zg!PL$nG@;^-vg<0GwQ?zJ@^>0SxP-qa+O*0_E=jZZ#V_G$T3tX zqD^1V_Ih*@WrGw->;ja$|7@VpRz7_$*~c&$X+&tX5CRs}y$4wooDa-N5N|4~kZb>d zTn?DaRT=$Owi0zpLfI%pJPca?R$J?MF?c(<6(AO4)aub9`N9mQC)-_rfmmb8zYz_c z6z@kc9E;HAze583y3_O~v+wqI92w|=elVGyw3-%E7~K(@tsgkCPw-+u6x=U~3^nna z9i&GcgWZ<)thX!#0Y8|niGCueH&T(sMc0!k65L+3Wsg6UFu$BWP}-s4u;^Bw^xNg> zfh8f!h+J7Nu|Z5WFEtlAvbE4VAU#s}cDliDg#(w2sByAwFY^k zEr;%0X?fXSEH3H1DY97tN!y)H;fM*N&MOVp6Ee;2aYRc*lq7)U20i#2&cC;8UNORO zj|i}h9r6|A(gOfCoZBJ2ZS9Pi28j`E&~Dwf5-fLSt@PwV^4U3aS}~J}UkW_#6*d6s z^9~aK>EPd?ttYv?&5M_98ddz>49|pYeZFpLddjlyRWDS^n1h#DBco!)<1z}xTogj= zlW`)7Im?-yPkhId-RTqHH+RL4q7BV&mC{tn$cm$2%7}Jd3UAWzuIVQT2Wu1%TA))a zNzMG>MBjq!xQ--OXZujf(h+5Q@B@e~Dt-JLZ^Ms8j5=bt+2fa3;zU%W9|`yt+dJYj zVSW>_{8oUI@n!wJkoQ%KQS;q@d`9dP#6)h1tC7=``F1lK4MhXlV|IQo?_P!A?({M1Fnt%-qK zavX~~t-kb)^93>jNt4eb0G|#$nft5ac_OMTG`F-jfgA=FQ5p{DV{JO#P|s7Dsq>1e<+R5?s6pE&7HRg? zn)@!X&<~xgY>HdDnqMoIjb8Ank0G%>o0v$AD1ZJuZZGf@BAZu_9Y2I1(Qj_WGygU; z(|tG2D}A>G`yFdzprF)@9uyxoAJN+v6#DYT&LwXL3Uqx|uxXkP05gFg>@B%=t)O5B zOkqEQu(!A)c5DGVjtp2v;VgFj2pP7%UuG#ZhXz!_&)CH2VBuTM!Kjjr{Uo8Da&t@3v)<0g;~x@NZ@Bj1E8dL zn_J!rD4-#@&%`ESV{Ee2(tSWUyT4^ZIw{camVro0D&>5RqoI zrmX){-$qf!=c3(iYN{F$PJVNLe(!h`a<)V()}Zl8}^aie9|-#1YG@qw|(@j|Y5 znC^4KGH~_>n^DGchP1mZ$IbLeHh8)XpaU>f+rDjg!s+Rv;)GtO? zJv@gm5Oq0~WrKVZLUaeVhXgHjRJjL|I?|+dkBpmIr9bBlMXWYvT#zA|Ib5;BQhS+uAFTy^%Fz0oC(oqw^`jMrJJ?D3jIumY%nbF?Kk8jn+6(B)uDe0=#nN|w zlu!k~a1{MPB)IA$olN^oWha#bb|@ca-i?XGVfYr|K5wxVA*f=_CcWc+M^>3su9 z9zWaD4`|RgKm3?aJWP$c!@dkOdPhmyD^6xd{Z6}@;e*YG^~Rtvc3vB+(TL6@ z4l$^N1BVQvOp#eSs|(`w~sBKE49XR$D*jrIU(XR%W=TTW3I`QY`WILt^8 zO)WvE<0|%&9j=L;5dJ@LBC9RCbphgT#~evNd@?;&LPgj_%L)rlMfPnpW9pSZk$;eT zSNu&^u=V_ab7b*Eee>EjY9w?)w7(a^r#4lojOk!bsZx#%v3alT9frMqHcx`x9w!lX zGvu@ODaSbpvA{?NCA=6_s5|KS5n&OFkWjHf`yiIOb=T?%0P{7Nk@o2Boi3 z!`edLj*w*Nwph`hCA265#~px^}+Xj-bRt{aiMemuRg!s?4aHF_~FxL zy(=T;m^x0GLe{bU=H>j+?CX{3W9DDs`M8WDd*ZE+;j6HM#WweSH9Pb1yl(>yq?J}* z3{n!iunNd?+dr}o@T#HD)?t)G>~40!(XQ85ma4=&e!PSo?I2q)f%fCu`umnE)h2K6w*ycG9-k)pV3}?^m8|+4bj#x4LU_&cNJ$6t%a*D zf02T5^FqrP1tsfe(Z1wt=Ei=jO@{E09p@+_Y<&NloJ8u8SHA~z3UWQA!mgN%zM^{N zG=<#(iWYuYxb2&xxFEiq^@*&~5V2X#Z+G+{-3@1vSEU$bDf8hIgJDX3*}k?o-SEV|U z5LPYUU>;>XGZMl*@IWNiu>bBX+&;fT;^L9se^Vt`);9WBo9n&nUdQdU-$K+>Ib%^N zO6p}BWS55AWRdg1a?#|T6ek;QSXX#m)bVaKewYMg+gDgW0Hu7h35v_@9xJpoJY5k) zP4Q!$6bFnc3uk?0>1CehhNClo1`1pR*_B?+)Ja|7SaH#$3_kGm_;xFWzpq=-_1GI8 zP3p#z8!dC3_Fi!qml+TH(kp5)EnJ^ck)D5ryWE?v_$8iF&c7^ddY!3@jpz!!Bnaz^ zVt|lGRjW#W=x+>je}epxsY&^_&;;sNOxttg*~pLCQugB3zeDbx99`_$3s(6b0-?58 z*g+^X*dOhmz=TA(g%NxZ3^9O@_S5*2e#A9>_~R(7S5yeIPoZw!&|E2JxPcY8jqnnP zKy&5*sF*0SE98G5WD@`PWCRphF6M8o5Wa^4w(5L7?)9mYzU4v$1UL~uOsq?X;l&4R z^ra}t#uN=%#2~=%jz$9Mrx!tJwb_ZmTH^+8u>ABhOUP0}xWR4r*V8t`odN>wKDwED z4V!V(nKD^1!6B0GaGnB;*G`qG=bb1>M+ma?w{U#LHudGQ4k-Y2M5o2;VFCC-W?1gj zL^sa>f~fV+!*w#D%{ZV`>6(>Qy&NU3vyHHkl%(LIB+ybfp7hOvlEKfRscFneE%Pw| z33O)k=x}+jtNOR--T0oes&&8?Uu<6U7p-tGwPQ?u@HPcB6Leu^<&hywx90s_uL})m zDIc$Zl@N>XspR*P)_Ibac`SeeA`}0ewRUj~>PtLlZD%-wP!5>Y={(+if47$EeCOHFg?SJGkA{IqrnWHr3sIo$&0R8Lubu+so&x2tKzmOB zfgqsJ1u8pJyGXCz6W8y0KqJlrqkWH}Z7;TNe#0eJB7pXqd4QtgKv6Q&{Os0=Zl;}E zpkO4onb0l=fJE>C-Aj2wez}DGlI-a0oWTG# z5}iGSXpOWDzeEOPqe(ph+62EsNlsY#r2N6sy*3LEAiSWOE7?9Hu|`5-CEf<0jY?FY zFz5u$7l`lz5RUw8Hb9CsKv%)35W88RgEWe+3`|Z#LWe0gB>?An3iQ|m=rQrLQS?fH zV8@UIcp&srShWg%k^W>zzUtZg&&B|GSJO+NB4J<%w17@QfZkWW#>n}q1Mhq6d`a>5 z@88paPGSJ?jG!shbz%~zRtHd_;}g)HkWkTd#IY9;CAdZfv`8bL7;$^Ysuci7oQa2J z#DuK0bFf~r!8z=lslR1jWCB3SCQu3aC%_pRfJKb4f!Yay+8uz}34y8P_fBxHYwy|( z7|j%*%SV8Igm3}f13T-QCs08x0#G-gDn~#ULP&rv^neF30Hj2+H_=x(xr(~%04Fp( zLIWO?sr41_;&7-6G|>Uo>j2Fu1ST#`TEtf@xOe~;bR6oA_C?lhJ+9?<4tm4@^aRYe zrBoF1%S6k)u;o}|ZfWCm3PQ;I|ourhka(wRD0~jaS-e+*JW5;xjwglY76`bhaF)uu-vtj7-3Sw$Supb)(WA!qC?INg<9#}MjJQOQ~T=CW333nO6 z=^g2Pi-1KidY`+0_nB(eJid!=bt(wBj`Dt0Ij@WLZo3Sefu5GLbFi-5?MPc z;CylopuJxb77jkPCbT?T0_MJ)vRy6Wk@6m%vjo6bBE)=0L)q`o0)OS}PM|x@bs@0u ze+kUPp7^ws@)RhJ6(KH%;%b8af1P;!9q^TXpxJfY z`fOADD@r~|KO)8SNhONn>m{k)%FO#10Jway4VC_&>H4!nVr`IUQPsPq+;0+*Xo#jG zNkP8Z`ACEthbk@;gZP1QC}Q)&F?JITaKb-B?!rj>;$(O33}-=s7uY9yU@-}zdHjf$ z1DK%Y7On}m(E-F27_%4KEz+F{6Veyb=<_w-U?fd{@rkK)!XRxPpC^&QLJZqh6=f!zH}sw!GwJ z0ax=Z$DL)ez&VaG7s*J8BDhdCaqDAynC!ZMx&fn`j?g&9maaG13nt3wL- zR~u-y8&6?0xk^?_qb}ic;FH;@tQ)AeFL-+C`?SIKZmtOJd?a7NzE28PZ7#|ypXQkD zJZoLt3%-!PBBGQcRpWa;J>S&wvi2+Zs=?!RD6Fjccn;q9O8pCBsVoS!hw1Fd+^E@4 zwrR4#kxl)Cf`vIO4tmmIE+n#%Tre!HDTzm-N-5L~R zO<)*39>pj>;#T7PTwh6sI9u(7P-h?oqfI751^EQRpU= zn38r|?fN_Bbvv^iHUgf$UCAePr%xhqsdQx69`55$oT&kTEFYn#T%S{6pq!adC5Xv$ zAVFAbb(Fzm@8qwV^e#5*K0#l`a1O*hD0n}6@C!R=BTaCzOmsqx4N$Pu`H*gE;MJ{= z&NxW~QEX+V=UnLza(D+dL7OdXvN{qGhJQWwL=T{vIlGMz@I`sBqu6FrCbve?JW+Nu zn7HkAKMxsMwy+_KQToA&3RS&#uMkf5mlcUo;`Q|AW56#47%IJsNICDRi_gNz;W_s! zKo|_!x(};p2L4mw>Dih2>U?W+(P;+EH>sM$!~U|suJOffAdl;_W9h(-{M(Q{LM2ft zA1S$=AG}x2+6EuJ6j?#|f+-{EgLa0}bnp87`^y_J$WWmSZmY1r%BtDXfjOdD@i3n* z_JSe#Cn-~q`{5)Hc9#shnz0dLF^-M`x3fAzf zycDo2M%zo%E}^%xB|QWY|O2iw*{^aXYus%l5DZN9X&3NJ97i_8T(p&(x4TW^0umy>fCgsKNgC3+a=LIc|MX2dq>yv7 zK9#E<$!!(>@4vBBwWA_I^;LF>prO;DyJ3CB_HOZEdn2Iv7+|-1<*Is-<91ufFE4rY z7H}JSfg=3~OjDK5|K1q?xY3B_JZt6x80q3D2p+ayd5fd{vr~?JI(d(`(LwMpIg|6@ zb8CqIQxy@=Re60s!!2#UE9Z7Exyga5K=KjvB0wc7$aN5q(Eto(v!``Z>(2<_%n$03$v7AFm`oj`-SS;05H51eLe3YV z2_^+JXT%JT(nZgqZa`8B8X+#ihYIn`Gk{Z|@0P_?u4G061wR95TRD@{9V~n=;EdB; z6Q?iBU(1Cez;|+_>1dB+k(Bp?=n12-FTN{vc(yLu*j~qWEO6p2fdA}BMf~X};k%0F z6LhWpoycchT&Zc!U6>`a*uRdLkUy_zlD9jRJKrACKCqiGfVVUYGb?<&GZ1`ZOo$(FD zUo2x$ir4(ZPvCRy?dcx5q>wSgzv?Vny#NH1oy+9@&Hxz%}P?TxukP?U= zZKnPFVoWH^g;anysIAo*yJ3tc7dzOmGMw}JSX>4LJwNs6m3CUFJ15Sp_dG7|ZL*bi z#%~o%aj52PpW+oXycy>p3aZ6qwBC99a*@=HHLKtx3Pe;?FMK;h{#cqC^F#CgrD(tZ zngRT4vKgL=@>@hjyzUY+SRj ze?`irqir-slE9Q?TVZ)KnihxS}I66tdYvG*=~pFzV}htig31w!{6@uuBd4E zte0XDw;^>@-h%t6+5Tad|(|D_Qt-n#0|wZ;0SaivER&eQE&*uU@T|)>IeZ(c-|2XgPE0 z5Z(LIpf0sgeR{(3t)!lfWms7#B9Z6CLdc*_3*UWNCn1Anx{{L?n4WbQUnF0-c1AU~ zUS2TCA~GKo%Y#(?4coyjPq#K&L;HdevnF$>lGjhDR>C4up25tV9CYSWLk@9Wr(^W@ zg$UYB7Yl1mcug{S1s%AIyddprtEZl9leZ33KXo#qm*&jgJ8K%C|9tNqr@T~eZy`K; z<(gE^>JtQPk1KX$jU83n7tM;>*HBI>xZ2aDlazLKctl0Qb! zR-dM3KAR^`KT)#x_mdB{AIw9<2vpKcV69M5TpXOk<)yVby|Gy&IF5F|pSBZSWa3DBQUa_%S zPt;LX^^N0UF1km5V|h~5;GztgEx}a5n>1u)C#93Mk=!ujUec-0iU}oo#R+{kVQDD5 zt`%tI5y3_8P2Z0+(@+`XUk4rJLN($GqB^2x@Gem8aM?r_^8x}3$9 zKE!)aK7}R#gw}G(suw6(v<7-yoQEprc_GT9)mH4QT%uDy+>8bS8tOV~>0b*^ftS|pV@uY^vxN2GX;~2K1+2i%grV(agmcNoLD`-yqNVgDJdnJY ztq)S(U*H=PT4X@wpArl5Tpo#f&nQPuaD#z2BLMPu9G!mg655@eUbigqX=o<!RaYTG2lO@4wQuyt|}bWGBeB!wT+0whIcCJ$2ar8nd?LprhND0GL7KlyxucE#J7> zTKb-ATxyg~b08&Dd@^f1i6R|GJqL1)#nr{Tl#b1m8 zk(T-)5*R8sBx&TsX7lAVs|3&l_!%B(hDa@|<{0$Xgg&GrM?r-;xsAIwA=n6f`&-xh z&0JLx!K=#iv0~Xz_p7;d<>d0dM7mN%4=t~5TOdlq*g!zxt<}1N{14+QeOD=?{4>0- zT9V2)M@x|;Fc$?gEAs;&QVwVeiBt-KUog{=o&6Sd@b-N1m**APVd4{P|DL++cXpJR zvw<`~{NW{rM0v2^yQpFs#KG!tY!^1^=dZ<7D>dGcfAZQW|r^O|MGV6&58b)XAfk(D8b=MGgkxY{EpCP^?;bx2~9jlrh{$)ybQX((kEj%5N%vOjkUc9ZY1*LE)7JF8t?xmfaIGFF9waw7u zH-QOLUFh+Gt?ZM2GAotkWXm43gP%0%CaJWENZ@7ruIN6KN^Ls_yM0v!bzFu@Z~c<& zJD7*i`cbkN>5!-4!-OTMQB`hecwh6S@%sUXf9~$U5Sm{vu}Le)0jg~H1Puqg>kmnW zzUlhEJ9t?gUoY_cjdzhpD)gBZ+jv5JvJwm}dR)|x-lPhwvxO)C#CFlSZ}`HB{N>q} zUdorYy^Oc4D8wB1IPw@LD3^tHZ=04LEI}~aP>yEg_reE0>wv_4w|M0#TlEuB--DyN zW8%8U`oLi_s*E*M11*?y45GnRpgB2F-0bj+BxO-M17$I=+CR)WWM$pOT`=W_fA|UL3%SPi^Mn;eAn5Ij#*E)#Dc?1zA5C08k z;=HrlF(Ah|Mo?#!TT#+5v1~s3A>cE~sDg$o5~tnBVy?*{Ar0QNed}FNU)5yIC=<}X zKFTL%ZKrED-qR~8u*p-Nvz5#7kiaM)xQNuPh>%x@tHq#pNOEX4tdoBSpi1b{EE{go z(MhaogP@c(eXYb;)p-ltqH$)E)`OyIwpw-NmAKfH;xb3w1#6#b5esPY$xf``XntmT z34?vgN&hj|@#HT5Lems)RvuCTXDjHxPM9Fo!qHchUz`tf?6L3gpi~Q|y z$UgsGb%>a_)lw-Vdu}L?JNQnL;*3otaS=Kx%jaTkx3S1h<{7^=!ZEAHk>dTiuBS1N z)vLnd!a!8FJ2fo@-JC+^y!0fF<5?86UXLJQ3-;-~!$@T7GSk_oLu{WSC&1BH!2BmN zIpE9AC>kIUGTd}W0TzyTd~G6ol2TRVqUhd(C$EF5^4HMkGJgD?qlI1u>hOgAx~fzP zHs0WB;N2EK+^C&2d^BecQ_CBd{S^5bnQYX_`6~8SJ9V##j&Y&Viv56L0R)j-oKBNr z5gQBc%kUCOr_Jn0i}NL~HKhJVK9>kmo93xE(qG2ky%DlVPqc@fD%fBDSY#4423&@O zErXcI)^k%5_z|2;ze%9B8&?VVO>MhF(P8umUAp1(9#QT}|+xnqa_ z`5=XTA;b%cgnv1hX8vSr{Ke}FBJfCWrHdO%e@{fNN!|i-;E@{15XakcUsoF+F-Lli z4TvWM>CFaDpr7QO_+GQS476X!oS(7V_3N*d)$eCS(er~g2fptDHEFjBqOaWg#aU#@ zyLn!AJX?!JCHwGLKP&ECbRwRjyisqv2eG*H<3qYNHD38N{l2~#Z@6a`e(cGt+_6@f z@iq?Q@%yE8$(zY}Z3JJ8M|UIs6b6UR4lDW}1Qg#vaPX0A z;Z(@zs1*p$h|8QLe0Ylyn+}?B{$nwe_12|j$qV2jrx7Y z7NC5w^=a;+f5ghy-Tq5?7{xLhugqjO*YK|EDnMQSf1}O!e{FiUKzPC6fz||YD#v9` zS)nXtO);zmzz+uM8I;am>M0-Z0oZ75>09uW3&I)$V0O zfFinboD-wXW{-2ot5#7Kr zO4kv-1CeV$_Fj0FU%BBIL19?Yub+z?8ynq0+Y=e^CSy=5^X?!oS0W|DTvl#IU+5Ld zqFEe0h)y@kbT~9hfW(##%ptu99YHbNU`0 zfJQ@UAJDdS0_Ec_k}6M$BwKm7neef~{SZE(WkZr++i*8ZIoWKkKf&hI{B@gze5XC9 zOZ-jmyHdsi=Kmr~7>~A7gZgTH|3}t&aswBCePvo-ER7ffJ04^vopZOciVLAdQldkyW`~Y_AvQ5TCZ28BvHQTCKhjtND03 zqQ)wF*K~*EgpNZv2E?D+e1vFkg+fi+#YRtC)8)DBCHfFKgqBLUmp+z2v`%-ZdWnKm zX-V7Vd0l`@{?QOv;w96pWS#T@zLOQM-n9=6)+0 z=^6h_4**u>8%!ygJ4WG26o#b|Uk32g#t)_BgKHF$R$T+x)0Bq+d0@-q!QkfiWh1!D zIh)aUuA5(C!} zUk2ihjoa{+HQtGF9pLbBLx{w(8A(}UrRAGqrmIsUx0PRR%+={C#-M0Bq3$%CHSN~V z&dkXp7b}Epy4=^N78LmWf0R%W|B}VmV&Y2OIT^D4`gJlAmLDK zb03~;w#vpbTA$Bu2-K{0$!QQ>#%Io`iO-m6#$hOCqz`*fcP%huRmQisH(|&&>q@kt z?#Q({Ll<2U9TGkEV!x{}+SML0IZvonMpmbn&pxYXxIu=hNPE68vnu5isyP{$Vn3;9 z-YVUf@^UX&>nrWK*FLCsZjRWs%!K@YPEU$ul^a{#V^2|GU}}nPF@ty>CtIsOW_Y{N_@EFj`CDFPQJTK^=;Zl@Vap5N4Oh=PE^{yJs;T{WWfcwSMCs*oc-UH zwIbGcp;1Be_nD$LTKgBvMU0LCX>UcWC*@bM`8rvz`$tJSXN>r^gc82c9?tJnlCGnc zQO^g`9vkZ%LvCL1#TxbSpe}SOtE{TbUyrxNk$z-$e--GLBQ2;`r9zSx*B<3Lt+*xp zX{rk9&(onfv&{hy^-US-TDQTAr#H3xw@-y~XmP3nxb4yaVC zkm4pTu}`aux>GmfX=-GOE`7j$eIagUc7=%<8~X!ie<&8Z?z
YFfI2IQr%T@L4+ z2Z?;i68Z9sQT7-)?d7}IJovj4eJ0PUb~HrrvMXNRJ*(l+%6QFgFov}Lhh^qPr@XiR zb=k1dREVA#@Ax&#R>4E*iL={-j4FH6AmeD@SPLd4}#>Uh?^kj}o(6?m_ zJfQAEFdriLO{>#f^7)5D&D5Dnr3jR}@A8(5ZFMw~8N~VmsxtuF&pgkBrzuP^pba5Z z-#Q*i%=ZH!RTgsKqcEe!xI4!*t>k+8KK8w)cX?-~hbrrMKO|a30^iC4o@Fth&jS2u zH<5&om%<2zgV<<(3z=y1WsCDN36mSO#}q^6Nf-^;nR=;+fEdn7z8RbT3b)mMmG8{! zpCt~8m%`UF}fk&var2C1(I2&{_oZzMIDuza#mH+q}U-_ac&92*2P#j4D^@jLDIUTYiXpv9i2)MD8& zHhb{lUGPur;hLX|a7K!sF)Hl++jT1xa#3c&oqMc zxCW!&*$l1gf!*7_PL7rK7Z__X2&8B$x6gQ{yWe`TO`7SD{89EEM$i6ft&GnW6>Hvc zg=}gd=}W)!Wf)RMce%1KAtRW7x>#M%k#r5BEtV&q<>hJwutzk5@KYr^`KeTcF$ZV#C@N5%2zV!qy0FOLz&c1N-w zY1Fz5>vW4|&Aw@W9>IQ~#}RU-)Re6+wq=A{vRJn`S%0Dkm7W?FbX=N5F-O*>MWJ3) z2yl91^Va*>?L-c7wjp_U*v?9W*#N% zL_I?A{sfa3ts_c3t>{1P&UaVY+z$(mPaF{HS5JDBkMS6ia((|{PiJ4RPKSqQUuabO zn`V*bgIJ9w#w=k$@7p4uI>%XWXR{SNDi%K1jSnR?c~f2kC1uj0gSHJWY4pS3`{3e= zeEGgDTcek++9T-EB&kR*_nGf|4j4u!m+pc!%?6obwYDfXO0=Z%Kvb49Pk)3&5_ zhgtz~I>C|LPN@4F1>U$yQiz)>I2(GC&d-@>fUm*R`tc1#mr%Y)okNJiYnb^5l)ndvyM9^lkYdV0sH#6>%r? z)h#J#H+bO?mx|Ih!Qw9QuA?~svzAiR5x>Q;P18d7_?Ep5eectW=)-@k1lJ|?Cmj>V zSMLVE@{ScJahT@1b?MU4=&3#QMfjt{zh+i8wR@T#ya#P0vgHNIKtX*+NOigfl+rC( z3YD0Xi$S54T1IGNseac<$c^oB9!%R$PwrDIiQ?{pMYJh>Tc1SFyV%&fepK82A5j2DtMt{lcEFh^)mK%UQwrU?6C9X`wmBHrdB=8LJ&92e0_n(CoNTg zdwtWMsqk+Hd+pN|d$qBY&dVIBzWK90yW5U=wpxhZayof1w)XOL9i1;~ zE%DE{)1c&y?>i0hhh)vAMG>ynR|u5JQbLgO?a$7kgR~Jeu~j~beTXgGF?4fwlWp(T zO=LTBgt_kwX1MrWN&RNA-a0sdi!39D+u?KE{~S%$34(L)5)3D zzjnOj%or0$8RzSYj8apG{Purc(A8jrmpcIGX9R@rI`GWA2siL_!w3i_fWs;R!W!hC1NHx39=iYf z()Z^0#XkixhzMMNEglx90f-0`@SMi~&o1&mZZopBFePFUJ;II`HbyuAJQKc?rqw7F zO`Se91^Dw3l;!ISMTpsl)*|2rEQUkCtE!(Z2 zpMDR3OG;fqSIH4(L^uF^puVRK)E{k>u0Q#X0T<0D0Eh;>;1Qgyd;!tt@EJO_0f_!< zrYkKSpG5v^xzm`;2Sooh<}u0wT_%S&UMb5`Ywsn(3Etm}6icIjE=Ce9HU52}tQ-tz zh8NzDOe~;F7I3>FFT$5E|E=?E=?GSn$bai``@gng{@>?q`(N5T$`L7Uk}6Kxf*KpP z`7`ydFt(q=%>)Pnpb_$7zsq{Rd-MQPe%=2;H<=I?&N_6)kxSkaC(JS7+P9SQ*B_2D zQapjns6)aqH$i<*pN4te_3M1gCzI;molr_Q6JNL>LH4^ytc_|=86b!OA5-yh4*Pm=C%&|W0iCFk{3MVB3ur@ z!%9q^&zTUG&S36ripOz`{Fp-alT>6S`szF0iL1M&k-Y_~{EdWkUo+(_sdS3vrfXn$ z-%qY>M-UP$r1==gqx#DANasRinu-MqxX2#7H;BmphSDUiYQR7;sA!ICZvo3{eN74E z<$BdMO@qfWR}V&Tag$z|?BtcCGaX$HzTfPi|Ld-1GH$IP`ZCLyn}_YGOmq?SPuBib z_%xCbn`FJJ1&ne>TGLxtxCAg#&WR>*q81u!dKWF^te4{NL@se%QvzA}y6* zt;&2a_Mt!9r4y5Ak($L=`-i-1wPu4O5oEaezm@P|VmJ8EO=Bc;jg0DE;46v@mnf71 z?@wHNnb(3=YA+smfM4Gx^Mt^{G}L5*S#-^@`IzLd7PK!=R$E*o8iSbFQ!GZ)vO=!{ zPnvcYv_HwPif%nQ5>h)M=10eF(mN6XB1_^jGYI`zXopR$QOeyzz!Y`Qud+K*_X7tjX|;~>Mh~( zR@(K@i%uaJ%kgH_cekn~F9Nvc8(Hr@>wn-NsktBG0vxWUW4ENG*->P?-VqR_DI1MK zk|*h0AJ&L5czBhBO;n0IzS7QhRk*IcgJVWZWjnKKLxs-9lc}w*Kmost(0u2&-+0dL zM(7W>oDhkVh=)dh)$`cbQre;FvWc7K@d|UK)JYaxV7S+AKIZ9>m#{ABzR~*v{H!kI<*$}X zb|UMwyvVmN6v0jFSN<6W9yCrxC$F^Y5-=R7$fo;UG;*gMz_Y*NIlh{DE~&EkyyH}@ z$5dI_&Cw5Lb$Es%4$uk$E~l%!5iH_3Mm%T|cV@-klwi`6D(ifZ!}@_$DT=_Mz<+#E zk8$K01wa1h<;RmY#DCmRKcEC{Q~u+8`UqeP#|Z!Ut*$hFLYwtSingDgZ|zHwyTw zi+6OQF7IZ)I~+fRfWE}c+~j>q7qGZ_P9UH;bO;bSh;ZiDGdFWFH?wBeT;*!#|JTmmJA1wF z`#s+Zv%%)B6f!Z3p$&T1X><25PJe{&<}95|@52(d#D7@&vx2^t;UBy$5;|d%JJ+6* zHpcyE>gj_yg(8kYDY0JSrW%aHZ#a-$Fo7crd;7^lZ`)gDk6tgCCTY)YryVc@gv39T z*9^$wKlG^^Cfgf$ys(dMT=9oDRB@7p2Qb{HAOn(&?RkpCk)`J_hc6Z{F+CaL2SiZ6 z!(Pl?ODCRHK6WUMHeKLB?VvY*{OHfct-jK%_?-;hs6t_NVR zR*wpNPMom4y>$12|F7TcknL>9kU^|9SxQaC$ut$VpAzk69cYF>ag5PTEzV8$W_B_w zC|EHPoTS_j%;hy6?%8trZDYK`_X*^wBoxjJ%3&n8`d0zua?f`KJ7niF?5z&9BL zK=7@8yL$?Y;u5ghyB{-l%q1))A9xx}Ip(<<_INh5dh7Na`W|x(Uqg5#dJjxE?T4wH ziYK?Hynf-(PmxdL2%qTw@M(hsc3))%DBYug ze=SQ~t{NfdOx*Wfe^#QLLSJ%x9RB;PAC%n7d8Pin$wY_C*U>vGUlsrae1`1}zrxFx zZAj_Vc=#E@$P^|?C1cbv`Lp1ImY=Un4`DaSNRh)iU&|xklH0>C%bH=9`U<=TpJ_|$ z=u*=F#_~6s_m6;c5Wx?~jLJV@9j$1+x-=0OXZ2ZyGBghKuwsD)9wTN@85+Y@uL7_*jkdqxK};S#I#jzlI)761kRRS zPWmZcMJ5C`(iNKOvvRG+oAIca&vxch{DNJ*3SwCIRE^AuizK5D5>sC$xe^D?P`3Sj z-$m7vB6tzIgP)7V3PO8_6j-q{M|-L1G_x`@EI!8*onDEC8$GMdhay748DLFw)lW*a zd^`d|Y`7uSH|eK_L2=7+1)+H;4PT$f+%{@T$Tle`W=`4A^NQ9mO6^RL=x}o9N6lDn zn3MgBDIiXo5pAdaZ79sXq_w)zFb?dSx0mTk?S`fx@+ZA%+;VTGr*v@px@>?Ex>lcF zo?pusA4_{z^E`L5zFH);b2Pd zivC6vvn*(TDS#D%-(1)l@IM1xBvrmldns?U%Q%J(7YHu_M<(EQTu4Z=xn`MA$5BNU zkriGKdeu1i#-U|8B*>oh2~^R(0c5L(I?Hp_Qqp`ILqgwIt2d5l)>;NO_-cKB@V;j< z?`M2;<79$dG*43=ZUyq*d6mx!58GG<8@=M36^Fx+V>7|1(GkY-!MPlD0b3r|KRhFQ zr12h~H|c)AsJl##bp22OnBnLRvA~pK2fho?PAzU4dg4VyvM1^p1djYyrkRnLbUMt3 zJ;hLUvmkI(<~kivon!k3rLAU(e`T_KvAnPVSuFz@N=Z}xU>R7nBe==B^POb6#`K}3 zY`)AwB}cp_oEoWkjo$;?C=3YCjV8xk31IFL_|t9qvq!KN+)$szg19+P=d)Ghm;Jw> z(ebFrX)YhiM0o40n3&)_ayoGG93+xDJ`mQjb$G~( zvI@*lDTMfE?gQ0bR~r@Vb?`i0%udM&8rcp@mg1@G`Y(E}}{HV>b)M z`mI`Cu;Dg!`=8UYXaxwfr&+(7=<2x^6OJCn0C~>fU7#iHg_#4CbufCS{Gig)bCXK7 zr5skN?4;-|IBhAHr>mmL43HAX8ub2X<2vvsMgQ%w9~aU*RL(H7z$cESL{%_i@XW|M zi|M}7{fpn`0m~h8sl!+Kko`;#b$kzX_UIq8rTd3g#i=&8<91|WZ*jm)`Jhys$KJ&z zPpoyzQ!AcaYAK+IGSNUny?QEX=l)mw| zxdDCW8FZ_}FyS?>o5m78=|+{>-O>mT2g_Np7O;s8(7uBsr?l4~LDwB)-;tmJfz>u`>2e z*6;1rXYJlNij!Vx5wtTM)dPI>*%zXV?!Ds^UF``;$Q6tjq+aakkpqmV+_;*$@ls;u zpkUsSyk1~2vHyv&v|Gsl!X(I#S=M0oF&At@SjUeVE6Tj+7cD_ay#nzQEU#h|*zZ1{ zeM{*PjZ9oWsC>_{xLuOeh9Ud)ubEh0dc2f*S5MbqOL6ej#K90k@A9_LS%$0^w+K;t zDB*0lGVjS(mG|yEyYIEi(L?>FSU;Bhk6pfo(^ql*m8OA8G4_b+`!*dSgwIPlKh-yJp5o3W1mdeBx$6 zjZqNI3S$uhp4xEWGTx-2m}$B={tu|$uE;-QAk)cTr9+ms*aUQ%A~QhiL>6*ac0Oe# zu-TTBXlb&QzYsJ;)}pCgCl;!O#*hTC+^Ag#q-nEGE zrnbm{toICavwK(kV%RKho*J)<7%pZSzmqe-;#Zzn%$gI#p)qUAZ`&ThVW#OVOAm_s z^!fxhukx>d>#9kui$LR-D^h>XyYQ%94cj?d20%yaNp@LJ78F=MWe8o*Fpen9p0qpt zyk<*nStgNVJHhaF6=;IA%A9P8b2-nX9$1x;VeuxmN3BuAa=EQ`%Ic?5l zXI@9znNMaaLhQk)>!8tSkKD(r_2NXkuuv}4)cr(D&%Yq~wmfp1@p|E0ecI32t8`J2 z1*`tXo^?U-`5>9_l@c-=?u&ZgJk3FM(tJCE?|6#7op?b+0)FrHM(1waU!M%DX4!D? zxyv6;P}TSBiUzzS7i;_f(In}EszH@Bx$&ix;qdN?SkOFeRQOs}=gW`Zt)Y-ggv$`k z*lgi86H=1Pz3L>Cn`csnL^$T$U5B(yi^N#=cpPj-Nz}H(cOEZDd!b88LmMK?52?q8 z6EjRxB^}g9B_D1L`<42d%*U2U6H~XvkC@S30aZqCKjEGWeqDkWls7Asi4kfD<&Wdy z-gH;l1Uvwf7B4Q*h_}Gx46}>nnVzgf_39E7zlZZ(=kuE6!J7)Xw$;Jjj>~|Yr--X9 zfk}s@QzHDHzv2+j=1VVLxB4_!ZDH>C4wUghln9_T}llu{`LTD1cTPkzL*&1j_D;d8;N`9HXgC<{E7PdxmO;)_1rVp z^~l{5IWX8AJ5`Rn5?WinH({TCe{AA#+yx#!X(6BNI>dRWeHAq~V$n4{gEDyBw$u1FrdE)w9oFJByafs%99~}nq~%q z*Y1$5>Z`D)Jt1!&92lDC>%j(+>nTRgnSVyC)U>AGlLjCXb;#E4}91y$egpFgxc3u0Wj-0+C-cPJdjrqxuJ@&K~fW*G}B-UJxX$P_bQEc@VzM z^5W3EUNG+2LOMrYu>a~81?W_hdhyu|2hQ;zA&}c)1G;dyGv{R*a7c&YeTxz8AD3B{ zD6whM#5byZUxzowKUp>>BIN6>6fSB`fv*a@5OJ5EbKY$8kcO5fc^7WzT;wQ?+-h(hzMK^b(N}71-n|OKt`1fy4@)N&L_{^N+3o|2Qp<0dT z!|FS;2hxHWl}%9RXuC12<}IUK&uIYWRCIj(23ByGFF)pnk;QWVtv^G{D*))p*@A-1 zYO(j@0wJ(JaX^eOc;e>u$D0D0xV%-ZUhrW{N#ORf{mJ&?!Z+_b@aVgd4{X^TeJYJP zkc~fAXTQN8j=@j(34{TZWQ>+UhM&}mG-1Z=YNd}A6#cW2lJG5uqBS-?HP^8_>gWsp zB11AsH+uMaJu{@VJAlyDRBImfiHg7v$8^vK$LfqsXH2}TLIe}NtHG1OJqeEhJkDs;bAJ_T7M4G|HSMrg<7vO(jjZR+%$z}$zSLMxY z!E6e;x_oa!0d!B|MXO*bWJO*6{DQ$=iPvJAIjgLw&bw}uMh<0vFCFlRIfpZVBBH|c z)BF5jCcKq~^+@{P$V)-|ioU@uke_8qt0wyq0(2OyL6?ksj&=^*-8PSp3~w#oxx}+; z5m-St+=UKiAWx_MMalmdgd~u0jVwHkm{TgMRuU-F-(2lFKS+&(KWOWutd4RM5!Tw} zf#o|Kv5^msd%Z-eq)9ZRW zd>Zv#{bP%sc>hg7&u|Knm(G|QBCSd-@DKkalF)VaDq!$sM{qIgw^33%d4vRj%W zq;t*QQ{v$1Ra{Rz%P@7P>Z_KhJDJt#i^rT1I7aXyshCoBsLg2+M_Y&;gw=if&aK4DR9raGzY zJ+rQuKDMzfra+HpWCSOIaUreG?fPD zR=dZBQW>(O`r@KOskh^l*!VYe9}JIf{p@E;@7-vc*;ju&T~9qJsQ;h=KL*h-=nPwl>DUaH4}aISOG%@IH?cAd z>!{&oo4aHfc*q&-%~n7MZ1ZPvbKGchGaTWkd4b5-to7l%NTPkV7vhZe!!OnO{a$ES z?sta~mF_>)VS27EU(?r$_15aLHE^ z+Te_`o$=jZ6_5IstY4&v#fX7ujbidKMec5w*4B#zWfBIcR_a{GmavbX;HuN2!geP* z>so|P0x|fOn3G=F#QpSJto^|q)@U+ot^+@5PRc~UY($q1=V`AVe)ZX413$QmF6#mb)(fh876~^7QDrUDW<1Vqa-t0%X;fzqv-V^E5uAsp`L_>py;r zO#hm(FYABFeGm74soNiC)r}ym|I(5E&%U1Pf}Wn;#gE#*9`&DI@`VOE;GG3)2~+{1 ze)JcvQz_H`uVp*_M}d(4$3eUQP!$PZIKLR|y9GpAgb~`05zdYX7l@OO3-ED4P8M`W zT2@|K9%Lq~3;`)Z>1<<1), + size: size, + } + + for i, leaf := range leaves { + mk.leaf[i] = leaf + } + + if size == 1 { + mk.root = mk.leaf[0] + return mk + } + + // copy the last hash if original size is odd number + if size != len(mk.leaf) { + mk.leaf[size] = mk.leaf[size-1] + mk.size = len(mk.leaf) + } + + return mk +} + +// HashTree calculates the root hash of a merkle tree +func (mk *Merkle) HashTree() Hash32B { + if mk.root != ZeroHash32B { + return mk.root + } + + length := mk.size >> 1 + merkle := make([]Hash32B, length) + + // first round, compute hash from original leaf + for i := 0; i < length; i++ { + h := mk.leaf[i<<1][:] + h = append(h, mk.leaf[i<<1+1][:]...) + merkle[i] = blake2b.Sum256(h) + } + + for length > 1 { + if length&1 != 0 { + merkle = append(merkle, merkle[length-1]) + length++ + } + + length >>= 1 + for i := 0; i < length; i++ { + h := merkle[i<<1][:] + h = append(h, merkle[i<<1+1][:]...) + merkle[i] = blake2b.Sum256(h) + } + merkle = merkle[0:length] + } + + mk.root = merkle[0] + return mk.root +} diff --git a/crypto/merkle_test.go b/crypto/merkle_test.go new file mode 100644 index 0000000000..ea82598329 --- /dev/null +++ b/crypto/merkle_test.go @@ -0,0 +1,69 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package crypto + +import ( + "bytes" + "encoding/hex" + "testing" + + "github.com/stretchr/testify/assert" +) + +func decodeHash(in string) [32]byte { + hash, _ := hex.DecodeString(in) + var arr [32]byte + copy(arr[:], hash[:32]) + return arr +} + +func TestMerkleTree(t *testing.T) { + var inputs []Hash32B + + // with no leave + m := NewMerkleTree(inputs) + assert.Nil(t, m) + + // with one leave + inputs = append(inputs, decodeHash("aeedd06eb44f08abbcc72a2293aff580f13662fa59cc1b0aa4a15ee7c118e4eb")) + m = NewMerkleTree(inputs) + expected := decodeHash("aeedd06eb44f08abbcc72a2293aff580f13662fa59cc1b0aa4a15ee7c118e4eb") + actual1 := m.HashTree() + assert.Equal(t, 0, bytes.Compare(expected[:], actual1[:])) + + // with two leaves + inputs = append(inputs, decodeHash("9de6306b08158c423330f7a27243a1a5cbe39bfd764f07818437882d21241567")) + m = NewMerkleTree(inputs) + expected = decodeHash("0bbcd13e801fdf4d70c62b3788173edaf52113e7bfca4603eb5d486f4a011411") + actual2 := m.HashTree() + assert.Equal(t, 0, bytes.Compare(expected[:], actual2[:])) + assert.Equal(t, -1, bytes.Compare(actual2[:], actual1[:])) + + // with three leaves + inputs = append(inputs, decodeHash("7959228bfdb316949973c08d8bb7bea2a21227a7b4ed85c35d247bf3d6b15a11")) + m = NewMerkleTree(inputs) + expected = decodeHash("94ecbea840734c55eaac296c10d3a11715c3a2081f5bd0323d80d8ede5db029a") + actual3 := m.HashTree() + assert.Equal(t, 0, bytes.Compare(expected[:], actual3[:])) + assert.Equal(t, 1, bytes.Compare(actual3[:], actual2[:])) + + // with four leaves, where 3rd leave == 4th leave + inputs = append(inputs, decodeHash("7959228bfdb316949973c08d8bb7bea2a21227a7b4ed85c35d247bf3d6b15a11")) + m = NewMerkleTree(inputs) + expected = decodeHash("94ecbea840734c55eaac296c10d3a11715c3a2081f5bd0323d80d8ede5db029a") + actual4 := m.HashTree() + assert.Equal(t, 0, bytes.Compare(expected[:], actual4[:])) + assert.Equal(t, 0, bytes.Compare(actual4[:], actual3[:])) + + // with five leaves, where 3rd leave == 4th leave + inputs = append(inputs, decodeHash("6368616e676520746869732070617373776f726420746f206120736563726574")) + m = NewMerkleTree(inputs) + expected = decodeHash("6bef5fe8bccd931a37bb0453f71a277ffb8651daf4e4ce08a6f75a1621ecbf5a") + actual5 := m.HashTree() + assert.Equal(t, 0, bytes.Compare(expected[:], actual5[:])) + assert.Equal(t, -1, bytes.Compare(actual5[:], actual4[:])) +} diff --git a/crypto/sig.go b/crypto/sig.go new file mode 100644 index 0000000000..3ebbf0624d --- /dev/null +++ b/crypto/sig.go @@ -0,0 +1,34 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package crypto + +import ( + "crypto/rand" + + "golang.org/x/crypto/ed25519" +) + +// NewKeyPair wraps ed25519.GenerateKey() for now. +func NewKeyPair() ([]byte, []byte, error) { + pub, priv, err := ed25519.GenerateKey(rand.Reader) + if err != nil { + return nil, nil, err + } + return pub, priv, nil +} + +// Sign wraps ed25519.Sign() for now. +func Sign(priv []byte, msg []byte) []byte { + p := ed25519.PrivateKey(priv) + return ed25519.Sign(p, msg) +} + +// Verify wraps ed25519.Verify(0 for now. +func Verify(pub []byte, msg, sig []byte) bool { + p := ed25519.PublicKey(pub) + return ed25519.Verify(p, msg, sig) +} diff --git a/crypto/sig_test.go b/crypto/sig_test.go new file mode 100644 index 0000000000..c2d8d152f1 --- /dev/null +++ b/crypto/sig_test.go @@ -0,0 +1,27 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package crypto + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestSignVerify(t *testing.T) { + pub, pri, err := NewKeyPair() + assert.Nil(t, err) + + message := []byte("hello iotex message") + sig := Sign(pri, message) + fmt.Println("signature created: ", sig) + assert.True(t, Verify(pub, message, sig)) + + wrongMessage := []byte("wrong message") + assert.False(t, Verify(pub, wrongMessage, sig)) +} diff --git a/delegate/delegate.go b/delegate/delegate.go new file mode 100644 index 0000000000..483b4af4b0 --- /dev/null +++ b/delegate/delegate.go @@ -0,0 +1,58 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package delegate + +import ( + "net" + + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/common/service" + "github.com/iotexproject/iotex-core/config" +) + +// Pool is the interface +type Pool interface { + // AllDelegates returns all delegates + AllDelegates() ([]net.Addr, error) + + // AnotherDelegate return another delegate that is not the passed-in address + AnotherDelegate(self string) net.Addr +} + +// ConfigBasedPool is the simple delegate pool implementing Pool interface +type ConfigBasedPool struct { + service.AbstractService + delegates []net.Addr +} + +// NewConfigBasedPool creates an instance of config-based delegate pool +func NewConfigBasedPool(delegate *config.Delegate) *ConfigBasedPool { + cbdp := &ConfigBasedPool{} + encountered := map[string]bool{} // dedup + for _, addr := range delegate.Addrs { + if encountered[addr] == false { + encountered[addr] = true + cbdp.delegates = append(cbdp.delegates, cm.NewTCPNode(addr)) + } + } + return cbdp +} + +// AllDelegates implements getting the delegates from config +func (cbdp *ConfigBasedPool) AllDelegates() ([]net.Addr, error) { + return cbdp.delegates, nil +} + +// AnotherDelegate return the first delegate that is not the passed-in address +func (cbdp *ConfigBasedPool) AnotherDelegate(self string) net.Addr { + for _, v := range cbdp.delegates { + if self != v.String() { + return v + } + } + return nil +} diff --git a/delegate/delegate_test.go b/delegate/delegate_test.go new file mode 100644 index 0000000000..bf555fc973 --- /dev/null +++ b/delegate/delegate_test.go @@ -0,0 +1,44 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package delegate + +import ( + "fmt" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/iotexproject/iotex-core/config" +) + +func TestConfigBasedPool(t *testing.T) { + cfg := config.Config{} + for i := 0; i < 4; i++ { + cfg.Delegate.Addrs = append(cfg.Delegate.Addrs, fmt.Sprintf("127.0.0.1:1000%d", i)) + } + + // duplicates should be ignored + for i := 0; i < 4; i++ { + cfg.Delegate.Addrs = append(cfg.Delegate.Addrs, fmt.Sprintf("127.0.0.1:1000%d", i)) + } + + cbdp := NewConfigBasedPool(&cfg.Delegate) + cbdp.Init() + cbdp.Start() + defer cbdp.Stop() + + delegates, err := cbdp.AllDelegates() + assert.Nil(t, err) + assert.Equal(t, 4, len(delegates)) + for i := 0; i < 4; i++ { + assert.Equal(t, "tcp", delegates[i].Network()) + assert.Equal(t, fmt.Sprintf("127.0.0.1:1000%d", i), delegates[i].String()) + } + + other := cbdp.AnotherDelegate("127.0.0.1:10000") + assert.Equal(t, fmt.Sprintf("127.0.0.1:10001"), other.String()) +} diff --git a/dispatcher/dispatcher.go b/dispatcher/dispatcher.go new file mode 100644 index 0000000000..8a6f187833 --- /dev/null +++ b/dispatcher/dispatcher.go @@ -0,0 +1,299 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package dispatcher + +import ( + "net" + "sync" + "sync/atomic" + + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/blocksync" + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/consensus" + "github.com/iotexproject/iotex-core/delegate" + pb "github.com/iotexproject/iotex-core/proto" + "github.com/iotexproject/iotex-core/txpool" +) + +// txMsg packages a proto tx message. +type txMsg struct { + tx *pb.TxPb + done chan bool +} + +// blockMsg packages a proto block message. +type blockMsg struct { + block *pb.BlockPb + blkType uint32 + done chan bool +} + +// blockSyncMsg packages a proto block sync message. +type blockSyncMsg struct { + sender string + sync *pb.BlockSync + done chan bool +} + +// dispatcher implements Dispatcher interface. +type dispatcher struct { + started int32 + shutdown int32 + newsChan chan interface{} + wg sync.WaitGroup + quit chan struct{} + + bs blocksync.BlockSync + cs consensus.Consensus + tp txpool.TxPool +} + +// NewDispatcher creates a new dispatcher +func NewDispatcher(cfg *config.Config, bc blockchain.IBlockchain, tp txpool.TxPool, bs blocksync.BlockSync, dp delegate.Pool) cm.Dispatcher { + if bc == nil || bs == nil { + glog.Fatal("Try to attach to a nil blockchain or a nil P2P") + return nil + } + + d := &dispatcher{ + newsChan: make(chan interface{}, 1024), + quit: make(chan struct{}), + tp: tp, + bs: bs, + } + + d.cs = consensus.NewConsensus(cfg, bc, tp, bs, dp) + return d +} + +// Start starts the dispatcher. +func (d *dispatcher) Start() error { + if atomic.AddInt32(&d.started, 1) != 1 { + return errors.New("Dispatcher already started") + } + + glog.Info("Starting dispatcher") + if err := d.cs.Start(); err != nil { + return err + } + + if err := d.bs.Start(); err != nil { + return err + } + + d.wg.Add(1) + go d.newsHandler() + return nil +} + +// Stop gracefully shuts down the dispatcher by stopping all handlers and waiting for them to finish. +func (d *dispatcher) Stop() error { + if atomic.AddInt32(&d.shutdown, 1) != 1 { + glog.Warning("Dispatcher already in the process of shutting down") + return nil + } + + glog.Infof("Dispatcher is shutting down") + if err := d.cs.Stop(); err != nil { + return err + } + + if err := d.bs.Stop(); err != nil { + return err + } + + close(d.quit) + d.wg.Wait() + return nil +} + +// newsHandler is the main handler for handling all news from peers. +func (d *dispatcher) newsHandler() { +loop: + for { + select { + case m := <-d.newsChan: + switch msg := m.(type) { + case *txMsg: + d.handleTxMsg(msg) + + case *blockMsg: + d.handleBlockMsg(msg) + + case *blockSyncMsg: + d.handleBlockSyncMsg(msg) + + default: + glog.Warning("Invalid message type in block handler: %T", msg) + } + + case <-d.quit: + break loop + } + } + + d.wg.Done() + glog.Info("News handler done") +} + +// handleTxMsg handles txMsg from all peers. +func (d *dispatcher) handleTxMsg(m *txMsg) { + tx := &blockchain.Tx{} + tx.ConvertFromTxPb(m.tx) + glog.Infof("receive txMsg, hash = %x", tx.Hash()) + + // dispatch to TxPool + if _, err := d.tp.ProcessTx(tx, true, true, 0); err != nil { + glog.Error(err) + } + + // signal to let caller know we are done + if m.done != nil { + m.done <- true + } + + return +} + +// handleBlockMsg handles blockMsg from peers. +func (d *dispatcher) handleBlockMsg(m *blockMsg) { + blk := &blockchain.Block{} + blk.ConvertFromBlockPb(m.block) + glog.Infof("receive blockMsg, block %d, hash = %x", blk.Height(), blk.HashBlock()) + + if m.blkType == pb.MsgBlockProtoMsgType { + if err := d.bs.ProcessBlock(blk); err != nil { + glog.Error(err) + } + } else if m.blkType == pb.MsgBlockSyncDataType { + if err := d.bs.ProcessBlockSync(blk); err != nil { + glog.Error(err) + } + } + + // signal to let caller know we are done + if m.done != nil { + m.done <- true + } + + return +} + +// handleBlockSyncMsg handles block messages from peers. +func (d *dispatcher) handleBlockSyncMsg(m *blockSyncMsg) { + glog.Infof("receive blockSyncMsg, addr = %s, start = %d, end = %d", m.sender, m.sync.Start, m.sync.End) + + // dispatch to block sync + if err := d.bs.ProcessSyncRequest(m.sender, m.sync); err != nil { + glog.Error(err) + } + + // signal to let caller know we are done + if m.done != nil { + m.done <- true + } + + return +} + +// dispatchTx adds the passed transaction message to the news handling queue. +func (d *dispatcher) dispatchTx(msg proto.Message, done chan bool) { + if atomic.LoadInt32(&d.shutdown) != 0 { + if done != nil { + close(done) + } + return + } + + d.newsChan <- &txMsg{(msg).(*pb.TxPb), done} +} + +// dispatchBlockCommit adds the passed block message to the news handling queue. +func (d *dispatcher) dispatchBlockCommit(msg proto.Message, done chan bool) { + if atomic.LoadInt32(&d.shutdown) != 0 { + if done != nil { + close(done) + } + return + } + + d.newsChan <- &blockMsg{(msg).(*pb.BlockPb), pb.MsgBlockProtoMsgType, done} +} + +// dispatchBlockSyncReq adds the passed block sync request to the news handling queue. +func (d *dispatcher) dispatchBlockSyncReq(sender string, msg proto.Message, done chan bool) { + if atomic.LoadInt32(&d.shutdown) != 0 { + if done != nil { + close(done) + } + return + } + + d.newsChan <- &blockSyncMsg{sender, (msg).(*pb.BlockSync), done} +} + +// dispatchBlockSyncData handles block sync data +func (d *dispatcher) dispatchBlockSyncData(msg proto.Message, done chan bool) { + if atomic.LoadInt32(&d.shutdown) != 0 { + if done != nil { + close(done) + } + return + } + + data := (msg).(*pb.BlockContainer) + d.newsChan <- &blockMsg{data.Block, pb.MsgBlockSyncDataType, done} +} + +// HandleBroadcast handles incoming broadcast message + +func (d *dispatcher) HandleBroadcast(message proto.Message, done chan bool) { + msgType, err := pb.GetTypeFromProtoMsg(message) + if err != nil { + glog.Warning("unexpected message handled by HandleBroadcast: ", err.Error()) + } + + switch msgType { + case pb.ViewChangeMsgType: + d.cs.HandleViewChange(message, done) + break + case pb.MsgTxProtoMsgType: + d.dispatchTx(message, done) + break + case pb.MsgBlockProtoMsgType: + d.dispatchBlockCommit(message, done) + break + default: + glog.Warning("unexpected msgType %v handled by HandleBroadcast", msgType) + } +} + +// HandleTell handles incoming unicast message +func (d *dispatcher) HandleTell(sender net.Addr, message proto.Message, done chan bool) { + msgType, err := pb.GetTypeFromProtoMsg(message) + if err != nil { + glog.Warning("unexpected message handled by HandleTell: ", err.Error()) + } + + glog.Info("dispatcher.HandleTell from", sender, message) + switch msgType { + case pb.MsgBlockSyncReqType: + d.dispatchBlockSyncReq(sender.String(), message, done) + case pb.MsgBlockSyncDataType: + d.dispatchBlockSyncData(message, done) + case pb.MsgBlockProtoMsgType: + d.cs.HandleBlockPropose(message, done) + default: + glog.Warning("unexpected msgType %v handled by HandleTell", msgType) + } +} diff --git a/dispatcher/dispatcher_test.go b/dispatcher/dispatcher_test.go new file mode 100644 index 0000000000..4568f1607f --- /dev/null +++ b/dispatcher/dispatcher_test.go @@ -0,0 +1,153 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package dispatcher + +import ( + "testing" + + "github.com/golang/mock/gomock" + "github.com/stretchr/testify/assert" + + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/proto" + "github.com/iotexproject/iotex-core/test/mock/mock_blockchain" + "github.com/iotexproject/iotex-core/test/mock/mock_blocksync" + "github.com/iotexproject/iotex-core/test/mock/mock_delegate" + "github.com/iotexproject/iotex-core/test/mock/mock_txpool" +) + +func TestNewDispatcher(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{Consensus: config.Consensus{Scheme: "NOOP"}} + bc := mock_blockchain.NewMockIBlockchain(ctrl) + tp := mock_txpool.NewMockTxPool(ctrl) + bs := mock_blocksync.NewMockBlockSync(ctrl) + dp := mock_delegate.NewMockPool(ctrl) + + d := NewDispatcher(cfg, bc, tp, bs, dp) + assert.NotNil(t, d) + + bs.EXPECT().Start().Times(1) + bs.EXPECT().Stop().Times(1) + d.Start() + defer d.Stop() +} + +func TestDispatchTxMsg(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{Consensus: config.Consensus{Scheme: "NOOP"}} + bc := mock_blockchain.NewMockIBlockchain(ctrl) + tp := mock_txpool.NewMockTxPool(ctrl) + bs := mock_blocksync.NewMockBlockSync(ctrl) + dp := mock_delegate.NewMockPool(ctrl) + + d := NewDispatcher(cfg, bc, tp, bs, dp) + assert.NotNil(t, d) + + bs.EXPECT().Start().Times(1) + bs.EXPECT().Stop().Times(1) + d.Start() + defer d.Stop() + + done := make(chan bool, 1000) + tp.EXPECT().ProcessTx(gomock.Any(), gomock.Any(), gomock.Any(), gomock.Any()).Times(1000).Return(nil, nil) + for i := 0; i < 1000; i++ { + d.HandleBroadcast(&iproto.TxPb{}, done) + } + for i := 0; i < 1000; i++ { + <-done + } +} + +func TestDispatchBlockMsg(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{Consensus: config.Consensus{Scheme: "NOOP"}} + bc := mock_blockchain.NewMockIBlockchain(ctrl) + tp := mock_txpool.NewMockTxPool(ctrl) + bs := mock_blocksync.NewMockBlockSync(ctrl) + dp := mock_delegate.NewMockPool(ctrl) + + d := NewDispatcher(cfg, bc, tp, bs, dp) + assert.NotNil(t, d) + + bs.EXPECT().Start().Times(1) + bs.EXPECT().Stop().Times(1) + d.Start() + defer d.Stop() + + done := make(chan bool, 1000) + bs.EXPECT().ProcessBlock(gomock.Any()).Times(1000).Return(nil) + for i := 0; i < 1000; i++ { + d.HandleBroadcast(&iproto.BlockPb{}, done) + } + for i := 0; i < 1000; i++ { + <-done + } +} + +func TestDispatchBlockSyncReq(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{Consensus: config.Consensus{Scheme: "NOOP"}} + bc := mock_blockchain.NewMockIBlockchain(ctrl) + tp := mock_txpool.NewMockTxPool(ctrl) + bs := mock_blocksync.NewMockBlockSync(ctrl) + dp := mock_delegate.NewMockPool(ctrl) + + d := NewDispatcher(cfg, bc, tp, bs, dp) + assert.NotNil(t, d) + + bs.EXPECT().Start().Times(1) + bs.EXPECT().Stop().Times(1) + d.Start() + defer d.Stop() + + done := make(chan bool, 1000) + bs.EXPECT().ProcessSyncRequest(gomock.Any(), gomock.Any()).Times(1000).Return(nil) + for i := 0; i < 1000; i++ { + d.HandleTell(cm.NewTCPNode("192.168.0.0:10000"), &iproto.BlockSync{}, done) + } + for i := 0; i < 1000; i++ { + <-done + } +} + +func TestDispatchBlockSyncData(t *testing.T) { + ctrl := gomock.NewController(t) + defer ctrl.Finish() + + cfg := &config.Config{Consensus: config.Consensus{Scheme: "NOOP"}} + bc := mock_blockchain.NewMockIBlockchain(ctrl) + tp := mock_txpool.NewMockTxPool(ctrl) + bs := mock_blocksync.NewMockBlockSync(ctrl) + dp := mock_delegate.NewMockPool(ctrl) + + d := NewDispatcher(cfg, bc, tp, bs, dp) + assert.NotNil(t, d) + + bs.EXPECT().Start().Times(1) + bs.EXPECT().Stop().Times(1) + d.Start() + defer d.Stop() + + done := make(chan bool, 1000) + bs.EXPECT().ProcessBlockSync(gomock.Any()).Times(1000).Return(nil) + for i := 0; i < 1000; i++ { + d.HandleTell(cm.NewTCPNode("192.168.0.0:10000"), &iproto.BlockContainer{Block: &iproto.BlockPb{}}, done) + } + for i := 0; i < 1000; i++ { + <-done + } +} diff --git a/e2etests/config_local_delegate.yaml b/e2etests/config_local_delegate.yaml new file mode 100644 index 0000000000..831b52772f --- /dev/null +++ b/e2etests/config_local_delegate.yaml @@ -0,0 +1,48 @@ +# go-yaml expects the YAML field corresponding to a struct field to be lowercase. So if your struct field is +# UpdateInterval, the corresponding field in YAML is updateinterval. + +nodetype: "delegate" # should be one of "delegate", "full_node", and "lightweight" + +network: + addr: "127.0.0.1:4689" + msglogscleaninginterval: 2s + msglogretention: 10s + healthcheckinterval: 1s + silentinterval: 5s + peermaintainerinterval: 1s + allowmulticonnsperip: true + numpeerslowerbound: 5 + numpeersupperbound: 5 + pinginterval: 1s + ratelimitenabled: false + ratelimitpersec: 5 + ratelimitwindowsize: 60s + bootstrapnodes: + - "127.0.0.1:4689" + maxmsgsize: 10485760 + peerdiscovery: true + +chain: + chaindbpath: "./chain.db" + totalsupply: 10000000000 + blockreward: 5 + mineraddr: "io1qyqsyqcy6nm58gjd2wr035wz5eyd5uq47zyqpng3gxe7nh" + +consensus: + scheme: "STANDALONE" + rdpos: + unmatchedeventttl: 2s + acceptpropose: + ttl: 2s + acceptprevote: + ttl: 2s + acceptvote: + ttl: 2s + blockcreationinterval: 1s + +delegate: + addrs: + - "127.0.0.1:4689" + +rpc: + port: ":42124" diff --git a/e2etests/config_local_fullnode.yaml b/e2etests/config_local_fullnode.yaml new file mode 100644 index 0000000000..93a2c97238 --- /dev/null +++ b/e2etests/config_local_fullnode.yaml @@ -0,0 +1,49 @@ +# go-yaml expects the YAML field corresponding to a struct field to be lowercase. So if your struct field is +# UpdateInterval, the corresponding field in YAML is updateinterval. + + +nodetype: "full_node" # should be one of "delegate", "full_node", and "lightweight" + +network: + addr: "127.0.0.1:10000" + msglogscleaninginterval: 2s + msglogretention: 10s + healthcheckinterval: 1s + silentinterval: 5s + peermaintainerinterval: 1s + allowmulticonnsperip: true + numpeerslowerbound: 5 + numpeersupperbound: 5 + pinginterval: 1s + ratelimitenabled: false + ratelimitpersec: 5 + ratelimitwindowsize: 60s + bootstrapnodes: + - "127.0.0.1:4689" + maxmsgsize: 10485760 + peerdiscovery: true + +chain: + chaindbpath: "./db.test" + totalsupply: 10000000000 + blockreward: 5 + mineraddr: "io1qyqsyqcy6nm58gjd2wr035wz5eyd5uq47zyqpng3gxe7nh" + +consensus: + scheme: "NOOP" + rdpos: + unmatchedeventttl: 2s + acceptpropose: + ttl: 2s + acceptprevote: + ttl: 2s + acceptvote: + ttl: 2s + blockcreationinterval: 1s + +delegate: + addrs: + - "127.0.0.1:4689" + +rpc: + port: ":42124" diff --git a/e2etests/config_local_rdpos.yaml b/e2etests/config_local_rdpos.yaml new file mode 100644 index 0000000000..06c9c5f75c --- /dev/null +++ b/e2etests/config_local_rdpos.yaml @@ -0,0 +1,57 @@ +# go-yaml expects the YAML field corresponding to a struct field to be lowercase. So if your struct field is +# UpdateInterval, the corresponding field in YAML is updateinterval. + +nodetype: "INIT FROM CODE" # should be one of "delegate", "full_node", and "lightweight" + +network: + addr: "INIT FROM CODE" + msglogscleaninginterval: 2s + msglogretention: 10s + healthcheckinterval: 1s + silentinterval: 5s + peermaintainerinterval: 1s + allowmulticonnsperip: true + numpeerslowerbound: 6 + numpeersupperbound: 12 + pinginterval: 1s + ratelimitenabled: false + ratelimitpersec: 5 + ratelimitwindowsize: 60s + bootstrapnodes: + - "127.0.0.1:40000" + - "127.0.0.1:40001" + - "127.0.0.1:40002" + - "127.0.0.1:40003" + maxmsgsize: 10485760 + peerdiscovery: true + +chain: + chaindbpath: "../chain.db" + totalsupply: 10000000000 + blockreward: 5 + mineraddr: "io1qyqsyqcy6nm58gjd2wr035wz5eyd5uq47zyqpng3gxe7nh" + +consensus: + scheme: "RDPOS" + rdpos: + proposerrotation: + enabled: true + interval: 1.5s + unmatchedeventttl: 90ms + acceptpropose: + ttl: 90ms + acceptprevote: + ttl: 90ms + acceptvote: + ttl: 90ms + blockcreationinterval: 1s + +delegate: + addrs: + - "127.0.0.1:40000" + - "127.0.0.1:40001" + - "127.0.0.1:40002" + - "127.0.0.1:40003" + +rpc: + port: ":42124" diff --git a/e2etests/local_rdpos_test.go b/e2etests/local_rdpos_test.go new file mode 100644 index 0000000000..edf2c60ec2 --- /dev/null +++ b/e2etests/local_rdpos_test.go @@ -0,0 +1,63 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package e2etests + +import ( + "flag" + "strconv" + "testing" + + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/server/itx" + "github.com/stretchr/testify/assert" +) + +const ( + // localRDPoSConfig is for local RDPoS testing + localRDPoSConfig = "./config_local_rdpos.yaml" +) + +// 4 delegates and 3 full nodes +func TestLocalRDPoS(t *testing.T) { + if testing.Short() { + t.Skip("Skipping TestLocalRDPoS in short mode.") + } + + flag.Parse() + + cfg, err := config.LoadConfigWithPathWithoutValidation(localRDPoSConfig) + assert.Nil(t, err) + + var svrs []itx.Server + + for i := 0; i < 3; i++ { + cfg.Chain.ChainDBPath = "./test_fullnode_chain" + strconv.Itoa(i) + ".db" + cfg.NodeType = config.FullNodeType + cfg.Network.Addr = "127.0.0.1:5000" + strconv.Itoa(i) + svr := itx.NewServer(*cfg) + svr.Init() + svr.Start() + svrs = append(svrs, svr) + } + + for i := 0; i < 4; i++ { + cfg.Chain.ChainDBPath = "./test_delegate_chain" + strconv.Itoa(i) + ".db" + cfg.NodeType = config.DelegateType + cfg.Network.Addr = "127.0.0.1:4000" + strconv.Itoa(i) + cfg.Consensus.Scheme = "RDPOS" + svr := itx.NewServer(*cfg) + svr.Init() + svr.Start() + svrs = append(svrs, svr) + } + + for _, svr := range svrs { + defer svr.Stop() + } + + select {} +} diff --git a/e2etests/local_test.go b/e2etests/local_test.go new file mode 100644 index 0000000000..611495dd9e --- /dev/null +++ b/e2etests/local_test.go @@ -0,0 +1,267 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package e2etests + +import ( + "os" + "testing" + "time" + + "github.com/stretchr/testify/assert" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/blocksync" + cm "github.com/iotexproject/iotex-core/common" + cfg "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/delegate" + "github.com/iotexproject/iotex-core/dispatcher" + "github.com/iotexproject/iotex-core/network" + "github.com/iotexproject/iotex-core/proto" + ta "github.com/iotexproject/iotex-core/test/testaddress" + "github.com/iotexproject/iotex-core/txpool" +) + +const ( + localTestConfigPath = "../config.yaml" + testDBPath = "db.test" +) + +func TestLocalCommit(t *testing.T) { + defer os.Remove(testDBPath) + assert := assert.New(t) + + config, err := cfg.LoadConfigWithPathWithoutValidation(localTestConfigPath) + assert.Nil(err) + config.Network.BootstrapNodes = []string{"127.0.0.1:10000"} + config.Chain.ChainDBPath = testDBPath + config.Consensus.Scheme = "NOOP" + config.Chain.TotalSupply = 50 << 22 + config.Chain.BlockReward = 0 + config.Delegate.Addrs = []string{"127.0.0.1:10000"} + + // create Blockchain + bc := blockchain.CreateBlockchain(ta.Addrinfo["miner"].Address, config) + assert.NotNil(bc) + t.Log("Create blockchain pass") + defer bc.Close() + + assert.Nil(addTestingBlocks(bc)) + + // create TxPool + tp := txpool.New(bc) + assert.NotNil(tp) + + p1 := network.NewOverlay(&config.Network) + assert.NotNil(p1) + p1.Init() + p1.PRC.Addr = "127.0.0.1:10001" + p1.Start() + defer p1.Stop() + + p2 := network.NewOverlay(&config.Network) + assert.NotNil(p2) + p2.Init() + p2.Start() + defer p2.Stop() + + pool := delegate.NewConfigBasedPool(&config.Delegate) + pool.Init() + pool.Start() + defer pool.Stop() + + // create block sync + bs := blocksync.NewBlockSyncer(config, bc, tp, p2, pool) + assert.NotNil(bs) + + // create dispatcher + dp := dispatcher.NewDispatcher(config, bc, tp, bs, pool) + assert.NotNil(dp) + p1.AttachDispatcher(dp) + p2.AttachDispatcher(dp) + dp.Start() + defer dp.Stop() + + time.Sleep(time.Second) + + // check UTXO + change := bc.BalanceOf(ta.Addrinfo["alfa"].Address) + t.Logf("Alfa balance = %d", change) + + beta := bc.BalanceOf(ta.Addrinfo["bravo"].Address) + t.Logf("Bravo balance = %d", beta) + change += beta + + beta = bc.BalanceOf(ta.Addrinfo["charlie"].Address) + t.Logf("Charlie balance = %d", beta) + change += beta + + beta = bc.BalanceOf(ta.Addrinfo["delta"].Address) + t.Logf("Delta balance = %d", beta) + change += beta + + beta = bc.BalanceOf(ta.Addrinfo["echo"].Address) + t.Logf("Echo balance = %d", beta) + change += beta + + fox := bc.BalanceOf(ta.Addrinfo["foxtrot"].Address) + t.Logf("Foxtrot balance = %d", fox) + change += fox + + test := bc.BalanceOf(ta.Addrinfo["miner"].Address) + t.Logf("test balance = %d", test) + change += test + + assert.Equal(uint64(50<<22), change) + t.Log("Total balance match") + + if beta == 0 || fox == 0 || test == 0 { + return + } + + height := bc.TipHeight() + + // transaction 1 + // C --> A + payee := []*blockchain.Payee{} + payee = append(payee, &blockchain.Payee{ta.Addrinfo["alfa"].Address, 1}) + tx := bc.CreateTransaction(ta.Addrinfo["charlie"], 1, payee) + bc.Reset() + p1.Broadcast(tx.ConvertToTxPb()) + time.Sleep(time.Second << 1) + + blk1 := bc.MintNewBlock(tp.Txs(), ta.Addrinfo["miner"].Address, "") + hash1 := blk1.HashBlock() + + // transaction 2 + // F --> D + payee = nil + payee = append(payee, &blockchain.Payee{ta.Addrinfo["delta"].Address, 1}) + tx2 := bc.CreateTransaction(ta.Addrinfo["foxtrot"], 1, payee) + blk2 := blockchain.NewBlock(0, height+2, hash1, []*blockchain.Tx{tx2}) + hash2 := blk2.HashBlock() + bc.Reset() + p2.Broadcast(tx2.ConvertToTxPb()) + + // transaction 3 + // B --> B + payee = nil + payee = append(payee, &blockchain.Payee{ta.Addrinfo["bravo"].Address, 1}) + tx3 := bc.CreateTransaction(ta.Addrinfo["bravo"], 1, payee) + blk3 := blockchain.NewBlock(0, height+3, hash2, []*blockchain.Tx{tx3}) + hash3 := blk3.HashBlock() + bc.Reset() + p1.Broadcast(tx3.ConvertToTxPb()) + + // transaction 4 + // test --> E + payee = nil + payee = append(payee, &blockchain.Payee{ta.Addrinfo["echo"].Address, 1}) + tx4 := bc.CreateTransaction(ta.Addrinfo["miner"], 1, payee) + blk4 := blockchain.NewBlock(0, height+4, hash3, []*blockchain.Tx{tx4}) + bc.Reset() + p2.Broadcast(tx4.ConvertToTxPb()) + + // send block 2-4-1-3 out of order + p2.Broadcast(blk2.ConvertToBlockPb()) + p1.Broadcast(blk4.ConvertToBlockPb()) + p1.Broadcast(blk1.ConvertToBlockPb()) + p2.Broadcast(blk3.ConvertToBlockPb()) + time.Sleep(time.Second << 1) + + t.Log("----- Block height = ", bc.TipHeight()) + + // check UTXO + change = bc.BalanceOf(ta.Addrinfo["alfa"].Address) + t.Logf("Alfa balance = %d", change) + + beta = bc.BalanceOf(ta.Addrinfo["bravo"].Address) + t.Logf("Bravo balance = %d", beta) + change += beta + + beta = bc.BalanceOf(ta.Addrinfo["charlie"].Address) + t.Logf("Charlie balance = %d", beta) + change += beta + + beta = bc.BalanceOf(ta.Addrinfo["delta"].Address) + t.Logf("Delta balance = %d", beta) + change += beta + + beta = bc.BalanceOf(ta.Addrinfo["echo"].Address) + t.Logf("Echo balance = %d", beta) + change += beta + + beta = bc.BalanceOf(ta.Addrinfo["foxtrot"].Address) + t.Logf("Foxtrot balance = %d", beta) + change += beta + + beta = bc.BalanceOf(ta.Addrinfo["miner"].Address) + t.Logf("test balance = %d", beta) + change += beta + + assert.Equal(uint64(50<<22), change) + t.Log("Total balance match") +} + +func TestLocalSync(t *testing.T) { + defer os.Remove(testDBPath) + assert := assert.New(t) + + config, err := cfg.LoadConfigWithPathWithoutValidation(localTestConfigPath) + config.Delegate.Addrs = []string{"127.0.0.1:10000"} + assert.Nil(err) + config.Chain.ChainDBPath = testDBPath + config.Consensus.Scheme = "NOOP" + + // create Blockchain + bc := blockchain.CreateBlockchain(ta.Addrinfo["miner"].Address, config) + assert.NotNil(bc) + t.Log("Create blockchain pass") + defer bc.Close() + assert.Nil(addTestingBlocks(bc)) + + // create TxPool + tp := txpool.New(bc) + assert.NotNil(tp) + + // create 2 peers + p1 := network.NewOverlay(&config.Network) + assert.NotNil(p1) + p1.Init() + p1.PRC.Addr = "127.0.0.1:10001" + p1.Start() + defer p1.Stop() + + config.NodeType = cfg.DelegateType + p2 := network.NewOverlay(&config.Network) + assert.NotNil(p2) + p2.Init() + p2.Start() + defer p2.Stop() + + pool := delegate.NewConfigBasedPool(&config.Delegate) + pool.Init() + pool.Start() + defer pool.Stop() + + // create block sync + bs := blocksync.NewBlockSyncer(config, bc, tp, p2, pool) + assert.NotNil(bs) + + // create dispatcher + dp := dispatcher.NewDispatcher(config, bc, nil, bs, pool) + assert.NotNil(dp) + p2.AttachDispatcher(dp) + p1.AttachDispatcher(dp) + dp.Start() + defer dp.Stop() + + time.Sleep(time.Second) + + // P1 tell a block sync message + p1.Tell(cm.NewTCPNode(p2.PRC.Addr), &iproto.BlockSync{1, 10}) + time.Sleep(time.Second) +} diff --git a/e2etests/net_test.go b/e2etests/net_test.go new file mode 100644 index 0000000000..7cbeeec4a1 --- /dev/null +++ b/e2etests/net_test.go @@ -0,0 +1,75 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package e2etests + +import ( + "os" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/blocksync" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/delegate" + "github.com/iotexproject/iotex-core/dispatcher" + "github.com/iotexproject/iotex-core/network" + ta "github.com/iotexproject/iotex-core/test/testaddress" + "github.com/iotexproject/iotex-core/txpool" +) + +const ( + // localFullnodeConfig is the testnet config path + localFullnodeConfig = "./config_local_fullnode.yaml" +) + +func TestNetSync(t *testing.T) { + defer os.Remove(testDBPath) + assert := assert.New(t) + + config, err := config.LoadConfigWithPathWithoutValidation(localFullnodeConfig) + assert.Nil(err) + if testing.Short() { + t.Skip("Skipping the overlay test in short mode.") + } + + // create Blockchain + // create Blockchain + bc := blockchain.CreateBlockchain(ta.Addrinfo["miner"].Address, config) + assert.NotNil(bc) + t.Log("Create blockchain pass") + defer bc.Close() + + // create TxPool + tp := txpool.New(bc) + assert.NotNil(tp) + + // create client + p1 := network.NewOverlay(&config.Network) + assert.NotNil(p1) + p1.Init() + p1.Start() + defer p1.Stop() + + pool := delegate.NewConfigBasedPool(&config.Delegate) + pool.Init() + pool.Start() + defer pool.Stop() + + // create block sync + bs := blocksync.NewBlockSyncer(config, bc, tp, p1, pool) + assert.NotNil(bs) + + // create dispatcher + dp := dispatcher.NewDispatcher(config, bc, nil, bs, pool) + assert.NotNil(dp) + p1.AttachDispatcher(dp) + dp.Start() + defer dp.Stop() + + select {} +} diff --git a/e2etests/util.go b/e2etests/util.go new file mode 100644 index 0000000000..f24fd76e9c --- /dev/null +++ b/e2etests/util.go @@ -0,0 +1,69 @@ +package e2etests + +import ( + "github.com/iotexproject/iotex-core/blockchain" + ta "github.com/iotexproject/iotex-core/test/testaddress" +) + +func addTestingBlocks(bc *blockchain.Blockchain) error { + // Add block 1 + // test --> A, B, C, D, E, F + payee := []*blockchain.Payee{} + payee = append(payee, &blockchain.Payee{ta.Addrinfo["alfa"].Address, 20}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["bravo"].Address, 30}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["charlie"].Address, 50}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["delta"].Address, 70}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["echo"].Address, 110}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["foxtrot"].Address, 50 << 20}) + tx := bc.CreateTransaction(ta.Addrinfo["miner"], 280+50<<20, payee) + blk := bc.MintNewBlock([]*blockchain.Tx{tx}, ta.Addrinfo["miner"].Address, "") + if err := bc.AddBlockCommit(blk); err != nil { + return err + } + bc.Reset() + + // Add block 2 + // Charlie --> A, B, D, E, test + payee = nil + payee = append(payee, &blockchain.Payee{ta.Addrinfo["alfa"].Address, 1}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["bravo"].Address, 1}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["delta"].Address, 1}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["echo"].Address, 1}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["miner"].Address, 1}) + tx = bc.CreateTransaction(ta.Addrinfo["charlie"], 5, payee) + blk = bc.MintNewBlock([]*blockchain.Tx{tx}, ta.Addrinfo["miner"].Address, "") + if err := bc.AddBlockCommit(blk); err != nil { + return err + } + bc.Reset() + + // Add block 3 + // Delta --> B, E, F, test + payee = payee[1:] + payee[1] = &blockchain.Payee{ta.Addrinfo["echo"].Address, 1} + payee[2] = &blockchain.Payee{ta.Addrinfo["foxtrot"].Address, 1} + tx = bc.CreateTransaction(ta.Addrinfo["delta"], 4, payee) + blk = bc.MintNewBlock([]*blockchain.Tx{tx}, ta.Addrinfo["miner"].Address, "") + if err := bc.AddBlockCommit(blk); err != nil { + return err + } + bc.Reset() + + // Add block 4 + // Delta --> A, B, C, D, F, test + payee = nil + payee = append(payee, &blockchain.Payee{ta.Addrinfo["alfa"].Address, 2}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["bravo"].Address, 2}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["charlie"].Address, 2}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["delta"].Address, 2}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["foxtrot"].Address, 2}) + payee = append(payee, &blockchain.Payee{ta.Addrinfo["miner"].Address, 2}) + tx = bc.CreateTransaction(ta.Addrinfo["echo"], 12, payee) + blk = bc.MintNewBlock([]*blockchain.Tx{tx}, ta.Addrinfo["miner"].Address, "") + if err := bc.AddBlockCommit(blk); err != nil { + return err + } + bc.Reset() + + return nil +} diff --git a/glide.lock b/glide.lock new file mode 100644 index 0000000000..386939f4a2 --- /dev/null +++ b/glide.lock @@ -0,0 +1,95 @@ +hash: 1f3a7dc40014311721e56973a9fe42b4d7adc1dffeef87f36074ed0bf4fb238d +updated: 2018-04-10T22:35:21.902391-07:00 +imports: +- name: github.com/boltdb/bolt + version: 2f1ce7a837dcb8da3ec595b1dac9d0632f0f99e8 +- name: github.com/davecgh/go-spew + version: 346938d642f2ec3594ed81d874461961cd0faa76 + subpackages: + - spew +- name: github.com/golang/glog + version: 23def4e6c14b4da8ac2ed8007337bc5eb5007998 +- name: github.com/golang/mock + version: c34cdb4725f4c3844d095133c6e40e448b86589b + subpackages: + - gomock +- name: github.com/golang/protobuf + version: 1e59b77b52bf8e4b449a57e6f79f21226d571845 + subpackages: + - proto + - protoc-gen-go/descriptor + - ptypes + - ptypes/any + - ptypes/duration + - ptypes/timestamp +- name: github.com/pkg/errors + version: 645ef00459ed84a119197bfb8d8205042c6df63d +- name: github.com/stretchr/testify + version: 12b6f73e6084dad08a7c6e575284b177ecafbc71 + subpackages: + - assert +- name: golang.org/x/crypto + version: 650f4a345ab4e5b245a3034b110ebc7299e68186 + subpackages: + - blake2b + - ed25519 + - ed25519/internal/edwards25519 +- name: golang.org/x/net + version: 309822c5b9b9f80db67f016069a12628d94fad34 + subpackages: + - context + - http2 + - http2/hpack + - idna + - internal/timeseries + - lex/httplex + - trace +- name: golang.org/x/sys + version: 37707fdb30a5b38865cfb95e5aab41707daec7fd + subpackages: + - unix +- name: golang.org/x/text + version: e19ae1496984b1c655b8044a65c0300a3c878dd3 + subpackages: + - secure/bidirule + - transform + - unicode/bidi + - unicode/norm +- name: google.golang.org/genproto + version: a8101f21cf983e773d0c1133ebc5424792003214 + subpackages: + - googleapis/rpc/status +- name: google.golang.org/grpc + version: d11072e7ca9811b1100b80ca0269ac831f06d024 + subpackages: + - balancer + - balancer/base + - balancer/roundrobin + - codes + - connectivity + - credentials + - encoding + - encoding/proto + - grpclb/grpc_lb_v1/messages + - grpclog + - internal + - keepalive + - metadata + - naming + - peer + - reflection + - reflection/grpc_reflection_v1alpha + - resolver + - resolver/dns + - resolver/passthrough + - stats + - status + - tap + - transport +- name: gopkg.in/yaml.v2 + version: 5420a8b6744d3b0345ab293f6fcba19c978f1183 +testImports: +- name: github.com/pmezard/go-difflib + version: 792786c7400a136282c1664665ae0a8db921c6c2 + subpackages: + - difflib diff --git a/glide.yaml b/glide.yaml new file mode 100644 index 0000000000..3e1ac55fb6 --- /dev/null +++ b/glide.yaml @@ -0,0 +1,31 @@ +package: github.com/iotexproject/iotex-core +import: +- package: github.com/boltdb/bolt + version: ^1.3.1 +- package: github.com/davecgh/go-spew + version: ^1.1.0 + subpackages: + - spew +- package: golang.org/x/crypto + subpackages: + - blake2b + - ed25519 +- package: github.com/golang/protobuf + version: 1e59b77b52bf8e4b449a57e6f79f21226d571845 + subpackages: + - proto +- package: github.com/stretchr/testify + version: ^1.2.0 + subpackages: + - assert +- package: github.com/golang/glog +- package: github.com/golang/mock + version: ^1.0.0 + subpackages: + - gomock +- package: google.golang.org/grpc + version: ^1.10.0 +- package: gopkg.in/yaml.v2 + version: ^2.1.1 +- package: github.com/pkg/errors + version: ^0.8.0 diff --git a/iotxaddress/bech32/bech32.go b/iotxaddress/bech32/bech32.go new file mode 100644 index 0000000000..7c10c4554a --- /dev/null +++ b/iotxaddress/bech32/bech32.go @@ -0,0 +1,265 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +// Package bech32 includes a Bech32 string which is at most 90 characters long and consists of: +// The human-readable part, which is intended to convey the type of data, or +// anything else that is relevant to the reader. This part MUST contain 1 to +// 83 US-ASCII characters, with each character having a value in the range +// [33-126]. HRP validity may be further restricted by specific applications. +// +// The separator, which is always "1". In case "1" is allowed inside the +// human-readable part, the last one in the string is the separator[3]. +// +// The data part, which is at least 6 characters long and only consists of +// alphanumeric characters excluding "1", "b", "i", and "o"[4]. +// +package bech32 + +import ( + "fmt" + "strings" +) + +const charset = "qpzry9x8gf2tvdw0s3jn54khce6mua7l" + +var gen = []int{0x3b6a57b2, 0x26508e6d, 0x1ea119fa, 0x3d4233dd, 0x2a1462b3} + +// Decode decodes a bech32 encoded string, returning the human-readable +// part and the data part excluding the checksum. +func Decode(bech string) (string, []byte, error) { + // The maximum allowed length for a bech32 string is 90. It must also + // be at least 8 characters, since it needs a non-empty HRP, a + // separator, and a 6 character checksum. + if len(bech) < 8 || len(bech) > 90 { + return "", nil, fmt.Errorf("invalid bech32 string length %d", + len(bech)) + } + // Only ASCII characters between 33 and 126 are allowed. + for i := 0; i < len(bech); i++ { + if bech[i] < 33 || bech[i] > 126 { + return "", nil, fmt.Errorf("invalid character in "+ + "string: '%c'", bech[i]) + } + } + + // The characters must be either all lowercase or all uppercase. + lower := strings.ToLower(bech) + upper := strings.ToUpper(bech) + if bech != lower && bech != upper { + return "", nil, fmt.Errorf("string not all lowercase or all " + + "uppercase") + } + + bech = lower + + // The string is invalid if the last '1' is non-existent, it is the + // first character of the string (no human-readable part) or one of the + // last 6 characters of the string (since checksum cannot contain '1'), + // or if the string is more than 90 characters in total. + one := strings.LastIndexByte(bech, '1') + if one < 1 || one+7 > len(bech) { + return "", nil, fmt.Errorf("invalid index of 1") + } + + // The human-readable part is everything before the last '1'. + hrp := bech[:one] + data := bech[one+1:] + + // Each character corresponds to the byte with value of the index in + // 'charset'. + decoded, err := toBytes(data) + if err != nil { + return "", nil, fmt.Errorf("failed converting data to bytes: "+ + "%v", err) + } + + if !bech32VerifyChecksum(hrp, decoded) { + moreInfo := "" + checksum := bech[len(bech)-6:] + expected, err := toChars(bech32Checksum(hrp, + decoded[:len(decoded)-6])) + if err == nil { + moreInfo = fmt.Sprintf("Expected %v, got %v.", + expected, checksum) + } + return "", nil, fmt.Errorf("checksum failed. " + moreInfo) + } + + // We exclude the last 6 bytes, which is the checksum. + return hrp, decoded[:len(decoded)-6], nil +} + +// Encode encodes a byte slice into a bech32 string with the +// human-readable part hrb. Note that the bytes must each encode 5 bits +// (base32). +func Encode(hrp string, data []byte) (string, error) { + // Calculate the checksum of the data and append it at the end. + checksum := bech32Checksum(hrp, data) + combined := append(data, checksum...) + + // The resulting bech32 string is the concatenation of the hrp, the + // separator 1, data and checksum. Everything after the separator is + // represented using the specified charset. + dataChars, err := toChars(combined) + if err != nil { + return "", fmt.Errorf("unable to convert data bytes to chars: "+ + "%v", err) + } + return hrp + "1" + dataChars, nil +} + +// toBytes converts each character in the string 'chars' to the value of the +// index of the corresponding character in 'charset'. +func toBytes(chars string) ([]byte, error) { + decoded := make([]byte, 0, len(chars)) + for i := 0; i < len(chars); i++ { + index := strings.IndexByte(charset, chars[i]) + if index < 0 { + return nil, fmt.Errorf("invalid character not part of "+ + "charset: %v", chars[i]) + } + decoded = append(decoded, byte(index)) + } + return decoded, nil +} + +// toChars converts the byte slice 'data' to a string where each byte in 'data' +// encodes the index of a character in 'charset'. +func toChars(data []byte) (string, error) { + result := make([]byte, 0, len(data)) + for _, b := range data { + if int(b) >= len(charset) { + return "", fmt.Errorf("invalid data byte: %v", b) + } + result = append(result, charset[b]) + } + return string(result), nil +} + +// ConvertBits converts a byte slice where each byte is encoding fromBits bits, +// to a byte slice where each byte is encoding toBits bits. +func ConvertBits(data []byte, fromBits, toBits uint8, pad bool) ([]byte, error) { + if fromBits < 1 || fromBits > 8 || toBits < 1 || toBits > 8 { + return nil, fmt.Errorf("only bit groups between 1 and 8 allowed") + } + + // The final bytes, each byte encoding toBits bits. + var regrouped []byte + + // Keep track of the next byte we create and how many bits we have + // added to it out of the toBits goal. + nextByte := byte(0) + filledBits := uint8(0) + + for _, b := range data { + + // Discard unused bits. + b = b << (8 - fromBits) + + // How many bits remaining to extract from the input data. + remFromBits := fromBits + for remFromBits > 0 { + // How many bits remaining to be added to the next byte. + remToBits := toBits - filledBits + + // The number of bytes to next extract is the minimum of + // remFromBits and remToBits. + toExtract := remFromBits + if remToBits < toExtract { + toExtract = remToBits + } + + // Add the next bits to nextByte, shifting the already + // added bits to the left. + nextByte = (nextByte << toExtract) | (b >> (8 - toExtract)) + + // Discard the bits we just extracted and get ready for + // next iteration. + b = b << toExtract + remFromBits -= toExtract + filledBits += toExtract + + // If the nextByte is completely filled, we add it to + // our regrouped bytes and start on the next byte. + if filledBits == toBits { + regrouped = append(regrouped, nextByte) + filledBits = 0 + nextByte = 0 + } + } + } + + // We pad any unfinished group if specified. + if pad && filledBits > 0 { + nextByte = nextByte << (toBits - filledBits) + regrouped = append(regrouped, nextByte) + filledBits = 0 + nextByte = 0 + } + + // Any incomplete group must be <= 4 bits, and all zeroes. + if filledBits > 0 && (filledBits > 4 || nextByte != 0) { + return nil, fmt.Errorf("invalid incomplete group") + } + + return regrouped, nil +} + +// For more details on the checksum calculation, please refer to BIP 173. +func bech32Checksum(hrp string, data []byte) []byte { + // Convert the bytes to list of integers, as this is needed for the + // checksum calculation. + integers := make([]int, len(data)) + for i, b := range data { + integers[i] = int(b) + } + values := append(bech32HrpExpand(hrp), integers...) + values = append(values, []int{0, 0, 0, 0, 0, 0}...) + polymod := bech32Polymod(values) ^ 1 + var res []byte + for i := 0; i < 6; i++ { + res = append(res, byte((polymod>>uint(5*(5-i)))&31)) + } + return res +} + +// For more details on the polymod calculation, please refer to BIP 173. +func bech32Polymod(values []int) int { + chk := 1 + for _, v := range values { + b := chk >> 25 + chk = (chk&0x1ffffff)<<5 ^ v + for i := 0; i < 5; i++ { + if (b>>uint(i))&1 == 1 { + chk ^= gen[i] + } + } + } + return chk +} + +// For more details on HRP expansion, please refer to BIP 173. +func bech32HrpExpand(hrp string) []int { + v := make([]int, 0, len(hrp)*2+1) + for i := 0; i < len(hrp); i++ { + v = append(v, int(hrp[i]>>5)) + } + v = append(v, 0) + for i := 0; i < len(hrp); i++ { + v = append(v, int(hrp[i]&31)) + } + return v +} + +// For more details on the checksum verification, please refer to BIP 173. +func bech32VerifyChecksum(hrp string, data []byte) bool { + integers := make([]int, len(data)) + for i, b := range data { + integers[i] = int(b) + } + concat := append(bech32HrpExpand(hrp), integers...) + return bech32Polymod(concat) == 1 +} diff --git a/iotxaddress/bech32/bech32_test.go b/iotxaddress/bech32/bech32_test.go new file mode 100644 index 0000000000..c92454e32a --- /dev/null +++ b/iotxaddress/bech32/bech32_test.go @@ -0,0 +1,70 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package bech32 + +import ( + "strings" + "testing" +) + +func TestBech32(t *testing.T) { + tests := []struct { + str string + valid bool + }{ + // Try some test vectors from https://github.com/bitcoin/bips/blob/master/bip-0173.mediawiki#Bech32 + {"A12UEL5L", true}, + {"a12uel5l", true}, + {"an83characterlonghumanreadablepartthatcontainsthenumber1andtheexcludedcharactersbio1tt5tgs", true}, + {"abcdef1qpzry9x8gf2tvdw0s3jn54khce6mua7lmqqqxw", true}, + {"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", true}, + {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", true}, + {"split1checkupstagehandshakeupstreamerranterredcaperred2y9e2w", false}, // invalid checksum + {"s lit1checkupstagehandshakeupstreamerranterredcaperredp8hs2p", false}, // invalid character (space) in hrp + {"spl" + string(127) + "t1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // invalid character (DEL) in hrp + {"split1cheo2y9e2w", false}, // invalid character (o) in data part + {"split1a2y9w", false}, // too short data part + {"1checkupstagehandshakeupstreamerranterredcaperred2y9e3w", false}, // empty hrp + {"li1dgmt3", false}, // Too short checksum + {"11qqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqsqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqqc8247j", false}, // overall max length exceeded + } + + for _, test := range tests { + str := test.str + hrp, decoded, err := Decode(str) + if !test.valid { + // Invalid string decoding should result in error. + if err == nil { + t.Errorf("expected decoding to fail for invalid string %v", test.str) + } + continue + } + + // Valid string decoding should result in no error. + if err != nil { + t.Errorf("expected string to be valid bech32: %v", err) + } + + // Check that it encodes to the same string + encoded, err := Encode(hrp, decoded) + if err != nil { + t.Errorf("encoding failed: %v", err) + } + + if encoded != strings.ToLower(str) { + t.Errorf("expected data to encode to %v, but got %v", str, encoded) + } + + // Flip a bit in the string an make sure it is caught. + pos := strings.LastIndexAny(str, "1") + flipped := str[:pos+1] + string((str[pos+1] ^ 1)) + str[pos+2:] + _, _, err = Decode(flipped) + if err == nil { + t.Error("expected decoding to fail") + } + } +} diff --git a/iotxaddress/iotxaddress.go b/iotxaddress/iotxaddress.go new file mode 100644 index 0000000000..799799d135 --- /dev/null +++ b/iotxaddress/iotxaddress.go @@ -0,0 +1,159 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +/* +IoTeX blockchain address is a Bech32 encoding of: +-- The human-readable part "io" for mainnet, and "it" for testnet. +-- The separator, as defined by Bech32 spec. +-- The data part is further consisted of: +---- 1 byte: version, starting with 0x01 +---- 4 bytes: chain identifier: 0x00000001 for the root chain and the remaining for subchains +---- Address on the specified blockchain +*/ + +package iotxaddress + +import ( + "errors" + + "golang.org/x/crypto/blake2b" + + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/iotxaddress/bech32" +) + +var ( + // ErrInvalidVersion is returned when invalid version has been detected. + ErrInvalidVersion = errors.New("invalid version") + // ErrInvalidChainID is returned when invalid chain ID has been detected. + ErrInvalidChainID = errors.New("invalid chain ID") +) + +const ( + mainnetPrefix = "io" + testnetPrefix = "it" +) + +// Address contains a pair of key and a string address +type Address struct { + PrivateKey []byte + PublicKey []byte + Address string +} + +// NewAddress returns a newly created public/private key pair together with the address derived. +func NewAddress(isTestnet bool, version byte, chainid []byte) (*Address, error) { + pub, pri, err := cp.NewKeyPair() + if err != nil { + return nil, err + } + + addr, err := GetAddress(pub, isTestnet, version, chainid) + if err != nil { + return nil, err + } + return &Address{PublicKey: pub, PrivateKey: pri, Address: addr}, nil +} + +// GetAddress returns the address given a public key and necessary params. +func GetAddress(pub []byte, isTestnet bool, version byte, chainid []byte) (string, error) { + if !isValidVersion(version) { + return "", ErrInvalidVersion + } + + if !isValidChainID(chainid) { + return "", ErrInvalidChainID + } + + hrp := mainnetPrefix + if isTestnet { + hrp = testnetPrefix + } + + payload := append([]byte{version}, append(chainid, HashPubKey(pub)...)...) + // Group the payload into 5 bit groups. + grouped, err := bech32.ConvertBits(payload, 8, 5, true) + if err != nil { + return "", err + } + addr, err := bech32.Encode(hrp, grouped) + if err != nil { + return "", err + } + return addr, nil +} + +// GetPubkeyHash extracts public key hash from address +func GetPubkeyHash(address string) []byte { + hrp, grouped, err := bech32.Decode(address) + if err != nil { + return nil + } + + // Exclude the separator, version and chainID + payload, err := bech32.ConvertBits(grouped[:], 5, 8, false) + if err != nil { + return nil + } + + if hrp != mainnetPrefix && hrp != testnetPrefix { + return nil + } + if !isValidVersion(payload[0]) { + return nil + } + if !isValidChainID(payload[1:5]) { + return nil + } + + return payload[5:25] +} + +// ValidateAddress check if address if valid. +func ValidateAddress(address string) bool { + hrp, grouped, err := bech32.Decode(address) + if err != nil { + return false + } + + // Exclude the separator, version and chainID + payload, err := bech32.ConvertBits(grouped[:], 5, 8, false) + if err != nil { + return false + } + + if hrp != mainnetPrefix && hrp != testnetPrefix { + return false + } + if !isValidVersion(payload[0]) { + return false + } + if !isValidChainID(payload[1:5]) { + return false + } + return true +} + +// HashPubKey returns the hash of public key +func HashPubKey(pubKey []byte) []byte { + // use Blake2b algorithm + digest := blake2b.Sum256(pubKey) + return digest[7:27] +} + +func isValidVersion(version byte) bool { + if version >= 0x01 { + return true + } + return false +} + +func isValidChainID(chainid []byte) bool { + if len(chainid) != 4 { + return false + } + return true +} diff --git a/iotxaddress/iotxaddress_test.go b/iotxaddress/iotxaddress_test.go new file mode 100644 index 0000000000..91fbffbdda --- /dev/null +++ b/iotxaddress/iotxaddress_test.go @@ -0,0 +1,56 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package iotxaddress + +import ( + "crypto/rand" + "strings" + "testing" + + "github.com/stretchr/testify/assert" + "golang.org/x/crypto/ed25519" +) + +// TestNewAddress tests create new asset address. +func TestNewAddress(t *testing.T) { + assert := assert.New(t) + addr, err := NewAddress(true, byte(0x01), []byte{0x00, 0x00, 0x00, 0x01}) + assert.Nil(err) + assert.NotNil(addr.PrivateKey) + assert.NotNil(addr.PublicKey) + assert.NotEqual("", addr.Address) + + t.Log("Generated address is ", addr.Address) + t.Logf("Generated public key = %x", addr.PublicKey) + t.Logf("Generated private key = %x", addr.PrivateKey) + + p2pkh := HashPubKey(addr.PublicKey) + if assert.Equal(p2pkh, GetPubkeyHash(addr.Address)) { + t.Logf("P2PKH = %x", p2pkh) + } + + rmsg := make([]byte, 2048) + rand.Read(rmsg) + + sig := ed25519.Sign(addr.PrivateKey, rmsg) + assert.True(ed25519.Verify(addr.PublicKey, rmsg, sig)) +} + +// TestGetAddress tests get address for a given public key and params. +func TestGetandValidateAddress(t *testing.T) { + assert := assert.New(t) + pub, _, err := ed25519.GenerateKey(rand.Reader) + assert.Nil(err) + + addr, err := GetAddress(pub, false, byte(0x01), []byte{0x00, 0x00, 0x00, 0x01}) + assert.Nil(err) + assert.True(strings.HasPrefix(addr, mainnetPrefix)) + assert.True(ValidateAddress(addr)) + + addr = strings.Replace(addr, "1", "?", -1) + assert.False(ValidateAddress(addr)) +} diff --git a/misc/scripts/mockgen.sh b/misc/scripts/mockgen.sh new file mode 100755 index 0000000000..ae89386dbe --- /dev/null +++ b/misc/scripts/mockgen.sh @@ -0,0 +1,42 @@ +#!/bin/bash + +rm -rf ./test/mock +mkdir -p ./test/mock + +mkdir -p ./test/mock/mock_dispatcher +mockgen -destination=./test/mock/mock_dispatcher/mock_dispatcher.go \ + -source=./common/idispatcher.go \ + -package=mock_dispatcher \ + Dispatcher + +mkdir -p ./test/mock/mock_rdpos +mockgen -destination=./test/mock/mock_rdpos/mock_rdpos.go \ + -source=./consensus/scheme/rdpos/rdpos.go \ + -package=mock_rdpos \ + DNet + +mkdir -p ./test/mock/mock_blockchain +mockgen -destination=./test/mock/mock_blockchain/mock_blockchain.go \ + -source=./blockchain/iblockchain.go \ + -imports =github.com/iotexproject/iotex-core/blockchain \ + -package=mock_blockchain \ + IBlockchain + +mkdir -p ./test/mock/mock_delegate +mockgen -destination=./test/mock/mock_delegate/mock_delegate.go \ + -source=./delegate/delegate.go \ + -package=mock_delegate \ + Delegate + +mkdir -p ./test/mock/mock_txpool +mockgen -destination=./test/mock/mock_txpool/mock_txpool.go \ + -source=./txpool/txpool.go \ + -imports =github.com/iotexproject/iotex-core/txpool \ + -package=mock_txpool \ + TxPool + +mkdir -p ./test/mock/mock_blocksync +mockgen -destination=./test/mock/mock_blocksync/mock_blocksync.go \ + -source=./blocksync/blocksync.go \ + -package=mock_blocksync \ + BlockSync diff --git a/misc/scripts/stringer.sh b/misc/scripts/stringer.sh new file mode 100644 index 0000000000..1bc61801a7 --- /dev/null +++ b/misc/scripts/stringer.sh @@ -0,0 +1,5 @@ +#!/bin/bash -ex +go get golang.org/x/tools/cmd/stringer +export PATH=$PATH:"$GOPATH/bin" +find . -name '*_string.go' -type f -not -path "./vendor/*" -not -path "$GOPATH/vendor/*" -delete +go generate `glide nv | grep -v go-build` diff --git a/network/gossip.go b/network/gossip.go new file mode 100644 index 0000000000..c027d0d05d --- /dev/null +++ b/network/gossip.go @@ -0,0 +1,124 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "encoding/hex" + "sync" + "time" + + "golang.org/x/crypto/blake2b" + + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/common/routine" + "github.com/iotexproject/iotex-core/common/service" + pb "github.com/iotexproject/iotex-core/network/proto" + pb1 "github.com/iotexproject/iotex-core/proto" +) + +// Gossip relays messages in the overlay (at least once semantics) +type Gossip struct { + service.CompositeService + Overlay *Overlay + Dispatcher cm.Dispatcher + MsgLogs sync.Map + CleanerTask *routine.RecurringTask +} + +// NewGossip generates a Gossip instance +func NewGossip(o *Overlay) *Gossip { + g := &Gossip{Overlay: o} + cleaner := NewMsgLogsCleaner(g) + cleanerTask := + routine.NewRecurringTask(cleaner, o.Config.MsgLogsCleaningInterval) + g.CleanerTask = cleanerTask + g.AddService(cleanerTask) + return g +} + +// AttachDispatcher attaches to a Dispatcher instance +func (g *Gossip) AttachDispatcher(dispatcher cm.Dispatcher) { + g.Dispatcher = dispatcher +} + +// OnReceivingMsg listens to and handles the incoming broadcast message +func (g *Gossip) OnReceivingMsg(msg *pb.BroadcastReq) error { + checksum := g.getBroadcastMsgChecksum(msg.MsgBody) + _, ok := g.MsgLogs.Load(checksum) + if ok { + return nil + } + // Call dispatch to notify that a new message comes in + err := g.processMsg(msg.MsgType, msg.MsgBody) + if err != nil { + return err + } + // Relay the message to the neighbors + err = g.relayMsg(msg.MsgType, msg.MsgBody, checksum) + if err != nil { + return nil + } + return nil +} + +func (g *Gossip) processMsg(msgType uint32, msgBody []byte) error { + protoMsg, err := pb1.TypifyProtoMsg(msgType, msgBody) + if err != nil { + return err + } + if g.Dispatcher != nil { + g.Dispatcher.HandleBroadcast(protoMsg, nil) + } + return nil +} + +func (g *Gossip) relayMsg(msgType uint32, msgBody []byte, checksum string) error { + // Record the message + g.storeBroadcastMsgChecksum(checksum) + // Send the message to all neighbors + g.Overlay.PM.Peers.Range(func(_, value interface{}) bool { + go func() { + value.(*Peer).BroadcastMsg(&pb.BroadcastReq{MsgType: msgType, MsgBody: msgBody}) + }() + return true + }) + return nil +} + +func (g *Gossip) getBroadcastMsgChecksum(msgBody []byte) string { + b := blake2b.Sum256(msgBody) + return hex.EncodeToString(b[:]) +} + +func (g *Gossip) storeBroadcastMsgChecksum(checksum string) { + g.MsgLogs.Store(checksum, time.Now()) +} + +// MsgLogsCleaner periodically refreshes the recent received message log +type MsgLogsCleaner struct { + G *Gossip +} + +// NewMsgLogsCleaner generates a MsgLogsCleaner instance +func NewMsgLogsCleaner(g *Gossip) *MsgLogsCleaner { + c := &MsgLogsCleaner{G: g} + return c +} + +// Do log cleaning +func (c *MsgLogsCleaner) Do() { + keys := []string{} + c.G.MsgLogs.Range(func(key, value interface{}) bool { + if time.Now().Sub(value.(time.Time)) > c.G.Overlay.Config.MsgLogRetention { + keys = append(keys, key.(string)) + } + return true + }) + for _, key := range keys { + c.G.MsgLogs.Delete(key) + } +} diff --git a/network/healthchecker.go b/network/healthchecker.go new file mode 100644 index 0000000000..56da7dc41f --- /dev/null +++ b/network/healthchecker.go @@ -0,0 +1,39 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "time" +) + +// HealthChecker will check its peers at constant interval. If a peer is found not reachable for given period, it would +// be removed from the peer list +type HealthChecker struct { + Overlay *Overlay + SilentInterval time.Duration +} + +// NewHealthChecker creates an instance of HealthChecker +func NewHealthChecker(o *Overlay) *HealthChecker { + hc := &HealthChecker{Overlay: o} + hc.SilentInterval = o.Config.SilentInterval + return hc +} + +// Do check peer health +func (hc *HealthChecker) Do() { + addrs := []string{} + hc.Overlay.PM.Peers.Range(func(key, value interface{}) bool { + if time.Now().Sub(value.(*Peer).LastResTime) > hc.SilentInterval { + addrs = append(addrs, key.(string)) + } + return true + }) + for _, addr := range addrs { + go hc.Overlay.PM.RemovePeer(addr) + } +} diff --git a/network/overlay.go b/network/overlay.go new file mode 100644 index 0000000000..c8c98f7df2 --- /dev/null +++ b/network/overlay.go @@ -0,0 +1,148 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "net" + "syscall" + + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/common/routine" + "github.com/iotexproject/iotex-core/common/service" + "github.com/iotexproject/iotex-core/config" + pb "github.com/iotexproject/iotex-core/network/proto" + "github.com/iotexproject/iotex-core/proto" +) + +var ( + // ErrPeerNotFound means the peer is not found + ErrPeerNotFound = errors.New("Peer not found") +) + +// Overlay represents the peer-to-peer network +type Overlay struct { + service.CompositeService + PM *PeerManager + PRC *RPCServer + Gossip *Gossip + Tasks []*routine.RecurringTask + Config *config.Network + Dispatcher cm.Dispatcher +} + +// NewOverlay creates an instance of Overlay +func NewOverlay(config *config.Network) *Overlay { + o := &Overlay{Config: config} + o.PRC = NewRPCServer(o) + o.PM = NewPeerManager(o, config.NumPeersLowerBound, config.NumPeersUpperBound) + o.Gossip = NewGossip(o) + o.AddService(o.PRC) + o.AddService(o.PM) + o.AddService(o.Gossip) + + o.addPingTask() + o.addHealthCheckTask() + if config.PeerDiscovery { + o.addPeerMaintainer() + } else { + o.addConfigBasedPeerMaintainer() + } + return o +} + +// AttachDispatcher attaches to a Dispatcher instance +func (o *Overlay) AttachDispatcher(dispatcher cm.Dispatcher) { + o.Dispatcher = dispatcher + o.Gossip.AttachDispatcher(dispatcher) +} + +func (o *Overlay) addPingTask() { + ping := NewPinger(o) + pingTask := routine.NewRecurringTask(ping, o.Config.PingInterval) + o.AddService(pingTask) + o.Tasks = append(o.Tasks, pingTask) +} + +func (o *Overlay) addHealthCheckTask() { + hc := NewHealthChecker(o) + hcTask := routine.NewRecurringTask(hc, o.Config.HealthCheckInterval) + o.AddService(hcTask) + o.Tasks = append(o.Tasks, hcTask) +} + +func (o *Overlay) addPeerMaintainer() { + pm := NewPeerMaintainer(o) + pmTask := routine.NewRecurringTask(pm, o.Config.PeerMaintainerInterval) + o.AddService(pmTask) + o.Tasks = append(o.Tasks, pmTask) +} + +func (o *Overlay) addConfigBasedPeerMaintainer() { + topology, err := config.LoadTopology(o.Config.TopologyPath) + if err != nil { + glog.Fatal(err.Error()) + syscall.Exit(syscall.SYS_EXIT) + } + cbpm := NewConfigBasedPeerMaintainer(o, topology) + cbpmTask := routine.NewRecurringTask(cbpm, o.Config.PeerMaintainerInterval) + o.AddService(cbpmTask) + o.Tasks = append(o.Tasks, cbpmTask) +} + +// Broadcast lets the caller to broadcast the message to all nodes in the P2P network +func (o *Overlay) Broadcast(msg proto.Message) error { + msgType, err := iproto.GetTypeFromProtoMsg(msg) + if err != nil { + return err + } + msgBody, err := proto.Marshal(msg) + if err != nil { + return err + } + // Source also needs to remember the message sent so that it wouldn't process it again + o.Gossip.relayMsg(msgType, msgBody, o.Gossip.getBroadcastMsgChecksum(msgBody)) + return nil +} + +// GetPeers returns the current neighbors' network identifiers +func (o *Overlay) GetPeers() []net.Addr { + nodes := []net.Addr{} + o.PM.Peers.Range(func(_, value interface{}) bool { + nodes = append(nodes, &cm.Node{Addr: value.(*Peer).String()}) + return true + }) + return nodes +} + +// Tell tells a given node a proto message +func (o *Overlay) Tell(node net.Addr, msg proto.Message) error { + peer := o.PM.GetOrAddPeer(node.String()) + if peer == nil { + return ErrPeerNotFound + } + glog.Info("node addr = ", peer.Addr) + msgType, err := iproto.GetTypeFromProtoMsg(msg) + if err != nil { + return err + } + msgBody, err := proto.Marshal(msg) + if err != nil { + return err + } + glog.Info("request addr = ", o.PRC.String()) + go peer.Tell(&pb.TellReq{Addr: o.PRC.String(), MsgType: msgType, MsgBody: msgBody}) + return nil +} + +// Self returns the PRC server address to receive messages +func (o *Overlay) Self() net.Addr { + return o.PRC +} diff --git a/network/overlay_test.go b/network/overlay_test.go new file mode 100644 index 0000000000..1567129fa6 --- /dev/null +++ b/network/overlay_test.go @@ -0,0 +1,401 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "fmt" + "io/ioutil" + "math" + "math/rand" + "net" + "os" + "sort" + "strconv" + "strings" + "testing" + "time" + + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/assert" + "gopkg.in/yaml.v2" + + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/proto" +) + +func LoadTestConfig(addr string, allowMultiConnsPerIP bool) *config.Network { + config := config.Config{ + NodeType: config.DelegateType, + Network: config.Network{ + Addr: addr, + MsgLogsCleaningInterval: 2 * time.Second, + MsgLogRetention: 10 * time.Second, + HealthCheckInterval: time.Second, + SilentInterval: 5 * time.Second, + PeerMaintainerInterval: time.Second, + NumPeersLowerBound: 5, + NumPeersUpperBound: 5, + AllowMultiConnsPerIP: allowMultiConnsPerIP, + RateLimitEnabled: false, + PingInterval: time.Second, + BootstrapNodes: []string{"127.0.0.1:10001", "127.0.0.1:10002"}, + MaxMsgSize: 1024 * 1024 * 10, + PeerDiscovery: true, + }, + } + return &config.Network +} + +func LoadTestConfigWithTLSEnabled(addr string, allowMultiConnsPerIP bool) *config.Network { + cfg := LoadTestConfig(addr, allowMultiConnsPerIP) + cfg.TLSEnabled = true + cfg.CACrtPath = "../test/assets/ssl/iotex.io.crt" + cfg.PeerCrtPath = "../test/assets/ssl/127.0.0.1.crt" + cfg.PeerKeyPath = "../test/assets/ssl/127.0.0.1.key" + return cfg +} + +type MockDispatcher struct { +} + +func (d *MockDispatcher) Start() error { + return nil +} + +func (d *MockDispatcher) Stop() error { + return nil +} + +func (d *MockDispatcher) HandleBroadcast(proto.Message, chan bool) { +} + +func (d *MockDispatcher) HandleTell(net.Addr, proto.Message, chan bool) { +} + +type MockDispatcher1 struct { + MockDispatcher + Count uint32 +} + +func (d1 *MockDispatcher1) HandleBroadcast(proto.Message, chan bool) { + d1.Count++ +} + +func TestOverlay(t *testing.T) { + if testing.Short() { + t.Skip("Skipping the overlay test in short mode.") + } + size := 10 + dps := []*MockDispatcher1{} + nodes := []*Overlay{} + for i := 0; i < size; i++ { + dp := &MockDispatcher1{} + dps = append(dps, dp) + var config *config.Network + if i == 0 { + config = LoadTestConfig("127.0.0.1:10001", true) + } else if i == 1 { + config = LoadTestConfig("127.0.0.1:10002", true) + } else { + config = LoadTestConfig("", true) + } + node := NewOverlay(config) + node.AttachDispatcher(dp) + node.Init() + node.Start() + nodes = append(nodes, node) + } + + defer func() { + for _, node := range nodes { + node.Stop() + } + }() + + time.Sleep(10 * time.Second) + for i := 0; i < size; i++ { + assert.True(t, lenSyncMap(nodes[i].PM.Peers) >= nodes[i].PM.NumPeersLowerBound) + } + + nodes[0].Broadcast(&iproto.TxPb{}) + time.Sleep(5 * time.Second) + for i, dp := range dps { + if i == 0 { + assert.Equal(t, uint32(0), dp.Count) + } else { + assert.Equal(t, uint32(1), dp.Count) + } + } +} + +type MockDispatcher2 struct { + MockDispatcher + T *testing.T + Count uint32 +} + +func (d2 *MockDispatcher2) HandleTell(sender net.Addr, message proto.Message, done chan bool) { + // Handle Tx Msg + msgType, err := iproto.GetTypeFromProtoMsg(message) + /* + switch (msgType) { + case iproto.MsgTxProtoMsgType: + break + default: + break + } + */ + assert.True(d2.T, strings.HasPrefix(sender.Network(), "tcp")) + assert.True(d2.T, strings.HasPrefix(sender.String(), "127.0.0.1")) + assert.Nil(d2.T, err) + assert.Equal(d2.T, iproto.MsgTxProtoMsgType, msgType) + d2.Count++ +} + +func TestTell(t *testing.T) { + if testing.Short() { + t.Skip("Skipping the overlay test in short mode.") + } + dp1 := &MockDispatcher2{T: t} + p1 := NewOverlay(LoadTestConfig("127.0.0.1:10001", true)) + p1.AttachDispatcher(dp1) + p1.Init() + p1.Start() + dp2 := &MockDispatcher2{T: t} + p2 := NewOverlay(LoadTestConfig("127.0.0.1:10002", true)) + p2.AttachDispatcher(dp2) + p2.Init() + p2.Start() + + time.Sleep(2 * time.Second) + + defer func() { + p1.Stop() + p2.Stop() + }() + + // P1 tell Tx Msg + p1.Tell(&cm.Node{Addr: "127.0.0.1:10002"}, &iproto.TxPb{Version: uint32(12345678)}) + time.Sleep(time.Second) + assert.Equal(t, uint32(1), dp2.Count) + + // P2 tell Tx Msg + p2.Tell(&cm.Node{Addr: "127.0.0.1:10001"}, &iproto.TxPb{Version: uint32(87654321)}) + time.Sleep(time.Second) + assert.Equal(t, uint32(1), dp1.Count) +} + +func TestOneConnPerIP(t *testing.T) { + if testing.Short() { + t.Skip("Skipping the overlay test in short mode.") + } + dp1 := &MockDispatcher2{T: t} + p1 := NewOverlay(LoadTestConfig("127.0.0.1:10001", false)) + p1.AttachDispatcher(dp1) + p1.Init() + p1.Start() + dp2 := &MockDispatcher2{T: t} + p2 := NewOverlay(LoadTestConfig("127.0.0.1:10002", false)) + p2.AttachDispatcher(dp2) + p2.Init() + p2.Start() + dp3 := &MockDispatcher2{T: t} + p3 := NewOverlay(LoadTestConfig("127.0.0.1:10003", false)) + p3.AttachDispatcher(dp3) + p3.Init() + p3.Start() + + time.Sleep(2 * time.Second) + + defer func() { + p1.Stop() + p2.Stop() + p3.Stop() + }() + + assert.Equal(t, uint(1), lenSyncMap(p1.PM.Peers)) + assert.Equal(t, uint(1), lenSyncMap(p2.PM.Peers)) + assert.Equal(t, uint(1), lenSyncMap(p3.PM.Peers)) +} + +func TestConfigBasedTopology(t *testing.T) { + if testing.Short() { + t.Skip("Skipping the overlay test in short mode.") + } + topology := config.Topology{ + NeighborList: map[string][]string{ + "127.0.0.1:10001": []string{"127.0.0.1:10002", "127.0.0.1:10003", "127.0.0.1:10004"}, + "127.0.0.1:10002": []string{"127.0.0.1:10001", "127.0.0.1:10003", "127.0.0.1:10004"}, + "127.0.0.1:10003": []string{"127.0.0.1:10001", "127.0.0.1:10002", "127.0.0.1:10004"}, + "127.0.0.1:10004": []string{"127.0.0.1:10001", "127.0.0.1:10002", "127.0.0.1:10003"}, + }, + } + topologyStr, err := yaml.Marshal(topology) + assert.Nil(t, err) + path := "/tmp/topology_" + strconv.Itoa(rand.Int()) + ".yaml" + ioutil.WriteFile(path, topologyStr, 0666) + + nodes := make([]*Overlay, 4) + for i := 1; i <= 4; i++ { + config := LoadTestConfig(fmt.Sprintf("127.0.0.1:1000%d", i), true) + config.PeerDiscovery = false + config.TopologyPath = path + dp := &MockDispatcher{} + node := NewOverlay(config) + node.AttachDispatcher(dp) + node.Init() + node.Start() + nodes[i-1] = node + } + + defer func() { + for _, node := range nodes { + node.Stop() + } + if os.Remove(path) != nil { + assert.Fail(t, "Error when deleting the test file") + } + }() + + time.Sleep(2 * time.Second) + + for _, node := range nodes { + assert.Equal(t, uint(3), lenSyncMap(node.PM.Peers)) + addrs := make([]string, 0) + node.PM.Peers.Range(func(key, value interface{}) bool { + addrs = append(addrs, key.(string)) + return true + }) + sort.Strings(addrs) + assert.Equal(t, topology.NeighborList[node.PRC.String()], addrs) + } +} + +type MockDispatcher3 struct { + MockDispatcher + C chan bool +} + +func (d3 *MockDispatcher3) HandleTell(net.Addr, proto.Message, chan bool) { + d3.C <- true +} + +func (d3 *MockDispatcher3) HandleBroadcast(proto.Message, chan bool) { + d3.C <- true +} + +func runBenchmarkOp(tell bool, size int, parallel bool, tls bool, b *testing.B) { + var cfg1, cfg2 *config.Network + if tls { + cfg1 = LoadTestConfigWithTLSEnabled("127.0.0.1:10001", true) + cfg2 = LoadTestConfigWithTLSEnabled("127.0.0.1:10002", true) + } else { + cfg1 = LoadTestConfig("127.0.0.1:10001", true) + cfg2 = LoadTestConfig("127.0.0.1:10002", true) + } + c1 := make(chan bool) + d1 := &MockDispatcher3{C: c1} + p1 := NewOverlay(cfg1) + p1.AttachDispatcher(d1) + p1.Init() + p1.Start() + c2 := make(chan bool) + d2 := &MockDispatcher3{C: c2} + p2 := NewOverlay(cfg2) + p2.AttachDispatcher(d2) + p2.Init() + p2.Start() + + defer func() { + p1.Stop() + p2.Stop() + }() + + time.Sleep(time.Second) + + bytes := make([]byte, size) + for i := 0; i < size; i++ { + bytes[i] = uint8(rand.Intn(math.MaxUint8)) + } + b.ResetTimer() + if parallel { + b.RunParallel(func(pb *testing.PB) { + for pb.Next() { + if tell { + p1.Tell(&cm.Node{Addr: "127.0.0.1:10002"}, &iproto.TestPayload{MsgBody: bytes}) + } else { + p1.Broadcast(&iproto.TestPayload{MsgBody: bytes}) + } + <-c2 + } + }) + } else { + for i := 0; i < b.N; i++ { + if tell { + p1.Tell(&cm.Node{Addr: "127.0.0.1:10002"}, &iproto.TestPayload{MsgBody: bytes}) + } else { + p1.Broadcast(&iproto.TestPayload{MsgBody: bytes}) + } + <-c2 + } + } +} + +func generateBlockConfig() *map[string]int { + return &map[string]int{ + "0K payload": 0, + "1K payload": 1024, + "10K payload": 1024 * 10, + "100K payload": 1024 * 100, + "1M payload": 1024 * 1024, + "2M payload": 1024 * 1024 * 2, + "5M payload": 1024 * 1024 * 5, + } +} + +func BenchmarkTell(b *testing.B) { + for name, size := range *generateBlockConfig() { + b.Run(name, func(b *testing.B) { + runBenchmarkOp(true, size, false, false, b) + }) + } +} + +func BenchmarkSecureTell(b *testing.B) { + for name, size := range *generateBlockConfig() { + b.Run(name, func(b *testing.B) { + runBenchmarkOp(true, size, false, true, b) + }) + } +} + +func BenchmarkParallelTell(b *testing.B) { + for name, size := range *generateBlockConfig() { + b.Run(name, func(b *testing.B) { + runBenchmarkOp(true, size, true, false, b) + }) + } +} + +func BenchmarkParallelSecureTell(b *testing.B) { + for name, size := range *generateBlockConfig() { + b.Run(name, func(b *testing.B) { + runBenchmarkOp(true, size, true, true, b) + }) + } +} + +/* +func BenchmarkBroadcast(b *testing.B) { + for name, size := range *generateBlockConfig() { + b.Run(name, func(b *testing.B) { + runBenchmarkOp(false, size, false, false, b) + }) + } +} +*/ diff --git a/network/peer.go b/network/peer.go new file mode 100644 index 0000000000..6a58e1b86f --- /dev/null +++ b/network/peer.go @@ -0,0 +1,114 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "time" + + "github.com/golang/glog" + "golang.org/x/net/context" + "google.golang.org/grpc" + + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/config" + pb "github.com/iotexproject/iotex-core/network/proto" + "github.com/iotexproject/iotex-core/proto" +) + +// Peer represents a node in the peer-to-peer networks +type Peer struct { + cm.Node + Client pb.PeerClient + Conn *grpc.ClientConn + Ctx context.Context + LastResTime time.Time +} + +// NewTCPPeer creates an instance of Peer with tcp transportation +func NewTCPPeer(addr string) *Peer { + return NewPeer("tcp", addr) +} + +// NewPeer creates an instance of Peer +func NewPeer(n string, addr string) *Peer { + p := &Peer{LastResTime: time.Now()} + p.NetworkType = n + p.Addr = addr + return p +} + +// Connect connects the peer +func (p *Peer) Connect(config *config.Network) error { + // Set up a connection to the peer. + var conn *grpc.ClientConn + var err error + if config.TLSEnabled { + creds, err := generateClientCredentials(config) + if err != nil { + return err + } + conn, err = grpc.Dial( + p.String(), + grpc.WithTransportCredentials(creds), + grpc.WithKeepaliveParams(config.KLClientParams), + grpc.WithMaxMsgSize(config.MaxMsgSize)) + } else { + conn, err = grpc.Dial( + p.String(), + grpc.WithInsecure(), + grpc.WithKeepaliveParams(config.KLClientParams)) + } + + if err != nil { + glog.Errorf("Peer did not connect: %v", err) + return err + } + p.Conn = conn + p.Client = pb.NewPeerClient(conn) + p.Ctx = context.Background() + return nil +} + +// Close terminates the connection +func (p *Peer) Close() error { + return p.Conn.Close() +} + +// Ping implements the client side RPC +func (p *Peer) Ping(ping *pb.Ping) (*pb.Pong, error) { + pong, e := p.Client.Ping(p.Ctx, ping) + p.updateLastResTime() + return pong, e +} + +// GetPeers implements the client side RPC +func (p *Peer) GetPeers(req *pb.GetPeersReq) (*pb.GetPeersRes, error) { + res, e := p.Client.GetPeers(p.Ctx, req) + p.updateLastResTime() + return res, e +} + +// BroadcastMsg implements the client side RPC +func (p *Peer) BroadcastMsg(req *pb.BroadcastReq) (*pb.BroadcastRes, error) { + req.Header = iproto.MagicBroadcastMsgHeader + res, e := p.Client.Broadcast(p.Ctx, req) + p.updateLastResTime() + return res, e +} + +// Tell implements the client side RPC +func (p *Peer) Tell(req *pb.TellReq) (*pb.TellRes, error) { + req.Header = iproto.MagicBroadcastMsgHeader + res, e := p.Client.Tell(p.Ctx, req) + p.updateLastResTime() + return res, e +} + +// Update the last time when successfully getting an response from the peer +func (p *Peer) updateLastResTime() { + p.LastResTime = time.Now() +} diff --git a/network/peermaintainer.go b/network/peermaintainer.go new file mode 100644 index 0000000000..a90a308faa --- /dev/null +++ b/network/peermaintainer.go @@ -0,0 +1,111 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "math/rand" + "net" + + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/config" + pb "github.com/iotexproject/iotex-core/network/proto" +) + +// PeerMaintainer helps maintain enough connections to other peers in the P2P networks +type PeerMaintainer struct { + Overlay *Overlay +} + +// NewPeerMaintainer creates an instance of PeerMaintainer +func NewPeerMaintainer(o *Overlay) *PeerMaintainer { + return &PeerMaintainer{Overlay: o} +} + +// Do maintain peer connection. Current strategy is to get the (upper_bound - count) peer addresses from one of the +// current peer if the count is lower than the lower bound +func (pm *PeerMaintainer) Do() { + count := lenSyncMap(pm.Overlay.PM.Peers) + if count == 0 { + // TODO: Now we simply read the bootstrap nodes from the config. This needs to be changed in the future + bns1 := pm.Overlay.Config.BootstrapNodes + bns2 := make([]string, len(bns1)) + copy(bns2, bns1) + stringsAreShuffled(bns2) + for i, bn := range bns2 { + if uint(i) >= pm.Overlay.PM.NumPeersLowerBound { + break + } + pm.Overlay.PM.AddPeer(bn) + } + } else if count < pm.Overlay.PM.NumPeersLowerBound { + targetIdx := rand.Intn(int(count)) + idx := 0 + // It's possible that no target is reached because the peer is deleted concurrently, so that targetIdx is + // greater than the number of peers + var target *Peer + pm.Overlay.PM.Peers.Range(func(_, value interface{}) bool { + if idx == targetIdx { + target = value.(*Peer) + return false + } + idx++ + return true + }) + go func() { + if target != nil { + res, error := target.GetPeers( + &pb.GetPeersReq{Count: uint32(pm.Overlay.PM.NumPeersUpperBound - count)}) + if res != nil && error == nil { + for _, addr := range res.Addr { + pm.Overlay.PM.AddPeer(addr) + } + } + } + }() + } else if count > pm.Overlay.PM.NumPeersUpperBound { + for count > pm.Overlay.PM.NumPeersUpperBound { + pm.Overlay.PM.RemoveLRUPeer() + count-- + } + } +} + +// ConfigBasedPeerMaintainer maintain the neighbors by reading the topology file +type ConfigBasedPeerMaintainer struct { + Overlay *Overlay + Addrs []net.Addr +} + +// NewConfigBasedPeerMaintainer creates an instance of ConfigBasedPeerMaintainer +func NewConfigBasedPeerMaintainer(o *Overlay, t *config.Topology) *ConfigBasedPeerMaintainer { + cbpm := &ConfigBasedPeerMaintainer{Overlay: o} + for host, neighbors := range t.NeighborList { + if host == o.PRC.String() { + for _, addr := range neighbors { + cbpm.Addrs = append(cbpm.Addrs, cm.NewTCPNode(addr)) + } + } + } + return cbpm +} + +// Do adds the configured addresses into the peer list +func (cbpm *ConfigBasedPeerMaintainer) Do() { + addrs := make(map[string]bool) + for _, addr := range cbpm.Addrs { + addrs[addr.String()] = false + } + cbpm.Overlay.PM.Peers.Range(func(key, value interface{}) bool { + addrs[key.(string)] = true + return true + }) + for addr, ok := range addrs { + if !ok { + cbpm.Overlay.PM.AddPeer(addr) + } + } +} diff --git a/network/peermanager.go b/network/peermanager.go new file mode 100644 index 0000000000..c545837cdd --- /dev/null +++ b/network/peermanager.go @@ -0,0 +1,123 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "net" + "sync" + + "github.com/golang/glog" + + "github.com/iotexproject/iotex-core/common/service" +) + +// PeerManager represents the outgoing neighbor list +// TODO: We should decouple peer address and peer. Node can know more nodes than it connects to +type PeerManager struct { + service.CompositeService + // TODO: Need to revisit sync.Map: https://github.com/golang/go/issues/24112 + Peers sync.Map + Overlay *Overlay + NumPeersLowerBound uint + NumPeersUpperBound uint +} + +// NewPeerManager creates an instance of PeerManager +func NewPeerManager(o *Overlay, lb uint, ub uint) *PeerManager { + return &PeerManager{Overlay: o, NumPeersLowerBound: lb, NumPeersUpperBound: ub} +} + +// AddPeer adds a new peer +func (pm *PeerManager) AddPeer(addr string) { + if lenSyncMap(pm.Peers) >= pm.NumPeersUpperBound { + glog.Infof("Node already reaches the max number of peers: %d", pm.NumPeersUpperBound) + return + } + if pm.Overlay.PRC.String() == addr { + glog.Infof("Node at address %s is the current node", addr) + return + } + _, ok := pm.Peers.Load(addr) + if ok { + glog.Infof("Node at address %s is already the peer", addr) + return + } + if !pm.Overlay.Config.AllowMultiConnsPerIP { + nHost, _, err := net.SplitHostPort(addr) + if err != nil { + glog.Errorf("Node address %s is invalid", addr) + return + } + found := false + pm.Peers.Range(func(key, value interface{}) bool { + host, _, err := net.SplitHostPort(value.(*Peer).String()) + // This should be impossible, otherwise the connection couldn't be established + if err != nil { + glog.Errorf("Node address %s is invalid", addr) + return true + } + if host == nHost { + found = true + return false + } + return true + }) + if found { + glog.Infof("Another node on the same IP %s is already the peer", nHost) + return + } + } + p := NewTCPPeer(addr) + p.Connect(pm.Overlay.Config) + pm.Peers.Store(addr, p) +} + +// RemovePeer removes an existing peer +func (pm *PeerManager) RemovePeer(addr string) { + p, found := pm.Peers.Load(addr) + if !found { + glog.Infof("Node at address %s is not a peer", addr) + return + } + pm.Peers.Delete(p.(*Peer).String()) + p.(*Peer).Close() +} + +// RemoveLRUPeer removes the least recently used (contacted) peer +func (pm *PeerManager) RemoveLRUPeer() { + minLastResTime := int64(0) + addr := "" + pm.Peers.Range(func(key, value interface{}) bool { + lastResTime := value.(*Peer).LastResTime.Unix() + if minLastResTime == 0 || lastResTime < minLastResTime { + minLastResTime = lastResTime + addr = key.(string) + } + return true + }) + if addr != "" { + pm.RemovePeer(addr) + } +} + +// GetOrAddPeer gets a peer. If it is still not in the neighbor list, it will be added first. +func (pm *PeerManager) GetOrAddPeer(addr string) *Peer { + peer, ok := pm.Peers.Load(addr) + if ok { + return peer.(*Peer) + } + if lenSyncMap(pm.Peers) >= pm.NumPeersUpperBound { + pm.RemoveLRUPeer() + } + // TODO: there could be race condition that another peer is added first + pm.AddPeer(addr) + peer, ok = pm.Peers.Load(addr) + if ok { + return peer.(*Peer) + } + return nil +} diff --git a/network/pinger.go b/network/pinger.go new file mode 100644 index 0000000000..ddee0a24b3 --- /dev/null +++ b/network/pinger.go @@ -0,0 +1,37 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "math/rand" + + pb "github.com/iotexproject/iotex-core/network/proto" +) + +// Pinger is the recurring logic to constantly check if the node can talk to its peers +type Pinger struct { + Overlay *Overlay +} + +// NewPinger creates an instance of Pinger +func NewPinger(o *Overlay) *Pinger { + return &Pinger{Overlay: o} +} + +// Do ping the neighbor peers +func (h *Pinger) Do() { + h.Overlay.PM.Peers.Range(func(_, value interface{}) bool { + go func() { + n := rand.Uint64() + pong, error := value.(*Peer).Ping(&pb.Ping{Nonce: n, Addr: h.Overlay.PRC.String()}) + if error == nil && pong.AckNonce == n { + // TODO: We need to handle wrong response case too + } + }() + return true + }) +} diff --git a/network/proto/rpc.pb.go b/network/proto/rpc.pb.go new file mode 100644 index 0000000000..ac5be0efe7 --- /dev/null +++ b/network/proto/rpc.pb.go @@ -0,0 +1,428 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: network/proto/rpc.proto + +/* +Package network is a generated protocol buffer package. + +It is generated from these files: + network/proto/rpc.proto + +It has these top-level messages: + Ping + Pong + GetPeersReq + GetPeersRes + BroadcastReq + BroadcastRes + TellReq + TellRes +*/ +package network + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type Ping struct { + Nonce uint64 `protobuf:"varint,1,opt,name=nonce" json:"nonce,omitempty"` + // Every one who participates into the network needs to tell others its address + // TODO: Seperate it as a standalone protocol + Addr string `protobuf:"bytes,2,opt,name=addr" json:"addr,omitempty"` +} + +func (m *Ping) Reset() { *m = Ping{} } +func (m *Ping) String() string { return proto.CompactTextString(m) } +func (*Ping) ProtoMessage() {} +func (*Ping) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *Ping) GetNonce() uint64 { + if m != nil { + return m.Nonce + } + return 0 +} + +func (m *Ping) GetAddr() string { + if m != nil { + return m.Addr + } + return "" +} + +type Pong struct { + AckNonce uint64 `protobuf:"varint,1,opt,name=ack_nonce,json=ackNonce" json:"ack_nonce,omitempty"` +} + +func (m *Pong) Reset() { *m = Pong{} } +func (m *Pong) String() string { return proto.CompactTextString(m) } +func (*Pong) ProtoMessage() {} +func (*Pong) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func (m *Pong) GetAckNonce() uint64 { + if m != nil { + return m.AckNonce + } + return 0 +} + +type GetPeersReq struct { + Count uint32 `protobuf:"varint,1,opt,name=count" json:"count,omitempty"` +} + +func (m *GetPeersReq) Reset() { *m = GetPeersReq{} } +func (m *GetPeersReq) String() string { return proto.CompactTextString(m) } +func (*GetPeersReq) ProtoMessage() {} +func (*GetPeersReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } + +func (m *GetPeersReq) GetCount() uint32 { + if m != nil { + return m.Count + } + return 0 +} + +type GetPeersRes struct { + Addr []string `protobuf:"bytes,1,rep,name=addr" json:"addr,omitempty"` +} + +func (m *GetPeersRes) Reset() { *m = GetPeersRes{} } +func (m *GetPeersRes) String() string { return proto.CompactTextString(m) } +func (*GetPeersRes) ProtoMessage() {} +func (*GetPeersRes) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } + +func (m *GetPeersRes) GetAddr() []string { + if m != nil { + return m.Addr + } + return nil +} + +type BroadcastReq struct { + Header uint32 `protobuf:"varint,1,opt,name=header" json:"header,omitempty"` + MsgType uint32 `protobuf:"varint,2,opt,name=msg_type,json=msgType" json:"msg_type,omitempty"` + MsgBody []byte `protobuf:"bytes,3,opt,name=msg_body,json=msgBody,proto3" json:"msg_body,omitempty"` +} + +func (m *BroadcastReq) Reset() { *m = BroadcastReq{} } +func (m *BroadcastReq) String() string { return proto.CompactTextString(m) } +func (*BroadcastReq) ProtoMessage() {} +func (*BroadcastReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } + +func (m *BroadcastReq) GetHeader() uint32 { + if m != nil { + return m.Header + } + return 0 +} + +func (m *BroadcastReq) GetMsgType() uint32 { + if m != nil { + return m.MsgType + } + return 0 +} + +func (m *BroadcastReq) GetMsgBody() []byte { + if m != nil { + return m.MsgBody + } + return nil +} + +type BroadcastRes struct { + Header uint32 `protobuf:"varint,1,opt,name=header" json:"header,omitempty"` +} + +func (m *BroadcastRes) Reset() { *m = BroadcastRes{} } +func (m *BroadcastRes) String() string { return proto.CompactTextString(m) } +func (*BroadcastRes) ProtoMessage() {} +func (*BroadcastRes) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } + +func (m *BroadcastRes) GetHeader() uint32 { + if m != nil { + return m.Header + } + return 0 +} + +type TellReq struct { + Header uint32 `protobuf:"varint,1,opt,name=header" json:"header,omitempty"` + Addr string `protobuf:"bytes,2,opt,name=addr" json:"addr,omitempty"` + MsgType uint32 `protobuf:"varint,3,opt,name=msg_type,json=msgType" json:"msg_type,omitempty"` + MsgBody []byte `protobuf:"bytes,4,opt,name=msg_body,json=msgBody,proto3" json:"msg_body,omitempty"` +} + +func (m *TellReq) Reset() { *m = TellReq{} } +func (m *TellReq) String() string { return proto.CompactTextString(m) } +func (*TellReq) ProtoMessage() {} +func (*TellReq) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } + +func (m *TellReq) GetHeader() uint32 { + if m != nil { + return m.Header + } + return 0 +} + +func (m *TellReq) GetAddr() string { + if m != nil { + return m.Addr + } + return "" +} + +func (m *TellReq) GetMsgType() uint32 { + if m != nil { + return m.MsgType + } + return 0 +} + +func (m *TellReq) GetMsgBody() []byte { + if m != nil { + return m.MsgBody + } + return nil +} + +type TellRes struct { + Header uint32 `protobuf:"varint,1,opt,name=header" json:"header,omitempty"` +} + +func (m *TellRes) Reset() { *m = TellRes{} } +func (m *TellRes) String() string { return proto.CompactTextString(m) } +func (*TellRes) ProtoMessage() {} +func (*TellRes) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } + +func (m *TellRes) GetHeader() uint32 { + if m != nil { + return m.Header + } + return 0 +} + +func init() { + proto.RegisterType((*Ping)(nil), "network.Ping") + proto.RegisterType((*Pong)(nil), "network.Pong") + proto.RegisterType((*GetPeersReq)(nil), "network.GetPeersReq") + proto.RegisterType((*GetPeersRes)(nil), "network.GetPeersRes") + proto.RegisterType((*BroadcastReq)(nil), "network.BroadcastReq") + proto.RegisterType((*BroadcastRes)(nil), "network.BroadcastRes") + proto.RegisterType((*TellReq)(nil), "network.TellReq") + proto.RegisterType((*TellRes)(nil), "network.TellRes") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for Peer service + +type PeerClient interface { + Ping(ctx context.Context, in *Ping, opts ...grpc.CallOption) (*Pong, error) + GetPeers(ctx context.Context, in *GetPeersReq, opts ...grpc.CallOption) (*GetPeersRes, error) + Broadcast(ctx context.Context, in *BroadcastReq, opts ...grpc.CallOption) (*BroadcastRes, error) + Tell(ctx context.Context, in *TellReq, opts ...grpc.CallOption) (*TellRes, error) +} + +type peerClient struct { + cc *grpc.ClientConn +} + +func NewPeerClient(cc *grpc.ClientConn) PeerClient { + return &peerClient{cc} +} + +func (c *peerClient) Ping(ctx context.Context, in *Ping, opts ...grpc.CallOption) (*Pong, error) { + out := new(Pong) + err := grpc.Invoke(ctx, "/network.Peer/ping", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *peerClient) GetPeers(ctx context.Context, in *GetPeersReq, opts ...grpc.CallOption) (*GetPeersRes, error) { + out := new(GetPeersRes) + err := grpc.Invoke(ctx, "/network.Peer/getPeers", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *peerClient) Broadcast(ctx context.Context, in *BroadcastReq, opts ...grpc.CallOption) (*BroadcastRes, error) { + out := new(BroadcastRes) + err := grpc.Invoke(ctx, "/network.Peer/broadcast", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *peerClient) Tell(ctx context.Context, in *TellReq, opts ...grpc.CallOption) (*TellRes, error) { + out := new(TellRes) + err := grpc.Invoke(ctx, "/network.Peer/tell", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for Peer service + +type PeerServer interface { + Ping(context.Context, *Ping) (*Pong, error) + GetPeers(context.Context, *GetPeersReq) (*GetPeersRes, error) + Broadcast(context.Context, *BroadcastReq) (*BroadcastRes, error) + Tell(context.Context, *TellReq) (*TellRes, error) +} + +func RegisterPeerServer(s *grpc.Server, srv PeerServer) { + s.RegisterService(&_Peer_serviceDesc, srv) +} + +func _Peer_Ping_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(Ping) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PeerServer).Ping(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/network.Peer/Ping", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PeerServer).Ping(ctx, req.(*Ping)) + } + return interceptor(ctx, in, info, handler) +} + +func _Peer_GetPeers_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetPeersReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PeerServer).GetPeers(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/network.Peer/GetPeers", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PeerServer).GetPeers(ctx, req.(*GetPeersReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Peer_Broadcast_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(BroadcastReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PeerServer).Broadcast(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/network.Peer/Broadcast", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PeerServer).Broadcast(ctx, req.(*BroadcastReq)) + } + return interceptor(ctx, in, info, handler) +} + +func _Peer_Tell_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(TellReq) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(PeerServer).Tell(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/network.Peer/Tell", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(PeerServer).Tell(ctx, req.(*TellReq)) + } + return interceptor(ctx, in, info, handler) +} + +var _Peer_serviceDesc = grpc.ServiceDesc{ + ServiceName: "network.Peer", + HandlerType: (*PeerServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "ping", + Handler: _Peer_Ping_Handler, + }, + { + MethodName: "getPeers", + Handler: _Peer_GetPeers_Handler, + }, + { + MethodName: "broadcast", + Handler: _Peer_Broadcast_Handler, + }, + { + MethodName: "tell", + Handler: _Peer_Tell_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "network/proto/rpc.proto", +} + +func init() { proto.RegisterFile("network/proto/rpc.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 340 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x7c, 0x52, 0xcd, 0x4a, 0xf3, 0x40, + 0x14, 0x6d, 0xbe, 0xce, 0xd7, 0x9f, 0x6b, 0x0b, 0x72, 0xa9, 0x5a, 0xe3, 0x26, 0x9d, 0x42, 0xe9, + 0x42, 0x5a, 0xd1, 0x8d, 0xe0, 0xae, 0x1b, 0x77, 0x52, 0x42, 0x97, 0x42, 0x49, 0x93, 0x4b, 0x94, + 0xa4, 0x33, 0x71, 0x66, 0x44, 0xf2, 0x9c, 0xbe, 0x90, 0x4c, 0x9a, 0x86, 0x54, 0x5a, 0x77, 0x73, + 0xee, 0xcf, 0xb9, 0x87, 0x73, 0x06, 0xae, 0x04, 0x99, 0x2f, 0xa9, 0x92, 0x79, 0xa6, 0xa4, 0x91, + 0x73, 0x95, 0x85, 0xb3, 0xe2, 0x85, 0xed, 0xb2, 0xc1, 0xef, 0x80, 0x2d, 0xdf, 0x45, 0x8c, 0x03, + 0xf8, 0x2f, 0xa4, 0x08, 0x69, 0xe8, 0x78, 0xce, 0x94, 0xf9, 0x3b, 0x80, 0x08, 0x2c, 0x88, 0x22, + 0x35, 0xfc, 0xe7, 0x39, 0xd3, 0xae, 0x5f, 0xbc, 0xf9, 0x18, 0xd8, 0x52, 0x8a, 0x18, 0x6f, 0xa0, + 0x1b, 0x84, 0xc9, 0xba, 0xbe, 0xd5, 0x09, 0xc2, 0xe4, 0xc5, 0x62, 0x3e, 0x86, 0xb3, 0x67, 0x32, + 0x4b, 0x22, 0xa5, 0x7d, 0xfa, 0xb0, 0xec, 0xa1, 0xfc, 0x14, 0xa6, 0x98, 0xeb, 0xfb, 0x3b, 0xc0, + 0x47, 0xf5, 0x21, 0x5d, 0x1d, 0x73, 0xbc, 0x66, 0x75, 0xec, 0x15, 0x7a, 0x0b, 0x25, 0x83, 0x28, + 0x0c, 0xb4, 0xb1, 0x44, 0x97, 0xd0, 0x7a, 0xa3, 0x20, 0x22, 0x55, 0x32, 0x95, 0x08, 0xaf, 0xa1, + 0xb3, 0xd5, 0xf1, 0xda, 0xe4, 0x19, 0x15, 0x62, 0xfb, 0x7e, 0x7b, 0xab, 0xe3, 0x55, 0x9e, 0xd1, + 0xbe, 0xb5, 0x91, 0x51, 0x3e, 0x6c, 0x7a, 0xce, 0xb4, 0x57, 0xb4, 0x16, 0x32, 0xca, 0xf9, 0xe4, + 0x80, 0x5d, 0x9f, 0x62, 0xe7, 0x09, 0xb4, 0x57, 0x94, 0xa6, 0x7f, 0x09, 0x38, 0xe2, 0xd4, 0x81, + 0xa8, 0xe6, 0x69, 0x51, 0xec, 0x50, 0xd4, 0x68, 0x7f, 0xec, 0xa4, 0x9e, 0xfb, 0x6f, 0x07, 0x98, + 0xb5, 0x0d, 0x27, 0xc0, 0x32, 0x9b, 0x5e, 0x7f, 0x56, 0xe6, 0x39, 0xb3, 0x61, 0xba, 0x35, 0x28, + 0x45, 0xcc, 0x1b, 0xf8, 0x08, 0x9d, 0xb8, 0x74, 0x1a, 0x07, 0x55, 0xb3, 0x96, 0x90, 0x7b, 0xac, + 0xaa, 0x79, 0x03, 0x9f, 0xa0, 0xbb, 0xd9, 0x5b, 0x84, 0x17, 0xd5, 0x50, 0x3d, 0x14, 0xf7, 0x68, + 0xd9, 0x2e, 0xdf, 0x02, 0x33, 0x94, 0xa6, 0x78, 0x5e, 0x0d, 0x94, 0x36, 0xba, 0xbf, 0x2b, 0x9a, + 0x37, 0x36, 0xad, 0xe2, 0x6b, 0x3e, 0xfc, 0x04, 0x00, 0x00, 0xff, 0xff, 0x16, 0x62, 0xc1, 0x08, + 0xb5, 0x02, 0x00, 0x00, +} diff --git a/network/proto/rpc.proto b/network/proto/rpc.proto new file mode 100644 index 0000000000..0841995941 --- /dev/null +++ b/network/proto/rpc.proto @@ -0,0 +1,58 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +// To compile the proto, run: +// protoc --go_out=plugins=grpc:. network/proto/rpc.proto +syntax = "proto3"; + +package network; + +service Peer { + rpc ping(Ping) returns (Pong) {} + rpc getPeers(GetPeersReq) returns (GetPeersRes) {} + rpc broadcast(BroadcastReq) returns (BroadcastRes) {} + rpc tell(TellReq) returns (TellRes) {} +} + +message Ping { + uint64 nonce = 1; + // Every one who participates into the network needs to tell others its address + // TODO: Seperate it as a standalone protocol + string addr = 2; +} + +message Pong { + uint64 ack_nonce = 1; +} + +message GetPeersReq { + uint32 count = 1; +} + +message GetPeersRes { + repeated string addr = 1; +} + +message BroadcastReq { + uint32 header = 1; + uint32 msg_type = 2; + bytes msg_body = 3; +} + +message BroadcastRes { + uint32 header = 1; +} + +message TellReq { + uint32 header = 1; + string addr = 2; + uint32 msg_type = 3; + bytes msg_body = 4; +} + +message TellRes { + uint32 header = 1; +} \ No newline at end of file diff --git a/network/rpcserver.go b/network/rpcserver.go new file mode 100644 index 0000000000..4e2d365629 --- /dev/null +++ b/network/rpcserver.go @@ -0,0 +1,188 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "fmt" + "net" + "sync" + "time" + + "github.com/golang/glog" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/peer" + "google.golang.org/grpc/reflection" + + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/common/service" + "github.com/iotexproject/iotex-core/common/utils" + pb "github.com/iotexproject/iotex-core/network/proto" + "github.com/iotexproject/iotex-core/proto" +) + +// RPCServer represents the listener at the transportation layer +type RPCServer struct { + service.AbstractService + cm.Node + Server *grpc.Server + Overlay *Overlay + counters sync.Map + rateLimit uint64 +} + +// NewRPCServer creates an instance of RPCServer +func NewRPCServer(o *Overlay) *RPCServer { + s := &RPCServer{Overlay: o} + s.Addr = o.Config.Addr + s.rateLimit = o.Config.RateLimitPerSec * uint64(o.Config.RateLimitWindowSize) / uint64(time.Second) + return s +} + +// Ping implements the server side RPC logic +func (s *RPCServer) Ping(ctx context.Context, ping *pb.Ping) (*pb.Pong, error) { + drop, err := s.shouldDropRequest(ctx) + if err != nil { + return nil, err + } + if drop { + return nil, fmt.Errorf("sended requests too frequently") + } + s.Overlay.PM.AddPeer(ping.Addr) + return &pb.Pong{AckNonce: ping.Nonce}, nil +} + +// GetPeers implements the server side RPC logic +func (s *RPCServer) GetPeers(ctx context.Context, req *pb.GetPeersReq) (*pb.GetPeersRes, error) { + drop, err := s.shouldDropRequest(ctx) + if err != nil { + return nil, err + } + if drop { + return nil, fmt.Errorf("sended requests too frequently") + } + addrs := []string{} + s.Overlay.PM.Peers.Range(func(key, value interface{}) bool { + addrs = append(addrs, value.(*Peer).String()) + return true + }) + stringsAreShuffled(addrs) + res := &pb.GetPeersRes{} + if req.Count <= uint32(len(addrs)) { + res.Addr = addrs[:req.Count] + } else { + res.Addr = addrs + } + return res, nil +} + +// Broadcast implements the server side RPC logic +func (s *RPCServer) Broadcast(ctx context.Context, req *pb.BroadcastReq) (*pb.BroadcastRes, error) { + drop, err := s.shouldDropRequest(ctx) + if err != nil { + return nil, err + } + if drop { + return nil, fmt.Errorf("sended requests too frequently") + } + err = s.Overlay.Gossip.OnReceivingMsg(req) + if err == nil { + return &pb.BroadcastRes{Header: iproto.MagicBroadcastMsgHeader}, nil + } + return nil, err +} + +// Tell implements the server side RPC logic +func (s *RPCServer) Tell(ctx context.Context, req *pb.TellReq) (*pb.TellRes, error) { + drop, err := s.shouldDropRequest(ctx) + if err != nil { + return nil, err + } + if drop { + return nil, fmt.Errorf("sended requests too frequently") + } + protoMsg, err := iproto.TypifyProtoMsg(req.MsgType, req.MsgBody) + if err != nil { + return nil, err + } + if s.Overlay.Dispatcher != nil { + s.Overlay.Dispatcher.HandleTell(cm.NewTCPNode(req.Addr), protoMsg, nil) + } + return &pb.TellRes{Header: iproto.MagicBroadcastMsgHeader}, nil +} + +// Start starts the rpc server +func (s *RPCServer) Start() error { + lis, err := net.Listen(s.Network(), s.String()) + if err != nil { + glog.Fatalf("Node failed to listen: %v", err) + return err + } + s.Addr = lis.Addr().String() + // Create the gRPC server with the credentials + if s.Overlay.Config.TLSEnabled { + creds, err := generateServerCredentials(s.Overlay.Config) + if err != nil { + return err + } + s.Server = grpc.NewServer( + grpc.Creds(creds), + grpc.KeepaliveEnforcementPolicy(s.Overlay.Config.KLPolicy), + grpc.KeepaliveParams(s.Overlay.Config.KLServerParams), + grpc.MaxMsgSize(s.Overlay.Config.MaxMsgSize)) + } else { + s.Server = grpc.NewServer( + grpc.KeepaliveEnforcementPolicy(s.Overlay.Config.KLPolicy), + grpc.KeepaliveParams(s.Overlay.Config.KLServerParams), + grpc.MaxMsgSize(1024*1024*10)) + } + + pb.RegisterPeerServer(s.Server, s) + // Register reflection service on gRPC peer. + reflection.Register(s.Server) + go func() { + if err := s.Server.Serve(lis); err != nil { + glog.Fatalf("Node failed to serve: %v", err) + } + }() + return nil +} + +// Stop stops the rpc server +func (s *RPCServer) Stop() error { + s.Server.Stop() + return nil +} + +func (s *RPCServer) shouldDropRequest(ctx context.Context) (bool, error) { + if !s.Overlay.Config.RateLimitEnabled { + return false, nil + } + addr, err := s.getClientAddr(ctx) + if err != nil { + return false, err + } + c, _ := s.counters.LoadOrStore( + addr, + utils.NewSlidingWindowCounterWithSecondSlot(s.Overlay.Config.RateLimitWindowSize)) + c.(*utils.SlidingWindowCounter).Increment() + if c.(*utils.SlidingWindowCounter).Count() > s.rateLimit { + return true, nil + } + return false, nil +} + +func (s *RPCServer) getClientAddr(ctx context.Context) (string, error) { + p, ok := peer.FromContext(ctx) + if !ok { + return "", fmt.Errorf("failed to get peer from ctx") + } + if p.Addr == net.Addr(nil) { + return "", fmt.Errorf("failed to get peer address") + } + return p.Addr.String(), nil +} diff --git a/network/rpcserver_test.go b/network/rpcserver_test.go new file mode 100644 index 0000000000..83c0045306 --- /dev/null +++ b/network/rpcserver_test.go @@ -0,0 +1,248 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "strings" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/assert" + + pb "github.com/iotexproject/iotex-core/network/proto" + "github.com/iotexproject/iotex-core/proto" + "github.com/iotexproject/iotex-core/test/mock/mock_dispatcher" +) + +func TestRpcPingPong(t *testing.T) { + config := LoadTestConfig("", true) + o := &Overlay{Config: config} + o.PM = &PeerManager{Overlay: o, NumPeersLowerBound: 1, NumPeersUpperBound: 1} + s := NewRPCServer(o) + o.PRC = s + s.Start() + p := NewPeer(s.Network(), s.String()) + p.Connect(config) + + defer func() { + p.Close() + s.Stop() + }() + + pong, err := p.Ping(&pb.Ping{Nonce: uint64(4689), Addr: "127.0.0.1:10001"}) + assert.Nil(t, err) + assert.NotNil(t, pong) + assert.Equal(t, uint64(4689), pong.AckNonce) + value, ok := o.PM.Peers.Load("127.0.0.1:10001") + assert.True(t, ok) + assert.NotNil(t, value) + assert.True(t, "127.0.0.1:10001" == value.(*Peer).String()) +} + +func TestGetPeers(t *testing.T) { + config := LoadTestConfig("", true) + o := &Overlay{Config: config} + o.PM = &PeerManager{Overlay: o} + o.PM.Peers.Store("127.0.0.1:10001", NewTCPPeer("127.0.0.1:10001")) + o.PM.Peers.Store("127.0.0.1:10002", NewTCPPeer("127.0.0.1:10002")) + s := NewRPCServer(o) + o.PRC = s + s.Start() + p := NewPeer(s.Network(), s.String()) + p.Connect(config) + + defer func() { + p.Close() + s.Stop() + }() + + res, err := p.GetPeers(&pb.GetPeersReq{Count: 1}) + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, 1, len(res.Addr)) + assert.True(t, res.Addr[0] == "127.0.0.1:10001" || res.Addr[0] == "127.0.0.1:10002") + + res, err = p.GetPeers(&pb.GetPeersReq{Count: 2}) + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, 2, len(res.Addr)) + assert.True(t, res.Addr[0] == "127.0.0.1:10001" || res.Addr[0] == "127.0.0.1:10002") + assert.True(t, res.Addr[1] == "127.0.0.1:10001" || res.Addr[1] == "127.0.0.1:10002") + assert.False(t, res.Addr[0] == res.Addr[1]) + + res, err = p.GetPeers(&pb.GetPeersReq{Count: 3}) + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, 2, len(res.Addr)) + assert.True(t, res.Addr[0] == "127.0.0.1:10001" || res.Addr[0] == "127.0.0.1:10002") + assert.True(t, res.Addr[1] == "127.0.0.1:10001" || res.Addr[1] == "127.0.0.1:10002") + assert.False(t, res.Addr[0] == res.Addr[1]) + +} + +func TestBroadcast(t *testing.T) { + config := LoadTestConfig("", true) + o := &Overlay{Config: config} + o.PM = &PeerManager{Overlay: o} + o.Gossip = &Gossip{Overlay: o} + s := NewRPCServer(o) + o.PRC = s + s.Start() + p := NewPeer(s.Network(), s.String()) + p.Connect(config) + + defer func() { + p.Close() + s.Stop() + }() + + txMsg := &iproto.TxPb{} + b, _ := proto.Marshal(txMsg) + res, err := p.BroadcastMsg( + &pb.BroadcastReq{Header: iproto.MagicBroadcastMsgHeader, MsgType: iproto.MsgTxProtoMsgType, MsgBody: b}) + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, iproto.MagicBroadcastMsgHeader, res.Header) +} + +func TestRPCTell(t *testing.T) { + mctrl := gomock.NewController(t) + dp := mock_dispatcher.NewMockDispatcher(mctrl) + dp.EXPECT().HandleTell(gomock.Any(), gomock.Any(), gomock.Any()).Times(1) + + config := LoadTestConfig("", true) + o := &Overlay{Dispatcher: dp, Config: config} + s := NewRPCServer(o) + o.PRC = s + s.Start() + p := NewPeer(s.Network(), s.String()) + p.Connect(config) + + defer func() { + p.Close() + s.Stop() + mctrl.Finish() + }() + + txMsg := &iproto.TxPb{} + b, _ := proto.Marshal(txMsg) + res, err := p.Tell(&pb.TellReq{Header: iproto.MagicBroadcastMsgHeader, + Addr: s.String(), + MsgType: iproto.MsgTxProtoMsgType, + MsgBody: b}) + assert.Nil(t, err) + assert.NotNil(t, res) + assert.Equal(t, iproto.MagicBroadcastMsgHeader, res.Header) +} + +func TestRateLimit(t *testing.T) { + mctrl := gomock.NewController(t) + dp := mock_dispatcher.NewMockDispatcher(mctrl) + dp.EXPECT().HandleTell(gomock.Any(), gomock.Any(), gomock.Any()).Times(5) + + config := LoadTestConfig("", true) + config.RateLimitEnabled = true + config.RateLimitPerSec = 5 + config.RateLimitWindowSize = time.Second + o := &Overlay{Dispatcher: dp, Config: config} + s := NewRPCServer(o) + o.PRC = s + s.Start() + p := NewPeer(s.Network(), s.String()) + p.Connect(config) + + defer func() { + p.Close() + s.Stop() + mctrl.Finish() + }() + + var res *pb.TellRes + var err error + for i := 0; i < 10; i++ { + txMsg := &iproto.TxPb{} + b, _ := proto.Marshal(txMsg) + res, err = p.Tell(&pb.TellReq{Header: iproto.MagicBroadcastMsgHeader, + Addr: s.String(), + MsgType: iproto.MsgTxProtoMsgType, + MsgBody: b}) + if i < 5 { + assert.Nil(t, err) + assert.NotNil(t, res, i) + assert.Equal(t, iproto.MagicBroadcastMsgHeader, res.Header) + } else { + assert.Nil(t, res) + assert.NotNil(t, err) + assert.True(t, strings.Contains(err.Error(), "sended requests too frequently")) + } + } +} + +func TestSecureRpcPingPong(t *testing.T) { + config := LoadTestConfig("", true) + config.TLSEnabled = true + config.CACrtPath = "../test/assets/ssl/iotex.io.crt" + config.PeerCrtPath = "../test/assets/ssl/127.0.0.1.crt" + config.PeerKeyPath = "../test/assets/ssl/127.0.0.1.key" + o := &Overlay{Config: config} + o.PM = &PeerManager{Overlay: o, NumPeersLowerBound: 1, NumPeersUpperBound: 1} + s := NewRPCServer(o) + o.PRC = s + s.Start() + p := NewPeer(s.Network(), s.String()) + p.Connect(config) + + defer func() { + p.Close() + s.Stop() + }() + + pong, err := p.Ping(&pb.Ping{Nonce: uint64(4689), Addr: "127.0.0.1:10001"}) + assert.Nil(t, err) + assert.NotNil(t, pong) + assert.Equal(t, uint64(4689), pong.AckNonce) + value, ok := o.PM.Peers.Load("127.0.0.1:10001") + assert.True(t, ok) + assert.NotNil(t, value) + assert.True(t, "127.0.0.1:10001" == value.(*Peer).String()) +} + +func TestKeepaliveParams(t *testing.T) { + // This only verfies the config doesn't break connections + config := LoadTestConfig("", true) + config.KLClientParams.Time = 50 * time.Millisecond + config.KLClientParams.Timeout = 20 * time.Millisecond + config.KLServerParams.Time = 50 * time.Second + config.KLClientParams.Timeout = 20 * time.Millisecond + config.KLPolicy.MinTime = 20 * time.Millisecond + o := &Overlay{Config: config} + o.PM = &PeerManager{Overlay: o, NumPeersLowerBound: 1, NumPeersUpperBound: 1} + s := NewRPCServer(o) + o.PRC = s + s.Start() + p := NewPeer(s.Network(), s.String()) + p.Connect(config) + + defer func() { + p.Close() + s.Stop() + }() + + for i := 0; i < 5; i++ { + time.Sleep(100 * time.Millisecond) + pong, err := p.Ping(&pb.Ping{Nonce: uint64(4689), Addr: "127.0.0.1:10001"}) + assert.Nil(t, err) + assert.NotNil(t, pong) + assert.Equal(t, uint64(4689), pong.AckNonce) + value, ok := o.PM.Peers.Load("127.0.0.1:10001") + assert.True(t, ok) + assert.NotNil(t, value) + assert.True(t, "127.0.0.1:10001" == value.(*Peer).String()) + } +} diff --git a/network/utils.go b/network/utils.go new file mode 100644 index 0000000000..30267f77dc --- /dev/null +++ b/network/utils.go @@ -0,0 +1,90 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package network + +import ( + "crypto/tls" + "crypto/x509" + "errors" + "io/ioutil" + "math/rand" + "sync" + + "github.com/golang/glog" + "google.golang.org/grpc/credentials" + + "github.com/iotexproject/iotex-core/config" +) + +// stringsAreShuffled shuffles a string slice +func stringsAreShuffled(slice []string) { + for i := range slice { + j := rand.Intn(i + 1) + slice[i], slice[j] = slice[j], slice[i] + } +} + +// lenSyncMap counts the length of a sync.map +func lenSyncMap(m sync.Map) uint { + len := uint(0) + m.Range(func(_, _ interface{}) bool { + len++ + return true + }) + return len +} + +func loadCertAndCertPool(config *config.Network) (*tls.Certificate, *x509.CertPool, error) { + // Load the certificates from disk + cert, err := tls.LoadX509KeyPair(config.PeerCrtPath, config.PeerKeyPath) + if err != nil { + glog.Fatalf("could not load peer key pair: %v", err) + return nil, nil, err + } + + // Create a certificate pool from the certificate authority + certPool := x509.NewCertPool() + caCert, err := ioutil.ReadFile(config.CACrtPath) + if err != nil { + glog.Fatalf("could not read ca certificate: %v", err) + return nil, nil, err + } + + // Append the peer certificates from the CA + ok := certPool.AppendCertsFromPEM(caCert) + if !ok { + return nil, nil, errors.New("failed to append peer certs") + } + return &cert, certPool, nil +} + +func generateServerCredentials(config *config.Network) (credentials.TransportCredentials, error) { + cert, certPool, err := loadCertAndCertPool(config) + if err != nil { + return nil, err + } + + // Return the server TLS credentials + return credentials.NewTLS(&tls.Config{ + ClientAuth: tls.RequireAndVerifyClientCert, + Certificates: []tls.Certificate{*cert}, + ClientCAs: certPool, + }), nil +} + +func generateClientCredentials(config *config.Network) (credentials.TransportCredentials, error) { + cert, certPool, err := loadCertAndCertPool(config) + if err != nil { + return nil, err + } + + // Return the client TLS credentials + return credentials.NewTLS(&tls.Config{ + Certificates: []tls.Certificate{*cert}, + RootCAs: certPool, + }), nil +} diff --git a/proto/blockchain.pb.go b/proto/blockchain.pb.go new file mode 100644 index 0000000000..84c4165eb7 --- /dev/null +++ b/proto/blockchain.pb.go @@ -0,0 +1,553 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: blockchain.proto + +/* +Package iproto is a generated protocol buffer package. + +It is generated from these files: + blockchain.proto + rpc.proto + utxo.proto + +It has these top-level messages: + TxInputPb + TxOutputPb + TxPb + BlockHeaderPb + BlockPb + BlockIndex + PingMsg + PongMsg + BlockSync + BlockContainer + ViewChangeMsg + TestPayload + CreateRawTxRequest + CreateRawTxReply + SendTxRequest + SendTxReply + UtxoPb + UtxoEntryPb + UtxoMapPb +*/ +package iproto + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the proto package it is being compiled against. +// A compilation error at this line likely means your copy of the +// proto package needs to be updated. +const _ = proto.ProtoPackageIsVersion2 // please upgrade the proto package + +type ViewChangeMsg_ViewChangeType int32 + +const ( + ViewChangeMsg_INVALID_VIEW_CHANGE_TYPE ViewChangeMsg_ViewChangeType = 0 + ViewChangeMsg_PROPOSE ViewChangeMsg_ViewChangeType = 1 + ViewChangeMsg_PREVOTE ViewChangeMsg_ViewChangeType = 2 + ViewChangeMsg_VOTE ViewChangeMsg_ViewChangeType = 3 +) + +var ViewChangeMsg_ViewChangeType_name = map[int32]string{ + 0: "INVALID_VIEW_CHANGE_TYPE", + 1: "PROPOSE", + 2: "PREVOTE", + 3: "VOTE", +} +var ViewChangeMsg_ViewChangeType_value = map[string]int32{ + "INVALID_VIEW_CHANGE_TYPE": 0, + "PROPOSE": 1, + "PREVOTE": 2, + "VOTE": 3, +} + +func (x ViewChangeMsg_ViewChangeType) String() string { + return proto.EnumName(ViewChangeMsg_ViewChangeType_name, int32(x)) +} +func (ViewChangeMsg_ViewChangeType) EnumDescriptor() ([]byte, []int) { + return fileDescriptor0, []int{10, 0} +} + +type TxInputPb struct { + TxHash []byte `protobuf:"bytes,1,opt,name=txHash,proto3" json:"txHash,omitempty"` + OutIndex int32 `protobuf:"varint,2,opt,name=outIndex" json:"outIndex,omitempty"` + UnlockScriptSize uint32 `protobuf:"varint,3,opt,name=unlockScriptSize" json:"unlockScriptSize,omitempty"` + UnlockScript []byte `protobuf:"bytes,4,opt,name=unlockScript,proto3" json:"unlockScript,omitempty"` + Sequence uint32 `protobuf:"varint,5,opt,name=sequence" json:"sequence,omitempty"` +} + +func (m *TxInputPb) Reset() { *m = TxInputPb{} } +func (m *TxInputPb) String() string { return proto.CompactTextString(m) } +func (*TxInputPb) ProtoMessage() {} +func (*TxInputPb) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{0} } + +func (m *TxInputPb) GetTxHash() []byte { + if m != nil { + return m.TxHash + } + return nil +} + +func (m *TxInputPb) GetOutIndex() int32 { + if m != nil { + return m.OutIndex + } + return 0 +} + +func (m *TxInputPb) GetUnlockScriptSize() uint32 { + if m != nil { + return m.UnlockScriptSize + } + return 0 +} + +func (m *TxInputPb) GetUnlockScript() []byte { + if m != nil { + return m.UnlockScript + } + return nil +} + +func (m *TxInputPb) GetSequence() uint32 { + if m != nil { + return m.Sequence + } + return 0 +} + +// TxOutput stores “coins”. It is indivisible, which means that you cannot reference a part of its value. +// When an output is referenced in a new transaction, it’s spent as a whole. And if its value is greater than required, +// a change is generated and sent back to the sender. +type TxOutputPb struct { + Value uint64 `protobuf:"varint,1,opt,name=value" json:"value,omitempty"` + LockScriptSize uint32 `protobuf:"varint,2,opt,name=lockScriptSize" json:"lockScriptSize,omitempty"` + LockScript []byte `protobuf:"bytes,3,opt,name=lockScript,proto3" json:"lockScript,omitempty"` +} + +func (m *TxOutputPb) Reset() { *m = TxOutputPb{} } +func (m *TxOutputPb) String() string { return proto.CompactTextString(m) } +func (*TxOutputPb) ProtoMessage() {} +func (*TxOutputPb) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{1} } + +func (m *TxOutputPb) GetValue() uint64 { + if m != nil { + return m.Value + } + return 0 +} + +func (m *TxOutputPb) GetLockScriptSize() uint32 { + if m != nil { + return m.LockScriptSize + } + return 0 +} + +func (m *TxOutputPb) GetLockScript() []byte { + if m != nil { + return m.LockScript + } + return nil +} + +type TxPb struct { + Version uint32 `protobuf:"varint,1,opt,name=version" json:"version,omitempty"` + NumTxIn uint32 `protobuf:"varint,2,opt,name=numTxIn" json:"numTxIn,omitempty"` + TxIn []*TxInputPb `protobuf:"bytes,3,rep,name=txIn" json:"txIn,omitempty"` + NumTxOut uint32 `protobuf:"varint,4,opt,name=numTxOut" json:"numTxOut,omitempty"` + TxOut []*TxOutputPb `protobuf:"bytes,5,rep,name=txOut" json:"txOut,omitempty"` + LockTime uint32 `protobuf:"varint,6,opt,name=lockTime" json:"lockTime,omitempty"` +} + +func (m *TxPb) Reset() { *m = TxPb{} } +func (m *TxPb) String() string { return proto.CompactTextString(m) } +func (*TxPb) ProtoMessage() {} +func (*TxPb) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{2} } + +func (m *TxPb) GetVersion() uint32 { + if m != nil { + return m.Version + } + return 0 +} + +func (m *TxPb) GetNumTxIn() uint32 { + if m != nil { + return m.NumTxIn + } + return 0 +} + +func (m *TxPb) GetTxIn() []*TxInputPb { + if m != nil { + return m.TxIn + } + return nil +} + +func (m *TxPb) GetNumTxOut() uint32 { + if m != nil { + return m.NumTxOut + } + return 0 +} + +func (m *TxPb) GetTxOut() []*TxOutputPb { + if m != nil { + return m.TxOut + } + return nil +} + +func (m *TxPb) GetLockTime() uint32 { + if m != nil { + return m.LockTime + } + return 0 +} + +// header of a block +type BlockHeaderPb struct { + Version uint32 `protobuf:"varint,1,opt,name=version" json:"version,omitempty"` + ChainID uint32 `protobuf:"varint,2,opt,name=chainID" json:"chainID,omitempty"` + Height uint32 `protobuf:"varint,3,opt,name=height" json:"height,omitempty"` + Timestamp uint64 `protobuf:"varint,4,opt,name=timestamp" json:"timestamp,omitempty"` + PrevBlockHash []byte `protobuf:"bytes,5,opt,name=prevBlockHash,proto3" json:"prevBlockHash,omitempty"` + MerkleRoot []byte `protobuf:"bytes,6,opt,name=merkleRoot,proto3" json:"merkleRoot,omitempty"` + TrnxNumber uint32 `protobuf:"varint,7,opt,name=trnxNumber" json:"trnxNumber,omitempty"` + TrnxDataSize uint32 `protobuf:"varint,8,opt,name=trnxDataSize" json:"trnxDataSize,omitempty"` +} + +func (m *BlockHeaderPb) Reset() { *m = BlockHeaderPb{} } +func (m *BlockHeaderPb) String() string { return proto.CompactTextString(m) } +func (*BlockHeaderPb) ProtoMessage() {} +func (*BlockHeaderPb) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{3} } + +func (m *BlockHeaderPb) GetVersion() uint32 { + if m != nil { + return m.Version + } + return 0 +} + +func (m *BlockHeaderPb) GetChainID() uint32 { + if m != nil { + return m.ChainID + } + return 0 +} + +func (m *BlockHeaderPb) GetHeight() uint32 { + if m != nil { + return m.Height + } + return 0 +} + +func (m *BlockHeaderPb) GetTimestamp() uint64 { + if m != nil { + return m.Timestamp + } + return 0 +} + +func (m *BlockHeaderPb) GetPrevBlockHash() []byte { + if m != nil { + return m.PrevBlockHash + } + return nil +} + +func (m *BlockHeaderPb) GetMerkleRoot() []byte { + if m != nil { + return m.MerkleRoot + } + return nil +} + +func (m *BlockHeaderPb) GetTrnxNumber() uint32 { + if m != nil { + return m.TrnxNumber + } + return 0 +} + +func (m *BlockHeaderPb) GetTrnxDataSize() uint32 { + if m != nil { + return m.TrnxDataSize + } + return 0 +} + +// block consists of header followed by transactions +// hash of current block can be computed from header hence not stored +type BlockPb struct { + Header *BlockHeaderPb `protobuf:"bytes,1,opt,name=Header" json:"Header,omitempty"` + Transactions []*TxPb `protobuf:"bytes,2,rep,name=Transactions" json:"Transactions,omitempty"` +} + +func (m *BlockPb) Reset() { *m = BlockPb{} } +func (m *BlockPb) String() string { return proto.CompactTextString(m) } +func (*BlockPb) ProtoMessage() {} +func (*BlockPb) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{4} } + +func (m *BlockPb) GetHeader() *BlockHeaderPb { + if m != nil { + return m.Header + } + return nil +} + +func (m *BlockPb) GetTransactions() []*TxPb { + if m != nil { + return m.Transactions + } + return nil +} + +// index of block raw data file +type BlockIndex struct { + Start uint32 `protobuf:"varint,1,opt,name=start" json:"start,omitempty"` + End uint32 `protobuf:"varint,2,opt,name=end" json:"end,omitempty"` + Offset []uint32 `protobuf:"varint,3,rep,packed,name=offset" json:"offset,omitempty"` +} + +func (m *BlockIndex) Reset() { *m = BlockIndex{} } +func (m *BlockIndex) String() string { return proto.CompactTextString(m) } +func (*BlockIndex) ProtoMessage() {} +func (*BlockIndex) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{5} } + +func (m *BlockIndex) GetStart() uint32 { + if m != nil { + return m.Start + } + return 0 +} + +func (m *BlockIndex) GetEnd() uint32 { + if m != nil { + return m.End + } + return 0 +} + +func (m *BlockIndex) GetOffset() []uint32 { + if m != nil { + return m.Offset + } + return nil +} + +// ////////////////////////////////////////////////////////////////////////////////////////////////// +// BELOW ARE DEFINITIONS FOR ON-WIRE MESSAGES! +// ////////////////////////////////////////////////////////////////////////////////////////////////// +type PingMsg struct { + Nonce uint64 `protobuf:"varint,1,opt,name=nonce" json:"nonce,omitempty"` +} + +func (m *PingMsg) Reset() { *m = PingMsg{} } +func (m *PingMsg) String() string { return proto.CompactTextString(m) } +func (*PingMsg) ProtoMessage() {} +func (*PingMsg) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{6} } + +func (m *PingMsg) GetNonce() uint64 { + if m != nil { + return m.Nonce + } + return 0 +} + +type PongMsg struct { + AckNonce uint64 `protobuf:"varint,1,opt,name=ack_nonce,json=ackNonce" json:"ack_nonce,omitempty"` +} + +func (m *PongMsg) Reset() { *m = PongMsg{} } +func (m *PongMsg) String() string { return proto.CompactTextString(m) } +func (*PongMsg) ProtoMessage() {} +func (*PongMsg) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{7} } + +func (m *PongMsg) GetAckNonce() uint64 { + if m != nil { + return m.AckNonce + } + return 0 +} + +type BlockSync struct { + Start uint32 `protobuf:"varint,2,opt,name=start" json:"start,omitempty"` + End uint32 `protobuf:"varint,3,opt,name=end" json:"end,omitempty"` +} + +func (m *BlockSync) Reset() { *m = BlockSync{} } +func (m *BlockSync) String() string { return proto.CompactTextString(m) } +func (*BlockSync) ProtoMessage() {} +func (*BlockSync) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{8} } + +func (m *BlockSync) GetStart() uint32 { + if m != nil { + return m.Start + } + return 0 +} + +func (m *BlockSync) GetEnd() uint32 { + if m != nil { + return m.End + } + return 0 +} + +// block container +// used to send old/existing blocks in block sync +type BlockContainer struct { + Block *BlockPb `protobuf:"bytes,1,opt,name=block" json:"block,omitempty"` +} + +func (m *BlockContainer) Reset() { *m = BlockContainer{} } +func (m *BlockContainer) String() string { return proto.CompactTextString(m) } +func (*BlockContainer) ProtoMessage() {} +func (*BlockContainer) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{9} } + +func (m *BlockContainer) GetBlock() *BlockPb { + if m != nil { + return m.Block + } + return nil +} + +type ViewChangeMsg struct { + Vctype ViewChangeMsg_ViewChangeType `protobuf:"varint,1,opt,name=vctype,enum=iproto.ViewChangeMsg_ViewChangeType" json:"vctype,omitempty"` + Block *BlockPb `protobuf:"bytes,2,opt,name=block" json:"block,omitempty"` + BlockHash []byte `protobuf:"bytes,3,opt,name=blockHash,proto3" json:"blockHash,omitempty"` + SenderAddr string `protobuf:"bytes,4,opt,name=senderAddr" json:"senderAddr,omitempty"` +} + +func (m *ViewChangeMsg) Reset() { *m = ViewChangeMsg{} } +func (m *ViewChangeMsg) String() string { return proto.CompactTextString(m) } +func (*ViewChangeMsg) ProtoMessage() {} +func (*ViewChangeMsg) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{10} } + +func (m *ViewChangeMsg) GetVctype() ViewChangeMsg_ViewChangeType { + if m != nil { + return m.Vctype + } + return ViewChangeMsg_INVALID_VIEW_CHANGE_TYPE +} + +func (m *ViewChangeMsg) GetBlock() *BlockPb { + if m != nil { + return m.Block + } + return nil +} + +func (m *ViewChangeMsg) GetBlockHash() []byte { + if m != nil { + return m.BlockHash + } + return nil +} + +func (m *ViewChangeMsg) GetSenderAddr() string { + if m != nil { + return m.SenderAddr + } + return "" +} + +// ////////////////////////////////////////////////////////////////////////////////////////////////// +// BELOW ARE DEFINITIONS FOR TEST-ONLY MESSAGES! +// ////////////////////////////////////////////////////////////////////////////////////////////////// +type TestPayload struct { + MsgBody []byte `protobuf:"bytes,1,opt,name=msg_body,json=msgBody,proto3" json:"msg_body,omitempty"` +} + +func (m *TestPayload) Reset() { *m = TestPayload{} } +func (m *TestPayload) String() string { return proto.CompactTextString(m) } +func (*TestPayload) ProtoMessage() {} +func (*TestPayload) Descriptor() ([]byte, []int) { return fileDescriptor0, []int{11} } + +func (m *TestPayload) GetMsgBody() []byte { + if m != nil { + return m.MsgBody + } + return nil +} + +func init() { + proto.RegisterType((*TxInputPb)(nil), "iproto.TxInputPb") + proto.RegisterType((*TxOutputPb)(nil), "iproto.TxOutputPb") + proto.RegisterType((*TxPb)(nil), "iproto.TxPb") + proto.RegisterType((*BlockHeaderPb)(nil), "iproto.BlockHeaderPb") + proto.RegisterType((*BlockPb)(nil), "iproto.BlockPb") + proto.RegisterType((*BlockIndex)(nil), "iproto.BlockIndex") + proto.RegisterType((*PingMsg)(nil), "iproto.PingMsg") + proto.RegisterType((*PongMsg)(nil), "iproto.PongMsg") + proto.RegisterType((*BlockSync)(nil), "iproto.BlockSync") + proto.RegisterType((*BlockContainer)(nil), "iproto.BlockContainer") + proto.RegisterType((*ViewChangeMsg)(nil), "iproto.ViewChangeMsg") + proto.RegisterType((*TestPayload)(nil), "iproto.TestPayload") + proto.RegisterEnum("iproto.ViewChangeMsg_ViewChangeType", ViewChangeMsg_ViewChangeType_name, ViewChangeMsg_ViewChangeType_value) +} + +func init() { proto.RegisterFile("blockchain.proto", fileDescriptor0) } + +var fileDescriptor0 = []byte{ + // 737 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x84, 0x54, 0x5d, 0x6f, 0xe2, 0x46, + 0x14, 0x2d, 0x06, 0xf3, 0x71, 0xf9, 0x28, 0x1d, 0xa5, 0x95, 0xdb, 0x46, 0x2d, 0xb2, 0x92, 0x08, + 0x55, 0x6a, 0x54, 0x25, 0x0f, 0x7d, 0xe9, 0x4b, 0x3e, 0x50, 0x83, 0x94, 0x82, 0x35, 0x58, 0x54, + 0xfb, 0x84, 0xc6, 0xf6, 0x04, 0x1c, 0xf0, 0x98, 0xb5, 0xc7, 0x2c, 0xec, 0xeb, 0x4a, 0xfb, 0x67, + 0xf6, 0x67, 0xec, 0x1f, 0x5b, 0xcd, 0xb5, 0x01, 0x3b, 0xbb, 0xda, 0x7d, 0x82, 0x73, 0xe6, 0xce, + 0xbd, 0x67, 0xce, 0x9c, 0x31, 0x74, 0x9d, 0x55, 0xe8, 0x2e, 0xdd, 0x05, 0xf3, 0xc5, 0xe5, 0x3a, + 0x0a, 0x65, 0x48, 0xaa, 0x3e, 0xfe, 0x9a, 0x1f, 0x4a, 0xd0, 0xb0, 0xb7, 0x43, 0xb1, 0x4e, 0xa4, + 0xe5, 0x90, 0x9f, 0xa0, 0x2a, 0xb7, 0x0f, 0x2c, 0x5e, 0x18, 0xa5, 0x5e, 0xa9, 0xdf, 0xa2, 0x19, + 0x22, 0xbf, 0x40, 0x3d, 0x4c, 0xe4, 0x50, 0x78, 0x7c, 0x6b, 0x68, 0xbd, 0x52, 0x5f, 0xa7, 0x07, + 0x4c, 0xfe, 0x80, 0x6e, 0x22, 0x54, 0xfb, 0x89, 0x1b, 0xf9, 0x6b, 0x39, 0xf1, 0xdf, 0x72, 0xa3, + 0xdc, 0x2b, 0xf5, 0xdb, 0xf4, 0x33, 0x9e, 0x98, 0xd0, 0xca, 0x73, 0x46, 0x05, 0xa7, 0x14, 0x38, + 0x35, 0x2b, 0xe6, 0xaf, 0x13, 0x2e, 0x5c, 0x6e, 0xe8, 0xd8, 0xe7, 0x80, 0xcd, 0x67, 0x00, 0x7b, + 0x3b, 0x4e, 0x64, 0xaa, 0xf6, 0x04, 0xf4, 0x0d, 0x5b, 0x25, 0x1c, 0xc5, 0x56, 0x68, 0x0a, 0xc8, + 0x05, 0x74, 0x5e, 0xa8, 0xd1, 0xb0, 0xcb, 0x0b, 0x96, 0xfc, 0x06, 0x90, 0x53, 0x52, 0x46, 0x25, + 0x39, 0xc6, 0xfc, 0x58, 0x82, 0x8a, 0xbd, 0xb5, 0x1c, 0x62, 0x40, 0x6d, 0xc3, 0xa3, 0xd8, 0x0f, + 0x05, 0x0e, 0x6a, 0xd3, 0x3d, 0x54, 0x2b, 0x22, 0x09, 0x94, 0x7d, 0xd9, 0x8c, 0x3d, 0x24, 0xe7, + 0x50, 0x91, 0x8a, 0x2e, 0xf7, 0xca, 0xfd, 0xe6, 0xd5, 0x0f, 0x97, 0xa9, 0xdb, 0x97, 0x07, 0xa7, + 0x29, 0x2e, 0xab, 0xb3, 0xe2, 0x8e, 0x71, 0x92, 0x7a, 0xd1, 0xa6, 0x07, 0x4c, 0xfa, 0xa0, 0x4b, + 0x5c, 0xd0, 0xb1, 0x07, 0x39, 0xf6, 0xd8, 0x1b, 0x40, 0xd3, 0x02, 0xd5, 0x45, 0xe9, 0xb6, 0xfd, + 0x80, 0x1b, 0xd5, 0xb4, 0xcb, 0x1e, 0x9b, 0xef, 0x34, 0x68, 0xdf, 0x2a, 0xf4, 0xc0, 0x99, 0xc7, + 0xa3, 0x6f, 0x1d, 0x07, 0x23, 0x32, 0xbc, 0xdf, 0x1f, 0x27, 0x83, 0x2a, 0x17, 0x0b, 0xee, 0xcf, + 0x17, 0x32, 0xbb, 0xd9, 0x0c, 0x91, 0x53, 0x68, 0x48, 0x3f, 0xe0, 0xb1, 0x64, 0xc1, 0x1a, 0x0f, + 0x50, 0xa1, 0x47, 0x82, 0x9c, 0x41, 0x7b, 0x1d, 0xf1, 0x4d, 0x3a, 0x5e, 0x85, 0x4a, 0x47, 0x93, + 0x8b, 0xa4, 0xba, 0x87, 0x80, 0x47, 0xcb, 0x15, 0xa7, 0x61, 0x28, 0x51, 0x7f, 0x8b, 0xe6, 0x18, + 0xb5, 0x2e, 0x23, 0xb1, 0x1d, 0x25, 0x81, 0xc3, 0x23, 0xa3, 0x86, 0xf3, 0x73, 0x8c, 0xca, 0x94, + 0x42, 0xf7, 0x4c, 0x32, 0xbc, 0xed, 0x3a, 0x56, 0x14, 0x38, 0xf3, 0x19, 0x6a, 0x38, 0xd0, 0x72, + 0xc8, 0x9f, 0x50, 0x4d, 0xad, 0xc0, 0xd3, 0x37, 0xaf, 0x7e, 0xdc, 0xfb, 0x5a, 0x70, 0x89, 0x66, + 0x45, 0xe4, 0x2f, 0x68, 0xd9, 0x11, 0x13, 0x31, 0x73, 0xa5, 0x1f, 0x8a, 0xd8, 0xd0, 0xf0, 0x32, + 0x5a, 0xc7, 0xcb, 0xb0, 0x1c, 0x5a, 0xa8, 0x30, 0x1f, 0x01, 0xb0, 0x55, 0xfa, 0x3a, 0x4e, 0x40, + 0x8f, 0x25, 0x8b, 0x64, 0xe6, 0x75, 0x0a, 0x48, 0x17, 0xca, 0x5c, 0x78, 0x99, 0xcb, 0xea, 0xaf, + 0x72, 0x38, 0x7c, 0x7a, 0x8a, 0xb9, 0xc4, 0xc8, 0xb4, 0x69, 0x86, 0xcc, 0xdf, 0xa1, 0x66, 0xf9, + 0x62, 0xfe, 0x5f, 0x3c, 0x57, 0xad, 0x44, 0xa8, 0x5e, 0x45, 0x16, 0x77, 0x04, 0xe6, 0x05, 0xd4, + 0xac, 0x30, 0x2d, 0xf8, 0x15, 0x1a, 0xcc, 0x5d, 0xce, 0xf2, 0x45, 0x75, 0xe6, 0x2e, 0x47, 0x58, + 0x77, 0x0d, 0x0d, 0x94, 0x35, 0xd9, 0x09, 0xf7, 0xa8, 0x4a, 0xfb, 0x82, 0xaa, 0xf2, 0x41, 0x95, + 0xf9, 0x37, 0x74, 0x70, 0xd3, 0x5d, 0x28, 0x24, 0xf3, 0x05, 0x8f, 0xc8, 0x39, 0xe8, 0xf8, 0x2d, + 0xc9, 0xdc, 0xfb, 0xbe, 0xe0, 0x9e, 0x8a, 0x24, 0xae, 0x9a, 0xef, 0x35, 0x68, 0x4f, 0x7d, 0xfe, + 0xe6, 0x6e, 0xc1, 0xc4, 0x9c, 0x2b, 0x71, 0xff, 0x40, 0x75, 0xe3, 0xca, 0xdd, 0x3a, 0x55, 0xd6, + 0xb9, 0x3a, 0xdb, 0xef, 0x2c, 0x94, 0xe5, 0x90, 0xbd, 0x5b, 0x73, 0x9a, 0xed, 0x39, 0x8e, 0xd5, + 0xbe, 0x36, 0x56, 0xe5, 0xd1, 0x39, 0xa4, 0x2d, 0x7d, 0xd2, 0x47, 0x42, 0x25, 0x29, 0xe6, 0xc2, + 0xe3, 0xd1, 0x8d, 0xe7, 0x45, 0x18, 0xd7, 0x06, 0xcd, 0x31, 0x26, 0x85, 0x4e, 0x71, 0x3c, 0x39, + 0x05, 0x63, 0x38, 0x9a, 0xde, 0x3c, 0x0e, 0xef, 0x67, 0xd3, 0xe1, 0xe0, 0xff, 0xd9, 0xdd, 0xc3, + 0xcd, 0xe8, 0xdf, 0xc1, 0xcc, 0x7e, 0x65, 0x0d, 0xba, 0xdf, 0x91, 0x26, 0xd4, 0x2c, 0x3a, 0xb6, + 0xc6, 0x93, 0x41, 0xb7, 0x94, 0x82, 0xc1, 0x74, 0x6c, 0x0f, 0xba, 0x1a, 0xa9, 0x43, 0x05, 0xff, + 0x95, 0xcd, 0x3e, 0x34, 0x6d, 0x1e, 0x4b, 0x8b, 0xed, 0x56, 0x21, 0xf3, 0xc8, 0xcf, 0x50, 0x0f, + 0xe2, 0xf9, 0xcc, 0x09, 0xbd, 0x5d, 0xf6, 0x89, 0xad, 0x05, 0xf1, 0xfc, 0x36, 0xf4, 0x76, 0x4e, + 0x15, 0x4f, 0x74, 0xfd, 0x29, 0x00, 0x00, 0xff, 0xff, 0x76, 0x07, 0x9e, 0xb6, 0xac, 0x05, 0x00, + 0x00, +} diff --git a/proto/blockchain.proto b/proto/blockchain.proto new file mode 100644 index 0000000000..6da9f8af29 --- /dev/null +++ b/proto/blockchain.proto @@ -0,0 +1,104 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +// To compile the proto, run: +// protoc --go_out=plugins=grpc:. *.proto +syntax = "proto3"; +package iproto; + +message TxInputPb { + bytes txHash = 1; // ref a prev output transaction + int32 outIndex = 2; // index of an output in the transaction + uint32 unlockScriptSize = 3; + bytes unlockScript = 4; + uint32 sequence = 5; +} + +// TxOutput stores “coins”. It is indivisible, which means that you cannot reference a part of its value. +// When an output is referenced in a new transaction, it’s spent as a whole. And if its value is greater than required, +// a change is generated and sent back to the sender. +message TxOutputPb { + uint64 value = 1; + uint32 lockScriptSize = 2; + bytes lockScript = 3; +} + +message TxPb { + uint32 version = 1; + uint32 numTxIn = 2; + repeated TxInputPb txIn = 3; + uint32 numTxOut = 4; + repeated TxOutputPb txOut = 5; + uint32 lockTime = 6; +} + +// header of a block +message BlockHeaderPb { + uint32 version = 1; + uint32 chainID = 2; + uint32 height = 3; + uint64 timestamp = 4; + bytes prevBlockHash = 5; + bytes merkleRoot = 6; + uint32 trnxNumber = 7; + uint32 trnxDataSize = 8; +} + +// block consists of header followed by transactions +// hash of current block can be computed from header hence not stored +message BlockPb { + BlockHeaderPb Header = 1; + repeated TxPb Transactions = 2; +} + +// index of block raw data file +message BlockIndex { + uint32 start = 1; + uint32 end = 2; + repeated uint32 offset = 3; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// BELOW ARE DEFINITIONS FOR ON-WIRE MESSAGES! +//////////////////////////////////////////////////////////////////////////////////////////////////// +message PingMsg { + uint64 nonce = 1; +} + +message PongMsg { + uint64 ack_nonce = 1; +} + +message BlockSync { + uint32 start = 2; + uint32 end = 3; +} + +// block container +// used to send old/existing blocks in block sync +message BlockContainer { + BlockPb block = 1; +} + +message ViewChangeMsg { + enum ViewChangeType { + INVALID_VIEW_CHANGE_TYPE = 0; + PROPOSE = 1; + PREVOTE = 2; + VOTE = 3; + } + ViewChangeType vctype = 1; + BlockPb block = 2; + bytes blockHash = 3; + string senderAddr = 4; +} + +//////////////////////////////////////////////////////////////////////////////////////////////////// +// BELOW ARE DEFINITIONS FOR TEST-ONLY MESSAGES! +//////////////////////////////////////////////////////////////////////////////////////////////////// +message TestPayload { + bytes msg_body = 1; +} \ No newline at end of file diff --git a/proto/extra_interface.go b/proto/extra_interface.go new file mode 100644 index 0000000000..feb00b64ce --- /dev/null +++ b/proto/extra_interface.go @@ -0,0 +1,46 @@ +/* + * Add extra interface functions for proto messages + */ + +package iproto + +import ( + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/txvm" +) + +// TxInputFixedSize defines teh fixed size of transaction input +const TxInputFixedSize = 44 + +// TotalSize returns the total size (in bytes) of transaction input +func (in *TxInputPb) TotalSize() uint32 { + return TxInputFixedSize + uint32(in.UnlockScriptSize) +} + +// ByteStream returns a raw byte stream of transaction input +func (in *TxInputPb) ByteStream() []byte { + stream := in.TxHash[:] + + temp := make([]byte, 4) + cm.MachineEndian.PutUint32(temp, uint32(in.OutIndex)) + stream = append(stream, temp...) + cm.MachineEndian.PutUint32(temp, in.UnlockScriptSize) + stream = append(stream, temp...) + stream = append(stream, in.UnlockScript...) + cm.MachineEndian.PutUint32(temp, in.Sequence) + stream = append(stream, temp...) + + return stream +} + +// UnlockSuccess checks whether the TxInput can unlock the provided script +func (in *TxInputPb) UnlockSuccess(lockScript []byte) bool { + v, err := txvm.NewIVM(in.ByteStream(), append(in.UnlockScript, lockScript...)) + if err != nil { + return false + } + if err := v.Execute(); err != nil { + return false + } + return true +} diff --git a/proto/rpc.pb.go b/proto/rpc.pb.go new file mode 100644 index 0000000000..41dfae605c --- /dev/null +++ b/proto/rpc.pb.go @@ -0,0 +1,240 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: rpc.proto + +package iproto + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +import ( + context "golang.org/x/net/context" + grpc "google.golang.org/grpc" +) + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type CreateRawTxRequest struct { + From string `protobuf:"bytes,1,opt,name=from" json:"from,omitempty"` + To string `protobuf:"bytes,2,opt,name=to" json:"to,omitempty"` + Fee uint64 `protobuf:"varint,3,opt,name=fee" json:"fee,omitempty"` + Value uint64 `protobuf:"varint,4,opt,name=value" json:"value,omitempty"` + Data []byte `protobuf:"bytes,5,opt,name=data,proto3" json:"data,omitempty"` +} + +func (m *CreateRawTxRequest) Reset() { *m = CreateRawTxRequest{} } +func (m *CreateRawTxRequest) String() string { return proto.CompactTextString(m) } +func (*CreateRawTxRequest) ProtoMessage() {} +func (*CreateRawTxRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{0} } + +func (m *CreateRawTxRequest) GetFrom() string { + if m != nil { + return m.From + } + return "" +} + +func (m *CreateRawTxRequest) GetTo() string { + if m != nil { + return m.To + } + return "" +} + +func (m *CreateRawTxRequest) GetFee() uint64 { + if m != nil { + return m.Fee + } + return 0 +} + +func (m *CreateRawTxRequest) GetValue() uint64 { + if m != nil { + return m.Value + } + return 0 +} + +func (m *CreateRawTxRequest) GetData() []byte { + if m != nil { + return m.Data + } + return nil +} + +type CreateRawTxReply struct { + SerializedTx []byte `protobuf:"bytes,1,opt,name=serialized_tx,json=serializedTx,proto3" json:"serialized_tx,omitempty"` +} + +func (m *CreateRawTxReply) Reset() { *m = CreateRawTxReply{} } +func (m *CreateRawTxReply) String() string { return proto.CompactTextString(m) } +func (*CreateRawTxReply) ProtoMessage() {} +func (*CreateRawTxReply) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{1} } + +func (m *CreateRawTxReply) GetSerializedTx() []byte { + if m != nil { + return m.SerializedTx + } + return nil +} + +type SendTxRequest struct { + SerializedTx []byte `protobuf:"bytes,1,opt,name=serialized_tx,json=serializedTx,proto3" json:"serialized_tx,omitempty"` +} + +func (m *SendTxRequest) Reset() { *m = SendTxRequest{} } +func (m *SendTxRequest) String() string { return proto.CompactTextString(m) } +func (*SendTxRequest) ProtoMessage() {} +func (*SendTxRequest) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{2} } + +func (m *SendTxRequest) GetSerializedTx() []byte { + if m != nil { + return m.SerializedTx + } + return nil +} + +type SendTxReply struct { +} + +func (m *SendTxReply) Reset() { *m = SendTxReply{} } +func (m *SendTxReply) String() string { return proto.CompactTextString(m) } +func (*SendTxReply) ProtoMessage() {} +func (*SendTxReply) Descriptor() ([]byte, []int) { return fileDescriptor1, []int{3} } + +func init() { + proto.RegisterType((*CreateRawTxRequest)(nil), "iproto.CreateRawTxRequest") + proto.RegisterType((*CreateRawTxReply)(nil), "iproto.CreateRawTxReply") + proto.RegisterType((*SendTxRequest)(nil), "iproto.SendTxRequest") + proto.RegisterType((*SendTxReply)(nil), "iproto.SendTxReply") +} + +// Reference imports to suppress errors if they are not otherwise used. +var _ context.Context +var _ grpc.ClientConn + +// This is a compile-time assertion to ensure that this generated file +// is compatible with the grpc package it is being compiled against. +const _ = grpc.SupportPackageIsVersion4 + +// Client API for ChainService service + +type ChainServiceClient interface { + CreateRawTx(ctx context.Context, in *CreateRawTxRequest, opts ...grpc.CallOption) (*CreateRawTxReply, error) + SendTx(ctx context.Context, in *SendTxRequest, opts ...grpc.CallOption) (*SendTxReply, error) +} + +type chainServiceClient struct { + cc *grpc.ClientConn +} + +func NewChainServiceClient(cc *grpc.ClientConn) ChainServiceClient { + return &chainServiceClient{cc} +} + +func (c *chainServiceClient) CreateRawTx(ctx context.Context, in *CreateRawTxRequest, opts ...grpc.CallOption) (*CreateRawTxReply, error) { + out := new(CreateRawTxReply) + err := grpc.Invoke(ctx, "/iproto.ChainService/CreateRawTx", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *chainServiceClient) SendTx(ctx context.Context, in *SendTxRequest, opts ...grpc.CallOption) (*SendTxReply, error) { + out := new(SendTxReply) + err := grpc.Invoke(ctx, "/iproto.ChainService/SendTx", in, out, c.cc, opts...) + if err != nil { + return nil, err + } + return out, nil +} + +// Server API for ChainService service + +type ChainServiceServer interface { + CreateRawTx(context.Context, *CreateRawTxRequest) (*CreateRawTxReply, error) + SendTx(context.Context, *SendTxRequest) (*SendTxReply, error) +} + +func RegisterChainServiceServer(s *grpc.Server, srv ChainServiceServer) { + s.RegisterService(&_ChainService_serviceDesc, srv) +} + +func _ChainService_CreateRawTx_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(CreateRawTxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChainServiceServer).CreateRawTx(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/iproto.ChainService/CreateRawTx", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChainServiceServer).CreateRawTx(ctx, req.(*CreateRawTxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _ChainService_SendTx_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(SendTxRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(ChainServiceServer).SendTx(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: "/iproto.ChainService/SendTx", + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(ChainServiceServer).SendTx(ctx, req.(*SendTxRequest)) + } + return interceptor(ctx, in, info, handler) +} + +var _ChainService_serviceDesc = grpc.ServiceDesc{ + ServiceName: "iproto.ChainService", + HandlerType: (*ChainServiceServer)(nil), + Methods: []grpc.MethodDesc{ + { + MethodName: "CreateRawTx", + Handler: _ChainService_CreateRawTx_Handler, + }, + { + MethodName: "SendTx", + Handler: _ChainService_SendTx_Handler, + }, + }, + Streams: []grpc.StreamDesc{}, + Metadata: "rpc.proto", +} + +func init() { proto.RegisterFile("rpc.proto", fileDescriptor1) } + +var fileDescriptor1 = []byte{ + // 252 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x8c, 0x90, 0x41, 0x4e, 0xc3, 0x30, + 0x10, 0x45, 0x71, 0x9a, 0x46, 0xea, 0x34, 0x41, 0xd5, 0x00, 0x92, 0x95, 0x55, 0x14, 0x36, 0x59, + 0x65, 0x01, 0x08, 0x0e, 0x50, 0x71, 0x01, 0xb7, 0x7b, 0x64, 0x9a, 0xa9, 0xb0, 0x64, 0x6a, 0xe3, + 0xba, 0x25, 0xe1, 0x0e, 0xdc, 0x19, 0xd5, 0x51, 0x55, 0x02, 0x2c, 0x58, 0xf9, 0xfb, 0xd9, 0x7f, + 0xfe, 0xd7, 0xc0, 0xc4, 0xd9, 0x55, 0x6d, 0x9d, 0xf1, 0x06, 0x13, 0x15, 0xce, 0xd2, 0x03, 0xce, + 0x1d, 0x49, 0x4f, 0x42, 0xbe, 0x2f, 0x5b, 0x41, 0x6f, 0x3b, 0xda, 0x7a, 0x44, 0x88, 0xd7, 0xce, + 0xbc, 0x72, 0x56, 0xb0, 0x6a, 0x22, 0x82, 0xc6, 0x73, 0x88, 0xbc, 0xe1, 0x51, 0x20, 0x91, 0x37, + 0x38, 0x83, 0xd1, 0x9a, 0x88, 0x8f, 0x0a, 0x56, 0xc5, 0xe2, 0x20, 0xf1, 0x12, 0xc6, 0x7b, 0xa9, + 0x77, 0xc4, 0xe3, 0xc0, 0xfa, 0xcb, 0x61, 0x56, 0x23, 0xbd, 0xe4, 0xe3, 0x82, 0x55, 0xa9, 0x08, + 0xba, 0x7c, 0x80, 0xd9, 0x20, 0xd5, 0xea, 0x0e, 0xaf, 0x21, 0xdb, 0x92, 0x53, 0x52, 0xab, 0x0f, + 0x6a, 0x9e, 0x7c, 0x1b, 0xc2, 0x53, 0x91, 0x9e, 0xe0, 0xb2, 0x2d, 0xef, 0x20, 0x5b, 0xd0, 0xa6, + 0x39, 0x35, 0xfd, 0x97, 0x2b, 0x83, 0xe9, 0xd1, 0x65, 0x75, 0x77, 0xf3, 0xc9, 0x20, 0x9d, 0xbf, + 0x48, 0xb5, 0x59, 0x90, 0xdb, 0xab, 0x15, 0xe1, 0x23, 0x4c, 0xbf, 0xd5, 0xc1, 0xbc, 0xee, 0x97, + 0x53, 0xff, 0xde, 0x4c, 0xce, 0xff, 0x7c, 0xb3, 0xba, 0x2b, 0xcf, 0xf0, 0x1e, 0x92, 0x3e, 0x06, + 0xaf, 0x8e, 0xbf, 0x06, 0x65, 0xf3, 0x8b, 0x9f, 0x38, 0xf8, 0x9e, 0x93, 0x00, 0x6f, 0xbf, 0x02, + 0x00, 0x00, 0xff, 0xff, 0x03, 0x67, 0x44, 0x9f, 0x9f, 0x01, 0x00, 0x00, +} diff --git a/proto/rpc.proto b/proto/rpc.proto new file mode 100644 index 0000000000..a22b0add04 --- /dev/null +++ b/proto/rpc.proto @@ -0,0 +1,35 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +// To compile the proto, run: +// protoc --go_out=plugins=grpc:. *.proto +syntax = "proto3"; +package iproto; + +// The blockchain service definition +service ChainService { + rpc CreateRawTx (CreateRawTxRequest) returns (CreateRawTxReply) {} + rpc SendTx (SendTxRequest) returns (SendTxReply) {} +} + +message CreateRawTxRequest { + string from = 1; + string to = 2; + uint64 fee = 3; + uint64 value = 4; + bytes data = 5; +} + +message CreateRawTxReply { + bytes serialized_tx = 1; +} + +message SendTxRequest { + bytes serialized_tx = 1; +} + +message SendTxReply { +} diff --git a/proto/utils.go b/proto/utils.go new file mode 100644 index 0000000000..e940edcfdd --- /dev/null +++ b/proto/utils.go @@ -0,0 +1,81 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package iproto + +import ( + "errors" + "github.com/golang/protobuf/proto" +) + +// Magic header to identify IoTex traffic +const ( + MagicBroadcastMsgHeader uint32 = 4689 +) + +const ( + // UnknownProtoMsgType is an unknown message type that is not expected + UnknownProtoMsgType uint32 = 0 + // MsgTxProtoMsgType is for transactions broadcasted within the network + MsgTxProtoMsgType uint32 = 1 + // MsgBlockProtoMsgType is for blocks broadcasted within the network + MsgBlockProtoMsgType uint32 = 2 + // ViewChangeMsgType is for consensus flows within the network + ViewChangeMsgType uint32 = 3 + // MsgBlockSyncReqType is for requests among peers to sync blocks + MsgBlockSyncReqType uint32 = 4 + // MsgBlockSyncDataType is the response to messages of type MsgBlockSyncReqType + MsgBlockSyncDataType uint32 = 5 + // TestPayloadType is a test payload message type + TestPayloadType uint32 = 10001 +) + +// GetTypeFromProtoMsg retrieves the proto message type +func GetTypeFromProtoMsg(msg proto.Message) (uint32, error) { + switch msg.(type) { + case *TxPb: + return MsgTxProtoMsgType, nil + case *BlockPb: + return MsgBlockProtoMsgType, nil + case *ViewChangeMsg: + return ViewChangeMsgType, nil + case *BlockSync: + return MsgBlockSyncReqType, nil + case *BlockContainer: + return MsgBlockSyncDataType, nil + case *TestPayload: + return TestPayloadType, nil + default: + return UnknownProtoMsgType, errors.New("UnknownProtoMsgType proto message type") + } +} + +// TypifyProtoMsg unmarshal a proto message based on the given MessageType +func TypifyProtoMsg(tp uint32, msg []byte) (proto.Message, error) { + var m proto.Message + switch tp { + case MsgTxProtoMsgType: + m = &TxPb{} + case MsgBlockProtoMsgType: + m = &BlockPb{} + case ViewChangeMsgType: + m = &ViewChangeMsg{} + case MsgBlockSyncReqType: + m = &BlockSync{} + case MsgBlockSyncDataType: + m = &BlockContainer{} + case TestPayloadType: + m = &TestPayload{} + default: + return nil, errors.New("UnknownProtoMsgType proto message type") + } + + err := proto.Unmarshal(msg, m) + if err != nil { + return nil, err + } + return m, nil +} diff --git a/proto/utxo.pb.go b/proto/utxo.pb.go new file mode 100644 index 0000000000..64720e2689 --- /dev/null +++ b/proto/utxo.pb.go @@ -0,0 +1,118 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// source: utxo.proto + +package iproto + +import proto "github.com/golang/protobuf/proto" +import fmt "fmt" +import math "math" + +// Reference imports to suppress errors if they are not otherwise used. +var _ = proto.Marshal +var _ = fmt.Errorf +var _ = math.Inf + +type UtxoPb struct { + Value uint64 `protobuf:"varint,1,opt,name=value" json:"value,omitempty"` + Index int32 `protobuf:"varint,2,opt,name=index" json:"index,omitempty"` + LockScriptSize uint32 `protobuf:"varint,3,opt,name=lockScriptSize" json:"lockScriptSize,omitempty"` + LockScript []byte `protobuf:"bytes,4,opt,name=lockScript,proto3" json:"lockScript,omitempty"` +} + +func (m *UtxoPb) Reset() { *m = UtxoPb{} } +func (m *UtxoPb) String() string { return proto.CompactTextString(m) } +func (*UtxoPb) ProtoMessage() {} +func (*UtxoPb) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{0} } + +func (m *UtxoPb) GetValue() uint64 { + if m != nil { + return m.Value + } + return 0 +} + +func (m *UtxoPb) GetIndex() int32 { + if m != nil { + return m.Index + } + return 0 +} + +func (m *UtxoPb) GetLockScriptSize() uint32 { + if m != nil { + return m.LockScriptSize + } + return 0 +} + +func (m *UtxoPb) GetLockScript() []byte { + if m != nil { + return m.LockScript + } + return nil +} + +type UtxoEntryPb struct { + Hash []byte `protobuf:"bytes,1,opt,name=hash,proto3" json:"hash,omitempty"` + Utxo []*UtxoPb `protobuf:"bytes,2,rep,name=utxo" json:"utxo,omitempty"` +} + +func (m *UtxoEntryPb) Reset() { *m = UtxoEntryPb{} } +func (m *UtxoEntryPb) String() string { return proto.CompactTextString(m) } +func (*UtxoEntryPb) ProtoMessage() {} +func (*UtxoEntryPb) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{1} } + +func (m *UtxoEntryPb) GetHash() []byte { + if m != nil { + return m.Hash + } + return nil +} + +func (m *UtxoEntryPb) GetUtxo() []*UtxoPb { + if m != nil { + return m.Utxo + } + return nil +} + +type UtxoMapPb struct { + UtxoEntry []*UtxoEntryPb `protobuf:"bytes,1,rep,name=utxoEntry" json:"utxoEntry,omitempty"` +} + +func (m *UtxoMapPb) Reset() { *m = UtxoMapPb{} } +func (m *UtxoMapPb) String() string { return proto.CompactTextString(m) } +func (*UtxoMapPb) ProtoMessage() {} +func (*UtxoMapPb) Descriptor() ([]byte, []int) { return fileDescriptor2, []int{2} } + +func (m *UtxoMapPb) GetUtxoEntry() []*UtxoEntryPb { + if m != nil { + return m.UtxoEntry + } + return nil +} + +func init() { + proto.RegisterType((*UtxoPb)(nil), "iproto.utxoPb") + proto.RegisterType((*UtxoEntryPb)(nil), "iproto.utxoEntryPb") + proto.RegisterType((*UtxoMapPb)(nil), "iproto.utxoMapPb") +} + +func init() { proto.RegisterFile("utxo.proto", fileDescriptor2) } + +var fileDescriptor2 = []byte{ + // 207 bytes of a gzipped FileDescriptorProto + 0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0x5c, 0x8e, 0x31, 0x6b, 0x87, 0x30, + 0x10, 0xc5, 0xc9, 0xdf, 0x28, 0xf4, 0xb4, 0x0e, 0xd7, 0x0e, 0x99, 0x4a, 0xc8, 0x50, 0x32, 0x09, + 0x6d, 0xf7, 0x6e, 0x8e, 0x05, 0x89, 0x9f, 0xc0, 0x58, 0xc1, 0x50, 0x31, 0x62, 0x63, 0xb1, 0xa5, + 0x1f, 0xbe, 0x24, 0x29, 0x28, 0xff, 0xe9, 0xf2, 0x7e, 0x77, 0x2f, 0xef, 0x01, 0x6c, 0x6e, 0xb7, + 0xd5, 0xb2, 0x5a, 0x67, 0x31, 0x33, 0x61, 0x8a, 0x5f, 0xc8, 0x3c, 0x6d, 0x34, 0xde, 0x43, 0xfa, + 0xd5, 0x4d, 0xdb, 0xc0, 0x08, 0x27, 0x92, 0xaa, 0x28, 0x3c, 0x35, 0xf3, 0xfb, 0xb0, 0xb3, 0x0b, + 0x27, 0x32, 0x55, 0x51, 0xe0, 0x23, 0x94, 0x93, 0xed, 0x3f, 0xda, 0x7e, 0x35, 0x8b, 0x6b, 0xcd, + 0xcf, 0xc0, 0x12, 0x4e, 0xe4, 0xad, 0xba, 0xa2, 0xf8, 0x00, 0x70, 0x10, 0x46, 0x39, 0x91, 0x85, + 0x3a, 0x11, 0x51, 0x43, 0xee, 0xd3, 0xeb, 0xd9, 0xad, 0xdf, 0x8d, 0x46, 0x04, 0x3a, 0x76, 0x9f, + 0x63, 0x68, 0x50, 0xa8, 0xf0, 0x46, 0x01, 0xd4, 0x9f, 0xb0, 0x0b, 0x4f, 0x64, 0xfe, 0x5c, 0x56, + 0xb1, 0x77, 0x15, 0x4b, 0xab, 0xb0, 0x13, 0xaf, 0x70, 0xe3, 0xe7, 0x5b, 0xb7, 0x34, 0x1a, 0x9f, + 0xa2, 0x08, 0x7f, 0x32, 0x12, 0x5c, 0x77, 0x67, 0xd7, 0x7f, 0x98, 0x3a, 0xae, 0x74, 0x16, 0xb6, + 0x2f, 0x7f, 0x01, 0x00, 0x00, 0xff, 0xff, 0x40, 0x3f, 0x78, 0x33, 0x21, 0x01, 0x00, 0x00, +} diff --git a/proto/utxo.proto b/proto/utxo.proto new file mode 100644 index 0000000000..d05def4f26 --- /dev/null +++ b/proto/utxo.proto @@ -0,0 +1,26 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +// To compile the proto, run: +// protoc --go_out=plugins=grpc:. *.proto +syntax = "proto3"; +package iproto; + +message utxoPb { + uint64 value = 1; + int32 index = 2; + uint32 lockScriptSize = 3; + bytes lockScript = 4; +} + +message utxoEntryPb { + bytes hash = 1; + repeated utxoPb utxo = 2; +} + +message utxoMapPb { + repeated utxoEntryPb utxoEntry = 1; +} \ No newline at end of file diff --git a/rpcservice/rpcservice.go b/rpcservice/rpcservice.go new file mode 100644 index 0000000000..8cdfbd8d0a --- /dev/null +++ b/rpcservice/rpcservice.go @@ -0,0 +1,112 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rpcservice + +import ( + "net" + + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + "github.com/pkg/errors" + "golang.org/x/net/context" + "google.golang.org/grpc" + "google.golang.org/grpc/reflection" + + "github.com/iotexproject/iotex-core/blockchain" + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/iotxaddress" + pb "github.com/iotexproject/iotex-core/proto" +) + +// Chainserver is used to implement Chain Service +type Chainserver struct { + blockchain blockchain.IBlockchain + config config.RPC + dispatcher cm.Dispatcher + grpcserver *grpc.Server + broadcastcb func(proto.Message) error +} + +// NewChainServer creates an instance of chainserver +func NewChainServer(c config.RPC, b blockchain.IBlockchain, dp cm.Dispatcher, cb func(proto.Message) error) *Chainserver { + if cb == nil { + glog.Fatal("cannot new chain server with nil callback") + } + return &Chainserver{blockchain: b, config: c, dispatcher: dp, broadcastcb: cb} +} + +// CreateRawTx creates a unsigned raw transaction +func (s *Chainserver) CreateRawTx(ctx context.Context, in *pb.CreateRawTxRequest) (*pb.CreateRawTxReply, error) { + if len(in.From) == 0 || len(in.To) == 0 || in.Value == 0 { + return nil, errors.New("invalid CreateRawTxRequest") + } + + bal := s.blockchain.BalanceOf(in.From) + if bal < in.Value { + return nil, errors.New("not enough balance from address: " + in.From) + } + + p := []*blockchain.Payee{{in.To, in.Value}} + tx := s.blockchain.CreateRawTransaction(iotxaddress.Address{Address: in.From}, in.Value, p) + stx, err := proto.Marshal(tx.ConvertToTxPb()) + if err != nil { + return nil, err + } + return &pb.CreateRawTxReply{SerializedTx: stx}, nil +} + +// SendTx sends out a signed raw transaction +func (s *Chainserver) SendTx(ctx context.Context, in *pb.SendTxRequest) (*pb.SendTxReply, error) { + if len(in.SerializedTx) == 0 { + return nil, errors.New("invalid SendTxRequest") + } + + tx := &pb.TxPb{} + if err := proto.Unmarshal(in.SerializedTx, tx); err != nil { + return nil, err + } + // broadcast to the network + if err := s.broadcastcb(tx); err != nil { + return nil, err + } + // send to txpool via dispatcher + s.dispatcher.HandleBroadcast(tx, nil) + return &pb.SendTxReply{}, nil +} + +// Start starts the chain server +func (s *Chainserver) Start() error { + if s.config == (config.RPC{}) { + glog.Warning("Chain service is not configured") + return nil + } + + lis, err := net.Listen("tcp", s.config.Port) + if err != nil { + glog.Fatalf("Chain server failed to listen: %v", err) + return err + } + glog.Infof("Chain server is listening on %v", lis.Addr().String()) + + s.grpcserver = grpc.NewServer() + pb.RegisterChainServiceServer(s.grpcserver, s) + reflection.Register(s.grpcserver) + + go func() { + if err := s.grpcserver.Serve(lis); err != nil { + glog.Fatalf("Node failed to serve: %v", err) + } + }() + return nil +} + +// Stop stops the chain server +func (s *Chainserver) Stop() error { + s.grpcserver.Stop() + return nil +} diff --git a/rpcservice/rpcservice_test.go b/rpcservice/rpcservice_test.go new file mode 100644 index 0000000000..a36fff7658 --- /dev/null +++ b/rpcservice/rpcservice_test.go @@ -0,0 +1,150 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package rpcservice + +import ( + "encoding/hex" + "testing" + "time" + + "github.com/golang/mock/gomock" + "github.com/golang/protobuf/proto" + "github.com/stretchr/testify/assert" + "golang.org/x/net/context" + "google.golang.org/grpc" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/config" + pb "github.com/iotexproject/iotex-core/proto" + "github.com/iotexproject/iotex-core/test/mock/mock_blockchain" + "github.com/iotexproject/iotex-core/test/mock/mock_dispatcher" +) + +func decodeHash(in string) []byte { + hash, _ := hex.DecodeString(in) + return hash +} + +func testingTx() *blockchain.Tx { + txIn1_0 := &pb.TxInputPb{ + TxHash: decodeHash("9de6306b08158c423330f7a27243a1a5cbe39bfd764f07818437882d21241567"), + OutIndex: 0, + UnlockScriptSize: 98, + UnlockScript: decodeHash("40f9ea2b1357dde55519246a6ad82c466b9f2b988ff81a7c2fb114c932d44f322ba2edd178c2326739638b536e5f803977c24332b8f5b8ebc5f6683ff2bcaad90720b9b8d7316705dc4ff62bb323e610f3f5072abedc9834e999d6537f6681284ea2"), + } + txOut1_0 := blockchain.NewTxOutput(10, 0) + txOut1_0.LockScriptSize = 25 + txOut1_0.LockScript = decodeHash("65b014a97ce8e76ade9b3181c63432a62330a5ca83ab9ba1b1") + txOut1_1 := blockchain.NewTxOutput(1, 1) + txOut1_1.LockScriptSize = 25 + txOut1_1.LockScript = decodeHash("65b014af33097c8fd571c6c1efc52b0a802514ea0fbb03a1b1") + txOut1_2 := blockchain.NewTxOutput(1, 2) + txOut1_2.LockScriptSize = 25 + txOut1_2.LockScript = decodeHash("65b0140fb02223c1a78c3f1fb81a1572e8b07adb700bffa1b1") + txOut1_3 := blockchain.NewTxOutput(1, 3) + txOut1_3.LockScriptSize = 25 + txOut1_3.LockScript = decodeHash("65b01443251ba4fd765a2cfa65256aabd64f98c5c00e40a1b1") + txOut1_4 := blockchain.NewTxOutput(1, 4) + txOut1_4.LockScriptSize = 25 + txOut1_4.LockScript = decodeHash("65b01430f1db72a44136e8634121b6730c2b8ef094f1c9a1b1") + txOut1_5 := blockchain.NewTxOutput(5, 5) + txOut1_5.LockScriptSize = 25 + txOut1_5.LockScript = decodeHash("65b014d94ee6c7205e85c3d97c557f08faf8ac41102806a1b1") + txOut1_6 := blockchain.NewTxOutput(9999999981, 6) + txOut1_6.LockScriptSize = 25 + txOut1_6.LockScript = decodeHash("65b014d4f743a24d5386f8d1c2a648da7015f08800cd11a1b1") + return &blockchain.Tx{ + Version: 1, + NumTxIn: 1, + TxIn: []*blockchain.TxInput{txIn1_0}, + NumTxOut: 7, + TxOut: []*blockchain.TxOutput{txOut1_0, txOut1_1, txOut1_2, txOut1_3, txOut1_4, txOut1_5, txOut1_6}, + LockTime: 0, + } +} + +func TestCreateRawTx(t *testing.T) { + cfg := config.Config{ + RPC: config.RPC{ + Port: ":42124", + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mbc := mock_blockchain.NewMockIBlockchain(ctrl) + mdp := mock_dispatcher.NewMockDispatcher(ctrl) + + cbinvoked := false + bcb := func(msg proto.Message) error { + cbinvoked = true + return nil + } + + s := NewChainServer(cfg.RPC, mbc, mdp, bcb) + s.Start() + defer s.Stop() + + // Set up a connection to the server. + conn, err := grpc.Dial("127.0.0.1:42124", grpc.WithInsecure()) + assert.Nil(t, err) + defer conn.Close() + + // Contact the server and print out its response. + c := pb.NewChainServiceClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + mbc.EXPECT().BalanceOf(gomock.Any()).Return(uint64(101)).Times(1) + mbc.EXPECT().CreateRawTransaction(gomock.Any(), gomock.Any(), gomock.Any()).Return(testingTx()).Times(1) + mdp.EXPECT().HandleBroadcast(gomock.Any(), gomock.Any()).Times(0) + r, err := c.CreateRawTx(ctx, &pb.CreateRawTxRequest{From: "Alice", To: "Bob", Value: 100}) + assert.Nil(t, err) + assert.Equal(t, 380, len(r.SerializedTx)) + assert.False(t, cbinvoked) +} + +func TestSendTx(t *testing.T) { + cfg := config.Config{ + RPC: config.RPC{ + Port: ":42124", + }, + } + + ctrl := gomock.NewController(t) + defer ctrl.Finish() + mbc := mock_blockchain.NewMockIBlockchain(ctrl) + mdp := mock_dispatcher.NewMockDispatcher(ctrl) + + cbinvoked := false + bcb := func(msg proto.Message) error { + cbinvoked = true + return nil + } + + s := NewChainServer(cfg.RPC, mbc, mdp, bcb) + s.Start() + defer s.Stop() + + // Set up a connection to the server. + conn, err := grpc.Dial("127.0.0.1:42124", grpc.WithInsecure()) + assert.Nil(t, err) + defer conn.Close() + + // Contact the server and print out its response. + c := pb.NewChainServiceClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + stx, err := proto.Marshal(testingTx().ConvertToTxPb()) + assert.Nil(t, err) + + mdp.EXPECT().HandleBroadcast(gomock.Any(), gomock.Any()).Times(1) + _, err = c.SendTx(ctx, &pb.SendTxRequest{SerializedTx: stx}) + assert.Nil(t, err) + assert.True(t, cbinvoked) +} diff --git a/server/itx/itxserver.go b/server/itx/itxserver.go new file mode 100644 index 0000000000..b82635ecb8 --- /dev/null +++ b/server/itx/itxserver.go @@ -0,0 +1,78 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package itx + +import ( + "os" + + "github.com/golang/glog" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/blocksync" + cm "github.com/iotexproject/iotex-core/common" + "github.com/iotexproject/iotex-core/common/service" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/delegate" + "github.com/iotexproject/iotex-core/dispatcher" + "github.com/iotexproject/iotex-core/network" + ta "github.com/iotexproject/iotex-core/test/testaddress" + "github.com/iotexproject/iotex-core/txpool" +) + +// Server is the iotex server instance containing all components. +type Server struct { + service.Service + bc blockchain.IBlockchain + o *network.Overlay + dp cm.Dispatcher + cfg config.Config +} + +// NewServer creates a new server +func NewServer(cfg config.Config) Server { + bc := blockchain.CreateBlockchain(ta.Addrinfo["miner"].Address, &cfg) + tp := txpool.New(bc) + + // server use first BootstrapNodes addr + o := network.NewOverlay(&cfg.Network) + pool := delegate.NewConfigBasedPool(&cfg.Delegate) + bs := blocksync.NewBlockSyncer(&cfg, bc, tp, o, pool) + + // create dispatcher instance + dp := dispatcher.NewDispatcher(&cfg, bc, tp, bs, pool) + o.AttachDispatcher(dp) + + return Server{ + bc: bc, + o: o, + dp: dp, + cfg: cfg, + } +} + +// Init initialize the server +func (s *Server) Init() { + s.dp.Start() + if err := s.o.Init(); err != nil { + glog.Fatal(err) + } +} + +// Start starts the server +func (s *Server) Start() { + if err := s.o.Start(); err != nil { + glog.Fatal(err) + } +} + +// Stop stops the server +func (s *Server) Stop() { + s.o.Stop() + s.dp.Stop() + s.bc.Close() + os.Remove(s.cfg.Chain.ChainDBPath) +} diff --git a/server/main.go b/server/main.go new file mode 100644 index 0000000000..f5dd19b783 --- /dev/null +++ b/server/main.go @@ -0,0 +1,43 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +// Usage: +// make build +// ./bin/server -stderrthreshold=WARNING -log_dir=./log -config=./config.yaml +// + +package main + +import ( + "flag" + "fmt" + "os" + + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/server/run" +) + +var configFile = flag.String("config", "./config.yaml", "specify configuration file path") + +func init() { + flag.Usage = func() { + fmt.Fprintf(os.Stderr, + "usage: server -stderrthreshold=[INFO|WARN|FATAL] -log_dir=[string] -config=[string]\n") + flag.PrintDefaults() + os.Exit(2) + } + flag.Parse() +} + +func main() { + cfg, err := config.LoadConfigWithPath(*configFile) + + if err != nil { + os.Exit(1) + } + + run.Run(cfg, nil) +} diff --git a/server/run/run.go b/server/run/run.go new file mode 100644 index 0000000000..5736c2ab11 --- /dev/null +++ b/server/run/run.go @@ -0,0 +1,74 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package run + +import ( + "os" + + "github.com/golang/glog" + "github.com/golang/protobuf/proto" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/blocksync" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/delegate" + "github.com/iotexproject/iotex-core/dispatcher" + "github.com/iotexproject/iotex-core/network" + "github.com/iotexproject/iotex-core/rpcservice" + ta "github.com/iotexproject/iotex-core/test/testaddress" + "github.com/iotexproject/iotex-core/txpool" +) + +// Run starts the iotex node and block on the stop chan. +func Run(cfg *config.Config, stop chan struct{}) { + // create Blockchain and TxPool instance + defer os.Remove(cfg.Chain.ChainDBPath) + bc := blockchain.CreateBlockchain(ta.Addrinfo["miner"].Address, cfg) + tp := txpool.New(bc) + defer bc.Close() + + overlay := network.NewOverlay(&cfg.Network) + pool := delegate.NewConfigBasedPool(&cfg.Delegate) + bs := blocksync.NewBlockSyncer(cfg, bc, tp, overlay, pool) + + // create dispatcher instance + dp := dispatcher.NewDispatcher(cfg, bc, tp, bs, pool) + overlay.AttachDispatcher(dp) + dp.Start() + defer dp.Stop() + + if err := overlay.Init(); err != nil { + glog.Fatal(err) + } + + if err := overlay.Start(); err != nil { + glog.Fatal(err) + } + defer overlay.Stop() + + if err := pool.Init(); err != nil { + glog.Fatal(err) + } + + if err := pool.Start(); err != nil { + glog.Fatal(err) + } + defer pool.Stop() + + if cfg.RPC != (config.RPC{}) { + bcb := func(msg proto.Message) error { + return bs.P2P().Broadcast(msg) + } + cs := rpcservice.NewChainServer(cfg.RPC, bc, dp, bcb) + cs.Start() + defer cs.Stop() + } + + select { + case <-stop: + } +} diff --git a/test/assets/ssl/127.0.0.1.crt b/test/assets/ssl/127.0.0.1.crt new file mode 100644 index 0000000000..4dcde6990f --- /dev/null +++ b/test/assets/ssl/127.0.0.1.crt @@ -0,0 +1,25 @@ +-----BEGIN CERTIFICATE----- +MIIENjCCAh6gAwIBAgIRANOHFc+NWoTZO9foXRKu7WYwDQYJKoZIhvcNAQELBQAw +EzERMA8GA1UEAxMIaW90ZXguaW8wHhcNMTgwMzIwMDc1MDQ2WhcNMTkwOTIwMDc0 +NzUyWjAUMRIwEAYDVQQDEwkxMjcuMC4wLjEwggEiMA0GCSqGSIb3DQEBAQUAA4IB +DwAwggEKAoIBAQCdMC88ZFiWbRGWnkUrLIT6h6MUWDIL8nCrDMZ/nbW8JYx/Rpms +k8lQtDcov3Z3I3PzlyqW7XnxDZU5dakIeRzYc1dWPPchO7eW4p6LKvousPeK2lX3 +CHRj2BrL+gTWEiLnZS063pFIgVXbbSZBq1ts3rUjSP54qKd2OWCc5Jal8lHM1R2z +zS0ySY1mSJ242Xl3IS8WbWlLfwk2tsm3PHCb0G4xxiiUMwqZ4kAhAraN/OZzcyJf +iG8IyD68mvlZDeC0QGwSTiyHXQ45EUMK5T85JCtWoqGHV09mUv5heHzj2zisT9vJ +GLv2F8NVMsR2HGKTgwY9jsG803GyLtAHyFTXAgMBAAGjgYMwgYAwDgYDVR0PAQH/ +BAQDAgO4MB0GA1UdJQQWMBQGCCsGAQUFBwMBBggrBgEFBQcDAjAdBgNVHQ4EFgQU +pXgPaYIW0fGcQmazTmtMgfw3MJIwHwYDVR0jBBgwFoAUTL4NEHNAOC37gMtHG9+H +skOKFvYwDwYDVR0RBAgwBocEfwAAATANBgkqhkiG9w0BAQsFAAOCAgEAl9Rre4s+ +EBJbI24Lbe6fuavNaBLmveFkpYAGjrR52bcMWG3P6ClInomFYaHWLQwsdplgFg6s +cikzThtsRhzSZpGC2IRixYtzNnqcsXGr+uAdlrix8IkPBZxtJNzaa9nP3J7HLnX1 +kFkWiuv9pU+g1Lxu5UhjageXeefUHdTOX766teeC+rBrGZMZgSY0kBWvuMetHRdF +E/9AxYW771Qir8khQD85J/1zIjO1Bub6/LgcvXo/mYL7BO+88vFO9NqOUkqBCf5S +KjI2FB0HrZLODIpyIG8vDtrtkKaIz5cOdJo3CrmYMOo1rx2hsb6x1go1nMzmyUXB +QfHeyErFE5aYGPY55sGI1l7CYpwPjyUbf9A4L+Lb09MiW/H2WptqJh3kf793hwgM +W43eJiQrnJONFPhDdzNYxf/g4PHzJdPPAyASZk3vJ8hUXsETJKPa4JKt3YbeP1qX +nZCRbgZnNBRtiz78NgtYx1BVBe9AEufc35o2oXmTNBIhfNM2VERniaQja7Zlm3+I +5j9TrS7E3sCrYdeJWl5a/VKSIKVgx49psJzRXScj1KI78g57NNQnLy43tSVEoCmu +TNO7hnfZ/MyUNdwxGQJNBjxMuUnAg9aZ/wG9AkkFmhnoU35Oj6P9+091AVl6wdzM +Roemm0ZxQfZ7VOGKyTy0nb7WhvHGjkeKvBs= +-----END CERTIFICATE----- diff --git a/test/assets/ssl/127.0.0.1.csr b/test/assets/ssl/127.0.0.1.csr new file mode 100644 index 0000000000..a30e449a7c --- /dev/null +++ b/test/assets/ssl/127.0.0.1.csr @@ -0,0 +1,16 @@ +-----BEGIN CERTIFICATE REQUEST----- +MIICezCCAWMCAQAwFDESMBAGA1UEAxMJMTI3LjAuMC4xMIIBIjANBgkqhkiG9w0B +AQEFAAOCAQ8AMIIBCgKCAQEAnTAvPGRYlm0Rlp5FKyyE+oejFFgyC/JwqwzGf521 +vCWMf0aZrJPJULQ3KL92dyNz85cqlu158Q2VOXWpCHkc2HNXVjz3ITu3luKeiyr6 +LrD3itpV9wh0Y9gay/oE1hIi52UtOt6RSIFV220mQatbbN61I0j+eKindjlgnOSW +pfJRzNUds80tMkmNZkiduNl5dyEvFm1pS38JNrbJtzxwm9BuMcYolDMKmeJAIQK2 +jfzmc3MiX4hvCMg+vJr5WQ3gtEBsEk4sh10OORFDCuU/OSQrVqKhh1dPZlL+YXh8 +49s4rE/byRi79hfDVTLEdhxik4MGPY7BvNNxsi7QB8hU1wIDAQABoCIwIAYJKoZI +hvcNAQkOMRMwETAPBgNVHREECDAGhwR/AAABMA0GCSqGSIb3DQEBCwUAA4IBAQBk +ENQGE0lE8fWpPaTXVhab3MYHM6kmvbpteDgoTM5VKaazQAUHaoG365NMKLTUlowE +i0Ri3JFPFctLpzRdQZxSXC+J0fmg16lXVYXPVSFvJGJyzdpgCt7ptRhdBJmeL6A8 +dpNODdP1WSyZLIMIVwzAeux3DpXqSR6nH/BPyjb9RSp9I8vPuKhCPJR9MhFDha4d +lR0tiuazOK+/zZBYAITBCbJZBgBO8j0UYKFVRidajrHnw4AQQB/sLgdgkz6oL0X3 +wgkZigFVhqH59AZAtFju/8NPHMV0TUeNUMcdKzAYuxHCKcAyMSUPCMU1m7uoD/0J +vts9h3+iB1XRodZK5SS8 +-----END CERTIFICATE REQUEST----- diff --git a/test/assets/ssl/127.0.0.1.key b/test/assets/ssl/127.0.0.1.key new file mode 100644 index 0000000000..fd7d422641 --- /dev/null +++ b/test/assets/ssl/127.0.0.1.key @@ -0,0 +1,27 @@ +-----BEGIN RSA PRIVATE KEY----- +MIIEowIBAAKCAQEAnTAvPGRYlm0Rlp5FKyyE+oejFFgyC/JwqwzGf521vCWMf0aZ +rJPJULQ3KL92dyNz85cqlu158Q2VOXWpCHkc2HNXVjz3ITu3luKeiyr6LrD3itpV +9wh0Y9gay/oE1hIi52UtOt6RSIFV220mQatbbN61I0j+eKindjlgnOSWpfJRzNUd +s80tMkmNZkiduNl5dyEvFm1pS38JNrbJtzxwm9BuMcYolDMKmeJAIQK2jfzmc3Mi +X4hvCMg+vJr5WQ3gtEBsEk4sh10OORFDCuU/OSQrVqKhh1dPZlL+YXh849s4rE/b +yRi79hfDVTLEdhxik4MGPY7BvNNxsi7QB8hU1wIDAQABAoIBAHsAvbXwgDIWX3ER +oObMC7NQeJju+xrGOMxhTuOGMjRs3airQo39h8eTfqZwTO+l2pJ/dGBurMGo82Dn +ZcKvo5btiyZ8xpZG+L8lzxLwKp5m9Q6/FA4fm5SWA0Ii/63Uu2N/lQlAKUgnjVwL +6afxw7VKAUz/TcJ1l7W36JZdelWYM926FZpuTAoNiFdpEld0BUmKNa69nVph5Av8 +6yX2XbpSRxyUhQmND/O2R3jMtuKpi0HoGctk/Vz5Kelo+FeThINrRfcEPxGuakve +NPytwffImcj7xq/heg5jCGqRHm1DuBJ67fJND8nzpvetg8lArFIm5U78P7HTzJX6 +UO7cd4ECgYEAx2QJhRrHBQ+S2PKg9l3cXTzY8aH3Ejy80OvzvgtkfnuighdPW2EB +9W2bPEVzXm4V3iHaMuwQ82CYvfAbCcfEI+ok8JvWXLnZQfV8H6Fl7844+1BcBBdf +Qj+zW791l6+w1vBzWtdi8p/IIXh5e/YgraNI1zuT8XnOHsCzpVIuEmcCgYEAydDQ +nHgEyWOoxpjoHoRtzTq+czO2RlM66OSVrzBHWDb7YMcVLHQOCF6+UsryPlmbDCzX +RkqFHevDFikxecxKvEFz3s7ui+eQjp0c+KC7o1UPzLgyNcoFOFc36VQw+Ynar5M7 +pT0NLDMr6AKt2Dr0RWhBGFDIXOaTMvyg2bdkhBECgYBDMqS+tiqSuw/ri8HRpgpp +ZeNbdBF3lP6xjoPD7cT9l59KJp75FfV6CGmD44ghMAqwpUoLVWHVFB4GDVCfeDLT +Q8sVR//zmjLtNtWRN2zKZ/7uO8P3IQVap2tEJX91EqIPnlPSLrrLOCxTk22l6GpU ++q3G0+5lOH7OAHXK5qpeSwKBgQDCeKWNIVn7dj08ojuKrLHiAalj+YC7Y5hws1RJ +7IvTRagWKcENhQfAqQtdXJLx4ga2RL9szIJZwsXKpQc4BtQ9dBTjryPPTOTPiDZS +nxAw49bFvvkjO/irwd/ux2IqE9zXM1lWcO4wmV7Fw08h/i1EdKHFfJKWctmFWErn +gQErAQKBgG3gGmpfXkJ1ci45Nm5LcjnovBLnuyuagmn+mclOapIj+FlyV43N55O/ +Zqj1Zx3+4+R2fE3/RpGzu2A0Ba34vRC3ZPXiLt/8cgCt0cyZCTmJuMfLMsBWhMMd +oxjhsFZTITcBRsXRWjk1JIbbnHIjCkFVBDuVI9xVXYV/Kkb62msH +-----END RSA PRIVATE KEY----- diff --git a/test/assets/ssl/README.md b/test/assets/ssl/README.md new file mode 100644 index 0000000000..d1ce3dae02 --- /dev/null +++ b/test/assets/ssl/README.md @@ -0,0 +1 @@ +Follow the intstructions on https://github.com/square/certstrap to generate certificates. \ No newline at end of file diff --git a/test/assets/ssl/iotex.io.crl b/test/assets/ssl/iotex.io.crl new file mode 100644 index 0000000000..540f2fdb04 --- /dev/null +++ b/test/assets/ssl/iotex.io.crl @@ -0,0 +1,16 @@ +-----BEGIN X509 CRL----- +MIICgjBsAgEBMA0GCSqGSIb3DQEBCwUAMBMxETAPBgNVBAMTCGlvdGV4LmlvFw0x +ODAzMjAwNzQ3NThaFw0xOTA5MjAwNzQ3NTNaMACgIzAhMB8GA1UdIwQYMBaAFEy+ +DRBzQDgt+4DLRxvfh7JDihb2MA0GCSqGSIb3DQEBCwUAA4ICAQCJpscfJRmcW5/N +26+TnPmxttsTiis0yd+SPC/RyGK+WlFa8ppDq6/di+ed8ZNd+YZfY/FWnVElnv5m +RKXmNcrlirQd2eJ56TDOhoo3bf1mqIzyCzEh7qO/0UuuUL+5bmYHujfXqu90rc54 +CFm/lIaGdqkv2aPRe+N98VUdAEo1aYgTo0Z8J9D1yEpw7GO/zda1sV4Wrm/7rUNx +4a4VuFfeJGPN13NCkaPVp4DvKY7/fvNTCLW4aGnBg7wDcKU2SLe5HS6QJsY1X3EG +fhgSkd4+s7wGQFi/by09n+0yPiVFZDfaKQX7yqKNiZB9CjoLoUGuCQEU/SafdfLo +dxBZooXhXCD8AZPW9Gb/q4MUxZEIUd6Rw8ySALVh1X0eFBkmv5t5zIrXqqu+PIff +Jdzeq03Geos2uWI+m9VH6WgXWtW7mhnTeIB4NDhw6bxKyQFqunXLt79+IyrE5lMR +7/CkgfoMW8o1LJMCHDCgY/s2lM6AbteQ5ceaWMGzrTB0QlyaSNARi56Ue5vv9xGc +S+im+0e/4w5hWOMWn+/qeMR551Bc6WSp/mCB6Eft2qc8JI7wSQ6LsOJqEeMHa/MV +v/Nv+jjDcBaktAfY3uNjTNQnKMtYdWnReDdZuk8uJXq1HznSSOMQh5FY4XgHdFKB +TPbkfHoj66oTSCueqaZw1JZmIoVLIA== +-----END X509 CRL----- diff --git a/test/assets/ssl/iotex.io.crt b/test/assets/ssl/iotex.io.crt new file mode 100644 index 0000000000..39f3d037c7 --- /dev/null +++ b/test/assets/ssl/iotex.io.crt @@ -0,0 +1,29 @@ +-----BEGIN CERTIFICATE----- +MIIE5jCCAs6gAwIBAgIBATANBgkqhkiG9w0BAQsFADATMREwDwYDVQQDEwhpb3Rl +eC5pbzAeFw0xODAzMjAwNzQ3NTNaFw0xOTA5MjAwNzQ3NTNaMBMxETAPBgNVBAMT +CGlvdGV4LmlvMIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAr9nnqP0j +dePKbabA35SlPrMrGWRAkjb4dhslgtMh2WnioHAgaKDcQnZT6xS0s5fOseNeF2pa +yf46v/5CdKgdND15JW0BxVA9+MuHDDaHRw2KyZY1+E2dkwzWYY/UXV8SVGxy8MYE +itjvulqhDaGfyLo/SrsrXJcu+8Ex2ohTN3sCe8Pb8PiN4dD1WxIvoBJuRYOu6pJ8 +0TXtKG766JIZWgU8UVRjKccqjFTz7AOZir8QERhSR1XtSi4UEs8BSHI4YYWDUP+N +DMTj73kEloBmD2BwJXjEaBAFagnP9wQ1qR+TwGGRVLYX2WpCBwEQGsjwDRr9AH+i +Yo+DpJaB7uqZGiw4RwaXxs+GPgYaLgJc/D3tFemZ69xK7kvH67WuXUQFAQTQAjYy +8HxIHjKcPdtt5N564f7Ni889J571xZauGsLC0z323gOGY35gjPmU+dmAUKTaBCgd +7AkvKYRTvALcCSzJnl53w9TeDnxhy0o/0tf7jNstMX/YFPe/IvKER0eUjyWbQ2Tp +kGtBeL2B4Vs4KtqmIIK2o/KFnIC/33egx7v6aDTVH4JgOoaQAcloZu6E/uosGUfS +zffdtgRJ617POD2zIwxPzkc2giG9mF1OpZZfpzoIqfCkLCjoo3kpYLBjc+7QHn/v +6kdlEmZ+2GpCxAiUHoVZ67ovMbHj2/Yqh+kCAwEAAaNFMEMwDgYDVR0PAQH/BAQD +AgEGMBIGA1UdEwEB/wQIMAYBAf8CAQAwHQYDVR0OBBYEFEy+DRBzQDgt+4DLRxvf +h7JDihb2MA0GCSqGSIb3DQEBCwUAA4ICAQBu+m/UtzTXnQ0BAirfhOeLqoRdKyOb +CfaxHj37/po3xojG8/LKlhzH+S5aBqTp7Drz6WmupL9/vs8pm9I3DVnkrUtdVEnA +A4LktmYzwB7x0Zmw8dAyNSns5vfJxPjzix5tE9X6GAQQy8fCyjxYu3qULiC9l8iw ++YdDgv/FqXaxTgL8dHwYkzzcsGyX2yRqNT2LhLi45uc58FD2Rcegmkt9KRobnwQK +b4agEQuPSqtbpaZDX5hc4uvWgAHiE7ZUERUR/SnfS+dqoFom4QDYr0jnqHg1kQj/ +Hzo2GHeiKD/duGd8j/adG4usZNgao2oPZ5pDWmJvv/qJYMlJOwasTHZoUhpcwl2b +cfUoIzDPn39ddLtpn8GAv0n4fuuXcerPB1E5hVykNKTD0gXQTYkYoyy1FcfeMmsn +TwX/q1cuukSYRXjQHOMIPPbYJMWI0/Zh909Micg382YRASmLcKITb5T7B0a6WSIG +Nmm1EdocNZj8f44PS6sRNjsqYa3cEE0/+BSMwPKqe+fPGSvs2s8MFWhzCkGN+TqA +m+DTIWgn4Dc4tT+VsaMyjAGS5aOiKx13RvTGuB5IbGt3SoSAYdVimanC0nQMR78t +0xaBgfEV8auxyOKQwZZc1wQzY5wMA/uEdWNkkpPrH25xbFRLDJiAG/fXcj9HKnxj +aF9vfrQx0FdPBw== +-----END CERTIFICATE----- diff --git a/test/assets/ssl/iotex.io.key b/test/assets/ssl/iotex.io.key new file mode 100644 index 0000000000..d5d012ec4b --- /dev/null +++ b/test/assets/ssl/iotex.io.key @@ -0,0 +1,51 @@ +-----BEGIN RSA PRIVATE KEY----- +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 +-----END RSA PRIVATE KEY----- diff --git a/test/mock/mock_blockchain/mock_blockchain.go b/test/mock/mock_blockchain/mock_blockchain.go new file mode 100644 index 0000000000..823ffd632e --- /dev/null +++ b/test/mock/mock_blockchain/mock_blockchain.go @@ -0,0 +1,242 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./blockchain/iblockchain.go + +// Package mock_blockchain is a generated GoMock package. +package mock_blockchain + +import ( + gomock "github.com/golang/mock/gomock" + blockchain "github.com/iotexproject/iotex-core/blockchain" + crypto "github.com/iotexproject/iotex-core/crypto" + iotxaddress "github.com/iotexproject/iotex-core/iotxaddress" + reflect "reflect" +) + +// MockIBlockchain is a mock of IBlockchain interface +type MockIBlockchain struct { + ctrl *gomock.Controller + recorder *MockIBlockchainMockRecorder +} + +// MockIBlockchainMockRecorder is the mock recorder for MockIBlockchain +type MockIBlockchainMockRecorder struct { + mock *MockIBlockchain +} + +// NewMockIBlockchain creates a new mock instance +func NewMockIBlockchain(ctrl *gomock.Controller) *MockIBlockchain { + mock := &MockIBlockchain{ctrl: ctrl} + mock.recorder = &MockIBlockchainMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockIBlockchain) EXPECT() *MockIBlockchainMockRecorder { + return m.recorder +} + +// Init mocks base method +func (m *MockIBlockchain) Init() error { + ret := m.ctrl.Call(m, "Init") + ret0, _ := ret[0].(error) + return ret0 +} + +// Init indicates an expected call of Init +func (mr *MockIBlockchainMockRecorder) Init() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Init", reflect.TypeOf((*MockIBlockchain)(nil).Init)) +} + +// Close mocks base method +func (m *MockIBlockchain) Close() error { + ret := m.ctrl.Call(m, "Close") + ret0, _ := ret[0].(error) + return ret0 +} + +// Close indicates an expected call of Close +func (mr *MockIBlockchainMockRecorder) Close() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Close", reflect.TypeOf((*MockIBlockchain)(nil).Close)) +} + +// GetHeightByHash mocks base method +func (m *MockIBlockchain) GetHeightByHash(hash crypto.Hash32B) (uint32, error) { + ret := m.ctrl.Call(m, "GetHeightByHash", hash) + ret0, _ := ret[0].(uint32) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHeightByHash indicates an expected call of GetHeightByHash +func (mr *MockIBlockchainMockRecorder) GetHeightByHash(hash interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHeightByHash", reflect.TypeOf((*MockIBlockchain)(nil).GetHeightByHash), hash) +} + +// GetHashByHeight mocks base method +func (m *MockIBlockchain) GetHashByHeight(height uint32) (crypto.Hash32B, error) { + ret := m.ctrl.Call(m, "GetHashByHeight", height) + ret0, _ := ret[0].(crypto.Hash32B) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetHashByHeight indicates an expected call of GetHashByHeight +func (mr *MockIBlockchainMockRecorder) GetHashByHeight(height interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetHashByHeight", reflect.TypeOf((*MockIBlockchain)(nil).GetHashByHeight), height) +} + +// GetBlockByHeight mocks base method +func (m *MockIBlockchain) GetBlockByHeight(height uint32) (*blockchain.Block, error) { + ret := m.ctrl.Call(m, "GetBlockByHeight", height) + ret0, _ := ret[0].(*blockchain.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlockByHeight indicates an expected call of GetBlockByHeight +func (mr *MockIBlockchainMockRecorder) GetBlockByHeight(height interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByHeight", reflect.TypeOf((*MockIBlockchain)(nil).GetBlockByHeight), height) +} + +// GetBlockByHash mocks base method +func (m *MockIBlockchain) GetBlockByHash(hash crypto.Hash32B) (*blockchain.Block, error) { + ret := m.ctrl.Call(m, "GetBlockByHash", hash) + ret0, _ := ret[0].(*blockchain.Block) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// GetBlockByHash indicates an expected call of GetBlockByHash +func (mr *MockIBlockchainMockRecorder) GetBlockByHash(hash interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "GetBlockByHash", reflect.TypeOf((*MockIBlockchain)(nil).GetBlockByHash), hash) +} + +// TipHash mocks base method +func (m *MockIBlockchain) TipHash() crypto.Hash32B { + ret := m.ctrl.Call(m, "TipHash") + ret0, _ := ret[0].(crypto.Hash32B) + return ret0 +} + +// TipHash indicates an expected call of TipHash +func (mr *MockIBlockchainMockRecorder) TipHash() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TipHash", reflect.TypeOf((*MockIBlockchain)(nil).TipHash)) +} + +// TipHeight mocks base method +func (m *MockIBlockchain) TipHeight() uint32 { + ret := m.ctrl.Call(m, "TipHeight") + ret0, _ := ret[0].(uint32) + return ret0 +} + +// TipHeight indicates an expected call of TipHeight +func (mr *MockIBlockchainMockRecorder) TipHeight() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TipHeight", reflect.TypeOf((*MockIBlockchain)(nil).TipHeight)) +} + +// Reset mocks base method +func (m *MockIBlockchain) Reset() { + m.ctrl.Call(m, "Reset") +} + +// Reset indicates an expected call of Reset +func (mr *MockIBlockchainMockRecorder) Reset() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Reset", reflect.TypeOf((*MockIBlockchain)(nil).Reset)) +} + +// ValidateBlock mocks base method +func (m *MockIBlockchain) ValidateBlock(blk *blockchain.Block) error { + ret := m.ctrl.Call(m, "ValidateBlock", blk) + ret0, _ := ret[0].(error) + return ret0 +} + +// ValidateBlock indicates an expected call of ValidateBlock +func (mr *MockIBlockchainMockRecorder) ValidateBlock(blk interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ValidateBlock", reflect.TypeOf((*MockIBlockchain)(nil).ValidateBlock), blk) +} + +// MintNewBlock mocks base method +func (m *MockIBlockchain) MintNewBlock(arg0 []*blockchain.Tx, arg1, arg2 string) *blockchain.Block { + ret := m.ctrl.Call(m, "MintNewBlock", arg0, arg1, arg2) + ret0, _ := ret[0].(*blockchain.Block) + return ret0 +} + +// MintNewBlock indicates an expected call of MintNewBlock +func (mr *MockIBlockchainMockRecorder) MintNewBlock(arg0, arg1, arg2 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MintNewBlock", reflect.TypeOf((*MockIBlockchain)(nil).MintNewBlock), arg0, arg1, arg2) +} + +// AddBlockCommit mocks base method +func (m *MockIBlockchain) AddBlockCommit(blk *blockchain.Block) error { + ret := m.ctrl.Call(m, "AddBlockCommit", blk) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddBlockCommit indicates an expected call of AddBlockCommit +func (mr *MockIBlockchainMockRecorder) AddBlockCommit(blk interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBlockCommit", reflect.TypeOf((*MockIBlockchain)(nil).AddBlockCommit), blk) +} + +// AddBlockSync mocks base method +func (m *MockIBlockchain) AddBlockSync(blk *blockchain.Block) error { + ret := m.ctrl.Call(m, "AddBlockSync", blk) + ret0, _ := ret[0].(error) + return ret0 +} + +// AddBlockSync indicates an expected call of AddBlockSync +func (mr *MockIBlockchainMockRecorder) AddBlockSync(blk interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AddBlockSync", reflect.TypeOf((*MockIBlockchain)(nil).AddBlockSync), blk) +} + +// BalanceOf mocks base method +func (m *MockIBlockchain) BalanceOf(arg0 string) uint64 { + ret := m.ctrl.Call(m, "BalanceOf", arg0) + ret0, _ := ret[0].(uint64) + return ret0 +} + +// BalanceOf indicates an expected call of BalanceOf +func (mr *MockIBlockchainMockRecorder) BalanceOf(arg0 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "BalanceOf", reflect.TypeOf((*MockIBlockchain)(nil).BalanceOf), arg0) +} + +// UtxoPool mocks base method +func (m *MockIBlockchain) UtxoPool() map[crypto.Hash32B][]*blockchain.TxOutput { + ret := m.ctrl.Call(m, "UtxoPool") + ret0, _ := ret[0].(map[crypto.Hash32B][]*blockchain.TxOutput) + return ret0 +} + +// UtxoPool indicates an expected call of UtxoPool +func (mr *MockIBlockchainMockRecorder) UtxoPool() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "UtxoPool", reflect.TypeOf((*MockIBlockchain)(nil).UtxoPool)) +} + +// CreateTransaction mocks base method +func (m *MockIBlockchain) CreateTransaction(from iotxaddress.Address, amount uint64, to []*blockchain.Payee) *blockchain.Tx { + ret := m.ctrl.Call(m, "CreateTransaction", from, amount, to) + ret0, _ := ret[0].(*blockchain.Tx) + return ret0 +} + +// CreateTransaction indicates an expected call of CreateTransaction +func (mr *MockIBlockchainMockRecorder) CreateTransaction(from, amount, to interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateTransaction", reflect.TypeOf((*MockIBlockchain)(nil).CreateTransaction), from, amount, to) +} + +// CreateRawTransaction mocks base method +func (m *MockIBlockchain) CreateRawTransaction(from iotxaddress.Address, amount uint64, to []*blockchain.Payee) *blockchain.Tx { + ret := m.ctrl.Call(m, "CreateRawTransaction", from, amount, to) + ret0, _ := ret[0].(*blockchain.Tx) + return ret0 +} + +// CreateRawTransaction indicates an expected call of CreateRawTransaction +func (mr *MockIBlockchainMockRecorder) CreateRawTransaction(from, amount, to interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "CreateRawTransaction", reflect.TypeOf((*MockIBlockchain)(nil).CreateRawTransaction), from, amount, to) +} diff --git a/test/mock/mock_blocksync/mock_blocksync.go b/test/mock/mock_blocksync/mock_blocksync.go new file mode 100644 index 0000000000..73701ad0c9 --- /dev/null +++ b/test/mock/mock_blocksync/mock_blocksync.go @@ -0,0 +1,108 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./blocksync/blocksync.go + +// Package mock_blocksync is a generated GoMock package. +package mock_blocksync + +import ( + gomock "github.com/golang/mock/gomock" + blockchain "github.com/iotexproject/iotex-core/blockchain" + network "github.com/iotexproject/iotex-core/network" + proto "github.com/iotexproject/iotex-core/proto" + reflect "reflect" +) + +// MockBlockSync is a mock of BlockSync interface +type MockBlockSync struct { + ctrl *gomock.Controller + recorder *MockBlockSyncMockRecorder +} + +// MockBlockSyncMockRecorder is the mock recorder for MockBlockSync +type MockBlockSyncMockRecorder struct { + mock *MockBlockSync +} + +// NewMockBlockSync creates a new mock instance +func NewMockBlockSync(ctrl *gomock.Controller) *MockBlockSync { + mock := &MockBlockSync{ctrl: ctrl} + mock.recorder = &MockBlockSyncMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockBlockSync) EXPECT() *MockBlockSyncMockRecorder { + return m.recorder +} + +// Start mocks base method +func (m *MockBlockSync) Start() error { + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start +func (mr *MockBlockSyncMockRecorder) Start() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockBlockSync)(nil).Start)) +} + +// Stop mocks base method +func (m *MockBlockSync) Stop() error { + ret := m.ctrl.Call(m, "Stop") + ret0, _ := ret[0].(error) + return ret0 +} + +// Stop indicates an expected call of Stop +func (mr *MockBlockSyncMockRecorder) Stop() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockBlockSync)(nil).Stop)) +} + +// P2P mocks base method +func (m *MockBlockSync) P2P() *network.Overlay { + ret := m.ctrl.Call(m, "P2P") + ret0, _ := ret[0].(*network.Overlay) + return ret0 +} + +// P2P indicates an expected call of P2P +func (mr *MockBlockSyncMockRecorder) P2P() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "P2P", reflect.TypeOf((*MockBlockSync)(nil).P2P)) +} + +// ProcessSyncRequest mocks base method +func (m *MockBlockSync) ProcessSyncRequest(sender string, sync *proto.BlockSync) error { + ret := m.ctrl.Call(m, "ProcessSyncRequest", sender, sync) + ret0, _ := ret[0].(error) + return ret0 +} + +// ProcessSyncRequest indicates an expected call of ProcessSyncRequest +func (mr *MockBlockSyncMockRecorder) ProcessSyncRequest(sender, sync interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessSyncRequest", reflect.TypeOf((*MockBlockSync)(nil).ProcessSyncRequest), sender, sync) +} + +// ProcessBlock mocks base method +func (m *MockBlockSync) ProcessBlock(blk *blockchain.Block) error { + ret := m.ctrl.Call(m, "ProcessBlock", blk) + ret0, _ := ret[0].(error) + return ret0 +} + +// ProcessBlock indicates an expected call of ProcessBlock +func (mr *MockBlockSyncMockRecorder) ProcessBlock(blk interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessBlock", reflect.TypeOf((*MockBlockSync)(nil).ProcessBlock), blk) +} + +// ProcessBlockSync mocks base method +func (m *MockBlockSync) ProcessBlockSync(blk *blockchain.Block) error { + ret := m.ctrl.Call(m, "ProcessBlockSync", blk) + ret0, _ := ret[0].(error) + return ret0 +} + +// ProcessBlockSync indicates an expected call of ProcessBlockSync +func (mr *MockBlockSyncMockRecorder) ProcessBlockSync(blk interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessBlockSync", reflect.TypeOf((*MockBlockSync)(nil).ProcessBlockSync), blk) +} diff --git a/test/mock/mock_delegate/mock_delegate.go b/test/mock/mock_delegate/mock_delegate.go new file mode 100644 index 0000000000..04a1eaf97c --- /dev/null +++ b/test/mock/mock_delegate/mock_delegate.go @@ -0,0 +1,59 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./delegate/delegate.go + +// Package mock_delegate is a generated GoMock package. +package mock_delegate + +import ( + gomock "github.com/golang/mock/gomock" + net "net" + reflect "reflect" +) + +// MockPool is a mock of Pool interface +type MockPool struct { + ctrl *gomock.Controller + recorder *MockPoolMockRecorder +} + +// MockPoolMockRecorder is the mock recorder for MockPool +type MockPoolMockRecorder struct { + mock *MockPool +} + +// NewMockPool creates a new mock instance +func NewMockPool(ctrl *gomock.Controller) *MockPool { + mock := &MockPool{ctrl: ctrl} + mock.recorder = &MockPoolMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockPool) EXPECT() *MockPoolMockRecorder { + return m.recorder +} + +// AllDelegates mocks base method +func (m *MockPool) AllDelegates() ([]net.Addr, error) { + ret := m.ctrl.Call(m, "AllDelegates") + ret0, _ := ret[0].([]net.Addr) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// AllDelegates indicates an expected call of AllDelegates +func (mr *MockPoolMockRecorder) AllDelegates() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AllDelegates", reflect.TypeOf((*MockPool)(nil).AllDelegates)) +} + +// AnotherDelegate mocks base method +func (m *MockPool) AnotherDelegate(self string) net.Addr { + ret := m.ctrl.Call(m, "AnotherDelegate", self) + ret0, _ := ret[0].(net.Addr) + return ret0 +} + +// AnotherDelegate indicates an expected call of AnotherDelegate +func (mr *MockPoolMockRecorder) AnotherDelegate(self interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "AnotherDelegate", reflect.TypeOf((*MockPool)(nil).AnotherDelegate), self) +} diff --git a/test/mock/mock_dispatcher/mock_dispatcher.go b/test/mock/mock_dispatcher/mock_dispatcher.go new file mode 100644 index 0000000000..0a5a404e9c --- /dev/null +++ b/test/mock/mock_dispatcher/mock_dispatcher.go @@ -0,0 +1,79 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./common/idispatcher.go + +// Package mock_dispatcher is a generated GoMock package. +package mock_dispatcher + +import ( + gomock "github.com/golang/mock/gomock" + proto "github.com/golang/protobuf/proto" + net "net" + reflect "reflect" +) + +// MockDispatcher is a mock of Dispatcher interface +type MockDispatcher struct { + ctrl *gomock.Controller + recorder *MockDispatcherMockRecorder +} + +// MockDispatcherMockRecorder is the mock recorder for MockDispatcher +type MockDispatcherMockRecorder struct { + mock *MockDispatcher +} + +// NewMockDispatcher creates a new mock instance +func NewMockDispatcher(ctrl *gomock.Controller) *MockDispatcher { + mock := &MockDispatcher{ctrl: ctrl} + mock.recorder = &MockDispatcherMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockDispatcher) EXPECT() *MockDispatcherMockRecorder { + return m.recorder +} + +// Start mocks base method +func (m *MockDispatcher) Start() error { + ret := m.ctrl.Call(m, "Start") + ret0, _ := ret[0].(error) + return ret0 +} + +// Start indicates an expected call of Start +func (mr *MockDispatcherMockRecorder) Start() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Start", reflect.TypeOf((*MockDispatcher)(nil).Start)) +} + +// Stop mocks base method +func (m *MockDispatcher) Stop() error { + ret := m.ctrl.Call(m, "Stop") + ret0, _ := ret[0].(error) + return ret0 +} + +// Stop indicates an expected call of Stop +func (mr *MockDispatcherMockRecorder) Stop() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Stop", reflect.TypeOf((*MockDispatcher)(nil).Stop)) +} + +// HandleBroadcast mocks base method +func (m *MockDispatcher) HandleBroadcast(arg0 proto.Message, arg1 chan bool) { + m.ctrl.Call(m, "HandleBroadcast", arg0, arg1) +} + +// HandleBroadcast indicates an expected call of HandleBroadcast +func (mr *MockDispatcherMockRecorder) HandleBroadcast(arg0, arg1 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleBroadcast", reflect.TypeOf((*MockDispatcher)(nil).HandleBroadcast), arg0, arg1) +} + +// HandleTell mocks base method +func (m *MockDispatcher) HandleTell(arg0 net.Addr, arg1 proto.Message, arg2 chan bool) { + m.ctrl.Call(m, "HandleTell", arg0, arg1, arg2) +} + +// HandleTell indicates an expected call of HandleTell +func (mr *MockDispatcherMockRecorder) HandleTell(arg0, arg1, arg2 interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HandleTell", reflect.TypeOf((*MockDispatcher)(nil).HandleTell), arg0, arg1, arg2) +} diff --git a/test/mock/mock_rdpos/mock_rdpos.go b/test/mock/mock_rdpos/mock_rdpos.go new file mode 100644 index 0000000000..040f55c641 --- /dev/null +++ b/test/mock/mock_rdpos/mock_rdpos.go @@ -0,0 +1,71 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./consensus/scheme/rdpos/rdpos.go + +// Package mock_rdpos is a generated GoMock package. +package mock_rdpos + +import ( + gomock "github.com/golang/mock/gomock" + proto "github.com/golang/protobuf/proto" + net "net" + reflect "reflect" +) + +// MockDNet is a mock of DNet interface +type MockDNet struct { + ctrl *gomock.Controller + recorder *MockDNetMockRecorder +} + +// MockDNetMockRecorder is the mock recorder for MockDNet +type MockDNetMockRecorder struct { + mock *MockDNet +} + +// NewMockDNet creates a new mock instance +func NewMockDNet(ctrl *gomock.Controller) *MockDNet { + mock := &MockDNet{ctrl: ctrl} + mock.recorder = &MockDNetMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockDNet) EXPECT() *MockDNetMockRecorder { + return m.recorder +} + +// Tell mocks base method +func (m *MockDNet) Tell(node net.Addr, msg proto.Message) error { + ret := m.ctrl.Call(m, "Tell", node, msg) + ret0, _ := ret[0].(error) + return ret0 +} + +// Tell indicates an expected call of Tell +func (mr *MockDNetMockRecorder) Tell(node, msg interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Tell", reflect.TypeOf((*MockDNet)(nil).Tell), node, msg) +} + +// Self mocks base method +func (m *MockDNet) Self() net.Addr { + ret := m.ctrl.Call(m, "Self") + ret0, _ := ret[0].(net.Addr) + return ret0 +} + +// Self indicates an expected call of Self +func (mr *MockDNetMockRecorder) Self() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Self", reflect.TypeOf((*MockDNet)(nil).Self)) +} + +// Broadcast mocks base method +func (m *MockDNet) Broadcast(msg proto.Message) error { + ret := m.ctrl.Call(m, "Broadcast", msg) + ret0, _ := ret[0].(error) + return ret0 +} + +// Broadcast indicates an expected call of Broadcast +func (mr *MockDNetMockRecorder) Broadcast(msg interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Broadcast", reflect.TypeOf((*MockDNet)(nil).Broadcast), msg) +} diff --git a/test/mock/mock_txpool/mock_txpool.go b/test/mock/mock_txpool/mock_txpool.go new file mode 100644 index 0000000000..9b9ffd4a12 --- /dev/null +++ b/test/mock/mock_txpool/mock_txpool.go @@ -0,0 +1,203 @@ +// Code generated by MockGen. DO NOT EDIT. +// Source: ./txpool/txpool.go + +// Package mock_txpool is a generated GoMock package. +package mock_txpool + +import ( + gomock "github.com/golang/mock/gomock" + blockchain "github.com/iotexproject/iotex-core/blockchain" + crypto "github.com/iotexproject/iotex-core/crypto" + txpool "github.com/iotexproject/iotex-core/txpool" + reflect "reflect" + time "time" +) + +// MockTxPool is a mock of TxPool interface +type MockTxPool struct { + ctrl *gomock.Controller + recorder *MockTxPoolMockRecorder +} + +// MockTxPoolMockRecorder is the mock recorder for MockTxPool +type MockTxPoolMockRecorder struct { + mock *MockTxPool +} + +// NewMockTxPool creates a new mock instance +func NewMockTxPool(ctrl *gomock.Controller) *MockTxPool { + mock := &MockTxPool{ctrl: ctrl} + mock.recorder = &MockTxPoolMockRecorder{mock} + return mock +} + +// EXPECT returns an object that allows the caller to indicate expected use +func (m *MockTxPool) EXPECT() *MockTxPoolMockRecorder { + return m.recorder +} + +// RemoveOrphanTx mocks base method +func (m *MockTxPool) RemoveOrphanTx(tx *blockchain.Tx) { + m.ctrl.Call(m, "RemoveOrphanTx", tx) +} + +// RemoveOrphanTx indicates an expected call of RemoveOrphanTx +func (mr *MockTxPoolMockRecorder) RemoveOrphanTx(tx interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveOrphanTx", reflect.TypeOf((*MockTxPool)(nil).RemoveOrphanTx), tx) +} + +// RemoveOrphanTxsByTag mocks base method +func (m *MockTxPool) RemoveOrphanTxsByTag(tag txpool.Tag) uint64 { + ret := m.ctrl.Call(m, "RemoveOrphanTxsByTag", tag) + ret0, _ := ret[0].(uint64) + return ret0 +} + +// RemoveOrphanTxsByTag indicates an expected call of RemoveOrphanTxsByTag +func (mr *MockTxPoolMockRecorder) RemoveOrphanTxsByTag(tag interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveOrphanTxsByTag", reflect.TypeOf((*MockTxPool)(nil).RemoveOrphanTxsByTag), tag) +} + +// HasOrphanTx mocks base method +func (m *MockTxPool) HasOrphanTx(hash crypto.Hash32B) bool { + ret := m.ctrl.Call(m, "HasOrphanTx", hash) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasOrphanTx indicates an expected call of HasOrphanTx +func (mr *MockTxPoolMockRecorder) HasOrphanTx(hash interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasOrphanTx", reflect.TypeOf((*MockTxPool)(nil).HasOrphanTx), hash) +} + +// HasTxOrOrphanTx mocks base method +func (m *MockTxPool) HasTxOrOrphanTx(hash crypto.Hash32B) bool { + ret := m.ctrl.Call(m, "HasTxOrOrphanTx", hash) + ret0, _ := ret[0].(bool) + return ret0 +} + +// HasTxOrOrphanTx indicates an expected call of HasTxOrOrphanTx +func (mr *MockTxPoolMockRecorder) HasTxOrOrphanTx(hash interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "HasTxOrOrphanTx", reflect.TypeOf((*MockTxPool)(nil).HasTxOrOrphanTx), hash) +} + +// RemoveTx mocks base method +func (m *MockTxPool) RemoveTx(tx *blockchain.Tx, removeDescendants bool) { + m.ctrl.Call(m, "RemoveTx", tx, removeDescendants) +} + +// RemoveTx indicates an expected call of RemoveTx +func (mr *MockTxPoolMockRecorder) RemoveTx(tx, removeDescendants interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTx", reflect.TypeOf((*MockTxPool)(nil).RemoveTx), tx, removeDescendants) +} + +// RemoveDoubleSpends mocks base method +func (m *MockTxPool) RemoveDoubleSpends(tx *blockchain.Tx) { + m.ctrl.Call(m, "RemoveDoubleSpends", tx) +} + +// RemoveDoubleSpends indicates an expected call of RemoveDoubleSpends +func (mr *MockTxPoolMockRecorder) RemoveDoubleSpends(tx interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveDoubleSpends", reflect.TypeOf((*MockTxPool)(nil).RemoveDoubleSpends), tx) +} + +// FetchTx mocks base method +func (m *MockTxPool) FetchTx(hash *crypto.Hash32B) (*blockchain.Tx, error) { + ret := m.ctrl.Call(m, "FetchTx", hash) + ret0, _ := ret[0].(*blockchain.Tx) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// FetchTx indicates an expected call of FetchTx +func (mr *MockTxPoolMockRecorder) FetchTx(hash interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "FetchTx", reflect.TypeOf((*MockTxPool)(nil).FetchTx), hash) +} + +// MaybeAcceptTx mocks base method +func (m *MockTxPool) MaybeAcceptTx(tx *blockchain.Tx, isNew, rateLimit bool) ([]crypto.Hash32B, *txpool.TxDesc, error) { + ret := m.ctrl.Call(m, "MaybeAcceptTx", tx, isNew, rateLimit) + ret0, _ := ret[0].([]crypto.Hash32B) + ret1, _ := ret[1].(*txpool.TxDesc) + ret2, _ := ret[2].(error) + return ret0, ret1, ret2 +} + +// MaybeAcceptTx indicates an expected call of MaybeAcceptTx +func (mr *MockTxPoolMockRecorder) MaybeAcceptTx(tx, isNew, rateLimit interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "MaybeAcceptTx", reflect.TypeOf((*MockTxPool)(nil).MaybeAcceptTx), tx, isNew, rateLimit) +} + +// ProcessOrphanTxs mocks base method +func (m *MockTxPool) ProcessOrphanTxs(acceptedTx *blockchain.Tx) []*txpool.TxDesc { + ret := m.ctrl.Call(m, "ProcessOrphanTxs", acceptedTx) + ret0, _ := ret[0].([]*txpool.TxDesc) + return ret0 +} + +// ProcessOrphanTxs indicates an expected call of ProcessOrphanTxs +func (mr *MockTxPoolMockRecorder) ProcessOrphanTxs(acceptedTx interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessOrphanTxs", reflect.TypeOf((*MockTxPool)(nil).ProcessOrphanTxs), acceptedTx) +} + +// ProcessTx mocks base method +func (m *MockTxPool) ProcessTx(tx *blockchain.Tx, allowOrphan, rateLimit bool, tag txpool.Tag) ([]*txpool.TxDesc, error) { + ret := m.ctrl.Call(m, "ProcessTx", tx, allowOrphan, rateLimit, tag) + ret0, _ := ret[0].([]*txpool.TxDesc) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// ProcessTx indicates an expected call of ProcessTx +func (mr *MockTxPoolMockRecorder) ProcessTx(tx, allowOrphan, rateLimit, tag interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "ProcessTx", reflect.TypeOf((*MockTxPool)(nil).ProcessTx), tx, allowOrphan, rateLimit, tag) +} + +// TxDescs mocks base method +func (m *MockTxPool) TxDescs() []*txpool.TxDesc { + ret := m.ctrl.Call(m, "TxDescs") + ret0, _ := ret[0].([]*txpool.TxDesc) + return ret0 +} + +// TxDescs indicates an expected call of TxDescs +func (mr *MockTxPoolMockRecorder) TxDescs() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "TxDescs", reflect.TypeOf((*MockTxPool)(nil).TxDescs)) +} + +// Txs mocks base method +func (m *MockTxPool) Txs() []*blockchain.Tx { + ret := m.ctrl.Call(m, "Txs") + ret0, _ := ret[0].([]*blockchain.Tx) + return ret0 +} + +// Txs indicates an expected call of Txs +func (mr *MockTxPoolMockRecorder) Txs() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "Txs", reflect.TypeOf((*MockTxPool)(nil).Txs)) +} + +// RemoveTxInBlock mocks base method +func (m *MockTxPool) RemoveTxInBlock(block *blockchain.Block) error { + ret := m.ctrl.Call(m, "RemoveTxInBlock", block) + ret0, _ := ret[0].(error) + return ret0 +} + +// RemoveTxInBlock indicates an expected call of RemoveTxInBlock +func (mr *MockTxPoolMockRecorder) RemoveTxInBlock(block interface{}) *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "RemoveTxInBlock", reflect.TypeOf((*MockTxPool)(nil).RemoveTxInBlock), block) +} + +// LastTimePoolUpdated mocks base method +func (m *MockTxPool) LastTimePoolUpdated() time.Time { + ret := m.ctrl.Call(m, "LastTimePoolUpdated") + ret0, _ := ret[0].(time.Time) + return ret0 +} + +// LastTimePoolUpdated indicates an expected call of LastTimePoolUpdated +func (mr *MockTxPoolMockRecorder) LastTimePoolUpdated() *gomock.Call { + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "LastTimePoolUpdated", reflect.TypeOf((*MockTxPool)(nil).LastTimePoolUpdated)) +} diff --git a/test/testaddress/testaddress.go b/test/testaddress/testaddress.go new file mode 100644 index 0000000000..3cbee2cb57 --- /dev/null +++ b/test/testaddress/testaddress.go @@ -0,0 +1,64 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package testaddress + +import ( + "encoding/hex" + + "github.com/iotexproject/iotex-core/iotxaddress" +) + +const ( + pubkeyA = "09d8c6fc6f5cb0a03df112da90486fad7cdece1501aaab658551f8afbe7f59ee" + prikeyA = "9bb680a519ac1c034231c33f0e2f46816f6263b2ff642808a5f33998f7e412b109d8c6fc6f5cb0a03df112da90486fad7cdece1501aaab658551f8afbe7f59ee" + pubkeyB = "247a92febf1daac9c9d1a0f5224bb61eb14b2f291d10e01daa0aa5cf17d3f83a" + prikeyB = "aad44df530cb1a52e546b4ecd84ccd63c078887e99c73b6f13f2794b20a4bf15247a92febf1daac9c9d1a0f5224bb61eb14b2f291d10e01daa0aa5cf17d3f83a" + pubkeyC = "335664daa5d054e02004c71c28ed4ab4aaec650098fb29e518e23e25274a7273" + prikeyC = "a0c0ce98a89f47dede4d380334612a0f155b5d161d4f89256386c9c8b5c92ab9335664daa5d054e02004c71c28ed4ab4aaec650098fb29e518e23e25274a7273" + pubkeyD = "7c5ec537514f4d33b7d99bf3f7a1c94c698db6121e322b54e073ab123212ca03" + prikeyD = "3ade330ac95c22517ea23bc37f8f980e2e63796af2124435ba0f862627cb579f7c5ec537514f4d33b7d99bf3f7a1c94c698db6121e322b54e073ab123212ca03" + pubkeyE = "705c9a0517381e060e148e65955c54ea8a1cf7c8b4ad47e2df1e14d779e6acc6" + prikeyE = "0ba67d6be4700b8825708b95d2489cb3150836794e1893278d1df7357677a161705c9a0517381e060e148e65955c54ea8a1cf7c8b4ad47e2df1e14d779e6acc6" + pubkeyF = "78214053ae1171cccf40e564f1d178e9356072b4e69b81d0f234472d4fc08304" + prikeyF = "73df37d072a90da42589546cae50558b732d8a0f9105b3db135da97540f93c8478214053ae1171cccf40e564f1d178e9356072b4e69b81d0f234472d4fc08304" + pubkeyG = "c9a3b81ce7ea795890b116be65d38ee80c5fa879ca56829291882ffc9f00cb5d" + prikeyG = "324819666a70f84c6ed5b352494775ab8eeb5e125faef96822326039576a06afc9a3b81ce7ea795890b116be65d38ee80c5fa879ca56829291882ffc9f00cb5d" + pubkeyMiner = "b9b8d7316705dc4ff62bb323e610f3f5072abedc9834e999d6537f6681284ea2" + prikeyMiner = "7fbb20b87d34eade61351165aa4c6fa5d87dd349368dd6b9034ea3d3e918c706b9b8d7316705dc4ff62bb323e610f3f5072abedc9834e999d6537f6681284ea2" +) + +// Addrinfo contains the address information +var Addrinfo map[string]iotxaddress.Address + +func constructAddress(pubkey, prikey string) iotxaddress.Address { + pubk, err := hex.DecodeString(pubkey) + if err != nil { + panic(err) + } + prik, err := hex.DecodeString(prikey) + if err != nil { + panic(err) + } + addr, err := iotxaddress.GetAddress(pubk, false, 0x01, []byte{0x01, 0x02, 0x03, 0x04}) + if err != nil { + panic(err) + } + return iotxaddress.Address{PublicKey: pubk, PrivateKey: prik, Address: addr} +} + +func init() { + Addrinfo = make(map[string]iotxaddress.Address) + + Addrinfo["miner"] = constructAddress(pubkeyMiner, prikeyMiner) + Addrinfo["alfa"] = constructAddress(pubkeyA, prikeyA) + Addrinfo["bravo"] = constructAddress(pubkeyB, prikeyB) + Addrinfo["charlie"] = constructAddress(pubkeyC, prikeyC) + Addrinfo["delta"] = constructAddress(pubkeyD, prikeyD) + Addrinfo["echo"] = constructAddress(pubkeyE, prikeyE) + Addrinfo["foxtrot"] = constructAddress(pubkeyF, prikeyF) + Addrinfo["galilei"] = constructAddress(pubkeyG, prikeyG) +} diff --git a/tools/cli/cli.go b/tools/cli/cli.go new file mode 100644 index 0000000000..ce03b5b9bb --- /dev/null +++ b/tools/cli/cli.go @@ -0,0 +1,94 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package cli + +import ( + "flag" + "fmt" + "os" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/config" +) + +// CLI defines the struct of command line interface +type CLI struct { + bc *blockchain.Blockchain +} + +func (cli *CLI) printUsage() { + fmt.Println("Usage:") + fmt.Println(" printchain # print all the blocks of the blockchain") + fmt.Println(" createchain -address ADDRESS # create a new blockchain with an address") + fmt.Println(" getbalance -address ADDRESS # get the balance of the address") + fmt.Println(" send -from FROM -to TO -amount AMOUNT # send from one address to another") +} + +func (cli *CLI) validateArgs() { + if len(os.Args) < 2 { + cli.printUsage() + os.Exit(1) + } +} + +// Run processes the command line input +func (cli *CLI) Run() { + cli.validateArgs() + + printChainCmd := flag.NewFlagSet("printchain", flag.ExitOnError) + + createChainCmd := flag.NewFlagSet("createchain", flag.ExitOnError) + createChainAddress := createChainCmd.String("address", "", "Coinbase transaction output address") + + getBalanceCmd := flag.NewFlagSet("getbalance", flag.ExitOnError) + getBalanceAddress := getBalanceCmd.String("address", "", "address") + + sendCmd := flag.NewFlagSet("send", flag.ExitOnError) + sendCmdFrom := sendCmd.String("from", "", "send from address") + sendCmdTo := sendCmd.String("to", "", "send to address") + sendCmdAmount := sendCmd.Int("amount", 0, "send amount") + + switch os.Args[1] { + case "printchain": + printChainCmd.Parse(os.Args[2:]) + case "createchain": + createChainCmd.Parse(os.Args[2:]) + case "getbalance": + getBalanceCmd.Parse(os.Args[2:]) + case "send": + sendCmd.Parse(os.Args[2:]) + default: + cli.printUsage() + os.Exit(1) + } + + config, err := config.LoadConfig() + if err != nil { + os.Exit(1) + } + + if printChainCmd.Parsed() { + cli.printChain(config) + } + if createChainCmd.Parsed() { + if *createChainAddress == "" { + os.Exit(1) + } + cli.bc = blockchain.CreateBlockchain(*createChainAddress, config) + defer cli.bc.Close() + } + if getBalanceCmd.Parsed() { + cli.getBalance(*getBalanceAddress, config) + } + if sendCmd.Parsed() { + if *sendCmdFrom == "" || *sendCmdTo == "" || *sendCmdAmount <= 0 { + sendCmd.Usage() + os.Exit(1) + } + cli.send(*sendCmdFrom, *sendCmdTo, uint64(*sendCmdAmount), config) + } +} diff --git a/tools/cli/get_balance.go b/tools/cli/get_balance.go new file mode 100644 index 0000000000..3b3dc27100 --- /dev/null +++ b/tools/cli/get_balance.go @@ -0,0 +1,28 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package cli + +import ( + "fmt" + + "github.com/golang/glog" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/iotxaddress" +) + +func (cli *CLI) getBalance(address string, config *config.Config) { + if !iotxaddress.ValidateAddress(address) { + glog.Fatal("ERROR: Address is not valid") + } + bc := blockchain.CreateBlockchain(address, config) + defer bc.Close() + + balance := bc.BalanceOf(address) + fmt.Printf("Balance of '%s': %d\n", address, balance) +} diff --git a/tools/cli/print_chain.go b/tools/cli/print_chain.go new file mode 100644 index 0000000000..9f78be23fe --- /dev/null +++ b/tools/cli/print_chain.go @@ -0,0 +1,33 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package cli + +import ( + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/config" +) + +func (cli *CLI) printChain(config *config.Config) { + cli.bc = blockchain.CreateBlockchain("it1qyqqqqqpj74jttuw2wdu2vlejv3xg6adu3v743w049htcg", config) + defer cli.bc.Close() + + /* + it := cli.bc.Iterator() + for { + block := it.Next() + for i, tx := range block.Tranxs { + fmt.Printf("Transaction %d: %x\n", i, tx.Hash()) + } + fmt.Printf("Hash: %x\n", block.HashBlock()) + fmt.Println() + + if bytes.Compare(it.CurrHash, blockchain.ZeroHash32B[:]) == 0 { + break + } + } + */ +} diff --git a/tools/cli/send.go b/tools/cli/send.go new file mode 100644 index 0000000000..6057a01f1e --- /dev/null +++ b/tools/cli/send.go @@ -0,0 +1,33 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package cli + +import ( + "fmt" + + "github.com/golang/glog" + + "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/iotxaddress" +) + +func (cli *CLI) send(from, to string, amount uint64, config *config.Config) { + if !iotxaddress.ValidateAddress(from) { + glog.Fatal("ERROR: Sender address is not valid") + } + if !iotxaddress.ValidateAddress(to) { + glog.Fatal("ERROR: Recipient address is not valid") + } + + bc := blockchain.CreateBlockchain(from, config) + defer bc.Close() + + //tx := blockchain.NewUTXOTransaction(from, to, amount, bc) + //bc.MineBlock([]*blockchain.Tx{tx}) + fmt.Println("Success!") +} diff --git a/tools/run_tnet.sh b/tools/run_tnet.sh new file mode 100755 index 0000000000..2a0643ec18 --- /dev/null +++ b/tools/run_tnet.sh @@ -0,0 +1,8 @@ +#!/bin/bash + +for i in `seq 0 4`; do + node=$1$i + docker run --rm --network host stonevan $node & +done + +exit 0 \ No newline at end of file diff --git a/tools/start_node.sh b/tools/start_node.sh new file mode 100755 index 0000000000..1b6abd84b3 --- /dev/null +++ b/tools/start_node.sh @@ -0,0 +1,7 @@ +#!/bin/bash + +file=$GOPATH/src/github.com/iotexproject/iotex-core/sampleconfig/stonevan/config_$1.yaml +echo "Starting node with config file:" $file +$GOPATH/src/github.com/iotexproject/iotex-core/bin/server -stderrthreshold=WARNING -log_dir=./log -config=$file + +exit 0 \ No newline at end of file diff --git a/tools/txinjector/txinjector.go b/tools/txinjector/txinjector.go new file mode 100644 index 0000000000..9e8fd42210 --- /dev/null +++ b/tools/txinjector/txinjector.go @@ -0,0 +1,103 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +// This is a testing tool to inject fake transactions to the blockchain +// To use, run "make build" and " ./bin/txinjector" + +package main + +import ( + "fmt" + "math/rand" + "time" + + "github.com/golang/protobuf/proto" + "golang.org/x/net/context" + "google.golang.org/grpc" + + "github.com/iotexproject/iotex-core/blockchain" + pb "github.com/iotexproject/iotex-core/proto" + ta "github.com/iotexproject/iotex-core/test/testaddress" + "github.com/iotexproject/iotex-core/txvm" +) + +const ( + port = ":42124" +) + +func main() { + conn, err := grpc.Dial("127.0.0.1"+port, grpc.WithInsecure()) + defer conn.Close() + if err != nil { + panic(err) + } + + c := pb.NewChainServiceClient(conn) + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + rand.Seed(time.Now().UnixNano()) + amount := uint64(0) + for amount == uint64(0) { + amount = uint64(rand.Intn(10)) + } + fmt.Printf("Sending %v coins from 'miner' to 'alfa'", amount) + + r, err := c.CreateRawTx(ctx, &pb.CreateRawTxRequest{ + From: ta.Addrinfo["miner"].Address, To: ta.Addrinfo["alfa"].Address, Value: amount}) + if err != nil { + panic(err) + } else { + fmt.Println("Created raw tx") + } + + tx := &pb.TxPb{} + if err := proto.Unmarshal(r.SerializedTx, tx); err != nil { + panic(err) + } + + in := []*blockchain.TxInput{} + for _, rin := range tx.TxIn { + unlock, err := txvm.SignatureScript(rin.UnlockScript, ta.Addrinfo["miner"].PublicKey, ta.Addrinfo["miner"].PrivateKey) + if err != nil { + panic(err) + } + in = append(in, &blockchain.TxInput{ + rin.TxHash, + rin.OutIndex, + uint32(len(unlock)), + unlock, + 0}) + } + tx.TxIn = in + + stx, err := proto.Marshal(tx) + if err != nil { + panic(err) + } + _, err = c.SendTx(ctx, &pb.SendTxRequest{SerializedTx: stx}) + if err != nil { + panic(err) + } + fmt.Println("Sent out the signed tx: ") + + fmt.Println("version: ", tx.Version) + for i := 0; i < len(tx.TxIn); i++ { + fmt.Printf("txIn[%v]:\n", i) + fmt.Printf("\thash: 0x%x\n", tx.TxIn[i].TxHash) + fmt.Println("\tout index:", tx.TxIn[i].OutIndex) + fmt.Println("\tunlock script:", tx.TxIn[i].UnlockScript) + fmt.Println("\tunlock script size:", tx.TxIn[i].UnlockScriptSize) + fmt.Println("\tsequence:", tx.TxIn[i].Sequence) + } + + for i := 0; i < len(tx.TxOut); i++ { + fmt.Printf("txOut[%v]:\n", i) + fmt.Println("\tvalue:", tx.TxOut[i].Value) + fmt.Println("\tlock script:", tx.TxOut[i].LockScript) + fmt.Println("\tlock script size:", tx.TxOut[i].LockScriptSize) + } +} diff --git a/txpool/txpool.go b/txpool/txpool.go new file mode 100644 index 0000000000..98aceaaf24 --- /dev/null +++ b/txpool/txpool.go @@ -0,0 +1,701 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txpool + +import ( + "container/heap" + "container/list" + "fmt" + "sync" + "sync/atomic" + "time" + + "github.com/golang/glog" + + "github.com/iotexproject/iotex-core/blockchain" + cp "github.com/iotexproject/iotex-core/crypto" +) + +// Basic constant settings for TxPool +const ( + orphanTxTTL = time.Hour / 2 + orphanTxExpireScanInterval = time.Minute * 5 + maxOrphanTxNum = 10000 + maxOrphanTxSize = 8192 + enableTagIndex = false + DefaultBlockPrioritySize = 12345 +) + +// Tag for OrphanTx +type Tag uint64 + +// TxDesc contains Transaction with misc +type TxDesc struct { + Tx *blockchain.Tx + AddedTime time.Time + BlockHeight uint32 + Fee int64 + FeePerKB int64 + Priority float64 + idx int +} + +type orphanTx struct { + Tag Tag + Tx *blockchain.Tx + ExpirationTime time.Time +} + +// TxSourcePointer is to refer to the output tx, hash + index in the output tx +type TxSourcePointer struct { + Hash cp.Hash32B + Index int32 +} + +// NewTxSourcePointer creates a new TxSourcePointer given TxInput +func NewTxSourcePointer(in *blockchain.TxInput) TxSourcePointer { + hash := cp.ZeroHash32B + copy(hash[:], in.TxHash) + return TxSourcePointer{ + hash, + in.OutIndex, + } +} + +type txDescPriorityQueue []*TxDesc + +func (pq txDescPriorityQueue) Len() int { + return len(pq) +} + +func (pq txDescPriorityQueue) Less(i, j int) bool { + return pq[i].Priority > pq[j].Priority +} + +func (pq txDescPriorityQueue) Swap(i, j int) { + pq[i], pq[j] = pq[j], pq[i] + pq[i].idx = i + pq[j].idx = j +} + +func (pq *txDescPriorityQueue) Push(x interface{}) { + n := len(*pq) + txDesc := x.(*TxDesc) + txDesc.idx = n + *pq = append(*pq, txDesc) +} + +func (pq *txDescPriorityQueue) Pop() interface{} { + n := len(*pq) + old := *pq + txDesc := old[n-1] + txDesc.idx = -1 + *pq = old[0 : n-1] + return txDesc +} + +func (pq *txDescPriorityQueue) Delete(i int) interface{} { + n := len(*pq) + pq.Swap(i, n-1) + txDesc := pq.Pop() + heap.Fix(pq, i) + + return txDesc +} + +// TxPool is a pool of received txs +type TxPool interface { + // RemoveOrphanTx remove an orphan transaction, but not its descendants + RemoveOrphanTx(tx *blockchain.Tx) + // RemoveOrphanTxsByTag remove all the orphan transactions with tag + RemoveOrphanTxsByTag(tag Tag) uint64 + // HasOrphanTx check whether hash is an orphan transaction in the pool + HasOrphanTx(hash cp.Hash32B) bool + // HasTxOrOrphanTx check whether hash is an accepted or orphan transaction in the pool + HasTxOrOrphanTx(hash cp.Hash32B) bool + // RemoveTx remove an accepted transaction + RemoveTx(tx *blockchain.Tx, removeDescendants bool) + // RemoveDoubleSpends remove the double spending transaction + RemoveDoubleSpends(tx *blockchain.Tx) + // FetchTx fetch accepted transaction from the pool + FetchTx(hash *cp.Hash32B) (*blockchain.Tx, error) + // MaybeAcceptTx add Tx into pool if it could be accpted + MaybeAcceptTx(tx *blockchain.Tx, isNew bool, rateLimit bool) ([]cp.Hash32B, *TxDesc, error) + // ProcessOrphanTxs process the orphan txs depending on the accepted tx + ProcessOrphanTxs(acceptedTx *blockchain.Tx) []*TxDesc + // ProcessTx process the tx + ProcessTx(tx *blockchain.Tx, allowOrphan bool, rateLimit bool, tag Tag) ([]*TxDesc, error) + // TxDescs return all the transaction descs + TxDescs() []*TxDesc + // Txs return all Transactions + Txs() []*blockchain.Tx + // RemoveTxInBlock remove all transactions in a block + RemoveTxInBlock(block *blockchain.Block) error + // LastTimePoolUpdated get the last time the pool got updated + LastTimePoolUpdated() time.Time +} + +// txPool implements TxPool interface +// Note that all locks should be placed in public functions (no lock inside of any private function) +type txPool struct { + bc blockchain.IBlockchain + + lastUpdatedUnixTime int64 + mutex sync.RWMutex + txDescs map[cp.Hash32B]*TxDesc + txDescPriorityQueue txDescPriorityQueue + orphanTxs map[cp.Hash32B]*orphanTx + orphanTxSourcePointers map[TxSourcePointer]map[cp.Hash32B]*blockchain.Tx + txSourcePointers map[TxSourcePointer]*blockchain.Tx + tags map[Tag]map[cp.Hash32B]*blockchain.Tx + nextExpirationScanTime time.Time +} + +// New creates a TxPool instance +func New(bc blockchain.IBlockchain) TxPool { + return &txPool{ + bc: bc, + tags: make(map[Tag]map[cp.Hash32B]*blockchain.Tx), + txDescs: make(map[cp.Hash32B]*TxDesc), + txSourcePointers: make(map[TxSourcePointer]*blockchain.Tx), + orphanTxs: make(map[cp.Hash32B]*orphanTx), + orphanTxSourcePointers: make(map[TxSourcePointer]map[cp.Hash32B]*blockchain.Tx), + } +} + +// remove an orphan transaction, and all the descendant orphan transactions if removeDescendants is true +func (tp *txPool) removeOrphanTx(tx *blockchain.Tx, removeDescendants bool) { + hash := tx.Hash() + orphanTx, ok := tp.orphanTxs[hash] + if !ok { + glog.Info("cannot find orphan tx: ", hash) + return + } + + for _, in := range orphanTx.Tx.TxIn { + txSourcePointer := NewTxSourcePointer(in) + orphanTxs, ok := tp.orphanTxSourcePointers[txSourcePointer] + if ok { + delete(orphanTxs, hash) + if len(orphanTxs) == 0 { + delete(tp.orphanTxSourcePointers, txSourcePointer) + } + } + } + + if removeDescendants { + txSourcePointer := TxSourcePointer{Hash: hash} + for index := range tx.TxOut { + txSourcePointer.Index = int32(index) + for _, orphanTx := range tp.orphanTxSourcePointers[txSourcePointer] { + tp.removeOrphanTx(orphanTx, true) + } + } + } + if enableTagIndex { + tagHashs, ok := tp.tags[orphanTx.Tag] + if ok { + _, ok := tagHashs[hash] + if ok { + delete(tagHashs, hash) + if len(tagHashs) == 0 { + delete(tp.tags, orphanTx.Tag) + } + } + } + } + // WARNING: is it possible that the hash is deleted twice? + delete(tp.orphanTxs, hash) +} + +// RemoveOrphanTx Remove an orphan transaction, but not its descendants +func (tp *txPool) RemoveOrphanTx(tx *blockchain.Tx) { + tp.mutex.Lock() + tp.removeOrphanTx(tx, false) + tp.mutex.Unlock() +} + +// RemoveOrphanTxsByTag removes all orphan txs with the given tag +func (tp *txPool) RemoveOrphanTxsByTag(tag Tag) uint64 { + var retval uint64 + tp.mutex.Lock() + if enableTagIndex { + hashs, ok := tp.tags[tag] + if ok { + for _, tx := range hashs { + tp.removeOrphanTx(tx, true) + retval++ + } + delete(tp.tags, tag) + } + } else { + for _, orphanTx := range tp.orphanTxs { + if orphanTx.Tag == tag { + tp.removeOrphanTx(orphanTx.Tx, true) + retval++ + } + } + } + tp.mutex.Unlock() + return retval +} + +func (tp *txPool) deleteExpiredOrphanTxs() { + now := time.Now() + if now.Before(tp.nextExpirationScanTime) { + return + } + for _, orphanTx := range tp.orphanTxs { + if now.After(orphanTx.ExpirationTime) { + tp.removeOrphanTx(orphanTx.Tx, true) + } + } + tp.nextExpirationScanTime = now.Add(orphanTxExpireScanInterval) + glog.Info("scan and delete expired orphan transactions") +} + +func (tp *txPool) emptyASpaceForNewOrphanTx() error { + if len(tp.orphanTxs) < maxOrphanTxNum { + return nil + } + + for _, orphanTx := range tp.orphanTxs { + tp.removeOrphanTx(orphanTx.Tx, false) + break + } + + return nil +} + +func (tp *txPool) addOrphanTx(tx *blockchain.Tx, tag Tag) { + if maxOrphanTxNum <= 0 { + return + } + + tp.deleteExpiredOrphanTxs() + tp.emptyASpaceForNewOrphanTx() + hash := tx.Hash() + tp.orphanTxs[hash] = &orphanTx{ + tag, + tx, + time.Now().Add(orphanTxTTL), + } + if enableTagIndex { + if _, ok := tp.tags[tag]; !ok { + tp.tags[tag] = make(map[cp.Hash32B]*blockchain.Tx) + } + tp.tags[tag][hash] = tx + } + for _, txIn := range tx.TxIn { + txSourcePointer := NewTxSourcePointer(txIn) + if _, ok := tp.orphanTxSourcePointers[txSourcePointer]; !ok { + tp.orphanTxSourcePointers[txSourcePointer] = make(map[cp.Hash32B]*blockchain.Tx) + } + tp.orphanTxSourcePointers[txSourcePointer][hash] = tx + } + glog.Info("Add orphan tx %x to pool", hash) +} + +func (tp *txPool) maybeAddOrphanTx(tx *blockchain.Tx, tag Tag) error { + serialize, error := tx.Serialize() + if error != nil { + return error + } + if len(serialize) > maxOrphanTxSize { + return fmt.Errorf("tx %x's size is larger than limit", tx.Hash()) + } + tp.addOrphanTx(tx, tag) + + return nil +} + +func (tp *txPool) removeOrphanTxDoubleSpends(tx *blockchain.Tx) { + for _, txIn := range tx.TxIn { + for _, orphanTx := range tp.orphanTxSourcePointers[NewTxSourcePointer(txIn)] { + tp.removeOrphanTx(orphanTx, true) + } + } +} + +func (tp *txPool) hasTx(hash cp.Hash32B) bool { + _, ok := tp.txDescs[hash] + + return ok +} + +// HasTx Check whether the pool contains tx with the given hash +func (tp *txPool) HasTx(hash cp.Hash32B) bool { + tp.mutex.RLock() + retval := tp.hasTx(hash) + tp.mutex.RUnlock() + + return retval +} + +func (tp *txPool) hasOrphanTx(hash cp.Hash32B) bool { + _, ok := tp.orphanTxs[hash] + + return ok +} + +// HasOrphanTx Check whether the pool contains orphan tx with the given hash +func (tp *txPool) HasOrphanTx(hash cp.Hash32B) bool { + tp.mutex.RLock() + retval := tp.hasOrphanTx(hash) + tp.mutex.RUnlock() + + return retval +} + +func (tp *txPool) hasTxOrOrphanTx(hash cp.Hash32B) bool { + return tp.hasTx(hash) || tp.hasOrphanTx(hash) +} + +// HasTxOrOrphanTx Check whether the pool contains tx or orphan tx with the given hash +func (tp *txPool) HasTxOrOrphanTx(hash cp.Hash32B) bool { + tp.mutex.RLock() + retval := tp.hasTxOrOrphanTx(hash) + tp.mutex.RUnlock() + + return retval +} + +func (tp *txPool) setLastUpdateUnixTime() { + atomic.StoreInt64(&tp.lastUpdatedUnixTime, time.Now().Unix()) +} + +func (tp *txPool) removeTx(tx *blockchain.Tx, removeDescendants bool) { + hash := tx.Hash() + if removeDescendants { + txSourcePointer := TxSourcePointer{Hash: hash} + for index := int32(0); index < int32(len(tx.TxOut)); index++ { + txSourcePointer.Index = index + if tx, ok := tp.txSourcePointers[txSourcePointer]; ok { + tp.removeTx(tx, true) + } + } + } + desc, ok := tp.txDescs[hash] + if !ok { + return + } + + for _, txIn := range desc.Tx.TxIn { + delete(tp.txSourcePointers, NewTxSourcePointer(txIn)) + } + + // TODO: soft delete desc from txDescPriorityQueue + //tp.txDescPriorityQueue.Delete(desc.idx) + delete(tp.txDescs, hash) + tp.setLastUpdateUnixTime() +} + +// RemoveTx removes tx from the pool +func (tp *txPool) RemoveTx(tx *blockchain.Tx, removeDescendants bool) { + tp.mutex.Lock() + tp.removeTx(tx, removeDescendants) + tp.mutex.Unlock() +} + +// RemoveDoubleSpends removes all transactions which share source pointers with input tx +func (tp *txPool) RemoveDoubleSpends(tx *blockchain.Tx) { + tp.mutex.Lock() + hash := tx.Hash() + for _, txIn := range tx.TxIn { + txSourcePointer := NewTxSourcePointer(txIn) + if txDescendant, ok := tp.txSourcePointers[txSourcePointer]; ok { + if txDescendant.Hash() != hash { + tp.removeTx(txDescendant, true) + } + } + } + tp.mutex.Unlock() +} + +func (tp *txPool) addTx(utxoTracker *blockchain.UtxoTracker, tx *blockchain.Tx, height uint32, fee int64) *TxDesc { + serialize, err := tx.Serialize() + if err != nil { + return nil + } + desc := TxDesc{ + Tx: tx, + AddedTime: time.Now(), + BlockHeight: height, + Fee: fee, + FeePerKB: fee * 1000 / int64(len(serialize)), + Priority: float64(fee), + } + tp.txDescs[tx.Hash()] = &desc + heap.Push(&tp.txDescPriorityQueue, &desc) + for _, txIn := range tx.TxIn { + tp.txSourcePointers[NewTxSourcePointer(txIn)] = tx + } + tp.setLastUpdateUnixTime() + + return &desc +} + +// Check whether any of tx's inputs have been spent by other transactions +func (tp *txPool) checkPoolDoubleSpend(tx *blockchain.Tx) error { + for _, txIn := range tx.TxIn { + txSourcePointer := NewTxSourcePointer(txIn) + if txSpend, ok := tp.txSourcePointers[txSourcePointer]; ok { + return fmt.Errorf("%v has already been spent by %v", txSourcePointer, txSpend.Hash()) + } + } + + return nil +} + +// IsFullySpent Check whether the output txs have been fully spent +func IsFullySpent(outputs []*blockchain.TxOutput) bool { + return false +} + +func (tp *txPool) fetchInputUtxos(tx *blockchain.Tx) (*blockchain.UtxoTracker, error) { + utxoTracker := blockchain.NewUtxoTracker() + utxoPool := tp.bc.UtxoPool() + for _, txIn := range tx.TxIn { + hash := cp.ZeroHash32B + copy(hash[:], txIn.TxHash) + outputs := utxoPool[hash] + utxoTracker.GetPool()[hash] = outputs + } + + // attempt to populate any missing input from the transaction pool + for hash, outputs := range utxoTracker.GetPool() { + if outputs != nil && !IsFullySpent(outputs) { + continue + } + if desc, ok := tp.txDescs[hash]; ok { + utxoTracker.AddTx(desc.Tx, 0) + } + } + + return utxoTracker, nil +} + +// FetchTx gets the tx with the given hash +func (tp *txPool) FetchTx(hash *cp.Hash32B) (*blockchain.Tx, error) { + tp.mutex.RLock() + desc, ok := tp.txDescs[*hash] + tp.mutex.RUnlock() + if ok { + return desc.Tx, nil + } + + return nil, fmt.Errorf("cannot find transaction in the pool") +} + +func calculateMinFee(size uint32) int64 { + return 0 +} + +func (tp *txPool) maybeAcceptTx(tx *blockchain.Tx, isNew bool, rateLimit bool, rejectDuplicateOrphanTxs bool) ([]cp.Hash32B, *TxDesc, error) { + hash := tx.Hash() + if tp.hasTx(hash) || (rejectDuplicateOrphanTxs && tp.hasOrphanTx(hash)) { + return nil, nil, fmt.Errorf("duplicate transaction") + } + if tx.IsCoinbase() { + return nil, nil, fmt.Errorf("unexpected coinbase transaction") + } + + err := tp.checkPoolDoubleSpend(tx) + if err != nil { + return nil, nil, err + } + + utxoTracker, err := tp.fetchInputUtxos(tx) + if err != nil { + // if it is chain rule error + // return chain rule error + return nil, nil, err + } + + outputs := utxoTracker.GetPool()[hash] + if outputs != nil && !IsFullySpent(outputs) { + // return nil, nil, fmt.Errorf("duplicate transaction") + } + delete(utxoTracker.GetPool(), hash) + + var missingParents []cp.Hash32B + for originHash, outputs := range utxoTracker.GetPool() { + if outputs == nil || IsFullySpent(outputs) { + missingParents = append(missingParents, originHash) + } + } + if len(missingParents) > 0 { + return missingParents, nil, nil + } + if int64(tx.LockTime) > time.Now().Unix() { + return nil, nil, fmt.Errorf("tx %s is still in lock", hash) + } + + fee := int64(0) + size := tx.TotalSize() + minFee := calculateMinFee(size) + if (size >= DefaultBlockPrioritySize-1000) && fee < minFee { + return nil, nil, fmt.Errorf("fee is lower than min requirement fee") + } + + height := tp.bc.TipHeight() + txDesc := tp.addTx(utxoTracker, tx, height, fee) + + return nil, txDesc, nil +} + +// MaybeAcceptTx Add Tx into pool if it will be accepted +func (tp *txPool) MaybeAcceptTx(tx *blockchain.Tx, isNew bool, rateLimit bool) ([]cp.Hash32B, *TxDesc, error) { + tp.mutex.Lock() + hashes, desc, error := tp.maybeAcceptTx(tx, isNew, rateLimit, true) + tp.mutex.Unlock() + + return hashes, desc, error +} + +func (tp *txPool) processOrphanTxs(acceptedTx *blockchain.Tx) []*TxDesc { + var acceptedTxDescs []*TxDesc + processList := list.New() + processList.PushBack(acceptedTx) + // remove all descendants + for processList.Len() > 0 { + firstElement := processList.Remove(processList.Front()) + item := firstElement.(*blockchain.Tx) + txSourcePointer := TxSourcePointer{Hash: item.Hash()} + for idx := range item.TxOut { + txSourcePointer.Index = int32(idx) + orphanTxs, ok := tp.orphanTxSourcePointers[txSourcePointer] + if !ok { + continue + } + for _, tx := range orphanTxs { + missingParents, desc, err := tp.maybeAcceptTx( + tx, + true, // is new + true, // rate limit + false, // reject duplicate + ) + if err != nil { + tp.removeOrphanTx(tx, true) + break + } + if len(missingParents) > 0 { + continue + } + acceptedTxDescs = append(acceptedTxDescs, desc) + tp.removeOrphanTx(tx, false) + processList.PushBack(tx) + break + } + } + } + + tp.removeOrphanTxDoubleSpends(acceptedTx) + for _, desc := range acceptedTxDescs { + tp.removeOrphanTxDoubleSpends(desc.Tx) + } + + return acceptedTxDescs +} + +// ProcessOrphanTxs Go through all the orphan txs, and process the ones depending on the accepted tx +func (tp *txPool) ProcessOrphanTxs(acceptedTx *blockchain.Tx) []*TxDesc { + tp.mutex.Lock() + acceptedTxDescs := tp.processOrphanTxs(acceptedTx) + tp.mutex.Unlock() + + return acceptedTxDescs +} + +// ProcessTx Process the tx as accepted or orphan +func (tp *txPool) ProcessTx(tx *blockchain.Tx, allowOrphan bool, rateLimit bool, tag Tag) ([]*TxDesc, error) { + // Protect concurrent access. + tp.mutex.Lock() + defer tp.mutex.Unlock() + + missingParents, desc, err := tp.maybeAcceptTx( + tx, + true, // is new + rateLimit, + true, // prevent duplications + ) + if err != nil { + return nil, err + } + + // if no missing parents, return all the descendants with no missing parents + if len(missingParents) == 0 { + newTxDescs := tp.processOrphanTxs(tx) + acceptedTxDescs := make([]*TxDesc, len(newTxDescs)+1) + + acceptedTxDescs[0] = desc + copy(acceptedTxDescs[1:], newTxDescs) + + return acceptedTxDescs, nil + } + + if !allowOrphan { + return nil, fmt.Errorf("orphan transaction %v references "+ + "outputs of unknown or fully-spent "+ + "transaction %v", tx.Hash(), missingParents[0]) + } + + err = tp.maybeAddOrphanTx(tx, tag) + return nil, err +} + +// Count The number of accepted txs in the pool +func (tp *txPool) count() int { + tp.mutex.RLock() + count := len(tp.txDescs) + tp.mutex.RUnlock() + + return count +} + +// TxDescs The list of descs of accepted txs +func (tp *txPool) TxDescs() []*TxDesc { + tp.mutex.RLock() + txDescs := make([]*TxDesc, len(tp.txDescs)) + i := 0 + for _, desc := range tp.txDescs { + txDescs[i] = desc + i++ + } + tp.mutex.RUnlock() + + return txDescs +} + +// Txs returns the list of accepted txs +func (tp *txPool) Txs() []*blockchain.Tx { + tp.mutex.RLock() + tx := make([]*blockchain.Tx, len(tp.txDescs)) + i := 0 + for _, desc := range tp.txDescs { + tx[i] = desc.Tx + i++ + } + tp.mutex.RUnlock() + + return tx +} + +// RemoveTxInBlock removes the transaction in the block from pool +func (tp *txPool) RemoveTxInBlock(block *blockchain.Block) error { + tp.mutex.Lock() + for _, tx := range block.Tranxs { + tp.removeTx(tx, true) + } + tp.mutex.Unlock() + return nil +} + +// LastTimePoolUpdated The last unix time the pool get updated +func (tp *txPool) LastTimePoolUpdated() time.Time { + return time.Unix(atomic.LoadInt64(&tp.lastUpdatedUnixTime), 0) +} diff --git a/txpool/txpool_test.go b/txpool/txpool_test.go new file mode 100644 index 0000000000..9ff321c187 --- /dev/null +++ b/txpool/txpool_test.go @@ -0,0 +1,188 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txpool + +import ( + "encoding/hex" + "os" + "testing" + + "github.com/stretchr/testify/assert" + + . "github.com/iotexproject/iotex-core/blockchain" + "github.com/iotexproject/iotex-core/config" + "github.com/iotexproject/iotex-core/proto" + ta "github.com/iotexproject/iotex-core/test/testaddress" +) + +const ( + testingConfigPath = "../config.yaml" + testDBPath = "db.test" +) + +func decodeHash(in string) []byte { + hash, _ := hex.DecodeString(in) + return hash +} + +func TestTxPool(t *testing.T) { + defer os.Remove(testDBPath) + assert := assert.New(t) + + config, err := config.LoadConfigWithPathWithoutValidation(testingConfigPath) + assert.Nil(err) + config.Chain.ChainDBPath = testDBPath + // Disable block reward to make bookkeeping easier + config.Chain.BlockReward = 0 + + // Create a blockchain from scratch + // bc := CreateBlockchain(Addrinfo["miner"].Address, &config.Config{Chain: config.Chain{ChainDBPath: testDBPath}}) + bc := CreateBlockchain(ta.Addrinfo["miner"].Address, config) + //ctrl := gomock.NewController(t) + //defer ctrl.Finish() + + //bc := mock_blockchain.NewMockIBlockchain(ctrl) + if assert.NotNil(bc) { + t.Log("blockchain created") + } + defer bc.Close() + + tp := New(bc) + cbTx := NewCoinbaseTx(ta.Addrinfo["miner"].Address, 50, GenesisCoinbaseData) + if _, err := tp.ProcessTx(cbTx, true, false, 13245); assert.NotNil(err) { + t.Logf("Coinbase Tx cannot be processed") + } + /* + if true { + pool := bc.UtxoPool() + fmt.Println("utxoPool before tx1:", len(pool)) + for hash, _ := range pool { + fmt.Printf("hash:%x\n", hash) + } + payees := []*Payee{} + payees = append(payees, &Payee{Addrinfo["alfa"].Address, 10}) + payees = append(payees, &Payee{Addrinfo["bravo"].Address, 1}) + payees = append(payees, &Payee{Addrinfo["charlie"].Address, 1}) + payees = append(payees, &Payee{Addrinfo["delta"].Address, 1}) + payees = append(payees, &Payee{Addrinfo["echo"].Address, 1}) + payees = append(payees, &Payee{Addrinfo["foxtrot"].Address, 5}) + tx1 := bc.CreateTransaction(Addrinfo["miner"], 19, payees) + fmt.Printf("tx1: %x\n", tx1.Hash()) + fmt.Println("version:", tx1.Version) + fmt.Println("NumTxIn:", tx1.NumTxIn) + fmt.Println("TxIn:") + for idx, txIn := range tx1.TxIn { + hash := ZeroHash32B + copy(hash[:], txIn.TxHash) + fmt.Printf("tx1.TxIn.%d %x %d %x\n", idx, hash, txIn.UnlockScriptSize, txIn.UnlockScript) + } + fmt.Println("NumTxOut:", tx1.NumTxOut) + fmt.Println("TxOut:") + for idx, txOut := range tx1.TxOut { + fmt.Printf("tx1.TxOut.%d %d %x %d\n", idx, txOut.LockScriptSize, txOut.LockScript, txOut.Value) + } + blk := bc.MintNewBlock([]*Tx{tx1}, Addrinfo["miner"].Address, "") + fmt.Println(blk) + err := bc.AddBlockCommit(blk) + assert.Nil(err) + fmt.Println("Add 1st block") + bc.Reset() + payees = nil + payees = append(payees, &Payee{Addrinfo["bravo"].Address, 3}) + payees = append(payees, &Payee{Addrinfo["delta"].Address, 2}) + payees = append(payees, &Payee{Addrinfo["echo"].Address, 1}) + tx2 := bc.CreateTransaction(Addrinfo["alfa"], 6, payees) + fmt.Printf("tx2: %x\n", tx2.Hash()) + fmt.Println("tx2.TxIn:", tx2.NumTxIn) + for idx, txIn := range tx2.TxIn { + hash := ZeroHash32B + copy(hash[:], txIn.TxHash) + fmt.Printf("tx2.TxIn.%d %x %d %x\n", idx, hash, txIn.UnlockScriptSize, txIn.UnlockScript) + } + fmt.Println("TxOut:") + for idx, txOut := range tx2.TxOut { + fmt.Printf("tx2.TxOut.%d %d %x %d\n", idx, txOut.LockScriptSize, txOut.LockScript, txOut.Value) + } + fmt.Println(tx2.TxIn) + return + } //*/ + txIn1_0 := &iproto.TxInputPb{ + TxHash: decodeHash("9de6306b08158c423330f7a27243a1a5cbe39bfd764f07818437882d21241567"), + OutIndex: 0, + UnlockScriptSize: 98, + UnlockScript: decodeHash("40f9ea2b1357dde55519246a6ad82c466b9f2b988ff81a7c2fb114c932d44f322ba2edd178c2326739638b536e5f803977c24332b8f5b8ebc5f6683ff2bcaad90720b9b8d7316705dc4ff62bb323e610f3f5072abedc9834e999d6537f6681284ea2"), + } + txOut1_0 := NewTxOutput(10, 0) + txOut1_0.LockScriptSize = 25 + txOut1_0.LockScript = decodeHash("65b014a97ce8e76ade9b3181c63432a62330a5ca83ab9ba1b1") + txOut1_1 := NewTxOutput(1, 1) + txOut1_1.LockScriptSize = 25 + txOut1_1.LockScript = decodeHash("65b014af33097c8fd571c6c1efc52b0a802514ea0fbb03a1b1") + txOut1_2 := NewTxOutput(1, 2) + txOut1_2.LockScriptSize = 25 + txOut1_2.LockScript = decodeHash("65b0140fb02223c1a78c3f1fb81a1572e8b07adb700bffa1b1") + txOut1_3 := NewTxOutput(1, 3) + txOut1_3.LockScriptSize = 25 + txOut1_3.LockScript = decodeHash("65b01443251ba4fd765a2cfa65256aabd64f98c5c00e40a1b1") + txOut1_4 := NewTxOutput(1, 4) + txOut1_4.LockScriptSize = 25 + txOut1_4.LockScript = decodeHash("65b01430f1db72a44136e8634121b6730c2b8ef094f1c9a1b1") + txOut1_5 := NewTxOutput(5, 5) + txOut1_5.LockScriptSize = 25 + txOut1_5.LockScript = decodeHash("65b014d94ee6c7205e85c3d97c557f08faf8ac41102806a1b1") + txOut1_6 := NewTxOutput(9999999981, 6) + txOut1_6.LockScriptSize = 25 + txOut1_6.LockScript = decodeHash("65b014d4f743a24d5386f8d1c2a648da7015f08800cd11a1b1") + tx1 := &Tx{ + Version: 1, + NumTxIn: 1, + TxIn: []*TxInput{txIn1_0}, + NumTxOut: 7, + TxOut: []*TxOutput{txOut1_0, txOut1_1, txOut1_2, txOut1_3, txOut1_4, txOut1_5, txOut1_6}, + LockTime: 0, + } + + txIn2_0 := &iproto.TxInputPb{ + TxHash: decodeHash("aeedd06eb44f08abbcc72a2293aff580f13662fa59cc1b0aa4a15ee7c118e4eb"), + OutIndex: 0, + UnlockScriptSize: 98, + UnlockScript: decodeHash("40535e20b5c5075fa80d6bff220aea755737e3787bfbc7122c0c45015f6c249fbca28d069dc028fad01fda2766ea90411aad38ce9a9de7c59a30e4bebc80b940002009d8c6fc6f5cb0a03df112da90486fad7cdece1501aaab658551f8afbe7f59ee"), + } + txOut2_0 := NewTxOutput(3, 0) + txOut2_0.LockScriptSize = 25 + txOut2_0.LockScript = decodeHash("65b014af33097c8fd571c6c1efc52b0a802514ea0fbb03a1b1") + txOut2_1 := NewTxOutput(2, 1) + txOut2_1.LockScriptSize = 25 + txOut2_1.LockScript = decodeHash("65b01443251ba4fd765a2cfa65256aabd64f98c5c00e40a1b1") + txOut2_2 := NewTxOutput(1, 2) + txOut2_2.LockScriptSize = 25 + txOut2_2.LockScript = decodeHash("65b01430f1db72a44136e8634121b6730c2b8ef094f1c9a1b1") + txOut2_3 := NewTxOutput(4, 2) + txOut2_3.LockScriptSize = 25 + txOut2_3.LockScript = decodeHash("65b014a97ce8e76ade9b3181c63432a62330a5ca83ab9ba1b1") + tx2 := &Tx{ + Version: 1, + NumTxIn: 1, + TxIn: []*TxInput{txIn2_0}, + NumTxOut: 4, + TxOut: []*TxOutput{txOut2_0, txOut2_1, txOut2_2, txOut2_3}, + LockTime: 0, + } + + t.Logf("tx1 hash: %x", tx1.Hash()) + t.Logf("tx2 hash: %x", tx2.Hash()) + descs, err := tp.ProcessTx(tx2, true, false, 12341234) + assert.Nil(err) + assert.Equal(0, len(descs)) + descs, err = tp.ProcessTx(tx1, true, false, 12341234) + t.Log(tp.TxDescs()) + for hash, desc := range tp.TxDescs() { + t.Logf("hash: %x desc: %v", hash, desc) + } + assert.Nil(err) + assert.Equal(2, len(tp.TxDescs())) +} diff --git a/txvm/ast.go b/txvm/ast.go new file mode 100755 index 0000000000..7c74c0f62e --- /dev/null +++ b/txvm/ast.go @@ -0,0 +1,35 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +// OpNode defines the struct of operation node +type OpNode struct { + opcode byte + data []byte + asts []IAST +} + +// IAST is a Abstract Syntax Tree. +type IAST struct { + nodes []OpNode +} + +// ParseRaw builds an AST from byte codes. +func ParseRaw(bytecodes []byte) (*IAST, error) { + ast := IAST{} + start := 0 + for start < len(bytecodes) { + newNode, offset, err := BuildOpNodeFromBytes(bytecodes[start:]) + if err != nil { + return nil, err + } + + ast.nodes = append(ast.nodes, *newNode) + start += offset + } + return &ast, nil +} diff --git a/txvm/ast_test.go b/txvm/ast_test.go new file mode 100755 index 0000000000..8d8295b371 --- /dev/null +++ b/txvm/ast_test.go @@ -0,0 +1,40 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestParseRaw(t *testing.T) { + t.Parallel() + + // Test 1: a noop script + builder := NewScriptBuilder() + assert.Nil(t, builder.AddOp(Op0)) + assert.Nil(t, builder.AddOp(OpData2)) + assert.Nil(t, builder.AddData([]byte{0x12, 0x34})) + assert.Nil(t, builder.AddOp(OpNope)) + + assert.NotNil(t, builder.Bytecodes()) + ast, err := ParseRaw(builder.Bytecodes()) + if err != nil { + t.Errorf("Get an error: %s", err.Error()) + } + if len(ast.nodes) != 3 || ast.nodes[0].opcode != Op0 || len(ast.nodes[1].data) != 2 || ast.nodes[2].opcode != OpNope { + t.Errorf("Parsed OpNode is wrong!") + } + + // Test 2: Pay2AddrHash script + + // Test 3: script with branch + + // Test 4: script with errors + +} diff --git a/txvm/error.go b/txvm/error.go new file mode 100755 index 0000000000..2a36d49293 --- /dev/null +++ b/txvm/error.go @@ -0,0 +1,44 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +// ErrorCode identifies a kind of txvm error. +type ErrorCode int + +// These constants are used to identify a specific Error. +const ( + // ErrInternal is returned if internal consistency checks fail. + ErrInternal ErrorCode = iota + + // Errors emitted by builder + // ErrUnsupportedOpcode ... + ErrUnsupportedOpcode + // ErrInvalidOpcode ... + ErrInvalidOpcode + // ErrInvalidOpdata ... + ErrInvalidOpdata + + // Errors emitted by IVM + // ErrEqualVerify... + ErrEqualVerify + // ErrInvalidStackOperation ... + ErrInvalidStackOperation +) + +// ScriptError defines the struct of script error +type ScriptError struct { + ErrorCode ErrorCode + Desc string +} + +func (e ScriptError) Error() string { + return e.Desc +} + +func scriptError(c ErrorCode, desc string) ScriptError { + return ScriptError{c, desc} +} diff --git a/txvm/error_test.go b/txvm/error_test.go new file mode 100755 index 0000000000..75e15b2eca --- /dev/null +++ b/txvm/error_test.go @@ -0,0 +1,30 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestScriptError(t *testing.T) { + t.Parallel() + + tests := []struct { + in ScriptError + want string + }{ + {scriptError(ErrUnsupportedOpcode, "It is a mistake"), "It is a mistake"}, + {scriptError(ErrUnsupportedOpcode, "readable"), "readable"}, + } + + t.Logf("Running %d tests for scriptError()", len(tests)) + for _, test := range tests { + assert.Equal(t, test.want, test.in.Error()) + } +} diff --git a/txvm/opcode.go b/txvm/opcode.go new file mode 100755 index 0000000000..3d0d89b6f4 --- /dev/null +++ b/txvm/opcode.go @@ -0,0 +1,278 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +import ( + "bytes" + "fmt" + + "golang.org/x/crypto/blake2b" + + cp "github.com/iotexproject/iotex-core/crypto" +) + +// ConstructFunc takes an array of bytecodes to build an opnode. Note that: +// 1. The bytecodes must start with a valid opcode byte. +// 2. The bytecodes must be longer than required. +// 3. If it is insufficient to constructing an OpNode, e.g., data length not met, OpEndIf not found, error is thrown. +// 4. If an OpNode is successfully created, an int offset tells how many bytes it consumes. +type ConstructFunc func([]byte) (*OpNode, int, error) + +// RunFunc declares a function type +type RunFunc func(*OpNode, *IVM) error + +// BuildOpNodeFromBytes builds operation node from bytes. It is a specific implementation of ConstructFunc +func BuildOpNodeFromBytes(bytecodes []byte) (*OpNode, int, error) { + if len(bytecodes) == 0 { + return nil, 0, scriptError(ErrInvalidOpcode, "Empty bytecodes") + } + opcode := bytecodes[0] + opname := opinfoArray[opcode].name + if opname == "" { + return nil, 0, scriptError(ErrInvalidOpcode, "Opcode not found") + } + + constructor := opinfoArray[opcode].constructor + return constructor(bytecodes) +} + +// RunOpNode runs operation node. It is a specific implementation of RunFunc +func RunOpNode(node *OpNode, vm *IVM) error { + runfunc := opinfoArray[node.opcode].runfunc + opname := opinfoArray[node.opcode].name + if opname == "" { + return scriptError(ErrInvalidOpcode, "Opcode not found") + } + return runfunc(node, vm) +} + +// Enumerate of opcodes +const ( + Op0 = iota + OpData1 + OpData2 + OpData3 + OpData20 = 0x14 + OpData32 = 0x20 + OpData64 = 0x40 +) + +// Enumerate of opcodes +const ( + OpIf = iota + 0x61 + OpElse + OpEndIf + OpNope + OpDup +) + +// Verify +const ( + OpVerify = iota + 0xa0 + OpEqualVerify + OpScriptHashVerifyPop +) + +// Hash +const ( + OpHash160 = iota + 0xb0 + OpCheckSig +) + +// External Call +const ( + OpCheckLockTime = iota + 0xc0 + OpOtherSPVVerify +) + +// Enumerate unused opcodes +const ( + OpUnused = iota + 0xd0 +) + +type opinfo struct { + name string + constructor ConstructFunc + runfunc RunFunc +} + +// ************************************ +// Mapping from opcode to its ConstructFunc +// ************************************ +var opinfoArray [256]opinfo + +func opConstructDefault(bytecodes []byte) (*OpNode, int, error) { + opcode := bytecodes[0] + node := OpNode{} + node.opcode = opcode + return &node, 1, nil +} + +func opConstructData(bytecodes []byte) (*OpNode, int, error) { + datalen := int(bytecodes[0]) + opcode := bytecodes[0] + if len(bytecodes) <= datalen { + return nil, 0, scriptError(ErrInvalidOpcode, + fmt.Sprintf("bytecodes not long enough."+ + "Expect > %d, get %d", datalen, len(bytecodes))) + } + node := OpNode{} + node.opcode = opcode + node.data = bytecodes[1 : datalen+1] + return &node, datalen + 1, nil +} + +func opConstructBranch(bytecodes []byte) (*OpNode, int, error) { + node := OpNode{opcode: bytecodes[0]} + offset := 1 + branchIdx := 0 // IF part is indexed 0, ELSE part(if any) is indexed 1 + node.asts = append(node.asts, IAST{}) + + for offset < len(bytecodes) && bytecodes[offset] != OpEndIf { + if bytecodes[offset] == OpElse { + branchIdx = 1 + node.asts = append(node.asts, IAST{}) + offset++ + continue + } + newNode, newOffset, err := BuildOpNodeFromBytes(bytecodes[offset:]) + if err != nil { + return nil, 0, err + } + node.asts[branchIdx].nodes = append(node.asts[branchIdx].nodes, *newNode) + offset += newOffset + } + + if offset >= len(bytecodes) { + return nil, 0, scriptError(ErrInvalidOpcode, "Unbalanced conditional branch. No OpEndIf found") + } + return &node, offset + 1, nil + +} + +func opConstructError(bytecodes []byte) (*OpNode, int, error) { + return nil, 0, scriptError(ErrInvalidOpcode, + fmt.Sprintf("This opcode %x is not supposed to construct an OpNode", bytecodes[0])) +} + +// ************************************ +// Mapping from opcode to its RunFunc +// ************************************ +func opcodePushTrue(node *OpNode, vm *IVM) error { + vm.dstack = append(vm.dstack, []byte{0x01}) + return nil +} + +func opcodePushFalse(node *OpNode, vm *IVM) error { + vm.dstack = append(vm.dstack, []byte{0x00}) + return nil +} + +func opcodePushData(node *OpNode, vm *IVM) error { + vm.dstack = append(vm.dstack, node.data) + return nil +} + +func opcodeDup(node *OpNode, vm *IVM) error { + s := vm.dstack + if len(s) == 0 { + return scriptError(ErrInvalidStackOperation, "empty stack, cannot dup") + } + + vm.dstack = append(s, s[len(s)-1]) // pop + return nil +} + +func opcodeHash160(node *OpNode, vm *IVM) error { + s := vm.dstack + if len(s) == 0 { + return scriptError(ErrInvalidStackOperation, "empty stack, cannot hash160") + } + + // The public key is assumed to be 32 bytes. + m := s[len(s)-1] + news := s[:len(s)-1] // pop + hash := blake2b.Sum256(m) + news = append(news, hash[7:27]) + return nil +} + +func opcodeEqual(node *OpNode, vm *IVM) error { + if len(vm.dstack) < 2 { + return scriptError(ErrInvalidStackOperation, "stack has too few entries, cannot Equal") + } + + a := vm.dstack[len(vm.dstack)-1] + b := vm.dstack[len(vm.dstack)-2] + vm.dstack = vm.dstack[:len(vm.dstack)-2] // pop + if bytes.Equal(a, b) { + return opcodePushTrue(node, vm) + } + return opcodePushFalse(node, vm) +} + +func opcodeEqualVerify(node *OpNode, vm *IVM) error { + err := opcodeEqual(node, vm) + if err != nil { + return err + } + + verified := vm.dstack[len(vm.dstack)-1] + vm.dstack = vm.dstack[:len(vm.dstack)-1] // pop + if !bytes.Equal(verified, []byte{0x01}) { + return scriptError(ErrEqualVerify, "invalid signature") + } + return nil +} + +func opcodeCheckSig(node *OpNode, vm *IVM) error { + if len(vm.dstack) < 2 { + return scriptError(ErrInvalidStackOperation, "stack has too few entries, cannot Equal") + } + + pubkey := vm.dstack[len(vm.dstack)-1] + sig := vm.dstack[len(vm.dstack)-2] + vm.dstack = vm.dstack[:len(vm.dstack)-2] // pop + + hash := blake2b.Sum256(vm.txin) + if cp.Verify(pubkey, hash[:], sig) { + return opcodePushTrue(node, vm) + } + return opcodePushFalse(node, vm) +} + +func opcodeRunBranch(node *OpNode, vm *IVM) error { + return scriptError(ErrUnsupportedOpcode, "Unimplemented") +} + +func opcodeRunNothing(node *OpNode, vm *IVM) error { + return nil +} + +func opcodeRunError(node *OpNode, vm *IVM) error { + return scriptError(ErrInvalidOpcode, + fmt.Sprintf("This opcode %x is not supposed to run", node.opcode)) +} + +func init() { + opinfoArray[Op0] = opinfo{"Op0", opConstructDefault, opcodePushFalse} + opinfoArray[OpData1] = opinfo{"OpData1", opConstructData, opcodePushData} + opinfoArray[OpData2] = opinfo{"OpData2", opConstructData, opcodePushData} + opinfoArray[OpData3] = opinfo{"OpData2", opConstructData, opcodePushData} + opinfoArray[OpData20] = opinfo{"OpData20", opConstructData, opcodePushData} + opinfoArray[OpData32] = opinfo{"OpData32", opConstructData, opcodePushData} + opinfoArray[OpData64] = opinfo{"OpData64", opConstructData, opcodePushData} + + opinfoArray[OpIf] = opinfo{"OpIf", opConstructBranch, opcodeRunBranch} + opinfoArray[OpElse] = opinfo{"OpElse", opConstructError, opcodeRunError} + opinfoArray[OpNope] = opinfo{"OpNope", opConstructDefault, opcodeRunNothing} + + opinfoArray[OpDup] = opinfo{"OpDup", opConstructDefault, opcodeDup} + opinfoArray[OpHash160] = opinfo{"OpHash160", opConstructDefault, opcodeHash160} + opinfoArray[OpEqualVerify] = opinfo{"OpEqualVerify", opConstructDefault, opcodeEqualVerify} + opinfoArray[OpCheckSig] = opinfo{"OpCheckSig", opConstructDefault, opcodeCheckSig} +} diff --git a/txvm/opcode_test.go b/txvm/opcode_test.go new file mode 100755 index 0000000000..e5f32f4b27 --- /dev/null +++ b/txvm/opcode_test.go @@ -0,0 +1,77 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +import ( + "testing" +) + +func TestBuildOpNodeFromBytes(t *testing.T) { + t.Parallel() + + var testBytecodes []byte + var node *OpNode + var offset int + var err error + + // Test 1 + t.Logf("HandleTransition first BuildOpNodeFromBytes test") + testBytecodes = []byte{Op0, OpData1} + node, offset, err = BuildOpNodeFromBytes(testBytecodes) + if err != nil { + t.Errorf("Meet an error! %s", err.Error()) + } + if offset != 1 { + t.Errorf("Offset is wrong. Expect 1, get %d", offset) + } + if node.opcode != Op0 { + t.Errorf("Opcode is wrong. Expect %x, get %x", Op0, node.opcode) + } + + // Test 2: Opnode not found error + t.Logf("HandleTransition 2nd BuildOpNodeFromBytes test") + testBytecodes = []byte{OpUnused, Op0} + node, offset, err = BuildOpNodeFromBytes(testBytecodes) + if err == nil { + t.Errorf("Should get an error!") + } + + // Test 3: PushData opcode test + t.Logf("HandleTransition 3rd BuildOpNodeFromBytes test") + testBytecodes = []byte{OpData2, 0x14, 0xa0, 0xf3} + node, offset, err = BuildOpNodeFromBytes(testBytecodes) + if err != nil { + t.Errorf("Meet an error! %s", err.Error()) + } + if offset != 3 { + t.Errorf("Offset is wrong. Expect 3, get %d", offset) + } + if len(node.data) != 2 || node.data[0] != 0x14 || node.data[1] != 0xa0 { + t.Errorf("OpNode data is wrong") + } + + // Test 4: Branch opcode test + t.Logf("HandleTransition 4th BuildOpNodeFromBytes test") + testBytecodes = []byte{OpIf, OpData2, 0xa0, 0xf3, Op0, OpElse, Op0, OpIf, OpData1, 0x00, Op0, OpEndIf, OpEndIf} + node, offset, err = BuildOpNodeFromBytes(testBytecodes) + if err != nil { + t.Errorf("Meet an error ! %s", err.Error()) + } + if offset != len(testBytecodes) { + t.Errorf("Offset is wrong. Expect %d, get %d", len(testBytecodes), offset) + } + if len(node.asts) != 2 || len(node.asts[0].nodes) != 2 || len(node.asts[1].nodes[1].asts) != 1 { + t.Errorf("Get a wrong branch node") + } + + testBytecodes = []byte{OpIf, OpData2, 0xa0, 0xf3, Op0, OpElse, Op0, OpIf, OpData1, 0x00, Op0, OpEndIf} + node, offset, err = BuildOpNodeFromBytes(testBytecodes) + if err == nil || err.Error() != "Unbalanced conditional branch. No OpEndIf found" { + t.Errorf("Expect an unbalanced conditional branch error") + } + +} diff --git a/txvm/scriptbuilder.go b/txvm/scriptbuilder.go new file mode 100755 index 0000000000..8bf8140b28 --- /dev/null +++ b/txvm/scriptbuilder.go @@ -0,0 +1,79 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +const ( + // maxScriptSize is the maximum allowed length of a raw script. + maxScriptSize = 10000 +) + +// ScriptBuilder builds a script using opcodes +type ScriptBuilder interface { + // AddOp adds one opcode + AddOp(op byte) error + + // AddOps adds several opcodes + AddOps(op []byte) error + + // AddData adds data + AddData(data []byte) error + + // Bytecodes returns byte codes built + Bytecodes() []byte + + // Reset resets all internal states of the builder. + Reset() error +} + +// scriptBuilder implements ScriptBuilder infercace +type scriptBuilder struct { + bytecodes []byte +} + +// NewScriptBuilder creates an ScriptBuilder +func NewScriptBuilder() ScriptBuilder { + return &scriptBuilder{} +} + +// AddOp adds an opcode +func (b *scriptBuilder) AddOp(opcode byte) error { + if len(b.bytecodes)+1 > maxScriptSize { + return scriptError(ErrInvalidOpcode, "opcode cannot be added as script size reaches the maximum") + } + b.bytecodes = append(b.bytecodes, opcode) + return nil +} + +// AddOps adds several opcodes +func (b *scriptBuilder) AddOps(opcodes []byte) error { + if len(b.bytecodes)+len(opcodes) > maxScriptSize { + return scriptError(ErrInvalidOpcode, "opcode cannot be added as script size reaches the maximum") + } + b.bytecodes = append(b.bytecodes, opcodes...) + return nil +} + +// AddData adds data +func (b *scriptBuilder) AddData(data []byte) error { + if len(data) == 0 { + return scriptError(ErrInvalidOpdata, "invalid data for scriptBuilder") + } + // TODO: check agasint maxScriptSize + b.bytecodes = append(b.bytecodes, data...) + return nil +} + +// Bytecodes returns the byte codes the builder has so far +func (b *scriptBuilder) Bytecodes() []byte { + return b.bytecodes +} + +// Reset resets all internal states of the builder. +func (b *scriptBuilder) Reset() error { + b.bytecodes = b.bytecodes[0:0] + return nil +} diff --git a/txvm/scripts.go b/txvm/scripts.go new file mode 100644 index 0000000000..716f18689e --- /dev/null +++ b/txvm/scripts.go @@ -0,0 +1,77 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +import ( + "bytes" + "fmt" + + "golang.org/x/crypto/blake2b" + + cp "github.com/iotexproject/iotex-core/crypto" + "github.com/iotexproject/iotex-core/iotxaddress" +) + +// printScript formats a disassembled script for one line printing. +func printScript(bytecodes []byte) (string, error) { + var disbuf bytes.Buffer + for _, bytecode := range bytecodes { + // TODO: handle opcode that is unimplemented + disbuf.WriteString(opinfoArray[bytecode].name) + disbuf.WriteByte(' ') + } + + if disbuf.Len() > 0 { + disbuf.Truncate(disbuf.Len() - 1) + } + return disbuf.String(), nil +} + +// PayToAddrScript creates a new script to pay a transaction output to a given address. +func PayToAddrScript(addr string) ([]byte, error) { + b := NewScriptBuilder() + if err := b.AddOps([]byte{OpDup, OpHash160}); err != nil { + return nil, err + } + err := b.AddOp(OpData20) + if err != nil { + return nil, fmt.Errorf("cannot add data: %v", err) + } + if err := b.AddData(iotxaddress.GetPubkeyHash(addr)); err != nil { + return nil, err + } + if err := b.AddOps([]byte{OpEqualVerify, OpCheckSig}); err != nil { + return nil, err + } + return b.Bytecodes(), nil +} + +// SignatureScript creates an input signature script for a transaction. +func SignatureScript(txin []byte, pubkey []byte, privkey []byte) ([]byte, error) { + b := NewScriptBuilder() + err := b.AddOp(OpData64) + if err != nil { + return nil, fmt.Errorf("cannot add data: %v", err) + } + hash := blake2b.Sum256(txin) + sig := cp.Sign(privkey, hash[:]) + err = b.AddData(sig) + if err != nil { + return nil, fmt.Errorf("cannot add data: %v", err) + } + + err = b.AddOp(OpData32) + if err != nil { + return nil, fmt.Errorf("cannot add data: %v", err) + } + err = b.AddData(pubkey) + if err != nil { + return nil, fmt.Errorf("cannot add data: %v", err) + } + + return b.Bytecodes(), nil +} diff --git a/txvm/scripts_test.go b/txvm/scripts_test.go new file mode 100644 index 0000000000..0a574af316 --- /dev/null +++ b/txvm/scripts_test.go @@ -0,0 +1,59 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +import ( + "strings" + "testing" + + "github.com/stretchr/testify/assert" + + "github.com/iotexproject/iotex-core/iotxaddress" +) + +func TestPayToAddrScriptTrue(t *testing.T) { + addr, err := iotxaddress.NewAddress(true, 0x01, []byte{0xa4, 0x00, 0x00, 0x00}) + assert.Nil(t, err) + + locks, err := PayToAddrScript(addr.Address) + assert.Nil(t, err) + + t.Log(locks) + + parsed, err := printScript(locks) + assert.Nil(t, err) + + assert.True(t, strings.HasPrefix(parsed, "OpDup OpHash160")) + assert.True(t, strings.HasSuffix(parsed, "OpEqualVerify OpCheckSig")) + + txin := []byte{0x11, 0x22, 0x33, 0x44} + unlocks, err := SignatureScript(txin, addr.PublicKey, addr.PrivateKey) + assert.Nil(t, err) + + t.Log(append(unlocks, locks...)) + + v, err := NewIVM(txin, append(unlocks, locks...)) + assert.Nil(t, err) + + err = v.Execute() + assert.Nil(t, err) +} + +func TestPayToAddrScriptFalse(t *testing.T) { + addr, _ := iotxaddress.NewAddress(true, 0x01, []byte{0xa4, 0x00, 0x00, 0x00}) + locks, _ := PayToAddrScript(addr.Address) + + newAddr, _ := iotxaddress.NewAddress(true, 0x01, []byte{0xa4, 0x00, 0x00, 0x00}) + txin := []byte{0x11, 0x22, 0x33, 0x44} + unlocks, _ := SignatureScript(txin, newAddr.PublicKey, newAddr.PrivateKey) + + v, err := NewIVM(txin, append(unlocks, locks...)) + assert.Nil(t, err) + + err = v.Execute() + assert.Equal(t, "invalid signature", err.Error()) +} diff --git a/txvm/vm.go b/txvm/vm.go new file mode 100755 index 0000000000..4cab3f6369 --- /dev/null +++ b/txvm/vm.go @@ -0,0 +1,36 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +// IVM defines the struct of IoTeX Virtual Machine +type IVM struct { + ast *IAST + contextStack []*IAST + dstack [][]byte + txin []byte +} + +// Execute executes IoTeX Virtual Machine +func (vm *IVM) Execute() (err error) { + // TODO: evaluate AST recursively + for _, node := range vm.ast.nodes { + if err := opinfoArray[node.opcode].runfunc(&node, vm); err != nil { + return err + } + } + return nil +} + +// NewIVM creates a new IoTeX Virtual Machine +func NewIVM(txin, bytecodes []byte) (*IVM, error) { + ast, err := ParseRaw(bytecodes) + if err != nil { + return nil, err + } + vm := IVM{ast: ast, txin: txin} + return &vm, nil +} diff --git a/txvm/vm_test.go b/txvm/vm_test.go new file mode 100755 index 0000000000..9ec98e510f --- /dev/null +++ b/txvm/vm_test.go @@ -0,0 +1,33 @@ +// Copyright (c) 2018 IoTeX +// This is an alpha (internal) release and is not suitable for production. This source code is provided ‘as is’ and no +// warranties are given as to title or non-infringement, merchantability or fitness for purpose and, to the extent +// permitted by law, all liability for your use of the code is disclaimed. This source code is governed by Apache +// License 2.0 that can be found in the LICENSE file. + +package txvm + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestNewIVM(t *testing.T) { + t.Parallel() + + builder := NewScriptBuilder() + assert.Nil(t, builder.AddOp(Op0)) + assert.Nil(t, builder.AddOp(OpData3)) + assert.Nil(t, builder.AddData([]byte{0x12, 0x34, 0x56})) + assert.Nil(t, builder.AddOp(OpNope)) + + bytecodes := builder.Bytecodes() + assert.NotNil(t, bytecodes) + + vm, err := NewIVM([]byte{}, bytecodes) + assert.Nil(t, err) + assert.Equal(t, 3, len(vm.ast.nodes)) + + err = vm.Execute() + assert.Nil(t, err) +}