├── .github
├── dependabot.yml
└── workflows
│ └── test-and-release.yml
├── .gitignore
├── .rustfmt.nightly.toml
├── CODEOWNERS
├── Cargo.lock
├── Cargo.toml
├── LICENSE
├── README.md
├── scripts
├── build.release.sh
├── tests.e2e.sh
├── tests.lint.sh
├── tests.unit.sh
└── tests.unused.sh
├── tests
└── e2e
│ ├── Cargo.toml
│ └── src
│ ├── lib.rs
│ └── tests
│ └── mod.rs
└── timestampvm
├── Cargo.toml
└── src
├── api
├── chain_handlers.rs
├── mod.rs
└── static_handlers.rs
├── bin
└── timestampvm
│ ├── genesis.rs
│ ├── main.rs
│ └── vm_id.rs
├── block
└── mod.rs
├── client
└── mod.rs
├── genesis
└── mod.rs
├── lib.rs
├── state
└── mod.rs
└── vm
└── mod.rs
/.github/dependabot.yml:
--------------------------------------------------------------------------------
1 | version: 2
2 | updates:
3 | - package-ecosystem: cargo
4 | directory: "/"
5 | schedule:
6 | interval: daily
7 | time: "10:00"
8 | open-pull-requests-limit: 10
9 |
--------------------------------------------------------------------------------
/.github/workflows/test-and-release.yml:
--------------------------------------------------------------------------------
1 | name: Test and release
2 |
3 | # ref. https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions
4 | on:
5 | push:
6 | branches:
7 | - main
8 | tags:
9 | - "*"
10 | pull_request:
11 |
12 | permissions:
13 | contents: write
14 |
15 | jobs:
16 | static_analysis:
17 | name: Static analysis (lint)
18 | runs-on: ubuntu-latest
19 | steps:
20 | - name: Checkout
21 | uses: actions/checkout@v3
22 |
23 | - name: Install Rust
24 | uses: actions-rs/toolchain@v1
25 | with:
26 | toolchain: nightly
27 | profile: minimal
28 | components: rustfmt, clippy
29 | override: true
30 |
31 | - name: Check Rust version
32 | run: rustc --version
33 |
34 | - uses: Swatinem/rust-cache@v1
35 | with:
36 | cache-on-failure: true
37 |
38 | - name: Run static analysis tests
39 | shell: bash
40 | run: scripts/tests.lint.sh
41 |
42 | # check_cargo_unused:
43 | # name: Check Cargo unused
44 | # runs-on: ubuntu-latest
45 | # steps:
46 | # - name: Checkout
47 | # uses: actions/checkout@v3
48 |
49 | # # or use "abelfodil/protoc-action@v1"
50 | # # ref. https://github.com/hyperium/tonic/issues/1047#issuecomment-1222508191
51 | # - name: Install protoc
52 | # uses: arduino/setup-protoc@v1
53 | # with:
54 | # version: "3.x"
55 | # repo-token: ${{ secrets.GITHUB_TOKEN }}
56 |
57 | # - name: Install Rust
58 | # uses: actions-rs/toolchain@v1
59 | # with:
60 | # toolchain: nightly
61 | # profile: minimal
62 | # components: rustfmt, clippy
63 | # override: true
64 |
65 | # - name: Check Rust version
66 | # run: rustc --version
67 |
68 | # - uses: Swatinem/rust-cache@v1
69 | # with:
70 | # cache-on-failure: true
71 |
72 | # - name: Check unused Cargo dependencies
73 | # shell: bash
74 | # run: scripts/tests.unused.sh
75 |
76 | unit_tests:
77 | name: Unit tests
78 | runs-on: ubuntu-latest
79 | steps:
80 | - name: Checkout
81 | uses: actions/checkout@v3
82 |
83 | # or use "abelfodil/protoc-action@v1"
84 | # ref. https://github.com/hyperium/tonic/issues/1047#issuecomment-1222508191
85 | - name: Install protoc
86 | uses: arduino/setup-protoc@v1
87 | with:
88 | version: "3.x"
89 | repo-token: ${{ secrets.GITHUB_TOKEN }}
90 |
91 | - name: Install Rust
92 | uses: actions-rs/toolchain@v1
93 | with:
94 | toolchain: stable
95 | profile: minimal
96 | override: true
97 |
98 | - name: Check Rust version
99 | run: rustc --version
100 |
101 | - uses: Swatinem/rust-cache@v1
102 | with:
103 | cache-on-failure: true
104 |
105 | - name: Run unit tests
106 | run: scripts/tests.unit.sh
107 |
108 | e2e_tests:
109 | name: e2e tests
110 | runs-on: ubuntu-latest
111 | steps:
112 | - name: Checkout
113 | uses: actions/checkout@v3
114 |
115 | # or use "abelfodil/protoc-action@v1"
116 | # ref. https://github.com/hyperium/tonic/issues/1047#issuecomment-1222508191
117 | - name: Install protoc
118 | uses: arduino/setup-protoc@v1
119 | with:
120 | version: "3.x"
121 | repo-token: ${{ secrets.GITHUB_TOKEN }}
122 |
123 | - name: Install Rust
124 | uses: actions-rs/toolchain@v1
125 | with:
126 | toolchain: stable
127 | profile: minimal
128 | override: true
129 |
130 | - name: Check Rust version
131 | run: rustc --version
132 |
133 | - uses: Swatinem/rust-cache@v1
134 | with:
135 | cache-on-failure: true
136 |
137 | - name: Build plugin
138 | uses: actions-rs/cargo@v1
139 | with:
140 | command: build
141 | args: --release --bin timestampvm
142 |
143 | - name: Run e2e tests
144 | run: VM_PLUGIN_PATH=/home/runner/work/timestampvm-rs/timestampvm-rs/target/release/timestampvm scripts/tests.e2e.sh 1.11.1
145 |
146 | release:
147 | name: Release ${{ matrix.job.target }} (${{ matrix.job.os }})
148 | runs-on: ${{ matrix.job.os }}
149 | #needs: [static_analysis, check_cargo_unused, unit_tests, e2e_tests]
150 | needs: [static_analysis, unit_tests, e2e_tests]
151 | strategy:
152 | matrix:
153 | job:
154 | # https://doc.rust-lang.org/nightly/rustc/platform-support.html
155 | - os: ubuntu-20.04
156 | platform: linux
157 | target: x86_64-unknown-linux-gnu
158 | - os: macos-latest
159 | platform: darwin
160 | target: x86_64-apple-darwin
161 | # - os: ubuntu-latest
162 | # platform: linux
163 | # target: aarch64-unknown-linux-gnu
164 | - os: macos-latest
165 | platform: darwin
166 | target: aarch64-apple-darwin
167 | # - os: windows-latest
168 | # platform: win32
169 | # target: x86_64-pc-windows-msvc
170 |
171 | steps:
172 | - name: Checkout
173 | uses: actions/checkout@v3
174 |
175 | # or use "abelfodil/protoc-action@v1"
176 | # ref. https://github.com/hyperium/tonic/issues/1047#issuecomment-1222508191
177 | - name: Install protoc
178 | uses: arduino/setup-protoc@v1
179 | with:
180 | version: "3.x"
181 | repo-token: ${{ secrets.GITHUB_TOKEN }}
182 |
183 | - name: Install Rust
184 | uses: actions-rs/toolchain@v1
185 | with:
186 | profile: minimal
187 | toolchain: stable
188 | target: ${{ matrix.job.target }}
189 | override: true
190 |
191 | - name: Check Rust version
192 | run: rustc --version
193 |
194 | - uses: Swatinem/rust-cache@v1
195 | with:
196 | cache-on-failure: true
197 |
198 | # ref. https://github.com/gakonst/foundry/blob/master/.github/workflows/cross-platform.yml
199 | - name: Apple M1 setup
200 | if: matrix.job.target == 'aarch64-apple-darwin'
201 | run: |
202 | echo "SDKROOT=$(xcrun -sdk macosx --show-sdk-path)" >> $GITHUB_ENV
203 | echo "MACOSX_DEPLOYMENT_TARGET=$(xcrun -sdk macosx --show-sdk-platform-version)" >> $GITHUB_ENV
204 |
205 | - name: Linux setup
206 | if: matrix.job.platform == 'linux'
207 | run: |
208 | sudo apt-get install -y --no-install-recommends pkg-config libssl-dev musl-tools clang llvm
209 | echo "CC_aarch64_unknown_linux_musl=clang" >> $GITHUB_ENV
210 | echo "AR_aarch64_unknown_linux_musl=llvm-ar" >> $GITHUB_ENV
211 | echo "CARGO_TARGET_AARCH64_UNKNOWN_LINUX_MUSL_RUSTFLAGS=\"-Clink-self-contained=yes -Clinker=rust-lld\"" >> $GITHUB_ENV
212 | echo "PKG_CONFIG_ALLOW_CROSS=1" >> $GITHUB_ENV
213 |
214 | # ref. https://github.com/actions-rs/cargo
215 | - name: Build target
216 | env:
217 | RUSTFLAGS: -C link-args=-s
218 | uses: actions-rs/cargo@v1
219 | with:
220 | # use-cross: true
221 | command: build
222 | args: --release --bin timestampvm --target ${{ matrix.job.target }}
223 |
224 | - name: Compress binaries
225 | id: release_artifacts
226 | env:
227 | PLATFORM_NAME: ${{ matrix.job.platform }}
228 | TARGET: ${{ matrix.job.target }}
229 | shell: bash
230 | run: |
231 | if [ "$PLATFORM_NAME" == "linux" ]; then
232 |
233 | cp ./target/${TARGET}/release/timestampvm timestampvm.${TARGET}
234 | echo "file_name_timestampvm_rs=timestampvm.${TARGET}" >> $GITHUB_OUTPUT
235 | tar -czvf timestampvm_${TARGET}.tar.gz -C ./target/${TARGET}/release timestampvm
236 | echo "file_name_timestampvm_rs_tar_gz=timestampvm_${TARGET}.tar.gz" >> $GITHUB_OUTPUT
237 |
238 | elif [ "$PLATFORM_NAME" == "darwin" ]; then
239 |
240 | cp ./target/${TARGET}/release/timestampvm timestampvm.${TARGET}
241 | echo "file_name_timestampvm_rs=timestampvm.${TARGET}" >> $GITHUB_OUTPUT
242 | gtar -czvf timestampvm_${TARGET}.tar.gz -C ./target/${TARGET}/release timestampvm
243 | echo "file_name_timestampvm_rs_tar_gz=timestampvm_${TARGET}.tar.gz" >> $GITHUB_OUTPUT
244 |
245 | else
246 |
247 | echo "skipping $PLATFORM_NAME"
248 |
249 | fi
250 |
251 | # release tip from latest commits
252 | # https://github.com/softprops/action-gh-release
253 | # https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
254 | - name: Release latest
255 | uses: softprops/action-gh-release@v1
256 | if: ${{ github.ref == 'refs/heads/main' }}
257 | with:
258 | name: Latest release
259 | tag_name: latest
260 | prerelease: true
261 | body: Latest builds from the last commit.
262 | files: |
263 | ${{ steps.release_artifacts.outputs.file_name_timestampvm_rs }}
264 | ${{ steps.release_artifacts.outputs.file_name_timestampvm_rs_tar_gz }}
265 |
266 | # release only for tags
267 | # https://github.com/softprops/action-gh-release
268 | # https://docs.github.com/en/actions/learn-github-actions/contexts#github-context
269 | - name: Release tag
270 | uses: softprops/action-gh-release@v1
271 | if: startsWith(github.ref, 'refs/tags/')
272 | with:
273 | name: ${{ github.ref_name }}
274 | tag_name: ${{ github.ref_name }}
275 | draft: true
276 | prerelease: true
277 | body: Release builds for ${{ github.ref_name }}.
278 | files: |
279 | ${{ steps.release_artifacts.outputs.file_name_timestampvm_rs }}
280 | ${{ steps.release_artifacts.outputs.file_name_timestampvm_rs_tar_gz }}
281 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Generated by Cargo
2 | # will have compiled files and executables
3 | /target/
4 |
5 | # These are backup files generated by rustfmt
6 | **/*.rs.bk
7 |
--------------------------------------------------------------------------------
/.rustfmt.nightly.toml:
--------------------------------------------------------------------------------
1 | # ref. https://github.com/rust-lang/rustfmt/blob/master/Configurations.md
2 | binop_separator = "Back"
3 | blank_lines_lower_bound = 0
4 | blank_lines_upper_bound = 1
5 | brace_style = "SameLineWhere"
6 | color = "Auto"
7 | combine_control_expr = true
8 | comment_width = 80
9 | condense_wildcard_suffixes = true
10 | control_brace_style = "AlwaysSameLine"
11 | edition = "2021"
12 | empty_item_single_line = true
13 | enum_discrim_align_threshold = 20
14 | error_on_line_overflow = true
15 | error_on_unformatted = false
16 | fn_args_layout = "Tall"
17 | fn_call_width = 60
18 | fn_single_line = false
19 | force_explicit_abi = true
20 | force_multiline_blocks = false
21 | format_code_in_doc_comments = true
22 | format_generated_files = true
23 | format_macro_bodies = true
24 | format_macro_matchers = true
25 | format_strings = true
26 | hard_tabs = false
27 | imports_granularity = "Crate"
28 | imports_indent = "Block"
29 | imports_layout = "Mixed"
30 | indent_style = "Block"
31 | max_width = 100
32 | normalize_doc_attributes = true
33 | reorder_imports = true
34 | trailing_comma = "Vertical"
35 | trailing_semicolon = true
36 | unstable_features = true
37 | use_field_init_shorthand = true
38 | use_small_heuristics = "Off"
39 | use_try_shorthand = true
40 | where_single_line = false
41 | wrap_comments = true
--------------------------------------------------------------------------------
/CODEOWNERS:
--------------------------------------------------------------------------------
1 | @hexfusion @richardpringle
2 |
--------------------------------------------------------------------------------
/Cargo.toml:
--------------------------------------------------------------------------------
1 | [workspace]
2 | members = [
3 | "tests/e2e",
4 | "timestampvm",
5 | ]
6 |
7 | resolver = "2"
8 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | BSD 3-Clause License
2 |
3 | Copyright (c) 2022, Gyuho Lee
4 | All rights reserved.
5 |
6 | Redistribution and use in source and binary forms, with or without
7 | modification, are permitted provided that the following conditions are met:
8 |
9 | 1. Redistributions of source code must retain the above copyright notice, this
10 | list of conditions and the following disclaimer.
11 |
12 | 2. Redistributions in binary form must reproduce the above copyright notice,
13 | this list of conditions and the following disclaimer in the documentation
14 | and/or other materials provided with the distribution.
15 |
16 | 3. Neither the name of the copyright holder nor the names of its
17 | contributors may be used to endorse or promote products derived from
18 | this software without specific prior written permission.
19 |
20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
30 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 |
2 | [
](https://crates.io/crates/timestampvm)
3 | [
](https://docs.rs/timestampvm)
4 | 
5 |
6 | # `timestampvm-rs`
7 |
8 | `timestampvm-rs` is a virtual machine that can build blocks from a user-provided arbitrary data. It is a minimal implementation of an Avalanche custom virtual machine (VM) in Rust, using the Avalanche [Rust SDK](https://github.com/ava-labs/avalanche-types-rs).
9 |
10 | Currently, Avalanche custom VM requires the following:
11 |
12 | 1. Compiled to a binary that `avalanchego` can launch as a sub-process.
13 | 2. Plugin binary path in hash of 32 bytes.
14 | 3. Implements [`snowman.block.ChainVM`](https://pkg.go.dev/github.com/ava-labs/avalanchego/snow/engine/snowman/block#ChainVM) interface that can be be registered via [`rpcchainvm.Serve`](https://pkg.go.dev/github.com/ava-labs/avalanchego/vms/rpcchainvm#Serve).
15 | 4. Implements VM-specific services that can be served via URL path of the blockchain ID.
16 | 5. (Optionally) Implements VM-specific static handlers that can be served via URL path of the VM ID.
17 |
18 | For example, the timestamp VM can be run as follows:
19 |
20 | ```rust
21 | use avalanche_types::subnet;
22 | use timestampvm::vm;
23 | use tokio::sync::broadcast::{self, Receiver, Sender};
24 |
25 | #[tokio::main]
26 | async fn main() -> std::io::Result<()> {
27 | let (stop_ch_tx, stop_ch_rx): (Sender<()>, Receiver<()>) = broadcast::channel(1);
28 | let vm_server = subnet::rpc::vm::server::Server::new(vm::Vm::new(), stop_ch_tx);
29 | subnet::rpc::plugin::serve(vm_server, stop_ch_rx).await
30 | }
31 | ```
32 |
33 | See [`bin/timestampvm`](timestampvm/src/bin/timestampvm/main.rs) for plugin implementation and [`tests/e2e`](tests/e2e/src/tests/mod.rs) for full end-to-end tests.
34 |
35 | ## Dependencies
36 |
37 | - Latest version of stable Rust.
38 | - To build and test timestampvm you need [protoc](https://grpc.io/docs/protoc-installation/#install-pre-compiled-binaries-any-os) version >= 3.15.0.
39 |
40 | ## AvalancheGo Compatibility
41 | | Version(s) | AvalancheGo Version(s) |
42 | | --- | --- |
43 | | v0.0.6 | v1.9.2,v1.9.3 |
44 | | v0.0.7 | v1.9.4 |
45 | | v0.0.8, v0.0.9 | v1.9.7 |
46 | | v0.0.10 | v1.9.8, v1.9.9 |
47 | | v0.0.11,12 | v1.9.10 - v1.9.16 |
48 | | v0.0.13 | v1.10.0 |
49 | | v0.0.14..17 | v1.10.1 |
50 | | v0.0.18 | v1.10.9+ |
51 |
52 | ## Example
53 |
54 | ```bash
55 | # build the timestampvm plugin, run e2e tests, and keep the network running
56 | ./scripts/build.release.sh \
57 | && VM_PLUGIN_PATH=$(pwd)/target/release/timestampvm \
58 | ./scripts/tests.e2e.sh
59 |
60 | # or, specify the custom avalanchego binary
61 | ./scripts/build.release.sh \
62 | && VM_PLUGIN_PATH=$(pwd)/target/release/timestampvm \
63 | ./scripts/tests.e2e.sh ~/go/src/github.com/ava-labs/avalanchego/build/avalanchego
64 |
65 | # (optional) set NETWORK_RUNNER_ENABLE_SHUTDOWN=1 in "tests.e2e.sh"
66 | # to shut down the network afterwards
67 | ```
68 |
69 | To test `timestampvm` APIs, try the following commands:
70 |
71 | ```bash
72 | # "tGas3T58KzdjcJ2iKSyiYsWiqYctRXaPTqBCA11BqEkNg8kPc" is the Vm Id
73 | # e.g., timestampvm vm-id timestampvm
74 | curl -X POST --data '{
75 | "jsonrpc": "2.0",
76 | "id" : 1,
77 | "method" : "timestampvm.ping",
78 | "params" : []
79 | }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/vm/tGas3T58KzdjcJ2iKSyiYsWiqYctRXaPTqBCA11BqEkNg8kPc/static
80 |
81 | # {"jsonrpc":"2.0","result":{"success":true},"id":1}
82 | ```
83 |
84 | ```bash
85 | # "2wb1UXxAstB8ywwv4rU2rFCjLgXnhT44hbLPbwpQoGvFb2wRR7" is the blockchain Id
86 | curl -X POST --data '{
87 | "jsonrpc": "2.0",
88 | "id" : 1,
89 | "method" : "timestampvm.ping",
90 | "params" : []
91 | }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/2wb1UXxAstB8ywwv4rU2rFCjLgXnhT44hbLPbwpQoGvFb2wRR7/rpc
92 |
93 | # {"jsonrpc":"2.0","result":{"success":true},"id":1}
94 | ```
95 |
96 | ```bash
97 | # to get genesis block
98 | # "2wb1UXxAstB8ywwv4rU2rFCjLgXnhT44hbLPbwpQoGvFb2wRR7" is the blockchain Id
99 | curl -X POST --data '{
100 | "jsonrpc": "2.0",
101 | "id" : 1,
102 | "method" : "timestampvm.lastAccepted",
103 | "params" : []
104 | }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/2wb1UXxAstB8ywwv4rU2rFCjLgXnhT44hbLPbwpQoGvFb2wRR7/rpc
105 |
106 | # {"jsonrpc":"2.0","result":{"id":"SDfFUzkdzWZbJ6YMysPPNEF5dWLp9q35mEMaLa8Ha2w9aMKoC"},"id":1}
107 |
108 | # "2wb1UXxAstB8ywwv4rU2rFCjLgXnhT44hbLPbwpQoGvFb2wRR7" is the blockchain Id
109 | curl -X POST --data '{
110 | "jsonrpc": "2.0",
111 | "id" : 1,
112 | "method" : "timestampvm.getBlock",
113 | "params" : [{"id":"SDfFUzkdzWZbJ6YMysPPNEF5dWLp9q35mEMaLa8Ha2w9aMKoC"}]
114 | }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/2wb1UXxAstB8ywwv4rU2rFCjLgXnhT44hbLPbwpQoGvFb2wRR7/rpc
115 |
116 | # {"jsonrpc":"2.0","result":{"block":{"data":"0x32596655705939524358","height":0,"parent_id":"11111111111111111111111111111111LpoYY","timestamp":0}},"id":1}
117 | ```
118 |
119 | ```bash
120 | # to propose data
121 | echo 1 | base64 | tr -d \\n
122 | # MQo=
123 |
124 | curl -X POST --data '{
125 | "jsonrpc": "2.0",
126 | "id" : 1,
127 | "method" : "timestampvm.proposeBlock",
128 | "params" : [{"data":"MQo="}]
129 | }' -H 'content-type:application/json;' 127.0.0.1:9650/ext/bc/2wb1UXxAstB8ywwv4rU2rFCjLgXnhT44hbLPbwpQoGvFb2wRR7/rpc
130 |
131 | # {"jsonrpc":"2.0","result":{"success":true},"id":1}
132 | ```
133 |
--------------------------------------------------------------------------------
/scripts/build.release.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -xue
3 |
4 | if ! [[ "$0" =~ scripts/build.release.sh ]]; then
5 | echo "must be run from repository root"
6 | exit 255
7 | fi
8 |
9 | PROTOC_VERSION=$(protoc --version | cut -f2 -d' ')
10 | if [[ "${PROTOC_VERSION}" == "" ]] || [[ "${PROTOC_VERSION}" < 3.15.0 ]]; then
11 | echo "protoc must be installed and the version must be greater than 3.15.0"
12 | exit 255
13 | fi
14 |
15 | # "--bin" can be specified multiple times for each directory in "bin/*" or workspaces
16 | cargo build \
17 | --release \
18 | --bin timestampvm
19 |
20 | ./target/release/timestampvm --help
21 |
22 | ./target/release/timestampvm genesis "hello world"
23 | ./target/release/timestampvm vm-id timestampvm
24 |
--------------------------------------------------------------------------------
/scripts/tests.e2e.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -e
3 |
4 | # build timestampvm binary
5 | # ./scripts/build.release.sh
6 | #
7 | # download from github, keep network running
8 | # VM_PLUGIN_PATH=$(pwd)/target/release/timestampvm ./scripts/tests.e2e.sh
9 | #
10 | # download from github, shut down network
11 | # NETWORK_RUNNER_ENABLE_SHUTDOWN=1 VM_PLUGIN_PATH=$(pwd)/target/release/timestampvm ./scripts/tests.e2e.sh
12 | #
13 | # use custom avalanchego version
14 | # VM_PLUGIN_PATH=$(pwd)/target/release/timestampvm ./scripts/tests.e2e.sh 1.11.1
15 | #
16 | if ! [[ "$0" =~ scripts/tests.e2e.sh ]]; then
17 | echo "must be run from repository root"
18 | exit 255
19 | fi
20 |
21 | AVALANCHEGO_VERSION=$1
22 | if [[ -z "${AVALANCHEGO_VERSION}" ]]; then
23 | echo "Missing avalanchego version argument!"
24 | echo "Usage: ${0} [AVALANCHEGO_VERSION]" >> /dev/stderr
25 | exit 255
26 | fi
27 |
28 | echo "Running with:"
29 | echo AVALANCHEGO_VERSION: ${AVALANCHEGO_VERSION}
30 |
31 | echo VM_PLUGIN_PATH: ${VM_PLUGIN_PATH}
32 |
33 |
34 | ############################
35 | # download avalanchego
36 | # https://github.com/ava-labs/avalanchego/releases
37 | GOARCH=$(go env GOARCH)
38 | GOOS=$(go env GOOS)
39 | DOWNLOAD_URL=https://github.com/ava-labs/avalanchego/releases/download/v${AVALANCHEGO_VERSION}/avalanchego-linux-${GOARCH}-v${AVALANCHEGO_VERSION}.tar.gz
40 | DOWNLOAD_PATH=/tmp/avalanchego.tar.gz
41 | if [[ ${GOOS} == "darwin" ]]; then
42 | DOWNLOAD_URL=https://github.com/ava-labs/avalanchego/releases/download/v${AVALANCHEGO_VERSION}/avalanchego-macos-v${AVALANCHEGO_VERSION}.zip
43 | DOWNLOAD_PATH=/tmp/avalanchego.zip
44 | fi
45 |
46 | rm -rf /tmp/avalanchego-v${AVALANCHEGO_VERSION}
47 | rm -f ${DOWNLOAD_PATH}
48 |
49 | echo "downloading avalanchego ${AVALANCHEGO_VERSION} at ${DOWNLOAD_URL}"
50 | curl -L ${DOWNLOAD_URL} -o ${DOWNLOAD_PATH}
51 |
52 | echo "extracting downloaded avalanchego"
53 | if [[ ${GOOS} == "linux" ]]; then
54 | tar xzvf ${DOWNLOAD_PATH} -C /tmp
55 | elif [[ ${GOOS} == "darwin" ]]; then
56 | unzip ${DOWNLOAD_PATH} -d /tmp/avalanchego-build
57 | mv /tmp/avalanchego-build/build /tmp/avalanchego-v${AVALANCHEGO_VERSION}
58 | fi
59 | find /tmp/avalanchego-v${AVALANCHEGO_VERSION}
60 |
61 | AVALANCHEGO_PATH=/tmp/avalanchego-v${AVALANCHEGO_VERSION}/avalanchego
62 | AVALANCHEGO_PLUGIN_DIR=/tmp/avalanchego-v${AVALANCHEGO_VERSION}/plugins
63 |
64 | #################################
65 | # download avalanche-network-runner
66 | # https://github.com/ava-labs/avalanche-network-runner
67 | # TODO: use "go install -v github.com/ava-labs/avalanche-network-runner/cmd/avalanche-network-runner@v${NETWORK_RUNNER_VERSION}"
68 | GOOS=$(go env GOOS)
69 | NETWORK_RUNNER_VERSION=1.7.1
70 | DOWNLOAD_PATH=/tmp/avalanche-network-runner.tar.gz
71 | DOWNLOAD_URL=https://github.com/ava-labs/avalanche-network-runner/releases/download/v${NETWORK_RUNNER_VERSION}/avalanche-network-runner_${NETWORK_RUNNER_VERSION}_linux_amd64.tar.gz
72 | if [[ ${GOOS} == "darwin" ]]; then
73 | DOWNLOAD_URL=https://github.com/ava-labs/avalanche-network-runner/releases/download/v${NETWORK_RUNNER_VERSION}/avalanche-network-runner_${NETWORK_RUNNER_VERSION}_darwin_amd64.tar.gz
74 | fi
75 | echo ${DOWNLOAD_URL}
76 |
77 | rm -f ${DOWNLOAD_PATH}
78 | rm -f /tmp/avalanche-network-runner
79 |
80 | echo "downloading avalanche-network-runner ${NETWORK_RUNNER_VERSION} at ${DOWNLOAD_URL}"
81 | curl -L ${DOWNLOAD_URL} -o ${DOWNLOAD_PATH}
82 |
83 | echo "extracting downloaded avalanche-network-runner"
84 | tar xzvf ${DOWNLOAD_PATH} -C /tmp
85 | /tmp/avalanche-network-runner -h
86 |
87 | #################################
88 | # run "avalanche-network-runner" server
89 | echo "launch avalanche-network-runner in the background"
90 | /tmp/avalanche-network-runner \
91 | server \
92 | --log-level debug \
93 | --port=":12342" \
94 | --disable-grpc-gateway &
95 | NETWORK_RUNNER_PID=${!}
96 | sleep 5
97 |
98 | #################################
99 | echo "running e2e tests"
100 | NETWORK_RUNNER_GRPC_ENDPOINT=http://127.0.0.1:12342 \
101 | AVALANCHEGO_PATH=${AVALANCHEGO_PATH} \
102 | VM_PLUGIN_PATH=${VM_PLUGIN_PATH} \
103 | RUST_LOG=debug \
104 | cargo test --all-features --package e2e -- --show-output --nocapture
105 |
106 | #################################
107 | echo ""
108 | echo ""
109 | if [ -z "$NETWORK_RUNNER_ENABLE_SHUTDOWN" ]
110 | then
111 | echo "SKIPPED SHUTDOWN..."
112 | echo ""
113 | echo "RUN FOLLOWING TO CLEAN UP:"
114 | echo "pkill -P ${NETWORK_RUNNER_PID} || true"
115 | echo "kill -2 ${NETWORK_RUNNER_PID} || true"
116 | echo ""
117 | else
118 | echo "SHUTTING DOWN..."
119 | echo ""
120 | # "e2e.test" already terminates the cluster for "test" mode
121 | # just in case tests are aborted, manually terminate them again
122 | echo "network-runner RPC server was running on NETWORK_RUNNER_PID ${NETWORK_RUNNER_PID} as test mode; terminating the process..."
123 | pkill -P ${NETWORK_RUNNER_PID} || true
124 | kill -2 ${NETWORK_RUNNER_PID} || true
125 | fi
126 |
127 | echo "TEST SUCCESS"
128 |
--------------------------------------------------------------------------------
/scripts/tests.lint.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -xue
3 |
4 | if ! [[ "$0" =~ scripts/tests.lint.sh ]]; then
5 | echo "must be run from repository root"
6 | exit 255
7 | fi
8 |
9 | # https://rust-lang.github.io/rustup/installation/index.html
10 | # rustup toolchain install nightly --allow-downgrade --profile minimal --component clippy
11 | #
12 | # https://github.com/rust-lang/rustfmt
13 | # rustup component add rustfmt
14 | # rustup component add rustfmt --toolchain nightly
15 | # rustup component add clippy
16 | # rustup component add clippy --toolchain nightly
17 |
18 | rustup default stable
19 | cargo fmt --all --verbose -- --check
20 |
21 | # TODO: enable nightly fmt
22 | rustup default nightly
23 | cargo +nightly fmt --all -- --config-path .rustfmt.nightly.toml --verbose --check || true
24 |
25 | # TODO: enable this
26 | cargo +nightly clippy --all --all-features -- -D warnings || true
27 |
28 | rustup default stable
29 |
30 | echo "ALL SUCCESS!"
31 |
--------------------------------------------------------------------------------
/scripts/tests.unit.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -xue
3 |
4 | if ! [[ "$0" =~ scripts/tests.unit.sh ]]; then
5 | echo "must be run from repository root"
6 | exit 255
7 | fi
8 |
9 | RUST_LOG=debug cargo test --all --all-features \
10 | --exclude e2e \
11 | -- --show-output
12 |
13 | echo "ALL SUCCESS!"
14 |
--------------------------------------------------------------------------------
/scripts/tests.unused.sh:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env bash
2 | set -xue
3 |
4 | if ! [[ "$0" =~ scripts/tests.unused.sh ]]; then
5 | echo "must be run from repository root"
6 | exit 255
7 | fi
8 |
9 | # cargo install cargo-udeps --locked
10 | # https://github.com/est31/cargo-udeps
11 | cargo install cargo-udeps --locked
12 | cargo +nightly udeps
13 |
14 | echo "ALL SUCCESS!"
15 |
--------------------------------------------------------------------------------
/tests/e2e/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "e2e"
3 | version = "0.0.0"
4 | edition = "2021"
5 | rust-version = "1.70"
6 | publish = false
7 | description = "e2e tests for timestampvm"
8 | license = "BSD-3-Clause"
9 | homepage = "https://avax.network"
10 |
11 | [dependencies]
12 |
13 | [dev-dependencies]
14 | avalanche-installer = "0.0.77"
15 | avalanche-network-runner-sdk = "0.3.4" # https://crates.io/crates/avalanche-network-runner-sdk
16 | avalanche-types = { version = "0.1.5", features = ["jsonrpc_client", "subnet"] } # https://crates.io/crates/avalanche-types
17 | env_logger = "0.11.3"
18 | log = "0.4.21"
19 | random-manager = "0.0.5"
20 | serde_json = "1.0.116" # https://github.com/serde-rs/json/releases
21 | tempfile = "3.10.1"
22 | timestampvm = { path = "../../timestampvm" }
23 | tokio = { version = "1.37.0", features = ["full"] } # https://github.com/tokio-rs/tokio/releases
24 |
--------------------------------------------------------------------------------
/tests/e2e/src/lib.rs:
--------------------------------------------------------------------------------
1 | #[cfg(test)]
2 | mod tests;
3 |
4 | #[must_use]
5 | pub fn get_network_runner_grpc_endpoint() -> (String, bool) {
6 | match std::env::var("NETWORK_RUNNER_GRPC_ENDPOINT") {
7 | Ok(s) => (s, true),
8 | _ => (String::new(), false),
9 | }
10 | }
11 |
12 | #[must_use]
13 | pub fn get_network_runner_enable_shutdown() -> bool {
14 | matches!(std::env::var("NETWORK_RUNNER_ENABLE_SHUTDOWN"), Ok(_))
15 | }
16 |
17 | #[must_use]
18 | pub fn get_avalanchego_path() -> (String, bool) {
19 | match std::env::var("AVALANCHEGO_PATH") {
20 | Ok(s) => (s, true),
21 | _ => (String::new(), false),
22 | }
23 | }
24 |
25 | #[must_use]
26 | pub fn get_vm_plugin_path() -> (String, bool) {
27 | match std::env::var("VM_PLUGIN_PATH") {
28 | Ok(s) => (s, true),
29 | _ => (String::new(), false),
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/tests/e2e/src/tests/mod.rs:
--------------------------------------------------------------------------------
1 | use std::{
2 | fs,
3 | path::Path,
4 | str::FromStr,
5 | thread,
6 | time::{Duration, Instant},
7 | };
8 |
9 | use avalanche_network_runner_sdk::{BlockchainSpec, Client, GlobalConfig, StartRequest};
10 | use avalanche_types::{ids, jsonrpc::client::info as avalanche_sdk_info, subnet};
11 |
12 | const AVALANCHEGO_VERSION: &str = "v1.10.9";
13 |
14 | #[tokio::test]
15 | async fn e2e() {
16 | let _ = env_logger::builder()
17 | .filter_level(log::LevelFilter::Info)
18 | .is_test(true)
19 | .try_init();
20 |
21 | let (ep, is_set) = crate::get_network_runner_grpc_endpoint();
22 | assert!(is_set);
23 |
24 | let cli = Client::new(&ep).await;
25 |
26 | log::info!("ping...");
27 | let resp = cli.ping().await.expect("failed ping");
28 | log::info!("network-runner is running (ping response {:?})", resp);
29 |
30 | let (vm_plugin_path, exists) = crate::get_vm_plugin_path();
31 | log::info!("Vm Plugin path: {vm_plugin_path}");
32 | assert!(exists);
33 | assert!(Path::new(&vm_plugin_path).exists());
34 |
35 | let vm_id = Path::new(&vm_plugin_path)
36 | .file_stem()
37 | .unwrap()
38 | .to_str()
39 | .unwrap()
40 | .to_string();
41 | let vm_id = subnet::vm_name_to_id(&vm_id).unwrap();
42 |
43 | let (mut avalanchego_exec_path, _) = crate::get_avalanchego_path();
44 | let plugins_dir = if !avalanchego_exec_path.is_empty() {
45 | let parent_dir = Path::new(&avalanchego_exec_path)
46 | .parent()
47 | .expect("unexpected None parent");
48 | parent_dir
49 | .join("plugins")
50 | .as_os_str()
51 | .to_str()
52 | .unwrap()
53 | .to_string()
54 | } else {
55 | let exec_path = avalanche_installer::avalanchego::github::download(
56 | None,
57 | None,
58 | Some(AVALANCHEGO_VERSION.to_string()),
59 | )
60 | .await
61 | .unwrap();
62 | avalanchego_exec_path = exec_path;
63 | avalanche_installer::avalanchego::get_plugin_dir(&avalanchego_exec_path)
64 | };
65 |
66 | log::info!(
67 | "copying vm plugin {} to {}/{}",
68 | vm_plugin_path,
69 | plugins_dir,
70 | vm_id
71 | );
72 |
73 | fs::create_dir(&plugins_dir).unwrap();
74 | fs::copy(
75 | &vm_plugin_path,
76 | Path::new(&plugins_dir).join(vm_id.to_string()),
77 | )
78 | .unwrap();
79 |
80 | // write some random genesis file
81 | let genesis = timestampvm::genesis::Genesis {
82 | data: random_manager::secure_string(10),
83 | };
84 | let genesis_file_path = random_manager::tmp_path(10, None).unwrap();
85 | genesis.sync(&genesis_file_path).unwrap();
86 |
87 | log::info!(
88 | "starting {} with avalanchego {}, genesis file path {}",
89 | vm_id,
90 | &avalanchego_exec_path,
91 | genesis_file_path,
92 | );
93 | let resp = cli
94 | .start(StartRequest {
95 | exec_path: avalanchego_exec_path,
96 | num_nodes: Some(5),
97 | plugin_dir: plugins_dir,
98 | global_node_config: Some(
99 | serde_json::to_string(&GlobalConfig {
100 | log_level: String::from("info"),
101 | })
102 | .unwrap(),
103 | ),
104 | blockchain_specs: vec![BlockchainSpec {
105 | vm_name: String::from("timestampvm"),
106 | genesis: genesis_file_path.to_string(),
107 | ..Default::default()
108 | }],
109 | ..Default::default()
110 | })
111 | .await
112 | .expect("failed start");
113 | log::info!(
114 | "started avalanchego cluster with network-runner: {:?}",
115 | resp
116 | );
117 |
118 | // enough time for network-runner to get ready
119 | thread::sleep(Duration::from_secs(20));
120 |
121 | log::info!("checking cluster healthiness...");
122 | let mut ready = false;
123 |
124 | let timeout = Duration::from_secs(300);
125 | let interval = Duration::from_secs(15);
126 | let start = Instant::now();
127 | let mut cnt: u128 = 0;
128 | loop {
129 | let elapsed = start.elapsed();
130 | if elapsed.gt(&timeout) {
131 | break;
132 | }
133 |
134 | let itv = {
135 | if cnt == 0 {
136 | // first poll with no wait
137 | Duration::from_secs(1)
138 | } else {
139 | interval
140 | }
141 | };
142 | thread::sleep(itv);
143 |
144 | ready = {
145 | match cli.health().await {
146 | Ok(_) => {
147 | log::info!("healthy now!");
148 | true
149 | }
150 | Err(e) => {
151 | log::warn!("not healthy yet {}", e);
152 | false
153 | }
154 | }
155 | };
156 | if ready {
157 | break;
158 | }
159 |
160 | cnt += 1;
161 | }
162 | assert!(ready);
163 |
164 | log::info!("checking status...");
165 | let mut status = cli.status().await.expect("failed status");
166 | loop {
167 | let elapsed = start.elapsed();
168 | if elapsed.gt(&timeout) {
169 | break;
170 | }
171 |
172 | if let Some(ci) = &status.cluster_info {
173 | if !ci.custom_chains.is_empty() {
174 | break;
175 | }
176 | }
177 |
178 | log::info!("retrying checking status...");
179 | thread::sleep(interval);
180 | status = cli.status().await.expect("failed status");
181 | }
182 |
183 | assert!(status.cluster_info.is_some());
184 | let cluster_info = status.cluster_info.unwrap();
185 | let mut rpc_eps: Vec = Vec::new();
186 | for (node_name, iv) in cluster_info.node_infos.into_iter() {
187 | log::info!("{}: {}", node_name, iv.uri);
188 | rpc_eps.push(iv.uri.clone());
189 | }
190 | let mut blockchain_id = ids::Id::empty();
191 | for (k, v) in cluster_info.custom_chains.iter() {
192 | log::info!("custom chain info: {}={:?}", k, v);
193 | if v.chain_name == "timestampvm" {
194 | blockchain_id = ids::Id::from_str(&v.chain_id).unwrap();
195 | break;
196 | }
197 | }
198 | log::info!("avalanchego RPC endpoints: {:?}", rpc_eps);
199 |
200 | let resp = avalanche_sdk_info::get_network_id(&rpc_eps[0])
201 | .await
202 | .unwrap();
203 | let network_id = resp.result.unwrap().network_id;
204 | log::info!("network Id: {}", network_id);
205 |
206 | // log::info!("ping static handlers");
207 | // let static_url_path = format!("ext/vm/{vm_id}/static");
208 | // for ep in rpc_eps.iter() {
209 | // let resp = timestampvm::client::ping(ep.as_str(), &static_url_path)
210 | // .await
211 | // .unwrap();
212 | // log::info!("ping response from {}: {:?}", ep, resp);
213 | // assert!(resp.result.unwrap().success);
214 |
215 | // thread::sleep(Duration::from_millis(300));
216 | // }
217 |
218 | log::info!("ping chain handlers");
219 | let chain_url_path = format!("ext/bc/{blockchain_id}/rpc");
220 | for ep in rpc_eps.iter() {
221 | let resp = timestampvm::client::ping(ep.as_str(), &chain_url_path)
222 | .await
223 | .unwrap();
224 | log::info!("ping response from {}: {:?}", ep, resp);
225 | assert!(resp.result.unwrap().success);
226 |
227 | thread::sleep(Duration::from_millis(300));
228 | }
229 |
230 | let ep = rpc_eps[0].clone();
231 |
232 | log::info!("get last_accepted from chain handlers");
233 | let resp = timestampvm::client::last_accepted(&ep, &chain_url_path)
234 | .await
235 | .unwrap();
236 | log::info!("last_accepted response from {}: {:?}", ep, resp);
237 |
238 | let blk_id = resp.result.unwrap().id;
239 |
240 | log::info!("getting block {blk_id}");
241 | let resp = timestampvm::client::get_block(&ep, &chain_url_path, &blk_id)
242 | .await
243 | .unwrap();
244 | log::info!("get_block response from {}: {:?}", ep, resp);
245 | let height0 = resp.result.unwrap().block.height();
246 | assert_eq!(height0, 0);
247 |
248 | log::info!("propose block");
249 | let resp = timestampvm::client::propose_block(&ep, &chain_url_path, vec![0, 1, 2])
250 | .await
251 | .unwrap();
252 | log::info!("propose_block response from {}: {:?}", ep, resp);
253 |
254 | // enough time for block builds
255 | thread::sleep(Duration::from_secs(5));
256 |
257 | log::info!("get last_accepted from chain handlers");
258 | let resp = timestampvm::client::last_accepted(&ep, &chain_url_path)
259 | .await
260 | .unwrap();
261 | log::info!("last_accepted response from {}: {:?}", ep, resp);
262 |
263 | let blk_id = resp.result.unwrap().id;
264 |
265 | log::info!("getting block {blk_id}");
266 | let resp = timestampvm::client::get_block(&ep, &chain_url_path, &blk_id)
267 | .await
268 | .unwrap();
269 | log::info!("get_block response from {}: {:?}", ep, resp);
270 | let height1 = resp.result.unwrap().block.height();
271 | assert_eq!(height0 + 1, height1);
272 |
273 | // expects an error of
274 | // "error":{"code":-32603,"message":"data 1048586-byte exceeds the limit 1048576-byte"}
275 | log::info!("propose block beyond its limit");
276 | let resp = timestampvm::client::propose_block(
277 | &ep,
278 | &chain_url_path,
279 | vec![1; timestampvm::vm::PROPOSE_LIMIT_BYTES + 10],
280 | )
281 | .await
282 | .unwrap();
283 | assert!(resp.result.is_none());
284 | assert!(resp.error.is_some());
285 | log::info!("propose block response: {:?}", resp);
286 |
287 | if crate::get_network_runner_enable_shutdown() {
288 | log::info!("shutdown is enabled... stopping...");
289 | let _resp = cli.stop().await.expect("failed stop");
290 | log::info!("successfully stopped network");
291 | } else {
292 | log::info!("skipped network shutdown...");
293 | }
294 | }
295 |
--------------------------------------------------------------------------------
/timestampvm/Cargo.toml:
--------------------------------------------------------------------------------
1 | [package]
2 | name = "timestampvm"
3 | version = "0.0.19" # https://crates.io/crates/timestampvm
4 | edition = "2021"
5 | rust-version = "1.70"
6 | publish = true
7 | description = "Timestamp VM in Rust"
8 | documentation = "https://docs.rs/timestampvm"
9 | license = "BSD-3-Clause"
10 | repository = "https://github.com/ava-labs/timestampvm-rs"
11 | readme = "../README.md"
12 |
13 | [dependencies]
14 | avalanche-types = { version = "0.1.5", features = ["subnet", "codec_base64"] } # https://crates.io/crates/avalanche-types
15 | base64 = { version = "0.22.0" }
16 | bytes = "1.6.0"
17 | chrono = "0.4.38"
18 | clap = { version = "4.5.4", features = ["cargo", "derive"] } # https://github.com/clap-rs/clap/releases
19 | derivative = "2.2.0"
20 | env_logger = "0.11.3"
21 | http-manager = { version = "0.0.14" }
22 | jsonrpc-core = "18.0.0"
23 | jsonrpc-core-client = { version = "18.0.0" }
24 | jsonrpc-derive = "18.0.0"
25 | log = "0.4.21"
26 | semver = "1.0.22"
27 | serde = { version = "1.0.197", features = ["derive"] }
28 | serde_json = "1.0.116" # https://github.com/serde-rs/json/releases
29 | serde_with = { version = "3.7.0", features = ["hex"] }
30 | tokio = { version = "1.37.0", features = ["fs", "rt-multi-thread"] }
31 | tonic = { version = "0.11.0", features = ["gzip"] }
32 |
33 | [dev-dependencies]
34 | random-manager = "0.0.5"
35 |
--------------------------------------------------------------------------------
/timestampvm/src/api/chain_handlers.rs:
--------------------------------------------------------------------------------
1 | //! Implements chain/VM specific handlers.
2 | //! To be served via `[HOST]/ext/bc/[CHAIN ID]/rpc`.
3 |
4 | use crate::{block::Block, vm::Vm};
5 | use avalanche_types::{ids, proto::http::Element, subnet::rpc::http::handle::Handle};
6 | use bytes::Bytes;
7 | use jsonrpc_core::{BoxFuture, Error, ErrorCode, IoHandler, Result};
8 | use jsonrpc_derive::rpc;
9 | use serde::{Deserialize, Serialize};
10 | use std::{borrow::Borrow, io, marker::PhantomData, str::FromStr};
11 |
12 | use super::de_request;
13 |
14 | /// Defines RPCs specific to the chain.
15 | #[rpc]
16 | pub trait Rpc {
17 | /// Pings the VM.
18 | #[rpc(name = "ping", alias("timestampvm.ping"))]
19 | fn ping(&self) -> BoxFuture>;
20 |
21 | /// Proposes the arbitrary data.
22 | #[rpc(name = "proposeBlock", alias("timestampvm.proposeBlock"))]
23 | fn propose_block(&self, args: ProposeBlockArgs) -> BoxFuture>;
24 |
25 | /// Fetches the last accepted block.
26 | #[rpc(name = "lastAccepted", alias("timestampvm.lastAccepted"))]
27 | fn last_accepted(&self) -> BoxFuture>;
28 |
29 | /// Fetches the block.
30 | #[rpc(name = "getBlock", alias("timestampvm.getBlock"))]
31 | fn get_block(&self, args: GetBlockArgs) -> BoxFuture>;
32 | }
33 |
34 | #[derive(Deserialize, Serialize, Debug, Clone)]
35 | pub struct ProposeBlockArgs {
36 | #[serde(with = "avalanche_types::codec::serde::base64_bytes")]
37 | pub data: Vec,
38 | }
39 |
40 | #[derive(Deserialize, Serialize, Debug, Clone)]
41 | pub struct ProposeBlockResponse {
42 | /// TODO: returns Id for later query, using hash + time?
43 | pub success: bool,
44 | }
45 |
46 | #[derive(Deserialize, Serialize, Debug, Clone)]
47 | pub struct LastAcceptedResponse {
48 | pub id: ids::Id,
49 | }
50 |
51 | #[derive(Deserialize, Serialize, Debug, Clone)]
52 | pub struct GetBlockArgs {
53 | /// TODO: use "ids::Id"
54 | /// if we use "ids::Id", it fails with:
55 | /// "Invalid params: invalid type: string \"g25v3qDyAaHfR7kBev8tLUHouSgN5BJuZjy1BYS1oiHd2vres\", expected a borrowed string."
56 | pub id: String,
57 | }
58 |
59 | #[derive(Deserialize, Serialize, Debug, Clone)]
60 | pub struct GetBlockResponse {
61 | pub block: Block,
62 | }
63 |
64 | /// Implements API services for the chain-specific handlers.
65 | #[derive(Clone)]
66 | pub struct ChainService {
67 | pub vm: Vm,
68 | }
69 |
70 | impl ChainService {
71 | pub fn new(vm: Vm) -> Self {
72 | Self { vm }
73 | }
74 | }
75 |
76 | impl Rpc for ChainService
77 | where
78 | A: Send + Sync + Clone + 'static,
79 | {
80 | fn ping(&self) -> BoxFuture> {
81 | log::debug!("ping called");
82 | Box::pin(async move { Ok(crate::api::PingResponse { success: true }) })
83 | }
84 |
85 | fn propose_block(&self, args: ProposeBlockArgs) -> BoxFuture> {
86 | log::debug!("propose_block called");
87 | let vm = self.vm.clone();
88 |
89 | Box::pin(async move {
90 | vm.propose_block(args.data)
91 | .await
92 | .map_err(create_jsonrpc_error)?;
93 | Ok(ProposeBlockResponse { success: true })
94 | })
95 | }
96 |
97 | fn last_accepted(&self) -> BoxFuture> {
98 | log::debug!("last accepted method called");
99 | let vm = self.vm.clone();
100 |
101 | Box::pin(async move {
102 | let vm_state = vm.state.read().await;
103 | if let Some(state) = &vm_state.state {
104 | let last_accepted = state
105 | .get_last_accepted_block_id()
106 | .await
107 | .map_err(create_jsonrpc_error)?;
108 |
109 | return Ok(LastAcceptedResponse { id: last_accepted });
110 | }
111 |
112 | Err(Error {
113 | code: ErrorCode::InternalError,
114 | message: String::from("no state manager found"),
115 | data: None,
116 | })
117 | })
118 | }
119 |
120 | fn get_block(&self, args: GetBlockArgs) -> BoxFuture> {
121 | let blk_id = ids::Id::from_str(&args.id).unwrap();
122 | log::info!("get_block called for {}", blk_id);
123 |
124 | let vm = self.vm.clone();
125 |
126 | Box::pin(async move {
127 | let vm_state = vm.state.read().await;
128 | if let Some(state) = &vm_state.state {
129 | let block = state
130 | .get_block(&blk_id)
131 | .await
132 | .map_err(create_jsonrpc_error)?;
133 |
134 | return Ok(GetBlockResponse { block });
135 | }
136 |
137 | Err(Error {
138 | code: ErrorCode::InternalError,
139 | message: String::from("no state manager found"),
140 | data: None,
141 | })
142 | })
143 | }
144 | }
145 |
146 | #[derive(Clone, Debug)]
147 | pub struct ChainHandler {
148 | pub handler: IoHandler,
149 | _marker: PhantomData,
150 | }
151 |
152 | impl ChainHandler {
153 | pub fn new(service: T) -> Self {
154 | let mut handler = jsonrpc_core::IoHandler::new();
155 | handler.extend_with(Rpc::to_delegate(service));
156 | Self {
157 | handler,
158 | _marker: PhantomData,
159 | }
160 | }
161 | }
162 |
163 | #[tonic::async_trait]
164 | impl Handle for ChainHandler
165 | where
166 | T: Rpc + Send + Sync + Clone + 'static,
167 | {
168 | async fn request(
169 | &self,
170 | req: &Bytes,
171 | _headers: &[Element],
172 | ) -> std::io::Result<(Bytes, Vec)> {
173 | match self.handler.handle_request(&de_request(req)?).await {
174 | Some(resp) => Ok((Bytes::from(resp), Vec::new())),
175 | None => Err(io::Error::new(
176 | io::ErrorKind::Other,
177 | "failed to handle request",
178 | )),
179 | }
180 | }
181 | }
182 |
183 | fn create_jsonrpc_error>(e: E) -> Error {
184 | let e = e.borrow();
185 | let mut error = Error::new(ErrorCode::InternalError);
186 | error.message = format!("{e}");
187 | error
188 | }
189 |
--------------------------------------------------------------------------------
/timestampvm/src/api/mod.rs:
--------------------------------------------------------------------------------
1 | //! Implementation of timestampvm APIs, to be registered via
2 | //! `create_static_handlers` and `create_handlers` in the [`vm`](crate::vm) crate.
3 |
4 | pub mod chain_handlers;
5 | pub mod static_handlers;
6 |
7 | use std::io;
8 |
9 | use bytes::Bytes;
10 | use jsonrpc_core::MethodCall;
11 | use serde::{Deserialize, Serialize};
12 |
13 | #[derive(Deserialize, Serialize, Debug, Clone)]
14 | pub struct PingResponse {
15 | pub success: bool,
16 | }
17 |
18 | /// Deserializes JSON-RPC method call.
19 | /// # Errors
20 | /// Fails if the request is not a valid JSON-RPC method call.
21 | pub fn de_request(req: &Bytes) -> io::Result {
22 | let method_call: MethodCall = serde_json::from_slice(req).map_err(|e| {
23 | io::Error::new(
24 | io::ErrorKind::Other,
25 | format!("failed to deserialize request: {e}"),
26 | )
27 | })?;
28 | serde_json::to_string(&method_call).map_err(|e| {
29 | io::Error::new(
30 | io::ErrorKind::Other,
31 | format!("failed to serialize request: {e}"),
32 | )
33 | })
34 | }
35 |
--------------------------------------------------------------------------------
/timestampvm/src/api/static_handlers.rs:
--------------------------------------------------------------------------------
1 | //! Implements static handlers specific to this VM.
2 | //! To be served via `[HOST]/ext/vm/[VM ID]/static`.
3 |
4 | use std::io;
5 |
6 | use avalanche_types::{proto::http::Element, subnet::rpc::http::handle::Handle};
7 | use bytes::Bytes;
8 | use jsonrpc_core::{BoxFuture, IoHandler, Result};
9 | use jsonrpc_derive::rpc;
10 |
11 | use super::de_request;
12 |
13 | /// Defines static handler RPCs for this VM.
14 | #[rpc]
15 | pub trait Rpc {
16 | #[rpc(name = "ping", alias("timestampvm.ping"))]
17 | fn ping(&self) -> BoxFuture>;
18 | }
19 |
20 | /// Implements API services for the static handlers.
21 | #[derive(Default)]
22 | pub struct StaticService {}
23 |
24 | impl StaticService {
25 | #[must_use]
26 | pub fn new() -> Self {
27 | Self {}
28 | }
29 | }
30 |
31 | impl Rpc for StaticService {
32 | fn ping(&self) -> BoxFuture> {
33 | log::debug!("ping called");
34 | Box::pin(async move { Ok(crate::api::PingResponse { success: true }) })
35 | }
36 | }
37 | #[derive(Clone)]
38 | pub struct StaticHandler {
39 | pub handler: IoHandler,
40 | }
41 |
42 | impl StaticHandler {
43 | #[must_use]
44 | pub fn new(service: StaticService) -> Self {
45 | let mut handler = jsonrpc_core::IoHandler::new();
46 | handler.extend_with(Rpc::to_delegate(service));
47 | Self { handler }
48 | }
49 | }
50 |
51 | #[tonic::async_trait]
52 | impl Handle for StaticHandler {
53 | async fn request(
54 | &self,
55 | req: &Bytes,
56 | _headers: &[Element],
57 | ) -> std::io::Result<(Bytes, Vec)> {
58 | match self.handler.handle_request(&de_request(req)?).await {
59 | Some(resp) => Ok((Bytes::from(resp), Vec::new())),
60 | None => Err(io::Error::new(
61 | io::ErrorKind::Other,
62 | "failed to handle request",
63 | )),
64 | }
65 | }
66 | }
67 |
--------------------------------------------------------------------------------
/timestampvm/src/bin/timestampvm/genesis.rs:
--------------------------------------------------------------------------------
1 | use clap::{arg, Command};
2 |
3 | pub const NAME: &str = "genesis";
4 |
5 | #[must_use]
6 | pub fn command() -> Command {
7 | Command::new(NAME)
8 | .about("Write a genesis file")
9 | .arg(arg!( "Genesis message data"))
10 | .arg_required_else_help(true)
11 | }
12 |
--------------------------------------------------------------------------------
/timestampvm/src/bin/timestampvm/main.rs:
--------------------------------------------------------------------------------
1 | pub mod genesis;
2 | pub mod vm_id;
3 |
4 | use std::io;
5 |
6 | use avalanche_types::subnet;
7 | use clap::{crate_version, Command};
8 | use timestampvm::vm;
9 | use tokio::sync::broadcast::{self, Receiver, Sender};
10 |
11 | pub const APP_NAME: &str = "timestampvm";
12 |
13 | #[tokio::main]
14 | async fn main() -> io::Result<()> {
15 | let matches = Command::new(APP_NAME)
16 | .version(crate_version!())
17 | .about("Timestamp Vm")
18 | .subcommands(vec![genesis::command(), vm_id::command()])
19 | .get_matches();
20 |
21 | // ref. https://github.com/env-logger-rs/env_logger/issues/47
22 | env_logger::init_from_env(
23 | env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
24 | );
25 |
26 | match matches.subcommand() {
27 | Some((genesis::NAME, sub_matches)) => {
28 | let data = sub_matches.get_one::("DATA").expect("required");
29 | let genesis = timestampvm::genesis::Genesis { data: data.clone() };
30 | println!("{genesis}");
31 |
32 | Ok(())
33 | }
34 |
35 | Some((vm_id::NAME, sub_matches)) => {
36 | let vm_name = sub_matches.get_one::("VM_NAME").expect("required");
37 | let id = subnet::vm_name_to_id(vm_name)?;
38 | println!("{id}");
39 |
40 | Ok(())
41 | }
42 |
43 | _ => {
44 | log::info!("starting timestampvm");
45 |
46 | let (stop_ch_tx, stop_ch_rx): (Sender<()>, Receiver<()>) = broadcast::channel(1);
47 | let vm_server = subnet::rpc::vm::server::Server::new(vm::Vm::new(), stop_ch_tx);
48 | subnet::rpc::vm::serve(vm_server, stop_ch_rx).await
49 | }
50 | }
51 | }
52 |
--------------------------------------------------------------------------------
/timestampvm/src/bin/timestampvm/vm_id.rs:
--------------------------------------------------------------------------------
1 | use clap::{arg, Command};
2 |
3 | pub const NAME: &str = "vm-id";
4 |
5 | #[must_use]
6 | pub fn command() -> Command {
7 | Command::new(NAME)
8 | .about("Converts a given Vm name string to Vm Id")
9 | .arg(arg!( "A name of the Vm"))
10 | .arg_required_else_help(true)
11 | }
12 |
--------------------------------------------------------------------------------
/timestampvm/src/block/mod.rs:
--------------------------------------------------------------------------------
1 | //! Implementation of [`snowman.Block`](https://pkg.go.dev/github.com/ava-labs/avalanchego/snow/consensus/snowman#Block) interface for timestampvm.
2 |
3 | use std::{
4 | fmt,
5 | io::{self, Error, ErrorKind},
6 | };
7 |
8 | use crate::state;
9 | use avalanche_types::{
10 | choices,
11 | codec::serde::hex_0x_bytes::Hex0xBytes,
12 | ids,
13 | subnet::rpc::consensus::snowman::{self, Decidable},
14 | };
15 | use chrono::{Duration, Utc};
16 | use derivative::{self, Derivative};
17 | use serde::{Deserialize, Serialize};
18 | use serde_with::serde_as;
19 |
20 | /// Represents a block, specific to [`Vm`](crate::vm::Vm).
21 | #[serde_as]
22 | #[derive(Serialize, Deserialize, Clone, Derivative, Default)]
23 | #[derivative(Debug, PartialEq, Eq)]
24 | pub struct Block {
25 | /// The block Id of the parent block.
26 | parent_id: ids::Id,
27 | /// This block's height.
28 | /// The height of the genesis block is 0.
29 | height: u64,
30 | /// Unix second when this block was proposed.
31 | timestamp: u64,
32 | /// Arbitrary data.
33 | #[serde_as(as = "Hex0xBytes")]
34 | data: Vec,
35 |
36 | /// Current block status.
37 | #[serde(skip)]
38 | status: choices::status::Status,
39 | /// This block's encoded bytes.
40 | #[serde(skip)]
41 | bytes: Vec,
42 | /// Generated block Id.
43 | #[serde(skip)]
44 | id: ids::Id,
45 |
46 | /// Reference to the Vm state manager for blocks.
47 | #[derivative(Debug = "ignore", PartialEq = "ignore")]
48 | #[serde(skip)]
49 | state: state::State,
50 | }
51 |
52 | impl Block {
53 | /// Can fail if the block can't be serialized to JSON.
54 | /// # Errors
55 | /// Will fail if the block can't be serialized to JSON.
56 | pub fn try_new(
57 | parent_id: ids::Id,
58 | height: u64,
59 | timestamp: u64,
60 | data: Vec,
61 | status: choices::status::Status,
62 | ) -> io::Result {
63 | let mut b = Self {
64 | parent_id,
65 | height,
66 | timestamp,
67 | data,
68 | ..Default::default()
69 | };
70 |
71 | b.status = status;
72 | b.bytes = b.to_vec()?;
73 | b.id = ids::Id::sha256(&b.bytes);
74 |
75 | Ok(b)
76 | }
77 |
78 | /// # Errors
79 | /// Can fail if the block can't be serialized to JSON.
80 | pub fn to_json_string(&self) -> io::Result {
81 | serde_json::to_string(&self).map_err(|e| {
82 | Error::new(
83 | ErrorKind::Other,
84 | format!("failed to serialize Block to JSON string {e}"),
85 | )
86 | })
87 | }
88 |
89 | /// Encodes the [`Block`](Block) to JSON in bytes.
90 | /// # Errors
91 | /// Errors if the block can't be serialized to JSON.
92 | pub fn to_vec(&self) -> io::Result> {
93 | serde_json::to_vec(&self).map_err(|e| {
94 | Error::new(
95 | ErrorKind::Other,
96 | format!("failed to serialize Block to JSON bytes {e}"),
97 | )
98 | })
99 | }
100 |
101 | /// Loads [`Block`](Block) from JSON bytes.
102 | /// # Errors
103 | /// Will fail if the block can't be deserialized from JSON.
104 | pub fn from_slice(d: impl AsRef<[u8]>) -> io::Result {
105 | let dd = d.as_ref();
106 | let mut b: Self = serde_json::from_slice(dd).map_err(|e| {
107 | Error::new(
108 | ErrorKind::Other,
109 | format!("failed to deserialize Block from JSON {e}"),
110 | )
111 | })?;
112 |
113 | b.bytes = dd.to_vec();
114 | b.id = ids::Id::sha256(&b.bytes);
115 |
116 | Ok(b)
117 | }
118 |
119 | /// Returns the parent block Id.
120 | #[must_use]
121 | pub fn parent_id(&self) -> ids::Id {
122 | self.parent_id
123 | }
124 |
125 | /// Returns the height of this block.
126 | #[must_use]
127 | pub fn height(&self) -> u64 {
128 | self.height
129 | }
130 |
131 | /// Returns the timestamp of this block.
132 | #[must_use]
133 | pub fn timestamp(&self) -> u64 {
134 | self.timestamp
135 | }
136 |
137 | /// Returns the data of this block.
138 | #[must_use]
139 | pub fn data(&self) -> &[u8] {
140 | &self.data
141 | }
142 |
143 | /// Returns the status of this block.
144 | #[must_use]
145 | pub fn status(&self) -> choices::status::Status {
146 | self.status.clone()
147 | }
148 |
149 | /// Updates the status of this block.
150 | pub fn set_status(&mut self, status: choices::status::Status) {
151 | self.status = status;
152 | }
153 |
154 | /// Returns the byte representation of this block.
155 | #[must_use]
156 | pub fn bytes(&self) -> &[u8] {
157 | &self.bytes
158 | }
159 |
160 | /// Returns the ID of this block
161 | #[must_use]
162 | pub fn id(&self) -> ids::Id {
163 | self.id
164 | }
165 |
166 | /// Updates the state of the block.
167 | pub fn set_state(&mut self, state: state::State) {
168 | self.state = state;
169 | }
170 |
171 | /// Verifies [`Block`](Block) properties (e.g., heights),
172 | /// and once verified, records it to the [`State`](crate::state::State).
173 | /// # Errors
174 | /// Can fail if the parent block can't be retrieved.
175 | pub async fn verify(&mut self) -> io::Result<()> {
176 | if self.height == 0 && self.parent_id == ids::Id::empty() {
177 | log::debug!(
178 | "block {} has an empty parent Id since it's a genesis block -- skipping verify",
179 | self.id
180 | );
181 | self.state.add_verified(&self.clone()).await;
182 | return Ok(());
183 | }
184 |
185 | // if already exists in database, it means it's already accepted
186 | // thus no need to verify once more
187 | if self.state.get_block(&self.id).await.is_ok() {
188 | log::debug!("block {} already verified", self.id);
189 | return Ok(());
190 | }
191 |
192 | let prnt_blk = self.state.get_block(&self.parent_id).await?;
193 |
194 | // ensure the height of the block is immediately following its parent
195 | if prnt_blk.height != self.height - 1 {
196 | return Err(Error::new(
197 | ErrorKind::InvalidData,
198 | format!(
199 | "parent block height {} != current block height {} - 1",
200 | prnt_blk.height, self.height
201 | ),
202 | ));
203 | }
204 |
205 | // ensure block timestamp is after its parent
206 | if prnt_blk.timestamp > self.timestamp {
207 | return Err(Error::new(
208 | ErrorKind::InvalidData,
209 | format!(
210 | "parent block timestamp {} > current block timestamp {}",
211 | prnt_blk.timestamp, self.timestamp
212 | ),
213 | ));
214 | }
215 |
216 | let one_hour_from_now = Utc::now() + Duration::hours(1);
217 | let one_hour_from_now = one_hour_from_now
218 | .timestamp()
219 | .try_into()
220 | .expect("failed to convert timestamp from i64 to u64");
221 |
222 | // ensure block timestamp is no more than an hour ahead of this nodes time
223 | if self.timestamp >= one_hour_from_now {
224 | return Err(Error::new(
225 | ErrorKind::InvalidData,
226 | format!(
227 | "block timestamp {} is more than 1 hour ahead of local time",
228 | self.timestamp
229 | ),
230 | ));
231 | }
232 |
233 | // add newly verified block to memory
234 | self.state.add_verified(&self.clone()).await;
235 | Ok(())
236 | }
237 |
238 | /// Mark this [`Block`](Block) accepted and updates [`State`](crate::state::State) accordingly.
239 | /// # Errors
240 | /// Returns an error if the state can't be updated.
241 | pub async fn accept(&mut self) -> io::Result<()> {
242 | self.set_status(choices::status::Status::Accepted);
243 |
244 | // only decided blocks are persistent -- no reorg
245 | self.state.write_block(&self.clone()).await?;
246 | self.state.set_last_accepted_block(&self.id()).await?;
247 |
248 | self.state.remove_verified(&self.id()).await;
249 | Ok(())
250 | }
251 |
252 | /// Mark this [`Block`](Block) rejected and updates [`State`](crate::state::State) accordingly.
253 | /// # Errors
254 | /// Returns an error if the state can't be updated.
255 | pub async fn reject(&mut self) -> io::Result<()> {
256 | self.set_status(choices::status::Status::Rejected);
257 |
258 | // only decided blocks are persistent -- no reorg
259 | self.state.write_block(&self.clone()).await?;
260 |
261 | self.state.remove_verified(&self.id()).await;
262 | Ok(())
263 | }
264 | }
265 |
266 | impl fmt::Display for Block {
267 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
268 | let serialized = self.to_json_string().unwrap();
269 | write!(f, "{serialized}")
270 | }
271 | }
272 |
273 | /// RUST_LOG=debug cargo test --package timestampvm --lib -- block::test_block --exact --show-output
274 | #[tokio::test]
275 | async fn test_block() {
276 | let _ = env_logger::builder()
277 | .filter_level(log::LevelFilter::Info)
278 | .is_test(true)
279 | .try_init();
280 |
281 | let mut genesis_blk = Block::try_new(
282 | ids::Id::empty(),
283 | 0,
284 | Utc::now().timestamp() as u64,
285 | random_manager::secure_bytes(10).unwrap(),
286 | choices::status::Status::default(),
287 | )
288 | .unwrap();
289 | log::info!("deserialized: {genesis_blk} (block Id: {})", genesis_blk.id);
290 |
291 | let serialized = genesis_blk.to_vec().unwrap();
292 | let deserialized = Block::from_slice(&serialized).unwrap();
293 | log::info!("deserialized: {deserialized}");
294 |
295 | assert_eq!(genesis_blk, deserialized);
296 |
297 | let state = state::State::default();
298 | assert!(!state.has_last_accepted_block().await.unwrap());
299 |
300 | // inner db instance is protected with arc and mutex
301 | // so cloning outer struct "State" should implicitly
302 | // share the db instances
303 | genesis_blk.set_state(state.clone());
304 |
305 | genesis_blk.verify().await.unwrap();
306 | assert!(state.has_verified(&genesis_blk.id()).await);
307 |
308 | genesis_blk.accept().await.unwrap();
309 | assert_eq!(genesis_blk.status, choices::status::Status::Accepted);
310 | assert!(state.has_last_accepted_block().await.unwrap());
311 | assert!(!state.has_verified(&genesis_blk.id()).await); // removed after acceptance
312 |
313 | let last_accepted_blk_id = state.get_last_accepted_block_id().await.unwrap();
314 | assert_eq!(last_accepted_blk_id, genesis_blk.id());
315 |
316 | let read_blk = state.get_block(&genesis_blk.id()).await.unwrap();
317 | assert_eq!(genesis_blk, read_blk);
318 |
319 | let mut blk1 = Block::try_new(
320 | genesis_blk.id,
321 | genesis_blk.height + 1,
322 | genesis_blk.timestamp + 1,
323 | random_manager::secure_bytes(10).unwrap(),
324 | choices::status::Status::default(),
325 | )
326 | .unwrap();
327 | log::info!("blk1: {blk1}");
328 | blk1.set_state(state.clone());
329 |
330 | blk1.verify().await.unwrap();
331 | assert!(state.has_verified(&blk1.id()).await);
332 |
333 | blk1.accept().await.unwrap();
334 | assert_eq!(blk1.status, choices::status::Status::Accepted);
335 | assert!(!state.has_verified(&blk1.id()).await); // removed after acceptance
336 |
337 | let last_accepted_blk_id = state.get_last_accepted_block_id().await.unwrap();
338 | assert_eq!(last_accepted_blk_id, blk1.id());
339 |
340 | let read_blk = state.get_block(&blk1.id()).await.unwrap();
341 | assert_eq!(blk1, read_blk);
342 |
343 | let mut blk2 = Block::try_new(
344 | blk1.id,
345 | blk1.height + 1,
346 | blk1.timestamp + 1,
347 | random_manager::secure_bytes(10).unwrap(),
348 | choices::status::Status::default(),
349 | )
350 | .unwrap();
351 | log::info!("blk2: {blk2}");
352 | blk2.set_state(state.clone());
353 |
354 | blk2.verify().await.unwrap();
355 | assert!(state.has_verified(&blk2.id()).await);
356 |
357 | blk2.reject().await.unwrap();
358 | assert_eq!(blk2.status, choices::status::Status::Rejected);
359 | assert!(!state.has_verified(&blk2.id()).await); // removed after acceptance
360 |
361 | // "blk2" is rejected, so last accepted block must be "blk1"
362 | let last_accepted_blk_id = state.get_last_accepted_block_id().await.unwrap();
363 | assert_eq!(last_accepted_blk_id, blk1.id());
364 |
365 | let read_blk = state.get_block(&blk2.id()).await.unwrap();
366 | assert_eq!(blk2, read_blk);
367 |
368 | let mut blk3 = Block::try_new(
369 | blk2.id,
370 | blk2.height - 1,
371 | blk2.timestamp + 1,
372 | random_manager::secure_bytes(10).unwrap(),
373 | choices::status::Status::default(),
374 | )
375 | .unwrap();
376 | log::info!("blk3: {blk3}");
377 | blk3.set_state(state.clone());
378 |
379 | assert!(blk3.verify().await.is_err());
380 |
381 | assert!(state.has_last_accepted_block().await.unwrap());
382 |
383 | // blk4 built from blk2 has invalid timestamp built 2 hours in future
384 | let mut blk4 = Block::try_new(
385 | blk2.id,
386 | blk2.height + 1,
387 | (Utc::now() + Duration::hours(2)).timestamp() as u64,
388 | random_manager::secure_bytes(10).unwrap(),
389 | choices::status::Status::default(),
390 | )
391 | .unwrap();
392 | log::info!("blk4: {blk4}");
393 | blk4.set_state(state.clone());
394 | assert!(blk4
395 | .verify()
396 | .await
397 | .unwrap_err()
398 | .to_string()
399 | .contains("1 hour ahead"));
400 | }
401 |
402 | #[tonic::async_trait]
403 | impl snowman::Block for Block {
404 | async fn bytes(&self) -> &[u8] {
405 | return self.bytes.as_ref();
406 | }
407 |
408 | async fn height(&self) -> u64 {
409 | self.height
410 | }
411 |
412 | async fn timestamp(&self) -> u64 {
413 | self.timestamp
414 | }
415 |
416 | async fn parent(&self) -> ids::Id {
417 | self.parent_id
418 | }
419 |
420 | async fn verify(&mut self) -> io::Result<()> {
421 | self.verify().await
422 | }
423 | }
424 |
425 | #[tonic::async_trait]
426 | impl Decidable for Block {
427 | /// Implements "snowman.Block.choices.Decidable"
428 | async fn status(&self) -> choices::status::Status {
429 | self.status.clone()
430 | }
431 |
432 | async fn id(&self) -> ids::Id {
433 | self.id
434 | }
435 |
436 | async fn accept(&mut self) -> io::Result<()> {
437 | self.accept().await
438 | }
439 |
440 | async fn reject(&mut self) -> io::Result<()> {
441 | self.reject().await
442 | }
443 | }
444 |
--------------------------------------------------------------------------------
/timestampvm/src/client/mod.rs:
--------------------------------------------------------------------------------
1 | //! Implements client for timestampvm APIs.
2 |
3 | use std::{
4 | collections::HashMap,
5 | io::{self, Error, ErrorKind},
6 | };
7 |
8 | use avalanche_types::{ids, jsonrpc};
9 | use serde::{Deserialize, Serialize};
10 |
11 | /// Represents the RPC response for API `ping`.
12 | #[derive(Debug, Serialize, Deserialize, Clone)]
13 | pub struct PingResponse {
14 | pub jsonrpc: String,
15 | pub id: u32,
16 |
17 | #[serde(skip_serializing_if = "Option::is_none")]
18 | pub result: Option,
19 |
20 | /// Returns non-empty if any.
21 | #[serde(skip_serializing_if = "Option::is_none")]
22 | pub error: Option,
23 | }
24 |
25 | /// Ping the VM.
26 | /// # Errors
27 | /// Errors on an http failure or a failed deserialization.
28 | pub async fn ping(http_rpc: &str, url_path: &str) -> io::Result {
29 | log::info!("ping {http_rpc} with {url_path}");
30 |
31 | let mut data = jsonrpc::RequestWithParamsArray::default();
32 | data.method = String::from("timestampvm.ping");
33 |
34 | let d = data.encode_json()?;
35 | let rb = http_manager::post_non_tls(http_rpc, url_path, &d).await?;
36 |
37 | serde_json::from_slice(&rb)
38 | .map_err(|e| Error::new(ErrorKind::Other, format!("failed ping '{e}'")))
39 | }
40 |
41 | /// Represents the RPC response for API `last_accepted`.
42 | #[derive(Debug, Serialize, Deserialize, Clone)]
43 | pub struct LastAcceptedResponse {
44 | pub jsonrpc: String,
45 | pub id: u32,
46 |
47 | #[serde(skip_serializing_if = "Option::is_none")]
48 | pub result: Option,
49 |
50 | /// Returns non-empty if any.
51 | #[serde(skip_serializing_if = "Option::is_none")]
52 | pub error: Option,
53 | }
54 |
55 | /// Requests for the last accepted block Id.
56 | /// # Errors
57 | /// Errors on failed (de)serialization or an http failure.
58 | pub async fn last_accepted(http_rpc: &str, url_path: &str) -> io::Result {
59 | log::info!("last_accepted {http_rpc} with {url_path}");
60 |
61 | let mut data = jsonrpc::RequestWithParamsArray::default();
62 | data.method = String::from("timestampvm.lastAccepted");
63 |
64 | let d = data.encode_json()?;
65 | let rb = http_manager::post_non_tls(http_rpc, url_path, &d).await?;
66 |
67 | serde_json::from_slice(&rb)
68 | .map_err(|e| Error::new(ErrorKind::Other, format!("failed last_accepted '{e}'")))
69 | }
70 |
71 | /// Represents the RPC response for API `get_block`.
72 | #[derive(Debug, Serialize, Deserialize, Clone)]
73 | pub struct GetBlockResponse {
74 | pub jsonrpc: String,
75 | pub id: u32,
76 |
77 | #[serde(skip_serializing_if = "Option::is_none")]
78 | pub result: Option,
79 |
80 | /// Returns non-empty if any.
81 | #[serde(skip_serializing_if = "Option::is_none")]
82 | pub error: Option,
83 | }
84 |
85 | /// Fetches the block for the corresponding block Id (if any).
86 | /// # Errors
87 | /// Errors on failed (de)serialization or an http failure.
88 | pub async fn get_block(
89 | http_rpc: &str,
90 | url_path: &str,
91 | id: &ids::Id,
92 | ) -> io::Result {
93 | log::info!("get_block {http_rpc} with {url_path}");
94 |
95 | let mut data = jsonrpc::RequestWithParamsHashMapArray::default();
96 | data.method = String::from("timestampvm.getBlock");
97 |
98 | let mut m = HashMap::new();
99 | m.insert("id".to_string(), id.to_string());
100 |
101 | let params = vec![m];
102 | data.params = Some(params);
103 |
104 | let d = data.encode_json()?;
105 | let rb = http_manager::post_non_tls(http_rpc, url_path, &d).await?;
106 |
107 | serde_json::from_slice(&rb)
108 | .map_err(|e| Error::new(ErrorKind::Other, format!("failed get_block '{e}'")))
109 | }
110 |
111 | /// Represents the RPC response for API `propose_block`.
112 | #[derive(Debug, Serialize, Deserialize, Clone)]
113 | pub struct ProposeBlockResponse {
114 | pub jsonrpc: String,
115 | pub id: u32,
116 |
117 | #[serde(skip_serializing_if = "Option::is_none")]
118 | pub result: Option,
119 |
120 | /// Returns non-empty if any.
121 | /// e.g., "error":{"code":-32603,"message":"data 1048586-byte exceeds the limit 1048576-byte"}
122 | #[serde(skip_serializing_if = "Option::is_none")]
123 | pub error: Option,
124 | }
125 |
126 | /// Proposes arbitrary data.
127 | /// # Errors
128 | /// Errors on failed (de)serialization or an http failure.
129 | pub async fn propose_block(
130 | http_rpc: &str,
131 | url_path: &str,
132 | d: Vec,
133 | ) -> io::Result {
134 | log::info!("propose_block {http_rpc} with {url_path}");
135 |
136 | let mut data = jsonrpc::RequestWithParamsHashMapArray::default();
137 | data.method = String::from("timestampvm.proposeBlock");
138 |
139 | let mut m = HashMap::new();
140 | m.insert(
141 | "data".to_string(),
142 | base64::Engine::encode(&base64::engine::general_purpose::STANDARD, &d),
143 | );
144 |
145 | let params = vec![m];
146 | data.params = Some(params);
147 |
148 | let d = data.encode_json()?;
149 | let rb = http_manager::post_non_tls(http_rpc, url_path, &d).await?;
150 |
151 | serde_json::from_slice(&rb)
152 | .map_err(|e| Error::new(ErrorKind::Other, format!("failed propose_block '{e}'")))
153 | }
154 |
155 | /// Represents the error (if any) for APIs.
156 | #[derive(Debug, Serialize, Deserialize, Clone)]
157 | pub struct APIError {
158 | pub code: i32,
159 | pub message: String,
160 | }
161 |
--------------------------------------------------------------------------------
/timestampvm/src/genesis/mod.rs:
--------------------------------------------------------------------------------
1 | //! Defines timestampvm genesis block.
2 |
3 | use std::{
4 | fmt,
5 | fs::{self, File},
6 | io::{self, Error, ErrorKind, Write},
7 | path::Path,
8 | };
9 |
10 | use serde::{Deserialize, Serialize};
11 |
12 | /// Represents the genesis data specific to the VM.
13 | #[derive(Debug, Serialize, Deserialize, Eq, PartialEq, Clone)]
14 | pub struct Genesis {
15 | pub data: String,
16 | }
17 |
18 | impl Default for Genesis {
19 | fn default() -> Self {
20 | Self {
21 | data: String::from("Hello from Rust VM!"),
22 | }
23 | }
24 | }
25 |
26 | impl Genesis {
27 | /// Encodes the genesis to JSON bytes.
28 | /// # Errors
29 | /// Fails if `Self` can't be serialized
30 | pub fn to_vec(&self) -> io::Result> {
31 | serde_json::to_vec(&self).map_err(|e| {
32 | Error::new(
33 | ErrorKind::Other,
34 | format!("failed to serialize Genesis to JSON bytes {e}"),
35 | )
36 | })
37 | }
38 |
39 | /// Decodes the genesis from JSON bytes.
40 | /// # Errors
41 | /// Fails if the bytes can't be deserialized
42 | pub fn from_slice(d: S) -> io::Result
43 | where
44 | S: AsRef<[u8]>,
45 | {
46 | serde_json::from_slice(d.as_ref())
47 | .map_err(|e| Error::new(ErrorKind::Other, format!("failed to decode {e}")))
48 | }
49 |
50 | /// Persists the genesis to a file.
51 | /// # Errors
52 | /// Fails if the file can't be created, written to, or if `self` can't be serialized
53 | pub fn sync(&self, file_path: &str) -> io::Result<()> {
54 | log::info!("syncing genesis to '{}'", file_path);
55 |
56 | let path = Path::new(file_path);
57 | let parent_dir = path.parent().expect("Invalid path");
58 | fs::create_dir_all(parent_dir)?;
59 |
60 | let d = serde_json::to_vec(&self).map_err(|e| {
61 | Error::new(
62 | ErrorKind::Other,
63 | format!("failed to serialize genesis info to JSON {e}"),
64 | )
65 | })?;
66 |
67 | let mut f = File::create(file_path)?;
68 | f.write_all(&d)?;
69 |
70 | Ok(())
71 | }
72 | }
73 |
74 | impl fmt::Display for Genesis {
75 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76 | let s = serde_json::to_string(&self).unwrap();
77 | write!(f, "{s}")
78 | }
79 | }
80 |
--------------------------------------------------------------------------------
/timestampvm/src/lib.rs:
--------------------------------------------------------------------------------
1 | //! A minimal implementation of custom virtual machine (VM) for Avalanche subnet.
2 | //!
3 | //! This project implements timestampvm that allows anyone to propose and read
4 | //! blocks, each of which is tagged with the proposed timestamp. It implements
5 | //! the snowman block.ChainVM interface in Rust, pluggable to `AvalancheGo` nodes.
6 | //!
7 | //! See [`ava-labs/timestampvm`](https://github.com/ava-labs/timestampvm) for the original Go implementation.
8 | //!
9 | //! # Layout
10 | //!
11 | //! The project is structured such that it can be used as a template to build
12 | //! more complex VMs (e.g., Ethereum VM, key-value store VM).
13 | //!
14 | //! The major components are:
15 | //!
16 | //! * [`api`](https://docs.rs/timestampvm/latest/timestampvm/api): Implementation of timestampvm APIs.
17 | //! * [`bin/timestampvm`](https://github.com/ava-labs/timestampvm-rs/tree/main/timestampvm/src/bin/timestampvm): Command-line interface, and plugin server.
18 | //! * [`block`](https://docs.rs/timestampvm/latest/timestampvm/block): Implementation of [`snowman.Block`](https://pkg.go.dev/github.com/ava-labs/avalanchego/snow/consensus/snowman#Block) interface for timestampvm.
19 | //! * [`client`](https://docs.rs/timestampvm/latest/timestampvm/client): Implements client for timestampvm APIs.
20 | //! * [`genesis`](https://docs.rs/timestampvm/latest/timestampvm/genesis): Defines timestampvm genesis block.
21 | //! * [`state`](https://docs.rs/timestampvm/latest/timestampvm/state): Manages the virtual machine states.
22 | //! * [`vm`](https://docs.rs/timestampvm/latest/timestampvm/vm): Implementation of [`snowman.block.ChainVM`](https://pkg.go.dev/github.com/ava-labs/avalanchego/snow/engine/snowman/block#ChainVM) interface for timestampvm.
23 | //!
24 | //! ## Example
25 | //!
26 | //! A simple example that prepares an HTTP/1 connection over a Tokio TCP stream.
27 | //!
28 | //! ```no_run
29 | //! use avalanche_types::subnet;
30 | //! use timestampvm::vm;
31 | //! use tokio::sync::broadcast::{self, Receiver, Sender};
32 | //!
33 | //! #[tokio::main]
34 | //! async fn main() -> std::io::Result<()> {
35 | //! let (stop_ch_tx, stop_ch_rx): (Sender<()>, Receiver<()>) = broadcast::channel(1);
36 | //! let vm_server = subnet::rpc::vm::server::Server::new(vm::Vm::new(), stop_ch_tx);
37 | //! subnet::rpc::vm::serve(vm_server, stop_ch_rx).await
38 | //! }
39 | //! ```
40 |
41 | #![deny(clippy::pedantic)]
42 |
43 | pub mod api;
44 | pub mod block;
45 | pub mod client;
46 | pub mod genesis;
47 | pub mod state;
48 | pub mod vm;
49 |
--------------------------------------------------------------------------------
/timestampvm/src/state/mod.rs:
--------------------------------------------------------------------------------
1 | //! Manages the virtual machine states.
2 |
3 | use std::{
4 | collections::HashMap,
5 | io::{self, Error, ErrorKind},
6 | sync::Arc,
7 | };
8 |
9 | use crate::block::Block;
10 | use avalanche_types::{choices, ids, subnet};
11 | use serde::{Deserialize, Serialize};
12 | use tokio::sync::RwLock;
13 |
14 | /// Manages block and chain states for this Vm, both in-memory and persistent.
15 | #[derive(Clone)]
16 | pub struct State {
17 | pub db: Arc>>,
18 |
19 | /// Maps block Id to Block.
20 | /// Each element is verified but not yet accepted/rejected (e.g., preferred).
21 | pub verified_blocks: Arc>>,
22 | }
23 |
24 | impl Default for State {
25 | fn default() -> State {
26 | Self {
27 | db: Arc::new(RwLock::new(
28 | subnet::rpc::database::memdb::Database::new_boxed(),
29 | )),
30 | verified_blocks: Arc::new(RwLock::new(HashMap::new())),
31 | }
32 | }
33 | }
34 |
35 | const LAST_ACCEPTED_BLOCK_KEY: &[u8] = b"last_accepted_block";
36 |
37 | const STATUS_PREFIX: u8 = 0x0;
38 |
39 | const DELIMITER: u8 = b'/';
40 |
41 | /// Returns a vec of bytes used as a key for identifying blocks in state.
42 | /// '`STATUS_PREFIX`' + '`BYTE_DELIMITER`' + [`block_id`]
43 | fn block_with_status_key(blk_id: &ids::Id) -> Vec {
44 | let mut k: Vec = Vec::with_capacity(ids::LEN + 2);
45 | k.push(STATUS_PREFIX);
46 | k.push(DELIMITER);
47 | k.extend_from_slice(&blk_id.to_vec());
48 | k
49 | }
50 |
51 | /// Wraps a [`Block`](crate::block::Block) and its status.
52 | /// This is the data format that [`State`](State) uses to persist blocks.
53 | #[derive(Serialize, Deserialize, Clone)]
54 | struct BlockWithStatus {
55 | block_bytes: Vec,
56 | status: choices::status::Status,
57 | }
58 |
59 | impl BlockWithStatus {
60 | fn encode(&self) -> io::Result> {
61 | serde_json::to_vec(&self).map_err(|e| {
62 | Error::new(
63 | ErrorKind::Other,
64 | format!("failed to serialize BlockStatus to JSON bytes: {e}"),
65 | )
66 | })
67 | }
68 |
69 | fn from_slice(d: impl AsRef<[u8]>) -> io::Result {
70 | let dd = d.as_ref();
71 | serde_json::from_slice(dd).map_err(|e| {
72 | Error::new(
73 | ErrorKind::Other,
74 | format!("failed to deserialize BlockStatus from JSON: {e}"),
75 | )
76 | })
77 | }
78 | }
79 |
80 | impl State {
81 | /// Persists the last accepted block Id to state.
82 | /// # Errors
83 | /// Fails if the db can't be updated
84 | pub async fn set_last_accepted_block(&self, blk_id: &ids::Id) -> io::Result<()> {
85 | let mut db = self.db.write().await;
86 | db.put(LAST_ACCEPTED_BLOCK_KEY, &blk_id.to_vec())
87 | .await
88 | .map_err(|e| {
89 | Error::new(
90 | ErrorKind::Other,
91 | format!("failed to put last accepted block: {e:?}"),
92 | )
93 | })
94 | }
95 |
96 | /// Returns "true" if there's a last accepted block found.
97 | /// # Errors
98 | /// Fails if the db can't be read
99 | pub async fn has_last_accepted_block(&self) -> io::Result {
100 | let db = self.db.read().await;
101 | match db.has(LAST_ACCEPTED_BLOCK_KEY).await {
102 | Ok(found) => Ok(found),
103 | Err(e) => Err(Error::new(
104 | ErrorKind::Other,
105 | format!("failed to load last accepted block: {e}"),
106 | )),
107 | }
108 | }
109 |
110 | /// Returns the last accepted block Id from state.
111 | /// # Errors
112 | /// Can fail if the db can't be read
113 | pub async fn get_last_accepted_block_id(&self) -> io::Result {
114 | let db = self.db.read().await;
115 | match db.get(LAST_ACCEPTED_BLOCK_KEY).await {
116 | Ok(d) => Ok(ids::Id::from_slice(&d)),
117 | Err(e) => {
118 | if subnet::rpc::errors::is_not_found(&e) {
119 | return Ok(ids::Id::empty());
120 | }
121 | Err(e)
122 | }
123 | }
124 | }
125 |
126 | /// Adds a block to "`verified_blocks`".
127 | pub async fn add_verified(&mut self, block: &Block) {
128 | let blk_id = block.id();
129 | log::info!("verified added {blk_id}");
130 |
131 | let mut verified_blocks = self.verified_blocks.write().await;
132 | verified_blocks.insert(blk_id, block.clone());
133 | }
134 |
135 | /// Removes a block from "`verified_blocks`".
136 | pub async fn remove_verified(&mut self, blk_id: &ids::Id) {
137 | let mut verified_blocks = self.verified_blocks.write().await;
138 | verified_blocks.remove(blk_id);
139 | }
140 |
141 | /// Returns "true" if the block Id has been already verified.
142 | pub async fn has_verified(&self, blk_id: &ids::Id) -> bool {
143 | let verified_blocks = self.verified_blocks.read().await;
144 | verified_blocks.contains_key(blk_id)
145 | }
146 |
147 | /// Writes a block to the state storage.
148 | /// # Errors
149 | /// Can fail if the block fails to serialize or if the db can't be updated
150 | pub async fn write_block(&mut self, block: &Block) -> io::Result<()> {
151 | let blk_id = block.id();
152 | let blk_bytes = block.to_vec()?;
153 |
154 | let mut db = self.db.write().await;
155 |
156 | let blk_status = BlockWithStatus {
157 | block_bytes: blk_bytes,
158 | status: block.status(),
159 | };
160 | let blk_status_bytes = blk_status.encode()?;
161 |
162 | db.put(&block_with_status_key(&blk_id), &blk_status_bytes)
163 | .await
164 | .map_err(|e| Error::new(ErrorKind::Other, format!("failed to put block: {e:?}")))
165 | }
166 |
167 | /// Reads a block from the state storage using the `block_with_status_key`.
168 | /// # Errors
169 | /// Can fail if the block is not found in the state storage, or if the block fails to deserialize
170 | pub async fn get_block(&self, blk_id: &ids::Id) -> io::Result {
171 | // check if the block exists in memory as previously verified.
172 | let verified_blocks = self.verified_blocks.read().await;
173 | if let Some(b) = verified_blocks.get(blk_id) {
174 | return Ok(b.clone());
175 | }
176 |
177 | let db = self.db.read().await;
178 |
179 | let blk_status_bytes = db.get(&block_with_status_key(blk_id)).await?;
180 | let blk_status = BlockWithStatus::from_slice(blk_status_bytes)?;
181 |
182 | let mut blk = Block::from_slice(&blk_status.block_bytes)?;
183 | blk.set_status(blk_status.status);
184 |
185 | Ok(blk)
186 | }
187 | }
188 |
189 | /// RUST_LOG=debug cargo test --package timestampvm --lib -- state::test_state --exact --show-output
190 | #[tokio::test]
191 | async fn test_state() {
192 | let _ = env_logger::builder()
193 | .filter_level(log::LevelFilter::Info)
194 | .is_test(true)
195 | .try_init();
196 |
197 | let genesis_blk = Block::try_new(
198 | ids::Id::empty(),
199 | 0,
200 | random_manager::u64(),
201 | random_manager::secure_bytes(10).unwrap(),
202 | choices::status::Status::Accepted,
203 | )
204 | .unwrap();
205 | log::info!("genesis block: {genesis_blk}");
206 |
207 | let blk1 = Block::try_new(
208 | genesis_blk.id(),
209 | 1,
210 | genesis_blk.timestamp() + 1,
211 | random_manager::secure_bytes(10).unwrap(),
212 | choices::status::Status::Accepted,
213 | )
214 | .unwrap();
215 | log::info!("blk1: {blk1}");
216 |
217 | let mut state = State::default();
218 | assert!(!state.has_last_accepted_block().await.unwrap());
219 |
220 | state.write_block(&genesis_blk).await.unwrap();
221 | assert!(!state.has_last_accepted_block().await.unwrap());
222 |
223 | state.write_block(&blk1).await.unwrap();
224 | state.set_last_accepted_block(&blk1.id()).await.unwrap();
225 | assert!(state.has_last_accepted_block().await.unwrap());
226 |
227 | let last_accepted_blk_id = state.get_last_accepted_block_id().await.unwrap();
228 | assert_eq!(last_accepted_blk_id, blk1.id());
229 |
230 | let read_blk = state.get_block(&genesis_blk.id()).await.unwrap();
231 | assert_eq!(genesis_blk, read_blk);
232 |
233 | let read_blk = state.get_block(&blk1.id()).await.unwrap();
234 | assert_eq!(blk1, read_blk);
235 | }
236 |
--------------------------------------------------------------------------------
/timestampvm/src/vm/mod.rs:
--------------------------------------------------------------------------------
1 | //! Implementation of [`snowman.block.ChainVM`](https://pkg.go.dev/github.com/ava-labs/avalanchego/snow/engine/snowman/block#ChainVM) interface for timestampvm.
2 |
3 | use std::{
4 | collections::{HashMap, VecDeque},
5 | io::{self, Error, ErrorKind},
6 | sync::Arc,
7 | time::Duration,
8 | };
9 |
10 | use crate::{
11 | api::{
12 | chain_handlers::{ChainHandler, ChainService},
13 | static_handlers::{StaticHandler, StaticService},
14 | },
15 | block::Block,
16 | genesis::Genesis,
17 | state,
18 | };
19 | use avalanche_types::{
20 | choices, ids,
21 | subnet::{
22 | self,
23 | rpc::{
24 | context::Context,
25 | database::{manager::DatabaseManager, BoxedDatabase},
26 | health::Checkable,
27 | snow::{
28 | self,
29 | engine::common::{
30 | appsender::AppSender,
31 | engine::{AppHandler, CrossChainAppHandler, NetworkAppHandler},
32 | http_handler::{HttpHandler, LockOptions},
33 | vm::{CommonVm, Connector},
34 | },
35 | validators::client::ValidatorStateClient,
36 | },
37 | snowman::block::{BatchedChainVm, ChainVm, Getter, Parser},
38 | },
39 | },
40 | };
41 | use bytes::Bytes;
42 | use chrono::{DateTime, Utc};
43 | use semver::Version;
44 | use tokio::sync::{mpsc::Sender, RwLock};
45 |
46 | const VERSION: &str = env!("CARGO_PKG_VERSION");
47 |
48 | /// Limits how much data a user can propose.
49 | pub const PROPOSE_LIMIT_BYTES: usize = 1024 * 1024;
50 |
51 | /// Represents VM-specific states.
52 | /// Defined in a separate struct, for interior mutability in [`Vm`](Vm).
53 | /// To be protected with `Arc` and `RwLock`.
54 | pub struct State {
55 | pub ctx: Option>,
56 | pub version: Version,
57 | pub genesis: Genesis,
58 |
59 | /// Represents persistent Vm state.
60 | pub state: Option,
61 | /// Currently preferred block Id.
62 | pub preferred: ids::Id,
63 | /// Channel to send messages to the snowman consensus engine.
64 | pub to_engine: Option>,
65 | /// Set "true" to indicate that the Vm has finished bootstrapping
66 | /// for the chain.
67 | pub bootstrapped: bool,
68 | }
69 |
70 | impl Default for State {
71 | fn default() -> Self {
72 | Self {
73 | ctx: None,
74 | version: Version::new(0, 0, 0),
75 | genesis: Genesis::default(),
76 |
77 | state: None,
78 | preferred: ids::Id::empty(),
79 | to_engine: None,
80 | bootstrapped: false,
81 | }
82 | }
83 | }
84 |
85 | /// Implements [`snowman.block.ChainVM`](https://pkg.go.dev/github.com/ava-labs/avalanchego/snow/engine/snowman/block#ChainVM) interface.
86 | #[derive(Clone)]
87 | pub struct Vm {
88 | /// Maintains the Vm-specific states.
89 | pub state: Arc>,
90 | pub app_sender: Option,
91 |
92 | /// A queue of data that have not been put into a block and proposed yet.
93 | /// Mempool is not persistent, so just keep in memory via Vm.
94 | pub mempool: Arc>>>,
95 | }
96 |
97 | impl Default for Vm
98 | where
99 | A: Send + Sync + Clone + 'static,
100 | {
101 | fn default() -> Self {
102 | Self::new()
103 | }
104 | }
105 |
106 | impl Vm
107 | where
108 | A: Send + Sync + Clone + 'static,
109 | {
110 | #[must_use]
111 | pub fn new() -> Self {
112 | Self {
113 | state: Arc::new(RwLock::new(State::default())),
114 | app_sender: None,
115 | mempool: Arc::new(RwLock::new(VecDeque::with_capacity(100))),
116 | }
117 | }
118 |
119 | pub async fn is_bootstrapped(&self) -> bool {
120 | let vm_state = self.state.read().await;
121 | vm_state.bootstrapped
122 | }
123 |
124 | /// Signals the consensus engine that a new block is ready to be created.
125 | pub async fn notify_block_ready(&self) {
126 | let vm_state = self.state.read().await;
127 | if let Some(to_engine) = &vm_state.to_engine {
128 | to_engine
129 | .send(snow::engine::common::message::Message::PendingTxs)
130 | .await
131 | .unwrap_or_else(|e| log::warn!("dropping message to consensus engine: {}", e));
132 |
133 | log::info!("notified block ready!");
134 | } else {
135 | log::error!("consensus engine channel failed to initialized");
136 | }
137 | }
138 |
139 | /// Proposes arbitrary data to mempool and notifies that a block is ready for builds.
140 | /// Other VMs may optimize mempool with more complicated batching mechanisms.
141 | /// # Errors
142 | /// Can fail if the data size exceeds `PROPOSE_LIMIT_BYTES`.
143 | pub async fn propose_block(&self, d: Vec) -> io::Result<()> {
144 | let size = d.len();
145 | log::info!("received propose_block of {size} bytes");
146 |
147 | if size > PROPOSE_LIMIT_BYTES {
148 | log::info!("limit exceeded... returning an error...");
149 | return Err(Error::new(
150 | ErrorKind::InvalidInput,
151 | format!("data {size}-byte exceeds the limit {PROPOSE_LIMIT_BYTES}-byte"),
152 | ));
153 | }
154 |
155 | let mut mempool = self.mempool.write().await;
156 | mempool.push_back(d);
157 | log::info!("proposed {size} bytes of data for a block");
158 |
159 | self.notify_block_ready().await;
160 | Ok(())
161 | }
162 |
163 | /// Sets the state of the Vm.
164 | /// # Errors
165 | /// Will fail if the `snow::State` is syncing
166 | pub async fn set_state(&self, snow_state: snow::State) -> io::Result<()> {
167 | let mut vm_state = self.state.write().await;
168 | match snow_state {
169 | // called by chains manager when it is creating the chain.
170 | snow::State::Initializing => {
171 | log::info!("set_state: initializing");
172 | vm_state.bootstrapped = false;
173 | Ok(())
174 | }
175 |
176 | snow::State::StateSyncing => {
177 | log::info!("set_state: state syncing");
178 | Err(Error::new(ErrorKind::Other, "state sync is not supported"))
179 | }
180 |
181 | // called by the bootstrapper to signal bootstrapping has started.
182 | snow::State::Bootstrapping => {
183 | log::info!("set_state: bootstrapping");
184 | vm_state.bootstrapped = false;
185 | Ok(())
186 | }
187 |
188 | // called when consensus has started signalling bootstrap phase is complete.
189 | snow::State::NormalOp => {
190 | log::info!("set_state: normal op");
191 | vm_state.bootstrapped = true;
192 | Ok(())
193 | }
194 | }
195 | }
196 |
197 | /// Returns the last accepted block Id.
198 | /// # Errors
199 | /// Will fail if there's no state or if the db can't be accessed
200 | pub async fn last_accepted(&self) -> io::Result {
201 | let vm_state = self.state.read().await;
202 |
203 | match &vm_state.state {
204 | Some(state) => state.get_last_accepted_block_id().await,
205 | None => Err(Error::new(ErrorKind::NotFound, "state manager not found")),
206 | }
207 | }
208 | }
209 |
210 | #[tonic::async_trait]
211 | impl CommonVm for Vm
212 | where
213 | A: AppSender + Send + Sync + Clone + 'static,
214 | {
215 | type DatabaseManager = DatabaseManager;
216 | type AppSender = A;
217 | type ChainHandler = ChainHandler>;
218 | type StaticHandler = StaticHandler;
219 | type ValidatorState = ValidatorStateClient;
220 |
221 | async fn initialize(
222 | &mut self,
223 | ctx: Option>,
224 | db_manager: BoxedDatabase,
225 | genesis_bytes: &[u8],
226 | _upgrade_bytes: &[u8],
227 | _config_bytes: &[u8],
228 | to_engine: Sender,
229 | _fxs: &[snow::engine::common::vm::Fx],
230 | app_sender: Self::AppSender,
231 | ) -> io::Result<()> {
232 | log::info!("initializing Vm");
233 | let mut vm_state = self.state.write().await;
234 |
235 | vm_state.ctx = ctx;
236 |
237 | let version =
238 | Version::parse(VERSION).map_err(|e| Error::new(ErrorKind::Other, e.to_string()))?;
239 | vm_state.version = version;
240 |
241 | let genesis = Genesis::from_slice(genesis_bytes)?;
242 | vm_state.genesis = genesis;
243 |
244 | let state = state::State {
245 | db: Arc::new(RwLock::new(db_manager)),
246 | verified_blocks: Arc::new(RwLock::new(HashMap::new())),
247 | };
248 | vm_state.state = Some(state.clone());
249 |
250 | vm_state.to_engine = Some(to_engine);
251 |
252 | self.app_sender = Some(app_sender);
253 |
254 | let has_last_accepted = state.has_last_accepted_block().await?;
255 | if has_last_accepted {
256 | let last_accepted_blk_id = state.get_last_accepted_block_id().await?;
257 | vm_state.preferred = last_accepted_blk_id;
258 | log::info!("initialized Vm with last accepted block {last_accepted_blk_id}");
259 | } else {
260 | let mut genesis_block = Block::try_new(
261 | ids::Id::empty(),
262 | 0,
263 | 0,
264 | vm_state.genesis.data.as_bytes().to_vec(),
265 | choices::status::Status::default(),
266 | )?;
267 | genesis_block.set_state(state.clone());
268 | genesis_block.accept().await?;
269 |
270 | let genesis_blk_id = genesis_block.id();
271 | vm_state.preferred = genesis_blk_id;
272 | log::info!("initialized Vm with genesis block {genesis_blk_id}");
273 | }
274 |
275 | self.mempool = Arc::new(RwLock::new(VecDeque::with_capacity(100)));
276 |
277 | log::info!("successfully initialized Vm");
278 | Ok(())
279 | }
280 |
281 | /// Called when the node is shutting down.
282 | async fn shutdown(&self) -> io::Result<()> {
283 | // grpc servers are shutdown via broadcast channel
284 | // if additional shutdown is required we can extend.
285 | Ok(())
286 | }
287 |
288 | async fn set_state(&self, snow_state: subnet::rpc::snow::State) -> io::Result<()> {
289 | self.set_state(snow_state).await
290 | }
291 |
292 | async fn version(&self) -> io::Result {
293 | Ok(String::from(VERSION))
294 | }
295 |
296 | /// Creates static handlers.
297 | async fn create_static_handlers(
298 | &mut self,
299 | ) -> io::Result>> {
300 | let handler = StaticHandler::new(StaticService::new());
301 | let mut handlers = HashMap::new();
302 | handlers.insert(
303 | "/static".to_string(),
304 | HttpHandler {
305 | lock_option: LockOptions::WriteLock,
306 | handler,
307 | server_addr: None,
308 | },
309 | );
310 |
311 | Ok(handlers)
312 | }
313 |
314 | /// Creates VM-specific handlers.
315 | async fn create_handlers(
316 | &mut self,
317 | ) -> io::Result>> {
318 | let handler = ChainHandler::new(ChainService::new(self.clone()));
319 | let mut handlers = HashMap::new();
320 | handlers.insert(
321 | "/rpc".to_string(),
322 | HttpHandler {
323 | lock_option: LockOptions::WriteLock,
324 | handler,
325 | server_addr: None,
326 | },
327 | );
328 |
329 | Ok(handlers)
330 | }
331 | }
332 |
333 | #[tonic::async_trait]
334 | impl ChainVm for Vm
335 | where
336 | A: AppSender + Send + Sync + Clone + 'static,
337 | {
338 | type Block = Block;
339 |
340 | /// Builds a block from mempool data.
341 | async fn build_block(&self) -> io::Result<::Block> {
342 | let mut mempool = self.mempool.write().await;
343 |
344 | log::info!("build_block called for {} mempool", mempool.len());
345 | if mempool.is_empty() {
346 | return Err(Error::new(ErrorKind::Other, "no pending block"));
347 | }
348 |
349 | let vm_state = self.state.read().await;
350 | if let Some(state) = &vm_state.state {
351 | self.notify_block_ready().await;
352 |
353 | // "state" must have preferred block in cache/verified_block
354 | // otherwise, not found error from rpcchainvm database
355 | let prnt_blk = state.get_block(&vm_state.preferred).await?;
356 | let unix_now = Utc::now()
357 | .timestamp()
358 | .try_into()
359 | .expect("timestamp to convert from i64 to u64");
360 |
361 | let first = mempool.pop_front().unwrap();
362 | let mut block = Block::try_new(
363 | prnt_blk.id(),
364 | prnt_blk.height() + 1,
365 | unix_now,
366 | first,
367 | choices::status::Status::Processing,
368 | )?;
369 | block.set_state(state.clone());
370 | block.verify().await?;
371 |
372 | log::info!("successfully built block");
373 | return Ok(block);
374 | }
375 |
376 | Err(Error::new(ErrorKind::NotFound, "state manager not found"))
377 | }
378 |
379 | async fn set_preference(&self, id: ids::Id) -> io::Result<()> {
380 | let mut vm_state = self.state.write().await;
381 | vm_state.preferred = id;
382 |
383 | Ok(())
384 | }
385 |
386 | async fn last_accepted(&self) -> io::Result {
387 | self.last_accepted().await
388 | }
389 |
390 | async fn issue_tx(&self) -> io::Result<::Block> {
391 | Err(Error::new(
392 | ErrorKind::Unsupported,
393 | "issue_tx not implemented",
394 | ))
395 | }
396 |
397 | // Passes back ok as a no-op for now.
398 | // TODO: Remove after v1.11.x activates
399 | async fn verify_height_index(&self) -> io::Result<()> {
400 | Ok(())
401 | }
402 |
403 | // Returns an error as a no-op for now.
404 | async fn get_block_id_at_height(&self, _height: u64) -> io::Result {
405 | Err(Error::new(ErrorKind::NotFound, "block id not found"))
406 | }
407 |
408 | async fn state_sync_enabled(&self) -> io::Result {
409 | Ok(false)
410 | }
411 | }
412 |
413 | #[tonic::async_trait]
414 | impl BatchedChainVm for Vm
415 | where
416 | A: Send + Sync + Clone + 'static,
417 | {
418 | type Block = Block;
419 |
420 | async fn get_ancestors(
421 | &self,
422 | _block_id: ids::Id,
423 | _max_block_num: i32,
424 | _max_block_size: i32,
425 | _max_block_retrival_time: Duration,
426 | ) -> io::Result> {
427 | Err(Error::new(
428 | ErrorKind::Unsupported,
429 | "get_ancestors not implemented",
430 | ))
431 | }
432 | async fn batched_parse_block(&self, _blocks: &[Vec]) -> io::Result> {
433 | Err(Error::new(
434 | ErrorKind::Unsupported,
435 | "batched_parse_block not implemented",
436 | ))
437 | }
438 | }
439 |
440 | #[tonic::async_trait]
441 | impl NetworkAppHandler for Vm
442 | where
443 | A: AppSender + Send + Sync + Clone + 'static,
444 | {
445 | /// Currently, no app-specific messages, so returning Ok.
446 | async fn app_request(
447 | &self,
448 | _node_id: &ids::node::Id,
449 | _request_id: u32,
450 | _deadline: DateTime,
451 | _request: &[u8],
452 | ) -> io::Result<()> {
453 | Ok(())
454 | }
455 |
456 | /// Currently, no app-specific messages, so returning Ok.
457 | async fn app_request_failed(
458 | &self,
459 | _node_id: &ids::node::Id,
460 | _request_id: u32,
461 | ) -> io::Result<()> {
462 | Ok(())
463 | }
464 |
465 | /// Currently, no app-specific messages, so returning Ok.
466 | async fn app_response(
467 | &self,
468 | _node_id: &ids::node::Id,
469 | _request_id: u32,
470 | _response: &[u8],
471 | ) -> io::Result<()> {
472 | Ok(())
473 | }
474 |
475 | /// Currently, no app-specific messages, so returning Ok.
476 | async fn app_gossip(&self, _node_id: &ids::node::Id, _msg: &[u8]) -> io::Result<()> {
477 | Ok(())
478 | }
479 | }
480 |
481 | #[tonic::async_trait]
482 | impl CrossChainAppHandler for Vm
483 | where
484 | A: AppSender + Send + Sync + Clone + 'static,
485 | {
486 | /// Currently, no cross chain specific messages, so returning Ok.
487 | async fn cross_chain_app_request(
488 | &self,
489 | _chain_id: &ids::Id,
490 | _request_id: u32,
491 | _deadline: DateTime,
492 | _request: &[u8],
493 | ) -> io::Result<()> {
494 | Ok(())
495 | }
496 |
497 | /// Currently, no cross chain specific messages, so returning Ok.
498 | async fn cross_chain_app_request_failed(
499 | &self,
500 | _chain_id: &ids::Id,
501 | _request_id: u32,
502 | ) -> io::Result<()> {
503 | Ok(())
504 | }
505 |
506 | /// Currently, no cross chain specific messages, so returning Ok.
507 | async fn cross_chain_app_response(
508 | &self,
509 | _chain_id: &ids::Id,
510 | _request_id: u32,
511 | _response: &[u8],
512 | ) -> io::Result<()> {
513 | Ok(())
514 | }
515 | }
516 |
517 | impl AppHandler for Vm where A: AppSender + Send + Sync + Clone + 'static {}
518 |
519 | #[tonic::async_trait]
520 | impl Connector for Vm
521 | where
522 | A: AppSender + Send + Sync + Clone + 'static,
523 | {
524 | async fn connected(&self, _id: &ids::node::Id) -> io::Result<()> {
525 | // no-op
526 | Ok(())
527 | }
528 |
529 | async fn disconnected(&self, _id: &ids::node::Id) -> io::Result<()> {
530 | // no-op
531 | Ok(())
532 | }
533 | }
534 |
535 | #[tonic::async_trait]
536 | impl Checkable for Vm
537 | where
538 | A: AppSender + Send + Sync + Clone + 'static,
539 | {
540 | async fn health_check(&self) -> io::Result> {
541 | Ok("200".as_bytes().to_vec())
542 | }
543 | }
544 |
545 | #[tonic::async_trait]
546 | impl Getter for Vm
547 | where
548 | A: AppSender + Send + Sync + Clone + 'static,
549 | {
550 | type Block = Block;
551 |
552 | async fn get_block(&self, blk_id: ids::Id) -> io::Result<::Block> {
553 | let vm_state = self.state.read().await;
554 | if let Some(state) = &vm_state.state {
555 | let block = state.get_block(&blk_id).await?;
556 | return Ok(block);
557 | }
558 |
559 | Err(Error::new(ErrorKind::NotFound, "state manager not found"))
560 | }
561 | }
562 |
563 | #[tonic::async_trait]
564 | impl Parser for Vm
565 | where
566 | A: AppSender + Send + Sync + Clone + 'static,
567 | {
568 | type Block = Block;
569 |
570 | async fn parse_block(&self, bytes: &[u8]) -> io::Result<::Block> {
571 | let vm_state = self.state.read().await;
572 | if let Some(state) = &vm_state.state {
573 | let mut new_block = Block::from_slice(bytes)?;
574 | new_block.set_status(choices::status::Status::Processing);
575 | new_block.set_state(state.clone());
576 | log::debug!("parsed block {}", new_block.id());
577 |
578 | match state.get_block(&new_block.id()).await {
579 | Ok(prev) => {
580 | log::debug!("returning previously parsed block {}", prev.id());
581 | return Ok(prev);
582 | }
583 | Err(_) => return Ok(new_block),
584 | };
585 | }
586 |
587 | Err(Error::new(ErrorKind::NotFound, "state manager not found"))
588 | }
589 | }
590 |
--------------------------------------------------------------------------------