├── .github └── workflows │ ├── build_and_test.sh │ └── lint_test_build.yml ├── .gitignore ├── .golangci.yml ├── LICENSE ├── LICENSE.header ├── README.md ├── client └── client.go ├── go.mod ├── go.sum ├── main ├── main.go └── params.go ├── scripts ├── build.sh ├── build_test.sh ├── run.sh ├── tests.lint.sh ├── tests.load.sh └── versions.sh ├── tests ├── e2e │ └── e2e_test.go └── load │ ├── client │ ├── client.go │ └── requester.go │ ├── load.go │ └── load_test.go └── timestampvm ├── block.go ├── block_state.go ├── codec.go ├── factory.go ├── service.go ├── singleton_state.go ├── state.go ├── static_service.go ├── utils.go ├── vm.go └── vm_test.go /.github/workflows/build_and_test.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o nounset 5 | set -o pipefail 6 | 7 | # Avalanche root directory 8 | VM_PATH=$( cd "$( dirname "${BASH_SOURCE[0]}" )"; cd ../.. && pwd ) 9 | 10 | "$VM_PATH"/scripts/build.sh 11 | # Check to see if the build script creates any unstaged changes to prevent 12 | # regression where builds go.mod/go.sum files get out of date. 13 | if [[ -z $(git status -s) ]]; then 14 | echo "Build script created unstaged changes in the repository" 15 | # TODO: Revise this check once we can reliably build without changes 16 | # exit 1 17 | fi 18 | "$VM_PATH"/scripts/build_test.sh 19 | -------------------------------------------------------------------------------- /.github/workflows/lint_test_build.yml: -------------------------------------------------------------------------------- 1 | name: Lint+Test+Build 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | branches: [main] 8 | 9 | jobs: 10 | lint: 11 | name: Lint 12 | runs-on: ubuntu-20.04 13 | steps: 14 | - uses: actions/checkout@v3 15 | - uses: actions/setup-go@v3 16 | with: 17 | go-version: "1.19" 18 | - name: golangci-lint 19 | uses: golangci/golangci-lint-action@v3 20 | with: 21 | version: v1.51 22 | working-directory: . 23 | args: --timeout 3m 24 | 25 | build-test: 26 | name: Build & Test 27 | runs-on: ubuntu-latest 28 | needs: lint 29 | steps: 30 | - uses: actions/checkout@v2.3.4 31 | - uses: actions/setup-go@v2 32 | with: 33 | go-version: 1.19 34 | - name: build_test 35 | shell: bash 36 | run: .github/workflows/build_and_test.sh 37 | 38 | e2e-test: 39 | name: E2E Test 40 | runs-on: ubuntu-latest 41 | needs: build-test 42 | steps: 43 | - name: Git checkout 44 | uses: actions/checkout@v2 45 | with: 46 | fetch-depth: 0 47 | - name: Set up Go 48 | uses: actions/setup-go@v2 49 | with: 50 | go-version: 1.19 51 | - name: Run e2e tests 52 | shell: bash 53 | run: scripts/run.sh 54 | env: 55 | E2E: true 56 | 57 | load-test: 58 | name: Load Test 59 | runs-on: ubuntu-latest 60 | needs: build-test 61 | steps: 62 | - name: Git checkout 63 | uses: actions/checkout@v2 64 | with: 65 | fetch-depth: 0 66 | - name: Set up Go 67 | uses: actions/setup-go@v2 68 | with: 69 | go-version: 1.19 70 | - name: Run load tests 71 | shell: bash 72 | run: scripts/tests.load.sh 73 | env: 74 | TERMINAL_HEIGHT: 1000 # Set the terminal height for the load test to reach before terminating. 75 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ./main 2 | 3 | *.log 4 | *~ 5 | .DS_Store 6 | 7 | awscpu 8 | 9 | # Binaries for programs and plugins 10 | *.exe 11 | *.exe~ 12 | *.dll 13 | *.so 14 | *.dylib 15 | *.profile 16 | 17 | # Test binary, build with `go test -c` 18 | *.test 19 | 20 | # Output of the go coverage tool, specifically when used with LiteIDE 21 | *.out 22 | 23 | # ignore GoLand metafiles directory 24 | .idea/ 25 | 26 | *logs/ 27 | 28 | .vscode* 29 | 30 | *.pb* 31 | 32 | db* 33 | 34 | *cpu[0-9]* 35 | *mem[0-9]* 36 | *lock[0-9]* 37 | *.profile 38 | *.swp 39 | *.aux 40 | *.fdb* 41 | *.fls 42 | *.gz 43 | *.pdf 44 | 45 | .coverage 46 | 47 | bin/ 48 | build/ 49 | -------------------------------------------------------------------------------- /.golangci.yml: -------------------------------------------------------------------------------- 1 | # https://golangci-lint.run/usage/configuration/ 2 | run: 3 | timeout: 10m 4 | # skip auto-generated files. 5 | skip-files: 6 | - ".*\\.pb\\.go$" 7 | - ".*mock.*" 8 | 9 | issues: 10 | # Maximum count of issues with the same text. Set to 0 to disable. Default is 3. 11 | max-same-issues: 0 12 | 13 | linters: 14 | # please, do not use `enable-all`: it's deprecated and will be removed soon. 15 | # inverted configuration with `enable-all` and `disable` is not scalable during updates of golangci-lint 16 | disable-all: true 17 | enable: 18 | - asciicheck 19 | - bodyclose 20 | - depguard 21 | - errcheck 22 | - exportloopref 23 | - goconst 24 | - gocritic 25 | - gofmt 26 | - gofumpt 27 | - goimports 28 | - goprintffuncname 29 | - gosec 30 | - gosimple 31 | - govet 32 | - ineffassign 33 | - misspell 34 | - nakedret 35 | - noctx 36 | - nolintlint 37 | - prealloc 38 | - revive 39 | - staticcheck 40 | - stylecheck 41 | - typecheck 42 | - unconvert 43 | - unparam 44 | - unused 45 | - whitespace 46 | # - errorlint (TODO: re-enable in go1.20 migration) 47 | # - goerr113 48 | # - gomnd 49 | # - lll 50 | 51 | linters-settings: 52 | errorlint: 53 | # Check for plain type assertions and type switches. 54 | asserts: false 55 | # Check for plain error comparisons. 56 | comparison: false 57 | revive: 58 | rules: 59 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#bool-literal-in-expr 60 | - name: bool-literal-in-expr 61 | disabled: false 62 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#early-return 63 | - name: early-return 64 | disabled: false 65 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#empty-lines 66 | - name: empty-lines 67 | disabled: false 68 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#string-format 69 | - name: string-format 70 | disabled: false 71 | arguments: 72 | - ["fmt.Errorf[0]", "/.*%.*/", "no format directive, use errors.New instead"] 73 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#struct-tag 74 | - name: struct-tag 75 | disabled: false 76 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unexported-naming 77 | - name: unexported-naming 78 | disabled: false 79 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unhandled-error 80 | - name: unhandled-error 81 | disabled: false 82 | arguments: 83 | - "fmt\\.Fprint" 84 | - "fmt\\.Fprintf" 85 | - "fmt\\.Print" 86 | - "fmt\\.Printf" 87 | - "fmt\\.Println" 88 | - "math/rand\\.Read" 89 | - "strings\\.Builder\\.WriteString" 90 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-parameter 91 | - name: unused-parameter 92 | disabled: false 93 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#unused-receiver 94 | - name: unused-receiver 95 | disabled: false 96 | # https://github.com/mgechev/revive/blob/master/RULES_DESCRIPTIONS.md#useless-break 97 | - name: useless-break 98 | disabled: false 99 | staticcheck: 100 | go: "1.19" 101 | # https://staticcheck.io/docs/options#checks 102 | checks: 103 | - "all" 104 | - "-SA6002" # argument should be pointer-like to avoid allocation, for sync.Pool 105 | - "-SA1019" # deprecated packages e.g., golang.org/x/crypto/ripemd160 106 | # https://golangci-lint.run/usage/linters#gosec 107 | gosec: 108 | excludes: 109 | - G107 # https://securego.io/docs/rules/g107.html 110 | depguard: 111 | list-type: blacklist 112 | packages-with-error-message: 113 | - io/ioutil: 'io/ioutil is deprecated. Use package io or os instead.' 114 | - github.com/stretchr/testify/assert: 'github.com/stretchr/testify/require should be used instead.' 115 | include-go-root: true 116 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2022, Ava Labs, Inc. 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 | -------------------------------------------------------------------------------- /LICENSE.header: -------------------------------------------------------------------------------- 1 | Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | See the file LICENSE for licensing terms. 3 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Timestamp Virtual Machine 2 | 3 | [![Lint+Test+Build](https://github.com/ava-labs/timestampvm/actions/workflows/lint_test_build.yml/badge.svg)](https://github.com/ava-labs/timestampvm/actions/workflows/lint_test_build.yml) 4 | 5 | Avalanche is a network composed of multiple blockchains. Each blockchain is an instance of a [Virtual Machine (VM)](https://docs.avax.network/learn/platform-overview#virtual-machines), much like an object in an object-oriented language is an instance of a class. That is, the VM defines the behavior of the blockchain. 6 | 7 | TimestampVM defines a blockchain that is a timestamp server. Each block in the blockchain contains the timestamp when it was created along with a 32-byte piece of data (payload). Each block’s timestamp is after its parent’s timestamp. This VM demonstrates capabilities of custom VMs and custom blockchains. For more information, see: [Create a Virtual Machine](https://docs.avax.network/build/tutorials/platform/create-a-virtual-machine-vm) 8 | 9 | ## Running the VM 10 | [`scripts/run.sh`](scripts/run.sh) automatically installs [avalanchego], sets up a local network, 11 | and creates a `timestampvm` genesis file. To build and run E2E tests, you need to set the variable `E2E` before it: `E2E=true ./scripts/run.sh 1.7.11` 12 | 13 | *Note: The above script relies on ginkgo to run successfully. Ensure that $GOPATH/bin is part of your $PATH before running the script.* 14 | 15 | _See [`tests/e2e`](tests/e2e) to see how it's set up and how its client requests are made._ 16 | 17 | ```bash 18 | # to startup a local cluster (good for development) 19 | cd ${HOME}/go/src/github.com/ava-labs/timestampvm 20 | ./scripts/run.sh 1.9.3 21 | 22 | # to run full e2e tests and shut down cluster afterwards 23 | cd ${HOME}/go/src/github.com/ava-labs/timestampvm 24 | E2E=true ./scripts/run.sh 1.9.3 25 | 26 | # inspect cluster endpoints when ready 27 | cat /tmp/avalanchego-v1.9.3/output.yaml 28 | </tmp/.genesis 99 | 100 | ############################ 101 | 102 | ############################ 103 | 104 | echo "creating vm config" 105 | echo -n "{}" >/tmp/.config 106 | 107 | ############################ 108 | 109 | ############################ 110 | echo "building e2e.test" 111 | # to install the ginkgo binary (required for test build and run) 112 | go install -v github.com/onsi/ginkgo/v2/ginkgo@v2.1.4 113 | ACK_GINKGO_RC=true ginkgo build ./tests/e2e 114 | 115 | ################################# 116 | # download avalanche-network-runner 117 | # https://github.com/ava-labs/avalanche-network-runner 118 | ANR_REPO_PATH=github.com/ava-labs/avalanche-network-runner 119 | ANR_VERSION=$avalanche_network_runner_version 120 | # version set 121 | go install -v ${ANR_REPO_PATH}@${ANR_VERSION} 122 | 123 | ################################# 124 | # run "avalanche-network-runner" server 125 | GOPATH=$(go env GOPATH) 126 | if [[ -z ${GOBIN+x} ]]; then 127 | # no gobin set 128 | BIN=${GOPATH}/bin/avalanche-network-runner 129 | else 130 | # gobin set 131 | BIN=${GOBIN}/avalanche-network-runner 132 | fi 133 | 134 | echo "launch avalanche-network-runner in the background" 135 | $BIN server \ 136 | --log-level debug \ 137 | --port=":12342" \ 138 | --disable-grpc-gateway & 139 | PID=${!} 140 | 141 | ############################ 142 | # By default, it runs all e2e test cases! 143 | # Use "--ginkgo.skip" to skip tests. 144 | # Use "--ginkgo.focus" to select tests. 145 | echo "running e2e tests" 146 | ./tests/e2e/e2e.test \ 147 | --ginkgo.v \ 148 | --network-runner-log-level info \ 149 | --network-runner-grpc-endpoint="0.0.0.0:12342" \ 150 | --avalanchego-path=${AVALANCHEGO_PATH} \ 151 | --avalanchego-plugin-dir=${AVALANCHEGO_PLUGIN_DIR} \ 152 | --vm-genesis-path=/tmp/.genesis \ 153 | --vm-config-path=/tmp/.config \ 154 | --output-path=/tmp/avalanchego-${avalanche_version}/output.yaml \ 155 | --mode=${MODE} 156 | STATUS=$? 157 | 158 | ############################ 159 | if [[ -f "/tmp/avalanchego-${avalanche_version}/output.yaml" ]]; then 160 | echo "cluster is ready!" 161 | cat /tmp/avalanchego-${avalanche_version}/output.yaml 162 | else 163 | echo "cluster is not ready in time... terminating ${PID}" 164 | kill ${PID} 165 | exit 255 166 | fi 167 | 168 | ############################ 169 | if [[ ${MODE} == "test" ]]; then 170 | # "e2e.test" already terminates the cluster for "test" mode 171 | # just in case tests are aborted, manually terminate them again 172 | echo "network-runner RPC server was running on PID ${PID} as test mode; terminating the process..." 173 | pkill -P ${PID} || true 174 | kill -2 ${PID} || true 175 | pkill -9 -f tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH || true # in case pkill didn't work 176 | exit ${STATUS} 177 | else 178 | echo "network-runner RPC server is running on PID ${PID}..." 179 | echo "" 180 | echo "use the following command to terminate:" 181 | echo "" 182 | echo "pkill -P ${PID} && kill -2 ${PID} && pkill -9 -f tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH" 183 | echo "" 184 | fi 185 | -------------------------------------------------------------------------------- /scripts/tests.lint.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | set -o errexit 4 | set -o pipefail 5 | set -e 6 | 7 | if ! [[ "$0" =~ scripts/tests.lint.sh ]]; then 8 | echo "must be run from repository root" 9 | exit 255 10 | fi 11 | 12 | if [ "$#" -eq 0 ]; then 13 | # by default, check all source code 14 | # to test only "mempool" package 15 | # ./scripts/lint.sh ./mempool/... 16 | TARGET="./..." 17 | else 18 | TARGET="${1}" 19 | fi 20 | 21 | # by default, "./scripts/lint.sh" runs all lint tests 22 | # to run only "license_header" test 23 | # TESTS='license_header' ./scripts/lint.sh 24 | TESTS=${TESTS:-"golangci_lint license_header"} 25 | 26 | function test_golangci_lint { 27 | go install -v github.com/golangci/golangci-lint/cmd/golangci-lint@v1.51.2 28 | golangci-lint run --config .golangci.yml 29 | } 30 | 31 | # find_go_files [package] 32 | # all go files except generated ones 33 | function find_go_files { 34 | local target="${1}" 35 | go fmt -n "${target}" | grep -Eo "([^ ]*)$" | grep -vE "(\\.pb\\.go|\\.pb\\.gw.go)" 36 | } 37 | 38 | # automatically checks license headers 39 | # to modify the file headers (if missing), remove "--check" flag 40 | # TESTS='license_header' ADDLICENSE_FLAGS="-v" ./scripts/lint.sh 41 | _addlicense_flags=${ADDLICENSE_FLAGS:-"--check -v"} 42 | function test_license_header { 43 | go install -v github.com/google/addlicense@latest 44 | local target="${1}" 45 | local files=() 46 | while IFS= read -r line; do files+=("$line"); done < <(find_go_files "${target}") 47 | 48 | addlicense \ 49 | -f ./LICENSE.header \ 50 | ${_addlicense_flags} \ 51 | "${files[@]}" 52 | } 53 | 54 | function run { 55 | local test="${1}" 56 | shift 1 57 | echo "START: '${test}' at $(date)" 58 | if "test_${test}" "$@"; then 59 | echo "SUCCESS: '${test}' completed at $(date)" 60 | else 61 | echo "FAIL: '${test}' failed at $(date)" 62 | exit 255 63 | fi 64 | } 65 | 66 | echo "Running '$TESTS' at: $(date)" 67 | for test in $TESTS; do 68 | run "${test}" "${TARGET}" 69 | done 70 | 71 | echo "ALL SUCCESS!" 72 | -------------------------------------------------------------------------------- /scripts/tests.load.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | # (c) 2019-2023, Ava Labs, Inc. All rights reserved. 3 | # See the file LICENSE for licensing terms. 4 | 5 | set -e 6 | 7 | # Set the CGO flags to use the portable version of BLST 8 | # 9 | # We use "export" here instead of just setting a bash variable because we need 10 | # to pass this flag to all child processes spawned by the shell. 11 | export CGO_CFLAGS="-O -D__BLST_PORTABLE__" 12 | 13 | # Ensure we are in the right location 14 | if ! [[ "$0" =~ scripts/tests.load.sh ]]; then 15 | echo "must be run from repository root" 16 | exit 255 17 | fi 18 | 19 | # TimestampVM root directory 20 | TIMESTAMPVM_PATH=$( 21 | cd "$(dirname "${BASH_SOURCE[0]}")" 22 | cd .. && pwd 23 | ) 24 | TERMINAL_HEIGHT=${TERMINAL_HEIGHT:-'1000000'} 25 | 26 | # Load the versions 27 | source "$TIMESTAMPVM_PATH"/scripts/versions.sh 28 | 29 | # PWD is used in the avalanchego build script so we use a different var 30 | PPWD=$(pwd) 31 | ############################ 32 | echo "building avalanchego" 33 | ROOT_PATH=/tmp/timestampvm-load 34 | rm -rf ${ROOT_PATH} 35 | mkdir ${ROOT_PATH} 36 | cd ${ROOT_PATH} 37 | git clone https://github.com/ava-labs/avalanchego.git 38 | cd avalanchego 39 | git checkout ${avalanche_version} 40 | # We build AvalancheGo manually instead of downloading binaries 41 | # because the machine code will be better optimized for the local environment 42 | # 43 | # For example, using the pre-built binary on Apple Silicon is about ~40% slower 44 | # because it requires Rosetta 2 emulation. 45 | ./scripts/build.sh 46 | cd ${PPWD} 47 | 48 | ############################ 49 | echo "building timestampvm" 50 | BUILD_PATH=${ROOT_PATH}/avalanchego/build 51 | PLUGINS_PATH=${BUILD_PATH}/plugins 52 | 53 | # previous binary already deleted in last build phase 54 | go build \ 55 | -o ${PLUGINS_PATH}/tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH \ 56 | ./main/ 57 | 58 | ############################ 59 | echo "creating genesis file" 60 | echo -n "e2e" >${ROOT_PATH}/.genesis 61 | 62 | ############################ 63 | echo "creating vm config" 64 | echo -n "{}" >${ROOT_PATH}/.config 65 | 66 | ############################ 67 | echo "creating subnet config" 68 | rm -f /tmp/.subnet 69 | cat <${ROOT_PATH}/.subnet 70 | { 71 | "proposerMinBlockDelay":0 72 | } 73 | EOF 74 | 75 | ############################ 76 | echo "building load.test" 77 | # to install the ginkgo binary (required for test build and run) 78 | go install -v github.com/onsi/ginkgo/v2/ginkgo@v2.1.4 79 | ACK_GINKGO_RC=true ginkgo build ./tests/load 80 | 81 | ################################# 82 | # download avalanche-network-runner 83 | # https://github.com/ava-labs/avalanche-network-runner 84 | ANR_REPO_PATH=github.com/ava-labs/avalanche-network-runner 85 | ANR_VERSION=$avalanche_network_runner_version 86 | # version set 87 | go install -v ${ANR_REPO_PATH}@${ANR_VERSION} 88 | 89 | ################################# 90 | # run "avalanche-network-runner" server 91 | GOPATH=$(go env GOPATH) 92 | if [[ -z ${GOBIN+x} ]]; then 93 | # no gobin set 94 | BIN=${GOPATH}/bin/avalanche-network-runner 95 | else 96 | # gobin set 97 | BIN=${GOBIN}/avalanche-network-runner 98 | fi 99 | 100 | echo "launch avalanche-network-runner in the background" 101 | $BIN server \ 102 | --log-level warn \ 103 | --port=":12342" \ 104 | --disable-grpc-gateway & 105 | PID=${!} 106 | 107 | ############################ 108 | # By default, it runs all e2e test cases! 109 | # Use "--ginkgo.skip" to skip tests. 110 | # Use "--ginkgo.focus" to select tests. 111 | echo "running load tests" 112 | ./tests/load/load.test \ 113 | --ginkgo.v \ 114 | --network-runner-log-level warn \ 115 | --network-runner-grpc-endpoint="0.0.0.0:12342" \ 116 | --avalanchego-path=${BUILD_PATH}/avalanchego \ 117 | --avalanchego-plugin-dir=${PLUGINS_PATH} \ 118 | --vm-genesis-path=${ROOT_PATH}/.genesis \ 119 | --vm-config-path=${ROOT_PATH}/.config \ 120 | --subnet-config-path=${ROOT_PATH}/.subnet \ 121 | --terminal-height=${TERMINAL_HEIGHT} 122 | 123 | ############################ 124 | # load.test" already terminates the cluster 125 | # just in case load tests are aborted, manually terminate them again 126 | echo "network-runner RPC server was running on PID ${PID} as test mode; terminating the process..." 127 | pkill -P ${PID} || true 128 | kill -2 ${PID} || true 129 | pkill -9 -f tGas3T58KzdjLHhBDMnH2TvrddhqTji5iZAMZ3RXs2NLpSnhH || true # in case pkill didn't work 130 | -------------------------------------------------------------------------------- /scripts/versions.sh: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env bash 2 | 3 | # Don't export them as they're used in the context of other calls 4 | avalanche_version=${AVALANCHE_VERSION:-'v1.10.2'} 5 | 6 | avalanche_network_runner_version=${AVALANCHE_NETWORK_RUNNER_VERSION:-'v1.6.0'} 7 | -------------------------------------------------------------------------------- /tests/e2e/e2e_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // e2e implements the e2e tests. 5 | package e2e_test 6 | 7 | import ( 8 | "context" 9 | "flag" 10 | "fmt" 11 | "os" 12 | "testing" 13 | "time" 14 | 15 | runner_sdk "github.com/ava-labs/avalanche-network-runner/client" 16 | "github.com/ava-labs/avalanche-network-runner/rpcpb" 17 | "github.com/ava-labs/avalanchego/ids" 18 | "github.com/ava-labs/avalanchego/utils/hashing" 19 | "github.com/ava-labs/avalanchego/utils/logging" 20 | "github.com/ava-labs/timestampvm/client" 21 | "github.com/ava-labs/timestampvm/timestampvm" 22 | log "github.com/inconshreveable/log15" 23 | ginkgo "github.com/onsi/ginkgo/v2" 24 | "github.com/onsi/ginkgo/v2/formatter" 25 | "github.com/onsi/gomega" 26 | "sigs.k8s.io/yaml" 27 | ) 28 | 29 | func TestE2e(t *testing.T) { 30 | gomega.RegisterFailHandler(ginkgo.Fail) 31 | ginkgo.RunSpecs(t, "timestampvm e2e test suites") 32 | } 33 | 34 | var ( 35 | requestTimeout time.Duration 36 | 37 | networkRunnerLogLevel string 38 | gRPCEp string 39 | gRPCGatewayEp string 40 | 41 | execPath string 42 | pluginDir string 43 | 44 | vmGenesisPath string 45 | vmConfigPath string 46 | outputPath string 47 | 48 | mode string 49 | ) 50 | 51 | func init() { 52 | flag.DurationVar( 53 | &requestTimeout, 54 | "request-timeout", 55 | 120*time.Second, 56 | "timeout for transaction issuance and confirmation", 57 | ) 58 | 59 | flag.StringVar( 60 | &networkRunnerLogLevel, 61 | "network-runner-log-level", 62 | "info", 63 | "gRPC server endpoint", 64 | ) 65 | 66 | flag.StringVar( 67 | &gRPCEp, 68 | "network-runner-grpc-endpoint", 69 | "0.0.0.0:8080", 70 | "gRPC server endpoint", 71 | ) 72 | flag.StringVar( 73 | &gRPCGatewayEp, 74 | "network-runner-grpc-gateway-endpoint", 75 | "0.0.0.0:8081", 76 | "gRPC gateway endpoint", 77 | ) 78 | 79 | flag.StringVar( 80 | &execPath, 81 | "avalanchego-path", 82 | "", 83 | "avalanchego executable path", 84 | ) 85 | 86 | flag.StringVar( 87 | &pluginDir, 88 | "avalanchego-plugin-dir", 89 | "", 90 | "avalanchego plugin directory", 91 | ) 92 | 93 | flag.StringVar( 94 | &vmGenesisPath, 95 | "vm-genesis-path", 96 | "", 97 | "VM genesis file path", 98 | ) 99 | 100 | flag.StringVar( 101 | &vmConfigPath, 102 | "vm-config-path", 103 | "", 104 | "VM configfile path", 105 | ) 106 | 107 | flag.StringVar( 108 | &outputPath, 109 | "output-path", 110 | "", 111 | "output YAML path to write local cluster information", 112 | ) 113 | 114 | flag.StringVar( 115 | &mode, 116 | "mode", 117 | "test", 118 | "'test' to shut down cluster after tests, 'run' to skip tests and only run without shutdown", 119 | ) 120 | } 121 | 122 | const vmName = "timestamp" 123 | 124 | var vmID ids.ID 125 | 126 | func init() { 127 | // TODO: add "getVMID" util function in avalanchego and import from "avalanchego" 128 | b := make([]byte, 32) 129 | copy(b, []byte(vmName)) 130 | var err error 131 | vmID, err = ids.ToID(b) 132 | if err != nil { 133 | panic(err) 134 | } 135 | } 136 | 137 | const ( 138 | modeTest = "test" 139 | modeRun = "run" 140 | ) 141 | 142 | var ( 143 | cli runner_sdk.Client 144 | timestampvmRPCEps []string 145 | ) 146 | 147 | var _ = ginkgo.BeforeSuite(func() { 148 | gomega.Expect(mode).Should(gomega.Or(gomega.Equal("test"), gomega.Equal("run"))) 149 | logLevel, err := logging.ToLevel(networkRunnerLogLevel) 150 | gomega.Expect(err).Should(gomega.BeNil()) 151 | logFactory := logging.NewFactory(logging.Config{ 152 | DisplayLevel: logLevel, 153 | LogLevel: logLevel, 154 | }) 155 | log, err := logFactory.Make("main") 156 | gomega.Expect(err).Should(gomega.BeNil()) 157 | 158 | cli, err = runner_sdk.New(runner_sdk.Config{ 159 | Endpoint: gRPCEp, 160 | DialTimeout: 10 * time.Second, 161 | }, log) 162 | gomega.Expect(err).Should(gomega.BeNil()) 163 | 164 | ginkgo.By("calling start API via network runner", func() { 165 | outf("{{green}}sending 'start' with binary path:{{/}} %q (%q)\n", execPath, vmID) 166 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 167 | resp, err := cli.Start( 168 | ctx, 169 | execPath, 170 | runner_sdk.WithPluginDir(pluginDir), 171 | runner_sdk.WithBlockchainSpecs( 172 | []*rpcpb.BlockchainSpec{ 173 | { 174 | VmName: vmName, 175 | Genesis: vmGenesisPath, 176 | ChainConfig: vmConfigPath, 177 | }, 178 | }, 179 | ), 180 | // Disable all rate limiting 181 | runner_sdk.WithGlobalNodeConfig(`{ 182 | "log-level":"debug", 183 | "throttler-inbound-validator-alloc-size":"107374182", 184 | "throttler-inbound-node-max-processing-msgs":"100000", 185 | "throttler-inbound-bandwidth-refill-rate":"1073741824", 186 | "throttler-inbound-bandwidth-max-burst-size":"1073741824", 187 | "throttler-inbound-cpu-validator-alloc":"100000", 188 | "throttler-inbound-disk-validator-alloc":"10737418240000", 189 | "throttler-outbound-validator-alloc-size":"107374182" 190 | }`), 191 | ) 192 | cancel() 193 | gomega.Expect(err).Should(gomega.BeNil()) 194 | outf("{{green}}successfully started:{{/}} %+v\n", resp.ClusterInfo.NodeNames) 195 | }) 196 | 197 | // TODO: network runner health should imply custom VM healthiness 198 | // or provide a separate API for custom VM healthiness 199 | // "start" is async, so wait some time for cluster health 200 | outf("\n{{magenta}}waiting for all vms to report healthy...{{/}}: %s\n", vmID) 201 | for { 202 | _, err = cli.Health(context.Background()) 203 | if err != nil { 204 | time.Sleep(1 * time.Second) 205 | continue 206 | } 207 | // TODO: clean this up 208 | gomega.Expect(err).Should(gomega.BeNil()) 209 | break 210 | } 211 | 212 | timestampvmRPCEps = make([]string, 0) 213 | blockchainID, logsDir := "", "" 214 | 215 | // wait up to 5-minute for custom VM installation 216 | outf("\n{{magenta}}waiting for all custom VMs to report healthy...{{/}}\n") 217 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 218 | done: 219 | for ctx.Err() == nil { 220 | select { 221 | case <-ctx.Done(): 222 | break done 223 | case <-time.After(5 * time.Second): 224 | } 225 | 226 | cctx, ccancel := context.WithTimeout(context.Background(), 2*time.Minute) 227 | resp, err := cli.Status(cctx) 228 | ccancel() 229 | gomega.Expect(err).Should(gomega.BeNil()) 230 | 231 | // all logs are stored under root data dir 232 | logsDir = resp.GetClusterInfo().GetRootDataDir() 233 | 234 | for _, v := range resp.ClusterInfo.CustomChains { 235 | if v.VmId == vmID.String() { 236 | blockchainID = v.ChainId 237 | outf("{{blue}}timestampvm is ready:{{/}} %+v\n", v) 238 | break done 239 | } 240 | } 241 | } 242 | gomega.Expect(ctx.Err()).Should(gomega.BeNil()) 243 | cancel() 244 | 245 | gomega.Expect(blockchainID).Should(gomega.Not(gomega.BeEmpty())) 246 | gomega.Expect(logsDir).Should(gomega.Not(gomega.BeEmpty())) 247 | 248 | cctx, ccancel := context.WithTimeout(context.Background(), 2*time.Minute) 249 | uris, err := cli.URIs(cctx) 250 | ccancel() 251 | gomega.Expect(err).Should(gomega.BeNil()) 252 | outf("{{blue}}avalanche HTTP RPCs URIs:{{/}} %q\n", uris) 253 | 254 | for _, u := range uris { 255 | rpcEP := fmt.Sprintf("%s/ext/bc/%s/rpc", u, blockchainID) 256 | timestampvmRPCEps = append(timestampvmRPCEps, rpcEP) 257 | outf("{{blue}}avalanche timestampvm RPC:{{/}} %q\n", rpcEP) 258 | } 259 | 260 | pid := os.Getpid() 261 | outf("{{blue}}{{bold}}writing output %q with PID %d{{/}}\n", outputPath, pid) 262 | ci := clusterInfo{ 263 | URIs: uris, 264 | Endpoint: fmt.Sprintf("/ext/bc/%s", blockchainID), 265 | PID: pid, 266 | LogsDir: logsDir, 267 | } 268 | gomega.Expect(ci.Save(outputPath)).Should(gomega.BeNil()) 269 | 270 | b, err := os.ReadFile(outputPath) 271 | gomega.Expect(err).Should(gomega.BeNil()) 272 | outf("\n{{blue}}$ cat %s:{{/}}\n%s\n", outputPath, string(b)) 273 | 274 | instances = make([]instance, len(uris)) 275 | for i := range uris { 276 | u := uris[i] + fmt.Sprintf("/ext/bc/%s", blockchainID) 277 | instances[i] = instance{ 278 | uri: u, 279 | cli: client.New(u), 280 | } 281 | } 282 | }) 283 | 284 | var instances []instance 285 | 286 | type instance struct { 287 | uri string 288 | cli client.Client 289 | } 290 | 291 | var _ = ginkgo.AfterSuite(func() { 292 | switch mode { 293 | case modeTest: 294 | outf("{{red}}shutting down cluster{{/}}\n") 295 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) 296 | _, err := cli.Stop(ctx) 297 | cancel() 298 | gomega.Expect(err).Should(gomega.BeNil()) 299 | log.Warn("cluster shutdown result", "err", err) 300 | 301 | case modeRun: 302 | outf("{{yellow}}skipping shutting down cluster{{/}}\n") 303 | } 304 | 305 | outf("{{red}}shutting down client{{/}}\n") 306 | err := cli.Close() 307 | gomega.Expect(err).Should(gomega.BeNil()) 308 | log.Warn("client shutdown result", "err", err) 309 | }) 310 | 311 | var _ = ginkgo.Describe("[ProposeBlock]", func() { 312 | var gid ids.ID 313 | ginkgo.It("get genesis block", func() { 314 | for _, inst := range instances { 315 | cli := inst.cli 316 | timestamp, data, height, id, _, err := cli.GetBlock(context.Background(), nil) 317 | gid = id 318 | gomega.Ω(timestamp).Should(gomega.Equal(uint64(0))) 319 | gomega.Ω(data).Should(gomega.Equal(timestampvm.BytesToData([]byte("e2e")))) 320 | gomega.Ω(height).Should(gomega.Equal(uint64(0))) 321 | gomega.Ω(err).Should(gomega.BeNil()) 322 | } 323 | }) 324 | 325 | switch mode { 326 | case modeRun: 327 | outf("{{yellow}}skipping ProposeBlock tests{{/}}\n") 328 | return 329 | } 330 | 331 | data := timestampvm.BytesToData(hashing.ComputeHash256([]byte("test"))) 332 | now := time.Now().Unix() 333 | ginkgo.It("create new block", func() { 334 | cli := instances[0].cli 335 | success, err := cli.ProposeBlock(context.Background(), data) 336 | gomega.Ω(success).Should(gomega.BeTrue()) 337 | gomega.Ω(err).Should(gomega.BeNil()) 338 | }) 339 | 340 | ginkgo.It("confirm block processed on all nodes", func() { 341 | for i, inst := range instances { 342 | cli := inst.cli 343 | for { // Wait for block to be accepted 344 | timestamp, bdata, height, _, pid, err := cli.GetBlock(context.Background(), nil) 345 | if height == 0 { 346 | log.Info("waiting for height to increase", "instance", i) 347 | time.Sleep(1 * time.Second) 348 | continue 349 | } 350 | gomega.Ω(uint64(now)-5 < timestamp).Should(gomega.BeTrue()) 351 | gomega.Ω(bdata).Should(gomega.Equal(data)) 352 | gomega.Ω(height).Should(gomega.Equal(uint64(1))) 353 | gomega.Ω(pid).Should(gomega.Equal(gid)) 354 | gomega.Ω(err).Should(gomega.BeNil()) 355 | log.Info("height increased", "instance", i) 356 | break 357 | } 358 | } 359 | }) 360 | }) 361 | 362 | // Outputs to stdout. 363 | // 364 | // e.g., 365 | // 366 | // Out("{{green}}{{bold}}hi there %q{{/}}", "aa") 367 | // Out("{{magenta}}{{bold}}hi therea{{/}} {{cyan}}{{underline}}b{{/}}") 368 | // 369 | // ref. 370 | // https://github.com/onsi/ginkgo/blob/v2.0.0/formatter/formatter.go#L52-L73 371 | func outf(format string, args ...interface{}) { 372 | s := formatter.F(format, args...) 373 | fmt.Fprint(formatter.ColorableStdOut, s) 374 | } 375 | 376 | // clusterInfo represents the local cluster information. 377 | type clusterInfo struct { 378 | URIs []string `json:"uris"` 379 | Endpoint string `json:"endpoint"` 380 | PID int `json:"pid"` 381 | LogsDir string `json:"logsDir"` 382 | } 383 | 384 | const fsModeWrite = 0o600 385 | 386 | func (ci clusterInfo) Save(p string) error { 387 | ob, err := yaml.Marshal(ci) 388 | if err != nil { 389 | return err 390 | } 391 | return os.WriteFile(p, ob, fsModeWrite) 392 | } 393 | -------------------------------------------------------------------------------- /tests/load/client/client.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package client 5 | 6 | import ( 7 | "context" 8 | 9 | "github.com/ava-labs/avalanchego/ids" 10 | "github.com/ava-labs/avalanchego/utils/formatting" 11 | "github.com/ava-labs/timestampvm/timestampvm" 12 | ) 13 | 14 | // Client defines timestampvm client operations. 15 | type Client interface { 16 | // ProposeBlock submits data for a block 17 | ProposeBlock(ctx context.Context, data [timestampvm.DataLen]byte) (bool, error) 18 | 19 | // GetBlock fetches the contents of a block 20 | GetBlock(ctx context.Context, blockID *ids.ID) (uint64, [timestampvm.DataLen]byte, uint64, ids.ID, ids.ID, error) 21 | } 22 | 23 | // New creates a new client object. 24 | func New(uri string) Client { 25 | req := NewEndpointRequester(uri, "timestampvm") 26 | return &client{req: req} 27 | } 28 | 29 | type client struct { 30 | req *EndpointRequester 31 | } 32 | 33 | func (cli *client) ProposeBlock(ctx context.Context, data [timestampvm.DataLen]byte) (bool, error) { 34 | bytes, err := formatting.Encode(formatting.Hex, data[:]) 35 | if err != nil { 36 | return false, err 37 | } 38 | 39 | resp := new(timestampvm.ProposeBlockReply) 40 | err = cli.req.SendRequest(ctx, 41 | "proposeBlock", 42 | ×tampvm.ProposeBlockArgs{Data: bytes}, 43 | resp, 44 | ) 45 | if err != nil { 46 | return false, err 47 | } 48 | return resp.Success, nil 49 | } 50 | 51 | func (cli *client) GetBlock(ctx context.Context, blockID *ids.ID) (uint64, [timestampvm.DataLen]byte, uint64, ids.ID, ids.ID, error) { 52 | resp := new(timestampvm.GetBlockReply) 53 | err := cli.req.SendRequest(ctx, 54 | "getBlock", 55 | ×tampvm.GetBlockArgs{ID: blockID}, 56 | resp, 57 | ) 58 | if err != nil { 59 | return 0, [timestampvm.DataLen]byte{}, 0, ids.Empty, ids.Empty, err 60 | } 61 | bytes, err := formatting.Decode(formatting.Hex, resp.Data) 62 | if err != nil { 63 | return 0, [timestampvm.DataLen]byte{}, 0, ids.Empty, ids.Empty, err 64 | } 65 | return uint64(resp.Timestamp), timestampvm.BytesToData(bytes), uint64(resp.Height), resp.ID, resp.ParentID, nil 66 | } 67 | -------------------------------------------------------------------------------- /tests/load/client/requester.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2022, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package client 5 | 6 | import ( 7 | "bytes" 8 | "context" 9 | "fmt" 10 | "net/http" 11 | "net/url" 12 | "time" 13 | 14 | rpc "github.com/gorilla/rpc/v2/json2" 15 | ) 16 | 17 | type Option func(*Options) 18 | 19 | type Options struct { 20 | headers http.Header 21 | queryParams url.Values 22 | } 23 | 24 | func NewOptions(ops []Option) *Options { 25 | o := &Options{ 26 | headers: http.Header{}, 27 | queryParams: url.Values{}, 28 | } 29 | o.applyOptions(ops) 30 | return o 31 | } 32 | 33 | func (o *Options) applyOptions(ops []Option) { 34 | for _, op := range ops { 35 | op(o) 36 | } 37 | } 38 | 39 | func (o *Options) Headers() http.Header { 40 | return o.headers 41 | } 42 | 43 | func (o *Options) QueryParams() url.Values { 44 | return o.queryParams 45 | } 46 | 47 | func WithHeader(key, val string) Option { 48 | return func(o *Options) { 49 | o.headers.Set(key, val) 50 | } 51 | } 52 | 53 | func WithQueryParam(key, val string) Option { 54 | return func(o *Options) { 55 | o.queryParams.Set(key, val) 56 | } 57 | } 58 | 59 | // EndpointRequester is an extension of AvalancheGo's [EndpointRequester] with 60 | // [http.Client] reuse. 61 | type EndpointRequester struct { 62 | cli *http.Client 63 | uri, base string 64 | } 65 | 66 | func NewEndpointRequester(uri, base string) *EndpointRequester { 67 | t := http.DefaultTransport.(*http.Transport).Clone() 68 | t.MaxIdleConns = 100_000 69 | t.MaxConnsPerHost = 100_000 70 | t.MaxIdleConnsPerHost = 100_000 71 | 72 | return &EndpointRequester{ 73 | cli: &http.Client{ 74 | Timeout: 3600 * time.Second, // allow waiting requests 75 | Transport: t, 76 | }, 77 | uri: uri, 78 | base: base, 79 | } 80 | } 81 | 82 | func (e *EndpointRequester) SendRequest( 83 | ctx context.Context, 84 | method string, 85 | params interface{}, 86 | reply interface{}, 87 | options ...Option, 88 | ) error { 89 | uri, err := url.Parse(e.uri) 90 | if err != nil { 91 | return err 92 | } 93 | return SendJSONRequest( 94 | ctx, 95 | e.cli, 96 | uri, 97 | fmt.Sprintf("%s.%s", e.base, method), 98 | params, 99 | reply, 100 | options..., 101 | ) 102 | } 103 | 104 | func SendJSONRequest( 105 | ctx context.Context, 106 | cli *http.Client, 107 | uri *url.URL, 108 | method string, 109 | params interface{}, 110 | reply interface{}, 111 | options ...Option, 112 | ) error { 113 | requestBodyBytes, err := rpc.EncodeClientRequest(method, params) 114 | if err != nil { 115 | return fmt.Errorf("failed to encode client params: %w", err) 116 | } 117 | 118 | ops := NewOptions(options) 119 | uri.RawQuery = ops.queryParams.Encode() 120 | 121 | request, err := http.NewRequestWithContext( 122 | ctx, 123 | "POST", 124 | uri.String(), 125 | bytes.NewBuffer(requestBodyBytes), 126 | ) 127 | if err != nil { 128 | return fmt.Errorf("failed to create request: %w", err) 129 | } 130 | 131 | request.Header = ops.headers 132 | request.Header.Set("Content-Type", "application/json") 133 | 134 | resp, err := cli.Do(request) 135 | if err != nil { 136 | return fmt.Errorf("failed to issue request: %w", err) 137 | } 138 | 139 | // Return an error for any non successful status code 140 | if resp.StatusCode < 200 || resp.StatusCode > 299 { 141 | // Drop any error during close to report the original error 142 | _ = resp.Body.Close() 143 | return fmt.Errorf("received status code: %d", resp.StatusCode) 144 | } 145 | 146 | if err := rpc.DecodeClientResponse(resp.Body, reply); err != nil { 147 | // Drop any error during close to report the original error 148 | _ = resp.Body.Close() 149 | return fmt.Errorf("failed to decode client response: %w", err) 150 | } 151 | return resp.Body.Close() 152 | } 153 | -------------------------------------------------------------------------------- /tests/load/load.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // load implements the load tests. 5 | package load_test 6 | 7 | import ( 8 | "context" 9 | "crypto/rand" 10 | "fmt" 11 | "time" 12 | 13 | "github.com/ava-labs/timestampvm/tests/load/client" 14 | "github.com/ava-labs/timestampvm/timestampvm" 15 | log "github.com/inconshreveable/log15" 16 | "golang.org/x/sync/errgroup" 17 | ) 18 | 19 | const ( 20 | backoffDur = 50 * time.Millisecond 21 | maxBackoff = 5 * time.Second 22 | ) 23 | 24 | var _ Worker = (*timestampvmLoadWorker)(nil) 25 | 26 | type Worker interface { 27 | // Name returns the name of the worker 28 | Name() string 29 | // AddLoad adds load to the underlying network of the worker. 30 | // Terminates without an error if the quit channel is closed. 31 | AddLoad(ctx context.Context, quit <-chan struct{}) error 32 | // GetLastAcceptedHeight returns the height of the last accepted block. 33 | GetLastAcceptedHeight(ctx context.Context) (uint64, error) 34 | } 35 | 36 | // timestampvmLoadWorker implements the LoadWorker interface and adds continuous load through its client. 37 | type timestampvmLoadWorker struct { 38 | uri string 39 | client.Client 40 | } 41 | 42 | func newLoadWorkers(uris []string, blockchainID string) []Worker { 43 | workers := make([]Worker, 0, len(uris)) 44 | for _, uri := range uris { 45 | workers = append(workers, newLoadWorker(uri, blockchainID)) 46 | } 47 | 48 | return workers 49 | } 50 | 51 | func newLoadWorker(uri string, blockchainID string) *timestampvmLoadWorker { 52 | uri = fmt.Sprintf("%s/ext/bc/%s", uri, blockchainID) 53 | return ×tampvmLoadWorker{ 54 | uri: uri, 55 | Client: client.New(uri), 56 | } 57 | } 58 | 59 | func (t *timestampvmLoadWorker) Name() string { 60 | return fmt.Sprintf("TimestampVM RPC Worker %s", t.uri) 61 | } 62 | 63 | func (t *timestampvmLoadWorker) AddLoad(ctx context.Context, quit <-chan struct{}) error { 64 | delay := time.Duration(0) 65 | for { 66 | select { 67 | case <-quit: 68 | return nil 69 | case <-ctx.Done(): 70 | return fmt.Errorf("%s finished: %w", t.Name(), ctx.Err()) 71 | default: 72 | } 73 | 74 | data := [timestampvm.DataLen]byte{} 75 | _, err := rand.Read(data[:]) 76 | if err != nil { 77 | return fmt.Errorf("failed to read random data: %w", err) 78 | } 79 | success, err := t.ProposeBlock(ctx, data) 80 | if err != nil { 81 | return fmt.Errorf("%s failed: %w", t.Name(), err) 82 | } 83 | if success && delay > 0 { 84 | delay -= backoffDur 85 | } else if !success && delay < maxBackoff { 86 | // If the mempool is full, pause before submitting more data 87 | // 88 | // TODO: in a robust testing scenario, we'd want to resubmit this 89 | // data to avoid loss 90 | delay += backoffDur 91 | } 92 | time.Sleep(delay) 93 | } 94 | } 95 | 96 | // GetLastAcceptedHeight returns the height of the last accepted block according to the worker 97 | func (t *timestampvmLoadWorker) GetLastAcceptedHeight(ctx context.Context) (uint64, error) { 98 | _, _, lastHeight, _, _, err := t.GetBlock(ctx, nil) 99 | return lastHeight, err 100 | } 101 | 102 | // RunLoadTest runs a load test on the workers 103 | // Assumes it's safe to call a worker in parallel 104 | func RunLoadTest(ctx context.Context, workers []Worker, terminalHeight uint64, maxDuration time.Duration) error { 105 | if len(workers) == 0 { 106 | return fmt.Errorf("cannot run a load test with %d workers", len(workers)) 107 | } 108 | 109 | var ( 110 | quit = make(chan struct{}) 111 | cancel context.CancelFunc 112 | start = time.Now() 113 | worker = workers[0] 114 | startHeight = uint64(0) 115 | err error 116 | ) 117 | 118 | for { 119 | startHeight, err = worker.GetLastAcceptedHeight(ctx) 120 | if err != nil { 121 | log.Warn("Failed to get last accepted height", "err", err, "worker", worker.Name()) 122 | time.Sleep(3 * time.Second) 123 | continue 124 | } 125 | break 126 | } 127 | log.Info("Running load test", "numWorkers", len(workers), "terminalHeight", terminalHeight, "maxDuration", maxDuration) 128 | 129 | if maxDuration != 0 { 130 | ctx, cancel = context.WithTimeout(ctx, maxDuration) 131 | defer cancel() 132 | } 133 | 134 | eg, egCtx := errgroup.WithContext(ctx) 135 | for _, worker := range workers { 136 | worker := worker 137 | eg.Go(func() error { 138 | if err := worker.AddLoad(egCtx, quit); err != nil { 139 | return fmt.Errorf("worker %q failed while adding load: %w", worker.Name(), err) 140 | } 141 | 142 | return nil 143 | }) 144 | } 145 | 146 | eg.Go(func() error { 147 | last := startHeight 148 | for egCtx.Err() == nil { 149 | lastHeight, err := worker.GetLastAcceptedHeight(egCtx) 150 | if err != nil { 151 | continue 152 | } 153 | log.Info("Stats", "height", lastHeight, 154 | "avg bps", float64(lastHeight)/time.Since(start).Seconds(), 155 | "last bps", float64(lastHeight-last)/3.0, 156 | ) 157 | if lastHeight > terminalHeight { 158 | log.Info("exiting at terminal height") 159 | close(quit) 160 | return nil 161 | } 162 | last = lastHeight 163 | time.Sleep(3 * time.Second) 164 | } 165 | 166 | return fmt.Errorf("failed to reach terminal height: %w", egCtx.Err()) 167 | }) 168 | 169 | return eg.Wait() 170 | } 171 | -------------------------------------------------------------------------------- /tests/load/load_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | // load implements the load tests. 5 | package load_test 6 | 7 | import ( 8 | "context" 9 | "flag" 10 | "fmt" 11 | "strings" 12 | "testing" 13 | "time" 14 | 15 | runner_sdk "github.com/ava-labs/avalanche-network-runner/client" 16 | "github.com/ava-labs/avalanche-network-runner/rpcpb" 17 | "github.com/ava-labs/avalanchego/ids" 18 | "github.com/ava-labs/avalanchego/utils/logging" 19 | log "github.com/inconshreveable/log15" 20 | ginkgo "github.com/onsi/ginkgo/v2" 21 | "github.com/onsi/ginkgo/v2/formatter" 22 | "github.com/onsi/gomega" 23 | ) 24 | 25 | func TestLoad(t *testing.T) { 26 | gomega.RegisterFailHandler(ginkgo.Fail) 27 | ginkgo.RunSpecs(t, "timestampvm load test suites") 28 | } 29 | 30 | var ( 31 | requestTimeout time.Duration 32 | 33 | networkRunnerLogLevel string 34 | gRPCEp string 35 | gRPCGatewayEp string 36 | 37 | execPath string 38 | pluginDir string 39 | 40 | vmGenesisPath string 41 | vmConfigPath string 42 | subnetConfigPath string 43 | 44 | // Comma separated list of client URIs 45 | // If the length is non-zero, this will skip using the network runner to start and stop a network. 46 | commaSeparatedClientURIs string 47 | // Specifies the full timestampvm client URIs to use for load test. 48 | // Populated in BeforeSuite 49 | clientURIs []string 50 | 51 | terminalHeight uint64 52 | 53 | blockchainID string 54 | logsDir string 55 | ) 56 | 57 | func init() { 58 | flag.DurationVar( 59 | &requestTimeout, 60 | "request-timeout", 61 | 120*time.Second, 62 | "timeout for transaction issuance and confirmation", 63 | ) 64 | 65 | flag.StringVar( 66 | &networkRunnerLogLevel, 67 | "network-runner-log-level", 68 | "info", 69 | "gRPC server endpoint", 70 | ) 71 | 72 | flag.StringVar( 73 | &gRPCEp, 74 | "network-runner-grpc-endpoint", 75 | "0.0.0.0:8080", 76 | "gRPC server endpoint", 77 | ) 78 | flag.StringVar( 79 | &gRPCGatewayEp, 80 | "network-runner-grpc-gateway-endpoint", 81 | "0.0.0.0:8081", 82 | "gRPC gateway endpoint", 83 | ) 84 | 85 | flag.StringVar( 86 | &execPath, 87 | "avalanchego-path", 88 | "", 89 | "avalanchego executable path", 90 | ) 91 | 92 | flag.StringVar( 93 | &pluginDir, 94 | "avalanchego-plugin-dir", 95 | "", 96 | "avalanchego plugin directory", 97 | ) 98 | 99 | flag.StringVar( 100 | &vmGenesisPath, 101 | "vm-genesis-path", 102 | "", 103 | "VM genesis file path", 104 | ) 105 | 106 | flag.StringVar( 107 | &vmConfigPath, 108 | "vm-config-path", 109 | "", 110 | "VM configfile path", 111 | ) 112 | 113 | flag.StringVar( 114 | &subnetConfigPath, 115 | "subnet-config-path", 116 | "", 117 | "Subnet configfile path", 118 | ) 119 | 120 | flag.Uint64Var( 121 | &terminalHeight, 122 | "terminal-height", 123 | 1_000_000, 124 | "height to quit at", 125 | ) 126 | 127 | flag.StringVar( 128 | &commaSeparatedClientURIs, 129 | "client-uris", 130 | "", 131 | "Specifies a comma separated list of full timestampvm client URIs to use in place of orchestrating a network. (Ex. 127.0.0.1:9650/ext/bc/q2aTwKuyzgs8pynF7UXBZCU7DejbZbZ6EUyHr3JQzYgwNPUPi/rpc,127.0.0.1:9652/ext/bc/q2aTwKuyzgs8pynF7UXBZCU7DejbZbZ6EUyHr3JQzYgwNPUPi/rpc", 132 | ) 133 | } 134 | 135 | const vmName = "timestamp" 136 | 137 | var vmID ids.ID 138 | 139 | func init() { 140 | // TODO: add "getVMID" util function in avalanchego and import from "avalanchego" 141 | b := make([]byte, 32) 142 | copy(b, []byte(vmName)) 143 | var err error 144 | vmID, err = ids.ToID(b) 145 | if err != nil { 146 | panic(err) 147 | } 148 | } 149 | 150 | var ( 151 | cli runner_sdk.Client 152 | timestampvmRPCEps []string 153 | ) 154 | 155 | var _ = ginkgo.BeforeSuite(func() { 156 | if len(commaSeparatedClientURIs) != 0 { 157 | clientURIs = strings.Split(commaSeparatedClientURIs, ",") 158 | outf("{{green}}creating %d clients from manually specified URIs:{{/}}\n", len(clientURIs)) 159 | return 160 | } 161 | 162 | logLevel, err := logging.ToLevel(networkRunnerLogLevel) 163 | gomega.Expect(err).Should(gomega.BeNil()) 164 | logFactory := logging.NewFactory(logging.Config{ 165 | DisplayLevel: logLevel, 166 | LogLevel: logLevel, 167 | }) 168 | log, err := logFactory.Make("main") 169 | gomega.Expect(err).Should(gomega.BeNil()) 170 | 171 | cli, err = runner_sdk.New(runner_sdk.Config{ 172 | Endpoint: gRPCEp, 173 | DialTimeout: 10 * time.Second, 174 | }, log) 175 | gomega.Expect(err).Should(gomega.BeNil()) 176 | 177 | ginkgo.By("calling start API via network runner", func() { 178 | outf("{{green}}sending 'start' with binary path:{{/}} %q (%q)\n", execPath, vmID) 179 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 180 | resp, err := cli.Start( 181 | ctx, 182 | execPath, 183 | runner_sdk.WithPluginDir(pluginDir), 184 | runner_sdk.WithBlockchainSpecs( 185 | []*rpcpb.BlockchainSpec{ 186 | { 187 | VmName: vmName, 188 | Genesis: vmGenesisPath, 189 | ChainConfig: vmConfigPath, 190 | SubnetSpec: &rpcpb.SubnetSpec{ 191 | SubnetConfig: subnetConfigPath, 192 | }, 193 | }, 194 | }, 195 | ), 196 | // Disable all rate limiting 197 | runner_sdk.WithGlobalNodeConfig(`{ 198 | "log-level":"warn", 199 | "proposervm-use-current-height":true, 200 | "throttler-inbound-validator-alloc-size":"107374182", 201 | "throttler-inbound-node-max-processing-msgs":"100000", 202 | "throttler-inbound-bandwidth-refill-rate":"1073741824", 203 | "throttler-inbound-bandwidth-max-burst-size":"1073741824", 204 | "throttler-inbound-cpu-validator-alloc":"100000", 205 | "throttler-inbound-disk-validator-alloc":"10737418240000", 206 | "throttler-outbound-validator-alloc-size":"107374182" 207 | }`), 208 | ) 209 | cancel() 210 | gomega.Expect(err).Should(gomega.BeNil()) 211 | outf("{{green}}successfully started:{{/}} %+v\n", resp.ClusterInfo.NodeNames) 212 | }) 213 | 214 | // TODO: network runner health should imply custom VM healthiness 215 | // or provide a separate API for custom VM healthiness 216 | // "start" is async, so wait some time for cluster health 217 | outf("\n{{magenta}}waiting for all vms to report healthy...{{/}}: %s\n", vmID) 218 | for { 219 | healthRes, err := cli.Health(context.Background()) 220 | if err != nil || !healthRes.ClusterInfo.Healthy { 221 | time.Sleep(1 * time.Second) 222 | continue 223 | } 224 | break 225 | } 226 | 227 | timestampvmRPCEps = make([]string, 0) 228 | 229 | // wait up to 5-minute for custom VM installation 230 | outf("\n{{magenta}}waiting for all custom VMs to report healthy...{{/}}\n") 231 | ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute) 232 | done: 233 | for ctx.Err() == nil { 234 | select { 235 | case <-ctx.Done(): 236 | break done 237 | case <-time.After(5 * time.Second): 238 | } 239 | 240 | cctx, ccancel := context.WithTimeout(context.Background(), 2*time.Minute) 241 | resp, err := cli.Status(cctx) 242 | ccancel() 243 | gomega.Expect(err).Should(gomega.BeNil()) 244 | 245 | // all logs are stored under root data dir 246 | logsDir = resp.GetClusterInfo().GetRootDataDir() 247 | 248 | for _, v := range resp.ClusterInfo.CustomChains { 249 | if v.VmId == vmID.String() { 250 | blockchainID = v.ChainId 251 | outf("{{blue}}timestampvm is ready:{{/}} %+v\n", v) 252 | break done 253 | } 254 | } 255 | } 256 | gomega.Expect(ctx.Err()).Should(gomega.BeNil()) 257 | cancel() 258 | 259 | gomega.Expect(blockchainID).Should(gomega.Not(gomega.BeEmpty())) 260 | gomega.Expect(logsDir).Should(gomega.Not(gomega.BeEmpty())) 261 | 262 | cctx, ccancel := context.WithTimeout(context.Background(), 2*time.Minute) 263 | uris, err := cli.URIs(cctx) 264 | ccancel() 265 | gomega.Expect(err).Should(gomega.BeNil()) 266 | outf("{{blue}}avalanche HTTP RPCs URIs:{{/}} %q\n", uris) 267 | 268 | clientURIs = uris 269 | 270 | outf("{{magenta}}logs dir:{{/}} %s\n", logsDir) 271 | }) 272 | 273 | var _ = ginkgo.AfterSuite(func() { 274 | // If clientURIs were manually specified, skip killing the network. 275 | if len(commaSeparatedClientURIs) != 0 { 276 | return 277 | } 278 | outf("{{red}}shutting down cluster{{/}}\n") 279 | ctx, cancel := context.WithTimeout(context.Background(), 2*time.Minute) 280 | _, err := cli.Stop(ctx) 281 | cancel() 282 | gomega.Expect(err).Should(gomega.BeNil()) 283 | log.Warn("cluster shutdown result", "err", err) 284 | 285 | outf("{{red}}shutting down client{{/}}\n") 286 | err = cli.Close() 287 | gomega.Expect(err).Should(gomega.BeNil()) 288 | log.Warn("client shutdown result", "err", err) 289 | }) 290 | 291 | // Tests only assumes that [clientURIs] has been populated by BeforeSuite 292 | var _ = ginkgo.Describe("[ProposeBlock]", func() { 293 | ginkgo.It("load test", func() { 294 | workers := newLoadWorkers(clientURIs, blockchainID) 295 | 296 | err := RunLoadTest(context.Background(), workers, terminalHeight, 2*time.Minute) 297 | gomega.Ω(err).Should(gomega.BeNil()) 298 | log.Info("Load test completed successfully") 299 | }) 300 | }) 301 | 302 | // Outputs to stdout. 303 | // 304 | // e.g., 305 | // 306 | // Out("{{green}}{{bold}}hi there %q{{/}}", "aa") 307 | // Out("{{magenta}}{{bold}}hi therea{{/}} {{cyan}}{{underline}}b{{/}}") 308 | // 309 | // ref. 310 | // https://github.com/onsi/ginkgo/blob/v2.0.0/formatter/formatter.go#L52-L73 311 | func outf(format string, args ...interface{}) { 312 | s := formatter.F(format, args...) 313 | fmt.Fprint(formatter.ColorableStdOut, s) 314 | } 315 | -------------------------------------------------------------------------------- /timestampvm/block.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/ava-labs/avalanchego/ids" 13 | "github.com/ava-labs/avalanchego/snow/choices" 14 | "github.com/ava-labs/avalanchego/snow/consensus/snowman" 15 | "github.com/ava-labs/avalanchego/utils/hashing" 16 | ) 17 | 18 | var ( 19 | errTimestampTooEarly = errors.New("block's timestamp is earlier than its parent's timestamp") 20 | errDatabaseGet = errors.New("error while retrieving data from database") 21 | errTimestampTooLate = errors.New("block's timestamp is more than 1 hour ahead of local time") 22 | 23 | _ snowman.Block = &Block{} 24 | ) 25 | 26 | // Block is a block on the chain. 27 | // Each block contains: 28 | // 1) ParentID 29 | // 2) Height 30 | // 3) Timestamp 31 | // 4) A piece of data (a string) 32 | type Block struct { 33 | PrntID ids.ID `serialize:"true" json:"parentID"` // parent's ID 34 | Hght uint64 `serialize:"true" json:"height"` // This block's height. The genesis block is at height 0. 35 | Tmstmp int64 `serialize:"true" json:"timestamp"` // Time this block was proposed at 36 | Dt [DataLen]byte `serialize:"true" json:"data"` // Arbitrary data 37 | 38 | id ids.ID // hold this block's ID 39 | bytes []byte // this block's encoded bytes 40 | status choices.Status // block's status 41 | vm *VM // the underlying VM reference, mostly used for state 42 | } 43 | 44 | // Verify returns nil iff this block is valid. 45 | // To be valid, it must be that: 46 | // b.parent.Timestamp < b.Timestamp <= [local time] + 1 hour 47 | func (b *Block) Verify(_ context.Context) error { 48 | // Get [b]'s parent 49 | parentID := b.Parent() 50 | parent, err := b.vm.getBlock(parentID) 51 | if err != nil { 52 | return errDatabaseGet 53 | } 54 | 55 | // Ensure [b]'s height comes right after its parent's height 56 | if expectedHeight := parent.Height() + 1; expectedHeight != b.Hght { 57 | return fmt.Errorf( 58 | "expected block to have height %d, but found %d", 59 | expectedHeight, 60 | b.Hght, 61 | ) 62 | } 63 | 64 | // Ensure [b]'s timestamp is after its parent's timestamp. 65 | if b.Timestamp().Unix() < parent.Timestamp().Unix() { 66 | return errTimestampTooEarly 67 | } 68 | 69 | // Ensure [b]'s timestamp is not more than an hour 70 | // ahead of this node's time 71 | if b.Timestamp().Unix() >= time.Now().Add(time.Hour).Unix() { 72 | return errTimestampTooLate 73 | } 74 | 75 | // Put that block to verified blocks in memory 76 | b.vm.verifiedBlocks[b.ID()] = b 77 | 78 | return nil 79 | } 80 | 81 | // Initialize sets [b.bytes] to [bytes], [b.id] to hash([b.bytes]), 82 | // [b.status] to [status] and [b.vm] to [vm] 83 | func (b *Block) Initialize(bytes []byte, status choices.Status, vm *VM) { 84 | b.bytes = bytes 85 | b.id = hashing.ComputeHash256Array(b.bytes) 86 | b.status = status 87 | b.vm = vm 88 | } 89 | 90 | // Accept sets this block's status to Accepted and sets lastAccepted to this 91 | // block's ID and saves this info to b.vm.DB 92 | func (b *Block) Accept(_ context.Context) error { 93 | b.SetStatus(choices.Accepted) // Change state of this block 94 | blkID := b.ID() 95 | 96 | // Persist data 97 | if err := b.vm.state.PutBlock(b); err != nil { 98 | return err 99 | } 100 | 101 | // Set last accepted ID to this block ID 102 | if err := b.vm.state.SetLastAccepted(blkID); err != nil { 103 | return err 104 | } 105 | 106 | // Delete this block from verified blocks as it's accepted 107 | delete(b.vm.verifiedBlocks, b.ID()) 108 | 109 | // Commit changes to database 110 | return b.vm.state.Commit() 111 | } 112 | 113 | // Reject sets this block's status to Rejected and saves the status in state 114 | // Recall that b.vm.DB.Commit() must be called to persist to the DB 115 | func (b *Block) Reject(_ context.Context) error { 116 | b.SetStatus(choices.Rejected) // Change state of this block 117 | if err := b.vm.state.PutBlock(b); err != nil { 118 | return err 119 | } 120 | // Delete this block from verified blocks as it's rejected 121 | delete(b.vm.verifiedBlocks, b.ID()) 122 | // Commit changes to database 123 | return b.vm.state.Commit() 124 | } 125 | 126 | // ID returns the ID of this block 127 | func (b *Block) ID() ids.ID { return b.id } 128 | 129 | // ParentID returns [b]'s parent's ID 130 | func (b *Block) Parent() ids.ID { return b.PrntID } 131 | 132 | // Height returns this block's height. The genesis block has height 0. 133 | func (b *Block) Height() uint64 { return b.Hght } 134 | 135 | // Timestamp returns this block's time. The genesis block has time 0. 136 | func (b *Block) Timestamp() time.Time { return time.Unix(b.Tmstmp, 0) } 137 | 138 | // Status returns the status of this block 139 | func (b *Block) Status() choices.Status { return b.status } 140 | 141 | // Bytes returns the byte repr. of this block 142 | func (b *Block) Bytes() []byte { return b.bytes } 143 | 144 | // Data returns the data of this block 145 | func (b *Block) Data() [DataLen]byte { return b.Dt } 146 | 147 | // SetStatus sets the status of this block 148 | func (b *Block) SetStatus(status choices.Status) { b.status = status } 149 | -------------------------------------------------------------------------------- /timestampvm/block_state.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | import ( 7 | "github.com/ava-labs/avalanchego/cache" 8 | "github.com/ava-labs/avalanchego/database" 9 | "github.com/ava-labs/avalanchego/ids" 10 | "github.com/ava-labs/avalanchego/snow/choices" 11 | ) 12 | 13 | const ( 14 | lastAcceptedByte byte = iota 15 | ) 16 | 17 | const ( 18 | // maximum block capacity of the cache 19 | blockCacheSize = 8192 20 | ) 21 | 22 | // persists lastAccepted block IDs with this key 23 | var lastAcceptedKey = []byte{lastAcceptedByte} 24 | 25 | var _ BlockState = &blockState{} 26 | 27 | // BlockState defines methods to manage state with Blocks and LastAcceptedIDs. 28 | type BlockState interface { 29 | GetBlock(blkID ids.ID) (*Block, error) 30 | PutBlock(blk *Block) error 31 | GetLastAccepted() (ids.ID, error) 32 | SetLastAccepted(ids.ID) error 33 | } 34 | 35 | // blockState implements BlocksState interface with database and cache. 36 | type blockState struct { 37 | // cache to store blocks 38 | blkCache cache.Cacher[ids.ID, *Block] 39 | // block database 40 | blockDB database.Database 41 | lastAccepted ids.ID 42 | 43 | // vm reference 44 | vm *VM 45 | } 46 | 47 | // blkWrapper wraps the actual blk bytes and status to persist them together 48 | type blkWrapper struct { 49 | Blk []byte `serialize:"true"` 50 | Status choices.Status `serialize:"true"` 51 | } 52 | 53 | // NewBlockState returns BlockState with a new cache and given db 54 | func NewBlockState(db database.Database, vm *VM) BlockState { 55 | return &blockState{ 56 | blkCache: &cache.LRU[ids.ID, *Block]{Size: blockCacheSize}, 57 | blockDB: db, 58 | vm: vm, 59 | } 60 | } 61 | 62 | // GetBlock gets Block from either cache or database 63 | func (s *blockState) GetBlock(blkID ids.ID) (*Block, error) { 64 | // Check if cache has this blkID 65 | if blk, cached := s.blkCache.Get(blkID); cached { 66 | // there is a key but value is nil, so return an error 67 | if blk == nil { 68 | return nil, database.ErrNotFound 69 | } 70 | // We found it return the block in cache 71 | return blk, nil 72 | } 73 | 74 | // get block bytes from db with the blkID key 75 | wrappedBytes, err := s.blockDB.Get(blkID[:]) 76 | if err != nil { 77 | // we could not find it in the db, let's cache this blkID with nil value 78 | // so next time we try to fetch the same key we can return error 79 | // without hitting the database 80 | if err == database.ErrNotFound { 81 | s.blkCache.Put(blkID, nil) 82 | } 83 | // could not find the block, return error 84 | return nil, err 85 | } 86 | 87 | // first decode/unmarshal the block wrapper so we can have status and block bytes 88 | blkw := blkWrapper{} 89 | if _, err := Codec.Unmarshal(wrappedBytes, &blkw); err != nil { 90 | return nil, err 91 | } 92 | 93 | // now decode/unmarshal the actual block bytes to block 94 | blk := &Block{} 95 | if _, err := Codec.Unmarshal(blkw.Blk, blk); err != nil { 96 | return nil, err 97 | } 98 | 99 | // initialize block with block bytes, status and vm 100 | blk.Initialize(blkw.Blk, blkw.Status, s.vm) 101 | 102 | // put block into cache 103 | s.blkCache.Put(blkID, blk) 104 | 105 | return blk, nil 106 | } 107 | 108 | // PutBlock puts block into both database and cache 109 | func (s *blockState) PutBlock(blk *Block) error { 110 | // create block wrapper with block bytes and status 111 | blkw := blkWrapper{ 112 | Blk: blk.Bytes(), 113 | Status: blk.Status(), 114 | } 115 | 116 | // encode block wrapper to its byte representation 117 | wrappedBytes, err := Codec.Marshal(CodecVersion, &blkw) 118 | if err != nil { 119 | return err 120 | } 121 | 122 | blkID := blk.ID() 123 | // put actual block to cache, so we can directly fetch it from cache 124 | s.blkCache.Put(blkID, blk) 125 | 126 | // put wrapped block bytes into database 127 | return s.blockDB.Put(blkID[:], wrappedBytes) 128 | } 129 | 130 | // DeleteBlock deletes block from both cache and database 131 | func (s *blockState) DeleteBlock(blkID ids.ID) error { 132 | s.blkCache.Put(blkID, nil) 133 | return s.blockDB.Delete(blkID[:]) 134 | } 135 | 136 | // GetLastAccepted returns last accepted block ID 137 | func (s *blockState) GetLastAccepted() (ids.ID, error) { 138 | // check if we already have lastAccepted ID in state memory 139 | if s.lastAccepted != ids.Empty { 140 | return s.lastAccepted, nil 141 | } 142 | 143 | // get lastAccepted bytes from database with the fixed lastAcceptedKey 144 | lastAcceptedBytes, err := s.blockDB.Get(lastAcceptedKey) 145 | if err != nil { 146 | return ids.ID{}, err 147 | } 148 | // parse bytes to ID 149 | lastAccepted, err := ids.ToID(lastAcceptedBytes) 150 | if err != nil { 151 | return ids.ID{}, err 152 | } 153 | // put lastAccepted ID into memory 154 | s.lastAccepted = lastAccepted 155 | return lastAccepted, nil 156 | } 157 | 158 | // SetLastAccepted persists lastAccepted ID into both cache and database 159 | func (s *blockState) SetLastAccepted(lastAccepted ids.ID) error { 160 | // if the ID in memory and the given memory are same don't do anything 161 | if s.lastAccepted == lastAccepted { 162 | return nil 163 | } 164 | // put lastAccepted ID to memory 165 | s.lastAccepted = lastAccepted 166 | // persist lastAccepted ID to database with fixed lastAcceptedKey 167 | return s.blockDB.Put(lastAcceptedKey, lastAccepted[:]) 168 | } 169 | -------------------------------------------------------------------------------- /timestampvm/codec.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | import ( 7 | "github.com/ava-labs/avalanchego/codec" 8 | "github.com/ava-labs/avalanchego/codec/linearcodec" 9 | ) 10 | 11 | const ( 12 | // CodecVersion is the current default codec version 13 | CodecVersion = 0 14 | ) 15 | 16 | // Codecs do serialization and deserialization 17 | var ( 18 | Codec codec.Manager 19 | ) 20 | 21 | func init() { 22 | // Create default codec and manager 23 | c := linearcodec.NewDefault() 24 | Codec = codec.NewDefaultManager() 25 | 26 | // Register codec to manager with CodecVersion 27 | if err := Codec.RegisterCodec(CodecVersion, c); err != nil { 28 | panic(err) 29 | } 30 | } 31 | -------------------------------------------------------------------------------- /timestampvm/factory.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | import ( 7 | "github.com/ava-labs/avalanchego/utils/logging" 8 | "github.com/ava-labs/avalanchego/vms" 9 | ) 10 | 11 | var _ vms.Factory = &Factory{} 12 | 13 | // Factory ... 14 | type Factory struct{} 15 | 16 | // New ... 17 | func (*Factory) New(logging.Logger) (interface{}, error) { return &VM{}, nil } 18 | -------------------------------------------------------------------------------- /timestampvm/service.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | import ( 7 | "errors" 8 | "net/http" 9 | 10 | "github.com/ava-labs/avalanchego/ids" 11 | "github.com/ava-labs/avalanchego/utils/formatting" 12 | "github.com/ava-labs/avalanchego/utils/json" 13 | ) 14 | 15 | var ( 16 | errBadData = errors.New("data must be hex representation of 32 bytes") 17 | errNoSuchBlock = errors.New("couldn't get block from database. Does it exist?") 18 | errCannotGetLastAccepted = errors.New("problem getting last accepted") 19 | ) 20 | 21 | // Service is the API service for this VM 22 | type Service struct{ vm *VM } 23 | 24 | // ProposeBlockArgs are the arguments to function ProposeValue 25 | type ProposeBlockArgs struct { 26 | // Data in the block. Must be hex encoding of 32 bytes. 27 | Data string `json:"data"` 28 | } 29 | 30 | // ProposeBlockReply is the reply from function ProposeBlock 31 | type ProposeBlockReply struct{ Success bool } 32 | 33 | // ProposeBlock is an API method to propose a new block whose data is [args].Data. 34 | // [args].Data must be a string repr. of a 32 byte array 35 | func (s *Service) ProposeBlock(_ *http.Request, args *ProposeBlockArgs, reply *ProposeBlockReply) error { 36 | bytes, err := formatting.Decode(formatting.Hex, args.Data) 37 | if err != nil || len(bytes) != DataLen { 38 | return errBadData 39 | } 40 | reply.Success = s.vm.proposeBlock(BytesToData(bytes)) 41 | return nil 42 | } 43 | 44 | // GetBlockArgs are the arguments to GetBlock 45 | type GetBlockArgs struct { 46 | // ID of the block we're getting. 47 | // If left blank, gets the latest block 48 | ID *ids.ID `json:"id"` 49 | } 50 | 51 | // GetBlockReply is the reply from GetBlock 52 | type GetBlockReply struct { 53 | Timestamp json.Uint64 `json:"timestamp"` // Timestamp of block 54 | Data string `json:"data"` // Data (hex-encoded) in block 55 | Height json.Uint64 `json:"height"` // Height of block 56 | ID ids.ID `json:"id"` // String repr. of ID of block 57 | ParentID ids.ID `json:"parentID"` // String repr. of ID of block's parent 58 | } 59 | 60 | // GetBlock gets the block whose ID is [args.ID] 61 | // If [args.ID] is empty, get the latest block 62 | func (s *Service) GetBlock(_ *http.Request, args *GetBlockArgs, reply *GetBlockReply) error { 63 | // If an ID is given, parse its string representation to an ids.ID 64 | // If no ID is given, ID becomes the ID of last accepted block 65 | var ( 66 | id ids.ID 67 | err error 68 | ) 69 | 70 | if args.ID == nil { 71 | id, err = s.vm.state.GetLastAccepted() 72 | if err != nil { 73 | return errCannotGetLastAccepted 74 | } 75 | } else { 76 | id = *args.ID 77 | } 78 | 79 | // Get the block from the database 80 | block, err := s.vm.getBlock(id) 81 | if err != nil { 82 | return errNoSuchBlock 83 | } 84 | 85 | // Fill out the response with the block's data 86 | reply.Timestamp = json.Uint64(block.Timestamp().Unix()) 87 | data := block.Data() 88 | reply.Data, err = formatting.Encode(formatting.Hex, data[:]) 89 | reply.Height = json.Uint64(block.Hght) 90 | reply.ID = block.ID() 91 | reply.ParentID = block.Parent() 92 | 93 | return err 94 | } 95 | -------------------------------------------------------------------------------- /timestampvm/singleton_state.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | import "github.com/ava-labs/avalanchego/database" 7 | 8 | const ( 9 | IsInitializedKey byte = iota 10 | ) 11 | 12 | var ( 13 | isInitializedKey = []byte{IsInitializedKey} 14 | _ SingletonState = (*singletonState)(nil) 15 | ) 16 | 17 | // SingletonState is a thin wrapper around a database to provide, caching, 18 | // serialization, and deserialization of singletons. 19 | type SingletonState interface { 20 | IsInitialized() (bool, error) 21 | SetInitialized() error 22 | } 23 | 24 | type singletonState struct { 25 | singletonDB database.Database 26 | } 27 | 28 | func NewSingletonState(db database.Database) SingletonState { 29 | return &singletonState{ 30 | singletonDB: db, 31 | } 32 | } 33 | 34 | func (s *singletonState) IsInitialized() (bool, error) { 35 | return s.singletonDB.Has(isInitializedKey) 36 | } 37 | 38 | func (s *singletonState) SetInitialized() error { 39 | return s.singletonDB.Put(isInitializedKey, nil) 40 | } 41 | -------------------------------------------------------------------------------- /timestampvm/state.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | import ( 7 | "github.com/ava-labs/avalanchego/database" 8 | "github.com/ava-labs/avalanchego/database/prefixdb" 9 | "github.com/ava-labs/avalanchego/database/versiondb" 10 | ) 11 | 12 | var ( 13 | // These are prefixes for db keys. 14 | // It's important to set different prefixes for each separate database objects. 15 | singletonStatePrefix = []byte("singleton") 16 | blockStatePrefix = []byte("block") 17 | 18 | _ State = &state{} 19 | ) 20 | 21 | // State is a wrapper around avax.SingleTonState and BlockState 22 | // State also exposes a few methods needed for managing database commits and close. 23 | type State interface { 24 | // SingletonState is defined in avalanchego, 25 | // it is used to understand if db is initialized already. 26 | SingletonState 27 | BlockState 28 | 29 | Commit() error 30 | Close() error 31 | } 32 | 33 | type state struct { 34 | SingletonState 35 | BlockState 36 | 37 | baseDB *versiondb.Database 38 | } 39 | 40 | func NewState(db database.Database, vm *VM) State { 41 | // create a new baseDB 42 | baseDB := versiondb.New(db) 43 | 44 | // create a prefixed "blockDB" from baseDB 45 | blockDB := prefixdb.New(blockStatePrefix, baseDB) 46 | // create a prefixed "singletonDB" from baseDB 47 | singletonDB := prefixdb.New(singletonStatePrefix, baseDB) 48 | 49 | // return state with created sub state components 50 | return &state{ 51 | BlockState: NewBlockState(blockDB, vm), 52 | SingletonState: NewSingletonState(singletonDB), 53 | baseDB: baseDB, 54 | } 55 | } 56 | 57 | // Commit commits pending operations to baseDB 58 | func (s *state) Commit() error { 59 | return s.baseDB.Commit() 60 | } 61 | 62 | // Close closes the underlying base database 63 | func (s *state) Close() error { 64 | return s.baseDB.Close() 65 | } 66 | -------------------------------------------------------------------------------- /timestampvm/static_service.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | import ( 7 | "errors" 8 | "fmt" 9 | "net/http" 10 | 11 | "github.com/ava-labs/avalanchego/utils/formatting" 12 | ) 13 | 14 | var errArgumentDataEmpty = errors.New("argument Data cannot be empty") 15 | 16 | // StaticService defines the base service for the timestamp vm 17 | type StaticService struct{} 18 | 19 | // CreateStaticService ... 20 | func CreateStaticService() *StaticService { 21 | return &StaticService{} 22 | } 23 | 24 | // EncodeArgs are arguments for Encode 25 | type EncodeArgs struct { 26 | Data string `json:"data"` 27 | Encoding formatting.Encoding `json:"encoding"` 28 | Length int32 `json:"length"` 29 | } 30 | 31 | // EncodeReply is the reply from Encode 32 | type EncodeReply struct { 33 | Bytes string `json:"bytes"` 34 | Encoding formatting.Encoding `json:"encoding"` 35 | } 36 | 37 | // Encode returns the encoded data 38 | func (*StaticService) Encode(_ *http.Request, args *EncodeArgs, reply *EncodeReply) error { 39 | if len(args.Data) == 0 { 40 | return errArgumentDataEmpty 41 | } 42 | var argBytes []byte 43 | if args.Length > 0 { 44 | argBytes = make([]byte, args.Length) 45 | copy(argBytes, args.Data) 46 | } else { 47 | argBytes = []byte(args.Data) 48 | } 49 | 50 | bytes, err := formatting.Encode(args.Encoding, argBytes) 51 | if err != nil { 52 | return fmt.Errorf("couldn't encode data as string: %s", err) 53 | } 54 | reply.Bytes = bytes 55 | reply.Encoding = args.Encoding 56 | return nil 57 | } 58 | 59 | // DecodeArgs are arguments for Decode 60 | type DecodeArgs struct { 61 | Bytes string `json:"bytes"` 62 | Encoding formatting.Encoding `json:"encoding"` 63 | } 64 | 65 | // DecodeReply is the reply from Decode 66 | type DecodeReply struct { 67 | Data string `json:"data"` 68 | Encoding formatting.Encoding `json:"encoding"` 69 | } 70 | 71 | // Decode returns the Decoded data 72 | func (*StaticService) Decode(_ *http.Request, args *DecodeArgs, reply *DecodeReply) error { 73 | bytes, err := formatting.Decode(args.Encoding, args.Bytes) 74 | if err != nil { 75 | return fmt.Errorf("couldn't Decode data as string: %s", err) 76 | } 77 | reply.Data = string(bytes) 78 | reply.Encoding = args.Encoding 79 | return nil 80 | } 81 | -------------------------------------------------------------------------------- /timestampvm/utils.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | // BytesToData converts a byte slice to an array. If the byte slice input is 7 | // larger than [DataLen], it will be truncated. 8 | func BytesToData(input []byte) [DataLen]byte { 9 | data := [DataLen]byte{} 10 | lim := len(input) 11 | if lim > DataLen { 12 | lim = DataLen 13 | } 14 | copy(data[:], input[:lim]) 15 | return data 16 | } 17 | -------------------------------------------------------------------------------- /timestampvm/vm.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | import ( 7 | "context" 8 | "errors" 9 | "fmt" 10 | "time" 11 | 12 | "github.com/gorilla/rpc/v2" 13 | log "github.com/inconshreveable/log15" 14 | "go.uber.org/zap" 15 | 16 | "github.com/ava-labs/avalanchego/database/manager" 17 | "github.com/ava-labs/avalanchego/ids" 18 | "github.com/ava-labs/avalanchego/snow" 19 | "github.com/ava-labs/avalanchego/snow/choices" 20 | "github.com/ava-labs/avalanchego/snow/consensus/snowman" 21 | "github.com/ava-labs/avalanchego/snow/engine/common" 22 | "github.com/ava-labs/avalanchego/snow/engine/snowman/block" 23 | "github.com/ava-labs/avalanchego/utils" 24 | "github.com/ava-labs/avalanchego/utils/json" 25 | "github.com/ava-labs/avalanchego/version" 26 | ) 27 | 28 | const ( 29 | DataLen = 32 30 | Name = "timestampvm" 31 | MaxMempoolSize = 4096 32 | ) 33 | 34 | var ( 35 | errNoPendingBlocks = errors.New("there is no block to propose") 36 | errBadGenesisBytes = errors.New("genesis data should be bytes (max length 32)") 37 | Version = &version.Semantic{ 38 | Major: 1, 39 | Minor: 3, 40 | Patch: 3, 41 | } 42 | 43 | _ block.ChainVM = &VM{} 44 | ) 45 | 46 | // VM implements the snowman.VM interface 47 | // Each block in this chain contains a Unix timestamp 48 | // and a piece of data (a string) 49 | type VM struct { 50 | // The context of this vm 51 | snowCtx *snow.Context 52 | dbManager manager.Manager 53 | 54 | // State of this VM 55 | state State 56 | 57 | // ID of the preferred block 58 | preferred ids.ID 59 | 60 | // channel to send messages to the consensus engine 61 | toEngine chan<- common.Message 62 | 63 | // Proposed pieces of data that haven't been put into a block and proposed yet 64 | mempool [][DataLen]byte 65 | 66 | // Block ID --> Block 67 | // Each element is a block that passed verification but 68 | // hasn't yet been accepted/rejected 69 | verifiedBlocks map[ids.ID]*Block 70 | 71 | // Indicates that this VM has finised bootstrapping for the chain 72 | bootstrapped utils.Atomic[bool] 73 | } 74 | 75 | // Initialize this vm 76 | // [ctx] is this vm's context 77 | // [dbManager] is the manager of this vm's database 78 | // [toEngine] is used to notify the consensus engine that new blocks are 79 | // 80 | // ready to be added to consensus 81 | // 82 | // The data in the genesis block is [genesisData] 83 | func (vm *VM) Initialize( 84 | ctx context.Context, 85 | snowCtx *snow.Context, 86 | dbManager manager.Manager, 87 | genesisData []byte, 88 | _ []byte, 89 | _ []byte, 90 | toEngine chan<- common.Message, 91 | _ []*common.Fx, 92 | _ common.AppSender, 93 | ) error { 94 | version, err := vm.Version(ctx) 95 | if err != nil { 96 | log.Error("error initializing Timestamp VM: %v", err) 97 | return err 98 | } 99 | log.Info("Initializing Timestamp VM", "Version", version) 100 | 101 | vm.dbManager = dbManager 102 | vm.snowCtx = snowCtx 103 | vm.toEngine = toEngine 104 | vm.verifiedBlocks = make(map[ids.ID]*Block) 105 | 106 | // Create new state 107 | vm.state = NewState(vm.dbManager.Current().Database, vm) 108 | 109 | // Initialize genesis 110 | if err := vm.initGenesis(genesisData); err != nil { 111 | return err 112 | } 113 | 114 | // Get last accepted 115 | lastAccepted, err := vm.state.GetLastAccepted() 116 | if err != nil { 117 | return err 118 | } 119 | 120 | snowCtx.Log.Info("initializing last accepted block", 121 | zap.Any("id", lastAccepted), 122 | ) 123 | 124 | // Build off the most recently accepted block 125 | return vm.SetPreference(ctx, lastAccepted) 126 | } 127 | 128 | // Initializes Genesis if required 129 | func (vm *VM) initGenesis(genesisData []byte) error { 130 | stateInitialized, err := vm.state.IsInitialized() 131 | if err != nil { 132 | return err 133 | } 134 | 135 | // if state is already initialized, skip init genesis. 136 | if stateInitialized { 137 | return nil 138 | } 139 | 140 | if len(genesisData) > DataLen { 141 | return errBadGenesisBytes 142 | } 143 | 144 | // genesisData is a byte slice but each block contains an byte array 145 | // Take the first [DataLen] bytes from genesisData and put them in an array 146 | genesisDataArr := BytesToData(genesisData) 147 | log.Debug("genesis", "data", genesisDataArr) 148 | 149 | // Create the genesis block 150 | // Timestamp of genesis block is 0. It has no parent. 151 | genesisBlock, err := vm.NewBlock(ids.Empty, 0, genesisDataArr, time.Unix(0, 0)) 152 | if err != nil { 153 | log.Error("error while creating genesis block: %v", err) 154 | return err 155 | } 156 | 157 | // Put genesis block to state 158 | if err := vm.state.PutBlock(genesisBlock); err != nil { 159 | log.Error("error while saving genesis block: %v", err) 160 | return err 161 | } 162 | 163 | // Accept the genesis block 164 | // Sets [vm.lastAccepted] and [vm.preferred] 165 | if err := genesisBlock.Accept(context.TODO()); err != nil { 166 | return fmt.Errorf("error accepting genesis block: %w", err) 167 | } 168 | 169 | // Mark this vm's state as initialized, so we can skip initGenesis in further restarts 170 | if err := vm.state.SetInitialized(); err != nil { 171 | return fmt.Errorf("error while setting db to initialized: %w", err) 172 | } 173 | 174 | // Flush VM's database to underlying db 175 | return vm.state.Commit() 176 | } 177 | 178 | // CreateHandlers returns a map where: 179 | // Keys: The path extension for this VM's API (empty in this case) 180 | // Values: The handler for the API 181 | func (vm *VM) CreateHandlers(_ context.Context) (map[string]*common.HTTPHandler, error) { 182 | server := rpc.NewServer() 183 | server.RegisterCodec(json.NewCodec(), "application/json") 184 | server.RegisterCodec(json.NewCodec(), "application/json;charset=UTF-8") 185 | if err := server.RegisterService(&Service{vm: vm}, Name); err != nil { 186 | return nil, err 187 | } 188 | 189 | return map[string]*common.HTTPHandler{ 190 | "": { 191 | LockOptions: common.WriteLock, 192 | Handler: server, 193 | }, 194 | }, nil 195 | } 196 | 197 | // CreateStaticHandlers returns a map where: 198 | // Keys: The path extension for this VM's static API 199 | // Values: The handler for that static API 200 | func (*VM) CreateStaticHandlers(_ context.Context) (map[string]*common.HTTPHandler, error) { 201 | server := rpc.NewServer() 202 | server.RegisterCodec(json.NewCodec(), "application/json") 203 | server.RegisterCodec(json.NewCodec(), "application/json;charset=UTF-8") 204 | if err := server.RegisterService(&StaticService{}, Name); err != nil { 205 | return nil, err 206 | } 207 | 208 | return map[string]*common.HTTPHandler{ 209 | "": { 210 | LockOptions: common.NoLock, 211 | Handler: server, 212 | }, 213 | }, nil 214 | } 215 | 216 | // Health implements the common.VM interface 217 | func (*VM) HealthCheck(_ context.Context) (interface{}, error) { return nil, nil } 218 | 219 | // BuildBlock returns a block that this vm wants to add to consensus 220 | func (vm *VM) BuildBlock(ctx context.Context) (snowman.Block, error) { 221 | if len(vm.mempool) == 0 { // There is no block to be built 222 | return nil, errNoPendingBlocks 223 | } 224 | 225 | // Get the value to put in the new block 226 | value := vm.mempool[0] 227 | vm.mempool = vm.mempool[1:] 228 | 229 | // Notify consensus engine that there are more pending data for blocks 230 | // (if that is the case) when done building this block 231 | if len(vm.mempool) > 0 { 232 | defer vm.NotifyBlockReady() 233 | } 234 | 235 | // Gets Preferred Block 236 | preferredBlock, err := vm.getBlock(vm.preferred) 237 | if err != nil { 238 | return nil, fmt.Errorf("couldn't get preferred block: %w", err) 239 | } 240 | preferredHeight := preferredBlock.Height() 241 | 242 | // Build the block with preferred height 243 | newBlock, err := vm.NewBlock(vm.preferred, preferredHeight+1, value, time.Now()) 244 | if err != nil { 245 | return nil, fmt.Errorf("couldn't build block: %w", err) 246 | } 247 | 248 | // Verifies block 249 | if err := newBlock.Verify(ctx); err != nil { 250 | return nil, err 251 | } 252 | return newBlock, nil 253 | } 254 | 255 | // NotifyBlockReady tells the consensus engine that a new block 256 | // is ready to be created 257 | func (vm *VM) NotifyBlockReady() { 258 | select { 259 | case vm.toEngine <- common.PendingTxs: 260 | default: 261 | vm.snowCtx.Log.Debug("dropping message to consensus engine") 262 | } 263 | } 264 | 265 | // GetBlock implements the snowman.ChainVM interface 266 | func (vm *VM) GetBlock(_ context.Context, blkID ids.ID) (snowman.Block, error) { 267 | return vm.getBlock(blkID) 268 | } 269 | 270 | func (vm *VM) getBlock(blkID ids.ID) (*Block, error) { 271 | // If block is in memory, return it. 272 | if blk, exists := vm.verifiedBlocks[blkID]; exists { 273 | return blk, nil 274 | } 275 | 276 | return vm.state.GetBlock(blkID) 277 | } 278 | 279 | // LastAccepted returns the block most recently accepted 280 | func (vm *VM) LastAccepted(_ context.Context) (ids.ID, error) { return vm.state.GetLastAccepted() } 281 | 282 | // proposeBlock appends [data] to [p.mempool]. 283 | // Then it notifies the consensus engine 284 | // that a new block is ready to be added to consensus 285 | // (namely, a block with data [data]) 286 | func (vm *VM) proposeBlock(data [DataLen]byte) bool { 287 | if len(vm.mempool) > MaxMempoolSize { 288 | return false 289 | } 290 | vm.mempool = append(vm.mempool, data) 291 | vm.NotifyBlockReady() 292 | return true 293 | } 294 | 295 | // ParseBlock parses [bytes] to a snowman.Block 296 | // This function is used by the vm's state to unmarshal blocks saved in state 297 | // and by the consensus layer when it receives the byte representation of a block 298 | // from another node 299 | func (vm *VM) ParseBlock(_ context.Context, bytes []byte) (snowman.Block, error) { 300 | // A new empty block 301 | block := &Block{} 302 | 303 | // Unmarshal the byte repr. of the block into our empty block 304 | _, err := Codec.Unmarshal(bytes, block) 305 | if err != nil { 306 | return nil, err 307 | } 308 | 309 | // Initialize the block 310 | block.Initialize(bytes, choices.Processing, vm) 311 | 312 | if blk, err := vm.getBlock(block.ID()); err == nil { 313 | // If we have seen this block before, return it with the most up-to-date 314 | // info 315 | return blk, nil 316 | } 317 | 318 | // Return the block 319 | return block, nil 320 | } 321 | 322 | // NewBlock returns a new Block where: 323 | // - the block's parent is [parentID] 324 | // - the block's data is [data] 325 | // - the block's timestamp is [timestamp] 326 | func (vm *VM) NewBlock(parentID ids.ID, height uint64, data [DataLen]byte, timestamp time.Time) (*Block, error) { 327 | block := &Block{ 328 | PrntID: parentID, 329 | Hght: height, 330 | Tmstmp: timestamp.Unix(), 331 | Dt: data, 332 | } 333 | 334 | // Get the byte representation of the block 335 | blockBytes, err := Codec.Marshal(CodecVersion, block) 336 | if err != nil { 337 | return nil, err 338 | } 339 | 340 | // Initialize the block by providing it with its byte representation 341 | // and a reference to this VM 342 | block.Initialize(blockBytes, choices.Processing, vm) 343 | return block, nil 344 | } 345 | 346 | // Shutdown this vm 347 | func (vm *VM) Shutdown(_ context.Context) error { 348 | if vm.state == nil { 349 | return nil 350 | } 351 | 352 | return vm.state.Close() // close versionDB 353 | } 354 | 355 | // SetPreference sets the block with ID [ID] as the preferred block 356 | func (vm *VM) SetPreference(_ context.Context, id ids.ID) error { 357 | vm.preferred = id 358 | return nil 359 | } 360 | 361 | // SetState sets this VM state according to given snow.State 362 | func (vm *VM) SetState(_ context.Context, state snow.State) error { 363 | switch state { 364 | // Engine reports it's bootstrapping 365 | case snow.Bootstrapping: 366 | return vm.onBootstrapStarted() 367 | case snow.NormalOp: 368 | // Engine reports it can start normal operations 369 | return vm.onNormalOperationsStarted() 370 | default: 371 | return snow.ErrUnknownState 372 | } 373 | } 374 | 375 | // onBootstrapStarted marks this VM as bootstrapping 376 | func (vm *VM) onBootstrapStarted() error { 377 | vm.bootstrapped.Set(false) 378 | return nil 379 | } 380 | 381 | // onNormalOperationsStarted marks this VM as bootstrapped 382 | func (vm *VM) onNormalOperationsStarted() error { 383 | // No need to set it again 384 | if vm.bootstrapped.Get() { 385 | return nil 386 | } 387 | vm.bootstrapped.Set(true) 388 | return nil 389 | } 390 | 391 | // Returns this VM's version 392 | func (*VM) Version(_ context.Context) (string, error) { 393 | return Version.String(), nil 394 | } 395 | 396 | func (*VM) Connected(_ context.Context, _ ids.NodeID, _ *version.Application) error { 397 | return nil // noop 398 | } 399 | 400 | func (*VM) Disconnected(_ context.Context, _ ids.NodeID) error { 401 | return nil // noop 402 | } 403 | 404 | // This VM doesn't (currently) have any app-specific messages 405 | func (*VM) AppGossip(_ context.Context, _ ids.NodeID, _ []byte) error { 406 | return nil 407 | } 408 | 409 | // This VM doesn't (currently) have any app-specific messages 410 | func (*VM) AppRequest(_ context.Context, _ ids.NodeID, _ uint32, _ time.Time, _ []byte) error { 411 | return nil 412 | } 413 | 414 | // This VM doesn't (currently) have any app-specific messages 415 | func (*VM) AppResponse(_ context.Context, _ ids.NodeID, _ uint32, _ []byte) error { 416 | return nil 417 | } 418 | 419 | // This VM doesn't (currently) have any app-specific messages 420 | func (*VM) AppRequestFailed(_ context.Context, _ ids.NodeID, _ uint32) error { 421 | return nil 422 | } 423 | 424 | func (*VM) CrossChainAppRequest(_ context.Context, _ ids.ID, _ uint32, _ time.Time, _ []byte) error { 425 | return nil 426 | } 427 | 428 | func (*VM) CrossChainAppRequestFailed(_ context.Context, _ ids.ID, _ uint32) error { 429 | return nil 430 | } 431 | 432 | func (*VM) CrossChainAppResponse(_ context.Context, _ ids.ID, _ uint32, _ []byte) error { 433 | return nil 434 | } 435 | -------------------------------------------------------------------------------- /timestampvm/vm_test.go: -------------------------------------------------------------------------------- 1 | // Copyright (C) 2019-2023, Ava Labs, Inc. All rights reserved. 2 | // See the file LICENSE for licensing terms. 3 | 4 | package timestampvm 5 | 6 | import ( 7 | "context" 8 | "testing" 9 | 10 | "github.com/ava-labs/avalanchego/database/manager" 11 | "github.com/ava-labs/avalanchego/ids" 12 | "github.com/ava-labs/avalanchego/snow" 13 | "github.com/ava-labs/avalanchego/snow/engine/common" 14 | "github.com/ava-labs/avalanchego/version" 15 | "github.com/stretchr/testify/require" 16 | ) 17 | 18 | var blockchainID = ids.ID{1, 2, 3} 19 | 20 | // require that after initialization, the vm has the state we expect 21 | func TestGenesis(t *testing.T) { 22 | require := require.New(t) 23 | ctx := context.TODO() 24 | // Initialize the vm 25 | vm, _, _, err := newTestVM() 26 | require.NoError(err) 27 | // Verify that the db is initialized 28 | ok, err := vm.state.IsInitialized() 29 | require.NoError(err) 30 | require.True(ok) 31 | 32 | // Get lastAccepted 33 | lastAccepted, err := vm.LastAccepted(ctx) 34 | require.NoError(err) 35 | require.NotEqual(ids.Empty, lastAccepted) 36 | 37 | // Verify that getBlock returns the genesis block, and the genesis block 38 | // is the type we expect 39 | genesisBlock, err := vm.getBlock(lastAccepted) // genesisBlock as snowman.Block 40 | require.NoError(err) 41 | 42 | // Verify that the genesis block has the data we expect 43 | require.Equal(ids.Empty, genesisBlock.Parent()) 44 | require.Equal([32]byte{0, 0, 0, 0, 0}, genesisBlock.Data()) 45 | } 46 | 47 | func TestHappyPath(t *testing.T) { 48 | require := require.New(t) 49 | ctx := context.TODO() 50 | 51 | // Initialize the vm 52 | vm, snowCtx, msgChan, err := newTestVM() 53 | require.NoError(err) 54 | 55 | lastAcceptedID, err := vm.LastAccepted(ctx) 56 | require.NoError(err) 57 | genesisBlock, err := vm.getBlock(lastAcceptedID) 58 | require.NoError(err) 59 | 60 | // in an actual execution, the engine would set the preference 61 | require.NoError(vm.SetPreference(ctx, genesisBlock.ID())) 62 | 63 | snowCtx.Lock.Lock() 64 | vm.proposeBlock([DataLen]byte{0, 0, 0, 0, 1}) // propose a value 65 | snowCtx.Lock.Unlock() 66 | 67 | select { // require there is a pending tx message to the engine 68 | case msg := <-msgChan: 69 | require.Equal(common.PendingTxs, msg) 70 | default: 71 | require.FailNow("should have been pendingTxs message on channel") 72 | } 73 | 74 | // build the block 75 | snowCtx.Lock.Lock() 76 | snowmanBlock2, err := vm.BuildBlock(ctx) 77 | require.NoError(err) 78 | 79 | require.NoError(snowmanBlock2.Verify(ctx)) 80 | require.NoError(snowmanBlock2.Accept(ctx)) 81 | require.NoError(vm.SetPreference(ctx, snowmanBlock2.ID())) 82 | 83 | lastAcceptedID, err = vm.LastAccepted(ctx) 84 | require.NoError(err) 85 | 86 | // Should be the block we just accepted 87 | block2, err := vm.getBlock(lastAcceptedID) 88 | require.NoError(err) 89 | 90 | // require the block we accepted has the data we expect 91 | require.Equal(genesisBlock.ID(), block2.Parent()) 92 | require.Equal([DataLen]byte{0, 0, 0, 0, 1}, block2.Data()) 93 | require.Equal(snowmanBlock2.ID(), block2.ID()) 94 | require.NoError(block2.Verify(ctx)) 95 | 96 | vm.proposeBlock([DataLen]byte{0, 0, 0, 0, 2}) // propose a block 97 | snowCtx.Lock.Unlock() 98 | 99 | select { // verify there is a pending tx message to the engine 100 | case msg := <-msgChan: 101 | require.Equal(common.PendingTxs, msg) 102 | default: 103 | require.FailNow("should have been pendingTxs message on channel") 104 | } 105 | 106 | snowCtx.Lock.Lock() 107 | 108 | // build the block 109 | snowmanBlock3, err := vm.BuildBlock(ctx) 110 | require.NoError(err) 111 | require.NoError(snowmanBlock3.Verify(ctx)) 112 | require.NoError(snowmanBlock3.Accept(ctx)) 113 | require.NoError(vm.SetPreference(ctx, snowmanBlock3.ID())) 114 | 115 | lastAcceptedID, err = vm.LastAccepted(ctx) 116 | require.NoError(err) 117 | // The block we just accepted 118 | block3, err := vm.getBlock(lastAcceptedID) 119 | require.NoError(err) 120 | 121 | // require the block we accepted has the data we expect 122 | require.Equal(snowmanBlock2.ID(), block3.Parent()) 123 | require.Equal([DataLen]byte{0, 0, 0, 0, 2}, block3.Data()) 124 | require.Equal(snowmanBlock3.ID(), block3.ID()) 125 | require.NoError(block3.Verify(ctx)) 126 | 127 | // Next, check the blocks we added are there 128 | block2FromState, err := vm.getBlock(block2.ID()) 129 | require.NoError(err) 130 | require.Equal(block2.ID(), block2FromState.ID()) 131 | 132 | block3FromState, err := vm.getBlock(snowmanBlock3.ID()) 133 | require.NoError(err) 134 | require.Equal(snowmanBlock3.ID(), block3FromState.ID()) 135 | 136 | snowCtx.Lock.Unlock() 137 | } 138 | 139 | func TestService(t *testing.T) { 140 | // Initialize the vm 141 | require := require.New(t) 142 | // Initialize the vm 143 | vm, _, _, err := newTestVM() 144 | require.NoError(err) 145 | service := Service{vm} 146 | require.NoError(service.GetBlock(nil, &GetBlockArgs{}, &GetBlockReply{})) 147 | } 148 | 149 | func TestSetState(t *testing.T) { 150 | // Initialize the vm 151 | require := require.New(t) 152 | ctx := context.TODO() 153 | // Initialize the vm 154 | vm, _, _, err := newTestVM() 155 | require.NoError(err) 156 | // bootstrapping 157 | require.NoError(vm.SetState(ctx, snow.Bootstrapping)) 158 | require.False(vm.bootstrapped.Get()) 159 | // bootstrapped 160 | require.NoError(vm.SetState(ctx, snow.NormalOp)) 161 | require.True(vm.bootstrapped.Get()) 162 | // unknown 163 | unknownState := snow.State(99) 164 | require.ErrorIs(vm.SetState(ctx, unknownState), snow.ErrUnknownState) 165 | } 166 | 167 | func newTestVM() (*VM, *snow.Context, chan common.Message, error) { 168 | dbManager := manager.NewMemDB(&version.Semantic{ 169 | Major: 1, 170 | Minor: 0, 171 | Patch: 0, 172 | }) 173 | msgChan := make(chan common.Message, 1) 174 | vm := &VM{} 175 | snowCtx := snow.DefaultContextTest() 176 | snowCtx.ChainID = blockchainID 177 | err := vm.Initialize(context.TODO(), snowCtx, dbManager, []byte{0, 0, 0, 0, 0}, nil, nil, msgChan, nil, nil) 178 | return vm, snowCtx, msgChan, err 179 | } 180 | --------------------------------------------------------------------------------