├── .github └── workflows │ └── release.yml ├── .gitignore ├── COPYING ├── COPYING.LESSER ├── Deploy.md ├── Docker.md ├── Dockerfile ├── Installation.md ├── LICENSE ├── Makefile ├── Readme.md ├── TIPs ├── Readme.md ├── TIP-1.md └── TIP-2.md ├── cmd └── tbb │ ├── balances.go │ ├── main.go │ ├── run.go │ ├── version.go │ └── wallet.go ├── database ├── block.go ├── database.go ├── fs.go ├── genesis.go ├── state.go ├── state.json └── tx.go ├── docker-compose.yaml ├── fs └── fs.go ├── go.mod ├── go.sum ├── node ├── block_explorer_integration_test.go ├── http_req_res_utils.go ├── http_routes.go ├── mempool_view_integration_test.go ├── miner.go ├── miner_test.go ├── node.go ├── node_integration_test.go ├── sync.go ├── test_andrej--3eb92807f1f91a8d4d85bc908c7f86dcddb1df57 ├── test_babayaga--6fdc0d8d15ae6b4ebf45c52fd2aafbcbb19a65c8 └── test_block_explorer_db │ ├── database │ ├── block.db │ └── genesis.json │ └── keystore │ └── UTC--2021-12-26T20-49-55.107305000Z--da8206ac3495db5077b5ee340bd4cee6ce8d7e4f ├── public └── img │ ├── andrej_babayaga_caesar_sync_p2p.png │ ├── andrej_babayaga_crypto_sign_summary.png │ ├── book_cover2.png │ ├── mining_p2p.png │ └── test_ethereum_signature.png └── wallet ├── wallet.go └── wallet_test.go /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release TBB binaries 2 | 3 | on: 4 | release: 5 | types: [created] 6 | 7 | jobs: 8 | releases-matrix: 9 | name: Release Go Binary 10 | runs-on: ubuntu-latest 11 | strategy: 12 | matrix: 13 | # build and publish in parallel: linux/amd64 14 | goos: [linux] 15 | goarch: [amd64] 16 | steps: 17 | - uses: actions/checkout@v2 18 | - name: Run tests 19 | run: go test -v -p=1 -timeout=0 ./... 20 | - uses: wangyoucao577/go-release-action@v1.16 21 | with: 22 | github_token: ${{ secrets.GITHUB_TOKEN }} 23 | goos: ${{ matrix.goos }} 24 | goarch: ${{ matrix.goarch }} 25 | project_path: "./cmd/tbb" 26 | binary_name: "tbb" 27 | ldflags: "-s -w" 28 | extra_files: LICENSE Readme.md 29 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode -------------------------------------------------------------------------------- /COPYING.LESSER: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. -------------------------------------------------------------------------------- /Deploy.md: -------------------------------------------------------------------------------- 1 | # Deployment 2 | 3 | ## Deploy to the official TBB server 4 | ``` 5 | ssh tbb 6 | sudo supervisorctl stop tbb 7 | sudo rm /usr/local/bin/tbb 8 | sudo rm /home/ec2-user/tbb 9 | cd /home/ec2-user/go/src/github.com/web3coach/the-blockchain-bar 10 | git checkout 11 | git pull 12 | make install 13 | sudo ln -s $GOPATH/bin/tbb /usr/local/bin/tbb 14 | which tbb 15 | tbb version 16 | sudo supervisorctl start tbb 17 | ``` -------------------------------------------------------------------------------- /Docker.md: -------------------------------------------------------------------------------- 1 | ## Docker 2 | 3 | [Docker](https://www.docker.com) is a tool that enables packaging software as a [container image](https://www.docker.com/resources/what-container) that includes all of the dependencies to run a given application. In the context of this project, Docker enables building and running the `tbb` application across multiple operating systems. 4 | 5 | ### Installation 6 | 7 | To get started, follow the official [Getting Started](https://docs.docker.com/get-docker/) guide to install Docker for your operating system. If you are using Linux, be sure to also follow [the guide for installing docker-compose](https://docs.docker.com/compose/install/#install-compose-on-linux-systems). This extra step is not necessary on Windows and MacOS. 8 | 9 | Once all tools are installed, you will have two new executables that you can run in your terminal: 10 | 1. `docker` - This is the application used for building and running containers 11 | 2. `docker-compose` - [Docker Compose](https://docs.docker.com/compose/) is a tool for defining how to run any number of containers 12 | 13 | You can check that both of these applications are installed by running the following commands in your terminal: 14 | 15 | ``` 16 | $ which docker 17 | /usr/local/bin/docker 18 | 19 | $ which docker-compose 20 | /usr/local/bin/docker-compose 21 | ``` 22 | 23 | This uses the UNIX [which](https://www.tutorialspoint.com/unix_commands/which.htm) command to print the absolute path to the executable in question. As long as a path is printed for both `docker` and `docker-compose` then you are good to go - the paths do not need to be the same as those shown above. 24 | 25 | ### Building 26 | 27 | You can build the Docker image for the `tbb` application by running `make image` in the root of this repository. This will take a minute or two to build and the output in your terminal will look something like this: 28 | 29 | ``` 30 | $ make image 31 | docker build -t tbb:latest . 32 | [+] Building 36.0s (9/9) FINISHED 33 | => [internal] load build definition from Dockerfile 0.0s 34 | => => transferring dockerfile: 137B 0.0s 35 | => [internal] load .dockerignore 0.0s 36 | => => transferring context: 2B 0.0s 37 | => [internal] load metadata for docker.io/library/golang:1.16 0.4s 38 | => [internal] load build context 0.2s 39 | => => transferring context: 6.78MB 0.2s 40 | => [1/4] FROM docker.io/library/golang:1.16@sha256:8f29258b4b992b383d03290acde77 0.0s 41 | => CACHED [2/4] WORKDIR /build 0.0s 42 | => [3/4] ADD ./ /build 0.1s 43 | => [4/4] RUN make install 33.0s 44 | => exporting to image 2.3s 45 | => => exporting layers 2.3s 46 | => => writing image sha256:b088ed645ed132544fe8257284a5ed03961da0801016a5544a5a6 0.0s 47 | => => naming to docker.io/library/tbb:latest 0.0s 48 | 49 | Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them 50 | ``` 51 | 52 | You can verify that the image was built successfully by running the command `docker images` in your terminal: 53 | 54 | ``` 55 | $ docker images 56 | REPOSITORY TAG IMAGE ID CREATED SIZE 57 | tbb latest b088ed645ed1 15 seconds ago 1.12GB 58 | ``` 59 | 60 | Now that you have the image available, you can run it like so: 61 | 62 | ``` 63 | $ docker run -ti tbb 64 | The Blockchain Bar CLI 65 | 66 | Usage: 67 | tbb [flags] 68 | tbb [command] 69 | 70 | Available Commands: 71 | balances Interacts with balances (list...). 72 | help Help about any command 73 | run Launches the TBB node and its HTTP API. 74 | version Describes version. 75 | wallet Manages blockchain accounts and keys. 76 | 77 | Flags: 78 | -h, --help help for tbb 79 | 80 | Use "tbb [command] --help" for more information about a command. 81 | ``` 82 | 83 | ### Docker for Local Development 84 | 85 | Docker makes it much easier to package and distribute a runnable image of your application. However, Docker can be used for local development as well. This is useful in many situations, such as if your Operating System or CPU architecture is not supported by the libraries used by `tbb`. In this case, [Docker Compose](https://docs.docker.com/compose/) can be used to give you a running [shell](https://en.wikipedia.org/wiki/Shell_(computing)) within a `tbb` container. You can even mount this repository within the container so that the changes you make to the code can be immediately run within the container. 86 | 87 | To try this out, run the command `make local` in the root of this repository: 88 | 89 | ``` 90 | $ make local 91 | docker-compose build 92 | Building dev 93 | [+] Building 36.7s (9/9) FINISHED 94 | => [internal] load build definition from Dockerfile 0.0s 95 | => => transferring dockerfile: 137B 0.0s 96 | => [internal] load .dockerignore 0.0s 97 | => => transferring context: 2B 0.0s 98 | => [internal] load metadata for docker.io/library/golang:1.16 1.1s 99 | => [internal] load build context 0.0s 100 | => => transferring context: 9.17kB 0.0s 101 | => [1/4] FROM docker.io/library/golang:1.16@sha256:8f29258b4b992b383d03290acde77c 0.0s 102 | => CACHED [2/4] WORKDIR /build 0.0s 103 | => [3/4] ADD ./ /build 0.1s 104 | => [4/4] RUN make install 33.1s 105 | => exporting to image 2.3s 106 | => => exporting layers 2.3s 107 | => => writing image sha256:f98072939f4707f5259495dc85dc2510febc8282372b3c2e2246fe 0.0s 108 | => => naming to docker.io/library/the-blockchain-bar_dev 0.0s 109 | 110 | Use 'docker scan' to run Snyk tests against images to find vulnerabilities and learn how to fix them 111 | docker-compose run \ 112 | --rm -v "/Users/daniel/src/the-blockchain-bar":/build:consistent dev bash 113 | Creating the-blockchain-bar_dev_run ... done 114 | 115 | root@693ca11edb0f:/build# 116 | ``` 117 | 118 | Notice that when this command is completed, that your shell prompt looks slightly different: 119 | 120 | ``` 121 | root@693ca11edb0f:/build# 122 | ``` 123 | 124 | This is because you now have a shell _within_ the container. Let's try building `tbb` and seeing which version is running: 125 | 126 | ``` 127 | root@693ca11edb0f:/build# make install 128 | go install -ldflags "-X main.GitCommit=c912bcf8a465bf81848b7789995a7f8e7261024c" ./... 129 | 130 | root@693ca11edb0f:/build# tbb version 131 | Version: 1.9.2-alpha c912bc TX Gas 132 | ``` 133 | 134 | That's pretty cool. Let's see how we can make a change to the code and see that change reflected inside the container. 135 | 136 | First, open [cmd/tbb/version.go](./cmd/tbb/version.go) in your text editor of choice and change the following block: 137 | 138 | ``` 139 | var versionCmd = &cobra.Command{ 140 | Use: "version", 141 | Short: "Describes version.", 142 | Run: func(cmd *cobra.Command, args []string) { 143 | fmt.Println(fmt.Sprintf("Version: %s.%s.%s-alpha %s %s", Major, Minor, Fix, shortGitCommit(GitCommit), Verbal)) 144 | }, 145 | } 146 | ``` 147 | 148 | to 149 | 150 | ``` 151 | var versionCmd = &cobra.Command{ 152 | Use: "version", 153 | Short: "Describes version.", 154 | Run: func(cmd *cobra.Command, args []string) { 155 | fmt.Println(fmt.Sprintf("My Cool Version: %s.%s.%s-alpha %s %s", Major, Minor, Fix, shortGitCommit(GitCommit), Verbal)) 156 | }, 157 | } 158 | 159 | ``` 160 | 161 | Now let's try building `tbb` again and checking the current version: 162 | 163 | ``` 164 | root@693ca11edb0f:/build# make install 165 | go install -ldflags "-X main.GitCommit=c912bcf8a465bf81848b7789995a7f8e7261024c" ./... 166 | root@693ca11edb0f:/build# tbb version 167 | My Cool Version: 1.9.2-alpha c912bc TX Gas 168 | ``` 169 | 170 | Awesome! You can now write code using your preferred tooling and immediately see your changes reflected when you build and run the application within the container. 171 | 172 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.16 2 | 3 | WORKDIR /build 4 | ADD ./ /build 5 | 6 | RUN apt install bash 7 | 8 | RUN make install 9 | 10 | ENTRYPOINT ["tbb"] 11 | CMD ["-h"] 12 | -------------------------------------------------------------------------------- /Installation.md: -------------------------------------------------------------------------------- 1 | ## Installation 2 | 3 | ### Install Go 1.14 or higher 4 | Follow the official docs or use your favorite dependency manager 5 | to install Go: [https://golang.org/doc/install](https://golang.org/doc/install) 6 | 7 | Verify your `$GOPATH` is correctly set before continuing! 8 | 9 | ### Setup this repository 10 | 11 | Go is bit picky about where you store your repositories. 12 | 13 | The convention is to store: 14 | - the source code inside the `$GOPATH/src` 15 | - the compiled program binaries inside the `$GOPATH/bin` 16 | 17 | #### Using Git 18 | ```bash 19 | mkdir -p $GOPATH/src/github.com/web3coach 20 | cd $GOPATH/src/github.com/web3coach 21 | 22 | git clone https://github.com/web3coach/the-blockchain-bar.git 23 | ``` 24 | 25 | PS: Make sure you actually clone it inside the `src/github.com/web3coach` directory, not your own, otherwise it won't compile. Go rules. 26 | 27 | ### Apple Silicon 28 | 29 | This project currently depends on [gopsutil](https://github.com/shirou/gopsutil) which is a library that provides a cross-platform interface for querying operating system information. Unfortunately, [Apple Silicon/M1](https://en.wikipedia.org/wiki/Apple_silicon) machines are not yet supported by this library which will result in build failures when trying to compile this project locally. If you are using an Apple M1 machine, it is recommended to follow [the guide](./Docker.md) for using [Docker](https://www.docker.com) for local development. 30 | 31 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU LESSER GENERAL PUBLIC LICENSE 2 | Version 3, 29 June 2007 3 | 4 | Copyright (C) 2007 Free Software Foundation, Inc. 5 | Everyone is permitted to copy and distribute verbatim copies 6 | of this license document, but changing it is not allowed. 7 | 8 | 9 | This version of the GNU Lesser General Public License incorporates 10 | the terms and conditions of version 3 of the GNU General Public 11 | License, supplemented by the additional permissions listed below. 12 | 13 | 0. Additional Definitions. 14 | 15 | As used herein, "this License" refers to version 3 of the GNU Lesser 16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 17 | General Public License. 18 | 19 | "The Library" refers to a covered work governed by this License, 20 | other than an Application or a Combined Work as defined below. 21 | 22 | An "Application" is any work that makes use of an interface provided 23 | by the Library, but which is not otherwise based on the Library. 24 | Defining a subclass of a class defined by the Library is deemed a mode 25 | of using an interface provided by the Library. 26 | 27 | A "Combined Work" is a work produced by combining or linking an 28 | Application with the Library. The particular version of the Library 29 | with which the Combined Work was made is also called the "Linked 30 | Version". 31 | 32 | The "Minimal Corresponding Source" for a Combined Work means the 33 | Corresponding Source for the Combined Work, excluding any source code 34 | for portions of the Combined Work that, considered in isolation, are 35 | based on the Application, and not on the Linked Version. 36 | 37 | The "Corresponding Application Code" for a Combined Work means the 38 | object code and/or source code for the Application, including any data 39 | and utility programs needed for reproducing the Combined Work from the 40 | Application, but excluding the System Libraries of the Combined Work. 41 | 42 | 1. Exception to Section 3 of the GNU GPL. 43 | 44 | You may convey a covered work under sections 3 and 4 of this License 45 | without being bound by section 3 of the GNU GPL. 46 | 47 | 2. Conveying Modified Versions. 48 | 49 | If you modify a copy of the Library, and, in your modifications, a 50 | facility refers to a function or data to be supplied by an Application 51 | that uses the facility (other than as an argument passed when the 52 | facility is invoked), then you may convey a copy of the modified 53 | version: 54 | 55 | a) under this License, provided that you make a good faith effort to 56 | ensure that, in the event an Application does not supply the 57 | function or data, the facility still operates, and performs 58 | whatever part of its purpose remains meaningful, or 59 | 60 | b) under the GNU GPL, with none of the additional permissions of 61 | this License applicable to that copy. 62 | 63 | 3. Object Code Incorporating Material from Library Header Files. 64 | 65 | The object code form of an Application may incorporate material from 66 | a header file that is part of the Library. You may convey such object 67 | code under terms of your choice, provided that, if the incorporated 68 | material is not limited to numerical parameters, data structure 69 | layouts and accessors, or small macros, inline functions and templates 70 | (ten or fewer lines in length), you do both of the following: 71 | 72 | a) Give prominent notice with each copy of the object code that the 73 | Library is used in it and that the Library and its use are 74 | covered by this License. 75 | 76 | b) Accompany the object code with a copy of the GNU GPL and this license 77 | document. 78 | 79 | 4. Combined Works. 80 | 81 | You may convey a Combined Work under terms of your choice that, 82 | taken together, effectively do not restrict modification of the 83 | portions of the Library contained in the Combined Work and reverse 84 | engineering for debugging such modifications, if you also do each of 85 | the following: 86 | 87 | a) Give prominent notice with each copy of the Combined Work that 88 | the Library is used in it and that the Library and its use are 89 | covered by this License. 90 | 91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 92 | document. 93 | 94 | c) For a Combined Work that displays copyright notices during 95 | execution, include the copyright notice for the Library among 96 | these notices, as well as a reference directing the user to the 97 | copies of the GNU GPL and this license document. 98 | 99 | d) Do one of the following: 100 | 101 | 0) Convey the Minimal Corresponding Source under the terms of this 102 | License, and the Corresponding Application Code in a form 103 | suitable for, and under terms that permit, the user to 104 | recombine or relink the Application with a modified version of 105 | the Linked Version to produce a modified Combined Work, in the 106 | manner specified by section 6 of the GNU GPL for conveying 107 | Corresponding Source. 108 | 109 | 1) Use a suitable shared library mechanism for linking with the 110 | Library. A suitable mechanism is one that (a) uses at run time 111 | a copy of the Library already present on the user's computer 112 | system, and (b) will operate properly with a modified version 113 | of the Library that is interface-compatible with the Linked 114 | Version. 115 | 116 | e) Provide Installation Information, but only if you would otherwise 117 | be required to provide such information under section 6 of the 118 | GNU GPL, and only to the extent that such information is 119 | necessary to install and execute a modified version of the 120 | Combined Work produced by recombining or relinking the 121 | Application with a modified version of the Linked Version. (If 122 | you use option 4d0, the Installation Information must accompany 123 | the Minimal Corresponding Source and Corresponding Application 124 | Code. If you use option 4d1, you must provide the Installation 125 | Information in the manner specified by section 6 of the GNU GPL 126 | for conveying Corresponding Source.) 127 | 128 | 5. Combined Libraries. 129 | 130 | You may place library facilities that are a work based on the 131 | Library side by side in a single library together with other library 132 | facilities that are not Applications and are not covered by this 133 | License, and convey such a combined library under terms of your 134 | choice, if you do both of the following: 135 | 136 | a) Accompany the combined library with a copy of the same work based 137 | on the Library, uncombined with any other library facilities, 138 | conveyed under the terms of this License. 139 | 140 | b) Give prominent notice with the combined library that part of it 141 | is a work based on the Library, and explaining where to find the 142 | accompanying uncombined form of the same work. 143 | 144 | 6. Revised Versions of the GNU Lesser General Public License. 145 | 146 | The Free Software Foundation may publish revised and/or new versions 147 | of the GNU Lesser General Public License from time to time. Such new 148 | versions will be similar in spirit to the present version, but may 149 | differ in detail to address new problems or concerns. 150 | 151 | Each version is given a distinguishing version number. If the 152 | Library as you received it specifies that a certain numbered version 153 | of the GNU Lesser General Public License "or any later version" 154 | applies to it, you have the option of following the terms and 155 | conditions either of that published version or of any later version 156 | published by the Free Software Foundation. If the Library as you 157 | received it does not specify a version number of the GNU Lesser 158 | General Public License, you may choose any version of the GNU Lesser 159 | General Public License ever published by the Free Software Foundation. 160 | 161 | If the Library as you received it specifies that a proxy can decide 162 | whether future versions of the GNU Lesser General Public License shall 163 | apply, that proxy's public statement of acceptance of any version is 164 | permanent authorization for you to choose that version for the 165 | Library. 166 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | GIT_COMMIT:=$(shell git rev-list -1 HEAD) 2 | LDFLAGS:=-X main.GitCommit=${GIT_COMMIT} 3 | 4 | install: 5 | go install -ldflags "$(LDFLAGS)" ./... 6 | 7 | test: 8 | go test -v -p=1 -timeout=0 ./... 9 | 10 | image: 11 | docker build -t tbb:latest . 12 | 13 | local: 14 | docker-compose build 15 | docker-compose run tbb-local 16 | 17 | -------------------------------------------------------------------------------- /Readme.md: -------------------------------------------------------------------------------- 1 | # The Blockchain Bar 2 | 3 | > The source-code for: "Build a Blockchain from Scratch in Go" eBook. 4 | 5 | :books: Get the eBook from: [https://gumroad.com/l/build-a-blockchain-from-scratch-in-go](https://gumroad.com/l/build-a-blockchain-from-scratch-in-go) 6 | 7 | [![book cover](public/img/book_cover2.png)](https://web3.coach#book) 8 | 9 | > [!TIP] 10 | > Now, in May 2025, I am writing an in-depth "second edition" where we will together reverse-engineer the Ethereum internals written in GoLang. 11 | > 12 | > Open: [https://web3coach.gumroad.com/l/go-ethereum-internals](https://web3coach.gumroad.com/l/go-ethereum-internals) 13 | 14 | 15 | Table of Contents 16 | ================= 17 | 18 | - [TBB Training Ledger](#tbb-training-ledger) 19 | * [What will you build?](#what-will-you-build-) 20 | + [1) You will build a peer-to-peer system from scratch](#1--you-will-build-a-peer-to-peer-system-from-scratch) 21 | + [2) You will secure the system with a day-to-day practical cryptography](#2--you-will-secure-the-system-with-a-day-to-day-practical-cryptography) 22 | + [3) You will implement Bitcoin, Ethereum and XRP backend components](#3--you-will-implement-bitcoin--ethereum-and-xrp-backend-components) 23 | + [4) You will write unit tests and integration tests for all core components](#4--you-will-write-unit-tests-and-integration-tests-for-all-core-components) 24 | * [How to use this repository](#how-to-use-this-repository) 25 | * [Installation](#installation) 26 | * [Getting started](#getting-started) 27 | - [Usage](#usage) 28 | * [Install](#install) 29 | * [Install with Docker](#install-with-docker) 30 | * [CLI](#cli) 31 | + [Show available commands and flags](#show-available-commands-and-flags) 32 | + [Create a blockchain wallet account](#create-a-blockchain-wallet-account) 33 | + [Run a TBB node connected to the official book's test network](#run-a-tbb-node-connected-to-the-official-book-s-test-network) 34 | + [Or run a TBB bootstrap node in isolation (only on your machine)](#or-run-a-tbb-bootstrap-node-in-isolation--only-on-your-machine-) 35 | * [Test Network](#test-network) 36 | * [Tests](#tests) 37 | - [Start](#start) 38 | * [Tutorial](#tutorial) 39 | - [Finish](#finish) 40 | * [Request 1000 TBB testing tokens](#request-1000-tbb-testing-tokens) 41 | - [Disclaimer](#disclaimer) 42 | - [License](#license) 43 | 44 | # TBB Training Ledger 45 | Hi! :wave: 46 | 47 | My name is Lukas. Nice to meet you, and welcome to the peer-to-peer world. 48 | 49 | Is blockchain just an over-engineered database and a monetary mess? No. Every blockchain component and its architecture design decision has a technical reason enabling the decentralized, open, verifiable, transparent vision of Web3. 50 | 51 | I am working in Web3 full-time since 2018. Do you know what was my biggest struggle? **Understanding HOW blockchains work internally and WHY do we need components like Transaction, Block, Mempool, Consensus, Sync and Wallet** in the first place. Reading low-level technical Yellow Papers is a noble, but honestly, a difficult starting point. The goal of this book is to introduce software developers to blockchain via a story - follow how a software developer, who is looking to revolutionize his local bar, implements blockchain technology for its payment system. 52 | 53 | Enjoy the eBook. 54 | 55 | ## What will you build? 56 | 57 | Chapter by chapter, you will build a full peer-to-peer, autonomous training blockchain system in Go and **learn all standard blockchain components!** 58 | 59 | ### 1) You will build a peer-to-peer system from scratch 60 | 61 | You start with 0 lines of code and end-up with 16 branches and a completely executable blockchain node. 62 | 63 | PS: Don't worry if anything on the screen makes sense yet, it will once you go chapter by chapter; release by release. 64 | 65 | ![peer-to-peer blockchain system in action](public/img/andrej_babayaga_caesar_sync_p2p.png) 66 | 67 | ### 2) You will secure the system with a day-to-day practical cryptography 68 | 69 | No boring theory. Only modern practices. 70 | 71 | ![elliptic curve cryptography](public/img/andrej_babayaga_crypto_sign_summary.png) 72 | 73 | ### 3) You will implement Bitcoin, Ethereum and XRP backend components 74 | 75 | From diagrams of mining algorithms to actual, implemented and working crypto wallets for storing the mined tokens and all other fundamental components that make blockchain special. 76 | 77 | ![decentralized consensus](public/img/mining_p2p.png) 78 | 79 | ### 4) You will write unit tests and integration tests for all core components 80 | 81 | You will test your cryptographic functions, a Bitcoin's like Proof of Work mining algorithm and other key components. 82 | 83 | ![ethereum signature](public/img/test_ethereum_signature.png) 84 | 85 | ## How to use this repository 86 | Every eBook chapter has a dedicated branch where you can experiment with the code first-hand. 87 | 88 | ```git 89 | git pull --all 90 | git branch 91 | 92 | > c1_genesis_json 93 | > c2_db_changes_txt 94 | > c3_state_blockchain_component 95 | > c4_caesar_transfer 96 | > c5_broken_trust 97 | > c6_immutable_hash 98 | > c7_blockchain_programming_model 99 | > c8_transparent_db 100 | > c9_tango 101 | > c10_peer_sync 102 | > c11_consensus 103 | > c12_crypto 104 | > c13_training_network 105 | > c14_why_transaction_costs_gas 106 | > c15_blockchain_forks_what_why_how 107 | > c16_blockchain_explorer 108 | ``` 109 | 110 | ## Installation 111 | 112 | [Open full instructions.](./Installation.md) 113 | 114 | ## Getting started 115 | 1. Buy the eBook from: [https://gumroad.com/l/build-a-blockchain-from-scratch-in-go](https://gumroad.com/l/build-a-blockchain-from-scratch-in-go) 116 | 1. Open the book at Chapter 1 117 | 1. Checkout the first chapter's branch `c1_genesis_json` 118 | 119 | ```git 120 | git pull --all 121 | 122 | git checkout c1_genesis_json 123 | ``` 124 | 125 | # Usage 126 | 127 | ## Install 128 | ``` 129 | go install ./cmd/... 130 | ``` 131 | 132 | ## Install with Docker 133 | 134 | [Open Docker instructions.](./Docker.md) 135 | 136 | ## CLI 137 | ### Show available commands and flags 138 | ```bash 139 | tbb help 140 | 141 | tbb run --help 142 | 143 | Launches the TBB node and its HTTP API. 144 | 145 | Usage: 146 | tbb run [flags] 147 | 148 | Flags: 149 | --bootstrap-account string default bootstrap Web3Coach's Genesis account with 1M TBB tokens (default "0x09ee50f2f37fcba1845de6fe5c762e83e65e755c") 150 | --bootstrap-ip string default bootstrap Web3Coach's server to interconnect peers (default "node.tbb.web3.coach") 151 | --bootstrap-port uint default bootstrap Web3Coach's server port to interconnect peers (default 443) 152 | --datadir string Absolute path to your node's data dir where the DB will be/is stored 153 | --disable-ssl should the HTTP API SSL certificate be disabled? (default false) 154 | -h, --help help for run 155 | --ip string your node's public IP to communication with other peers (default "127.0.0.1") 156 | --miner string your node's miner account to receive the block rewards (default "0x0000000000000000000000000000000000000000") 157 | --port uint your node's public HTTP port for communication with other peers (configurable if SSL is disabled) (default 443) 158 | ``` 159 | 160 | ### Create a blockchain wallet account 161 | ``` 162 | tbb wallet new-account --datadir=$HOME/.tbb 163 | 164 | > Please enter a password to encrypt the new wallet: 165 | > Password: 166 | ``` 167 | 168 | ### Run a TBB node connected to the official book's test network 169 | 170 | ``` 171 | tbb version 172 | > Version: 1.9.2-alpha TX Gas 173 | 174 | tbb run --datadir=$HOME/.tbb --ip=127.0.0.1 --port=8081 --miner=0x_YOUR_WALLET_ACCOUNT --disable-ssl 175 | ``` 176 | 177 | Your blockchain database will synchronize with rest of the students: 178 | 179 | ```json 180 | Launching TBB node and its HTTP API... 181 | Listening on: 127.0.0.1:8081 182 | Blockchain state: 183 | - height: 0 184 | - hash: 0000000000000000000000000000000000000000000000000000000000000000 185 | Searching for new Peers and their Blocks and Peers: 'node.tbb.web3.coach:443' 186 | Found 37 new blocks from Peer node.tbb.web3.coach:443 187 | Importing blocks from Peer node.tbb.web3.coach:443... 188 | 189 | Persisting new Block to disk: 190 | {"hash":"000000a9d18730c133869d175a886d576df5675e0e73900bf072c59047b9d734","block":{"header":{"parent":"0000000000000000000000000000000000000000000000000000000000000000","number":0,"nonce":1925346453,"time":1590684713,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x22ba1f80452e6220c7cc6ea2d1e3eeddac5f694a","value":5,"nonce":1,"data":"","time":1590684702,"signature":"0JE1yEoA3gwIiTj5ayanUZfo5ZnN7kHIRQPOw8/OZIRYWjbvbMA7vWdPgoqxnhFGiTH7FIbjCQJ25fQlvMvmPwA="}]}} 191 | 192 | Persisting new Block to disk: 193 | {"hash":"0000004d3faa1f7b8802aa809c8b77253859846602de3402a1bc67a0026cd94d","block":{"header":{"parent":"000000a9d18730c133869d175a886d576df5675e0e73900bf072c59047b9d734","number":1,"nonce":1331825342,"time":1592320406,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x596b0709ed646e3e76b6c1fd58297b145b68387c","value":1000,"nonce":2,"data":"","time":1592320398,"signature":"1JIi9sYEZ+9RBG7IwACmm9vC4D7QVXqvBH1Es7cmeCJljTknVM80AzrhoLAW9RwCguunRO0qpN4JJ287VLFNfAE="}]}} 194 | ... 195 | ``` 196 | 197 | ### Or run a TBB bootstrap node in isolation (only on your machine) 198 | ``` 199 | tbb run --datadir=$HOME/.tbb_boostrap --ip=127.0.0.1 --port=8080 --bootstrap-ip=127.0.0.1 --bootstrap-port=8080 --disable-ssl 200 | ``` 201 | 202 | ## Test Network 203 | You can also set up a server and be part of TBB blockchain network validating other student's transactions. Here is an example how the official TBB bootstrap node is launched. Customize the `--datadir`, `--miner`, and `--ip` values to match your server. 204 | 205 | ```bash 206 | /usr/local/bin/tbb run --datadir=/home/ec2-user/.tbb --miner=0x09ee50f2f37fcba1845de6fe5c762e83e65e755c --ip=node.tbb.web3.coach --port=443 --ssl-email=lukas@web3.coach --bootstrap-ip=node.tbb.web3.coach --bootstrap-port=443 --bootstrap-account=0x09ee50f2f37fcba1845de6fe5c762e83e65e755c 207 | ``` 208 | 209 | ## Tests 210 | Run all tests with verbosity but one at a time, without timeout, to avoid ports collisions: 211 | ``` 212 | go test -v -p=1 -timeout=0 ./... 213 | ``` 214 | 215 | Run an individual test: 216 | ``` 217 | go test -timeout=0 ./node -test.v -test.run ^TestNode_Mining$ 218 | ``` 219 | 220 | **Note:** Majority are integration tests and take time. Expect the test suite to finish in ~30 mins. 221 | 222 | # Start 223 | ## Tutorial 224 | 225 | :books: Get the eBook from: [https://gumroad.com/l/build-a-blockchain-from-scratch-in-go](https://gumroad.com/l/build-a-blockchain-from-scratch-in-go) 226 | 227 | # Finish 228 | ## Request 1000 TBB testing tokens 229 | 230 | Write a tweet and let me know how did you like this book! Tag me in it [@Web3Coach](https://twitter.com/Web3Coach) and include your account address 0xYOUR_ADDRESS. I will send you 1000 testing TBB tokens. 231 | 232 | See you on Twitter - [@Web3Coach.](https://twitter.com/Web3Coach) 233 | 234 | Or LinkedIn - [LukasLukac](https://www.linkedin.com/in/llukac/) 235 | 236 | --- 237 | 238 | # Disclaimer 239 | The Blockchain Bar repository, the `tbb` binary and `Build a Blockchain from Scratch in Go` eBook is **for learning, educational purposes.** The codebase is NOT ready for production. The components are purposefully simplified to don't overwhelm new blockchain students, but complex enough to teach you how blockchains work under the hood. 240 | 241 | # License 242 | The Blockchain Bar library (i.e. all code outside of the `cmd` directory) is licensed under the 243 | [GNU Lesser General Public License v3.0](https://www.gnu.org/licenses/lgpl-3.0.en.html), 244 | also included in our repository in the `COPYING.LESSER` file. 245 | 246 | The Blockchain Bar binaries (i.e. all code inside of the `cmd` directory) is licensed under the 247 | [GNU General Public License v3.0](https://www.gnu.org/licenses/gpl-3.0.en.html), also 248 | included in our repository in the `COPYING` file. 249 | -------------------------------------------------------------------------------- /TIPs/Readme.md: -------------------------------------------------------------------------------- 1 | # TheBlockchainBar Improvement Proposals (TIPs) 2 | 3 | The Blockchain Bar Improvement Proposals (TIPs) describe standards for the TheBlockchainBar platform, including core protocol specifications, client APIs, and contract standards. 4 | 5 | - [TIP-1: Dynamic Transaction Cost like in Ethereum](./TIP-1.md) 6 | - [TIP-2: The Blockchain Bar UI - Database Explorer, Faucet, Wallet](./TIP-2.md) 7 | 8 | ## Ideas 9 | [The Blockchain Bar eBook](https://web3coach.gumroad.com/l/build-a-blockchain-from-scratch-in-go) serves as a learning playfield. A testing, experimental blockchain where you can practice peer-to-peer development and programming crypto applications. 10 | 11 | If you want to improve The Blockchain Bar with a new feature available in Bitcoin, Ethereum, XRP or any other blockchain protocol, create a new TIP-X proposal and describe the feature specifications. -------------------------------------------------------------------------------- /TIPs/TIP-1.md: -------------------------------------------------------------------------------- 1 | # Dynamic Transaction Cost like in Ethereum 2 | ## Current Context 3 | Every TBB transfer costs miner's `txFee` hardcoded to 50 TBB tokens to prevent users from spamming the network. 4 | 5 | ```go 6 | func (t Tx) Cost() uint { 7 | return t.Value + TxFee 8 | } 9 | ``` 10 | 11 | There are a few downsides to this approach: 12 | - depending on the single TBB token price, the $ cost of spending 50 TBB tokens could make the network too expensive to use 13 | - opposite also applies, if the single TBB token price is too low, the network is susceptible to DOS (denial of service) attack 14 | - hardcoded cost limits the network to only one type of operations and its costs, transfers (maybe we define a new TIP later and implement Smart Contracts via a Virtual Machine?) Exciting. 15 | 16 | The static cost from [Chapter 14 of TBB eBook](https://web3coach.gumroad.com/l/build-a-blockchain-from-scratch-in-go) is simple to understand for learning purposes but production-ready blockchains like Ethereum use a more sophisticated, dynamic calculation. 17 | 18 | ### What Ethereum does 19 | 20 | The official [go-ethereum/core/types/transaction.go](https://github.com/ethereum/go-ethereum/blob/57feabea663496109e59df669238398239438fb1/core/types/transaction.go#L296) has several fee/gas related methods: 21 | ```go 22 | // Cost returns gas * gasPrice + value. 23 | func (tx *Transaction) Cost() *big.Int { 24 | total := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) 25 | total.Add(total, tx.Value()) 26 | return total 27 | } 28 | 29 | // Gas returns the gas limit of the transaction. 30 | func (tx *Transaction) Gas() uint64 { return tx.inner.gas() } 31 | 32 | // GasPrice returns the gas price of the transaction. 33 | func (tx *Transaction) GasPrice() *big.Int { return new(big.Int).Set(tx.inner.gasPrice()) } 34 | 35 | // GasTipCap returns the gasTipCap per gas of the transaction. 36 | func (tx *Transaction) GasTipCap() *big.Int { return new(big.Int).Set(tx.inner.gasTipCap()) } 37 | 38 | // GasFeeCap returns the fee cap per gas of the transaction. 39 | func (tx *Transaction) GasFeeCap() *big.Int { return new(big.Int).Set(tx.inner.gasFeeCap()) } 40 | ``` 41 | 42 | The final Ethereum `tx.Cost()` is calculated as **gas * gasPrice + value**. 43 | 44 | Andrej decides he doesn't need the latest EIP-1559 miner tipping feature for now, but he definitely wants to copy the cost calculation to learn a programming technique from the PROs and grow as a developer: 45 | ```go 46 | // Cost returns gas * gasPrice + value. 47 | func (tx *Transaction) Cost() *big.Int { 48 | total := new(big.Int).Mul(tx.GasPrice(), new(big.Int).SetUint64(tx.Gas())) 49 | total.Add(total, tx.Value()) 50 | return total 51 | } 52 | ``` 53 | 54 | #### Example 55 | 56 | The legacy Ethereum TX transfer from account A to account B costs 21 000 gas. The Price of Gas depends on the network usage. If the Price is 2 GWei per 1 Gas, the Gas Price would be 2 * 21 000 = 42 000 Gwei or 0.000042 ETH. 57 | 58 | The Ether currency structure with **Wei** being the smallest unit: 59 | ```go 60 | const ( 61 | Wei = 1 62 | GWei = 1e9 63 | Ether = 1e18 64 | ) 65 | ``` 66 | 67 | ## New Specification 68 | The TBB protocol will define how much **Gas** each action will require, and the user and network economics will decide the **Gas Price**. 69 | 70 | | Action | Gas Required | 71 | |--------|--------------| 72 | | Transfer | 21 | 73 | 74 | Each Transaction will require two new attributes: 75 | - **Gas:** user must set this value to 21 76 | - **GasPrice:** user decides how much he wants to spend, miner chooses whenever it's enough to include this transaction into a block and cover the mining costs 77 | 78 | The default **GasPrice value will be 1 TBB** token for simplicity (1 TBB token is the smallest protocol unit). 79 | 80 | Miners can create a new The Blockchain Bar Improvement Proposal and define according to what criteria the **GasPrice** will be sufficient to pay for a transaction to get included into a block. 81 | 82 | ```go 83 | type Tx struct { 84 | From common.Address `json:"from"` 85 | To common.Address `json:"to"` 86 | 87 | Gas uint `json:"gas"` 88 | GasPrice uint `json:"gasPrice"` 89 | 90 | Value uint `json:"value"` 91 | Nonce uint `json:"nonce"` 92 | Data string `json:"data"` 93 | Time uint64 `json:"time"` 94 | } 95 | ``` 96 | 97 | ## Proposed Consensus Fork Number 98 | Block number 35. -------------------------------------------------------------------------------- /TIPs/TIP-2.md: -------------------------------------------------------------------------------- 1 | # The Blockchain Bar UI - Database Explorer, Faucet, Wallet 2 | ## Vision 3 | To help developers practice blockchain programming in a test-network by creating new features, API endpoints and database indexing. 4 | 5 | To help developers learn what's happening behind the scenes in the blockchain network by providing several graphical components painting a better picture on: 6 | - how the blocks are chained 7 | - how account balances are calculated and validated 8 | - what are the transaction attributes 9 | - how to create a transaction 10 | - how to sign a transaction 11 | - how a transaction waits in a Mempool until is mined 12 | - how the mining process works 13 | 14 | // TODO: Add more of your ideas here. 15 | 16 | **PS for frontend devs: We will need a new `web` directory in this repository with all the frontend code for Explorer, Faucet and Wallet.** 17 | 18 | ## Database Explorer 19 | ### Idea - Block Traversal 20 | #### User Story 21 | As a User I want to traverse database blocks, 22 | so I can see all The Blockchain Bar's users activity and what happened when. 23 | 24 | #### Backend Proposal [DONE ✓] 25 | Add a new API endpoint `/block/$height` to `./node/http_routes.go` to retrieve block by number. 26 | 27 | Add a new API endpoint `/block/$hash` to `./node/http_routes.go` to retrieve block by hash. 28 | 29 | Both endpoints will return a Block as JSON: 30 | ``` 31 | type Block struct { 32 | Header BlockHeader `json:"header"` 33 | TXs []SignedTx `json:"payload"` 34 | } 35 | 36 | type BlockHeader struct { 37 | Parent Hash `json:"parent"` 38 | Number uint64 `json:"number"` 39 | Nonce uint32 `json:"nonce"` 40 | Time uint64 `json:"time"` 41 | Miner common.Address `json:"miner"` 42 | } 43 | 44 | type BlockFS struct { 45 | Key Hash `json:"hash"` 46 | Value Block `json:"block"` 47 | } 48 | ``` 49 | 50 | The easiest way to achieve this ATM will be by modifying the `func GetBlocksAfter(blockHash Hash, dataDir string) ([]Block, error) {` and adding a new argument `limit uint` to the function. As well as creating another getter to the `database` package like `func GetBlocksByNumberAfter(blockHash Hash, limit uint, dataDir string) ([]Block, error) {` and abstracting the common code shared between these two functions (the already implement blocks retrieval from disk) to a third function. 51 | 52 | #### Frontend Proposal [PENDING] 53 | // TODO: Add here some UI ideas how this could be displayed in the UI. 54 | 55 | --- 56 | 57 | ### Idea - Query Balance 58 | #### User Story 59 | As a User I want to check a balance of an account. 60 | 61 | #### Backend Proposal 62 | 63 | #### Frontend Proposal 64 | 65 | --- 66 | 67 | ### Idea - Visualize Mempool 68 | #### User Story 69 | As a User I want to see what's the difference between a database and a Mempool. 70 | 71 | #### Backend Proposal 72 | 73 | #### Frontend Proposal 74 | 75 | --- 76 | 77 | ### Idea - Visualize P2P with Animations 78 | #### User Story 79 | As a User I want to see how transactions are broadcasted over p2p, 80 | so I have a better idea what's happening after a transaction is broadcasted. 81 | 82 | #### Backend Proposal 83 | 84 | #### Frontend Proposal 85 | 86 | --- 87 | 88 | ## Wallet 89 | ### Idea - Create Account 90 | #### User Story 91 | As a User I want to create my The Blockchain Bar account, 92 | so I can receive and send testing tokens. 93 | 94 | #### Backend Proposal 95 | 96 | #### Frontend Proposal 97 | 98 | --- 99 | 100 | ### Idea - Create a Transaction and Sign it on Frontend 101 | #### User Story 102 | As a User I want to learn how to construct a valid Transactions, 103 | so I learn about gas fees, nonce and crypto signing. 104 | 105 | #### Backend Proposal 106 | 107 | #### Frontend Proposal 108 | 109 | --- 110 | 111 | ## Faucet 112 | ### Idea - Faucet for Giving Away Free Tokens 113 | #### User Story 114 | As a User I want to request free testing tokens, 115 | so I can experiment around with my Wallet. 116 | 117 | #### Backend Proposal 118 | 119 | #### Frontend Proposal 120 | 121 | --- 122 | 123 | ### Idea - Visualize Mempool 124 | #### User Story 125 | As a User I want to see what's the difference between a database and a Mempool. 126 | 127 | #### Backend Proposal 128 | 129 | #### Frontend Proposal 130 | 131 | --- 132 | 133 | ## Currently Available API endpoints: 134 | The below list is very limited and would need a lot of new API endpoints to support all above UI features. Great opportunity to practice Go programming. 135 | 136 | ### Query the latest balances of all accounts 137 | https://node.tbb.web3.coach/balances/list 138 | 139 | ```json 140 | { 141 | "block_hash": "00000050159ee00da041cb8bd1a1974ea80da53f8489ade6c4886dcf48205ca9", 142 | "balances": { 143 | "0x09ee50f2f37fcba1845de6fe5c762e83e65e755c": 988845, 144 | "0x0fc524c45b51e3215701ef7c12e58c781b0642ac": 1000, 145 | ... 146 | ... 147 | "0x9f23369f02924f94765711bb09ebe1937123e37d": 1000, 148 | "0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b": 150, 149 | "0xf04679700642fd1455782e1acc37c314aab1a847": 1550, 150 | "0xf336ed6a7c7529df70552377d641088be4dc4519": 1000 151 | } 152 | } 153 | ``` 154 | 155 | ### Query the latest node status 156 | https://node.tbb.web3.coach/node/status 157 | 158 | ```json 159 | { 160 | "block_hash": "00000050159ee00da041cb8bd1a1974ea80da53f8489ade6c4886dcf48205ca9", 161 | "block_number": 36, 162 | "peers_known": { 163 | "node.tbb.web3.coach:443": { 164 | "ip": "node.tbb.web3.coach", 165 | "port": 443, 166 | "is_bootstrap": true, 167 | "account": "0x09ee50f2f37fcba1845de6fe5c762e83e65e755c", 168 | "node_version": "" 169 | } 170 | }, 171 | "pending_txs": [], 172 | "node_version": "1.9.0-alpha 959541 TX Gas", 173 | "account": "0x09ee50f2f37fcba1845de6fe5c762e83e65e755c" 174 | } 175 | ``` 176 | 177 | ### Unlock wallet to sign a transaction and broadcast it 178 | ``` 179 | curl --location --request POST 'http://localhost:8080/tx/add' \ 180 | --header 'Content-Type: application/json' \ 181 | --data-raw '{ 182 | "from": "0x22ba1f...", 183 | "from_pwd": "", 184 | "to": "0x6fdc0d...", 185 | "value": 100, 186 | "gas": 21, 187 | "gas_price": 1 188 | }' 189 | ``` -------------------------------------------------------------------------------- /cmd/tbb/balances.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "os" 21 | 22 | "github.com/spf13/cobra" 23 | "github.com/web3coach/the-blockchain-bar/database" 24 | "github.com/web3coach/the-blockchain-bar/node" 25 | ) 26 | 27 | func balancesCmd() *cobra.Command { 28 | var balancesCmd = &cobra.Command{ 29 | Use: "balances", 30 | Short: "Interacts with balances (list...).", 31 | PreRunE: func(cmd *cobra.Command, args []string) error { 32 | return incorrectUsageErr() 33 | }, 34 | Run: func(cmd *cobra.Command, args []string) { 35 | }, 36 | } 37 | 38 | balancesCmd.AddCommand(balancesListCmd()) 39 | 40 | return balancesCmd 41 | } 42 | 43 | func balancesListCmd() *cobra.Command { 44 | var balancesListCmd = &cobra.Command{ 45 | Use: "list", 46 | Short: "Lists all balances.", 47 | Run: func(cmd *cobra.Command, args []string) { 48 | state, err := database.NewStateFromDisk(getDataDirFromCmd(cmd), node.DefaultMiningDifficulty) 49 | if err != nil { 50 | fmt.Fprintln(os.Stderr, err) 51 | os.Exit(1) 52 | } 53 | defer state.Close() 54 | 55 | fmt.Printf("Accounts balances at %x:\n", state.LatestBlockHash()) 56 | fmt.Println("__________________") 57 | fmt.Println("") 58 | for account, balance := range state.Balances { 59 | fmt.Println(fmt.Sprintf("%s: %d", account.String(), balance)) 60 | } 61 | fmt.Println("") 62 | fmt.Printf("Accounts nonces:") 63 | fmt.Println("") 64 | fmt.Println("__________________") 65 | fmt.Println("") 66 | for account, nonce := range state.Account2Nonce { 67 | fmt.Println(fmt.Sprintf("%s: %d", account.String(), nonce)) 68 | } 69 | }, 70 | } 71 | 72 | addDefaultRequiredFlags(balancesListCmd) 73 | 74 | return balancesListCmd 75 | } 76 | -------------------------------------------------------------------------------- /cmd/tbb/main.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "github.com/spf13/cobra" 21 | "github.com/web3coach/the-blockchain-bar/fs" 22 | "os" 23 | ) 24 | 25 | const flagKeystoreFile = "keystore" 26 | const flagDataDir = "datadir" 27 | const flagMiner = "miner" 28 | const flagSSLEmail = "ssl-email" 29 | const flagDisableSSL = "disable-ssl" 30 | const flagIP = "ip" 31 | const flagPort = "port" 32 | const flagBootstrapAcc = "bootstrap-account" 33 | const flagBootstrapIp = "bootstrap-ip" 34 | const flagBootstrapPort = "bootstrap-port" 35 | 36 | func main() { 37 | var tbbCmd = &cobra.Command{ 38 | Use: "tbb", 39 | Short: "The Blockchain Bar CLI", 40 | Run: func(cmd *cobra.Command, args []string) { 41 | }, 42 | } 43 | 44 | tbbCmd.AddCommand(versionCmd) 45 | tbbCmd.AddCommand(balancesCmd()) 46 | tbbCmd.AddCommand(walletCmd()) 47 | tbbCmd.AddCommand(runCmd()) 48 | 49 | err := tbbCmd.Execute() 50 | if err != nil { 51 | fmt.Fprintln(os.Stderr, err) 52 | os.Exit(1) 53 | } 54 | } 55 | 56 | func addDefaultRequiredFlags(cmd *cobra.Command) { 57 | cmd.Flags().String(flagDataDir, "", "Absolute path to your node's data dir where the DB will be/is stored") 58 | cmd.MarkFlagRequired(flagDataDir) 59 | } 60 | 61 | func addKeystoreFlag(cmd *cobra.Command) { 62 | cmd.Flags().String(flagKeystoreFile, "", "Absolute path to the encrypted keystore file") 63 | cmd.MarkFlagRequired(flagKeystoreFile) 64 | } 65 | 66 | func getDataDirFromCmd(cmd *cobra.Command) string { 67 | dataDir, _ := cmd.Flags().GetString(flagDataDir) 68 | 69 | return fs.ExpandPath(dataDir) 70 | } 71 | 72 | func incorrectUsageErr() error { 73 | return fmt.Errorf("incorrect usage") 74 | } 75 | -------------------------------------------------------------------------------- /cmd/tbb/run.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package main 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "os" 22 | 23 | "github.com/spf13/cobra" 24 | "github.com/web3coach/the-blockchain-bar/database" 25 | "github.com/web3coach/the-blockchain-bar/node" 26 | ) 27 | 28 | func runCmd() *cobra.Command { 29 | var runCmd = &cobra.Command{ 30 | Use: "run", 31 | Short: "Launches the TBB node and its HTTP API.", 32 | Run: func(cmd *cobra.Command, args []string) { 33 | miner, _ := cmd.Flags().GetString(flagMiner) 34 | sslEmail, _ := cmd.Flags().GetString(flagSSLEmail) 35 | isSSLDisabled, _ := cmd.Flags().GetBool(flagDisableSSL) 36 | ip, _ := cmd.Flags().GetString(flagIP) 37 | port, _ := cmd.Flags().GetUint64(flagPort) 38 | bootstrapIp, _ := cmd.Flags().GetString(flagBootstrapIp) 39 | bootstrapPort, _ := cmd.Flags().GetUint64(flagBootstrapPort) 40 | bootstrapAcc, _ := cmd.Flags().GetString(flagBootstrapAcc) 41 | 42 | fmt.Println("Launching TBB node and its HTTP API...") 43 | 44 | bootstrap := node.NewPeerNode( 45 | bootstrapIp, 46 | bootstrapPort, 47 | true, 48 | database.NewAccount(bootstrapAcc), 49 | false, 50 | "", 51 | ) 52 | 53 | if !isSSLDisabled { 54 | port = node.HttpSSLPort 55 | } 56 | 57 | version := fmt.Sprintf("%s.%s.%s-alpha %s %s", Major, Minor, Fix, shortGitCommit(GitCommit), Verbal) 58 | n := node.New(getDataDirFromCmd(cmd), ip, port, database.NewAccount(miner), bootstrap, version, node.DefaultMiningDifficulty) 59 | err := n.Run(context.Background(), isSSLDisabled, sslEmail) 60 | if err != nil { 61 | fmt.Println(err) 62 | os.Exit(1) 63 | } 64 | }, 65 | } 66 | 67 | addDefaultRequiredFlags(runCmd) 68 | runCmd.Flags().Bool(flagDisableSSL, false, "should the HTTP API SSL certificate be disabled? (default false)") 69 | runCmd.Flags().String(flagSSLEmail, "", "your node's HTTP SSL certificate email") 70 | runCmd.Flags().String(flagMiner, node.DefaultMiner, "your node's miner account to receive the block rewards") 71 | runCmd.Flags().String(flagIP, node.DefaultIP, "your node's public IP to communication with other peers") 72 | runCmd.Flags().Uint64(flagPort, node.HttpSSLPort, "your node's public HTTP port for communication with other peers (configurable if SSL is disabled)") 73 | runCmd.Flags().String(flagBootstrapIp, node.DefaultBootstrapIp, "default bootstrap Web3Coach's server to interconnect peers") 74 | runCmd.Flags().Uint64(flagBootstrapPort, node.HttpSSLPort, "default bootstrap Web3Coach's server port to interconnect peers") 75 | runCmd.Flags().String(flagBootstrapAcc, node.DefaultBootstrapAcc, "default bootstrap Web3Coach's Genesis account with 1M TBB tokens") 76 | 77 | return runCmd 78 | } 79 | -------------------------------------------------------------------------------- /cmd/tbb/version.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | 21 | "github.com/spf13/cobra" 22 | ) 23 | 24 | const Major = "1" 25 | const Minor = "9" 26 | const Fix = "2" 27 | const Verbal = "TX Gas" 28 | 29 | // Configured via -ldflags during build 30 | var GitCommit string 31 | 32 | var versionCmd = &cobra.Command{ 33 | Use: "version", 34 | Short: "Describes version.", 35 | Run: func(cmd *cobra.Command, args []string) { 36 | fmt.Println(fmt.Sprintf("Version: %s.%s.%s-alpha %s %s", Major, Minor, Fix, shortGitCommit(GitCommit), Verbal)) 37 | }, 38 | } 39 | 40 | func shortGitCommit(fullGitCommit string) string { 41 | shortCommit := "" 42 | if len(fullGitCommit) >= 6 { 43 | shortCommit = fullGitCommit[0:6] 44 | } 45 | 46 | return shortCommit 47 | } 48 | -------------------------------------------------------------------------------- /cmd/tbb/wallet.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package main 17 | 18 | import ( 19 | "fmt" 20 | "io/ioutil" 21 | "os" 22 | 23 | "github.com/davecgh/go-spew/spew" 24 | "github.com/ethereum/go-ethereum/accounts/keystore" 25 | "github.com/ethereum/go-ethereum/cmd/utils" 26 | "github.com/spf13/cobra" 27 | "github.com/web3coach/the-blockchain-bar/wallet" 28 | ) 29 | 30 | func walletCmd() *cobra.Command { 31 | var walletCmd = &cobra.Command{ 32 | Use: "wallet", 33 | Short: "Manages blockchain accounts and keys.", 34 | PreRunE: func(cmd *cobra.Command, args []string) error { 35 | return incorrectUsageErr() 36 | }, 37 | Run: func(cmd *cobra.Command, args []string) { 38 | }, 39 | } 40 | 41 | walletCmd.AddCommand(walletNewAccountCmd()) 42 | walletCmd.AddCommand(walletPrintPrivKeyCmd()) 43 | 44 | return walletCmd 45 | } 46 | 47 | func walletNewAccountCmd() *cobra.Command { 48 | var cmd = &cobra.Command{ 49 | Use: "new-account", 50 | Short: "Creates a new account with a new set of a elliptic-curve Private + Public keys.", 51 | Run: func(cmd *cobra.Command, args []string) { 52 | password := getPassPhrase("Please enter a password to encrypt the new wallet:", true) 53 | dataDir := getDataDirFromCmd(cmd) 54 | 55 | acc, err := wallet.NewKeystoreAccount(dataDir, password) 56 | if err != nil { 57 | fmt.Println(err) 58 | os.Exit(1) 59 | } 60 | 61 | fmt.Printf("New account created: %s\n", acc.Hex()) 62 | fmt.Printf("Saved in: %s\n", wallet.GetKeystoreDirPath(dataDir)) 63 | }, 64 | } 65 | 66 | addDefaultRequiredFlags(cmd) 67 | 68 | return cmd 69 | } 70 | 71 | func walletPrintPrivKeyCmd() *cobra.Command { 72 | var cmd = &cobra.Command{ 73 | Use: "pk-print", 74 | Short: "Unlocks keystore file and prints the Private + Public keys.", 75 | Run: func(cmd *cobra.Command, args []string) { 76 | ksFile, _ := cmd.Flags().GetString(flagKeystoreFile) 77 | password := getPassPhrase("Please enter a password to decrypt the wallet:", false) 78 | 79 | keyJson, err := ioutil.ReadFile(ksFile) 80 | if err != nil { 81 | fmt.Println(err.Error()) 82 | os.Exit(1) 83 | } 84 | 85 | key, err := keystore.DecryptKey(keyJson, password) 86 | if err != nil { 87 | fmt.Println(err.Error()) 88 | os.Exit(1) 89 | } 90 | 91 | spew.Dump(key) 92 | }, 93 | } 94 | 95 | addKeystoreFlag(cmd) 96 | 97 | return cmd 98 | } 99 | 100 | func getPassPhrase(prompt string, confirmation bool) string { 101 | return utils.GetPassPhrase(prompt, confirmation) 102 | } 103 | -------------------------------------------------------------------------------- /database/block.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package database 17 | 18 | import ( 19 | "bytes" 20 | "crypto/sha256" 21 | "encoding/hex" 22 | "encoding/json" 23 | "fmt" 24 | 25 | "github.com/ethereum/go-ethereum/common" 26 | ) 27 | 28 | const BlockReward = 100 29 | 30 | type Hash [32]byte 31 | 32 | func (h Hash) MarshalText() ([]byte, error) { 33 | return []byte(h.Hex()), nil 34 | } 35 | 36 | func (h *Hash) UnmarshalText(data []byte) error { 37 | _, err := hex.Decode(h[:], data) 38 | return err 39 | } 40 | 41 | func (h Hash) Hex() string { 42 | return hex.EncodeToString(h[:]) 43 | } 44 | 45 | func (h Hash) IsEmpty() bool { 46 | emptyHash := Hash{} 47 | 48 | return bytes.Equal(emptyHash[:], h[:]) 49 | } 50 | 51 | type Block struct { 52 | Header BlockHeader `json:"header"` 53 | TXs []SignedTx `json:"payload"` 54 | } 55 | 56 | type BlockHeader struct { 57 | Parent Hash `json:"parent"` 58 | Number uint64 `json:"number"` 59 | Nonce uint32 `json:"nonce"` 60 | Time uint64 `json:"time"` 61 | Miner common.Address `json:"miner"` 62 | } 63 | 64 | type BlockFS struct { 65 | Key Hash `json:"hash"` 66 | Value Block `json:"block"` 67 | } 68 | 69 | func NewBlock(parent Hash, number uint64, nonce uint32, time uint64, miner common.Address, txs []SignedTx) Block { 70 | return Block{BlockHeader{parent, number, nonce, time, miner}, txs} 71 | } 72 | 73 | func (b Block) Hash() (Hash, error) { 74 | blockJson, err := json.Marshal(b) 75 | if err != nil { 76 | return Hash{}, err 77 | } 78 | 79 | return sha256.Sum256(blockJson), nil 80 | } 81 | 82 | func (b Block) GasReward() uint { 83 | reward := uint(0) 84 | 85 | for _, tx := range b.TXs { 86 | reward += tx.GasCost() 87 | } 88 | 89 | return reward 90 | } 91 | 92 | func IsBlockHashValid(hash Hash, miningDifficulty uint) bool { 93 | zeroesCount := uint(0) 94 | 95 | for i := uint(0); i < miningDifficulty; i++ { 96 | if fmt.Sprintf("%x", hash[i]) == "0" { 97 | zeroesCount++ 98 | } 99 | } 100 | 101 | if fmt.Sprintf("%x", hash[miningDifficulty]) == "0" { 102 | return false 103 | } 104 | 105 | return zeroesCount == miningDifficulty 106 | } 107 | -------------------------------------------------------------------------------- /database/database.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package database 17 | 18 | import ( 19 | "bufio" 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | "reflect" 24 | ) 25 | 26 | func GetBlocksAfter(blockHash Hash, dataDir string) ([]Block, error) { 27 | f, err := os.OpenFile(getBlocksDbFilePath(dataDir), os.O_RDONLY, 0600) 28 | if err != nil { 29 | return nil, err 30 | } 31 | 32 | blocks := make([]Block, 0) 33 | shouldStartCollecting := false 34 | 35 | if reflect.DeepEqual(blockHash, Hash{}) { 36 | shouldStartCollecting = true 37 | } 38 | 39 | scanner := bufio.NewScanner(f) 40 | for scanner.Scan() { 41 | if err := scanner.Err(); err != nil { 42 | return nil, err 43 | } 44 | 45 | var blockFs BlockFS 46 | err = json.Unmarshal(scanner.Bytes(), &blockFs) 47 | if err != nil { 48 | return nil, err 49 | } 50 | 51 | if shouldStartCollecting { 52 | blocks = append(blocks, blockFs.Value) 53 | continue 54 | } 55 | 56 | if blockHash == blockFs.Key { 57 | shouldStartCollecting = true 58 | } 59 | } 60 | 61 | return blocks, nil 62 | } 63 | 64 | // GetBlockByHeightOrHash returns the requested block by hash or height. 65 | // It uses cached data in the State struct (HashCache / HeightCache) 66 | func GetBlockByHeightOrHash(state *State, height uint64, hash, dataDir string) (BlockFS, error) { 67 | 68 | var block BlockFS 69 | 70 | key, ok := state.HeightCache[height] 71 | if hash != "" { 72 | key, ok = state.HashCache[hash] 73 | } 74 | 75 | if !ok { 76 | if hash != "" { 77 | return block, fmt.Errorf("invalid hash: '%v'", hash) 78 | } 79 | return block, fmt.Errorf("invalid height: '%v'", height) 80 | } 81 | 82 | f, err := os.OpenFile(getBlocksDbFilePath(dataDir), os.O_RDONLY, 0600) 83 | if err != nil { 84 | return block, err 85 | } 86 | defer f.Close() 87 | 88 | _, err = f.Seek(key, 0) 89 | if err != nil { 90 | return block, err 91 | } 92 | scanner := bufio.NewScanner(f) 93 | if scanner.Scan() { 94 | if err := scanner.Err(); err != nil { 95 | return block, err 96 | } 97 | err = json.Unmarshal(scanner.Bytes(), &block) 98 | if err != nil { 99 | return block, err 100 | } 101 | } 102 | 103 | return block, nil 104 | } 105 | -------------------------------------------------------------------------------- /database/fs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package database 17 | 18 | import ( 19 | "io/ioutil" 20 | "os" 21 | "path/filepath" 22 | ) 23 | 24 | func InitDataDirIfNotExists(dataDir string, genesis []byte) error { 25 | if fileExist(getGenesisJsonFilePath(dataDir)) { 26 | return nil 27 | } 28 | 29 | if err := os.MkdirAll(getDatabaseDirPath(dataDir), os.ModePerm); err != nil { 30 | return err 31 | } 32 | 33 | if err := writeGenesisToDisk(getGenesisJsonFilePath(dataDir), genesis); err != nil { 34 | return err 35 | } 36 | 37 | if err := writeEmptyBlocksDbToDisk(getBlocksDbFilePath(dataDir)); err != nil { 38 | return err 39 | } 40 | 41 | return nil 42 | } 43 | 44 | func getDatabaseDirPath(dataDir string) string { 45 | return filepath.Join(dataDir, "database") 46 | } 47 | 48 | func getGenesisJsonFilePath(dataDir string) string { 49 | return filepath.Join(getDatabaseDirPath(dataDir), "genesis.json") 50 | } 51 | 52 | func getBlocksDbFilePath(dataDir string) string { 53 | return filepath.Join(getDatabaseDirPath(dataDir), "block.db") 54 | } 55 | 56 | func fileExist(filePath string) bool { 57 | _, err := os.Stat(filePath) 58 | if err != nil && os.IsNotExist(err) { 59 | return false 60 | } 61 | 62 | return true 63 | } 64 | 65 | func dirExists(path string) (bool, error) { 66 | _, err := os.Stat(path) 67 | if err == nil { 68 | return true, nil 69 | } 70 | if os.IsNotExist(err) { 71 | return false, nil 72 | } 73 | 74 | return true, err 75 | } 76 | 77 | func writeEmptyBlocksDbToDisk(path string) error { 78 | return ioutil.WriteFile(path, []byte(""), os.ModePerm) 79 | } 80 | -------------------------------------------------------------------------------- /database/genesis.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package database 17 | 18 | import ( 19 | "encoding/json" 20 | "io/ioutil" 21 | 22 | "github.com/ethereum/go-ethereum/common" 23 | ) 24 | 25 | var genesisJson = `{ 26 | "genesis_time": "2020-06-01T00:00:00.000000000Z", 27 | "chain_id": "the-blockchain-bar-ledger", 28 | "symbol": "TBB", 29 | "balances": { 30 | "0x09eE50f2F37FcBA1845dE6FE5C762E83E65E755c": 1000000 31 | }, 32 | "fork_tip_1": 35 33 | }` 34 | 35 | type Genesis struct { 36 | Balances map[common.Address]uint `json:"balances"` 37 | Symbol string `json:"symbol"` 38 | 39 | ForkTIP1 uint64 `json:"fork_tip_1"` 40 | } 41 | 42 | func loadGenesis(path string) (Genesis, error) { 43 | content, err := ioutil.ReadFile(path) 44 | if err != nil { 45 | return Genesis{}, err 46 | } 47 | 48 | var loadedGenesis Genesis 49 | err = json.Unmarshal(content, &loadedGenesis) 50 | if err != nil { 51 | return Genesis{}, err 52 | } 53 | 54 | return loadedGenesis, nil 55 | } 56 | 57 | func writeGenesisToDisk(path string, genesis []byte) error { 58 | return ioutil.WriteFile(path, genesis, 0644) 59 | } 60 | -------------------------------------------------------------------------------- /database/state.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package database 17 | 18 | import ( 19 | "bufio" 20 | "encoding/json" 21 | "fmt" 22 | "os" 23 | "reflect" 24 | "sort" 25 | 26 | "github.com/ethereum/go-ethereum/common" 27 | ) 28 | 29 | const TxGas = 21 30 | const TxGasPriceDefault = 1 31 | const TxFee = uint(50) 32 | 33 | type State struct { 34 | Balances map[common.Address]uint 35 | Account2Nonce map[common.Address]uint 36 | 37 | dbFile *os.File 38 | 39 | latestBlock Block 40 | latestBlockHash Hash 41 | hasGenesisBlock bool 42 | 43 | miningDifficulty uint 44 | 45 | forkTIP1 uint64 46 | 47 | HashCache map[string]int64 48 | HeightCache map[uint64]int64 49 | } 50 | 51 | func NewStateFromDisk(dataDir string, miningDifficulty uint) (*State, error) { 52 | err := InitDataDirIfNotExists(dataDir, []byte(genesisJson)) 53 | if err != nil { 54 | return nil, err 55 | } 56 | 57 | gen, err := loadGenesis(getGenesisJsonFilePath(dataDir)) 58 | if err != nil { 59 | return nil, err 60 | } 61 | 62 | balances := make(map[common.Address]uint) 63 | for account, balance := range gen.Balances { 64 | balances[account] = balance 65 | } 66 | 67 | account2nonce := make(map[common.Address]uint) 68 | 69 | dbFilepath := getBlocksDbFilePath(dataDir) 70 | f, err := os.OpenFile(dbFilepath, os.O_APPEND|os.O_RDWR, 0600) 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | scanner := bufio.NewScanner(f) 76 | 77 | state := &State{balances, account2nonce, f, Block{}, Hash{}, false, miningDifficulty, gen.ForkTIP1, map[string]int64{}, map[uint64]int64{}} 78 | 79 | // set file position 80 | filePos := int64(0) 81 | 82 | for scanner.Scan() { 83 | if err := scanner.Err(); err != nil { 84 | return nil, err 85 | } 86 | 87 | blockFsJson := scanner.Bytes() 88 | 89 | if len(blockFsJson) == 0 { 90 | break 91 | } 92 | 93 | var blockFs BlockFS 94 | err = json.Unmarshal(blockFsJson, &blockFs) 95 | if err != nil { 96 | return nil, err 97 | } 98 | 99 | err = applyBlock(blockFs.Value, state) 100 | if err != nil { 101 | return nil, err 102 | } 103 | 104 | // set search caches 105 | state.HashCache[blockFs.Key.Hex()] = filePos 106 | state.HeightCache[blockFs.Value.Header.Number] = filePos 107 | filePos += int64(len(blockFsJson)) + 1 108 | 109 | state.latestBlock = blockFs.Value 110 | state.latestBlockHash = blockFs.Key 111 | state.hasGenesisBlock = true 112 | } 113 | 114 | return state, nil 115 | } 116 | 117 | func (s *State) AddBlocks(blocks []Block) error { 118 | for _, b := range blocks { 119 | _, err := s.AddBlock(b) 120 | if err != nil { 121 | return err 122 | } 123 | } 124 | 125 | return nil 126 | } 127 | 128 | func (s *State) AddBlock(b Block) (Hash, error) { 129 | pendingState := s.Copy() 130 | 131 | err := applyBlock(b, &pendingState) 132 | if err != nil { 133 | return Hash{}, err 134 | } 135 | 136 | blockHash, err := b.Hash() 137 | if err != nil { 138 | return Hash{}, err 139 | } 140 | 141 | blockFs := BlockFS{blockHash, b} 142 | 143 | blockFsJson, err := json.Marshal(blockFs) 144 | if err != nil { 145 | return Hash{}, err 146 | } 147 | 148 | fmt.Printf("\nPersisting new Block to disk:\n") 149 | fmt.Printf("\t%s\n", blockFsJson) 150 | 151 | // get file pos for cache 152 | fs, _ := s.dbFile.Stat() 153 | filePos := fs.Size() + 1 154 | 155 | _, err = s.dbFile.Write(append(blockFsJson, '\n')) 156 | if err != nil { 157 | return Hash{}, err 158 | } 159 | 160 | // set search caches 161 | s.HashCache[blockFs.Key.Hex()] = filePos 162 | s.HeightCache[blockFs.Value.Header.Number] = filePos 163 | 164 | s.Balances = pendingState.Balances 165 | s.Account2Nonce = pendingState.Account2Nonce 166 | s.latestBlockHash = blockHash 167 | s.latestBlock = b 168 | s.hasGenesisBlock = true 169 | s.miningDifficulty = pendingState.miningDifficulty 170 | 171 | return blockHash, nil 172 | } 173 | 174 | func (s *State) NextBlockNumber() uint64 { 175 | if !s.hasGenesisBlock { 176 | return uint64(0) 177 | } 178 | 179 | return s.LatestBlock().Header.Number + 1 180 | } 181 | 182 | func (s *State) LatestBlock() Block { 183 | return s.latestBlock 184 | } 185 | 186 | func (s *State) LatestBlockHash() Hash { 187 | return s.latestBlockHash 188 | } 189 | 190 | func (s *State) GetNextAccountNonce(account common.Address) uint { 191 | return s.Account2Nonce[account] + 1 192 | } 193 | 194 | func (s *State) ChangeMiningDifficulty(newDifficulty uint) { 195 | s.miningDifficulty = newDifficulty 196 | } 197 | 198 | func (s *State) IsTIP1Fork() bool { 199 | return s.NextBlockNumber() >= s.forkTIP1 200 | } 201 | 202 | func (s *State) Copy() State { 203 | c := State{} 204 | c.hasGenesisBlock = s.hasGenesisBlock 205 | c.latestBlock = s.latestBlock 206 | c.latestBlockHash = s.latestBlockHash 207 | c.Balances = make(map[common.Address]uint) 208 | c.Account2Nonce = make(map[common.Address]uint) 209 | c.miningDifficulty = s.miningDifficulty 210 | c.forkTIP1 = s.forkTIP1 211 | 212 | for acc, balance := range s.Balances { 213 | c.Balances[acc] = balance 214 | } 215 | 216 | for acc, nonce := range s.Account2Nonce { 217 | c.Account2Nonce[acc] = nonce 218 | } 219 | 220 | return c 221 | } 222 | 223 | func (s *State) Close() error { 224 | return s.dbFile.Close() 225 | } 226 | 227 | // applyBlock verifies if block can be added to the blockchain. 228 | // 229 | // Block metadata are verified as well as transactions within (sufficient balances, etc). 230 | func applyBlock(b Block, s *State) error { 231 | nextExpectedBlockNumber := s.latestBlock.Header.Number + 1 232 | 233 | if s.hasGenesisBlock && b.Header.Number != nextExpectedBlockNumber { 234 | return fmt.Errorf("next expected block must be '%d' not '%d'", nextExpectedBlockNumber, b.Header.Number) 235 | } 236 | 237 | if s.hasGenesisBlock && s.latestBlock.Header.Number > 0 && !reflect.DeepEqual(b.Header.Parent, s.latestBlockHash) { 238 | return fmt.Errorf("next block parent hash must be '%x' not '%x'", s.latestBlockHash, b.Header.Parent) 239 | } 240 | 241 | hash, err := b.Hash() 242 | if err != nil { 243 | return err 244 | } 245 | 246 | if !IsBlockHashValid(hash, s.miningDifficulty) { 247 | return fmt.Errorf("invalid block hash %x", hash) 248 | } 249 | 250 | err = applyTXs(b.TXs, s) 251 | if err != nil { 252 | return err 253 | } 254 | 255 | s.Balances[b.Header.Miner] += BlockReward 256 | if s.IsTIP1Fork() { 257 | s.Balances[b.Header.Miner] += b.GasReward() 258 | } else { 259 | s.Balances[b.Header.Miner] += uint(len(b.TXs)) * TxFee 260 | } 261 | 262 | return nil 263 | } 264 | 265 | func applyTXs(txs []SignedTx, s *State) error { 266 | sort.Slice(txs, func(i, j int) bool { 267 | return txs[i].Time < txs[j].Time 268 | }) 269 | 270 | for _, tx := range txs { 271 | err := ApplyTx(tx, s) 272 | if err != nil { 273 | return err 274 | } 275 | } 276 | 277 | return nil 278 | } 279 | 280 | func ApplyTx(tx SignedTx, s *State) error { 281 | err := ValidateTx(tx, s) 282 | if err != nil { 283 | return err 284 | } 285 | 286 | s.Balances[tx.From] -= tx.Cost(s.IsTIP1Fork()) 287 | s.Balances[tx.To] += tx.Value 288 | 289 | s.Account2Nonce[tx.From] = tx.Nonce 290 | 291 | return nil 292 | } 293 | 294 | func ValidateTx(tx SignedTx, s *State) error { 295 | ok, err := tx.IsAuthentic() 296 | if err != nil { 297 | return err 298 | } 299 | 300 | if !ok { 301 | return fmt.Errorf("wrong TX. Sender '%s' is forged", tx.From.String()) 302 | } 303 | 304 | expectedNonce := s.GetNextAccountNonce(tx.From) 305 | if tx.Nonce != expectedNonce { 306 | return fmt.Errorf("wrong TX. Sender '%s' next nonce must be '%d', not '%d'", tx.From.String(), expectedNonce, tx.Nonce) 307 | } 308 | 309 | if s.IsTIP1Fork() { 310 | // For now we only have one type, transfer TXs, so all TXs must pay 21 gas like on Ethereum (21 000) 311 | if tx.Gas != TxGas { 312 | return fmt.Errorf("insufficient TX gas %v. required: %v", tx.Gas, TxGas) 313 | } 314 | 315 | if tx.GasPrice < TxGasPriceDefault { 316 | return fmt.Errorf("insufficient TX gasPrice %v. required at least: %v", tx.GasPrice, TxGasPriceDefault) 317 | } 318 | 319 | } else { 320 | // Prior to TIP1, a signed TX must NOT populate the Gas fields to prevent consensus from crashing 321 | // It's not enough to add this validation to http_routes.go because a TX could come from another node 322 | // that could modify its software and broadcast such a TX, it must be validated here too. 323 | if tx.Gas != 0 || tx.GasPrice != 0 { 324 | return fmt.Errorf("invalid TX. `Gas` and `GasPrice` can't be populated before TIP1 fork is active") 325 | } 326 | } 327 | 328 | if tx.Cost(s.IsTIP1Fork()) > s.Balances[tx.From] { 329 | return fmt.Errorf("wrong TX. Sender '%s' balance is %d TBB. Tx cost is %d TBB", tx.From.String(), s.Balances[tx.From], tx.Cost(s.IsTIP1Fork())) 330 | } 331 | 332 | return nil 333 | } 334 | -------------------------------------------------------------------------------- /database/state.json: -------------------------------------------------------------------------------- 1 | { 2 | "balances": { 3 | "andrej": 998801, 4 | "babayaga": 1999 5 | } 6 | } -------------------------------------------------------------------------------- /database/tx.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package database 17 | 18 | import ( 19 | "crypto/elliptic" 20 | "crypto/sha256" 21 | "encoding/json" 22 | "time" 23 | 24 | "github.com/ethereum/go-ethereum/common" 25 | "github.com/ethereum/go-ethereum/crypto" 26 | ) 27 | 28 | func NewAccount(value string) common.Address { 29 | return common.HexToAddress(value) 30 | } 31 | 32 | type Tx struct { 33 | From common.Address `json:"from"` 34 | To common.Address `json:"to"` 35 | Gas uint `json:"gas"` 36 | GasPrice uint `json:"gasPrice"` 37 | Value uint `json:"value"` 38 | Nonce uint `json:"nonce"` 39 | Data string `json:"data"` 40 | Time uint64 `json:"time"` 41 | } 42 | 43 | type SignedTx struct { 44 | Tx 45 | Sig []byte `json:"signature"` 46 | } 47 | 48 | func NewTx(from, to common.Address, gas uint, gasPrice uint, value, nonce uint, data string) Tx { 49 | return Tx{from, to, gas, gasPrice, value, nonce, data, uint64(time.Now().Unix())} 50 | } 51 | 52 | func NewBaseTx(from, to common.Address, value, nonce uint, data string) Tx { 53 | return NewTx(from, to, TxGas, TxGasPriceDefault, value, nonce, data) 54 | } 55 | 56 | func NewSignedTx(tx Tx, sig []byte) SignedTx { 57 | return SignedTx{tx, sig} 58 | } 59 | 60 | func (t Tx) IsReward() bool { 61 | return t.Data == "reward" 62 | } 63 | 64 | func (t Tx) Cost(isTip1Fork bool) uint { 65 | if isTip1Fork { 66 | return t.Value + t.GasCost() 67 | } 68 | 69 | return t.Value + TxFee 70 | } 71 | 72 | func (t Tx) GasCost() uint { 73 | return t.Gas * t.GasPrice 74 | } 75 | 76 | func (t Tx) Hash() (Hash, error) { 77 | txJson, err := t.Encode() 78 | if err != nil { 79 | return Hash{}, err 80 | } 81 | 82 | return sha256.Sum256(txJson), nil 83 | } 84 | 85 | func (t Tx) Encode() ([]byte, error) { 86 | return json.Marshal(t) 87 | } 88 | 89 | // MarshalJSON is the main source of truth for encoding a TX for hash calculation from expected attributes. 90 | // 91 | // The logic is bit ugly and hacky but prevents infinite marshaling loops of embedded objects and allows 92 | // the structure to change with new TIPs. 93 | func (t Tx) MarshalJSON() ([]byte, error) { 94 | // Prior TIP1 95 | if t.Gas == 0 { 96 | type legacyTx struct { 97 | From common.Address `json:"from"` 98 | To common.Address `json:"to"` 99 | Value uint `json:"value"` 100 | Nonce uint `json:"nonce"` 101 | Data string `json:"data"` 102 | Time uint64 `json:"time"` 103 | } 104 | return json.Marshal(legacyTx{ 105 | From: t.From, 106 | To: t.To, 107 | Value: t.Value, 108 | Nonce: t.Nonce, 109 | Data: t.Data, 110 | Time: t.Time, 111 | }) 112 | } 113 | 114 | type tip1Tx struct { 115 | From common.Address `json:"from"` 116 | To common.Address `json:"to"` 117 | Gas uint `json:"gas"` 118 | GasPrice uint `json:"gasPrice"` 119 | Value uint `json:"value"` 120 | Nonce uint `json:"nonce"` 121 | Data string `json:"data"` 122 | Time uint64 `json:"time"` 123 | } 124 | 125 | return json.Marshal(tip1Tx{ 126 | From: t.From, 127 | To: t.To, 128 | Gas: t.Gas, 129 | GasPrice: t.GasPrice, 130 | Value: t.Value, 131 | Nonce: t.Nonce, 132 | Data: t.Data, 133 | Time: t.Time, 134 | }) 135 | } 136 | 137 | // MarshalJSON is the main source of truth for encoding a TX for hash calculation (backwards compatible for TIPs). 138 | // 139 | // The logic is bit ugly and hacky but prevents infinite marshaling loops of embedded objects and allows 140 | // the structure to change with new TIPs. 141 | func (t SignedTx) MarshalJSON() ([]byte, error) { 142 | // Prior TIP1 143 | if t.Gas == 0 { 144 | type legacyTx struct { 145 | From common.Address `json:"from"` 146 | To common.Address `json:"to"` 147 | Value uint `json:"value"` 148 | Nonce uint `json:"nonce"` 149 | Data string `json:"data"` 150 | Time uint64 `json:"time"` 151 | Sig []byte `json:"signature"` 152 | } 153 | return json.Marshal(legacyTx{ 154 | From: t.From, 155 | To: t.To, 156 | Value: t.Value, 157 | Nonce: t.Nonce, 158 | Data: t.Data, 159 | Time: t.Time, 160 | Sig: t.Sig, 161 | }) 162 | } 163 | 164 | type tip1Tx struct { 165 | From common.Address `json:"from"` 166 | To common.Address `json:"to"` 167 | Gas uint `json:"gas"` 168 | GasPrice uint `json:"gasPrice"` 169 | Value uint `json:"value"` 170 | Nonce uint `json:"nonce"` 171 | Data string `json:"data"` 172 | Time uint64 `json:"time"` 173 | Sig []byte `json:"signature"` 174 | } 175 | 176 | return json.Marshal(tip1Tx{ 177 | From: t.From, 178 | To: t.To, 179 | Gas: t.Gas, 180 | GasPrice: t.GasPrice, 181 | Value: t.Value, 182 | Nonce: t.Nonce, 183 | Data: t.Data, 184 | Time: t.Time, 185 | Sig: t.Sig, 186 | }) 187 | } 188 | 189 | func (t SignedTx) Hash() (Hash, error) { 190 | txJson, err := t.Encode() 191 | if err != nil { 192 | return Hash{}, err 193 | } 194 | 195 | return sha256.Sum256(txJson), nil 196 | } 197 | 198 | func (t SignedTx) IsAuthentic() (bool, error) { 199 | txHash, err := t.Tx.Hash() 200 | if err != nil { 201 | return false, err 202 | } 203 | 204 | recoveredPubKey, err := crypto.SigToPub(txHash[:], t.Sig) 205 | if err != nil { 206 | return false, err 207 | } 208 | 209 | recoveredPubKeyBytes := elliptic.Marshal(crypto.S256(), recoveredPubKey.X, recoveredPubKey.Y) 210 | recoveredPubKeyBytesHash := crypto.Keccak256(recoveredPubKeyBytes[1:]) 211 | recoveredAccount := common.BytesToAddress(recoveredPubKeyBytesHash[12:]) 212 | 213 | return recoveredAccount.Hex() == t.From.Hex(), nil 214 | } 215 | -------------------------------------------------------------------------------- /docker-compose.yaml: -------------------------------------------------------------------------------- 1 | version: "3.5" 2 | services: 3 | tbb-local: 4 | build: 5 | context: ./ 6 | entrypoint: /bin/sh 7 | command: ["-c", "bash"] 8 | volumes: 9 | - ./:/build 10 | 11 | -------------------------------------------------------------------------------- /fs/fs.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package fs 17 | 18 | import ( 19 | "os" 20 | "os/user" 21 | "path" 22 | "strings" 23 | ) 24 | 25 | // Expands a file path 26 | // 1. replace tilde with users home dir 27 | // 2. expands embedded environment variables 28 | // 3. cleans the path, e.g. /a/b/../c -> /a/c 29 | // Note, it has limitations, e.g. ~someuser/tmp will not be expanded 30 | func ExpandPath(p string) string { 31 | if i := strings.Index(p, ":"); i > 0 { 32 | return p 33 | } 34 | if i := strings.Index(p, "@"); i > 0 { 35 | return p 36 | } 37 | if strings.HasPrefix(p, "~/") || strings.HasPrefix(p, "~\\") { 38 | if home := homeDir(); home != "" { 39 | p = home + p[1:] 40 | } 41 | } 42 | return path.Clean(os.ExpandEnv(p)) 43 | } 44 | 45 | func RemoveDir(path string) error { 46 | return os.RemoveAll(path) 47 | } 48 | 49 | func homeDir() string { 50 | if home := os.Getenv("HOME"); home != "" { 51 | return home 52 | } 53 | if usr, err := user.Current(); err == nil { 54 | return usr.HomeDir 55 | } 56 | return "" 57 | } 58 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/web3coach/the-blockchain-bar 2 | 3 | go 1.16 4 | 5 | require ( 6 | github.com/aristanetworks/goarista v0.0.0-20200211191935-58c705f5cf52 // indirect 7 | github.com/btcsuite/btcd v0.20.1-beta // indirect 8 | github.com/caddyserver/certmagic v0.11.2 9 | github.com/davecgh/go-spew v1.1.1 10 | github.com/ethereum/go-ethereum v1.9.25 11 | github.com/inconshreveable/mousetrap v1.0.0 // indirect 12 | github.com/pborman/uuid v0.0.0-20170112150404-1b00554d8222 13 | github.com/spf13/cobra v0.0.3 14 | github.com/spf13/pflag v1.0.3 // indirect 15 | github.com/stretchr/testify v1.5.1 16 | ) 17 | -------------------------------------------------------------------------------- /node/block_explorer_integration_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net/http" 7 | "net/http/httptest" 8 | "testing" 9 | 10 | "github.com/web3coach/the-blockchain-bar/database" 11 | ) 12 | 13 | func TestBlockExplorer(t *testing.T) { 14 | 15 | tc := []struct { 16 | arg string 17 | want uint64 18 | }{ 19 | {"12", 12}, 20 | {"34", 34}, 21 | {"0000003dc60b50d8f98e5e49f1cf520a84f95f51890849b1ac37eda6c07718df", 8}, 22 | {"000000244ab3ada6479fd06f0eb81b3b97051859191380758cc546bfe2074759", 2}, 23 | {"99", 99}, // this must return http.Status != 200 24 | } 25 | datadir := "test_block_explorer_db" 26 | 27 | n := New(datadir, "127.0.0.1", 8085, database.NewAccount(DefaultMiner), PeerNode{}, nodeVersion, 3) 28 | 29 | t.Log(fmt.Sprintf("Listening on: %s:%d", n.info.IP, n.info.Port)) 30 | 31 | state, err := database.NewStateFromDisk(n.dataDir, n.miningDifficulty) 32 | if err != nil { 33 | t.Fatal(err) 34 | } 35 | defer state.Close() 36 | 37 | n.state = state 38 | 39 | pendingState := state.Copy() 40 | n.pendingState = &pendingState 41 | 42 | t.Log("Blockchain state:") 43 | t.Logf(" - height: %d\n", n.state.LatestBlock().Header.Number) 44 | t.Logf(" - hash: %s\n", n.state.LatestBlockHash().Hex()) 45 | 46 | for _, tc := range tc { 47 | rr := httptest.NewRecorder() 48 | req, _ := http.NewRequest(http.MethodGet, "/block/"+tc.arg, nil) 49 | 50 | func(w http.ResponseWriter, r *http.Request, node *Node) { 51 | blockByNumberOrHash(w, r, node) 52 | }(rr, req, n) 53 | 54 | if rr.Code != http.StatusOK { 55 | if tc.want == 99 { // this is an error case, so continue 56 | continue 57 | } 58 | t.Error("unexpected status code: ", rr.Code, rr.Body.String()) 59 | } 60 | 61 | resp := new(database.BlockFS) 62 | dec := json.NewDecoder(rr.Body) 63 | err = dec.Decode(resp) 64 | if err != nil { 65 | t.Error("error decoding", err) 66 | } 67 | 68 | got := resp.Value.Header.Number 69 | if got != tc.want { 70 | t.Errorf("block explorer(%q) = %v; want %v", tc.arg, got, tc.want) 71 | } 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /node/http_req_res_utils.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package node 17 | 18 | import ( 19 | "encoding/json" 20 | "fmt" 21 | "io/ioutil" 22 | "net/http" 23 | ) 24 | 25 | func writeErrRes(w http.ResponseWriter, err error) { 26 | jsonErrRes, _ := json.Marshal(ErrRes{err.Error()}) 27 | w.Header().Set("Content-Type", "application/json") 28 | w.WriteHeader(http.StatusInternalServerError) 29 | w.Write(jsonErrRes) 30 | } 31 | 32 | func writeRes(w http.ResponseWriter, content interface{}) { 33 | contentJson, err := json.Marshal(content) 34 | if err != nil { 35 | writeErrRes(w, err) 36 | return 37 | } 38 | 39 | w.Header().Set("Content-Type", "application/json") 40 | w.WriteHeader(http.StatusOK) 41 | w.Write(contentJson) 42 | } 43 | 44 | func readReq(r *http.Request, reqBody interface{}) error { 45 | reqBodyJson, err := ioutil.ReadAll(r.Body) 46 | if err != nil { 47 | return fmt.Errorf("unable to read request body. %s", err.Error()) 48 | } 49 | defer r.Body.Close() 50 | 51 | err = json.Unmarshal(reqBodyJson, reqBody) 52 | if err != nil { 53 | return fmt.Errorf("unable to unmarshal request body. %s", err.Error()) 54 | } 55 | 56 | return nil 57 | } 58 | 59 | func readRes(r *http.Response, reqBody interface{}) error { 60 | resBodyJson, err := ioutil.ReadAll(r.Body) 61 | if err != nil { 62 | return fmt.Errorf("unable to read response body. %s", err.Error()) 63 | } 64 | defer r.Body.Close() 65 | 66 | if r.StatusCode != http.StatusOK { 67 | return fmt.Errorf("unable to process response. %s", string(resBodyJson)) 68 | } 69 | 70 | err = json.Unmarshal(resBodyJson, reqBody) 71 | if err != nil { 72 | return fmt.Errorf("unable to unmarshal response body. %s", err.Error()) 73 | } 74 | 75 | return nil 76 | } 77 | 78 | func enableCors(w *http.ResponseWriter) { 79 | (*w).Header().Set("Access-Control-Allow-Origin", "*") 80 | } 81 | -------------------------------------------------------------------------------- /node/http_routes.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package node 17 | 18 | import ( 19 | "errors" 20 | "fmt" 21 | "net/http" 22 | "strconv" 23 | "strings" 24 | 25 | "github.com/ethereum/go-ethereum/common" 26 | "github.com/web3coach/the-blockchain-bar/database" 27 | "github.com/web3coach/the-blockchain-bar/wallet" 28 | ) 29 | 30 | type ErrRes struct { 31 | Error string `json:"error"` 32 | } 33 | 34 | type BalancesRes struct { 35 | Hash database.Hash `json:"block_hash"` 36 | Balances map[common.Address]uint `json:"balances"` 37 | } 38 | 39 | type TxAddReq struct { 40 | From string `json:"from"` 41 | FromPwd string `json:"from_pwd"` 42 | To string `json:"to"` 43 | Gas uint `json:"gas"` 44 | GasPrice uint `json:"gasPrice"` 45 | Value uint `json:"value"` 46 | Data string `json:"data"` 47 | } 48 | 49 | type TxAddRes struct { 50 | Success bool `json:"success"` 51 | } 52 | 53 | type StatusRes struct { 54 | Hash database.Hash `json:"block_hash"` 55 | Number uint64 `json:"block_number"` 56 | KnownPeers map[string]PeerNode `json:"peers_known"` 57 | PendingTXs []database.SignedTx `json:"pending_txs"` 58 | NodeVersion string `json:"node_version"` 59 | Account common.Address `json:"account"` 60 | } 61 | 62 | type SyncRes struct { 63 | Blocks []database.Block `json:"blocks"` 64 | } 65 | 66 | type AddPeerRes struct { 67 | Success bool `json:"success"` 68 | Error string `json:"error"` 69 | } 70 | 71 | func listBalancesHandler(w http.ResponseWriter, r *http.Request, state *database.State) { 72 | enableCors(&w) 73 | 74 | writeRes(w, BalancesRes{state.LatestBlockHash(), state.Balances}) 75 | } 76 | 77 | func txAddHandler(w http.ResponseWriter, r *http.Request, node *Node) { 78 | req := TxAddReq{} 79 | err := readReq(r, &req) 80 | if err != nil { 81 | writeErrRes(w, err) 82 | return 83 | } 84 | 85 | from := database.NewAccount(req.From) 86 | 87 | if from.String() == common.HexToAddress("").String() { 88 | writeErrRes(w, fmt.Errorf("%s is an invalid 'from' sender", from.String())) 89 | return 90 | } 91 | 92 | if req.FromPwd == "" { 93 | writeErrRes(w, fmt.Errorf("password to decrypt the %s account is required. 'from_pwd' is empty", from.String())) 94 | return 95 | } 96 | 97 | nonce := node.state.GetNextAccountNonce(from) 98 | tx := database.NewTx(from, database.NewAccount(req.To), req.Gas, req.GasPrice, req.Value, nonce, req.Data) 99 | 100 | signedTx, err := wallet.SignTxWithKeystoreAccount(tx, from, req.FromPwd, wallet.GetKeystoreDirPath(node.dataDir)) 101 | if err != nil { 102 | writeErrRes(w, err) 103 | return 104 | } 105 | 106 | err = node.AddPendingTX(signedTx, node.info) 107 | if err != nil { 108 | writeErrRes(w, err) 109 | return 110 | } 111 | 112 | writeRes(w, TxAddRes{Success: true}) 113 | } 114 | 115 | func statusHandler(w http.ResponseWriter, r *http.Request, node *Node) { 116 | enableCors(&w) 117 | 118 | res := StatusRes{ 119 | Hash: node.state.LatestBlockHash(), 120 | Number: node.state.LatestBlock().Header.Number, 121 | KnownPeers: node.knownPeers, 122 | PendingTXs: node.getPendingTXsAsArray(), 123 | NodeVersion: node.nodeVersion, 124 | Account: database.NewAccount(node.info.Account.String()), 125 | } 126 | 127 | writeRes(w, res) 128 | } 129 | 130 | func syncHandler(w http.ResponseWriter, r *http.Request, node *Node) { 131 | reqHash := r.URL.Query().Get(endpointSyncQueryKeyFromBlock) 132 | 133 | hash := database.Hash{} 134 | err := hash.UnmarshalText([]byte(reqHash)) 135 | if err != nil { 136 | writeErrRes(w, err) 137 | return 138 | } 139 | 140 | blocks, err := database.GetBlocksAfter(hash, node.dataDir) 141 | if err != nil { 142 | writeErrRes(w, err) 143 | return 144 | } 145 | 146 | writeRes(w, SyncRes{Blocks: blocks}) 147 | } 148 | 149 | func addPeerHandler(w http.ResponseWriter, r *http.Request, node *Node) { 150 | peerIP := r.URL.Query().Get(endpointAddPeerQueryKeyIP) 151 | peerPortRaw := r.URL.Query().Get(endpointAddPeerQueryKeyPort) 152 | minerRaw := r.URL.Query().Get(endpointAddPeerQueryKeyMiner) 153 | versionRaw := r.URL.Query().Get(endpointAddPeerQueryKeyVersion) 154 | 155 | peerPort, err := strconv.ParseUint(peerPortRaw, 10, 32) 156 | if err != nil { 157 | writeRes(w, AddPeerRes{false, err.Error()}) 158 | return 159 | } 160 | 161 | peer := NewPeerNode(peerIP, peerPort, false, database.NewAccount(minerRaw), true, versionRaw) 162 | 163 | node.AddPeer(peer) 164 | 165 | fmt.Printf("Peer '%s' was added into KnownPeers\n", peer.TcpAddress()) 166 | 167 | writeRes(w, AddPeerRes{true, ""}) 168 | } 169 | 170 | func blockByNumberOrHash(w http.ResponseWriter, r *http.Request, node *Node) { 171 | enableCors(&w) 172 | 173 | errorParamsRequired := errors.New("height or hash param is required") 174 | 175 | params := strings.Split(r.URL.Path, "/")[1:] 176 | if len(params) < 2 { 177 | writeErrRes(w, errorParamsRequired) 178 | return 179 | } 180 | 181 | p := strings.TrimSpace(params[1]) 182 | if len(p) == 0 { 183 | writeErrRes(w, errorParamsRequired) 184 | return 185 | } 186 | hsh := "" 187 | height, err := strconv.ParseUint(p, 10, 64) 188 | if err != nil { 189 | hsh = p 190 | } 191 | 192 | block, err := database.GetBlockByHeightOrHash(node.state, height, hsh, node.dataDir) 193 | if err != nil { 194 | writeErrRes(w, err) 195 | return 196 | } 197 | 198 | writeRes(w, block) 199 | } 200 | 201 | func mempoolViewer(w http.ResponseWriter, r *http.Request, txs map[string]database.SignedTx) { 202 | enableCors(&w) 203 | 204 | writeRes(w, txs) 205 | } 206 | -------------------------------------------------------------------------------- /node/mempool_view_integration_test.go: -------------------------------------------------------------------------------- 1 | package node 2 | 3 | import ( 4 | "encoding/base64" 5 | "encoding/json" 6 | "net/http" 7 | "net/http/httptest" 8 | "reflect" 9 | "testing" 10 | 11 | "github.com/ethereum/go-ethereum/common" 12 | "github.com/web3coach/the-blockchain-bar/database" 13 | "github.com/web3coach/the-blockchain-bar/fs" 14 | "github.com/web3coach/the-blockchain-bar/wallet" 15 | ) 16 | 17 | func TestNode_MempoolViewer(t *testing.T) { 18 | 19 | babaYaga := database.NewAccount(testKsBabaYagaAccount) 20 | andrej := database.NewAccount(testKsAndrejAccount) 21 | 22 | // test cases 23 | poolLen := 3 24 | txn3From := babaYaga 25 | 26 | dataDir, err := getTestDataDirPath() 27 | if err != nil { 28 | t.Fatal(err) 29 | } 30 | 31 | genesisBalances := make(map[common.Address]uint) 32 | genesisBalances[andrej] = 1000000 33 | genesisBalances[babaYaga] = 1000000 34 | genesis := database.Genesis{Balances: genesisBalances, ForkTIP1: 0} 35 | genesisJson, err := json.Marshal(genesis) 36 | if err != nil { 37 | t.Fatal(err) 38 | } 39 | 40 | err = database.InitDataDirIfNotExists(dataDir, genesisJson) 41 | defer fs.RemoveDir(dataDir) 42 | 43 | err = copyKeystoreFilesIntoTestDataDirPath(dataDir) 44 | if err != nil { 45 | t.Fatal(err) 46 | } 47 | 48 | // Required for AddPendingTX() to describe 49 | // from what node the TX came from (local node in this case) 50 | nInfo := NewPeerNode( 51 | "127.0.0.1", 52 | 8085, 53 | false, 54 | database.NewAccount(""), 55 | true, 56 | nodeVersion, 57 | ) 58 | 59 | // Start mining with a high mining difficulty, just to be slow on purpose and let a synced block arrive first 60 | n := New(dataDir, nInfo.IP, nInfo.Port, babaYaga, nInfo, nodeVersion, uint(5)) 61 | 62 | state, err := database.NewStateFromDisk(n.dataDir, n.miningDifficulty) 63 | if err != nil { 64 | t.Fatal(err) 65 | } 66 | defer state.Close() 67 | 68 | n.state = state 69 | 70 | pendingState := state.Copy() 71 | n.pendingState = &pendingState 72 | 73 | tx1 := database.NewBaseTx(andrej, babaYaga, 1, 1, "") 74 | tx2 := database.NewBaseTx(andrej, babaYaga, 2, 2, "") 75 | tx3 := database.NewBaseTx(babaYaga, andrej, 1, 1, "") 76 | 77 | signedTx1, err := wallet.SignTxWithKeystoreAccount(tx1, andrej, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 78 | if err != nil { 79 | t.Error(err) 80 | return 81 | } 82 | 83 | signedTx2, err := wallet.SignTxWithKeystoreAccount(tx2, andrej, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 84 | if err != nil { 85 | t.Error(err) 86 | return 87 | } 88 | 89 | signedTx3, err := wallet.SignTxWithKeystoreAccount(tx3, babaYaga, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 90 | if err != nil { 91 | t.Error(err) 92 | return 93 | } 94 | 95 | // Add 3 new TXs 96 | err = n.AddPendingTX(signedTx1, nInfo) 97 | if err != nil { 98 | t.Fatal(err) 99 | } 100 | err = n.AddPendingTX(signedTx2, nInfo) 101 | if err != nil { 102 | t.Fatal(err) 103 | } 104 | err = n.AddPendingTX(signedTx3, nInfo) 105 | if err != nil { 106 | t.Fatal(err) 107 | } 108 | 109 | rr := httptest.NewRecorder() 110 | req, _ := http.NewRequest(http.MethodGet, "/mempool/", nil) 111 | 112 | func(w http.ResponseWriter, r *http.Request, node *Node) { 113 | mempoolViewer(w, r, node.pendingTXs) 114 | }(rr, req, n) 115 | 116 | if rr.Code != http.StatusOK { 117 | t.Fatal("unexpected status code: ", rr.Code, rr.Body.String()) 118 | } 119 | 120 | var resp map[string]database.SignedTx 121 | dec := json.NewDecoder(rr.Body) 122 | err = dec.Decode(&resp) 123 | if err != nil { 124 | t.Fatal("error decoding", err) 125 | } 126 | 127 | // check pool length 128 | if len(resp) != poolLen { 129 | t.Fatalf("mempool viewer reponse len wrong, got %v; want %v", len(resp), poolLen) 130 | } 131 | 132 | for _, v := range resp { 133 | // check for third case 134 | if v.From.Hex() == txn3From.Hex() { 135 | if !reflect.DeepEqual(signedTx3.Sig, v.Sig) { 136 | t.Errorf("invalid signature for txn, got %q, want %q", base64.StdEncoding.EncodeToString(v.Sig), base64.StdEncoding.EncodeToString(signedTx3.Sig)) 137 | } 138 | } 139 | } 140 | } 141 | -------------------------------------------------------------------------------- /node/miner.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package node 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "math/rand" 22 | "time" 23 | 24 | "github.com/ethereum/go-ethereum/common" 25 | "github.com/web3coach/the-blockchain-bar/database" 26 | ) 27 | 28 | type PendingBlock struct { 29 | parent database.Hash 30 | number uint64 31 | time uint64 32 | miner common.Address 33 | txs []database.SignedTx 34 | } 35 | 36 | func NewPendingBlock(parent database.Hash, number uint64, miner common.Address, txs []database.SignedTx) PendingBlock { 37 | return PendingBlock{parent, number, uint64(time.Now().Unix()), miner, txs} 38 | } 39 | 40 | func Mine(ctx context.Context, pb PendingBlock, miningDifficulty uint) (database.Block, error) { 41 | if len(pb.txs) == 0 { 42 | return database.Block{}, fmt.Errorf("mining empty blocks is not allowed") 43 | } 44 | 45 | start := time.Now() 46 | attempt := 0 47 | var block database.Block 48 | var hash database.Hash 49 | var nonce uint32 50 | 51 | for !database.IsBlockHashValid(hash, miningDifficulty) { 52 | select { 53 | case <-ctx.Done(): 54 | fmt.Println("Mining cancelled!") 55 | 56 | return database.Block{}, fmt.Errorf("mining cancelled. %s", ctx.Err()) 57 | default: 58 | } 59 | 60 | attempt++ 61 | nonce = generateNonce() 62 | 63 | if attempt%1000000 == 0 || attempt == 1 { 64 | fmt.Printf("Mining %d Pending TXs. Attempt: %d\n", len(pb.txs), attempt) 65 | } 66 | 67 | block = database.NewBlock(pb.parent, pb.number, nonce, pb.time, pb.miner, pb.txs) 68 | blockHash, err := block.Hash() 69 | if err != nil { 70 | return database.Block{}, fmt.Errorf("couldn't mine block. %s", err.Error()) 71 | } 72 | 73 | hash = blockHash 74 | } 75 | 76 | fmt.Printf("\nMined new Block '%x' using PoW 🎉🎉🎉\n", hash) 77 | fmt.Printf("\tHeight: '%v'\n", block.Header.Number) 78 | fmt.Printf("\tNonce: '%v'\n", block.Header.Nonce) 79 | fmt.Printf("\tCreated: '%v'\n", block.Header.Time) 80 | fmt.Printf("\tMiner: '%v'\n", block.Header.Miner.String()) 81 | fmt.Printf("\tParent: '%v'\n\n", block.Header.Parent.Hex()) 82 | 83 | fmt.Printf("\tAttempt: '%v'\n", attempt) 84 | fmt.Printf("\tTime: %s\n\n", time.Since(start)) 85 | 86 | return block, nil 87 | } 88 | 89 | func generateNonce() uint32 { 90 | rand.Seed(time.Now().UTC().UnixNano()) 91 | 92 | return rand.Uint32() 93 | } 94 | -------------------------------------------------------------------------------- /node/miner_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package node 17 | 18 | import ( 19 | "context" 20 | "crypto/ecdsa" 21 | "crypto/elliptic" 22 | "crypto/rand" 23 | "encoding/hex" 24 | "testing" 25 | "time" 26 | 27 | "github.com/ethereum/go-ethereum/common" 28 | "github.com/ethereum/go-ethereum/crypto" 29 | "github.com/web3coach/the-blockchain-bar/database" 30 | "github.com/web3coach/the-blockchain-bar/wallet" 31 | ) 32 | 33 | const defaultTestMiningDifficulty = 2 34 | 35 | func TestValidBlockHash(t *testing.T) { 36 | hexHash := "0000fa04f8160395c387277f8b2f14837603383d33809a4db586086168edfa" 37 | var hash = database.Hash{} 38 | 39 | hex.Decode(hash[:], []byte(hexHash)) 40 | 41 | isValid := database.IsBlockHashValid(hash, defaultTestMiningDifficulty) 42 | if !isValid { 43 | t.Fatalf("hash '%s' starting with 4 zeroes is suppose to be valid", hexHash) 44 | } 45 | } 46 | 47 | func TestInvalidBlockHash(t *testing.T) { 48 | hexHash := "0001fa04f8160395c387277f8b2f14837603383d33809a4db586086168edfa" 49 | var hash = database.Hash{} 50 | 51 | hex.Decode(hash[:], []byte(hexHash)) 52 | 53 | isValid := database.IsBlockHashValid(hash, defaultTestMiningDifficulty) 54 | if isValid { 55 | t.Fatal("hash is not suppose to be valid") 56 | } 57 | } 58 | 59 | func TestMine(t *testing.T) { 60 | minerPrivKey, _, miner, err := generateKey() 61 | if err != nil { 62 | t.Fatal(err) 63 | } 64 | 65 | pendingBlock, err := createRandomPendingBlock(minerPrivKey, miner) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | 70 | ctx := context.Background() 71 | 72 | minedBlock, err := Mine(ctx, pendingBlock, defaultTestMiningDifficulty) 73 | if err != nil { 74 | t.Fatal(err) 75 | } 76 | 77 | minedBlockHash, err := minedBlock.Hash() 78 | if err != nil { 79 | t.Fatal(err) 80 | } 81 | 82 | if !database.IsBlockHashValid(minedBlockHash, defaultTestMiningDifficulty) { 83 | t.Fatal() 84 | } 85 | 86 | if minedBlock.Header.Miner.String() != miner.String() { 87 | t.Fatal("mined block miner should equal miner from pending block") 88 | } 89 | } 90 | 91 | func TestMineWithTimeout(t *testing.T) { 92 | minerPrivKey, _, miner, err := generateKey() 93 | if err != nil { 94 | t.Fatal(err) 95 | } 96 | 97 | pendingBlock, err := createRandomPendingBlock(minerPrivKey, miner) 98 | if err != nil { 99 | t.Fatal(err) 100 | } 101 | 102 | ctx, _ := context.WithTimeout(context.Background(), time.Microsecond*100) 103 | 104 | _, err = Mine(ctx, pendingBlock, defaultTestMiningDifficulty) 105 | if err == nil { 106 | t.Fatal(err) 107 | } 108 | } 109 | 110 | func generateKey() (*ecdsa.PrivateKey, ecdsa.PublicKey, common.Address, error) { 111 | privKey, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader) 112 | if err != nil { 113 | return nil, ecdsa.PublicKey{}, common.Address{}, err 114 | } 115 | 116 | pubKey := privKey.PublicKey 117 | pubKeyBytes := elliptic.Marshal(crypto.S256(), pubKey.X, pubKey.Y) 118 | pubKeyBytesHash := crypto.Keccak256(pubKeyBytes[1:]) 119 | 120 | account := common.BytesToAddress(pubKeyBytesHash[12:]) 121 | 122 | return privKey, pubKey, account, nil 123 | } 124 | 125 | func createRandomPendingBlock(privKey *ecdsa.PrivateKey, acc common.Address) (PendingBlock, error) { 126 | tx := database.NewBaseTx(acc, database.NewAccount(testKsBabaYagaAccount), 1, 1, "") 127 | signedTx, err := wallet.SignTx(tx, privKey) 128 | if err != nil { 129 | return PendingBlock{}, err 130 | } 131 | 132 | return NewPendingBlock( 133 | database.Hash{}, 134 | 0, 135 | acc, 136 | []database.SignedTx{signedTx}, 137 | ), nil 138 | } 139 | -------------------------------------------------------------------------------- /node/node.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package node 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "fmt" 22 | "net/http" 23 | "time" 24 | 25 | "github.com/caddyserver/certmagic" 26 | "github.com/ethereum/go-ethereum/common" 27 | 28 | "github.com/web3coach/the-blockchain-bar/database" 29 | ) 30 | 31 | const DefaultBootstrapIp = "node.tbb.web3.coach" 32 | 33 | // The Web3Coach's Genesis account with 1M TBB tokens 34 | const DefaultBootstrapAcc = "0x09ee50f2f37fcba1845de6fe5c762e83e65e755c" 35 | const DefaultMiner = "0x0000000000000000000000000000000000000000" 36 | const DefaultIP = "127.0.0.1" 37 | const HttpSSLPort = 443 38 | const endpointStatus = "/node/status" 39 | 40 | const endpointSync = "/node/sync" 41 | const endpointSyncQueryKeyFromBlock = "fromBlock" 42 | 43 | const endpointAddPeer = "/node/peer" 44 | const endpointAddPeerQueryKeyIP = "ip" 45 | const endpointAddPeerQueryKeyPort = "port" 46 | const endpointAddPeerQueryKeyMiner = "miner" 47 | const endpointAddPeerQueryKeyVersion = "version" 48 | 49 | const endpointBlockByNumberOrHash = "/block/" 50 | const endpointMempoolViewer = "/mempool/" 51 | 52 | const miningIntervalSeconds = 10 53 | const DefaultMiningDifficulty = 3 54 | 55 | type PeerNode struct { 56 | IP string `json:"ip"` 57 | Port uint64 `json:"port"` 58 | IsBootstrap bool `json:"is_bootstrap"` 59 | Account common.Address `json:"account"` 60 | NodeVersion string `json:"node_version"` 61 | 62 | // Whenever my node already established connection, sync with this Peer 63 | connected bool 64 | } 65 | 66 | func (pn PeerNode) TcpAddress() string { 67 | return fmt.Sprintf("%s:%d", pn.IP, pn.Port) 68 | } 69 | 70 | func (pn PeerNode) ApiProtocol() string { 71 | if pn.Port == HttpSSLPort { 72 | return "https" 73 | } 74 | 75 | return "http" 76 | } 77 | 78 | type Node struct { 79 | dataDir string 80 | info PeerNode 81 | 82 | // The main blockchain state after all TXs from mined blocks were applied 83 | state *database.State 84 | 85 | // temporary pending state validating new incoming TXs but reset after the block is mined 86 | pendingState *database.State 87 | 88 | knownPeers map[string]PeerNode 89 | pendingTXs map[string]database.SignedTx 90 | archivedTXs map[string]database.SignedTx 91 | newSyncedBlocks chan database.Block 92 | newPendingTXs chan database.SignedTx 93 | nodeVersion string 94 | 95 | // Number of zeroes the hash must start with to be considered valid. Default 3 96 | miningDifficulty uint 97 | isMining bool 98 | } 99 | 100 | func New(dataDir string, ip string, port uint64, acc common.Address, bootstrap PeerNode, version string, miningDifficulty uint) *Node { 101 | knownPeers := make(map[string]PeerNode) 102 | 103 | n := &Node{ 104 | dataDir: dataDir, 105 | info: NewPeerNode(ip, port, false, acc, true, version), 106 | knownPeers: knownPeers, 107 | pendingTXs: make(map[string]database.SignedTx), 108 | archivedTXs: make(map[string]database.SignedTx), 109 | newSyncedBlocks: make(chan database.Block), 110 | newPendingTXs: make(chan database.SignedTx, 10000), 111 | nodeVersion: version, 112 | isMining: false, 113 | miningDifficulty: miningDifficulty, 114 | } 115 | 116 | n.AddPeer(bootstrap) 117 | 118 | return n 119 | } 120 | 121 | func NewPeerNode(ip string, port uint64, isBootstrap bool, acc common.Address, connected bool, version string) PeerNode { 122 | return PeerNode{ip, port, isBootstrap, acc, version, connected} 123 | } 124 | 125 | func (n *Node) Run(ctx context.Context, isSSLDisabled bool, sslEmail string) error { 126 | fmt.Println(fmt.Sprintf("Listening on: %s:%d", n.info.IP, n.info.Port)) 127 | 128 | state, err := database.NewStateFromDisk(n.dataDir, n.miningDifficulty) 129 | if err != nil { 130 | return err 131 | } 132 | defer state.Close() 133 | 134 | n.state = state 135 | 136 | pendingState := state.Copy() 137 | n.pendingState = &pendingState 138 | 139 | fmt.Println("Blockchain state:") 140 | fmt.Printf(" - height: %d\n", n.state.LatestBlock().Header.Number) 141 | fmt.Printf(" - hash: %s\n", n.state.LatestBlockHash().Hex()) 142 | 143 | go n.sync(ctx) 144 | go n.mine(ctx) 145 | 146 | return n.serveHttp(ctx, isSSLDisabled, sslEmail) 147 | } 148 | 149 | func (n *Node) LatestBlockHash() database.Hash { 150 | return n.state.LatestBlockHash() 151 | } 152 | 153 | func (n *Node) serveHttp(ctx context.Context, isSSLDisabled bool, sslEmail string) error { 154 | handler := http.NewServeMux() 155 | 156 | handler.HandleFunc("/balances/list", func(w http.ResponseWriter, r *http.Request) { 157 | listBalancesHandler(w, r, n.state) 158 | }) 159 | 160 | handler.HandleFunc("/tx/add", func(w http.ResponseWriter, r *http.Request) { 161 | txAddHandler(w, r, n) 162 | }) 163 | 164 | handler.HandleFunc(endpointStatus, func(w http.ResponseWriter, r *http.Request) { 165 | statusHandler(w, r, n) 166 | }) 167 | 168 | handler.HandleFunc(endpointSync, func(w http.ResponseWriter, r *http.Request) { 169 | syncHandler(w, r, n) 170 | }) 171 | 172 | handler.HandleFunc(endpointAddPeer, func(w http.ResponseWriter, r *http.Request) { 173 | addPeerHandler(w, r, n) 174 | }) 175 | 176 | handler.HandleFunc(endpointBlockByNumberOrHash, func(w http.ResponseWriter, r *http.Request) { 177 | blockByNumberOrHash(w, r, n) 178 | }) 179 | 180 | handler.HandleFunc(endpointMempoolViewer, func(w http.ResponseWriter, r *http.Request) { 181 | mempoolViewer(w, r, n.pendingTXs) 182 | }) 183 | 184 | if isSSLDisabled { 185 | server := &http.Server{Addr: fmt.Sprintf(":%d", n.info.Port), Handler: handler} 186 | 187 | go func() { 188 | <-ctx.Done() 189 | _ = server.Close() 190 | }() 191 | 192 | err := server.ListenAndServe() 193 | // This shouldn't be an error! 194 | if err != http.ErrServerClosed { 195 | return err 196 | } 197 | 198 | return nil 199 | } else { 200 | certmagic.DefaultACME.Email = sslEmail 201 | 202 | return certmagic.HTTPS([]string{n.info.IP}, handler) 203 | } 204 | } 205 | 206 | func (n *Node) mine(ctx context.Context) error { 207 | var miningCtx context.Context 208 | var stopCurrentMining context.CancelFunc 209 | 210 | ticker := time.NewTicker(time.Second * miningIntervalSeconds) 211 | 212 | for { 213 | select { 214 | case <-ticker.C: 215 | go func() { 216 | if len(n.pendingTXs) > 0 && !n.isMining { 217 | n.isMining = true 218 | 219 | miningCtx, stopCurrentMining = context.WithCancel(ctx) 220 | err := n.minePendingTXs(miningCtx) 221 | if err != nil { 222 | fmt.Printf("ERROR: %s\n", err) 223 | } 224 | 225 | n.isMining = false 226 | } 227 | }() 228 | 229 | case block, _ := <-n.newSyncedBlocks: 230 | if n.isMining { 231 | blockHash, _ := block.Hash() 232 | fmt.Printf("\nPeer mined next Block '%s' faster :(\n", blockHash.Hex()) 233 | 234 | n.removeMinedPendingTXs(block) 235 | stopCurrentMining() 236 | } 237 | 238 | case <-ctx.Done(): 239 | ticker.Stop() 240 | return nil 241 | } 242 | } 243 | } 244 | 245 | func (n *Node) minePendingTXs(ctx context.Context) error { 246 | blockToMine := NewPendingBlock( 247 | n.state.LatestBlockHash(), 248 | n.state.NextBlockNumber(), 249 | n.info.Account, 250 | n.getPendingTXsAsArray(), 251 | ) 252 | 253 | minedBlock, err := Mine(ctx, blockToMine, n.miningDifficulty) 254 | if err != nil { 255 | return err 256 | } 257 | 258 | n.removeMinedPendingTXs(minedBlock) 259 | 260 | err = n.addBlock(minedBlock) 261 | if err != nil { 262 | return err 263 | } 264 | 265 | return nil 266 | } 267 | 268 | func (n *Node) removeMinedPendingTXs(block database.Block) { 269 | if len(block.TXs) > 0 && len(n.pendingTXs) > 0 { 270 | fmt.Println("Updating in-memory Pending TXs Pool:") 271 | } 272 | 273 | for _, tx := range block.TXs { 274 | txHash, _ := tx.Hash() 275 | if _, exists := n.pendingTXs[txHash.Hex()]; exists { 276 | fmt.Printf("\t-archiving mined TX: %s\n", txHash.Hex()) 277 | 278 | n.archivedTXs[txHash.Hex()] = tx 279 | delete(n.pendingTXs, txHash.Hex()) 280 | } 281 | } 282 | } 283 | 284 | func (n *Node) ChangeMiningDifficulty(newDifficulty uint) { 285 | n.miningDifficulty = newDifficulty 286 | n.state.ChangeMiningDifficulty(newDifficulty) 287 | } 288 | 289 | func (n *Node) AddPeer(peer PeerNode) { 290 | n.knownPeers[peer.TcpAddress()] = peer 291 | } 292 | 293 | func (n *Node) RemovePeer(peer PeerNode) { 294 | delete(n.knownPeers, peer.TcpAddress()) 295 | } 296 | 297 | func (n *Node) IsKnownPeer(peer PeerNode) bool { 298 | if peer.IP == n.info.IP && peer.Port == n.info.Port { 299 | return true 300 | } 301 | 302 | _, isKnownPeer := n.knownPeers[peer.TcpAddress()] 303 | 304 | return isKnownPeer 305 | } 306 | 307 | func (n *Node) AddPendingTX(tx database.SignedTx, fromPeer PeerNode) error { 308 | txHash, err := tx.Hash() 309 | if err != nil { 310 | return err 311 | } 312 | 313 | txJson, err := json.Marshal(tx) 314 | if err != nil { 315 | return err 316 | } 317 | 318 | err = n.validateTxBeforeAddingToMempool(tx) 319 | if err != nil { 320 | return err 321 | } 322 | 323 | _, isAlreadyPending := n.pendingTXs[txHash.Hex()] 324 | _, isArchived := n.archivedTXs[txHash.Hex()] 325 | 326 | if !isAlreadyPending && !isArchived { 327 | fmt.Printf("Added Pending TX %s from Peer %s\n", txJson, fromPeer.TcpAddress()) 328 | n.pendingTXs[txHash.Hex()] = tx 329 | n.newPendingTXs <- tx 330 | } 331 | 332 | return nil 333 | } 334 | 335 | // addBlock is a wrapper around the n.state.AddBlock() to have a single function for changing the main state 336 | // from the Node perspective, so we can also reset the pending state in the same time. 337 | func (n *Node) addBlock(block database.Block) error { 338 | _, err := n.state.AddBlock(block) 339 | if err != nil { 340 | return err 341 | } 342 | 343 | // Reset the pending state 344 | pendingState := n.state.Copy() 345 | n.pendingState = &pendingState 346 | 347 | return nil 348 | } 349 | 350 | // validateTxBeforeAddingToMempool ensures the TX is authentic, with correct nonce, and the sender has sufficient 351 | // funds so we waste PoW resources on TX we can tell in advance are wrong. 352 | func (n *Node) validateTxBeforeAddingToMempool(tx database.SignedTx) error { 353 | return database.ApplyTx(tx, n.pendingState) 354 | } 355 | 356 | func (n *Node) getPendingTXsAsArray() []database.SignedTx { 357 | txs := make([]database.SignedTx, len(n.pendingTXs)) 358 | 359 | i := 0 360 | for _, tx := range n.pendingTXs { 361 | txs[i] = tx 362 | i++ 363 | } 364 | 365 | return txs 366 | } 367 | -------------------------------------------------------------------------------- /node/node_integration_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package node 17 | 18 | import ( 19 | "context" 20 | "encoding/json" 21 | "io" 22 | "io/ioutil" 23 | "os" 24 | "path/filepath" 25 | "testing" 26 | "time" 27 | 28 | "github.com/ethereum/go-ethereum/common" 29 | 30 | "github.com/web3coach/the-blockchain-bar/database" 31 | "github.com/web3coach/the-blockchain-bar/fs" 32 | "github.com/web3coach/the-blockchain-bar/wallet" 33 | ) 34 | 35 | // The password for testing keystore files: 36 | // 37 | // ./test_andrej--3eb92807f1f91a8d4d85bc908c7f86dcddb1df57 38 | // ./test_babayaga--6fdc0d8d15ae6b4ebf45c52fd2aafbcbb19a65c8 39 | // 40 | // Pre-generated for testing purposes using wallet_test.go. 41 | // 42 | // It's necessary to have pre-existing accounts before a new node 43 | // with fresh new, empty keystore is initialized and booted in order 44 | // to configure the accounts balances in genesis.json 45 | // 46 | // I.e: A quick solution to a chicken-egg problem. 47 | const testKsAndrejAccount = "0x3eb92807f1f91a8d4d85bc908c7f86dcddb1df57" 48 | const testKsBabaYagaAccount = "0x6fdc0d8d15ae6b4ebf45c52fd2aafbcbb19a65c8" 49 | const testKsAndrejFile = "test_andrej--3eb92807f1f91a8d4d85bc908c7f86dcddb1df57" 50 | const testKsBabaYagaFile = "test_babayaga--6fdc0d8d15ae6b4ebf45c52fd2aafbcbb19a65c8" 51 | const testKsAccountsPwd = "security123" 52 | const nodeVersion = "0.0.0-alpha 01abcd Test Run" 53 | 54 | func TestNode_Run(t *testing.T) { 55 | datadir, err := getTestDataDirPath() 56 | if err != nil { 57 | t.Fatal(err) 58 | } 59 | err = fs.RemoveDir(datadir) 60 | if err != nil { 61 | t.Fatal(err) 62 | } 63 | 64 | n := New(datadir, "127.0.0.1", 8085, database.NewAccount(DefaultMiner), PeerNode{}, nodeVersion, defaultTestMiningDifficulty) 65 | 66 | ctx, _ := context.WithTimeout(context.Background(), time.Second*5) 67 | err = n.Run(ctx, true, "") 68 | if err != nil { 69 | t.Fatal(err) 70 | } 71 | } 72 | 73 | func TestNode_Mining(t *testing.T) { 74 | dataDir, andrej, babaYaga, err := setupTestNodeDir(1000000, 0) 75 | if err != nil { 76 | t.Error(err) 77 | } 78 | defer fs.RemoveDir(dataDir) 79 | 80 | // Required for AddPendingTX() to describe 81 | // from what node the TX came from (local node in this case) 82 | nInfo := NewPeerNode( 83 | "127.0.0.1", 84 | 8085, 85 | false, 86 | babaYaga, 87 | true, 88 | nodeVersion, 89 | ) 90 | 91 | // Construct a new Node instance and configure 92 | // Andrej as a miner 93 | n := New(dataDir, nInfo.IP, nInfo.Port, andrej, nInfo, nodeVersion, defaultTestMiningDifficulty) 94 | 95 | // Allow the mining to run for 30 mins, in the worst case 96 | ctx, closeNode := context.WithTimeout( 97 | context.Background(), 98 | time.Minute*30, 99 | ) 100 | 101 | // Schedule a new TX in 3 seconds from now, in a separate thread 102 | // because the n.Run() few lines below is a blocking call 103 | go func() { 104 | time.Sleep(time.Second * miningIntervalSeconds / 3) 105 | 106 | tx := database.NewBaseTx(andrej, babaYaga, 1, 1, "") 107 | signedTx, err := wallet.SignTxWithKeystoreAccount(tx, andrej, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 108 | if err != nil { 109 | t.Error(err) 110 | return 111 | } 112 | 113 | _ = n.AddPendingTX(signedTx, nInfo) 114 | }() 115 | 116 | // Schedule a TX with insufficient funds in 4 seconds validating 117 | // the AddPendingTX won't add it to the Mempool 118 | go func() { 119 | time.Sleep(time.Second*(miningIntervalSeconds/3) + 1) 120 | 121 | tx := database.NewBaseTx(babaYaga, andrej, 50, 1, "") 122 | signedTx, err := wallet.SignTxWithKeystoreAccount(tx, babaYaga, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 123 | if err != nil { 124 | t.Error(err) 125 | return 126 | } 127 | 128 | err = n.AddPendingTX(signedTx, nInfo) 129 | t.Log(err) 130 | if err == nil { 131 | t.Errorf("TX should not be added to Mempool because BabaYaga doesn't have %d TBB tokens", tx.Value) 132 | return 133 | } 134 | }() 135 | 136 | // Schedule a new TX in 12 seconds from now simulating 137 | // that it came in - while the first TX is being mined 138 | go func() { 139 | time.Sleep(time.Second * (miningIntervalSeconds + 2)) 140 | 141 | tx := database.NewBaseTx(andrej, babaYaga, 2, 2, "") 142 | signedTx, err := wallet.SignTxWithKeystoreAccount(tx, andrej, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 143 | if err != nil { 144 | t.Error(err) 145 | return 146 | } 147 | 148 | err = n.AddPendingTX(signedTx, nInfo) 149 | if err != nil { 150 | t.Error(err) 151 | return 152 | } 153 | }() 154 | 155 | go func() { 156 | // Periodically check if we mined the 2 blocks 157 | ticker := time.NewTicker(10 * time.Second) 158 | 159 | for { 160 | select { 161 | case <-ticker.C: 162 | if n.state.LatestBlock().Header.Number == 1 { 163 | closeNode() 164 | return 165 | } 166 | } 167 | } 168 | }() 169 | 170 | // Run the node, mining and everything in a blocking call (hence the go-routines before) 171 | _ = n.Run(ctx, true, "") 172 | 173 | if n.state.LatestBlock().Header.Number != 1 { 174 | t.Fatal("2 pending TX not mined into 2 blocks under 30m") 175 | } 176 | } 177 | 178 | // Expect: 179 | // ERROR: wrong TX. Sender '0x3EB9....' is forged 180 | // 181 | // TODO: Improve this with TX Receipt concept in next chapters. 182 | // TODO: Improve this with a 100% clear error check. 183 | func TestNode_ForgedTx(t *testing.T) { 184 | dataDir, andrej, babaYaga, err := setupTestNodeDir(1000000, 0) 185 | if err != nil { 186 | t.Error(err) 187 | } 188 | defer fs.RemoveDir(dataDir) 189 | 190 | n := New(dataDir, "127.0.0.1", 8085, andrej, PeerNode{}, nodeVersion, defaultTestMiningDifficulty) 191 | ctx, closeNode := context.WithTimeout(context.Background(), time.Minute*30) 192 | andrejPeerNode := NewPeerNode("127.0.0.1", 8085, false, andrej, true, nodeVersion) 193 | 194 | txValue := uint(5) 195 | txNonce := uint(1) 196 | tx := database.NewBaseTx(andrej, babaYaga, txValue, txNonce, "") 197 | 198 | validSignedTx, err := wallet.SignTxWithKeystoreAccount(tx, andrej, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 199 | if err != nil { 200 | t.Error(err) 201 | closeNode() 202 | return 203 | } 204 | 205 | go func() { 206 | // Wait for the node to run 207 | time.Sleep(time.Second * 1) 208 | 209 | err = n.AddPendingTX(validSignedTx, andrejPeerNode) 210 | if err != nil { 211 | t.Error(err) 212 | closeNode() 213 | return 214 | } 215 | }() 216 | 217 | go func() { 218 | ticker := time.NewTicker(time.Second * (miningIntervalSeconds - 3)) 219 | wasForgedTxAdded := false 220 | 221 | for { 222 | select { 223 | case <-ticker.C: 224 | if !n.state.LatestBlockHash().IsEmpty() { 225 | if wasForgedTxAdded && !n.isMining { 226 | closeNode() 227 | return 228 | } 229 | 230 | if !wasForgedTxAdded { 231 | // Attempt to forge the same TX but with modified time 232 | // Because the TX.time changed, the TX.signature will be considered forged 233 | // database.NewTx() changes the TX time 234 | forgedTx := database.NewBaseTx(andrej, babaYaga, txValue, txNonce, "") 235 | // Use the signature from a valid TX 236 | forgedSignedTx := database.NewSignedTx(forgedTx, validSignedTx.Sig) 237 | 238 | err = n.AddPendingTX(forgedSignedTx, andrejPeerNode) 239 | t.Log(err) 240 | if err == nil { 241 | t.Errorf("adding a forged TX to the Mempool should not be possible") 242 | closeNode() 243 | return 244 | } 245 | 246 | wasForgedTxAdded = true 247 | 248 | time.Sleep(time.Second * (miningIntervalSeconds + 3)) 249 | } 250 | } 251 | } 252 | } 253 | }() 254 | 255 | _ = n.Run(ctx, true, "") 256 | 257 | if n.state.LatestBlock().Header.Number != 0 { 258 | t.Fatal("was suppose to mine only one TX. The second TX was forged") 259 | } 260 | 261 | if n.state.Balances[babaYaga] != txValue { 262 | t.Fatal("forged tx succeeded") 263 | } 264 | } 265 | 266 | // Expect: 267 | // ERROR: wrong TX. Sender '0x3EB9...' next nonce must be '2', not '1' 268 | // 269 | // TODO: Improve this with TX Receipt concept in next chapters. 270 | // TODO: Improve this with a 100% clear error check. 271 | func TestNode_ReplayedTx(t *testing.T) { 272 | dataDir, andrej, babaYaga, err := setupTestNodeDir(1000000, 0) 273 | if err != nil { 274 | t.Error(err) 275 | } 276 | defer fs.RemoveDir(dataDir) 277 | 278 | n := New(dataDir, "127.0.0.1", 8085, andrej, PeerNode{}, nodeVersion, defaultTestMiningDifficulty) 279 | ctx, closeNode := context.WithCancel(context.Background()) 280 | andrejPeerNode := NewPeerNode("127.0.0.1", 8085, false, andrej, true, nodeVersion) 281 | babaYagaPeerNode := NewPeerNode("127.0.0.1", 8086, false, babaYaga, true, nodeVersion) 282 | 283 | txValue := uint(5) 284 | txNonce := uint(1) 285 | tx := database.NewBaseTx(andrej, babaYaga, txValue, txNonce, "") 286 | 287 | signedTx, err := wallet.SignTxWithKeystoreAccount(tx, andrej, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 288 | if err != nil { 289 | t.Error(err) 290 | closeNode() 291 | return 292 | } 293 | 294 | go func() { 295 | // Wait for the node to run 296 | time.Sleep(time.Second * 1) 297 | 298 | err = n.AddPendingTX(signedTx, andrejPeerNode) 299 | if err != nil { 300 | t.Error(err) 301 | closeNode() 302 | return 303 | } 304 | }() 305 | 306 | go func() { 307 | ticker := time.NewTicker(time.Second * (miningIntervalSeconds - 3)) 308 | wasReplayedTxAdded := false 309 | 310 | for { 311 | select { 312 | case <-ticker.C: 313 | if !n.state.LatestBlockHash().IsEmpty() { 314 | if wasReplayedTxAdded && !n.isMining { 315 | closeNode() 316 | return 317 | } 318 | 319 | // The Andrej's original TX got mined. 320 | // Execute the attack by replaying the TX again! 321 | if !wasReplayedTxAdded { 322 | // Simulate the TX was submitted to different node 323 | n.archivedTXs = make(map[string]database.SignedTx) 324 | // Execute the attack 325 | err = n.AddPendingTX(signedTx, babaYagaPeerNode) 326 | t.Log(err) 327 | if err == nil { 328 | t.Errorf("re-adding a TX to the Mempool should not be possible because of Nonce") 329 | closeNode() 330 | return 331 | } 332 | 333 | wasReplayedTxAdded = true 334 | 335 | time.Sleep(time.Second * (miningIntervalSeconds + 3)) 336 | } 337 | } 338 | } 339 | } 340 | }() 341 | 342 | _ = n.Run(ctx, true, "") 343 | 344 | if n.state.Balances[babaYaga] == txValue*2 { 345 | t.Errorf("replayed attack was successful :( Damn digital signatures!") 346 | return 347 | } 348 | 349 | if n.state.Balances[babaYaga] != txValue { 350 | t.Errorf("replayed attack was successful :( Damn digital signatures!") 351 | return 352 | } 353 | 354 | if n.state.LatestBlock().Header.Number == 1 { 355 | t.Errorf("the second block was not suppose to be persisted because it contained a malicious TX") 356 | return 357 | } 358 | } 359 | 360 | // The test logic summary: 361 | // - BabaYaga runs the node 362 | // - BabaYaga tries to mine 2 TXs 363 | // - The mining gets interrupted because a new block from Andrej gets synced 364 | // - Andrej will get the block reward for this synced block 365 | // - The synced block contains 1 of the TXs BabaYaga tried to mine 366 | // - BabaYaga tries to mine 1 TX left 367 | // - BabaYaga succeeds and gets her block reward 368 | func TestNode_MiningStopsOnNewSyncedBlock(t *testing.T) { 369 | tc := []struct { 370 | name string 371 | ForkTIP1 uint64 372 | }{ 373 | {"Legacy", 35}, // Prior ForkTIP1 was activated on number 35 374 | {"ForkTIP1", 0}, // To test new blocks when the ForkTIP1 is active 375 | } 376 | 377 | for _, tc := range tc { 378 | t.Run(tc.name, func(t *testing.T) { 379 | babaYaga := database.NewAccount(testKsBabaYagaAccount) 380 | andrej := database.NewAccount(testKsAndrejAccount) 381 | 382 | dataDir, err := getTestDataDirPath() 383 | if err != nil { 384 | t.Fatal(err) 385 | } 386 | 387 | genesisBalances := make(map[common.Address]uint) 388 | genesisBalances[andrej] = 1000000 389 | genesis := database.Genesis{Balances: genesisBalances, ForkTIP1: tc.ForkTIP1} 390 | genesisJson, err := json.Marshal(genesis) 391 | if err != nil { 392 | t.Fatal(err) 393 | } 394 | 395 | err = database.InitDataDirIfNotExists(dataDir, genesisJson) 396 | defer fs.RemoveDir(dataDir) 397 | 398 | err = copyKeystoreFilesIntoTestDataDirPath(dataDir) 399 | if err != nil { 400 | t.Fatal(err) 401 | } 402 | 403 | // Required for AddPendingTX() to describe 404 | // from what node the TX came from (local node in this case) 405 | nInfo := NewPeerNode( 406 | "127.0.0.1", 407 | 8085, 408 | false, 409 | database.NewAccount(""), 410 | true, 411 | nodeVersion, 412 | ) 413 | 414 | // Start mining with a high mining difficulty, just to be slow on purpose and let a synced block arrive first 415 | n := New(dataDir, nInfo.IP, nInfo.Port, babaYaga, nInfo, nodeVersion, uint(5)) 416 | 417 | // Allow the test to run for 30 mins, in the worst case 418 | ctx, closeNode := context.WithTimeout(context.Background(), time.Minute*30) 419 | 420 | tx1 := database.NewBaseTx(andrej, babaYaga, 1, 1, "") 421 | tx2 := database.NewBaseTx(andrej, babaYaga, 2, 2, "") 422 | 423 | if tc.name == "Legacy" { 424 | tx1.Gas = 0 425 | tx1.GasPrice = 0 426 | tx2.Gas = 0 427 | tx2.GasPrice = 0 428 | } 429 | 430 | signedTx1, err := wallet.SignTxWithKeystoreAccount(tx1, andrej, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 431 | if err != nil { 432 | t.Error(err) 433 | return 434 | } 435 | 436 | signedTx2, err := wallet.SignTxWithKeystoreAccount(tx2, andrej, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 437 | if err != nil { 438 | t.Error(err) 439 | return 440 | } 441 | tx2Hash, err := signedTx2.Hash() 442 | if err != nil { 443 | t.Error(err) 444 | return 445 | } 446 | 447 | // Pre-mine a valid block without running the `n.Run()` 448 | // with Andrej as a miner who will receive the block reward, 449 | // to simulate the block came on the fly from another peer 450 | validPreMinedPb := NewPendingBlock(database.Hash{}, 0, andrej, []database.SignedTx{signedTx1}) 451 | validSyncedBlock, err := Mine(ctx, validPreMinedPb, defaultTestMiningDifficulty) 452 | if err != nil { 453 | t.Fatal(err) 454 | } 455 | 456 | // Add 2 new TXs into the BabaYaga's node, triggers mining 457 | go func() { 458 | time.Sleep(time.Second * (miningIntervalSeconds - 2)) 459 | 460 | err := n.AddPendingTX(signedTx1, nInfo) 461 | if err != nil { 462 | t.Fatal(err) 463 | } 464 | 465 | err = n.AddPendingTX(signedTx2, nInfo) 466 | if err != nil { 467 | t.Fatal(err) 468 | } 469 | }() 470 | 471 | // Interrupt the previously started mining with a new synced block 472 | // BUT this block contains only 1 TX the previous mining activity tried to mine 473 | // which means the mining will start again for the one pending TX that is left and wasn't in 474 | // the synced block 475 | go func() { 476 | time.Sleep(time.Second * (miningIntervalSeconds + 2)) 477 | if !n.isMining { 478 | t.Fatal("should be mining") 479 | } 480 | 481 | // Change the mining difficulty back to the testing level from previously purposefully slow, high value 482 | // otherwise the synced block would be invalid. 483 | n.ChangeMiningDifficulty(defaultTestMiningDifficulty) 484 | _, err := n.state.AddBlock(validSyncedBlock) 485 | if err != nil { 486 | t.Fatal(err) 487 | } 488 | // Mock the Andrej's block came from a network 489 | n.newSyncedBlocks <- validSyncedBlock 490 | 491 | time.Sleep(time.Second) 492 | if n.isMining { 493 | t.Fatal("synced block should have canceled mining") 494 | } 495 | 496 | // Mined TX1 by Andrej should be removed from the Mempool 497 | _, onlyTX2IsPending := n.pendingTXs[tx2Hash.Hex()] 498 | 499 | if len(n.pendingTXs) != 1 && !onlyTX2IsPending { 500 | t.Fatal("synced block should have canceled mining of already mined TX") 501 | } 502 | }() 503 | 504 | go func() { 505 | // Regularly check whenever both TXs are now mined 506 | ticker := time.NewTicker(time.Second * 10) 507 | 508 | for { 509 | select { 510 | case <-ticker.C: 511 | if n.state.LatestBlock().Header.Number == 1 { 512 | closeNode() 513 | return 514 | } 515 | } 516 | } 517 | }() 518 | 519 | go func() { 520 | time.Sleep(time.Second * 2) 521 | 522 | // Take a snapshot of the DB balances 523 | // before the mining is finished and the 2 blocks 524 | // are created. 525 | startingAndrejBalance := n.state.Balances[andrej] 526 | startingBabaYagaBalance := n.state.Balances[babaYaga] 527 | 528 | // Wait until the 30 mins timeout is reached or 529 | // the 2 blocks got already mined and the closeNode() was triggered 530 | <-ctx.Done() 531 | 532 | endAndrejBalance := n.state.Balances[andrej] 533 | endBabaYagaBalance := n.state.Balances[babaYaga] 534 | 535 | // In TX1 Andrej transferred 1 TBB token to BabaYaga 536 | // In TX2 Andrej transferred 2 TBB tokens to BabaYaga 537 | 538 | var expectedEndAndrejBalance uint 539 | var expectedEndBabaYagaBalance uint 540 | 541 | // Andrej will occur the cost of SENDING 2 TXs but will collect the reward for mining one block with tx1 in it 542 | // BabaYaga will RECEIVE value from 2 TXs and will also collect the reward for mining one block with tx2 in it 543 | 544 | if n.state.IsTIP1Fork() { 545 | expectedEndAndrejBalance = startingAndrejBalance - tx1.Cost(true) - tx2.Cost(true) + database.BlockReward + tx1.GasCost() 546 | expectedEndBabaYagaBalance = startingBabaYagaBalance + tx1.Value + tx2.Value + database.BlockReward + tx2.GasCost() 547 | } else { 548 | expectedEndAndrejBalance = startingAndrejBalance - tx1.Cost(false) - tx2.Cost(false) + database.BlockReward + database.TxFee 549 | expectedEndBabaYagaBalance = startingBabaYagaBalance + tx1.Value + tx2.Value + database.BlockReward + database.TxFee 550 | } 551 | 552 | if endAndrejBalance != expectedEndAndrejBalance { 553 | t.Errorf("Andrej expected end balance is %d not %d", expectedEndAndrejBalance, endAndrejBalance) 554 | } 555 | 556 | if endBabaYagaBalance != expectedEndBabaYagaBalance { 557 | t.Errorf("BabaYaga expected end balance is %d not %d", expectedEndBabaYagaBalance, endBabaYagaBalance) 558 | } 559 | 560 | t.Logf("Starting Andrej balance: %d", startingAndrejBalance) 561 | t.Logf("Starting BabaYaga balance: %d", startingBabaYagaBalance) 562 | t.Logf("Ending Andrej balance: %d", endAndrejBalance) 563 | t.Logf("Ending BabaYaga balance: %d", endBabaYagaBalance) 564 | }() 565 | 566 | _ = n.Run(ctx, true, "") 567 | 568 | if n.state.LatestBlock().Header.Number != 1 { 569 | t.Fatal("was suppose to mine 2 pending TX into 2 valid blocks under 30m") 570 | } 571 | 572 | if len(n.pendingTXs) != 0 { 573 | t.Fatal("no pending TXs should be left to mine") 574 | } 575 | }) 576 | } 577 | } 578 | 579 | func TestNode_MiningSpamTransactions(t *testing.T) { 580 | tc := []struct { 581 | name string 582 | ForkTIP1 uint64 583 | }{ 584 | {"Legacy", 35}, // Prior ForkTIP1 was activated on number 35 585 | {"ForkTIP1", 0}, // To test new blocks when the ForkTIP1 is active 586 | } 587 | 588 | for _, tc := range tc { 589 | t.Run(tc.name, func(t *testing.T) { 590 | 591 | andrejBalance := uint(1000) 592 | babaYagaBalance := uint(0) 593 | minerBalance := uint(0) 594 | minerKey, err := wallet.NewRandomKey() 595 | if err != nil { 596 | t.Fatal(err) 597 | } 598 | miner := minerKey.Address 599 | dataDir, andrej, babaYaga, err := setupTestNodeDir(andrejBalance, tc.ForkTIP1) 600 | if err != nil { 601 | t.Fatal(err) 602 | } 603 | defer fs.RemoveDir(dataDir) 604 | 605 | n := New(dataDir, "127.0.0.1", 8085, miner, PeerNode{}, nodeVersion, defaultTestMiningDifficulty) 606 | ctx, closeNode := context.WithCancel(context.Background()) 607 | minerPeerNode := NewPeerNode("127.0.0.1", 8085, false, miner, true, nodeVersion) 608 | 609 | txValue := uint(200) 610 | txCount := uint(4) 611 | spamTXs := make([]database.SignedTx, txCount) 612 | 613 | go func() { 614 | // Wait for the node to run and initialize its state and other components 615 | time.Sleep(time.Second) 616 | 617 | now := uint64(time.Now().Unix()) 618 | // Schedule 4 transfers from Andrej -> BabaYaga 619 | for i := uint(1); i <= txCount; i++ { 620 | txNonce := i 621 | tx := database.NewBaseTx(andrej, babaYaga, txValue, txNonce, "") 622 | // Ensure every TX has a unique timestamp and the nonce 0 has oldest timestamp, nonce 1 younger timestamp etc 623 | tx.Time = now - uint64(txCount-i*100) 624 | 625 | if tc.name == "Legacy" { 626 | tx.Gas = 0 627 | tx.GasPrice = 0 628 | } 629 | 630 | signedTx, err := wallet.SignTxWithKeystoreAccount(tx, andrej, testKsAccountsPwd, wallet.GetKeystoreDirPath(dataDir)) 631 | if err != nil { 632 | t.Fatal(err) 633 | } 634 | 635 | spamTXs[i-1] = signedTx 636 | } 637 | 638 | // Collect pre-signed TXs to an array to make sure all 4 fit into a block within the mining interval, 639 | // otherwise slower machines can start mining after TX 3 or so, making the test fail on e.g: Github Actions. 640 | for _, tx := range spamTXs { 641 | _ = n.AddPendingTX(tx, minerPeerNode) 642 | } 643 | }() 644 | 645 | go func() { 646 | // Periodically check if we mined the block 647 | ticker := time.NewTicker(10 * time.Second) 648 | 649 | for { 650 | select { 651 | case <-ticker.C: 652 | if !n.state.LatestBlockHash().IsEmpty() { 653 | closeNode() 654 | return 655 | } 656 | } 657 | } 658 | }() 659 | 660 | // Run the node, mining and everything in a blocking call (hence the go-routines before) 661 | _ = n.Run(ctx, true, "") 662 | 663 | var expectedAndrejBalance uint 664 | var expectedBabaYagaBalance uint 665 | var expectedMinerBalance uint 666 | 667 | // in nutshell: sender occurs tx.Cost(), receiver gains tx.Value() and miner collects tx.GasCost() 668 | if n.state.IsTIP1Fork() { 669 | expectedAndrejBalance = andrejBalance 670 | expectedMinerBalance = minerBalance + database.BlockReward 671 | 672 | for _, tx := range spamTXs { 673 | expectedAndrejBalance -= tx.Cost(true) 674 | expectedMinerBalance += tx.GasCost() 675 | } 676 | 677 | expectedBabaYagaBalance = babaYagaBalance + (txCount * txValue) 678 | } else { 679 | expectedAndrejBalance = andrejBalance - (txCount * txValue) - (txCount * database.TxFee) 680 | expectedBabaYagaBalance = babaYagaBalance + (txCount * txValue) 681 | expectedMinerBalance = minerBalance + database.BlockReward + (txCount * database.TxFee) 682 | } 683 | 684 | if n.state.Balances[andrej] != expectedAndrejBalance { 685 | t.Errorf("Andrej balance is incorrect. Expected: %d. Got: %d", expectedAndrejBalance, n.state.Balances[andrej]) 686 | } 687 | 688 | if n.state.Balances[babaYaga] != expectedBabaYagaBalance { 689 | t.Errorf("BabaYaga balance is incorrect. Expected: %d. Got: %d", expectedBabaYagaBalance, n.state.Balances[babaYaga]) 690 | } 691 | 692 | if n.state.Balances[miner] != expectedMinerBalance { 693 | t.Errorf("Miner balance is incorrect. Expected: %d. Got: %d", expectedMinerBalance, n.state.Balances[miner]) 694 | } 695 | 696 | t.Logf("Andrej final balance: %d TBB", n.state.Balances[andrej]) 697 | t.Logf("BabaYaga final balance: %d TBB", n.state.Balances[babaYaga]) 698 | t.Logf("Miner final balance: %d TBB", n.state.Balances[miner]) 699 | }) 700 | } 701 | } 702 | 703 | // Creates dir like: "/tmp/tbb_test945924586" 704 | func getTestDataDirPath() (string, error) { 705 | return ioutil.TempDir(os.TempDir(), "tbb_test") 706 | } 707 | 708 | // Copy the pre-generated, commited keystore files from this folder into the new testDataDirPath() 709 | // 710 | // Afterwards the test datadir path will look like: 711 | // "/tmp/tbb_test945924586/keystore/test_andrej--3eb92807f1f91a8d4d85bc908c7f86dcddb1df57" 712 | // "/tmp/tbb_test945924586/keystore/test_babayaga--6fdc0d8d15ae6b4ebf45c52fd2aafbcbb19a65c8" 713 | func copyKeystoreFilesIntoTestDataDirPath(dataDir string) error { 714 | andrejSrcKs, err := os.Open(testKsAndrejFile) 715 | if err != nil { 716 | return err 717 | } 718 | defer andrejSrcKs.Close() 719 | 720 | ksDir := filepath.Join(wallet.GetKeystoreDirPath(dataDir)) 721 | 722 | err = os.Mkdir(ksDir, 0777) 723 | if err != nil { 724 | return err 725 | } 726 | 727 | andrejDstKs, err := os.Create(filepath.Join(ksDir, testKsAndrejFile)) 728 | if err != nil { 729 | return err 730 | } 731 | defer andrejDstKs.Close() 732 | 733 | _, err = io.Copy(andrejDstKs, andrejSrcKs) 734 | if err != nil { 735 | return err 736 | } 737 | 738 | babayagaSrcKs, err := os.Open(testKsBabaYagaFile) 739 | if err != nil { 740 | return err 741 | } 742 | defer babayagaSrcKs.Close() 743 | 744 | babayagaDstKs, err := os.Create(filepath.Join(ksDir, testKsBabaYagaFile)) 745 | if err != nil { 746 | return err 747 | } 748 | defer babayagaDstKs.Close() 749 | 750 | _, err = io.Copy(babayagaDstKs, babayagaSrcKs) 751 | if err != nil { 752 | return err 753 | } 754 | 755 | return nil 756 | } 757 | 758 | // setupTestNodeDir creates a default testing node directory with 2 keystore accounts 759 | // 760 | // Remember to remove the dir once test finishes: defer fs.RemoveDir(dataDir) 761 | func setupTestNodeDir(andrejBalance uint, forkTip1 uint64) (dataDir string, andrej, babaYaga common.Address, err error) { 762 | babaYaga = database.NewAccount(testKsBabaYagaAccount) 763 | andrej = database.NewAccount(testKsAndrejAccount) 764 | 765 | dataDir, err = getTestDataDirPath() 766 | if err != nil { 767 | return "", common.Address{}, common.Address{}, err 768 | } 769 | 770 | genesisBalances := make(map[common.Address]uint) 771 | genesisBalances[andrej] = andrejBalance 772 | genesis := database.Genesis{Balances: genesisBalances, ForkTIP1: forkTip1} 773 | genesisJson, err := json.Marshal(genesis) 774 | if err != nil { 775 | return "", common.Address{}, common.Address{}, err 776 | } 777 | 778 | err = database.InitDataDirIfNotExists(dataDir, genesisJson) 779 | if err != nil { 780 | return "", common.Address{}, common.Address{}, err 781 | } 782 | 783 | err = copyKeystoreFilesIntoTestDataDirPath(dataDir) 784 | if err != nil { 785 | return "", common.Address{}, common.Address{}, err 786 | } 787 | 788 | return dataDir, andrej, babaYaga, nil 789 | } 790 | -------------------------------------------------------------------------------- /node/sync.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package node 17 | 18 | import ( 19 | "context" 20 | "fmt" 21 | "net/http" 22 | "net/url" 23 | "time" 24 | 25 | "github.com/web3coach/the-blockchain-bar/database" 26 | ) 27 | 28 | func (n *Node) sync(ctx context.Context) error { 29 | n.doSync() 30 | 31 | ticker := time.NewTicker(45 * time.Second) 32 | 33 | for { 34 | select { 35 | case <-ticker.C: 36 | n.doSync() 37 | 38 | case <-ctx.Done(): 39 | ticker.Stop() 40 | } 41 | } 42 | } 43 | 44 | func (n *Node) doSync() { 45 | for _, peer := range n.knownPeers { 46 | if n.info.IP == peer.IP && n.info.Port == peer.Port { 47 | continue 48 | } 49 | 50 | if peer.IP == "" { 51 | continue 52 | } 53 | 54 | fmt.Printf("Searching for new Peers and their Blocks and Peers: '%s'\n", peer.TcpAddress()) 55 | 56 | status, err := queryPeerStatus(peer) 57 | if err != nil { 58 | fmt.Printf("ERROR: %s\n", err) 59 | fmt.Printf("Peer '%s' was removed from KnownPeers\n", peer.TcpAddress()) 60 | 61 | n.RemovePeer(peer) 62 | 63 | continue 64 | } 65 | 66 | err = n.joinKnownPeers(peer) 67 | if err != nil { 68 | fmt.Printf("ERROR: %s\n", err) 69 | continue 70 | } 71 | 72 | err = n.syncBlocks(peer, status) 73 | if err != nil { 74 | fmt.Printf("ERROR: %s\n", err) 75 | continue 76 | } 77 | 78 | err = n.syncKnownPeers(status) 79 | if err != nil { 80 | fmt.Printf("ERROR: %s\n", err) 81 | continue 82 | } 83 | 84 | err = n.syncPendingTXs(peer, status.PendingTXs) 85 | if err != nil { 86 | fmt.Printf("ERROR: %s\n", err) 87 | continue 88 | } 89 | } 90 | } 91 | 92 | func (n *Node) syncBlocks(peer PeerNode, status StatusRes) error { 93 | localBlockNumber := n.state.LatestBlock().Header.Number 94 | 95 | // If the peer has no blocks, ignore it 96 | if status.Hash.IsEmpty() { 97 | return nil 98 | } 99 | 100 | // If the peer has less blocks than us, ignore it 101 | if status.Number < localBlockNumber { 102 | return nil 103 | } 104 | 105 | // If it's the genesis block and we already synced it, ignore it 106 | if status.Number == 0 && !n.state.LatestBlockHash().IsEmpty() { 107 | return nil 108 | } 109 | 110 | // Display found 1 new block if we sync the genesis block 0 111 | newBlocksCount := status.Number - localBlockNumber 112 | if localBlockNumber == 0 && status.Number == 0 { 113 | newBlocksCount = 1 114 | } 115 | fmt.Printf("Found %d new blocks from Peer %s\n", newBlocksCount, peer.TcpAddress()) 116 | 117 | blocks, err := fetchBlocksFromPeer(peer, n.state.LatestBlockHash()) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | for _, block := range blocks { 123 | err = n.addBlock(block) 124 | if err != nil { 125 | return err 126 | } 127 | 128 | n.newSyncedBlocks <- block 129 | } 130 | 131 | return nil 132 | } 133 | 134 | func (n *Node) syncKnownPeers(status StatusRes) error { 135 | for _, statusPeer := range status.KnownPeers { 136 | if !n.IsKnownPeer(statusPeer) { 137 | fmt.Printf("Found new Peer %s\n", statusPeer.TcpAddress()) 138 | 139 | n.AddPeer(statusPeer) 140 | } 141 | } 142 | 143 | return nil 144 | } 145 | 146 | func (n *Node) syncPendingTXs(peer PeerNode, txs []database.SignedTx) error { 147 | for _, tx := range txs { 148 | err := n.AddPendingTX(tx, peer) 149 | if err != nil { 150 | return err 151 | } 152 | } 153 | 154 | return nil 155 | } 156 | 157 | func (n *Node) joinKnownPeers(peer PeerNode) error { 158 | if peer.connected { 159 | return nil 160 | } 161 | 162 | p_url := fmt.Sprintf( 163 | "%s://%s%s?%s=%s&%s=%d&%s=%s&%s=%s", 164 | peer.ApiProtocol(), 165 | peer.TcpAddress(), 166 | endpointAddPeer, 167 | endpointAddPeerQueryKeyIP, 168 | n.info.IP, 169 | endpointAddPeerQueryKeyPort, 170 | n.info.Port, 171 | endpointAddPeerQueryKeyMiner, 172 | n.info.Account.String(), 173 | endpointAddPeerQueryKeyVersion, 174 | url.QueryEscape(n.info.NodeVersion), 175 | ) 176 | 177 | res, err := http.Get(p_url) 178 | if err != nil { 179 | return err 180 | } 181 | 182 | addPeerRes := AddPeerRes{} 183 | err = readRes(res, &addPeerRes) 184 | if err != nil { 185 | return err 186 | } 187 | if addPeerRes.Error != "" { 188 | return fmt.Errorf(addPeerRes.Error) 189 | } 190 | 191 | knownPeer := n.knownPeers[peer.TcpAddress()] 192 | knownPeer.connected = addPeerRes.Success 193 | 194 | n.AddPeer(knownPeer) 195 | 196 | if !addPeerRes.Success { 197 | return fmt.Errorf("unable to join KnownPeers of '%s'", peer.TcpAddress()) 198 | } 199 | 200 | return nil 201 | } 202 | 203 | func queryPeerStatus(peer PeerNode) (StatusRes, error) { 204 | url := fmt.Sprintf("%s://%s%s", peer.ApiProtocol(), peer.TcpAddress(), endpointStatus) 205 | res, err := http.Get(url) 206 | if err != nil { 207 | return StatusRes{}, err 208 | } 209 | 210 | statusRes := StatusRes{} 211 | err = readRes(res, &statusRes) 212 | if err != nil { 213 | return StatusRes{}, err 214 | } 215 | 216 | return statusRes, nil 217 | } 218 | 219 | func fetchBlocksFromPeer(peer PeerNode, fromBlock database.Hash) ([]database.Block, error) { 220 | fmt.Printf("Importing blocks from Peer %s...\n", peer.TcpAddress()) 221 | 222 | url := fmt.Sprintf( 223 | "%s://%s%s?%s=%s", 224 | peer.ApiProtocol(), 225 | peer.TcpAddress(), 226 | endpointSync, 227 | endpointSyncQueryKeyFromBlock, 228 | fromBlock.Hex(), 229 | ) 230 | 231 | res, err := http.Get(url) 232 | if err != nil { 233 | return nil, err 234 | } 235 | 236 | syncRes := SyncRes{} 237 | err = readRes(res, &syncRes) 238 | if err != nil { 239 | return nil, err 240 | } 241 | 242 | return syncRes.Blocks, nil 243 | } 244 | -------------------------------------------------------------------------------- /node/test_andrej--3eb92807f1f91a8d4d85bc908c7f86dcddb1df57: -------------------------------------------------------------------------------- 1 | {"address":"3eb92807f1f91a8d4d85bc908c7f86dcddb1df57","crypto":{"cipher":"aes-128-ctr","ciphertext":"64111e0277ea5e027f01f47000de3b67a4b3a3ed5c2749d135e59e0d2ae0b33a","cipherparams":{"iv":"6c127aff8648b281552801dc8d0eb51f"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"8f9d07fff1704aba399d7b209618a1de1855629117e4035a4f93cbbccd13a30c"},"mac":"8a40bb8b1be6fad67c85bd2fa2e1fcf90791dd04a46f774d572dd4017dcdab01"},"id":"9a2305a5-d19f-4c3d-8906-b88ccfdd39c9","version":3} -------------------------------------------------------------------------------- /node/test_babayaga--6fdc0d8d15ae6b4ebf45c52fd2aafbcbb19a65c8: -------------------------------------------------------------------------------- 1 | {"address":"6fdc0d8d15ae6b4ebf45c52fd2aafbcbb19a65c8","crypto":{"cipher":"aes-128-ctr","ciphertext":"98b688f7d1dc134aac9bdbf923fb846818f2aeee677342e1e6eb5b4698d23696","cipherparams":{"iv":"6d8c03ff27685f6630a3ceb9e66e5862"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"48abfd4e458e2df6956f843d1cfa6d10335731a0fe8a4aaccc5712209342cbe1"},"mac":"fbd6d5ad1844fb3c4eec20b6a3dc036ee55aa3495abde9fcc01b9947e8a17f8f"},"id":"407fb433-ad9f-41d1-9151-eedf3c063338","version":3} -------------------------------------------------------------------------------- /node/test_block_explorer_db/database/block.db: -------------------------------------------------------------------------------- 1 | {"hash":"000000a9d18730c133869d175a886d576df5675e0e73900bf072c59047b9d734","block":{"header":{"parent":"0000000000000000000000000000000000000000000000000000000000000000","number":0,"nonce":1925346453,"time":1590684713,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x22ba1f80452e6220c7cc6ea2d1e3eeddac5f694a","value":5,"nonce":1,"data":"","time":1590684702,"signature":"0JE1yEoA3gwIiTj5ayanUZfo5ZnN7kHIRQPOw8/OZIRYWjbvbMA7vWdPgoqxnhFGiTH7FIbjCQJ25fQlvMvmPwA="}]}} 2 | {"hash":"0000004d3faa1f7b8802aa809c8b77253859846602de3402a1bc67a0026cd94d","block":{"header":{"parent":"000000a9d18730c133869d175a886d576df5675e0e73900bf072c59047b9d734","number":1,"nonce":1331825342,"time":1592320406,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x596b0709ed646e3e76b6c1fd58297b145b68387c","value":1000,"nonce":2,"data":"","time":1592320398,"signature":"1JIi9sYEZ+9RBG7IwACmm9vC4D7QVXqvBH1Es7cmeCJljTknVM80AzrhoLAW9RwCguunRO0qpN4JJ287VLFNfAE="}]}} 3 | {"hash":"000000244ab3ada6479fd06f0eb81b3b97051859191380758cc546bfe2074759","block":{"header":{"parent":"0000004d3faa1f7b8802aa809c8b77253859846602de3402a1bc67a0026cd94d","number":2,"nonce":1155082345,"time":1592383596,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x83418bb627fcd78eb76c2457179a13c8b4ae4b9f","value":1000,"nonce":3,"data":"","time":1592383595,"signature":"K1hmALGdW3rqBYLW1BCK6yM+xuhsgkt1IZTsloKTz2VSglfC+HEF+Mg1ParxyFWES3LASfhY14A3unXP/tIIcwE="}]}} 4 | {"hash":"000000565deb1a48e87c1a07c31d45e94f2aaf4e83da9219d839a0ef1ae1c4af","block":{"header":{"parent":"000000244ab3ada6479fd06f0eb81b3b97051859191380758cc546bfe2074759","number":3,"nonce":119113350,"time":1592427706,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x904143617b429ff403743803966828971ea40e9a","value":1000,"nonce":4,"data":"","time":1592427702,"signature":"weEDE24sWVslOMC/q7gUtv8NOUotLR+WcWdiuwJIzJsJgEJos7DOyaFApcN4zy0E87bHSZL8oTkgBgwIcgEszAA="}]}} 5 | {"hash":"000000a95dce445c617e80eb0d9fc2df228ab398c302270c36ccb53f528987bf","block":{"header":{"parent":"000000565deb1a48e87c1a07c31d45e94f2aaf4e83da9219d839a0ef1ae1c4af","number":4,"nonce":2506440680,"time":1592461176,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x37a600f3096488e4464a08d90317340bf4be7782","value":1000,"nonce":5,"data":"","time":1592461167,"signature":"WvZbDX2yRDf0BSZ1W/Y2JJg6ZpvWUl0QF+lSM/2VJDclPFQXAj1YNtQoHKrkr0bglkv2vO3+1STfBeMNgrDJKwE="}]}} 6 | {"hash":"0000000c3d424a9e5fb0b3c80c6969c411308f1743d090669f5b8220ad6c755b","block":{"header":{"parent":"000000a95dce445c617e80eb0d9fc2df228ab398c302270c36ccb53f528987bf","number":5,"nonce":2310725417,"time":1592600826,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x9bbd16fd48ec35d5e3bde307f451ead7e7593461","value":1000,"nonce":6,"data":"","time":1592600819,"signature":"CZiEEPmH26j6uuKDsOtMTRQ8m7naTTCSRR2rsWsGB+QJPmx4Lb7/B/eJrtYzbZZ3y5Y1uXkQ94Amt1YipbFPMgA="}]}} 7 | {"hash":"0000005404d3131b57f9d6a390e7e3f8891d2368d2b116926c49e373b397f728","block":{"header":{"parent":"0000000c3d424a9e5fb0b3c80c6969c411308f1743d090669f5b8220ad6c755b","number":6,"nonce":2695265356,"time":1592773326,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x0fc524c45b51e3215701ef7c12e58c781b0642ac","value":1000,"nonce":7,"data":"","time":1592773318,"signature":"7X79s99DFaCpStSvr8b5Rgd1c9FcQKvMZ0NOD062oLYF0I+cU4Qt/eX4eJheAC2x28PUaIrsC/Dde+etr3gUlgA="}]}} 8 | {"hash":"000000fa799bae07541778f8f2d30eb9c6865ae53369d66dd9907323176c9bba","block":{"header":{"parent":"0000005404d3131b57f9d6a390e7e3f8891d2368d2b116926c49e373b397f728","number":7,"nonce":2126336925,"time":1598449296,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x9f23369f02924f94765711bb09ebe1937123e37d","value":1000,"nonce":8,"data":"","time":1598449291,"signature":"4ZPYoYzIV8LAuDVM09nsU+ZbLvM9O0DXSr1ij0rYxS0TewFL3N0JA+SF3gYtZ1aWa8v2TJX1XYCXlbtNAk3V/AE="}]}} 9 | {"hash":"0000003dc60b50d8f98e5e49f1cf520a84f95f51890849b1ac37eda6c07718df","block":{"header":{"parent":"000000fa799bae07541778f8f2d30eb9c6865ae53369d66dd9907323176c9bba","number":8,"nonce":2334636694,"time":1609095306,"miner":"0x3a8072e42b2402aaa9cf58c2e0cdde617cc5e76d"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x2d4307d3ff635a42eae76b1525c37cc006327a8f","value":1000,"nonce":9,"data":"","time":1609095275,"signature":"KhWr8R2OnJH0hzmVYIMnWylHTz/ozvmNFmTLWg8j5GhjRFSqIXyVc98dHFYRCyouj+LdQKpvTElNyKhH6EAziAE="}]}} 10 | {"hash":"000000732f438ae899c68cab1d266a65b2b16045415a45a27bf98a681175ec36","block":{"header":{"parent":"0000003dc60b50d8f98e5e49f1cf520a84f95f51890849b1ac37eda6c07718df","number":9,"nonce":3902028757,"time":1611767176,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x2ee775fe224f1d0d2f0649fe5dc5fa1a26694c2c","value":1000,"nonce":10,"data":"","time":1611767173,"signature":"PQmWVTayjslFKWNmOujSyhcPkyOn7Wn5Q2B75yTmfsVFewSwUP84GbBTSphl/ZBxjty1XMHqM36DJz5i2V6P/wA="}]}} 11 | {"hash":"000000e12db79ac4158db3eb66d819d73d975efd476ec5f297ca831da44e7247","block":{"header":{"parent":"000000732f438ae899c68cab1d266a65b2b16045415a45a27bf98a681175ec36","number":10,"nonce":2672111272,"time":1615626954,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0xf336ed6a7c7529df70552377d641088be4dc4519","value":1000,"nonce":11,"data":"","time":1615626952,"signature":"Y/RNc135PAK7ddzaJXg+uLlJU93Qiw9Ppo0l4xcGkYUmHX6BiP93w2xtGHZWQu/OeCYb+DEBEceUAS2GChGaTAE="}]}} 12 | {"hash":"000000d9a08b683845947e2e21229de2ac4a5a70b890a4d0a13a072b994f04e1","block":{"header":{"parent":"000000e12db79ac4158db3eb66d819d73d975efd476ec5f297ca831da44e7247","number":11,"nonce":2607470896,"time":1616575354,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x42d56b60d8dae07b027427d1dd5afa1f172ddfb3","value":1000,"nonce":12,"data":"","time":1616575349,"signature":"YmOCnP90jnwdBdVaUDulgWdteaSdj0FadxKDdTTRnP9Dk3a6sBmgMscaIEDEwZw6491mnKIOsJcXaGQ3mz+IMQA="}]}} 13 | {"hash":"00000091e605d79f8147772a059f16dd01231878a2200e0e3ad5c432f21addce","block":{"header":{"parent":"000000d9a08b683845947e2e21229de2ac4a5a70b890a4d0a13a072b994f04e1","number":12,"nonce":1345370972,"time":1620888654,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","value":1000,"nonce":13,"data":"","time":1620888643,"signature":"ATYkPzclQMhZj4biqQy9vrsZ32BOzdPzCbFY3EIPnYh62jkHX9fI3FHzEAwmqMwHn6YkWCI9JVc8xR6xWDPLewA="}]}} 14 | {"hash":"000000d77d99ec7e4daa6bd218768ed8e7ecf5e471b7d3b1e59d8d64bdfa3a92","block":{"header":{"parent":"00000091e605d79f8147772a059f16dd01231878a2200e0e3ad5c432f21addce","number":13,"nonce":2995118,"time":1621244489,"miner":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x19a08bd5df98c09f07913b2f0d71c0db9b0e561b","value":1000,"nonce":14,"data":"","time":1621244464,"signature":"6ccRK3J+DDv8zWSBIna3E77m6HuCSkgCKNox70kGKlYIiSaCOUwHxtVLbIeNqXtFpQTmmxhqrSLWFhLWM4HvyAA="}]}} 15 | {"hash":"00000025570fac3aafe9c75db731bd111955b59c18dd0bf71b921affa5958b75","block":{"header":{"parent":"000000d77d99ec7e4daa6bd218768ed8e7ecf5e471b7d3b1e59d8d64bdfa3a92","number":14,"nonce":3063498884,"time":1621521994,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":15,"nonce":1,"data":"Transfer 15","time":1621521941,"signature":"opsGHHEnMDzRDGq8jzxN4kwgT6l/iWLpa3rCtprOozwVMTFGufes14jqackH6xqiTORIzSp3ARr44emYE7bY5QA="}]}} 16 | {"hash":"0000009059f57d90155a86baac9ef87e5715cc38561e16b14454ef8e72cbb35e","block":{"header":{"parent":"00000025570fac3aafe9c75db731bd111955b59c18dd0bf71b921affa5958b75","number":15,"nonce":1178105190,"time":1621523117,"miner":"0xf04679700642fd1455782e1acc37c314aab1a847"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":6,"nonce":2,"data":"Transfer 6","time":1621522944,"signature":"7YJnIQy5JX1HESkVKcCpto60auqLoyEHZtMev5wLfIQYbb8UQyWPMNFJRkPy5jEo3z463VRbxDcdZCv0H3zp9QE="}]}} 17 | {"hash":"000000c317c51739c093d3a71b2295d72b578c9b615b47dfa3eaa2aaf5484fcf","block":{"header":{"parent":"0000009059f57d90155a86baac9ef87e5715cc38561e16b14454ef8e72cbb35e","number":16,"nonce":106539439,"time":1621524244,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":29,"nonce":3,"data":"Transfer 29","time":1621524206,"signature":"Ow6gCXp1Z+lCVBtvjkTGu1M4r3S5VvhTGx3xF57CEztvN45yyC9/mJBxjRKl4tvn0zGb0b2TAtSRzS7Mq9MzLAA="}]}} 18 | {"hash":"000000887716bf87f5df97f6e82d163ad14de1cfa06d6f1330cac15905261730","block":{"header":{"parent":"000000c317c51739c093d3a71b2295d72b578c9b615b47dfa3eaa2aaf5484fcf","number":17,"nonce":174110813,"time":1621524644,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":200,"nonce":4,"data":"Transfer 200","time":1621524608,"signature":"Se/W/qj8D3tDxXW36Ypv5E9cbzG1fBafOarxB786tIxRCPCv3chACmsxZJi39yO4/koLIGKHTdGMK4dvqtADzwA="}]}} 19 | {"hash":"00000087b131393cebb4d31648f5ae5acb71fdf766c9e4d9aec941afe41670f9","block":{"header":{"parent":"000000887716bf87f5df97f6e82d163ad14de1cfa06d6f1330cac15905261730","number":18,"nonce":3582736810,"time":1621524824,"miner":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":100,"nonce":5,"data":"Transfer 100","time":1621524749,"signature":"lCASIjXLb7YvEIltY5ADVTScPk1WJlTlmQAL8nwephV1mnjDsdnLm02X0xGgdO4oRJ/ejMxTAepELh9br6wUOwE="}]}} 20 | {"hash":"0000007414e771708a2f5e2cdd16e29dd2b2a5a858bbb5341a4eb8d855fc8861","block":{"header":{"parent":"00000087b131393cebb4d31648f5ae5acb71fdf766c9e4d9aec941afe41670f9","number":19,"nonce":228085503,"time":1621525024,"miner":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":100,"nonce":6,"data":"Transfer 100","time":1621525020,"signature":"57mo1Qp+hQkur1T184zdXIFtf8yhdWFicH3wpf685tZFjqEz4pLSiakUp/rpBjkC7rWiR++SaC2mOq1HbrZxigE="}]}} 21 | {"hash":"0000007f621be554d5d549257805d892ed9d814c7183c3fd4a0339462459f71d","block":{"header":{"parent":"0000007414e771708a2f5e2cdd16e29dd2b2a5a858bbb5341a4eb8d855fc8861","number":20,"nonce":3083865082,"time":1621570954,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":125,"nonce":7,"data":"Transfer 125","time":1621570918,"signature":"8DyvnWCvBSu8IbhfJgOJ4E4R1misjyT96l42Idrzks4OUXMxh8WFJMR6SAm2B51NC2A9yO4pyr10nBalMNJNsQE="}]}} 22 | {"hash":"0000008ab8ac1b65d32f443e02a313beadedd3698d3cfc2909fda3b48fb071ae","block":{"header":{"parent":"0000007f621be554d5d549257805d892ed9d814c7183c3fd4a0339462459f71d","number":21,"nonce":3499724786,"time":1621571266,"miner":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":25,"nonce":8,"data":"Transfer 25","time":1621571257,"signature":"vLh0mRM9QZPNZcVVBjn2KQIGsQUeA507vXrF9ue9kk90pylWbTsF6LEcsGJOzE8ijqzg0Cx954mMLnnOW4wlaQA="}]}} 23 | {"hash":"00000010a7e6e8c128b03d3bfbe8cac1213af85ffc25fe5cf1e143b2f33e639c","block":{"header":{"parent":"0000008ab8ac1b65d32f443e02a313beadedd3698d3cfc2909fda3b48fb071ae","number":22,"nonce":2106401972,"time":1621571757,"miner":"0xf04679700642fd1455782e1acc37c314aab1a847"},"payload":[{"from":"0xf04679700642fd1455782e1acc37c314aab1a847","to":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","value":175,"nonce":1,"data":"Transfer 175","time":1621571756,"signature":"R/dcCTl1kWfcTWptQaV05XgoHVgi58NvqfE2U0lhA/8zPpsv0QXHnSHVugPyI+jiJbQpbXylgnLIT/NRIMMmiAE="}]}} 24 | {"hash":"0000003368ec17588b38d0bb4405c6c01f602977f3f2b16db1d821bf166a50d8","block":{"header":{"parent":"00000010a7e6e8c128b03d3bfbe8cac1213af85ffc25fe5cf1e143b2f33e639c","number":23,"nonce":4004882252,"time":1621594222,"miner":"0xf04679700642fd1455782e1acc37c314aab1a847"},"payload":[{"from":"0xf04679700642fd1455782e1acc37c314aab1a847","to":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","value":123,"nonce":2,"data":"Transfer 123","time":1621594188,"signature":"+vv9l+RMcmj7RMKekGzmSWaj6mA/WoZp8LC63gVsl2BQFQc5dz0RqSJFeknpd6tDo5fRm16PEN/LwBL0kD36vwE="}]}} 25 | {"hash":"000000affacd3b491dab7b2548883f35f6bea351cf560b6dd896d1e9677bd973","block":{"header":{"parent":"0000003368ec17588b38d0bb4405c6c01f602977f3f2b16db1d821bf166a50d8","number":24,"nonce":3143630587,"time":1621627784,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0xf04679700642fd1455782e1acc37c314aab1a847","to":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","value":12,"nonce":3,"data":"Transfer 12","time":1621627747,"signature":"k6NEpB3zhEtDo1tfn+vgDCJP1WxBsNIdjyjRlkoBXktVbkE2lIW7tsq0df0YU2Cvznmwbu5slcaJWfpsXOcpfwE="}]}} 26 | {"hash":"000000cc3e2b5b1e02bbafadbc377037bfc776a04637e320ab0b9a7e2baac4f1","block":{"header":{"parent":"000000affacd3b491dab7b2548883f35f6bea351cf560b6dd896d1e9677bd973","number":25,"nonce":551018895,"time":1621628288,"miner":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b"},"payload":[{"from":"0xf04679700642fd1455782e1acc37c314aab1a847","to":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","value":140,"nonce":4,"data":"Transfer 140","time":1621628234,"signature":"ztC8Ge1QkvjQjP9hqlRet/22G3lEqE9PiW4fFWEVjhVClZues+2Ep7+pD1mkVn8LUerTtnBfRF1Gy9nLOGa1gwE="}]}} 27 | {"hash":"00000023f875dad99a0d70b40f48c14adb803928ae02461d8ef2fe7e341e53d8","block":{"header":{"parent":"000000cc3e2b5b1e02bbafadbc377037bfc776a04637e320ab0b9a7e2baac4f1","number":26,"nonce":3629766773,"time":1621817922,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0xf04679700642fd1455782e1acc37c314aab1a847","to":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","value":200,"nonce":5,"data":"Transfer 200","time":1621817890,"signature":"0N6WmYWgqM1DShcix85ZYS5jmatsD3GnA9XkRmCrA38HE61m0wdKWhG7+kM0yJ4K0jUpaplMFgpkjz/UP/pCZgE="}]}} 28 | {"hash":"000000ee513055d5cc7474567c3e98ee7e3550ef1f0a9aaf83fe9fb0b27f6731","block":{"header":{"parent":"00000023f875dad99a0d70b40f48c14adb803928ae02461d8ef2fe7e341e53d8","number":27,"nonce":1547133409,"time":1622638672,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":250,"nonce":9,"data":"Transfer 250","time":1622638385,"signature":"3mjZ57RTxfHjq2+MaRI3D4QkFYE2lRSAUVIXRUBS7/036wMm/4DwK8SJID4ja1S96EslNoh8UAf220cPdvZbiQE="}]}} 29 | {"hash":"00000089d776367e1ab2d1dfcc0bc757b68fe48ffa441685ea59e6810312765f","block":{"header":{"parent":"000000ee513055d5cc7474567c3e98ee7e3550ef1f0a9aaf83fe9fb0b27f6731","number":28,"nonce":4280779019,"time":1622640944,"miner":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":210,"nonce":10,"data":"Transfer 210","time":1622640932,"signature":"bATeothT00ljHBdSvQKJ0npXu+CUQQGyxH4/PSvKVgEL506bjadFggNIwNZr/MEjl2Cs88q6s4iKf7lAVTxy+wA="}]}} 30 | {"hash":"0000001847e770dcb19328443d192ba8057bfbbb9580f86118ad2af3d9324de2","block":{"header":{"parent":"00000089d776367e1ab2d1dfcc0bc757b68fe48ffa441685ea59e6810312765f","number":29,"nonce":2071690315,"time":1622641332,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":190,"nonce":11,"data":"Transfer 190","time":1622641302,"signature":"E9edaGYRADMQmB0Ac4D7+MZvncSPJMBh7dVjIkXjp91M5a+75q6WXWpBmlLEnVxBfQIR3DnuJuaanOOkdYYKKQA="}]}} 31 | {"hash":"0000004ca9bc420867431151290adfa7451065efa2cc9782fb507864912ee9cf","block":{"header":{"parent":"0000001847e770dcb19328443d192ba8057bfbbb9580f86118ad2af3d9324de2","number":30,"nonce":3794034894,"time":1622641429,"miner":"0xf04679700642fd1455782e1acc37c314aab1a847"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":150,"nonce":12,"data":"Transfer 150","time":1622641419,"signature":"i8HK+zjieJ/b+e/ogxXCa7twFQ+iEWK8K7s2/pIdMVEWvmvWXPgumu1uoXT37o4hksQnB+zVLSjI+SD8fkIJdAE="}]}} 32 | {"hash":"00000046c70d0b6ddb21cb5eb590c11fabd33f44ad5a728be8c7afbc2a61ab5f","block":{"header":{"parent":"0000004ca9bc420867431151290adfa7451065efa2cc9782fb507864912ee9cf","number":31,"nonce":2302011767,"time":1622643435,"miner":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":345,"nonce":13,"data":"Transfer 345","time":1622643433,"signature":"hgAipbaCCVFb7P+Spi6tzL8G/duiEatPi7kqxV+3d8hetKPOw5Q1OfF0Uweh2ezgVaQ2e+IIMVxHW22WWWLzbwE="}]}} 33 | {"hash":"00000055e944d6cca5c8591816151842629e1f16382c15a019f3b2f21ce8ecd9","block":{"header":{"parent":"00000046c70d0b6ddb21cb5eb590c11fabd33f44ad5a728be8c7afbc2a61ab5f","number":32,"nonce":1777160254,"time":1622644842,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0xdd1fbd71d7f78a810ae10829220fbb7bd2f1818b","to":"0xf04679700642fd1455782e1acc37c314aab1a847","value":105,"nonce":14,"data":"Transfer 105","time":1622644819,"signature":"tydYuOcQFKR6WNC5iGCHyG1aFKd6TPdZf3O55ZXyO9oQnh2sWcyzzgFRV+TWKjU0bniNMQHfw2PIkrV+W4MRIQE="}]}} 34 | {"hash":"000000418a2f268ff27a8438de43993a2c6b9c19aca2fb28838953bf7d902265","block":{"header":{"parent":"00000055e944d6cca5c8591816151842629e1f16382c15a019f3b2f21ce8ecd9","number":33,"nonce":3099312863,"time":1631090502,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","value":1,"nonce":15,"data":"","time":1631090498,"signature":"AotKql35vnuf/k+FEVxuhvz0nrFY73FJJBSsm9a8aF1tVSAUwdFspunoC55uxFs12AYBNEhBUmTHXAAapN5+HgA="}]}} 35 | {"hash":"0000002024e93f35778d929a16c25914faeeadedcd7f92b958ee73cc803f6982","block":{"header":{"parent":"000000418a2f268ff27a8438de43993a2c6b9c19aca2fb28838953bf7d902265","number":34,"nonce":1877354617,"time":1631213624,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","value":1,"nonce":16,"data":"","time":1631213616,"signature":"UTsxR703lJ7JkBwN6ym31emOYbLx5yLfvInjxtNtJCRELFBkfq5v1dwUz0d2YPCs4DhLrx3IZ+BtnwT1+XwXRwE="}]}} 36 | {"hash":"000000bc85637b14ce4eaf304a193f1834c1a0f7fd05de4b278b87c5cfe8c5c3","block":{"header":{"parent":"0000002024e93f35778d929a16c25914faeeadedcd7f92b958ee73cc803f6982","number":35,"nonce":200816263,"time":1631213784,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","gas":21,"gasPrice":1,"value":1,"nonce":17,"data":"","time":1631213780,"signature":"B8cYHGEI/VUopm3BdofsHAIMdmbRhytUNdKG4Wy92Qh9zeYnG9I+FuTYJ8HGWEXxHoA9UEJtdSwKk5+E4myx0AE="}]}} 37 | {"hash":"00000050159ee00da041cb8bd1a1974ea80da53f8489ade6c4886dcf48205ca9","block":{"header":{"parent":"000000bc85637b14ce4eaf304a193f1834c1a0f7fd05de4b278b87c5cfe8c5c3","number":36,"nonce":3757832204,"time":1633941374,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0x5027d26d187a4f35c62ecc39fcbf2f0cfa78ea44","gas":21,"gasPrice":1,"value":1000,"nonce":18,"data":"","time":1633941370,"signature":"jcm/CT/187u8zKjSLJkIFT2JwAZMMq7713RWXdJ3pBRSNlG4ZYJJGs1dtA+/qUx5bNFvRg4paxyQVeS8Gg9qTQA="}]}} 38 | {"hash":"000000a0718c1f556177e470dc42a486f04226fa1a39c123446585457597b5c1","block":{"header":{"parent":"00000050159ee00da041cb8bd1a1974ea80da53f8489ade6c4886dcf48205ca9","number":37,"nonce":963635204,"time":1642954064,"miner":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c"},"payload":[{"from":"0x09ee50f2f37fcba1845de6fe5c762e83e65e755c","to":"0xe16b3d233331688b44fe4329644dd4956df40a15","gas":21,"gasPrice":1,"value":1000,"nonce":19,"data":"","time":1642954054,"signature":"wv5VX/ln0NV5LhoZh49qdRgWa3Z4gOK5BlDP9u/zJn115NlWi29r0Vb+SurVfAy88yRWpySGusUp/7lYevV5rQE="}]}} 39 | -------------------------------------------------------------------------------- /node/test_block_explorer_db/database/genesis.json: -------------------------------------------------------------------------------- 1 | { 2 | "genesis_time": "2020-06-01T00:00:00.000000000Z", 3 | "chain_id": "the-blockchain-bar-ledger", 4 | "symbol": "TBB", 5 | "balances": { 6 | "0x09eE50f2F37FcBA1845dE6FE5C762E83E65E755c": 1000000 7 | }, 8 | "fork_tip_1": 35 9 | } 10 | -------------------------------------------------------------------------------- /node/test_block_explorer_db/keystore/UTC--2021-12-26T20-49-55.107305000Z--da8206ac3495db5077b5ee340bd4cee6ce8d7e4f: -------------------------------------------------------------------------------- 1 | {"address":"da8206ac3495db5077b5ee340bd4cee6ce8d7e4f","crypto":{"cipher":"aes-128-ctr","ciphertext":"582fd1f142c11edd0b7de813716e069206bf1d9e4e2658bdaa694a4adc5352b0","cipherparams":{"iv":"c6dcdd7949792789c17c546569030bb2"},"kdf":"scrypt","kdfparams":{"dklen":32,"n":262144,"p":1,"r":8,"salt":"0c07a465476176a73e355dd203e3b8a2a9de58ac2e9d43ba4ece1d6202eb4df5"},"mac":"70eaf165885fe6aa2dc87c6f71860c0eef8f71d90f1c434d92e0d4c7627d3d46"},"id":"3d47581f-b5d5-413f-ba1b-593a13ae5f94","version":3} -------------------------------------------------------------------------------- /public/img/andrej_babayaga_caesar_sync_p2p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web3coach/the-blockchain-bar/09c02ae08b109f080116130fe9c3e0b9329cc53b/public/img/andrej_babayaga_caesar_sync_p2p.png -------------------------------------------------------------------------------- /public/img/andrej_babayaga_crypto_sign_summary.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web3coach/the-blockchain-bar/09c02ae08b109f080116130fe9c3e0b9329cc53b/public/img/andrej_babayaga_crypto_sign_summary.png -------------------------------------------------------------------------------- /public/img/book_cover2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web3coach/the-blockchain-bar/09c02ae08b109f080116130fe9c3e0b9329cc53b/public/img/book_cover2.png -------------------------------------------------------------------------------- /public/img/mining_p2p.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web3coach/the-blockchain-bar/09c02ae08b109f080116130fe9c3e0b9329cc53b/public/img/mining_p2p.png -------------------------------------------------------------------------------- /public/img/test_ethereum_signature.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/web3coach/the-blockchain-bar/09c02ae08b109f080116130fe9c3e0b9329cc53b/public/img/test_ethereum_signature.png -------------------------------------------------------------------------------- /wallet/wallet.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package wallet 17 | 18 | import ( 19 | "crypto/ecdsa" 20 | "crypto/rand" 21 | "crypto/sha256" 22 | "fmt" 23 | "github.com/ethereum/go-ethereum/accounts" 24 | "github.com/ethereum/go-ethereum/accounts/keystore" 25 | "github.com/ethereum/go-ethereum/common" 26 | "github.com/ethereum/go-ethereum/crypto" 27 | "github.com/pborman/uuid" 28 | "github.com/web3coach/the-blockchain-bar/database" 29 | "io/ioutil" 30 | "path/filepath" 31 | ) 32 | 33 | const keystoreDirName = "keystore" 34 | 35 | func GetKeystoreDirPath(dataDir string) string { 36 | return filepath.Join(dataDir, keystoreDirName) 37 | } 38 | 39 | func NewKeystoreAccount(dataDir, password string) (common.Address, error) { 40 | ks := keystore.NewKeyStore(GetKeystoreDirPath(dataDir), keystore.StandardScryptN, keystore.StandardScryptP) 41 | acc, err := ks.NewAccount(password) 42 | if err != nil { 43 | return common.Address{}, err 44 | } 45 | 46 | return acc.Address, nil 47 | } 48 | 49 | func SignTxWithKeystoreAccount(tx database.Tx, acc common.Address, pwd, keystoreDir string) (database.SignedTx, error) { 50 | ks := keystore.NewKeyStore(keystoreDir, keystore.StandardScryptN, keystore.StandardScryptP) 51 | ksAccount, err := ks.Find(accounts.Account{Address: acc}) 52 | if err != nil { 53 | return database.SignedTx{}, err 54 | } 55 | 56 | ksAccountJson, err := ioutil.ReadFile(ksAccount.URL.Path) 57 | if err != nil { 58 | return database.SignedTx{}, err 59 | } 60 | 61 | key, err := keystore.DecryptKey(ksAccountJson, pwd) 62 | if err != nil { 63 | return database.SignedTx{}, err 64 | } 65 | 66 | signedTx, err := SignTx(tx, key.PrivateKey) 67 | if err != nil { 68 | return database.SignedTx{}, err 69 | } 70 | 71 | return signedTx, nil 72 | } 73 | 74 | func SignTx(tx database.Tx, privKey *ecdsa.PrivateKey) (database.SignedTx, error) { 75 | rawTx, err := tx.Encode() 76 | if err != nil { 77 | return database.SignedTx{}, err 78 | } 79 | 80 | sig, err := Sign(rawTx, privKey) 81 | if err != nil { 82 | return database.SignedTx{}, err 83 | } 84 | 85 | return database.NewSignedTx(tx, sig), nil 86 | } 87 | 88 | func Sign(msg []byte, privKey *ecdsa.PrivateKey) (sig []byte, err error) { 89 | msgHash := sha256.Sum256(msg) 90 | 91 | return crypto.Sign(msgHash[:], privKey) 92 | } 93 | 94 | func Verify(msg, sig []byte) (*ecdsa.PublicKey, error) { 95 | msgHash := sha256.Sum256(msg) 96 | 97 | recoveredPubKey, err := crypto.SigToPub(msgHash[:], sig) 98 | if err != nil { 99 | return nil, fmt.Errorf("unable to verify message signature. %s", err.Error()) 100 | } 101 | 102 | return recoveredPubKey, nil 103 | } 104 | 105 | func NewRandomKey() (*keystore.Key, error) { 106 | privateKeyECDSA, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader) 107 | if err != nil { 108 | return nil, err 109 | } 110 | 111 | id := uuid.NewRandom() 112 | key := &keystore.Key{ 113 | Id: id, 114 | Address: crypto.PubkeyToAddress(privateKeyECDSA.PublicKey), 115 | PrivateKey: privateKeyECDSA, 116 | } 117 | 118 | return key, nil 119 | } 120 | -------------------------------------------------------------------------------- /wallet/wallet_test.go: -------------------------------------------------------------------------------- 1 | // Copyright 2020 The the-blockchain-bar Authors 2 | // This file is part of the the-blockchain-bar library. 3 | // 4 | // The the-blockchain-bar library is free software: you can redistribute it and/or modify 5 | // it under the terms of the GNU Lesser General Public License as published by 6 | // the Free Software Foundation, either version 3 of the License, or 7 | // (at your option) any later version. 8 | // 9 | // The the-blockchain-bar library is distributed in the hope that it will be useful, 10 | // but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | // GNU Lesser General Public License for more details. 13 | // 14 | // You should have received a copy of the GNU Lesser General Public License 15 | // along with the go-ethereum library. If not, see . 16 | package wallet 17 | 18 | import ( 19 | "crypto/ecdsa" 20 | "crypto/elliptic" 21 | "crypto/rand" 22 | "encoding/json" 23 | "fmt" 24 | "github.com/stretchr/testify/require" 25 | "io/ioutil" 26 | "math/big" 27 | "testing" 28 | 29 | "github.com/davecgh/go-spew/spew" 30 | "github.com/ethereum/go-ethereum/common" 31 | "github.com/ethereum/go-ethereum/crypto" 32 | "github.com/web3coach/the-blockchain-bar/database" 33 | "github.com/web3coach/the-blockchain-bar/fs" 34 | ) 35 | 36 | // The password for testing keystore files: 37 | // ./node/test_andrej--3eb92807f1f91a8d4d85bc908c7f86dcddb1df57 38 | // ./node/test_babayaga--6fdc0d8d15ae6b4ebf45c52fd2aafbcbb19a65c8 39 | const testKeystoreAccountsPwd = "security123" 40 | 41 | // Prints a PK: 42 | // 43 | // (*ecdsa.PrivateKey)(0xc000099980)({ 44 | // PublicKey: (ecdsa.PublicKey) { 45 | // Curve: (*secp256k1.BitCurve)(0xc0000982d0)({ 46 | // P: (*big.Int)(0xc0000b03c0)(115792089237316195423570985008687907853269984665640564039457584007908834671663), 47 | // N: (*big.Int)(0xc0000b0400)(115792089237316195423570985008687907852837564279074904382605163141518161494337), 48 | // B: (*big.Int)(0xc0000b0440)(7), 49 | // Gx: (*big.Int)(0xc0000b0480)(55066263022277343669578718895168534326250603453777594175500187360389116729240), 50 | // Gy: (*big.Int)(0xc0000b04c0)(32670510020758816978083085130507043184471273380659243275938904335757337482424), 51 | // BitSize: (int) 256 52 | // }), 53 | // X: (*big.Int)(0xc0000b1aa0)(1344160861301624411922901086431771879005615956563347131047269353924650464711), 54 | // Y: (*big.Int)(0xc0000b1ac0)(73524953917715096899857106141372214583670064515671280443711113049610951453654) 55 | // }, 56 | // D: (*big.Int)(0xc0000b1a40)(41116516511979929812568468771132209652243963107895293136581156908462828164432) 57 | //}) 58 | // 59 | // And r, s, v signature params: 60 | // 61 | // (*big.Int)(0xc0000b1b20)(88181292280759186801869952076472415807575357966745986437065510600744149574656) 62 | // (*big.Int)(0xc0000b1b40)(23476722530623450948411712153618947971604430187320320363672662539909827697049) 63 | // (*big.Int)(0xc0000b1b60)(1) 64 | func TestSignCryptoParams(t *testing.T) { 65 | privKey, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader) 66 | if err != nil { 67 | t.Fatal(err) 68 | } 69 | spew.Dump(privKey) 70 | 71 | msg := []byte("the Web3Coach students are awesome") 72 | 73 | sig, err := Sign(msg, privKey) 74 | if err != nil { 75 | t.Fatal(err) 76 | } 77 | 78 | if len(sig) != crypto.SignatureLength { 79 | t.Fatal(fmt.Errorf("wrong size for signature: got %d, want %d", len(sig), crypto.SignatureLength)) 80 | } 81 | 82 | r := new(big.Int).SetBytes(sig[:32]) 83 | s := new(big.Int).SetBytes(sig[32:64]) 84 | v := new(big.Int).SetBytes([]byte{sig[64]}) 85 | 86 | spew.Dump(r, s, v) 87 | } 88 | 89 | func TestSign(t *testing.T) { 90 | privKey, err := ecdsa.GenerateKey(crypto.S256(), rand.Reader) 91 | if err != nil { 92 | t.Fatal(err) 93 | } 94 | 95 | pubKey := privKey.PublicKey 96 | pubKeyBytes := elliptic.Marshal(crypto.S256(), pubKey.X, pubKey.Y) 97 | pubKeyBytesHash := crypto.Keccak256(pubKeyBytes[1:]) 98 | 99 | account := common.BytesToAddress(pubKeyBytesHash[12:]) 100 | 101 | msg := []byte("the Web3Coach students are awesome") 102 | 103 | sig, err := Sign(msg, privKey) 104 | if err != nil { 105 | t.Fatal(err) 106 | } 107 | 108 | recoveredPubKey, err := Verify(msg, sig) 109 | if err != nil { 110 | t.Fatal(err) 111 | } 112 | 113 | recoveredPubKeyBytes := elliptic.Marshal(crypto.S256(), recoveredPubKey.X, recoveredPubKey.Y) 114 | recoveredPubKeyBytesHash := crypto.Keccak256(recoveredPubKeyBytes[1:]) 115 | recoveredAccount := common.BytesToAddress(recoveredPubKeyBytesHash[12:]) 116 | 117 | if account.Hex() != recoveredAccount.Hex() { 118 | t.Fatalf("msg was signed by account %s but signature recovery produced an account %s", account.Hex(), recoveredAccount.Hex()) 119 | } 120 | } 121 | 122 | func TestSignTxWithKeystoreAccount(t *testing.T) { 123 | tmpDir, err := ioutil.TempDir("", "wallet_test") 124 | if err != nil { 125 | t.Fatal(err) 126 | } 127 | defer fs.RemoveDir(tmpDir) 128 | 129 | andrej, err := NewKeystoreAccount(tmpDir, testKeystoreAccountsPwd) 130 | if err != nil { 131 | t.Error(err) 132 | return 133 | } 134 | 135 | babaYaga, err := NewKeystoreAccount(tmpDir, testKeystoreAccountsPwd) 136 | if err != nil { 137 | t.Error(err) 138 | return 139 | } 140 | 141 | tx := database.NewBaseTx(andrej, babaYaga, 100, 1, "") 142 | 143 | signedTx, err := SignTxWithKeystoreAccount(tx, andrej, testKeystoreAccountsPwd, GetKeystoreDirPath(tmpDir)) 144 | if err != nil { 145 | t.Error(err) 146 | return 147 | } 148 | 149 | ok, err := signedTx.IsAuthentic() 150 | if err != nil { 151 | t.Error(err) 152 | return 153 | } 154 | 155 | if !ok { 156 | t.Error("the TX was signed by 'from' account and should have been authentic") 157 | return 158 | } 159 | 160 | // Test marshaling 161 | signedTxJson, err := json.Marshal(signedTx) 162 | if err != nil { 163 | t.Error(err) 164 | return 165 | } 166 | 167 | var signedTxUnmarshaled database.SignedTx 168 | err = json.Unmarshal(signedTxJson, &signedTxUnmarshaled) 169 | if err != nil { 170 | t.Error(err) 171 | return 172 | } 173 | 174 | require.Equal(t, signedTx, signedTxUnmarshaled) 175 | } 176 | 177 | func TestSignForgedTxWithKeystoreAccount(t *testing.T) { 178 | tmpDir, err := ioutil.TempDir("", "wallet_test") 179 | if err != nil { 180 | t.Fatal(err) 181 | } 182 | defer fs.RemoveDir(tmpDir) 183 | 184 | hacker, err := NewKeystoreAccount(tmpDir, testKeystoreAccountsPwd) 185 | if err != nil { 186 | t.Error(err) 187 | return 188 | } 189 | 190 | babaYaga, err := NewKeystoreAccount(tmpDir, testKeystoreAccountsPwd) 191 | if err != nil { 192 | t.Error(err) 193 | return 194 | } 195 | 196 | forgedTx := database.NewBaseTx(babaYaga, hacker, 100, 1, "") 197 | 198 | signedTx, err := SignTxWithKeystoreAccount(forgedTx, hacker, testKeystoreAccountsPwd, GetKeystoreDirPath(tmpDir)) 199 | if err != nil { 200 | t.Error(err) 201 | return 202 | } 203 | 204 | ok, err := signedTx.IsAuthentic() 205 | if err != nil { 206 | t.Error(err) 207 | return 208 | } 209 | 210 | if ok { 211 | t.Fatal("the TX 'from' attribute was forged and should have not be authentic") 212 | } 213 | } 214 | --------------------------------------------------------------------------------