diff --git a/.github/actions/setup-go/action.yml b/.github/actions/setup-go/action.yml deleted file mode 100644 index 1bfd4a41f..000000000 --- a/.github/actions/setup-go/action.yml +++ /dev/null @@ -1,55 +0,0 @@ -name: 'Setup the Go environment' -description: 'Installs go and restores/saves the build/module cache' -inputs: - go-version: - required: true -runs: - using: "composite" - steps: - - name: Set up Go - uses: actions/setup-go@v2 - with: - go-version: ${{ inputs.go-version }} - stable: ${{ !(contains(inputs.go-version, 'beta') || contains(inputs.go-version, 'rc')) }} - - # Restore original modification time of files based on the date of the most - # recent commit that modified them as mtimes affect the Go test cache. - - name: Restore modification time of checkout files - shell: bash - run: | - # Set a base, fixed modification time of all directories. - # git-restore-mtime doesn't set the mtime of all directories. - # (see https://github.com/MestreLion/git-tools/issues/47 for details) - touch -m -t '201509301646' $(find . -type d -not -path '.git/*') - # Restore original modification time from git. git clone sets the - # modification time to the current time, but Go tests that access fixtures - # get invalidated if their modification times change. - sudo apt-get install -y git-restore-mtime - git restore-mtime - - # The PREFIX must uniquely identify the specific instance of a job executing. - - shell: bash - run: echo 'PREFIX=${{ github.workflow }}-${{ github.job }}-${{ runner.os }}-${{ inputs.go-version }}-matrix(${{ join(matrix.*,'|') }})' >> $GITHUB_ENV - - # Cache the Go Modules downloaded during the job. - - uses: actions/cache@v2 - with: - path: ~/go/pkg/mod - key: ${{ env.PREFIX }}-go-mod-${{ hashFiles('**/go.sum') }} - restore-keys: ${{ env.PREFIX }}-go-mod- - - # Cache any build and test artifacts during the job, which will speed up - # rebuilds and cause test runs to skip tests that have no reason to rerun. - - uses: actions/cache@v2 - with: - path: ~/.cache/go-build - key: ${{ env.PREFIX }}-go-build-${{ github.ref }}-${{ hashFiles('**', '!.git') }} - restore-keys: | - ${{ env.PREFIX }}-go-build-${{ github.ref }}- - ${{ env.PREFIX }}-go-build- - - # Reset the cache for master/protected branches, to ensure they build and run the tests from zero - # and that the module cache is cleaned (otherwise it accumulates orphan dependencies over time). - - if: github.ref_protected - shell: bash - run: sudo rm -rf ~/.cache/go-build ~/go/pkg/mod diff --git a/.github/actions/setup-integration-tests/action.yml b/.github/actions/setup-integration-tests/action.yml deleted file mode 100644 index 938acb6d4..000000000 --- a/.github/actions/setup-integration-tests/action.yml +++ /dev/null @@ -1,61 +0,0 @@ -name: 'Set up integration tests' -description: 'Set up Go & Rust, build artifacts, work around cache issues and Ubuntu quirks' -inputs: - go-version: - required: true -runs: - using: "composite" - steps: - - uses: ./.github/actions/setup-go - with: - go-version: ${{ matrix.go }} - - uses: stellar/actions/rust-cache@main - - name: Build soroban contract fixtures - shell: bash - run: | - rustup update - rustup target add wasm32-unknown-unknown - make build_rust - make build-test-wasms - - - name: Install Captive Core - shell: bash - run: | - # Workaround for https://github.com/actions/virtual-environments/issues/5245, - # libc++1-8 won't be installed if another version is installed (but apt won't give you a helpful - # message about why the installation fails) - sudo apt-get remove -y libc++1-10 libc++abi1-10 || true - - sudo wget -qO - https://apt.stellar.org/SDF.asc | APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=true sudo apt-key add - - sudo bash -c 'echo "deb https://apt.stellar.org focal unstable" > /etc/apt/sources.list.d/SDF-unstable.list' - sudo apt-get update && sudo apt-get install -y stellar-core="$PROTOCOL_20_CORE_DEBIAN_PKG_VERSION" - echo "Using stellar core version $(stellar-core version)" - - # Docker-compose's remote contexts on Ubuntu 20 started failing with an OpenSSL versioning error. - # See https://stackoverflow.com/questions/66579446/error-executing-docker-compose-building-webserver-unable-to-prepare-context-un - - name: Work around Docker Compose problem - shell: bash - run: | - sudo apt-get update - sudo apt-get install -y ca-certificates curl gnupg - - # Install docker apt repo - sudo install -m 0755 -d /etc/apt/keyrings - curl -fsSL https://download.docker.com/linux/ubuntu/gpg | sudo gpg --dearmor -o /etc/apt/keyrings/docker.gpg - sudo chmod a+r /etc/apt/keyrings/docker.gpg - echo \ - "deb [arch="$(dpkg --print-architecture)" signed-by=/etc/apt/keyrings/docker.gpg] https://download.docker.com/linux/ubuntu \ - "$(. /etc/os-release && echo "$VERSION_CODENAME")" stable" | \ - sudo tee /etc/apt/sources.list.d/docker.list > /dev/null - - # Install docker-compose v2 from apt repo - sudo apt-get update - sudo apt-get remove -y moby-compose - sudo apt-get install -y docker-compose-plugin - - echo "Docker Compose Version:" - docker-compose version - - - name: Build libpreflight - shell: bash - run: make build-libpreflight diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml index a4ec65851..999c41878 100644 --- a/.github/workflows/dependency-check.yml +++ b/.github/workflows/dependency-check.yml @@ -15,9 +15,6 @@ jobs: steps: - uses: actions/checkout@v3 - run: rustup update - - uses: actions/setup-go@v3 - with: - go-version: 1.21 - run: scripts/check-dependencies.bash validate-rust-git-rev-deps: runs-on: ubuntu-latest diff --git a/.github/workflows/e2e.yml b/.github/workflows/e2e.yml index 0ca517d07..bb226b0fc 100644 --- a/.github/workflows/e2e.yml +++ b/.github/workflows/e2e.yml @@ -16,9 +16,10 @@ jobs: # the gh tag of system-test repo version to run SYSTEM_TEST_GIT_REF: master - # the soroban tools source code to compile and run from system test + # the soroban CLI & RPC source code to compile and run from system test # refers to checked out source of current git hub ref context - SYSTEM_TEST_SOROBAN_TOOLS_REF: ${{ github.workspace }}/soroban-tools + SYSTEM_TEST_SOROBAN_CLI_REF: ${{ github.workspace }}/soroban-tools + SYSTEM_TEST_SOROBAN_RPC_REF: https://github.com/stellar/soroban-rpc.git # core git ref should be latest commit for stable soroban functionality # the core bin can either be compiled in-line here as part of ci, @@ -66,7 +67,13 @@ jobs: - uses: actions/checkout@v3 name: checkout soroban-tools with: + repository: stellar/soroban-tools path: soroban-tools + - uses: actions/checkout@v3 + name: checkout soroban-rpc + with: + repository: stellar/soroban-rpc + path: soroban-rpc - if: ${{ env.SYSTEM_TEST_JS_STELLAR_SDK_GH_REPO != ''}} name: prepare local js-stellar-sdk run: | @@ -90,8 +97,8 @@ jobs: CORE_GIT_REF=$SYSTEM_TEST_CORE_GIT_REF \ CORE_COMPILE_CONFIGURE_FLAGS="$SYSTEM_TEST_CORE_COMPILE_CONFIGURE_FLAGS" \ CORE_IMAGE=$SYSTEM_TEST_CORE_IMAGE \ - SOROBAN_RPC_GIT_REF=$SYSTEM_TEST_SOROBAN_TOOLS_REF \ - SOROBAN_CLI_GIT_REF=$SYSTEM_TEST_SOROBAN_TOOLS_REF \ + SOROBAN_RPC_GIT_REF=$SYSTEM_TEST_SOROBAN_RPC_REF \ + SOROBAN_CLI_GIT_REF=$SYSTEM_TEST_SOROBAN_CLI_REF \ RUST_TOOLCHAIN_VERSION=$SYSTEM_TEST_RUST_TOOLCHAIN_VERSION \ RS_XDR_GIT_REF=$SYSTEM_TEST_RS_XDR_GIT_REF \ QUICKSTART_GIT_REF=$SYSTEM_TEST_QUICKSTART_GIT_REF \ diff --git a/.github/workflows/golangci-lint.yml b/.github/workflows/golangci-lint.yml deleted file mode 100644 index 1781a6ef0..000000000 --- a/.github/workflows/golangci-lint.yml +++ /dev/null @@ -1,41 +0,0 @@ -name: Linters -on: - push: - branches: - - main - pull_request: - -permissions: - contents: read - # Optional: allow read access to pull request. Use with `only-new-issues` option. - pull-requests: read - -jobs: - golangci: - runs-on: ubuntu-latest - steps: - - name: Checkout - uses: actions/checkout@2541b1294d2704b0964813337f33b291d3f8596b # version v3.0.2 - with: - fetch-depth: 0 # required for new-from-rev option in .golangci.yml - - name: Setup GO - uses: actions/setup-go@268d8c0ca0432bb2cf416faae41297df9d262d7f # version v3.3.0 - with: - go-version: '>=1.21.0' - - - name: Build libpreflight - run: | - rustup update - make build-libpreflight - - - name: Run golangci-lint - uses: golangci/golangci-lint-action@537aa1903e5d359d0b27dbc19ddd22c5087f3fbc # version v3.2.0 - with: - version: v1.51.1 # this is the golangci-lint version - args: --issues-exit-code=0 # exit without errors for now - won't fail the build - github-token: ${{ secrets.GITHUB_TOKEN }} - only-new-issues: true - - - - diff --git a/.github/workflows/rust.yml b/.github/workflows/rust.yml index 9028df78e..cf1d3343f 100644 --- a/.github/workflows/rust.yml +++ b/.github/workflows/rust.yml @@ -75,7 +75,7 @@ jobs: - run: cargo clippy --all-targets --target ${{ matrix.target }} - run: make build-test - if: startsWith(matrix.target, 'x86_64') - # specify directories explicitly to avoid building the preflight library (otherwise it will fail with missing symbols) + # specify directories explicitly (otherwise it will fail with missing symbols) run: | for I in cmd/soroban-cli cmd/crates/* cmd/crates/soroban-test/tests/fixtures/test-wasms/hello_world ; do cargo test --target ${{ matrix.target }} --manifest-path $I/Cargo.toml diff --git a/.github/workflows/soroban-rpc.yml b/.github/workflows/soroban-rpc.yml deleted file mode 100644 index ee58034f0..000000000 --- a/.github/workflows/soroban-rpc.yml +++ /dev/null @@ -1,129 +0,0 @@ -name: Soroban RPC - -defaults: - run: - shell: bash - -on: - push: - branches: [main, release/**] - pull_request: - -jobs: - test: - name: Unit tests - strategy: - matrix: - os: [ubuntu-20.04, ubuntu-22.04] - go: [1.21] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v3 - with: - # For pull requests, build and test the PR head not a merge of the PR with the destination. - ref: ${{ github.event.pull_request.head.sha || github.ref }} - # We need to full history for git-restore-mtime to know what modification dates to use. - # Otherwise, the Go test cache will fail (due to the modification time of fixtures changing). - fetch-depth: "0" - - uses: ./.github/actions/setup-go - with: - go-version: ${{ matrix.go }} - - name: Build soroban contract fixtures - run: | - rustup update - rustup target add wasm32-unknown-unknown - make build-test-wasms - - run: make build-libpreflight - - run: go test -race -cover -timeout 25m -v ./cmd/soroban-rpc/... - - build: - name: Build - strategy: - matrix: - include: - - os: ubuntu-latest - rust_target: x86_64-unknown-linux-gnu - go_arch: amd64 - - os: ubuntu-latest - rust_target: aarch64-unknown-linux-gnu - go_arch: arm64 - - os: macos-latest - rust_target: x86_64-apple-darwin - go_arch: amd64 - - os: macos-latest - rust_target: aarch64-apple-darwin - go_arch: arm64 - - os: windows-latest - rust_target: x86_64-pc-windows-gnu - go_arch: amd64 - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v3 - # we cannot use our own ./.github/actions/setup-go action - # because it uses apt-get and some OSs (e.g. windows) don't have it - - uses: actions/setup-go@v3 - with: - go-version: 1.21 - - - run: | - rustup target add ${{ matrix.rust_target }} - rustup update - - # On windows, make sure we have the same compiler (linker) used by rust. - # This is important since the symbols names won't match otherwise. - - if: matrix.os == 'windows-latest' - name: Install the same mingw gcc compiler used by rust - run: | - C:/msys64/usr/bin/pacman.exe -S mingw-w64-x86_64-gcc --noconfirm - echo "CC=C:/msys64/mingw64/bin/gcc.exe" >> $GITHUB_ENV - echo "C:/msys64/mingw64/bin" >> $GITHUB_PATH - - # Use cross-compiler for linux aarch64 - - if: matrix.rust_target == 'aarch64-unknown-linux-gnu' - name: Install aarch64 cross-compilation toolchain - run: | - sudo apt-get update - sudo apt-get install -y gcc-10-aarch64-linux-gnu - echo 'CC=aarch64-linux-gnu-gcc-10' >> $GITHUB_ENV - - - name: Build libpreflight - run: make build-libpreflight - env: - CARGO_BUILD_TARGET: ${{ matrix.rust_target }} - - - name: Build Soroban RPC reproducible build - run: | - go build -trimpath -buildvcs=false ./cmd/soroban-rpc - ls -lh soroban-rpc - file soroban-rpc - env: - CGO_ENABLED: 1 - GOARCH: ${{ matrix.go_arch }} - - integration: - name: Integration tests - continue-on-error: true - strategy: - matrix: - os: [ubuntu-20.04, ubuntu-22.04] - go: [1.21] - test: ['.*CLI.*', '^Test(([^C])|(C[^L])|(CL[^I])).*$'] - runs-on: ${{ matrix.os }} - env: - SOROBAN_RPC_INTEGRATION_TESTS_ENABLED: true - SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN: /usr/bin/stellar-core - PROTOCOL_20_CORE_DEBIAN_PKG_VERSION: 20.2.0-1716.rc3.34d82fc00.focal - steps: - - uses: actions/checkout@v3 - with: - # For pull requests, build and test the PR head not a merge of the PR with the destination. - ref: ${{ github.event.pull_request.head.sha || github.ref }} - # We need to full history for git-restore-mtime to know what modification dates to use. - # Otherwise, the Go test cache will fail (due to the modification time of fixtures changing). - fetch-depth: "0" - - uses: ./.github/actions/setup-integration-tests - with: - go-version: ${{ matrix.go }} - - name: Run Soroban RPC Integration Tests - run: | - go test -race -run '${{ matrix.test }}' -timeout 60m -v ./cmd/soroban-rpc/internal/test/... diff --git a/.golangci.yml b/.golangci.yml deleted file mode 100644 index 28fbe1f96..000000000 --- a/.golangci.yml +++ /dev/null @@ -1,122 +0,0 @@ -linters-settings: - depguard: - dupl: - threshold: 100 - funlen: - lines: 100 - statements: 50 - goconst: - min-len: 2 - min-occurrences: 3 - gocritic: - enabled-tags: - - diagnostic - - experimental - - opinionated - - performance - - style - disabled-checks: - - dupImport # https://github.com/go-critic/go-critic/issues/845 - - ifElseChain - - octalLiteral - - whyNoLint - gocyclo: - min-complexity: 15 - goimports: - local-prefixes: github.com/golangci/golangci-lint - gomnd: - # don't include the "operation" and "assign" - checks: - - argument - - case - - condition - - return - ignored-numbers: - - '0' - - '1' - - '2' - - '3' - ignored-functions: - - strings.SplitN - - govet: - check-shadowing: true - settings: - printf: - funcs: - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Infof - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Warnf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Errorf - - (github.com/golangci/golangci-lint/pkg/logutils.Log).Fatalf - lll: - line-length: 140 - misspell: - locale: US - nolintlint: - allow-unused: false # report any unused nolint directives - require-explanation: false # don't require an explanation for nolint directives - require-specific: false # don't require nolint directives to be specific about which linter is being skipped - -linters: - disable-all: true - enable: - - bodyclose - - depguard - - dogsled - - dupl - - errcheck - - exportloopref - #- funlen - - gochecknoinits - - goconst - #- gocritic - #- gocyclo - - gofmt - - goimports - #- gomnd - - goprintffuncname - - gosec - - gosimple - - govet - - ineffassign - #- lll - - misspell - - nakedret - - noctx - - nolintlint - - staticcheck - - stylecheck - - typecheck - - unconvert - - unparam - - unused - - whitespace - - # don't enable: - # - asciicheck - # - scopelint - # - gochecknoglobals - # - gocognit - # - godot - # - godox - # - goerr113 - # - interfacer - # - maligned - # - nestif - # - prealloc - # - testpackage - # - revive - # - wsl - -issues: - # Excluding configuration per-path, per-linter, per-text and per-source - exclude-rules: - - path: _test\.go - linters: - - govet - -run: - timeout: 5m - skip-dirs: - - docs - - vendor diff --git a/Cargo.lock b/Cargo.lock index 2de033529..28f8aaf13 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2824,17 +2824,6 @@ dependencies = [ "termtree", ] -[[package]] -name = "preflight" -version = "20.3.0" -dependencies = [ - "base64 0.21.5", - "libc", - "sha2 0.10.8", - "soroban-env-host", - "soroban-simulation", -] - [[package]] name = "pretty_assertions" version = "1.4.0" @@ -3743,19 +3732,6 @@ dependencies = [ "syn 2.0.39", ] -[[package]] -name = "soroban-simulation" -version = "20.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "210b093c6d08b8e85ef5f4e4a231d5fa25d1d2787d4fecd50e11040849f259ba" -dependencies = [ - "anyhow", - "rand", - "soroban-env-host", - "static_assertions", - "thiserror", -] - [[package]] name = "soroban-spec" version = "20.3.1" diff --git a/Cargo.toml b/Cargo.toml index 06340e499..4b63e4f01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,6 @@ members = [ "cmd/crates/*", "cmd/crates/soroban-test/tests/fixtures/test-wasms/*", "cmd/crates/soroban-test/tests/fixtures/hello", - "cmd/soroban-rpc/lib/preflight", ] default-members = ["cmd/soroban-cli", "cmd/crates/soroban-test"] exclude = ["cmd/crates/soroban-test/tests/fixtures/hello"] diff --git a/Makefile b/Makefile index 000b2aef5..6ef25299a 100644 --- a/Makefile +++ b/Makefile @@ -14,11 +14,6 @@ ifeq ($(strip $(REPOSITORY_VERSION)),) endif REPOSITORY_BRANCH := "$(shell git rev-parse --abbrev-ref HEAD)" BUILD_TIMESTAMP ?= $(shell date '+%Y-%m-%dT%H:%M:%S') -GOLDFLAGS := -X 'github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config.Version=${REPOSITORY_VERSION}' \ - -X 'github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config.CommitHash=${REPOSITORY_COMMIT_HASH}' \ - -X 'github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config.BuildTimestamp=${BUILD_TIMESTAMP}' \ - -X 'github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config.Branch=${REPOSITORY_BRANCH}' - # The following works around incompatibility between the rust and the go linkers - # the rust would generate an object file with min-version of 13.0 where-as the go @@ -29,13 +24,6 @@ ifeq ($(shell uname -s),Darwin) MACOS_MIN_VER = -ldflags='-extldflags -mmacosx-version-min=13.0' endif -# Always specify the build target so that libpreflight.a is always put into -# an architecture subdirectory (i.e. target/$(CARGO_BUILD_TARGET)/release-with-panic-unwind ) -# Otherwise it will be much harder for Golang to find the library since -# it would need to distinguish when we are crosscompiling and when we are not -# (libpreflight.a is put at target/release-with-panic-unwind/ when not cross compiling) -CARGO_BUILD_TARGET ?= $(shell rustc -vV | sed -n 's|host: ||p') - # update the Cargo.lock every time the Cargo.toml changes. Cargo.lock: Cargo.toml cargo update --workspace @@ -44,22 +32,15 @@ install_rust: Cargo.lock cargo install --path ./cmd/soroban-cli --debug cargo install --path ./cmd/crates/soroban-test/tests/fixtures/hello --root ./target --debug --quiet -install: install_rust build-libpreflight - go install -ldflags="${GOLDFLAGS}" ${MACOS_MIN_VER} ./... +install: install_rust build_rust: Cargo.lock cargo build -build_go: build-libpreflight - go build -ldflags="${GOLDFLAGS}" ${MACOS_MIN_VER} ./... - # regenerate the example lib in `cmd/crates/soroban-spec-typsecript/fixtures/ts` build-snapshot: typescript-bindings-fixtures -build: build_rust build_go - -build-libpreflight: Cargo.lock - cd cmd/soroban-rpc/lib/preflight && cargo build --target $(CARGO_BUILD_TARGET) --profile release-with-panic-unwind +build: build_rust build-test-wasms: Cargo.lock cargo build --package 'test_*' --profile test-wasms --target wasm32-unknown-unknown @@ -83,23 +64,10 @@ fmt: clean: cargo clean - go clean ./... publish: cargo workspaces publish --all --force '*' --from-git --yes -# the build-soroban-rpc build target is an optimized build target used by -# https://github.com/stellar/pipelines/stellar-horizon/Jenkinsfile-soroban-rpc-package-builder -# as part of the package building. -build-soroban-rpc: build-libpreflight - go build -ldflags="${GOLDFLAGS}" ${MACOS_MIN_VER} -o soroban-rpc -trimpath -v ./cmd/soroban-rpc - -lint-changes: - golangci-lint run ./... --new-from-rev $$(git rev-parse HEAD) - -lint: - golangci-lint run ./... - typescript-bindings-fixtures: build-test-wasms cargo run -- contract bindings typescript \ --wasm ./target/wasm32-unknown-unknown/test-wasms/test_custom_types.wasm \ @@ -110,4 +78,4 @@ typescript-bindings-fixtures: build-test-wasms # PHONY lists all the targets that aren't file names, so that make would skip the timestamp based check. -.PHONY: publish clean fmt watch check e2e-test test build-test-wasms install build build-soroban-rpc build-libpreflight lint lint-changes build-snapshot typescript-bindings-fixtures +.PHONY: publish clean fmt watch check e2e-test test build-test-wasms install build build-snapshot typescript-bindings-fixtures diff --git a/README.md b/README.md index e61560158..0d052ba36 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,9 @@ # soroban-tools -This repo is home to the suite of Soroban development tools. -- [soroban](https://github.com/stellar/soroban-tools/tree/main/cmd/soroban-cli): The command-line multi-tool for running and deploying Soroban contracts. -- [soroban-rpc](https://github.com/stellar/soroban-tools/tree/main/cmd/soroban-rpc): The jsonrpc server for interacting with a running Soroban network. +This repo is home to the [Soroban CLI](https://github.com/stellar/soroban-tools/tree/main/cmd/soroban-cli): The command-line multi-tool for running and deploying Soroban contracts. Soroban: https://soroban.stellar.org -# linting - -Before submitting a PR for review, please run - -``` -make lint-changes -``` - -to review all the linting issues with the PR. Alternatively, you can run - -``` -make lint -``` - -to review all the linting issues with the current codebase. - # Adding git hooks To add git hooks for commits and pushes run: diff --git a/cmd/deptool/analyze.go b/cmd/deptool/analyze.go deleted file mode 100644 index ec638905c..000000000 --- a/cmd/deptool/analyze.go +++ /dev/null @@ -1,350 +0,0 @@ -package main - -import ( - "errors" - "fmt" - "strings" - "time" - - git "github.com/go-git/go-git/v5" - - "github.com/go-git/go-git/v5/plumbing" - "github.com/go-git/go-git/v5/plumbing/object" - "github.com/go-git/go-git/v5/plumbing/storer" - "github.com/go-git/go-git/v5/storage/memory" -) - -type analyzedProjectDependency struct { - projectDependency - branchName string - fullCommitHash string - latestBranchCommit string - latestBranchCommitTime time.Time - latestBranchVersion string - workspaceVersion bool // is the version is defined per workspace or package ? -} - -type analyzedDependencyFunc func(string, analyzedProjectDependency) - -func analyze(dependencies *projectDependencies, analyzedDependencyFunc analyzedDependencyFunc) map[string]analyzedProjectDependency { - out := make(map[string]analyzedProjectDependency) - -outerDependenciesLoop: - for pkg, depInfo := range dependencies.dependencyNames { - // check if we've already analyzed this project before - // ( since multiple dependencies might refer to the same repo) - for _, prevAnalyzedDep := range out { - if prevAnalyzedDep.githubPath == depInfo.githubPath && - prevAnalyzedDep.githubCommit == depInfo.githubCommit && - prevAnalyzedDep.workspaceVersion { - // yes, we did. - out[pkg] = analyzedProjectDependency{ - projectDependency: *depInfo, - branchName: prevAnalyzedDep.branchName, - fullCommitHash: prevAnalyzedDep.fullCommitHash, - latestBranchCommit: prevAnalyzedDep.latestBranchCommit, - latestBranchCommitTime: prevAnalyzedDep.latestBranchCommitTime, - workspaceVersion: prevAnalyzedDep.workspaceVersion, - latestBranchVersion: prevAnalyzedDep.latestBranchVersion, - } - if analyzedDependencyFunc != nil { - analyzedDependencyFunc(pkg, out[pkg]) - } - continue outerDependenciesLoop - } - } - out[pkg] = analyzedDependency(*depInfo) - - if analyzedDependencyFunc != nil { - analyzedDependencyFunc(pkg, out[pkg]) - } - } - - return out -} - -func analyzedDependency(depInfo projectDependency) analyzedProjectDependency { - path := depInfo.githubPath - if !strings.HasPrefix(path, "https://") { - path = "https://" + path - } - repo, err := git.Clone(memory.NewStorage(), nil, &git.CloneOptions{ - URL: path, - Tags: git.AllTags, - }) - if err != nil { - fmt.Printf("unable to clone repository at %s\n", path) - exitErr() - } - - revCommit, err := lookupShortCommit(repo, depInfo.githubCommit) - if err != nil { - exitErr() - } - - branches, err := getBranches(repo) - if err != nil { - exitErr() - } - - latestCommitRef, err := findBranchFromCommit(repo, branches, revCommit) - if err != nil { - exitErr() - } - if latestCommitRef == nil { - if err != nil { - fmt.Printf("unable to find parent branch for logged commit ?! : %v\n", err) - } else { - fmt.Printf("unable to find parent branch for logged commit %s on %s\n", revCommit.Hash.String(), path) - } - exitErr() - } - parentBranchName := strings.ReplaceAll(latestCommitRef.Name().String(), "refs/heads/", "") - - latestCommit, err := repo.CommitObject(latestCommitRef.Hash()) - if err != nil { - fmt.Printf("unable to get latest commit : %v\n", err) - exitErr() - } - - var updatedVersion string - var workspaceVersion bool - if depInfo.class == depClassCargo { - // for cargo versions, we need to look into the actual repository in order to determine - // the earliest version of the most up-to-date version. - latestCommit, updatedVersion, workspaceVersion, err = findLatestVersion(repo, latestCommitRef, revCommit, depInfo.name) - if err != nil { - exitErr() - } - } - - return analyzedProjectDependency{ - projectDependency: depInfo, - branchName: parentBranchName, - fullCommitHash: revCommit.Hash.String(), - latestBranchCommit: latestCommit.Hash.String(), - latestBranchCommitTime: latestCommit.Committer.When.UTC(), - latestBranchVersion: updatedVersion, - workspaceVersion: workspaceVersion, - } -} - -func findBranchFromCommit(repo *git.Repository, branches map[plumbing.Hash]*plumbing.Reference, revCommit *object.Commit) (branch *plumbing.Reference, err error) { - visited := make(map[plumbing.Hash]bool, 0) - for len(branches) > 0 { - for commit, branch := range branches { - if commit.String() == revCommit.Hash.String() { - // we found the branch. - return branch, nil - } - visited[commit] = true - delete(branches, commit) - - parentCommit, err := repo.CommitObject(commit) - if err != nil { - fmt.Printf("unable to get parent commit : %v\n", err) - return nil, err - } - for _, parent := range parentCommit.ParentHashes { - if !visited[parent] { - branches[parent] = branch - } - } - } - } - return nil, nil -} - -func lookupShortCommit(repo *git.Repository, shortCommit string) (revCommit *object.Commit, err error) { - cIter, err := repo.Log(&git.LogOptions{ - All: true, - }) - if err != nil { - fmt.Printf("unable to get log entries for %s: %v\n", shortCommit, err) - return nil, err - } - - // ... just iterates over the commits, looking for a commit with a specific hash. - lookoutCommit := strings.ToLower(shortCommit) - - err = cIter.ForEach(func(c *object.Commit) error { - revString := strings.ToLower(c.Hash.String()) - if strings.HasPrefix(revString, lookoutCommit) { - // found ! - revCommit = c - return storer.ErrStop - } - return nil - }) - if err != nil && err != storer.ErrStop { - fmt.Printf("unable to iterate on log entries : %v\n", err) - exitErr() - } - if revCommit == nil { - fmt.Printf("the commit object for short commit %s was missing ?!\n", lookoutCommit) - exitErr() - } - cIter.Close() - return revCommit, nil -} - -func getBranches(repo *git.Repository) (branches map[plumbing.Hash]*plumbing.Reference, err error) { - remoteOrigin, err := repo.Remote("origin") - if err != nil { - fmt.Printf("unable to retrieve origin remote : %v\n", err) - return nil, err - } - - remoteRefs, err := remoteOrigin.List(&git.ListOptions{}) - if err != nil { - fmt.Printf("unable to list remote refs : %v\n", err) - return nil, err - } - branchPrefix := "refs/heads/" - branches = make(map[plumbing.Hash]*plumbing.Reference, 0) - for _, remoteRef := range remoteRefs { - refName := remoteRef.Name().String() - if !strings.HasPrefix(refName, branchPrefix) { - continue - } - branches[remoteRef.Hash()] = remoteRef - } - return branches, nil -} - -func findLatestVersion(repo *git.Repository, latestCommitRef *plumbing.Reference, revCommit *object.Commit, pkgName string) (updatedLatestCommit *object.Commit, version string, workspaceVersion bool, err error) { - // create a list of all the commits between the head and the current. - commits := []*object.Commit{} - headCommit, err := repo.CommitObject(latestCommitRef.Hash()) - if err != nil { - return nil, "", false, err - } - for { - commits = append(commits, headCommit) - if headCommit.Hash == revCommit.Hash { - // we're done. - break - } - if parent, err := headCommit.Parent(0); err != nil || parent == nil { - break - } else { - headCommit = parent - } - } - - var versions []string - var workspaceVer []bool - for _, commit := range commits { - version, workspaceVersion, err := findCargoVersionForCommit(pkgName, commit) - if err != nil { - return nil, "", false, err - } - versions = append(versions, version) - workspaceVer = append(workspaceVer, workspaceVersion) - } - for i := 1; i < len(versions); i++ { - if versions[i] != versions[i-1] { - // the version at i-1 is "newer", so we should pick that one. - return commits[i-1], versions[i-1], workspaceVer[i-1], nil - } - } - - return commits[len(commits)-1], versions[len(commits)-1], workspaceVer[len(commits)-1], nil -} - -//lint:ignore funlen gocyclo -func findCargoVersionForCommit(pkgName string, commit *object.Commit) (string, bool, error) { - treeRoot, err := commit.Tree() - if err != nil { - return "", false, err - } - rootCargoFile, err := treeRoot.File("Cargo.toml") - if err != nil { - fmt.Printf("The package %s has unsupported repository structure\n", pkgName) - return "", false, errors.New("unsupported repository structure") - } - internalWorkspacePackage := false - - rootCargoFileLines, err := rootCargoFile.Lines() - if err != nil { - return "", false, err - } - var section string - var curPkgName string - for _, line := range rootCargoFileLines { - if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { - section = line[1 : len(line)-1] - continue - } - if strings.HasPrefix(line, "members") { - section = "members" - continue - } - switch section { - case "members": - if strings.Contains(line, pkgName) { - // this is a workspace that points to an internal member; - // the member is the package we're after. - internalWorkspacePackage = true - } - case "workspace.package": - lineParts := strings.Split(line, "=") - if len(lineParts) != 2 { - continue - } - if !strings.HasPrefix(lineParts[0], "version") { - continue - } - version := strings.ReplaceAll(strings.TrimSpace(lineParts[1]), "\"", "") - return version, true, nil - case "package": - lineParts := strings.Split(line, "=") - if len(lineParts) != 2 { - continue - } - if strings.HasPrefix(lineParts[0], "name") { - curPkgName = strings.ReplaceAll(strings.TrimSpace(lineParts[1]), "\"", "") - continue - } else if strings.HasPrefix(lineParts[0], "version") && curPkgName == pkgName { - version := strings.ReplaceAll(strings.TrimSpace(lineParts[1]), "\"", "") - return version, false, nil - } - } - } - // fall-back to package specific versioning. - - if internalWorkspacePackage { - pkgCargoFile, err := treeRoot.File(pkgName + "/Cargo.toml") - if err != nil { - return "", false, err - } - pkgCargoFileLines, err := pkgCargoFile.Lines() - if err != nil { - return "", false, err - } - var section string - var curPkgName string - for _, line := range pkgCargoFileLines { - if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") { - section = line[1 : len(line)-1] - continue - } - switch section { - case "package": - lineParts := strings.Split(line, "=") - if len(lineParts) != 2 { - continue - } - if strings.HasPrefix(lineParts[0], "name") { - curPkgName = strings.ReplaceAll(strings.TrimSpace(lineParts[1]), "\"", "") - continue - } else if strings.HasPrefix(lineParts[0], "version") && curPkgName == pkgName { - version := strings.ReplaceAll(strings.TrimSpace(lineParts[1]), "\"", "") - return version, false, nil - } - } - } - } - fmt.Printf("The package %s has unsupported repository structure\n", pkgName) - return "", false, errors.New("unsupported repository structure") -} diff --git a/cmd/deptool/deptool.go b/cmd/deptool/deptool.go deleted file mode 100644 index cc6599f28..000000000 --- a/cmd/deptool/deptool.go +++ /dev/null @@ -1,85 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" -) - -var versionCheck bool -var projectDir string -var writeChanges bool -var writeChangesInPlace bool - -var rootCmd = &cobra.Command{ - Use: "deptool", - Short: "Repository dependency tool", - Long: `Repository dependency tool`, - Run: func(cmd *cobra.Command, args []string) { - if versionCheck { - fmt.Println("Build version: 1.0") - return - } - - //If no arguments passed, we should fallback to help - cmd.HelpFunc()(cmd, args) - }, -} - -var scanCmd = &cobra.Command{ - Use: "scan", - Short: "scan project dependencies", - Run: func(cmd *cobra.Command, args []string) { - deps := scanProject(projectDir) - printDependencies(deps) - }, -} - -var analyzeCmd = &cobra.Command{ - Use: "analyze", - Short: "analyze project dependencies", - Run: func(cmd *cobra.Command, args []string) { - deps := scanProject(projectDir) - analyzed := analyze(deps, analyzedDepPrinter) - hasChanges := false - // see if any of the dependencies could be upgraded. - for _, dep := range analyzed { - if dep.latestBranchCommit != dep.fullCommitHash { - // yes, it could be upgraded. - hasChanges = true - break - } - } - - if hasChanges { - if writeChanges || writeChangesInPlace { - writeUpdates(projectDir, analyzed, writeChangesInPlace) - } - os.Exit(1) - } - }, -} - -func initCommandHandlers() { - rootCmd.Flags().BoolVarP(&versionCheck, "version", "v", false, "Display and write current build version and exit") - scanCmd.Flags().StringVarP(&projectDir, "directory", "d", ".", "The directory where the project resides") - analyzeCmd.Flags().StringVarP(&projectDir, "directory", "d", ".", "The directory where the project resides") - analyzeCmd.Flags().BoolVarP(&writeChanges, "write", "w", false, "Once analysis is complete, write out the proposed change to Cargo.toml.proposed and go.mod.proposed") - analyzeCmd.Flags().BoolVarP(&writeChangesInPlace, "writeInPlace", "p", false, "Once analysis is complete, write out the changes to the existing Cargo.toml and go.mod") - - rootCmd.AddCommand(scanCmd) - rootCmd.AddCommand(analyzeCmd) -} - -func main() { - initCommandHandlers() - if err := rootCmd.Execute(); err != nil { - fmt.Println(err) - exitErr() - } -} - -func exitErr() { - os.Exit(-1) -} diff --git a/cmd/deptool/printer.go b/cmd/deptool/printer.go deleted file mode 100644 index 19c59095d..000000000 --- a/cmd/deptool/printer.go +++ /dev/null @@ -1,81 +0,0 @@ -package main - -import "fmt" - -const ( - colorReset = "\033[0m" - //colorRed = "\033[31m" - colorGreen = "\033[32m" - colorYellow = "\033[33m" - //colorBlue = "\033[34m" - colorPurple = "\033[35m" - colorCyan = "\033[36m" - colorWhite = "\033[37m" -) - -func printDependencies(dependencies *projectDependencies) { - for _, dep := range dependencies.dependencies { - var version string - if dep.version != "" { - version = fmt.Sprintf(" %s%s", colorGreen, dep.version) - } - fmt.Printf("%s %s %s[%s%s%s@%s%s%s%s]%s\n", - colorGreen, - dep.name, - colorYellow, - colorCyan, - dep.githubPath, - colorWhite, - colorPurple, - dep.githubCommit, - version, - colorYellow, - colorReset) - } -} - -func analyzedDepPrinter(pkg string, dep analyzedProjectDependency) { - var version, latestBranchVersion string - if dep.version != "" { - version = fmt.Sprintf(" %s%s", colorGreen, dep.version) - } - // do we have an upgrade ? - if dep.fullCommitHash == dep.latestBranchCommit { - fmt.Printf("%s %s %s[%s%s%s@%s%s%s%s]%s\n", - colorGreen, - pkg, - colorYellow, - colorCyan, - dep.githubPath, - colorWhite, - colorPurple, - dep.githubCommit, - version, - colorYellow, - colorReset) - return - } - - if dep.latestBranchVersion != "" { - latestBranchVersion = fmt.Sprintf(" %s%s", colorGreen, dep.latestBranchVersion) - } - fmt.Printf("%s %s %s[%s%s%s@%s%s%s%s]%s Upgrade %s[%s%s%s%s]%s\n", - colorGreen, - pkg, - colorYellow, - colorCyan, - dep.githubPath, - colorWhite, - colorPurple, - dep.githubCommit, - version, - colorYellow, - colorReset, - colorYellow, - colorPurple, - dep.latestBranchCommit[:len(dep.githubCommit)], - latestBranchVersion, - colorYellow, - colorReset, - ) -} diff --git a/cmd/deptool/scanner.go b/cmd/deptool/scanner.go deleted file mode 100644 index 5045ff60f..000000000 --- a/cmd/deptool/scanner.go +++ /dev/null @@ -1,159 +0,0 @@ -package main - -import ( - "fmt" - "os" - "path" - "sort" - "strings" - - toml "github.com/pelletier/go-toml" - modfile "golang.org/x/mod/modfile" -) - -const cargoTomlFile = "Cargo.toml" -const goModFile = "go.mod" - -type depClass int - -const ( - depClassCargo depClass = iota - depClassMod -) - -type projectDependencies struct { - dependencies []*projectDependency - dependencyNames map[string]*projectDependency -} - -type projectDependency struct { - class depClass - githubPath string - githubCommit string - direct bool - version string - name string -} - -type cargoDependencyToml struct { - Git string `toml:"git"` - Rev string `toml:"rev"` - Version string `toml:"version"` -} - -type workspaceDepenenciesToml struct { - Dependencies map[string]cargoDependencyToml `toml:"dependencies"` -} - -type patchCratesIOToml struct { - CratesIO map[string]cargoDependencyToml `toml:"crates-io"` -} - -type cargoToml struct { - Workspace workspaceDepenenciesToml // this is the workspace.dependencies entry; the toml decoder breaks it into workspace and dependencies - Patch patchCratesIOToml // this is the patch.crates-io entry -} - -func scanProject(dir string) *projectDependencies { - dependencies := &projectDependencies{ - dependencyNames: make(map[string]*projectDependency), - } - - loadParseCargoToml(dir, dependencies) - loadParseGoMod(dir, dependencies) - - return dependencies -} - -func loadParseCargoToml(dir string, dependencies *projectDependencies) { - cargoFileBytes, err := os.ReadFile(path.Join(dir, cargoTomlFile)) - if err != nil { - fmt.Printf("Unable to read Cargo.toml file : %v\n", err) - exitErr() - } - - var parsedCargo cargoToml - err = toml.Unmarshal(cargoFileBytes, &parsedCargo) - if err != nil { - fmt.Printf("Unable to parse Cargo.toml file : %v\n", err) - exitErr() - } - addTomlDependencies(dependencies, parsedCargo.Patch.CratesIO, false) - addTomlDependencies(dependencies, parsedCargo.Workspace.Dependencies, true) -} - -func addTomlDependencies(dependencies *projectDependencies, tomlDeps map[string]cargoDependencyToml, direct bool) { - names := make([]string, 0, len(tomlDeps)) - for name := range tomlDeps { - names = append(names, name) - } - sort.Strings(names) - for _, pkgName := range names { - crateGit := tomlDeps[pkgName] - if crateGit.Git == "" { - continue - } - - current := &projectDependency{ - class: depClassCargo, - githubPath: crateGit.Git, - githubCommit: crateGit.Rev, - version: crateGit.Version, - direct: direct, - name: pkgName, - } - if existing, has := dependencies.dependencyNames[pkgName]; has && (existing.githubCommit != current.githubCommit || existing.githubPath != current.githubPath) { - fmt.Printf("Conflicting entries in Cargo.toml file :\n%v\nvs.\n%v\n", existing, current) - exitErr() - } - if current.githubPath == "" { - continue - } - dependencies.dependencyNames[pkgName] = current - dependencies.dependencies = append(dependencies.dependencies, current) - } -} - -func loadParseGoMod(dir string, dependencies *projectDependencies) { - fileName := path.Join(dir, goModFile) - - cargoFileBytes, err := os.ReadFile(fileName) - if err != nil { - fmt.Printf("Unable to read go.mod file : %v\n", err) - exitErr() - } - - modFile, err := modfile.Parse("", cargoFileBytes, nil) - if err != nil { - fmt.Printf("Unable to read go.mod file : %v\n", err) - exitErr() - } - // scan all the stellar related required modules. - for _, require := range modFile.Require { - if !strings.Contains(require.Mod.Path, "github.com/stellar") || require.Indirect { - continue - } - splittedVersion := strings.Split(require.Mod.Version, "-") - if len(splittedVersion) != 3 { - continue - } - - pathComp := strings.Split(require.Mod.Path, "/") - pkgName := pathComp[len(pathComp)-1] - - current := &projectDependency{ - class: depClassMod, - githubPath: require.Mod.Path, - githubCommit: splittedVersion[2], - direct: true, - name: pkgName, - } - - if existing, has := dependencies.dependencyNames[pkgName]; has && (existing.githubCommit != current.githubCommit || existing.githubPath != current.githubPath) { - fmt.Printf("Conflicting entries in go.mod file :\n%v\nvs.\n%v\n", existing, current) - exitErr() - } - dependencies.dependencyNames[pkgName] = current - dependencies.dependencies = append(dependencies.dependencies, current) - } -} diff --git a/cmd/deptool/writeout.go b/cmd/deptool/writeout.go deleted file mode 100644 index 38e2e002c..000000000 --- a/cmd/deptool/writeout.go +++ /dev/null @@ -1,137 +0,0 @@ -package main - -import ( - "bytes" - "fmt" - "os" - "path" - "strings" - - modfile "golang.org/x/mod/modfile" -) - -func writeUpdates(dir string, deps map[string]analyzedProjectDependency, inplace bool) { - writeUpdatesGoMod(dir, deps, inplace) - writeUpdatesCargoToml(dir, deps, inplace) -} - -func writeUpdatesGoMod(dir string, deps map[string]analyzedProjectDependency, inplace bool) { - fileName := path.Join(dir, goModFile) - - modFileBytes, err := os.ReadFile(fileName) - if err != nil { - fmt.Printf("Unable to read go.mod file : %v\n", err) - exitErr() - } - - modFile, err := modfile.Parse("", modFileBytes, nil) - if err != nil { - fmt.Printf("Unable to read go.mod file : %v\n", err) - exitErr() - } - - changed := false - for _, analyzed := range deps { - if analyzed.class != depClassMod { - continue - } - if analyzed.latestBranchCommit == analyzed.githubCommit { - continue - } - // find if we have entry in the mod file. - for _, req := range modFile.Require { - if req.Mod.Path != analyzed.githubPath { - continue - } - // this entry needs to be updated. - splittedVersion := strings.Split(req.Mod.Version, "-") - splittedVersion[2] = analyzed.latestBranchCommit[:len(splittedVersion[2])] - splittedVersion[1] = fmt.Sprintf("%04d%02d%02d%02d%02d%02d", - analyzed.latestBranchCommitTime.Year(), - analyzed.latestBranchCommitTime.Month(), - analyzed.latestBranchCommitTime.Day(), - analyzed.latestBranchCommitTime.Hour(), - analyzed.latestBranchCommitTime.Minute(), - analyzed.latestBranchCommitTime.Second()) - newVer := fmt.Sprintf("%s-%s-%s", splittedVersion[0], splittedVersion[1], splittedVersion[2]) - curPath := req.Mod.Path - err = modFile.DropRequire(req.Mod.Path) - if err != nil { - fmt.Printf("Unable to drop requirement : %v\n", err) - exitErr() - } - err = modFile.AddRequire(curPath, newVer) - if err != nil { - fmt.Printf("Unable to add requirement : %v\n", err) - exitErr() - } - changed = true - } - } - - if !changed { - return - } - - outputBytes, err := modFile.Format() - if err != nil { - fmt.Printf("Unable to format mod file : %v\n", err) - exitErr() - } - if !inplace { - fileName += ".proposed" - } - err = os.WriteFile(fileName, outputBytes, 0200) - if err != nil { - fmt.Printf("Unable to write %s file : %v\n", fileName, err) - exitErr() - } - err = os.Chmod(fileName, 0644) - if err != nil { - fmt.Printf("Unable to chmod %s file : %v\n", fileName, err) - exitErr() - } -} - -func writeUpdatesCargoToml(dir string, deps map[string]analyzedProjectDependency, inplace bool) { - fileName := path.Join(dir, cargoTomlFile) - - modFileBytes, err := os.ReadFile(fileName) - if err != nil { - fmt.Printf("Unable to read go.mod file : %v\n", err) - exitErr() - } - - changed := false - for _, analyzed := range deps { - if analyzed.class != depClassCargo { - continue - } - if analyzed.latestBranchCommit == analyzed.githubCommit { - continue - } - newCommit := analyzed.latestBranchCommit[:len(analyzed.githubCommit)] - // we want to replace every instance of analyzed.githubCommit with newCommit - modFileBytes = bytes.ReplaceAll(modFileBytes, []byte(analyzed.githubCommit), []byte(newCommit)) - - // set the changed flag - changed = true - } - - if !changed { - return - } - if !inplace { - fileName = fileName + ".proposed" - } - err = os.WriteFile(fileName, modFileBytes, 0200) - if err != nil { - fmt.Printf("Unable to write %s file : %v\n", fileName, err) - exitErr() - } - err = os.Chmod(fileName, 0644) - if err != nil { - fmt.Printf("Unable to chmod %s file : %v\n", fileName, err) - exitErr() - } -} diff --git a/cmd/soroban-rpc/README.md b/cmd/soroban-rpc/README.md deleted file mode 100644 index da2baf4e0..000000000 --- a/cmd/soroban-rpc/README.md +++ /dev/null @@ -1,58 +0,0 @@ -# Soroban-RPC - -Soroban-RPC allows you to communicate directly with Soroban via a JSON RPC interface. - -For example, you can build an application and have it send a transaction, get ledger and event data or simulate transactions. - -## Dependencies - - [Git](https://git-scm.com/downloads) - - [Go](https://golang.org/doc/install) - - [Rust](https://www.rust-lang.org/tools/install) - - [Cargo](https://doc.rust-lang.org/cargo/getting-started/installation.html) - -## Building Stellar-Core -Soroban-RPC requires an instance of stellar-core binary on the same host. This is referred to as the `Captive Core`. -Since, we are building RPC from source, we recommend considering two approaches to get the stellar-core binary: -- If saving time is top priority and your development machine is on a linux debian OS, then consider installing the -testnet release candidates from the [testing repository.](https://apt.stellar.org/pool/unstable/s/stellar-core/) -- The recommended option is to compile the core source directly on your machine: - - Clone the stellar-core repo: - ```bash - git clone https://github.com/stellar/stellar-core.git - cd stellar-core - ``` - - Fetch the tags and checkout the testnet release tag: - ```bash - git fetch --tags - git checkout tags/v20.0.0-rc.2.1 -b soroban-testnet-release - ``` - - Follow the build steps listed in [INSTALL.md](https://github.com/stellar/stellar-core/blob/master/INSTALL.md) file for the instructions on building the local binary - -## Building Soroban-RPC -- Similar to stellar-core, we will clone the soroban-tools repo and checkout the testnet release tag: -```bash -git clone https://github.com/stellar/soroban-tools.git -cd soroban-tools -git fetch --tags -git checkout tags/v20.0.0-rc4 -b soroban-testnet-release -``` -- Build soroban-rpc target: -```bash -make build-soroban-rpc -``` -This will install and build the required dependencies and generate a `soroban-rpc` binary in the working directory. - -## Configuring and Running RPC Server -- Both stellar-core and soroban-rpc require configuration files to run. - - For production, we specifically recommend running Soroban RPC with a TOML configuration file rather than CLI flags. - - There is a new subcommand `gen-config-file` which takes all the same arguments as the root command (or no arguments at all), - and outputs the resulting config toml file to stdout. - ```bash - ./soroban-rpc gen-config-file - ``` - - Paste the output to a file and save it as `.toml` file in any directory. - - Make sure to update the config values to testnet specific ones. You can refer to [Configuring](https://docs.google.com/document/d/1SIbrFWFgju5RAsi6stDyEtgTa78VEt8f3HhqCLoySx4/edit#heading=h.80d1jdtd7ktj) section in the Runbook for specific config settings. -- If everything is set up correctly, then you can run the RPC server with the following command: -```bash -./soroban-rpc --config-path -``` \ No newline at end of file diff --git a/cmd/soroban-rpc/docker/Dockerfile b/cmd/soroban-rpc/docker/Dockerfile deleted file mode 100644 index 0b0cc231b..000000000 --- a/cmd/soroban-rpc/docker/Dockerfile +++ /dev/null @@ -1,39 +0,0 @@ -FROM golang:1.21-bullseye as build -ARG RUST_TOOLCHAIN_VERSION=stable -ARG REPOSITORY_VERSION - -WORKDIR /go/src/github.com/stellar/soroban-tools - -ADD . ./ - -RUN git config --global --add safe.directory "/go/src/github.com/stellar/soroban-tools" - -ENV CARGO_HOME=/rust/.cargo -ENV RUSTUP_HOME=/rust/.rust -ENV PATH="/usr/local/go/bin:$CARGO_HOME/bin:${PATH}" -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update -RUN apt-get install -y build-essential -RUN apt-get clean - -RUN curl https://sh.rustup.rs -sSf | sh -s -- -y --default-toolchain $RUST_TOOLCHAIN_VERSION - -RUN make REPOSITORY_VERSION=${REPOSITORY_VERSION} build-soroban-rpc -RUN mv soroban-rpc /bin/soroban-rpc - -FROM ubuntu:22.04 -ARG STELLAR_CORE_VERSION -ENV STELLAR_CORE_VERSION=${STELLAR_CORE_VERSION:-*} -ENV STELLAR_CORE_BINARY_PATH /usr/bin/stellar-core -ENV DEBIAN_FRONTEND=noninteractive - -# ca-certificates are required to make tls connections -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils -RUN wget -qO - https://apt.stellar.org/SDF.asc | APT_KEY_DONT_WARN_ON_DANGEROUS_USAGE=true apt-key add - -RUN echo "deb https://apt.stellar.org focal stable" >/etc/apt/sources.list.d/SDF.list -RUN echo "deb https://apt.stellar.org focal unstable" >/etc/apt/sources.list.d/SDF-unstable.list -RUN apt-get update && apt-get install -y stellar-core=${STELLAR_CORE_VERSION} -RUN apt-get clean - -COPY --from=build /bin/soroban-rpc /app/ -ENTRYPOINT ["/app/soroban-rpc"] diff --git a/cmd/soroban-rpc/docker/Dockerfile.release b/cmd/soroban-rpc/docker/Dockerfile.release deleted file mode 100644 index de894a8b3..000000000 --- a/cmd/soroban-rpc/docker/Dockerfile.release +++ /dev/null @@ -1,20 +0,0 @@ -FROM ubuntu:22.04 -ARG STELLAR_CORE_VERSION -ENV STELLAR_CORE_VERSION=${STELLAR_CORE_VERSION:-*} -ARG SOROBAN_RPC_VERSION -ENV SOROBAN_RPC_VERSION=${SOROBAN_RPC_VERSION:-*} - -ENV STELLAR_CORE_BINARY_PATH /usr/bin/stellar-core -ENV DEBIAN_FRONTEND=noninteractive - -# ca-certificates are required to make tls connections -RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates curl wget gnupg apt-utils gpg && \ - curl -sSL https://apt.stellar.org/SDF.asc | gpg --dearmor >/etc/apt/trusted.gpg.d/SDF.gpg && \ - echo "deb https://apt.stellar.org focal stable" >/etc/apt/sources.list.d/SDF.list && \ - echo "deb https://apt.stellar.org focal testing" >/etc/apt/sources.list.d/SDF-testing.list && \ - echo "deb https://apt.stellar.org focal unstable" >/etc/apt/sources.list.d/SDF-unstable.list && \ - apt-get update && \ - apt-get install -y stellar-core=${STELLAR_CORE_VERSION} stellar-soroban-rpc=${SOROBAN_RPC_VERSION} && \ - apt-get clean - -ENTRYPOINT ["/usr/bin/stellar-soroban-rpc"] diff --git a/cmd/soroban-rpc/docker/Makefile b/cmd/soroban-rpc/docker/Makefile deleted file mode 100644 index b95af2b13..000000000 --- a/cmd/soroban-rpc/docker/Makefile +++ /dev/null @@ -1,34 +0,0 @@ -SUDO := $(shell docker version >/dev/null 2>&1 || echo "sudo") - -# https://github.com/opencontainers/image-spec/blob/master/annotations.md -BUILD_DATE := $(shell date -u +%FT%TZ) - -# Extract latest release semver from GitHub -SOROBAN_RPC_LATEST_RELEASE := $(shell curl -sS https://api.github.com/repos/stellar/soroban-tools/releases/latest|jq -r ".tag_name"| tr -d "v" ) - -# If deb version was provided via the SOROBAN_RPC_VERSION variable use it. -# If not get latest deb build matching release from GitHub -ifndef SOROBAN_RPC_VERSION - SOROBAN_RPC_VERSION_PACKAGE_VERSION := $(shell curl -sS https://apt.stellar.org/dists/focal/unstable/binary-amd64/Packages|grep -A 18 stellar-soroban-rpc|grep Version|grep $(SOROBAN_RPC_LATEST_RELEASE)|head -1|cut -d' ' -f2 ) -else - SOROBAN_RPC_VERSION_PACKAGE_VERSION := $(SOROBAN_RPC_VERSION) -endif - -ifndef SOROBAN_RPC_VERSION_PACKAGE_VERSION - $(error Couldn't establish deb build from version $(SOROBAN_RPC_LATEST_RELEASE). Has the package been built?) -endif - -ifndef STELLAR_CORE_VERSION - $(error STELLAR_CORE_VERSION environment variable must be set. For example 19.10.1-1310.6649f5173.focal~soroban) -endif - -TAG ?= stellar/stellar-soroban-rpc:$(SOROBAN_RPC_VERSION_PACKAGE_VERSION) - -docker-build: - $(SUDO) docker build --pull --platform linux/amd64 $(DOCKER_OPTS) \ - --label org.opencontainers.image.created="$(BUILD_DATE)" \ - --build-arg STELLAR_CORE_VERSION=$(STELLAR_CORE_VERSION) --build-arg SOROBAN_RPC_VERSION=$(SOROBAN_RPC_VERSION_PACKAGE_VERSION) \ - -t $(TAG) -f Dockerfile.release . - -docker-push: - $(SUDO) docker push $(TAG) diff --git a/cmd/soroban-rpc/internal/config/config.go b/cmd/soroban-rpc/internal/config/config.go deleted file mode 100644 index 1f89ab2b5..000000000 --- a/cmd/soroban-rpc/internal/config/config.go +++ /dev/null @@ -1,164 +0,0 @@ -package config - -import ( - "os" - "time" - - "github.com/sirupsen/logrus" - "github.com/spf13/pflag" -) - -// Config represents the configuration of a soroban-rpc server -type Config struct { - ConfigPath string - - Strict bool - - StellarCoreURL string - CaptiveCoreStoragePath string - StellarCoreBinaryPath string - CaptiveCoreConfigPath string - CaptiveCoreHTTPPort uint - - Endpoint string - AdminEndpoint string - CheckpointFrequency uint32 - CoreRequestTimeout time.Duration - DefaultEventsLimit uint - EventLedgerRetentionWindow uint32 - FriendbotURL string - HistoryArchiveURLs []string - IngestionTimeout time.Duration - LogFormat LogFormat - LogLevel logrus.Level - MaxEventsLimit uint - MaxHealthyLedgerLatency time.Duration - NetworkPassphrase string - PreflightWorkerCount uint - PreflightWorkerQueueSize uint - PreflightEnableDebug bool - SQLiteDBPath string - TransactionLedgerRetentionWindow uint32 - RequestBacklogGlobalQueueLimit uint - RequestBacklogGetHealthQueueLimit uint - RequestBacklogGetEventsQueueLimit uint - RequestBacklogGetNetworkQueueLimit uint - RequestBacklogGetLatestLedgerQueueLimit uint - RequestBacklogGetLedgerEntriesQueueLimit uint - RequestBacklogGetTransactionQueueLimit uint - RequestBacklogSendTransactionQueueLimit uint - RequestBacklogSimulateTransactionQueueLimit uint - RequestExecutionWarningThreshold time.Duration - MaxRequestExecutionDuration time.Duration - MaxGetHealthExecutionDuration time.Duration - MaxGetEventsExecutionDuration time.Duration - MaxGetNetworkExecutionDuration time.Duration - MaxGetLatestLedgerExecutionDuration time.Duration - MaxGetLedgerEntriesExecutionDuration time.Duration - MaxGetTransactionExecutionDuration time.Duration - MaxSendTransactionExecutionDuration time.Duration - MaxSimulateTransactionExecutionDuration time.Duration - - // We memoize these, so they bind to pflags correctly - optionsCache *ConfigOptions - flagset *pflag.FlagSet -} - -func (cfg *Config) SetValues(lookupEnv func(string) (string, bool)) error { - // We start with the defaults - if err := cfg.loadDefaults(); err != nil { - return err - } - - // Then we load from the environment variables and cli flags, to try to find - // the config file path - if err := cfg.loadEnv(lookupEnv); err != nil { - return err - } - if err := cfg.loadFlags(); err != nil { - return err - } - - // If we specified a config file, we load that - if cfg.ConfigPath != "" { - // Merge in the config file flags - if err := cfg.loadConfigPath(); err != nil { - return err - } - - // Load from cli flags and environment variables again, to overwrite what we - // got from the config file - if err := cfg.loadEnv(lookupEnv); err != nil { - return err - } - if err := cfg.loadFlags(); err != nil { - return err - } - } - - return nil -} - -// loadDefaults populates the config with default values -func (cfg *Config) loadDefaults() error { - for _, option := range cfg.options() { - if option.DefaultValue != nil { - if err := option.setValue(option.DefaultValue); err != nil { - return err - } - } - } - return nil -} - -// loadEnv populates the config with values from the environment variables -func (cfg *Config) loadEnv(lookupEnv func(string) (string, bool)) error { - for _, option := range cfg.options() { - key, ok := option.getEnvKey() - if !ok { - continue - } - value, ok := lookupEnv(key) - if !ok { - continue - } - if err := option.setValue(value); err != nil { - return err - } - } - return nil -} - -// loadFlags populates the config with values from the cli flags -func (cfg *Config) loadFlags() error { - for _, option := range cfg.options() { - if option.flag == nil || !option.flag.Changed { - continue - } - val, err := option.GetFlag(cfg.flagset) - if err != nil { - return err - } - if err := option.setValue(val); err != nil { - return err - } - } - return nil -} - -// loadConfigPath loads a new config from a toml file at the given path. Strict -// mode will return an error if there are any unknown toml variables set. Note, -// strict-mode can also be set by putting `STRICT=true` in the config.toml file -// itself. -func (cfg *Config) loadConfigPath() error { - file, err := os.Open(cfg.ConfigPath) - if err != nil { - return err - } - defer file.Close() - return parseToml(file, cfg.Strict, cfg) -} - -func (cfg *Config) Validate() error { - return cfg.options().Validate() -} diff --git a/cmd/soroban-rpc/internal/config/config_option.go b/cmd/soroban-rpc/internal/config/config_option.go deleted file mode 100644 index 3c8ca87a4..000000000 --- a/cmd/soroban-rpc/internal/config/config_option.go +++ /dev/null @@ -1,142 +0,0 @@ -package config - -import ( - "fmt" - "reflect" - "strconv" - "time" - - "github.com/spf13/pflag" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/strutils" -) - -// ConfigOptions is a group of ConfigOptions that can be for convenience -// initialized and set at the same time. -type ConfigOptions []*ConfigOption - -// Validate all the config options. -func (options ConfigOptions) Validate() error { - var missingOptions []errMissingRequiredOption - for _, option := range options { - if option.Validate != nil { - err := option.Validate(option) - if err == nil { - continue - } - if missingOption, ok := err.(errMissingRequiredOption); ok { - missingOptions = append(missingOptions, missingOption) - continue - } - return errors.Wrap(err, fmt.Sprintf("Invalid config value for %s", option.Name)) - } - } - if len(missingOptions) > 0 { - // we had one or more missing options, combine these all into a single error. - errString := "The following required configuration parameters are missing:" - for _, missingOpt := range missingOptions { - errString += "\n*\t" + missingOpt.strErr - errString += "\n \t" + missingOpt.usage - } - return &errMissingRequiredOption{strErr: errString} - } - return nil -} - -// ConfigOption is a complete description of the configuration of a command line option -type ConfigOption struct { - Name string // e.g. "database-url" - EnvVar string // e.g. "DATABASE_URL". Defaults to uppercase/underscore representation of name - TomlKey string // e.g. "DATABASE_URL". Defaults to uppercase/underscore representation of name. - to omit from toml - Usage string // Help text - DefaultValue interface{} // A default if no option is provided. Omit or set to `nil` if no default - ConfigKey interface{} // Pointer to the final key in the linked Config struct - CustomSetValue func(*ConfigOption, interface{}) error // Optional function for custom validation/transformation - Validate func(*ConfigOption) error // Function called after loading all options, to validate the configuration - MarshalTOML func(*ConfigOption) (interface{}, error) - - flag *pflag.Flag // The persistent flag that the config option is attached to -} - -// Returns false if this option is omitted in the toml -func (o ConfigOption) getTomlKey() (string, bool) { - if o.TomlKey == "-" || o.TomlKey == "_" { - return "", false - } - if o.TomlKey != "" { - return o.TomlKey, true - } - if envVar, ok := o.getEnvKey(); ok { - return envVar, true - } - return strutils.KebabToConstantCase(o.Name), true -} - -// Returns false if this option is omitted in the env -func (o ConfigOption) getEnvKey() (string, bool) { - if o.EnvVar == "-" || o.EnvVar == "_" { - return "", false - } - if o.EnvVar != "" { - return o.EnvVar, true - } - return strutils.KebabToConstantCase(o.Name), true -} - -// TODO: See if we can remove CustomSetValue into just SetValue/ParseValue -func (o *ConfigOption) setValue(i interface{}) (err error) { - if o.CustomSetValue != nil { - return o.CustomSetValue(o, i) - } - defer func() { - if recoverRes := recover(); recoverRes != nil { - var ok bool - if err, ok = recoverRes.(error); ok { - return - } - - err = errors.Errorf("config option setting error ('%s') %v", o.Name, recoverRes) - } - }() - parser := func(option *ConfigOption, i interface{}) error { - return errors.Errorf("no parser for flag %s", o.Name) - } - switch o.ConfigKey.(type) { - case *bool: - parser = parseBool - case *int, *int8, *int16, *int32, *int64: - parser = parseInt - case *uint, *uint8, *uint16, *uint32: - parser = parseUint32 - case *uint64: - parser = parseUint - case *float32, *float64: - parser = parseFloat - case *string: - parser = parseString - case *[]string: - parser = parseStringSlice - case *time.Duration: - parser = parseDuration - } - - return parser(o, i) -} - -func (o *ConfigOption) marshalTOML() (interface{}, error) { - if o.MarshalTOML != nil { - return o.MarshalTOML(o) - } - // go-toml doesn't handle ints other than `int`, so we have to do that ourselves. - switch v := o.ConfigKey.(type) { - case *int, *int8, *int16, *int32, *int64: - return []byte(strconv.FormatInt(reflect.ValueOf(v).Elem().Int(), 10)), nil - case *uint, *uint8, *uint16, *uint32, *uint64: - return []byte(strconv.FormatUint(reflect.ValueOf(v).Elem().Uint(), 10)), nil - case *time.Duration: - return v.String(), nil - default: - // Unknown, hopefully go-toml knows what to do with it! :crossed_fingers: - return reflect.ValueOf(o.ConfigKey).Elem().Interface(), nil - } -} diff --git a/cmd/soroban-rpc/internal/config/config_option_test.go b/cmd/soroban-rpc/internal/config/config_option_test.go deleted file mode 100644 index a6309cb3e..000000000 --- a/cmd/soroban-rpc/internal/config/config_option_test.go +++ /dev/null @@ -1,271 +0,0 @@ -package config - -import ( - "fmt" - "math" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestConfigOptionGetTomlKey(t *testing.T) { - // Explicitly set toml key - key, ok := ConfigOption{TomlKey: "TOML_KEY"}.getTomlKey() - assert.Equal(t, "TOML_KEY", key) - assert.True(t, ok) - - // Explicitly disabled toml key via `-` - key, ok = ConfigOption{TomlKey: "-"}.getTomlKey() - assert.Equal(t, "", key) - assert.False(t, ok) - - // Explicitly disabled toml key via `_` - key, ok = ConfigOption{TomlKey: "_"}.getTomlKey() - assert.Equal(t, "", key) - assert.False(t, ok) - - // Fallback to env var - key, ok = ConfigOption{EnvVar: "ENV_VAR"}.getTomlKey() - assert.Equal(t, "ENV_VAR", key) - assert.True(t, ok) - - // Env-var disabled, autogenerate from name - key, ok = ConfigOption{Name: "test-flag", EnvVar: "-"}.getTomlKey() - assert.Equal(t, "TEST_FLAG", key) - assert.True(t, ok) - - // Env-var not set, autogenerate from name - key, ok = ConfigOption{Name: "test-flag"}.getTomlKey() - assert.Equal(t, "TEST_FLAG", key) - assert.True(t, ok) -} - -func TestValidateRequired(t *testing.T) { - var strVal string - o := &ConfigOption{ - Name: "required-option", - ConfigKey: &strVal, - Validate: required, - } - - // unset - assert.ErrorContains(t, o.Validate(o), "required-option is required") - - // set with blank value - require.NoError(t, o.setValue("")) - assert.ErrorContains(t, o.Validate(o), "required-option is required") - - // set with valid value - require.NoError(t, o.setValue("not-blank")) - assert.NoError(t, o.Validate(o)) -} - -func TestValidatePositiveUint32(t *testing.T) { - var val uint32 - o := &ConfigOption{ - Name: "positive-option", - ConfigKey: &val, - Validate: positive, - } - - // unset - assert.ErrorContains(t, o.Validate(o), "positive-option must be positive") - - // set with 0 value - require.NoError(t, o.setValue(uint32(0))) - assert.ErrorContains(t, o.Validate(o), "positive-option must be positive") - - // set with valid value - require.NoError(t, o.setValue(uint32(1))) - assert.NoError(t, o.Validate(o)) -} - -func TestValidatePositiveInt(t *testing.T) { - var val int - o := &ConfigOption{ - Name: "positive-option", - ConfigKey: &val, - Validate: positive, - } - - // unset - assert.ErrorContains(t, o.Validate(o), "positive-option must be positive") - - // set with 0 value - require.NoError(t, o.setValue(0)) - assert.ErrorContains(t, o.Validate(o), "positive-option must be positive") - - // set with negative value - require.NoError(t, o.setValue(-1)) - assert.ErrorContains(t, o.Validate(o), "positive-option must be positive") - - // set with valid value - require.NoError(t, o.setValue(1)) - assert.NoError(t, o.Validate(o)) -} - -func TestUnassignableField(t *testing.T) { - var co ConfigOption - var b bool - co.Name = "mykey" - co.ConfigKey = &b - err := co.setValue("abc") - require.Error(t, err) - require.Contains(t, err.Error(), co.Name) -} - -func TestNoParserForFlag(t *testing.T) { - var co ConfigOption - var invalidKey []time.Duration - co.Name = "mykey" - co.ConfigKey = &invalidKey - err := co.setValue("abc") - require.Error(t, err) - require.Contains(t, err.Error(), "no parser for flag mykey") -} - -func TestSetValue(t *testing.T) { - var b bool - var i int - var u32 uint32 - var u64 uint64 - var f64 float64 - var s string - - for _, scenario := range []struct { - name string - key interface{} - value interface{} - err error - }{ - { - name: "valid-bool", - key: &b, - value: true, - err: nil, - }, - { - name: "valid-bool-string", - key: &b, - value: "true", - err: nil, - }, - { - name: "valid-bool-string-false", - key: &b, - value: "false", - err: nil, - }, - { - name: "valid-bool-string-uppercase", - key: &b, - value: "TRUE", - err: nil, - }, - { - name: "invalid-bool-string", - key: &b, - value: "foobar", - err: fmt.Errorf("invalid boolean value invalid-bool-string: foobar"), - }, - { - name: "invalid-bool-string", - key: &b, - value: "foobar", - err: fmt.Errorf("invalid boolean value invalid-bool-string: foobar"), - }, - { - name: "valid-int", - key: &i, - value: 1, - err: nil, - }, - { - name: "valid-int-string", - key: &i, - value: "1", - err: nil, - }, - { - name: "invalid-int-string", - key: &i, - value: "abcd", - err: fmt.Errorf("strconv.ParseInt: parsing \"abcd\": invalid syntax"), - }, - { - name: "valid-uint32", - key: &u32, - value: 1, - err: nil, - }, - { - name: "overflow-uint32", - key: &u32, - value: uint64(math.MaxUint32) + 1, - err: fmt.Errorf("overflow-uint32 overflows uint32"), - }, - { - name: "negative-uint32", - key: &u32, - value: -1, - err: fmt.Errorf("negative-uint32 cannot be negative"), - }, - { - name: "valid-uint", - key: &u64, - value: 1, - err: nil, - }, - { - name: "negative-uint", - key: &u64, - value: -1, - err: fmt.Errorf("negative-uint cannot be negative"), - }, - { - name: "valid-float", - key: &f64, - value: 1.05, - err: nil, - }, - { - name: "valid-float-int", - key: &f64, - value: int64(1234), - err: nil, - }, - { - name: "valid-float-string", - key: &f64, - value: "1.05", - err: nil, - }, - { - name: "invalid-float-string", - key: &f64, - value: "foobar", - err: fmt.Errorf("strconv.ParseFloat: parsing \"foobar\": invalid syntax"), - }, - { - name: "valid-string", - key: &s, - value: "foobar", - err: nil, - }, - } { - t.Run(scenario.name, func(t *testing.T) { - co := ConfigOption{ - Name: scenario.name, - ConfigKey: scenario.key, - } - err := co.setValue(scenario.value) - if scenario.err != nil { - require.EqualError(t, err, scenario.err.Error()) - } else { - require.NoError(t, err) - } - }) - } -} diff --git a/cmd/soroban-rpc/internal/config/config_test.go b/cmd/soroban-rpc/internal/config/config_test.go deleted file mode 100644 index 67769a3ca..000000000 --- a/cmd/soroban-rpc/internal/config/config_test.go +++ /dev/null @@ -1,79 +0,0 @@ -package config - -import ( - "runtime" - "testing" - "time" - - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -func TestLoadConfigPathPrecedence(t *testing.T) { - var cfg Config - - cmd := &cobra.Command{} - require.NoError(t, cfg.AddFlags(cmd)) - require.NoError(t, cmd.ParseFlags([]string{ - "--config-path", "./test.soroban.rpc.config", - "--stellar-core-binary-path", "/usr/overridden/stellar-core", - "--network-passphrase", "CLI test passphrase", - })) - - require.NoError(t, cfg.SetValues(func(key string) (string, bool) { - switch key { - case "STELLAR_CORE_BINARY_PATH": - return "/env/stellar-core", true - case "DB_PATH": - return "/env/overridden/db", true - default: - return "", false - } - })) - require.NoError(t, cfg.Validate()) - - assert.Equal(t, "/opt/stellar/soroban-rpc/etc/stellar-captive-core.cfg", cfg.CaptiveCoreConfigPath, "should read values from the config path file") - assert.Equal(t, "CLI test passphrase", cfg.NetworkPassphrase, "cli flags should override --config-path values") - assert.Equal(t, "/usr/overridden/stellar-core", cfg.StellarCoreBinaryPath, "cli flags should override --config-path values and env vars") - assert.Equal(t, "/env/overridden/db", cfg.SQLiteDBPath, "env var should override config file") - assert.Equal(t, 2*time.Second, cfg.CoreRequestTimeout, "default value should be used, if not set anywhere else") -} - -func TestConfigLoadDefaults(t *testing.T) { - // Set up a default config - cfg := Config{} - require.NoError(t, cfg.loadDefaults()) - - // Check that the defaults are set - assert.Equal(t, defaultHTTPEndpoint, cfg.Endpoint) - assert.Equal(t, uint(runtime.NumCPU()), cfg.PreflightWorkerCount) -} - -func TestConfigLoadFlagsDefaultValuesOverrideExisting(t *testing.T) { - // Set up a config with an existing non-default value - cfg := Config{ - NetworkPassphrase: "existing value", - LogLevel: logrus.InfoLevel, - Endpoint: "localhost:8000", - } - - cmd := &cobra.Command{} - require.NoError(t, cfg.AddFlags(cmd)) - // Set up a flag set with the default value - require.NoError(t, cmd.ParseFlags([]string{ - "--network-passphrase", "", - "--log-level", logrus.PanicLevel.String(), - })) - - // Load the flags - require.NoError(t, cfg.loadFlags()) - - // Check that the flag value is set - assert.Equal(t, "", cfg.NetworkPassphrase) - assert.Equal(t, logrus.PanicLevel, cfg.LogLevel) - - // Check it didn't overwrite values which were not set in the flags - assert.Equal(t, "localhost:8000", cfg.Endpoint) -} diff --git a/cmd/soroban-rpc/internal/config/flags.go b/cmd/soroban-rpc/internal/config/flags.go deleted file mode 100644 index d313aa31c..000000000 --- a/cmd/soroban-rpc/internal/config/flags.go +++ /dev/null @@ -1,172 +0,0 @@ -package config - -import ( - "fmt" - "net" - "time" - - "github.com/spf13/cobra" - "github.com/spf13/pflag" -) - -// Init adds the CLI flags to the command. This lets the command output the -// flags as part of the --help output. -func (cfg *Config) AddFlags(cmd *cobra.Command) error { - cfg.flagset = cmd.PersistentFlags() - for _, option := range cfg.options() { - if err := option.AddFlag(cfg.flagset); err != nil { - return err - } - } - return nil -} - -// AddFlag adds a CLI flag for this option to the given flagset. -func (co *ConfigOption) AddFlag(flagset *pflag.FlagSet) error { - // config options that has no names do not represent a valid flag. - if len(co.Name) == 0 { - return nil - } - // Treat any option with a custom parser as a string option. - if co.CustomSetValue != nil { - if co.DefaultValue == nil { - co.DefaultValue = "" - } - flagset.String(co.Name, fmt.Sprint(co.DefaultValue), co.UsageText()) - co.flag = flagset.Lookup(co.Name) - return nil - } - - // Infer the type of the flag based on the type of the ConfigKey. This list - // of options is based on the available flag types from pflags - switch co.ConfigKey.(type) { - case *bool: - flagset.Bool(co.Name, co.DefaultValue.(bool), co.UsageText()) - case *time.Duration: - flagset.Duration(co.Name, co.DefaultValue.(time.Duration), co.UsageText()) - case *float32: - flagset.Float32(co.Name, co.DefaultValue.(float32), co.UsageText()) - case *float64: - flagset.Float64(co.Name, co.DefaultValue.(float64), co.UsageText()) - case *net.IP: - flagset.IP(co.Name, co.DefaultValue.(net.IP), co.UsageText()) - case *net.IPNet: - flagset.IPNet(co.Name, co.DefaultValue.(net.IPNet), co.UsageText()) - case *int: - flagset.Int(co.Name, co.DefaultValue.(int), co.UsageText()) - case *int8: - flagset.Int8(co.Name, co.DefaultValue.(int8), co.UsageText()) - case *int16: - flagset.Int16(co.Name, co.DefaultValue.(int16), co.UsageText()) - case *int32: - flagset.Int32(co.Name, co.DefaultValue.(int32), co.UsageText()) - case *int64: - flagset.Int64(co.Name, co.DefaultValue.(int64), co.UsageText()) - case *[]int: - flagset.IntSlice(co.Name, co.DefaultValue.([]int), co.UsageText()) - case *[]int32: - flagset.Int32Slice(co.Name, co.DefaultValue.([]int32), co.UsageText()) - case *[]int64: - flagset.Int64Slice(co.Name, co.DefaultValue.([]int64), co.UsageText()) - case *string: - // Set an empty string if no default was provided, since some value is always required for pflags - if co.DefaultValue == nil { - co.DefaultValue = "" - } - flagset.String(co.Name, co.DefaultValue.(string), co.UsageText()) - case *[]string: - // Set an empty string if no default was provided, since some value is always required for pflags - if co.DefaultValue == nil { - co.DefaultValue = []string{} - } - flagset.StringSlice(co.Name, co.DefaultValue.([]string), co.UsageText()) - case *uint: - flagset.Uint(co.Name, co.DefaultValue.(uint), co.UsageText()) - case *uint8: - flagset.Uint8(co.Name, co.DefaultValue.(uint8), co.UsageText()) - case *uint16: - flagset.Uint16(co.Name, co.DefaultValue.(uint16), co.UsageText()) - case *uint32: - flagset.Uint32(co.Name, co.DefaultValue.(uint32), co.UsageText()) - case *uint64: - flagset.Uint64(co.Name, co.DefaultValue.(uint64), co.UsageText()) - case *[]uint: - flagset.UintSlice(co.Name, co.DefaultValue.([]uint), co.UsageText()) - default: - return fmt.Errorf("unexpected option type: %T", co.ConfigKey) - } - - co.flag = flagset.Lookup(co.Name) - return nil -} - -func (co *ConfigOption) GetFlag(flagset *pflag.FlagSet) (interface{}, error) { - // Treat any option with a custom parser as a string option. - if co.CustomSetValue != nil { - return flagset.GetString(co.Name) - } - - // Infer the type of the flag based on the type of the ConfigKey. This list - // of options is based on the available flag types from pflags, and must - // match the above in `AddFlag`. - switch co.ConfigKey.(type) { - case *bool: - return flagset.GetBool(co.Name) - case *time.Duration: - return flagset.GetDuration(co.Name) - case *float32: - return flagset.GetFloat32(co.Name) - case *float64: - return flagset.GetFloat64(co.Name) - case *net.IP: - return flagset.GetIP(co.Name) - case *net.IPNet: - return flagset.GetIPNet(co.Name) - case *int: - return flagset.GetInt(co.Name) - case *int8: - return flagset.GetInt8(co.Name) - case *int16: - return flagset.GetInt16(co.Name) - case *int32: - return flagset.GetInt32(co.Name) - case *int64: - return flagset.GetInt64(co.Name) - case *[]int: - return flagset.GetIntSlice(co.Name) - case *[]int32: - return flagset.GetInt32Slice(co.Name) - case *[]int64: - return flagset.GetInt64Slice(co.Name) - case *string: - return flagset.GetString(co.Name) - case *[]string: - return flagset.GetStringSlice(co.Name) - case *uint: - return flagset.GetUint(co.Name) - case *uint8: - return flagset.GetUint8(co.Name) - case *uint16: - return flagset.GetUint16(co.Name) - case *uint32: - return flagset.GetUint32(co.Name) - case *uint64: - return flagset.GetUint64(co.Name) - case *[]uint: - return flagset.GetUintSlice(co.Name) - default: - return nil, fmt.Errorf("unexpected option type: %T", co.ConfigKey) - } -} - -// UsageText returns the string to use for the usage text of the option. The -// string returned will be the Usage defined on the ConfigOption, along with -// the environment variable. -func (co *ConfigOption) UsageText() string { - envVar, hasEnvVar := co.getEnvKey() - if hasEnvVar { - return fmt.Sprintf("%s (%s)", co.Usage, envVar) - } else { - return co.Usage - } -} diff --git a/cmd/soroban-rpc/internal/config/log_format.go b/cmd/soroban-rpc/internal/config/log_format.go deleted file mode 100644 index 1ab4c7fc6..000000000 --- a/cmd/soroban-rpc/internal/config/log_format.go +++ /dev/null @@ -1,61 +0,0 @@ -package config - -import ( - "fmt" -) - -type LogFormat int - -const ( - LogFormatText LogFormat = iota - LogFormatJSON -) - -func (f LogFormat) MarshalText() ([]byte, error) { - switch f { - case LogFormatText: - return []byte("text"), nil - case LogFormatJSON: - return []byte("json"), nil - default: - return nil, fmt.Errorf("unknown log format: %d", f) - } -} - -func (f *LogFormat) UnmarshalText(text []byte) error { - switch string(text) { - case "text": - *f = LogFormatText - case "json": - *f = LogFormatJSON - default: - return fmt.Errorf("unknown log format: %s", text) - } - return nil -} - -func (f LogFormat) MarshalTOML() ([]byte, error) { - return f.MarshalText() -} - -func (f *LogFormat) UnmarshalTOML(i interface{}) error { - switch v := i.(type) { - case []byte: - return f.UnmarshalText(v) - case string: - return f.UnmarshalText([]byte(v)) - default: - return fmt.Errorf("unknown log format: %v", v) - } -} - -func (f LogFormat) String() (string, error) { - switch f { - case LogFormatText: - return "text", nil - case LogFormatJSON: - return "json", nil - default: - return "", fmt.Errorf("unknown log format: %d", f) - } -} diff --git a/cmd/soroban-rpc/internal/config/options.go b/cmd/soroban-rpc/internal/config/options.go deleted file mode 100644 index d38dd7cd5..000000000 --- a/cmd/soroban-rpc/internal/config/options.go +++ /dev/null @@ -1,464 +0,0 @@ -package config - -import ( - "fmt" - "os" - "os/exec" - "reflect" - "runtime" - "time" - - "github.com/sirupsen/logrus" - - "github.com/stellar/go/network" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/strutils" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/ledgerbucketwindow" -) - -const defaultHTTPEndpoint = "localhost:8000" - -func (cfg *Config) options() ConfigOptions { - if cfg.optionsCache != nil { - return *cfg.optionsCache - } - defaultStellarCoreBinaryPath, _ := exec.LookPath("stellar-core") - cfg.optionsCache = &ConfigOptions{ - { - Name: "config-path", - EnvVar: "SOROBAN_RPC_CONFIG_PATH", - TomlKey: "-", - Usage: "File path to the toml configuration file", - ConfigKey: &cfg.ConfigPath, - }, - { - Name: "config-strict", - EnvVar: "SOROBAN_RPC_CONFIG_STRICT", - TomlKey: "STRICT", - Usage: "Enable strict toml configuration file parsing. This will prevent unknown fields in the config toml from being parsed.", - ConfigKey: &cfg.Strict, - DefaultValue: false, - }, - { - Name: "endpoint", - Usage: "Endpoint to listen and serve on", - ConfigKey: &cfg.Endpoint, - DefaultValue: defaultHTTPEndpoint, - }, - { - Name: "admin-endpoint", - Usage: "Admin endpoint to listen and serve on. WARNING: this should not be accessible from the Internet and does not use TLS. \"\" (default) disables the admin server", - ConfigKey: &cfg.AdminEndpoint, - }, - { - Name: "stellar-core-url", - Usage: "URL used to query Stellar Core (local captive core by default)", - ConfigKey: &cfg.StellarCoreURL, - Validate: func(co *ConfigOption) error { - // This is a bit awkward. We're actually setting a default, but we - // can't do that until the config is fully parsed, so we do it as a - // validator here. - if cfg.StellarCoreURL == "" { - cfg.StellarCoreURL = fmt.Sprintf("http://localhost:%d", cfg.CaptiveCoreHTTPPort) - } - return nil - }, - }, - { - Name: "stellar-core-timeout", - Usage: "Timeout used when submitting requests to stellar-core", - ConfigKey: &cfg.CoreRequestTimeout, - DefaultValue: 2 * time.Second, - }, - { - Name: "stellar-captive-core-http-port", - Usage: "HTTP port for Captive Core to listen on (0 disables the HTTP server)", - ConfigKey: &cfg.CaptiveCoreHTTPPort, - DefaultValue: uint(11626), - }, - { - Name: "log-level", - Usage: "minimum log severity (debug, info, warn, error) to log", - ConfigKey: &cfg.LogLevel, - DefaultValue: logrus.InfoLevel, - CustomSetValue: func(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case nil: - return nil - case string: - ll, err := logrus.ParseLevel(v) - if err != nil { - return fmt.Errorf("could not parse %s: %q", option.Name, v) - } - cfg.LogLevel = ll - case logrus.Level: - cfg.LogLevel = v - case *logrus.Level: - cfg.LogLevel = *v - default: - return fmt.Errorf("could not parse %s: %q", option.Name, v) - } - return nil - }, - MarshalTOML: func(option *ConfigOption) (interface{}, error) { - return cfg.LogLevel.String(), nil - }, - }, - { - Name: "log-format", - Usage: "format used for output logs (json or text)", - ConfigKey: &cfg.LogFormat, - DefaultValue: LogFormatText, - CustomSetValue: func(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case nil: - return nil - case string: - return errors.Wrapf( - cfg.LogFormat.UnmarshalText([]byte(v)), - "could not parse %s", - option.Name, - ) - case LogFormat: - cfg.LogFormat = v - case *LogFormat: - cfg.LogFormat = *v - default: - return fmt.Errorf("could not parse %s: %q", option.Name, v) - } - return nil - }, - MarshalTOML: func(option *ConfigOption) (interface{}, error) { - return cfg.LogFormat.String() - }, - }, - { - Name: "stellar-core-binary-path", - Usage: "path to stellar core binary", - ConfigKey: &cfg.StellarCoreBinaryPath, - DefaultValue: defaultStellarCoreBinaryPath, - Validate: required, - }, - { - Name: "captive-core-config-path", - Usage: "path to additional configuration for the Stellar Core configuration file used by captive core. It must, at least, include enough details to define a quorum set", - ConfigKey: &cfg.CaptiveCoreConfigPath, - Validate: required, - }, - { - Name: "captive-core-storage-path", - Usage: "Storage location for Captive Core bucket data", - ConfigKey: &cfg.CaptiveCoreStoragePath, - CustomSetValue: func(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case string: - if v == "" || v == "." { - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("unable to determine the current directory: %s", err) - } - v = cwd - } - cfg.CaptiveCoreStoragePath = v - return nil - case nil: - cwd, err := os.Getwd() - if err != nil { - return fmt.Errorf("unable to determine the current directory: %s", err) - } - cfg.CaptiveCoreStoragePath = cwd - return nil - default: - return fmt.Errorf("could not parse %s: %v", option.Name, v) - } - }, - }, - { - Name: "history-archive-urls", - Usage: "comma-separated list of stellar history archives to connect with", - ConfigKey: &cfg.HistoryArchiveURLs, - Validate: required, - }, - { - Name: "friendbot-url", - Usage: "The friendbot URL to be returned by getNetwork endpoint", - ConfigKey: &cfg.FriendbotURL, - }, - { - Name: "network-passphrase", - Usage: "Network passphrase of the Stellar network transactions should be signed for. Commonly used values are \"" + network.FutureNetworkPassphrase + "\", \"" + network.TestNetworkPassphrase + "\" and \"" + network.PublicNetworkPassphrase + "\"", - ConfigKey: &cfg.NetworkPassphrase, - Validate: required, - }, - { - Name: "db-path", - Usage: "SQLite DB path", - ConfigKey: &cfg.SQLiteDBPath, - DefaultValue: "soroban_rpc.sqlite", - }, - { - Name: "ingestion-timeout", - Usage: "Ingestion Timeout when bootstrapping data (checkpoint and in-memory initialization) and preparing ledger reads", - ConfigKey: &cfg.IngestionTimeout, - DefaultValue: 30 * time.Minute, - }, - { - Name: "checkpoint-frequency", - Usage: "establishes how many ledgers exist between checkpoints, do NOT change this unless you really know what you are doing", - ConfigKey: &cfg.CheckpointFrequency, - DefaultValue: uint32(64), - }, - { - Name: "event-retention-window", - Usage: fmt.Sprintf("configures the event retention window expressed in number of ledgers,"+ - " the default value is %d which corresponds to about 24 hours of history", ledgerbucketwindow.DefaultEventLedgerRetentionWindow), - ConfigKey: &cfg.EventLedgerRetentionWindow, - DefaultValue: uint32(ledgerbucketwindow.DefaultEventLedgerRetentionWindow), - Validate: positive, - }, - { - Name: "transaction-retention-window", - Usage: "configures the transaction retention window expressed in number of ledgers," + - " the default value is 1440 which corresponds to about 2 hours of history", - ConfigKey: &cfg.TransactionLedgerRetentionWindow, - DefaultValue: uint32(1440), - Validate: positive, - }, - { - Name: "max-events-limit", - Usage: "Maximum amount of events allowed in a single getEvents response", - ConfigKey: &cfg.MaxEventsLimit, - DefaultValue: uint(10000), - }, - { - Name: "default-events-limit", - Usage: "Default cap on the amount of events included in a single getEvents response", - ConfigKey: &cfg.DefaultEventsLimit, - DefaultValue: uint(100), - Validate: func(co *ConfigOption) error { - if cfg.DefaultEventsLimit > cfg.MaxEventsLimit { - return fmt.Errorf( - "default-events-limit (%v) cannot exceed max-events-limit (%v)", - cfg.DefaultEventsLimit, - cfg.MaxEventsLimit, - ) - } - return nil - }, - }, - { - Name: "max-healthy-ledger-latency", - Usage: "maximum ledger latency (i.e. time elapsed since the last known ledger closing time) considered to be healthy" + - " (used for the /health endpoint)", - ConfigKey: &cfg.MaxHealthyLedgerLatency, - DefaultValue: 30 * time.Second, - }, - { - Name: "preflight-worker-count", - Usage: "Number of workers (read goroutines) used to compute preflights for the simulateTransaction endpoint. Defaults to the number of CPUs.", - ConfigKey: &cfg.PreflightWorkerCount, - DefaultValue: uint(runtime.NumCPU()), - Validate: positive, - }, - { - Name: "preflight-worker-queue-size", - Usage: "Maximum number of outstanding preflight requests for the simulateTransaction endpoint. Defaults to the number of CPUs.", - ConfigKey: &cfg.PreflightWorkerQueueSize, - DefaultValue: uint(runtime.NumCPU()), - Validate: positive, - }, - { - Name: "preflight-enable-debug", - Usage: "Enable debug information in preflighting (provides more detailed errors). It should not be enabled in production deployments.", - ConfigKey: &cfg.PreflightEnableDebug, - DefaultValue: true, - }, - { - TomlKey: strutils.KebabToConstantCase("request-backlog-global-queue-limit"), - Usage: "Maximum number of outstanding requests", - ConfigKey: &cfg.RequestBacklogGlobalQueueLimit, - DefaultValue: uint(5000), - Validate: positive, - }, - { - TomlKey: strutils.KebabToConstantCase("request-backlog-get-health-queue-limit"), - Usage: "Maximum number of outstanding GetHealth requests", - ConfigKey: &cfg.RequestBacklogGetHealthQueueLimit, - DefaultValue: uint(1000), - Validate: positive, - }, - { - TomlKey: strutils.KebabToConstantCase("request-backlog-get-events-queue-limit"), - Usage: "Maximum number of outstanding GetEvents requests", - ConfigKey: &cfg.RequestBacklogGetEventsQueueLimit, - DefaultValue: uint(1000), - Validate: positive, - }, - { - TomlKey: strutils.KebabToConstantCase("request-backlog-get-network-queue-limit"), - Usage: "Maximum number of outstanding GetNetwork requests", - ConfigKey: &cfg.RequestBacklogGetNetworkQueueLimit, - DefaultValue: uint(1000), - Validate: positive, - }, - { - TomlKey: strutils.KebabToConstantCase("request-backlog-get-latest-ledger-queue-limit"), - Usage: "Maximum number of outstanding GetLatestsLedger requests", - ConfigKey: &cfg.RequestBacklogGetLatestLedgerQueueLimit, - DefaultValue: uint(1000), - Validate: positive, - }, - { - TomlKey: strutils.KebabToConstantCase("request-backlog-get-ledger-entries-queue-limit"), - Usage: "Maximum number of outstanding GetLedgerEntries requests", - ConfigKey: &cfg.RequestBacklogGetLedgerEntriesQueueLimit, - DefaultValue: uint(1000), - Validate: positive, - }, - { - TomlKey: strutils.KebabToConstantCase("request-backlog-get-transaction-queue-limit"), - Usage: "Maximum number of outstanding GetTransaction requests", - ConfigKey: &cfg.RequestBacklogGetTransactionQueueLimit, - DefaultValue: uint(1000), - Validate: positive, - }, - { - TomlKey: strutils.KebabToConstantCase("request-backlog-send-transaction-queue-limit"), - Usage: "Maximum number of outstanding SendTransaction requests", - ConfigKey: &cfg.RequestBacklogSendTransactionQueueLimit, - DefaultValue: uint(500), - Validate: positive, - }, - { - TomlKey: strutils.KebabToConstantCase("request-backlog-simulate-transaction-queue-limit"), - Usage: "Maximum number of outstanding SimulateTransaction requests", - ConfigKey: &cfg.RequestBacklogSimulateTransactionQueueLimit, - DefaultValue: uint(100), - Validate: positive, - }, - { - TomlKey: strutils.KebabToConstantCase("request-execution-warning-threshold"), - Usage: "The request execution warning threshold is the predetermined maximum duration of time that a request can take to be processed before a warning would be generated", - ConfigKey: &cfg.RequestExecutionWarningThreshold, - DefaultValue: 5 * time.Second, - }, - { - TomlKey: strutils.KebabToConstantCase("max-request-execution-duration"), - Usage: "The max request execution duration is the predefined maximum duration of time allowed for processing a request. When that time elapses, the server would return 504 and abort the request's execution", - ConfigKey: &cfg.MaxRequestExecutionDuration, - DefaultValue: 25 * time.Second, - }, - { - TomlKey: strutils.KebabToConstantCase("max-get-health-execution-duration"), - Usage: "The maximum duration of time allowed for processing a getHealth request. When that time elapses, the rpc server would return -32001 and abort the request's execution", - ConfigKey: &cfg.MaxGetHealthExecutionDuration, - DefaultValue: 5 * time.Second, - }, - { - TomlKey: strutils.KebabToConstantCase("max-get_events-execution-duration"), - Usage: "The maximum duration of time allowed for processing a getEvents request. When that time elapses, the rpc server would return -32001 and abort the request's execution", - ConfigKey: &cfg.MaxGetEventsExecutionDuration, - DefaultValue: 10 * time.Second, - }, - { - TomlKey: strutils.KebabToConstantCase("max-get-network-execution-duration"), - Usage: "The maximum duration of time allowed for processing a getNetwork request. When that time elapses, the rpc server would return -32001 and abort the request's execution", - ConfigKey: &cfg.MaxGetNetworkExecutionDuration, - DefaultValue: 5 * time.Second, - }, - { - TomlKey: strutils.KebabToConstantCase("max-get-latest-ledger-execution-duration"), - Usage: "The maximum duration of time allowed for processing a getLatestLedger request. When that time elapses, the rpc server would return -32001 and abort the request's execution", - ConfigKey: &cfg.MaxGetLatestLedgerExecutionDuration, - DefaultValue: 5 * time.Second, - }, - { - TomlKey: strutils.KebabToConstantCase("max-get_ledger-entries-execution-duration"), - Usage: "The maximum duration of time allowed for processing a getLedgerEntries request. When that time elapses, the rpc server would return -32001 and abort the request's execution", - ConfigKey: &cfg.MaxGetLedgerEntriesExecutionDuration, - DefaultValue: 5 * time.Second, - }, - { - TomlKey: strutils.KebabToConstantCase("max-get-transaction-execution-duration"), - Usage: "The maximum duration of time allowed for processing a getTransaction request. When that time elapses, the rpc server would return -32001 and abort the request's execution", - ConfigKey: &cfg.MaxGetTransactionExecutionDuration, - DefaultValue: 5 * time.Second, - }, - { - TomlKey: strutils.KebabToConstantCase("max-send-transaction-execution-duration"), - Usage: "The maximum duration of time allowed for processing a sendTransaction request. When that time elapses, the rpc server would return -32001 and abort the request's execution", - ConfigKey: &cfg.MaxSendTransactionExecutionDuration, - DefaultValue: 15 * time.Second, - }, - { - TomlKey: strutils.KebabToConstantCase("max-simulate-transaction-execution-duration"), - Usage: "The maximum duration of time allowed for processing a simulateTransaction request. When that time elapses, the rpc server would return -32001 and abort the request's execution", - ConfigKey: &cfg.MaxSimulateTransactionExecutionDuration, - DefaultValue: 15 * time.Second, - }, - } - return *cfg.optionsCache -} - -type errMissingRequiredOption struct { - strErr string - usage string -} - -func (e errMissingRequiredOption) Error() string { - return e.strErr -} - -func required(option *ConfigOption) error { - switch reflect.ValueOf(option.ConfigKey).Elem().Kind() { - case reflect.Slice: - if reflect.ValueOf(option.ConfigKey).Elem().Len() > 0 { - return nil - } - default: - if !reflect.ValueOf(option.ConfigKey).Elem().IsZero() { - return nil - } - } - - waysToSet := []string{} - if option.Name != "" && option.Name != "-" { - waysToSet = append(waysToSet, fmt.Sprintf("specify --%s on the command line", option.Name)) - } - if option.EnvVar != "" && option.EnvVar != "-" { - waysToSet = append(waysToSet, fmt.Sprintf("set the %s environment variable", option.EnvVar)) - } - - if tomlKey, hasTomlKey := option.getTomlKey(); hasTomlKey { - waysToSet = append(waysToSet, fmt.Sprintf("set %s in the config file", tomlKey)) - } - - advice := "" - switch len(waysToSet) { - case 1: - advice = fmt.Sprintf(" Please %s.", waysToSet[0]) - case 2: - advice = fmt.Sprintf(" Please %s or %s.", waysToSet[0], waysToSet[1]) - case 3: - advice = fmt.Sprintf(" Please %s, %s, or %s.", waysToSet[0], waysToSet[1], waysToSet[2]) - } - - return errMissingRequiredOption{strErr: fmt.Sprintf("%s is required.%s", option.Name, advice), usage: option.Usage} -} - -func positive(option *ConfigOption) error { - switch v := option.ConfigKey.(type) { - case *int, *int8, *int16, *int32, *int64: - if reflect.ValueOf(v).Elem().Int() <= 0 { - return fmt.Errorf("%s must be positive", option.Name) - } - case *uint, *uint8, *uint16, *uint32, *uint64: - if reflect.ValueOf(v).Elem().Uint() <= 0 { - return fmt.Errorf("%s must be positive", option.Name) - } - default: - return fmt.Errorf("%s is not a positive integer", option.Name) - } - return nil -} diff --git a/cmd/soroban-rpc/internal/config/options_test.go b/cmd/soroban-rpc/internal/config/options_test.go deleted file mode 100644 index 958306abf..000000000 --- a/cmd/soroban-rpc/internal/config/options_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package config - -import ( - "reflect" - "regexp" - "testing" - "unsafe" - - "github.com/stretchr/testify/assert" -) - -func TestAllConfigKeysMustBePointers(t *testing.T) { - // This test is to ensure we've set up all the config keys correctly. - cfg := Config{} - for _, option := range cfg.options() { - kind := reflect.ValueOf(option.ConfigKey).Type().Kind() - if kind != reflect.Pointer { - t.Errorf("ConfigOption.ConfigKey must be a pointer, got %s for %s", kind, option.Name) - } - } -} - -func TestAllConfigFieldsMustHaveASingleOption(t *testing.T) { - // This test ensures we've documented all the config options, and not missed - // any when adding new flags (or accidentally added conflicting duplicates). - - // Allow us to explicitly exclude any fields on the Config struct, which are not going to have Options. - // e.g. "ConfigPath" - excluded := map[string]bool{} - - cfg := Config{} - cfgValue := reflect.ValueOf(cfg) - cfgType := cfgValue.Type() - - options := cfg.options() - optionsByField := map[uintptr]*ConfigOption{} - for _, option := range options { - key := uintptr(reflect.ValueOf(option.ConfigKey).UnsafePointer()) - if existing, ok := optionsByField[key]; ok { - t.Errorf("Conflicting ConfigOptions %s and %s, point to the same struct field", existing.Name, option.Name) - } - optionsByField[key] = option - } - - // Get the base address of the struct - cfgPtr := uintptr(unsafe.Pointer(&cfg)) - for _, structField := range reflect.VisibleFields(cfgType) { - if excluded[structField.Name] { - continue - } - if !structField.IsExported() { - continue - } - - // Each field has an offset within that struct - fieldPointer := cfgPtr + structField.Offset - - // There should be an option which points to this field - _, ok := optionsByField[fieldPointer] - if !ok { - t.Errorf("Missing ConfigOption for field Config.%s", structField.Name) - } - } -} - -// Use this regex to validate all our config toml keys. -// This is based on the simple bare key regex at: https://toml.io/en/v1.0.0#keys -// Toml, actually allows much more complex keys, via quoted keys, but we want -// to keep things simple. -// -// The one exception we make is `.` in keys, which allows us to have nested -// objects. -var keyRegex = regexp.MustCompile(`^[.A-Za-z0-9_-]+$`) - -func TestAllOptionsMustHaveAUniqueValidTomlKey(t *testing.T) { - // This test ensures we've set a toml key for all the config options, and the - // keys are all unique & valid. Note, we don't need to check that all struct - // fields on the config have an option, because the test above checks that. - - // Allow us to explicitly exclude any fields on the Config struct, which are - // not going to be in the toml. This should be the "Name" field of the - // ConfigOption we wish to exclude. - excluded := map[string]bool{ - "config-path": true, - } - - cfg := Config{} - options := cfg.options() - optionsByTomlKey := map[string]interface{}{} - for _, option := range options { - key, ok := option.getTomlKey() - if excluded[option.Name] { - if ok { - t.Errorf("Found unexpected toml key for excluded ConfigOption %s. Does the test need updating?", option.Name) - } - continue - } - if !ok { - t.Errorf("Missing toml key for ConfigOption %s", option.Name) - } - if existing, ok := optionsByTomlKey[key]; ok { - t.Errorf("Conflicting ConfigOptions %s and %s, have the same toml key: %s", existing, option.Name, key) - } - optionsByTomlKey[key] = option.Name - - // Ensure the keys are simple valid toml keys - assert.True(t, keyRegex.MatchString(key), "Invalid toml key for ConfigOption %s: %s", option.Name, key) - } -} diff --git a/cmd/soroban-rpc/internal/config/parse.go b/cmd/soroban-rpc/internal/config/parse.go deleted file mode 100644 index 00d93a177..000000000 --- a/cmd/soroban-rpc/internal/config/parse.go +++ /dev/null @@ -1,180 +0,0 @@ -package config - -import ( - "fmt" - "math" - "reflect" - "strconv" - "strings" - "time" - - "github.com/stellar/go/support/errors" -) - -func parseBool(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case nil: - return nil - case bool: - *option.ConfigKey.(*bool) = v - case string: - lower := strings.ToLower(v) - if lower == "true" { - *option.ConfigKey.(*bool) = true - } else if lower == "false" { - *option.ConfigKey.(*bool) = false - } else { - return fmt.Errorf("invalid boolean value %s: %s", option.Name, v) - } - default: - return fmt.Errorf("could not parse boolean %s: %v", option.Name, i) - } - return nil -} - -func parseInt(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case nil: - return nil - case string: - parsed, err := strconv.ParseInt(v, 10, 64) - if err != nil { - return err - } - reflect.ValueOf(option.ConfigKey).Elem().SetInt(parsed) - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - return parseInt(option, fmt.Sprint(v)) - default: - return fmt.Errorf("could not parse int %s: %v", option.Name, i) - } - return nil -} - -func parseUint(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case nil: - return nil - case string: - parsed, err := strconv.ParseUint(v, 10, 64) - if err != nil { - return err - } - reflect.ValueOf(option.ConfigKey).Elem().SetUint(parsed) - case int, int8, int16, int32, int64: - if reflect.ValueOf(v).Int() < 0 { - return fmt.Errorf("%s cannot be negative", option.Name) - } - return parseUint(option, fmt.Sprint(v)) - case uint, uint8, uint16, uint32, uint64: - return parseUint(option, fmt.Sprint(v)) - default: - return fmt.Errorf("could not parse uint %s: %v", option.Name, i) - } - return nil -} - -func parseFloat(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case nil: - return nil - case string: - parsed, err := strconv.ParseFloat(v, 64) - if err != nil { - return err - } - reflect.ValueOf(option.ConfigKey).Elem().SetFloat(parsed) - case uint, uint8, uint16, uint32, uint64, int, int8, int16, int32, int64, float32, float64: - return parseFloat(option, fmt.Sprint(v)) - default: - return fmt.Errorf("could not parse float %s: %v", option.Name, i) - } - return nil -} - -func parseString(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case nil: - return nil - case string: - *option.ConfigKey.(*string) = v - default: - return fmt.Errorf("could not parse string %s: %v", option.Name, i) - } - return nil -} - -func parseUint32(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case nil: - return nil - case string: - parsed, err := strconv.ParseUint(v, 10, 64) - if err != nil { - return err - } - if parsed > math.MaxUint32 { - return fmt.Errorf("%s overflows uint32", option.Name) - } - reflect.ValueOf(option.ConfigKey).Elem().SetUint(parsed) - case int, int8, int16, int32, int64: - if reflect.ValueOf(v).Int() < 0 { - return fmt.Errorf("%s cannot be negative", option.Name) - } - return parseUint32(option, fmt.Sprint(v)) - case uint, uint8, uint16, uint32, uint64: - return parseUint32(option, fmt.Sprint(v)) - default: - return fmt.Errorf("could not parse uint32 %s: %v", option.Name, i) - } - return nil -} - -func parseDuration(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case nil: - return nil - case string: - d, err := time.ParseDuration(v) - if err != nil { - return errors.Wrapf(err, "could not parse duration: %q", v) - } - *option.ConfigKey.(*time.Duration) = d - case time.Duration: - *option.ConfigKey.(*time.Duration) = v - case *time.Duration: - *option.ConfigKey.(*time.Duration) = *v - default: - return fmt.Errorf("%s is not a duration", option.Name) - } - return nil -} - -func parseStringSlice(option *ConfigOption, i interface{}) error { - switch v := i.(type) { - case nil: - return nil - case string: - if v == "" { - *option.ConfigKey.(*[]string) = nil - } else { - *option.ConfigKey.(*[]string) = strings.Split(v, ",") - } - return nil - case []string: - *option.ConfigKey.(*[]string) = v - return nil - case []interface{}: - *option.ConfigKey.(*[]string) = make([]string, len(v)) - for i, s := range v { - switch s := s.(type) { - case string: - (*option.ConfigKey.(*[]string))[i] = s - default: - return fmt.Errorf("could not parse %s: %v", option.Name, v) - } - } - return nil - default: - return fmt.Errorf("could not parse %s: %v", option.Name, v) - } -} diff --git a/cmd/soroban-rpc/internal/config/test.soroban.rpc.config b/cmd/soroban-rpc/internal/config/test.soroban.rpc.config deleted file mode 100644 index c28a9c17b..000000000 --- a/cmd/soroban-rpc/internal/config/test.soroban.rpc.config +++ /dev/null @@ -1,11 +0,0 @@ -ENDPOINT="localhost:8003" -FRIENDBOT_URL="http://localhost:8000/friendbot" -NETWORK_PASSPHRASE="Standalone Network ; February 2017" -STELLAR_CORE_URL="http://localhost:11626" -CAPTIVE_CORE_CONFIG_PATH="/opt/stellar/soroban-rpc/etc/stellar-captive-core.cfg" -CAPTIVE_CORE_STORAGE_PATH="/opt/stellar/soroban-rpc/captive-core" -STELLAR_CORE_BINARY_PATH="/usr/bin/stellar-core" -HISTORY_ARCHIVE_URLS=["http://localhost:1570"] -DB_PATH="/opt/stellar/soroban-rpc/rpc_db.sqlite" -STELLAR_CAPTIVE_CORE_HTTP_PORT=0 -CHECKPOINT_FREQUENCY=64 diff --git a/cmd/soroban-rpc/internal/config/toml.go b/cmd/soroban-rpc/internal/config/toml.go deleted file mode 100644 index e6ea5a911..000000000 --- a/cmd/soroban-rpc/internal/config/toml.go +++ /dev/null @@ -1,109 +0,0 @@ -package config - -import ( - "fmt" - "io" - "reflect" - "strings" - - "github.com/pelletier/go-toml" -) - -func parseToml(r io.Reader, strict bool, cfg *Config) error { - tree, err := toml.LoadReader(r) - if err != nil { - return err - } - - validKeys := map[string]struct{}{} - for _, option := range cfg.options() { - key, ok := option.getTomlKey() - if !ok { - continue - } - validKeys[key] = struct{}{} - value := tree.Get(key) - if value == nil { - // not found - continue - } - if err := option.setValue(value); err != nil { - return err - } - } - - if cfg.Strict || strict { - for _, key := range tree.Keys() { - if _, ok := validKeys[key]; !ok { - return fmt.Errorf("invalid config: unexpected entry specified in toml file %q", key) - } - } - } - - return nil -} - -func (cfg *Config) MarshalTOML() ([]byte, error) { - tree, err := toml.TreeFromMap(map[string]interface{}{}) - if err != nil { - return nil, err - } - - for _, option := range cfg.options() { - key, ok := option.getTomlKey() - if !ok { - continue - } - - // Downcast a couple primitive types which are not directly supported by the toml encoder - // For non-primitives, you should implement toml.Marshaler instead. - value, err := option.marshalTOML() - if err != nil { - return nil, err - } - - if m, ok := value.(toml.Marshaler); ok { - value, err = m.MarshalTOML() - if err != nil { - return nil, err - } - } - - tree.SetWithOptions( - key, - toml.SetOptions{ - Comment: strings.ReplaceAll( - wordWrap(option.Usage, 80-2), - "\n", - "\n ", - ), - // output unset values commented out - // TODO: Provide commented example values for these - Commented: reflect.ValueOf(option.ConfigKey).Elem().IsZero(), - }, - value, - ) - } - - return tree.Marshal() -} - -// From https://gist.github.com/kennwhite/306317d81ab4a885a965e25aa835b8ef -func wordWrap(text string, lineWidth int) string { - words := strings.Fields(strings.TrimSpace(text)) - if len(words) == 0 { - return text - } - wrapped := words[0] - spaceLeft := lineWidth - len(wrapped) - for _, word := range words[1:] { - if len(word)+1 > spaceLeft { - wrapped += "\n" + word - spaceLeft = lineWidth - len(word) - } else { - wrapped += " " + word - spaceLeft -= 1 + len(word) - } - } - return wrapped -} diff --git a/cmd/soroban-rpc/internal/config/toml_test.go b/cmd/soroban-rpc/internal/config/toml_test.go deleted file mode 100644 index 93fa1809b..000000000 --- a/cmd/soroban-rpc/internal/config/toml_test.go +++ /dev/null @@ -1,138 +0,0 @@ -package config - -import ( - "bytes" - "reflect" - "strings" - "testing" - "time" - - "github.com/sirupsen/logrus" - "github.com/stellar/go/network" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" -) - -const basicToml = ` -HISTORY_ARCHIVE_URLS = [ "http://history-futurenet.stellar.org" ] -NETWORK_PASSPHRASE = "Test SDF Future Network ; October 2022" - -# testing comments work ok -STELLAR_CORE_BINARY_PATH = "/usr/bin/stellar-core" -CAPTIVE_CORE_STORAGE_PATH = "/etc/stellar/soroban-rpc" -CAPTIVE_CORE_CONFIG_PATH = "/etc/stellar/soroban-rpc/captive-core.cfg" -` - -func TestBasicTomlReading(t *testing.T) { - cfg := Config{} - require.NoError(t, parseToml(strings.NewReader(basicToml), false, &cfg)) - - // Check the fields got read correctly - assert.Equal(t, []string{"http://history-futurenet.stellar.org"}, cfg.HistoryArchiveURLs) - assert.Equal(t, network.FutureNetworkPassphrase, cfg.NetworkPassphrase) - assert.Equal(t, "/etc/stellar/soroban-rpc", cfg.CaptiveCoreStoragePath) - assert.Equal(t, "/etc/stellar/soroban-rpc/captive-core.cfg", cfg.CaptiveCoreConfigPath) -} - -func TestBasicTomlReadingStrictMode(t *testing.T) { - invalidToml := `UNKNOWN = "key"` - cfg := Config{} - - // Should ignore unknown fields when strict is not set - require.NoError(t, parseToml(strings.NewReader(invalidToml), false, &cfg)) - - // Should panic when unknown key is present and strict is set in the cli - // flags - require.EqualError( - t, - parseToml(strings.NewReader(invalidToml), true, &cfg), - "invalid config: unexpected entry specified in toml file \"UNKNOWN\"", - ) - - // Should panic when unknown key is present and strict is set in the - // config file - invalidStrictToml := ` - STRICT = true - UNKNOWN = "key" -` - require.EqualError( - t, - parseToml(strings.NewReader(invalidStrictToml), false, &cfg), - "invalid config: unexpected entry specified in toml file \"UNKNOWN\"", - ) - - // It succeeds with a valid config - require.NoError(t, parseToml(strings.NewReader(basicToml), true, &cfg)) -} - -func TestBasicTomlWriting(t *testing.T) { - // Set up a default config - cfg := Config{} - require.NoError(t, cfg.loadDefaults()) - - // Output it to toml - outBytes, err := cfg.MarshalTOML() - require.NoError(t, err) - - out := string(outBytes) - - // Spot-check that the output looks right. Try to check one value for each - // type of option. (string, duration, uint, etc...) - assert.Contains(t, out, "ENDPOINT = \"localhost:8000\"") - assert.Contains(t, out, "STELLAR_CORE_TIMEOUT = \"2s\"") - assert.Contains(t, out, "STELLAR_CAPTIVE_CORE_HTTP_PORT = 11626") - assert.Contains(t, out, "LOG_LEVEL = \"info\"") - assert.Contains(t, out, "LOG_FORMAT = \"text\"") - - // Check that the output contains comments about each option - assert.Contains(t, out, "# Network passphrase of the Stellar network transactions should be signed for") - - // Test that it wraps long lines. - // Note the newline at char 80. This also checks it adds a space after the - // comment when outputting multi-line comments, which go-toml does *not* do - // by default. - assert.Contains(t, out, "# configures the event retention window expressed in number of ledgers, the\n# default value is 17280 which corresponds to about 24 hours of history") -} - -func TestRoundTrip(t *testing.T) { - // Set up a default config - cfg := Config{} - require.NoError(t, cfg.loadDefaults()) - - // Generate test values for every option, so we can round-trip test them all. - for _, option := range cfg.options() { - optType := reflect.ValueOf(option.ConfigKey).Elem().Type() - switch option.ConfigKey.(type) { - case *bool: - *option.ConfigKey.(*bool) = true - case *string: - *option.ConfigKey.(*string) = "test" - case *uint: - *option.ConfigKey.(*uint) = 42 - case *uint32: - *option.ConfigKey.(*uint32) = 32 - case *time.Duration: - *option.ConfigKey.(*time.Duration) = 5 * time.Second - case *[]string: - *option.ConfigKey.(*[]string) = []string{"a", "b"} - case *logrus.Level: - *option.ConfigKey.(*logrus.Level) = logrus.InfoLevel - case *LogFormat: - *option.ConfigKey.(*LogFormat) = LogFormatText - default: - t.Fatalf("TestRoundTrip not implemented for type %s, on option %s, please add a test value", optType.Kind(), option.Name) - } - } - - // Output it to toml - outBytes, err := cfg.MarshalTOML() - require.NoError(t, err) - - // t.Log(string(outBytes)) - - // Parse it back - require.NoError( - t, - parseToml(bytes.NewReader(outBytes), false, &cfg), - ) -} diff --git a/cmd/soroban-rpc/internal/config/version.go b/cmd/soroban-rpc/internal/config/version.go deleted file mode 100644 index 909fdaea2..000000000 --- a/cmd/soroban-rpc/internal/config/version.go +++ /dev/null @@ -1,15 +0,0 @@ -package config - -var ( - // Version is the soroban-rpc version number, which is injected during build time. - Version = "0.0.0" - - // CommitHash is the soroban-rpc git commit hash, which is injected during build time. - CommitHash = "" - - // BuildTimestamp is the timestamp at which the soroban-rpc was built, injected during build time. - BuildTimestamp = "" - - // Branch is the git branch from which the soroban-rpc was built, injected during build time. - Branch = "" -) diff --git a/cmd/soroban-rpc/internal/daemon/daemon.go b/cmd/soroban-rpc/internal/daemon/daemon.go deleted file mode 100644 index d2ee9c3ac..000000000 --- a/cmd/soroban-rpc/internal/daemon/daemon.go +++ /dev/null @@ -1,314 +0,0 @@ -package daemon - -import ( - "context" - "errors" - "net/http" - "net/http/pprof" //nolint:gosec - "os" - "os/signal" - runtimePprof "runtime/pprof" - "sync" - "syscall" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/stellar/go/clients/stellarcore" - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/ingest/ledgerbackend" - supporthttp "github.com/stellar/go/support/http" - supportlog "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/events" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/ingest" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/ledgerbucketwindow" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/preflight" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/transactions" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/util" -) - -const ( - prometheusNamespace = "soroban_rpc" - maxLedgerEntryWriteBatchSize = 150 - defaultReadTimeout = 5 * time.Second - defaultShutdownGracePeriod = 10 * time.Second -) - -type Daemon struct { - core *ledgerbackend.CaptiveStellarCore - coreClient *CoreClientWithMetrics - ingestService *ingest.Service - db *db.DB - jsonRPCHandler *internal.Handler - logger *supportlog.Entry - preflightWorkerPool *preflight.PreflightWorkerPool - server *http.Server - adminServer *http.Server - closeOnce sync.Once - closeError error - done chan struct{} - metricsRegistry *prometheus.Registry -} - -func (d *Daemon) GetDB() *db.DB { - return d.db -} - -func (d *Daemon) close() { - shutdownCtx, shutdownRelease := context.WithTimeout(context.Background(), defaultShutdownGracePeriod) - defer shutdownRelease() - var closeErrors []error - - if err := d.server.Shutdown(shutdownCtx); err != nil { - d.logger.WithError(err).Error("error during Soroban JSON RPC server Shutdown") - closeErrors = append(closeErrors, err) - } - if d.adminServer != nil { - if err := d.adminServer.Shutdown(shutdownCtx); err != nil { - d.logger.WithError(err).Error("error during Soroban JSON admin server Shutdown") - closeErrors = append(closeErrors, err) - } - } - - if err := d.ingestService.Close(); err != nil { - d.logger.WithError(err).Error("error closing ingestion service") - closeErrors = append(closeErrors, err) - } - if err := d.core.Close(); err != nil { - d.logger.WithError(err).Error("error closing captive core") - closeErrors = append(closeErrors, err) - } - d.jsonRPCHandler.Close() - if err := d.db.Close(); err != nil { - d.logger.WithError(err).Error("Error closing db") - closeErrors = append(closeErrors, err) - } - d.preflightWorkerPool.Close() - d.closeError = errors.Join(closeErrors...) - close(d.done) -} - -func (d *Daemon) Close() error { - d.closeOnce.Do(d.close) - return d.closeError -} - -// newCaptiveCore creates a new captive core backend instance and returns it. -func newCaptiveCore(cfg *config.Config, logger *supportlog.Entry) (*ledgerbackend.CaptiveStellarCore, error) { - captiveCoreTomlParams := ledgerbackend.CaptiveCoreTomlParams{ - HTTPPort: &cfg.CaptiveCoreHTTPPort, - HistoryArchiveURLs: cfg.HistoryArchiveURLs, - NetworkPassphrase: cfg.NetworkPassphrase, - Strict: true, - UseDB: true, - EnforceSorobanDiagnosticEvents: true, - } - captiveCoreToml, err := ledgerbackend.NewCaptiveCoreTomlFromFile(cfg.CaptiveCoreConfigPath, captiveCoreTomlParams) - if err != nil { - logger.WithError(err).Fatal("Invalid captive core toml") - } - - captiveConfig := ledgerbackend.CaptiveCoreConfig{ - BinaryPath: cfg.StellarCoreBinaryPath, - StoragePath: cfg.CaptiveCoreStoragePath, - NetworkPassphrase: cfg.NetworkPassphrase, - HistoryArchiveURLs: cfg.HistoryArchiveURLs, - CheckpointFrequency: cfg.CheckpointFrequency, - Log: logger.WithField("subservice", "stellar-core"), - Toml: captiveCoreToml, - UserAgent: "captivecore", - UseDB: true, - } - return ledgerbackend.NewCaptive(captiveConfig) - -} - -func MustNew(cfg *config.Config) *Daemon { - logger := supportlog.New() - logger.SetLevel(cfg.LogLevel) - - if cfg.LogFormat == config.LogFormatJSON { - logger.UseJSONFormatter() - } - - core, err := newCaptiveCore(cfg, logger) - if err != nil { - logger.WithError(err).Fatal("could not create captive core") - } - - if len(cfg.HistoryArchiveURLs) == 0 { - logger.Fatal("no history archives url were provided") - } - historyArchive, err := historyarchive.Connect( - cfg.HistoryArchiveURLs[0], - historyarchive.ArchiveOptions{ - CheckpointFrequency: cfg.CheckpointFrequency, - }, - ) - if err != nil { - logger.WithError(err).Fatal("could not connect to history archive") - } - - metricsRegistry := prometheus.NewRegistry() - dbConn, err := db.OpenSQLiteDBWithPrometheusMetrics(cfg.SQLiteDBPath, prometheusNamespace, "db", metricsRegistry) - if err != nil { - logger.WithError(err).Fatal("could not open database") - } - - daemon := &Daemon{ - logger: logger, - core: core, - db: dbConn, - done: make(chan struct{}), - metricsRegistry: metricsRegistry, - coreClient: newCoreClientWithMetrics(stellarcore.Client{ - URL: cfg.StellarCoreURL, - HTTP: &http.Client{Timeout: cfg.CoreRequestTimeout}, - }, metricsRegistry), - } - - eventStore := events.NewMemoryStore( - daemon, - cfg.NetworkPassphrase, - cfg.EventLedgerRetentionWindow, - ) - transactionStore := transactions.NewMemoryStore( - daemon, - cfg.NetworkPassphrase, - cfg.TransactionLedgerRetentionWindow, - ) - - // initialize the stores using what was on the DB - readTxMetaCtx, cancelReadTxMeta := context.WithTimeout(context.Background(), cfg.IngestionTimeout) - defer cancelReadTxMeta() - // NOTE: We could optimize this to avoid unnecessary ingestion calls - // (the range of txmetads can be larger than the store retention windows) - // but it's probably not worth the pain. - err = db.NewLedgerReader(dbConn).StreamAllLedgers(readTxMetaCtx, func(txmeta xdr.LedgerCloseMeta) error { - if err := eventStore.IngestEvents(txmeta); err != nil { - logger.WithError(err).Fatal("could not initialize event memory store") - } - if err := transactionStore.IngestTransactions(txmeta); err != nil { - logger.WithError(err).Fatal("could not initialize transaction memory store") - } - return nil - }) - if err != nil { - logger.WithError(err).Fatal("could not obtain txmeta cache from the database") - } - - onIngestionRetry := func(err error, dur time.Duration) { - logger.WithError(err).Error("could not run ingestion. Retrying") - } - maxRetentionWindow := cfg.EventLedgerRetentionWindow - if cfg.TransactionLedgerRetentionWindow > maxRetentionWindow { - maxRetentionWindow = cfg.TransactionLedgerRetentionWindow - } else if cfg.EventLedgerRetentionWindow == 0 && cfg.TransactionLedgerRetentionWindow > ledgerbucketwindow.DefaultEventLedgerRetentionWindow { - maxRetentionWindow = ledgerbucketwindow.DefaultEventLedgerRetentionWindow - } - ingestService := ingest.NewService(ingest.Config{ - Logger: logger, - DB: db.NewReadWriter(dbConn, maxLedgerEntryWriteBatchSize, maxRetentionWindow), - EventStore: eventStore, - TransactionStore: transactionStore, - NetworkPassPhrase: cfg.NetworkPassphrase, - Archive: historyArchive, - LedgerBackend: core, - Timeout: cfg.IngestionTimeout, - OnIngestionRetry: onIngestionRetry, - Daemon: daemon, - }) - - ledgerEntryReader := db.NewLedgerEntryReader(dbConn) - preflightWorkerPool := preflight.NewPreflightWorkerPool( - daemon, - cfg.PreflightWorkerCount, - cfg.PreflightWorkerQueueSize, - cfg.PreflightEnableDebug, - ledgerEntryReader, - cfg.NetworkPassphrase, - logger, - ) - - jsonRPCHandler := internal.NewJSONRPCHandler(cfg, internal.HandlerParams{ - Daemon: daemon, - EventStore: eventStore, - TransactionStore: transactionStore, - Logger: logger, - LedgerReader: db.NewLedgerReader(dbConn), - LedgerEntryReader: db.NewLedgerEntryReader(dbConn), - PreflightGetter: preflightWorkerPool, - }) - - httpHandler := supporthttp.NewAPIMux(logger) - httpHandler.Handle("/", jsonRPCHandler) - - daemon.preflightWorkerPool = preflightWorkerPool - daemon.ingestService = ingestService - daemon.jsonRPCHandler = &jsonRPCHandler - - daemon.server = &http.Server{ - Addr: cfg.Endpoint, - Handler: httpHandler, - ReadTimeout: defaultReadTimeout, - } - if cfg.AdminEndpoint != "" { - adminMux := supporthttp.NewMux(logger) - adminMux.HandleFunc("/debug/pprof/", pprof.Index) - adminMux.HandleFunc("/debug/pprof/cmdline", pprof.Cmdline) - adminMux.HandleFunc("/debug/pprof/profile", pprof.Profile) - adminMux.HandleFunc("/debug/pprof/symbol", pprof.Symbol) - adminMux.HandleFunc("/debug/pprof/trace", pprof.Trace) - // add the entry points for: - // goroutine, threadcreate, heap, allocs, block, mutex - for _, profile := range runtimePprof.Profiles() { - adminMux.Handle("/debug/pprof/"+profile.Name(), pprof.Handler(profile.Name())) - } - adminMux.Handle("/metrics", promhttp.HandlerFor(metricsRegistry, promhttp.HandlerOpts{})) - daemon.adminServer = &http.Server{Addr: cfg.AdminEndpoint, Handler: adminMux} - } - daemon.registerMetrics() - return daemon -} - -func (d *Daemon) Run() { - d.logger.WithFields(supportlog.F{ - "version": config.Version, - "commit": config.CommitHash, - "addr": d.server.Addr, - }).Info("starting Soroban JSON RPC server") - - panicGroup := util.UnrecoverablePanicGroup.Log(d.logger) - panicGroup.Go(func() { - if err := d.server.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { - // Error starting or closing listener: - d.logger.WithError(err).Fatal("soroban JSON RPC server encountered fatal error") - } - }) - - if d.adminServer != nil { - panicGroup.Go(func() { - if err := d.adminServer.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) { - d.logger.WithError(err).Error("soroban admin server encountered fatal error") - } - }) - } - - // Shutdown gracefully when we receive an interrupt signal. - // First server.Shutdown closes all open listeners, then closes all idle connections. - // Finally, it waits a grace period (10s here) for connections to return to idle and then shut down. - signals := make(chan os.Signal, 1) - signal.Notify(signals, syscall.SIGINT, syscall.SIGTERM) - - select { - case <-signals: - d.Close() - case <-d.done: - return - } -} diff --git a/cmd/soroban-rpc/internal/daemon/interfaces/interfaces.go b/cmd/soroban-rpc/internal/daemon/interfaces/interfaces.go deleted file mode 100644 index 529ecdef2..000000000 --- a/cmd/soroban-rpc/internal/daemon/interfaces/interfaces.go +++ /dev/null @@ -1,22 +0,0 @@ -package interfaces - -import ( - "context" - - "github.com/prometheus/client_golang/prometheus" - proto "github.com/stellar/go/protocols/stellarcore" -) - -// Daemon defines the interface that the Daemon would be implementing. -// this would be useful for decoupling purposes, allowing to test components without -// the actual daemon. -type Daemon interface { - MetricsRegistry() *prometheus.Registry - MetricsNamespace() string - CoreClient() CoreClient -} - -type CoreClient interface { - Info(ctx context.Context) (*proto.InfoResponse, error) - SubmitTransaction(context.Context, string) (*proto.TXResponse, error) -} diff --git a/cmd/soroban-rpc/internal/daemon/interfaces/noOpDaemon.go b/cmd/soroban-rpc/internal/daemon/interfaces/noOpDaemon.go deleted file mode 100644 index e73689a5d..000000000 --- a/cmd/soroban-rpc/internal/daemon/interfaces/noOpDaemon.go +++ /dev/null @@ -1,46 +0,0 @@ -package interfaces - -import ( - "context" - - "github.com/prometheus/client_golang/prometheus" - proto "github.com/stellar/go/protocols/stellarcore" -) - -// The noOpDeamon is a dummy daemon implementation, supporting the Daemon interface. -// Used only in testing. -type noOpDaemon struct { - metricsRegistry *prometheus.Registry - metricsNamespace string - coreClient noOpCoreClient -} - -func MakeNoOpDeamon() *noOpDaemon { - return &noOpDaemon{ - metricsRegistry: prometheus.NewRegistry(), - metricsNamespace: "soroban_rpc", - coreClient: noOpCoreClient{}, - } -} - -func (d *noOpDaemon) MetricsRegistry() *prometheus.Registry { - return d.metricsRegistry -} - -func (d *noOpDaemon) MetricsNamespace() string { - return d.metricsNamespace -} - -func (d *noOpDaemon) CoreClient() CoreClient { - return d.coreClient -} - -type noOpCoreClient struct{} - -func (s noOpCoreClient) Info(context.Context) (*proto.InfoResponse, error) { - return &proto.InfoResponse{}, nil -} - -func (s noOpCoreClient) SubmitTransaction(context.Context, string) (*proto.TXResponse, error) { - return &proto.TXResponse{Status: proto.PreflightStatusOk}, nil -} diff --git a/cmd/soroban-rpc/internal/daemon/metrics.go b/cmd/soroban-rpc/internal/daemon/metrics.go deleted file mode 100644 index c7c444840..000000000 --- a/cmd/soroban-rpc/internal/daemon/metrics.go +++ /dev/null @@ -1,104 +0,0 @@ -package daemon - -import ( - "context" - "runtime" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/clients/stellarcore" - proto "github.com/stellar/go/protocols/stellarcore" - "github.com/stellar/go/support/logmetrics" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" -) - -func (d *Daemon) registerMetrics() { - buildInfoGauge := prometheus.NewGaugeVec( - prometheus.GaugeOpts{Namespace: prometheusNamespace, Subsystem: "build", Name: "info"}, - []string{"version", "goversion", "commit", "branch", "build_timestamp"}, - ) - // LogMetricsHook is a metric which counts log lines emitted by soroban rpc - LogMetricsHook := logmetrics.New(prometheusNamespace) - // - buildInfoGauge.With(prometheus.Labels{ - "version": config.Version, - "commit": config.CommitHash, - "branch": config.Branch, - "build_timestamp": config.BuildTimestamp, - "goversion": runtime.Version(), - }).Inc() - - d.metricsRegistry.MustRegister(prometheus.NewGoCollector()) - d.metricsRegistry.MustRegister(prometheus.NewProcessCollector(prometheus.ProcessCollectorOpts{})) - d.metricsRegistry.MustRegister(buildInfoGauge) - - for _, counter := range LogMetricsHook { - d.metricsRegistry.MustRegister(counter) - } -} - -func (d *Daemon) MetricsRegistry() *prometheus.Registry { - return d.metricsRegistry -} - -func (d *Daemon) MetricsNamespace() string { - return prometheusNamespace -} - -type CoreClientWithMetrics struct { - stellarcore.Client - submitMetric *prometheus.SummaryVec - opCountMetric *prometheus.SummaryVec -} - -func newCoreClientWithMetrics(client stellarcore.Client, registry *prometheus.Registry) *CoreClientWithMetrics { - submitMetric := prometheus.NewSummaryVec(prometheus.SummaryOpts{ - Namespace: prometheusNamespace, Subsystem: "txsub", Name: "submission_duration_seconds", - Help: "submission durations to Stellar-Core, sliding window = 10m", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, []string{"status"}) - opCountMetric := prometheus.NewSummaryVec(prometheus.SummaryOpts{ - Namespace: prometheusNamespace, Subsystem: "txsub", Name: "operation_count", - Help: "number of operations included in a transaction, sliding window = 10m", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, []string{"status"}) - registry.MustRegister(submitMetric, opCountMetric) - - return &CoreClientWithMetrics{ - Client: client, - submitMetric: submitMetric, - opCountMetric: opCountMetric, - } -} - -func (c *CoreClientWithMetrics) SubmitTransaction(ctx context.Context, envelopeBase64 string) (*proto.TXResponse, error) { - var envelope xdr.TransactionEnvelope - err := xdr.SafeUnmarshalBase64(envelopeBase64, &envelope) - if err != nil { - return nil, err - } - - startTime := time.Now() - response, err := c.Client.SubmitTransaction(ctx, envelopeBase64) - duration := time.Since(startTime).Seconds() - - var label prometheus.Labels - if err != nil { - label = prometheus.Labels{"status": "request_error"} - } else if response.IsException() { - label = prometheus.Labels{"status": "exception"} - } else { - label = prometheus.Labels{"status": response.Status} - } - - c.submitMetric.With(label).Observe(duration) - c.opCountMetric.With(label).Observe(float64(len(envelope.Operations()))) - return response, err -} - -func (d *Daemon) CoreClient() interfaces.CoreClient { - return d.coreClient -} diff --git a/cmd/soroban-rpc/internal/db/db.go b/cmd/soroban-rpc/internal/db/db.go deleted file mode 100644 index 428f29fe4..000000000 --- a/cmd/soroban-rpc/internal/db/db.go +++ /dev/null @@ -1,267 +0,0 @@ -package db - -import ( - "context" - "database/sql" - "embed" - "fmt" - "strconv" - "sync" - - sq "github.com/Masterminds/squirrel" - _ "github.com/mattn/go-sqlite3" - "github.com/prometheus/client_golang/prometheus" - migrate "github.com/rubenv/sql-migrate" - - "github.com/stellar/go/support/db" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -//go:embed migrations/*.sql -var migrations embed.FS - -var ErrEmptyDB = errors.New("DB is empty") - -const ( - metaTableName = "metadata" - latestLedgerSequenceMetaKey = "LatestLedgerSequence" -) - -type ReadWriter interface { - NewTx(ctx context.Context) (WriteTx, error) - GetLatestLedgerSequence(ctx context.Context) (uint32, error) -} - -type WriteTx interface { - LedgerEntryWriter() LedgerEntryWriter - LedgerWriter() LedgerWriter - Commit(ledgerSeq uint32) error - Rollback() error -} - -type dbCache struct { - latestLedgerSeq uint32 - ledgerEntries transactionalCache // Just like the DB: compress-encoded ledger key -> ledger entry XDR - sync.RWMutex -} - -type DB struct { - db.SessionInterface - cache dbCache -} - -func openSQLiteDB(dbFilePath string) (*db.Session, error) { - // 1. Use Write-Ahead Logging (WAL). - // 2. Disable WAL auto-checkpointing (we will do the checkpointing ourselves with wal_checkpoint pragmas - // after every write transaction). - // 3. Use synchronous=NORMAL, which is faster and still safe in WAL mode. - session, err := db.Open("sqlite3", fmt.Sprintf("file:%s?_journal_mode=WAL&_wal_autocheckpoint=0&_synchronous=NORMAL", dbFilePath)) - if err != nil { - return nil, errors.Wrap(err, "open failed") - } - - if err = runMigrations(session.DB.DB, "sqlite3"); err != nil { - _ = session.Close() - return nil, errors.Wrap(err, "could not run migrations") - } - return session, nil -} - -func OpenSQLiteDBWithPrometheusMetrics(dbFilePath string, namespace string, sub db.Subservice, registry *prometheus.Registry) (*DB, error) { - session, err := openSQLiteDB(dbFilePath) - if err != nil { - return nil, err - } - result := DB{ - SessionInterface: db.RegisterMetrics(session, namespace, sub, registry), - cache: dbCache{ - ledgerEntries: newTransactionalCache(), - }, - } - return &result, nil -} - -func OpenSQLiteDB(dbFilePath string) (*DB, error) { - session, err := openSQLiteDB(dbFilePath) - if err != nil { - return nil, err - } - result := DB{ - SessionInterface: session, - cache: dbCache{ - ledgerEntries: newTransactionalCache(), - }, - } - return &result, nil -} - -func getLatestLedgerSequence(ctx context.Context, q db.SessionInterface, cache *dbCache) (uint32, error) { - sql := sq.Select("value").From(metaTableName).Where(sq.Eq{"key": latestLedgerSequenceMetaKey}) - var results []string - if err := q.Select(ctx, &results, sql); err != nil { - return 0, err - } - switch len(results) { - case 0: - return 0, ErrEmptyDB - case 1: - // expected length on an initialized DB - default: - return 0, fmt.Errorf("multiple entries (%d) for key %q in table %q", len(results), latestLedgerSequenceMetaKey, metaTableName) - } - latestLedgerStr := results[0] - latestLedger, err := strconv.ParseUint(latestLedgerStr, 10, 32) - if err != nil { - return 0, err - } - result := uint32(latestLedger) - - // Add missing ledger sequence to the top cache. - // Otherwise, the write-through cache won't get updated until the first ingestion commit - cache.Lock() - if cache.latestLedgerSeq == 0 { - // Only update the cache if value is missing (0), otherwise - // we may end up overwriting the entry with an older version - cache.latestLedgerSeq = result - } - cache.Unlock() - - return result, nil -} - -type readWriter struct { - db *DB - maxBatchSize int - ledgerRetentionWindow uint32 -} - -// NewReadWriter constructs a new ReadWriter instance and configures -// the size of ledger entry batches when writing ledger entries -// and the retention window for how many historical ledgers are -// recorded in the database. -func NewReadWriter(db *DB, maxBatchSize int, ledgerRetentionWindow uint32) ReadWriter { - return &readWriter{ - db: db, - maxBatchSize: maxBatchSize, - ledgerRetentionWindow: ledgerRetentionWindow, - } -} - -func (rw *readWriter) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - return getLatestLedgerSequence(ctx, rw.db, &rw.db.cache) -} - -func (rw *readWriter) NewTx(ctx context.Context) (WriteTx, error) { - txSession := rw.db.Clone() - if err := txSession.Begin(ctx); err != nil { - return nil, err - } - stmtCache := sq.NewStmtCache(txSession.GetTx()) - db := rw.db - return writeTx{ - globalCache: &db.cache, - postCommit: func() error { - _, err := db.ExecRaw(ctx, "PRAGMA wal_checkpoint(TRUNCATE)") - return err - }, - tx: txSession, - stmtCache: stmtCache, - ledgerWriter: ledgerWriter{stmtCache: stmtCache}, - ledgerEntryWriter: ledgerEntryWriter{ - stmtCache: stmtCache, - buffer: xdr.NewEncodingBuffer(), - keyToEntryBatch: make(map[string]*xdr.LedgerEntry, rw.maxBatchSize), - ledgerEntryCacheWriteTx: db.cache.ledgerEntries.newWriteTx(rw.maxBatchSize), - maxBatchSize: rw.maxBatchSize, - }, - ledgerRetentionWindow: rw.ledgerRetentionWindow, - }, nil -} - -type writeTx struct { - globalCache *dbCache - postCommit func() error - tx db.SessionInterface - stmtCache *sq.StmtCache - ledgerEntryWriter ledgerEntryWriter - ledgerWriter ledgerWriter - ledgerRetentionWindow uint32 -} - -func (w writeTx) LedgerEntryWriter() LedgerEntryWriter { - return w.ledgerEntryWriter -} - -func (w writeTx) LedgerWriter() LedgerWriter { - return w.ledgerWriter -} - -func (w writeTx) Commit(ledgerSeq uint32) error { - if err := w.ledgerEntryWriter.flush(); err != nil { - return err - } - - if err := w.ledgerWriter.trimLedgers(ledgerSeq, w.ledgerRetentionWindow); err != nil { - return err - } - - _, err := sq.Replace(metaTableName).RunWith(w.stmtCache). - Values(latestLedgerSequenceMetaKey, fmt.Sprintf("%d", ledgerSeq)).Exec() - if err != nil { - return err - } - - // We need to make the cache update atomic with the transaction commit. - // Otherwise, the cache can be made inconsistent if a write transaction finishes - // in between, updating the cache in the wrong order. - commitAndUpdateCache := func() error { - w.globalCache.Lock() - defer w.globalCache.Unlock() - if err = w.tx.Commit(); err != nil { - return err - } - w.globalCache.latestLedgerSeq = ledgerSeq - w.ledgerEntryWriter.ledgerEntryCacheWriteTx.commit() - return nil - } - if err := commitAndUpdateCache(); err != nil { - return err - } - - return w.postCommit() -} - -func (w writeTx) Rollback() error { - // errors.New("not in transaction") is returned when rolling back a transaction which has - // already been committed or rolled back. We can ignore those errors - // because we allow rolling back after commits in defer statements. - if err := w.tx.Rollback(); err == nil || err.Error() == "not in transaction" { - return nil - } else { - return err - } -} - -func runMigrations(db *sql.DB, dialect string) error { - m := &migrate.AssetMigrationSource{ - Asset: migrations.ReadFile, - AssetDir: func() func(string) ([]string, error) { - return func(path string) ([]string, error) { - dirEntry, err := migrations.ReadDir(path) - if err != nil { - return nil, err - } - entries := make([]string, 0) - for _, e := range dirEntry { - entries = append(entries, e.Name()) - } - - return entries, nil - } - }(), - Dir: "migrations", - } - _, err := migrate.ExecMax(db, dialect, m, migrate.Up, 0) - return err -} diff --git a/cmd/soroban-rpc/internal/db/ledger.go b/cmd/soroban-rpc/internal/db/ledger.go deleted file mode 100644 index 1b4b0aa24..000000000 --- a/cmd/soroban-rpc/internal/db/ledger.go +++ /dev/null @@ -1,94 +0,0 @@ -package db - -import ( - "context" - "fmt" - - sq "github.com/Masterminds/squirrel" - - "github.com/stellar/go/xdr" -) - -const ( - ledgerCloseMetaTableName = "ledger_close_meta" -) - -type StreamLedgerFn func(xdr.LedgerCloseMeta) error - -type LedgerReader interface { - GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) - StreamAllLedgers(ctx context.Context, f StreamLedgerFn) error -} - -type LedgerWriter interface { - InsertLedger(ledger xdr.LedgerCloseMeta) error -} - -type ledgerReader struct { - db *DB -} - -func NewLedgerReader(db *DB) LedgerReader { - return ledgerReader{db: db} -} - -// StreamAllLedgers runs f over all the ledgers in the database (until f errors or signals it's done). -func (r ledgerReader) StreamAllLedgers(ctx context.Context, f StreamLedgerFn) error { - sql := sq.Select("meta").From(ledgerCloseMetaTableName).OrderBy("sequence asc") - q, err := r.db.Query(ctx, sql) - if err != nil { - return err - } - defer q.Close() - for q.Next() { - var closeMeta xdr.LedgerCloseMeta - if err = q.Scan(&closeMeta); err != nil { - return err - } - if err = f(closeMeta); err != nil { - return err - } - } - return nil -} - -// GetLedger fetches a single ledger from the db. -func (r ledgerReader) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) { - sql := sq.Select("meta").From(ledgerCloseMetaTableName).Where(sq.Eq{"sequence": sequence}) - var results []xdr.LedgerCloseMeta - if err := r.db.Select(ctx, &results, sql); err != nil { - return xdr.LedgerCloseMeta{}, false, err - } - switch len(results) { - case 0: - return xdr.LedgerCloseMeta{}, false, nil - case 1: - return results[0], true, nil - default: - return xdr.LedgerCloseMeta{}, false, fmt.Errorf("multiple lcm entries (%d) for sequence %d in table %q", len(results), sequence, ledgerCloseMetaTableName) - } -} - -type ledgerWriter struct { - stmtCache *sq.StmtCache -} - -// trimLedgers removes all ledgers which fall outside the retention window. -func (l ledgerWriter) trimLedgers(latestLedgerSeq uint32, retentionWindow uint32) error { - if latestLedgerSeq+1 <= retentionWindow { - return nil - } - cutoff := latestLedgerSeq + 1 - retentionWindow - deleteSQL := sq.StatementBuilder.RunWith(l.stmtCache).Delete(ledgerCloseMetaTableName).Where(sq.Lt{"sequence": cutoff}) - _, err := deleteSQL.Exec() - return err -} - -// InsertLedger inserts a ledger in the db. -func (l ledgerWriter) InsertLedger(ledger xdr.LedgerCloseMeta) error { - _, err := sq.StatementBuilder.RunWith(l.stmtCache). - Insert(ledgerCloseMetaTableName). - Values(ledger.LedgerSequence(), ledger). - Exec() - return err -} diff --git a/cmd/soroban-rpc/internal/db/ledger_test.go b/cmd/soroban-rpc/internal/db/ledger_test.go deleted file mode 100644 index bbbfdbee8..000000000 --- a/cmd/soroban-rpc/internal/db/ledger_test.go +++ /dev/null @@ -1,95 +0,0 @@ -package db - -import ( - "context" - "testing" - - "github.com/stretchr/testify/assert" - - "github.com/stellar/go/xdr" -) - -func createLedger(ledgerSequence uint32) xdr.LedgerCloseMeta { - return xdr.LedgerCloseMeta{ - V: 1, - V1: &xdr.LedgerCloseMetaV1{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Hash: xdr.Hash{}, - Header: xdr.LedgerHeader{ - LedgerSeq: xdr.Uint32(ledgerSequence), - }, - }, - TxSet: xdr.GeneralizedTransactionSet{ - V: 1, - V1TxSet: &xdr.TransactionSetV1{}, - }, - }, - } -} - -func assertLedgerRange(t *testing.T, reader LedgerReader, start, end uint32) { - var allLedgers []xdr.LedgerCloseMeta - err := reader.StreamAllLedgers(context.Background(), func(txmeta xdr.LedgerCloseMeta) error { - allLedgers = append(allLedgers, txmeta) - return nil - }) - assert.NoError(t, err) - for i := start - 1; i <= end+1; i++ { - ledger, exists, err := reader.GetLedger(context.Background(), i) - assert.NoError(t, err) - if i < start || i > end { - assert.False(t, exists) - continue - } - assert.True(t, exists) - ledgerBinary, err := ledger.MarshalBinary() - assert.NoError(t, err) - expected := createLedger(i) - expectedBinary, err := expected.MarshalBinary() - assert.NoError(t, err) - assert.Equal(t, expectedBinary, ledgerBinary) - - ledgerBinary, err = allLedgers[0].MarshalBinary() - assert.NoError(t, err) - assert.Equal(t, expectedBinary, ledgerBinary) - allLedgers = allLedgers[1:] - } - assert.Empty(t, allLedgers) -} - -func TestLedgers(t *testing.T) { - db := NewTestDB(t) - - reader := NewLedgerReader(db) - _, exists, err := reader.GetLedger(context.Background(), 1) - assert.NoError(t, err) - assert.False(t, exists) - - for i := 1; i <= 10; i++ { - ledgerSequence := uint32(i) - tx, err := NewReadWriter(db, 150, 15).NewTx(context.Background()) - assert.NoError(t, err) - assert.NoError(t, tx.LedgerWriter().InsertLedger(createLedger(ledgerSequence))) - assert.NoError(t, tx.Commit(ledgerSequence)) - // rolling back after a commit is a no-op - assert.NoError(t, tx.Rollback()) - } - - assertLedgerRange(t, reader, 1, 10) - - ledgerSequence := uint32(11) - tx, err := NewReadWriter(db, 150, 15).NewTx(context.Background()) - assert.NoError(t, err) - assert.NoError(t, tx.LedgerWriter().InsertLedger(createLedger(ledgerSequence))) - assert.NoError(t, tx.Commit(ledgerSequence)) - - assertLedgerRange(t, reader, 1, 11) - - ledgerSequence = uint32(12) - tx, err = NewReadWriter(db, 150, 5).NewTx(context.Background()) - assert.NoError(t, err) - assert.NoError(t, tx.LedgerWriter().InsertLedger(createLedger(ledgerSequence))) - assert.NoError(t, tx.Commit(ledgerSequence)) - - assertLedgerRange(t, reader, 8, 12) -} diff --git a/cmd/soroban-rpc/internal/db/ledgerentry.go b/cmd/soroban-rpc/internal/db/ledgerentry.go deleted file mode 100644 index 1553ecf59..000000000 --- a/cmd/soroban-rpc/internal/db/ledgerentry.go +++ /dev/null @@ -1,394 +0,0 @@ -package db - -import ( - "context" - "crypto/sha256" - "database/sql" - "fmt" - - sq "github.com/Masterminds/squirrel" - - "github.com/stellar/go/support/db" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" -) - -const ( - ledgerEntriesTableName = "ledger_entries" -) - -type LedgerEntryReader interface { - GetLatestLedgerSequence(ctx context.Context) (uint32, error) - NewTx(ctx context.Context) (LedgerEntryReadTx, error) - NewCachedTx(ctx context.Context) (LedgerEntryReadTx, error) -} - -type LedgerKeyAndEntry struct { - Key xdr.LedgerKey - Entry xdr.LedgerEntry - LiveUntilLedgerSeq *uint32 // optional live-until ledger seq, when applicable. -} - -type LedgerEntryReadTx interface { - GetLatestLedgerSequence() (uint32, error) - GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKeyAndEntry, error) - Done() error -} - -type LedgerEntryWriter interface { - UpsertLedgerEntry(entry xdr.LedgerEntry) error - DeleteLedgerEntry(key xdr.LedgerKey) error -} - -type ledgerEntryWriter struct { - stmtCache *sq.StmtCache - buffer *xdr.EncodingBuffer - // nil entries imply deletion - keyToEntryBatch map[string]*xdr.LedgerEntry - ledgerEntryCacheWriteTx transactionalCacheWriteTx - maxBatchSize int -} - -func (l ledgerEntryWriter) UpsertLedgerEntry(entry xdr.LedgerEntry) error { - // We can do a little extra validation to ensure the entry and key match, - // because the key can be derived from the entry. - key, err := entry.LedgerKey() - if err != nil { - return errors.Wrap(err, "could not get ledger key from entry") - } - - encodedKey, err := encodeLedgerKey(l.buffer, key) - if err != nil { - return err - } - - l.keyToEntryBatch[encodedKey] = &entry - return l.maybeFlush() -} - -func (l ledgerEntryWriter) DeleteLedgerEntry(key xdr.LedgerKey) error { - encodedKey, err := encodeLedgerKey(l.buffer, key) - if err != nil { - return err - } - l.keyToEntryBatch[encodedKey] = nil - return l.maybeFlush() -} - -func (l ledgerEntryWriter) maybeFlush() error { - if len(l.keyToEntryBatch) >= l.maxBatchSize { - return l.flush() - } - return nil -} - -func (l ledgerEntryWriter) flush() error { - upsertCount := 0 - upsertSQL := sq.StatementBuilder.RunWith(l.stmtCache).Replace(ledgerEntriesTableName) - var deleteKeys = make([]string, 0, len(l.keyToEntryBatch)) - - upsertCacheUpdates := make(map[string]*string, len(l.keyToEntryBatch)) - for key, entry := range l.keyToEntryBatch { - if entry != nil { - // safe since we cast to string right away - encodedEntry, err := l.buffer.UnsafeMarshalBinary(entry) - if err != nil { - return err - } - encodedEntryStr := string(encodedEntry) - upsertSQL = upsertSQL.Values(key, encodedEntryStr) - upsertCount += 1 - // Only cache Config entries for now - if entry.Data.Type == xdr.LedgerEntryTypeConfigSetting { - upsertCacheUpdates[key] = &encodedEntryStr - } - } else { - deleteKeys = append(deleteKeys, key) - } - // Delete each entry instead of reassigning l.keyToEntryBatch - // to the empty map because the map was allocated with a - // capacity of: make(map[string]*string, rw.maxBatchSize). - // We want to reuse the hashtable buckets in subsequent - // calls to UpsertLedgerEntry / DeleteLedgerEntry. - delete(l.keyToEntryBatch, key) - } - - if upsertCount > 0 { - if _, err := upsertSQL.Exec(); err != nil { - return err - } - for key, entry := range upsertCacheUpdates { - l.ledgerEntryCacheWriteTx.upsert(key, *entry) - } - } - - if len(deleteKeys) > 0 { - deleteSQL := sq.StatementBuilder.RunWith(l.stmtCache).Delete(ledgerEntriesTableName).Where(sq.Eq{"key": deleteKeys}) - if _, err := deleteSQL.Exec(); err != nil { - return err - } - for _, key := range deleteKeys { - l.ledgerEntryCacheWriteTx.delete(key) - } - } - - return nil -} - -type ledgerEntryReadTx struct { - globalCache *dbCache - stmtCache *sq.StmtCache - latestLedgerSeqCache uint32 - ledgerEntryCacheReadTx *transactionalCacheReadTx - tx db.SessionInterface - buffer *xdr.EncodingBuffer -} - -func (l *ledgerEntryReadTx) GetLatestLedgerSequence() (uint32, error) { - if l.latestLedgerSeqCache != 0 { - return l.latestLedgerSeqCache, nil - } - latestLedgerSeq, err := getLatestLedgerSequence(context.Background(), l.tx, l.globalCache) - if err == nil { - l.latestLedgerSeqCache = latestLedgerSeq - } - return latestLedgerSeq, err -} - -// From compressed XDR keys to XDR entries (i.e. using the DB's representation) -func (l *ledgerEntryReadTx) getRawLedgerEntries(keys ...string) (map[string]string, error) { - result := make(map[string]string, len(keys)) - keysToQueryInDB := keys - if l.ledgerEntryCacheReadTx != nil { - keysToQueryInDB = make([]string, 0, len(keys)) - for _, k := range keys { - entry, ok := l.ledgerEntryCacheReadTx.get(k) - if !ok { - keysToQueryInDB = append(keysToQueryInDB, k) - } - if entry != nil { - result[k] = *entry - } - } - } - - if len(keysToQueryInDB) == 0 { - return result, nil - } - - builder := sq.StatementBuilder - if l.stmtCache != nil { - builder = builder.RunWith(l.stmtCache) - } else { - builder = builder.RunWith(l.tx.GetTx()) - } - sql := builder.Select("key", "entry").From(ledgerEntriesTableName).Where(sq.Eq{"key": keysToQueryInDB}) - q, err := sql.Query() - if err != nil { - return nil, err - } - defer q.Close() - for q.Next() { - var key, entry string - if err = q.Scan(&key, &entry); err != nil { - return nil, err - } - result[key] = entry - if l.ledgerEntryCacheReadTx != nil { - l.ledgerEntryCacheReadTx.upsert(key, &entry) - - // Add missing config setting entries to the top cache. - // Otherwise, the write-through cache won't get updated on restarts - // (after which we don't process past config setting updates) - keyType, err := xdr.GetBinaryCompressedLedgerKeyType([]byte(key)) - if err != nil { - return nil, err - } - if keyType == xdr.LedgerEntryTypeConfigSetting { - l.globalCache.Lock() - // Only update the cache if the entry is missing, otherwise - // we may end up overwriting the entry with an older version - if _, ok := l.globalCache.ledgerEntries.entries[key]; !ok { - l.globalCache.ledgerEntries.entries[key] = entry - } - defer l.globalCache.Unlock() - } - } - } - return result, nil -} - -func GetLedgerEntry(tx LedgerEntryReadTx, key xdr.LedgerKey) (bool, xdr.LedgerEntry, *uint32, error) { - keyEntries, err := tx.GetLedgerEntries(key) - if err != nil { - return false, xdr.LedgerEntry{}, nil, err - } - switch len(keyEntries) { - case 0: - return false, xdr.LedgerEntry{}, nil, nil - case 1: - // expected length - return true, keyEntries[0].Entry, keyEntries[0].LiveUntilLedgerSeq, nil - default: - return false, xdr.LedgerEntry{}, nil, fmt.Errorf("multiple entries (%d) for key %v", len(keyEntries), key) - } -} - -// hasTTLKey check to see if the key type is expected to be accompanied by a LedgerTTLEntry -func hasTTLKey(key xdr.LedgerKey) bool { - switch key.Type { - case xdr.LedgerEntryTypeContractData: - return true - case xdr.LedgerEntryTypeContractCode: - return true - default: - } - return false -} - -func entryKeyToTTLEntryKey(key xdr.LedgerKey) (xdr.LedgerKey, error) { - buf, err := key.MarshalBinary() - if err != nil { - return xdr.LedgerKey{}, err - } - var ttlEntry xdr.LedgerKey - err = ttlEntry.SetTtl(sha256.Sum256(buf)) - if err != nil { - return xdr.LedgerKey{}, err - } - return ttlEntry, nil -} - -func (l *ledgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]LedgerKeyAndEntry, error) { - encodedKeys := make([]string, 0, 2*len(keys)) - type keyToEncoded struct { - key xdr.LedgerKey - encodedKey string - encodedTTLKey *string - } - keysToEncoded := make([]keyToEncoded, len(keys)) - for i, k := range keys { - k2 := k - keysToEncoded[i].key = k2 - encodedKey, err := encodeLedgerKey(l.buffer, k) - if err != nil { - return nil, err - } - keysToEncoded[i].encodedKey = encodedKey - encodedKeys = append(encodedKeys, encodedKey) - if !hasTTLKey(k) { - continue - } - ttlEntryKey, err := entryKeyToTTLEntryKey(k) - if err != nil { - return nil, err - } - encodedTTLKey, err := encodeLedgerKey(l.buffer, ttlEntryKey) - if err != nil { - return nil, err - } - keysToEncoded[i].encodedTTLKey = &encodedTTLKey - encodedKeys = append(encodedKeys, encodedTTLKey) - } - - rawResult, err := l.getRawLedgerEntries(encodedKeys...) - if err != nil { - return nil, err - } - - result := make([]LedgerKeyAndEntry, 0, len(keys)) - for _, k2e := range keysToEncoded { - encodedEntry, ok := rawResult[k2e.encodedKey] - if !ok { - continue - } - var entry xdr.LedgerEntry - if err := xdr.SafeUnmarshal([]byte(encodedEntry), &entry); err != nil { - return nil, errors.Wrap(err, "cannot decode ledger entry from DB") - } - if k2e.encodedTTLKey == nil { - result = append(result, LedgerKeyAndEntry{k2e.key, entry, nil}) - continue - } - encodedTTLEntry, ok := rawResult[*k2e.encodedTTLKey] - if !ok { - // missing ttl key. This should not happen. - return nil, errors.New("missing ttl key entry") - } - var ttlEntry xdr.LedgerEntry - if err := xdr.SafeUnmarshal([]byte(encodedTTLEntry), &ttlEntry); err != nil { - return nil, errors.Wrap(err, "cannot decode TTL ledger entry from DB") - } - liveUntilSeq := uint32(ttlEntry.Data.Ttl.LiveUntilLedgerSeq) - result = append(result, LedgerKeyAndEntry{k2e.key, entry, &liveUntilSeq}) - } - - return result, nil -} - -func (l ledgerEntryReadTx) Done() error { - // Since it's a read-only transaction, we don't - // care whether we commit it or roll it back as long as we close it - return l.tx.Rollback() -} - -type ledgerEntryReader struct { - db *DB -} - -func NewLedgerEntryReader(db *DB) LedgerEntryReader { - return ledgerEntryReader{db: db} -} - -func (r ledgerEntryReader) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - return getLatestLedgerSequence(ctx, r.db, &r.db.cache) -} - -// NewCachedTx() caches all accessed ledger entries and select statements. If many ledger entries are accessed, it will grow without bounds. -func (r ledgerEntryReader) NewCachedTx(ctx context.Context) (LedgerEntryReadTx, error) { - txSession := r.db.Clone() - // We need to copy the cached ledger entries locally when we start the transaction - // since otherwise we would break the consistency between the transaction and the cache. - - // We need to make the parent cache access atomic with the read transaction creation. - // Otherwise, the cache can be made inconsistent if a write transaction finishes - // in between, updating the cache. - r.db.cache.RLock() - defer r.db.cache.RUnlock() - if err := txSession.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}); err != nil { - return nil, err - } - cacheReadTx := r.db.cache.ledgerEntries.newReadTx() - return &ledgerEntryReadTx{ - globalCache: &r.db.cache, - stmtCache: sq.NewStmtCache(txSession.GetTx()), - latestLedgerSeqCache: r.db.cache.latestLedgerSeq, - ledgerEntryCacheReadTx: &cacheReadTx, - tx: txSession, - buffer: xdr.NewEncodingBuffer(), - }, nil -} - -func (r ledgerEntryReader) NewTx(ctx context.Context) (LedgerEntryReadTx, error) { - txSession := r.db.Clone() - if err := txSession.BeginTx(ctx, &sql.TxOptions{ReadOnly: true}); err != nil { - return nil, err - } - r.db.cache.RLock() - defer r.db.cache.RUnlock() - return &ledgerEntryReadTx{ - globalCache: &r.db.cache, - latestLedgerSeqCache: r.db.cache.latestLedgerSeq, - tx: txSession, - buffer: xdr.NewEncodingBuffer(), - }, nil -} - -func encodeLedgerKey(buffer *xdr.EncodingBuffer, key xdr.LedgerKey) (string, error) { - // this is safe since we are converting to string right away, which causes a copy - binKey, err := buffer.LedgerKeyUnsafeMarshalBinaryCompress(key) - if err != nil { - return "", err - } - return string(binKey), nil -} diff --git a/cmd/soroban-rpc/internal/db/ledgerentry_test.go b/cmd/soroban-rpc/internal/db/ledgerentry_test.go deleted file mode 100644 index 2e6b0012c..000000000 --- a/cmd/soroban-rpc/internal/db/ledgerentry_test.go +++ /dev/null @@ -1,584 +0,0 @@ -package db - -import ( - "context" - "fmt" - "math/rand" - "path" - "sync" - "testing" - "time" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/xdr" -) - -func getLedgerEntryAndLatestLedgerSequenceWithErr(db *DB, key xdr.LedgerKey) (bool, xdr.LedgerEntry, uint32, *uint32, error) { - tx, err := NewLedgerEntryReader(db).NewTx(context.Background()) - if err != nil { - return false, xdr.LedgerEntry{}, 0, nil, err - } - var doneErr error - defer func() { - doneErr = tx.Done() - }() - - latestSeq, err := tx.GetLatestLedgerSequence() - if err != nil { - return false, xdr.LedgerEntry{}, 0, nil, err - } - - present, entry, expSeq, err := GetLedgerEntry(tx, key) - if err != nil { - return false, xdr.LedgerEntry{}, 0, nil, err - } - - return present, entry, latestSeq, expSeq, doneErr -} - -func getLedgerEntryAndLatestLedgerSequence(t require.TestingT, db *DB, key xdr.LedgerKey) (bool, xdr.LedgerEntry, uint32, *uint32) { - present, entry, latestSeq, expSeq, err := getLedgerEntryAndLatestLedgerSequenceWithErr(db, key) - require.NoError(t, err) - return present, entry, latestSeq, expSeq -} - -func TestGoldenPath(t *testing.T) { - db := NewTestDB(t) - // Check that we get an empty DB error - _, err := NewLedgerEntryReader(db).GetLatestLedgerSequence(context.Background()) - assert.Equal(t, ErrEmptyDB, err) - - tx, err := NewReadWriter(db, 150, 15).NewTx(context.Background()) - assert.NoError(t, err) - writer := tx.LedgerEntryWriter() - - // Fill the DB with a single entry and fetch it - four := xdr.Uint32(4) - six := xdr.Uint32(6) - data := xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &xdr.Hash{0xca, 0xfe}, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &four, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &six, - }, - } - key, entry := getContractDataLedgerEntry(t, data) - assert.NoError(t, writer.UpsertLedgerEntry(entry)) - - expLedgerKey, err := entryKeyToTTLEntryKey(key) - assert.NoError(t, err) - expLegerEntry := getTTLLedgerEntry(expLedgerKey) - assert.NoError(t, writer.UpsertLedgerEntry(expLegerEntry)) - - ledgerSequence := uint32(23) - assert.NoError(t, tx.Commit(ledgerSequence)) - - present, obtainedEntry, obtainedLedgerSequence, liveUntilSeq := getLedgerEntryAndLatestLedgerSequence(t, db, key) - assert.True(t, present) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - require.NotNil(t, liveUntilSeq) - assert.Equal(t, uint32(expLegerEntry.Data.Ttl.LiveUntilLedgerSeq), *liveUntilSeq) - assert.Equal(t, obtainedEntry.Data.Type, xdr.LedgerEntryTypeContractData) - assert.Equal(t, xdr.Hash{0xca, 0xfe}, *obtainedEntry.Data.ContractData.Contract.ContractId) - assert.Equal(t, six, *obtainedEntry.Data.ContractData.Val.U32) - - obtainedLedgerSequence, err = NewLedgerEntryReader(db).GetLatestLedgerSequence(context.Background()) - assert.NoError(t, err) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - - // Do another round, overwriting the ledger entry - tx, err = NewReadWriter(db, 150, 15).NewTx(context.Background()) - assert.NoError(t, err) - writer = tx.LedgerEntryWriter() - eight := xdr.Uint32(8) - entry.Data.ContractData.Val.U32 = &eight - - assert.NoError(t, writer.UpsertLedgerEntry(entry)) - - ledgerSequence = uint32(24) - assert.NoError(t, tx.Commit(ledgerSequence)) - - present, obtainedEntry, obtainedLedgerSequence, liveUntilSeq = getLedgerEntryAndLatestLedgerSequence(t, db, key) - assert.True(t, present) - require.NotNil(t, liveUntilSeq) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - assert.Equal(t, eight, *obtainedEntry.Data.ContractData.Val.U32) - - // Do another round, deleting the ledger entry - tx, err = NewReadWriter(db, 150, 15).NewTx(context.Background()) - assert.NoError(t, err) - writer = tx.LedgerEntryWriter() - assert.NoError(t, err) - - assert.NoError(t, writer.DeleteLedgerEntry(key)) - ledgerSequence = uint32(25) - assert.NoError(t, tx.Commit(ledgerSequence)) - - present, _, obtainedLedgerSequence, liveUntilSeq = getLedgerEntryAndLatestLedgerSequence(t, db, key) - assert.False(t, present) - assert.Nil(t, liveUntilSeq) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - - obtainedLedgerSequence, err = NewLedgerEntryReader(db).GetLatestLedgerSequence(context.Background()) - assert.NoError(t, err) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) -} - -func TestDeleteNonExistentLedgerEmpty(t *testing.T) { - db := NewTestDB(t) - - // Simulate a ledger which creates and deletes a ledger entry - // which would result in trying to delete a ledger entry which isn't there - tx, err := NewReadWriter(db, 150, 15).NewTx(context.Background()) - assert.NoError(t, err) - writer := tx.LedgerEntryWriter() - - four := xdr.Uint32(4) - six := xdr.Uint32(6) - data := xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &xdr.Hash{0xca, 0xfe}, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &four, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &six, - }, - } - key, _ := getContractDataLedgerEntry(t, data) - assert.NoError(t, writer.DeleteLedgerEntry(key)) - ledgerSequence := uint32(23) - assert.NoError(t, tx.Commit(ledgerSequence)) - - // Make sure that the ledger number was submitted - obtainedLedgerSequence, err := NewLedgerEntryReader(db).GetLatestLedgerSequence(context.Background()) - assert.NoError(t, err) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - - // And that the entry doesn't exist - present, _, obtainedLedgerSequence, expSeq := getLedgerEntryAndLatestLedgerSequence(t, db, key) - assert.False(t, present) - require.Nil(t, expSeq) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) -} - -func getContractDataLedgerEntry(t require.TestingT, data xdr.ContractDataEntry) (xdr.LedgerKey, xdr.LedgerEntry) { - entry := xdr.LedgerEntry{ - LastModifiedLedgerSeq: 1, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &data, - }, - Ext: xdr.LedgerEntryExt{}, - } - var key xdr.LedgerKey - err := key.SetContractData(data.Contract, data.Key, data.Durability) - require.NoError(t, err) - return key, entry -} - -func getTTLLedgerEntry(key xdr.LedgerKey) xdr.LedgerEntry { - var expLegerEntry xdr.LedgerEntry - expLegerEntry.Data.Ttl = &xdr.TtlEntry{ - KeyHash: key.Ttl.KeyHash, - LiveUntilLedgerSeq: 100, - } - expLegerEntry.Data.Type = key.Type - return expLegerEntry -} - -// Make sure that (multiple, simultaneous) read transactions can happen while a write-transaction is ongoing, -// and write is only visible once the transaction is committed -func TestReadTxsDuringWriteTx(t *testing.T) { - db := NewTestDB(t) - - // Check that we get an empty DB error - _, err := NewLedgerEntryReader(db).GetLatestLedgerSequence(context.Background()) - assert.Equal(t, ErrEmptyDB, err) - - // Start filling the DB with a single entry (enforce flushing right away) - tx, err := NewReadWriter(db, 0, 15).NewTx(context.Background()) - assert.NoError(t, err) - writer := tx.LedgerEntryWriter() - - four := xdr.Uint32(4) - six := xdr.Uint32(6) - data := xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &xdr.Hash{0xca, 0xfe}, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &four, - }, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &six, - }, - } - key, entry := getContractDataLedgerEntry(t, data) - assert.NoError(t, writer.UpsertLedgerEntry(entry)) - - expLedgerKey, err := entryKeyToTTLEntryKey(key) - assert.NoError(t, err) - expLegerEntry := getTTLLedgerEntry(expLedgerKey) - assert.NoError(t, writer.UpsertLedgerEntry(expLegerEntry)) - - // Before committing the changes, make sure multiple concurrent transactions can query the DB - readTx1, err := NewLedgerEntryReader(db).NewTx(context.Background()) - assert.NoError(t, err) - readTx2, err := NewLedgerEntryReader(db).NewTx(context.Background()) - assert.NoError(t, err) - - _, err = readTx1.GetLatestLedgerSequence() - assert.Equal(t, ErrEmptyDB, err) - present, _, expSeq, err := GetLedgerEntry(readTx1, key) - require.Nil(t, expSeq) - assert.NoError(t, err) - assert.False(t, present) - assert.NoError(t, readTx1.Done()) - - _, err = readTx2.GetLatestLedgerSequence() - assert.Equal(t, ErrEmptyDB, err) - present, _, expSeq, err = GetLedgerEntry(readTx2, key) - assert.NoError(t, err) - assert.False(t, present) - assert.Nil(t, expSeq) - assert.NoError(t, readTx2.Done()) - - // Finish the write transaction and check that the results are present - ledgerSequence := uint32(23) - assert.NoError(t, tx.Commit(ledgerSequence)) - - obtainedLedgerSequence, err := NewLedgerEntryReader(db).GetLatestLedgerSequence(context.Background()) - assert.NoError(t, err) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - - present, obtainedEntry, obtainedLedgerSequence, expSeq := getLedgerEntryAndLatestLedgerSequence(t, db, key) - assert.True(t, present) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - assert.Equal(t, six, *obtainedEntry.Data.ContractData.Val.U32) - assert.NotNil(t, expSeq) -} - -// Make sure that a write transaction can happen while multiple read transactions are ongoing, -// and write is only visible once the transaction is committed -func TestWriteTxsDuringReadTxs(t *testing.T) { - db := NewTestDB(t) - - // Check that we get an empty DB error - _, err := NewLedgerEntryReader(db).GetLatestLedgerSequence(context.Background()) - assert.Equal(t, ErrEmptyDB, err) - - // Create a multiple read transactions, interleaved with the writing process - - // First read transaction, before the write transaction is created - readTx1, err := NewLedgerEntryReader(db).NewTx(context.Background()) - assert.NoError(t, err) - - // Start filling the DB with a single entry (enforce flushing right away) - tx, err := NewReadWriter(db, 0, 15).NewTx(context.Background()) - assert.NoError(t, err) - writer := tx.LedgerEntryWriter() - - // Second read transaction, after the write transaction is created - readTx2, err := NewLedgerEntryReader(db).NewTx(context.Background()) - assert.NoError(t, err) - - four := xdr.Uint32(4) - six := xdr.Uint32(6) - data := xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &xdr.Hash{0xca, 0xfe}, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &four, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &six, - }, - } - key, entry := getContractDataLedgerEntry(t, data) - assert.NoError(t, writer.UpsertLedgerEntry(entry)) - - expLedgerKey, err := entryKeyToTTLEntryKey(key) - assert.NoError(t, err) - expLegerEntry := getTTLLedgerEntry(expLedgerKey) - assert.NoError(t, writer.UpsertLedgerEntry(expLegerEntry)) - - // Third read transaction, after the first insert has happened in the write transaction - readTx3, err := NewLedgerEntryReader(db).NewTx(context.Background()) - assert.NoError(t, err) - - // Make sure that all the read transactions get an emptyDB error before and after the write transaction is committed - for _, readTx := range []LedgerEntryReadTx{readTx1, readTx2, readTx3} { - _, err = readTx.GetLatestLedgerSequence() - assert.Equal(t, ErrEmptyDB, err) - present, _, _, err := GetLedgerEntry(readTx, key) - assert.NoError(t, err) - assert.False(t, present) - } - - // commit the write transaction - ledgerSequence := uint32(23) - assert.NoError(t, tx.Commit(ledgerSequence)) - - for _, readTx := range []LedgerEntryReadTx{readTx1, readTx2, readTx3} { - _, err = readTx.GetLatestLedgerSequence() - assert.Equal(t, ErrEmptyDB, err) - present, _, _, err := GetLedgerEntry(readTx, key) - assert.NoError(t, err) - assert.False(t, present) - } - - // Check that the results are present in the transactions happening after the commit - - obtainedLedgerSequence, err := NewLedgerEntryReader(db).GetLatestLedgerSequence(context.Background()) - assert.NoError(t, err) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - - present, obtainedEntry, obtainedLedgerSequence, expSeq := getLedgerEntryAndLatestLedgerSequence(t, db, key) - assert.True(t, present) - require.NotNil(t, expSeq) - assert.Equal(t, ledgerSequence, obtainedLedgerSequence) - assert.Equal(t, six, *obtainedEntry.Data.ContractData.Val.U32) - - for _, readTx := range []LedgerEntryReadTx{readTx1, readTx2, readTx3} { - assert.NoError(t, readTx.Done()) - } -} - -// Check that we can have coexisting reader and writer goroutines without deadlocks or errors -func TestConcurrentReadersAndWriter(t *testing.T) { - db := NewTestDB(t) - - contractID := xdr.Hash{0xca, 0xfe} - done := make(chan struct{}) - var wg sync.WaitGroup - logMessageCh := make(chan string, 1) - writer := func() { - defer wg.Done() - data := func(i int) xdr.ContractDataEntry { - val := xdr.Uint32(i) - return xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractID, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &val, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &val, - }, - } - } - rw := NewReadWriter(db, 10, 15) - for ledgerSequence := uint32(0); ledgerSequence < 1000; ledgerSequence++ { - tx, err := rw.NewTx(context.Background()) - assert.NoError(t, err) - writer := tx.LedgerEntryWriter() - for i := 0; i < 200; i++ { - key, entry := getContractDataLedgerEntry(t, data(i)) - assert.NoError(t, writer.UpsertLedgerEntry(entry)) - expLedgerKey, err := entryKeyToTTLEntryKey(key) - assert.NoError(t, err) - expLegerEntry := getTTLLedgerEntry(expLedgerKey) - assert.NoError(t, writer.UpsertLedgerEntry(expLegerEntry)) - } - assert.NoError(t, tx.Commit(ledgerSequence)) - logMessageCh <- fmt.Sprintf("Wrote ledger %d", ledgerSequence) - time.Sleep(time.Duration(rand.Int31n(30)) * time.Millisecond) - } - close(done) - } - reader := func(keyVal int) { - defer wg.Done() - val := xdr.Uint32(keyVal) - key := xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.LedgerKeyContractData{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractID, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &val, - }, - Durability: xdr.ContractDataDurabilityPersistent, - }, - } - for { - select { - case <-done: - return - default: - } - found, ledgerEntry, ledger, _, err := getLedgerEntryAndLatestLedgerSequenceWithErr(db, key) - if err != nil { - if err != ErrEmptyDB { - t.Fatalf("reader %d failed with error %v\n", keyVal, err) - } - } else { - // All entries should be found once the first write commit is done - assert.True(t, found) - logMessageCh <- fmt.Sprintf("reader %d: for ledger %d", keyVal, ledger) - assert.Equal(t, xdr.Uint32(keyVal), *ledgerEntry.Data.ContractData.Val.U32) - } - time.Sleep(time.Duration(rand.Int31n(30)) * time.Millisecond) - } - } - - // one readWriter, 32 readers - wg.Add(1) - go writer() - - for i := 1; i <= 32; i++ { - wg.Add(1) - go reader(i) - } - - workersExitCh := make(chan struct{}) - go func() { - defer close(workersExitCh) - wg.Wait() - }() - -forloop: - for { - select { - case <-workersExitCh: - break forloop - case msg := <-logMessageCh: - t.Log(msg) - } - } - -} - -func benchmarkLedgerEntry(b *testing.B, cached bool) { - db := NewTestDB(b) - keyUint32 := xdr.Uint32(0) - data := xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &xdr.Hash{0xca, 0xfe}, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &keyUint32, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &keyUint32, - }, - } - key, entry := getContractDataLedgerEntry(b, data) - tx, err := NewReadWriter(db, 150, 15).NewTx(context.Background()) - assert.NoError(b, err) - assert.NoError(b, tx.LedgerEntryWriter().UpsertLedgerEntry(entry)) - assert.NoError(b, tx.Commit(2)) - reader := NewLedgerEntryReader(db) - const numQueriesPerOp = 15 - b.ResetTimer() - b.StopTimer() - for i := 0; i < b.N; i++ { - var readTx LedgerEntryReadTx - var err error - if cached { - readTx, err = reader.NewCachedTx(context.Background()) - } else { - readTx, err = reader.NewTx(context.Background()) - } - assert.NoError(b, err) - for i := 0; i < numQueriesPerOp; i++ { - b.StartTimer() - found, _, _, err := GetLedgerEntry(readTx, key) - b.StopTimer() - assert.NoError(b, err) - assert.True(b, found) - } - assert.NoError(b, readTx.Done()) - } -} - -func BenchmarkGetLedgerEntry(b *testing.B) { - b.Run("With cache", func(b *testing.B) { benchmarkLedgerEntry(b, true) }) - b.Run("Without cache", func(b *testing.B) { benchmarkLedgerEntry(b, false) }) -} - -func BenchmarkLedgerUpdate(b *testing.B) { - db := NewTestDB(b) - keyUint32 := xdr.Uint32(0) - data := xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &xdr.Hash{0xca, 0xfe}, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &keyUint32, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &keyUint32, - }, - } - _, entry := getContractDataLedgerEntry(b, data) - const numEntriesPerOp = 3500 - b.ResetTimer() - for i := 0; i < b.N; i++ { - tx, err := NewReadWriter(db, 150, 15).NewTx(context.Background()) - assert.NoError(b, err) - writer := tx.LedgerEntryWriter() - for j := 0; j < numEntriesPerOp; j++ { - keyUint32 = xdr.Uint32(j) - assert.NoError(b, writer.UpsertLedgerEntry(entry)) - } - assert.NoError(b, tx.Commit(uint32(i+1))) - } -} - -func NewTestDB(tb testing.TB) *DB { - tmp := tb.TempDir() - dbPath := path.Join(tmp, "db.sqlite") - db, err := OpenSQLiteDB(dbPath) - if err != nil { - assert.NoError(tb, db.Close()) - } - tb.Cleanup(func() { - assert.NoError(tb, db.Close()) - }) - return &DB{ - SessionInterface: db, - cache: dbCache{ - ledgerEntries: newTransactionalCache(), - }, - } -} diff --git a/cmd/soroban-rpc/internal/db/migrations/01_init.sql b/cmd/soroban-rpc/internal/db/migrations/01_init.sql deleted file mode 100644 index 54c7e1cbc..000000000 --- a/cmd/soroban-rpc/internal/db/migrations/01_init.sql +++ /dev/null @@ -1,22 +0,0 @@ --- +migrate Up -CREATE TABLE ledger_entries ( - key BLOB NOT NULL PRIMARY KEY, - entry BLOB NOT NULL -); - --- metadata key-value store -CREATE TABLE metadata ( - key TEXT NOT NULL PRIMARY KEY, - value TEXT NOT NULL -); - --- table to store all ledgers -CREATE TABLE ledger_close_meta ( - sequence INTEGER NOT NULL PRIMARY KEY, - meta BLOB NOT NULL -); - --- +migrate Down -drop table ledger_entries cascade; -drop table ledger_entries_meta cascade; -drop table ledger_close_meta cascade; diff --git a/cmd/soroban-rpc/internal/db/transactionalcache.go b/cmd/soroban-rpc/internal/db/transactionalcache.go deleted file mode 100644 index dd14aa411..000000000 --- a/cmd/soroban-rpc/internal/db/transactionalcache.go +++ /dev/null @@ -1,65 +0,0 @@ -package db - -type transactionalCache struct { - entries map[string]string -} - -func newTransactionalCache() transactionalCache { - return transactionalCache{entries: map[string]string{}} -} - -func (c transactionalCache) newReadTx() transactionalCacheReadTx { - entries := make(map[string]*string, len(c.entries)) - for k, v := range c.entries { - localV := v - entries[k] = &localV - } - return transactionalCacheReadTx{entries: entries} -} - -func (c transactionalCache) newWriteTx(estimatedWriteCount int) transactionalCacheWriteTx { - return transactionalCacheWriteTx{ - pendingUpdates: make(map[string]*string, estimatedWriteCount), - parent: &c, - } -} - -// nil indicates not present in the underlying storage -type transactionalCacheReadTx struct { - entries map[string]*string -} - -// nil indicates not present in the underlying storage -func (r transactionalCacheReadTx) get(key string) (*string, bool) { - val, ok := r.entries[key] - return val, ok -} - -// nil indicates not present in the underlying storage -func (r transactionalCacheReadTx) upsert(key string, value *string) { - r.entries[key] = value -} - -type transactionalCacheWriteTx struct { - // nil indicates deletion - pendingUpdates map[string]*string - parent *transactionalCache -} - -func (w transactionalCacheWriteTx) upsert(key, val string) { - w.pendingUpdates[key] = &val -} - -func (w transactionalCacheWriteTx) delete(key string) { - w.pendingUpdates[key] = nil -} - -func (w transactionalCacheWriteTx) commit() { - for key, newValue := range w.pendingUpdates { - if newValue == nil { - delete(w.parent.entries, key) - } else { - w.parent.entries[key] = *newValue - } - } -} diff --git a/cmd/soroban-rpc/internal/events/cursor.go b/cmd/soroban-rpc/internal/events/cursor.go deleted file mode 100644 index 3fbfbecb8..000000000 --- a/cmd/soroban-rpc/internal/events/cursor.go +++ /dev/null @@ -1,124 +0,0 @@ -package events - -import ( - "encoding/json" - "fmt" - "math" - "strconv" - "strings" - - "github.com/stellar/go/toid" -) - -// Cursor represents the position of a Soroban event. -// Soroban events are sorted in ascending order by -// ledger sequence, transaction index, operation index, -// and event index. -type Cursor struct { - // Ledger is the sequence of the ledger which emitted the event. - Ledger uint32 - // Tx is the index of the transaction within the ledger which emitted the event. - Tx uint32 - // Op is the index of the operation within the transaction which emitted the event. - // Note: Currently, there is no use for it (events are transaction-wide and not operation-specific) - // but we keep it in order to make the API future-proof. - Op uint32 - // Event is the index of the event within in the operation which emitted the event. - Event uint32 -} - -// String returns a string representation of this cursor -func (c Cursor) String() string { - return fmt.Sprintf( - "%019d-%010d", - toid.New(int32(c.Ledger), int32(c.Tx), int32(c.Op)).ToInt64(), - c.Event, - ) -} - -// MarshalJSON marshals the cursor into JSON -func (c Cursor) MarshalJSON() ([]byte, error) { - return json.Marshal(c.String()) -} - -// UnmarshalJSON unmarshalls a cursor from the given JSON -func (c *Cursor) UnmarshalJSON(b []byte) error { - var s string - if err := json.Unmarshal(b, &s); err != nil { - return err - } - - if parsed, err := ParseCursor(s); err != nil { - return err - } else { - *c = parsed - } - return nil -} - -// ParseCursor parses the given string and returns the corresponding cursor -func ParseCursor(input string) (Cursor, error) { - parts := strings.SplitN(input, "-", 2) - if len(parts) != 2 { - return Cursor{}, fmt.Errorf("invalid event id %s", input) - } - - // Parse the first part (toid) - idInt, err := strconv.ParseInt(parts[0], 10, 64) //lint:ignore gomnd - if err != nil { - return Cursor{}, fmt.Errorf("invalid event id %s: %w", input, err) - } - parsed := toid.Parse(idInt) - - // Parse the second part (event order) - eventOrder, err := strconv.ParseUint(parts[1], 10, 32) //lint:ignore gomnd - if err != nil { - return Cursor{}, fmt.Errorf("invalid event id %s: %w", input, err) - } - - return Cursor{ - Ledger: uint32(parsed.LedgerSequence), - Tx: uint32(parsed.TransactionOrder), - Op: uint32(parsed.OperationOrder), - Event: uint32(eventOrder), - }, nil -} - -func cmp(a, b uint32) int { - if a < b { - return -1 - } - if a > b { - return 1 - } - return 0 -} - -// Cmp compares two cursors. -// 0 is returned if the c is equal to other. -// 1 is returned if c is greater than other. -// -1 is returned if c is less than other. -func (c Cursor) Cmp(other Cursor) int { - if c.Ledger == other.Ledger { - if c.Tx == other.Tx { - if c.Op == other.Op { - return cmp(c.Event, other.Event) - } - return cmp(c.Op, other.Op) - } - return cmp(c.Tx, other.Tx) - } - return cmp(c.Ledger, other.Ledger) -} - -var ( - // MinCursor is the smallest possible cursor - MinCursor = Cursor{} - // MaxCursor is the largest possible cursor - MaxCursor = Cursor{ - Ledger: math.MaxUint32, - Tx: math.MaxUint32, - Op: math.MaxUint32, - Event: math.MaxUint32, - } -) diff --git a/cmd/soroban-rpc/internal/events/cursor_test.go b/cmd/soroban-rpc/internal/events/cursor_test.go deleted file mode 100644 index 6dfe1e585..000000000 --- a/cmd/soroban-rpc/internal/events/cursor_test.go +++ /dev/null @@ -1,109 +0,0 @@ -package events - -import ( - "encoding/json" - "math" - "testing" - - "github.com/stretchr/testify/assert" -) - -func TestParseCursor(t *testing.T) { - for _, cursor := range []Cursor{ - { - Ledger: math.MaxInt32, - Tx: 1048575, - Op: 4095, - Event: math.MaxInt32, - }, - { - Ledger: 0, - Tx: 0, - Op: 0, - Event: 0, - }, - { - Ledger: 123, - Tx: 10, - Op: 5, - Event: 1, - }, - } { - parsed, err := ParseCursor(cursor.String()) - assert.NoError(t, err) - assert.Equal(t, cursor, parsed) - } -} - -func TestCursorJSON(t *testing.T) { - type options struct { - Cursor *Cursor `json:"cursor,omitempty"` - Limit uint `json:"limit,omitempty"` - } - for _, testCase := range []options{ - {nil, 100}, - {nil, 0}, - {&Cursor{ - Ledger: 1, - Tx: 2, - Op: 3, - Event: 4, - }, 100}, - } { - result, err := json.Marshal(testCase) - assert.NoError(t, err) - var parsed options - assert.NoError(t, json.Unmarshal(result, &parsed)) - assert.Equal(t, testCase, parsed) - } -} - -func TestCursorCmp(t *testing.T) { - for _, testCase := range []struct { - a Cursor - b Cursor - expected int - }{ - {MinCursor, MaxCursor, -1}, - {MinCursor, MinCursor, 0}, - {MaxCursor, MaxCursor, 0}, - { - Cursor{Ledger: 1, Tx: 2, Op: 3, Event: 4}, - Cursor{Ledger: 1, Tx: 2, Op: 3, Event: 4}, - 0, - }, - { - Cursor{Ledger: 5, Tx: 2, Op: 3, Event: 4}, - Cursor{Ledger: 7, Tx: 2, Op: 3, Event: 4}, - -1, - }, - { - Cursor{Ledger: 5, Tx: 2, Op: 3, Event: 4}, - Cursor{Ledger: 5, Tx: 7, Op: 3, Event: 4}, - -1, - }, - { - Cursor{Ledger: 5, Tx: 2, Op: 3, Event: 4}, - Cursor{Ledger: 5, Tx: 2, Op: 7, Event: 4}, - -1, - }, - { - Cursor{Ledger: 5, Tx: 2, Op: 3, Event: 4}, - Cursor{Ledger: 5, Tx: 2, Op: 3, Event: 7}, - -1, - }, - } { - a := testCase.a - b := testCase.b - expected := testCase.expected - - if got := a.Cmp(b); got != expected { - t.Fatalf("expected (%v).Cmp(%v) to be %v but got %v", a, b, expected, got) - } - a, b = b, a - expected *= -1 - if got := a.Cmp(b); got != expected { - t.Fatalf("expected (%v).Cmp(%v) to be %v but got %v", a, b, expected, got) - } - } -} diff --git a/cmd/soroban-rpc/internal/events/events.go b/cmd/soroban-rpc/internal/events/events.go deleted file mode 100644 index e854cd773..000000000 --- a/cmd/soroban-rpc/internal/events/events.go +++ /dev/null @@ -1,261 +0,0 @@ -package events - -import ( - "errors" - "io" - "sort" - "sync" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/ingest" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/ledgerbucketwindow" -) - -type event struct { - diagnosticEventXDR []byte - txIndex uint32 - eventIndex uint32 - txHash *xdr.Hash // intentionally stored as a pointer to save memory (amortized as soon as there are two events in a transaction) -} - -func (e event) cursor(ledgerSeq uint32) Cursor { - return Cursor{ - Ledger: ledgerSeq, - Tx: e.txIndex, - Event: e.eventIndex, - } -} - -// MemoryStore is an in-memory store of soroban events. -type MemoryStore struct { - // networkPassphrase is an immutable string containing the - // Stellar network passphrase. - // Accessing networkPassphrase does not need to be protected - // by the lock - networkPassphrase string - // lock protects the mutable fields below - lock sync.RWMutex - eventsByLedger *ledgerbucketwindow.LedgerBucketWindow[[]event] - eventsDurationMetric *prometheus.SummaryVec - eventCountMetric prometheus.Summary -} - -// NewMemoryStore creates a new MemoryStore. -// The retention window is in units of ledgers. -// All events occurring in the following ledger range -// [ latestLedger - retentionWindow, latestLedger ] -// will be included in the MemoryStore. If the MemoryStore -// is full, any events from new ledgers will evict -// older entries outside the retention window. -func NewMemoryStore(daemon interfaces.Daemon, networkPassphrase string, retentionWindow uint32) *MemoryStore { - window := ledgerbucketwindow.NewLedgerBucketWindow[[]event](retentionWindow) - - // eventsDurationMetric is a metric for measuring latency of event store operations - eventsDurationMetric := prometheus.NewSummaryVec(prometheus.SummaryOpts{ - Namespace: daemon.MetricsNamespace(), Subsystem: "events", Name: "operation_duration_seconds", - Help: "event store operation durations, sliding window = 10m", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, - []string{"operation"}, - ) - - eventCountMetric := prometheus.NewSummary(prometheus.SummaryOpts{ - Namespace: daemon.MetricsNamespace(), Subsystem: "events", Name: "count", - Help: "count of events ingested, sliding window = 10m", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }) - daemon.MetricsRegistry().MustRegister(eventCountMetric, eventsDurationMetric) - return &MemoryStore{ - networkPassphrase: networkPassphrase, - eventsByLedger: window, - eventsDurationMetric: eventsDurationMetric, - eventCountMetric: eventCountMetric, - } -} - -// Range defines a [Start, End) interval of Soroban events. -type Range struct { - // Start defines the (inclusive) start of the range. - Start Cursor - // ClampStart indicates whether Start should be clamped up - // to the earliest ledger available if Start is too low. - ClampStart bool - // End defines the (exclusive) end of the range. - End Cursor - // ClampEnd indicates whether End should be clamped down - // to the latest ledger available if End is too high. - ClampEnd bool -} - -type ScanFunction func(xdr.DiagnosticEvent, Cursor, int64, *xdr.Hash) bool - -// Scan applies f on all the events occurring in the given range. -// The events are processed in sorted ascending Cursor order. -// If f returns false, the scan terminates early (f will not be applied on -// remaining events in the range). Note that a read lock is held for the -// entire duration of the Scan function so f should be written in a way -// to minimize latency. -func (m *MemoryStore) Scan(eventRange Range, f ScanFunction) (uint32, error) { - startTime := time.Now() - m.lock.RLock() - defer m.lock.RUnlock() - - if err := m.validateRange(&eventRange); err != nil { - return 0, err - } - - firstLedgerInRange := eventRange.Start.Ledger - firstLedgerInWindow := m.eventsByLedger.Get(0).LedgerSeq - lastLedgerInWindow := firstLedgerInWindow + (m.eventsByLedger.Len() - 1) - for i := firstLedgerInRange - firstLedgerInWindow; i < m.eventsByLedger.Len(); i++ { - bucket := m.eventsByLedger.Get(i) - events := bucket.BucketContent - if bucket.LedgerSeq == firstLedgerInRange { - // we need to seek for the beginning of the events in the first bucket in the range - events = seek(events, eventRange.Start) - } - timestamp := bucket.LedgerCloseTimestamp - for _, event := range events { - cur := event.cursor(bucket.LedgerSeq) - if eventRange.End.Cmp(cur) <= 0 { - return lastLedgerInWindow, nil - } - var diagnosticEvent xdr.DiagnosticEvent - err := xdr.SafeUnmarshal(event.diagnosticEventXDR, &diagnosticEvent) - if err != nil { - return 0, err - } - if !f(diagnosticEvent, cur, timestamp, event.txHash) { - return lastLedgerInWindow, nil - } - } - } - m.eventsDurationMetric.With(prometheus.Labels{"operation": "scan"}). - Observe(time.Since(startTime).Seconds()) - return lastLedgerInWindow, nil -} - -// validateRange checks if the range falls within the bounds -// of the events in the memory store. -// validateRange should be called with the read lock. -func (m *MemoryStore) validateRange(eventRange *Range) error { - if m.eventsByLedger.Len() == 0 { - return errors.New("event store is empty") - } - firstBucket := m.eventsByLedger.Get(0) - min := Cursor{Ledger: firstBucket.LedgerSeq} - if eventRange.Start.Cmp(min) < 0 { - if eventRange.ClampStart { - eventRange.Start = min - } else { - return errors.New("start is before oldest ledger") - } - } - max := Cursor{Ledger: min.Ledger + m.eventsByLedger.Len()} - if eventRange.Start.Cmp(max) >= 0 { - return errors.New("start is after newest ledger") - } - if eventRange.End.Cmp(max) > 0 { - if eventRange.ClampEnd { - eventRange.End = max - } else { - return errors.New("end is after latest ledger") - } - } - - if eventRange.Start.Cmp(eventRange.End) >= 0 { - return errors.New("start is not before end") - } - - return nil -} - -// seek returns the subset of all events which occur -// at a point greater than or equal to the given cursor. -// events must be sorted in ascending order. -func seek(events []event, cursor Cursor) []event { - j := sort.Search(len(events), func(i int) bool { - return cursor.Cmp(events[i].cursor(cursor.Ledger)) <= 0 - }) - return events[j:] -} - -// IngestEvents adds new events from the given ledger into the store. -// As a side effect, events which fall outside the retention window are -// removed from the store. -func (m *MemoryStore) IngestEvents(ledgerCloseMeta xdr.LedgerCloseMeta) error { - startTime := time.Now() - // no need to acquire the lock because the networkPassphrase field - // is immutable - events, err := readEvents(m.networkPassphrase, ledgerCloseMeta) - if err != nil { - return err - } - bucket := ledgerbucketwindow.LedgerBucket[[]event]{ - LedgerSeq: ledgerCloseMeta.LedgerSequence(), - LedgerCloseTimestamp: int64(ledgerCloseMeta.LedgerHeaderHistoryEntry().Header.ScpValue.CloseTime), - BucketContent: events, - } - m.lock.Lock() - if _, err = m.eventsByLedger.Append(bucket); err != nil { - return err - } - m.lock.Unlock() - m.eventsDurationMetric.With(prometheus.Labels{"operation": "ingest"}). - Observe(time.Since(startTime).Seconds()) - m.eventCountMetric.Observe(float64(len(events))) - return nil -} - -func readEvents(networkPassphrase string, ledgerCloseMeta xdr.LedgerCloseMeta) (events []event, err error) { - var txReader *ingest.LedgerTransactionReader - txReader, err = ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(networkPassphrase, ledgerCloseMeta) - if err != nil { - return - } - defer func() { - closeErr := txReader.Close() - if err == nil { - err = closeErr - } - }() - - for { - var tx ingest.LedgerTransaction - tx, err = txReader.Read() - if err == io.EOF { - err = nil - break - } - if err != nil { - return - } - - if !tx.Result.Successful() { - continue - } - - txEvents, err := tx.GetDiagnosticEvents() - if err != nil { - return nil, err - } - txHash := tx.Result.TransactionHash - for index, e := range txEvents { - diagnosticEventXDR, err := e.MarshalBinary() - if err != nil { - return nil, err - } - events = append(events, event{ - diagnosticEventXDR: diagnosticEventXDR, - txIndex: tx.Index, - eventIndex: uint32(index), - txHash: &txHash, - }) - } - } - return events, err -} diff --git a/cmd/soroban-rpc/internal/events/events_test.go b/cmd/soroban-rpc/internal/events/events_test.go deleted file mode 100644 index df9b3385c..000000000 --- a/cmd/soroban-rpc/internal/events/events_test.go +++ /dev/null @@ -1,391 +0,0 @@ -package events - -import ( - "bytes" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/require" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/ledgerbucketwindow" -) - -var ( - ledger5CloseTime = ledgerCloseTime(5) - ledger5Events = []event{ - newEvent(1, 0, 100), - newEvent(1, 1, 200), - newEvent(2, 0, 300), - newEvent(2, 1, 400), - } - ledger6CloseTime = ledgerCloseTime(6) - ledger6Events []event = nil - ledger7CloseTime = ledgerCloseTime(7) - ledger7Events = []event{ - newEvent(1, 0, 500), - } - ledger8CloseTime = ledgerCloseTime(8) - ledger8Events = []event{ - newEvent(1, 0, 600), - newEvent(2, 0, 700), - newEvent(2, 1, 800), - newEvent(2, 2, 900), - newEvent(2, 3, 1000), - } -) - -func ledgerCloseTime(seq uint32) int64 { - return int64(seq)*25 + 100 -} - -func newEvent(txIndex, eventIndex, val uint32) event { - v := xdr.Uint32(val) - - e := xdr.DiagnosticEvent{ - InSuccessfulContractCall: true, - Event: xdr.ContractEvent{ - Type: xdr.ContractEventTypeSystem, - Body: xdr.ContractEventBody{ - V: 0, - V0: &xdr.ContractEventV0{ - Data: xdr.ScVal{ - Type: xdr.ScValTypeScvU32, - U32: &v, - }, - }, - }, - }, - } - diagnosticEventXDR, err := e.MarshalBinary() - if err != nil { - panic(err) - } - return event{ - diagnosticEventXDR: diagnosticEventXDR, - txIndex: txIndex, - eventIndex: eventIndex, - } -} - -func (e event) equals(other event) bool { - return e.txIndex == other.txIndex && - e.eventIndex == other.eventIndex && - bytes.Equal(e.diagnosticEventXDR, other.diagnosticEventXDR) -} - -func eventsAreEqual(t *testing.T, a, b []event) { - require.Equal(t, len(a), len(b)) - for i := range a { - require.True(t, a[i].equals(b[i])) - } -} - -func TestScanRangeValidation(t *testing.T) { - m := NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 4) - assertNoCalls := func(xdr.DiagnosticEvent, Cursor, int64, *xdr.Hash) bool { - t.Fatalf("unexpected call") - return true - } - _, err := m.Scan(Range{ - Start: MinCursor, - ClampStart: true, - End: MaxCursor, - ClampEnd: true, - }, assertNoCalls) - require.EqualError(t, err, "event store is empty") - - m = createStore(t) - - for _, testCase := range []struct { - input Range - err string - }{ - { - Range{ - Start: MinCursor, - ClampStart: false, - End: MaxCursor, - ClampEnd: true, - }, - "start is before oldest ledger", - }, - { - Range{ - Start: Cursor{Ledger: 4}, - ClampStart: false, - End: MaxCursor, - ClampEnd: true, - }, - "start is before oldest ledger", - }, - { - Range{ - Start: MinCursor, - ClampStart: true, - End: MaxCursor, - ClampEnd: false, - }, - "end is after latest ledger", - }, - { - Range{ - Start: Cursor{Ledger: 5}, - ClampStart: true, - End: Cursor{Ledger: 10}, - ClampEnd: false, - }, - "end is after latest ledger", - }, - { - Range{ - Start: Cursor{Ledger: 10}, - ClampStart: true, - End: Cursor{Ledger: 3}, - ClampEnd: true, - }, - "start is after newest ledger", - }, - { - Range{ - Start: Cursor{Ledger: 10}, - ClampStart: false, - End: Cursor{Ledger: 3}, - ClampEnd: false, - }, - "start is after newest ledger", - }, - { - Range{ - Start: Cursor{Ledger: 9}, - ClampStart: false, - End: Cursor{Ledger: 10}, - ClampEnd: true, - }, - "start is after newest ledger", - }, - { - Range{ - Start: Cursor{Ledger: 9}, - ClampStart: false, - End: Cursor{Ledger: 10}, - ClampEnd: false, - }, - "start is after newest ledger", - }, - { - Range{ - Start: Cursor{Ledger: 2}, - ClampStart: true, - End: Cursor{Ledger: 3}, - ClampEnd: false, - }, - "start is not before end", - }, - { - Range{ - Start: Cursor{Ledger: 2}, - ClampStart: false, - End: Cursor{Ledger: 3}, - ClampEnd: false, - }, - "start is before oldest ledger", - }, - { - Range{ - Start: Cursor{Ledger: 6}, - ClampStart: false, - End: Cursor{Ledger: 6}, - ClampEnd: false, - }, - "start is not before end", - }, - } { - _, err := m.Scan(testCase.input, assertNoCalls) - require.EqualError(t, err, testCase.err, testCase.input) - } -} - -func createStore(t *testing.T) *MemoryStore { - m := NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 4) - m.eventsByLedger.Append(ledgerbucketwindow.LedgerBucket[[]event]{ - LedgerSeq: 5, - LedgerCloseTimestamp: ledger5CloseTime, - BucketContent: ledger5Events, - }) - m.eventsByLedger.Append(ledgerbucketwindow.LedgerBucket[[]event]{ - LedgerSeq: 6, - LedgerCloseTimestamp: ledger6CloseTime, - BucketContent: nil, - }) - m.eventsByLedger.Append(ledgerbucketwindow.LedgerBucket[[]event]{ - LedgerSeq: 7, - LedgerCloseTimestamp: ledger7CloseTime, - BucketContent: ledger7Events, - }) - m.eventsByLedger.Append(ledgerbucketwindow.LedgerBucket[[]event]{ - LedgerSeq: 8, - LedgerCloseTimestamp: ledger8CloseTime, - BucketContent: ledger8Events, - }) - - return m -} - -func concat(slices ...[]event) []event { - var result []event - for _, slice := range slices { - result = append(result, slice...) - } - return result -} - -func TestScan(t *testing.T) { - m := createStore(t) - - genEquivalentInputs := func(input Range) []Range { - results := []Range{input} - if !input.ClampStart { - rangeCopy := input - rangeCopy.ClampStart = true - results = append(results, rangeCopy) - } - if !input.ClampEnd { - rangeCopy := input - rangeCopy.ClampEnd = true - results = append(results, rangeCopy) - } - if !input.ClampStart && !input.ClampEnd { - rangeCopy := input - rangeCopy.ClampStart = true - rangeCopy.ClampEnd = true - results = append(results, rangeCopy) - } - return results - } - - for _, testCase := range []struct { - input Range - expected []event - }{ - { - Range{ - Start: MinCursor, - ClampStart: true, - End: MaxCursor, - ClampEnd: true, - }, - concat(ledger5Events, ledger6Events, ledger7Events, ledger8Events), - }, - { - Range{ - Start: Cursor{Ledger: 5}, - ClampStart: false, - End: Cursor{Ledger: 9}, - ClampEnd: false, - }, - concat(ledger5Events, ledger6Events, ledger7Events, ledger8Events), - }, - { - Range{ - Start: Cursor{Ledger: 5, Tx: 2}, - ClampStart: false, - End: Cursor{Ledger: 9}, - ClampEnd: false, - }, - concat(ledger5Events[2:], ledger6Events, ledger7Events, ledger8Events), - }, - { - Range{ - Start: Cursor{Ledger: 5, Tx: 3}, - ClampStart: false, - End: MaxCursor, - ClampEnd: true, - }, - concat(ledger6Events, ledger7Events, ledger8Events), - }, - { - Range{ - Start: Cursor{Ledger: 6}, - ClampStart: false, - End: MaxCursor, - ClampEnd: true, - }, - concat(ledger7Events, ledger8Events), - }, - { - Range{ - Start: Cursor{Ledger: 6, Tx: 1}, - ClampStart: false, - End: MaxCursor, - ClampEnd: true, - }, - concat(ledger7Events, ledger8Events), - }, - { - Range{ - Start: Cursor{Ledger: 8, Tx: 2, Event: 3}, - ClampStart: false, - End: MaxCursor, - ClampEnd: true, - }, - ledger8Events[len(ledger8Events)-1:], - }, - { - Range{ - Start: Cursor{Ledger: 8, Tx: 2, Event: 3}, - ClampStart: false, - End: Cursor{Ledger: 9}, - ClampEnd: false, - }, - ledger8Events[len(ledger8Events)-1:], - }, - { - Range{ - Start: Cursor{Ledger: 5}, - ClampStart: false, - End: Cursor{Ledger: 7}, - ClampEnd: false, - }, - concat(ledger5Events, ledger6Events), - }, - { - Range{ - Start: Cursor{Ledger: 5, Tx: 2}, - ClampStart: false, - End: Cursor{Ledger: 8, Tx: 2}, - ClampEnd: false, - }, - concat(ledger5Events[2:], ledger6Events, ledger7Events, ledger8Events[:1]), - }, - } { - for _, input := range genEquivalentInputs(testCase.input) { - var events []event - iterateAll := true - f := func(contractEvent xdr.DiagnosticEvent, cursor Cursor, ledgerCloseTimestamp int64, hash *xdr.Hash) bool { - require.Equal(t, ledgerCloseTime(cursor.Ledger), ledgerCloseTimestamp) - diagnosticEventXDR, err := contractEvent.MarshalBinary() - require.NoError(t, err) - events = append(events, event{ - diagnosticEventXDR: diagnosticEventXDR, - txIndex: cursor.Tx, - eventIndex: cursor.Event, - txHash: hash, - }) - return iterateAll - } - latest, err := m.Scan(input, f) - require.NoError(t, err) - require.Equal(t, uint32(8), latest) - eventsAreEqual(t, testCase.expected, events) - if len(events) > 0 { - events = nil - iterateAll = false - latest, err := m.Scan(input, f) - require.NoError(t, err) - require.Equal(t, uint32(8), latest) - eventsAreEqual(t, []event{testCase.expected[0]}, events) - } - } - } -} diff --git a/cmd/soroban-rpc/internal/ingest/ledgerentry.go b/cmd/soroban-rpc/internal/ingest/ledgerentry.go deleted file mode 100644 index 0b8fc48f7..000000000 --- a/cmd/soroban-rpc/internal/ingest/ledgerentry.go +++ /dev/null @@ -1,87 +0,0 @@ -package ingest - -import ( - "context" - "io" - "strings" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/ingest" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" -) - -func (s *Service) ingestLedgerEntryChanges(ctx context.Context, reader ingest.ChangeReader, tx db.WriteTx, progressLogPeriod int) error { - entryCount := 0 - startTime := time.Now() - writer := tx.LedgerEntryWriter() - - changeStatsProcessor := ingest.StatsChangeProcessor{} - for ctx.Err() == nil { - if change, err := reader.Read(); err == io.EOF { - return nil - } else if err != nil { - return err - } else if err = ingestLedgerEntryChange(writer, change); err != nil { - return err - } else if err = changeStatsProcessor.ProcessChange(ctx, change); err != nil { - return err - } - entryCount++ - if progressLogPeriod > 0 && entryCount%progressLogPeriod == 0 { - s.logger.Infof("processed %d ledger entry changes", entryCount) - } - } - - results := changeStatsProcessor.GetResults() - for stat, value := range results.Map() { - stat = strings.Replace(stat, "stats_", "change_", 1) - s.ledgerStatsMetric. - With(prometheus.Labels{"type": stat}).Add(float64(value.(int64))) - } - s.ingestionDurationMetric. - With(prometheus.Labels{"type": "ledger_entries"}).Observe(time.Since(startTime).Seconds()) - return ctx.Err() -} - -func (s *Service) ingestTempLedgerEntryEvictions( - ctx context.Context, - evictedTempLedgerKeys []xdr.LedgerKey, - tx db.WriteTx, -) error { - startTime := time.Now() - writer := tx.LedgerEntryWriter() - counts := map[string]int{} - - for _, key := range evictedTempLedgerKeys { - if err := writer.DeleteLedgerEntry(key); err != nil { - return err - } - counts["evicted_"+key.Type.String()]++ - if ctx.Err() != nil { - return ctx.Err() - } - } - - for evictionType, count := range counts { - s.ledgerStatsMetric. - With(prometheus.Labels{"type": evictionType}).Add(float64(count)) - } - s.ingestionDurationMetric. - With(prometheus.Labels{"type": "evicted_temp_ledger_entries"}).Observe(time.Since(startTime).Seconds()) - return ctx.Err() -} - -func ingestLedgerEntryChange(writer db.LedgerEntryWriter, change ingest.Change) error { - if change.Post == nil { - ledgerKey, err := xdr.GetLedgerKeyFromData(change.Pre.Data) - if err != nil { - return err - } - return writer.DeleteLedgerEntry(ledgerKey) - } else { - return writer.UpsertLedgerEntry(*change.Post) - } -} diff --git a/cmd/soroban-rpc/internal/ingest/mock_db_test.go b/cmd/soroban-rpc/internal/ingest/mock_db_test.go deleted file mode 100644 index 221bdc70d..000000000 --- a/cmd/soroban-rpc/internal/ingest/mock_db_test.go +++ /dev/null @@ -1,78 +0,0 @@ -package ingest - -import ( - "context" - - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/mock" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" -) - -var ( - _ db.ReadWriter = (*MockDB)(nil) - _ db.WriteTx = (*MockTx)(nil) - _ db.LedgerEntryWriter = (*MockLedgerEntryWriter)(nil) - _ db.LedgerWriter = (*MockLedgerWriter)(nil) -) - -type MockDB struct { - mock.Mock -} - -func (m MockDB) NewTx(ctx context.Context) (db.WriteTx, error) { - args := m.Called(ctx) - return args.Get(0).(db.WriteTx), args.Error(1) -} - -func (m MockDB) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - args := m.Called(ctx) - return args.Get(0).(uint32), args.Error(1) -} - -type MockTx struct { - mock.Mock -} - -func (m MockTx) LedgerEntryWriter() db.LedgerEntryWriter { - args := m.Called() - return args.Get(0).(db.LedgerEntryWriter) -} - -func (m MockTx) LedgerWriter() db.LedgerWriter { - args := m.Called() - return args.Get(0).(db.LedgerWriter) -} - -func (m MockTx) Commit(ledgerSeq uint32) error { - args := m.Called(ledgerSeq) - return args.Error(0) -} - -func (m MockTx) Rollback() error { - args := m.Called() - return args.Error(0) -} - -type MockLedgerEntryWriter struct { - mock.Mock -} - -func (m MockLedgerEntryWriter) UpsertLedgerEntry(entry xdr.LedgerEntry) error { - args := m.Called(entry) - return args.Error(0) -} - -func (m MockLedgerEntryWriter) DeleteLedgerEntry(key xdr.LedgerKey) error { - args := m.Called(key) - return args.Error(0) -} - -type MockLedgerWriter struct { - mock.Mock -} - -func (m MockLedgerWriter) InsertLedger(ledger xdr.LedgerCloseMeta) error { - args := m.Called(ledger) - return args.Error(0) -} diff --git a/cmd/soroban-rpc/internal/ingest/service.go b/cmd/soroban-rpc/internal/ingest/service.go deleted file mode 100644 index 6240f7cb8..000000000 --- a/cmd/soroban-rpc/internal/ingest/service.go +++ /dev/null @@ -1,311 +0,0 @@ -package ingest - -import ( - "context" - "errors" - "fmt" - "sync" - "time" - - "github.com/cenkalti/backoff/v4" - "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/historyarchive" - "github.com/stellar/go/ingest" - backends "github.com/stellar/go/ingest/ledgerbackend" - supportdb "github.com/stellar/go/support/db" - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/util" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/events" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/transactions" -) - -const ( - ledgerEntryBaselineProgressLogPeriod = 10000 -) - -var errEmptyArchives = fmt.Errorf("cannot start ingestion without history archives, wait until first history archives are published") - -type Config struct { - Logger *log.Entry - DB db.ReadWriter - EventStore *events.MemoryStore - TransactionStore *transactions.MemoryStore - NetworkPassPhrase string - Archive historyarchive.ArchiveInterface - LedgerBackend backends.LedgerBackend - Timeout time.Duration - OnIngestionRetry backoff.Notify - Daemon interfaces.Daemon -} - -func NewService(cfg Config) *Service { - service := newService(cfg) - startService(service, cfg) - return service -} - -func newService(cfg Config) *Service { - // ingestionDurationMetric is a metric for measuring the latency of ingestion - ingestionDurationMetric := prometheus.NewSummaryVec(prometheus.SummaryOpts{ - Namespace: cfg.Daemon.MetricsNamespace(), Subsystem: "ingest", Name: "ledger_ingestion_duration_seconds", - Help: "ledger ingestion durations, sliding window = 10m", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, - []string{"type"}, - ) - // latestLedgerMetric is a metric for measuring the latest ingested ledger - latestLedgerMetric := prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: cfg.Daemon.MetricsNamespace(), Subsystem: "ingest", Name: "local_latest_ledger", - Help: "sequence number of the latest ledger ingested by this ingesting instance", - }) - - // ledgerStatsMetric is a metric which measures statistics on all ledger entries ingested by soroban rpc - ledgerStatsMetric := prometheus.NewCounterVec( - prometheus.CounterOpts{ - Namespace: cfg.Daemon.MetricsNamespace(), Subsystem: "ingest", Name: "ledger_stats_total", - Help: "counters of different ledger stats", - }, - []string{"type"}, - ) - - cfg.Daemon.MetricsRegistry().MustRegister(ingestionDurationMetric, latestLedgerMetric, ledgerStatsMetric) - - service := &Service{ - logger: cfg.Logger, - db: cfg.DB, - eventStore: cfg.EventStore, - transactionStore: cfg.TransactionStore, - ledgerBackend: cfg.LedgerBackend, - networkPassPhrase: cfg.NetworkPassPhrase, - timeout: cfg.Timeout, - ingestionDurationMetric: ingestionDurationMetric, - latestLedgerMetric: latestLedgerMetric, - ledgerStatsMetric: ledgerStatsMetric, - } - - return service -} - -func startService(service *Service, cfg Config) { - ctx, done := context.WithCancel(context.Background()) - service.done = done - service.wg.Add(1) - panicGroup := util.UnrecoverablePanicGroup.Log(cfg.Logger) - panicGroup.Go(func() { - defer service.wg.Done() - // Retry running ingestion every second for 5 seconds. - constantBackoff := backoff.WithMaxRetries(backoff.NewConstantBackOff(1*time.Second), 5) - // Don't want to keep retrying if the context gets canceled. - contextBackoff := backoff.WithContext(constantBackoff, ctx) - err := backoff.RetryNotify( - func() error { - err := service.run(ctx, cfg.Archive) - if errors.Is(err, errEmptyArchives) { - // keep retrying until history archives are published - constantBackoff.Reset() - } - return err - }, - contextBackoff, - cfg.OnIngestionRetry) - if err != nil && !errors.Is(err, context.Canceled) { - service.logger.WithError(err).Fatal("could not run ingestion") - } - }) -} - -type Service struct { - logger *log.Entry - db db.ReadWriter - eventStore *events.MemoryStore - transactionStore *transactions.MemoryStore - ledgerBackend backends.LedgerBackend - timeout time.Duration - networkPassPhrase string - done context.CancelFunc - wg sync.WaitGroup - ingestionDurationMetric *prometheus.SummaryVec - latestLedgerMetric prometheus.Gauge - ledgerStatsMetric *prometheus.CounterVec -} - -func (s *Service) Close() error { - s.done() - s.wg.Wait() - return nil -} - -func (s *Service) run(ctx context.Context, archive historyarchive.ArchiveInterface) error { - // Create a ledger-entry baseline from a checkpoint if it wasn't done before - // (after that we will be adding deltas from txmeta ledger entry changes) - nextLedgerSeq, checkPointFillErr, err := s.maybeFillEntriesFromCheckpoint(ctx, archive) - if err != nil { - return err - } - - prepareRangeCtx, cancelPrepareRange := context.WithTimeout(ctx, s.timeout) - if err := s.ledgerBackend.PrepareRange(prepareRangeCtx, backends.UnboundedRange(nextLedgerSeq)); err != nil { - cancelPrepareRange() - return err - } - cancelPrepareRange() - - // Make sure that the checkpoint prefill (if any), happened before starting to apply deltas - if err := <-checkPointFillErr; err != nil { - return err - } - - for ; ; nextLedgerSeq++ { - if err := s.ingest(ctx, nextLedgerSeq); err != nil { - return err - } - } -} - -func (s *Service) maybeFillEntriesFromCheckpoint(ctx context.Context, archive historyarchive.ArchiveInterface) (uint32, chan error, error) { - checkPointFillErr := make(chan error, 1) - // Skip creating a ledger-entry baseline if the DB was initialized - curLedgerSeq, err := s.db.GetLatestLedgerSequence(ctx) - if err == db.ErrEmptyDB { - var checkpointLedger uint32 - if root, rootErr := archive.GetRootHAS(); rootErr != nil { - return 0, checkPointFillErr, rootErr - } else if root.CurrentLedger == 0 { - return 0, checkPointFillErr, errEmptyArchives - } else { - checkpointLedger = root.CurrentLedger - } - - // DB is empty, let's fill it from the History Archive, using the latest available checkpoint - // Do it in parallel with the upcoming captive core preparation to save time - s.logger.Infof("found an empty database, creating ledger-entry baseline from the most recent checkpoint (%d). This can take up to 30 minutes, depending on the network", checkpointLedger) - panicGroup := util.UnrecoverablePanicGroup.Log(s.logger) - panicGroup.Go(func() { - checkPointFillErr <- s.fillEntriesFromCheckpoint(ctx, archive, checkpointLedger) - }) - return checkpointLedger + 1, checkPointFillErr, nil - } else if err != nil { - return 0, checkPointFillErr, err - } else { - checkPointFillErr <- nil - return curLedgerSeq + 1, checkPointFillErr, nil - } -} - -func (s *Service) fillEntriesFromCheckpoint(ctx context.Context, archive historyarchive.ArchiveInterface, checkpointLedger uint32) error { - checkpointCtx, cancelCheckpointCtx := context.WithTimeout(ctx, s.timeout) - defer cancelCheckpointCtx() - - reader, err := ingest.NewCheckpointChangeReader(checkpointCtx, archive, checkpointLedger) - if err != nil { - return err - } - - tx, err := s.db.NewTx(ctx) - if err != nil { - return err - } - transactionCommitted := false - defer func() { - if !transactionCommitted { - // Internally, we might already have rolled back the transaction. We should - // not generate benign error/warning here in case the transaction was already rolled back. - if rollbackErr := tx.Rollback(); rollbackErr != nil && rollbackErr != supportdb.ErrAlreadyRolledback { - s.logger.WithError(rollbackErr).Warn("could not rollback fillEntriesFromCheckpoint write transactions") - } - } - }() - - if err := s.ingestLedgerEntryChanges(ctx, reader, tx, ledgerEntryBaselineProgressLogPeriod); err != nil { - return err - } - if err := reader.Close(); err != nil { - return err - } - - s.logger.Info("committing checkpoint ledger entries") - err = tx.Commit(checkpointLedger) - transactionCommitted = true - if err != nil { - return err - } - - s.logger.Info("finished checkpoint processing") - return nil -} - -func (s *Service) ingest(ctx context.Context, sequence uint32) error { - startTime := time.Now() - s.logger.Infof("Ingesting ledger %d", sequence) - ledgerCloseMeta, err := s.ledgerBackend.GetLedger(ctx, sequence) - if err != nil { - return err - } - reader, err := ingest.NewLedgerChangeReaderFromLedgerCloseMeta(s.networkPassPhrase, ledgerCloseMeta) - if err != nil { - return err - } - tx, err := s.db.NewTx(ctx) - if err != nil { - return err - } - defer func() { - if err := tx.Rollback(); err != nil { - s.logger.WithError(err).Warn("could not rollback ingest write transactions") - } - }() - - if err := s.ingestLedgerEntryChanges(ctx, reader, tx, 0); err != nil { - return err - } - if err := reader.Close(); err != nil { - return err - } - - // EvictedTemporaryLedgerKeys will include both temporary ledger keys which - // have been evicted and their corresponding ttl ledger entries - evictedTempLedgerKeys, err := ledgerCloseMeta.EvictedTemporaryLedgerKeys() - if err != nil { - return err - } - if err := s.ingestTempLedgerEntryEvictions(ctx, evictedTempLedgerKeys, tx); err != nil { - return err - } - - if err := s.ingestLedgerCloseMeta(tx, ledgerCloseMeta); err != nil { - return err - } - - if err := tx.Commit(sequence); err != nil { - return err - } - s.logger.Debugf("Ingested ledger %d", sequence) - - s.ingestionDurationMetric. - With(prometheus.Labels{"type": "total"}).Observe(time.Since(startTime).Seconds()) - s.latestLedgerMetric.Set(float64(sequence)) - return nil -} - -func (s *Service) ingestLedgerCloseMeta(tx db.WriteTx, ledgerCloseMeta xdr.LedgerCloseMeta) error { - startTime := time.Now() - if err := tx.LedgerWriter().InsertLedger(ledgerCloseMeta); err != nil { - return err - } - s.ingestionDurationMetric. - With(prometheus.Labels{"type": "ledger_close_meta"}).Observe(time.Since(startTime).Seconds()) - - if err := s.eventStore.IngestEvents(ledgerCloseMeta); err != nil { - return err - } - - if err := s.transactionStore.IngestTransactions(ledgerCloseMeta); err != nil { - return err - } - return nil -} diff --git a/cmd/soroban-rpc/internal/ingest/service_test.go b/cmd/soroban-rpc/internal/ingest/service_test.go deleted file mode 100644 index c2e4def09..000000000 --- a/cmd/soroban-rpc/internal/ingest/service_test.go +++ /dev/null @@ -1,263 +0,0 @@ -package ingest - -import ( - "context" - "encoding/hex" - "errors" - "sync" - "testing" - "time" - - "github.com/stellar/go/ingest/ledgerbackend" - "github.com/stellar/go/network" - supportlog "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/assert" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/events" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/transactions" -) - -type ErrorReadWriter struct { -} - -func (rw *ErrorReadWriter) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - return 0, errors.New("could not get latest ledger sequence") -} -func (rw *ErrorReadWriter) NewTx(ctx context.Context) (db.WriteTx, error) { - return nil, errors.New("could not create new tx") -} - -func TestRetryRunningIngestion(t *testing.T) { - - var retryWg sync.WaitGroup - retryWg.Add(1) - - numRetries := 0 - var lastErr error - incrementRetry := func(err error, dur time.Duration) { - defer retryWg.Done() - numRetries++ - lastErr = err - } - config := Config{ - Logger: supportlog.New(), - DB: &ErrorReadWriter{}, - EventStore: nil, - TransactionStore: nil, - NetworkPassPhrase: "", - Archive: nil, - LedgerBackend: nil, - Timeout: time.Second, - OnIngestionRetry: incrementRetry, - Daemon: interfaces.MakeNoOpDeamon(), - } - service := NewService(config) - retryWg.Wait() - service.Close() - assert.Equal(t, 1, numRetries) - assert.Error(t, lastErr) - assert.ErrorContains(t, lastErr, "could not get latest ledger sequence") -} - -func TestIngestion(t *testing.T) { - mockDB := &MockDB{} - mockLedgerBackend := &ledgerbackend.MockDatabaseBackend{} - daemon := interfaces.MakeNoOpDeamon() - config := Config{ - Logger: supportlog.New(), - DB: mockDB, - EventStore: events.NewMemoryStore(daemon, network.TestNetworkPassphrase, 1), - TransactionStore: transactions.NewMemoryStore(daemon, network.TestNetworkPassphrase, 1), - LedgerBackend: mockLedgerBackend, - Daemon: daemon, - NetworkPassPhrase: network.TestNetworkPassphrase, - } - sequence := uint32(3) - service := newService(config) - mockTx := &MockTx{} - mockLedgerEntryWriter := &MockLedgerEntryWriter{} - mockLedgerWriter := &MockLedgerWriter{} - ctx := context.Background() - mockDB.On("NewTx", ctx).Return(mockTx, nil).Once() - mockTx.On("Commit", sequence).Return(nil).Once() - mockTx.On("Rollback").Return(nil).Once() - mockTx.On("LedgerEntryWriter").Return(mockLedgerEntryWriter).Twice() - mockTx.On("LedgerWriter").Return(mockLedgerWriter).Once() - - src := xdr.MustAddress("GBXGQJWVLWOYHFLVTKWV5FGHA3LNYY2JQKM7OAJAUEQFU6LPCSEFVXON") - firstTx := xdr.TransactionEnvelope{ - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: &xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - Fee: 1, - SourceAccount: src.ToMuxedAccount(), - }, - }, - } - firstTxHash, err := network.HashTransactionInEnvelope(firstTx, network.TestNetworkPassphrase) - assert.NoError(t, err) - - baseFee := xdr.Int64(100) - tempKey := xdr.ScSymbol("TEMPKEY") - persistentKey := xdr.ScSymbol("TEMPVAL") - contractIDBytes, err := hex.DecodeString("df06d62447fd25da07c0135eed7557e5a5497ee7d15b7fe345bd47e191d8f577") - assert.NoError(t, err) - var contractID xdr.Hash - copy(contractID[:], contractIDBytes) - contractAddress := xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractID, - } - xdrTrue := true - operationChanges := xdr.LedgerEntryChanges{ - { - Type: xdr.LedgerEntryChangeTypeLedgerEntryState, - State: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 1, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.ContractDataEntry{ - Contract: contractAddress, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &persistentKey, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvBool, - B: &xdrTrue, - }, - }, - }, - }, - }, - { - Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, - Updated: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: 1, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractID, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &persistentKey, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvBool, - B: &xdrTrue, - }, - }, - }, - }, - }, - } - evictedPersistentLedgerEntry := xdr.LedgerEntry{ - LastModifiedLedgerSeq: 123, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.ContractDataEntry{ - Contract: contractAddress, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &persistentKey, - }, - Durability: xdr.ContractDataDurabilityTemporary, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvBool, - B: &xdrTrue, - }, - }, - }, - } - evictedTempLedgerKey := xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.LedgerKeyContractData{ - Contract: contractAddress, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &tempKey, - }, - Durability: xdr.ContractDataDurabilityTemporary, - }, - } - ledger := xdr.LedgerCloseMeta{ - V: 1, - V1: &xdr.LedgerCloseMetaV1{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{Header: xdr.LedgerHeader{LedgerVersion: 10}}, - TxSet: xdr.GeneralizedTransactionSet{ - V: 1, - V1TxSet: &xdr.TransactionSetV1{ - PreviousLedgerHash: xdr.Hash{1, 2, 3}, - Phases: []xdr.TransactionPhase{ - { - V0Components: &[]xdr.TxSetComponent{ - { - Type: xdr.TxSetComponentTypeTxsetCompTxsMaybeDiscountedFee, - TxsMaybeDiscountedFee: &xdr.TxSetComponentTxsMaybeDiscountedFee{ - BaseFee: &baseFee, - Txs: []xdr.TransactionEnvelope{ - firstTx, - }, - }, - }, - }, - }, - }, - }, - }, - TxProcessing: []xdr.TransactionResultMeta{ - { - Result: xdr.TransactionResultPair{ - TransactionHash: firstTxHash, - Result: xdr.TransactionResult{ - Result: xdr.TransactionResultResult{ - Results: &[]xdr.OperationResult{}, - }, - }, - }, - FeeProcessing: xdr.LedgerEntryChanges{}, - TxApplyProcessing: xdr.TransactionMeta{ - V: 3, - V3: &xdr.TransactionMetaV3{ - Operations: []xdr.OperationMeta{ - { - Changes: operationChanges, - }, - }, - }, - }, - }, - }, - UpgradesProcessing: []xdr.UpgradeEntryMeta{}, - EvictedTemporaryLedgerKeys: []xdr.LedgerKey{evictedTempLedgerKey}, - EvictedPersistentLedgerEntries: []xdr.LedgerEntry{evictedPersistentLedgerEntry}, - }, - } - mockLedgerBackend.On("GetLedger", ctx, sequence). - Return(ledger, nil).Once() - mockLedgerEntryWriter.On("UpsertLedgerEntry", operationChanges[1].MustUpdated()). - Return(nil).Once() - evictedPresistentLedgerKey, err := evictedPersistentLedgerEntry.LedgerKey() - assert.NoError(t, err) - mockLedgerEntryWriter.On("DeleteLedgerEntry", evictedPresistentLedgerKey). - Return(nil).Once() - mockLedgerEntryWriter.On("DeleteLedgerEntry", evictedTempLedgerKey). - Return(nil).Once() - mockLedgerWriter.On("InsertLedger", ledger). - Return(nil).Once() - assert.NoError(t, service.ingest(ctx, sequence)) - - mockDB.AssertExpectations(t) - mockTx.AssertExpectations(t) - mockLedgerEntryWriter.AssertExpectations(t) - mockLedgerWriter.AssertExpectations(t) - mockLedgerBackend.AssertExpectations(t) -} diff --git a/cmd/soroban-rpc/internal/jsonrpc.go b/cmd/soroban-rpc/internal/jsonrpc.go deleted file mode 100644 index 4bc4f17c2..000000000 --- a/cmd/soroban-rpc/internal/jsonrpc.go +++ /dev/null @@ -1,298 +0,0 @@ -package internal - -import ( - "context" - "encoding/json" - "net/http" - "strconv" - "strings" - "time" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - "github.com/creachadair/jrpc2/jhttp" - "github.com/go-chi/chi/middleware" - "github.com/prometheus/client_golang/prometheus" - "github.com/rs/cors" - "github.com/stellar/go/support/log" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/events" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/methods" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/network" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/transactions" -) - -// maxHTTPRequestSize defines the largest request size that the http handler -// would be willing to accept before dropping the request. The implementation -// uses the default MaxBytesHandler to limit the request size. -const maxHTTPRequestSize = 512 * 1024 // half a megabyte - -// Handler is the HTTP handler which serves the Soroban JSON RPC responses -type Handler struct { - bridge jhttp.Bridge - logger *log.Entry - http.Handler -} - -// Close closes all the resources held by the Handler instances. -// After Close is called the Handler instance will stop accepting JSON RPC requests. -func (h Handler) Close() { - if err := h.bridge.Close(); err != nil { - h.logger.WithError(err).Warn("could not close bridge") - } -} - -type HandlerParams struct { - EventStore *events.MemoryStore - TransactionStore *transactions.MemoryStore - LedgerEntryReader db.LedgerEntryReader - LedgerReader db.LedgerReader - Logger *log.Entry - PreflightGetter methods.PreflightGetter - Daemon interfaces.Daemon -} - -func decorateHandlers(daemon interfaces.Daemon, logger *log.Entry, m handler.Map) handler.Map { - requestMetric := prometheus.NewSummaryVec(prometheus.SummaryOpts{ - Namespace: daemon.MetricsNamespace(), - Subsystem: "json_rpc", - Name: "request_duration_seconds", - Help: "JSON RPC request duration", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, []string{"endpoint", "status"}) - decorated := handler.Map{} - for endpoint, h := range m { - // create copy of h so it can be used in closure bleow - h := h - decorated[endpoint] = handler.New(func(ctx context.Context, r *jrpc2.Request) (interface{}, error) { - reqID := strconv.FormatUint(middleware.NextRequestID(), 10) - logRequest(logger, reqID, r) - startTime := time.Now() - result, err := h(ctx, r) - duration := time.Since(startTime) - label := prometheus.Labels{"endpoint": r.Method(), "status": "ok"} - simulateTransactionResponse, ok := result.(methods.SimulateTransactionResponse) - if ok && simulateTransactionResponse.Error != "" { - label["status"] = "error" - } else if err != nil { - if jsonRPCErr, ok := err.(*jrpc2.Error); ok { - prometheusLabelReplacer := strings.NewReplacer(" ", "_", "-", "_", "(", "", ")", "") - status := prometheusLabelReplacer.Replace(jsonRPCErr.Code.String()) - label["status"] = status - } - } - requestMetric.With(label).Observe(duration.Seconds()) - logResponse(logger, reqID, duration, label["status"], result) - return result, err - }) - } - daemon.MetricsRegistry().MustRegister(requestMetric) - return decorated -} - -func logRequest(logger *log.Entry, reqID string, req *jrpc2.Request) { - logger = logger.WithFields(log.F{ - "subsys": "jsonrpc", - "req": reqID, - "json_req": req.ID(), - "method": req.Method(), - }) - logger.Info("starting JSONRPC request") - - // Params are useful but can be really verbose, let's only print them in debug level - logger = logger.WithField("params", req.ParamString()) - logger.Debug("starting JSONRPC request params") -} - -func logResponse(logger *log.Entry, reqID string, duration time.Duration, status string, response any) { - logger = logger.WithFields(log.F{ - "subsys": "jsonrpc", - "req": reqID, - "duration": duration.String(), - "json_req": reqID, - "status": status, - }) - logger.Info("finished JSONRPC request") - - if status == "ok" { - responseBytes, err := json.Marshal(response) - if err == nil { - // the result is useful but can be really verbose, let's only print it with debug level - logger = logger.WithField("result", string(responseBytes)) - logger.Debug("finished JSONRPC request result") - } - } -} - -// NewJSONRPCHandler constructs a Handler instance -func NewJSONRPCHandler(cfg *config.Config, params HandlerParams) Handler { - bridgeOptions := jhttp.BridgeOptions{ - Server: &jrpc2.ServerOptions{ - Logger: func(text string) { params.Logger.Debug(text) }, - }, - } - handlers := []struct { - methodName string - underlyingHandler jrpc2.Handler - queueLimit uint - longName string - requestDurationLimit time.Duration - }{ - { - methodName: "getHealth", - underlyingHandler: methods.NewHealthCheck(params.TransactionStore, cfg.MaxHealthyLedgerLatency), - longName: "get_health", - queueLimit: cfg.RequestBacklogGetHealthQueueLimit, - requestDurationLimit: cfg.MaxGetHealthExecutionDuration, - }, - { - methodName: "getEvents", - underlyingHandler: methods.NewGetEventsHandler(params.EventStore, cfg.MaxEventsLimit, cfg.DefaultEventsLimit), - longName: "get_events", - queueLimit: cfg.RequestBacklogGetEventsQueueLimit, - requestDurationLimit: cfg.MaxGetEventsExecutionDuration, - }, - { - methodName: "getNetwork", - underlyingHandler: methods.NewGetNetworkHandler(params.Daemon, cfg.NetworkPassphrase, cfg.FriendbotURL), - longName: "get_network", - queueLimit: cfg.RequestBacklogGetNetworkQueueLimit, - requestDurationLimit: cfg.MaxGetNetworkExecutionDuration, - }, - { - methodName: "getLatestLedger", - underlyingHandler: methods.NewGetLatestLedgerHandler(params.LedgerEntryReader, params.LedgerReader), - longName: "get_latest_ledger", - queueLimit: cfg.RequestBacklogGetLatestLedgerQueueLimit, - requestDurationLimit: cfg.MaxGetLatestLedgerExecutionDuration, - }, - { - methodName: "getLedgerEntry", - underlyingHandler: methods.NewGetLedgerEntryHandler(params.Logger, params.LedgerEntryReader), - longName: "get_ledger_entry", - queueLimit: cfg.RequestBacklogGetLedgerEntriesQueueLimit, // share with getLedgerEntries - requestDurationLimit: cfg.MaxGetLedgerEntriesExecutionDuration, - }, - { - methodName: "getLedgerEntries", - underlyingHandler: methods.NewGetLedgerEntriesHandler(params.Logger, params.LedgerEntryReader), - longName: "get_ledger_entries", - queueLimit: cfg.RequestBacklogGetLedgerEntriesQueueLimit, - requestDurationLimit: cfg.MaxGetLedgerEntriesExecutionDuration, - }, - { - methodName: "getTransaction", - underlyingHandler: methods.NewGetTransactionHandler(params.TransactionStore), - longName: "get_transaction", - queueLimit: cfg.RequestBacklogGetTransactionQueueLimit, - requestDurationLimit: cfg.MaxGetTransactionExecutionDuration, - }, - { - methodName: "sendTransaction", - underlyingHandler: methods.NewSendTransactionHandler(params.Daemon, params.Logger, params.TransactionStore, cfg.NetworkPassphrase), - longName: "send_transaction", - queueLimit: cfg.RequestBacklogSendTransactionQueueLimit, - requestDurationLimit: cfg.MaxSendTransactionExecutionDuration, - }, - { - methodName: "simulateTransaction", - underlyingHandler: methods.NewSimulateTransactionHandler(params.Logger, params.LedgerEntryReader, params.LedgerReader, params.PreflightGetter), - longName: "simulate_transaction", - queueLimit: cfg.RequestBacklogSimulateTransactionQueueLimit, - requestDurationLimit: cfg.MaxSimulateTransactionExecutionDuration, - }, - } - handlersMap := handler.Map{} - for _, handler := range handlers { - queueLimiterGaugeName := handler.longName + "_inflight_requests" - queueLimiterGaugeHelp := "Number of concurrenty in-flight " + handler.methodName + " requests" - - queueLimiterGauge := prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: params.Daemon.MetricsNamespace(), Subsystem: "network", - Name: queueLimiterGaugeName, - Help: queueLimiterGaugeHelp, - }) - queueLimiter := network.MakeJrpcBacklogQueueLimiter( - handler.underlyingHandler, - queueLimiterGauge, - uint64(handler.queueLimit), - params.Logger) - - durationWarnCounterName := handler.longName + "_execution_threshold_warning" - durationLimitCounterName := handler.longName + "_execution_threshold_limit" - durationWarnCounterHelp := "The metric measures the count of " + handler.methodName + " requests that surpassed the warning threshold for execution time" - durationLimitCounterHelp := "The metric measures the count of " + handler.methodName + " requests that surpassed the limit threshold for execution time" - - requestDurationWarnCounter := prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: params.Daemon.MetricsNamespace(), Subsystem: "network", - Name: durationWarnCounterName, - Help: durationWarnCounterHelp, - }) - requestDurationLimitCounter := prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: params.Daemon.MetricsNamespace(), Subsystem: "network", - Name: durationLimitCounterName, - Help: durationLimitCounterHelp, - }) - // set the warning threshold to be one third of the limit. - requestDurationWarn := handler.requestDurationLimit / 3 - durationLimiter := network.MakeJrpcRequestDurationLimiter( - queueLimiter.Handle, - requestDurationWarn, - handler.requestDurationLimit, - requestDurationWarnCounter, - requestDurationLimitCounter, - params.Logger) - handlersMap[handler.methodName] = durationLimiter.Handle - } - bridge := jhttp.NewBridge(decorateHandlers( - params.Daemon, - params.Logger, - handlersMap), - &bridgeOptions) - - // globalQueueRequestBacklogLimiter is a metric for measuring the total concurrent inflight requests - globalQueueRequestBacklogLimiter := prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: params.Daemon.MetricsNamespace(), Subsystem: "network", Name: "global_inflight_requests", - Help: "Number of concurrenty in-flight http requests", - }) - - queueLimitedBridge := network.MakeHTTPBacklogQueueLimiter( - bridge, - globalQueueRequestBacklogLimiter, - uint64(cfg.RequestBacklogGlobalQueueLimit), - params.Logger) - - globalQueueRequestExecutionDurationWarningCounter := prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: params.Daemon.MetricsNamespace(), Subsystem: "network", Name: "global_request_execution_duration_threshold_warning", - Help: "The metric measures the count of requests that surpassed the warning threshold for execution time", - }) - globalQueueRequestExecutionDurationLimitCounter := prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: params.Daemon.MetricsNamespace(), Subsystem: "network", Name: "global_request_execution_duration_threshold_limit", - Help: "The metric measures the count of requests that surpassed the limit threshold for execution time", - }) - var handler http.Handler = network.MakeHTTPRequestDurationLimiter( - queueLimitedBridge, - cfg.RequestExecutionWarningThreshold, - cfg.MaxRequestExecutionDuration, - globalQueueRequestExecutionDurationWarningCounter, - globalQueueRequestExecutionDurationLimitCounter, - params.Logger) - - handler = http.MaxBytesHandler(handler, maxHTTPRequestSize) - - corsMiddleware := cors.New(cors.Options{ - AllowedOrigins: []string{}, - AllowOriginRequestFunc: func(*http.Request, string) bool { return true }, - AllowedHeaders: []string{"*"}, - AllowedMethods: []string{"GET", "PUT", "POST", "PATCH", "DELETE", "HEAD", "OPTIONS"}, - }) - - return Handler{ - bridge: bridge, - logger: params.Logger, - Handler: corsMiddleware.Handler(handler), - } -} diff --git a/cmd/soroban-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go b/cmd/soroban-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go deleted file mode 100644 index 8234b6076..000000000 --- a/cmd/soroban-rpc/internal/ledgerbucketwindow/ledgerbucketwindow.go +++ /dev/null @@ -1,76 +0,0 @@ -package ledgerbucketwindow - -import ( - "fmt" -) - -// LedgerBucketWindow is a sequence of buckets associated to a ledger window. -type LedgerBucketWindow[T any] struct { - // buckets is a circular buffer where each cell represents - // the content stored for a specific ledger. - buckets []LedgerBucket[T] - // start is the index of the head in the circular buffer. - start uint32 -} - -// LedgerBucket holds the content associated to a ledger -type LedgerBucket[T any] struct { - LedgerSeq uint32 - LedgerCloseTimestamp int64 - BucketContent T -} - -// DefaultEventLedgerRetentionWindow represents the max number of ledgers we would like to keep -// an incoming event in memory. The value was calculated to align with (roughly) 24 hours window. -const DefaultEventLedgerRetentionWindow = 17280 - -// NewLedgerBucketWindow creates a new LedgerBucketWindow -func NewLedgerBucketWindow[T any](retentionWindow uint32) *LedgerBucketWindow[T] { - if retentionWindow == 0 { - retentionWindow = DefaultEventLedgerRetentionWindow - } - return &LedgerBucketWindow[T]{ - buckets: make([]LedgerBucket[T], 0, retentionWindow), - } -} - -// Append adds a new bucket to the window. If the window is full a bucket will be evicted and returned. -func (w *LedgerBucketWindow[T]) Append(bucket LedgerBucket[T]) (*LedgerBucket[T], error) { - length := w.Len() - if length > 0 { - expectedLedgerSequence := w.buckets[w.start].LedgerSeq + length - if expectedLedgerSequence != bucket.LedgerSeq { - return &LedgerBucket[T]{}, fmt.Errorf("error appending ledgers: ledgers not contiguous: expected ledger sequence %v but received %v", expectedLedgerSequence, bucket.LedgerSeq) - } - } - - var evicted *LedgerBucket[T] - if length < uint32(cap(w.buckets)) { - // The buffer isn't full, just place the bucket at the end - w.buckets = append(w.buckets, bucket) - } else { - // overwrite the first bucket and shift the circular buffer so that it - // becomes the last bucket - saved := w.buckets[w.start] - evicted = &saved - w.buckets[w.start] = bucket - w.start = (w.start + 1) % length - } - - return evicted, nil -} - -// Len returns the length (number of buckets in the window) -func (w *LedgerBucketWindow[T]) Len() uint32 { - return uint32(len(w.buckets)) -} - -// Get obtains a bucket from the window -func (w *LedgerBucketWindow[T]) Get(i uint32) *LedgerBucket[T] { - length := w.Len() - if i >= length { - panic(fmt.Errorf("index out of range [%d] with length %d", i, length)) - } - index := (w.start + i) % length - return &w.buckets[index] -} diff --git a/cmd/soroban-rpc/internal/ledgerbucketwindow/ledgerbucketwindow_test.go b/cmd/soroban-rpc/internal/ledgerbucketwindow/ledgerbucketwindow_test.go deleted file mode 100644 index 2e50ed6d5..000000000 --- a/cmd/soroban-rpc/internal/ledgerbucketwindow/ledgerbucketwindow_test.go +++ /dev/null @@ -1,104 +0,0 @@ -package ledgerbucketwindow - -import ( - "testing" - - "github.com/stretchr/testify/require" -) - -func bucket(ledgerSeq uint32) LedgerBucket[uint32] { - return LedgerBucket[uint32]{ - LedgerSeq: ledgerSeq, - LedgerCloseTimestamp: int64(ledgerSeq)*25 + 100, - BucketContent: ledgerSeq, - } -} - -func TestAppend(t *testing.T) { - m := NewLedgerBucketWindow[uint32](3) - require.Equal(t, uint32(0), m.Len()) - - // Test appending first bucket of events - evicted, err := m.Append(bucket(5)) - require.NoError(t, err) - require.Nil(t, evicted) - require.Equal(t, uint32(1), m.Len()) - require.Equal(t, bucket(5), *m.Get(0)) - - // The next bucket must follow the previous bucket (ledger 5) - _, err = m.Append(LedgerBucket[uint32]{ - LedgerSeq: 10, - LedgerCloseTimestamp: 100, - BucketContent: 10, - }) - require.Errorf(t, err, "ledgers not contiguous: expected ledger sequence 6 but received 10") - - _, err = m.Append(LedgerBucket[uint32]{ - LedgerSeq: 4, - LedgerCloseTimestamp: 100, - BucketContent: 4, - }) - require.Errorf(t, err, "ledgers not contiguous: expected ledger sequence 6 but received 4") - - // check that none of the calls above modified our buckets - require.Equal(t, uint32(1), m.Len()) - require.Equal(t, bucket(5), *m.Get(0)) - - // Append ledger 6 bucket, now we have two buckets filled - evicted, err = m.Append(bucket(6)) - require.NoError(t, err) - require.Nil(t, evicted) - require.Equal(t, uint32(2), m.Len()) - require.Equal(t, bucket(5), *m.Get(0)) - require.Equal(t, bucket(6), *m.Get(1)) - - // Append ledger 7, now we have all three buckets filled - evicted, err = m.Append(bucket(7)) - require.NoError(t, err) - require.Nil(t, evicted) - require.Equal(t, uint32(3), m.Len()) - require.Equal(t, bucket(5), *m.Get(0)) - require.Equal(t, bucket(6), *m.Get(1)) - require.Equal(t, bucket(7), *m.Get(2)) - - // Append ledger 8, but all buckets are full, so we need to evict ledger 5 - evicted, err = m.Append(bucket(8)) - require.NoError(t, err) - require.Equal(t, bucket(5), *evicted) - require.Equal(t, uint32(3), m.Len()) - require.Equal(t, bucket(6), *m.Get(0)) - require.Equal(t, bucket(7), *m.Get(1)) - require.Equal(t, bucket(8), *m.Get(2)) - - // Append ledger 9 events, but all buckets are full, so we need to evict ledger 6 - evicted, err = m.Append(bucket(9)) - require.NoError(t, err) - require.Equal(t, bucket(6), *evicted) - require.Equal(t, uint32(3), m.Len()) - require.Equal(t, bucket(7), *m.Get(0)) - require.Equal(t, bucket(8), *m.Get(1)) - require.Equal(t, bucket(9), *m.Get(2)) - - // Append ledger 10, but all buckets are full, so we need to evict ledger 7. - // The start index must have wrapped around - evicted, err = m.Append(bucket(10)) - require.NoError(t, err) - require.Equal(t, bucket(7), *evicted) - require.Equal(t, uint32(3), m.Len()) - require.Equal(t, bucket(8), *m.Get(0)) - require.Equal(t, bucket(9), *m.Get(1)) - require.Equal(t, bucket(10), *m.Get(2)) -} - -func TestAppendError(t *testing.T) { - m := NewLedgerBucketWindow[uint32](3) - require.Equal(t, uint32(0), m.Len()) - - evicted, err := m.Append(bucket(5)) - require.NoError(t, err) - require.Nil(t, evicted) - - evicted, err = m.Append(bucket(1)) - require.Error(t, err) - require.Contains(t, err.Error(), "error appending ledgers: ledgers not contiguous: expected ledger sequence 6 but received 1") -} diff --git a/cmd/soroban-rpc/internal/methods/get_events.go b/cmd/soroban-rpc/internal/methods/get_events.go deleted file mode 100644 index 71cad1e9d..000000000 --- a/cmd/soroban-rpc/internal/methods/get_events.go +++ /dev/null @@ -1,435 +0,0 @@ -package methods - -import ( - "context" - "encoding/json" - "fmt" - "strings" - "time" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - - "github.com/stellar/go/strkey" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/events" -) - -type eventTypeSet map[string]interface{} - -func (e eventTypeSet) valid() error { - for key := range e { - switch key { - case EventTypeSystem, EventTypeContract, EventTypeDiagnostic: - // ok - default: - return errors.New("if set, type must be either 'system', 'contract' or 'diagnostic'") - } - } - return nil -} - -func (e *eventTypeSet) UnmarshalJSON(data []byte) error { - if len(data) == 0 { - *e = map[string]interface{}{} - return nil - } - var joined string - if err := json.Unmarshal(data, &joined); err != nil { - return err - } - *e = map[string]interface{}{} - if len(joined) == 0 { - return nil - } - for _, key := range strings.Split(joined, ",") { - (*e)[key] = nil - } - return nil -} - -func (e eventTypeSet) MarshalJSON() ([]byte, error) { - var keys []string - for key := range e { - keys = append(keys, key) - } - return json.Marshal(strings.Join(keys, ",")) -} - -func (e eventTypeSet) matches(event xdr.ContractEvent) bool { - if len(e) == 0 { - return true - } - _, ok := e[eventTypeFromXDR[event.Type]] - return ok -} - -type EventInfo struct { - EventType string `json:"type"` - Ledger int32 `json:"ledger"` - LedgerClosedAt string `json:"ledgerClosedAt"` - ContractID string `json:"contractId"` - ID string `json:"id"` - PagingToken string `json:"pagingToken"` - Topic []string `json:"topic"` - Value string `json:"value"` - InSuccessfulContractCall bool `json:"inSuccessfulContractCall"` - TransactionHash string `json:"txHash"` -} - -type GetEventsRequest struct { - StartLedger uint32 `json:"startLedger,omitempty"` - Filters []EventFilter `json:"filters"` - Pagination *PaginationOptions `json:"pagination,omitempty"` -} - -func (g *GetEventsRequest) Valid(maxLimit uint) error { - // Validate start - // Validate the paging limit (if it exists) - if g.Pagination != nil && g.Pagination.Cursor != nil { - if g.StartLedger != 0 { - return errors.New("startLedger and cursor cannot both be set") - } - } else if g.StartLedger <= 0 { - return errors.New("startLedger must be positive") - } - if g.Pagination != nil && g.Pagination.Limit > maxLimit { - return fmt.Errorf("limit must not exceed %d", maxLimit) - } - - // Validate filters - if len(g.Filters) > 5 { - return errors.New("maximum 5 filters per request") - } - for i, filter := range g.Filters { - if err := filter.Valid(); err != nil { - return errors.Wrapf(err, "filter %d invalid", i+1) - } - } - - return nil -} - -func (g *GetEventsRequest) Matches(event xdr.DiagnosticEvent) bool { - if len(g.Filters) == 0 { - return true - } - for _, filter := range g.Filters { - if filter.Matches(event) { - return true - } - } - return false -} - -const EventTypeSystem = "system" -const EventTypeContract = "contract" -const EventTypeDiagnostic = "diagnostic" - -var eventTypeFromXDR = map[xdr.ContractEventType]string{ - xdr.ContractEventTypeSystem: EventTypeSystem, - xdr.ContractEventTypeContract: EventTypeContract, - xdr.ContractEventTypeDiagnostic: EventTypeDiagnostic, -} - -type EventFilter struct { - EventType eventTypeSet `json:"type,omitempty"` - ContractIDs []string `json:"contractIds,omitempty"` - Topics []TopicFilter `json:"topics,omitempty"` -} - -func (e *EventFilter) Valid() error { - if err := e.EventType.valid(); err != nil { - return errors.Wrap(err, "filter type invalid") - } - if len(e.ContractIDs) > 5 { - return errors.New("maximum 5 contract IDs per filter") - } - if len(e.Topics) > 5 { - return errors.New("maximum 5 topics per filter") - } - for i, id := range e.ContractIDs { - _, err := strkey.Decode(strkey.VersionByteContract, id) - if err != nil { - return fmt.Errorf("contract ID %d invalid", i+1) - } - } - for i, topic := range e.Topics { - if err := topic.Valid(); err != nil { - return errors.Wrapf(err, "topic %d invalid", i+1) - } - } - return nil -} - -func (e *EventFilter) Matches(event xdr.DiagnosticEvent) bool { - return e.EventType.matches(event.Event) && e.matchesContractIDs(event.Event) && e.matchesTopics(event.Event) -} - -func (e *EventFilter) matchesContractIDs(event xdr.ContractEvent) bool { - if len(e.ContractIDs) == 0 { - return true - } - if event.ContractId == nil { - return false - } - needle := strkey.MustEncode(strkey.VersionByteContract, (*event.ContractId)[:]) - for _, id := range e.ContractIDs { - if id == needle { - return true - } - } - return false -} - -func (e *EventFilter) matchesTopics(event xdr.ContractEvent) bool { - if len(e.Topics) == 0 { - return true - } - v0, ok := event.Body.GetV0() - if !ok { - return false - } - for _, topicFilter := range e.Topics { - if topicFilter.Matches(v0.Topics) { - return true - } - } - return false -} - -type TopicFilter []SegmentFilter - -const minTopicCount = 1 -const maxTopicCount = 4 - -func (t *TopicFilter) Valid() error { - if len(*t) < minTopicCount { - return errors.New("topic must have at least one segment") - } - if len(*t) > maxTopicCount { - return errors.New("topic cannot have more than 4 segments") - } - for i, segment := range *t { - if err := segment.Valid(); err != nil { - return errors.Wrapf(err, "segment %d invalid", i+1) - } - } - return nil -} - -// An event matches a topic filter iff: -// - the event has EXACTLY as many topic segments as the filter AND -// - each segment either: matches exactly OR is a wildcard. -func (t TopicFilter) Matches(event []xdr.ScVal) bool { - if len(event) != len(t) { - return false - } - - for i, segmentFilter := range t { - if !segmentFilter.Matches(event[i]) { - return false - } - } - - return true -} - -type SegmentFilter struct { - wildcard *string - scval *xdr.ScVal -} - -func (s *SegmentFilter) Matches(segment xdr.ScVal) bool { - if s.wildcard != nil && *s.wildcard == "*" { - return true - } else if s.scval != nil { - if !s.scval.Equals(segment) { - return false - } - } else { - panic("invalid segmentFilter") - } - - return true -} - -func (s *SegmentFilter) Valid() error { - if s.wildcard != nil && s.scval != nil { - return errors.New("cannot set both wildcard and scval") - } - if s.wildcard == nil && s.scval == nil { - return errors.New("must set either wildcard or scval") - } - if s.wildcard != nil && *s.wildcard != "*" { - return errors.New("wildcard must be '*'") - } - return nil -} - -func (s *SegmentFilter) UnmarshalJSON(p []byte) error { - s.wildcard = nil - s.scval = nil - - var tmp string - if err := json.Unmarshal(p, &tmp); err != nil { - return err - } - if tmp == "*" { - s.wildcard = &tmp - } else { - var out xdr.ScVal - if err := xdr.SafeUnmarshalBase64(tmp, &out); err != nil { - return err - } - s.scval = &out - } - return nil -} - -type PaginationOptions struct { - Cursor *events.Cursor `json:"cursor,omitempty"` - Limit uint `json:"limit,omitempty"` -} - -type GetEventsResponse struct { - Events []EventInfo `json:"events"` - LatestLedger int64 `json:"latestLedger"` -} - -type eventScanner interface { - Scan(eventRange events.Range, f events.ScanFunction) (uint32, error) -} - -type eventsRPCHandler struct { - scanner eventScanner - maxLimit uint - defaultLimit uint -} - -func (h eventsRPCHandler) getEvents(request GetEventsRequest) (GetEventsResponse, error) { - if err := request.Valid(h.maxLimit); err != nil { - return GetEventsResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: err.Error(), - } - } - - start := events.Cursor{Ledger: uint32(request.StartLedger)} - limit := h.defaultLimit - if request.Pagination != nil { - if request.Pagination.Cursor != nil { - start = *request.Pagination.Cursor - // increment event index because, when paginating, - // we start with the item right after the cursor - start.Event++ - } - if request.Pagination.Limit > 0 { - limit = request.Pagination.Limit - } - } - - type entry struct { - cursor events.Cursor - ledgerCloseTimestamp int64 - event xdr.DiagnosticEvent - txHash *xdr.Hash - } - var found []entry - latestLedger, err := h.scanner.Scan( - events.Range{ - Start: start, - ClampStart: false, - End: events.MaxCursor, - ClampEnd: true, - }, - func(event xdr.DiagnosticEvent, cursor events.Cursor, ledgerCloseTimestamp int64, txHash *xdr.Hash) bool { - if request.Matches(event) { - found = append(found, entry{cursor, ledgerCloseTimestamp, event, txHash}) - } - return uint(len(found)) < limit - }, - ) - if err != nil { - return GetEventsResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidRequest, - Message: err.Error(), - } - } - - results := []EventInfo{} - for _, entry := range found { - info, err := eventInfoForEvent( - entry.event, - entry.cursor, - time.Unix(entry.ledgerCloseTimestamp, 0).UTC().Format(time.RFC3339), - entry.txHash.HexString(), - ) - if err != nil { - return GetEventsResponse{}, errors.Wrap(err, "could not parse event") - } - results = append(results, info) - } - return GetEventsResponse{ - LatestLedger: int64(latestLedger), - Events: results, - }, nil -} - -func eventInfoForEvent(event xdr.DiagnosticEvent, cursor events.Cursor, ledgerClosedAt string, txHash string) (EventInfo, error) { - v0, ok := event.Event.Body.GetV0() - if !ok { - return EventInfo{}, errors.New("unknown event version") - } - - eventType, ok := eventTypeFromXDR[event.Event.Type] - if !ok { - return EventInfo{}, fmt.Errorf("unknown XDR ContractEventType type: %d", event.Event.Type) - } - - // base64-xdr encode the topic - topic := make([]string, 0, 4) - for _, segment := range v0.Topics { - seg, err := xdr.MarshalBase64(segment) - if err != nil { - return EventInfo{}, err - } - topic = append(topic, seg) - } - - // base64-xdr encode the data - data, err := xdr.MarshalBase64(v0.Data) - if err != nil { - return EventInfo{}, err - } - - info := EventInfo{ - EventType: eventType, - Ledger: int32(cursor.Ledger), - LedgerClosedAt: ledgerClosedAt, - ID: cursor.String(), - PagingToken: cursor.String(), - Topic: topic, - Value: data, - InSuccessfulContractCall: event.InSuccessfulContractCall, - TransactionHash: txHash, - } - if event.Event.ContractId != nil { - info.ContractID = strkey.MustEncode(strkey.VersionByteContract, (*event.Event.ContractId)[:]) - } - return info, nil -} - -// NewGetEventsHandler returns a json rpc handler to fetch and filter events -func NewGetEventsHandler(eventsStore *events.MemoryStore, maxLimit, defaultLimit uint) jrpc2.Handler { - eventsHandler := eventsRPCHandler{ - scanner: eventsStore, - maxLimit: maxLimit, - defaultLimit: defaultLimit, - } - return handler.New(func(ctx context.Context, request GetEventsRequest) (GetEventsResponse, error) { - return eventsHandler.getEvents(request) - }) -} diff --git a/cmd/soroban-rpc/internal/methods/get_events_test.go b/cmd/soroban-rpc/internal/methods/get_events_test.go deleted file mode 100644 index 5d1b929c6..000000000 --- a/cmd/soroban-rpc/internal/methods/get_events_test.go +++ /dev/null @@ -1,1201 +0,0 @@ -package methods - -import ( - "encoding/json" - "fmt" - "strings" - "testing" - "time" - - "github.com/stretchr/testify/assert" - - "github.com/stellar/go/keypair" - "github.com/stellar/go/network" - "github.com/stellar/go/strkey" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/events" -) - -func TestEventTypeSetMatches(t *testing.T) { - var defaultSet eventTypeSet - - all := eventTypeSet{} - all[EventTypeContract] = nil - all[EventTypeDiagnostic] = nil - all[EventTypeSystem] = nil - - onlyContract := eventTypeSet{} - onlyContract[EventTypeContract] = nil - - contractEvent := xdr.ContractEvent{Type: xdr.ContractEventTypeContract} - diagnosticEvent := xdr.ContractEvent{Type: xdr.ContractEventTypeDiagnostic} - systemEvent := xdr.ContractEvent{Type: xdr.ContractEventTypeSystem} - - for _, testCase := range []struct { - name string - set eventTypeSet - event xdr.ContractEvent - matches bool - }{ - { - "all matches Contract events", - all, - contractEvent, - true, - }, - { - "all matches System events", - all, - systemEvent, - true, - }, - { - "all matches Diagnostic events", - all, - systemEvent, - true, - }, - { - "defaultSet matches Contract events", - defaultSet, - contractEvent, - true, - }, - { - "defaultSet matches System events", - defaultSet, - systemEvent, - true, - }, - { - "defaultSet matches Diagnostic events", - defaultSet, - systemEvent, - true, - }, - { - "onlyContract set matches Contract events", - onlyContract, - contractEvent, - true, - }, - { - "onlyContract does not match System events", - onlyContract, - systemEvent, - false, - }, - { - "onlyContract does not match Diagnostic events", - defaultSet, - diagnosticEvent, - true, - }, - } { - t.Run(testCase.name, func(t *testing.T) { - assert.Equal(t, testCase.matches, testCase.set.matches(testCase.event)) - }) - } -} - -func TestEventTypeSetValid(t *testing.T) { - for _, testCase := range []struct { - name string - keys []string - expectedError bool - }{ - { - "empty set", - []string{}, - false, - }, - { - "set with one valid element", - []string{EventTypeSystem}, - false, - }, - { - "set with two valid elements", - []string{EventTypeSystem, EventTypeContract}, - false, - }, - { - "set with three valid elements", - []string{EventTypeSystem, EventTypeContract, EventTypeDiagnostic}, - false, - }, - { - "set with one invalid element", - []string{"abc"}, - true, - }, - { - "set with multiple invalid elements", - []string{"abc", "def"}, - true, - }, - { - "set with valid elements mixed with invalid elements", - []string{EventTypeSystem, "abc"}, - true, - }, - } { - t.Run(testCase.name, func(t *testing.T) { - set := eventTypeSet{} - for _, key := range testCase.keys { - set[key] = nil - } - if testCase.expectedError { - assert.Error(t, set.valid()) - } else { - assert.NoError(t, set.valid()) - } - }) - } -} - -func TestEventTypeSetMarshaling(t *testing.T) { - for _, testCase := range []struct { - name string - input string - expected []string - }{ - { - "empty set", - "", - []string{}, - }, - { - "set with one element", - "a", - []string{"a"}, - }, - { - "set with more than one element", - "a,b,c", - []string{"a", "b", "c"}, - }, - } { - t.Run(testCase.name, func(t *testing.T) { - var set eventTypeSet - input, err := json.Marshal(testCase.input) - assert.NoError(t, err) - err = set.UnmarshalJSON(input) - assert.NoError(t, err) - assert.Equal(t, len(testCase.expected), len(set)) - for _, val := range testCase.expected { - _, ok := set[val] - assert.True(t, ok) - } - }) - } -} - -func TestTopicFilterMatches(t *testing.T) { - transferSym := xdr.ScSymbol("transfer") - transfer := xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &transferSym, - } - sixtyfour := xdr.Uint64(64) - number := xdr.ScVal{ - Type: xdr.ScValTypeScvU64, - U64: &sixtyfour, - } - star := "*" - for _, tc := range []struct { - name string - filter TopicFilter - includes []xdr.ScVec - excludes []xdr.ScVec - }{ - { - name: "", - filter: nil, - includes: []xdr.ScVec{ - {}, - }, - excludes: []xdr.ScVec{ - {transfer}, - }, - }, - - // Exact matching - { - name: "ScSymbol(transfer)", - filter: []SegmentFilter{ - {scval: &transfer}, - }, - includes: []xdr.ScVec{ - {transfer}, - }, - excludes: []xdr.ScVec{ - {number}, - {transfer, transfer}, - }, - }, - - // Star - { - name: "*", - filter: []SegmentFilter{ - {wildcard: &star}, - }, - includes: []xdr.ScVec{ - {transfer}, - }, - excludes: []xdr.ScVec{ - {transfer, transfer}, - }, - }, - { - name: "*/transfer", - filter: []SegmentFilter{ - {wildcard: &star}, - {scval: &transfer}, - }, - includes: []xdr.ScVec{ - {number, transfer}, - {transfer, transfer}, - }, - excludes: []xdr.ScVec{ - {number}, - {number, number}, - {number, transfer, number}, - {transfer}, - {transfer, number}, - {transfer, transfer, transfer}, - }, - }, - { - name: "transfer/*", - filter: []SegmentFilter{ - {scval: &transfer}, - {wildcard: &star}, - }, - includes: []xdr.ScVec{ - {transfer, number}, - {transfer, transfer}, - }, - excludes: []xdr.ScVec{ - {number}, - {number, number}, - {number, transfer, number}, - {transfer}, - {number, transfer}, - {transfer, transfer, transfer}, - }, - }, - { - name: "transfer/*/*", - filter: []SegmentFilter{ - {scval: &transfer}, - {wildcard: &star}, - {wildcard: &star}, - }, - includes: []xdr.ScVec{ - {transfer, number, number}, - {transfer, transfer, transfer}, - }, - excludes: []xdr.ScVec{ - {number}, - {number, number}, - {number, transfer}, - {number, transfer, number, number}, - {transfer}, - {transfer, transfer, transfer, transfer}, - }, - }, - { - name: "transfer/*/number", - filter: []SegmentFilter{ - {scval: &transfer}, - {wildcard: &star}, - {scval: &number}, - }, - includes: []xdr.ScVec{ - {transfer, number, number}, - {transfer, transfer, number}, - }, - excludes: []xdr.ScVec{ - {number}, - {number, number}, - {number, number, number}, - {number, transfer, number}, - {transfer}, - {number, transfer}, - {transfer, transfer, transfer}, - {transfer, number, transfer}, - }, - }, - } { - name := tc.name - if name == "" { - name = topicFilterToString(tc.filter) - } - t.Run(name, func(t *testing.T) { - for _, include := range tc.includes { - assert.True( - t, - tc.filter.Matches(include), - "Expected %v filter to include %v", - name, - include, - ) - } - for _, exclude := range tc.excludes { - assert.False( - t, - tc.filter.Matches(exclude), - "Expected %v filter to exclude %v", - name, - exclude, - ) - } - }) - } -} - -func TestTopicFilterJSON(t *testing.T) { - var got TopicFilter - - assert.NoError(t, json.Unmarshal([]byte("[]"), &got)) - assert.Equal(t, TopicFilter{}, got) - - star := "*" - assert.NoError(t, json.Unmarshal([]byte("[\"*\"]"), &got)) - assert.Equal(t, TopicFilter{{wildcard: &star}}, got) - - sixtyfour := xdr.Uint64(64) - scval := xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &sixtyfour} - scvalstr, err := xdr.MarshalBase64(scval) - assert.NoError(t, err) - assert.NoError(t, json.Unmarshal([]byte(fmt.Sprintf("[%q]", scvalstr)), &got)) - assert.Equal(t, TopicFilter{{scval: &scval}}, got) -} - -func topicFilterToString(t TopicFilter) string { - var s []string - for _, segment := range t { - if segment.wildcard != nil { - s = append(s, *segment.wildcard) - } else if segment.scval != nil { - out, err := xdr.MarshalBase64(*segment.scval) - if err != nil { - panic(err) - } - s = append(s, out) - } else { - panic("Invalid topic filter") - } - } - if len(s) == 0 { - s = append(s, "") - } - return strings.Join(s, "/") -} - -func TestGetEventsRequestValid(t *testing.T) { - // omit startLedger but include cursor - var request GetEventsRequest - assert.NoError(t, json.Unmarshal( - []byte("{ \"filters\": [], \"pagination\": { \"cursor\": \"0000000021474840576-0000000000\"} }"), - &request, - )) - assert.Equal(t, uint32(0), request.StartLedger) - assert.NoError(t, request.Valid(1000)) - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{}, - Pagination: &PaginationOptions{Cursor: &events.Cursor{}}, - }).Valid(1000), "startLedger and cursor cannot both be set") - - assert.NoError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{}, - Pagination: nil, - }).Valid(1000)) - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{}, - Pagination: &PaginationOptions{Limit: 1001}, - }).Valid(1000), "limit must not exceed 1000") - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 0, - Filters: []EventFilter{}, - Pagination: nil, - }).Valid(1000), "startLedger must be positive") - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {}, {}, {}, {}, {}, {}, - }, - Pagination: nil, - }).Valid(1000), "maximum 5 filters per request") - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {EventType: map[string]interface{}{"foo": nil}}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: filter type invalid: if set, type must be either 'system', 'contract' or 'diagnostic'") - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {ContractIDs: []string{ - "CCVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKUD2U", - "CC53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53WQD5", - "CDGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZTGMZLND", - "CDO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53YUK", - "CDXO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO53XO4M7R", - "CD7777777777777777777777777777777777777777777777777767GY", - }}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: maximum 5 contract IDs per filter") - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {ContractIDs: []string{"a"}}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: contract ID 1 invalid") - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {ContractIDs: []string{"CCVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVKVINVALID"}}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: contract ID 1 invalid") - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - { - Topics: []TopicFilter{ - {}, {}, {}, {}, {}, {}, - }, - }, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: maximum 5 topics per filter") - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {Topics: []TopicFilter{ - {}, - }}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: topic 1 invalid: topic must have at least one segment") - - assert.EqualError(t, (&GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {Topics: []TopicFilter{ - { - {}, - {}, - {}, - {}, - {}, - }, - }}, - }, - Pagination: nil, - }).Valid(1000), "filter 1 invalid: topic 1 invalid: topic cannot have more than 4 segments") -} - -func TestGetEvents(t *testing.T) { - now := time.Now().UTC() - counter := xdr.ScSymbol("COUNTER") - counterScVal := xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter} - counterXdr, err := xdr.MarshalBase64(counterScVal) - assert.NoError(t, err) - - t.Run("empty", func(t *testing.T) { - store := events.NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 100) - handler := eventsRPCHandler{ - scanner: store, - maxLimit: 10000, - defaultLimit: 100, - } - _, err = handler.getEvents(GetEventsRequest{ - StartLedger: 1, - }) - assert.EqualError(t, err, "[-32600] event store is empty") - }) - - t.Run("startLedger validation", func(t *testing.T) { - contractID := xdr.Hash([32]byte{}) - store := events.NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 100) - var txMeta []xdr.TransactionMeta - txMeta = append(txMeta, transactionMetaWithEvents( - contractEvent( - contractID, - xdr.ScVec{xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &counter, - }}, - xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &counter, - }, - ), - )) - assert.NoError(t, store.IngestEvents(ledgerCloseMetaWithEvents(2, now.Unix(), txMeta...))) - - handler := eventsRPCHandler{ - scanner: store, - maxLimit: 10000, - defaultLimit: 100, - } - _, err = handler.getEvents(GetEventsRequest{ - StartLedger: 1, - }) - assert.EqualError(t, err, "[-32600] start is before oldest ledger") - - _, err = handler.getEvents(GetEventsRequest{ - StartLedger: 3, - }) - assert.EqualError(t, err, "[-32600] start is after newest ledger") - }) - - t.Run("no filtering returns all", func(t *testing.T) { - contractID := xdr.Hash([32]byte{}) - store := events.NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 100) - var txMeta []xdr.TransactionMeta - for i := 0; i < 10; i++ { - txMeta = append(txMeta, transactionMetaWithEvents( - contractEvent( - contractID, - xdr.ScVec{xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &counter, - }}, - xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &counter, - }, - ), - )) - } - ledgerCloseMeta := ledgerCloseMetaWithEvents(1, now.Unix(), txMeta...) - assert.NoError(t, store.IngestEvents(ledgerCloseMeta)) - - handler := eventsRPCHandler{ - scanner: store, - maxLimit: 10000, - defaultLimit: 100, - } - results, err := handler.getEvents(GetEventsRequest{ - StartLedger: 1, - }) - assert.NoError(t, err) - - var expected []EventInfo - for i := range txMeta { - id := events.Cursor{ - Ledger: 1, - Tx: uint32(i + 1), - Op: 0, - Event: 0, - }.String() - value, err := xdr.MarshalBase64(xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &counter, - }) - assert.NoError(t, err) - expected = append(expected, EventInfo{ - EventType: EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - PagingToken: id, - Topic: []string{value}, - Value: value, - InSuccessfulContractCall: true, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - }) - } - assert.Equal(t, GetEventsResponse{expected, 1}, results) - }) - - t.Run("filtering by contract id", func(t *testing.T) { - store := events.NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 100) - var txMeta []xdr.TransactionMeta - contractIds := []xdr.Hash{ - xdr.Hash([32]byte{}), - xdr.Hash([32]byte{1}), - } - for i := 0; i < 5; i++ { - txMeta = append(txMeta, transactionMetaWithEvents( - contractEvent( - contractIds[i%len(contractIds)], - xdr.ScVec{xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &counter, - }}, - xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &counter, - }, - ), - )) - } - assert.NoError(t, store.IngestEvents(ledgerCloseMetaWithEvents(1, now.Unix(), txMeta...))) - - handler := eventsRPCHandler{ - scanner: store, - maxLimit: 10000, - defaultLimit: 100, - } - results, err := handler.getEvents(GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {ContractIDs: []string{strkey.MustEncode(strkey.VersionByteContract, contractIds[0][:])}}, - }, - }) - assert.NoError(t, err) - assert.Equal(t, int64(1), results.LatestLedger) - - expectedIds := []string{ - events.Cursor{Ledger: 1, Tx: 1, Op: 0, Event: 0}.String(), - events.Cursor{Ledger: 1, Tx: 3, Op: 0, Event: 0}.String(), - events.Cursor{Ledger: 1, Tx: 5, Op: 0, Event: 0}.String(), - } - eventIds := []string{} - for _, event := range results.Events { - eventIds = append(eventIds, event.ID) - } - assert.Equal(t, expectedIds, eventIds) - }) - - t.Run("filtering by topic", func(t *testing.T) { - store := events.NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 100) - var txMeta []xdr.TransactionMeta - contractID := xdr.Hash([32]byte{}) - for i := 0; i < 10; i++ { - number := xdr.Uint64(i) - txMeta = append(txMeta, transactionMetaWithEvents( - // Generate a unique topic like /counter/4 for each event so we can check - contractEvent( - contractID, - xdr.ScVec{ - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}, - }, - xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}, - ), - )) - } - ledgerCloseMeta := ledgerCloseMetaWithEvents(1, now.Unix(), txMeta...) - assert.NoError(t, store.IngestEvents(ledgerCloseMeta)) - - number := xdr.Uint64(4) - handler := eventsRPCHandler{ - scanner: store, - maxLimit: 10000, - defaultLimit: 100, - } - results, err := handler.getEvents(GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {Topics: []TopicFilter{ - []SegmentFilter{ - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}}, - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}}, - }, - }}, - }, - }) - assert.NoError(t, err) - - id := events.Cursor{Ledger: 1, Tx: 5, Op: 0, Event: 0}.String() - assert.NoError(t, err) - value, err := xdr.MarshalBase64(xdr.ScVal{ - Type: xdr.ScValTypeScvU64, - U64: &number, - }) - assert.NoError(t, err) - expected := []EventInfo{ - { - EventType: EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - PagingToken: id, - Topic: []string{counterXdr, value}, - Value: value, - InSuccessfulContractCall: true, - TransactionHash: ledgerCloseMeta.TransactionHash(4).HexString(), - }, - } - assert.Equal(t, GetEventsResponse{expected, 1}, results) - }) - - t.Run("filtering by both contract id and topic", func(t *testing.T) { - store := events.NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 100) - contractID := xdr.Hash([32]byte{}) - otherContractID := xdr.Hash([32]byte{1}) - number := xdr.Uint64(1) - txMeta := []xdr.TransactionMeta{ - // This matches neither the contract id nor the topic - transactionMetaWithEvents( - contractEvent( - otherContractID, - xdr.ScVec{ - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - }, - xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}, - ), - ), - // This matches the contract id but not the topic - transactionMetaWithEvents( - contractEvent( - contractID, - xdr.ScVec{ - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - }, - xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}, - ), - ), - // This matches the topic but not the contract id - transactionMetaWithEvents( - contractEvent( - otherContractID, - xdr.ScVec{ - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}, - }, - xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}, - ), - ), - // This matches both the contract id and the topic - transactionMetaWithEvents( - contractEvent( - contractID, - xdr.ScVec{ - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}, - }, - xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}, - ), - ), - } - ledgerCloseMeta := ledgerCloseMetaWithEvents(1, now.Unix(), txMeta...) - assert.NoError(t, store.IngestEvents(ledgerCloseMeta)) - - handler := eventsRPCHandler{ - scanner: store, - maxLimit: 10000, - defaultLimit: 100, - } - results, err := handler.getEvents(GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - { - ContractIDs: []string{strkey.MustEncode(strkey.VersionByteContract, contractID[:])}, - Topics: []TopicFilter{ - []SegmentFilter{ - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}}, - {scval: &xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}}, - }, - }, - }, - }, - }) - assert.NoError(t, err) - - id := events.Cursor{Ledger: 1, Tx: 4, Op: 0, Event: 0}.String() - value, err := xdr.MarshalBase64(xdr.ScVal{ - Type: xdr.ScValTypeScvU64, - U64: &number, - }) - assert.NoError(t, err) - expected := []EventInfo{ - { - EventType: EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - PagingToken: id, - Topic: []string{counterXdr, value}, - Value: value, - InSuccessfulContractCall: true, - TransactionHash: ledgerCloseMeta.TransactionHash(3).HexString(), - }, - } - assert.Equal(t, GetEventsResponse{expected, 1}, results) - }) - - t.Run("filtering by event type", func(t *testing.T) { - store := events.NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 100) - contractID := xdr.Hash([32]byte{}) - txMeta := []xdr.TransactionMeta{ - transactionMetaWithEvents( - contractEvent( - contractID, - xdr.ScVec{ - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - }, - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - ), - systemEvent( - contractID, - xdr.ScVec{ - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - }, - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - ), - diagnosticEvent( - contractID, - xdr.ScVec{ - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - }, - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &counter}, - ), - ), - } - ledgerCloseMeta := ledgerCloseMetaWithEvents(1, now.Unix(), txMeta...) - assert.NoError(t, store.IngestEvents(ledgerCloseMeta)) - - handler := eventsRPCHandler{ - scanner: store, - maxLimit: 10000, - defaultLimit: 100, - } - results, err := handler.getEvents(GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{ - {EventType: map[string]interface{}{EventTypeSystem: nil}}, - }, - }) - assert.NoError(t, err) - - id := events.Cursor{Ledger: 1, Tx: 1, Op: 0, Event: 1}.String() - expected := []EventInfo{ - { - EventType: EventTypeSystem, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - PagingToken: id, - Topic: []string{counterXdr}, - Value: counterXdr, - InSuccessfulContractCall: true, - TransactionHash: ledgerCloseMeta.TransactionHash(0).HexString(), - }, - } - assert.Equal(t, GetEventsResponse{expected, 1}, results) - }) - - t.Run("with limit", func(t *testing.T) { - store := events.NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 100) - contractID := xdr.Hash([32]byte{}) - var txMeta []xdr.TransactionMeta - for i := 0; i < 180; i++ { - number := xdr.Uint64(i) - txMeta = append(txMeta, transactionMetaWithEvents( - contractEvent( - contractID, - xdr.ScVec{ - xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}, - }, - xdr.ScVal{Type: xdr.ScValTypeScvU64, U64: &number}, - ), - )) - } - ledgerCloseMeta := ledgerCloseMetaWithEvents(1, now.Unix(), txMeta...) - assert.NoError(t, store.IngestEvents(ledgerCloseMeta)) - - handler := eventsRPCHandler{ - scanner: store, - maxLimit: 10000, - defaultLimit: 100, - } - results, err := handler.getEvents(GetEventsRequest{ - StartLedger: 1, - Filters: []EventFilter{}, - Pagination: &PaginationOptions{Limit: 10}, - }) - assert.NoError(t, err) - - var expected []EventInfo - for i := 0; i < 10; i++ { - id := events.Cursor{ - Ledger: 1, - Tx: uint32(i + 1), - Op: 0, - Event: 0, - }.String() - value, err := xdr.MarshalBase64(txMeta[i].MustV3().SorobanMeta.Events[0].Body.MustV0().Data) - assert.NoError(t, err) - expected = append(expected, EventInfo{ - EventType: EventTypeContract, - Ledger: 1, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: "CAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABSC4", - ID: id, - PagingToken: id, - Topic: []string{value}, - Value: value, - InSuccessfulContractCall: true, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - }) - } - assert.Equal(t, GetEventsResponse{expected, 1}, results) - }) - - t.Run("with cursor", func(t *testing.T) { - store := events.NewMemoryStore(interfaces.MakeNoOpDeamon(), "unit-tests", 100) - contractID := xdr.Hash([32]byte{}) - datas := []xdr.ScSymbol{ - // ledger/transaction/operation/event - xdr.ScSymbol("5/1/0/0"), - xdr.ScSymbol("5/1/0/1"), - xdr.ScSymbol("5/2/0/0"), - xdr.ScSymbol("5/2/0/1"), - } - txMeta := []xdr.TransactionMeta{ - transactionMetaWithEvents( - contractEvent( - contractID, - xdr.ScVec{ - counterScVal, - }, - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &datas[0]}, - ), - contractEvent( - contractID, - xdr.ScVec{ - counterScVal, - }, - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &datas[1]}, - ), - ), - transactionMetaWithEvents( - contractEvent( - contractID, - xdr.ScVec{ - counterScVal, - }, - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &datas[2]}, - ), - contractEvent( - contractID, - xdr.ScVec{ - counterScVal, - }, - xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &datas[3]}, - ), - ), - } - ledgerCloseMeta := ledgerCloseMetaWithEvents(5, now.Unix(), txMeta...) - assert.NoError(t, store.IngestEvents(ledgerCloseMeta)) - - id := &events.Cursor{Ledger: 5, Tx: 1, Op: 0, Event: 0} - handler := eventsRPCHandler{ - scanner: store, - maxLimit: 10000, - defaultLimit: 100, - } - results, err := handler.getEvents(GetEventsRequest{ - Pagination: &PaginationOptions{ - Cursor: id, - Limit: 2, - }, - }) - assert.NoError(t, err) - - var expected []EventInfo - expectedIDs := []string{ - events.Cursor{Ledger: 5, Tx: 1, Op: 0, Event: 1}.String(), - events.Cursor{Ledger: 5, Tx: 2, Op: 0, Event: 0}.String(), - } - symbols := datas[1:3] - for i, id := range expectedIDs { - expectedXdr, err := xdr.MarshalBase64(xdr.ScVal{Type: xdr.ScValTypeScvSymbol, Sym: &symbols[i]}) - assert.NoError(t, err) - expected = append(expected, EventInfo{ - EventType: EventTypeContract, - Ledger: 5, - LedgerClosedAt: now.Format(time.RFC3339), - ContractID: strkey.MustEncode(strkey.VersionByteContract, contractID[:]), - ID: id, - PagingToken: id, - Topic: []string{counterXdr}, - Value: expectedXdr, - InSuccessfulContractCall: true, - TransactionHash: ledgerCloseMeta.TransactionHash(i).HexString(), - }) - } - assert.Equal(t, GetEventsResponse{expected, 5}, results) - - results, err = handler.getEvents(GetEventsRequest{ - Pagination: &PaginationOptions{ - Cursor: &events.Cursor{Ledger: 5, Tx: 2, Op: 0, Event: 1}, - Limit: 2, - }, - }) - assert.NoError(t, err) - assert.Equal(t, GetEventsResponse{[]EventInfo{}, 5}, results) - }) -} - -func ledgerCloseMetaWithEvents(sequence uint32, closeTimestamp int64, txMeta ...xdr.TransactionMeta) xdr.LedgerCloseMeta { - var txProcessing []xdr.TransactionResultMeta - var phases []xdr.TransactionPhase - - for _, item := range txMeta { - var operations []xdr.Operation - for range item.MustV3().SorobanMeta.Events { - operations = append(operations, - xdr.Operation{ - Body: xdr.OperationBody{ - Type: xdr.OperationTypeInvokeHostFunction, - InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &xdr.Hash{0x1, 0x2}, - }, - FunctionName: "foo", - Args: nil, - }, - }, - Auth: []xdr.SorobanAuthorizationEntry{}, - }, - }, - }) - } - envelope := xdr.TransactionEnvelope{ - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: &xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - SourceAccount: xdr.MustMuxedAddress(keypair.MustRandom().Address()), - Operations: operations, - }, - }, - } - txHash, err := network.HashTransactionInEnvelope(envelope, "unit-tests") - if err != nil { - panic(err) - } - - txProcessing = append(txProcessing, xdr.TransactionResultMeta{ - TxApplyProcessing: item, - Result: xdr.TransactionResultPair{ - TransactionHash: txHash, - }, - }) - components := []xdr.TxSetComponent{ - { - Type: xdr.TxSetComponentTypeTxsetCompTxsMaybeDiscountedFee, - TxsMaybeDiscountedFee: &xdr.TxSetComponentTxsMaybeDiscountedFee{ - Txs: []xdr.TransactionEnvelope{ - envelope, - }, - }, - }, - } - phases = append(phases, xdr.TransactionPhase{ - V: 0, - V0Components: &components, - }) - } - - return xdr.LedgerCloseMeta{ - V: 1, - V1: &xdr.LedgerCloseMetaV1{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Hash: xdr.Hash{}, - Header: xdr.LedgerHeader{ - ScpValue: xdr.StellarValue{ - CloseTime: xdr.TimePoint(closeTimestamp), - }, - LedgerSeq: xdr.Uint32(sequence), - }, - }, - TxSet: xdr.GeneralizedTransactionSet{ - V: 1, - V1TxSet: &xdr.TransactionSetV1{ - PreviousLedgerHash: xdr.Hash{}, - Phases: phases, - }, - }, - TxProcessing: txProcessing, - }, - } -} - -func transactionMetaWithEvents(events ...xdr.ContractEvent) xdr.TransactionMeta { - return xdr.TransactionMeta{ - V: 3, - Operations: &[]xdr.OperationMeta{}, - V3: &xdr.TransactionMetaV3{ - SorobanMeta: &xdr.SorobanTransactionMeta{ - Events: events, - }, - }, - } -} - -func contractEvent(contractID xdr.Hash, topic []xdr.ScVal, body xdr.ScVal) xdr.ContractEvent { - return xdr.ContractEvent{ - ContractId: &contractID, - Type: xdr.ContractEventTypeContract, - Body: xdr.ContractEventBody{ - V: 0, - V0: &xdr.ContractEventV0{ - Topics: topic, - Data: body, - }, - }, - } -} - -func systemEvent(contractID xdr.Hash, topic []xdr.ScVal, body xdr.ScVal) xdr.ContractEvent { - return xdr.ContractEvent{ - ContractId: &contractID, - Type: xdr.ContractEventTypeSystem, - Body: xdr.ContractEventBody{ - V: 0, - V0: &xdr.ContractEventV0{ - Topics: topic, - Data: body, - }, - }, - } -} - -func diagnosticEvent(contractID xdr.Hash, topic []xdr.ScVal, body xdr.ScVal) xdr.ContractEvent { - return xdr.ContractEvent{ - ContractId: &contractID, - Type: xdr.ContractEventTypeDiagnostic, - Body: xdr.ContractEventBody{ - V: 0, - V0: &xdr.ContractEventV0{ - Topics: topic, - Data: body, - }, - }, - } -} diff --git a/cmd/soroban-rpc/internal/methods/get_latest_ledger.go b/cmd/soroban-rpc/internal/methods/get_latest_ledger.go deleted file mode 100644 index 11bd997ac..000000000 --- a/cmd/soroban-rpc/internal/methods/get_latest_ledger.go +++ /dev/null @@ -1,58 +0,0 @@ -package methods - -import ( - "context" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" -) - -type GetLatestLedgerResponse struct { - // Hash of the latest ledger as a hex-encoded string - Hash string `json:"id"` - // Stellar Core protocol version associated with the ledger. - ProtocolVersion uint32 `json:"protocolVersion"` - // Sequence number of the latest ledger. - Sequence uint32 `json:"sequence"` -} - -// NewGetLatestLedgerHandler returns a JSON RPC handler to retrieve the latest ledger entry from Stellar core. -func NewGetLatestLedgerHandler(ledgerEntryReader db.LedgerEntryReader, ledgerReader db.LedgerReader) jrpc2.Handler { - return handler.New(func(ctx context.Context) (GetLatestLedgerResponse, error) { - tx, err := ledgerEntryReader.NewTx(ctx) - if err != nil { - return GetLatestLedgerResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not create read transaction", - } - } - defer func() { - _ = tx.Done() - }() - - latestSequence, err := tx.GetLatestLedgerSequence() - if err != nil { - return GetLatestLedgerResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not get latest ledger sequence", - } - } - - latestLedger, found, err := ledgerReader.GetLedger(ctx, latestSequence) - if (err != nil) || (!found) { - return GetLatestLedgerResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not get latest ledger", - } - } - - response := GetLatestLedgerResponse{ - Hash: latestLedger.LedgerHash().HexString(), - ProtocolVersion: latestLedger.ProtocolVersion(), - Sequence: latestSequence, - } - return response, nil - }) -} diff --git a/cmd/soroban-rpc/internal/methods/get_latest_ledger_test.go b/cmd/soroban-rpc/internal/methods/get_latest_ledger_test.go deleted file mode 100644 index 474b3b8da..000000000 --- a/cmd/soroban-rpc/internal/methods/get_latest_ledger_test.go +++ /dev/null @@ -1,87 +0,0 @@ -package methods - -import ( - "context" - "testing" - - "github.com/creachadair/jrpc2" - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/assert" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" -) - -const ( - expectedLatestLedgerSequence uint32 = 960 - expectedLatestLedgerProtocolVersion uint32 = 20 - expectedLatestLedgerHashBytes byte = 42 -) - -type ConstantLedgerEntryReader struct { -} - -type ConstantLedgerEntryReaderTx struct { -} - -type ConstantLedgerReader struct { -} - -func (entryReader *ConstantLedgerEntryReader) GetLatestLedgerSequence(ctx context.Context) (uint32, error) { - return expectedLatestLedgerSequence, nil -} - -func (entryReader *ConstantLedgerEntryReader) NewTx(ctx context.Context) (db.LedgerEntryReadTx, error) { - return ConstantLedgerEntryReaderTx{}, nil -} - -func (entryReader *ConstantLedgerEntryReader) NewCachedTx(ctx context.Context) (db.LedgerEntryReadTx, error) { - return ConstantLedgerEntryReaderTx{}, nil -} - -func (entryReaderTx ConstantLedgerEntryReaderTx) GetLatestLedgerSequence() (uint32, error) { - return expectedLatestLedgerSequence, nil -} - -func (entryReaderTx ConstantLedgerEntryReaderTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]db.LedgerKeyAndEntry, error) { - return nil, nil -} - -func (entryReaderTx ConstantLedgerEntryReaderTx) Done() error { - return nil -} - -func (ledgerReader *ConstantLedgerReader) GetLedger(ctx context.Context, sequence uint32) (xdr.LedgerCloseMeta, bool, error) { - return createLedger(sequence, expectedLatestLedgerProtocolVersion, expectedLatestLedgerHashBytes), true, nil -} - -func (ledgerReader *ConstantLedgerReader) StreamAllLedgers(ctx context.Context, f db.StreamLedgerFn) error { - return nil -} - -func createLedger(ledgerSequence uint32, protocolVersion uint32, hash byte) xdr.LedgerCloseMeta { - return xdr.LedgerCloseMeta{ - V: 1, - V1: &xdr.LedgerCloseMetaV1{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Hash: xdr.Hash{hash}, - Header: xdr.LedgerHeader{ - LedgerSeq: xdr.Uint32(ledgerSequence), - LedgerVersion: xdr.Uint32(protocolVersion), - }, - }, - }, - } -} - -func TestGetLatestLedger(t *testing.T) { - getLatestLedgerHandler := NewGetLatestLedgerHandler(&ConstantLedgerEntryReader{}, &ConstantLedgerReader{}) - latestLedgerRespI, err := getLatestLedgerHandler(context.Background(), &jrpc2.Request{}) - latestLedgerResp := latestLedgerRespI.(GetLatestLedgerResponse) - assert.NoError(t, err) - - expectedLatestLedgerHashStr := xdr.Hash{expectedLatestLedgerHashBytes}.HexString() - assert.Equal(t, expectedLatestLedgerHashStr, latestLedgerResp.Hash) - - assert.Equal(t, expectedLatestLedgerProtocolVersion, latestLedgerResp.ProtocolVersion) - assert.Equal(t, expectedLatestLedgerSequence, latestLedgerResp.Sequence) -} diff --git a/cmd/soroban-rpc/internal/methods/get_ledger_entries.go b/cmd/soroban-rpc/internal/methods/get_ledger_entries.go deleted file mode 100644 index 4063858c2..000000000 --- a/cmd/soroban-rpc/internal/methods/get_ledger_entries.go +++ /dev/null @@ -1,137 +0,0 @@ -package methods - -import ( - "context" - "fmt" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" -) - -var ErrLedgerTtlEntriesCannotBeQueriedDirectly = "ledger ttl entries cannot be queried directly" - -type GetLedgerEntriesRequest struct { - Keys []string `json:"keys"` -} - -type LedgerEntryResult struct { - // Original request key matching this LedgerEntryResult. - Key string `json:"key"` - // Ledger entry data encoded in base 64. - XDR string `json:"xdr"` - // Last modified ledger for this entry. - LastModifiedLedger uint32 `json:"lastModifiedLedgerSeq"` - // The ledger sequence until the entry is live, available for entries that have associated ttl ledger entries. - LiveUntilLedgerSeq *uint32 `json:"liveUntilLedgerSeq,omitempty"` -} - -type GetLedgerEntriesResponse struct { - // All found ledger entries. - Entries []LedgerEntryResult `json:"entries"` - // Sequence number of the latest ledger at time of request. - LatestLedger uint32 `json:"latestLedger"` -} - -const getLedgerEntriesMaxKeys = 200 - -// NewGetLedgerEntriesHandler returns a JSON RPC handler to retrieve the specified ledger entries from Stellar Core. -func NewGetLedgerEntriesHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader) jrpc2.Handler { - return handler.New(func(ctx context.Context, request GetLedgerEntriesRequest) (GetLedgerEntriesResponse, error) { - if len(request.Keys) > getLedgerEntriesMaxKeys { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: fmt.Sprintf("key count (%d) exceeds maximum supported (%d)", len(request.Keys), getLedgerEntriesMaxKeys), - } - } - var ledgerKeys []xdr.LedgerKey - for i, requestKey := range request.Keys { - var ledgerKey xdr.LedgerKey - if err := xdr.SafeUnmarshalBase64(requestKey, &ledgerKey); err != nil { - logger.WithError(err).WithField("request", request). - Infof("could not unmarshal requestKey %s at index %d from getLedgerEntries request", requestKey, i) - return GetLedgerEntriesResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: fmt.Sprintf("cannot unmarshal key value %s at index %d", requestKey, i), - } - } - if ledgerKey.Type == xdr.LedgerEntryTypeTtl { - logger.WithField("request", request). - Infof("could not provide ledger ttl entry %s at index %d from getLedgerEntries request", requestKey, i) - return GetLedgerEntriesResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: ErrLedgerTtlEntriesCannotBeQueriedDirectly, - } - } - ledgerKeys = append(ledgerKeys, ledgerKey) - } - - tx, err := ledgerEntryReader.NewTx(ctx) - if err != nil { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not create read transaction", - } - } - defer func() { - _ = tx.Done() - }() - - latestLedger, err := tx.GetLatestLedgerSequence() - if err != nil { - return GetLedgerEntriesResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not get latest ledger", - } - } - - ledgerEntryResults := make([]LedgerEntryResult, 0, len(ledgerKeys)) - ledgerKeysAndEntries, err := tx.GetLedgerEntries(ledgerKeys...) - if err != nil { - logger.WithError(err).WithField("request", request). - Info("could not obtain ledger entries from storage") - return GetLedgerEntriesResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not obtain ledger entries from storage", - } - } - - for _, ledgerKeyAndEntry := range ledgerKeysAndEntries { - keyXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Key) - if err != nil { - logger.WithError(err).WithField("request", request). - Infof("could not serialize ledger key %v", ledgerKeyAndEntry.Key) - return GetLedgerEntriesResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: fmt.Sprintf("could not serialize ledger key %v", ledgerKeyAndEntry.Key), - } - } - - entryXDR, err := xdr.MarshalBase64(ledgerKeyAndEntry.Entry.Data) - if err != nil { - logger.WithError(err).WithField("request", request). - Infof("could not serialize ledger entry data for ledger entry %v", ledgerKeyAndEntry.Entry) - return GetLedgerEntriesResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: fmt.Sprintf("could not serialize ledger entry data for ledger entry %v", ledgerKeyAndEntry.Entry), - } - } - - ledgerEntryResults = append(ledgerEntryResults, LedgerEntryResult{ - Key: keyXDR, - XDR: entryXDR, - LastModifiedLedger: uint32(ledgerKeyAndEntry.Entry.LastModifiedLedgerSeq), - LiveUntilLedgerSeq: ledgerKeyAndEntry.LiveUntilLedgerSeq, - }) - } - - response := GetLedgerEntriesResponse{ - Entries: ledgerEntryResults, - LatestLedger: uint32(latestLedger), - } - return response, nil - }) -} diff --git a/cmd/soroban-rpc/internal/methods/get_ledger_entry.go b/cmd/soroban-rpc/internal/methods/get_ledger_entry.go deleted file mode 100644 index b78d10996..000000000 --- a/cmd/soroban-rpc/internal/methods/get_ledger_entry.go +++ /dev/null @@ -1,106 +0,0 @@ -package methods - -import ( - "context" - "fmt" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" -) - -// Deprecated. Use GetLedgerEntriesRequest instead. -// TODO(https://github.com/stellar/soroban-tools/issues/374) remove after getLedgerEntries is deployed. -type GetLedgerEntryRequest struct { - Key string `json:"key"` -} - -// Deprecated. Use GetLedgerEntriesResponse instead. -// TODO(https://github.com/stellar/soroban-tools/issues/374) remove after getLedgerEntries is deployed. -type GetLedgerEntryResponse struct { - XDR string `json:"xdr"` - LastModifiedLedger uint32 `json:"lastModifiedLedgerSeq"` - LatestLedger uint32 `json:"latestLedger"` - // The ledger sequence until the entry is live, available for entries that have associated ttl ledger entries. - LiveUntilLedgerSeq *uint32 `json:"LiveUntilLedgerSeq,omitempty"` -} - -// NewGetLedgerEntryHandler returns a json rpc handler to retrieve the specified ledger entry from stellar core -// Deprecated. use NewGetLedgerEntriesHandler instead. -// TODO(https://github.com/stellar/soroban-tools/issues/374) remove after getLedgerEntries is deployed. -func NewGetLedgerEntryHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader) jrpc2.Handler { - return handler.New(func(ctx context.Context, request GetLedgerEntryRequest) (GetLedgerEntryResponse, error) { - var key xdr.LedgerKey - if err := xdr.SafeUnmarshalBase64(request.Key, &key); err != nil { - logger.WithError(err).WithField("request", request). - Info("could not unmarshal ledgerKey from getLedgerEntry request") - return GetLedgerEntryResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: "cannot unmarshal key value", - } - } - - if key.Type == xdr.LedgerEntryTypeTtl { - return GetLedgerEntryResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: ErrLedgerTtlEntriesCannotBeQueriedDirectly, - } - } - - tx, err := ledgerEntryReader.NewTx(ctx) - if err != nil { - return GetLedgerEntryResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not create read transaction", - } - } - defer func() { - _ = tx.Done() - }() - - latestLedger, err := tx.GetLatestLedgerSequence() - if err != nil { - return GetLedgerEntryResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not get latest ledger", - } - } - - present, ledgerEntry, liveUntilLedgerSeq, err := db.GetLedgerEntry(tx, key) - if err != nil { - logger.WithError(err).WithField("request", request). - Info("could not obtain ledger entry from storage") - return GetLedgerEntryResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not obtain ledger entry from storage", - } - } - - if !present { - return GetLedgerEntryResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidRequest, - Message: fmt.Sprintf("not found (at ledger %d)", latestLedger), - } - } - - response := GetLedgerEntryResponse{ - LastModifiedLedger: uint32(ledgerEntry.LastModifiedLedgerSeq), - LatestLedger: latestLedger, - LiveUntilLedgerSeq: liveUntilLedgerSeq, - } - if response.XDR, err = xdr.MarshalBase64(ledgerEntry.Data); err != nil { - logger.WithError(err).WithField("request", request). - Info("could not serialize ledger entry data") - return GetLedgerEntryResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not serialize ledger entry data", - } - } - - return response, nil - }) -} diff --git a/cmd/soroban-rpc/internal/methods/get_network.go b/cmd/soroban-rpc/internal/methods/get_network.go deleted file mode 100644 index be2e0305f..000000000 --- a/cmd/soroban-rpc/internal/methods/get_network.go +++ /dev/null @@ -1,37 +0,0 @@ -package methods - -import ( - "context" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" -) - -type GetNetworkRequest struct{} - -type GetNetworkResponse struct { - FriendbotURL string `json:"friendbotUrl,omitempty"` - Passphrase string `json:"passphrase"` - ProtocolVersion int `json:"protocolVersion"` -} - -// NewGetNetworkHandler returns a json rpc handler to for the getNetwork method -func NewGetNetworkHandler(daemon interfaces.Daemon, networkPassphrase, friendbotURL string) jrpc2.Handler { - coreClient := daemon.CoreClient() - return handler.New(func(ctx context.Context, request GetNetworkRequest) (GetNetworkResponse, error) { - info, err := coreClient.Info(ctx) - if err != nil { - return GetNetworkResponse{}, (&jrpc2.Error{ - Code: jrpc2.InternalError, - Message: err.Error(), - }) - } - return GetNetworkResponse{ - FriendbotURL: friendbotURL, - Passphrase: networkPassphrase, - ProtocolVersion: info.Info.ProtocolVersion, - }, nil - }) -} diff --git a/cmd/soroban-rpc/internal/methods/get_transaction.go b/cmd/soroban-rpc/internal/methods/get_transaction.go deleted file mode 100644 index 7a2fe6575..000000000 --- a/cmd/soroban-rpc/internal/methods/get_transaction.go +++ /dev/null @@ -1,120 +0,0 @@ -package methods - -import ( - "context" - "encoding/base64" - "encoding/hex" - "fmt" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/transactions" -) - -const ( - // TransactionStatusSuccess indicates the transaction was included in the ledger and - // it was executed without errors. - TransactionStatusSuccess = "SUCCESS" - // TransactionStatusNotFound indicates the transaction was not found in Soroban-RPC's - // transaction store. - TransactionStatusNotFound = "NOT_FOUND" - // TransactionStatusFailed indicates the transaction was included in the ledger and - // it was executed with an error. - TransactionStatusFailed = "FAILED" -) - -// GetTransactionResponse is the response for the Soroban-RPC getTransaction() endpoint -type GetTransactionResponse struct { - // Status is one of: TransactionSuccess, TransactionNotFound, or TransactionFailed. - Status string `json:"status"` - // LatestLedger is the latest ledger stored in Soroban-RPC. - LatestLedger uint32 `json:"latestLedger"` - // LatestLedgerCloseTime is the unix timestamp of when the latest ledger was closed. - LatestLedgerCloseTime int64 `json:"latestLedgerCloseTime,string"` - // LatestLedger is the oldest ledger stored in Soroban-RPC. - OldestLedger uint32 `json:"oldestLedger"` - // LatestLedgerCloseTime is the unix timestamp of when the oldest ledger was closed. - OldestLedgerCloseTime int64 `json:"oldestLedgerCloseTime,string"` - - // The fields below are only present if Status is not TransactionNotFound. - - // ApplicationOrder is the index of the transaction among all the transactions - // for that ledger. - ApplicationOrder int32 `json:"applicationOrder,omitempty"` - // FeeBump indicates whether the transaction is a feebump transaction - FeeBump bool `json:"feeBump,omitempty"` - // EnvelopeXdr is the TransactionEnvelope XDR value. - EnvelopeXdr string `json:"envelopeXdr,omitempty"` - // ResultXdr is the TransactionResult XDR value. - ResultXdr string `json:"resultXdr,omitempty"` - // ResultMetaXdr is the TransactionMeta XDR value. - ResultMetaXdr string `json:"resultMetaXdr,omitempty"` - - // Ledger is the sequence of the ledger which included the transaction. - Ledger uint32 `json:"ledger,omitempty"` - // LedgerCloseTime is the unix timestamp of when the transaction was included in the ledger. - LedgerCloseTime int64 `json:"createdAt,string,omitempty"` -} - -type GetTransactionRequest struct { - Hash string `json:"hash"` -} - -type transactionGetter interface { - GetTransaction(hash xdr.Hash) (transactions.Transaction, bool, transactions.StoreRange) -} - -func GetTransaction(getter transactionGetter, request GetTransactionRequest) (GetTransactionResponse, error) { - // parse hash - if hex.DecodedLen(len(request.Hash)) != len(xdr.Hash{}) { - return GetTransactionResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: fmt.Sprintf("unexpected hash length (%d)", len(request.Hash)), - } - } - - var txHash xdr.Hash - _, err := hex.Decode(txHash[:], []byte(request.Hash)) - if err != nil { - return GetTransactionResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: fmt.Sprintf("incorrect hash: %v", err), - } - } - - tx, found, storeRange := getter.GetTransaction(txHash) - response := GetTransactionResponse{ - LatestLedger: storeRange.LastLedger.Sequence, - LatestLedgerCloseTime: storeRange.LastLedger.CloseTime, - OldestLedger: storeRange.FirstLedger.Sequence, - OldestLedgerCloseTime: storeRange.FirstLedger.CloseTime, - } - if !found { - response.Status = TransactionStatusNotFound - return response, nil - } - - response.ApplicationOrder = tx.ApplicationOrder - response.FeeBump = tx.FeeBump - response.Ledger = tx.Ledger.Sequence - response.LedgerCloseTime = tx.Ledger.CloseTime - - response.ResultXdr = base64.StdEncoding.EncodeToString(tx.Result) - response.EnvelopeXdr = base64.StdEncoding.EncodeToString(tx.Envelope) - response.ResultMetaXdr = base64.StdEncoding.EncodeToString(tx.Meta) - if tx.Successful { - response.Status = TransactionStatusSuccess - } else { - response.Status = TransactionStatusFailed - } - return response, nil -} - -// NewGetTransactionHandler returns a get transaction json rpc handler -func NewGetTransactionHandler(getter transactionGetter) jrpc2.Handler { - return handler.New(func(ctx context.Context, request GetTransactionRequest) (GetTransactionResponse, error) { - return GetTransaction(getter, request) - }) -} diff --git a/cmd/soroban-rpc/internal/methods/get_transaction_test.go b/cmd/soroban-rpc/internal/methods/get_transaction_test.go deleted file mode 100644 index 85847f00c..000000000 --- a/cmd/soroban-rpc/internal/methods/get_transaction_test.go +++ /dev/null @@ -1,210 +0,0 @@ -package methods - -import ( - "encoding/hex" - "testing" - - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/network" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/transactions" -) - -func txHash(acctSeq uint32) xdr.Hash { - envelope := txEnvelope(acctSeq) - hash, err := network.HashTransactionInEnvelope(envelope, "passphrase") - if err != nil { - panic(err) - } - - return hash -} - -func ledgerCloseTime(ledgerSequence uint32) int64 { - return int64(ledgerSequence)*25 + 100 -} - -func transactionResult(successful bool) xdr.TransactionResult { - code := xdr.TransactionResultCodeTxBadSeq - if successful { - code = xdr.TransactionResultCodeTxSuccess - } - opResults := []xdr.OperationResult{} - return xdr.TransactionResult{ - FeeCharged: 100, - Result: xdr.TransactionResultResult{ - Code: code, - Results: &opResults, - }, - } -} - -func txMeta(acctSeq uint32, successful bool) xdr.LedgerCloseMeta { - envelope := txEnvelope(acctSeq) - - txProcessing := []xdr.TransactionResultMeta{ - { - TxApplyProcessing: xdr.TransactionMeta{ - V: 3, - Operations: &[]xdr.OperationMeta{}, - V3: &xdr.TransactionMetaV3{}, - }, - Result: xdr.TransactionResultPair{ - TransactionHash: txHash(acctSeq), - Result: transactionResult(successful), - }, - }, - } - - components := []xdr.TxSetComponent{ - { - Type: xdr.TxSetComponentTypeTxsetCompTxsMaybeDiscountedFee, - TxsMaybeDiscountedFee: &xdr.TxSetComponentTxsMaybeDiscountedFee{ - BaseFee: nil, - Txs: []xdr.TransactionEnvelope{ - envelope, - }, - }, - }, - } - return xdr.LedgerCloseMeta{ - V: 1, - V1: &xdr.LedgerCloseMetaV1{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Header: xdr.LedgerHeader{ - ScpValue: xdr.StellarValue{ - CloseTime: xdr.TimePoint(ledgerCloseTime(acctSeq + 100)), - }, - LedgerSeq: xdr.Uint32(acctSeq + 100), - }, - }, - TxProcessing: txProcessing, - TxSet: xdr.GeneralizedTransactionSet{ - V: 1, - V1TxSet: &xdr.TransactionSetV1{ - PreviousLedgerHash: xdr.Hash{1}, - Phases: []xdr.TransactionPhase{ - { - V: 0, - V0Components: &components, - }, - }, - }, - }, - }, - } -} - -func txEnvelope(acctSeq uint32) xdr.TransactionEnvelope { - envelope, err := xdr.NewTransactionEnvelope(xdr.EnvelopeTypeEnvelopeTypeTx, xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - Fee: 1, - SeqNum: xdr.SequenceNumber(acctSeq), - SourceAccount: xdr.MustMuxedAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"), - }, - }) - if err != nil { - panic(err) - } - return envelope -} - -func TestGetTransaction(t *testing.T) { - store := transactions.NewMemoryStore(interfaces.MakeNoOpDeamon(), "passphrase", 100) - _, err := GetTransaction(store, GetTransactionRequest{"ab"}) - require.EqualError(t, err, "[-32602] unexpected hash length (2)") - _, err = GetTransaction(store, GetTransactionRequest{"foo "}) - require.EqualError(t, err, "[-32602] incorrect hash: encoding/hex: invalid byte: U+006F 'o'") - - hash := "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" - tx, err := GetTransaction(store, GetTransactionRequest{hash}) - require.NoError(t, err) - require.Equal(t, GetTransactionResponse{ - Status: TransactionStatusNotFound, - }, tx) - - meta := txMeta(1, true) - err = store.IngestTransactions(meta) - require.NoError(t, err) - - xdrHash := txHash(1) - hash = hex.EncodeToString(xdrHash[:]) - tx, err = GetTransaction(store, GetTransactionRequest{hash}) - require.NoError(t, err) - - expectedTxResult, err := xdr.MarshalBase64(meta.V1.TxProcessing[0].Result.Result) - require.NoError(t, err) - expectedEnvelope, err := xdr.MarshalBase64(txEnvelope(1)) - require.NoError(t, err) - expectedTxMeta, err := xdr.MarshalBase64(meta.V1.TxProcessing[0].TxApplyProcessing) - require.NoError(t, err) - require.Equal(t, GetTransactionResponse{ - Status: TransactionStatusSuccess, - LatestLedger: 101, - LatestLedgerCloseTime: 2625, - OldestLedger: 101, - OldestLedgerCloseTime: 2625, - ApplicationOrder: 1, - FeeBump: false, - EnvelopeXdr: expectedEnvelope, - ResultXdr: expectedTxResult, - ResultMetaXdr: expectedTxMeta, - Ledger: 101, - LedgerCloseTime: 2625, - }, tx) - - // ingest another (failed) transaction - meta = txMeta(2, false) - err = store.IngestTransactions(meta) - require.NoError(t, err) - - // the first transaction should still be there - tx, err = GetTransaction(store, GetTransactionRequest{hash}) - require.NoError(t, err) - require.Equal(t, GetTransactionResponse{ - Status: TransactionStatusSuccess, - LatestLedger: 102, - LatestLedgerCloseTime: 2650, - OldestLedger: 101, - OldestLedgerCloseTime: 2625, - ApplicationOrder: 1, - FeeBump: false, - EnvelopeXdr: expectedEnvelope, - ResultXdr: expectedTxResult, - ResultMetaXdr: expectedTxMeta, - Ledger: 101, - LedgerCloseTime: 2625, - }, tx) - - // the new transaction should also be there - xdrHash = txHash(2) - hash = hex.EncodeToString(xdrHash[:]) - - expectedTxResult, err = xdr.MarshalBase64(meta.V1.TxProcessing[0].Result.Result) - require.NoError(t, err) - expectedEnvelope, err = xdr.MarshalBase64(txEnvelope(2)) - require.NoError(t, err) - expectedTxMeta, err = xdr.MarshalBase64(meta.V1.TxProcessing[0].TxApplyProcessing) - require.NoError(t, err) - - tx, err = GetTransaction(store, GetTransactionRequest{hash}) - require.NoError(t, err) - require.NoError(t, err) - require.Equal(t, GetTransactionResponse{ - Status: TransactionStatusFailed, - LatestLedger: 102, - LatestLedgerCloseTime: 2650, - OldestLedger: 101, - OldestLedgerCloseTime: 2625, - ApplicationOrder: 1, - FeeBump: false, - EnvelopeXdr: expectedEnvelope, - ResultXdr: expectedTxResult, - ResultMetaXdr: expectedTxMeta, - Ledger: 102, - LedgerCloseTime: 2650, - }, tx) -} diff --git a/cmd/soroban-rpc/internal/methods/health.go b/cmd/soroban-rpc/internal/methods/health.go deleted file mode 100644 index ab51cc789..000000000 --- a/cmd/soroban-rpc/internal/methods/health.go +++ /dev/null @@ -1,40 +0,0 @@ -package methods - -import ( - "context" - "fmt" - "time" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/transactions" -) - -type HealthCheckResult struct { - Status string `json:"status"` -} - -// NewHealthCheck returns a health check json rpc handler -func NewHealthCheck(txStore *transactions.MemoryStore, maxHealthyLedgerLatency time.Duration) jrpc2.Handler { - return handler.New(func(ctx context.Context) (HealthCheckResult, error) { - ledgerInfo := txStore.GetLatestLedger() - if ledgerInfo.Sequence < 1 { - return HealthCheckResult{}, jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "data stores are not initialized", - } - } - lastKnownLedgerCloseTime := time.Unix(ledgerInfo.CloseTime, 0) - lastKnownLedgerLatency := time.Since(lastKnownLedgerCloseTime) - if lastKnownLedgerLatency > maxHealthyLedgerLatency { - roundedLatency := lastKnownLedgerLatency.Round(time.Second) - msg := fmt.Sprintf("latency (%s) since last known ledger closed is too high (>%s)", roundedLatency, maxHealthyLedgerLatency) - return HealthCheckResult{}, jrpc2.Error{ - Code: jrpc2.InternalError, - Message: msg, - } - } - return HealthCheckResult{Status: "healthy"}, nil - }) -} diff --git a/cmd/soroban-rpc/internal/methods/send_transaction.go b/cmd/soroban-rpc/internal/methods/send_transaction.go deleted file mode 100644 index c8a0ff844..000000000 --- a/cmd/soroban-rpc/internal/methods/send_transaction.go +++ /dev/null @@ -1,132 +0,0 @@ -package methods - -import ( - "context" - "encoding/hex" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - "github.com/stellar/go/network" - proto "github.com/stellar/go/protocols/stellarcore" - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/transactions" -) - -// SendTransactionResponse represents the transaction submission response returned Soroban-RPC -type SendTransactionResponse struct { - // ErrorResultXDR is present only if Status is equal to proto.TXStatusError. - // ErrorResultXDR is a TransactionResult xdr string which contains details on why - // the transaction could not be accepted by stellar-core. - ErrorResultXDR string `json:"errorResultXdr,omitempty"` - // DiagnosticEventsXDR is present only if Status is equal to proto.TXStatusError. - // DiagnosticEventsXDR is a base64-encoded slice of xdr.DiagnosticEvent - DiagnosticEventsXDR []string `json:"diagnosticEventsXdr,omitempty"` - // Status represents the status of the transaction submission returned by stellar-core. - // Status can be one of: proto.TXStatusPending, proto.TXStatusDuplicate, - // proto.TXStatusTryAgainLater, or proto.TXStatusError. - Status string `json:"status"` - // Hash is a hash of the transaction which can be used to look up whether - // the transaction was included in the ledger. - Hash string `json:"hash"` - // LatestLedger is the latest ledger known to Soroban-RPC at the time it handled - // the transaction submission request. - LatestLedger uint32 `json:"latestLedger"` - // LatestLedgerCloseTime is the unix timestamp of the close time of the latest ledger known to - // Soroban-RPC at the time it handled the transaction submission request. - LatestLedgerCloseTime int64 `json:"latestLedgerCloseTime,string"` -} - -// SendTransactionRequest is the Soroban-RPC request to submit a transaction. -type SendTransactionRequest struct { - // Transaction is the base64 encoded transaction envelope. - Transaction string `json:"transaction"` -} - -// LatestLedgerStore is a store which returns the latest ingested ledger. -type LatestLedgerStore interface { - // GetLatestLedger returns the latest ingested ledger. - GetLatestLedger() transactions.LedgerInfo -} - -// NewSendTransactionHandler returns a submit transaction json rpc handler -func NewSendTransactionHandler(daemon interfaces.Daemon, logger *log.Entry, store LatestLedgerStore, passphrase string) jrpc2.Handler { - submitter := daemon.CoreClient() - return handler.New(func(ctx context.Context, request SendTransactionRequest) (SendTransactionResponse, error) { - var envelope xdr.TransactionEnvelope - err := xdr.SafeUnmarshalBase64(request.Transaction, &envelope) - if err != nil { - return SendTransactionResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: "invalid_xdr", - } - } - - var hash [32]byte - hash, err = network.HashTransactionInEnvelope(envelope, passphrase) - if err != nil { - return SendTransactionResponse{}, &jrpc2.Error{ - Code: jrpc2.InvalidParams, - Message: "invalid_hash", - } - } - txHash := hex.EncodeToString(hash[:]) - - ledgerInfo := store.GetLatestLedger() - resp, err := submitter.SubmitTransaction(ctx, request.Transaction) - if err != nil { - logger.WithError(err). - WithField("tx", request.Transaction).Error("could not submit transaction") - return SendTransactionResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not submit transaction to stellar-core", - } - } - - // interpret response - if resp.IsException() { - logger.WithField("exception", resp.Exception). - WithField("tx", request.Transaction).Error("received exception from stellar core") - return SendTransactionResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "received exception from stellar-core", - } - } - - switch resp.Status { - case proto.TXStatusError: - events, err := proto.DiagnosticEventsToSlice(resp.DiagnosticEvents) - if err != nil { - logger.WithField("tx", request.Transaction).Error("Cannot decode diagnostic events:", err) - return SendTransactionResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "could not decode diagnostic events", - } - } - return SendTransactionResponse{ - ErrorResultXDR: resp.Error, - DiagnosticEventsXDR: events, - Status: resp.Status, - Hash: txHash, - LatestLedger: ledgerInfo.Sequence, - LatestLedgerCloseTime: ledgerInfo.CloseTime, - }, nil - case proto.TXStatusPending, proto.TXStatusDuplicate, proto.TXStatusTryAgainLater: - return SendTransactionResponse{ - Status: resp.Status, - Hash: txHash, - LatestLedger: ledgerInfo.Sequence, - LatestLedgerCloseTime: ledgerInfo.CloseTime, - }, nil - default: - logger.WithField("status", resp.Status). - WithField("tx", request.Transaction).Error("Unrecognized stellar-core status response") - return SendTransactionResponse{}, &jrpc2.Error{ - Code: jrpc2.InternalError, - Message: "invalid status from stellar-core", - } - } - }) -} diff --git a/cmd/soroban-rpc/internal/methods/simulate_transaction.go b/cmd/soroban-rpc/internal/methods/simulate_transaction.go deleted file mode 100644 index a278c9c23..000000000 --- a/cmd/soroban-rpc/internal/methods/simulate_transaction.go +++ /dev/null @@ -1,189 +0,0 @@ -package methods - -import ( - "context" - "encoding/base64" - "fmt" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/preflight" -) - -type SimulateTransactionRequest struct { - Transaction string `json:"transaction"` - ResourceConfig *preflight.ResourceConfig `json:"resourceConfig,omitempty"` -} - -type SimulateTransactionCost struct { - CPUInstructions uint64 `json:"cpuInsns,string"` - MemoryBytes uint64 `json:"memBytes,string"` -} - -// SimulateHostFunctionResult contains the simulation result of each HostFunction within the single InvokeHostFunctionOp allowed in a Transaction -type SimulateHostFunctionResult struct { - Auth []string `json:"auth"` - XDR string `json:"xdr"` -} - -type RestorePreamble struct { - TransactionData string `json:"transactionData"` // SorobanTransactionData XDR in base64 - MinResourceFee int64 `json:"minResourceFee,string"` -} - -type SimulateTransactionResponse struct { - Error string `json:"error,omitempty"` - TransactionData string `json:"transactionData,omitempty"` // SorobanTransactionData XDR in base64 - MinResourceFee int64 `json:"minResourceFee,string,omitempty"` - Events []string `json:"events,omitempty"` // DiagnosticEvent XDR in base64 - Results []SimulateHostFunctionResult `json:"results,omitempty"` // an array of the individual host function call results - Cost SimulateTransactionCost `json:"cost,omitempty"` // the effective cpu and memory cost of the invoked transaction execution. - RestorePreamble *RestorePreamble `json:"restorePreamble,omitempty"` // If present, it indicates that a prior RestoreFootprint is required - LatestLedger uint32 `json:"latestLedger"` -} - -type PreflightGetter interface { - GetPreflight(ctx context.Context, params preflight.PreflightGetterParameters) (preflight.Preflight, error) -} - -// NewSimulateTransactionHandler returns a json rpc handler to run preflight simulations -func NewSimulateTransactionHandler(logger *log.Entry, ledgerEntryReader db.LedgerEntryReader, ledgerReader db.LedgerReader, getter PreflightGetter) jrpc2.Handler { - - return handler.New(func(ctx context.Context, request SimulateTransactionRequest) SimulateTransactionResponse { - var txEnvelope xdr.TransactionEnvelope - if err := xdr.SafeUnmarshalBase64(request.Transaction, &txEnvelope); err != nil { - logger.WithError(err).WithField("request", request). - Info("could not unmarshal simulate transaction envelope") - return SimulateTransactionResponse{ - Error: "Could not unmarshal transaction", - } - } - if len(txEnvelope.Operations()) != 1 { - return SimulateTransactionResponse{ - Error: "Transaction contains more than one operation", - } - } - op := txEnvelope.Operations()[0] - - var sourceAccount xdr.AccountId - if opSourceAccount := op.SourceAccount; opSourceAccount != nil { - sourceAccount = opSourceAccount.ToAccountId() - } else { - sourceAccount = txEnvelope.SourceAccount().ToAccountId() - } - - footprint := xdr.LedgerFootprint{} - switch op.Body.Type { - case xdr.OperationTypeInvokeHostFunction: - case xdr.OperationTypeExtendFootprintTtl, xdr.OperationTypeRestoreFootprint: - if txEnvelope.Type != xdr.EnvelopeTypeEnvelopeTypeTx && txEnvelope.V1.Tx.Ext.V != 1 { - return SimulateTransactionResponse{ - Error: "To perform a SimulateTransaction for ExtendFootprintTtl or RestoreFootprint operations, SorobanTransactionData must be provided", - } - } - footprint = txEnvelope.V1.Tx.Ext.SorobanData.Resources.Footprint - default: - return SimulateTransactionResponse{ - Error: "Transaction contains unsupported operation type: " + op.Body.Type.String(), - } - } - - readTx, err := ledgerEntryReader.NewCachedTx(ctx) - if err != nil { - return SimulateTransactionResponse{ - Error: "Cannot create read transaction", - } - } - defer func() { - _ = readTx.Done() - }() - latestLedger, err := readTx.GetLatestLedgerSequence() - if err != nil { - return SimulateTransactionResponse{ - Error: err.Error(), - } - } - bucketListSize, err := getBucketListSize(ctx, ledgerReader, latestLedger) - if err != nil { - return SimulateTransactionResponse{ - Error: err.Error(), - } - } - - resource_config := preflight.DefaultResourceConfig() - if request.ResourceConfig != nil { - resource_config = *request.ResourceConfig - } - params := preflight.PreflightGetterParameters{ - LedgerEntryReadTx: readTx, - BucketListSize: bucketListSize, - SourceAccount: sourceAccount, - OperationBody: op.Body, - Footprint: footprint, - ResourceConfig: resource_config, - } - result, err := getter.GetPreflight(ctx, params) - if err != nil { - return SimulateTransactionResponse{ - Error: err.Error(), - LatestLedger: latestLedger, - } - } - - var results []SimulateHostFunctionResult - if len(result.Result) != 0 { - results = append(results, SimulateHostFunctionResult{ - XDR: base64.StdEncoding.EncodeToString(result.Result), - Auth: base64EncodeSlice(result.Auth), - }) - } - var restorePreamble *RestorePreamble = nil - if len(result.PreRestoreTransactionData) != 0 { - restorePreamble = &RestorePreamble{ - TransactionData: base64.StdEncoding.EncodeToString(result.PreRestoreTransactionData), - MinResourceFee: result.PreRestoreMinFee, - } - } - - return SimulateTransactionResponse{ - Error: result.Error, - Results: results, - Events: base64EncodeSlice(result.Events), - TransactionData: base64.StdEncoding.EncodeToString(result.TransactionData), - MinResourceFee: result.MinFee, - Cost: SimulateTransactionCost{ - CPUInstructions: result.CPUInstructions, - MemoryBytes: result.MemoryBytes, - }, - LatestLedger: latestLedger, - RestorePreamble: restorePreamble, - } - }) -} - -func base64EncodeSlice(in [][]byte) []string { - result := make([]string, len(in)) - for i, v := range in { - result[i] = base64.StdEncoding.EncodeToString(v) - } - return result -} - -func getBucketListSize(ctx context.Context, ledgerReader db.LedgerReader, latestLedger uint32) (uint64, error) { - // obtain bucket size - var closeMeta, ok, err = ledgerReader.GetLedger(ctx, latestLedger) - if err != nil { - return 0, err - } - if !ok { - return 0, fmt.Errorf("missing meta for latest ledger (%d)", latestLedger) - } - if closeMeta.V != 1 { - return 0, fmt.Errorf("latest ledger (%d) meta has unexpected verion (%d)", latestLedger, closeMeta.V) - } - return uint64(closeMeta.V1.TotalByteSizeOfBucketList), nil -} diff --git a/cmd/soroban-rpc/internal/network/backlogQ.go b/cmd/soroban-rpc/internal/network/backlogQ.go deleted file mode 100644 index 6a8f691bd..000000000 --- a/cmd/soroban-rpc/internal/network/backlogQ.go +++ /dev/null @@ -1,128 +0,0 @@ -package network - -import ( - "context" - "net/http" - "sync/atomic" - - "github.com/creachadair/jrpc2" - "github.com/stellar/go/support/errors" - "github.com/stellar/go/support/log" -) - -const RequestBacklogQueueNoLimit = maxUint - -// The gauge is a subset of prometheus.Gauge, and it allows us to mock the -// gauge usage for testing purposes without requiring the implementation of the true -// prometheus.Gauge. -type gauge interface { - Inc() - Dec() -} - -type backlogQLimiter struct { - limit uint64 - pending uint64 - gauge gauge - limitReached uint64 - logger *log.Entry -} - -type backlogHTTPQLimiter struct { - httpDownstreamHandler http.Handler - backlogQLimiter -} - -func MakeHTTPBacklogQueueLimiter(downstream http.Handler, gauge gauge, limit uint64, logger *log.Entry) *backlogHTTPQLimiter { - return &backlogHTTPQLimiter{ - httpDownstreamHandler: downstream, - backlogQLimiter: backlogQLimiter{ - limit: limit, - gauge: gauge, - logger: logger, - }, - } -} - -type backlogJrpcQLimiter struct { - jrpcDownstreamHandler jrpc2.Handler - backlogQLimiter -} - -func MakeJrpcBacklogQueueLimiter(downstream jrpc2.Handler, gauge gauge, limit uint64, logger *log.Entry) *backlogJrpcQLimiter { - return &backlogJrpcQLimiter{ - jrpcDownstreamHandler: downstream, - backlogQLimiter: backlogQLimiter{ - limit: limit, - gauge: gauge, - logger: logger, - }, - } -} - -func (q *backlogHTTPQLimiter) ServeHTTP(res http.ResponseWriter, req *http.Request) { - if q.limit == RequestBacklogQueueNoLimit { - // if specified max duration, pass-through - q.httpDownstreamHandler.ServeHTTP(res, req) - return - } - if newPending := atomic.AddUint64(&q.pending, 1); newPending > q.limit { - // we've reached our queue limit - let the caller know we're too busy. - atomic.AddUint64(&q.pending, ^uint64(0)) - res.WriteHeader(http.StatusServiceUnavailable) - if atomic.CompareAndSwapUint64(&q.limitReached, 0, 1) { - // if the limit was reached, log a message. - if q.logger != nil { - q.logger.Infof("Backlog queue limiter reached the queue limit of %d executing concurrent http requests.", q.limit) - } - } - return - } else { - if q.gauge != nil { - q.gauge.Inc() - } - } - defer func() { - - atomic.AddUint64(&q.pending, ^uint64(0)) - if q.gauge != nil { - q.gauge.Dec() - } - atomic.StoreUint64(&q.limitReached, 0) - }() - - q.httpDownstreamHandler.ServeHTTP(res, req) -} - -func (q *backlogJrpcQLimiter) Handle(ctx context.Context, req *jrpc2.Request) (interface{}, error) { - if q.limit == RequestBacklogQueueNoLimit { - // if specified max duration, pass-through - return q.jrpcDownstreamHandler(ctx, req) - } - - if newPending := atomic.AddUint64(&q.pending, 1); newPending > q.limit { - // we've reached our queue limit - let the caller know we're too busy. - atomic.AddUint64(&q.pending, ^uint64(0)) - if atomic.CompareAndSwapUint64(&q.limitReached, 0, 1) { - // if the limit was reached, log a message. - if q.logger != nil { - q.logger.Infof("Backlog queue limiter reached the queue limit of %d executing concurrent rpc %s requests.", q.limit, req.Method()) - } - } - return nil, errors.Errorf("rpc queue for %s surpassed queue limit of %d requests", req.Method(), q.limit) - } else { - if q.gauge != nil { - q.gauge.Inc() - } - } - - defer func() { - atomic.AddUint64(&q.pending, ^uint64(0)) - if q.gauge != nil { - q.gauge.Dec() - } - atomic.StoreUint64(&q.limitReached, 0) - }() - - return q.jrpcDownstreamHandler(ctx, req) -} diff --git a/cmd/soroban-rpc/internal/network/backlogQ_test.go b/cmd/soroban-rpc/internal/network/backlogQ_test.go deleted file mode 100644 index 3fb059593..000000000 --- a/cmd/soroban-rpc/internal/network/backlogQ_test.go +++ /dev/null @@ -1,237 +0,0 @@ -package network - -import ( - "context" - "math/rand" - "net/http" - "sync" - "sync/atomic" - "testing" - - "github.com/creachadair/jrpc2" - "github.com/stretchr/testify/require" -) - -type TestingHandlerWrapper struct { - f func(http.ResponseWriter, *http.Request) -} - -func (t *TestingHandlerWrapper) ServeHTTP(res http.ResponseWriter, req *http.Request) { - t.f(res, req) -} - -type TestingJrpcHandlerWrapper struct { - f func(context.Context, *jrpc2.Request) (interface{}, error) -} - -func (t *TestingJrpcHandlerWrapper) Handle(ctx context.Context, req *jrpc2.Request) (interface{}, error) { - return t.f(ctx, req) -} - -// The goal of the TestBacklogQueueLimiter_HttpNonBlocking is to try -// and enquque load against the queue limiter, without hitting the -// limit. All request should pass through. -func TestBacklogQueueLimiter_HttpNonBlocking(t *testing.T) { - var sum uint64 - var wg sync.WaitGroup - requestsSizeLimit := uint64(1000) - adding := &TestingHandlerWrapper{f: func(res http.ResponseWriter, req *http.Request) { - atomic.AddUint64(&sum, 1) - }} - - logCounter := makeTestLogCounter() - testGauge := &TestingGauge{} - limiter := MakeHTTPBacklogQueueLimiter(adding, testGauge, requestsSizeLimit, logCounter.Entry()) - for i := 1; i < 50; i++ { - n := rand.Int63n(int64(requestsSizeLimit)) //nolint:gosec - require.Zero(t, int(testGauge.count)) - wg.Add(int(n)) - for k := n; k > 0; k-- { - go func() { - limiter.ServeHTTP(nil, nil) - wg.Done() - }() - } - wg.Wait() - require.Equal(t, uint64(n), sum) - require.Zero(t, int(testGauge.count)) - sum = 0 - } - require.Equal(t, [7]int{0, 0, 0, 0, 0, 0, 0}, logCounter.writtenLogEntries) -} - -// The goal of the TestBacklogQueueLimiter_HttpNonBlocking is to try -// and enquque load against the queue limiter, without hitting the -// limit. All request should pass through. -func TestBacklogQueueLimiter_JrpcNonBlocking(t *testing.T) { - var sum uint64 - var wg sync.WaitGroup - requestsSizeLimit := uint64(1000) - adding := &TestingJrpcHandlerWrapper{f: func(context.Context, *jrpc2.Request) (interface{}, error) { - atomic.AddUint64(&sum, 1) - return nil, nil - }} - logCounter := makeTestLogCounter() - testGauge := &TestingGauge{} - limiter := MakeJrpcBacklogQueueLimiter(adding.Handle, testGauge, requestsSizeLimit, logCounter.Entry()) - for i := 1; i < 50; i++ { - n := rand.Int63n(int64(requestsSizeLimit)) //nolint:gosec - require.Zero(t, int(testGauge.count)) - wg.Add(int(n)) - for k := n; k > 0; k-- { - go func() { - _, err := limiter.Handle(context.Background(), nil) - require.Nil(t, err) - wg.Done() - }() - } - wg.Wait() - require.Zero(t, int(testGauge.count)) - require.Equal(t, uint64(n), sum) - sum = 0 - } - require.Equal(t, [7]int{0, 0, 0, 0, 0, 0, 0}, logCounter.writtenLogEntries) -} - -// The goal of the TestBacklogQueueLimiter_HttpBlocking is to set -// up a queue that already reached it's limit and see that -// additional requests are being rejected. Then, unblock the queue -// and see that requests could go though. -func TestBacklogQueueLimiter_HttpBlocking(t *testing.T) { - for _, queueSize := range []uint64{7, 50, 80} { - blockedCh := make(chan interface{}) - var initialGroupBlocking sync.WaitGroup - initialGroupBlocking.Add(int(queueSize) / 2) - blockedHandlers := &TestingHandlerWrapper{f: func(res http.ResponseWriter, req *http.Request) { - initialGroupBlocking.Done() - <-blockedCh - }} - logCounter := makeTestLogCounter() - testGauge := &TestingGauge{} - limiter := MakeHTTPBacklogQueueLimiter(blockedHandlers, testGauge, queueSize, logCounter.Entry()) - for i := uint64(0); i < queueSize/2; i++ { - go func() { - limiter.ServeHTTP(nil, nil) - initialGroupBlocking.Done() - }() - } - - initialGroupBlocking.Wait() - require.Equal(t, int(queueSize)/2, int(testGauge.count)) - - var secondBlockingGroupWg sync.WaitGroup - secondBlockingGroupWg.Add(int(queueSize) - int(queueSize)/2) - secondBlockingGroupWgCh := make(chan interface{}) - secondBlockingGroupWgHandlers := &TestingHandlerWrapper{f: func(res http.ResponseWriter, req *http.Request) { - secondBlockingGroupWg.Done() - <-secondBlockingGroupWgCh - }} - - limiter.httpDownstreamHandler = secondBlockingGroupWgHandlers - for i := queueSize / 2; i < queueSize; i++ { - go func() { - limiter.ServeHTTP(nil, nil) - secondBlockingGroupWg.Done() - }() - } - - secondBlockingGroupWg.Wait() - require.Equal(t, [7]int{0, 0, 0, 0, 0, 0, 0}, logCounter.writtenLogEntries) - require.Equal(t, int(queueSize), int(testGauge.count)) - // now, try to place additional entry - which should be blocked. - var res TestingResponseWriter - limiter.ServeHTTP(&res, nil) - require.Equal(t, http.StatusServiceUnavailable, res.statusCode) - require.Equal(t, [7]int{0, 0, 0, 0, 1, 0, 0}, logCounter.writtenLogEntries) - require.Equal(t, int(queueSize), int(testGauge.count)) - - secondBlockingGroupWg.Add(int(queueSize) - int(queueSize)/2) - // unblock the second group. - close(secondBlockingGroupWgCh) - secondBlockingGroupWg.Wait() - require.Equal(t, int(queueSize)/2, int(testGauge.count)) - - // see that we have no blocking - res = TestingResponseWriter{} - require.Equal(t, 0, res.statusCode) - - // unblock the first group. - initialGroupBlocking.Add(int(queueSize) / 2) - close(blockedCh) - initialGroupBlocking.Wait() - require.Equal(t, [7]int{0, 0, 0, 0, 1, 0, 0}, logCounter.writtenLogEntries) - require.Zero(t, int(testGauge.count)) - } -} - -// The goal of the TestBacklogQueueLimiter_JrpcBlocking is to set -// up a queue that already reached it's limit and see that -// additional requests are being rejected. Then, unblock the queue -// and see that requests could go though. -func TestBacklogQueueLimiter_JrpcBlocking(t *testing.T) { - for _, queueSize := range []uint64{7, 50, 80} { - blockedCh := make(chan interface{}) - var initialGroupBlocking sync.WaitGroup - initialGroupBlocking.Add(int(queueSize) / 2) - blockedHandlers := &TestingJrpcHandlerWrapper{f: func(context.Context, *jrpc2.Request) (interface{}, error) { - initialGroupBlocking.Done() - <-blockedCh - return nil, nil - }} - logCounter := makeTestLogCounter() - testGauge := &TestingGauge{} - limiter := MakeJrpcBacklogQueueLimiter(blockedHandlers.Handle, testGauge, queueSize, logCounter.Entry()) - for i := uint64(0); i < queueSize/2; i++ { - go func() { - _, err := limiter.Handle(context.Background(), &jrpc2.Request{}) - require.Nil(t, err) - initialGroupBlocking.Done() - }() - } - initialGroupBlocking.Wait() - require.Equal(t, int(queueSize)/2, int(testGauge.count)) - - var secondBlockingGroupWg sync.WaitGroup - secondBlockingGroupWg.Add(int(queueSize) - int(queueSize)/2) - secondBlockingGroupWgCh := make(chan interface{}) - secondBlockingGroupWgHandlers := &TestingJrpcHandlerWrapper{f: func(context.Context, *jrpc2.Request) (interface{}, error) { - secondBlockingGroupWg.Done() - <-secondBlockingGroupWgCh - return nil, nil - }} - - limiter.jrpcDownstreamHandler = secondBlockingGroupWgHandlers.Handle - for i := queueSize / 2; i < queueSize; i++ { - go func() { - _, err := limiter.Handle(context.Background(), &jrpc2.Request{}) - require.Nil(t, err) - secondBlockingGroupWg.Done() - }() - } - secondBlockingGroupWg.Wait() - require.Equal(t, [7]int{0, 0, 0, 0, 0, 0, 0}, logCounter.writtenLogEntries) - require.Equal(t, int(queueSize), int(testGauge.count)) - // now, try to place additional entry - which should be blocked. - var res TestingResponseWriter - _, err := limiter.Handle(context.Background(), &jrpc2.Request{}) - require.NotNil(t, err) - require.Equal(t, [7]int{0, 0, 0, 0, 1, 0, 0}, logCounter.writtenLogEntries) - - secondBlockingGroupWg.Add(int(queueSize) - int(queueSize)/2) - // unblock the second group. - close(secondBlockingGroupWgCh) - secondBlockingGroupWg.Wait() - require.Equal(t, int(queueSize)/2, int(testGauge.count)) - - // see that we have no blocking - res = TestingResponseWriter{} - require.Equal(t, 0, res.statusCode) - - // unblock the first group. - initialGroupBlocking.Add(int(queueSize) / 2) - close(blockedCh) - initialGroupBlocking.Wait() - require.Equal(t, [7]int{0, 0, 0, 0, 1, 0, 0}, logCounter.writtenLogEntries) - require.Zero(t, int(testGauge.count)) - } -} diff --git a/cmd/soroban-rpc/internal/network/requestdurationlimiter.go b/cmd/soroban-rpc/internal/network/requestdurationlimiter.go deleted file mode 100644 index 05204591f..000000000 --- a/cmd/soroban-rpc/internal/network/requestdurationlimiter.go +++ /dev/null @@ -1,304 +0,0 @@ -package network - -import ( - "context" - "net/http" - "reflect" - "runtime" - "time" - - "github.com/creachadair/jrpc2" - "github.com/stellar/go/support/log" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/util" -) - -const maxUint = ^uint64(0) //18446744073709551615 -const maxInt = int64(maxUint >> 1) // 9223372036854775807 -const maxDuration = time.Duration(maxInt) - -const RequestDurationLimiterNoLimit = maxDuration - -// The increasingCounter is a subset of prometheus.Counter, and it allows us to mock the -// counter usage for testing purposes without requiring the implementation of the true -// prometheus.Counter. -type increasingCounter interface { - // Inc increments the counter by 1. Use Add to increment it by arbitrary - // non-negative values. - Inc() -} - -type requestDurationLimiter struct { - warningThreshold time.Duration - limitThreshold time.Duration - logger *log.Entry - warningCounter increasingCounter - limitCounter increasingCounter -} - -type httpRequestDurationLimiter struct { - httpDownstreamHandler http.Handler - requestDurationLimiter -} - -func MakeHTTPRequestDurationLimiter( - downstream http.Handler, - warningThreshold time.Duration, - limitThreshold time.Duration, - warningCounter increasingCounter, - limitCounter increasingCounter, - logger *log.Entry) *httpRequestDurationLimiter { - // make sure the warning threshold is less then the limit threshold; otherwise, just set it to the limit threshold. - if warningThreshold > limitThreshold { - warningThreshold = limitThreshold - } - return &httpRequestDurationLimiter{ - httpDownstreamHandler: downstream, - requestDurationLimiter: requestDurationLimiter{ - warningThreshold: warningThreshold, - limitThreshold: limitThreshold, - logger: logger, - warningCounter: warningCounter, - limitCounter: limitCounter, - }, - } -} - -type bufferedResponseWriter struct { - header http.Header - buffer []byte - statusCode int -} - -func makeBufferedResponseWriter(rw http.ResponseWriter) *bufferedResponseWriter { - header := rw.Header() - bw := &bufferedResponseWriter{ - header: make(http.Header, 0), - } - for k, v := range header { - bw.header[k] = v - } - return bw -} - -func (w *bufferedResponseWriter) Header() http.Header { - return w.header -} -func (w *bufferedResponseWriter) Write(buf []byte) (int, error) { - w.buffer = append(w.buffer, buf...) - return len(buf), nil -} -func (w *bufferedResponseWriter) WriteHeader(statusCode int) { - w.statusCode = statusCode -} - -func (w *bufferedResponseWriter) WriteOut(ctx context.Context, rw http.ResponseWriter) { - // update the headers map. - headers := rw.Header() - for k := range headers { - delete(headers, k) - } - for k, v := range w.header { - headers[k] = v - } - - if len(w.buffer) == 0 { - if w.statusCode != 0 { - rw.WriteHeader(w.statusCode) - } - return - } - if w.statusCode != 0 { - rw.WriteHeader(w.statusCode) - } - - if ctx.Err() == nil { - // the following return size/error won't help us much at this point. The request is already finalized. - rw.Write(w.buffer) //nolint:errcheck - } -} - -func (q *httpRequestDurationLimiter) ServeHTTP(res http.ResponseWriter, req *http.Request) { - if q.limitThreshold == RequestDurationLimiterNoLimit { - // if specified max duration, pass-through - q.httpDownstreamHandler.ServeHTTP(res, req) - return - } - var warningCh <-chan time.Time - if q.warningThreshold != time.Duration(0) && q.warningThreshold < q.limitThreshold { - warningCh = time.NewTimer(q.warningThreshold).C - } - var limitCh <-chan time.Time - if q.limitThreshold != time.Duration(0) { - limitCh = time.NewTimer(q.limitThreshold).C - } - requestCompleted := make(chan []string, 1) - requestCtx, requestCtxCancel := context.WithTimeout(req.Context(), q.limitThreshold) - defer requestCtxCancel() - timeLimitedRequest := req.WithContext(requestCtx) - responseBuffer := makeBufferedResponseWriter(res) - go func() { - defer func() { - if err := recover(); err != nil { - functionName := runtime.FuncForPC(reflect.ValueOf(q.httpDownstreamHandler.ServeHTTP).Pointer()).Name() - callStack := util.CallStack(err, functionName, "(*httpRequestDurationLimiter).ServeHTTP.func1()", 8) - requestCompleted <- callStack - } else { - close(requestCompleted) - } - }() - q.httpDownstreamHandler.ServeHTTP(responseBuffer, timeLimitedRequest) - }() - - warn := false - for { - select { - case <-warningCh: - // warn - warn = true - case <-limitCh: - // limit - requestCtxCancel() - if q.limitCounter != nil { - q.limitCounter.Inc() - } - if q.logger != nil { - q.logger.Infof("Request processing for %s exceed limiting threshold of %v", req.URL.Path, q.limitThreshold) - } - if req.Context().Err() == nil { - res.WriteHeader(http.StatusGatewayTimeout) - } - return - case errStrings := <-requestCompleted: - if warn { - if q.warningCounter != nil { - q.warningCounter.Inc() - } - if q.logger != nil { - q.logger.Infof("Request processing for %s exceed warning threshold of %v", req.URL.Path, q.warningThreshold) - } - } - if len(errStrings) == 0 { - responseBuffer.WriteOut(req.Context(), res) - } else { - res.WriteHeader(http.StatusInternalServerError) - for _, errStr := range errStrings { - if q.logger != nil { - q.logger.Warn(errStr) - } - } - } - return - } - } -} - -type rpcRequestDurationLimiter struct { - jrpcDownstreamHandler jrpc2.Handler - requestDurationLimiter -} - -func MakeJrpcRequestDurationLimiter( - downstream jrpc2.Handler, - warningThreshold time.Duration, - limitThreshold time.Duration, - warningCounter increasingCounter, - limitCounter increasingCounter, - logger *log.Entry) *rpcRequestDurationLimiter { - // make sure the warning threshold is less then the limit threshold; otherwise, just set it to the limit threshold. - if warningThreshold > limitThreshold { - warningThreshold = limitThreshold - } - - return &rpcRequestDurationLimiter{ - jrpcDownstreamHandler: downstream, - requestDurationLimiter: requestDurationLimiter{ - warningThreshold: warningThreshold, - limitThreshold: limitThreshold, - logger: logger, - warningCounter: warningCounter, - limitCounter: limitCounter, - }, - } -} - -func (q *rpcRequestDurationLimiter) Handle(ctx context.Context, req *jrpc2.Request) (interface{}, error) { - if q.limitThreshold == RequestDurationLimiterNoLimit { - // if specified max duration, pass-through - return q.jrpcDownstreamHandler(ctx, req) - } - var warningCh <-chan time.Time - if q.warningThreshold != time.Duration(0) && q.warningThreshold < q.limitThreshold { - warningCh = time.NewTimer(q.warningThreshold).C - } - var limitCh <-chan time.Time - if q.limitThreshold != time.Duration(0) { - limitCh = time.NewTimer(q.limitThreshold).C - } - type requestResultOutput struct { - data interface{} - err error - } - requestCompleted := make(chan requestResultOutput, 1) - requestCtx, requestCtxCancel := context.WithTimeout(ctx, q.limitThreshold) - defer requestCtxCancel() - - go func() { - defer func() { - if err := recover(); err != nil { - q.logger.Errorf("Request for method %s resulted in an error : %v", req.Method(), err) - } - close(requestCompleted) - }() - var res requestResultOutput - res.data, res.err = q.jrpcDownstreamHandler(requestCtx, req) - requestCompleted <- res - }() - - warn := false - for { - select { - case <-warningCh: - // warn - warn = true - case <-limitCh: - // limit - requestCtxCancel() - if q.limitCounter != nil { - q.limitCounter.Inc() - } - if q.logger != nil { - q.logger.Infof("Request processing for %s exceed limiting threshold of %v", req.Method(), q.limitThreshold) - } - if ctxErr := ctx.Err(); ctxErr == nil { - return nil, ErrRequestExceededProcessingLimitThreshold - } else { - return nil, ctxErr - } - case requestRes, ok := <-requestCompleted: - if warn { - if q.warningCounter != nil { - q.warningCounter.Inc() - } - if q.logger != nil { - q.logger.Infof("Request processing for %s exceed warning threshold of %v", req.Method(), q.warningThreshold) - } - } - if ok { - return requestRes.data, requestRes.err - } else { - // request panicked ? - return nil, ErrFailToProcessDueToInternalIssue - } - } - } -} - -var ErrRequestExceededProcessingLimitThreshold = jrpc2.Error{ - Code: -32001, - Message: "request exceeded processing limit threshold", -} - -var ErrFailToProcessDueToInternalIssue = jrpc2.Error{ - Code: -32003, // internal error - Message: "request failed to process due to internal issue", -} diff --git a/cmd/soroban-rpc/internal/network/requestdurationlimiter_test.go b/cmd/soroban-rpc/internal/network/requestdurationlimiter_test.go deleted file mode 100644 index 5be64cead..000000000 --- a/cmd/soroban-rpc/internal/network/requestdurationlimiter_test.go +++ /dev/null @@ -1,339 +0,0 @@ -package network - -import ( - "context" - "io" - "net" - "net/http" - "testing" - "time" - - "github.com/stretchr/testify/require" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/handler" - "github.com/creachadair/jrpc2/jhttp" -) - -type TestServerHandlerWrapper struct { - f func(http.ResponseWriter, *http.Request) -} - -func (h *TestServerHandlerWrapper) ServeHTTP(res http.ResponseWriter, req *http.Request) { - h.f(res, req) -} - -func createTestServer() (serverAddr string, redirector *TestServerHandlerWrapper, shutdown context.CancelFunc) { - ipAddr, _ := net.ResolveTCPAddr("tcp", "127.0.0.1:0") - listener, _ := net.ListenTCP("tcp", ipAddr) - handlerRedirector := &TestServerHandlerWrapper{} - server := http.Server{ - Handler: handlerRedirector, - ReadHeaderTimeout: 10 * time.Second, - } - - serverDown := make(chan error) - go func() { - serverDown <- server.Serve(listener) - }() - - return listener.Addr().String(), handlerRedirector, func() { - server.Shutdown(context.Background()) //nolint:errcheck - <-serverDown - } -} - -func TestHTTPRequestDurationLimiter_Limiting(t *testing.T) { - addr, redirector, shutdown := createTestServer() - longExecutingHandler := &TestServerHandlerWrapper{ - f: func(res http.ResponseWriter, req *http.Request) { - select { - case <-req.Context().Done(): - return - case <-time.After(time.Second * 10): - } - n, err := res.Write([]byte{1, 2, 3}) - require.Equal(t, 3, n) - require.Nil(t, err) - }, - } - warningCounter := TestingCounter{} - limitCounter := TestingCounter{} - logCounter := makeTestLogCounter() - redirector.f = MakeHTTPRequestDurationLimiter( - longExecutingHandler, - time.Second/20, - time.Second/10, - &warningCounter, - &limitCounter, - logCounter.Entry()).ServeHTTP - - client := http.Client{} - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://"+addr+"/", nil) - require.NoError(t, err) - resp, err := client.Do(req) - require.NoError(t, err) - bytes, err := io.ReadAll(resp.Body) - require.NoError(t, resp.Body.Close()) - require.NoError(t, err) - require.Equal(t, []byte{}, bytes) - require.Equal(t, resp.StatusCode, http.StatusGatewayTimeout) - require.Zero(t, warningCounter.count) - require.Equal(t, int64(1), limitCounter.count) - require.Equal(t, [7]int{0, 0, 0, 0, 1, 0, 0}, logCounter.writtenLogEntries) - shutdown() -} - -func TestHTTPRequestDurationLimiter_NoLimiting(t *testing.T) { - addr, redirector, shutdown := createTestServer() - longExecutingHandler := &TestServerHandlerWrapper{ - f: func(res http.ResponseWriter, req *http.Request) { - select { - case <-req.Context().Done(): - return - case <-time.After(time.Second / 10): - } - n, err := res.Write([]byte{1, 2, 3}) - require.Equal(t, 3, n) - require.Nil(t, err) - }, - } - warningCounter := TestingCounter{} - limitCounter := TestingCounter{} - logCounter := makeTestLogCounter() - redirector.f = MakeHTTPRequestDurationLimiter( - longExecutingHandler, - time.Second*5, - time.Second*10, - &warningCounter, - &limitCounter, - logCounter.Entry()).ServeHTTP - - client := http.Client{} - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://"+addr+"/", nil) - require.NoError(t, err) - resp, err := client.Do(req) - require.NoError(t, err) - bytes, err := io.ReadAll(resp.Body) - require.NoError(t, resp.Body.Close()) - require.NoError(t, err) - require.Equal(t, []byte{1, 2, 3}, bytes) - require.Equal(t, resp.StatusCode, http.StatusOK) - require.Zero(t, warningCounter.count) - require.Zero(t, limitCounter.count) - require.Equal(t, [7]int{0, 0, 0, 0, 0, 0, 0}, logCounter.writtenLogEntries) - shutdown() -} - -func TestHTTPRequestDurationLimiter_NoLimiting_Warn(t *testing.T) { - addr, redirector, shutdown := createTestServer() - longExecutingHandler := &TestServerHandlerWrapper{ - f: func(res http.ResponseWriter, req *http.Request) { - select { - case <-req.Context().Done(): - return - case <-time.After(time.Second / 5): - } - n, err := res.Write([]byte{1, 2, 3}) - require.Equal(t, 3, n) - require.Nil(t, err) - }, - } - warningCounter := TestingCounter{} - limitCounter := TestingCounter{} - logCounter := makeTestLogCounter() - redirector.f = MakeHTTPRequestDurationLimiter( - longExecutingHandler, - time.Second/10, - time.Second*10, - &warningCounter, - &limitCounter, - logCounter.Entry()).ServeHTTP - - client := http.Client{} - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://"+addr+"/", nil) - require.NoError(t, err) - resp, err := client.Do(req) - require.NoError(t, err) - bytes, err := io.ReadAll(resp.Body) - require.NoError(t, resp.Body.Close()) - require.NoError(t, err) - require.Equal(t, []byte{1, 2, 3}, bytes) - require.Equal(t, resp.StatusCode, http.StatusOK) - require.Equal(t, int64(1), warningCounter.count) - require.Zero(t, limitCounter.count) - require.Equal(t, [7]int{0, 0, 0, 0, 1, 0, 0}, logCounter.writtenLogEntries) - shutdown() -} - -type JRPCHandlerFunc func(ctx context.Context, r *jrpc2.Request) (interface{}, error) - -func bindRPCHoist(redirector *TestServerHandlerWrapper) *JRPCHandlerFunc { - var hoistFunction JRPCHandlerFunc - - bridgeMap := handler.Map{ - "method": handler.New(func(ctx context.Context, r *jrpc2.Request) (interface{}, error) { - return hoistFunction(ctx, r) - }), - } - - redirector.f = jhttp.NewBridge(bridgeMap, &jhttp.BridgeOptions{}).ServeHTTP - return &hoistFunction -} - -func TestJRPCRequestDurationLimiter_Limiting(t *testing.T) { - addr, redirector, shutdown := createTestServer() - hoistFunction := bindRPCHoist(redirector) - - longExecutingHandler := handler.New(func(ctx context.Context, r *jrpc2.Request) (interface{}, error) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-time.After(time.Second * 10): - } - return "", nil - }) - - warningCounter := TestingCounter{} - limitCounter := TestingCounter{} - logCounter := makeTestLogCounter() - *hoistFunction = MakeJrpcRequestDurationLimiter( - longExecutingHandler, - time.Second/20, - time.Second/10, - &warningCounter, - &limitCounter, - logCounter.Entry()).Handle - - ch := jhttp.NewChannel("http://"+addr+"/", nil) - client := jrpc2.NewClient(ch, nil) - - var res interface{} - req := struct { - i int - }{1} - err := client.CallResult(context.Background(), "method", req, &res) - require.NotNil(t, err) - jrpcError, ok := err.(*jrpc2.Error) - require.True(t, ok) - require.Equal(t, ErrRequestExceededProcessingLimitThreshold.Code, jrpcError.Code) - require.Equal(t, nil, res) - require.Zero(t, warningCounter.count) - require.Equal(t, int64(1), limitCounter.count) - require.Equal(t, [7]int{0, 0, 0, 0, 1, 0, 0}, logCounter.writtenLogEntries) - shutdown() -} - -func TestJRPCRequestDurationLimiter_NoLimiting(t *testing.T) { - addr, redirector, shutdown := createTestServer() - hoistFunction := bindRPCHoist(redirector) - - returnString := "ok" - longExecutingHandler := handler.New(func(ctx context.Context, r *jrpc2.Request) (interface{}, error) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-time.After(time.Second / 10): - } - return returnString, nil - }) - - warningCounter := TestingCounter{} - limitCounter := TestingCounter{} - logCounter := makeTestLogCounter() - *hoistFunction = MakeJrpcRequestDurationLimiter( - longExecutingHandler, - time.Second*5, - time.Second*10, - &warningCounter, - &limitCounter, - logCounter.Entry()).Handle - - ch := jhttp.NewChannel("http://"+addr+"/", nil) - client := jrpc2.NewClient(ch, nil) - - var res interface{} - req := struct { - i int - }{1} - err := client.CallResult(context.Background(), "method", req, &res) - require.Nil(t, err) - require.Equal(t, returnString, res) - require.Zero(t, warningCounter.count) - require.Zero(t, limitCounter.count) - require.Equal(t, [7]int{0, 0, 0, 0, 0, 0, 0}, logCounter.writtenLogEntries) - shutdown() -} - -func TestJRPCRequestDurationLimiter_NoLimiting_Warn(t *testing.T) { - addr, redirector, shutdown := createTestServer() - hoistFunction := bindRPCHoist(redirector) - - returnString := "ok" - longExecutingHandler := handler.New(func(ctx context.Context, r *jrpc2.Request) (interface{}, error) { - select { - case <-ctx.Done(): - return nil, ctx.Err() - case <-time.After(time.Second / 5): - } - return returnString, nil - }) - - warningCounter := TestingCounter{} - limitCounter := TestingCounter{} - logCounter := makeTestLogCounter() - *hoistFunction = MakeJrpcRequestDurationLimiter( - longExecutingHandler, - time.Second/10, - time.Second*10, - &warningCounter, - &limitCounter, - logCounter.Entry()).Handle - - ch := jhttp.NewChannel("http://"+addr+"/", nil) - client := jrpc2.NewClient(ch, nil) - - var res interface{} - req := struct { - i int - }{1} - err := client.CallResult(context.Background(), "method", req, &res) - require.Nil(t, err) - require.Equal(t, returnString, res) - require.Equal(t, int64(1), warningCounter.count) - require.Zero(t, limitCounter.count) - require.Equal(t, [7]int{0, 0, 0, 0, 1, 0, 0}, logCounter.writtenLogEntries) - shutdown() -} - -func TestHTTPRequestDurationLimiter_Panicing(t *testing.T) { - addr, redirector, shutdown := createTestServer() - longExecutingHandler := &TestServerHandlerWrapper{ - f: func(res http.ResponseWriter, req *http.Request) { - var panicWrite *int - *panicWrite = 1 - }, - } - - logCounter := makeTestLogCounter() - redirector.f = MakeHTTPRequestDurationLimiter( - longExecutingHandler, - time.Second*10, - time.Second*10, - nil, - nil, - logCounter.Entry()).ServeHTTP - - client := http.Client{} - req, err := http.NewRequestWithContext(context.Background(), http.MethodGet, "http://"+addr+"/", nil) - require.NoError(t, err) - resp, err := client.Do(req) - require.NoError(t, err) - bytes, err := io.ReadAll(resp.Body) - require.NoError(t, err) - require.NoError(t, resp.Body.Close()) - require.Equal(t, http.StatusInternalServerError, resp.StatusCode) - require.Equal(t, []byte{}, bytes) - require.Equal(t, [7]int{0, 0, 0, 7, 0, 0, 0}, logCounter.writtenLogEntries) - shutdown() -} diff --git a/cmd/soroban-rpc/internal/network/utils_test.go b/cmd/soroban-rpc/internal/network/utils_test.go deleted file mode 100644 index 8109a63d4..000000000 --- a/cmd/soroban-rpc/internal/network/utils_test.go +++ /dev/null @@ -1,68 +0,0 @@ -package network - -import ( - "net/http" - "sync/atomic" - - "github.com/sirupsen/logrus" - "github.com/stellar/go/support/log" -) - -type TestingCounter struct { - count int64 -} - -func (tc *TestingCounter) Inc() { - atomic.AddInt64(&tc.count, 1) -} - -type TestingGauge struct { - count int64 -} - -func (tg *TestingGauge) Inc() { - atomic.AddInt64(&tg.count, 1) -} - -func (tg *TestingGauge) Dec() { - atomic.AddInt64(&tg.count, -1) -} - -type TestLogsCounter struct { - entry *log.Entry - writtenLogEntries [logrus.TraceLevel + 1]int -} - -func makeTestLogCounter() *TestLogsCounter { - out := &TestLogsCounter{ - entry: log.New(), - } - out.entry.AddHook(out) - out.entry.SetLevel(logrus.DebugLevel) - return out -} -func (te *TestLogsCounter) Entry() *log.Entry { - return te.entry -} -func (te *TestLogsCounter) Levels() []logrus.Level { - return []logrus.Level{logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel, logrus.WarnLevel, logrus.InfoLevel, logrus.DebugLevel, logrus.TraceLevel} -} -func (te *TestLogsCounter) Fire(e *logrus.Entry) error { - te.writtenLogEntries[e.Level]++ - return nil -} - -type TestingResponseWriter struct { - statusCode int -} - -func (t *TestingResponseWriter) Header() http.Header { - return http.Header{} -} -func (t *TestingResponseWriter) Write([]byte) (int, error) { - return 0, nil -} - -func (t *TestingResponseWriter) WriteHeader(statusCode int) { - t.statusCode = statusCode -} diff --git a/cmd/soroban-rpc/internal/preflight/pool.go b/cmd/soroban-rpc/internal/preflight/pool.go deleted file mode 100644 index 1d1824115..000000000 --- a/cmd/soroban-rpc/internal/preflight/pool.go +++ /dev/null @@ -1,181 +0,0 @@ -package preflight - -import ( - "context" - "errors" - "sync" - "sync/atomic" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" -) - -type workerResult struct { - preflight Preflight - err error -} - -type workerRequest struct { - ctx context.Context - params PreflightParameters - resultChan chan<- workerResult -} - -type PreflightWorkerPool struct { - ledgerEntryReader db.LedgerEntryReader - networkPassphrase string - enableDebug bool - logger *log.Entry - isClosed atomic.Bool - requestChan chan workerRequest - concurrentRequestsMetric prometheus.Gauge - errorFullCounter prometheus.Counter - durationMetric *prometheus.SummaryVec - ledgerEntriesFetchedMetric prometheus.Summary - wg sync.WaitGroup -} - -func NewPreflightWorkerPool(daemon interfaces.Daemon, workerCount uint, jobQueueCapacity uint, enableDebug bool, ledgerEntryReader db.LedgerEntryReader, networkPassphrase string, logger *log.Entry) *PreflightWorkerPool { - preflightWP := PreflightWorkerPool{ - ledgerEntryReader: ledgerEntryReader, - networkPassphrase: networkPassphrase, - enableDebug: enableDebug, - logger: logger, - requestChan: make(chan workerRequest, jobQueueCapacity), - } - requestQueueMetric := prometheus.NewGaugeFunc(prometheus.GaugeOpts{ - Namespace: daemon.MetricsNamespace(), - Subsystem: "preflight_pool", - Name: "queue_length", - Help: "number of preflight requests in the queue", - }, func() float64 { - return float64(len(preflightWP.requestChan)) - }) - preflightWP.concurrentRequestsMetric = prometheus.NewGauge(prometheus.GaugeOpts{ - Namespace: daemon.MetricsNamespace(), - Subsystem: "preflight_pool", - Name: "concurrent_requests", - Help: "number of preflight requests currently running", - }) - preflightWP.errorFullCounter = prometheus.NewCounter(prometheus.CounterOpts{ - Namespace: daemon.MetricsNamespace(), - Subsystem: "preflight_pool", - Name: "queue_full_errors", - Help: "number of preflight full queue errors", - }) - preflightWP.durationMetric = prometheus.NewSummaryVec(prometheus.SummaryOpts{ - Namespace: daemon.MetricsNamespace(), - Subsystem: "preflight_pool", - Name: "request_ledger_get_duration_seconds", - Help: "preflight request duration broken down by status", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, []string{"status", "type"}) - preflightWP.ledgerEntriesFetchedMetric = prometheus.NewSummary(prometheus.SummaryOpts{ - Namespace: daemon.MetricsNamespace(), - Subsystem: "preflight_pool", - Name: "request_ledger_entries_fetched", - Help: "ledger entries fetched by simulate transaction calls", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }) - daemon.MetricsRegistry().MustRegister( - requestQueueMetric, - preflightWP.concurrentRequestsMetric, - preflightWP.errorFullCounter, - preflightWP.durationMetric, - preflightWP.ledgerEntriesFetchedMetric, - ) - for i := uint(0); i < workerCount; i++ { - preflightWP.wg.Add(1) - go preflightWP.work() - } - return &preflightWP -} - -func (pwp *PreflightWorkerPool) work() { - defer pwp.wg.Done() - for request := range pwp.requestChan { - pwp.concurrentRequestsMetric.Inc() - startTime := time.Now() - preflight, err := GetPreflight(request.ctx, request.params) - status := "ok" - if err != nil { - status = "error" - } - pwp.durationMetric.With( - prometheus.Labels{"type": "all", "status": status}, - ).Observe(time.Since(startTime).Seconds()) - pwp.concurrentRequestsMetric.Dec() - request.resultChan <- workerResult{preflight, err} - } -} - -func (pwp *PreflightWorkerPool) Close() { - if !pwp.isClosed.CompareAndSwap(false, true) { - // it was already closed - return - } - close(pwp.requestChan) - pwp.wg.Wait() -} - -var PreflightQueueFullErr = errors.New("preflight queue full") - -type metricsLedgerEntryWrapper struct { - db.LedgerEntryReadTx - totalDurationMs uint64 - ledgerEntriesFetched uint32 -} - -func (m *metricsLedgerEntryWrapper) GetLedgerEntries(keys ...xdr.LedgerKey) ([]db.LedgerKeyAndEntry, error) { - startTime := time.Now() - entries, err := m.LedgerEntryReadTx.GetLedgerEntries(keys...) - atomic.AddUint64(&m.totalDurationMs, uint64(time.Since(startTime).Milliseconds())) - atomic.AddUint32(&m.ledgerEntriesFetched, uint32(len(keys))) - return entries, err -} - -func (pwp *PreflightWorkerPool) GetPreflight(ctx context.Context, params PreflightGetterParameters) (Preflight, error) { - if pwp.isClosed.Load() { - return Preflight{}, errors.New("preflight worker pool is closed") - } - wrappedTx := metricsLedgerEntryWrapper{ - LedgerEntryReadTx: params.LedgerEntryReadTx, - } - preflightParams := PreflightParameters{ - Logger: pwp.logger, - SourceAccount: params.SourceAccount, - OpBody: params.OperationBody, - NetworkPassphrase: pwp.networkPassphrase, - LedgerEntryReadTx: &wrappedTx, - BucketListSize: params.BucketListSize, - Footprint: params.Footprint, - ResourceConfig: params.ResourceConfig, - EnableDebug: pwp.enableDebug, - } - resultC := make(chan workerResult) - select { - case pwp.requestChan <- workerRequest{ctx, preflightParams, resultC}: - result := <-resultC - if wrappedTx.ledgerEntriesFetched > 0 { - status := "ok" - if result.err != nil { - status = "error" - } - pwp.durationMetric.With( - prometheus.Labels{"type": "db", "status": status}, - ).Observe(float64(wrappedTx.totalDurationMs) / 1000.0) - } - pwp.ledgerEntriesFetchedMetric.Observe(float64(wrappedTx.ledgerEntriesFetched)) - return result.preflight, result.err - case <-ctx.Done(): - return Preflight{}, ctx.Err() - default: - pwp.errorFullCounter.Inc() - return Preflight{}, PreflightQueueFullErr - } -} diff --git a/cmd/soroban-rpc/internal/preflight/preflight.go b/cmd/soroban-rpc/internal/preflight/preflight.go deleted file mode 100644 index e342ab434..000000000 --- a/cmd/soroban-rpc/internal/preflight/preflight.go +++ /dev/null @@ -1,277 +0,0 @@ -package preflight - -import ( - "context" - "errors" - "fmt" - "runtime/cgo" - "time" - "unsafe" - - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" -) - -/* -#include "../../lib/preflight.h" -#include -// This assumes that the Rust compiler should be using a -gnu target (i.e. MinGW compiler) in Windows -// (I (fons) am not even sure if CGo supports MSVC, see https://github.com/golang/go/issues/20982) -#cgo windows,amd64 LDFLAGS: -L${SRCDIR}/../../../../target/x86_64-pc-windows-gnu/release-with-panic-unwind/ -lpreflight -lntdll -static -lws2_32 -lbcrypt -luserenv -// You cannot compile with -static in macOS (and it's not worth it in Linux, at least with glibc) -#cgo darwin,amd64 LDFLAGS: -L${SRCDIR}/../../../../target/x86_64-apple-darwin/release-with-panic-unwind/ -lpreflight -ldl -lm -#cgo darwin,arm64 LDFLAGS: -L${SRCDIR}/../../../../target/aarch64-apple-darwin/release-with-panic-unwind/ -lpreflight -ldl -lm -// In Linux, at least for now, we will be dynamically linking glibc. See https://github.com/2opremio/soroban-go-rust-preflight-poc/issues/3 for details -// I (fons) did try linking statically against musl but it caused problems catching (unwinding) Rust panics. -#cgo linux,amd64 LDFLAGS: -L${SRCDIR}/../../../../target/x86_64-unknown-linux-gnu/release-with-panic-unwind/ -lpreflight -ldl -lm -#cgo linux,arm64 LDFLAGS: -L${SRCDIR}/../../../../target/aarch64-unknown-linux-gnu/release-with-panic-unwind/ -lpreflight -ldl -lm -*/ -import "C" - -type snapshotSourceHandle struct { - readTx db.LedgerEntryReadTx - logger *log.Entry -} - -const ( - defaultInstructionLeeway uint64 = 3000000 -) - -// SnapshotSourceGet takes a LedgerKey XDR in base64 string and returns its matching LedgerEntry XDR in base64 string -// It's used by the Rust preflight code to obtain ledger entries. -// -//export SnapshotSourceGet -func SnapshotSourceGet(handle C.uintptr_t, cLedgerKey C.xdr_t) C.xdr_t { - h := cgo.Handle(handle).Value().(snapshotSourceHandle) - ledgerKeyXDR := GoXDR(cLedgerKey) - var ledgerKey xdr.LedgerKey - if err := xdr.SafeUnmarshal(ledgerKeyXDR, &ledgerKey); err != nil { - panic(err) - } - // TODO : the live-until sequence here is being ignored for now; it should be passed downstream. - present, entry, _, err := db.GetLedgerEntry(h.readTx, ledgerKey) - if err != nil { - h.logger.WithError(err).Error("SnapshotSourceGet(): GetLedgerEntry() failed") - return C.xdr_t{} - } - if !present { - return C.xdr_t{} - } - out, err := entry.MarshalBinary() - if err != nil { - panic(err) - } - - return C.xdr_t{ - xdr: (*C.uchar)(C.CBytes(out)), - len: C.size_t(len(out)), - } -} - -//export FreeGoXDR -func FreeGoXDR(xdr C.xdr_t) { - C.free(unsafe.Pointer(xdr.xdr)) -} - -type ResourceConfig struct { - InstructionLeeway uint64 `json:"instructionLeeway"` -} - -func DefaultResourceConfig() ResourceConfig { - return ResourceConfig{ - InstructionLeeway: defaultInstructionLeeway, - } -} - -type PreflightGetterParameters struct { - LedgerEntryReadTx db.LedgerEntryReadTx - BucketListSize uint64 - SourceAccount xdr.AccountId - OperationBody xdr.OperationBody - Footprint xdr.LedgerFootprint - ResourceConfig ResourceConfig -} - -type PreflightParameters struct { - Logger *log.Entry - SourceAccount xdr.AccountId - OpBody xdr.OperationBody - Footprint xdr.LedgerFootprint - NetworkPassphrase string - LedgerEntryReadTx db.LedgerEntryReadTx - BucketListSize uint64 - ResourceConfig ResourceConfig - EnableDebug bool -} - -type Preflight struct { - Error string - Events [][]byte // DiagnosticEvents XDR - TransactionData []byte // SorobanTransactionData XDR - MinFee int64 - Result []byte // XDR SCVal in base64 - Auth [][]byte // SorobanAuthorizationEntries XDR - CPUInstructions uint64 - MemoryBytes uint64 - PreRestoreTransactionData []byte // SorobanTransactionData XDR - PreRestoreMinFee int64 -} - -func CXDR(xdr []byte) C.xdr_t { - return C.xdr_t{ - xdr: (*C.uchar)(C.CBytes(xdr)), - len: C.size_t(len(xdr)), - } -} - -func GoXDR(xdr C.xdr_t) []byte { - return C.GoBytes(unsafe.Pointer(xdr.xdr), C.int(xdr.len)) -} - -func GoXDRVector(xdrVector C.xdr_vector_t) [][]byte { - result := make([][]byte, xdrVector.len) - inputSlice := unsafe.Slice(xdrVector.array, xdrVector.len) - for i, v := range inputSlice { - result[i] = GoXDR(v) - } - return result -} - -func GetPreflight(ctx context.Context, params PreflightParameters) (Preflight, error) { - switch params.OpBody.Type { - case xdr.OperationTypeInvokeHostFunction: - return getInvokeHostFunctionPreflight(params) - case xdr.OperationTypeExtendFootprintTtl, xdr.OperationTypeRestoreFootprint: - return getFootprintTtlPreflight(params) - default: - return Preflight{}, fmt.Errorf("unsupported operation type: %s", params.OpBody.Type.String()) - } -} - -func getFootprintTtlPreflight(params PreflightParameters) (Preflight, error) { - opBodyXDR, err := params.OpBody.MarshalBinary() - if err != nil { - return Preflight{}, err - } - opBodyCXDR := CXDR(opBodyXDR) - footprintXDR, err := params.Footprint.MarshalBinary() - if err != nil { - return Preflight{}, err - } - footprintCXDR := CXDR(footprintXDR) - handle := cgo.NewHandle(snapshotSourceHandle{params.LedgerEntryReadTx, params.Logger}) - defer handle.Delete() - - simulationLedgerSeq, err := getSimulationLedgerSeq(params.LedgerEntryReadTx) - if err != nil { - return Preflight{}, err - } - - res := C.preflight_footprint_ttl_op( - C.uintptr_t(handle), - C.uint64_t(params.BucketListSize), - opBodyCXDR, - footprintCXDR, - C.uint32_t(simulationLedgerSeq), - ) - - FreeGoXDR(opBodyCXDR) - FreeGoXDR(footprintCXDR) - - return GoPreflight(res), nil -} - -func getSimulationLedgerSeq(readTx db.LedgerEntryReadTx) (uint32, error) { - latestLedger, err := readTx.GetLatestLedgerSequence() - if err != nil { - return 0, err - } - // It's of utmost importance to simulate the transactions like we were on the next ledger. - // Otherwise, users would need to wait for an extra ledger to close in order to observe the effects of the latest ledger - // transaction submission. - sequenceNumber := latestLedger + 1 - return sequenceNumber, nil -} - -func getInvokeHostFunctionPreflight(params PreflightParameters) (Preflight, error) { - invokeHostFunctionXDR, err := params.OpBody.MustInvokeHostFunctionOp().MarshalBinary() - if err != nil { - return Preflight{}, err - } - invokeHostFunctionCXDR := CXDR(invokeHostFunctionXDR) - sourceAccountXDR, err := params.SourceAccount.MarshalBinary() - if err != nil { - return Preflight{}, err - } - sourceAccountCXDR := CXDR(sourceAccountXDR) - - hasConfig, stateArchivalConfig, _, err := db.GetLedgerEntry(params.LedgerEntryReadTx, xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeConfigSetting, - ConfigSetting: &xdr.LedgerKeyConfigSetting{ - ConfigSettingId: xdr.ConfigSettingIdConfigSettingStateArchival, - }, - }) - if err != nil { - return Preflight{}, err - } - if !hasConfig { - return Preflight{}, errors.New("state archival config setting missing in ledger storage") - } - - simulationLedgerSeq, err := getSimulationLedgerSeq(params.LedgerEntryReadTx) - if err != nil { - return Preflight{}, err - } - - stateArchival := stateArchivalConfig.Data.MustConfigSetting().MustStateArchivalSettings() - li := C.ledger_info_t{ - network_passphrase: C.CString(params.NetworkPassphrase), - sequence_number: C.uint32_t(simulationLedgerSeq), - protocol_version: 20, - timestamp: C.uint64_t(time.Now().Unix()), - // Current base reserve is 0.5XLM (in stroops) - base_reserve: 5_000_000, - min_temp_entry_ttl: C.uint(stateArchival.MinTemporaryTtl), - min_persistent_entry_ttl: C.uint(stateArchival.MinPersistentTtl), - max_entry_ttl: C.uint(stateArchival.MaxEntryTtl), - } - - handle := cgo.NewHandle(snapshotSourceHandle{params.LedgerEntryReadTx, params.Logger}) - defer handle.Delete() - resourceConfig := C.resource_config_t{ - instruction_leeway: C.uint64_t(params.ResourceConfig.InstructionLeeway), - } - res := C.preflight_invoke_hf_op( - C.uintptr_t(handle), - C.uint64_t(params.BucketListSize), - invokeHostFunctionCXDR, - sourceAccountCXDR, - li, - resourceConfig, - C.bool(params.EnableDebug), - ) - FreeGoXDR(invokeHostFunctionCXDR) - FreeGoXDR(sourceAccountCXDR) - - return GoPreflight(res), nil -} - -func GoPreflight(result *C.preflight_result_t) Preflight { - defer C.free_preflight_result(result) - - preflight := Preflight{ - Error: C.GoString(result.error), - Events: GoXDRVector(result.events), - TransactionData: GoXDR(result.transaction_data), - MinFee: int64(result.min_fee), - Result: GoXDR(result.result), - Auth: GoXDRVector(result.auth), - CPUInstructions: uint64(result.cpu_instructions), - MemoryBytes: uint64(result.memory_bytes), - PreRestoreTransactionData: GoXDR(result.pre_restore_transaction_data), - PreRestoreMinFee: int64(result.pre_restore_min_fee), - } - return preflight -} diff --git a/cmd/soroban-rpc/internal/preflight/preflight_test.go b/cmd/soroban-rpc/internal/preflight/preflight_test.go deleted file mode 100644 index 57a2e82b0..000000000 --- a/cmd/soroban-rpc/internal/preflight/preflight_test.go +++ /dev/null @@ -1,441 +0,0 @@ -package preflight - -import ( - "context" - "crypto/sha256" - "os" - "path" - "runtime" - "testing" - - "github.com/stellar/go/support/log" - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/require" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" -) - -var mockContractID = xdr.Hash{0xa, 0xb, 0xc} -var mockContractHash = xdr.Hash{0xd, 0xe, 0xf} - -var contractCostParams = func() *xdr.ContractCostParams { - var result xdr.ContractCostParams - - for i := 0; i < 23; i++ { - result = append(result, xdr.ContractCostParamEntry{ - Ext: xdr.ExtensionPoint{}, - ConstTerm: 0, - LinearTerm: 0, - }) - } - - return &result -}() - -var mockLedgerEntriesWithoutTTLs = []xdr.LedgerEntry{ - { - LastModifiedLedgerSeq: 1, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &mockContractID, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvLedgerKeyContractInstance, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvContractInstance, - Instance: &xdr.ScContractInstance{ - Executable: xdr.ContractExecutable{ - Type: xdr.ContractExecutableTypeContractExecutableWasm, - WasmHash: &mockContractHash, - }, - Storage: nil, - }, - }, - }, - }, - }, - { - LastModifiedLedgerSeq: 2, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeContractCode, - ContractCode: &xdr.ContractCodeEntry{ - Hash: mockContractHash, - Code: helloWorldContract, - }, - }, - }, - { - LastModifiedLedgerSeq: 2, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeConfigSetting, - ConfigSetting: &xdr.ConfigSettingEntry{ - ConfigSettingId: xdr.ConfigSettingIdConfigSettingContractComputeV0, - ContractCompute: &xdr.ConfigSettingContractComputeV0{ - LedgerMaxInstructions: 100000000, - TxMaxInstructions: 100000000, - FeeRatePerInstructionsIncrement: 1, - TxMemoryLimit: 100000000, - }, - }, - }, - }, - { - LastModifiedLedgerSeq: 2, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeConfigSetting, - ConfigSetting: &xdr.ConfigSettingEntry{ - ConfigSettingId: xdr.ConfigSettingIdConfigSettingContractLedgerCostV0, - ContractLedgerCost: &xdr.ConfigSettingContractLedgerCostV0{ - LedgerMaxReadLedgerEntries: 100, - LedgerMaxReadBytes: 100, - LedgerMaxWriteLedgerEntries: 100, - LedgerMaxWriteBytes: 100, - TxMaxReadLedgerEntries: 100, - TxMaxReadBytes: 100, - TxMaxWriteLedgerEntries: 100, - TxMaxWriteBytes: 100, - FeeReadLedgerEntry: 100, - FeeWriteLedgerEntry: 100, - FeeRead1Kb: 100, - BucketListTargetSizeBytes: 100, - WriteFee1KbBucketListLow: 1, - WriteFee1KbBucketListHigh: 1, - BucketListWriteFeeGrowthFactor: 1, - }, - }, - }, - }, - { - LastModifiedLedgerSeq: 2, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeConfigSetting, - ConfigSetting: &xdr.ConfigSettingEntry{ - ConfigSettingId: xdr.ConfigSettingIdConfigSettingContractHistoricalDataV0, - ContractHistoricalData: &xdr.ConfigSettingContractHistoricalDataV0{ - FeeHistorical1Kb: 100, - }, - }, - }, - }, - { - LastModifiedLedgerSeq: 2, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeConfigSetting, - ConfigSetting: &xdr.ConfigSettingEntry{ - ConfigSettingId: xdr.ConfigSettingIdConfigSettingContractEventsV0, - ContractEvents: &xdr.ConfigSettingContractEventsV0{ - TxMaxContractEventsSizeBytes: 10000, - FeeContractEvents1Kb: 1, - }, - }, - }, - }, - { - LastModifiedLedgerSeq: 2, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeConfigSetting, - ConfigSetting: &xdr.ConfigSettingEntry{ - ConfigSettingId: xdr.ConfigSettingIdConfigSettingContractBandwidthV0, - ContractBandwidth: &xdr.ConfigSettingContractBandwidthV0{ - LedgerMaxTxsSizeBytes: 100000, - TxMaxSizeBytes: 1000, - FeeTxSize1Kb: 1, - }, - }, - }, - }, - { - LastModifiedLedgerSeq: 2, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeConfigSetting, - ConfigSetting: &xdr.ConfigSettingEntry{ - ConfigSettingId: xdr.ConfigSettingIdConfigSettingStateArchival, - StateArchivalSettings: &xdr.StateArchivalSettings{ - MaxEntryTtl: 100, - MinTemporaryTtl: 100, - MinPersistentTtl: 100, - PersistentRentRateDenominator: 100, - TempRentRateDenominator: 100, - MaxEntriesToArchive: 100, - BucketListSizeWindowSampleSize: 100, - EvictionScanSize: 100, - }, - }, - }, - }, - { - LastModifiedLedgerSeq: 2, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeConfigSetting, - ConfigSetting: &xdr.ConfigSettingEntry{ - ConfigSettingId: xdr.ConfigSettingIdConfigSettingContractCostParamsCpuInstructions, - // Obtained with TestGetLedgerEntryConfigSettings - ContractCostParamsCpuInsns: contractCostParams, - }, - }, - }, - { - LastModifiedLedgerSeq: 2, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeConfigSetting, - ConfigSetting: &xdr.ConfigSettingEntry{ - ConfigSettingId: xdr.ConfigSettingIdConfigSettingContractCostParamsMemoryBytes, - // Obtained with TestGetLedgerEntryConfigSettings - ContractCostParamsMemBytes: contractCostParams, - }, - }, - }, -} - -// Adds ttl entries to mockLedgerEntriesWithoutTTLs -var mockLedgerEntries = func() []xdr.LedgerEntry { - result := make([]xdr.LedgerEntry, 0, len(mockLedgerEntriesWithoutTTLs)) - for _, entry := range mockLedgerEntriesWithoutTTLs { - result = append(result, entry) - - if entry.Data.Type == xdr.LedgerEntryTypeContractData || entry.Data.Type == xdr.LedgerEntryTypeContractCode { - key, err := entry.LedgerKey() - if err != nil { - panic(err) - } - bin, err := key.MarshalBinary() - if err != nil { - panic(err) - } - ttlEntry := xdr.LedgerEntry{ - LastModifiedLedgerSeq: entry.LastModifiedLedgerSeq, - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeTtl, - Ttl: &xdr.TtlEntry{ - KeyHash: sha256.Sum256(bin), - // Make sure it doesn't ttl - LiveUntilLedgerSeq: 1000, - }, - }, - } - result = append(result, ttlEntry) - } - } - return result -}() - -var helloWorldContract = func() []byte { - _, filename, _, _ := runtime.Caller(0) - testDirName := path.Dir(filename) - contractFile := path.Join(testDirName, "../../../../target/wasm32-unknown-unknown/test-wasms/test_hello_world.wasm") - ret, err := os.ReadFile(contractFile) - if err != nil { - log.Fatalf("unable to read test_hello_world.wasm (%v) please run `make build-test-wasms` at the project root directory", err) - } - return ret -}() - -type inMemoryLedgerEntryReadTx map[string]xdr.LedgerEntry - -func (m inMemoryLedgerEntryReadTx) GetLedgerEntries(keys ...xdr.LedgerKey) ([]db.LedgerKeyAndEntry, error) { - result := make([]db.LedgerKeyAndEntry, 0, len(keys)) - for _, key := range keys { - serializedKey, err := key.MarshalBinaryBase64() - if err != nil { - return nil, err - } - entry, ok := m[serializedKey] - if !ok { - continue - } - // We don't check the TTL but that's ok for the test - result = append(result, db.LedgerKeyAndEntry{ - Key: key, - Entry: entry, - }) - } - return result, nil -} - -func newInMemoryLedgerEntryReadTx(entries []xdr.LedgerEntry) (inMemoryLedgerEntryReadTx, error) { - result := make(map[string]xdr.LedgerEntry, len(entries)) - for _, entry := range entries { - key, err := entry.LedgerKey() - if err != nil { - return inMemoryLedgerEntryReadTx{}, err - } - serialized, err := key.MarshalBinaryBase64() - if err != nil { - return inMemoryLedgerEntryReadTx{}, err - } - result[serialized] = entry - } - return result, nil -} - -func (m inMemoryLedgerEntryReadTx) GetLatestLedgerSequence() (uint32, error) { - return 2, nil -} - -func (m inMemoryLedgerEntryReadTx) Done() error { - return nil -} - -func getDB(t testing.TB, restartDB bool) *db.DB { - dbPath := path.Join(t.TempDir(), "soroban_rpc.sqlite") - dbInstance, err := db.OpenSQLiteDB(dbPath) - require.NoError(t, err) - readWriter := db.NewReadWriter(dbInstance, 100, 10000) - tx, err := readWriter.NewTx(context.Background()) - require.NoError(t, err) - for _, e := range mockLedgerEntries { - err := tx.LedgerEntryWriter().UpsertLedgerEntry(e) - require.NoError(t, err) - } - err = tx.Commit(2) - require.NoError(t, err) - if restartDB { - // Restarting the DB resets the ledger entries write-through cache - require.NoError(t, dbInstance.Close()) - dbInstance, err = db.OpenSQLiteDB(dbPath) - require.NoError(t, err) - } - return dbInstance -} - -type preflightParametersDBConfig struct { - dbInstance *db.DB - disableCache bool -} - -func getPreflightParameters(t testing.TB, dbConfig *preflightParametersDBConfig) PreflightParameters { - var ledgerEntryReadTx db.LedgerEntryReadTx - if dbConfig != nil { - entryReader := db.NewLedgerEntryReader(dbConfig.dbInstance) - var err error - if dbConfig.disableCache { - ledgerEntryReadTx, err = entryReader.NewTx(context.Background()) - } else { - ledgerEntryReadTx, err = entryReader.NewCachedTx(context.Background()) - } - require.NoError(t, err) - } else { - var err error - ledgerEntryReadTx, err = newInMemoryLedgerEntryReadTx(mockLedgerEntries) - require.NoError(t, err) - } - argSymbol := xdr.ScSymbol("world") - params := PreflightParameters{ - EnableDebug: true, - Logger: log.New(), - SourceAccount: xdr.MustAddress("GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H"), - OpBody: xdr.OperationBody{Type: xdr.OperationTypeInvokeHostFunction, - InvokeHostFunctionOp: &xdr.InvokeHostFunctionOp{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &mockContractID, - }, - FunctionName: "hello", - Args: []xdr.ScVal{ - { - Type: xdr.ScValTypeScvSymbol, - Sym: &argSymbol, - }, - }, - }, - }, - }}, - NetworkPassphrase: "foo", - LedgerEntryReadTx: ledgerEntryReadTx, - BucketListSize: 200, - } - return params -} - -func TestGetPreflight(t *testing.T) { - // in-memory - params := getPreflightParameters(t, nil) - result, err := GetPreflight(context.Background(), params) - require.NoError(t, err) - require.Empty(t, result.Error) - require.NoError(t, params.LedgerEntryReadTx.Done()) - - // using a restarted db with caching and - getDB(t, true) - dbConfig := &preflightParametersDBConfig{ - dbInstance: getDB(t, true), - disableCache: false, - } - params = getPreflightParameters(t, dbConfig) - result, err = GetPreflight(context.Background(), params) - require.NoError(t, err) - require.Empty(t, result.Error) - require.NoError(t, params.LedgerEntryReadTx.Done()) - require.NoError(t, dbConfig.dbInstance.Close()) -} - -func TestGetPreflightDebug(t *testing.T) { - params := getPreflightParameters(t, nil) - // Cause an error - params.OpBody.InvokeHostFunctionOp.HostFunction.InvokeContract.FunctionName = "bar" - - resultWithDebug, err := GetPreflight(context.Background(), params) - require.NoError(t, err) - require.NotZero(t, resultWithDebug.Error) - require.Contains(t, resultWithDebug.Error, "Backtrace") - require.Contains(t, resultWithDebug.Error, "Event log") - require.NotContains(t, resultWithDebug.Error, "DebugInfo not available") - - // Disable debug - params.EnableDebug = false - resultWithoutDebug, err := GetPreflight(context.Background(), params) - require.NoError(t, err) - require.NotZero(t, resultWithoutDebug.Error) - require.NotContains(t, resultWithoutDebug.Error, "Backtrace") - require.NotContains(t, resultWithoutDebug.Error, "Event log") - require.Contains(t, resultWithoutDebug.Error, "DebugInfo not available") -} - -type benchmarkDBConfig struct { - restart bool - disableCache bool -} - -type benchmarkConfig struct { - useDB *benchmarkDBConfig -} - -func benchmark(b *testing.B, config benchmarkConfig) { - var dbConfig *preflightParametersDBConfig - if config.useDB != nil { - dbConfig = &preflightParametersDBConfig{ - dbInstance: getDB(b, config.useDB.restart), - disableCache: config.useDB.disableCache, - } - } - - b.ResetTimer() - b.StopTimer() - for i := 0; i < b.N; i++ { - params := getPreflightParameters(b, dbConfig) - b.StartTimer() - result, err := GetPreflight(context.Background(), params) - b.StopTimer() - require.NoError(b, err) - require.Empty(b, result.Error) - require.NoError(b, params.LedgerEntryReadTx.Done()) - } - if dbConfig != nil { - require.NoError(b, dbConfig.dbInstance.Close()) - } -} - -func BenchmarkGetPreflight(b *testing.B) { - b.Run("In-memory storage", func(b *testing.B) { benchmark(b, benchmarkConfig{}) }) - b.Run("DB storage", func(b *testing.B) { benchmark(b, benchmarkConfig{useDB: &benchmarkDBConfig{}}) }) - b.Run("DB storage, restarting", func(b *testing.B) { benchmark(b, benchmarkConfig{useDB: &benchmarkDBConfig{restart: true}}) }) - b.Run("DB storage, no cache", func(b *testing.B) { benchmark(b, benchmarkConfig{useDB: &benchmarkDBConfig{disableCache: true}}) }) -} diff --git a/cmd/soroban-rpc/internal/test/captive-core-integration-tests.cfg b/cmd/soroban-rpc/internal/test/captive-core-integration-tests.cfg deleted file mode 100644 index 275599bac..000000000 --- a/cmd/soroban-rpc/internal/test/captive-core-integration-tests.cfg +++ /dev/null @@ -1,19 +0,0 @@ -PEER_PORT=11725 -ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true - -UNSAFE_QUORUM=true -FAILURE_SAFETY=0 - -ENABLE_SOROBAN_DIAGNOSTIC_EVENTS=true -# Lower the TTL of persistent ledger entries -# so that ledger entry extension/restoring becomes testeable -TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 -TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true - -[[VALIDATORS]] -NAME="local_core" -HOME_DOMAIN="core.local" -# From "SACJC372QBSSKJYTV5A7LWT4NXWHTQO6GHG4QDAVC2XDPX6CNNXFZ4JK" -PUBLIC_KEY="GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS" -ADDRESS="localhost" -QUALITY="MEDIUM" diff --git a/cmd/soroban-rpc/internal/test/cli_test.go b/cmd/soroban-rpc/internal/test/cli_test.go deleted file mode 100644 index 997372ed8..000000000 --- a/cmd/soroban-rpc/internal/test/cli_test.go +++ /dev/null @@ -1,383 +0,0 @@ -package test - -import ( - "context" - "crypto/sha256" - "encoding/hex" - "fmt" - "os" - "strconv" - "strings" - "testing" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" - "github.com/google/shlex" - "github.com/stellar/go/keypair" - "github.com/stellar/go/strkey" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - "gotest.tools/v3/icmd" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/methods" -) - -func cargoTest(t *testing.T, name string) { - NewCLITest(t) - c := icmd.Command("cargo", "test", "--features", "integration", "--package", "soroban-test", "--test", "it", "--", name, "--exact", "--nocapture") - c.Env = append(os.Environ(), - fmt.Sprintf("SOROBAN_RPC_URL=http://localhost:%d/", sorobanRPCPort), - fmt.Sprintf("SOROBAN_NETWORK_PASSPHRASE=%s", StandaloneNetworkPassphrase), - ) - res := icmd.RunCmd(c) - require.NoError(t, res.Error, res.Stdout(), res.Stderr()) -} - -func TestCLICargoTest(t *testing.T) { - names := icmd.RunCmd(icmd.Command("cargo", "-q", "test", "integration::", "--package", "soroban-test", "--features", "integration", "--", "--list")) - input := names.Stdout() - lines := strings.Split(strings.TrimSpace(input), "\n") - for _, line := range lines { - testName := strings.TrimSuffix(line, ": test") - t.Run(testName, func(t *testing.T) { - cargoTest(t, testName) - }) - } -} - -func TestCLIWrapCustom(t *testing.T) { - it := NewCLITest(t) - assetCode := "deadbeef" - issuerAccount := getCLIDefaultAccount(t) - strkeyContractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract asset deploy --asset=%s:%s", assetCode, issuerAccount)) - require.Equal(t, "true", runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id=%s -- authorized --id=%s", strkeyContractID, issuerAccount))) - asset := txnbuild.CreditAsset{ - Code: assetCode, - Issuer: issuerAccount, - } - establishAccountTrustline(t, it, it.MasterKey(), it.MasterAccount(), asset) - masterAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id=%s -- mint --to=%s --amount 1", strkeyContractID, masterAccount)) -} - -func TestCLIWrapNative(t *testing.T) { - NewCLITest(t) - testAccount := getCLIDefaultAccount(t) - strkeyContractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract asset deploy --asset=native:%s", testAccount)) - require.Equal(t, "CAMTHSPKXZJIRTUXQP5QWJIFH3XIDMKLFAWVQOFOXPTKAW5GKV37ZC4N", strkeyContractID) - require.Equal(t, "true", runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id=%s -- authorized --id=%s", strkeyContractID, testAccount))) - require.Equal(t, "\"9223372036854775807\"", runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id=%s -- balance --id %s", strkeyContractID, testAccount))) -} - -func TestCLIContractInstall(t *testing.T) { - NewCLITest(t) - output := runSuccessfulCLICmd(t, fmt.Sprintf("contract install --wasm %s --ignore-checks", helloWorldContractPath)) - wasm := getHelloWorldContract(t) - contractHash := xdr.Hash(sha256.Sum256(wasm)) - require.Contains(t, output, contractHash.HexString()) -} - -func TestCLIContractInstallAndDeploy(t *testing.T) { - NewCLITest(t) - runSuccessfulCLICmd(t, fmt.Sprintf("contract install --wasm %s --ignore-checks", helloWorldContractPath)) - wasm := getHelloWorldContract(t) - contractHash := xdr.Hash(sha256.Sum256(wasm)) - output := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt %s --wasm-hash %s --ignore-checks", hex.EncodeToString(testSalt[:]), contractHash.HexString())) - outputsContractIDInLastLine(t, output) -} - -func TestCLIContractDeploy(t *testing.T) { - NewCLITest(t) - output := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt %s --wasm %s --ignore-checks", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) - outputsContractIDInLastLine(t, output) -} - -func outputsContractIDInLastLine(t *testing.T, output string) { - lines := strings.Split(output, "\n") - nonEmptyLines := make([]string, 0, len(lines)) - for _, l := range lines { - if l != "" { - nonEmptyLines = append(nonEmptyLines, l) - } - } - require.GreaterOrEqual(t, len(nonEmptyLines), 1) - contractID := nonEmptyLines[len(nonEmptyLines)-1] - require.Len(t, contractID, 56) - require.Regexp(t, "^C", contractID) -} - -func TestCLIContractDeployAndInvoke(t *testing.T) { - NewCLITest(t) - contractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt=%s --wasm %s --ignore-checks", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) - output := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- hello --world=world", contractID)) - require.Contains(t, output, `["Hello","world"]`) -} - -func TestCLIRestorePreamble(t *testing.T) { - test := NewCLITest(t) - strkeyContractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt=%s --wasm %s --ignore-checks", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) - count := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) - require.Equal(t, "1", count) - count = runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) - require.Equal(t, "2", count) - - // Wait for the counter ledger entry to ttl and successfully invoke the `inc` contract function again - // This ensures that the CLI restores the entry (using the RestorePreamble in the simulateTransaction response) - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - waitUntilLedgerEntryTTL(t, client, getCounterLedgerKey(parseContractStrKey(t, strkeyContractID))) - - count = runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) - require.Equal(t, "3", count) -} - -func TestCLIExtend(t *testing.T) { - test := NewCLITest(t) - strkeyContractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt=%s --wasm %s --ignore-checks", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) - count := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) - require.Equal(t, "1", count) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - ttlKey := getCounterLedgerKey(parseContractStrKey(t, strkeyContractID)) - initialLiveUntilSeq := getLedgerEntryLiveUntil(t, client, ttlKey) - - extendOutput := runSuccessfulCLICmd( - t, - fmt.Sprintf( - "contract extend --id %s --key COUNTER --durability persistent --ledgers-to-extend 20", - strkeyContractID, - ), - ) - - newLiveUntilSeq := getLedgerEntryLiveUntil(t, client, ttlKey) - assert.Greater(t, newLiveUntilSeq, initialLiveUntilSeq) - assert.Equal(t, fmt.Sprintf("New ttl ledger: %d", newLiveUntilSeq), extendOutput) -} -func TestCLIExtendTooLow(t *testing.T) { - test := NewCLITest(t) - strkeyContractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt=%s --wasm %s --ignore-checks", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) - count := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) - require.Equal(t, "1", count) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - ttlKey := getCounterLedgerKey(parseContractStrKey(t, strkeyContractID)) - initialLiveUntilSeq := parseInt(t, getLedgerEntryLiveUntil(t, client, ttlKey).GoString()) - - extendOutput := extend(t, strkeyContractID, "400", "--key COUNTER ") - - newLiveUntilSeq := parseInt(t, getLedgerEntryLiveUntil(t, client, ttlKey).GoString()) - assert.Greater(t, newLiveUntilSeq, initialLiveUntilSeq) - assert.Equal(t, newLiveUntilSeq, extendOutput) - - updatedLiveUntilSeq := extend(t, strkeyContractID, "15", "--key COUNTER") - assert.Equal(t, extendOutput, updatedLiveUntilSeq) -} - -func TestCLIExtendTooHigh(t *testing.T) { - test := NewCLITest(t) - strkeyContractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt=%s --wasm %s --ignore-checks", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) - count := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) - require.Equal(t, "1", count) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - ttlKey := getCounterLedgerKey(parseContractStrKey(t, strkeyContractID)) - initialLiveUntilSeq := parseInt(t, getLedgerEntryLiveUntil(t, client, ttlKey).GoString()) - - extendOutput := extend(t, strkeyContractID, "100000000", "--key COUNTER ") - - newLiveUntilSeq := parseInt(t, getLedgerEntryLiveUntil(t, client, ttlKey).GoString()) - assert.Greater(t, newLiveUntilSeq, initialLiveUntilSeq) - assert.Equal(t, newLiveUntilSeq, extendOutput) -} - -func TestCLIRestore(t *testing.T) { - test := NewCLITest(t) - strkeyContractID := runSuccessfulCLICmd(t, fmt.Sprintf("contract deploy --salt=%s --wasm %s --ignore-checks", hex.EncodeToString(testSalt[:]), helloWorldContractPath)) - count := runSuccessfulCLICmd(t, fmt.Sprintf("contract invoke --id %s -- inc", strkeyContractID)) - require.Equal(t, "1", count) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - ttlKey := getCounterLedgerKey(parseContractStrKey(t, strkeyContractID)) - initialLiveUntilSeq := getLedgerEntryLiveUntil(t, client, ttlKey) - // Wait for the counter ledger entry to ttl and successfully invoke the `inc` contract function again - // This ensures that the CLI restores the entry (using the RestorePreamble in the simulateTransaction response) - waitUntilLedgerEntryTTL(t, client, ttlKey) - - restoreOutput := runSuccessfulCLICmd( - t, - fmt.Sprintf( - "contract restore --id %s --key COUNTER --durability persistent", - strkeyContractID, - ), - ) - - newLiveUntilSeq := getLedgerEntryLiveUntil(t, client, getCounterLedgerKey(parseContractStrKey(t, strkeyContractID))) - assert.Greater(t, newLiveUntilSeq, initialLiveUntilSeq) - assert.Equal(t, fmt.Sprintf("New ttl ledger: %d", newLiveUntilSeq), restoreOutput) - - // FIXME: the following checks shouldn't live here: - - // test to see that we get an error when requesting the ttl ledger entry explicitly. - ledgerTTLEntry := getTtlKey(t, getCounterLedgerKey(parseContractStrKey(t, strkeyContractID))) - ledgerTTLEntryB64, err := xdr.MarshalBase64(ledgerTTLEntry) - require.NoError(t, err) - var getLedgerEntryResult methods.GetLedgerEntryResponse - err = client.CallResult(context.Background(), "getLedgerEntry", methods.GetLedgerEntryRequest{ - Key: ledgerTTLEntryB64, - }, &getLedgerEntryResult) - require.Error(t, err) - require.Contains(t, err.Error(), methods.ErrLedgerTtlEntriesCannotBeQueriedDirectly) - - // repeat with getLedgerEntries - var getLedgerEntriesResult methods.GetLedgerEntriesResponse - err = client.CallResult(context.Background(), "getLedgerEntries", methods.GetLedgerEntriesRequest{ - Keys: []string{ledgerTTLEntryB64}, - }, &getLedgerEntriesResult) - require.Error(t, err) - require.Contains(t, err.Error(), methods.ErrLedgerTtlEntriesCannotBeQueriedDirectly) -} - -func getTtlKey(t *testing.T, key xdr.LedgerKey) xdr.LedgerKey { - assert.True(t, key.Type == xdr.LedgerEntryTypeContractCode || key.Type == xdr.LedgerEntryTypeContractData) - binKey, err := key.MarshalBinary() - assert.NoError(t, err) - return xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeTtl, - Ttl: &xdr.LedgerKeyTtl{ - KeyHash: sha256.Sum256(binKey), - }, - } -} - -func parseContractStrKey(t *testing.T, strkeyContractID string) [32]byte { - contractIDBytes := strkey.MustDecode(strkey.VersionByteContract, strkeyContractID) - var contractID [32]byte - require.Len(t, contractIDBytes, len(contractID)) - copy(contractID[:], contractIDBytes) - return contractID -} - -func runSuccessfulCLICmd(t *testing.T, cmd string) string { - res := runCLICommand(t, cmd) - stdout, stderr := res.Stdout(), res.Stderr() - outputs := fmt.Sprintf("stderr:\n%s\nstdout:\n%s\n", stderr, stdout) - require.NoError(t, res.Error, outputs) - fmt.Print(outputs) - return strings.TrimSpace(stdout) -} - -func runCLICommand(t *testing.T, cmd string) *icmd.Result { - args := []string{"run", "-q", "--", "--vv"} - parsedArgs, err := shlex.Split(cmd) - require.NoError(t, err, cmd) - args = append(args, parsedArgs...) - c := icmd.Command("cargo", args...) - c.Env = append(os.Environ(), - fmt.Sprintf("SOROBAN_RPC_URL=http://localhost:%d/", sorobanRPCPort), - fmt.Sprintf("SOROBAN_NETWORK_PASSPHRASE=%s", StandaloneNetworkPassphrase), - "SOROBAN_ACCOUNT=test", - ) - return icmd.RunCmd(c) -} - -func getCLIDefaultAccount(t *testing.T) string { - runSuccessfulCLICmd(t, "keys generate -d test --no-fund") - return "GDIY6AQQ75WMD4W46EYB7O6UYMHOCGQHLAQGQTKHDX4J2DYQCHVCR4W4" -} - -func NewCLITest(t *testing.T) *Test { - test := NewTest(t) - fundAccount(t, test, getCLIDefaultAccount(t), "1000000") - return test -} - -func fundAccount(t *testing.T, test *Test, account string, amount string) { - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: test.MasterAccount(), - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{&txnbuild.CreateAccount{ - Destination: account, - Amount: amount, - }}, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, test.MasterKey(), tx) -} - -func establishAccountTrustline(t *testing.T, test *Test, kp *keypair.Full, account txnbuild.Account, asset txnbuild.Asset) { - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - line := asset.MustToChangeTrustAsset() - tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{&txnbuild.ChangeTrust{ - Line: line, - Limit: "2000", - }}, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, kp, tx) -} - -func parseInt(t *testing.T, s string) uint64 { - i, err := strconv.ParseUint(strings.TrimSpace(s), 10, 64) - require.NoError(t, err) - return i -} - -func extend(t *testing.T, contractId string, amount string, rest string) uint64 { - - res := runSuccessfulCLICmd( - t, - fmt.Sprintf( - "contract extend --ttl-ledger-only --id=%s --durability persistent --ledgers-to-extend=%s %s", - contractId, - amount, - rest, - ), - ) - - return parseInt(t, res) -} - -func getLedgerEntryLiveUntil(t *testing.T, client *jrpc2.Client, ttlLedgerKey xdr.LedgerKey) xdr.Uint32 { - keyB64, err := xdr.MarshalBase64(ttlLedgerKey) - require.NoError(t, err) - getLedgerEntryrequest := methods.GetLedgerEntryRequest{ - Key: keyB64, - } - var getLedgerEntryResult methods.GetLedgerEntryResponse - err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) - require.NoError(t, err) - var entry xdr.LedgerEntryData - require.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) - - require.Contains(t, []xdr.LedgerEntryType{xdr.LedgerEntryTypeContractCode, xdr.LedgerEntryTypeContractData}, entry.Type) - require.NotNil(t, getLedgerEntryResult.LiveUntilLedgerSeq) - return xdr.Uint32(*getLedgerEntryResult.LiveUntilLedgerSeq) -} diff --git a/cmd/soroban-rpc/internal/test/core-start.sh b/cmd/soroban-rpc/internal/test/core-start.sh deleted file mode 100755 index 9dd89ba64..000000000 --- a/cmd/soroban-rpc/internal/test/core-start.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/usr/bin/env bash - -set -e -set -x - -source /etc/profile -# work within the current docker working dir -if [ ! -f "./stellar-core.cfg" ]; then - cp /stellar-core.cfg ./ -fi - -echo "using config:" -cat stellar-core.cfg - -# initialize new db -stellar-core new-db - -if [ "$1" = "standalone" ]; then - # initialize for new history archive path, remove any pre-existing on same path from base image - rm -rf ./history - stellar-core new-hist vs - - # serve history archives to horizon on port 1570 - pushd ./history/vs/ - python3 -m http.server 1570 & - popd -fi - -exec stellar-core run --console diff --git a/cmd/soroban-rpc/internal/test/cors_test.go b/cmd/soroban-rpc/internal/test/cors_test.go deleted file mode 100644 index 2e0cdb3eb..000000000 --- a/cmd/soroban-rpc/internal/test/cors_test.go +++ /dev/null @@ -1,32 +0,0 @@ -package test - -import ( - "bytes" - "io" - "net/http" - "testing" - - "github.com/stretchr/testify/require" -) - -// TestCORS ensures that we receive the correct CORS headers as a response to an HTTP request. -// Specifically, when we include an Origin header in the request, a soroban-rpc should response -// with a corresponding Access-Control-Allow-Origin. -func TestCORS(t *testing.T) { - test := NewTest(t) - - request, err := http.NewRequest("POST", test.sorobanRPCURL(), bytes.NewBufferString("{\"jsonrpc\": \"2.0\", \"id\": 1, \"method\": \"getHealth\"}")) - require.NoError(t, err) - request.Header.Set("Content-Type", "application/json") - origin := "testorigin.com" - request.Header.Set("Origin", origin) - - var client http.Client - response, err := client.Do(request) - require.NoError(t, err) - _, err = io.ReadAll(response.Body) - require.NoError(t, err) - - accessControl := response.Header.Get("Access-Control-Allow-Origin") - require.Equal(t, origin, accessControl) -} diff --git a/cmd/soroban-rpc/internal/test/docker-compose.yml b/cmd/soroban-rpc/internal/test/docker-compose.yml deleted file mode 100644 index 1b67d8363..000000000 --- a/cmd/soroban-rpc/internal/test/docker-compose.yml +++ /dev/null @@ -1,35 +0,0 @@ -version: '3' -services: - core-postgres: - image: postgres:9.6.17-alpine - restart: on-failure - environment: - - POSTGRES_PASSWORD=mysecretpassword - - POSTGRES_DB=stellar - expose: - - "5641" - command: [ "-p", "5641" ] - - core: - platform: linux/amd64 - # Note: Please keep the image pinned to an immutable tag matching the Captive Core version. - # This avoids implicit updates which break compatibility between - # the Core container and captive core. - image: ${CORE_IMAGE:-stellar/unsafe-stellar-core:20.2.0-1716.rc3.34d82fc00.focal} - depends_on: - - core-postgres - restart: on-failure - environment: - - TRACY_NO_INVARIANT_CHECK=1 - ports: - - "11625:11625" - - "11626:11626" - # add extra port for history archive server - - "1570:1570" - entrypoint: /usr/bin/env - command: /start standalone - volumes: - - ./stellar-core-integration-tests.cfg:/stellar-core.cfg - - ./core-start.sh:/start - extra_hosts: - - "host.docker.internal:host-gateway" diff --git a/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go b/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go deleted file mode 100644 index 46b0b25dd..000000000 --- a/cmd/soroban-rpc/internal/test/get_ledger_entries_test.go +++ /dev/null @@ -1,179 +0,0 @@ -package test - -import ( - "context" - "crypto/sha256" - "testing" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/keypair" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/methods" -) - -func TestGetLedgerEntriesNotFound(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase) - contractIDHash := xdr.Hash(contractID) - keyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.LedgerKeyContractData{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractIDHash, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvLedgerKeyContractInstance, - }, - Durability: xdr.ContractDataDurabilityPersistent, - }, - }) - require.NoError(t, err) - - var keys []string - keys = append(keys, keyB64) - request := methods.GetLedgerEntriesRequest{ - Keys: keys, - } - - var result methods.GetLedgerEntriesResponse - err = client.CallResult(context.Background(), "getLedgerEntries", request, &result) - require.NoError(t, err) - - assert.Equal(t, 0, len(result.Entries)) - assert.Greater(t, result.LatestLedger, uint32(0)) -} - -func TestGetLedgerEntriesInvalidParams(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - var keys []string - keys = append(keys, "<>@@#$") - request := methods.GetLedgerEntriesRequest{ - Keys: keys, - } - - var result methods.GetLedgerEntriesResponse - jsonRPCErr := client.CallResult(context.Background(), "getLedgerEntries", request, &result).(*jrpc2.Error) - assert.Contains(t, jsonRPCErr.Message, "cannot unmarshal key value") - assert.Equal(t, jrpc2.InvalidParams, jsonRPCErr.Code) -} - -func TestGetLedgerEntriesSucceeds(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - contractBinary := getHelloWorldContract(t) - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - - contractHash := sha256.Sum256(contractBinary) - contractCodeKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractCode, - ContractCode: &xdr.LedgerKeyContractCode{ - Hash: contractHash, - }, - }) - - // Doesn't exist. - notFoundKeyB64, err := xdr.MarshalBase64(getCounterLedgerKey(contractID)) - require.NoError(t, err) - - contractIDHash := xdr.Hash(contractID) - contractInstanceKeyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.LedgerKeyContractData{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractIDHash, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvLedgerKeyContractInstance, - }, - Durability: xdr.ContractDataDurabilityPersistent, - }, - }) - require.NoError(t, err) - - keys := []string{contractCodeKeyB64, notFoundKeyB64, contractInstanceKeyB64} - request := methods.GetLedgerEntriesRequest{ - Keys: keys, - } - - var result methods.GetLedgerEntriesResponse - err = client.CallResult(context.Background(), "getLedgerEntries", request, &result) - require.NoError(t, err) - require.Equal(t, 2, len(result.Entries)) - require.Greater(t, result.LatestLedger, uint32(0)) - - require.Greater(t, result.Entries[0].LastModifiedLedger, uint32(0)) - require.LessOrEqual(t, result.Entries[0].LastModifiedLedger, result.LatestLedger) - require.NotNil(t, result.Entries[0].LiveUntilLedgerSeq) - require.Greater(t, *result.Entries[0].LiveUntilLedgerSeq, result.LatestLedger) - require.Equal(t, contractCodeKeyB64, result.Entries[0].Key) - var firstEntry xdr.LedgerEntryData - require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].XDR, &firstEntry)) - require.Equal(t, xdr.LedgerEntryTypeContractCode, firstEntry.Type) - require.Equal(t, contractBinary, firstEntry.MustContractCode().Code) - - require.Greater(t, result.Entries[1].LastModifiedLedger, uint32(0)) - require.LessOrEqual(t, result.Entries[1].LastModifiedLedger, result.LatestLedger) - require.NotNil(t, result.Entries[1].LiveUntilLedgerSeq) - require.Greater(t, *result.Entries[1].LiveUntilLedgerSeq, result.LatestLedger) - require.Equal(t, contractInstanceKeyB64, result.Entries[1].Key) - var secondEntry xdr.LedgerEntryData - require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[1].XDR, &secondEntry)) - require.Equal(t, xdr.LedgerEntryTypeContractData, secondEntry.Type) - require.True(t, secondEntry.MustContractData().Key.Equals(xdr.ScVal{ - Type: xdr.ScValTypeScvLedgerKeyContractInstance, - })) -} diff --git a/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go b/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go deleted file mode 100644 index dd4879d53..000000000 --- a/cmd/soroban-rpc/internal/test/get_ledger_entry_test.go +++ /dev/null @@ -1,115 +0,0 @@ -package test - -import ( - "context" - "crypto/sha256" - "testing" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" - "github.com/stellar/go/txnbuild" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/keypair" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/methods" -) - -func TestGetLedgerEntryNotFound(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - contractID := getContractID(t, sourceAccount, testSalt, StandaloneNetworkPassphrase) - contractIDHash := xdr.Hash(contractID) - keyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.LedgerKeyContractData{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractIDHash, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvLedgerKeyContractInstance, - }, - Durability: xdr.ContractDataDurabilityPersistent, - }, - }) - require.NoError(t, err) - request := methods.GetLedgerEntryRequest{ - Key: keyB64, - } - - var result methods.GetLedgerEntryResponse - jsonRPCErr := client.CallResult(context.Background(), "getLedgerEntry", request, &result).(*jrpc2.Error) - assert.Contains(t, jsonRPCErr.Message, "not found") - assert.Equal(t, jrpc2.InvalidRequest, jsonRPCErr.Code) -} - -func TestGetLedgerEntryInvalidParams(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - request := methods.GetLedgerEntryRequest{ - Key: "<>@@#$", - } - - var result methods.GetLedgerEntryResponse - jsonRPCErr := client.CallResult(context.Background(), "getLedgerEntry", request, &result).(*jrpc2.Error) - assert.Equal(t, "cannot unmarshal key value", jsonRPCErr.Message) - assert.Equal(t, jrpc2.InvalidParams, jsonRPCErr.Code) -} - -func TestGetLedgerEntrySucceeds(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - kp := keypair.Root(StandaloneNetworkPassphrase) - account := txnbuild.NewSimpleAccount(kp.Address(), 0) - - contractBinary := getHelloWorldContract(t) - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - - sendSuccessfulTransaction(t, client, kp, tx) - - contractHash := sha256.Sum256(contractBinary) - keyB64, err := xdr.MarshalBase64(xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractCode, - ContractCode: &xdr.LedgerKeyContractCode{ - Hash: contractHash, - }, - }) - require.NoError(t, err) - request := methods.GetLedgerEntryRequest{ - Key: keyB64, - } - - var result methods.GetLedgerEntryResponse - err = client.CallResult(context.Background(), "getLedgerEntry", request, &result) - assert.NoError(t, err) - assert.Greater(t, result.LatestLedger, uint32(0)) - assert.GreaterOrEqual(t, result.LatestLedger, result.LastModifiedLedger) - var entry xdr.LedgerEntryData - assert.NoError(t, xdr.SafeUnmarshalBase64(result.XDR, &entry)) - assert.Equal(t, contractBinary, entry.MustContractCode().Code) -} diff --git a/cmd/soroban-rpc/internal/test/get_network_test.go b/cmd/soroban-rpc/internal/test/get_network_test.go deleted file mode 100644 index 39805d86d..000000000 --- a/cmd/soroban-rpc/internal/test/get_network_test.go +++ /dev/null @@ -1,28 +0,0 @@ -package test - -import ( - "context" - "testing" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" - "github.com/stretchr/testify/assert" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/methods" -) - -func TestGetNetworkSucceeds(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - request := methods.GetNetworkRequest{} - - var result methods.GetNetworkResponse - err := client.CallResult(context.Background(), "getNetwork", request, &result) - assert.NoError(t, err) - assert.Equal(t, friendbotURL, result.FriendbotURL) - assert.Equal(t, StandaloneNetworkPassphrase, result.Passphrase) - assert.Equal(t, stellarCoreProtocolVersion, result.ProtocolVersion) -} diff --git a/cmd/soroban-rpc/internal/test/health_test.go b/cmd/soroban-rpc/internal/test/health_test.go deleted file mode 100644 index 4afbf7f8f..000000000 --- a/cmd/soroban-rpc/internal/test/health_test.go +++ /dev/null @@ -1,24 +0,0 @@ -package test - -import ( - "context" - "testing" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/methods" - "github.com/stretchr/testify/assert" -) - -func TestHealth(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - var result methods.HealthCheckResult - if err := client.CallResult(context.Background(), "getHealth", nil, &result); err != nil { - t.Fatalf("rpc call failed: %v", err) - } - assert.Equal(t, methods.HealthCheckResult{Status: "healthy"}, result) -} diff --git a/cmd/soroban-rpc/internal/test/integration.go b/cmd/soroban-rpc/internal/test/integration.go deleted file mode 100644 index ea918d13d..000000000 --- a/cmd/soroban-rpc/internal/test/integration.go +++ /dev/null @@ -1,356 +0,0 @@ -package test - -import ( - "context" - "fmt" - "os" - "os/exec" - "os/signal" - "path" - "path/filepath" - "strconv" - "sync" - "syscall" - "testing" - "time" - - "github.com/sirupsen/logrus" - "github.com/spf13/cobra" - "github.com/stellar/go/clients/stellarcore" - "github.com/stellar/go/keypair" - "github.com/stellar/go/txnbuild" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/db" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/ledgerbucketwindow" -) - -const ( - StandaloneNetworkPassphrase = "Standalone Network ; February 2017" - stellarCoreProtocolVersion = 20 - stellarCorePort = 11626 - goModFile = "go.mod" - goMonorepoGithubPath = "github.com/stellar/go" - friendbotURL = "http://localhost:8000/friendbot" - // Needed when Core is run with ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true - checkpointFrequency = 8 - sorobanRPCPort = 8000 - adminPort = 8080 - helloWorldContractPath = "../../../../target/wasm32-unknown-unknown/test-wasms/test_hello_world.wasm" -) - -type Test struct { - t *testing.T - - composePath string // docker compose yml file - - daemon *daemon.Daemon - - coreClient *stellarcore.Client - - masterAccount txnbuild.Account - shutdownOnce sync.Once - shutdownCalls []func() -} - -func NewTest(t *testing.T) *Test { - if os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_ENABLED") == "" { - t.Skip("skipping integration test: SOROBAN_RPC_INTEGRATION_TESTS_ENABLED not set") - } - coreBinaryPath := os.Getenv("SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") - if coreBinaryPath == "" { - t.Fatal("missing SOROBAN_RPC_INTEGRATION_TESTS_CAPTIVE_CORE_BIN") - } - - i := &Test{ - t: t, - composePath: findDockerComposePath(), - } - i.masterAccount = &txnbuild.SimpleAccount{ - AccountID: i.MasterKey().Address(), - Sequence: 0, - } - i.runComposeCommand("up", "--detach", "--quiet-pull", "--no-color") - i.prepareShutdownHandlers() - i.coreClient = &stellarcore.Client{URL: "http://localhost:" + strconv.Itoa(stellarCorePort)} - i.waitForCore() - i.waitForCheckpoint() - i.launchDaemon(coreBinaryPath) - - return i -} - -func (i *Test) MasterKey() *keypair.Full { - return keypair.Root(StandaloneNetworkPassphrase) -} - -func (i *Test) MasterAccount() txnbuild.Account { - return i.masterAccount -} - -func (i *Test) sorobanRPCURL() string { - return fmt.Sprintf("http://localhost:%d", sorobanRPCPort) -} - -func (i *Test) adminURL() string { - return fmt.Sprintf("http://localhost:%d", adminPort) -} - -func (i *Test) waitForCheckpoint() { - i.t.Log("Waiting for core to be up...") - for t := 30 * time.Second; t >= 0; t -= time.Second { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - info, err := i.coreClient.Info(ctx) - cancel() - if err != nil { - i.t.Logf("could not obtain info response: %v", err) - time.Sleep(time.Second) - continue - } - if info.Info.Ledger.Num <= checkpointFrequency { - i.t.Logf("checkpoint not reached yet: %v", info) - time.Sleep(time.Second) - continue - } - return - } - i.t.Fatal("Core could not reach checkpoint ledger after 30s") -} - -func (i *Test) launchDaemon(coreBinaryPath string) { - var config config.Config - cmd := &cobra.Command{} - if err := config.AddFlags(cmd); err != nil { - i.t.FailNow() - } - if err := config.SetValues(func(string) (string, bool) { return "", false }); err != nil { - i.t.FailNow() - } - - config.Endpoint = fmt.Sprintf("localhost:%d", sorobanRPCPort) - config.AdminEndpoint = fmt.Sprintf("localhost:%d", adminPort) - config.StellarCoreURL = "http://localhost:" + strconv.Itoa(stellarCorePort) - config.CoreRequestTimeout = time.Second * 2 - config.StellarCoreBinaryPath = coreBinaryPath - config.CaptiveCoreConfigPath = path.Join(i.composePath, "captive-core-integration-tests.cfg") - config.CaptiveCoreStoragePath = i.t.TempDir() - config.CaptiveCoreHTTPPort = 0 - config.FriendbotURL = friendbotURL - config.NetworkPassphrase = StandaloneNetworkPassphrase - config.HistoryArchiveURLs = []string{"http://localhost:1570"} - config.LogLevel = logrus.DebugLevel - config.SQLiteDBPath = path.Join(i.t.TempDir(), "soroban_rpc.sqlite") - config.IngestionTimeout = 10 * time.Minute - config.EventLedgerRetentionWindow = ledgerbucketwindow.DefaultEventLedgerRetentionWindow - config.CheckpointFrequency = checkpointFrequency - config.MaxHealthyLedgerLatency = time.Second * 10 - config.PreflightEnableDebug = true - - i.daemon = daemon.MustNew(&config) - go i.daemon.Run() - - // wait for the storage to catch up for 1 minute - info, err := i.coreClient.Info(context.Background()) - if err != nil { - i.t.Fatalf("cannot obtain latest ledger from core: %v", err) - } - targetLedgerSequence := uint32(info.Info.Ledger.Num) - - reader := db.NewLedgerEntryReader(i.daemon.GetDB()) - success := false - for t := 30; t >= 0; t -= 1 { - sequence, err := reader.GetLatestLedgerSequence(context.Background()) - if err != nil { - if err != db.ErrEmptyDB { - i.t.Fatalf("cannot access ledger entry storage: %v", err) - } - } else { - if sequence >= targetLedgerSequence { - success = true - break - } - } - time.Sleep(time.Second) - } - if !success { - i.t.Fatalf("LedgerEntryStorage failed to sync in 1 minute") - } -} - -// Runs a docker-compose command applied to the above configs -func (i *Test) runComposeCommand(args ...string) { - integrationYaml := filepath.Join(i.composePath, "docker-compose.yml") - - cmdline := append([]string{"-f", integrationYaml}, args...) - cmd := exec.Command("docker-compose", cmdline...) - - i.t.Log("Running", cmd.Env, cmd.Args) - out, innerErr := cmd.Output() - if exitErr, ok := innerErr.(*exec.ExitError); ok { - fmt.Printf("stdout:\n%s\n", string(out)) - fmt.Printf("stderr:\n%s\n", string(exitErr.Stderr)) - } - - if innerErr != nil { - i.t.Fatalf("Compose command failed: %v", innerErr) - } -} - -func (i *Test) prepareShutdownHandlers() { - i.shutdownCalls = append(i.shutdownCalls, - func() { - if i.daemon != nil { - i.daemon.Close() - } - i.runComposeCommand("down", "-v") - }, - ) - - // Register cleanup handlers (on panic and ctrl+c) so the containers are - // stopped even if ingestion or testing fails. - i.t.Cleanup(i.Shutdown) - - c := make(chan os.Signal, 1) - signal.Notify(c, os.Interrupt, syscall.SIGTERM) - go func() { - <-c - i.Shutdown() - os.Exit(int(syscall.SIGTERM)) - }() -} - -// Shutdown stops the integration tests and destroys all its associated -// resources. It will be implicitly called when the calling test (i.e. the -// `testing.Test` passed to `New()`) is finished if it hasn't been explicitly -// called before. -func (i *Test) Shutdown() { - i.shutdownOnce.Do(func() { - // run them in the opposite order in which they where added - for callI := len(i.shutdownCalls) - 1; callI >= 0; callI-- { - i.shutdownCalls[callI]() - } - }) -} - -// Wait for core to be up and manually close the first ledger -func (i *Test) waitForCore() { - i.t.Log("Waiting for core to be up...") - for t := 30 * time.Second; t >= 0; t -= time.Second { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - _, err := i.coreClient.Info(ctx) - cancel() - if err != nil { - i.t.Logf("could not obtain info response: %v", err) - time.Sleep(time.Second) - continue - } - break - } - - i.UpgradeProtocol(stellarCoreProtocolVersion) - - for t := 0; t < 5; t++ { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - info, err := i.coreClient.Info(ctx) - cancel() - if err != nil || !info.IsSynced() { - i.t.Logf("Core is still not synced: %v %v", err, info) - time.Sleep(time.Second) - continue - } - i.t.Log("Core is up.") - return - } - i.t.Fatal("Core could not sync after 30s") -} - -// UpgradeProtocol arms Core with upgrade and blocks until protocol is upgraded. -func (i *Test) UpgradeProtocol(version uint32) { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - err := i.coreClient.Upgrade(ctx, int(version)) - cancel() - if err != nil { - i.t.Fatalf("could not upgrade protocol: %v", err) - } - - for t := 0; t < 10; t++ { - ctx, cancel := context.WithTimeout(context.Background(), time.Second) - info, err := i.coreClient.Info(ctx) - cancel() - if err != nil { - i.t.Logf("could not obtain info response: %v", err) - time.Sleep(time.Second) - continue - } - - if info.Info.Ledger.Version == int(version) { - i.t.Logf("Protocol upgraded to: %d", info.Info.Ledger.Version) - return - } - time.Sleep(time.Second) - } - - i.t.Fatalf("could not upgrade protocol in 10s") -} - -// Cluttering code with if err != nil is absolute nonsense. -func panicIf(err error) { - if err != nil { - panic(err) - } -} - -// findProjectRoot iterates upward on the directory until go.mod file is found. -func findProjectRoot(current string) string { - // Lets you check if a particular directory contains a file. - directoryContainsFilename := func(dir string, filename string) bool { - files, innerErr := os.ReadDir(dir) - panicIf(innerErr) - - for _, file := range files { - if file.Name() == filename { - return true - } - } - return false - } - var err error - - // In either case, we try to walk up the tree until we find "go.mod", - // which we hope is the root directory of the project. - for !directoryContainsFilename(current, goModFile) { - current, err = filepath.Abs(filepath.Join(current, "..")) - - // FIXME: This only works on *nix-like systems. - if err != nil || filepath.Base(current)[0] == filepath.Separator { - fmt.Println("Failed to establish project root directory.") - panic(err) - } - } - return current -} - -// findDockerComposePath performs a best-effort attempt to find the project's -// Docker Compose files. -func findDockerComposePath() string { - current, err := os.Getwd() - panicIf(err) - - // - // We have a primary and backup attempt for finding the necessary docker - // files: via $GOPATH and via local directory traversal. - // - - if gopath := os.Getenv("GOPATH"); gopath != "" { - monorepo := filepath.Join(gopath, "src", "github.com", "stellar", "soroban-tools") - if _, err = os.Stat(monorepo); !os.IsNotExist(err) { - current = monorepo - } - } - - current = findProjectRoot(current) - - // Directly jump down to the folder that should contain the configs - return filepath.Join(current, "cmd", "soroban-rpc", "internal", "test") -} diff --git a/cmd/soroban-rpc/internal/test/integration_test.go b/cmd/soroban-rpc/internal/test/integration_test.go deleted file mode 100644 index 684a61ad3..000000000 --- a/cmd/soroban-rpc/internal/test/integration_test.go +++ /dev/null @@ -1,15 +0,0 @@ -package test - -import ( - "fmt" - "testing" -) - -func TestFindDockerComposePath(t *testing.T) { - dockerPath := findDockerComposePath() - - if len(dockerPath) == 0 { - t.Fail() - } - fmt.Printf("docker compose path is %s\n", dockerPath) -} diff --git a/cmd/soroban-rpc/internal/test/metrics_test.go b/cmd/soroban-rpc/internal/test/metrics_test.go deleted file mode 100644 index 5608a2f90..000000000 --- a/cmd/soroban-rpc/internal/test/metrics_test.go +++ /dev/null @@ -1,39 +0,0 @@ -package test - -import ( - "fmt" - "io" - "net/http" - "net/url" - "runtime" - "testing" - - "github.com/stretchr/testify/require" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config" -) - -func TestMetrics(t *testing.T) { - test := NewTest(t) - metrics := getMetrics(test) - buildMetric := fmt.Sprintf( - "soroban_rpc_build_info{branch=\"%s\",build_timestamp=\"%s\",commit=\"%s\",goversion=\"%s\",version=\"%s\"} 1", - config.Branch, - config.BuildTimestamp, - config.CommitHash, - runtime.Version(), - config.Version, - ) - require.Contains(t, metrics, buildMetric) -} - -func getMetrics(test *Test) string { - metricsURL, err := url.JoinPath(test.adminURL(), "/metrics") - require.NoError(test.t, err) - response, err := http.Get(metricsURL) - require.NoError(test.t, err) - responseBytes, err := io.ReadAll(response.Body) - require.NoError(test.t, err) - require.NoError(test.t, response.Body.Close()) - return string(responseBytes) -} diff --git a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go b/cmd/soroban-rpc/internal/test/simulate_transaction_test.go deleted file mode 100644 index ec785050e..000000000 --- a/cmd/soroban-rpc/internal/test/simulate_transaction_test.go +++ /dev/null @@ -1,1136 +0,0 @@ -package test - -import ( - "context" - "crypto/sha256" - "fmt" - "os" - "path" - "runtime" - "testing" - "time" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/keypair" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/methods" -) - -var ( - testSalt = sha256.Sum256([]byte("a1")) -) - -func getHelloWorldContract(t *testing.T) []byte { - _, filename, _, _ := runtime.Caller(0) - testDirName := path.Dir(filename) - contractFile := path.Join(testDirName, helloWorldContractPath) - ret, err := os.ReadFile(contractFile) - if err != nil { - t.Fatalf("unable to read test_hello_world.wasm (%v) please run `make build-test-wasms` at the project root directory", err) - } - return ret -} - -func createInvokeHostOperation(sourceAccount string, contractID xdr.Hash, method string, args ...xdr.ScVal) *txnbuild.InvokeHostFunction { - return &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractID, - }, - FunctionName: xdr.ScSymbol(method), - Args: args, - }, - }, - Auth: nil, - SourceAccount: sourceAccount, - } -} - -func createInstallContractCodeOperation(sourceAccount string, contractCode []byte) *txnbuild.InvokeHostFunction { - return &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeUploadContractWasm, - Wasm: &contractCode, - }, - SourceAccount: sourceAccount, - } -} - -func createCreateContractOperation(sourceAccount string, contractCode []byte) *txnbuild.InvokeHostFunction { - saltParam := xdr.Uint256(testSalt) - contractHash := xdr.Hash(sha256.Sum256(contractCode)) - - sourceAccountID := xdr.MustAddress(sourceAccount) - return &txnbuild.InvokeHostFunction{ - HostFunction: xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeCreateContract, - CreateContract: &xdr.CreateContractArgs{ - ContractIdPreimage: xdr.ContractIdPreimage{ - Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAddress, - FromAddress: &xdr.ContractIdPreimageFromAddress{ - Address: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeAccount, - AccountId: &sourceAccountID, - }, - Salt: saltParam, - }, - }, - Executable: xdr.ContractExecutable{ - Type: xdr.ContractExecutableTypeContractExecutableWasm, - WasmHash: &contractHash, - }, - }, - }, - Auth: []xdr.SorobanAuthorizationEntry{}, - SourceAccount: sourceAccount, - } -} - -func getContractID(t *testing.T, sourceAccount string, salt [32]byte, networkPassphrase string) [32]byte { - sourceAccountID := xdr.MustAddress(sourceAccount) - preImage := xdr.HashIdPreimage{ - Type: xdr.EnvelopeTypeEnvelopeTypeContractId, - ContractId: &xdr.HashIdPreimageContractId{ - NetworkId: sha256.Sum256([]byte(networkPassphrase)), - ContractIdPreimage: xdr.ContractIdPreimage{ - Type: xdr.ContractIdPreimageTypeContractIdPreimageFromAddress, - FromAddress: &xdr.ContractIdPreimageFromAddress{ - Address: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeAccount, - AccountId: &sourceAccountID, - }, - Salt: salt, - }, - }, - }, - } - - xdrPreImageBytes, err := preImage.MarshalBinary() - require.NoError(t, err) - hashedContractID := sha256.Sum256(xdrPreImageBytes) - return hashedContractID -} - -func simulateTransactionFromTxParams(t *testing.T, client *jrpc2.Client, params txnbuild.TransactionParams) methods.SimulateTransactionResponse { - savedAutoIncrement := params.IncrementSequenceNum - params.IncrementSequenceNum = false - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - params.IncrementSequenceNum = savedAutoIncrement - txB64, err := tx.Base64() - assert.NoError(t, err) - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = client.CallResult(context.Background(), "simulateTransaction", request, &response) - assert.NoError(t, err) - return response -} - -func preflightTransactionParamsLocally(t *testing.T, params txnbuild.TransactionParams, response methods.SimulateTransactionResponse) txnbuild.TransactionParams { - if !assert.Empty(t, response.Error) { - fmt.Println(response.Error) - } - var transactionData xdr.SorobanTransactionData - err := xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) - require.NoError(t, err) - - op := params.Operations[0] - switch v := op.(type) { - case *txnbuild.InvokeHostFunction: - require.Len(t, response.Results, 1) - v.Ext = xdr.TransactionExt{ - V: 1, - SorobanData: &transactionData, - } - var auth []xdr.SorobanAuthorizationEntry - for _, b64 := range response.Results[0].Auth { - var a xdr.SorobanAuthorizationEntry - err := xdr.SafeUnmarshalBase64(b64, &a) - assert.NoError(t, err) - auth = append(auth, a) - } - v.Auth = auth - case *txnbuild.ExtendFootprintTtl: - require.Len(t, response.Results, 0) - v.Ext = xdr.TransactionExt{ - V: 1, - SorobanData: &transactionData, - } - case *txnbuild.RestoreFootprint: - require.Len(t, response.Results, 0) - v.Ext = xdr.TransactionExt{ - V: 1, - SorobanData: &transactionData, - } - default: - t.Fatalf("Wrong operation type %v", op) - } - - params.Operations = []txnbuild.Operation{op} - - params.BaseFee += response.MinResourceFee - return params -} - -func preflightTransactionParams(t *testing.T, client *jrpc2.Client, params txnbuild.TransactionParams) txnbuild.TransactionParams { - response := simulateTransactionFromTxParams(t, client, params) - // The preamble should be zero except for the special restore case - assert.Nil(t, response.RestorePreamble) - return preflightTransactionParamsLocally(t, params, response) -} - -func TestSimulateTransactionSucceeds(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - contractBinary := getHelloWorldContract(t) - params := txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: sourceAccount, - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(sourceAccount, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - result := simulateTransactionFromTxParams(t, client, params) - - contractHash := sha256.Sum256(contractBinary) - contractHashBytes := xdr.ScBytes(contractHash[:]) - expectedXdr := xdr.ScVal{Type: xdr.ScValTypeScvBytes, Bytes: &contractHashBytes} - assert.Greater(t, result.LatestLedger, uint32(0)) - assert.Greater(t, result.Cost.CPUInstructions, uint64(0)) - assert.Greater(t, result.Cost.MemoryBytes, uint64(0)) - - expectedTransactionData := xdr.SorobanTransactionData{ - Resources: xdr.SorobanResources{ - Footprint: xdr.LedgerFootprint{ - ReadWrite: []xdr.LedgerKey{ - { - Type: xdr.LedgerEntryTypeContractCode, - ContractCode: &xdr.LedgerKeyContractCode{ - Hash: xdr.Hash(contractHash), - }, - }, - }, - }, - Instructions: 4378462, - ReadBytes: 0, - WriteBytes: 7048, - }, - // the resulting fee is derived from the compute factors and a default padding is applied to instructions by preflight - // for test purposes, the most deterministic way to assert the resulting fee is expected value in test scope, is to capture - // the resulting fee from current preflight output and re-plug it in here, rather than try to re-implement the cost-model algo - // in the test. - ResourceFee: 132146, - } - - // First, decode and compare the transaction data so we get a decent diff if it fails. - var transactionData xdr.SorobanTransactionData - err := xdr.SafeUnmarshalBase64(result.TransactionData, &transactionData) - assert.NoError(t, err) - assert.Equal(t, expectedTransactionData.Resources.Footprint, transactionData.Resources.Footprint) - assert.InDelta(t, uint32(expectedTransactionData.Resources.Instructions), uint32(transactionData.Resources.Instructions), 3200000) - assert.InDelta(t, uint32(expectedTransactionData.Resources.ReadBytes), uint32(transactionData.Resources.ReadBytes), 10) - assert.InDelta(t, uint32(expectedTransactionData.Resources.WriteBytes), uint32(transactionData.Resources.WriteBytes), 300) - assert.InDelta(t, int64(expectedTransactionData.ResourceFee), int64(transactionData.ResourceFee), 4000) - - // Then decode and check the result xdr, separately so we get a decent diff if it fails. - assert.Len(t, result.Results, 1) - var resultXdr xdr.ScVal - err = xdr.SafeUnmarshalBase64(result.Results[0].XDR, &resultXdr) - assert.NoError(t, err) - assert.Equal(t, expectedXdr, resultXdr) - - // test operation which does not have a source account - withoutSourceAccountOp := createInstallContractCodeOperation("", contractBinary) - params = txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: sourceAccount, - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{withoutSourceAccountOp}, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - require.NoError(t, err) - - resultForRequestWithoutOpSource := simulateTransactionFromTxParams(t, client, params) - // Let's not compare the latest ledger since it may change - result.LatestLedger = resultForRequestWithoutOpSource.LatestLedger - assert.Equal(t, result, resultForRequestWithoutOpSource) - - // test that operation source account takes precedence over tx source account - params = txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: keypair.Root("test passphrase").Address(), - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(sourceAccount, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - - resultForRequestWithDifferentTxSource := simulateTransactionFromTxParams(t, client, params) - assert.GreaterOrEqual(t, resultForRequestWithDifferentTxSource.LatestLedger, result.LatestLedger) - // apart from latest ledger the response should be the same - resultForRequestWithDifferentTxSource.LatestLedger = result.LatestLedger - assert.Equal(t, result, resultForRequestWithDifferentTxSource) -} - -func TestSimulateTransactionWithAuth(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - deployContractOp := createCreateContractOperation(address, helloWorldContract) - deployContractParams := txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - deployContractOp, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - response := simulateTransactionFromTxParams(t, client, deployContractParams) - require.NotEmpty(t, response.Results) - require.Len(t, response.Results[0].Auth, 1) - require.Empty(t, deployContractOp.Auth) - - var auth xdr.SorobanAuthorizationEntry - assert.NoError(t, xdr.SafeUnmarshalBase64(response.Results[0].Auth[0], &auth)) - require.Equal(t, auth.Credentials.Type, xdr.SorobanCredentialsTypeSorobanCredentialsSourceAccount) - deployContractOp.Auth = append(deployContractOp.Auth, auth) - deployContractParams.Operations = []txnbuild.Operation{deployContractOp} - - // preflight deployContractOp with auth - deployContractParams = preflightTransactionParams(t, client, deployContractParams) - tx, err = txnbuild.NewTransaction(deployContractParams) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) -} - -func TestSimulateInvokeContractTransactionSucceeds(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - contractFnParameterSym := xdr.ScSymbol("world") - authAddrArg := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" - authAccountIDArg := xdr.MustAddress(authAddrArg) - tx, err = txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.CreateAccount{ - Destination: authAddrArg, - Amount: "100000", - SourceAccount: address, - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - params = txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInvokeHostOperation( - address, - contractID, - "auth", - xdr.ScVal{ - Type: xdr.ScValTypeScvAddress, - Address: &xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeAccount, - AccountId: &authAccountIDArg, - }, - }, - xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &contractFnParameterSym, - }, - ), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - tx, err = txnbuild.NewTransaction(params) - - assert.NoError(t, err) - - txB64, err := tx.Base64() - assert.NoError(t, err) - - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = client.CallResult(context.Background(), "simulateTransaction", request, &response) - assert.NoError(t, err) - assert.Empty(t, response.Error) - - // check the result - assert.Len(t, response.Results, 1) - var obtainedResult xdr.ScVal - err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) - assert.NoError(t, err) - assert.Equal(t, xdr.ScValTypeScvAddress, obtainedResult.Type) - require.NotNil(t, obtainedResult.Address) - assert.Equal(t, authAccountIDArg, obtainedResult.Address.MustAccountId()) - - // check the footprint - var obtainedTransactionData xdr.SorobanTransactionData - err = xdr.SafeUnmarshalBase64(response.TransactionData, &obtainedTransactionData) - obtainedFootprint := obtainedTransactionData.Resources.Footprint - assert.NoError(t, err) - assert.Len(t, obtainedFootprint.ReadWrite, 1) - assert.Len(t, obtainedFootprint.ReadOnly, 3) - ro0 := obtainedFootprint.ReadOnly[0] - assert.Equal(t, xdr.LedgerEntryTypeAccount, ro0.Type) - assert.Equal(t, authAddrArg, ro0.Account.AccountId.Address()) - ro1 := obtainedFootprint.ReadOnly[1] - assert.Equal(t, xdr.LedgerEntryTypeContractData, ro1.Type) - assert.Equal(t, xdr.ScAddressTypeScAddressTypeContract, ro1.ContractData.Contract.Type) - assert.Equal(t, xdr.Hash(contractID), *ro1.ContractData.Contract.ContractId) - assert.Equal(t, xdr.ScValTypeScvLedgerKeyContractInstance, ro1.ContractData.Key.Type) - ro2 := obtainedFootprint.ReadOnly[2] - assert.Equal(t, xdr.LedgerEntryTypeContractCode, ro2.Type) - contractHash := sha256.Sum256(helloWorldContract) - assert.Equal(t, xdr.Hash(contractHash), ro2.ContractCode.Hash) - assert.NoError(t, err) - - assert.NotZero(t, obtainedTransactionData.ResourceFee) - assert.NotZero(t, obtainedTransactionData.Resources.Instructions) - assert.NotZero(t, obtainedTransactionData.Resources.ReadBytes) - assert.NotZero(t, obtainedTransactionData.Resources.WriteBytes) - - // check the auth - assert.Len(t, response.Results[0].Auth, 1) - var obtainedAuth xdr.SorobanAuthorizationEntry - err = xdr.SafeUnmarshalBase64(response.Results[0].Auth[0], &obtainedAuth) - assert.NoError(t, err) - assert.Equal(t, obtainedAuth.Credentials.Type, xdr.SorobanCredentialsTypeSorobanCredentialsAddress) - assert.Equal(t, obtainedAuth.Credentials.Address.Signature.Type, xdr.ScValTypeScvVoid) - - assert.NotZero(t, obtainedAuth.Credentials.Address.Nonce) - assert.Equal(t, xdr.ScAddressTypeScAddressTypeAccount, obtainedAuth.Credentials.Address.Address.Type) - assert.Equal(t, authAddrArg, obtainedAuth.Credentials.Address.Address.AccountId.Address()) - - assert.Equal(t, xdr.SorobanCredentialsTypeSorobanCredentialsAddress, obtainedAuth.Credentials.Type) - assert.Equal(t, xdr.ScAddressTypeScAddressTypeAccount, obtainedAuth.Credentials.Address.Address.Type) - assert.Equal(t, authAddrArg, obtainedAuth.Credentials.Address.Address.AccountId.Address()) - assert.Equal(t, xdr.SorobanAuthorizedFunctionTypeSorobanAuthorizedFunctionTypeContractFn, obtainedAuth.RootInvocation.Function.Type) - assert.Equal(t, xdr.ScSymbol("auth"), obtainedAuth.RootInvocation.Function.ContractFn.FunctionName) - assert.Len(t, obtainedAuth.RootInvocation.Function.ContractFn.Args, 2) - world := obtainedAuth.RootInvocation.Function.ContractFn.Args[1] - assert.Equal(t, xdr.ScValTypeScvSymbol, world.Type) - assert.Equal(t, xdr.ScSymbol("world"), *world.Sym) - assert.Nil(t, obtainedAuth.RootInvocation.SubInvocations) - - // check the events. There will be 2 debug events and the event emitted by the "auth" function - // which is the one we are going to check. - assert.Len(t, response.Events, 3) - var event xdr.DiagnosticEvent - err = xdr.SafeUnmarshalBase64(response.Events[1], &event) - assert.NoError(t, err) - assert.True(t, event.InSuccessfulContractCall) - assert.NotNil(t, event.Event.ContractId) - assert.Equal(t, xdr.Hash(contractID), *event.Event.ContractId) - assert.Equal(t, xdr.ContractEventTypeContract, event.Event.Type) - assert.Equal(t, int32(0), event.Event.Body.V) - assert.Equal(t, xdr.ScValTypeScvSymbol, event.Event.Body.V0.Data.Type) - assert.Equal(t, xdr.ScSymbol("world"), *event.Event.Body.V0.Data.Sym) - assert.Len(t, event.Event.Body.V0.Topics, 1) - assert.Equal(t, xdr.ScValTypeScvString, event.Event.Body.V0.Topics[0].Type) - assert.Equal(t, xdr.ScString("auth"), *event.Event.Body.V0.Topics[0].Str) - metrics := getMetrics(test) - require.Contains(t, metrics, "soroban_rpc_json_rpc_request_duration_seconds_count{endpoint=\"simulateTransaction\",status=\"ok\"} 3") - require.Contains(t, metrics, "soroban_rpc_preflight_pool_request_ledger_get_duration_seconds_count{status=\"ok\",type=\"db\"} 3") - require.Contains(t, metrics, "soroban_rpc_preflight_pool_request_ledger_get_duration_seconds_count{status=\"ok\",type=\"all\"} 3") - require.Contains(t, metrics, "soroban_rpc_preflight_pool_request_ledger_entries_fetched_sum 67") -} - -func TestSimulateTransactionError(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - invokeHostOp := createInvokeHostOperation(sourceAccount, xdr.Hash{}, "noMethod") - invokeHostOp.HostFunction = xdr.HostFunction{ - Type: xdr.HostFunctionTypeHostFunctionTypeInvokeContract, - InvokeContract: &xdr.InvokeContractArgs{ - ContractAddress: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &xdr.Hash{0x1, 0x2}, - }, - FunctionName: "", - Args: nil, - }, - } - params := txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: keypair.Root(StandaloneNetworkPassphrase).Address(), - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{invokeHostOp}, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - result := simulateTransactionFromTxParams(t, client, params) - assert.Greater(t, result.LatestLedger, uint32(0)) - assert.Contains(t, result.Error, "MissingValue") - require.Len(t, result.Events, 1) - var event xdr.DiagnosticEvent - require.NoError(t, xdr.SafeUnmarshalBase64(result.Events[0], &event)) -} - -func TestSimulateTransactionMultipleOperations(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase).Address() - contractBinary := getHelloWorldContract(t) - params := txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: keypair.Root(StandaloneNetworkPassphrase).Address(), - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(sourceAccount, contractBinary), - createCreateContractOperation(sourceAccount, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - - result := simulateTransactionFromTxParams(t, client, params) - assert.Equal( - t, - methods.SimulateTransactionResponse{ - Error: "Transaction contains more than one operation", - }, - result, - ) -} - -func TestSimulateTransactionWithoutInvokeHostFunction(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - params := txnbuild.TransactionParams{ - SourceAccount: &txnbuild.SimpleAccount{ - AccountID: keypair.Root(StandaloneNetworkPassphrase).Address(), - Sequence: 0, - }, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - &txnbuild.BumpSequence{BumpTo: 1}, - }, - BaseFee: txnbuild.MinBaseFee, - Memo: nil, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - result := simulateTransactionFromTxParams(t, client, params) - assert.Equal( - t, - methods.SimulateTransactionResponse{ - Error: "Transaction contains unsupported operation type: OperationTypeBumpSequence", - }, - result, - ) -} - -func TestSimulateTransactionUnmarshalError(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - request := methods.SimulateTransactionRequest{Transaction: "invalid"} - var result methods.SimulateTransactionResponse - err := client.CallResult(context.Background(), "simulateTransaction", request, &result) - assert.NoError(t, err) - assert.Equal( - t, - "Could not unmarshal transaction", - result.Error, - ) -} - -func TestSimulateTransactionExtendAndRestoreFootprint(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - invokeIncPresistentEntryParams := txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInvokeHostOperation( - address, - contractID, - "inc", - ), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - params = preflightTransactionParams(t, client, invokeIncPresistentEntryParams) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - // get the counter ledger entry TTL - key := getCounterLedgerKey(contractID) - - keyB64, err := xdr.MarshalBase64(key) - require.NoError(t, err) - getLedgerEntryrequest := methods.GetLedgerEntryRequest{ - Key: keyB64, - } - var getLedgerEntryResult methods.GetLedgerEntryResponse - err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) - assert.NoError(t, err) - - var entry xdr.LedgerEntryData - assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) - assert.Equal(t, xdr.LedgerEntryTypeContractData, entry.Type) - require.NotNil(t, getLedgerEntryResult.LiveUntilLedgerSeq) - - initialLiveUntil := *getLedgerEntryResult.LiveUntilLedgerSeq - - // Extend the initial TTL - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.ExtendFootprintTtl{ - ExtendTo: 20, - Ext: xdr.TransactionExt{ - V: 1, - SorobanData: &xdr.SorobanTransactionData{ - Resources: xdr.SorobanResources{ - Footprint: xdr.LedgerFootprint{ - ReadOnly: []xdr.LedgerKey{key}, - }, - }, - }, - }, - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - err = client.CallResult(context.Background(), "getLedgerEntry", getLedgerEntryrequest, &getLedgerEntryResult) - assert.NoError(t, err) - assert.NoError(t, xdr.SafeUnmarshalBase64(getLedgerEntryResult.XDR, &entry)) - assert.Equal(t, xdr.LedgerEntryTypeContractData, entry.Type) - require.NotNil(t, getLedgerEntryResult.LiveUntilLedgerSeq) - newLiveUntilSeq := *getLedgerEntryResult.LiveUntilLedgerSeq - assert.Greater(t, newLiveUntilSeq, initialLiveUntil) - - // Wait until it is not live anymore - waitUntilLedgerEntryTTL(t, client, key) - - // and restore it - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.RestoreFootprint{ - Ext: xdr.TransactionExt{ - V: 1, - SorobanData: &xdr.SorobanTransactionData{ - Resources: xdr.SorobanResources{ - Footprint: xdr.LedgerFootprint{ - ReadWrite: []xdr.LedgerKey{key}, - }, - }, - }, - }, - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - // Wait for TTL again and check the pre-restore field when trying to exec the contract again - waitUntilLedgerEntryTTL(t, client, key) - - simulationResult := simulateTransactionFromTxParams(t, client, invokeIncPresistentEntryParams) - require.NotNil(t, simulationResult.RestorePreamble) - assert.NotZero(t, simulationResult.RestorePreamble) - - params = preflightTransactionParamsLocally(t, - txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.RestoreFootprint{}, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }, - methods.SimulateTransactionResponse{ - TransactionData: simulationResult.RestorePreamble.TransactionData, - MinResourceFee: simulationResult.RestorePreamble.MinResourceFee, - }, - ) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - // Finally, we should be able to send the inc host function invocation now that we - // have pre-restored the entries - params = preflightTransactionParamsLocally(t, invokeIncPresistentEntryParams, simulationResult) - tx, err = txnbuild.NewTransaction(params) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) -} - -func getCounterLedgerKey(contractID [32]byte) xdr.LedgerKey { - contractIDHash := xdr.Hash(contractID) - counterSym := xdr.ScSymbol("COUNTER") - key := xdr.LedgerKey{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.LedgerKeyContractData{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractIDHash, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &counterSym, - }, - Durability: xdr.ContractDataDurabilityPersistent, - }, - } - return key -} - -func waitUntilLedgerEntryTTL(t *testing.T, client *jrpc2.Client, ledgerKey xdr.LedgerKey) { - keyB64, err := xdr.MarshalBase64(ledgerKey) - require.NoError(t, err) - request := methods.GetLedgerEntriesRequest{ - Keys: []string{keyB64}, - } - ttled := false - for i := 0; i < 50; i++ { - var result methods.GetLedgerEntriesResponse - var entry xdr.LedgerEntryData - err := client.CallResult(context.Background(), "getLedgerEntries", request, &result) - require.NoError(t, err) - require.NotEmpty(t, result.Entries) - require.NoError(t, xdr.SafeUnmarshalBase64(result.Entries[0].XDR, &entry)) - require.NotEqual(t, xdr.LedgerEntryTypeTtl, entry.Type) - liveUntilLedgerSeq := xdr.Uint32(*result.Entries[0].LiveUntilLedgerSeq) - // See https://soroban.stellar.org/docs/fundamentals-and-concepts/state-expiration#expiration-ledger - currentLedger := result.LatestLedger + 1 - if xdr.Uint32(currentLedger) > liveUntilLedgerSeq { - ttled = true - t.Logf("ledger entry ttl'ed") - break - } - t.Log("waiting for ledger entry to ttl at ledger", liveUntilLedgerSeq) - time.Sleep(time.Second) - } - require.True(t, ttled) -} - -func TestSimulateInvokePrng_u64_in_range(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err := txnbuild.NewTransaction(params) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err = txnbuild.NewTransaction(params) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - authAddrArg := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" - tx, err = txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.CreateAccount{ - Destination: authAddrArg, - Amount: "100000", - SourceAccount: address, - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - low := xdr.Uint64(1500) - high := xdr.Uint64(10000) - params = txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInvokeHostOperation( - address, - contractID, - "prng_u64_in_range", - xdr.ScVal{ - Type: xdr.ScValTypeScvU64, - U64: &low, - }, - xdr.ScVal{ - Type: xdr.ScValTypeScvU64, - U64: &high, - }, - ), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - tx, err = txnbuild.NewTransaction(params) - - require.NoError(t, err) - - txB64, err := tx.Base64() - require.NoError(t, err) - - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = client.CallResult(context.Background(), "simulateTransaction", request, &response) - require.NoError(t, err) - require.Empty(t, response.Error) - - // check the result - require.Len(t, response.Results, 1) - var obtainedResult xdr.ScVal - err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) - require.NoError(t, err) - require.Equal(t, xdr.ScValTypeScvU64, obtainedResult.Type) - require.LessOrEqual(t, uint64(*obtainedResult.U64), uint64(high)) - require.GreaterOrEqual(t, uint64(*obtainedResult.U64), uint64(low)) -} - -func TestSimulateSystemEvent(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - sourceAccount := keypair.Root(StandaloneNetworkPassphrase) - address := sourceAccount.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - helloWorldContract := getHelloWorldContract(t) - - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err := txnbuild.NewTransaction(params) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - params = preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createCreateContractOperation(address, helloWorldContract), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - tx, err = txnbuild.NewTransaction(params) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractID := getContractID(t, address, testSalt, StandaloneNetworkPassphrase) - authAddrArg := "GBRPYHIL2CI3FNQ4BXLFMNDLFJUNPU2HY3ZMFSHONUCEOASW7QC7OX2H" - tx, err = txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.CreateAccount{ - Destination: authAddrArg, - Amount: "100000", - SourceAccount: address, - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - require.NoError(t, err) - sendSuccessfulTransaction(t, client, sourceAccount, tx) - - contractHash := sha256.Sum256(helloWorldContract) - byteSlice := xdr.ScBytes(contractHash[:]) - - params = txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: false, - Operations: []txnbuild.Operation{ - createInvokeHostOperation( - address, - contractID, - "upgrade_contract", - xdr.ScVal{ - Type: xdr.ScValTypeScvBytes, - Bytes: &byteSlice, - }, - ), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - } - tx, err = txnbuild.NewTransaction(params) - - require.NoError(t, err) - - txB64, err := tx.Base64() - require.NoError(t, err) - - request := methods.SimulateTransactionRequest{Transaction: txB64} - var response methods.SimulateTransactionResponse - err = client.CallResult(context.Background(), "simulateTransaction", request, &response) - require.NoError(t, err) - require.Empty(t, response.Error) - - // check the result - require.Len(t, response.Results, 1) - var obtainedResult xdr.ScVal - err = xdr.SafeUnmarshalBase64(response.Results[0].XDR, &obtainedResult) - require.NoError(t, err) - - var transactionData xdr.SorobanTransactionData - err = xdr.SafeUnmarshalBase64(response.TransactionData, &transactionData) - require.NoError(t, err) - assert.InDelta(t, 6856, uint32(transactionData.Resources.ReadBytes), 200) - - // the resulting fee is derived from compute factors and a default padding is applied to instructions by preflight - // for test purposes, the most deterministic way to assert the resulting fee is expected value in test scope, is to capture - // the resulting fee from current preflight output and re-plug it in here, rather than try to re-implement the cost-model algo - // in the test. - assert.InDelta(t, 100980, int64(transactionData.ResourceFee), 5000) - assert.InDelta(t, 104, uint32(transactionData.Resources.WriteBytes), 15) - require.GreaterOrEqual(t, len(response.Events), 3) -} diff --git a/cmd/soroban-rpc/internal/test/stellar-core-integration-tests.cfg b/cmd/soroban-rpc/internal/test/stellar-core-integration-tests.cfg deleted file mode 100644 index c194dbae1..000000000 --- a/cmd/soroban-rpc/internal/test/stellar-core-integration-tests.cfg +++ /dev/null @@ -1,30 +0,0 @@ -ARTIFICIALLY_ACCELERATE_TIME_FOR_TESTING=true -ENABLE_DIAGNOSTICS_FOR_TX_SUBMISSION=true - -NETWORK_PASSPHRASE="Standalone Network ; February 2017" - -PEER_PORT=11625 -HTTP_PORT=11626 -PUBLIC_HTTP_PORT=true - -NODE_SEED="SACJC372QBSSKJYTV5A7LWT4NXWHTQO6GHG4QDAVC2XDPX6CNNXFZ4JK" - -NODE_IS_VALIDATOR=true -UNSAFE_QUORUM=true -FAILURE_SAFETY=0 - -DATABASE="postgresql://user=postgres password=mysecretpassword host=core-postgres port=5641 dbname=stellar" - -# Lower the TTL of persistent ledger entries -# so that ledger entry extension/restoring becomes testeable -TESTING_MINIMUM_PERSISTENT_ENTRY_LIFETIME=10 -TESTING_SOROBAN_HIGH_LIMIT_OVERRIDE=true - -[QUORUM_SET] -THRESHOLD_PERCENT=100 -VALIDATORS=["GD5KD2KEZJIGTC63IGW6UMUSMVUVG5IHG64HUTFWCHVZH2N2IBOQN7PS"] - -[HISTORY.vs] -get="cp history/vs/{0} {1}" -put="cp {0} history/vs/{1}" -mkdir="mkdir -p history/vs/{0}" diff --git a/cmd/soroban-rpc/internal/test/transaction_test.go b/cmd/soroban-rpc/internal/test/transaction_test.go deleted file mode 100644 index 1cd0d198a..000000000 --- a/cmd/soroban-rpc/internal/test/transaction_test.go +++ /dev/null @@ -1,355 +0,0 @@ -package test - -import ( - "context" - "crypto/sha256" - "fmt" - "testing" - "time" - - "github.com/creachadair/jrpc2" - "github.com/creachadair/jrpc2/jhttp" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/require" - - "github.com/stellar/go/keypair" - proto "github.com/stellar/go/protocols/stellarcore" - "github.com/stellar/go/txnbuild" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/methods" -) - -func TestSendTransactionSucceedsWithoutResults(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.SetOptions{HomeDomain: txnbuild.NewHomeDomain("soroban.com")}, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - assert.NoError(t, err) - sendSuccessfulTransaction(t, client, kp, tx) -} - -func TestSendTransactionSucceedsWithResults(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - contractBinary := getHelloWorldContract(t) - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - response := sendSuccessfulTransaction(t, client, kp, tx) - - // Check the result is what we expect - var transactionResult xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXdr, &transactionResult)) - opResults, ok := transactionResult.OperationResults() - assert.True(t, ok) - invokeHostFunctionResult, ok := opResults[0].MustTr().GetInvokeHostFunctionResult() - assert.True(t, ok) - assert.Equal(t, invokeHostFunctionResult.Code, xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess) - contractHash := sha256.Sum256(contractBinary) - contractHashBytes := xdr.ScBytes(contractHash[:]) - expectedScVal := xdr.ScVal{Type: xdr.ScValTypeScvBytes, Bytes: &contractHashBytes} - var transactionMeta xdr.TransactionMeta - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultMetaXdr, &transactionMeta)) - assert.True(t, expectedScVal.Equals(transactionMeta.V3.SorobanMeta.ReturnValue)) - var resultXdr xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXdr, &resultXdr)) - expectedResult := xdr.TransactionResult{ - FeeCharged: resultXdr.FeeCharged, - Result: xdr.TransactionResultResult{ - Code: xdr.TransactionResultCodeTxSuccess, - Results: &[]xdr.OperationResult{ - { - Code: xdr.OperationResultCodeOpInner, - Tr: &xdr.OperationResultTr{ - Type: xdr.OperationTypeInvokeHostFunction, - InvokeHostFunctionResult: &xdr.InvokeHostFunctionResult{ - Code: xdr.InvokeHostFunctionResultCodeInvokeHostFunctionSuccess, - Success: (*resultXdr.Result.Results)[0].Tr.InvokeHostFunctionResult.Success, - }, - }, - }, - }, - }, - } - - assert.Equal(t, expectedResult, resultXdr) -} - -func TestSendTransactionBadSequence(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - Operations: []txnbuild.Operation{ - &txnbuild.SetOptions{HomeDomain: txnbuild.NewHomeDomain("soroban.com")}, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - assert.NoError(t, err) - tx, err = tx.Sign(StandaloneNetworkPassphrase, kp) - assert.NoError(t, err) - b64, err := tx.Base64() - assert.NoError(t, err) - - request := methods.SendTransactionRequest{Transaction: b64} - var result methods.SendTransactionResponse - err = client.CallResult(context.Background(), "sendTransaction", request, &result) - assert.NoError(t, err) - - assert.NotZero(t, result.LatestLedger) - assert.NotZero(t, result.LatestLedgerCloseTime) - expectedHashHex, err := tx.HashHex(StandaloneNetworkPassphrase) - assert.NoError(t, err) - assert.Equal(t, expectedHashHex, result.Hash) - assert.Equal(t, proto.TXStatusError, result.Status) - var errorResult xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &errorResult)) - assert.Equal(t, xdr.TransactionResultCodeTxBadSeq, errorResult.Result.Code) -} - -func TestSendTransactionFailedInsufficientResourceFee(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - contractBinary := getHelloWorldContract(t) - params := preflightTransactionParams(t, client, txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - createInstallContractCodeOperation(account.AccountID, contractBinary), - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - - // make the transaction fail due to insufficient resource fees - params.Operations[0].(*txnbuild.InvokeHostFunction).Ext.SorobanData.ResourceFee /= 2 - - tx, err := txnbuild.NewTransaction(params) - assert.NoError(t, err) - - assert.NoError(t, err) - tx, err = tx.Sign(StandaloneNetworkPassphrase, kp) - assert.NoError(t, err) - b64, err := tx.Base64() - assert.NoError(t, err) - - request := methods.SendTransactionRequest{Transaction: b64} - var result methods.SendTransactionResponse - err = client.CallResult(context.Background(), "sendTransaction", request, &result) - assert.NoError(t, err) - - assert.Equal(t, proto.TXStatusError, result.Status) - var errorResult xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &errorResult)) - assert.Equal(t, xdr.TransactionResultCodeTxSorobanInvalid, errorResult.Result.Code) - - assert.Greater(t, len(result.DiagnosticEventsXDR), 0) - var event xdr.DiagnosticEvent - err = xdr.SafeUnmarshalBase64(result.DiagnosticEventsXDR[0], &event) - assert.NoError(t, err) - -} - -func TestSendTransactionFailedInLedger(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - kp := keypair.Root(StandaloneNetworkPassphrase) - address := kp.Address() - account := txnbuild.NewSimpleAccount(address, 0) - - tx, err := txnbuild.NewTransaction(txnbuild.TransactionParams{ - SourceAccount: &account, - IncrementSequenceNum: true, - Operations: []txnbuild.Operation{ - &txnbuild.Payment{ - // Destination doesn't exist, making the transaction fail - Destination: "GA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVSGZ", - Amount: "100000.0000000", - Asset: txnbuild.NativeAsset{}, - SourceAccount: "", - }, - }, - BaseFee: txnbuild.MinBaseFee, - Preconditions: txnbuild.Preconditions{ - TimeBounds: txnbuild.NewInfiniteTimeout(), - }, - }) - assert.NoError(t, err) - tx, err = tx.Sign(StandaloneNetworkPassphrase, kp) - assert.NoError(t, err) - b64, err := tx.Base64() - assert.NoError(t, err) - - request := methods.SendTransactionRequest{Transaction: b64} - var result methods.SendTransactionResponse - err = client.CallResult(context.Background(), "sendTransaction", request, &result) - assert.NoError(t, err) - - expectedHashHex, err := tx.HashHex(StandaloneNetworkPassphrase) - assert.NoError(t, err) - - assert.Equal(t, expectedHashHex, result.Hash) - if !assert.Equal(t, proto.TXStatusPending, result.Status) { - var txResult xdr.TransactionResult - err := xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &txResult) - assert.NoError(t, err) - fmt.Printf("error: %#v\n", txResult) - } - assert.NotZero(t, result.LatestLedger) - assert.NotZero(t, result.LatestLedgerCloseTime) - - response := getTransaction(t, client, expectedHashHex) - assert.Equal(t, methods.TransactionStatusFailed, response.Status) - var transactionResult xdr.TransactionResult - assert.NoError(t, xdr.SafeUnmarshalBase64(response.ResultXdr, &transactionResult)) - assert.Equal(t, xdr.TransactionResultCodeTxFailed, transactionResult.Result.Code) - assert.Greater(t, response.Ledger, result.LatestLedger) - assert.Greater(t, response.LedgerCloseTime, result.LatestLedgerCloseTime) - assert.GreaterOrEqual(t, response.LatestLedger, response.Ledger) - assert.GreaterOrEqual(t, response.LatestLedgerCloseTime, response.LedgerCloseTime) -} - -func TestSendTransactionFailedInvalidXDR(t *testing.T) { - test := NewTest(t) - - ch := jhttp.NewChannel(test.sorobanRPCURL(), nil) - client := jrpc2.NewClient(ch, nil) - - request := methods.SendTransactionRequest{Transaction: "abcdef"} - var response methods.SendTransactionResponse - jsonRPCErr := client.CallResult(context.Background(), "sendTransaction", request, &response).(*jrpc2.Error) - assert.Equal(t, "invalid_xdr", jsonRPCErr.Message) - assert.Equal(t, jrpc2.InvalidParams, jsonRPCErr.Code) -} - -func sendSuccessfulTransaction(t *testing.T, client *jrpc2.Client, kp *keypair.Full, transaction *txnbuild.Transaction) methods.GetTransactionResponse { - tx, err := transaction.Sign(StandaloneNetworkPassphrase, kp) - assert.NoError(t, err) - b64, err := tx.Base64() - assert.NoError(t, err) - - request := methods.SendTransactionRequest{Transaction: b64} - var result methods.SendTransactionResponse - err = client.CallResult(context.Background(), "sendTransaction", request, &result) - assert.NoError(t, err) - - expectedHashHex, err := tx.HashHex(StandaloneNetworkPassphrase) - assert.NoError(t, err) - - assert.Equal(t, expectedHashHex, result.Hash) - if !assert.Equal(t, proto.TXStatusPending, result.Status) { - var txResult xdr.TransactionResult - err := xdr.SafeUnmarshalBase64(result.ErrorResultXDR, &txResult) - assert.NoError(t, err) - fmt.Printf("error: %#v\n", txResult) - } - assert.NotZero(t, result.LatestLedger) - assert.NotZero(t, result.LatestLedgerCloseTime) - - response := getTransaction(t, client, expectedHashHex) - if !assert.Equal(t, methods.TransactionStatusSuccess, response.Status) { - var txResult xdr.TransactionResult - err := xdr.SafeUnmarshalBase64(response.ResultXdr, &txResult) - assert.NoError(t, err) - fmt.Printf("error: %#v\n", txResult) - var txMeta xdr.TransactionMeta - err = xdr.SafeUnmarshalBase64(response.ResultMetaXdr, &txMeta) - assert.NoError(t, err) - if txMeta.V == 3 && txMeta.V3.SorobanMeta != nil { - if len(txMeta.V3.SorobanMeta.Events) > 0 { - fmt.Println("Contract events:") - for i, e := range txMeta.V3.SorobanMeta.Events { - fmt.Printf(" %d: %s\n", i, e) - } - } - - if len(txMeta.V3.SorobanMeta.DiagnosticEvents) > 0 { - fmt.Println("Diagnostic events:") - for i, d := range txMeta.V3.SorobanMeta.DiagnosticEvents { - fmt.Printf(" %d: %s\n", i, d) - } - } - } - } - - require.NotNil(t, response.ResultXdr) - assert.Greater(t, response.Ledger, result.LatestLedger) - assert.Greater(t, response.LedgerCloseTime, result.LatestLedgerCloseTime) - assert.GreaterOrEqual(t, response.LatestLedger, response.Ledger) - assert.GreaterOrEqual(t, response.LatestLedgerCloseTime, response.LedgerCloseTime) - return response -} - -func getTransaction(t *testing.T, client *jrpc2.Client, hash string) methods.GetTransactionResponse { - var result methods.GetTransactionResponse - for i := 0; i < 60; i++ { - request := methods.GetTransactionRequest{Hash: hash} - err := client.CallResult(context.Background(), "getTransaction", request, &result) - assert.NoError(t, err) - - if result.Status == methods.TransactionStatusNotFound { - time.Sleep(time.Second) - continue - } - - return result - } - t.Fatal("getTransaction timed out") - return result -} diff --git a/cmd/soroban-rpc/internal/transactions/transactions.go b/cmd/soroban-rpc/internal/transactions/transactions.go deleted file mode 100644 index 5ed719ade..000000000 --- a/cmd/soroban-rpc/internal/transactions/transactions.go +++ /dev/null @@ -1,216 +0,0 @@ -package transactions - -import ( - "sync" - "time" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/ingest" - "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/ledgerbucketwindow" -) - -type transaction struct { - bucket *ledgerbucketwindow.LedgerBucket[[]xdr.Hash] - result []byte // encoded XDR of xdr.TransactionResult - meta []byte // encoded XDR of xdr.TransactionMeta - envelope []byte // encoded XDR of xdr.TransactionEnvelope - feeBump bool - successful bool - applicationOrder int32 -} - -// MemoryStore is an in-memory store of Stellar transactions. -type MemoryStore struct { - // networkPassphrase is an immutable string containing the - // Stellar network passphrase. - // Accessing networkPassphrase does not need to be protected - // by the lock - networkPassphrase string - lock sync.RWMutex - transactions map[xdr.Hash]transaction - transactionsByLedger *ledgerbucketwindow.LedgerBucketWindow[[]xdr.Hash] - transactionDurationMetric *prometheus.SummaryVec - transactionCountMetric prometheus.Summary -} - -// NewMemoryStore creates a new MemoryStore. -// The retention window is in units of ledgers. -// All events occurring in the following ledger range -// [ latestLedger - retentionWindow, latestLedger ] -// will be included in the MemoryStore. If the MemoryStore -// is full, any transactions from new ledgers will evict -// older entries outside the retention window. -func NewMemoryStore(daemon interfaces.Daemon, networkPassphrase string, retentionWindow uint32) *MemoryStore { - window := ledgerbucketwindow.NewLedgerBucketWindow[[]xdr.Hash](retentionWindow) - - // transactionDurationMetric is a metric for measuring latency of transaction store operations - transactionDurationMetric := prometheus.NewSummaryVec(prometheus.SummaryOpts{ - Namespace: daemon.MetricsNamespace(), Subsystem: "transactions", Name: "operation_duration_seconds", - Help: "transaction store operation durations, sliding window = 10m", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }, - []string{"operation"}, - ) - transactionCountMetric := prometheus.NewSummary(prometheus.SummaryOpts{ - Namespace: daemon.MetricsNamespace(), Subsystem: "transactions", Name: "count", - Help: "count of transactions ingested, sliding window = 10m", - Objectives: map[float64]float64{0.5: 0.05, 0.9: 0.01, 0.99: 0.001}, - }) - daemon.MetricsRegistry().MustRegister(transactionDurationMetric, transactionCountMetric) - - return &MemoryStore{ - networkPassphrase: networkPassphrase, - transactions: make(map[xdr.Hash]transaction), - transactionsByLedger: window, - transactionDurationMetric: transactionDurationMetric, - transactionCountMetric: transactionCountMetric, - } -} - -// IngestTransactions adds new transactions from the given ledger into the store. -// As a side effect, transactions which fall outside the retention window are -// removed from the store. -func (m *MemoryStore) IngestTransactions(ledgerCloseMeta xdr.LedgerCloseMeta) error { - startTime := time.Now() - reader, err := ingest.NewLedgerTransactionReaderFromLedgerCloseMeta(m.networkPassphrase, ledgerCloseMeta) - if err != nil { - return err - } - - txCount := ledgerCloseMeta.CountTransactions() - transactions := make([]transaction, txCount) - hashes := make([]xdr.Hash, 0, txCount) - hashMap := map[xdr.Hash]transaction{} - var bucket ledgerbucketwindow.LedgerBucket[[]xdr.Hash] - - for i := 0; i < txCount; i++ { - tx, err := reader.Read() - if err != nil { - return err - } - transactions[i] = transaction{ - bucket: &bucket, - feeBump: tx.Envelope.IsFeeBump(), - applicationOrder: int32(tx.Index), - successful: tx.Result.Result.Successful(), - } - if transactions[i].result, err = tx.Result.Result.MarshalBinary(); err != nil { - return err - } - if transactions[i].meta, err = tx.UnsafeMeta.MarshalBinary(); err != nil { - return err - } - if transactions[i].envelope, err = tx.Envelope.MarshalBinary(); err != nil { - return err - } - if transactions[i].feeBump { - innerHash := tx.Result.InnerHash() - hashMap[innerHash] = transactions[i] - hashes = append(hashes, innerHash) - } - hashMap[tx.Result.TransactionHash] = transactions[i] - hashes = append(hashes, tx.Result.TransactionHash) - } - bucket = ledgerbucketwindow.LedgerBucket[[]xdr.Hash]{ - LedgerSeq: ledgerCloseMeta.LedgerSequence(), - LedgerCloseTimestamp: int64(ledgerCloseMeta.LedgerHeaderHistoryEntry().Header.ScpValue.CloseTime), - BucketContent: hashes, - } - - m.lock.Lock() - defer m.lock.Unlock() - evicted, err := m.transactionsByLedger.Append(bucket) - if err != nil { - return err - } - if evicted != nil { - // garbage-collect evicted entries - for _, evictedTxHash := range evicted.BucketContent { - delete(m.transactions, evictedTxHash) - } - } - for hash, tx := range hashMap { - m.transactions[hash] = tx - } - m.transactionDurationMetric.With(prometheus.Labels{"operation": "ingest"}).Observe(time.Since(startTime).Seconds()) - m.transactionCountMetric.Observe(float64(txCount)) - return nil -} - -type LedgerInfo struct { - Sequence uint32 - CloseTime int64 -} - -type Transaction struct { - Result []byte // XDR encoded xdr.TransactionResult - Meta []byte // XDR encoded xdr.TransactionMeta - Envelope []byte // XDR encoded xdr.TransactionEnvelope - FeeBump bool - ApplicationOrder int32 - Successful bool - Ledger LedgerInfo -} - -type StoreRange struct { - FirstLedger LedgerInfo - LastLedger LedgerInfo -} - -// GetLatestLedger returns the latest ledger available in the store. -func (m *MemoryStore) GetLatestLedger() LedgerInfo { - m.lock.RLock() - defer m.lock.RUnlock() - if m.transactionsByLedger.Len() > 0 { - lastBucket := m.transactionsByLedger.Get(m.transactionsByLedger.Len() - 1) - return LedgerInfo{ - Sequence: lastBucket.LedgerSeq, - CloseTime: lastBucket.LedgerCloseTimestamp, - } - } - return LedgerInfo{} -} - -// GetTransaction obtains a transaction from the store and whether it's present and the current store range -func (m *MemoryStore) GetTransaction(hash xdr.Hash) (Transaction, bool, StoreRange) { - startTime := time.Now() - m.lock.RLock() - defer m.lock.RUnlock() - var storeRange StoreRange - if m.transactionsByLedger.Len() > 0 { - firstBucket := m.transactionsByLedger.Get(0) - lastBucket := m.transactionsByLedger.Get(m.transactionsByLedger.Len() - 1) - storeRange = StoreRange{ - FirstLedger: LedgerInfo{ - Sequence: firstBucket.LedgerSeq, - CloseTime: firstBucket.LedgerCloseTimestamp, - }, - LastLedger: LedgerInfo{ - Sequence: lastBucket.LedgerSeq, - CloseTime: lastBucket.LedgerCloseTimestamp, - }, - } - } - internalTx, ok := m.transactions[hash] - if !ok { - return Transaction{}, false, storeRange - } - tx := Transaction{ - Result: internalTx.result, - Meta: internalTx.meta, - Envelope: internalTx.envelope, - FeeBump: internalTx.feeBump, - Successful: internalTx.successful, - ApplicationOrder: internalTx.applicationOrder, - Ledger: LedgerInfo{ - Sequence: internalTx.bucket.LedgerSeq, - CloseTime: internalTx.bucket.LedgerCloseTimestamp, - }, - } - - m.transactionDurationMetric.With(prometheus.Labels{"operation": "get"}).Observe(time.Since(startTime).Seconds()) - return tx, true, storeRange -} diff --git a/cmd/soroban-rpc/internal/transactions/transactions_test.go b/cmd/soroban-rpc/internal/transactions/transactions_test.go deleted file mode 100644 index d32a62c6d..000000000 --- a/cmd/soroban-rpc/internal/transactions/transactions_test.go +++ /dev/null @@ -1,377 +0,0 @@ -package transactions - -import ( - "encoding/hex" - "fmt" - "math" - "runtime" - "testing" - "time" - - "github.com/stellar/go/network" - "github.com/stellar/go/xdr" - "github.com/stretchr/testify/require" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon/interfaces" -) - -func expectedTransaction(t *testing.T, ledger uint32, feeBump bool) Transaction { - tx := Transaction{ - FeeBump: feeBump, - ApplicationOrder: 1, - Ledger: expectedLedgerInfo(ledger), - } - var err error - tx.Result, err = transactionResult(ledger, feeBump).MarshalBinary() - require.NoError(t, err) - tx.Meta, err = xdr.TransactionMeta{ - V: 3, - Operations: &[]xdr.OperationMeta{}, - V3: &xdr.TransactionMetaV3{}, - }.MarshalBinary() - require.NoError(t, err) - tx.Envelope, err = txEnvelope(ledger, feeBump).MarshalBinary() - require.NoError(t, err) - return tx -} - -func expectedLedgerInfo(ledgerSequence uint32) LedgerInfo { - return LedgerInfo{ - Sequence: ledgerSequence, - CloseTime: ledgerCloseTime(ledgerSequence), - } - -} - -func expectedStoreRange(startLedger uint32, endLedger uint32) StoreRange { - return StoreRange{ - FirstLedger: expectedLedgerInfo(startLedger), - LastLedger: expectedLedgerInfo(endLedger), - } -} - -func txHash(ledgerSequence uint32, feebump bool) xdr.Hash { - envelope := txEnvelope(ledgerSequence, feebump) - hash, err := network.HashTransactionInEnvelope(envelope, "passphrase") - if err != nil { - panic(err) - } - - return hash -} - -func ledgerCloseTime(ledgerSequence uint32) int64 { - return int64(ledgerSequence)*25 + 100 -} - -func transactionResult(ledgerSequence uint32, feeBump bool) xdr.TransactionResult { - if feeBump { - return xdr.TransactionResult{ - FeeCharged: 100, - Result: xdr.TransactionResultResult{ - Code: xdr.TransactionResultCodeTxFeeBumpInnerFailed, - InnerResultPair: &xdr.InnerTransactionResultPair{ - TransactionHash: txHash(ledgerSequence, false), - Result: xdr.InnerTransactionResult{ - Result: xdr.InnerTransactionResultResult{ - Code: xdr.TransactionResultCodeTxBadSeq, - }, - }, - }, - }, - } - } - return xdr.TransactionResult{ - FeeCharged: 100, - Result: xdr.TransactionResultResult{ - Code: xdr.TransactionResultCodeTxBadSeq, - }, - } -} - -func txMeta(ledgerSequence uint32, feeBump bool) xdr.LedgerCloseMeta { - envelope := txEnvelope(ledgerSequence, feeBump) - persistentKey := xdr.ScSymbol("TEMPVAL") - contractIDBytes, _ := hex.DecodeString("df06d62447fd25da07c0135eed7557e5a5497ee7d15b7fe345bd47e191d8f577") - var contractID xdr.Hash - copy(contractID[:], contractIDBytes) - contractAddress := xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractID, - } - xdrTrue := true - operationChanges := xdr.LedgerEntryChanges{ - { - Type: xdr.LedgerEntryChangeTypeLedgerEntryState, - State: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: xdr.Uint32(ledgerSequence - 1), - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.ContractDataEntry{ - Contract: contractAddress, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &persistentKey, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvBool, - B: &xdrTrue, - }, - }, - }, - }, - }, - { - Type: xdr.LedgerEntryChangeTypeLedgerEntryUpdated, - Updated: &xdr.LedgerEntry{ - LastModifiedLedgerSeq: xdr.Uint32(ledgerSequence - 1), - Data: xdr.LedgerEntryData{ - Type: xdr.LedgerEntryTypeContractData, - ContractData: &xdr.ContractDataEntry{ - Contract: xdr.ScAddress{ - Type: xdr.ScAddressTypeScAddressTypeContract, - ContractId: &contractID, - }, - Key: xdr.ScVal{ - Type: xdr.ScValTypeScvSymbol, - Sym: &persistentKey, - }, - Durability: xdr.ContractDataDurabilityPersistent, - Val: xdr.ScVal{ - Type: xdr.ScValTypeScvBool, - B: &xdrTrue, - }, - }, - }, - }, - }, - } - txProcessing := []xdr.TransactionResultMeta{ - { - TxApplyProcessing: xdr.TransactionMeta{ - V: 3, - Operations: &[]xdr.OperationMeta{ - { - Changes: operationChanges, - }, - }, - V3: &xdr.TransactionMetaV3{}, - }, - Result: xdr.TransactionResultPair{ - TransactionHash: txHash(ledgerSequence, feeBump), - Result: transactionResult(ledgerSequence, feeBump), - }, - }, - } - - components := []xdr.TxSetComponent{ - { - Type: xdr.TxSetComponentTypeTxsetCompTxsMaybeDiscountedFee, - TxsMaybeDiscountedFee: &xdr.TxSetComponentTxsMaybeDiscountedFee{ - BaseFee: nil, - Txs: []xdr.TransactionEnvelope{ - envelope, - }, - }, - }, - } - return xdr.LedgerCloseMeta{ - V: 1, - V1: &xdr.LedgerCloseMetaV1{ - LedgerHeader: xdr.LedgerHeaderHistoryEntry{ - Header: xdr.LedgerHeader{ - ScpValue: xdr.StellarValue{ - CloseTime: xdr.TimePoint(ledgerCloseTime(ledgerSequence)), - }, - LedgerSeq: xdr.Uint32(ledgerSequence), - }, - }, - TxProcessing: txProcessing, - TxSet: xdr.GeneralizedTransactionSet{ - V: 1, - V1TxSet: &xdr.TransactionSetV1{ - PreviousLedgerHash: xdr.Hash{1}, - Phases: []xdr.TransactionPhase{ - { - V: 0, - V0Components: &components, - }, - }, - }, - }, - }, - } -} - -func txEnvelope(ledgerSequence uint32, feeBump bool) xdr.TransactionEnvelope { - var envelope xdr.TransactionEnvelope - var err error - if feeBump { - envelope, err = xdr.NewTransactionEnvelope(xdr.EnvelopeTypeEnvelopeTypeTxFeeBump, xdr.FeeBumpTransactionEnvelope{ - Tx: xdr.FeeBumpTransaction{ - Fee: 10, - FeeSource: xdr.MustMuxedAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"), - InnerTx: xdr.FeeBumpTransactionInnerTx{ - Type: xdr.EnvelopeTypeEnvelopeTypeTx, - V1: &xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - Fee: 1, - SeqNum: xdr.SequenceNumber(ledgerSequence + 90), - SourceAccount: xdr.MustMuxedAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"), - }, - }, - }, - }, - }) - } else { - envelope, err = xdr.NewTransactionEnvelope(xdr.EnvelopeTypeEnvelopeTypeTx, xdr.TransactionV1Envelope{ - Tx: xdr.Transaction{ - Fee: 1, - SeqNum: xdr.SequenceNumber(ledgerSequence + 90), - SourceAccount: xdr.MustMuxedAddress("MA7QYNF7SOWQ3GLR2BGMZEHXAVIRZA4KVWLTJJFC7MGXUA74P7UJVAAAAAAAAAAAAAJLK"), - }, - }) - } - if err != nil { - panic(err) - } - return envelope -} - -func requirePresent(t *testing.T, store *MemoryStore, feeBump bool, ledgerSequence, firstSequence, lastSequence uint32) { - tx, ok, storeRange := store.GetTransaction(txHash(ledgerSequence, false)) - require.True(t, ok) - require.Equal(t, expectedTransaction(t, ledgerSequence, feeBump), tx) - require.Equal(t, expectedStoreRange(firstSequence, lastSequence), storeRange) - if feeBump { - tx, ok, storeRange = store.GetTransaction(txHash(ledgerSequence, true)) - require.True(t, ok) - require.Equal(t, expectedTransaction(t, ledgerSequence, feeBump), tx) - require.Equal(t, expectedStoreRange(firstSequence, lastSequence), storeRange) - } -} - -func TestIngestTransactions(t *testing.T) { - // Use a small retention window to test eviction - store := NewMemoryStore(interfaces.MakeNoOpDeamon(), "passphrase", 3) - - _, ok, storeRange := store.GetTransaction(txHash(1, false)) - require.False(t, ok) - require.Equal(t, StoreRange{}, storeRange) - - // Insert ledger 1 - require.NoError(t, store.IngestTransactions(txMeta(1, false))) - requirePresent(t, store, false, 1, 1, 1) - require.Len(t, store.transactions, 1) - - // Insert ledger 2 - require.NoError(t, store.IngestTransactions(txMeta(2, true))) - requirePresent(t, store, false, 1, 1, 2) - requirePresent(t, store, true, 2, 1, 2) - require.Len(t, store.transactions, 3) - - // Insert ledger 3 - require.NoError(t, store.IngestTransactions(txMeta(3, false))) - requirePresent(t, store, false, 1, 1, 3) - requirePresent(t, store, true, 2, 1, 3) - requirePresent(t, store, false, 3, 1, 3) - require.Len(t, store.transactions, 4) - - // Now we have filled the memory store - - // Insert ledger 4, which will cause the window to move and evict ledger 1 - require.NoError(t, store.IngestTransactions(txMeta(4, false))) - requirePresent(t, store, true, 2, 2, 4) - requirePresent(t, store, false, 3, 2, 4) - requirePresent(t, store, false, 4, 2, 4) - - _, ok, storeRange = store.GetTransaction(txHash(1, false)) - require.False(t, ok) - require.Equal(t, expectedStoreRange(2, 4), storeRange) - require.Equal(t, uint32(3), store.transactionsByLedger.Len()) - require.Len(t, store.transactions, 4) - - // Insert ledger 5, which will cause the window to move and evict ledger 2 - require.NoError(t, store.IngestTransactions(txMeta(5, false))) - requirePresent(t, store, false, 3, 3, 5) - requirePresent(t, store, false, 4, 3, 5) - requirePresent(t, store, false, 5, 3, 5) - - _, ok, storeRange = store.GetTransaction(txHash(2, false)) - require.False(t, ok) - require.Equal(t, expectedStoreRange(3, 5), storeRange) - require.Equal(t, uint32(3), store.transactionsByLedger.Len()) - require.Len(t, store.transactions, 3) - - _, ok, storeRange = store.GetTransaction(txHash(2, true)) - require.False(t, ok) - require.Equal(t, expectedStoreRange(3, 5), storeRange) - require.Equal(t, uint32(3), store.transactionsByLedger.Len()) - require.Len(t, store.transactions, 3) -} - -func stableHeapInUse() int64 { - var ( - m = runtime.MemStats{} - prevInUse uint64 - prevNumGC uint32 - ) - - for { - runtime.GC() - - // Sleeping to allow GC to run a few times and collect all temporary data. - time.Sleep(100 * time.Millisecond) - - runtime.ReadMemStats(&m) - - // Considering heap stable if recent cycle collected less than 10KB. - if prevNumGC != 0 && m.NumGC > prevNumGC && math.Abs(float64(m.HeapInuse-prevInUse)) < 10*1024 { - break - } - - prevInUse = m.HeapInuse - prevNumGC = m.NumGC - } - - return int64(m.HeapInuse) -} - -func byteCountBinary(b int64) string { - const unit = 1024 - if b < unit { - return fmt.Sprintf("%d B", b) - } - div, exp := int64(unit), 0 - for n := b / unit; n >= unit; n /= unit { - div *= unit - exp++ - } - return fmt.Sprintf("%.1f %ciB", float64(b)/float64(div), "KMGTPE"[exp]) -} - -func BenchmarkIngestTransactionsMemory(b *testing.B) { - roundsNumber := uint32(b.N * 100000) - // Use a small retention window to test eviction - store := NewMemoryStore(interfaces.MakeNoOpDeamon(), "passphrase", roundsNumber) - - heapSizeBefore := stableHeapInUse() - - for i := uint32(0); i < roundsNumber; i++ { - // Insert ledger i - require.NoError(b, store.IngestTransactions(txMeta(i, false))) - } - heapSizeAfter := stableHeapInUse() - b.ReportMetric(float64(heapSizeAfter), "bytes/100k_transactions") - b.Logf("Memory consumption for %d transactions %v", roundsNumber, byteCountBinary(heapSizeAfter-heapSizeBefore)) - - // we want to generate 500*20000 transactions total, to cover the expected daily amount of transactions. - projectedTransactionCount := int64(500 * 20000) - projectedMemoryUtiliztion := (heapSizeAfter - heapSizeBefore) * projectedTransactionCount / int64(roundsNumber) - b.Logf("Projected memory consumption for %d transactions %v", projectedTransactionCount, byteCountBinary(projectedMemoryUtiliztion)) - b.ReportMetric(float64(projectedMemoryUtiliztion), "bytes/10M_transactions") - - // add another call to store to prevent the GC from collecting. - store.GetTransaction(xdr.Hash{}) -} diff --git a/cmd/soroban-rpc/internal/util/panicgroup.go b/cmd/soroban-rpc/internal/util/panicgroup.go deleted file mode 100644 index b131e91c6..000000000 --- a/cmd/soroban-rpc/internal/util/panicgroup.go +++ /dev/null @@ -1,116 +0,0 @@ -package util - -import ( - "fmt" - "os" - "reflect" - "runtime" - "runtime/debug" - "strings" - - "github.com/prometheus/client_golang/prometheus" - "github.com/stellar/go/support/log" -) - -var UnrecoverablePanicGroup = panicGroup{ - logPanicsToStdErr: true, - exitProcessOnPanic: true, -} - -var RecoverablePanicGroup = panicGroup{ - logPanicsToStdErr: true, - exitProcessOnPanic: false, -} - -type panicGroup struct { - log *log.Entry - logPanicsToStdErr bool - exitProcessOnPanic bool - panicsCounter prometheus.Counter -} - -func (pg *panicGroup) Log(log *log.Entry) *panicGroup { - return &panicGroup{ - log: log, - logPanicsToStdErr: pg.logPanicsToStdErr, - exitProcessOnPanic: pg.exitProcessOnPanic, - panicsCounter: pg.panicsCounter, - } -} - -func (pg *panicGroup) Counter(counter prometheus.Counter) *panicGroup { - return &panicGroup{ - log: pg.log, - logPanicsToStdErr: pg.logPanicsToStdErr, - exitProcessOnPanic: pg.exitProcessOnPanic, - panicsCounter: counter, - } -} - -// panicGroup give us the ability to spin a goroutine, with clear upfront definitions on what should be done in the -// case of an internal panic. -func (pg *panicGroup) Go(fn func()) { - go func() { - defer pg.recoverRoutine(fn) - fn() - }() -} - -func (pg *panicGroup) recoverRoutine(fn func()) { - recoverRes := recover() - if recoverRes == nil { - return - } - cs := getPanicCallStack(recoverRes, fn) - if len(cs) <= 0 { - return - } - if pg.log != nil { - for _, line := range cs { - pg.log.Warn(line) - } - } - if pg.logPanicsToStdErr { - for _, line := range cs { - fmt.Fprintln(os.Stderr, line) - } - } - - if pg.panicsCounter != nil { - pg.panicsCounter.Inc() - } - if pg.exitProcessOnPanic { - os.Exit(1) - } -} - -func getPanicCallStack(recoverRes any, fn func()) (outCallStack []string) { - functionName := runtime.FuncForPC(reflect.ValueOf(fn).Pointer()).Name() - return CallStack(recoverRes, functionName, "(*panicGroup).Go", 10) -} - -// CallStack returns an array of strings representing the current call stack. The method is -// tuned for the purpose of panic handler, and used as a helper in contructing the list of entries we want -// to write to the log / stderr / telemetry. -func CallStack(recoverRes any, topLevelFunctionName string, lastCallstackMethod string, unwindStackLines int) (callStack []string) { - if topLevelFunctionName != "" { - callStack = append(callStack, fmt.Sprintf("%v when calling %v", recoverRes, topLevelFunctionName)) - } else { - callStack = append(callStack, fmt.Sprintf("%v", recoverRes)) - } - // while we're within the recoverRoutine, the debug.Stack() would return the - // call stack where the panic took place. - callStackStrings := string(debug.Stack()) - for i, callStackLine := range strings.FieldsFunc(callStackStrings, func(r rune) bool { return r == '\n' || r == '\t' }) { - // skip the first (unwindStackLines) entries, since these are the "debug.Stack()" entries, which aren't really useful. - if i < unwindStackLines { - continue - } - callStack = append(callStack, callStackLine) - // once we reached the limiter entry, stop. - if strings.Contains(callStackLine, lastCallstackMethod) { - break - } - } - return callStack -} diff --git a/cmd/soroban-rpc/internal/util/panicgroup_test.go b/cmd/soroban-rpc/internal/util/panicgroup_test.go deleted file mode 100644 index 63c42206a..000000000 --- a/cmd/soroban-rpc/internal/util/panicgroup_test.go +++ /dev/null @@ -1,111 +0,0 @@ -package util - -import ( - "os" - "sync" - "testing" - "time" - - "github.com/sirupsen/logrus" - "github.com/stellar/go/support/log" - "github.com/stretchr/testify/require" -) - -func TestTrivialPanicGroup(t *testing.T) { - ch := make(chan int) - - panicGroup := panicGroup{} - panicGroup.Go(func() { ch <- 1 }) - - <-ch -} - -type TestLogsCounter struct { - entry *log.Entry - mu sync.Mutex - writtenLogEntries [logrus.TraceLevel + 1]int -} - -func makeTestLogCounter() *TestLogsCounter { - out := &TestLogsCounter{ - entry: log.New(), - } - out.entry.AddHook(out) - out.entry.SetLevel(logrus.DebugLevel) - return out -} -func (te *TestLogsCounter) Entry() *log.Entry { - return te.entry -} -func (te *TestLogsCounter) Levels() []logrus.Level { - return []logrus.Level{logrus.PanicLevel, logrus.FatalLevel, logrus.ErrorLevel, logrus.WarnLevel, logrus.InfoLevel, logrus.DebugLevel, logrus.TraceLevel} -} -func (te *TestLogsCounter) Fire(e *logrus.Entry) error { - te.mu.Lock() - defer te.mu.Unlock() - te.writtenLogEntries[e.Level]++ - return nil -} -func (te *TestLogsCounter) GetLevel(i int) int { - te.mu.Lock() - defer te.mu.Unlock() - return te.writtenLogEntries[i] -} - -func PanicingFunctionA(w *int) { - *w = 0 -} - -func IndirectPanicingFunctionB() { - PanicingFunctionA(nil) -} - -func IndirectPanicingFunctionC() { - IndirectPanicingFunctionB() -} - -func TestPanicGroupLog(t *testing.T) { - logCounter := makeTestLogCounter() - panicGroup := panicGroup{ - log: logCounter.Entry(), - } - panicGroup.Go(IndirectPanicingFunctionC) - // wait until we get all the log entries. - waitStarted := time.Now() - for time.Since(waitStarted) < 5*time.Second { - warningCount := logCounter.GetLevel(3) - if warningCount >= 9 { - return - } - time.Sleep(1 * time.Millisecond) - } - t.FailNow() -} - -func TestPanicGroupStdErr(t *testing.T) { - tmpFile, err := os.CreateTemp("", "TestPanicGroupStdErr") - require.NoError(t, err) - defaultStdErr := os.Stderr - os.Stderr = tmpFile - defer func() { - os.Stderr = defaultStdErr - tmpFile.Close() - os.Remove(tmpFile.Name()) - }() - - panicGroup := panicGroup{ - logPanicsToStdErr: true, - } - panicGroup.Go(IndirectPanicingFunctionC) - // wait until we get all the log entries. - waitStarted := time.Now() - for time.Since(waitStarted) < 5*time.Second { - outErrBytes, err := os.ReadFile(tmpFile.Name()) - require.NoError(t, err) - if len(outErrBytes) >= 100 { - return - } - time.Sleep(1 * time.Millisecond) - } - t.FailNow() -} diff --git a/cmd/soroban-rpc/lib/preflight.h b/cmd/soroban-rpc/lib/preflight.h deleted file mode 100644 index 81db0c54f..000000000 --- a/cmd/soroban-rpc/lib/preflight.h +++ /dev/null @@ -1,65 +0,0 @@ -// NOTE: You could use https://michael-f-bryan.github.io/rust-ffi-guide/cbindgen.html to generate -// this header automatically from your Rust code. But for now, we'll just write it by hand. - -#include -#include - -typedef struct ledger_info_t { - uint32_t protocol_version; - uint32_t sequence_number; - uint64_t timestamp; - const char *network_passphrase; - uint32_t base_reserve; - uint32_t min_temp_entry_ttl; - uint32_t min_persistent_entry_ttl; - uint32_t max_entry_ttl; -} ledger_info_t; - -typedef struct xdr_t { - unsigned char *xdr; - size_t len; -} xdr_t; - -typedef struct xdr_vector_t { - xdr_t *array; - size_t len; -} xdr_vector_t; - -typedef struct resource_config_t { - uint64_t instruction_leeway; // Allow this many extra instructions when budgeting -} resource_config_t; - -typedef struct preflight_result_t { - char *error; // Error string in case of error, otherwise null - xdr_vector_t auth; // array of SorobanAuthorizationEntries - xdr_t result; // XDR SCVal - xdr_t transaction_data; - int64_t min_fee; // Minimum recommended resource fee - xdr_vector_t events; // array of XDR DiagnosticEvents - uint64_t cpu_instructions; - uint64_t memory_bytes; - xdr_t pre_restore_transaction_data; // SorobanTransactionData XDR for a prerequired RestoreFootprint operation - int64_t pre_restore_min_fee; // Minimum recommended resource fee for a prerequired RestoreFootprint operation -} preflight_result_t; - -preflight_result_t *preflight_invoke_hf_op(uintptr_t handle, // Go Handle to forward to SnapshotSourceGet - uint64_t bucket_list_size, // Bucket list size of current ledger - const xdr_t invoke_hf_op, // InvokeHostFunctionOp XDR - const xdr_t source_account, // AccountId XDR - const ledger_info_t ledger_info, - const resource_config_t resource_config, - bool enable_debug); - -preflight_result_t *preflight_footprint_ttl_op(uintptr_t handle, // Go Handle to forward to SnapshotSourceGet - uint64_t bucket_list_size, // Bucket list size of current ledger - const xdr_t op_body, // OperationBody XDR - const xdr_t footprint, // LedgerFootprint XDR - uint32_t current_ledger_seq); // Current ledger sequence - - -// LedgerKey XDR to LedgerEntry XDR -extern xdr_t SnapshotSourceGet(uintptr_t handle, xdr_t ledger_key); - -void free_preflight_result(preflight_result_t *result); - -extern void FreeGoXDR(xdr_t xdr); diff --git a/cmd/soroban-rpc/lib/preflight/Cargo.toml b/cmd/soroban-rpc/lib/preflight/Cargo.toml deleted file mode 100644 index 418f8c190..000000000 --- a/cmd/soroban-rpc/lib/preflight/Cargo.toml +++ /dev/null @@ -1,16 +0,0 @@ -[package] -name = "preflight" -version = "20.3.0" -publish = false - -[lib] -crate-type = ["staticlib"] - -[dependencies] -base64 = { workspace = true } -libc = "0.2.147" -sha2 = { workspace = true } -# we need the testutils feature in order to get backtraces in the preflight library -# when soroban rpc is configured to run with --preflight-enable-debug -soroban-env-host = { workspace = true, features = ["recording_mode", "testutils"]} -soroban-simulation = { workspace = true } diff --git a/cmd/soroban-rpc/lib/preflight/src/lib.rs b/cmd/soroban-rpc/lib/preflight/src/lib.rs deleted file mode 100644 index 25746e745..000000000 --- a/cmd/soroban-rpc/lib/preflight/src/lib.rs +++ /dev/null @@ -1,460 +0,0 @@ -extern crate base64; -extern crate libc; -extern crate sha2; -extern crate soroban_env_host; -extern crate soroban_simulation; - -use sha2::{Digest, Sha256}; -use soroban_env_host::xdr::{ - AccountId, Hash, InvokeHostFunctionOp, LedgerEntry, LedgerEntryData, LedgerFootprint, - LedgerKey, LedgerKeyTtl, Limits, OperationBody, ReadXdr, TtlEntry, WriteXdr, -}; -use soroban_env_host::LedgerInfo; -use soroban_simulation::{ledger_storage, ResourceConfig}; -use soroban_simulation::{ - simulate_footprint_ttl_op, simulate_invoke_hf_op, LedgerStorage, SimulationResult, -}; -use std::error::Error; -use std::ffi::{CStr, CString}; -use std::panic; -use std::ptr::null_mut; -use std::{mem, slice}; - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct CLedgerInfo { - pub protocol_version: u32, - pub sequence_number: u32, - pub timestamp: u64, - pub network_passphrase: *const libc::c_char, - pub base_reserve: u32, - pub min_temp_entry_ttl: u32, - pub min_persistent_entry_ttl: u32, - pub max_entry_ttl: u32, -} - -impl From for LedgerInfo { - fn from(c: CLedgerInfo) -> Self { - let network_passphrase = from_c_string(c.network_passphrase); - Self { - protocol_version: c.protocol_version, - sequence_number: c.sequence_number, - timestamp: c.timestamp, - network_id: Sha256::digest(network_passphrase).into(), - base_reserve: c.base_reserve, - min_temp_entry_ttl: c.min_temp_entry_ttl, - min_persistent_entry_ttl: c.min_persistent_entry_ttl, - max_entry_ttl: c.max_entry_ttl, - } - } -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct CXDR { - pub xdr: *mut libc::c_uchar, - pub len: libc::size_t, -} - -// It would be nicer to derive Default, but we can't. It errors with: -// The trait bound `*mut u8: std::default::Default` is not satisfied -fn get_default_c_xdr() -> CXDR { - CXDR { - xdr: null_mut(), - len: 0, - } -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct CXDRVector { - pub array: *mut CXDR, - pub len: libc::size_t, -} - -fn get_default_c_xdr_vector() -> CXDRVector { - CXDRVector { - array: null_mut(), - len: 0, - } -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct CResourceConfig { - pub instruction_leeway: u64, -} - -impl From for ResourceConfig { - fn from(r: CResourceConfig) -> Self { - return ResourceConfig { - instruction_leeway: r.instruction_leeway, - }; - } -} - -#[repr(C)] -#[derive(Copy, Clone)] -pub struct CPreflightResult { - // Error string in case of error, otherwise null - pub error: *mut libc::c_char, - // Error string in case of error, otherwise null - pub auth: CXDRVector, - // XDR SCVal - pub result: CXDR, - // SorobanTransactionData XDR - pub transaction_data: CXDR, - // Minimum recommended resource fee - pub min_fee: i64, - // array of XDR ContractEvents - pub events: CXDRVector, - pub cpu_instructions: u64, - pub memory_bytes: u64, - // SorobanTransactionData XDR for a prerequired RestoreFootprint operation - pub pre_restore_transaction_data: CXDR, - // Minimum recommended resource fee for a prerequired RestoreFootprint operation - pub pre_restore_min_fee: i64, -} - -impl From for CPreflightResult { - fn from(s: SimulationResult) -> Self { - let mut result = Self { - error: string_to_c(s.error), - auth: xdr_vec_to_c(s.auth), - result: option_xdr_to_c(s.result), - transaction_data: option_xdr_to_c(s.transaction_data), - min_fee: s.min_fee, - events: xdr_vec_to_c(s.events), - cpu_instructions: s.cpu_instructions, - memory_bytes: s.memory_bytes, - pre_restore_transaction_data: get_default_c_xdr(), - pre_restore_min_fee: 0, - }; - if let Some(p) = s.restore_preamble { - result.pre_restore_min_fee = p.min_fee; - result.pre_restore_transaction_data = xdr_to_c(p.transaction_data); - }; - result - } -} - -#[no_mangle] -pub extern "C" fn preflight_invoke_hf_op( - handle: libc::uintptr_t, // Go Handle to forward to SnapshotSourceGet and SnapshotSourceHas - bucket_list_size: u64, // Bucket list size for current ledger - invoke_hf_op: CXDR, // InvokeHostFunctionOp XDR in base64 - source_account: CXDR, // AccountId XDR in base64 - ledger_info: CLedgerInfo, - resource_config: CResourceConfig, - enable_debug: bool, -) -> *mut CPreflightResult { - catch_preflight_panic(Box::new(move || { - preflight_invoke_hf_op_or_maybe_panic( - handle, - bucket_list_size, - invoke_hf_op, - source_account, - ledger_info, - resource_config, - enable_debug, - ) - })) -} - -fn preflight_invoke_hf_op_or_maybe_panic( - handle: libc::uintptr_t, - bucket_list_size: u64, // Go Handle to forward to SnapshotSourceGet and SnapshotSourceHas - invoke_hf_op: CXDR, // InvokeHostFunctionOp XDR in base64 - source_account: CXDR, // AccountId XDR in base64 - ledger_info: CLedgerInfo, - resource_config: CResourceConfig, - enable_debug: bool, -) -> Result> { - let invoke_hf_op = - InvokeHostFunctionOp::from_xdr(from_c_xdr(invoke_hf_op), Limits::none()).unwrap(); - let source_account = AccountId::from_xdr(from_c_xdr(source_account), Limits::none()).unwrap(); - let go_storage = GoLedgerStorage { - golang_handle: handle, - current_ledger_sequence: ledger_info.sequence_number, - }; - let ledger_storage = - LedgerStorage::with_restore_tracking(Box::new(go_storage), ledger_info.sequence_number)?; - let result = simulate_invoke_hf_op( - ledger_storage, - bucket_list_size, - invoke_hf_op, - source_account, - LedgerInfo::from(ledger_info), - resource_config.into(), - enable_debug, - ); - match result { - Ok(r) => Ok(r.into()), - Err(e) => Err(e), - } -} - -#[no_mangle] -pub extern "C" fn preflight_footprint_ttl_op( - handle: libc::uintptr_t, // Go Handle to forward to SnapshotSourceGet and SnapshotSourceHas - bucket_list_size: u64, // Bucket list size for current ledger - op_body: CXDR, // OperationBody XDR - footprint: CXDR, // LedgerFootprint XDR - current_ledger_seq: u32, -) -> *mut CPreflightResult { - catch_preflight_panic(Box::new(move || { - preflight_footprint_ttl_op_or_maybe_panic( - handle, - bucket_list_size, - op_body, - footprint, - current_ledger_seq, - ) - })) -} - -fn preflight_footprint_ttl_op_or_maybe_panic( - handle: libc::uintptr_t, - bucket_list_size: u64, - op_body: CXDR, - footprint: CXDR, - current_ledger_seq: u32, -) -> Result> { - let op_body = OperationBody::from_xdr(from_c_xdr(op_body), Limits::none()).unwrap(); - let footprint = LedgerFootprint::from_xdr(from_c_xdr(footprint), Limits::none()).unwrap(); - let go_storage = GoLedgerStorage { - golang_handle: handle, - current_ledger_sequence: current_ledger_seq, - }; - let ledger_storage = &LedgerStorage::new(Box::new(go_storage), current_ledger_seq); - let result = simulate_footprint_ttl_op( - ledger_storage, - bucket_list_size, - op_body, - footprint, - current_ledger_seq, - ); - match result { - Ok(r) => Ok(r.into()), - Err(e) => Err(e), - } -} - -fn preflight_error(str: String) -> CPreflightResult { - let c_str = CString::new(str).unwrap(); - CPreflightResult { - error: c_str.into_raw(), - auth: get_default_c_xdr_vector(), - result: get_default_c_xdr(), - transaction_data: get_default_c_xdr(), - min_fee: 0, - events: get_default_c_xdr_vector(), - cpu_instructions: 0, - memory_bytes: 0, - pre_restore_transaction_data: get_default_c_xdr(), - pre_restore_min_fee: 0, - } -} - -fn catch_preflight_panic( - op: Box Result>>, -) -> *mut CPreflightResult { - // catch panics before they reach foreign callers (which otherwise would result in - // undefined behavior) - let res: std::thread::Result>> = - panic::catch_unwind(panic::AssertUnwindSafe(op)); - let c_preflight_result = match res { - Err(panic) => match panic.downcast::() { - Ok(panic_msg) => preflight_error(format!("panic during preflight() call: {panic_msg}")), - Err(_) => preflight_error("panic during preflight() call: unknown cause".to_string()), - }, - // See https://docs.rs/anyhow/latest/anyhow/struct.Error.html#display-representations - Ok(r) => r.unwrap_or_else(|e| preflight_error(format!("{e:?}"))), - }; - // transfer ownership to caller - // caller needs to invoke free_preflight_result(result) when done - Box::into_raw(Box::new(c_preflight_result)) -} - -fn xdr_to_c(v: impl WriteXdr) -> CXDR { - let (xdr, len) = vec_to_c_array(v.to_xdr(Limits::none()).unwrap()); - CXDR { xdr, len } -} - -fn option_xdr_to_c(v: Option) -> CXDR { - v.map_or( - CXDR { - xdr: null_mut(), - len: 0, - }, - xdr_to_c, - ) -} - -fn xdr_vec_to_c(v: Vec) -> CXDRVector { - let c_v = v.into_iter().map(xdr_to_c).collect(); - let (array, len) = vec_to_c_array(c_v); - CXDRVector { array, len } -} - -fn string_to_c(str: String) -> *mut libc::c_char { - CString::new(str).unwrap().into_raw() -} - -fn vec_to_c_array(mut v: Vec) -> (*mut T, libc::size_t) { - // Make sure length and capacity are the same - // (this allows using the length as the capacity when deallocating the vector) - v.shrink_to_fit(); - let len = v.len(); - assert_eq!(len, v.capacity()); - - // Get the pointer to our vector, we will deallocate it in free_c_null_terminated_char_array() - // TODO: replace by `out_vec.into_raw_parts()` once the API stabilizes - let ptr = v.as_mut_ptr(); - mem::forget(v); - - (ptr, len) -} - -/// . -/// -/// # Safety -/// -/// . -#[no_mangle] -pub unsafe extern "C" fn free_preflight_result(result: *mut CPreflightResult) { - if result.is_null() { - return; - } - let boxed = Box::from_raw(result); - free_c_string(boxed.error); - free_c_xdr_array(boxed.auth); - free_c_xdr(boxed.result); - free_c_xdr(boxed.transaction_data); - free_c_xdr_array(boxed.events); - free_c_xdr(boxed.pre_restore_transaction_data); -} - -fn free_c_string(str: *mut libc::c_char) { - if str.is_null() { - return; - } - unsafe { - _ = CString::from_raw(str); - } -} - -fn free_c_xdr(xdr: CXDR) { - if xdr.xdr.is_null() { - return; - } - unsafe { - let _ = Vec::from_raw_parts(xdr.xdr, xdr.len, xdr.len); - } -} - -fn free_c_xdr_array(xdr_array: CXDRVector) { - if xdr_array.array.is_null() { - return; - } - unsafe { - let v = Vec::from_raw_parts(xdr_array.array, xdr_array.len, xdr_array.len); - for xdr in v { - free_c_xdr(xdr); - } - } -} - -fn from_c_string(str: *const libc::c_char) -> String { - let c_str = unsafe { CStr::from_ptr(str) }; - c_str.to_str().unwrap().to_string() -} - -fn from_c_xdr(xdr: CXDR) -> Vec { - let s = unsafe { slice::from_raw_parts(xdr.xdr, xdr.len) }; - s.to_vec() -} - -// Functions imported from Golang -extern "C" { - // Free Strings returned from Go functions - fn FreeGoXDR(xdr: CXDR); - // LedgerKey XDR in base64 string to LedgerEntry XDR in base64 string - fn SnapshotSourceGet(handle: libc::uintptr_t, ledger_key: CXDR) -> CXDR; -} - -struct GoLedgerStorage { - golang_handle: libc::uintptr_t, - current_ledger_sequence: u32, -} - -impl GoLedgerStorage { - // Get the XDR, regardless of ttl - fn get_xdr_internal( - &self, - key_xdr: &mut Vec, - ) -> std::result::Result, ledger_storage::Error> { - let key_c_xdr = CXDR { - xdr: key_xdr.as_mut_ptr(), - len: key_xdr.len(), - }; - let res = unsafe { SnapshotSourceGet(self.golang_handle, key_c_xdr) }; - if res.xdr.is_null() { - return Err(ledger_storage::Error::NotFound); - } - let v = from_c_xdr(res); - unsafe { FreeGoXDR(res) }; - Ok(v) - } -} - -impl ledger_storage::LedgerGetter for GoLedgerStorage { - fn get( - &self, - key: &LedgerKey, - include_not_live: bool, - ) -> std::result::Result<(LedgerEntry, Option), ledger_storage::Error> { - let mut key_xdr = key.to_xdr(Limits::none())?; - let xdr = self.get_xdr_internal(&mut key_xdr)?; - - let live_until_ledger_seq = match key { - // TODO: it would probably be more efficient to do all of this in the Go side - // (e.g. it would allow us to query multiple entries at once) - LedgerKey::ContractData(_) | LedgerKey::ContractCode(_) => { - let key_hash: [u8; 32] = Sha256::digest(key_xdr).into(); - let ttl_key = LedgerKey::Ttl(LedgerKeyTtl { - key_hash: Hash(key_hash), - }); - let mut ttl_key_xdr = ttl_key.to_xdr(Limits::none())?; - let ttl_entry_xdr = self.get_xdr_internal(&mut ttl_key_xdr)?; - let ttl_entry = LedgerEntry::from_xdr(ttl_entry_xdr, Limits::none())?; - if let LedgerEntryData::Ttl(TtlEntry { - live_until_ledger_seq, - .. - }) = ttl_entry.data - { - Some(live_until_ledger_seq) - } else { - return Err(ledger_storage::Error::UnexpectedLedgerEntryTypeForTtlKey { - ledger_entry_type: ttl_entry.data.name().to_string(), - }); - } - } - _ => None, - }; - - if !include_not_live - && live_until_ledger_seq.is_some() - && !is_live(live_until_ledger_seq.unwrap(), self.current_ledger_sequence) - { - return Err(ledger_storage::Error::NotLive); - } - - let entry = LedgerEntry::from_xdr(xdr, Limits::none())?; - Ok((entry, live_until_ledger_seq)) - } -} - -pub(crate) fn is_live(live_until_ledger_seq: u32, current_ledger_seq: u32) -> bool { - live_until_ledger_seq >= current_ledger_seq -} diff --git a/cmd/soroban-rpc/main.go b/cmd/soroban-rpc/main.go deleted file mode 100644 index 130ea78d7..000000000 --- a/cmd/soroban-rpc/main.go +++ /dev/null @@ -1,86 +0,0 @@ -package main - -import ( - "fmt" - "os" - - "github.com/spf13/cobra" - goxdr "github.com/stellar/go/xdr" - - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/config" - "github.com/stellar/soroban-tools/cmd/soroban-rpc/internal/daemon" -) - -func main() { - var cfg config.Config - - rootCmd := &cobra.Command{ - Use: "soroban-rpc", - Short: "Start the remote soroban-rpc server", - Run: func(_ *cobra.Command, _ []string) { - if err := cfg.SetValues(os.LookupEnv); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - if err := cfg.Validate(); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - daemon.MustNew(&cfg).Run() - }, - } - - versionCmd := &cobra.Command{ - Use: "version", - Short: "Print version information and exit", - Run: func(_ *cobra.Command, _ []string) { - if config.CommitHash == "" { - fmt.Printf("soroban-rpc dev\n") - } else { - // avoid printing the branch for the main branch - // ( since that's what the end-user would typically have ) - // but keep it for internal build ( so that we'll know from which branch it - // was built ) - branch := config.Branch - if branch == "main" { - branch = "" - } - fmt.Printf("soroban-rpc %s (%s) %s\n", config.Version, config.CommitHash, branch) - } - fmt.Printf("stellar-xdr %s\n", goxdr.CommitHash) - }, - } - - genConfigFileCmd := &cobra.Command{ - Use: "gen-config-file", - Short: "Generate a config file with default settings", - Run: func(_ *cobra.Command, _ []string) { - // We can't call 'Validate' here because the config file we are - // generating might not be complete. e.g. It might not include a network passphrase. - if err := cfg.SetValues(os.LookupEnv); err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - out, err := cfg.MarshalTOML() - if err != nil { - fmt.Fprintln(os.Stderr, err) - os.Exit(1) - } - fmt.Println(string(out)) - }, - } - - rootCmd.AddCommand(versionCmd) - rootCmd.AddCommand(genConfigFileCmd) - - if err := cfg.AddFlags(rootCmd); err != nil { - fmt.Fprintf(os.Stderr, "could not parse config options: %v\n", err) - os.Exit(1) - } - - if err := rootCmd.Execute(); err != nil { - fmt.Fprintf(os.Stderr, "could not run: %v\n", err) - - os.Exit(1) - } -} diff --git a/docs/MONITORING.md b/docs/MONITORING.md deleted file mode 100644 index 2a511198d..000000000 --- a/docs/MONITORING.md +++ /dev/null @@ -1,61 +0,0 @@ -# Monitoring and Tuning Guide for soroban-rpc - -## Introduction - -This document provides a comprehensive guide to monitoring and tuning soroban-rpc, a backend server that communicates using the jrpc (JSON-RPC) protocol over HTTP. To ensure high -availability, high performance, and efficient resource utilization, soroban-rpc incorporates various features like limiting concurrent requests, controlling execution times, and providing -warning and limiting mechanisms. This guide aims to help operators effectively monitor the server, detect potential issues, and apply tuning strategies to maintain optimal performance. - -## Monitoring Metrics - -To ensure the smooth operation of soroban-rpc, several key metrics should be monitored continuously: - -1. **Global Inflight Requests (Concurrent HTTP Requests)**: Monitor the number of concurrent HTTP requests being enqueued at the HTTP endpoint. This metric is tracked via the - `global_inflight_requests` gauge. If this number reaches the predefined limit, an HTTP 503 error is generated. This metric helps identify if the server is reaching its limit in handling - incoming requests. - -2. **Method-specific Inflight Requests (Concurrent JRPC Requests)**: Track the number of concurrent JRPC requests for each method using the `_inflight_requests` gauge. This - allows you to limit the workload of specific methods in case the server runs out of resources. - -3. **HTTP Request Duration**: Monitor the duration taken to process each HTTP request. This metric helps identify if any requests are taking too long to process and may lead to potential - performance issues. If the duration limit is reached, an HTTP 504 error is generated. The total number of warnings generated is tracked by the - `global_request_execution_duration_threshold_warning` counter, and the number of terminated methods is tracked via the `global_request_execution_duration_threshold_limit` counter. - -4. **Method-specific Execution Warnings and Limits**: Measure the execution time of each method and compare it against the predefined threshold. Track the execution warnings using the - `_execution_threshold_warning` counter and the execution limits using the `_execution_threshold_limit` counter. These metrics help operators identify - slow-performing methods and set execution limits to prevent resource exhaustion. - -## Best Practices - -Follow these best practices to maintain a stable and performant soroban-rpc deployment: - -1. **Set Sensible Limits**: Determine appropriate limits for concurrent requests, method execution times, and HTTP request duration based on your server's resources and expected workload. - Avoid overly restrictive limits that may hinder normal operations. - -2. **Logging and Alerts**: The soroban-rpc comes ready with logging and metric endpoint, which reports operational status. On your end, develop the toolings that would allow you to be aware of these events. These toolings could be Grafana alerts, log scraping or similar tools. - -3. **Load Testing**: Regularly conduct load testing to assess the server's performance under varying workloads. Use this data to adjust limits and execution times as needed. - -4. **Scaling Strategies**: Plan scaling strategies for both vertical and horizontal scaling. Vertical scaling involves upgrading hardware resources like CPU, memory, and disk, while - horizontal scaling uses HTTP-aware load balancers to distribute the load across multiple machines. - -## Tuning Suggestions - -When monitoring the resource utilization and identifying gradual increases in method execution times, consider the following tuning suggestions: - -1. **Vertical Tuning**: - - - Increase CPU resources: Faster processors can reduce method execution times, improving overall performance. - - Add Memory: Sufficient memory helps reduce disk I/O and can optimize processing times. - - Use Faster Disk: SSDs or faster disk technologies can significantly improve I/O performance. - -2. **Horizontal Tuning**: - - - Employ HTTP-Aware Load Balancers: Use load balancers that are aware of HTTP error codes and response times. This enables effective distribution of requests across multiple instances - while considering their respective loads and response times. - -3. **Quantitative Tuning**: - - Adjust Concurrency Levels: Fine-tune the concurrency limits for specific methods based on their individual resource requirements and importance. This allows you to prioritize critical - methods and prevent resource contention. - - Limit Execution Times: Set appropriate execution time limits for methods, ensuring that no single method consumes excessive resources. - - Divide and Conquer: Create several service performance groups, allowing a subset of the users to receive a favorable method execution times. diff --git a/go.mod b/go.mod deleted file mode 100644 index d7c19cf49..000000000 --- a/go.mod +++ /dev/null @@ -1,122 +0,0 @@ -module github.com/stellar/soroban-tools - -go 1.21 - -toolchain go1.21.1 - -require ( - github.com/Masterminds/squirrel v1.5.4 - github.com/cenkalti/backoff/v4 v4.2.1 - github.com/creachadair/jrpc2 v1.1.2 - github.com/go-chi/chi v4.1.2+incompatible - github.com/go-git/go-git/v5 v5.9.0 - github.com/mattn/go-sqlite3 v1.14.17 - github.com/pelletier/go-toml v1.9.5 - github.com/prometheus/client_golang v1.17.0 - github.com/rs/cors v1.10.1 - github.com/rubenv/sql-migrate v1.5.2 - github.com/sirupsen/logrus v1.9.3 - github.com/spf13/cobra v1.7.0 - github.com/spf13/pflag v1.0.5 - github.com/stellar/go v0.0.0-20240202231803-b0df9f046eb4 - github.com/stretchr/testify v1.8.4 - golang.org/x/mod v0.13.0 - gotest.tools/v3 v3.5.0 -) - -require ( - cloud.google.com/go v0.111.0 // indirect - cloud.google.com/go/compute v1.23.3 // indirect - cloud.google.com/go/compute/metadata v0.2.3 // indirect - cloud.google.com/go/iam v1.1.5 // indirect - cloud.google.com/go/storage v1.30.1 // indirect - dario.cat/mergo v1.0.0 // indirect - github.com/cloudflare/circl v1.3.5 // indirect - github.com/cyphar/filepath-securejoin v0.2.4 // indirect - github.com/go-logr/logr v1.2.4 // indirect - github.com/go-logr/stdr v1.2.2 // indirect - github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect - github.com/golang/protobuf v1.5.3 // indirect - github.com/google/s2a-go v0.1.7 // indirect - github.com/google/uuid v1.4.0 // indirect - github.com/googleapis/enterprise-certificate-proxy v0.3.2 // indirect - github.com/googleapis/gax-go/v2 v2.12.0 // indirect - github.com/hashicorp/golang-lru v1.0.2 // indirect - github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 // indirect - github.com/pelletier/go-toml/v2 v2.1.0 // indirect - github.com/pjbgf/sha1cd v0.3.0 // indirect - github.com/sagikazarmark/locafero v0.3.0 // indirect - github.com/sagikazarmark/slog-shim v0.1.0 // indirect - github.com/skeema/knownhosts v1.2.1 // indirect - github.com/sourcegraph/conc v0.3.0 // indirect - github.com/subosito/gotenv v1.6.0 // indirect - go.opencensus.io v0.24.0 // indirect - go.opentelemetry.io/otel v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.19.0 // indirect - go.opentelemetry.io/otel/trace v1.19.0 // indirect - go.uber.org/multierr v1.11.0 // indirect - golang.org/x/oauth2 v0.13.0 // indirect - golang.org/x/tools v0.14.0 // indirect - google.golang.org/api v0.149.0 // indirect - google.golang.org/appengine v1.6.8 // indirect - google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 // indirect - google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 // indirect - google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 // indirect - google.golang.org/grpc v1.60.1 // indirect - gopkg.in/ini.v1 v1.67.0 // indirect -) - -require ( - github.com/BurntSushi/toml v1.3.2 // indirect - github.com/Microsoft/go-winio v0.6.1 // indirect - github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c // indirect - github.com/acomagu/bufpipe v1.0.4 // indirect - github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 // indirect - github.com/aws/aws-sdk-go v1.45.27 // indirect - github.com/beorn7/perks v1.0.1 // indirect - github.com/cespare/xxhash/v2 v2.2.0 // indirect - github.com/creachadair/mds v0.3.0 // indirect - github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc // indirect - github.com/emirpasic/gods v1.18.1 // indirect - github.com/fsnotify/fsnotify v1.6.0 // indirect - github.com/go-errors/errors v1.5.1 // indirect - github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 // indirect - github.com/go-git/go-billy/v5 v5.5.0 // indirect - github.com/go-gorp/gorp/v3 v3.1.0 // indirect - github.com/google/go-cmp v0.6.0 // indirect - github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 - github.com/hashicorp/hcl v1.0.0 // indirect - github.com/inconshreveable/mousetrap v1.1.0 // indirect - github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 // indirect - github.com/jmespath/go-jmespath v0.4.0 // indirect - github.com/jmoiron/sqlx v1.3.5 // indirect - github.com/kevinburke/ssh_config v1.2.0 // indirect - github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 // indirect - github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 // indirect - github.com/lib/pq v1.10.9 // indirect - github.com/magiconair/properties v1.8.7 // indirect - github.com/mitchellh/mapstructure v1.5.0 // indirect - github.com/pkg/errors v0.9.1 // indirect - github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 // indirect - github.com/prometheus/client_model v0.5.0 // indirect - github.com/prometheus/common v0.45.0 // indirect - github.com/prometheus/procfs v0.12.0 // indirect - github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 // indirect - github.com/sergi/go-diff v1.3.1 // indirect - github.com/spf13/afero v1.10.0 // indirect - github.com/spf13/cast v1.5.1 // indirect - github.com/spf13/viper v1.17.0 // indirect - github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 // indirect - github.com/stretchr/objx v0.5.1 // indirect - github.com/xanzy/ssh-agent v0.3.3 // indirect - golang.org/x/crypto v0.16.0 // indirect - golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect - golang.org/x/net v0.19.0 // indirect - golang.org/x/sync v0.5.0 // indirect - golang.org/x/sys v0.16.0 // indirect - golang.org/x/text v0.14.0 // indirect - google.golang.org/protobuf v1.32.0 // indirect - gopkg.in/tylerb/graceful.v1 v1.2.15 // indirect - gopkg.in/warnings.v0 v0.1.2 // indirect - gopkg.in/yaml.v3 v3.0.1 // indirect -) diff --git a/go.sum b/go.sum deleted file mode 100644 index 56e9fc7a1..000000000 --- a/go.sum +++ /dev/null @@ -1,818 +0,0 @@ -cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.34.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= -cloud.google.com/go v0.38.0/go.mod h1:990N+gfupTy94rShfmMCWGDn0LpTmnzTp2qbd1dvSRU= -cloud.google.com/go v0.44.1/go.mod h1:iSa0KzasP4Uvy3f1mN/7PiObzGgflwredwwASm/v6AU= -cloud.google.com/go v0.44.2/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.44.3/go.mod h1:60680Gw3Yr4ikxnPRS/oxxkBccT6SA1yMk63TGekxKY= -cloud.google.com/go v0.45.1/go.mod h1:RpBamKRgapWJb87xiFSdk4g1CME7QZg3uwTez+TSTjc= -cloud.google.com/go v0.46.3/go.mod h1:a6bKKbmY7er1mI7TEI4lsAkts/mkhTSZK8w33B4RAg0= -cloud.google.com/go v0.50.0/go.mod h1:r9sluTvynVuxRIOHXQEHMFffphuXHOMZMycpNR5e6To= -cloud.google.com/go v0.52.0/go.mod h1:pXajvRH/6o3+F9jDHZWQ5PbGhn+o8w9qiu/CffaVdO4= -cloud.google.com/go v0.53.0/go.mod h1:fp/UouUEsRkN6ryDKNW/Upv/JBKnv6WDthjR6+vze6M= -cloud.google.com/go v0.54.0/go.mod h1:1rq2OEkV3YMf6n/9ZvGWI3GWw0VoqH/1x2nd8Is/bPc= -cloud.google.com/go v0.56.0/go.mod h1:jr7tqZxxKOVYizybht9+26Z/gUq7tiRzu+ACVAMbKVk= -cloud.google.com/go v0.57.0/go.mod h1:oXiQ6Rzq3RAkkY7N6t3TcE6jE+CIBBbA36lwQ1JyzZs= -cloud.google.com/go v0.62.0/go.mod h1:jmCYTdRCQuc1PHIIJ/maLInMho30T/Y0M4hTdTShOYc= -cloud.google.com/go v0.65.0/go.mod h1:O5N8zS7uWy9vkA9vayVHs65eM1ubvY4h553ofrNHObY= -cloud.google.com/go v0.72.0/go.mod h1:M+5Vjvlc2wnp6tjzE102Dw08nGShTscUx2nZMufOKPI= -cloud.google.com/go v0.74.0/go.mod h1:VV1xSbzvo+9QJOxLDaJfTjx5e+MePCpCWwvftOeQmWk= -cloud.google.com/go v0.75.0/go.mod h1:VGuuCn7PG0dwsd5XPVm2Mm3wlh3EL55/79EKB6hlPTY= -cloud.google.com/go v0.111.0 h1:YHLKNupSD1KqjDbQ3+LVdQ81h/UJbJyZG203cEfnQgM= -cloud.google.com/go v0.111.0/go.mod h1:0mibmpKP1TyOOFYQY5izo0LnT+ecvOQ0Sg3OdmMiNRU= -cloud.google.com/go/bigquery v1.0.1/go.mod h1:i/xbL2UlR5RvWAURpBYZTtm/cXjCha9lbfbpx4poX+o= -cloud.google.com/go/bigquery v1.3.0/go.mod h1:PjpwJnslEMmckchkHFfq+HTD2DmtT67aNFKH1/VBDHE= -cloud.google.com/go/bigquery v1.4.0/go.mod h1:S8dzgnTigyfTmLBfrtrhyYhwRxG72rYxvftPBK2Dvzc= -cloud.google.com/go/bigquery v1.5.0/go.mod h1:snEHRnqQbz117VIFhE8bmtwIDY80NLUZUMb4Nv6dBIg= -cloud.google.com/go/bigquery v1.7.0/go.mod h1://okPTzCYNXSlb24MZs83e2Do+h+VXtc4gLoIoXIAPc= -cloud.google.com/go/bigquery v1.8.0/go.mod h1:J5hqkt3O0uAFnINi6JXValWIb1v0goeZM77hZzJN/fQ= -cloud.google.com/go/compute v1.23.3 h1:6sVlXXBmbd7jNX0Ipq0trII3e4n1/MsADLK6a+aiVlk= -cloud.google.com/go/compute v1.23.3/go.mod h1:VCgBUoMnIVIR0CscqQiPJLAG25E3ZRZMzcFZeQ+h8CI= -cloud.google.com/go/compute/metadata v0.2.3 h1:mg4jlk7mCAj6xXp9UJ4fjI9VUI5rubuGBW5aJ7UnBMY= -cloud.google.com/go/compute/metadata v0.2.3/go.mod h1:VAV5nSsACxMJvgaAuX6Pk2AawlZn8kiOGuCv6gTkwuA= -cloud.google.com/go/datastore v1.0.0/go.mod h1:LXYbyblFSglQ5pkeyhO+Qmw7ukd3C+pD7TKLgZqpHYE= -cloud.google.com/go/datastore v1.1.0/go.mod h1:umbIZjpQpHh4hmRpGhH4tLFup+FVzqBi1b3c64qFpCk= -cloud.google.com/go/iam v1.1.5 h1:1jTsCu4bcsNsE4iiqNT5SHwrDRCfRmIaaaVFhRveTJI= -cloud.google.com/go/iam v1.1.5/go.mod h1:rB6P/Ic3mykPbFio+vo7403drjlgvoWfYpJhMXEbzv8= -cloud.google.com/go/pubsub v1.0.1/go.mod h1:R0Gpsv3s54REJCy4fxDixWD93lHJMoZTyQ2kNxGRt3I= -cloud.google.com/go/pubsub v1.1.0/go.mod h1:EwwdRX2sKPjnvnqCa270oGRyludottCI76h+R3AArQw= -cloud.google.com/go/pubsub v1.2.0/go.mod h1:jhfEVHT8odbXTkndysNHCcx0awwzvfOlguIAii9o8iA= -cloud.google.com/go/pubsub v1.3.1/go.mod h1:i+ucay31+CNRpDW4Lu78I4xXG+O1r/MAHgjpRVR+TSU= -cloud.google.com/go/storage v1.0.0/go.mod h1:IhtSnM/ZTZV8YYJWCY8RULGVqBDmpoyjwiyrjsg+URw= -cloud.google.com/go/storage v1.5.0/go.mod h1:tpKbwo567HUNpVclU5sGELwQWBDZ8gh0ZeosJ0Rtdos= -cloud.google.com/go/storage v1.6.0/go.mod h1:N7U0C8pVQ/+NIKOBQyamJIeKQKkZ+mxpohlUTyfDhBk= -cloud.google.com/go/storage v1.8.0/go.mod h1:Wv1Oy7z6Yz3DshWRJFhqM/UCfaWIRTdp0RXyy7KQOVs= -cloud.google.com/go/storage v1.10.0/go.mod h1:FLPqc6j+Ki4BU591ie1oL6qBQGu2Bl/tZ9ullr3+Kg0= -cloud.google.com/go/storage v1.14.0/go.mod h1:GrKmX003DSIwi9o29oFT7YDnHYwZoctc3fOKtUw0Xmo= -cloud.google.com/go/storage v1.30.1 h1:uOdMxAs8HExqBlnLtnQyP0YkvbiDpdGShGKtx6U/oNM= -cloud.google.com/go/storage v1.30.1/go.mod h1:NfxhC0UJE1aXSx7CIIbCf7y9HKT7BiccwkR7+P7gN8E= -dario.cat/mergo v1.0.0 h1:AGCNq9Evsj31mOgNPcLyXc+4PNABt905YmuqPYYpBWk= -dario.cat/mergo v1.0.0/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= -dmitri.shuralyov.com/gpu/mtl v0.0.0-20190408044501-666a987793e9/go.mod h1:H6x//7gZCb22OMCxBHrMx7a5I7Hp++hsVxbQ4BYO7hU= -github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= -github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= -github.com/BurntSushi/xgb v0.0.0-20160522181843-27f122750802/go.mod h1:IVnqGOEym/WlBOVXweHU+Q+/VP0lqqI8lqeDx9IjBqo= -github.com/Masterminds/squirrel v1.5.4 h1:uUcX/aBc8O7Fg9kaISIUsHXdKuqehiXAMQTYX8afzqM= -github.com/Masterminds/squirrel v1.5.4/go.mod h1:NNaOrjSoIDfDA40n7sr2tPNZRfjzjA400rg+riTZj10= -github.com/Microsoft/go-winio v0.5.2/go.mod h1:WpS1mjBmmwHBEWmogvA2mj8546UReBk4v8QkMxJ6pZY= -github.com/Microsoft/go-winio v0.6.1 h1:9/kr64B9VUZrLm5YYwbGtUJnMgqWVOdUAXu6Migciow= -github.com/Microsoft/go-winio v0.6.1/go.mod h1:LRdKpFKfdobln8UmuiYcKPot9D2v6svN5+sAH+4kjUM= -github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c h1:kMFnB0vCcX7IL/m9Y5LO+KQYv+t1CQOiFe6+SV2J7bE= -github.com/ProtonMail/go-crypto v0.0.0-20230923063757-afb1ddc0824c/go.mod h1:EjAoLdwvbIOoOQr3ihjnSoLZRtE8azugULFRteWMNc0= -github.com/acomagu/bufpipe v1.0.4 h1:e3H4WUzM3npvo5uv95QuJM3cQspFNtFBzvJ2oNjKIDQ= -github.com/acomagu/bufpipe v1.0.4/go.mod h1:mxdxdup/WdsKVreO5GpW4+M/1CE2sMG4jeGJ2sYmHc4= -github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f h1:zvClvFQwU++UpIUBGC8YmDlfhUrweEy1R1Fj1gu5iIM= -github.com/ajg/form v0.0.0-20160822230020-523a5da1a92f/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= -github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= -github.com/andybalholm/brotli v1.0.4/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be h1:9AeTilPcZAjCFIImctFaOjnTIavg87rW78vTPkQqLI8= -github.com/anmitsu/go-shlex v0.0.0-20200514113438-38f4b401e2be/go.mod h1:ySMOLuWl6zY27l47sB3qLNK6tF2fkHG55UZxx8oIVo4= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= -github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so= -github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw= -github.com/aws/aws-sdk-go v1.45.27 h1:b+zOTPkAG4i2RvqPdHxkJZafmhhVaVHBp4r41Tu4I6U= -github.com/aws/aws-sdk-go v1.45.27/go.mod h1:aVsgQcEevwlmQ7qHE9I3h+dtQgpqhFB+i8Phjh7fkwI= -github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= -github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= -github.com/bwesterb/go-ristretto v1.2.3/go.mod h1:fUIoIZaG73pV5biE2Blr2xEzDoMj7NFEuV9ekS419A0= -github.com/cenkalti/backoff/v4 v4.2.1 h1:y4OZtCnogmCPw98Zjyt5a6+QwPLGkiQsYW5oUqylYbM= -github.com/cenkalti/backoff/v4 v4.2.1/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= -github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= -github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44= -github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= -github.com/chzyer/logex v1.1.10/go.mod h1:+Ywpsq7O8HXn0nuIou7OrIPyXbp3wmkHB+jjWRnGsAI= -github.com/chzyer/readline v0.0.0-20180603132655-2972be24d48e/go.mod h1:nSuG5e5PlCu98SY8svDHJxuZscDgtXS6KTTbou5AhLI= -github.com/chzyer/test v0.0.0-20180213035817-a1ea475d72b1/go.mod h1:Q3SI9o4m/ZMnBNeIyt5eFwwo7qiLfzFZmjNmxjkiQlU= -github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= -github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cloudflare/circl v1.3.5 h1:g+wWynZqVALYAlpSQFAa7TscDnUK8mKYtrxMpw6AUKo= -github.com/cloudflare/circl v1.3.5/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA= -github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= -github.com/cncf/udpa/go v0.0.0-20200629203442-efcf912fb354/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cncf/udpa/go v0.0.0-20201120205902-5459f2c99403/go.mod h1:WmhPx2Nbnhtbo57+VJT5O0JRkEi1Wbu0z5j0R8u5Hbk= -github.com/cpuguy83/go-md2man/v2 v2.0.2/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= -github.com/creachadair/jrpc2 v1.1.2 h1:UOYMipEFYlwd5qmcvs9GZBurn3oXt1UDIX5JLjWWFzo= -github.com/creachadair/jrpc2 v1.1.2/go.mod h1:JcCe2Eny3lIvVwZLm92WXyU+tNUgTBWFCLMsfNkjEGk= -github.com/creachadair/mds v0.3.0 h1:uKbCKVtd3iOKVv3uviOm13fFNfe9qoCXJh1Vo7y3Kr0= -github.com/creachadair/mds v0.3.0/go.mod h1:4vrFYUzTXMJpMBU+OA292I6IUxKWCCfZkgXg+/kBZMo= -github.com/cyphar/filepath-securejoin v0.2.4 h1:Ugdm7cg7i6ZK6x3xDF1oEu1nfkyfH53EtKeQYTC3kyg= -github.com/cyphar/filepath-securejoin v0.2.4/go.mod h1:aPGpWjXOXUn2NCNjFvBE6aRxGGx79pTxQpKOJNYHHl4= -github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM= -github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a h1:mATvB/9r/3gvcejNsXKSkQ6lcIaNec2nyfOdlTBR2lU= -github.com/elazarl/goproxy v0.0.0-20230808193330-2592e75ae04a/go.mod h1:Ro8st/ElPeALwNFlcTpWmkr6IoMFfkjXAvTHpevnDsM= -github.com/emirpasic/gods v1.18.1 h1:FXtiHYKDGKCW2KzwZKx0iC0PQmdlorYgdFG9jPXJ1Bc= -github.com/emirpasic/gods v1.18.1/go.mod h1:8tpGGwCnJ5H4r6BWwaV6OrWmMoPhUl5jm/FMNAnJvWQ= -github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= -github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= -github.com/envoyproxy/go-control-plane v0.9.7/go.mod h1:cwu0lG7PUMfa9snN8LXBig5ynNVH9qI8YYLbd1fK2po= -github.com/envoyproxy/go-control-plane v0.9.9-0.20201210154907-fd9021fe5dad/go.mod h1:cXg6YxExXjJnVBQHBLXeUAgxn2UodCpnH306RInaBQk= -github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= -github.com/fatih/structs v1.0.0 h1:BrX964Rv5uQ3wwS+KRUAJCBBw5PQmgJfJ6v4yly5QwU= -github.com/fatih/structs v1.0.0/go.mod h1:9NiDSp5zOcgEDl+j00MP/WkGVPOlPRLejGD8Ga6PJ7M= -github.com/fortytw2/leaktest v1.3.0 h1:u8491cBMTQ8ft8aeV+adlcytMZylmA5nnwwkRZjI8vw= -github.com/fortytw2/leaktest v1.3.0/go.mod h1:jDsjWgpAGjm2CA7WthBh/CdZYEPF31XHquHwclZch5g= -github.com/frankban/quicktest v1.14.4 h1:g2rn0vABPOOXmZUj+vbmUp0lPoXEMuhTpIluN0XL9UY= -github.com/frankban/quicktest v1.14.4/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0= -github.com/fsnotify/fsnotify v1.6.0 h1:n+5WquG0fcWoWp6xPWfHdbskMCQaFnG6PfBrh1Ky4HY= -github.com/fsnotify/fsnotify v1.6.0/go.mod h1:sl3t1tCWJFWoRz9R8WJCbQihKKwmorjAbSClcnxKAGw= -github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955 h1:gmtGRvSexPU4B1T/yYo0sLOKzER1YT+b4kPxPpm0Ty4= -github.com/gavv/monotime v0.0.0-20161010190848-47d58efa6955/go.mod h1:vmp8DIyckQMXOPl0AQVHt+7n5h7Gb7hS6CUydiV8QeA= -github.com/gliderlabs/ssh v0.3.5 h1:OcaySEmAQJgyYcArR+gGGTHCyE7nvhEMTlYY+Dp8CpY= -github.com/gliderlabs/ssh v0.3.5/go.mod h1:8XB4KraRrX39qHhT6yxPsHedjA08I/uBVwj4xC+/+z4= -github.com/go-chi/chi v4.1.2+incompatible h1:fGFk2Gmi/YKXk0OmGfBh0WgmN3XB8lVnEyNz34tQRec= -github.com/go-chi/chi v4.1.2+incompatible/go.mod h1:eB3wogJHnLi3x/kFX2A+IbTBlXxmMeXJVKy9tTv1XzQ= -github.com/go-errors/errors v1.5.1 h1:ZwEMSLRCapFLflTpT7NKaAc7ukJ8ZPEjzlxt8rPN8bk= -github.com/go-errors/errors v1.5.1/go.mod h1:sIVyrIiJhuEF+Pj9Ebtd6P/rEYROXFi3BopGUQ5a5Og= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376 h1:+zs/tPmkDkHx3U66DAb0lQFJrpS6731Oaa12ikc+DiI= -github.com/go-git/gcfg v1.5.1-0.20230307220236-3a3c6141e376/go.mod h1:an3vInlBmSxCcxctByoQdvwPiA7DTK7jaaFDBTtu0ic= -github.com/go-git/go-billy/v5 v5.5.0 h1:yEY4yhzCDuMGSv83oGxiBotRzhwhNr8VZyphhiu+mTU= -github.com/go-git/go-billy/v5 v5.5.0/go.mod h1:hmexnoNsr2SJU1Ju67OaNz5ASJY3+sHgFRpCtpDCKow= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f h1:Pz0DHeFij3XFhoBRGUDPzSJ+w2UcK5/0JvF8DRI58r8= -github.com/go-git/go-git-fixtures/v4 v4.3.2-0.20230305113008-0c11038e723f/go.mod h1:8LHG1a3SRW71ettAD/jW13h8c6AqjVSeL11RAdgaqpo= -github.com/go-git/go-git/v5 v5.9.0 h1:cD9SFA7sHVRdJ7AYck1ZaAa/yeuBvGPxwXDL8cxrObY= -github.com/go-git/go-git/v5 v5.9.0/go.mod h1:RKIqga24sWdMGZF+1Ekv9kylsDz6LzdTSI2s/OsZWE0= -github.com/go-gl/glfw v0.0.0-20190409004039-e6da0acd62b1/go.mod h1:vR7hzQXu2zJy9AVAgeJqvqgH9Q5CA+iKCZ2gyEVpxRU= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20191125211704-12ad95a8df72/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gl/glfw/v3.3/glfw v0.0.0-20200222043503-6f7a984d4dc4/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8= -github.com/go-gorp/gorp/v3 v3.1.0 h1:ItKF/Vbuj31dmV4jxA1qblpSwkl9g1typ24xoe70IGs= -github.com/go-gorp/gorp/v3 v3.1.0/go.mod h1:dLEjIyyRNiXvNZ8PSmzpt1GsWAUK8kjVhEpjH8TixEw= -github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ= -github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= -github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= -github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= -github.com/go-sql-driver/mysql v1.6.0 h1:BCTh4TKNUYmOmMUcQ3IipzF5prigylS7XXjEkfCHuOE= -github.com/go-sql-driver/mysql v1.6.0/go.mod h1:DCzpHaOWr8IXmIStZouvnhqoel9Qv2LBy8hT2VhHyBg= -github.com/gobuffalo/logger v1.0.6 h1:nnZNpxYo0zx+Aj9RfMPBm+x9zAU2OayFh/xrAWi34HU= -github.com/gobuffalo/logger v1.0.6/go.mod h1:J31TBEHR1QLV2683OXTAItYIg8pv2JMHnF/quuAbMjs= -github.com/gobuffalo/packd v1.0.2 h1:Yg523YqnOxGIWCp69W12yYBKsoChwI7mtu6ceM9Bwfw= -github.com/gobuffalo/packd v1.0.2/go.mod h1:sUc61tDqGMXON80zpKGp92lDb86Km28jfvX7IAyxFT8= -github.com/gobuffalo/packr/v2 v2.8.3 h1:xE1yzvnO56cUC0sTpKR3DIbxZgB54AftTFMhB2XEWlY= -github.com/gobuffalo/packr/v2 v2.8.3/go.mod h1:0SahksCVcx4IMnigTjiFuyldmTrdTctXsOdiU5KwbKc= -github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q= -github.com/golang/groupcache v0.0.0-20190702054246-869f871628b6/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20191227052852-215e87163ea7/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20200121045136-8c9f03a8e57e/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE= -github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc= -github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A= -github.com/golang/mock v1.3.1/go.mod h1:sBzyDLLjw3U8JLTeZvSv8jJB+tU5PVekmnlKIyFUx0Y= -github.com/golang/mock v1.4.0/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.1/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.3/go.mod h1:UOMv5ysSaYNkG+OFQykRIcU/QvvxJf3p21QfJ2Bt3cw= -github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= -github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U= -github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.4/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= -github.com/golang/protobuf v1.3.5/go.mod h1:6O5/vntMXwX2lRkT1hjjk0nAC1IDOTvTlVgjlRvqsdk= -github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8= -github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA= -github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs= -github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w= -github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0= -github.com/golang/protobuf v1.4.1/go.mod h1:U8fpvMrcmy5pZrNK1lt4xCsGvpyWQ/VVv6QDs8UjoX8= -github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.4.3/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI= -github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= -github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= -github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= -github.com/google/btree v0.0.0-20180813153112-4030bb1f1f0c/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/btree v1.0.0/go.mod h1:lNA+9X1NB3Zf8V7Ke586lFgjr2dZNuvo3lPJSGZ5JPQ= -github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M= -github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU= -github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.4.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.1/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.3/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.4/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5 h1:oERTZ1buOUYlpmKaqlO5fYmz8cZ1rYu5DieJzF4ZVmU= -github.com/google/go-querystring v0.0.0-20160401233042-9235644dd9e5/go.mod h1:odCYkC5MyYFN7vkCjXpyrEuKhc/BUO6wN/zVPAxq5ck= -github.com/google/martian v2.1.0+incompatible h1:/CP5g8u/VJHijgedC/Legn3BAbAaWPgecwXBIDzw5no= -github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs= -github.com/google/martian/v3 v3.0.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.1.0/go.mod h1:y5Zk1BBys9G+gd6Jrk0W3cC1+ELVxBWuIGO+w/tUAp0= -github.com/google/martian/v3 v3.3.2 h1:IqNFLAmvJOgVlpdEBiQbDc2EwKW77amAycfTuWKdfvw= -github.com/google/martian/v3 v3.3.2/go.mod h1:oBOf6HBosgwRXnUGWUB05QECsc6uvmMiJ3+6W4l/CUk= -github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20190515194954-54271f7e092f/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc= -github.com/google/pprof v0.0.0-20191218002539-d4f498aebedc/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200212024743-f11f1df84d12/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200229191704-1ebb73c60ed3/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200430221834-fc25d7d30c6d/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20200708004538-1a94d8640e99/go.mod h1:ZgVRPoUq/hfqzAqh7sHMqb3I9Rq5C59dIz2SbBwJ4eM= -github.com/google/pprof v0.0.0-20201023163331-3e6fc7fc9c4c/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201203190320-1bf35d6f28c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/pprof v0.0.0-20201218002935-b9804c9f04c2/go.mod h1:kpwsk12EmLew5upagYY7GY0pfYCcupk39gWOCRROcvE= -github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI= -github.com/google/s2a-go v0.1.7 h1:60BLSyTrOV4/haCDW4zb1guZItoSq8foHCXrAnjBo/o= -github.com/google/s2a-go v0.1.7/go.mod h1:50CgR4k1jNlWBu4UfS4AcfhVe1r6pdZPygJ3R8F0Qdw= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510 h1:El6M4kTTCOh6aBiKaUGG7oYTSPP8MxqL4YI3kZKwcP4= -github.com/google/shlex v0.0.0-20191202100458-e7afc7fbc510/go.mod h1:pupxD2MaaD3pAXIBCelhxNneeOaAeabZDe5s4K6zSpQ= -github.com/google/uuid v1.1.2/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/google/uuid v1.4.0 h1:MtMxsa51/r9yyhkyLsVeVt0B+BGQZzpQiTQ4eHZ8bc4= -github.com/google/uuid v1.4.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= -github.com/googleapis/enterprise-certificate-proxy v0.3.2 h1:Vie5ybvEvT75RniqhfFxPRy3Bf7vr3h0cechB90XaQs= -github.com/googleapis/enterprise-certificate-proxy v0.3.2/go.mod h1:VLSiSSBs/ksPL8kq3OBOQ6WRI2QnaFynd1DCjZ62+V0= -github.com/googleapis/gax-go/v2 v2.0.4/go.mod h1:0Wqv26UfaUD9n4G6kQubkQ+KchISgw+vpHVxEJEs9eg= -github.com/googleapis/gax-go/v2 v2.0.5/go.mod h1:DWXyrwAJ9X0FpwwEdw+IPEYBICEFu5mhpdKc/us6bOk= -github.com/googleapis/gax-go/v2 v2.12.0 h1:A+gCJKdRfqXkr+BIRGtZLibNXf0m1f9E4HG56etFpas= -github.com/googleapis/gax-go/v2 v2.12.0/go.mod h1:y+aIqrI5eb1YGMVJfuV3185Ts/D7qKpsEkdD5+I6QGU= -github.com/googleapis/google-cloud-go-testing v0.0.0-20200911160855-bcd43fbb19e8/go.mod h1:dvDLG8qkwmyD9a/MJJN3XJcT3xFxOKAvTZGvuZmac9g= -github.com/gorilla/schema v1.2.0 h1:YufUaxZYCKGFuAq3c96BOhjgd5nmXiOY9NGzF247Tsc= -github.com/gorilla/schema v1.2.0/go.mod h1:kgLaKoK1FELgZqMAVxx/5cbj0kT+57qxUrAlIO2eleU= -github.com/guregu/null v4.0.0+incompatible h1:4zw0ckM7ECd6FNNddc3Fu4aty9nTlpkkzH7dPn4/4Gw= -github.com/guregu/null v4.0.0+incompatible/go.mod h1:ePGpQaN9cw0tj45IR5E5ehMvsFlLlQZAkkOXZurJ3NM= -github.com/hashicorp/golang-lru v0.5.0/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v0.5.1/go.mod h1:/m3WP610KZHVQ1SGc6re/UDhFvYD7pJ4Ao+sR/qLZy8= -github.com/hashicorp/golang-lru v1.0.2 h1:dV3g9Z/unq5DpblPpw+Oqcv4dU/1omnb4Ok8iPY6p1c= -github.com/hashicorp/golang-lru v1.0.2/go.mod h1:iADmTwqILo4mZ8BN3D2Q6+9jd8WM5uGBxy+E8yxSoD4= -github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4= -github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ= -github.com/ianlancetaylor/demangle v0.0.0-20181102032728-5e5cf60278f6/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/ianlancetaylor/demangle v0.0.0-20200824232613-28f6c0f3b639/go.mod h1:aSSvb/t6k1mPoxDqO4vJh6VOCGPwU4O0C2/Eqndh1Sc= -github.com/imkira/go-interpol v1.1.0 h1:KIiKr0VSG2CUW1hl1jpiyuzuJeKUUpC8iM1AIE7N1Vk= -github.com/imkira/go-interpol v1.1.0/go.mod h1:z0h2/2T3XF8kyEPpRgJ3kmNv+C43p+I/CoI+jC3w2iA= -github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8= -github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw= -github.com/jarcoal/httpmock v0.0.0-20161210151336-4442edb3db31 h1:Aw95BEvxJ3K6o9GGv5ppCd1P8hkeIeEJ30FO+OhOJpM= -github.com/jarcoal/httpmock v0.0.0-20161210151336-4442edb3db31/go.mod h1:ks+b9deReOc7jgqp+e7LuFiCBH6Rm5hL32cLcEAArb4= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99 h1:BQSFePA1RWJOlocH6Fxy8MmwDt+yVQYULKfN0RoTN8A= -github.com/jbenet/go-context v0.0.0-20150711004518-d14ea06fba99/go.mod h1:1lJo3i6rXxKeerYnT8Nvf0QmHCRC1n8sfWVwXF2Frvo= -github.com/jmespath/go-jmespath v0.4.0 h1:BEgLn5cpjn8UN1mAw4NjwDrS35OdebyEtFe+9YPoQUg= -github.com/jmespath/go-jmespath v0.4.0/go.mod h1:T8mJZnbsbmF+m6zOOFylbeCJqk5+pHWvzYPziyZiYoo= -github.com/jmespath/go-jmespath/internal/testify v1.5.1 h1:shLQSRRSCCPj3f2gpwzGwWFoC7ycTf1rcQZHOlsJ6N8= -github.com/jmespath/go-jmespath/internal/testify v1.5.1/go.mod h1:L3OGu8Wl2/fWfCI6z80xFu9LTZmf1ZRjMHUOPmWr69U= -github.com/jmoiron/sqlx v1.3.5 h1:vFFPA71p1o5gAeqtEAwLU4dnX2napprKtHr7PYIcN3g= -github.com/jmoiron/sqlx v1.3.5/go.mod h1:nRVWtLre0KfCLJvgxzCsLVMogSvQ1zNJtpYr2Ccp0mQ= -github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= -github.com/jstemmer/go-junit-report v0.9.1/go.mod h1:Brl9GWCQeLvo8nXZwPNNblvFj/XSXhF0NWZEnDohbsk= -github.com/karrick/godirwalk v1.16.1 h1:DynhcF+bztK8gooS0+NDJFrdNZjJ3gzVzC545UNA9iw= -github.com/karrick/godirwalk v1.16.1/go.mod h1:j4mkqPuvaLI8mp1DroR3P6ad7cyYd4c1qeJ3RV7ULlk= -github.com/kevinburke/ssh_config v1.2.0 h1:x584FjTGwHzMwvHx18PXxbBVzfnxogHaAReU4gf13a4= -github.com/kevinburke/ssh_config v1.2.0/go.mod h1:CT57kijsi8u/K/BOFA39wgDQJ9CxiF4nAY/ojJ6r6mM= -github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= -github.com/klauspost/compress v1.17.0 h1:Rnbp4K9EjcDuVuHtd0dgA4qNuv9yKDYKK1ulpJwgrqM= -github.com/klauspost/compress v1.17.0/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= -github.com/kr/fs v0.1.0/go.mod h1:FFnZGqtBN9Gxj7eW1uZ42v5BccTP0vu6NEaFoC2HwRg= -github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= -github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= -github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= -github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ= -github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= -github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= -github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0 h1:SOEGU9fKiNWd/HOJuq6+3iTQz8KNCLtVX6idSoTLdUw= -github.com/lann/builder v0.0.0-20180802200727-47ae307949d0/go.mod h1:dXGbAdH5GtBTC4WfIxhKZfyBF/HBFgRZSWwZ9g/He9o= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0 h1:P6pPBnrTSX3DEVR4fDembhRWSsG5rVo6hYhAB/ADZrk= -github.com/lann/ps v0.0.0-20150810152359-62de8c46ede0/go.mod h1:vmVJ0l/dxyfGW6FmdpVm2joNMFikkuWg0EoCKLGUMNw= -github.com/lib/pq v1.2.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= -github.com/lib/pq v1.10.9 h1:YXG7RB+JIjhP29X+OtkiDnYaXQwpS4JEWq7dtCCRUEw= -github.com/lib/pq v1.10.9/go.mod h1:AlVN5x4E4T544tWzH6hKfbfQvm3HdbOxrmggDNAPY9o= -github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= -github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= -github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739 h1:ykXz+pRRTibcSjG1yRhpdSHInF8yZY/mfn+Rz2Nd1rE= -github.com/manucorporat/sse v0.0.0-20160126180136-ee05b128a739/go.mod h1:zUx1mhth20V3VKgL5jbd1BSQcW4Fy6Qs4PZvQwRFwzM= -github.com/markbates/errx v1.1.0 h1:QDFeR+UP95dO12JgW+tgi2UVfo0V8YBHiUIOaeBPiEI= -github.com/markbates/errx v1.1.0/go.mod h1:PLa46Oex9KNbVDZhKel8v1OT7hD5JZ2eI7AHhA0wswc= -github.com/markbates/oncer v1.0.0 h1:E83IaVAHygyndzPimgUYJjbshhDTALZyXxvk9FOlQRY= -github.com/markbates/oncer v1.0.0/go.mod h1:Z59JA581E9GP6w96jai+TGqafHPW+cPfRxz2aSZ0mcI= -github.com/markbates/safe v1.0.1 h1:yjZkbvRM6IzKj9tlu/zMJLS0n/V351OZWRnF3QfaUxI= -github.com/markbates/safe v1.0.1/go.mod h1:nAqgmRi7cY2nqMc92/bSEeQA+R4OheNU2T1kNSCBdG0= -github.com/matryer/is v1.2.0 h1:92UTHpy8CDwaJ08GqLDzhhuixiBUUD1p3AU6PHddz4A= -github.com/matryer/is v1.2.0/go.mod h1:2fLPjFQM9rhQ15aVEtbuwhJinnOqrmgXPNdZsdwlWXA= -github.com/mattn/go-sqlite3 v1.14.6/go.mod h1:NyWgC/yNuGj7Q9rpYnZvas74GogHl5/Z4A/KQRfk6bU= -github.com/mattn/go-sqlite3 v1.14.17 h1:mCRHCLDUBXgpKAqIKsaAaAsrAlbkeomtRFKXh2L6YIM= -github.com/mattn/go-sqlite3 v1.14.17/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0 h1:jWpvCLoY8Z/e3VKvlsiIGKtc+UG6U5vzxaoagmhXfyg= -github.com/matttproud/golang_protobuf_extensions/v2 v2.0.0/go.mod h1:QUyp042oQthUoa9bqDv0ER0wrtXnBruoNd7aNjkbP+k= -github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= -github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= -github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db h1:eZgFHVkk9uOTaOQLC6tgjkzdp7Ays8eEVecBcfHZlJQ= -github.com/moul/http2curl v0.0.0-20161031194548-4e24498b31db/go.mod h1:8UbvGypXm98wA/IqH45anm5Y2Z6ep6O31QGOAZ3H0fQ= -github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE= -github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU= -github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE= -github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU= -github.com/onsi/gomega v1.27.10 h1:naR28SdDFlqrG6kScpT8VWpu1xWY5nJRCF3XaYyBjhI= -github.com/onsi/gomega v1.27.10/go.mod h1:RsS8tutOdbdgzbPtzzATp12yT7kM5I5aElG3evPbQ0M= -github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= -github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= -github.com/pelletier/go-toml/v2 v2.1.0 h1:FnwAJ4oYMvbT/34k9zzHuZNrhlz48GB3/s6at6/MHO4= -github.com/pelletier/go-toml/v2 v2.1.0/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= -github.com/pjbgf/sha1cd v0.3.0 h1:4D5XXmUUBUl/xQ6IjCkEAbqXskkq/4O7LmGn0AqMDs4= -github.com/pjbgf/sha1cd v0.3.0/go.mod h1:nZ1rrWOcGJ5uZgEEVL1VUM9iRQiZvWdbZjkKyFzPPsI= -github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= -github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= -github.com/pkg/sftp v1.13.1/go.mod h1:3HaPG6Dq1ILlpPZRO0HVMrsydcdLt6HRDccSgb87qRg= -github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U= -github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= -github.com/poy/onpar v1.1.2 h1:QaNrNiZx0+Nar5dLgTVp5mXkyoVFIbepjyEoGSnhbAY= -github.com/poy/onpar v1.1.2/go.mod h1:6X8FLNoxyr9kkmnlqpK6LSoiOtrO6MICtWwEuWkLjzg= -github.com/prometheus/client_golang v1.17.0 h1:rl2sfwZMtSthVU752MqfjQozy7blglC+1SOtjMAMh+Q= -github.com/prometheus/client_golang v1.17.0/go.mod h1:VeL+gMmOAxkS2IqfCq0ZmHSL+LjWfWDUmp1mBz9JgUY= -github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA= -github.com/prometheus/client_model v0.5.0 h1:VQw1hfvPvk3Uv6Qf29VrPF32JB6rtbgI6cYPYQjL0Qw= -github.com/prometheus/client_model v0.5.0/go.mod h1:dTiFglRmd66nLR9Pv9f0mZi7B7fk5Pm3gvsjB5tr+kI= -github.com/prometheus/common v0.45.0 h1:2BGz0eBc2hdMDLnO/8n0jeB3oPrt2D08CekT0lneoxM= -github.com/prometheus/common v0.45.0/go.mod h1:YJmSTw9BoKxJplESWWxlbyttQR4uaEcGyv9MZjVOJsY= -github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo= -github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo= -github.com/rogpeppe/go-internal v1.3.0/go.mod h1:M8bDsm7K2OlrFYOpmOWEs/qY81heoFRclV5y23lUDJ4= -github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= -github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA= -github.com/rs/cors v1.10.1 h1:L0uuZVXIKlI1SShY2nhFfo44TYvDPQ1w4oFkUJNfhyo= -github.com/rs/cors v1.10.1/go.mod h1:XyqrcTp5zjWr1wsJ8PIRZssZ8b/WMcMf71DJnit4EMU= -github.com/rubenv/sql-migrate v1.5.2 h1:bMDqOnrJVV/6JQgQ/MxOpU+AdO8uzYYA/TxFUBzFtS0= -github.com/rubenv/sql-migrate v1.5.2/go.mod h1:H38GW8Vqf8F0Su5XignRyaRcbXbJunSWxs+kmzlg0Is= -github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= -github.com/sagikazarmark/locafero v0.3.0 h1:zT7VEGWC2DTflmccN/5T1etyKvxSxpHsjb9cJvm4SvQ= -github.com/sagikazarmark/locafero v0.3.0/go.mod h1:w+v7UsPNFwzF1cHuOajOOzoq4U7v/ig1mpRjqV+Bu1U= -github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE= -github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ= -github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2 h1:S4OC0+OBKz6mJnzuHioeEat74PuQ4Sgvbf8eus695sc= -github.com/segmentio/go-loggly v0.5.1-0.20171222203950-eb91657e62b2/go.mod h1:8zLRYR5npGjaOXgPSKat5+oOh+UHd8OdbS18iqX9F6Y= -github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8= -github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I= -github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0= -github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= -github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= -github.com/skeema/knownhosts v1.2.1 h1:SHWdIUa82uGZz+F+47k8SY4QhhI291cXCpopT1lK2AQ= -github.com/skeema/knownhosts v1.2.1/go.mod h1:xYbVRSPxqBZFrdmDyMmsOs+uX1UZC3nTN3ThzgDxUwo= -github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo= -github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0= -github.com/spf13/afero v1.10.0 h1:EaGW2JJh15aKOejeuJ+wpFSHnbd7GE6Wvp3TsNhb6LY= -github.com/spf13/afero v1.10.0/go.mod h1:UBogFpq8E9Hx+xc5CNTTEpTnuHVmXDwZcZcE1eb/UhQ= -github.com/spf13/cast v1.5.1 h1:R+kOtfhWQE6TVQzY+4D7wJLBgkdVasCEFxSUBYBYIlA= -github.com/spf13/cast v1.5.1/go.mod h1:b9PdjNptOpzXr7Rq1q9gJML/2cdGQAo69NKzQ10KN48= -github.com/spf13/cobra v1.7.0 h1:hyqWnYt1ZQShIddO5kBpj3vu05/++x6tJ6dg8EC572I= -github.com/spf13/cobra v1.7.0/go.mod h1:uLxZILRyS/50WlhOIKD7W6V5bgeIt+4sICxh6uRMrb0= -github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA= -github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg= -github.com/spf13/viper v1.17.0 h1:I5txKw7MJasPL/BrfkbA0Jyo/oELqVmux4pR/UxOMfI= -github.com/spf13/viper v1.17.0/go.mod h1:BmMMMLQXSbcHK6KAOiFLz0l5JHrU89OdIRHvsk0+yVI= -github.com/stellar/go v0.0.0-20240202231803-b0df9f046eb4 h1:1DQT7eta18GSv+z6wF7AMUf7NqQ0qOrr2uJPGMRakRg= -github.com/stellar/go v0.0.0-20240202231803-b0df9f046eb4/go.mod h1:Ka4piwZT4Q9799f+BZeaKkAiYo4UpIWXyu0oSUbCVfM= -github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2 h1:OzCVd0SV5qE3ZcDeSFCmOWLZfEWZ3Oe8KtmSOYKEVWE= -github.com/stellar/go-xdr v0.0.0-20231122183749-b53fb00bcac2/go.mod h1:yoxyU/M8nl9LKeWIoBrbDPQ7Cy+4jxRcWcOayZ4BMps= -github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= -github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= -github.com/stretchr/objx v0.5.1 h1:4VhoImhV/Bm0ToFkXFi8hXNXwpDRZ/ynw3amt82mzq0= -github.com/stretchr/objx v0.5.1/go.mod h1:/iHQpkQwBD6DLUmQ4pE+s1TXdob1mORJ4/UFdrifcy0= -github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs= -github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= -github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA= -github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= -github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= -github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= -github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= -github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= -github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8= -github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU= -github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= -github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= -github.com/valyala/fasthttp v1.34.0 h1:d3AAQJ2DRcxJYHm7OXNXtXt2as1vMDfxeIcFvhmGGm4= -github.com/valyala/fasthttp v1.34.0/go.mod h1:epZA5N+7pY6ZaEKRmstzOuYJx9HI8DI1oaCGZpdH4h0= -github.com/xanzy/ssh-agent v0.3.3 h1:+/15pJfg/RsTxqYcX6fHqOXZwwMP+2VyYWJeWM2qQFM= -github.com/xanzy/ssh-agent v0.3.3/go.mod h1:6dzNDKs0J9rVPHPhaGCukekBHKqfl+L3KghI1Bc68Uw= -github.com/xdrpp/goxdr v0.1.1 h1:E1B2c6E8eYhOVyd7yEpOyopzTPirUeF6mVOfXfGyJyc= -github.com/xdrpp/goxdr v0.1.1/go.mod h1:dXo1scL/l6s7iME1gxHWo2XCppbHEKZS7m/KyYWkNzA= -github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076 h1:KM4T3G70MiR+JtqplcYkNVoNz7pDwYaBxWBXQK804So= -github.com/xeipuuv/gojsonpointer v0.0.0-20151027082146-e0fe6f683076/go.mod h1:N2zxlSyiKSe5eX1tZViRH5QA0qijqEDrYZiPEAiq3wU= -github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c h1:XZWnr3bsDQWAZg4Ne+cPoXRPILrNlPNQfxBuwLl43is= -github.com/xeipuuv/gojsonreference v0.0.0-20150808065054-e02fc20de94c/go.mod h1:GwrjFmJcFw6At/Gs6z4yjiIwzuJ1/+UwLxMQDVQXShQ= -github.com/xeipuuv/gojsonschema v0.0.0-20161231055540-f06f290571ce h1:cVSRGH8cOveJNwFEEZLXtB+XMnRqKLjUP6V/ZFYQCXI= -github.com/xeipuuv/gojsonschema v0.0.0-20161231055540-f06f290571ce/go.mod h1:5yf86TLmAcydyeJq5YvxkGPE2fm/u4myDekKRoLuqhs= -github.com/yalp/jsonpath v0.0.0-20150812003900-31a79c7593bb h1:06WAhQa+mYv7BiOk13B/ywyTlkoE/S7uu6TBKU6FHnE= -github.com/yalp/jsonpath v0.0.0-20150812003900-31a79c7593bb/go.mod h1:/LWChgwKmvncFJFHJ7Gvn9wZArjbV5/FppcK2fKk/tI= -github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d h1:yJIizrfO599ot2kQ6Af1enICnwBD3XoxgX3MrMwot2M= -github.com/yudai/gojsondiff v0.0.0-20170107030110-7b1b7adf999d/go.mod h1:AY32+k2cwILAkW1fbgxQ5mUmMiZFgLIV+FBNExI05xg= -github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce h1:888GrqRxabUce7lj4OaoShPxodm3kXOMpSa85wdYzfY= -github.com/yudai/golcs v0.0.0-20150405163532-d1c525dea8ce/go.mod h1:lgjkn3NuSvDfVJdfcVVdX+jpBxNmX4rDAzaS45IcYoM= -github.com/yuin/goldmark v1.1.25/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= -github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= -go.opencensus.io v0.21.0/go.mod h1:mSImk1erAIZhrmZN+AvHh14ztQfjbGwt4TtuofqLduU= -go.opencensus.io v0.22.0/go.mod h1:+kGneAE2xo2IficOXnaByMWTGM9T73dGwxeWcUqIpI8= -go.opencensus.io v0.22.2/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.3/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.4/go.mod h1:yxeiOL68Rb0Xd1ddK5vPZ/oVn4vY4Ynel7k9FzqtOIw= -go.opencensus.io v0.22.5/go.mod h1:5pWMHQbX5EPX2/62yrJeAkowc+lfs/XD7Uxpq3pI6kk= -go.opencensus.io v0.24.0 h1:y73uSU6J157QMP2kn2r30vwW1A2W2WFwSCGnAVxeaD0= -go.opencensus.io v0.24.0/go.mod h1:vNK8G9p7aAivkbmorf4v+7Hgx+Zs0yY+0fOtgBfjQKo= -go.opentelemetry.io/otel v1.19.0 h1:MuS/TNf4/j4IXsZuJegVzI1cwut7Qc00344rgH7p8bs= -go.opentelemetry.io/otel v1.19.0/go.mod h1:i0QyjOq3UPoTzff0PJB2N66fb4S0+rSbSB15/oyH9fY= -go.opentelemetry.io/otel/metric v1.19.0 h1:aTzpGtV0ar9wlV4Sna9sdJyII5jTVJEvKETPiOKwvpE= -go.opentelemetry.io/otel/metric v1.19.0/go.mod h1:L5rUsV9kM1IxCj1MmSdS+JQAcVm319EUrDVLrt7jqt8= -go.opentelemetry.io/otel/sdk v1.19.0 h1:6USY6zH+L8uMH8L3t1enZPR3WFEmSTADlqldyHtJi3o= -go.opentelemetry.io/otel/sdk v1.19.0/go.mod h1:NedEbbS4w3C6zElbLdPJKOpJQOrGUJ+GfzpjUvI0v1A= -go.opentelemetry.io/otel/trace v1.19.0 h1:DFVQmlVbfVeOuBRrwdtaehRrWiL1JoVs9CPIQ1Dzxpg= -go.opentelemetry.io/otel/trace v1.19.0/go.mod h1:mfaSyvGyEJEI0nyV2I4qhNQnbBOUUmYZpYojqMnX2vo= -go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= -go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= -golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= -golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20190605123033-f99c8df09eb5/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= -golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= -golang.org/x/crypto v0.0.0-20210421170649-83a5a9bb288b/go.mod h1:T9bdIzuCu7OtxOm1hfPfRQxPLYneinmdGuTeoZ9dtd4= -golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= -golang.org/x/crypto v0.0.0-20220622213112-05595931fe9d/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.0.0-20220722155217-630584e8d5aa/go.mod h1:IxCIyHEi3zRg3s0A5j5BB6A9Jmi73HwBIUl50j+osU4= -golang.org/x/crypto v0.3.1-0.20221117191849-2c476679df9a/go.mod h1:hebNnKkNXi2UzZN1eVRvBB7co0a+JxK6XbPiWVs/3J4= -golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU= -golang.org/x/crypto v0.16.0 h1:mMMrFzRSCF0GvB7Ne27XVtVAaXLrPmgPC7/v0tkwHaY= -golang.org/x/crypto v0.16.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4= -golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20190510132918-efd6b22b2522/go.mod h1:ZjyILWgesfNpC6sMxTJOJm9Kp84zZh5NQWvqDGG3Qr8= -golang.org/x/exp v0.0.0-20190829153037-c13cbed26979/go.mod h1:86+5VVa7VpoJ4kLfm080zCjGlMRFzhUhsZKEZO7MGek= -golang.org/x/exp v0.0.0-20191030013958-a1ab85dbe136/go.mod h1:JXzH8nQsPlswgeRAPE3MuO9GYsAcnJvJ4vnMwN/5qkY= -golang.org/x/exp v0.0.0-20191129062945-2f5052295587/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20191227195350-da58074b4299/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200119233911-0405dc783f0a/go.mod h1:2RIsYlXP63K8oxa1u096TMicItID8zy7Y6sNkU49FU4= -golang.org/x/exp v0.0.0-20200207192155-f17229e696bd/go.mod h1:J/WKrq2StrnmMY6+EHIKF9dgMWnmCNThgcyBT1FY9mM= -golang.org/x/exp v0.0.0-20200224162631-6cc2880d07d6/go.mod h1:3jZMyOhIsHpP37uCMkUooju7aAi5cS1Q23tOzKc+0MU= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI= -golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo= -golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js= -golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= -golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= -golang.org/x/lint v0.0.0-20190301231843-5614ed5bae6f/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= -golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190409202823-959b441ac422/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190909230951-414d861bb4ac/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= -golang.org/x/lint v0.0.0-20191125180803-fdd1cda4f05f/go.mod h1:5qLYkcX4OjUUV8bRuDixDT3tpyyb+LUpUlRWLxfhWrs= -golang.org/x/lint v0.0.0-20200130185559-910be7a94367/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/lint v0.0.0-20201208152925-83fdc39ff7b5/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY= -golang.org/x/mobile v0.0.0-20190312151609-d3739f865fa6/go.mod h1:z+o9i4GpDbdi3rU15maQ/Ox0txvL9dWGYEHz965HBQE= -golang.org/x/mobile v0.0.0-20190719004257-d2bd2a29d028/go.mod h1:E/iHnbuqvinMTCcRqshq8CkpyQDoeVncDDYHnLhea+o= -golang.org/x/mod v0.0.0-20190513183733-4bf6d317e70e/go.mod h1:mXi4GBBbnImb6dmsKGUJ2LatrhH/nqhxcFungHvyanc= -golang.org/x/mod v0.1.0/go.mod h1:0QHyrYULN0/3qlju5TqG8bIK38QM8yzMo5ekMj3DlcY= -golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg= -golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.4.1/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= -golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= -golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= -golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= -golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= -golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= -golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190501004415-9ce7a6920f09/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190503192946-f4e77d36d62c/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= -golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks= -golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190628185345-da137c7871d7/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20190724013045-ca1201d0de80/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20191209160850-c0dbc17a3553/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200114155413-6afb5195e5aa/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200222125558-5a598a2470a0/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200226121028-0de0cce0169b/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200301022130-244492dfa37a/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= -golang.org/x/net v0.0.0-20200324143707-d3edc9973b7e/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200501053045-e0ff5e5a1de5/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200513185701-a91f0712d120/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200520182314-0ba52f642ac2/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A= -golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200707034311-ab3426394381/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20200822124328-c89045814202/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= -golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= -golang.org/x/net v0.0.0-20201209123823-ac852fbbde11/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20201224014010-6772e930b67b/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= -golang.org/x/net v0.0.0-20211112202133-69e39bad7dc2/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= -golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= -golang.org/x/net v0.1.0/go.mod h1:Cx3nUiGt4eDBEyega/BKRp+/AlGL8hYe7U9odMt2Cco= -golang.org/x/net v0.2.0/go.mod h1:KqCZLdyyvdV855qA2rE3GC2aiw5xGR5TEjj8smXukLY= -golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= -golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc= -golang.org/x/net v0.19.0 h1:zTwKpTd2XuCqf8huc7Fo2iSy+4RHPd10s4KzeTnVr1c= -golang.org/x/net v0.19.0/go.mod h1:CfAk/cbD4CthTvqiEl8NpboMuiuOYsAr/7NOjZJtv1U= -golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= -golang.org/x/oauth2 v0.0.0-20190226205417-e64efc72b421/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20190604053449-0f29369cfe45/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20191202225959-858c2ad4c8b6/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200107190931-bf48bf16ab8d/go.mod h1:gOpvHmFTYa4IltrdGE7lF6nIHvwfUNPOp7c8zoXwtLw= -golang.org/x/oauth2 v0.0.0-20200902213428-5d25da1a8d43/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201109201403-9fd604954f58/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20201208152858-08078c50e5b5/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.0.0-20210218202405-ba52d332ba99/go.mod h1:KelEdhl1UZF7XfJ4dDtk6s++YSgaE7mD/BuKKDLBl4A= -golang.org/x/oauth2 v0.13.0 h1:jDDenyj+WgFtmV3zYVoi8aE2BwtXFLWOA67ZfNWftiY= -golang.org/x/oauth2 v0.13.0/go.mod h1:/JMhi4ZRXAf4HG9LiNmxvk+45+96RUlVThiH8FzNBn0= -golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190227155943-e225da77a7e6/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= -golang.org/x/sync v0.5.0 h1:60k92dhOjHxJkrqnwsfl8KuaHbn/5dl0lUPUklKo3qE= -golang.org/x/sync v0.5.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= -golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= -golang.org/x/sys v0.0.0-20190312061237-fead79001313/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190502145724-3ef323f4f1fd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190507160741-ecd444e8653b/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190606165138-5da285871e9c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190624142023-c5567b49c5d0/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20190726091711-fc99dfbffb4e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191001151750-bb3f8db39f24/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191204072324-ce4227a45e2e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20191228213918-04cbcbbfeed8/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200113162924-86b910548bc1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200122134326-e047566fdf82/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200212091648-12a6c2dcc1e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200331124033-c3d80250170d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200501052902-10377860bb8e/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200511232937-7e40ca221e25/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200515095857-1151b9dac4a9/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200523222454-059865788121/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200803210538-64077c9b5642/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200905004654-be1d3432aa8f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20201201145000-ef89a241ccb3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210104204734-6f8348627aad/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210225134936-a50acf3fe073/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210423185535-09eb48e85fd7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= -golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.3.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= -golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU= -golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= -golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.1.0/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= -golang.org/x/term v0.2.0/go.mod h1:TVmDHMZPmdnySmBfhjOoOdhjzdE1h4u1VwSiw2l1Nuc= -golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k= -golang.org/x/term v0.6.0/go.mod h1:m6U89DPEgQRMq3DNkDClhWw02AUbt2daBVO4cn4Hv9U= -golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4= -golang.org/x/term v0.15.0/go.mod h1:BDl952bC7+uMoWR75FIrCDx79TPU9oHkTZ9yRbYOrX0= -golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= -golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk= -golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.4/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= -golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= -golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= -golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= -golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= -golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= -golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= -golang.org/x/time v0.0.0-20181108054448-85acf8d2951c/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20190308202827-9d24e82272b4/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/time v0.0.0-20191024005414-555d28b269f0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= -golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= -golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= -golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312151545-0bb0c0a6e846/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190312170243-e65039ee4138/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= -golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190506145303-2d16b83fe98c/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= -golang.org/x/tools v0.0.0-20190606124116-d0a3d012864b/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190621195816-6e04913cbbac/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190628153133-6cdbf07be9d0/go.mod h1:/rFqwRUd4F7ZHNgwSSTFct+R/Kf4OFW1sUzUTQQTgfc= -golang.org/x/tools v0.0.0-20190816200558-6889da9d5479/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20190911174233-4f2ddba30aff/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191012152004-8de300cfc20a/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191113191852-77e3bb0ad9e7/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191115202509-3a792d9c32b2/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191125144606-a911d9008d1f/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191130070609-6e064ea0cf2d/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= -golang.org/x/tools v0.0.0-20191216173652-a0e659d51361/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20191227053925-7b8e75db28f4/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200117161641-43d50277825c/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200122220014-bf1340f18c4a/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200204074204-1cc6d1ef6c74/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200207183749-b753a1ba74fa/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200212150539-ea181f53ac56/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200224181240-023911ca70b2/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200227222343-706bc42d1f0d/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28= -golang.org/x/tools v0.0.0-20200304193943-95d2e580d8eb/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200312045724-11d5b4c81c7d/go.mod h1:o4KQGtdN14AW+yjsvvwRTJJuXz8XRtIHtEnmAXLyFUw= -golang.org/x/tools v0.0.0-20200331025713-a30bf2db82d4/go.mod h1:Sl4aGygMT6LrqrWclx+PTx3U+LnKx/seiNR+3G19Ar8= -golang.org/x/tools v0.0.0-20200501065659-ab2804fb9c9d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200512131952-2bc93b1c0c88/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200515010526-7d3b6ebf133d/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200618134242-20370b0cb4b2/go.mod h1:EkVYQZoAsY45+roYkvgYkIh4xh/qjgUK9TdY2XT94GE= -golang.org/x/tools v0.0.0-20200729194436-6467de6f59a7/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200804011535-6c149bb5ef0d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200825202427-b303f430e36d/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= -golang.org/x/tools v0.0.0-20200904185747-39188db58858/go.mod h1:Cj7w3i3Rnn0Xh82ur9kSqwfTHTeVxaDqrfMjpcNT6bE= -golang.org/x/tools v0.0.0-20201110124207-079ba7bd75cd/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201201161351-ac6f37ff4c2a/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20201208233053-a543418bbed2/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210105154028-b0ab187a4818/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.0.0-20210108195828-e2f9c7f1fc8e/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= -golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= -golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= -golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU= -golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= -golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg= -golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2 h1:H2TDz8ibqkAF6YGhCdN3jS9O0/s90v0rJh3X/OLHEUk= -golang.org/x/xerrors v0.0.0-20220907171357-04be3eba64a2/go.mod h1:K8+ghG5WaK9qNqU5K3HdILfMLy1f3aNYFI/wnl100a8= -google.golang.org/api v0.4.0/go.mod h1:8k5glujaEP+g9n7WNsDg8QP6cUVNI86fCNMcbazEtwE= -google.golang.org/api v0.7.0/go.mod h1:WtwebWUNSVBH/HAw79HIFXZNqEvBhG+Ra+ax0hx3E3M= -google.golang.org/api v0.8.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.9.0/go.mod h1:o4eAsZoiT+ibD93RtjEohWalFOjRDx6CVaqeizhEnKg= -google.golang.org/api v0.13.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.14.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.15.0/go.mod h1:iLdEw5Ide6rF15KTC1Kkl0iskquN2gFfn9o9XIsbkAI= -google.golang.org/api v0.17.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.18.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.19.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.20.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.22.0/go.mod h1:BwFmGc8tA3vsd7r/7kR8DY7iEEGSU04BFxCo5jP/sfE= -google.golang.org/api v0.24.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.28.0/go.mod h1:lIXQywCXRcnZPGlsd8NbLnOjtAoL6em04bJ9+z0MncE= -google.golang.org/api v0.29.0/go.mod h1:Lcubydp8VUV7KeIHD9z2Bys/sm/vGKnG1UHuDBSrHWM= -google.golang.org/api v0.30.0/go.mod h1:QGmEvQ87FHZNiUVJkT14jQNYJ4ZJjdRF23ZXz5138Fc= -google.golang.org/api v0.35.0/go.mod h1:/XrVsuzM0rZmrsbjJutiuftIzeuTQcEeaYcSk/mQ1dg= -google.golang.org/api v0.36.0/go.mod h1:+z5ficQTmoYpPn8LCUNVpK5I7hwkpjbcgqA7I34qYtE= -google.golang.org/api v0.40.0/go.mod h1:fYKFpnQN0DsDSKRVRcQSDQNtqWPfM9i+zNPxepjRCQ8= -google.golang.org/api v0.149.0 h1:b2CqT6kG+zqJIVKRQ3ELJVLN1PwHZ6DJ3dW8yl82rgY= -google.golang.org/api v0.149.0/go.mod h1:Mwn1B7JTXrzXtnvmzQE2BD6bYZQ8DShKZDZbeN9I7qI= -google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM= -google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.5.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4= -google.golang.org/appengine v1.6.1/go.mod h1:i06prIuMbXzDqacNJfV5OdTW448YApPu5ww/cMBSeb0= -google.golang.org/appengine v1.6.5/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.6/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.7/go.mod h1:8WjMMxjGQR8xUklV/ARdw2HLXBOI7O7uCIDZVag1xfc= -google.golang.org/appengine v1.6.8 h1:IhEN5q69dyKagZPYMSdIjS2HqprW324FRQZJcGqPAsM= -google.golang.org/appengine v1.6.8/go.mod h1:1jJ3jBArFh5pcgW8gCtRJnepW8FzD1V44FJffLiz/Ds= -google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= -google.golang.org/genproto v0.0.0-20190307195333-5fe7a883aa19/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190418145605-e7d98fc518a7/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190425155659-357c62f0e4bb/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190502173448-54afdca5d873/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= -google.golang.org/genproto v0.0.0-20190801165951-fa694d86fc64/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc= -google.golang.org/genproto v0.0.0-20190911173649-1774047e7e51/go.mod h1:IbNlFCBrqXvoKpeg0TB2l7cyZUmoaFKYIwrEpbDKLA8= -google.golang.org/genproto v0.0.0-20191108220845-16a3f7862a1a/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191115194625-c23dd37a84c9/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191216164720-4f79533eabd1/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20191230161307-f3c370f40bfb/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200115191322-ca5a22157cba/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200122232147-0452cf42e150/go.mod h1:n3cpQtvxv34hfy77yVDNjmbRyujviMdxYliBSkLhpCc= -google.golang.org/genproto v0.0.0-20200204135345-fa8e72b47b90/go.mod h1:GmwEX6Z4W5gMy59cAlVYjN9JhxgbQH6Gn+gFDQe2lzA= -google.golang.org/genproto v0.0.0-20200212174721-66ed5ce911ce/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200224152610-e50cd9704f63/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200228133532-8c2c7df3a383/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200305110556-506484158171/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200312145019-da6875a35672/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200331122359-1ee6d9798940/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200430143042-b979b6f78d84/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200511104702-f5ebc3bea380/go.mod h1:55QSHmfGQM9UVYDPBsyGGes0y52j32PQ3BqQfXhyH3c= -google.golang.org/genproto v0.0.0-20200515170657-fc4c6c6a6587/go.mod h1:YsZOwe1myG/8QRHRsmBRE1LrgQY60beZKjly0O1fX9U= -google.golang.org/genproto v0.0.0-20200526211855-cb27e3aa2013/go.mod h1:NbSheEEYHJ7i3ixzK3sjbqSGDJWnxyFXZblF3eUsNvo= -google.golang.org/genproto v0.0.0-20200618031413-b414f8b61790/go.mod h1:jDfRM7FcilCzHH/e9qn6dsT145K34l5v+OpcnNgKAAA= -google.golang.org/genproto v0.0.0-20200729003335-053ba62fc06f/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200804131852-c06518451d9c/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200825200019-8632dd797987/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20200904004341-0bd0a958aa1d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201109203340-2640f1f9cdfb/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201201144952-b05cb90ed32e/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201210142538-e3217bee35cc/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20201214200347-8c77b98c765d/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210108203827-ffc7fda8c3d7/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20210226172003-ab064af71705/go.mod h1:FWY/as6DDZQgahTzZj3fqbO1CbirC29ZNUFHwi0/+no= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0 h1:YJ5pD9rF8o9Qtta0Cmy9rdBwkSjrTCT6XTiUQVOtIos= -google.golang.org/genproto v0.0.0-20231212172506-995d672761c0/go.mod h1:l/k7rMz0vFTBPy+tFSGvXEd3z+BcoG1k7EHbqm+YBsY= -google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3 h1:EWIeHfGuUf00zrVZGEgYFxok7plSAXBGcH7NNdMAWvA= -google.golang.org/genproto/googleapis/api v0.0.0-20231211222908-989df2bf70f3/go.mod h1:k2dtGpRrbsSyKcNPKKI5sstZkrNCZwpU/ns96JoHbGg= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ= -google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU= -google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= -google.golang.org/grpc v1.20.1/go.mod h1:10oTOabMzJvdu6/UiuZezV6QK5dSlG84ov/aaiqXj38= -google.golang.org/grpc v1.21.1/go.mod h1:oYelfM1adQP15Ek0mdvEgi9Df8B9CZIaU1084ijfRaM= -google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg= -google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY= -google.golang.org/grpc v1.26.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.0/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk= -google.golang.org/grpc v1.28.0/go.mod h1:rpkK4SK4GF4Ach/+MFLZUBavHOvF2JJB5uozKKal+60= -google.golang.org/grpc v1.29.1/go.mod h1:itym6AZVZYACWQqET3MqgPpjcuV5QH3BxFS3IjizoKk= -google.golang.org/grpc v1.30.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.31.1/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak= -google.golang.org/grpc v1.33.2/go.mod h1:JMHMWHQWaTccqQQlmk3MJZS+GWXOdAesneDmEnv2fbc= -google.golang.org/grpc v1.34.0/go.mod h1:WotjhfgOW/POjDeRt8vscBtXq+2VjORFy659qA51WJ8= -google.golang.org/grpc v1.35.0/go.mod h1:qjiiYl8FncCW8feJPdyg3v6XW24KsRHe+dy9BAGRRjU= -google.golang.org/grpc v1.60.1 h1:26+wFr+cNqSGFcOXcabYC0lUVJVRa2Sb2ortSK7VrEU= -google.golang.org/grpc v1.60.1/go.mod h1:OlCHIeLYqSSsLi6i49B5QGdzaMZK9+M7LXN2FKz4eGM= -google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8= -google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0= -google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM= -google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE= -google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo= -google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= -google.golang.org/protobuf v1.24.0/go.mod h1:r/3tXBNzIEhYS9I1OUVjXDlt8tc493IdKGjtUeSXeh4= -google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= -google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= -google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= -google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= -gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20190902080502-41f04d3bba15/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= -gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= -gopkg.in/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI= -gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0 h1:r5ptJ1tBxVAeqw4CrYWhXIMr0SybY3CDHuIbCg5CFVw= -gopkg.in/gavv/httpexpect.v1 v1.0.0-20170111145843-40724cf1e4a0/go.mod h1:WtiW9ZA1LdaWqtQRo1VbIL/v4XZ8NDta+O/kSpGgVek= -gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= -gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ= -gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw= -gopkg.in/tylerb/graceful.v1 v1.2.15 h1:1JmOyhKqAyX3BgTXMI84LwT6FOJ4tP2N9e2kwTCM0nQ= -gopkg.in/tylerb/graceful.v1 v1.2.15/go.mod h1:yBhekWvR20ACXVObSSdD3u6S9DeSylanL2PAbAC/uJ8= -gopkg.in/warnings.v0 v0.1.2 h1:wFXVbFY8DY5/xOe1ECiWdKCzZlxgshcYVNkBHstARME= -gopkg.in/warnings.v0 v0.1.2/go.mod h1:jksf8JmL6Qr/oQM2OXTHunEvvTAsrWBLb6OOjuVWRNI= -gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= -gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= -gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= -gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= -gotest.tools/v3 v3.5.0 h1:Ljk6PdHdOhAb5aDMWXjDLMMhph+BpztA4v1QdqEW2eY= -gotest.tools/v3 v3.5.0/go.mod h1:isy3WKz7GK6uNw/sbHzfKBLvlvXwUyV06n6brMxxopU= -honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190106161140-3f1c8253044a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190418001031-e561f6794a2a/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4= -honnef.co/go/tools v0.0.1-2019.2.3/go.mod h1:a3bituU0lyd329TUQxRnasdCoJDkEUEAqEt0JzvZhAg= -honnef.co/go/tools v0.0.1-2020.1.3/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -honnef.co/go/tools v0.0.1-2020.1.4/go.mod h1:X/FiERA/W4tHapMX5mGpAtMSVEeEUOyHaw9vFzvIQ3k= -rsc.io/binaryregexp v0.2.0/go.mod h1:qTv7/COck+e2FymRvadv62gMdZztPaShugOCi3I+8D8= -rsc.io/quote/v3 v3.1.0/go.mod h1:yEA65RcK8LyAZtP9Kv3t0HmxON59tX3rD+tICJqUlj0= -rsc.io/sampler v1.3.0/go.mod h1:T1hPZKmBbMNahiBKFy5HrXp6adAjACjK9JXDnKaTXpA= diff --git a/scripts/check-dependencies.bash b/scripts/check-dependencies.bash index 91b4e22a8..c82d2f2b1 100755 --- a/scripts/check-dependencies.bash +++ b/scripts/check-dependencies.bash @@ -8,7 +8,6 @@ if [ -z "$(sed --version 2>&1 | grep GNU)" ]; then fi CURL="curl -sL --fail-with-body" - if ! CARGO_OUTPUT=$(cargo tree -p soroban-env-host 2>&1); then echo "The project depends on multiple versions of the soroban-env-host Rust library, please unify them." echo "Make sure the soroban-sdk dependency indirectly points to the same soroban-env-host dependency imported explicitly." @@ -23,7 +22,6 @@ if ! CARGO_OUTPUT=$(cargo tree -p soroban-env-host 2>&1); then exit 1 fi - # revision of the https://github.com/stellar/rs-stellar-xdr library used by the Rust code RS_STELLAR_XDR_REVISION="" @@ -61,52 +59,3 @@ else echo "Full error:" echo $CARGO_OUTPUT fi - -# Now, lets compare the Rust and Go XDR revisions -# TODO: The sed extraction below won't work for version tags -GO_XDR_REVISION=$(go list -m -f '{{.Version}}' github.com/stellar/go | $SED 's/.*-\(.*\)/\1/') - -# revision of https://github.com/stellar/stellar-xdr/ used by the Go code -STELLAR_XDR_REVISION_FROM_GO=$($CURL https://raw.githubusercontent.com/stellar/go/${GO_XDR_REVISION}/xdr/xdr_commit_generated.txt) - -if [ "$STELLAR_XDR_REVISION_FROM_GO" != "$STELLAR_XDR_REVISION_FROM_RUST" ]; then - echo "Go and Rust dependencies are using different revisions of https://github.com/stellar/stellar-xdr" - echo - echo "Rust dependencies are using commit $STELLAR_XDR_REVISION_FROM_RUST" - echo "Go dependencies are using commit $STELLAR_XDR_REVISION_FROM_GO" - exit 1 -fi - -# Now, lets make sure that the core and captive core version used in the tests use the same version and that they depend -# on the same XDR revision - -# TODO: The sed extractions below won't work when the commit is not included in the Core image tag/debian packages version -CORE_CONTAINER_REVISION=$($SED -n 's/.*\/\(stellar-core\|unsafe-stellar-core\(-next\)\{0,1\}\)\:.*\.\([a-zA-Z0-9]*\)\..*/\3/p' < cmd/soroban-rpc/internal/test/docker-compose.yml) -CAPTIVE_CORE_PKG_REVISION=$($SED -n 's/.*DEBIAN_PKG_VERSION:..*\.\([a-zA-Z0-9]*\)\..*/\1/p' < .github/workflows/soroban-rpc.yml) - -if [ "$CORE_CONTAINER_REVISION" != "$CAPTIVE_CORE_PKG_REVISION" ]; then - echo "Soroban RPC integration tests are using different versions of the Core container and Captive Core Debian package." - echo - echo "Core container image commit $CORE_CONTAINER_REVISION" - echo "Captive core debian package commit $CAPTIVE_CORE_PKG_REVISION" - exit 1 -fi - -# Revision of https://github.com/stellar/rs-stellar-xdr by Core. -# We obtain it from src/rust/src/host-dep-tree-curr.txt but Alternatively/in addition we could: -# * Check the rs-stellar-xdr revision of host-dep-tree-prev.txt -# * Check the stellar-xdr revision -CORE_HOST_DEP_TREE_CURR=$($CURL https://raw.githubusercontent.com/stellar/stellar-core/${CORE_CONTAINER_REVISION}/src/rust/src/host-dep-tree-curr.txt) - - -RS_STELLAR_XDR_REVISION_FROM_CORE=$(echo "$CORE_HOST_DEP_TREE_CURR" | stellar_xdr_version_from_rust_dep_tree) -if [ "$RS_STELLAR_XDR_REVISION" != "$RS_STELLAR_XDR_REVISION_FROM_CORE" ]; then - echo "The Core revision used in integration tests (${CORE_CONTAINER_REVISION}) uses a different revision of https://github.com/stellar/rs-stellar-xdr" - echo - echo "Current repository's revision $RS_STELLAR_XDR_REVISION" - echo "Core's revision $RS_STELLAR_XDR_REVISION_FROM_CORE" - exit 1 -fi - - -