├── .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 | [](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 | 
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 | 
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 | 
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 | 
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 |
--------------------------------------------------------------------------------