├── .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 | [crates.io](https://crates.io/crates/timestampvm) 3 | [docs.rs](https://docs.rs/timestampvm) 4 | ![Github Actions](https://github.com/ava-labs/timestampvm-rs/actions/workflows/test-and-release.yml/badge.svg) 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 | --------------------------------------------------------------------------------